Weak models write tool calls as prose. We started executing them FIIT.AI added a runtime step that extracts prose-shaped tool calls from weak models' message bodies and converts them into real tool_calls, after its didNothing guard failed to fire on a turn where an agent wrote functions.fiitx_affiliate_ops({"operation":"target_invite","commit":true}) inside a fenced code block instead of the tool_calls channel, resulting in zero tool calls and no messages sent. The original guard keyed on zero tool calls AND an empty body, so the non-empty body let the turn close normally and the UI render "done"; a nudge-and-retry fix also failed four days later on a different agent, where the model repeated the same prose call across three "continue" presses with zero executions. The new step runs extracted calls through the ordinary path with the same preflight, approval gates, and execution, restricted to tools declared that turn, skipping placeholder arguments, taking only the first occurrence, and still requiring human approval for anything that sends, posts, pays, or deletes. ← FIIT.AI https://fiit.ai/ Weak models write tool calls as prose. We started executing them. A user told one of our agents to send a batch of outreach messages. The agent replied with this: functions.fiitx affiliate ops {"operation":"target invite","commit":true} — inside a fenced code block, in the body of the message. Not in the tool calls channel. The runtime only executes tool calls , so the turn made zero tool calls. Nothing was sent. The interesting part is what our own guard did. We had a check for exactly this class of dishonesty — an agent that finishes a turn without doing anything. It fired on didNothing : zero tool calls and an empty body. Here the body was not empty. It was full of text that looked like a successful execution. So the guard stayed quiet, the turn closed normally, and the UI rendered “done”. The user saw a code block that looked like a receipt, believed the invites had gone out, and only found out on the next turn when they asked why nobody had replied. The guard was testing the wrong thing zero calls AND empty body encodes an assumption: that a model which did nothing will also say nothing. That assumption is wrong for the models where it matters most. A model good enough to produce well-formed arguments but not reliable enough to emit them through the function-calling channel will happily narrate the call instead — and the narration is the most confident-looking text in the whole transcript. The fix for detection was to stop guessing at shape and start keying on identity. For each turn we already build the list of tools we declared to the model. We now keep that list and look for a declared tool name in the body followed immediately by { or " . Mentioning a tool by name in a sentence does not trip it. A normal function call in a code sample does not trip it either, because the names never match. Retrying was not enough The first version just pushed back: a nudge telling the model it had not actually called anything, try again. That works on capable models. It does not work on the ones that produce this failure in the first place. We hit it again four days later on a different agent. The nudge fired, the model got the message, and on the next turn it wrote the same call into the body again. The user pressed “continue” three times and got three identical blocks of text and zero executions. At that point the argument for materialising the text became hard to ignore. The model had already done the hard part. The arguments were complete and correct. It had picked the right tool. The only thing wrong was the transport. Refusing to honour it was costing the user a working turn to protect a purity that bought nothing. So we added a step before the assistant message is pushed onto the transcript: extract prose-shaped calls, convert them into real tool calls on that assistant message, and let them run through the ordinary path — same preflight, same approval gates, same execution. Nothing bypasses anything. The assistant message ends up carrying the calls it should have emitted, which also keeps the following role: "tool" messages structurally valid. What keeps this from being reckless Materialising text into side effects deserves suspicion. The constraints that make it safe: - Only tools declared this turn. Same identity check as the detector. The model cannot invent a tool by writing its name. - A plan is not a call. If the arguments contain placeholders — /path/to/… ,