Skip to content

Anthropic adapter passes null tool input when model produces empty tool_use block, causing agent loop to stall #265

@KrunchMuffin

Description

@KrunchMuffin

TanStack AI version

v0.3.0

Framework/Library version

React v19.2.4

Describe the bug and the steps to reproduce it

TanStack AI version

0.3.0 (@tanstack/ai and @tanstack/ai-anthropic)

Framework/Library version

React 19.2.4, Cloudflare Workers (Hono 4.11)

Describe the bug

When Claude occasionally produces a tool_use content block with no input_json_delta events (or with "null" as the partial JSON), the Anthropic adapter's content_block_stop handler passes null as the parsed input instead of defaulting to {}.

This causes executeToolCalls to fail Zod schema validation (since null isn't an object). The error result is correctly captured, but in practice the agent loop appears to stall after the failed tool execution — the model either doesn't produce a follow-up text response, or the follow-up text isn't emitted through the stream.

The end result for users: the assistant says "Let me search for that..." then silence. No error, no follow-up.

Root cause

In @tanstack/ai-anthropicadapters/text.js, the processAnthropicStream method, inside the content_block_stop handler (~line 389):

let parsedInput = {};
try {
  parsedInput = existing.input ? JSON.parse(existing.input) : {};
} catch {
  parsedInput = {};
}

When existing.input is the string "null" (from the model streaming partial_json: "null"), JSON.parse("null") returns JavaScript null — which passes the truthy check but isn't {}. The resulting TOOL_CALL_END event has input: null.

Downstream in executeToolCalls (@tanstack/aitools/tool-calls.js), the flow is:

  1. toolCall.function.arguments is "null" (set by completeToolCall via JSON.stringify(null))
  2. argsStr = "null".trim() || "{}""null" (truthy, so no fallback)
  3. JSON.parse("null")null
  4. parseWithStandardSchema(tool.inputSchema, null) → Zod throws (null is not an object)
  5. Error result pushed: { error: "Input validation failed for tool ..." }
  6. Error result fed back to model via emitToolResults

The error handling itself works correctly. The issue is that after the error result is fed back, the next model iteration either produces no text or the text doesn't make it through the stream — the conversation just stops from the user's perspective.

Suggested fix

Normalize the parsed input in the adapter's content_block_stop handler:

let parsedInput = {};
try {
  const parsed = existing.input ? JSON.parse(existing.input) : {};
  parsedInput = parsed && typeof parsed === 'object' ? parsed : {};
} catch {
  parsedInput = {};
}

This ensures that even if the model produces null, "null", or other non-object JSON as tool input, the adapter normalizes it to {} before emitting the TOOL_CALL_END event. Since tool schemas define which fields are required, {} is a safe default that lets validation handle the rest cleanly.

Steps to reproduce

This is intermittent and depends on model behavior. We've observed it with claude-sonnet-4-5 when the model generates a second tool_use block with no meaningful arguments (e.g., after a first tool call that returned zero results). The model seems to "start" a follow-up tool call but doesn't commit to arguments.

Typical pattern:

  1. User asks a question that triggers a tool call
  2. Model produces text + first tool_use (with valid arguments) + second tool_use (with null/empty arguments)
  3. First tool executes successfully, second fails validation
  4. Agent loop feeds error back to model
  5. No follow-up text is produced — stream ends silently

We have two production sessions exhibiting this with full message data available if helpful.

Example session data

The saved tool call shows arguments: "null" and the corresponding tool result message has no content:

{
  "role": "assistant",
  "content": "Let me search for disc mowers near you in Saskatoon:",
  "toolCalls": [
    {
      "id": "toolu_1770336584303_0",
      "type": "function",
      "function": {
        "name": "search_equipment",
        "arguments": "null"
      }
    }
  ]
}
{
  "role": "tool",
  "toolCallId": "toolu_1770336584303_0"
}

Note the tool result message has no content field at all — this is because JSON.stringify(undefined) returns undefined (the value), which gets dropped during JSON serialization. This is a downstream consequence in our code, but the root cause is the null input from the adapter.

Your Minimal, Reproducible Example - (Sandbox Highly Recommended)

This bug depends on intermittent Anthropic model behavior (producing a tool_use block with null input) that can't be reliably triggered in a sandbox. We've provided the full code trace through the adapter and executeToolCalls with exact line references, plus two production sessions with raw message data. Happy to share full stream chunk logs if needed.

Screenshots or Videos (Optional)

No response

Do you intend to try to help solve this bug with your own PR?

Yes, I am also opening a PR that solves the problem along side this issue

Terms & Code of Conduct

  • I agree to follow this project's Code of Conduct
  • I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions