feat(request): 恢复被丢弃的推理签名并在工具循环中回放 - #79
Merged
Merged
Conversation
package-lock.json 锁定 @aws/codewhisperer-streaming-client@1.0.39,而 bun.lock 与 node_modules 已是 1.0.45。CI 和发布流程均只用 bun install,npm 仅用于 npm publish(不读锁文件),因此该文件不参与任何流程,仅会误导执行 npm ci 的 贡献者装到过旧的依赖树。 - git rm package-lock.json - .gitignore 新增 package-lock.json,避免误跑 npm install 后静默复活 - README.md / docs/readme/README.zh.md 在既有开发小节内说明 Bun 是唯一 受支持的开发包管理器
@ai-sdk/openai-compatible 把助手推理序列化为顶层 reasoning_content 字符串、 content 保持普通字符串,而 buildHistory 只在 content 为数组且含 type:'thinking' 时才走 thinking 分支,否则回落到只筛 type:'text' 的 getContentText。结果是 OpenCode 路径上的推理文本每轮都被静默丢弃,原有的 <thinking> 重建实为死代码。 - message-transformer.ts 新增共享解析器 parseAssistantMessage,以及 extractReasoningText / findActiveToolLoopStart / applyThinkingToContent - history-builder.ts 与 request.ts 的两处重复助手分支改为调用共享解析器, 各由约 30 行缩至 3 行 - 恢复范围限定在进行中的工具循环,叠加未改动的 collapseAgenticLoops, 上线的 <thinking> 块恒定不超过 2 个(实测 +211 est. tokens,真实会话约 +1.8~2.8%) - 恢复文本只进 assistantResponseMessage.content;无签名的 reasoningContent 会被服务端硬拒 400,故此处不写该字段 - 新增 reasoning-text-recovery.test.ts(22 例)覆盖字符串 content 携带 reasoning_content、数组 thinking 回归、工具循环边界
transformSdkStream 此前以 `event.reasoningContentEvent?.text` 为门,导致只带 signature 或只带 redactedContent 的事件穿过所有分支后消失。Kiro 每个响应尾部 恰有一个这样的事件,携带整段推理的签名,因此签名从未被捕获。 - types.ts 新增 KiroReasoningContent(text 变体的 signature 为必填:无签名的 reasoning 块会被服务端硬拒 400),并为 assistantResponseMessage 加上可选的 嵌套 reasoningContent 字段 - 新增 ReasoningAccumulator:逐字追加文本、接受纯签名事件、保留最后一个非空 签名、两个不同的非空签名判为不支持而非猜测、redacted 字节按 SDK 类型复制 且不经文本或 base64 往返 - 门改为按 reasoningContentEvent 存在性分派,内部再按字段派发;纯签名事件不 触碰 reasoningStarted/reasoningClosed/thinkingBlockIndex,可见输出逐字节不变 - 诊断日志只含种类与长度,签名与 redacted 字节永不落日志 - 新增 reasoning-accumulator.test.ts(21 例),含可见输出字节一致性对照与 finalize 后修改源数组证明字节为复制而非别名 本波仅做观测,不改变任何可见行为;发布时机属 Wave 4。
Kiro 会校验历史里回放的每个推理签名,压缩历史、跨模型回放或签名过期都会得到 一个流开始之前的 400 THINKING_SIGNATURE_INVALID。这是请求级错误,正确处理是 剥离全部历史推理后原账号重试一次,而不是换账号或标记账号不健康。 三处管道此前都不通: - request-handler.ts 的合成错误体只带 message/__type,SDK 建模的 reason 字段 被丢弃,导致 errorData?.reason 恒为 undefined(而 402/403 分支正依赖它) - error-handler.ts 的 400 分支无条件返回 shouldRetry: false,从不看 reason - RequestContext 只有 retry 与 forcedRefreshAccountIds,而 prepareSdkRequest 每次重试都从原始 body 重建,无处记录"本次已尝试恢复" 改动: - 合成错误体条件性附加 reason,无 reason 的错误产出与此前逐字节相同 - 400 分支先判定 size-overflow 并作为恢复分支的守卫,确保 413 重映射路径不被 拦截;匹配 reason 精确相等,不做文本模糊匹配 - RequestContext 新增 signatureRecoveryAttempted(限一次,模仿 forcedRefreshAccountIds 的收敛方式)与 disableReasoningReplay - 新增 ErrorHandlerResult.pinAccount,复用既有 forcedStreamAccount 机制钉住 同一账号,不消耗轮换额度,不触碰健康状态 - transformToSdkRequest 第 7 参改为 SdkRequestOptions,置位时调用新增的 stripReasoningContent 清除 history 与 currentMessage 上的 reasoningContent - 新增 signature-recovery.test.ts(20 例),含同时带 overflow 文案与签名 reason 的对抗用例、重复被拒的收敛用例,以及穿过真实模块边界断言标志确实抵达 transformToSdkRequest 的用例
- 新增 EmittedOutputAccumulator,从发出的 OpenAI 分块累积指纹来源 - 新增 loopId 派生与长度前缀指纹键,模型作为命名空间隔离 - 新增有界关联缓存,读取非消耗,歧义与近似一律拒绝 - onComplete 扩展为完整载荷,三个完成路径共用同一发布入口
- 新增 log-redaction 模块,将签名与 redacted 字节收敛为
{present,length|byteLength,sha256Prefix}
- 去敏落在两个日志 sink 内部而非调用点,任何调用方无法再通过打印
prepared request 或 conversationState 泄露签名
- 抽出 sdk-log-payload,出站请求日志改为携带按轮次的去敏推理摘要
- 环形结构改为抛出与 JSON.stringify 相同的 TypeError,保持原有
不可序列化行为不变
- 新增 14 条测试,覆盖两个 sink、出站载荷、抛出的错误与共享子对象
- 提交去敏后的 A/B probe 与 README,凭据运行时读取、邮箱与 ARN 掩码、
写盘前自检,并记录原始两轮结果 JSON 已随 /tmp 丢失
- 提交 A/B 探针的真机原始结果 JSON,作为 §3.1 在 Wave 5 之后的可复现基线 - 新增跨账号回放探针:双账号必须显式指定,任一额度不足或令牌将过期即拒绝 - 新增插件级轮换测试:真实 AccountManager 轮换后出站体仍携带原账号的字节级签名 - README 记录哪些原始结果是权威的、为何不提交 dry 结果、以及探针与插件的验证边界 - 新增 .prettierignore 保护原始证据不被重新格式化
在真实 OpenCode 二进制上,用同一条长依赖工具链、同一个 Kiro 模型与同一份 fixture,对比已发布的修复前插件 0.15.4(== 80782f9)与本地工作树 HEAD, 唯一变量是加载了哪个 kiro-auth 构建。不涉及发版,未改动任何生产代码或既有测试。 - build-fixture.ts 幂等生成两套 fixture:baseline 10 跳(FINAL_TOTAL=231)与 stress 20 跳 + 每轮模数校验和(459 / 8553),遍历顺序刻意非顺序,模型无法 预测下一跳文件名,因此提前停止可被客观计数 - run-ab.ts 交替执行两臂并落盘完整 --format json 事件流、opencode export 会话(含 reasoning 部件)与插件出站 api 日志;analyze-ab.ts 从原始产物重算 全部表格;sanitize-runs.ts 产出脱敏可提交副本 - 隔离机制实证:OPENCODE_CONFIG 与全局配置【合并】而非替换,无法用于隔离插件; OPENCODE_CONFIG_DIR 在 1.18.5 上无可观察效果;只有 XDG_CONFIG_HOME 能整体 重定位配置解析。插件的 kiro.db 与刷新锁同样挂在该变量下,故符号链接回真实 目录(拷贝会因 refresh token 一次性而废掉真实账号) - 臂身份用 4 格置换对照证明:两构建的 package.json 版本号与 dist/index.js 哈希 完全相同,只有隐藏对侧 dist 后观察 kiro 模型是否消失才能区分 - 结果(24 次真机运行,486 次真实调用):机制确认端到端生效(NEW 臂最多 18/21 个请求携带 historyReasoning、66 个签名信封;OLD 臂结构上为 0), 未命中即无操作;但“宣告下一步后停止”在两臂都出现(10 跳 2/8 vs 1/8, Fisher p=1.0;20 跳两臂各 0/4),且三次停止全在第 2 轮、此前推理量为 0, 故该症状不由推理坍缩解释 - AB-OPENCODE-COMPARISON.md 为独立可读的对比报告,含复现命令、作废批次记账、 残留混淆项与诚实裁决
Merged
Codecov Report✅ All modified and coverable lines are covered by tests.
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
Kiro 在流式响应中会返回一段带有不透明
signature的原生推理内容(reasoningContent.reasoningText.{text, signature})。插件此前只渲染了其中的text用于展示,把signature直接丢弃了。后果是:在多轮工具循环里,历史消息中不再携带可被上游校验的推理签名,模型无法延续上一轮的思考链,推理质量随轮次逐步退化——到后几轮甚至完全不再产生推理帧。
修复
signature与redactedContent,不再丢弃。assistantResponseMessage.reasoningContent.reasoningText.{text, signature},把签名回放给上游。THINKING_SIGNATURE_INVALID时,剥离已回放的历史推理内容,在同一个已固定的账号上重试一次,避免账号轮换引入新的不一致。设计约束(有意为之)
<thinking>:两种表达互斥,避免上游收到重复/冲突的思考内容。验证证据
真机 A/B 对比(提交在
scripts/probes/AB-OPENCODE-COMPARISON.md,24 次真实 OpenCode 运行):相关证据与脚本:
scripts/probes/AB-OPENCODE-COMPARISON.md— 端到端 A/B 对比scripts/probes/ab-reasoning-probe.ts、scripts/probes/*/results/— 原始数据scripts/probes/README.md— probe 使用说明明确的范围边界
本 PR 不修复「模型提前结束回合」的问题。那是一个完全独立的缺陷(工具结果回合的
content填充文本被模型当成真实用户发言),在另一个 PR 中单独修复。请不要把两者的效果互相归因:签名回放不解决提前结束,提前结束的修复也不影响推理连续性。本 PR 同样不涉及另有记录的「HTTP 200 但响应体完全为空」的失败模式。