Skip to content

feat(trajectory): 重试/切换/传输事件写入轨迹账本——LLM 可观测性补齐 - #601

Open
AlphaCatMeow wants to merge 6 commits into
Stack-Cairn:mainfrom
AlphaCatMeow:feat-llm-trajectory-events
Open

feat(trajectory): 重试/切换/传输事件写入轨迹账本——LLM 可观测性补齐#601
AlphaCatMeow wants to merge 6 commits into
Stack-Cairn:mainfrom
AlphaCatMeow:feat-llm-trajectory-events

Conversation

@AlphaCatMeow

@AlphaCatMeow AlphaCatMeow commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Closes #600

Depends-On: #599
Stack-Root: #590

改动说明

seam 改造第五层(stack 收尾,PR-4/5)。把流内重试、跨供应商 failover 与逐候选传输装配三类运行时事实写入轨迹账本,落盘可回放——决策 #3(重试保留流内执行)承诺的可审计性由本层补齐。

线格式(共享包 lib/trajectory/types.ts,只增不改)

  • 新增 failover 事件:n(本请求内切换序号)、from/to(候选标签)、ti(稳定候选下标,0=主选)、err
  • 新增 transport 事件:p(候选标签)、o(上游 origin)、sp(use-system-proxy)、fu(full-URL 模式)、hn(头名列表)——只含头名与路由标记,永不含头值
  • retry 增补 p(候选标签)与既有 delay 的真实来源;LedgerStepfailovers/transports;eventLog 归约、layout 投影、fromMessages 降级路径同步
  • 旧读端对未知事件种类走既有收敛路径静默忽略,无迁移

传输快照(新增 runtime/transportSnapshot.ts)

  • 在 finalize 之后、出站之前对最终头集采样:头名小写去重排序,x-liveagent-upstream-origin 取 origin,use-system-proxy/full-URL 归为布尔标记
  • 逐候选独立采样:agentRunner buildTargetRoundStream 与 textOnly 各候选 start() 各记一份,审计 failover 逐候选传输装配独立性(核心正确性要求的运行时回放通道)
  • 鉴权头(authorization/x-api-key/x-goog-api-key)、代理 token、base64 覆盖包只出现头名;full-URL 模式只记布尔,URL 本体(可能含 query 凭据)不落盘;null 值(删除标记)过滤

密钥洗涤(新增 trajectory/scrub.ts)

  • recorder 全部 err 出口(stepEnd/noteRetry/noteFailover/compactionEnd/endTurn)统一过洗涤器:URL query 密钥参数、Bearer token、已知 key 形状(sk-/AIza)替换为 [redacted],正常报错文本不受影响

接线

  • withStreamRetry:退避在 onRetry 回调前计算并作为第四参上报——上报的就是实际要睡的值;取整为整毫秒,同时关闭浮点往返身份漂移(实时通道的 JS double 与 serde_json 读回值有 ULP 级差异,同一条重试会在收敛账本里出现两份;人工验收中实测抓到并修复)
  • agentRunner 新增 onFailoverAttempt/onTransportAttempt 回调,agent 模式 failover 首次落账(此前仅 onToolStatus 临时状态);toIndex 经 targetOrder 映射回稳定候选下标,sticky 重排不影响账本身份
  • text 模式 failover 从误记 noteRetry 改为独立 noteFailover,fromLabel/toLabel/targetIndex 不再丢失
  • 视图:OptionsTab 渲染传输/重试/切换明细行,OverviewTab 增切换计数,中英文案 4 键

零 Rust 改动(事件走既有 trajectory_append_events JSON 通道);buffer-until-commit 逐行保持(唯一贴近控制流的改动是退避计算提前一行,行为中性)。

验证结果

  • 新增 test/trajectory/scrub-transport.test.mjs(14 用例):洗涤器五组模式(URL key/Bearer/裸 key/正常报错不误伤 ×2)、快照头名不含值(含 base64 包与代理 token 反向断言)、full-URL 不记 URL 本体、null 头过滤、逐候选独立性(主选带 proxy 头/备选不带互不泄漏)— 14/14
  • recorder/event-log/stream-retry 增补 12 用例:noteFailover/noteTransport 线格式、err 洗涤端到端反向断言(google/openai/anthropic key 样本)、retry 候选标签入账本、failover 乱序/重复收敛、transport 逐候选独立、onRetry 第四参在 codex 退避区间内 — 全绿
  • PR-0 golden 13 例 + PR-1 seam + PR-2 retry-policy + PR-3 拦截器 + failover/text-only-failover 回归零修改通过(127/127)
  • pnpm --dir crates/agent-gui build ✅ / test:frontend ✅(仅 5 个与本 PR 无关的失败,经 stash 后在基线复跑确认为既有:3 个本机 CRLF 检出噪音 + 2 个 composer 既有)
  • 共享包触双端:crates/agent-gateway/web build ✅ / test 631/631 ✅;agent-ui tsc --noEmit ✅ / 改动文件 biome lint 零错
  • cargo check --tests ✅;全 diff 不含 src-tauri/**;git diff --check
  • node scripts/check-ui-boundaries.mjs:仅既有 3 个文件不合规(与 upstream/main 逐字节一致,非本 PR 引入)

未运行项

  • Go Gateway 测试(本 PR 无 gateway 后端改动;WebUI 前端三件套已运行)

人工验收结果

Windows Tauri 调试客户端(start-tauri-dev.bat,分支 HEAD)验收通过:

  • 正常请求:轨迹详情"请求参数"页出现传输行(候选标签 · 上游 origin · 直连/系统代理 · 头名列表,无任何头值)✅
  • 流内重试(不可达供应商):逐条重试行带候选标签、503 报错与整毫秒退避(201/407/797/1614/2915,codex 退避曲线)✅
  • agent 模式 failover:切换 1 行(RightCode → apiwharf,带触发错误)+ 两条传输行各自独立 ✅
  • text 模式 failover:显示为切换行(修正前误显示为重试)✅
  • 重试退避中停止:回合以已中断收尾,已发生的重试行保留(验收中发现重试计数虚高——浮点往返身份漂移导致同一条重试收敛出两份,已修复为整毫秒并回归)✅
  • 重启持久化:关闭应用重开,重试/切换/传输行全部从 SQLite 回放,重复计数自愈 ✅
  • 脱敏:失败轮错误文本中搜索真实 key 片段无命中 ✅

自动化专用用例(UI 无法直接触发,附命令与结果):node --test test/trajectory/scrub-transport.test.mjs → 14/14(base64 覆盖包值不泄漏、full-URL 不落盘、null 头过滤、洗涤器模式矩阵)。

Screenshots / preview

agent 模式 failover 落账:切换详情与逐候选传输快照(RightCode 与 apiwharf 两条传输行,头名列表各自独立):

failover-detail

重试明细:候选标签 · 503 报错 · 整毫秒退避,以及切换 1 行(A → B):

retry-detail

为 LLM seam 改造提供行为等价判定基准:
- wire-payload-golden:anthropic-messages / openai-completions /
  openai-responses / google-generative-ai / deepseek-responses 五协议
  固定输入下的完整请求体逐字段锁定(thinking 档位、工具、缓存断点、
  text-only 双形态),走真实 pi-ai stream() 与全部 payload 中间件,
  onPayload 链尾截获后中断,零网络。
- transport-golden:prepareProviderRequest 完整输出快照(反代 URL、
  全量头集、base64 覆盖包解码断言、鉴权头排除、full URL 模式、
  useSystemProxy 开关),并锁定 failover 逐候选传输配置独立性
  (主选走代理+备选直连互不泄漏,双向拓扑)。

零生产代码改动。
将 streamByApi.ts 的五协议 switch 原样搬移为 service/ 下的双适配器
(piAiAdapter 承接 4 条 pi-ai 协议,deepSeekAdapter 承接 deepseek 原生),
经 api→adapter 注册表分发;新增统一流式入口 llm.stream(),携带仅 dev
构建生效的请求信封冻结与一次性分发不变量。streamByApi.ts 收缩为保留
原签名的兼容壳(分发针孔),agentRunner 与 textOnlyRuntime 共 5 处调用
换用统一入口。行为严格等价:PR-0 两个 golden 套件(13 用例)零修改
通过;新增 seam 单测 8 用例覆盖注册表分发、错误文案逐字等价、dev
冻结开关与双入口 wire payload 等价。零 Rust 改动、零镜像文件改动。
将流内重试策略的归属权从全局常量反转到供应商配置(PR-2,stack 第 3 层):

- 共享真源 agent-ui settings 新增 CustomProvider.retryPolicy?(off |
  custom+maxRetries),maxRetries 为首次失败后的重试次数(不含首次请求,
  钳位 1..10)。normalizeProviderRetryPolicy 保证非法/缺省一律落 default
  且不落字段,旧配置零迁移;default 态在持久层不存在。
- agent-gui 运行时经 ProviderRuntimeConfig 唯一构造点透传策略,新增
  resolveStreamRetryConfig 把策略解析为 withStreamRetry 选项(off →
  disabled,custom → maxAttempts=maxRetries+1,缺省 → 空对象落全局默认)。
  agentRunner 与 textOnlyRuntime 两个 streamRetry 注入点展开合并,回调
  语义与 buffer-until-commit 不变;failover 逐候选使用各自 runtime 的
  策略。streamRetry.ts 与协议适配器零改动,
  DEFAULT_STREAM_RETRY_MAX_ATTEMPTS 降级为未配置时的默认值。
- 共享 settings UI(ProviderModal/ProviderModalView)请求面板新增
  流式重试三态控件(默认/关闭/自定义次数),GUI 与 WebUI 镜像同源;
  UI 展示镜像常量 PROVIDER_RETRY_DEFAULT_MAX_RETRIES 与运行时真源的
  一致性由单测锁定。
- 新增 provider-retry-policy.test.mjs(13 用例):归一化矩阵、构造点
  透传、消费方合并语义三种 mode、failover 候选策略独立、口径换算。
  PR-0 golden 两套件与既有 stream-retry 套件零修改通过。
把 payload 中间件的组织权反转到 LlmService seam(PR-3,stack 第 4 层)。
未注册任何自定义拦截器时行为与 PR-2 严格等价:

- 新增 service/interceptors.ts 注册表:具名 PayloadInterceptor
  (name + intercept),usePayloadInterceptor / llm.use() 返回幂等
  dispose,同名重复注册抛错;组合链带失效缓存,注册/移除时重建,
  finalize 热路径(agentRunner 每轮、textOnly 每次调用)零重组开销。
- payloadPipeline.ts 的 10 个中间件原样包装为具名默认拦截器,模块
  初始化时一次性安装,顺序与注册化前数组逐项一致(顺序即协议正确性
  的一部分,由顺序快照测试锁定)。payload-debug-logging 钉住链尾:
  自定义拦截器插入默认之后、链尾之前,自定义改动仍被调试日志观测。
- finalizeProviderStreamOptions 改为从注册表组合执行;agentRunner
  与 textOnlyRuntime 两处调用零改动,composePayloadMiddlewares 等
  既有导出保留,10 个中间件实现文件零改动。
- 新增 llm-interceptors.test.mjs(8 用例):默认顺序快照(10 个
  名字)、llm.use 同源、params 可见与 options 变换、插入位置、链尾
  观测不变量、dispose 幂等、同名抛错、与旧数组组合逐字段等价(多
  形态参数矩阵)。golden 两套件、seam、retry-policy、stream-retry
  及中间件相关回归零修改通过。
seam 改造第五层(PR-4):流内重试与跨供应商 failover 此前只喂 UI 临时状态,
审计线索随进程消失。本层把三类事实落入轨迹账本,重启后仍可回放:

- 线格式新增 failover(from/to/ti/err)与 transport(p/o/sp/fu/hn)事件;
  retry 增补 p(候选标签)与真实退避时长——failover 下各候选的重试可区分
- transport 快照只采头名与路由标记,逐候选独立记录,审计"主选带
  use-system-proxy、备选不带"的传输装配独立性;头值一律不采集
- recorder 全部 err 出口接入密钥洗涤(URL query key/Bearer/已知 key 形状),
  供应商报错回显的凭据不落盘;测试含反向断言
- withStreamRetry 退避提前计算并经 onRetry 上报整毫秒值——上报的就是实际
  要睡的值;取整同时关闭浮点往返身份漂移(serde_json ULP 误差会让同一条
  重试在收敛账本里出现两份)
- agent 模式 failover 首次落账(此前仅 onToolStatus);text 模式 failover
  从误记 noteRetry 改为独立 failover 事件,fromLabel/toLabel/targetIndex 不再丢失
- OptionsTab/OverviewTab 渲染三类新行,中英文案;旧读端对未知事件种类
  按既有收敛路径静默忽略,线格式只增不改
@StackCairn
StackCairn marked this pull request as draft August 22, 2026 19:06
@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

PR governance checks passed. Awaiting human review.

@AlphaCatMeow
AlphaCatMeow marked this pull request as ready for review August 22, 2026 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(trajectory): 重试/切换事件写入轨迹账本(LLM seam 第五层)

1 participant