Skip to content

fix(ai): avoid crashing on malformed streamed tool-call input#1707

Open
craze3 wants to merge 2 commits intovercel:mainfrom
craze3:codex/fix-malformed-tool-call-input
Open

fix(ai): avoid crashing on malformed streamed tool-call input#1707
craze3 wants to merge 2 commits intovercel:mainfrom
craze3:codex/fix-malformed-tool-call-input

Conversation

@craze3
Copy link
Copy Markdown

@craze3 craze3 commented Apr 12, 2026

Description

Closes #1706

  • add a small safe parser for streamed tool-call.input
  • replace eager JSON.parse(...) calls in do-stream-step.ts and stream-text-iterator.ts that could crash the workflow step before experimental_repairToolCall was reached
  • preserve malformed input as the raw string instead of throwing so existing repair and error-handling paths remain reachable

This keeps the fix narrowly scoped to internal DurableAgent reconstruction paths. It does not change the public API or introduce provider-specific behavior.

How did you test your changes?

  • added unit coverage for the new safe parser in packages/ai/src/agent/do-stream-step.test.ts
  • added a regression test that exercises doStreamStep with a mock streamed tool call whose input is malformed JSON and verifies the step no longer throws
  • added a regression test in packages/ai/src/agent/stream-text-iterator.test.ts that verifies malformed tool-call input is preserved into the reconstructed assistant message instead of crashing during prompt rebuild
  • ran the focused @workflow/ai test suite for the touched agent paths:
pnpm --filter @workflow/ai test -- \
  src/agent/do-stream-step.test.ts \
  src/agent/stream-text-iterator.test.ts \
  src/agent/durable-agent.test.ts \
  src/agent/telemetry.test.ts
  • ran the package build:
pnpm --filter @workflow/ai build

Recommended commands to rerun in a fresh fork before marking ready:

pnpm --filter @workflow/ai test -- \
  src/agent/do-stream-step.test.ts \
  src/agent/stream-text-iterator.test.ts \
  src/agent/durable-agent.test.ts \
  src/agent/telemetry.test.ts
pnpm --filter @workflow/ai build

Notes

  • This intentionally preserves the raw malformed input string on parse failure rather than coercing to {} so repair hooks and debugging still have access to the original provider output.
  • This does not attempt to repair malformed input itself; it only prevents internal reconstruction from crashing before repair/error handling can occur.

PR Checklist - Required to merge

  • 📦 pnpm changeset was run to create a changelog for this PR
  • 🔒 DCO sign-off passes
  • 📝 Ping @vercel/workflow in a comment once the PR is ready, and the above checklist is complete

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 12, 2026

🦋 Changeset detected

Latest commit: 54db77b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@workflow/ai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 12, 2026

@craze3 is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@craze3
Copy link
Copy Markdown
Author

craze3 commented Apr 12, 2026

@vercel/workflow ready for review.

This fixes #1706 by preserving malformed streamed tool-call input during DurableAgent reconstruction so experimental_repairToolCall and normal error handling can run instead of crashing early.

It looks like the Vercel preview checks on this fork PR are blocked pending team authorization.

@craze3 craze3 marked this pull request as ready for review April 12, 2026 22:22
@craze3 craze3 requested a review from a team as a code owner April 12, 2026 22:22
Copy link
Copy Markdown
Member

@VaguelySerious VaguelySerious left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM generally, I was concerned about leaving the malformed string in there potentially, so I had AI do a quick assessment. Let me know what you think about it / adjust as you see fit, then feel free to re-tag me for review here or send a comment on the issue


If the repair function fixes the tool call and execution succeeds, the loop continues and the prompt is sent to the provider on the next turn. Providers serialize this input directly:

  • OpenAI (@ai-sdk/openai, internal/index.mjs:224): does arguments: JSON.stringify(part.input). If input is already a string like '{"city":"San', JSON.stringify produces a double-encoded
    string literal '"{\"city\":\"San"' — not a valid function arguments value.
  • Anthropic (@ai-sdk/anthropic, internal/index.mjs:2451): passes input: part.input directly as tool_use.input. Anthropic's API expects a JSON object here; a string will be rejected.

So the flow is:

  1. Model emits malformed tool-call input
  2. safeParseToolCallInput preserves it as a string (no crash — good)
  3. String is baked into conversationPrompt
  4. executeTool in durable-agent.ts:1581 hits JSON.parse → throws → repair runs → tool executes successfully
  5. Next doStreamStep call sends the contaminated prompt to the provider
  6. Provider API error on the next turn — a new failure that didn't exist before

Before this PR, the crash at step 2 prevented the loop from ever reaching step 5-6. After the PR, you trade an immediate crash for a deferred one on the next model call. This only
matters when all three conditions hold: malformed input + repair succeeds + there's a subsequent model turn. But that's exactly the scenario the PR is designed to enable.

Suggested fix:
After repair succeeds in executeTool, the repaired tool call input should be patched back into the conversationPrompt. Alternatively, streamTextIterator could defer writing the
assistant tool-call message until after tool execution/repair, using the (potentially repaired) input.

// Mock doStreamStep
vi.mock('./do-stream-step.js', () => ({
doStreamStep: vi.fn(),
safeParseToolCallInput: (input: string | undefined) => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can just be original.safeParseToolCallInput instead of rewriting the function, to avoid drift

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved the parser into a shared internal helper, so stream-text-iterator now imports the real implementation instead of re-declaring it in the test mock.

* Parse streamed tool-call input without crashing the workflow step when a
* provider emits malformed or truncated JSON.
*/
export function safeParseToolCallInput(input: string | undefined): unknown {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this also re-use safeParseInput from durable-agent.ts? Or is the undefined fallthrough intentional?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pulled this into a shared helper and now reuse it from do-stream-step, stream-text-iterator, and durable-agent. The undefined/empty-input -> {} behavior is still intentional for no-args tool calls.

@VaguelySerious VaguelySerious added the backport-stable Cherry-pick this PR to the stable branch when merged label Apr 13, 2026
Signed-off-by: zz <craze3@gmail.com>
@craze3
Copy link
Copy Markdown
Author

craze3 commented Apr 14, 2026

Good catch on the deferred-failure path. I pushed a follow-up commit that patches repaired tool-call input back into the accumulated assistant tool-call message before the next model step, and also updates the original tool-call object so downstream toolCalls / toolResults reflect the repaired value too.

I also moved the parser into a shared helper to avoid drift between the touched files and tests. Re-ran the focused @workflow/ai checks after the change:

pnpm --filter @workflow/ai test -- \
  src/agent/do-stream-step.test.ts \
  src/agent/stream-text-iterator.test.ts \
  src/agent/durable-agent.test.ts \
  src/agent/telemetry.test.ts
pnpm --filter @workflow/ai build

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-stable Cherry-pick this PR to the stable branch when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DurableAgent crashes on malformed streamed tool-call input before experimental_repairToolCall can run

2 participants