fix(core-agent): prevent plain-text tool call leaks - #2459
fix(core-agent): prevent plain-text tool call leaks#2459zimo-xiao-zheng wants to merge 8 commits into
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f9a179cd0c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f7f529284c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1b19dfefea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2404f84274
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1a333626ea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b6fe504ce1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 95d441a884
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 300b675f1b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (event.type === 'step.done') { | ||
| await finishPossibleToolCall() |
There was a problem hiding this comment.
Preserve incomplete candidates across step boundaries
When a tool-enabled generation splits a serialized call across tool rounds—for example, one step emits {"name":"tool", and the next emits "arguments":{}}—this branch flushes the incomplete prefix and resets the guard. The suffix then contains no opening brace and streams unchecked. chat-orchestrator-runtime.ts:824-834 concatenates text deltas across these steps, so users still receive the complete raw tool-call JSON. Carry unmatched candidates into the next step, or inspect the combined channel through full-stream completion.
Useful? React with 👍 / 👎.
Description
Some models print a tool call as JSON instead of using the native tool protocol. This exposes the call in chat or reasoning output.
Linked Issues
Closes #2161
Additional Context
The guard matches a complete JSON object with a known tool name and a top-level
parametersorargumentsfield. It handles nested parameters, escaped characters, and malformed prefixes. Valid outer objects keep their nested examples intact.Candidate lengths can total at most eight times the channel length, measured in UTF-16 code units. Each span consumes this budget before slicing or parsing. Valid objects still need one pass. If recovery exhausts the budget, the response rejects and discards unchecked buffered output. This error does not trigger a store retry or change tool compatibility. Deeply malformed ordinary output can also fail.
Ordinary text and reasoning stream until either channel starts a JSON candidate. Candidates and subsequent native UI notifications stay buffered until step completion. Native events do not disable inspection.
The chat store receives a synchronous native-activity signal because xsAI does not await UI callbacks before tool execution. This prevents replay even when a leak rejects buffered notifications. It does not cancel or roll back SDK tool execution.
The two compatibility fallbacks can apply in either order, with at most three top-level stream attempts. SDK tool rounds are not compatibility retries. Repeated errors reject. Explicit
supportsContentArray: truekeeps its override instead of retrying the same payload.Cache-disabled requests resolve current tool names for inspection without sending tools or
tool_choice. Each request owns its names, including during retries. ExplicitsupportsTools: falserequests skip tool resolution. A request that starts without tools does not retry a tool leak.Provider completion closes event admission. Accepted errors still reject, while later provider events remain ignored. Failure stops queued output and buffered flushes. An active consumer callback must settle before rejection.
Verification
pnpm lint: passed with 10 existing warnings.pnpm typecheck: passed across 58 workspace projects.pnpm build:packages: 33 tasks passed.pnpm -F @proj-airi/core-agent exec vitest run: 142 tests passed.AIRI_TEST_REAL_SSE=1 pnpm -F @proj-airi/stage-ui exec vitest run: 815 tests passed, including browser tests.git diff --checkpassed.A local experiment used the public core build, real xsAI, and a loopback HTTP/SSE server.
It supplied
{"a":repeated N times, thenxand N closing braces.Each case ran once on Windows with Node 24.14.0, after a 256-level warm-up.
Times include the complete local request, not just the guard.
Both text and reasoning cases rejected on exhaustion without output, message, finish, or usage callbacks.
These measurements check this fixture; they are not a general latency guarantee.
The work bound is proportional to input length, not an absolute response-size or time limit.
The SSE responses are scripted transport fixtures, not model inference. Earlier evidence remains separate: initial
llama3:8bandqwen3:0.6bsimulations, fourth-round repeated-leak model runs, and fifth-round callback-drain SSE checks.Visual changes
The original comparison below records the initial chat recovery scenario. This revision changes stream handling, not component layout. It does not include new captures.