Compare commits

...

2 Commits

Author SHA1 Message Date
oudecheng
631c61fea2 feat(agent): 支持子代理最大嵌套深度控制
- 在配置结构体中新增 max_nesting_depth 字段,设置子代理最大嵌套深度
- 在 AgentFactory、todo_read、todo_write 等处初始化 nesting_depth 字段为 0
- 允许 Task 工具注册,使用 max_nesting_depth 控制子代理嵌套层数
- SubAgentRuntimeConfig 新增 max_nesting_depth 配置项,默认值为 1
- TaskTool 新增 max_nesting_depth 字段和带深度限制的构造函数
- 任务执行时增加嵌套深度校验,超过最大深度返回错误提示,防止无限递归创建子代理
2026-06-17 14:43:55 +08:00
oudecheng
e842ae0608 feat(chat): 实现todo_write完成后自动刷新待办列表
- 在useChat中添加sendMessage引用,支持从handleServerMessage内部发送命令
- 在App.tsx中通过useEffect将sendMessage注入useChat
- 子代理todo_write完成后自动发送请求刷新对应待办列表命令
- 主视图todo_write完成后自动刷新待办列表,支持子代理和主任务区分
- 优化消息处理逻辑,避免不同子代理消息混淆
2026-06-17 14:33:02 +08:00
11 changed files with 84 additions and 5 deletions

View File

@ -221,6 +221,8 @@ pub struct TaskConfig {
pub ttl_hours: u64,
#[serde(default = "default_task_allowed_tools")]
pub allowed_tools: Vec<String>,
#[serde(default = "default_task_max_nesting_depth")]
pub max_nesting_depth: u32,
}
fn default_task_enabled() -> bool {
@ -239,6 +241,10 @@ fn default_task_ttl_hours() -> u64 {
24
}
fn default_task_max_nesting_depth() -> u32 {
1
}
fn default_task_allowed_tools() -> Vec<String> {
vec![
"read".to_string(),
@ -264,6 +270,7 @@ impl Default for TaskConfig {
explore_max_execution_secs: default_task_explore_max_execution_secs(),
ttl_hours: default_task_ttl_hours(),
allowed_tools: default_task_allowed_tools(),
max_nesting_depth: default_task_max_nesting_depth(),
}
}
}

View File

@ -79,6 +79,7 @@ impl AgentFactory {
message_id: request.message_id.map(str::to_string),
message_seq: None,
subagent_description: None,
nesting_depth: 0,
});
// 如果有取消信号接收端,注入 Agent
if let Some(token) = request.cancel_token {

View File

@ -193,6 +193,7 @@ pub(crate) fn build_session_manager_with_sender(
explore_max_execution_secs: task_config.explore_max_execution_secs,
ttl_hours: task_config.ttl_hours,
skills_index: skills.system_index_prompt(),
max_nesting_depth: task_config.max_nesting_depth,
};
let subagent_runtime = Arc::new(DefaultSubAgentRuntime::new(

View File

@ -243,7 +243,15 @@ impl ToolRegistryFactory {
}
}
// 注意:不注册 task 工具,防止递归创建子代理
// 注册 task 工具,允许子代理创建孙代理(深度由 TaskTool 运行时控制)
if self.is_enabled("task") && self.task_config.enabled {
if let Some(runtime) = &self.subagent_runtime {
registry.register(TaskTool::new_with_depth(
runtime.clone(),
self.task_config.max_nesting_depth,
));
}
}
registry
}

View File

@ -34,6 +34,8 @@ pub struct SubAgentRuntimeConfig {
pub ttl_hours: u64,
/// 技能索引(可选,预生成的技能列表字符串)
pub skills_index: Option<String>,
/// 子代理最大嵌套深度0 = 禁止嵌套1 = 允许 1 层孙代理)
pub max_nesting_depth: u32,
}
impl Default for SubAgentRuntimeConfig {
@ -57,6 +59,7 @@ impl Default for SubAgentRuntimeConfig {
explore_max_execution_secs: 3600, // 60分钟
ttl_hours: 24,
skills_index: None,
max_nesting_depth: 1,
}
}
}
@ -323,6 +326,7 @@ impl DefaultSubAgentRuntime {
&self,
session: &TaskSession,
system_prompt: String,
parent_nesting_depth: u32,
) -> Result<AgentLoop, TaskError> {
let prompt_provider = Arc::new(StaticSystemPromptProvider::new(system_prompt));
@ -342,6 +346,7 @@ impl DefaultSubAgentRuntime {
message_id: None,
message_seq: None,
subagent_description: Some(session.description.clone()),
nesting_depth: parent_nesting_depth + 1,
});
// 如果有 MessageBus附加实时广播 emitter
@ -561,7 +566,7 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
);
// 7. 创建子代理
let agent = self.create_subagent(&session, system_prompt)?;
let agent = self.create_subagent(&session, system_prompt, parent_context.nesting_depth)?;
// 8. 执行任务
let result = self
@ -642,7 +647,7 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
);
// 5. 创建子代理
let agent = self.create_subagent(&session, system_prompt)?;
let agent = self.create_subagent(&session, system_prompt, parent_context.nesting_depth)?;
// 6. 使用历史继续执行
let result = self

View File

@ -10,11 +10,24 @@ use super::types::{TaskDefinition, TaskToolArgs};
/// Task 工具 - 创建和管理子代理
pub struct TaskTool {
runtime: Arc<dyn SubAgentRuntime>,
/// 最大嵌套深度0 = 主 agent 不允许创建子代理1 = 子 agent 可创建 1 层孙 agent
max_nesting_depth: u32,
}
impl TaskTool {
pub fn new(runtime: Arc<dyn SubAgentRuntime>) -> Self {
Self { runtime }
Self {
runtime,
max_nesting_depth: 0, // 主 agent 无深度限制
}
}
/// 创建带嵌套深度限制的 TaskTool用于子代理
pub fn new_with_depth(runtime: Arc<dyn SubAgentRuntime>, max_nesting_depth: u32) -> Self {
Self {
runtime,
max_nesting_depth,
}
}
}
@ -126,7 +139,19 @@ impl Tool for TaskTool {
});
}
// 4. 执行任务
// 4. 深度校验(仅对嵌套场景生效,主 agent 的 max_nesting_depth = 0 不限制)
if self.max_nesting_depth > 0 && context.nesting_depth >= self.max_nesting_depth {
return Ok(ToolResult {
success: false,
output: String::new(),
error: Some(format!(
"Cannot create nested subagent: max nesting depth ({}) reached",
self.max_nesting_depth
)),
});
}
// 5. 执行任务
let result = if let Some(task_id) = task_args.task_id {
// 恢复现有任务
self.runtime

View File

@ -182,6 +182,7 @@ mod tests {
message_id: Some("msg-1".to_string()),
message_seq: Some(1),
subagent_description: None,
nesting_depth: 0,
}
}

View File

@ -402,6 +402,7 @@ mod tests {
message_id: Some("msg-1".to_string()),
message_seq: Some(1),
subagent_description: None,
nesting_depth: 0,
}
}

View File

@ -18,6 +18,8 @@ pub struct ToolContext {
pub message_seq: Option<i64>,
/// 子代理标识,用于标注消息来源
pub subagent_description: Option<String>,
/// 当前嵌套深度0 = 主 agent1 = 子 agent2 = 孙 agent...
pub nesting_depth: u32,
}
#[async_trait]

View File

@ -75,6 +75,7 @@ function App() {
handleCommand,
clearMessages,
handleServerMessage,
setSendMessage,
selectTopic,
createTopic,
switchTopic,
@ -92,6 +93,11 @@ function App() {
onMessage: handleServerMessage,
})
// 将 sendMessage 注入到 useChat供 handleServerMessage 内部发送命令
useEffect(() => {
setSendMessage(sendMessage)
}, [setSendMessage, sendMessage])
// ---- 主题状态 ----
const [memoryPanelOpen, setMemoryPanelOpen] = useState(() => {

View File

@ -29,6 +29,7 @@ import type {
ChannelList,
StreamDelta,
StreamEnd,
WsInbound,
} from '../types/protocol'
// 简化后的层级状态
@ -66,6 +67,7 @@ interface UseChatReturn {
handleCommand: (command: Command) => void
clearMessages: () => void
handleServerMessage: (message: WsOutbound) => void
setSendMessage: (fn: (msg: WsInbound) => boolean) => void
// Topic 方法
selectTopic: (topicId: string) => void
@ -171,6 +173,12 @@ export function useChat(): UseChatReturn {
const selectedTopicRef = useRef<string | null>(null)
const pendingNewTopicRef = useRef(false)
// Ref to send commands from within handleServerMessage (set by App.tsx)
const sendMessageRef = useRef<((msg: WsInbound) => boolean) | null>(null)
const setSendMessage = useCallback((fn: (msg: WsInbound) => boolean) => {
sendMessageRef.current = fn
}, [])
const isConnected = useMemo(() => connectionId !== null, [connectionId])
const selectedSession = useMemo(
() => sessions.find(s => s.session_id === selectedSessionId) ?? null,
@ -377,6 +385,11 @@ export function useChat(): UseChatReturn {
const msgSubagentTaskId = getSubagentTaskId(message)
if (msgSubagentTaskId && msgSubagentTaskId === currentSubAgentView.taskId) {
appendToSubAgentViewMessage(message)
// 子代理 todo_write 完成后自动刷新待办列表
if (message.type === 'tool_result' && (message as ToolResult).tool_name === 'todo_write') {
const refreshCmd = requestSubAgentTodoList(currentSubAgentView.taskId)
sendMessageRef.current?.({ type: 'command', payload: JSON.stringify(refreshCmd) })
}
return
}
// 丢弃其他子智能体的消息,避免 fall through 到主消息处理
@ -694,6 +707,14 @@ export function useChat(): UseChatReturn {
// 忽略这些消息
break
}
// 主视图 todo_write 完成后自动刷新待办列表
if (message.type === 'tool_result' && (message as ToolResult).tool_name === 'todo_write') {
const refreshCmd = subAgentViewRef.current?.taskId
? requestSubAgentTodoList(subAgentViewRef.current.taskId)
: requestTodoList()
sendMessageRef.current?.({ type: 'command', payload: JSON.stringify(refreshCmd) })
}
}, [])
const handleMessage = useCallback((content: string, attachments?: Attachment[]) => {
@ -938,6 +959,7 @@ export function useChat(): UseChatReturn {
handleCommand,
clearMessages,
handleServerMessage,
setSendMessage,
selectTopic,
createTopic,
switchTopic,