Merged
Size
M
Change Breakdown
Bug Fix80%
Testing20%
#61956fix(agents): preserve malformed function-call arguments instead of silent {} replacement

Malformed tool-call arguments preserved on replay

Tool-call arguments that fail JSON parsing are now preserved as raw strings instead of silently disappearing into empty objects, fixing irrecoverable data loss during assistant history replay.

When replaying OpenAI assistant history, tool calls with malformed arguments were silently destroyed. The code caught JSON parsing failures and replaced the original arguments with empty objects {}, making it impossible to debug why a tool failed or to retry the call with actual data.

Malformed function-call arguments are now preserved as raw strings in stored assistant history. This keeps the original payload available for debugging while still working correctly with the replay pipeline — OpenAI transport knows to send strings as-is, while Anthropic and Google transports parse them back into objects for their respective SDKs.

The fix spans three transport paths in the agents codebase: OpenAI WebSocket message conversion now returns raw strings from parse errors, the OpenAI transport stream avoids double-encoding those strings, and a new shared coercion helper bridges the gap for Anthropic and Google providers that expect parsed JSON objects.

This repair is part of ongoing work to ensure replay fidelity in the OpenAI function-call flow.

View Original GitHub Description

Fixes #61478.

Recreated from #61527 with single-concern scope (as requested by @steipete).

What this fixes (plain English)

When replaying OpenAI assistant history, if a tool call's arguments contain malformed JSON, the code silently replaced them with an empty object {}. This caused irrecoverable data loss — the original arguments were permanently destroyed, making it impossible to debug or retry the tool call. The same data loss occurred through the HTTP/WS transport replay path, where preserved raw strings were double-encoded by JSON.stringify. The same coercion gap existed on the Anthropic and Google transport boundaries, where replayed string arguments would reach provider SDKs that expect a parsed object.

Technical details

Root cause: In buildAssistantMessageFromResponse(), the JSON.parse(item.arguments) catch block returned {} as Record<string, unknown> instead of preserving the raw string. Additionally, createOpenAIResponsesTransportStreamFn blindly JSON.stringify'd tool-call arguments, double-encoding any preserved raw strings. The Anthropic and Google transport stream builders both passed raw tool-call arguments straight into provider SDK payloads, so a preserved string would reach those providers un-coerced.

Fix:

  1. Catch block now returns item.arguments (the raw string) with a type assertion. Downstream convertMessagesToInputItems already handles typeof block.arguments === "string" explicitly.
  2. OpenAI transport stream serializer uses a typeof guard to avoid double-encoding.
  3. New shared helper coerceTransportToolCallArguments in src/agents/transport-stream-shared.ts parses raw string arguments back into objects at the Anthropic and Google transport boundaries (the providers that need parsed JSON), falling back to {} only if parsing fails — preserving the debug path for OpenAI while keeping other providers working.

Files changed

OpenAI path (commit 1)

  • src/agents/openai-ws-message-conversion.ts — catch block preserves raw arguments
  • src/agents/openai-transport-stream.ts — typeof guard prevents double-encoding string arguments
  • src/agents/openai-ws-stream.test.ts — regression tests for parse preservation + e2e replay round-trip
  • src/agents/openai-transport-stream.test.ts — regression test for transport stream string argument serialization

Cross-provider replay coercion (commit 2)

  • src/agents/transport-stream-shared.ts — new coerceTransportToolCallArguments helper
  • src/agents/anthropic-transport-stream.ts — coerces replayed string arguments at the Anthropic boundary
  • src/agents/anthropic-transport-stream.test.ts — regression tests covering string/object/malformed argument replay
  • src/agents/google-transport-stream.ts — coerces replayed string arguments at the Google boundary
  • src/agents/google-transport-stream.test.ts — regression tests covering string/object/malformed argument replay

Related

  • Parent fix: #59643
  • Companion PRs: #61529 (phase inheritance), #61463 (phase extraction, merged)
  • Initiative: #25592

Test plan

  • Malformed JSON arguments preserved as raw string (buildAssistantMessageFromResponse)
  • Raw string arguments survive replay round-trip (convertMessagesToInputItems)
  • OpenAI transport stream serializes string arguments without double-encoding
  • Anthropic transport stream coerces replayed string arguments to objects
  • Google transport stream coerces replayed string arguments to objects
  • Valid JSON arguments still parsed normally across all transports

🤖 Generated with Claude Code

© 2026 · via Gitpulse