fix(cloud-agent): 工具失败归类与可行动字段

工具失败此前只有一句自然语言(tool_failed + text),无法区分"模型自己出的错"和真实的
权限/状态/上游问题:模型拿不到可自纠的反馈,排查时分不清该改提示词、改权限还是等上游。

- 新增 backend/internal/app/cloud_agent_tool_error_class.go:把失败归成
  invalid_model_output(幻觉工具名;本轮契约之外、只被 contextScope 挡掉的调用)、
  schema_error(缺必填字段/类型错/未知字段/非法 JSON)、state_conflict(画布在读取之后被改动)、
  permission_violation(只读轮发起写入等真实授权边界)、upstream_failure(上游 4xx/5xx、
  超时、媒体上游拒绝),其余归 tool_error。
- 每类附 retryable 与稳定的 requiredAction(fix_arguments / reread_canvas /
  use_advertised_tools / ask_user / report_to_user),写进 tool_failed 的回执与载荷顶层。
  只加标注:没有任何放行或拒绝逻辑读它。
- 缺必填字段的通用判定:各工具的校验点抛的多是普通 AppError,从错误类型看不出"模型漏了字段"。
  现在用本轮实际暴露的 schema 的 required 与参数比对,命中即按参数契约错误处理(回传 schema 与
  guidance 供模型自纠);只在业务侧确实按参数问题拒绝(400/422)时套用,已分类的参数错误不套壳。
- 幻觉工具名的拒绝文案更具体:过去一律"工具未获本轮权限授权",现在指向"不存在的工具 + 只使用
  本轮工具表里列出的工具"。
- 前端工具卡失败时显示归类标签,比统一的"执行失败"更能说明下一步该做什么。
This commit is contained in:
AetherNo2332
2026-09-22 00:53:32 +08:00
parent 6f45240599
commit 4e2df72eeb
6 changed files with 514 additions and 79 deletions

View File

@@ -4,21 +4,25 @@ export const AGENT_TOOL_METADATA: Record<string, { summary: string | ((context:
canvas_list_node_types: { summary: "已读取可用节点类型", failureMessage: "获取可用节点类型失败" },
canvas_get_state: { summary: "已读取当前画布", failureMessage: "获取画布内容失败" },
task_get: { summary: "已查询任务状态", failureMessage: "查询任务状态失败" },
canvas_apply_ops: { summary: ({ pending }) => pending ? "准备更新画布内容" : "画布内容已保存至服务端", failureMessage: "更新画布内容失败" },
canvas_apply_ops: { summary: ({ pending }) => (pending ? "准备更新画布内容" : "画布内容已保存至服务端"), failureMessage: "更新画布内容失败" },
model_list: { summary: "已获取可用模型", failureMessage: "获取可用模型失败" },
generate_media: { summary: ({ pending, detail }) => pending ? "准备创建媒体节点并生成" : field(detail, "eventType") === "tool_completed" ? "生成结果已回写画布节点" : "媒体节点已创建,生成任务已提交", failureMessage: "媒体生成未完成" },
generate_media: { summary: ({ pending, detail }) => (pending ? "准备创建媒体节点并生成" : field(detail, "eventType") === "tool_completed" ? "生成结果已回写画布节点" : "媒体节点已创建,生成任务已提交"), failureMessage: "媒体生成未完成" },
};
export type AgentToolCategory = "read" | "create" | "operate";
function record(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
}
function toolArguments(detail?: unknown) {
const value = record(detail).arguments;
if (typeof value !== "string") return record(value);
try { return record(JSON.parse(value)); } catch { return {}; }
try {
return record(JSON.parse(value));
} catch {
return {};
}
}
/**
@@ -88,3 +92,27 @@ export function friendlyAgentToolSummary(toolName: string, text: string, detail?
const failure = metadata?.failureMessage;
return failed ? failure || "操作未完成" : summary || (pending ? "准备执行操作" : "操作已完成");
}
/**
* 工具失败的稳定归类标签(后端 `errorClass`,见 handoff 工作项 B 的第一步)。
* 界面上把"模型自己出的错"和"真实的权限/状态/上游问题"分开显示,用户与排查都不用猜。
*/
export const AGENT_TOOL_ERROR_CLASS_LABELS: Record<string, string> = {
invalid_model_output: "模型输出问题",
schema_error: "参数不符合契约",
state_conflict: "画布状态已变化",
permission_violation: "超出本轮权限",
upstream_failure: "上游故障",
tool_error: "工具执行失败",
};
/** 从工具事件载荷里取归类标签:优先用后端给的 label,其次查本地映射。 */
export function agentToolErrorClassLabel(detail?: unknown): string | undefined {
const payload = record(detail);
const nested = record(payload.result);
const label = payload.errorClassLabel ?? nested.errorClassLabel;
if (typeof label === "string" && label) return label;
const errorClass = payload.errorClass ?? nested.errorClass;
if (typeof errorClass !== "string" || !errorClass) return undefined;
return AGENT_TOOL_ERROR_CLASS_LABELS[errorClass] ?? "工具执行失败";
}