fix(agent-runtime): keep zod internals when copying tool definitions - #1342
Open
KazenDev wants to merge 1 commit into
Open
fix(agent-runtime): keep zod internals when copying tool definitions#1342KazenDev wants to merge 1 commit into
KazenDev wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
With any MCP server configured, the first message of a run dies with an error overlay in the TUI:
The message only mentions the MCP server, so it reads like a server problem — it isn't. It's
cloneDeepdropping zod's internals whengetToolSetcopies a tool definition, and every MCP tool is a zod schema by the time it gets there.Root cause
MCP tools are the only tool definitions whose
inputSchemais a live Zod schema —mcp.tsbuilds them withconvertJsonSchemaToZod(inputSchema), while SDK custom tools stay JSON Schema. And zod v4 keeps its internals on a non-enumerable property:cloneDeepcopies own enumerable properties only, so the clone loses_zod— but it keeps the prototype, which is what makes it dangerous:ensureZodSchemacheckstypeof schema.safeParse === 'function', that still passes, so the broken clone is handed to the AI SDK as a valid schema. It then fails inside zod:ensureJsonSchemaCompatibleeven has a fallback for schemas that can't be converted — but reaching it readsschema.description, which is exactly the call that throws.Fix
Copy the definition, carry the schema by reference. Schemas are immutable, and
ensureZodSchemaaccepts either a Zod schema or JSON Schema, so nothing downstream changes for SDK custom tools:How to reproduce
Verified directly against
getToolSet(no MCP server needed — the definition is what an MCP tool looks like oncemcp.tshas converted it):main:CRASH: undefined is not an object (evaluating 'schema._zod.parent')+ the stack above.OK, tools: [ "exa__web_search_exa" ].In the product the same thing happens end to end: put a
mcpServersentry in.agents/mcp.json, start a run, and send a message — the first step loads the MCP tools and the run dies.The regression test added here fails on
mainand passes with the patch:mainVerification
mainand passes with the patch (load check, not just a green run).getToolSetprobe above: crash onmain, clean with the patch.bun run --cwd packages/agent-runtime test: the only remaining failure in that package is the pre-existing one below.bunx prettier --checkpasses on the test file.prompts.tswas already unformatted onmain(prettier wants changes inhasMeaningfulJsonSchemaandparamsSection, lines I don't touch), so I left it alone rather than mix unrelated reformatting into this patch.bun.lockpinszod@4.6.2, which is what I tested against.Reproduced against a real MCP server, then fixed
Same binary, same machine, same
mcpServersentry (Exa over HTTP) — only the build differs:undefined is not an object (evaluating 'H._zod.parent')and the run diesThe error is an overlay in the TUI rather than something in the log, since it is thrown while the tool set is assembled, before the step runs. One thing worth knowing when you build this yourself: with MCP configured, this patch alone surfaces a separate crash (“JSON.stringify cannot serialize cyclic structures”) from the run-state clone in #1341 — with both applied the run is clean. That is how I found out the two are independent.
Two tests are already red on main (FYI, not touched)
The public mirror doesn't run the test suite, so these have been sitting there — both in
prompts-schema-handling.test.ts, both with the lockfile's zod:getToolSet handles custom tools with problematic schemas— same root cause as this PR, and this patch repairs it.buildToolDescription preserves MCP params when schema is represented as allOf— a stale expectation: zod 4.6.2 merges that intersection into a flat object (nameandcb_easpboth survive,.and()just no longer emitsallOf). Changing that assertion is a call about intended output, so I left it out of this PR — happy to send it separately if you want it.Not in this PR, on purpose
Three other places deep-clone something that can hold a schema. They are fine today, and the difference is why this patch stays narrow:
run-agent-step.ts:154andtool-executor.ts:678clonefileContext.customToolDefinitions, but everything in that map is JSON Schema by then: the SDK's custom tools are converted withz.toJSONSchemainsdk/src/run-state.tsbefore they get there, and the MCP entries are written into a per-step copy that is never written back. There is no live schema forcloneDeepto strip.sdk/src/run-state.ts:1074(cloneSessionState) does clone a state whose agent templates hold live schemas, but every consumer converts inside atry/catch(templates/strings.ts:247,lookup-agent-info.ts:85), so a stripped copy degrades to a fallback instead of throwing.getToolSetis the one place where the copy is followed by an unguarded zod read, which is why it is the one that crashes.If you'd rather have a single zod-aware clone helper used everywhere, say so and I'll follow up with it.
Scope
Two files, +44/-2:
packages/agent-runtime/src/tools/prompts.tsand its test file. No dependency changes, no behavior change for SDK custom tools.