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,
"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) => {

View File

@ -648,6 +648,39 @@ impl DefaultSubAgentRuntime {
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]
@ -763,6 +796,10 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
}
}
// 6-8. 构建提示词、创建子代理、执行任务
// 统一为单个 Result 表达式model_resolver / create_subagent / execute_task
// 的任何失败都流入下方 match 的 Err 分支,经 handle_task_failure 返回结构化结果。
let result: Result<TaskToolResult, TaskError> = {
// 6. 构建子代理系统提示词
// 实时按 def.capability 过滤技能索引(替代冻结快照,反映运行时技能增删)
let skills_index = if def.capability.has_skill_policy() {
@ -808,9 +845,9 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
)?;
// 8. 执行任务
let result = self
.execute_task(agent, &session, &def, task.prompt.clone())
.await;
self.execute_task(agent, &session, &def, task.prompt.clone())
.await
};
// 9. 更新会话状态并保存
match result {
@ -828,24 +865,9 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
Ok(tool_result)
}
Err(e) => {
let mut session = session;
let status = e.as_status();
tracing::warn!(
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)
// 会话创建后的任何失败(含 AgentCreationFailed、Timeout、ExecutionFailed
// 统一返回结构化结果,携带 task_id 供前端导航
self.handle_task_failure(session, e).await
}
}
}
@ -907,7 +929,8 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
.find_subagent_def(&session.subagent_type)
.map_err(TaskError::InvalidArguments)?;
// 5. 创建子代理
// 5-6. 创建子代理 + 执行(统一为 Result失败走 handle_task_failure
let result: Result<TaskToolResult, TaskError> = {
let agent = self.create_subagent(
&session,
system_prompt,
@ -915,11 +938,9 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
parent_context.nesting_depth,
parent_context.task_id.clone(),
)?;
// 6. 使用历史继续执行
let result = self
.execute_task_with_history(agent, &session, additional_prompt)
.await;
self.execute_task_with_history(agent, &session, additional_prompt)
.await
};
// 7. 更新会话状态
match result {
@ -932,12 +953,8 @@ impl SubAgentRuntime for DefaultSubAgentRuntime {
Ok(tool_result)
}
Err(e) => {
let mut session = session;
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)
// 修复:原代码一律 mark_failed未处理 timeout现统一走 handle_task_failure
self.handle_task_failure(session, e).await
}
}
}

View File

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