Skip to content

Parse stringified tool-call input before Zod validation#536

Merged
jahooma merged 1 commit intomainfrom
jahooma/write-file-string-input
Apr 22, 2026
Merged

Parse stringified tool-call input before Zod validation#536
jahooma merged 1 commit intomainfrom
jahooma/write-file-string-input

Conversation

@jahooma
Copy link
Copy Markdown
Contributor

@jahooma jahooma commented Apr 22, 2026

Summary

  • Fixes a confusing "Invalid parameters for write_file: expected object, received string" agents were hitting in base2-free. The AI SDK emits tool-call chunks with input as a raw JSON string when its repair pass can't produce a parsed object (see the catch block in parseToolCall in ai), and our Zod check saw the string directly.
  • processToolCallObject in tool-stream-parser.ts now JSON.parses string inputs before forwarding. If parsing fails, the string is passed through and parseRawToolCall / parseRawCustomToolCall surface 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 fail
  • New test: valid JSON-string input gets parsed and tool call succeeds end-to-end
  • New test: unparseable string input emits a clear error and no tool_call event
  • bun test packages/agent-runtime/src/__tests__/tool-stream-parser.test.ts packages/agent-runtime/src/__tests__/xml-tool-result-ordering.test.ts — still green
  • bun tsc --noEmit -p packages/agent-runtime/tsconfig.json — clean

🤖 Generated with Claude Code

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-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 22, 2026

Greptile Summary

This PR fixes a confusing "expected object, received string" error that agents hit when the AI SDK's repair pass emitted a tool-call chunk with a raw JSON string instead of a parsed object. The fix is applied in two layers:

  • Stream layer (tool-stream-parser.ts): processToolCallObject now attempts JSON.parse on string inputs before forwarding to the tool processor. Valid JSON objects are transparently unwrapped; anything that fails to parse falls through unchanged.
  • Executor layer (tool-executor.ts): Both parseRawToolCall and parseRawCustomToolCall now return a dedicated stringInputError early if the input is still a string, giving the agent a targeted, actionable message rather than a raw Zod error.

The change is well-tested with two new integration tests, and existing tests remain green.

Confidence Score: 4/5

Safe 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

Filename Overview
packages/agent-runtime/src/tool-stream-parser.ts Adds JSON.parse pre-processing for string inputs in processToolCallObject; minor edge case where non-object parsed values bypass the string guard in the executor
packages/agent-runtime/src/tools/tool-executor.ts Adds stringInputError helper and string-input guards in both parseRawToolCall and parseRawCustomToolCall; clean, well-structured defensive checks
packages/agent-runtime/src/tests/tool-validation-error.test.ts Two new integration tests covering the happy-path (valid JSON string parsed and tool executes) and error-path (unparseable string emits clear error with hadToolCallError=true)

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (1): Last reviewed commit: "Parse stringified tool-call input before..." | Re-trigger Greptile

Comment on lines +86 to +90
if (typeof input === 'string') {
try {
input = JSON.parse(input)
} catch {}
}
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.

P2 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:

Suggested change
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 {}
}

@jahooma jahooma merged commit 359a039 into main Apr 22, 2026
34 checks passed
@jahooma jahooma deleted the jahooma/write-file-string-input branch April 22, 2026 21:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant