Repository navigation
fix(ai): keep an unencodable tool result from killing the session - #60604
Draft
HX-Wrdzgzs wants to merge 4 commits into
Draft
HX-Wrdzgzs wants to merge 4 commits into
HX-Wrdzgzs wants to merge 4 commits into
Conversation
|
Your two changes match where I landed on a fork branch, so I won't duplicate them. Three pieces from that branch are not here (
Happy to split these into a PR against your branch if that is easier than cherry-picking. I cannot open one from my account — this repository caps the number of open pull requests it will accept from it. |
Protocol lowering called encodeJson on json tool results without a guard, so
`{ type: "json", value: undefined }` — what ToolOutput.toResultValue produces
when a tool returned undefined and produced no content parts — threw
`Expected a JSON-serializable value`. That throw is a defect: nothing reaches
the wire, and it unwinds through SessionStep.attempt into SessionRunner.drain,
so every later turn on that session fails the same way.
- toolResultText now sends json results through Schema.encodeUnknownOption and
falls back to String(value), the same guard schema/messages.ts already applies
to text results. Values that are JSON still encode exactly as before.
- LLM.compile converts a non-interrupt cause into a typed AIError through
streamError, so a lowering bug fails one step instead of the whole session.
Interrupts are re-raised untouched and an AIError already in the cause is
returned as-is.
HX-Wrdzgzs
force-pushed
the
tool-result-encode-fallback
branch
from
October 31, 2026 11:19
28d420c to
8332a7b
Compare
This branch has not been deployed
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.
Issue for this PR
Closes #58934
Type of change
What does this PR do?
toolResultTextcalledencodeJsonon json tool results without a guard.ToolOutput.toResultValueproduces{ type: "json", value: undefined }when a tool returnsundefinedwith no content parts (tool.ts:262), andSchema.encodeSync(Json)rejects a top-levelundefinedwithExpected a JSON-serializable value. It now goes throughSchema.encodeUnknownOption(Json)and falls back toString(value)— the same guardschema/messages.ts:123already applies to text results. Values that are JSON still encode exactly as before, including the quoted form for strings.LLM.compilehad nocatchCause, so that throw was a defect that unwound throughSessionStep.attemptintoSessionRunner.drain: nothing was sent, and the session never recovered. Compile now turns a non-interrupt cause into a typedAIErrorthrough the existingstreamErrorhelper, so one step fails instead of the whole session.I cannot show that this is the same trigger as the large pasted-image reports in #58934, and there is no end-to-end reproduction here. What is shown is the lowering path above, and that a throw at compile time is currently fatal.
How did you verify your code works?
From
packages/ai:bun run test→ 1992 pass / 28 skip / 0 fail (2020 tests, 154 files);bun run typecheck→ no type errors on both tsconfigs.The two new tests assert the lowered strings (
"undefined",'{"a":1}','"abc"'), not just "does not throw". Reverting theshared.tsline makes them fail withDie(SchemaError(Expected a JSON-serializable value)). Two existing tests still pass unchanged:anthropic-messages.test.ts -t "prepares tool call and tool result messages"(a json tool result still lowers to'{"forecast":"sunny"}') andmedia.test.ts -t "rejects local file references as request errors"(a typedAIErroris still surfaced, not swallowed).Real-session evidence (added 2026-10-31)
A second, independent trigger for the same fatal path, from a real session. A context-pruning plugin rewrites each
tool-resultpart and stores the wholeresultas a bare string:{ "type": "tool-result", "id": "call_575531866a004e69a5872af4", "name": "read", "result": "[已裁剪 · 原 238 token] 完整信息请点击 tm_fetch { ref: \"tm://runs/r-20261031-000247-c0bb87/steps/s0003/result\", access_token: \"<redacted>\", expire_at: … }", "providerExecuted": false }(The stub text is the plugin's, in Chinese; the access token is redacted. The plugin is
@te-river/opencode-team-mode1.9.5; a diagnostic build loggedtypeof part.result.type === "undefined"for this part.)Failed to drain Session,SchemaError: Expected a JSON-serializable value, 4.18 s after process spawnidle_outcome=succeededArm A, verbatim from the log (
ses_e7278d6dbffeKZzMxPMByoGqy9):Arm B: the affected session (
ses_e72dcf6ceffdGeH4rfE7YGdhMa) had been leftidle_outcome='failed'at 128 messages. After replacing only the plugin's pruning file, the same session resumed on 2.2.5 and finished at 133 messages withidle_outcome='succeeded'; it then kept running to 232 messages, with zeroFailed to drainin the monitoring window.Why the stock 2.2.5 build does not reproduce it on its own: the pruning layer only rewrites results once context passes 70%. Our 2.2.5 runs stayed below the threshold (
prune_checked=313 / below=313 / pct=68%, andchecked=33 / pct=35%), so no malformed part was ever written. On 2.2.3 the same plugin pruned 2,363 parts / 1.68 M tokens in one session. The trigger is "pruning fires", not the 2.2.3 → 2.2.5 difference.The same plugin also wrote
tool-call.inputas a bare string; the fixed build keeps both envelopes (result: {type, value}andinput: {…}), which is whattoolResultTextand the protocol adapters expect.Verification status at this head
The
bun run test/bun run typechecknumbers quoted above were taken on the original two-commit branch. This branch has since been rebased ontoa66964eand extended with two commits from @Te-River's construct-side work, andbunis not available in my environment. At the current head, verification istsc --noEmit -p packages/ai/tsconfig.json→ exit 0, plus a static check that every existing assertion touchingtoolResultText/toResultValuestill holds. The full suite needs CI.Checklist