feat: DeerFlow 2.0 core capabilities — Phase 1.0 + 1.1
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

Phase 1.0 — Butler Mode UI:
- Hide "自动化" and "技能市场" entries from sidebar navigation
- Remove AutomationPanel and SkillMarket view rendering from App.tsx
- Simplify MainViewType to only 'chat'
- Main interface is now: chat + conversation list + detail panel only

Phase 1.1 — Mode Differentiation:
- Add subagent_enabled field to ChatModeConfig (Rust), StreamChatRequest (Tauri),
  gateway-client, kernel-client, saas-relay-client, and streamStore
- TaskTool is now only registered when subagent_enabled=true (Ultra mode)
- System prompt includes sub-agent delegation instructions only in Ultra mode
- Frontend transmits subagent_enabled from ChatMode config through the full stack

This connects the 4-tier mode selector (Flash/Thinking/Pro/Ultra) to actual
backend behavioral differences — Ultra mode now truly enables sub-agent delegation.
This commit is contained in:
iven
2026-04-06 12:46:43 +08:00
parent 9c346ed6fb
commit cb140b5151
10 changed files with 72 additions and 105 deletions

View File

@@ -4,12 +4,13 @@ use tokio::sync::mpsc;
use zclaw_types::{AgentId, Result}; use zclaw_types::{AgentId, Result};
/// Chat mode configuration passed from the frontend. /// Chat mode configuration passed from the frontend.
/// Controls thinking, reasoning, and plan mode behavior. /// Controls thinking, reasoning, plan mode, and sub-agent behavior.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ChatModeConfig { pub struct ChatModeConfig {
pub thinking_enabled: Option<bool>, pub thinking_enabled: Option<bool>,
pub reasoning_effort: Option<String>, pub reasoning_effort: Option<String>,
pub plan_mode: Option<bool>, pub plan_mode: Option<bool>,
pub subagent_enabled: Option<bool>,
} }
use zclaw_runtime::{AgentLoop, tool::builtin::PathValidator}; use zclaw_runtime::{AgentLoop, tool::builtin::PathValidator};
@@ -45,7 +46,8 @@ impl Kernel {
let model = self.config.model().to_string(); let model = self.config.model().to_string();
// Create agent loop with model configuration // Create agent loop with model configuration
let tools = self.create_tool_registry(); let subagent_enabled = chat_mode.as_ref().and_then(|m| m.subagent_enabled).unwrap_or(false);
let tools = self.create_tool_registry(subagent_enabled);
let mut loop_runner = AgentLoop::new( let mut loop_runner = AgentLoop::new(
*agent_id, *agent_id,
self.driver.clone(), self.driver.clone(),
@@ -92,7 +94,10 @@ impl Kernel {
} }
// Build system prompt with skill information injected // Build system prompt with skill information injected
let system_prompt = self.build_system_prompt_with_skills(agent_config.system_prompt.as_ref()).await; let system_prompt = self.build_system_prompt_with_skills(
agent_config.system_prompt.as_ref(),
subagent_enabled,
).await;
let loop_runner = loop_runner.with_system_prompt(&system_prompt); let loop_runner = loop_runner.with_system_prompt(&system_prompt);
// Run the loop // Run the loop
@@ -147,7 +152,8 @@ impl Kernel {
let model = self.config.model().to_string(); let model = self.config.model().to_string();
// Create agent loop with model configuration // Create agent loop with model configuration
let tools = self.create_tool_registry(); let subagent_enabled = chat_mode.as_ref().and_then(|m| m.subagent_enabled).unwrap_or(false);
let tools = self.create_tool_registry(subagent_enabled);
let mut loop_runner = AgentLoop::new( let mut loop_runner = AgentLoop::new(
*agent_id, *agent_id,
self.driver.clone(), self.driver.clone(),
@@ -197,7 +203,10 @@ impl Kernel {
// Use external prompt if provided, otherwise build default // Use external prompt if provided, otherwise build default
let system_prompt = match system_prompt_override { let system_prompt = match system_prompt_override {
Some(prompt) => prompt, Some(prompt) => prompt,
None => self.build_system_prompt_with_skills(agent_config.system_prompt.as_ref()).await, None => self.build_system_prompt_with_skills(
agent_config.system_prompt.as_ref(),
subagent_enabled,
).await,
}; };
let loop_runner = loop_runner.with_system_prompt(&system_prompt); let loop_runner = loop_runner.with_system_prompt(&system_prompt);
@@ -206,8 +215,13 @@ impl Kernel {
loop_runner.run_streaming(session_id, message).await loop_runner.run_streaming(session_id, message).await
} }
/// Build a system prompt with skill information injected /// Build a system prompt with skill information injected.
pub(super) async fn build_system_prompt_with_skills(&self, base_prompt: Option<&String>) -> String { /// When `subagent_enabled` is true, adds sub-agent delegation instructions.
pub(super) async fn build_system_prompt_with_skills(
&self,
base_prompt: Option<&String>,
subagent_enabled: bool,
) -> String {
// Get skill list asynchronously // Get skill list asynchronously
let skills = self.skills.list().await; let skills = self.skills.list().await;
@@ -215,7 +229,8 @@ impl Kernel {
.map(|p| p.clone()) .map(|p| p.clone())
.unwrap_or_else(|| "You are a helpful AI assistant.".to_string()); .unwrap_or_else(|| "You are a helpful AI assistant.".to_string());
// Inject skill information with categories // Inject skill metadata only (progressive loading pattern from DeerFlow).
// Full skill content is loaded on-demand via `load_skill_content` tool.
if !skills.is_empty() { if !skills.is_empty() {
prompt.push_str("\n\n## Available Skills\n\n"); prompt.push_str("\n\n## Available Skills\n\n");
prompt.push_str("You have access to specialized skills. Analyze user intent and autonomously call `execute_skill` with the appropriate skill_id.\n\n"); prompt.push_str("You have access to specialized skills. Analyze user intent and autonomously call `execute_skill` with the appropriate skill_id.\n\n");
@@ -245,6 +260,21 @@ impl Kernel {
prompt.push_str("User: \"分析腾讯财报\" → Intent: Financial analysis → Call: execute_skill(\"finance-tracker\", {...})\n"); prompt.push_str("User: \"分析腾讯财报\" → Intent: Financial analysis → Call: execute_skill(\"finance-tracker\", {...})\n");
} }
// Sub-agent delegation instructions (Ultra mode only)
if subagent_enabled {
prompt.push_str("\n\n## Sub-Agent Delegation\n\n");
prompt.push_str("You can delegate complex sub-tasks to sub-agents using the `task` tool. This enables parallel execution of independent work.\n\n");
prompt.push_str("### When to use sub-agents:\n");
prompt.push_str("- Complex tasks that can be decomposed into independent parallel sub-tasks\n");
prompt.push_str("- Research tasks requiring multiple independent searches\n");
prompt.push_str("- Tasks requiring different expertise areas simultaneously\n\n");
prompt.push_str("### Guidelines:\n");
prompt.push_str("- Break complex work into clear, self-contained sub-tasks\n");
prompt.push_str("- Each sub-task should have a clear objective and expected output\n");
prompt.push_str("- Synthesize sub-agent results into a coherent final response\n");
prompt.push_str("- Maximum 3 concurrent sub-agents — batch if more are needed\n");
}
prompt prompt
} }

View File

@@ -162,18 +162,22 @@ impl Kernel {
}) })
} }
/// Create a tool registry with built-in tools /// Create a tool registry with built-in tools.
pub(crate) fn create_tool_registry(&self) -> ToolRegistry { /// When `subagent_enabled` is false, TaskTool is excluded to prevent
/// the LLM from attempting sub-agent delegation in non-Ultra modes.
pub(crate) fn create_tool_registry(&self, subagent_enabled: bool) -> ToolRegistry {
let mut tools = ToolRegistry::new(); let mut tools = ToolRegistry::new();
zclaw_runtime::tool::builtin::register_builtin_tools(&mut tools); zclaw_runtime::tool::builtin::register_builtin_tools(&mut tools);
// Register TaskTool with driver and memory for sub-agent delegation // Register TaskTool only when sub-agent mode is enabled (Ultra mode)
let task_tool = zclaw_runtime::tool::builtin::TaskTool::new( if subagent_enabled {
self.driver.clone(), let task_tool = zclaw_runtime::tool::builtin::TaskTool::new(
self.memory.clone(), self.driver.clone(),
self.config.model(), self.memory.clone(),
); self.config.model(),
tools.register(Box::new(task_tool)); );
tools.register(Box::new(task_tool));
}
tools tools
} }

View File

@@ -60,6 +60,9 @@ pub struct StreamChatRequest {
/// Enable plan mode /// Enable plan mode
#[serde(default)] #[serde(default)]
pub plan_mode: Option<bool>, pub plan_mode: Option<bool>,
/// Enable sub-agent delegation (Ultra mode only)
#[serde(default)]
pub subagent_enabled: Option<bool>,
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -216,6 +219,7 @@ pub async fn agent_chat_stream(
thinking_enabled: request.thinking_enabled, thinking_enabled: request.thinking_enabled,
reasoning_effort: request.reasoning_effort.clone(), reasoning_effort: request.reasoning_effort.clone(),
plan_mode: request.plan_mode, plan_mode: request.plan_mode,
subagent_enabled: request.subagent_enabled,
}; };
let rx = kernel.send_message_stream_with_prompt( let rx = kernel.send_message_stream_with_prompt(

View File

@@ -1,12 +1,9 @@
import { useState, useEffect, useCallback, useRef } from 'react'; import { useState, useEffect, useCallback, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import './index.css'; import './index.css';
import { Sidebar, MainViewType } from './components/Sidebar'; import { Sidebar } from './components/Sidebar';
import { ChatArea } from './components/ChatArea'; import { ChatArea } from './components/ChatArea';
import { RightPanel } from './components/RightPanel'; import { RightPanel } from './components/RightPanel';
import { SettingsLayout } from './components/Settings/SettingsLayout'; import { SettingsLayout } from './components/Settings/SettingsLayout';
import { AutomationPanel } from './components/Automation';
import { SkillMarket } from './components/SkillMarket';
import { AgentOnboardingWizard } from './components/AgentOnboardingWizard'; import { AgentOnboardingWizard } from './components/AgentOnboardingWizard';
import { HandApprovalModal } from './components/HandApprovalModal'; import { HandApprovalModal } from './components/HandApprovalModal';
import { TopBar } from './components/TopBar'; import { TopBar } from './components/TopBar';
@@ -17,7 +14,7 @@ import { useHandStore, type HandRun } from './store/handStore';
import { useChatStore } from './store/chatStore'; import { useChatStore } from './store/chatStore';
import { initializeStores } from './store'; import { initializeStores } from './store';
import { getStoredGatewayToken } from './lib/gateway-client'; import { getStoredGatewayToken } from './lib/gateway-client';
import { pageVariants, defaultTransition, fadeInVariants } from './lib/animations';
import { Loader2 } from 'lucide-react'; import { Loader2 } from 'lucide-react';
import { isTauriRuntime, getLocalGatewayStatus, startLocalGateway } from './lib/tauri-gateway'; import { isTauriRuntime, getLocalGatewayStatus, startLocalGateway } from './lib/tauri-gateway';
import { LoginPage } from './components/LoginPage'; import { LoginPage } from './components/LoginPage';
@@ -49,7 +46,6 @@ function BootstrapScreen({ status }: { status: string }) {
function App() { function App() {
const [view, setView] = useState<View>('main'); const [view, setView] = useState<View>('main');
const [mainContentView, setMainContentView] = useState<MainViewType>('chat');
const [bootstrapping, setBootstrapping] = useState(true); const [bootstrapping, setBootstrapping] = useState(true);
const [bootstrapStatus, setBootstrapStatus] = useState('Initializing...'); const [bootstrapStatus, setBootstrapStatus] = useState('Initializing...');
const [showOnboarding, setShowOnboarding] = useState(false); const [showOnboarding, setShowOnboarding] = useState(false);
@@ -379,11 +375,6 @@ function App() {
} }
}; };
// 处理主视图切换
const handleMainViewChange = (view: MainViewType) => {
setMainContentView(view);
};
// 登录门禁 — 必须登录才能使用 // 登录门禁 — 必须登录才能使用
if (isRestoring) { if (isRestoring) {
return <BootstrapScreen status="Restoring session..." />; return <BootstrapScreen status="Restoring session..." />;
@@ -457,7 +448,6 @@ function App() {
{/* 左侧边栏 */} {/* 左侧边栏 */}
<Sidebar <Sidebar
onOpenSettings={() => setView('settings')} onOpenSettings={() => setView('settings')}
onMainViewChange={handleMainViewChange}
/> />
{/* 主内容区 */} {/* 主内容区 */}
@@ -468,62 +458,8 @@ function App() {
onOpenDetail={() => setShowDetailDrawer(true)} onOpenDetail={() => setShowDetailDrawer(true)}
/> />
{/* 内容区域 */} {/* 聊天区域 */}
<AnimatePresence mode="wait"> <ChatArea />
<motion.main
key={mainContentView}
variants={pageVariants}
initial="initial"
animate="animate"
exit="exit"
transition={defaultTransition}
className="flex-1 overflow-hidden relative flex flex-col"
>
{mainContentView === 'automation' ? (
<motion.div
variants={fadeInVariants}
initial="initial"
animate="animate"
className="h-full overflow-y-auto"
>
<div className="sticky top-0 z-10 flex items-center gap-2 px-4 py-2 bg-white/80 dark:bg-gray-900/80 backdrop-blur-sm border-b border-gray-200 dark:border-gray-800">
<button
onClick={() => handleMainViewChange('chat')}
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-md transition-colors text-gray-600 dark:text-gray-400"
title="返回聊天"
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m15 18-6-6 6-6"/></svg>
</button>
<span className="text-sm font-medium text-gray-900 dark:text-gray-100"></span>
</div>
<AutomationPanel />
</motion.div>
) : mainContentView === 'skills' ? (
<motion.div
variants={fadeInVariants}
initial="initial"
animate="animate"
className="h-full overflow-hidden"
>
<div className="flex items-center gap-2 px-4 py-2 border-b border-gray-200 dark:border-gray-800">
<button
onClick={() => handleMainViewChange('chat')}
className="p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-md transition-colors text-gray-600 dark:text-gray-400"
title="返回聊天"
>
<svg xmlns="http://www.w3.org/2000/svg" className="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m15 18-6-6 6-6"/></svg>
</button>
<span className="text-sm font-medium text-gray-900 dark:text-gray-100"></span>
</div>
<div className="h-[calc(100%-40px)] overflow-auto">
<SkillMarket />
</div>
</motion.div>
) : (
<ChatArea />
)}
</motion.main>
</AnimatePresence>
</div> </div>
{/* 详情抽屉 - 按需显示 */} {/* 详情抽屉 - 按需显示 */}

View File

@@ -1,7 +1,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { import {
SquarePen, MessageSquare, Bot, Search, X, Settings, Zap, Sparkles SquarePen, MessageSquare, Bot, Search, X, Settings
} from 'lucide-react'; } from 'lucide-react';
import { ConversationList } from './ConversationList'; import { ConversationList } from './ConversationList';
import { CloneManager } from './CloneManager'; import { CloneManager } from './CloneManager';
@@ -15,7 +15,7 @@ const sidebarTabVariants = {
exit: { opacity: 0, transition: { duration: 0.1 } }, exit: { opacity: 0, transition: { duration: 0.1 } },
}; };
export type MainViewType = 'chat' | 'automation' | 'skills'; export type MainViewType = 'chat';
interface SidebarProps { interface SidebarProps {
onOpenSettings?: () => void; onOpenSettings?: () => void;
@@ -89,23 +89,6 @@ export function Sidebar({
</button> </button>
{/* Divider between primary nav and secondary tools */}
<div className="mx-1 my-1 border-t border-[#e8e6e1]/40 dark:border-gray-800" />
<button
onClick={() => onMainViewChange?.('automation')}
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
>
<Zap className="w-4 h-4" />
</button>
<button
onClick={() => onMainViewChange?.('skills')}
className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-gray-600 dark:text-gray-400 hover:bg-black/5 dark:hover:bg-white/5 transition-colors"
>
<Sparkles className="w-4 h-4" />
</button>
</div> </div>
{/* Divider */} {/* Divider */}

View File

@@ -476,6 +476,7 @@ export class GatewayClient {
thinking_enabled?: boolean; thinking_enabled?: boolean;
reasoning_effort?: string; reasoning_effort?: string;
plan_mode?: boolean; plan_mode?: boolean;
subagent_enabled?: boolean;
} }
): Promise<{ runId: string }> { ): Promise<{ runId: string }> {
const agentId = opts?.agentId || this.defaultAgentId; const agentId = opts?.agentId || this.defaultAgentId;
@@ -489,6 +490,7 @@ export class GatewayClient {
thinking_enabled: opts?.thinking_enabled, thinking_enabled: opts?.thinking_enabled,
reasoning_effort: opts?.reasoning_effort, reasoning_effort: opts?.reasoning_effort,
plan_mode: opts?.plan_mode, plan_mode: opts?.plan_mode,
subagent_enabled: opts?.subagent_enabled,
}; };
this.fetchDefaultAgentId().then(() => { this.fetchDefaultAgentId().then(() => {
const resolvedAgentId = this.defaultAgentId; const resolvedAgentId = this.defaultAgentId;
@@ -529,6 +531,7 @@ export class GatewayClient {
thinking_enabled?: boolean; thinking_enabled?: boolean;
reasoning_effort?: string; reasoning_effort?: string;
plan_mode?: boolean; plan_mode?: boolean;
subagent_enabled?: boolean;
} }
): void { ): void {
// Close existing connection if any // Close existing connection if any
@@ -570,6 +573,9 @@ export class GatewayClient {
if (chatModeOpts?.plan_mode !== undefined) { if (chatModeOpts?.plan_mode !== undefined) {
chatRequest.plan_mode = chatModeOpts.plan_mode; chatRequest.plan_mode = chatModeOpts.plan_mode;
} }
if (chatModeOpts?.subagent_enabled !== undefined) {
chatRequest.subagent_enabled = chatModeOpts.subagent_enabled;
}
this.zclawWs?.send(JSON.stringify(chatRequest)); this.zclawWs?.send(JSON.stringify(chatRequest));
}; };

View File

@@ -59,6 +59,7 @@ export function installChatMethods(ClientClass: { prototype: KernelClient }): vo
thinking_enabled?: boolean; thinking_enabled?: boolean;
reasoning_effort?: string; reasoning_effort?: string;
plan_mode?: boolean; plan_mode?: boolean;
subagent_enabled?: boolean;
} }
): Promise<{ runId: string }> { ): Promise<{ runId: string }> {
const runId = crypto.randomUUID(); const runId = crypto.randomUUID();
@@ -185,6 +186,7 @@ export function installChatMethods(ClientClass: { prototype: KernelClient }): vo
thinkingEnabled: opts?.thinking_enabled, thinkingEnabled: opts?.thinking_enabled,
reasoningEffort: opts?.reasoning_effort, reasoningEffort: opts?.reasoning_effort,
planMode: opts?.plan_mode, planMode: opts?.plan_mode,
subagentEnabled: opts?.subagent_enabled,
}, },
}); });
} catch (err: unknown) { } catch (err: unknown) {

View File

@@ -403,7 +403,7 @@ export interface KernelClient {
// Chat (kernel-chat.ts) // Chat (kernel-chat.ts)
chat(message: string, opts?: { sessionKey?: string; agentId?: string }): Promise<{ runId: string; sessionId?: string; response?: string }>; chat(message: string, opts?: { sessionKey?: string; agentId?: string }): Promise<{ runId: string; sessionId?: string; response?: string }>;
chatStream(message: string, callbacks: import('./kernel-types').StreamCallbacks, opts?: { sessionKey?: string; agentId?: string; thinking_enabled?: boolean; reasoning_effort?: string; plan_mode?: boolean }): Promise<{ runId: string }>; chatStream(message: string, callbacks: import('./kernel-types').StreamCallbacks, opts?: { sessionKey?: string; agentId?: string; thinking_enabled?: boolean; reasoning_effort?: string; plan_mode?: boolean; subagent_enabled?: boolean }): Promise<{ runId: string }>;
cancelStream(sessionId: string): Promise<void>; cancelStream(sessionId: string): Promise<void>;
fetchDefaultAgentId(): Promise<string | null>; fetchDefaultAgentId(): Promise<string | null>;
setDefaultAgentId(agentId: string): void; setDefaultAgentId(agentId: string): void;

View File

@@ -108,6 +108,7 @@ export function createSaaSRelayGatewayClient(
thinking_enabled?: boolean; thinking_enabled?: boolean;
reasoning_effort?: string; reasoning_effort?: string;
plan_mode?: boolean; plan_mode?: boolean;
subagent_enabled?: boolean;
}, },
): Promise<{ runId: string }> { ): Promise<{ runId: string }> {
const runId = `run_${Date.now()}`; const runId = `run_${Date.now()}`;

View File

@@ -425,6 +425,7 @@ export const useStreamStore = create<StreamState>()(
thinking_enabled: get().getChatModeConfig().thinking_enabled, thinking_enabled: get().getChatModeConfig().thinking_enabled,
reasoning_effort: get().getChatModeConfig().reasoning_effort, reasoning_effort: get().getChatModeConfig().reasoning_effort,
plan_mode: get().getChatModeConfig().plan_mode, plan_mode: get().getChatModeConfig().plan_mode,
subagent_enabled: get().getChatModeConfig().subagent_enabled,
} }
); );