@@ -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= { } d etail= {} 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 } m etadata= { 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 " } ) ,
) ;