Skip to content

fix: normalize null tool input from Anthropic adapter#266

Open
KrunchMuffin wants to merge 3 commits intoTanStack:mainfrom
KrunchMuffin:fix/null-tool-input-normalization
Open

fix: normalize null tool input from Anthropic adapter#266
KrunchMuffin wants to merge 3 commits intoTanStack:mainfrom
KrunchMuffin:fix/null-tool-input-normalization

Conversation

@KrunchMuffin
Copy link

@KrunchMuffin KrunchMuffin commented Feb 6, 2026

#265

Summary

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 the agent loop stalls 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.ts

The processAnthropicStream method, inside the content_block_stop handler:

parsedInput = existing.input ? JSON.parse(existing.input) : {}

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.

In @tanstack/aitools/tool-calls.ts

Downstream in executeToolCalls:

  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, fed back to model
  6. Agent loop stalls — no follow-up text produced

Changes

Anthropic adapter (content_block_stop): Normalize parsed input to {} when JSON.parse produces null or any non-object value:

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

Anthropic adapter (formatMessages): Same normalization when reconstructing tool calls for conversation history (prevents input: null being sent back to the Anthropic API on subsequent turns).

executeToolCalls + ToolCallManager.executeTools: Normalize "null" argument strings to "{}" before JSON parsing — a defense-in-depth fix for the same issue at the tool execution layer.

How to Reproduce

This is intermittent and depends on model behavior. We've observed it with claude-sonnet-4-5 when the model generates a 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

Example Session Data

{
  "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"
      }
    }
  ]
}

Test Plan

  • Verified fix handles JSON.parse("null") → normalizes to {}
  • Verified fix handles empty existing.input → defaults to {}
  • Verified normal tool calls with valid JSON input are unaffected
  • All three fix locations use consistent normalization logic

Summary by CodeRabbit

  • Bug Fixes
    • Tool argument parsing now treats empty, whitespace-only, and special literal inputs as empty objects, preventing JSON parse errors during tool execution.
    • Improved validation to ensure tool inputs are always treated as objects, defaulting to an empty object when parsing fails or yields a non-object value.

When Claude occasionally produces a tool_use content block with no
input_json_delta events (or with "null" as the partial JSON),
JSON.parse("null") returns JavaScript null — which passes the truthy
check but isn't {}. This causes downstream Zod schema validation to
fail, and the agent loop stalls silently.

Changes:
- Anthropic adapter (content_block_stop): normalize parsed input to {}
  when JSON.parse produces null or a non-object value
- Anthropic adapter (formatMessages): same normalization when
  reconstructing tool calls for conversation history
- executeToolCalls + ToolCallManager.executeTools: normalize "null"
  argument strings to "{}" before parsing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 6, 2026

📝 Walkthrough

Walkthrough

Validated and normalized JSON inputs for tool-handling: the anthropic text adapter now ensures parsed JSON is an object before assignment, and the tool-call handlers trim input, convert the literal "null" to "{}", and parse safely to avoid non-object or empty inputs being used.

Changes

Cohort / File(s) Summary
Anthropic Text Adapter
packages/typescript/ai-anthropic/src/adapters/text.ts
Validate parsed JSON is an object; if parsing yields a non-object or fails, assign {}.
Tool Call Argument Parsing
packages/typescript/ai/src/activities/chat/tools/tool-calls.ts
Normalize tool argument strings by trimming, treating empty/whitespace or 'null' as '{}', then parse JSON; ensures only object inputs are used and improves parse error handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I trimmed the strings and hugged the braces tight,
No more lone "null" in the pale moonlight.
Objects only hop through the gate,
Parsers smile — no more uncertain fate.
🥕✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: normalizing null tool input from the Anthropic adapter, which is the core issue addressed by this PR.
Description check ✅ Passed The description is comprehensive and follows the template structure with a clear summary of changes, root cause analysis, and test plan, though the checklist items are not marked as completed.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link

nx-cloud bot commented Feb 6, 2026

View your CI Pipeline Execution ↗ for commit f0c28a9


☁️ Nx Cloud last updated this comment at 2026-02-06 10:32:20 UTC

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.

3 participants