统一错误事件收成一个 message,去掉 public_text / recovery_hint / detail

- AgentRuntimeErrorEvent 只留一个 message:产生失败的 typed 错误在失败现场写好的人类可读文案;persist_agent_runtime_error 由 10 个入参减到 8 个,agent_runtime_error_app_log_lines 同步收口
- 删掉 recovery_hint 与 detail 两个字段及其入参;开发者信息不另开字段,全部进 metadata:tool、脱敏 arguments、directTurn、dispatchDenied
- 应用日志详情行由 hint=… summary=… detail=… metadata=… 收成 message=… metadata=…,身份行不变
- message 预算:sidecar 8 KiB、应用日志 1200 字符(取原来 detail 的预算,用户只上传 AppData 日志、拿不到 sidecar)
- direct-codex 路径把 typed Display 全文作为 message;retryable / recovery_hint 仍由 typed DirectTurnError 判定,只留在前端要解析的 direct-codex-failure:v2 文案里
- runtime_state.rs 那条改为只记原始 error(投影后的用户文案已经写进 project.jsonl,不再存第二份)
- 同步修正技术方案文档的字段表与共享记忆的决策记录
This commit is contained in:
2026-10-01 10:55:26 +08:00
parent 1dfe0dd8d4
commit 4999653a5e
6 changed files with 57 additions and 90 deletions
@@ -2012,8 +2012,6 @@ pub(crate) fn record_direct_codex_failure(
"direct-codex",
stage.id(),
error_code,
&summary,
recovery_hint,
&detail,
None,
serde_json::json!({
@@ -4010,7 +4010,8 @@ async fn handle_direct_tool_bridge(
.and_then(Value::as_str)
.unwrap_or("客户端工具执行失败");
// 失败分类早就由各工具的 typed 错误在产生层给出,这里不再解析文案:`code` 只放稳定
// 的工具名,供并发排障定位"是哪个工具、哪一轮";入参与上下文进 metadata。
// 的工具名,供并发排障定位"是哪个工具、哪一轮";文案是 typed 错误写好的那一句,
// 入参与上下文进 metadata。
let _ = persist_agent_runtime_error(
&state.root,
client_turn_id.as_deref(),
@@ -4018,8 +4019,6 @@ async fn handle_direct_tool_bridge(
"tool-execution",
request.tool.as_str(),
message,
"查看项目错误诊断后处理",
message,
None,
serde_json::json!({
"tool": request.tool.clone(),
@@ -1,9 +1,9 @@
//! Shared, project-bound error events for Agent Runtime and DirectProject.
//!
//! Every caller supplies a safe public summary and a private detail. This
//! module is the only persistence boundary for the latter: it redacts project
//! paths and credentials before writing a bounded diagnostic sidecar, and
//! projects the same bounded diagnosis into the AppData application log.
//! Every caller supplies one human-readable message that its typed error built
//! at the failure site. This module is the only persistence boundary for it:
//! it redacts project paths and credentials before writing a bounded diagnostic
//! sidecar, and projects the same bounded text into the AppData application log.
use super::{redact_agent_runtime_error, write_agent_runtime_json_sidecar_with_max_bytes};
use serde::{Deserialize, Serialize};
@@ -13,22 +13,18 @@ use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
pub(crate) const AGENT_RUNTIME_ERROR_SCHEMA_VERSION: &str = "agent-runtime-error.v2";
pub(crate) const AGENT_RUNTIME_ERROR_MAX_DETAIL_CHARS: usize = 8 * 1024;
/// 应用日志里 detail / metadata 的字符预算。
/// sidecar 里 `message` 的字符上限。
pub(crate) const AGENT_RUNTIME_ERROR_MAX_MESSAGE_CHARS: usize = 8 * 1024;
/// 应用日志里 `message` / `metadata` 的字符预算。
///
/// `application.log` 的每一行在落盘前还会被 `sanitize_diagnostic_message` 截到 2 KiB,
/// 这里的预算留出身份字段与中文摘要的位置,保证被截掉的是诊断正文的尾部,而不是
/// `eventId`、`code` 或 `detailRef`。
pub(crate) const AGENT_RUNTIME_ERROR_APP_LOG_DETAIL_CHARS: usize = 1_200;
/// 这里的预算留出身份字段的位置,保证被截掉的是正文尾部,而不是 `eventId`、`code`
/// 或 `detailRef`。
pub(crate) const AGENT_RUNTIME_ERROR_APP_LOG_MESSAGE_CHARS: usize = 1_200;
pub(crate) const AGENT_RUNTIME_ERROR_APP_LOG_METADATA_CHARS: usize = 200;
/// 详情行里 public summary 的字符预算。
///
/// `summary` 由调用方给,`direct_tool_bridge` 传的是工具错误原文;这里与 sidecar 侧的摘要
/// 预算同口径(320 字符)截断,并再脱敏一次,避免摘要把整条详情行占满。
pub(crate) const AGENT_RUNTIME_ERROR_APP_LOG_SUMMARY_CHARS: usize = 320;
static ERROR_EVENT_SEQUENCE: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
@@ -41,8 +37,7 @@ pub(crate) struct AgentRuntimeErrorEvent {
pub code: String,
pub occurred_at_unix_nanos: String,
pub elapsed_ms: Option<u64>,
pub public_text: String,
pub recovery_hint: String,
pub message: String,
pub detail_ref: String,
pub persistence_failed: bool,
pub metadata: Value,
@@ -54,9 +49,7 @@ pub(crate) fn persist_agent_runtime_error(
source: &str,
stage: &str,
code: &str,
public_text: &str,
recovery_hint: &str,
detail: &str,
message: &str,
elapsed_ms: Option<u64>,
metadata: Value,
) -> Result<AgentRuntimeErrorEvent, String> {
@@ -67,8 +60,8 @@ pub(crate) fn persist_agent_runtime_error(
let sequence = ERROR_EVENT_SEQUENCE.fetch_add(1, Ordering::Relaxed);
let event_id = format!("error-{occurred_at_unix_nanos}-{sequence}");
let detail_ref = format!(".agent/runtime/errors/{event_id}.json");
let safe_detail =
redact_agent_runtime_error(root, detail, AGENT_RUNTIME_ERROR_MAX_DETAIL_CHARS);
let safe_message =
redact_agent_runtime_error(root, message, AGENT_RUNTIME_ERROR_MAX_MESSAGE_CHARS);
let diagnostic = serde_json::json!({
"schemaVersion": AGENT_RUNTIME_ERROR_SCHEMA_VERSION,
"eventId": event_id,
@@ -78,13 +71,11 @@ pub(crate) fn persist_agent_runtime_error(
"code": code,
"occurredAtUnixNanos": occurred_at_unix_nanos.to_string(),
"elapsedMs": elapsed_ms,
"publicText": public_text,
"recoveryHint": recovery_hint,
"detail": safe_detail,
"message": safe_message,
"metadata": metadata,
});
// 统一错误事件的项目内 sidecar 只在项目目录可见:用户提交错误报告时上传的是 AppData
// 应用日志,诊断包拿不到 detail。这里先把同一份已脱敏诊断留进应用日志,再落项目文件,
// 应用日志,诊断包拿不到 sidecar。这里先把同一份已脱敏正文留进应用日志,再落项目文件,
// 于是 sidecar 写失败也仍然留下可提交的诊断。
let app_log_lines = agent_runtime_error_app_log_lines(
root,
@@ -93,10 +84,8 @@ pub(crate) fn persist_agent_runtime_error(
source,
stage,
code,
public_text,
recovery_hint,
&detail_ref,
&safe_detail,
message,
elapsed_ms,
&metadata,
);
@@ -119,8 +108,7 @@ pub(crate) fn persist_agent_runtime_error(
code: code.to_string(),
occurred_at_unix_nanos: occurred_at_unix_nanos.to_string(),
elapsed_ms,
public_text: public_text.to_string(),
recovery_hint: recovery_hint.to_string(),
message: message.to_string(),
detail_ref,
persistence_failed: false,
metadata,
@@ -129,19 +117,14 @@ pub(crate) fn persist_agent_runtime_error(
/// 把统一错误事件投影成 AppData `diagnostics/application.log` 里的两行。
///
/// 传进来的 `detail` 是 sidecar 用的脱敏文本,`metadata` 则从未脱敏过。两行落到 `app_log!`
/// 时都会先按应用日志预算(1200 / 200 字符)再脱敏、再截断:`app_log!` 还会把同一行写到
/// stderr,那里没有 `sanitize_diagnostic_message` 兜底,所以每个调用方给的外来文本
/// (`summary` 按 320 字符预算)都在这里过一遍脱敏。
///
/// 已经脱敏过的 `detail` 也照走同一遍流水线,不按「调用方已脱敏」走短路:截断会把
/// `[redacted-secret]` 这类标记切开,而且这里是 `pub(crate)` 边界,不假设未来调用方一定先脱敏。
///
/// 拆成「身份行 + 详情行」是因为整行只要出现凭据标记就会被
/// [`crate::sanitize_diagnostic_message`] 整体替换成脱敏占位。因此身份行**只放程序生成或
/// 调用方常量字段**(eventId / source / stage / code / clientTurnId / elapsedMs /
/// detailRef),`summary`、`hint` 这些自由文本全部放详情行:自由文本里一个裸词
/// (例如 `credential`)就能让整行被替换,放错了就会把事件定位信息一起吃掉。
/// detailRef),`message` 这类自由文本全部放详情行:自由文本里一个裸词(例如
/// `credential`)就能让整行被替换,放错了就会把事件定位信息一起吃掉。
///
/// `message` 与 `metadata` 在这里再走一遍脱敏 + 截断,不按「调用方已脱敏」走短路:截断会把
/// `[redacted-secret]` 这类标记切开,而且这里是 `pub(crate)` 边界,不假设调用方一定先脱敏。
///
/// 单行口径在这里落地:`app_log!` 同时把这行写到 stderr,那里没有人替我们压行,
/// 所以每个字段(含 `source` / `stage` / `code` / `detailRef` 这些调用方给的标识)都先
@@ -153,10 +136,8 @@ pub(crate) fn agent_runtime_error_app_log_lines(
source: &str,
stage: &str,
code: &str,
public_text: &str,
recovery_hint: &str,
detail_ref: &str,
detail: &str,
message: &str,
elapsed_ms: Option<u64>,
metadata: &Value,
) -> [String; 2] {
@@ -166,29 +147,24 @@ pub(crate) fn agent_runtime_error_app_log_lines(
let stage = single_line_log_field(stage);
let code = single_line_log_field(code);
let detail_ref = single_line_log_field(detail_ref);
let public_text = single_line_log_field(&redact_agent_runtime_error(
root,
public_text,
AGENT_RUNTIME_ERROR_APP_LOG_SUMMARY_CHARS,
));
let elapsed_ms = elapsed_ms
.map(|value| value.to_string())
.unwrap_or_else(|| "none".to_string());
let identity = format!(
"agent.runtime.error eventId={event_id} source={source} stage={stage} code={code} clientTurnId={client_turn_id} elapsedMs={elapsed_ms} detailRef={detail_ref}"
);
let detail = redact_agent_runtime_error(root, detail, AGENT_RUNTIME_ERROR_APP_LOG_DETAIL_CHARS);
let metadata = redact_agent_runtime_error(
let message = single_line_log_field(&redact_agent_runtime_error(
root,
message,
AGENT_RUNTIME_ERROR_APP_LOG_MESSAGE_CHARS,
));
let metadata = single_line_log_field(&redact_agent_runtime_error(
root,
&metadata.to_string(),
AGENT_RUNTIME_ERROR_APP_LOG_METADATA_CHARS,
);
));
let detail_line = format!(
"agent.runtime.error.detail eventId={event_id} hint={} summary={} detail={} metadata={}",
single_line_log_field(recovery_hint),
single_line_log_field(&public_text),
single_line_log_field(&detail),
single_line_log_field(&metadata),
"agent.runtime.error.detail eventId={event_id} message={message} metadata={metadata}"
);
[identity, detail_line]
}
@@ -239,9 +215,7 @@ mod tests {
"direct-codex",
"code-generation",
"turn-idle-timeout",
"本轮没有收到完成事件",
"查看诊断后重试",
"C:\\Users\\private\\project https://provider.example/a?token=secret",
"本轮没有收到完成事件 C:\\Users\\private\\project https://provider.example/a?token=secret",
Some(1200),
serde_json::json!({"lastEvent":"item/started"}),
)
@@ -260,13 +234,8 @@ mod tests {
let root = parent.path().join("project");
crate::project::init_local_game_project_at(&root, "runtime-error", "错误事件")
.expect("init project");
// 生产路径传进来的是已经脱敏的 safe_detail(8 KiB 口径),这里按同一口径造输入;
// metadata 在生产里从未脱敏,仍按原文传。
let safe_detail = redact_agent_runtime_error(
&root,
"C:\\Users\\private\\project https://provider.example/a?token=secret\n第二行诊断",
AGENT_RUNTIME_ERROR_MAX_DETAIL_CHARS,
);
// 生产路径传进来的 message 未脱敏,在函数里按同一条流水线脱敏 + 截断;
// metadata 在生产里同样未脱敏,仍按原文传。
let lines = agent_runtime_error_app_log_lines(
&root,
"error-1-1",
@@ -274,10 +243,8 @@ mod tests {
"direct-codex",
"code-generation",
"turn-idle-timeout",
"本轮没有收到完成事件\n附带换行",
"查看诊断后重试",
".agent/runtime/errors/error-1-1.json",
&safe_detail,
"本轮没有收到完成事件\n附带换行 C:\\Users\\private\\project https://provider.example/a?token=secret\n第二行诊断",
Some(1200),
&serde_json::json!({"authorization": "Bearer secret"}),
);
@@ -301,14 +268,12 @@ mod tests {
"{identity}"
);
// 身份行只放程序生成或调用方常量字段:自由文本放错行会让「整行命中标记」把它吃掉。
assert!(!identity.contains("summary="), "{identity}");
assert!(!identity.contains("message="), "{identity}");
let detail = &persisted[1];
assert!(
detail.contains("summary=本轮没有收到完成事件 附带换行"),
"{detail}"
);
assert!(
detail.contains("detail=<absolute-path> <redacted-url> 第二行诊断"),
detail.contains(
"message=本轮没有收到完成事件 附带换行 <absolute-path> <redacted-url> 第二行诊断"
),
"{detail}"
);
assert!(!detail.contains("Bearer secret"), "{detail}");
@@ -321,10 +286,8 @@ mod tests {
"agc-tools",
"tool-execution",
"tool-error",
"credential rotation failed",
"查看项目错误诊断后处理",
".agent/runtime/errors/error-3-1.json",
&safe_detail,
"credential rotation failed",
None,
&serde_json::json!({"tool": "agc_tools"}),
);
@@ -110,8 +110,6 @@ pub(crate) fn append_game_creator_agent_runtime_terminal_public_message_at(
"agent-runtime",
&state.phase,
"agent-runtime-terminal",
&content,
"查看项目错误诊断后处理",
error,
None,
serde_json::json!({
@@ -9288,3 +9288,12 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
- 改动范围:`agent/tool/**`(`error.rs` + 每个工具的错误模块;工具实现仍留在 `direct_tool_bridge.rs`,不搬家)、`agent/direct_tool_bridge.rs`、`agent/direct_tools_mcp.rs`、`agent/runtime_tools/context.rs`、`agent/runtime_error.rs`、`agent/runtime_state.rs`、`agent/direct_runtime/mod.rs`;同步去掉 `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md` 与 `docs/technical/【技术方案】AGC错误报告与诊断上传-2026-08-31.md` 身份行字段表里的 retryable。
- 验证:`cargo test --bin genarrative-ai-game-creator-shell agent::` 951 passed / 0 failed(5 ignored);`cargo check --tests` 通过;`npm run check:encoding`、`git diff --check` 通过;前端 `resourceCanvasAssetGenerationQueue`、`resourceCanvasGenerationHostLifecycle`、`appSurface` 套件通过(「项目权限策略拒绝执行:{id}」对外文案保持原字面)。
- 边界(未完成):Windows 专属的 Cocos / Unity / Godot 执行工具(含 MCP 预检)已按同一口径实现,但只做了交叉配置编译校验(Linux 上临时放开 `windows` cfg 后 `cargo check --features cocos-editor-execute,unity-editor-execute,godot-editor-execute --tests`),没有 Windows 真机构建;`agent/direct_validation.rs` 的 Runtime 动作(`run_command` / `run_browser`)与 `direct_tools_mcp.rs` 里 MCP 专有记录协议(Codex 返回记录、skill 资源读取)仍用各自的字符串错误,它们是 Runtime 动作 / MCP 协议而非内置工具。
## 2026-10-01 统一错误事件收成一个 message(去掉 public_text / recovery_hint / detail)
- 背景:`AgentRuntimeErrorEvent` 的三个文本字段在工具桥这条路径上完全退化——`publicText` 与 `detail` 逐字相同,`recoveryHint` 是常量「查看项目错误诊断后处理」,而且三个字段同时进 sidecar 与日志,读者分不清哪个才是「要给人看的话」。核对确认这个事件没有任何读取方:三处调用都是 `let _ = persist_agent_runtime_error(...)`;前端读的 `publicText` 属于 `AgentRuntimeEventRecord`(`runtime_state.rs` 的 runtime event,`app/types.ts` + `features/agent-runtime/model.ts` 的 `formatAgentRuntimeEvent`),是另一个结构;`.agent/runtime/errors/*.json` 只有测试在读,`read_agent_runtime_error_detail` 命令早已退役。这条链路的用途就是写日志与诊断包。
- 决策(字段):事件只保留一个 `message`——由产生失败的 typed 错误在失败现场写好的人类可读文案(工具侧就是 `ToolFailure::to_user_msg()` 的那一句);删掉 `recovery_hint` 与 `detail` 两个字段及其入参。开发者信息不另开字段,全部进 `metadata`:`tool` + 脱敏 `arguments` + `directTurn` / `dispatchDenied`,即「工具 + 参数(脱敏)+ 上下文」。`persist_agent_runtime_error` 由 10 个入参减到 8 个,`agent_runtime_error_app_log_lines` 同步收口。
- 决策(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`(单跑也时好时坏,与本次改动无关)。
@@ -1647,9 +1647,9 @@ DirectProject 在收到完整游戏策划或游戏制作请求后,必须把视
## 2026-09-15 AGC 统一错误事件、诊断落库与验收反馈
DirectProject、Agent Runtime、Provider、app-server、内置 MCP、命令执行、构建和浏览器试玩的失败必须先转换为统一的 `AgentRuntimeErrorEvent`,再分别投影到用户消息、运行面板和项目诊断文件;业务模块不得自行拼接只有一句“执行失败”的终态文案。统一事件至少包含 `schemaVersion / eventId / clientTurnId / source / stage / code / occurredAt / elapsedMs / publicText / recoveryHint / detailRef`,其中 `publicText` 是脱敏后的可行动摘要,`detailRef` 指向项目内有界诊断记录;Token、Cookie、URL/query、私钥、宿主绝对路径、原始请求正文和未脱敏 stderr 不得进入对话或用户可见文本。
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 不得进入对话或用户可见文本。
项目内统一落库目录为 `.agent/runtime/errors/`,事件记录采用幂等 JSONL 或 JSON sidecar;写入失败不能覆盖原始业务错误,但必须在事件中标记 `persistenceFailed`。DirectProject 对话历史必须持久化本轮用户消息、终态错误的安全 assistant 投影和诊断引用,使下一轮能够读取上一轮失败证据。前端只展示 `publicText`,点击详情后按 `detailRef` 读取有界、脱敏的诊断,不直接展示私有 `detail`。
项目内统一落库目录为 `.agent/runtime/errors/`,事件记录采用幂等 JSONL 或 JSON sidecar;写入失败不能覆盖原始业务错误,但必须在事件中标记 `persistenceFailed`。DirectProject 对话历史必须持久化本轮用户消息、终态错误的安全 assistant 投影和诊断引用,使下一轮能够读取上一轮失败证据。前端只展示宿主给的安全文案;诊断正文只留在 `detailRef` 指向的有界、脱敏记录里,不进入用户可见文本。
前端取回 `detailRef` 的口径是失败文案末尾的固定后缀「;详情:<detailRef>」:Rust 侧 `direct_codex_failure_text_keeps_the_detail_ref_marker_for_the_renderer` 与前端 `tests/agentRuntimeErrorDetail.test.ts` 各自钉住同一份文案形状与它的解析,任一侧改文案或改解析都会变红。失败提示只展示映射后的安全 `publicText`(v1 历史形状与 v2 现行形状都映射成「阶段 + 摘要 + 建议 + 是否可直接重试」),诊断正文不在提示里预读、也不写入历史投影,而是由聊天状态栏下方的「查看详情」入口按需调用只读命令读取并二次脱敏;这条交互由 `tests/appSurface/chat-composer.suite.ts` 的「失败提示保留可执行原因,诊断正文只在「查看详情」时读取」与 `tests/agentRuntimeModel.test.ts` 的 v2 映射用例钉住,渲染层仍不参与错误分类。
@@ -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`(详情行:`hint / summary / detail / 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 / metadata`)。原因是项目内 sidecar 只在项目目录可见,而“报告问题”只上传应用级日志:没有这两行时,用户提交的失败消息里只剩一个 `详情:.agent/runtime/errors/...json` 路径,团队拿不到诊断正文。
口径:两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,字段不退化成第二份来源;`summary` 按 320 字符、`detail` 与 `metadata` 按(1200 / 200 字符)预算先脱敏再截断,落盘前还会被 `sanitize_diagnostic_message` 二次脱敏并按行截断,因此自由文本字段在行内先压平换行。拆两行是因为整行一旦出现凭据标记会被整体替换成脱敏占位:所以**自由文本(summary / hint / detail)只放详情行**,身份行只留程序生成与调用方常量字段,详情行被整体脱敏时事件仍能按 eventId / detailRef 定位。写日志先于写 sidecar:sidecar 失败不能连日志一起丢。
口径:两行都由 `agent/runtime_error.rs` 从同一份 diagnosis 生成,字段不退化成第二份来源;sidecar 里的 `message` 按 8 KiB 上限、应用日志的 `message` 与 `metadata` 按(1200 / 200 字符)预算先脱敏再截断,落盘前还会被 `sanitize_diagnostic_message` 二次脱敏并按行截断,因此自由文本字段在行内先压平换行。拆两行是因为整行一旦出现凭据标记会被整体替换成脱敏占位:所以**自由文本(message)只放详情行**,身份行只留程序生成与调用方常量字段,详情行被整体脱敏时事件仍能按 eventId / detailRef 定位。写日志先于写 sidecar:sidecar 失败不能连日志一起丢。
## 2026-09-23 AGC UI 设计文档 Agent 工具化重写