Skip to content

fix(core): lower unparseable streaming tool arguments to an object - #56540

Open
argszero wants to merge 1 commit into
anomalyco:devfrom
argszero:streaming-tool-input
Open

argszero wants to merge 1 commit into
anomalyco:devfrom
argszero:streaming-tool-input

Conversation

@argszero

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #56127

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

toLLMMessages lowered a tool call still in streaming state to the raw argument text when that text was not valid JSON. The Anthropic lowerer copies input onto the wire unchanged, so the request carried tool_use.input: "not-json" and Anthropic rejected it with 400 tool_use.input: Input should be an object.

The streaming state stores the accumulated argument text as a String while every other tool state stores a record, so lowering now decodes the text as a JSON object and falls back to {} when it cannot: unparseable text, a truncated stream, or valid JSON that is not an object (an array, a number, null) all lower to an empty object rather than a non-object value. Nothing else changes — a streaming call whose arguments do parse still lowers to the parsed object, and running, completed and error states are untouched.

How did you verify your code works?

Regression test lowers unparseable streaming tool arguments to an object in packages/core/test/session-runner-message.test.ts lowers one assistant message containing five tool parts through toLLMMessages:

state raw input lowered tool-call.input
streaming not-json {}
streaming ["pwd"] {}
streaming {"command":"pwd" (truncated) {}
streaming {"command":"pwd"} { command: "pwd" }
running { command: "pwd" } { command: "pwd" }

It fails on the base with the string and array inputs passed through as raw text (29 pass / 1 fail before the fix, 30 pass / 0 fail after).

  • bun test test/session-runner-message.test.ts — 30 pass
  • bun test test/session-runner-message.test.ts test/session-generate.test.ts test/session-runner-retry.test.ts — 36 pass
  • bun run typecheck in packages/core — clean
  • oxlint on both changed files — 0 warnings, 0 errors

The test asserts the lowering boundary, which is where the defect lives: the Anthropic lowerer copies input verbatim, so an object here is what makes tool_use.input an object on the wire. It does not contact a live provider.

AI-assisted implementation and tests; reviewed before publication.

Screenshots / recordings

Not applicable; message lowering only.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. I reproduced the #56127 script on the base: a streaming tool call with argument text not-json compiles to an Anthropic tool_use.input of "not-json". With this change it compiles to {}, and valid JSON arguments still lower to the parsed object. session-runner-message.test.ts passes (30/30) and packages/core typechecks cleanly.

The {} fallback matches how settling a stale call already treats leftover string input (message-updater.ts, session.tool.failed), so lowering and settlement now agree. As the issue notes, the runner settles stuck streaming calls before building a request, so this is hardening at the lowering step rather than a fix for a path a normal session has been shown to reach.

The public API is unchanged. No concerns.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

toLLMMessages emits a string tool_use.input when streaming tool arguments are not valid JSON

1 participant