内置工具失败改成 typed 错误穿出派发边界,事件带原始 error,软失败清零
- dispatch 返回 Result<Value, ToolCallError>:不再在工具内部吞掉错误再补一个失败 Value,handle_direct_tool_bridge 在唯一一处 match 里同时组装发给模型的内容与诊断
- 新增 ToolCallError { message, redact_limit, error, images },error 直接存序列化后的 typed 错误;每个内置工具在自己的 agent/tool/<tool>/error.rs 定义 typed enum,共用 case(参数不是对象 / 未审核字段等)抽到 agent/tool/error.rs 的 ToolFailure
- AgentRuntimeErrorEvent 增加 error 字段携带原始 typed 错误,schema 升到 agent-runtime-error.v3;应用日志详情行扩成 message=… error=… metadata=…,各带独立长度预算
- 软失败清零:import_account_assets、run_validation、browser_playtest、environment_check、apply_patch、editor_execute、cocos_execute 原先返回 Ok(失败载荷) 的分支改为 typed 错误,并把 status / dispatched / retryAllowed 等载荷带进错误,模型仍能看到
- 失败证据截图走 ToolFailure::attached_images 与 ToolCallError.images,截图放 #[serde(skip)] 字段,不混进诊断里的 error
- bridge_tool_result 去掉 is_error 入参,isError 只在组装处按 Ok/Err 分支写一次;MCP 线上 isError 字段与校验不变
- 修正上一提交遗留的两处 rustfmt:direct_tools_mcp.rs、runtime_tools/context.rs(同一 workspace 整体格式门禁要求)
- 同步更新技术方案文档字段表与共享记忆决策记录
验证:cargo check --bin genarrative-ai-game-creator-shell --tests 通过;cargo test --bin genarrative-ai-game-creator-shell -- agent:: 951 passed / 0 failed;npm run check:encoding、git diff --check 通过;Windows 专属 cocos/unity/godot 分支在 Linux 下临时去 cfg 交叉编译通过(无真实 Windows 构建)
This commit is contained in:
@@ -9296,4 +9296,15 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 决策(schema 与日志):`AGENT_RUNTIME_ERROR_SCHEMA_VERSION` 是 `agent-runtime-error.v2`,sidecar 由 `publicText / recoveryHint / detail` 收成 `message`(无读取方,不需要兼容层)。应用日志详情行由 `hint=… summary=… detail=… metadata=…` 收成 `message=… metadata=…`,身份行不变(本来就只放程序生成与调用方常量字段)。`message` 在应用日志里取原 `detail` 的 1200 字符预算、在 sidecar 里按 8 KiB 上限:用户只上传 AppData 应用日志、拿不到 sidecar,日志这一份必须是信息量最大的那份。
|
||||
- 决策(保留项):`direct-codex-failure:v2 … retryable=… summary=…;建议:…` 是前端(`features/agent-runtime/model.ts` 的 v2 正则)要解析的用户可见文案,`retryable` / `recovery_hint` 由 typed `DirectTurnError::is_retryable()` / `recovery_hint()` 判定,原样保留,只是不再进统一事件;`direct-codex` 路径把信息量最大的 `failure.to_string()`(typed Display)作为 `message`。`runtime_state.rs` 那条改为只记原始 `error`(投影后的用户文案已写进 `project.jsonl`,不必再存一遍)。
|
||||
- 改动范围:`agent/runtime_error.rs`、`agent/runtime_state.rs`、`agent/direct_tool_bridge.rs`、`agent/direct_runtime/mod.rs`;同步修正 `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 里 2026-09-15 的「统一事件至少包含 …」字段表与 2026-09-21 的日志两行口径。
|
||||
- 验证:`cargo check --bin genarrative-ai-game-creator-shell --tests` 通过;`cargo test --bin genarrative-ai-game-creator-shell agent::runtime_error::` 3 passed;`cargo test --bin genarrative-ai-game-creator-shell agent::` 950 passed / 1 failed(5 ignored),失败的是已知并发 flaky 用例 `agent::runtime_protocol::steering::goal_contract_steer_transition_tests::concurrent_distinct_frozen_root_steers_create_only_one_replacement`(单跑也时好时坏,与本次改动无关)。
|
||||
|
||||
## 2026-10-01 工具失败改成 typed 错误穿出 dispatch 边界,统一事件携带原始 error
|
||||
|
||||
- 背景:工具桥的派发边界把失败吞成 `Value`——`bridge_outcome(root, result)` 内部就把 typed 错误压成一句 `bridge_tool_failure` 文案,`isError` 由工具与桥手传;诊断只能从结果值里回读 `/content/0/text` 与(上一阶段临时挂在结果上的)`error` 键。结果是「哪一轮、哪个工具、什么结构化事实」在派发期间被降级成字符串,composer 只能靠 `isError=true` 反推分支。
|
||||
- 决策(dispatch 边界):`handle_direct_tool_bridge` 里的 `dispatch` 现在返回 `Result<Value, ToolCallError>`,失败原样穿出,不再在派发期压成 `Value`。`ToolCallError { message, redact_limit, error }` 是唯一载体:泛型 `impl<T: ToolFailure + ?Sized> From<&T>` 把具体错误 enum 的 `to_user_msg()`、`redact_limit()` 和原样序列化一起带出来——不做跨工具大 enum,不 Box(`?Sized` 让 trait 对象也能转)。`bridge_outcome` 随之删除。
|
||||
- 决策(composer):工具桥只有一个出口 `compose_direct_tool_outcome(state, tool, arguments, dispatchDenied, outcome)`:`Ok` 补 `isError=false`,`Err` 补 `isError=true` 并落一次诊断。`isError` 由分支决定,工具与桥都不再手传;MCP 完成包仍必须带布尔 `isError`(`direct_tools_mcp.rs::call_client_tool_bridge` 会校验),所以两条分支都写。五条早退(付费 / 写入 / 执行许可取不到、许可任务丢失、结算未落盘 `ReceiptNotPersisted`)也走同一出口:它们以前直接 `return Json(bridge_tool_failure(…))`,不写诊断。
|
||||
- 决策(诊断字段):`AgentRuntimeErrorEvent` 新增 `error`——产生失败的 typed 错误 enum 的原样序列化,`Value::Null` 表示该调用方(`agent/runtime_state.rs` 的终态公开消息)没有 typed 错误;`message` 仍是给人(以及转述给用户的模型)的那句话。应用日志详情行由 `message=… metadata=…` 扩成 `message=… error=… metadata=…`,`error` 取 400 字符预算(`AGENT_RUNTIME_ERROR_APP_LOG_ERROR_CHARS`)。`AGENT_RUNTIME_ERROR_SCHEMA_VERSION` 升到 `agent-runtime-error.v3`(sidecar 无读取方,不需要兼容层)。sidecar 里的 `error` 与既有 `metadata` 一样按原文落盘、只在应用日志里脱敏:sidecar 留在项目内不上传,上传的日志那份已经过 `redact_agent_runtime_error`。
|
||||
- 决策(周边收口):`bridge_tool_result(text, images)` 去掉 `is_error` 入参,模板只剩 `{ content }`;`record_completed_regeneration` 不再用 `isError` 判「能否缓存这次美术重生成」——走到那里的只有 `Ok`(失败会 `?` 上抛),`ArtRegenerationAuthorizationRejection::CompletedResultNotSuccessful` 因此退役。
|
||||
- 决策(软失败清零,同日续做):不接受「工具调用本身成功、载荷里写着失败」的软失败,十处全部改成各工具自己的 typed 变体,载荷原样进变体、由 `to_user_msg()` 拼成给模型的文案:`ImportAccountAssetsError::ImportFailed / ImportPartial`、`RunValidationError::ValidationNotPassed`、`BrowserPlaytestError::PlaytestNotPassed`、`EnvironmentCheckError::EnvironmentNotReady`、`ApplyPatchError::PatchNotApplied`、`EditorExecuteError::ExecutionNotCompleted / ExecutionUnconfirmed`、`CocosExecuteError::ExecutionBusy / ReconciliationPending / ExecutionNotCompleted`。于是 `isError` 与执行租约的 `passed`(`result.is_ok()`)重新对齐:execute 类失败仍落 `ExecutionPhase::Draining`。
|
||||
- 决策(证据图):`ToolFailure` 增加 `attached_images() -> Vec<String>`(默认空),`ToolCallError` 增加 `images`;composer 的 `Err` 分支在 `bridge_tool_failure` 的成文结果上追加 MCP image block。带图的两个变体(`ValidationNotPassed` / `PlaytestNotPassed` / `ExecutionNotCompleted`)把截图正文放在 `#[serde(skip)]` 字段里:图要回到结果里,但 base64 不能进 sidecar 的 `error` 正文。`bridge_validation_result` 由泛型 `?` 改成接受一个 `fn(Value, Vec<String>) -> E` 构造器,验证与试玩共用同一份 `passed` / 截图投影。
|
||||
- 改动范围:`agent/direct_tool_bridge.rs`、`agent/runtime_error.rs`、`agent/tool/error.rs` 与 23 个 `agent/tool/<tool>/error.rs`(补 `serde::Serialize`,`EditorKind` 同样补)、`agent/tool/run_validation/error.rs`、`agent/tool/browser_playtest/error.rs`、`agent/tool/environment_check/error.rs`、`agent/tool/apply_patch/error.rs`、`agent/tool/import_account_assets/error.rs`、`agent/tool/editor_execute/error.rs`、`agent/tool/cocos_execute/error.rs`、`agent/tool/prepare_game_art/error.rs`、`agent/generation/canvas_generation.rs`(并发测试改用 `Result`)、`agent/runtime_state.rs`、`agent/direct_runtime/mod.rs`;同步修正 `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 的统一事件字段表与应用日志两行口径。
|
||||
- 验证:`cargo check --bin genarrative-ai-game-creator-shell --tests` 通过(无新增警告);Windows 专属的 Cocos / Unity / Godot 执行路径在 Linux 上临时去掉 `#[cfg(all(windows, …))]` 的 `windows` 条件后,用 `cargo check --features cocos-editor-execute,unity-editor-execute,godot-editor-execute --tests` 交叉编译校验通过,随后原样还原(没有 Windows 真机构建);`cargo test --bin genarrative-ai-game-creator-shell -- agent::` 951 passed / 0 failed(5 ignored);`-- agent::direct_tool_bridge:: agent::direct_tools_mcp:: agent::runtime_error::` 73 passed;`-- agent:: tests::project::` 1076 passed / 2 failed,两条都是并行负载下的已知 flake(`agent::runtime_actions::provider_request_builders::tests::art_director_request_exposes_canvas_only_for_the_keyed_owner_route` 与 `tests::project::background_agent_runtime_can_generate_platform_art_asset`),单跑各自通过;`npm run check:encoding`、`git diff --check` 通过。
|
||||
|
||||
@@ -1647,7 +1647,7 @@ DirectProject 在收到完整游戏策划或游戏制作请求后,必须把视
|
||||
|
||||
## 2026-09-15 AGC 统一错误事件、诊断落库与验收反馈
|
||||
|
||||
DirectProject、Agent Runtime、Provider、app-server、内置 MCP、命令执行、构建和浏览器试玩的失败必须先转换为统一的 `AgentRuntimeErrorEvent`,再分别投影到用户消息、运行面板和项目诊断文件;业务模块不得自行拼接只有一句“执行失败”的终态文案。统一事件至少包含 `schemaVersion / eventId / clientTurnId / source / stage / code / occurredAt / elapsedMs / message / detailRef`,其中 `message` 是产生失败的 typed 错误在失败现场写好、脱敏后的人类可读文案,`detailRef` 指向项目内有界诊断记录;Token、Cookie、URL/query、私钥、宿主绝对路径、原始请求正文和未脱敏 stderr 不得进入对话或用户可见文本。
|
||||
DirectProject、Agent Runtime、Provider、app-server、内置 MCP、命令执行、构建和浏览器试玩的失败必须先转换为统一的 `AgentRuntimeErrorEvent`,再分别投影到用户消息、运行面板和项目诊断文件;业务模块不得自行拼接只有一句“执行失败”的终态文案。统一事件至少包含 `schemaVersion / eventId / clientTurnId / source / stage / code / occurredAt / elapsedMs / message / error / detailRef`,其中 `message` 是产生失败的 typed 错误在失败现场写好、脱敏后的人类可读文案,`error` 是同一个 typed 错误 enum 的原样序列化(一个 case 一个变体,供开发者按变体与字段定位;没有 typed 错误的调用方写 `null`),`detailRef` 指向项目内有界诊断记录;Token、Cookie、URL/query、私钥、宿主绝对路径、原始请求正文和未脱敏 stderr 不得进入对话或用户可见文本。
|
||||
|
||||
项目内统一落库目录为 `.agent/runtime/errors/`,事件记录采用幂等 JSONL 或 JSON sidecar;写入失败不能覆盖原始业务错误,但必须在事件中标记 `persistenceFailed`。DirectProject 对话历史必须持久化本轮用户消息、终态错误的安全 assistant 投影和诊断引用,使下一轮能够读取上一轮失败证据。前端只展示宿主给的安全文案;诊断正文只留在 `detailRef` 指向的有界、脱敏记录里,不进入用户可见文本。
|
||||
|
||||
@@ -1797,9 +1797,9 @@ Direct 回合的所有权属于进程内项目身份锁,不属于当前页面
|
||||
|
||||
## 2026-09-21 统一错误事件同时落到 AppData 应用日志
|
||||
|
||||
`AgentRuntimeErrorEvent` 把失败投影到用户消息、运行面板和项目内 `.agent/runtime/errors/<eventId>.json` 时,同一份已脱敏诊断还要投影成 AppData `diagnostics/application.log` 的两行:`agent.runtime.error`(身份行:`eventId / source / stage / code / clientTurnId / elapsedMs / detailRef`)与 `agent.runtime.error.detail`(详情行:`message / metadata`)。原因是项目内 sidecar 只在项目目录可见,而“报告问题”只上传应用级日志:没有这两行时,用户提交的失败消息里只剩一个 `详情:.agent/runtime/errors/...json` 路径,团队拿不到诊断正文。
|
||||
`AgentRuntimeErrorEvent` 把失败投影到用户消息、运行面板和项目内 `.agent/runtime/errors/<eventId>.json` 时,同一份已脱敏诊断还要投影成 AppData `diagnostics/application.log` 的两行:`agent.runtime.error`(身份行:`eventId / source / stage / code / clientTurnId / elapsedMs / detailRef`)与 `agent.runtime.error.detail`(详情行:`message / error / metadata`)。原因是项目内 sidecar 只在项目目录可见,而“报告问题”只上传应用级日志:没有这两行时,用户提交的失败消息里只剩一个 `详情:.agent/runtime/errors/...json` 路径,团队拿不到诊断正文。
|
||||
|
||||
口径:两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,字段不退化成第二份来源;sidecar 里的 `message` 按 8 KiB 上限、应用日志的 `message` 与 `metadata` 按(1200 / 200 字符)预算先脱敏再截断,落盘前还会被 `sanitize_diagnostic_message` 二次脱敏并按行截断,因此自由文本字段在行内先压平换行。拆两行是因为整行一旦出现凭据标记会被整体替换成脱敏占位:所以**自由文本(message)只放详情行**,身份行只留程序生成与调用方常量字段,详情行被整体脱敏时事件仍能按 eventId / detailRef 定位。写日志先于写 sidecar:sidecar 失败不能连日志一起丢。
|
||||
口径:两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,字段不退化成第二份来源;sidecar 里的 `message` 按 8 KiB 上限、应用日志的 `message` / `error` / `metadata` 按(1200 / 400 / 200 字符)预算先脱敏再截断,落盘前还会被 `sanitize_diagnostic_message` 二次脱敏并按行截断,因此自由文本字段在行内先压平换行。拆两行是因为整行一旦出现凭据标记会被整体替换成脱敏占位:所以**自由文本(message)只放详情行**,身份行只留程序生成与调用方常量字段,详情行被整体脱敏时事件仍能按 eventId / detailRef 定位。写日志先于写 sidecar:sidecar 失败不能连日志一起丢。
|
||||
|
||||
## 2026-09-23 AGC UI 设计文档 Agent 工具化重写
|
||||
|
||||
|
||||
Reference in New Issue
Block a user