统一错误事件收成一个 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!({