Repository navigation
fix(responses): keep tool call/output pairs adjacent on the wire (#766) - #768
Conversation
Strict Responses backends (DeepSeek) reject a request whose function_call_output does not directly follow its call group: a message/reasoning item wedged between a call and its output yields 'No tool output found for tool call <id>' even though both items exist, permanently wedging the session until the client history is dropped. Root cause: surfaceReadonlyJson appended the acp_status visibility marker as an assistant message AFTER a real function_call in the same response, so the client recorded [function_call, message, function_call_output]. - New src/tool-pair-order.ts: hoistTrappedToolItems() hoists any non-tool item trapped between a call and its matching output to just before the earliest trapping call. No-op (same array ref) on healthy input, so well-formed sessions stay byte-stable for the prefix cache. - Enforce at every Responses input-build site: unified streaming buildRequest (adapter-responses.ts), non-streaming prepareResponses (server.ts), and the legacy JSON replay (compress-loop-responses.ts). This also heals already-poisoned sessions without dropping the jsonl. - Fix the root cause in surfaceReadonlyJson: insert the marker before the first tool call instead of appending after. typecheck clean, 1469 pass / 0 fail / 2 skipped (e2e-gated), build ok.
📦 Built Package ArtifactBranch: Option A — Install from npm PR tag (recommended)npm install -g billion-context@pr-768Each push to this PR publishes a new version under the Option B — Download artifact
tar xzf billion-context-pr768.tgz
npm install -g packageThis comment is automatically updated on each push. |
…e path (#766) prepareResponsesCompact returns its forged array as codex's new recorded session history; a non-tool item wedged between a call and its output there would re-wedge the session on the next forward. Apply the same hoistTrappedToolItems guard as the other Responses input-build sites (no-op on healthy input, so prefix-cache byte-stable).
[bot] 🏷 Reviewed PR #768 ( Diff cleanliness (first gate): clean. One logical change, 5 files (+204). No unrelated churn, no lockfile/version drift, no whitespace noise; every line serves the stated purpose. ✅ Bug is real and correctly layered. Strict Responses backends pair Re-verified locally (fresh
Gap I fixed directly (commit Minor, non-blocking (left to you): a single pass does not fully de-trap doubly-interleaved input where two call/output spans overlap. Repro: Net: correct, well-scoped, well-tested fix for a nasty wedge class; with the forge-path commit landed it's complete across all Responses input-build sites. ✅ 中文摘要:修复了严格 Responses 后端(DeepSeek)因 |
Problem
Strict Responses-API backends (DeepSeek) reject a request whose
function_call_outputdoes not directly follow its call group. A single non-tool item (amessage/reasoning) wedged between afunction_calland its matching output yields:even though both items are present — pairing is by position/adjacency, not just by
call_id. The client records the malformed[function_call, message, function_call_output]sequence into its session history, so every subsequent forward fails identically and the session is permanently wedged until the client history (codex jsonl) is dropped.Root cause
surfaceReadonlyJson(src/compress-loop-reponses.ts) appended theacp_statusvisibility marker as an assistant message after a realfunction_callin the same response. The client then recorded[function_call, assistant_marker, function_call_output], which DeepSeek rejects.Fix (two parts)
1. Wire-boundary guard — heals already-poisoned sessions without dropping the jsonl.
New
src/tool-pair-order.ts::hoistTrappedToolItems()hoists any non-tool item trapped between a call and its matching output to just before the earliest trapping call, restoring adjacency. It is a pure no-op (same array reference) on healthy input, so well-formed sessions stay byte-stable for the prefix cache. Enforced at every Responses input-build site:buildRequest(src/loop/adapter-responses.ts) — the live path serving codex/deepseek;prepareResponses(src/server.ts);src/compress-loop-responses.ts).Because it runs on the outbound request, an already-corrupted session self-heals on the very next forward — no manual jsonl drop required.
2. Root cause.
surfaceReadonlyJsonnow inserts the visibility marker before the first tool call instead of appending after it, so the marker can no longer wedge a pair at the source.Verification
npm run typecheck— cleannpm test— 1469 pass / 0 fail / 2 skipped (e2e-gated)tests/tool-pair-order.test.ts— 10 new cases incl. the exact [BUG] No tool output found for tool call call_xxxx #766 pattern, parallel calls, reasoning trap, custom_tool_call variant, healthy-input no-op (strictEqual same ref)npm run build— ok (new module bundled inline)E2E_CHECK=1) passes (codex 0.147.0 + dist); full 4-phase run needs a live upstream not present in this sandbox (CI e2e is manual-dispatch with secrets)Fixes #766