Version: @convex-dev/agent@0.6.1 (the same code path is unchanged in 0.6.2); convex@1.39.x; tools defined via createTool.
Symptom
When the model emits the same tool more than once within a single generateText step/turn, the first execution succeeds and the subsequent executions throw ctx.runMutation is not a function. (The AI SDK catches it, so it surfaces to the model as the tool result string rather than crashing the action.) The tool's execute does nothing unusual — it just calls ctx.runMutation(...) on the injected ctx.
What I traced (client/createTool.js)
wrapTools(toolCtx, tools) sets a single shared tool.ctx = { ...ctx, userId, threadId, ... } once per call; execute resolves it via getCtx(this) → this.ctx.
- The
if (!getCtx(this)) throw "...you must provide the ctx..." guard does not fire on the failing calls — so on the repeat execution getCtx(this) returns a truthy ctx that is missing runMutation (and presumably the other run* action capabilities). On the first execution the same ctx has runMutation (the tool succeeds).
- Net: within one step, the first same-tool execution receives a full action ctx and the repeats receive a degraded one.
Expected
Every tool execution within a step receives the same fully-capable injected ctx (with runMutation / runQuery / runAction).
Actual
Repeated same-tool executions in one step get a ctx lacking the action run* methods → ctx.runMutation is not a function.
Notes
Observed in a production app (a multi-tool agent turn where the model re-emitted one tool), not yet distilled to a minimal reproduction. I'm happy to build a minimal repro — a single Convex tool whose execute calls ctx.runMutation, prompted to call it twice in one step — if that helps triage. The symptom + code path are high-confidence; the exact per-call degradation point inside the step loop is inferred.
Version:
@convex-dev/agent@0.6.1(the same code path is unchanged in0.6.2);convex@1.39.x; tools defined viacreateTool.Symptom
When the model emits the same tool more than once within a single
generateTextstep/turn, the first execution succeeds and the subsequent executions throwctx.runMutation is not a function. (The AI SDK catches it, so it surfaces to the model as the tool result string rather than crashing the action.) The tool'sexecutedoes nothing unusual — it just callsctx.runMutation(...)on the injected ctx.What I traced (
client/createTool.js)wrapTools(toolCtx, tools)sets a single sharedtool.ctx = { ...ctx, userId, threadId, ... }once per call;executeresolves it viagetCtx(this) → this.ctx.if (!getCtx(this)) throw "...you must provide the ctx..."guard does not fire on the failing calls — so on the repeat executiongetCtx(this)returns a truthy ctx that is missingrunMutation(and presumably the otherrun*action capabilities). On the first execution the same ctx hasrunMutation(the tool succeeds).Expected
Every tool execution within a step receives the same fully-capable injected ctx (with
runMutation/runQuery/runAction).Actual
Repeated same-tool executions in one step get a ctx lacking the action
run*methods →ctx.runMutation is not a function.Notes
Observed in a production app (a multi-tool agent turn where the model re-emitted one tool), not yet distilled to a minimal reproduction. I'm happy to build a minimal repro — a single Convex tool whose
executecallsctx.runMutation, prompted to call it twice in one step — if that helps triage. The symptom + code path are high-confidence; the exact per-call degradation point inside the step loop is inferred.