The Workflow Kept Going. Nobody Answered the Tool Call.
A durable agent run in Mastra's open source framework is meant to stop and wait when it reaches a tool the client, not the model, is supposed to answer. A minimal reproduction filed on 8 September 2026 shows the durable loop instead recording an answer nobody sent and calling the model a second time. The ordinary, non durable loop in the same codebase gets this right.
Event analysed: . This analysis was published on 8 September 2026.
No, and Mastra's own codebase shows why the distinction matters. GitHub issue mastra-ai/mastra#23295, opened 8 September 2026 and unresolved at the time of this record, reports that an agent configured with durable: true does not stop at a tool declared without an execute function, the pattern Mastra uses for a tool the client or a human is meant to answer. A minimal, scripted reproduction shows the ordinary agent loop correctly calling the model once and leaving the tool call in a call state, waiting. The durable loop calls the model a second time, having recorded a result nobody sent. Reading the current source directly confirms the mechanism: the durable workflow's tool call step returns result: undefined for a tool with no execute function, and the durable merge step then writes that into the message history as state: result with no check for whether a result actually arrived, unlike the ordinary loop's merge step, which explicitly tests for exactly this case and halts. A fix was proposed the same day, pull request #23275, and closed unmerged by an automated policy for lacking a linked issue before #23295 existed to link. As of this record, the durable loop in Mastra's current source still exhibits the behavior, and no shipped release carries a fix.
A tool call that a model emits and a tool call that has been answered are two different facts. Most agent frameworks that support human approval or client side tools keep them separate on purpose: the model asks, and the loop stops until something outside the model supplies a result. What happens when the code responsible for keeping that distinction has two implementations of the same loop, and only one of them gets it right?
That is what a minimal reproduction filed against Mastra, an open source TypeScript agent framework, shows.
What the issue reports
GitHub issue mastra-ai/mastra#23295, titled "[BUG] durable: true, the agent loop does not stop at a client executed (execute less) tool call," was opened 8 September 2026 against @mastra/core 1.64.0 and remained open, labeled needs triage, at the time this record was checked. The reproduction is deterministic and self contained: an agent is configured with a tool, askClient, declared with no execute function, the pattern Mastra uses to mark a tool the client, not the server, must run. A scripted model emits a call to that tool and finishes with reason tool-calls, then, on a second turn, emits plain text.
With the agent's ordinary, non durable configuration, the loop calls the model once. The tool invocation is left in a call state and the run ends, exactly as a pending client tool should behave: waiting. With durable: true set on the same agent and the same scripted model, the loop calls the model twice. The second call runs despite no client ever having supplied a result for the pending tool.
What the current source actually does
This record verified the mechanism directly against Mastra's own source at commit d7bd6f7a91daf528f34d628faede4a916421b0dd, the current head of the repository's default branch at the time of verification, rather than taking the issue's own account on faith.
The durable workflow's tool call step, in packages/core/src/agent/durable/workflows/steps/tool-call.ts, is explicit about what happens when a tool has no execute function: it returns immediately with result: undefined, deferring to whatever, or whoever, is supposed to run the tool outside the model loop. That much is correct and matches the intended design.
The durable loop's merge step, packages/core/src/agent/durable/workflows/steps/llm-mapping.ts, is where the divergence happens. For every entry in that step's tool results, excluding only a denied approval, it computes const result = toolResult.error ? toolResult.error.message : toolResult.result and writes the tool invocation into message history as state: 'result', result, with no test anywhere in that path for whether toolResult.result is actually defined. A tool call nobody answered is written into history in exactly the same state as a tool call that succeeded, with an undefined result standing in for one. Whether the loop continues to a further model call, isContinued, is derived only from whether any tool call errored and from the model's own step result; a pending, unanswered, non erroring tool call does not stop it.
The ordinary, non durable loop's own merge step, packages/core/src/loop/workflows/agentic-execution/llm-mapping-step.ts, contains the check the durable step is missing. It explicitly computes whether any tool call has result === undefined while carrying no error, is not an aborted call, and is not a denied approval, calls that condition a pending HITL call in its own comments, and lets it override an otherwise continuing turn: the loop bails out, isContinued is set to false, and the tool invocation is deliberately left unwritten to a result state, skipped rather than resolved, so it remains in call state for a client to answer later. This is the same codebase, the same tool contract, and two different answers to the identical question: has this call actually been answered.
The proposed fix, and why it is not yet shipped
Pull request #23275, titled "fix(core): stop the durable loop at a client executed tool call," was opened 8 September 2026, several hours before the tracking issue. It proposed mirroring the ordinary loop's pending call check into the durable merge step, skipping result recording for a call nobody answered and ending the durable turn while the invocation stays in a call state, along with a regression test for both the initial suspension and a later, genuine client result resuming the run correctly.
The pull request was closed the same day, unmerged, by an automated repository policy that closes a pull request lacking a linked issue. Issue #23295 was opened afterward, specifically to give the proposed fix somewhere to attach to. As of this record, no replacement pull request references #23295, current source at the repository's default branch still lacks the pending call check described above, and the package's own changelog carries no entry for this fix. The reported behavior is current, not historical.
What this does, and does not, establish
What is verified: an autonomous model call proceeds past a tool call boundary that Mastra's own design, and its own non durable code path, treat as requiring an external answer before the loop may continue. That is a genuine divergence in control flow semantics across exactly the boundary a client side or human in the loop tool exists to enforce, reproduced deterministically and confirmed by reading the causing code directly, not merely by trusting the report.
What is not established: the reproduction's second model call only emits text. Nothing in the issue, the source, or this record's own verification shows a consequential external action executing as a result of this specific defect, and Mastra's own approval decline handling, a separate code path for a tool a human explicitly declined rather than never answered, is unaffected. Whether a given deployment routes a consequential action, an approval, a payment, a destructive operation, through an execute less tool depends entirely on how that deployment uses the framework, and this record does not extend the observed mechanism into a claim about any specific deployment's exposure.
Sources
This analysis interprets third-party reporting, research and announcements. Moona is not the original reporter of the underlying events.
