feat(task): 优化孙智能体任务消息的发送与工具注册

- 在发送任务消息时增加可选的任务仓库参数支持子任务重发
- 新增 extract_parent_task_id 函数用于提取孙智能体的父任务 ID
- 补发子任务(孙智能体)的 TaskStarted 事件,解决视图重进导致的 navigateToTaskId 丢失
- 判断并附加子任务的父任务 ID,完善日志记录与事件发送
- 在子智能体运行时根据深度排除 task 工具,防止无限嵌套调用
- ToolRegistry 新增 without 方法,可创建排除指定工具的新实例用于子智能体配置
This commit is contained in:
oudecheng 2026-06-22 11:31:41 +08:00
parent 606fcbcd29
commit 6a496ce212
4 changed files with 80 additions and 5 deletions

View File

@ -492,7 +492,7 @@ async fn handle_inbound(
if let Some(task_session_id) = response.metadata.get("task_session_id") { if let Some(task_session_id) = response.metadata.get("task_session_id") {
// 提前提取 task_id用于给历史消息打标记 // 提前提取 task_id用于给历史消息打标记
let task_id = response.metadata.get("task_id").cloned().unwrap_or_default(); let task_id = response.metadata.get("task_id").cloned().unwrap_or_default();
if let Err(e) = send_task_messages(&store, task_session_id, sender, Some(task_id.clone())).await { if let Err(e) = send_task_messages(&store, task_session_id, sender, Some(task_id.clone()), Some(&state.task_repository)).await {
tracing::warn!(error = %e, task_session_id = %task_session_id, "Failed to send task messages"); tracing::warn!(error = %e, task_session_id = %task_session_id, "Failed to send task messages");
} }
@ -570,7 +570,7 @@ async fn handle_inbound(
&load_chat_channel, &load_chat_channel,
load_chat_id, load_chat_id,
); );
if let Err(e) = send_task_messages(&store, &session_id, sender, None).await { if let Err(e) = send_task_messages(&store, &session_id, sender, None, None).await {
tracing::warn!( tracing::warn!(
error = %e, error = %e,
channel = %load_chat_channel, channel = %load_chat_channel,
@ -669,9 +669,13 @@ async fn send_topic_history(
for task in running_tasks { for task in running_tasks {
if task.state == TaskSessionState::Running { if task.state == TaskSessionState::Running {
// 判断是否为孙智能体parent_session_id 以 "sub:" 开头表示父会话是子智能体
let parent_task_id = extract_parent_task_id(&task);
tracing::info!( tracing::info!(
task_id = %task.id, task_id = %task.id,
description = %task.description, description = %task.description,
parent_task_id = ?parent_task_id,
"Re-sending TaskStarted for running task after topic history load" "Re-sending TaskStarted for running task after topic history load"
); );
let _ = sender let _ = sender
@ -680,7 +684,7 @@ async fn send_topic_history(
description: task.description.clone(), description: task.description.clone(),
subagent_type: task.subagent_type.clone(), subagent_type: task.subagent_type.clone(),
topic_id: Some(topic_id.to_string()), topic_id: Some(topic_id.to_string()),
parent_task_id: None, parent_task_id,
tool_call_id: None, tool_call_id: None,
}) })
.await; .await;
@ -696,6 +700,7 @@ async fn send_task_messages(
session_id: &str, session_id: &str,
sender: &mpsc::Sender<WsOutbound>, sender: &mpsc::Sender<WsOutbound>,
subagent_task_id: Option<String>, subagent_task_id: Option<String>,
task_repository: Option<&Arc<dyn TaskRepository>>,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let messages = store.load_messages(session_id)?; let messages = store.load_messages(session_id)?;
@ -713,6 +718,37 @@ async fn send_task_messages(
} }
} }
// 补发子任务(孙智能体)的 TaskStarted 事件
// 解决重新进入子智能体视图后 navigateToTaskId 丢失的问题
if let (Some(repo), Some(parent_task_id)) = (task_repository, &subagent_task_id) {
match repo.list_tasks_for_session(session_id).await {
Ok(child_tasks) => {
for child in child_tasks {
if child.state == TaskSessionState::Running {
tracing::info!(
child_task_id = %child.id,
parent_task_id = %parent_task_id,
"Re-sending TaskStarted for child task after sub-agent view re-enter"
);
let _ = sender
.send(WsOutbound::TaskStarted {
task_id: child.id.clone(),
description: child.description.clone(),
subagent_type: child.subagent_type.clone(),
topic_id: child.parent_topic_id.clone(),
parent_task_id: Some(parent_task_id.clone()),
tool_call_id: None,
})
.await;
}
}
}
Err(e) => {
tracing::warn!(error = %e, session_id = %session_id, "Failed to list child tasks for resend");
}
}
}
Ok(()) Ok(())
} }
@ -737,6 +773,20 @@ fn set_subagent_task_id(outbound: &mut WsOutbound, task_id: &str) {
} }
} }
/// 从 TaskSession 中提取父任务 ID仅孙智能体有值
/// 孙智能体的 parent_session_id 格式为 "sub:{grandparent_session}:task:{parent_task_uuid}"
/// 从中提取 "task:{parent_task_uuid}" 作为 parent_task_id。
fn extract_parent_task_id(task: &crate::tools::task::types::TaskSession) -> Option<String> {
let parent = &task.parent_session_id;
// 仅当父会话是子智能体会话时才提取(格式: "sub:...:task:{uuid}"
if parent.starts_with("sub:") {
if let Some(pos) = parent.find(":task:") {
return Some(parent[pos + 1..].to_string()); // "task:{uuid}"
}
}
None
}
/// 将 ChatMessage 转换为 WsOutbound 列表 /// 将 ChatMessage 转换为 WsOutbound 列表
fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Vec<WsOutbound> { fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Vec<WsOutbound> {
use crate::bus::message::ToolMessageState; use crate::bus::message::ToolMessageState;

View File

@ -73,6 +73,20 @@ impl ToolRegistry {
.cloned() .cloned()
.collect() .collect()
} }
/// 创建一个排除指定工具的新 registry 副本
pub fn without(&self, exclude: &[&str]) -> Self {
let exclude_set: std::collections::HashSet<&str> = exclude.iter().copied().collect();
let tools = self.tools.read().expect("ToolRegistry lock poisoned");
let filtered: HashMap<String, Arc<dyn ToolTrait>> = tools
.iter()
.filter(|(name, _)| !exclude_set.contains(name.as_str()))
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let new_registry = ToolRegistry::new();
*new_registry.tools.write().expect("ToolRegistry lock poisoned") = filtered;
new_registry
}
} }
impl Default for ToolRegistry { impl Default for ToolRegistry {

View File

@ -19,6 +19,7 @@ use crate::tools::{ToolContext, ToolRegistry};
use super::error::TaskError; use super::error::TaskError;
use super::prompt::{extract_summary, SubagentPromptBuilder}; use super::prompt::{extract_summary, SubagentPromptBuilder};
use super::repository::TaskRepository; use super::repository::TaskRepository;
use super::tool::TaskTool;
use super::types::{SubagentDef, SubagentSource, TaskDefinition, TaskSession, TaskToolResult}; use super::types::{SubagentDef, SubagentSource, TaskDefinition, TaskSession, TaskToolResult};
/// 子代理运行时配置 /// 子代理运行时配置
@ -332,9 +333,17 @@ impl DefaultSubAgentRuntime {
) -> Result<AgentLoop, TaskError> { ) -> Result<AgentLoop, TaskError> {
let prompt_provider = Arc::new(StaticSystemPromptProvider::new(system_prompt)); let prompt_provider = Arc::new(StaticSystemPromptProvider::new(system_prompt));
// 孙智能体depth >= 2不注册 task 工具,防止无限嵌套
let child_depth = parent_nesting_depth + 1;
let tools = if child_depth >= 2 {
Arc::new(self.subagent_tools.without(&[TaskTool::TOOL_NAME]))
} else {
self.subagent_tools.clone()
};
AgentLoop::with_tools_and_system_prompt_provider( AgentLoop::with_tools_and_system_prompt_provider(
AgentRuntimeConfig::from(self.provider_config.clone()), AgentRuntimeConfig::from(self.provider_config.clone()),
self.subagent_tools.clone(), tools,
prompt_provider, prompt_provider,
None, // 子代理不需要 skill provider None, // 子代理不需要 skill provider
) )

View File

@ -15,6 +15,8 @@ pub struct TaskTool {
} }
impl TaskTool { impl TaskTool {
pub const TOOL_NAME: &'static str = "task";
/// 创建 TaskTool /// 创建 TaskTool
/// - `max_nesting_depth = None`:无深度限制(主 agent /// - `max_nesting_depth = None`:无深度限制(主 agent
/// - `max_nesting_depth = Some(N)`:允许最多 N 层嵌套(子 agent /// - `max_nesting_depth = Some(N)`:允许最多 N 层嵌套(子 agent
@ -29,7 +31,7 @@ impl TaskTool {
#[async_trait] #[async_trait]
impl Tool for TaskTool { impl Tool for TaskTool {
fn name(&self) -> &str { fn name(&self) -> &str {
"task" Self::TOOL_NAME
} }
fn description(&self) -> &str { fn description(&self) -> &str {