fix(subagent): 失败/超时子智能体返回结构化结果,支持前端点击查看详情

spawn()/resume() 在会话创建后的失败(含超时、执行错误、AgentCreationFailed)
现返回 Ok(TaskToolResult { status: "failed"/"timeout", task_id }) 而非 Err(e),
使 tool_result 内容始终为结构化 JSON。前端已有的 taskStatusConfig、StatusIcon、
"查看完整会话"按钮自动生效,同时修复服务重启后点击入口丢失的问题。

- runtime.rs: 提取 handle_task_failure 辅助方法统一处理失败;将 model_resolver/
  create_subagent 的 ? 纳入 result 块,确保所有会话后失败都返回结构化结果;
  修复 resume() 未区分 timeout 的 bug
- tool.rs: 根据 task_result.status 判断 success,失败时 output 仍为 JSON
- agent_loop.rs: 失败时 result.output 非空则透传(仅影响 task 工具),其他工具回退
  到 "Error: {error}" 纯文本
This commit is contained in:
oudecheng 2026-08-05 09:05:35 +08:00
parent 4e74d66a7c
commit 1e9075e1ed
3 changed files with 122 additions and 89 deletions

View File

@ -1752,7 +1752,14 @@ impl AgentLoop {
output = %result.output, output = %result.output,
"Tool returned an error result" "Tool returned an error result"
); );
ToolExecutionOutcome::failure(format!("Error: {}", error), Some(error)) // task 工具在失败时提供结构化 JSON output含 task_id直接透传
// 其他工具 output 为空,走纯文本 "Error: {error}" 回退。
let failure_output = if !result.output.is_empty() {
result.output
} else {
format!("Error: {}", error)
};
ToolExecutionOutcome::failure(failure_output, Some(error))
} }
} }
Err(e) => { Err(e) => {

View File

@ -648,6 +648,39 @@ impl DefaultSubAgentRuntime {
Err(_) => Err(TaskError::Timeout), Err(_) => Err(TaskError::Timeout),
} }
} }
/// 会话创建后的失败处理:标记状态、持久化、发布错误事件、返回结构化失败结果。
///
/// 返回 `Ok(TaskToolResult)` 而非 `Err`,确保 tool_result 携带 `task_id` 供前端导航。
/// `save_task_session` 失败是基础设施故障,仍通过 `?` 返回 `Err`。
async fn handle_task_failure(
&self,
session: TaskSession,
error: TaskError,
) -> Result<TaskToolResult, TaskError> {
let status = error.as_status();
tracing::warn!(
task_id = %session.id,
session_id = %session.session_id,
status = %status,
error = %error,
"Task failed, updating session"
);
let mut session = session;
if status == "timeout" {
session.mark_timeout();
} else {
session.mark_failed(error.to_string());
}
self.task_repository.save_task_session(&session).await?;
publish_subagent_error(&self.bus, &session, &error.to_string()).await;
Ok(TaskToolResult {
status: status.to_string(),
summary: error.to_string(),
output: String::new(),
task_id: session.id.clone(),
})
}
} }
#[async_trait] #[async_trait]
@ -763,54 +796,58 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
} }
} }
// 6. 构建子代理系统提示词 // 6-8. 构建提示词、创建子代理、执行任务
// 实时按 def.capability 过滤技能索引(替代冻结快照,反映运行时技能增删) // 统一为单个 Result 表达式model_resolver / create_subagent / execute_task
let skills_index = if def.capability.has_skill_policy() { // 的任何失败都流入下方 match 的 Err 分支,经 handle_task_failure 返回结构化结果。
self.skills.system_index_prompt_filtered( let result: Result<TaskToolResult, TaskError> = {
def.capability.allowed_skills.as_deref(), // 6. 构建子代理系统提示词
&def.capability.denied_skills, // 实时按 def.capability 过滤技能索引(替代冻结快照,反映运行时技能增删)
) let skills_index = if def.capability.has_skill_policy() {
} else { self.skills.system_index_prompt_filtered(
self.skills.system_index_prompt() def.capability.allowed_skills.as_deref(),
}; &def.capability.denied_skills,
// 同步解析 def 中的 provider/model 覆盖,保证环境提示中的模型名与实际使用的模型一致
let effective_provider_config = match (def.provider.is_some(), def.model.is_some()) {
(true, _) | (_, true) => self
.model_resolver
.resolve(
def.provider.as_deref(),
def.model.as_deref(),
&self.provider_config,
) )
.map_err(|e| { } else {
TaskError::AgentCreationFailed(format!( self.skills.system_index_prompt()
"subagent '{}' model resolution failed: {}", };
def.name, e // 同步解析 def 中的 provider/model 覆盖,保证环境提示中的模型名与实际使用的模型一致
)) let effective_provider_config = match (def.provider.is_some(), def.model.is_some()) {
})?, (true, _) | (_, true) => self
_ => self.provider_config.clone(), .model_resolver
.resolve(
def.provider.as_deref(),
def.model.as_deref(),
&self.provider_config,
)
.map_err(|e| {
TaskError::AgentCreationFailed(format!(
"subagent '{}' model resolution failed: {}",
def.name, e
))
})?,
_ => self.provider_config.clone(),
};
let system_prompt = SubagentPromptBuilder::build(
&def,
&task.description,
&task.prompt,
&effective_provider_config,
skills_index.as_deref(),
);
// 7. 创建子代理
let agent = self.create_subagent(
&session,
system_prompt,
Some(&def),
parent_context.nesting_depth,
parent_context.task_id.clone(),
)?;
// 8. 执行任务
self.execute_task(agent, &session, &def, task.prompt.clone())
.await
}; };
let system_prompt = SubagentPromptBuilder::build(
&def,
&task.description,
&task.prompt,
&effective_provider_config,
skills_index.as_deref(),
);
// 7. 创建子代理
let agent = self.create_subagent(
&session,
system_prompt,
Some(&def),
parent_context.nesting_depth,
parent_context.task_id.clone(),
)?;
// 8. 执行任务
let result = self
.execute_task(agent, &session, &def, task.prompt.clone())
.await;
// 9. 更新会话状态并保存 // 9. 更新会话状态并保存
match result { match result {
@ -828,24 +865,9 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
Ok(tool_result) Ok(tool_result)
} }
Err(e) => { Err(e) => {
let mut session = session; // 会话创建后的任何失败(含 AgentCreationFailed、Timeout、ExecutionFailed
let status = e.as_status(); // 统一返回结构化结果,携带 task_id 供前端导航
tracing::warn!( self.handle_task_failure(session, e).await
task_id = %session.id,
session_id = %session.session_id,
status = %status,
error = %e,
"Task failed, updating session"
);
if status == "timeout" {
session.mark_timeout();
} else {
session.mark_failed(e.to_string());
}
self.task_repository.save_task_session(&session).await?;
// 发布子智能体 ErrorNotificationmetadata 注入 subagent_task_id 供前端路由到对应子智能体层
publish_subagent_error(&self.bus, &session, &e.to_string()).await;
Err(e)
} }
} }
} }
@ -907,19 +929,18 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
.find_subagent_def(&session.subagent_type) .find_subagent_def(&session.subagent_type)
.map_err(TaskError::InvalidArguments)?; .map_err(TaskError::InvalidArguments)?;
// 5. 创建子代理 // 5-6. 创建子代理 + 执行(统一为 Result失败走 handle_task_failure
let agent = self.create_subagent( let result: Result<TaskToolResult, TaskError> = {
&session, let agent = self.create_subagent(
system_prompt, &session,
Some(&def), system_prompt,
parent_context.nesting_depth, Some(&def),
parent_context.task_id.clone(), parent_context.nesting_depth,
)?; parent_context.task_id.clone(),
)?;
// 6. 使用历史继续执行 self.execute_task_with_history(agent, &session, additional_prompt)
let result = self .await
.execute_task_with_history(agent, &session, additional_prompt) };
.await;
// 7. 更新会话状态 // 7. 更新会话状态
match result { match result {
@ -932,12 +953,8 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
Ok(tool_result) Ok(tool_result)
} }
Err(e) => { Err(e) => {
let mut session = session; // 修复:原代码一律 mark_failed未处理 timeout现统一走 handle_task_failure
session.mark_failed(e.to_string()); self.handle_task_failure(session, e).await
self.task_repository.save_task_session(&session).await?;
// 发布子智能体 ErrorNotificationmetadata 注入 subagent_task_id 供前端路由到对应子智能体层
publish_subagent_error(&self.bus, &session, &e.to_string()).await;
Err(e)
} }
} }
} }

View File

@ -162,12 +162,21 @@ impl Tool for TaskTool {
}; };
// 5. 构建返回结果 // 5. 构建返回结果
// spawn()/resume() 在会话创建后的失败也返回 Ok(TaskToolResult { status: "failed"/"timeout" })
// 携带 task_id 供前端导航。仅会话创建前的失败参数校验、def 未找到等)返回 Err。
match result { match result {
Ok(task_result) => Ok(ToolResult { Ok(task_result) => {
success: true, let success = task_result.status == "success";
output: serde_json::to_string(&task_result)?, Ok(ToolResult {
error: None, success,
}), output: serde_json::to_string(&task_result)?,
error: if success {
None
} else {
Some(task_result.summary.clone())
},
})
}
Err(e) => Ok(ToolResult { Err(e) => Ok(ToolResult {
success: false, success: false,
output: String::new(), output: String::new(),