A tool-calling pass reports whether it did any work
An all-repeat pass returns a full list of tool messages and a successful outcome while having invoked nothing, so an agent loop gated on "messages non-empty" re-enters forever. This is the port that answers the question directly.
The rule
- A node that invokes tools reports
executed_any, a boolean, andexecuted, the call identifiers behind it, the complement of theskippedlist. executed_anyis true when at least one call was handled for the first time in this run, which includes a call naming a tool that is not wired: nothing was invoked, but the recoverable "not an available tool" result is new and the model must re-plan against it.- An empty batch reports false.
- The two lists partition only the calls that reach the guard: a call with no identifier always runs and appears in neither while setting
executed_any, and a malformed call (one that is not a map, or whose name is blank or not a scalar) is dropped before the guard, appears in neither, and leavesexecuted_anyfalse. executed_anyis exposed by default;executedis not.
What it means
A repeat batch can look, from every other port, exactly like work getting
done: it still returns a full list of tool messages (each repeat re-emits
its stored message) and a successful outcome, having invoked nothing new.
executed_any is the one port that answers the question a loop actually
needs answered — whether anything happened this pass — and it is the only
one that says no.
A name that is not wired to any tool still sets executed_any: nothing ran,
but the recoverable failure the model gets back is a new result it has not
seen, and that is enough for a loop to re-enter safely. A call with no
identifier always runs and always sets executed_any, but is named in
neither executed nor skipped — it cannot be recorded either way. Only a
malformed call is excluded from all of it: dropped before anything is
checked, present in neither list, and leaving executed_any false.
Example
A batch resending a call that already ran, with nothing new in it:
{ "tool_calls": [{ "name": "do_thing", "args": [], "tool_call_id": "call_a" }] }falseThe same repeat alongside one genuinely new call:
{
"tool_calls": [
{ "name": "do_thing", "args": [], "tool_call_id": "call_a" },
{ "name": "other_thing", "args": [], "tool_call_id": "call_b" }
]
}["call_b"]skipped is ["call_a"] in both passes, ok is true in both and
tool_messages is non-empty in both; only executed_any and executed
tell them apart.