Parse stringified tool-call input before Zod validation#536
Conversation
AI SDK can emit tool-call chunks with `input` as a raw JSON string when its repair pass can't produce a parsed object, making Zod fail with a confusing "expected object, received string". Parse the string in the stream parser, and emit a clearer error if parsing fails. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Greptile SummaryThis PR fixes a confusing
The change is well-tested with two new integration tests, and existing tests remain green. Confidence Score: 4/5Safe to merge; the two-layer fix is correct for the primary use case and improves the error experience meaningfully The primary use case (AI SDK emitting a JSON-stringified object) is handled correctly and is well-tested end-to-end. A minor edge case exists where JSON.parse succeeds but returns a non-object (null, array, number, boolean), which would bypass stringInputError and produce a less-helpful Zod error instead. This is a P2 concern given how unlikely the scenario is in practice. No security issues or data-loss risks were found. packages/agent-runtime/src/tool-stream-parser.ts — the JSON.parse guard could be tightened to only accept plain objects Important Files Changed
Sequence DiagramsequenceDiagram
participant SDK as AI SDK
participant Parser as processToolCallObject<br/>(tool-stream-parser.ts)
participant Executor as parseRawToolCall /<br/>parseRawCustomToolCall<br/>(tool-executor.ts)
participant Agent as Agent / onResponseChunk
SDK->>Parser: tool-call chunk {input: string}
Note over Parser: typeof input === 'string'?
alt JSON.parse succeeds → object
Parser->>Executor: input = parsed object
Executor->>Agent: tool_call event (success)
else JSON.parse fails
Parser->>Executor: input = original string
Executor-->>Agent: error event: tool arguments were a string, not a JSON object
end
Reviews (1): Last reviewed commit: "Parse stringified tool-call input before..." | Re-trigger Greptile |
| if (typeof input === 'string') { | ||
| try { | ||
| input = JSON.parse(input) | ||
| } catch {} | ||
| } |
There was a problem hiding this comment.
Non-object parsed values bypass the string-input guard
JSON.parse can succeed but return a non-object (null, a number, a boolean, an array). In those cases input is no longer a string, so both parseRawToolCall and parseRawCustomToolCall skip the stringInputError path and fall through to Zod validation, which will surface a less-clear error (e.g. "expected object, received null").
For the realistic AI-SDK case (a stringified object) this is fine, but a guard on the parsed result would future-proof it:
| if (typeof input === 'string') { | |
| try { | |
| input = JSON.parse(input) | |
| } catch {} | |
| } | |
| if (typeof input === 'string') { | |
| try { | |
| const parsed = JSON.parse(input) | |
| if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) { | |
| input = parsed | |
| } | |
| } catch {} | |
| } |
Summary
write_file: expected object, received string" agents were hitting inbase2-free. The AI SDK emitstool-callchunks withinputas a raw JSON string when its repair pass can't produce a parsed object (see the catch block inparseToolCallinai), and our Zod check saw the string directly.processToolCallObjectintool-stream-parser.tsnowJSON.parses string inputs before forwarding. If parsing fails, the string is passed through andparseRawToolCall/parseRawCustomToolCallsurface a clearer "tool arguments were a string, not a JSON object" error that hints at the likely cause (unescaped newlines/quotes) so the agent can self-correct.Test plan
bun test packages/agent-runtime/src/__tests__/tool-validation-error.test.ts— 5 pass, 0 failbun test packages/agent-runtime/src/__tests__/tool-stream-parser.test.ts packages/agent-runtime/src/__tests__/xml-tool-result-ordering.test.ts— still greenbun tsc --noEmit -p packages/agent-runtime/tsconfig.json— clean🤖 Generated with Claude Code