Multi-agent guard
Halts a runaway agent loop before it is paid for — a depth cap and a repeat detector.
problem it solves
An agent whose plan is broken does not stop — it re-sends a prompt that grows on every turn, so a stuck loop bills superlinearly and every individual request looks well-formed. A per-request timeout never sees it.
What it does
What it does: Reads the trajectory your request already carries — the messages array with its tool calls and tool results — and refuses the request when that trajectory is the shape of a loop.
Two limits, because runaway loops fail in two different shapes
- ·Depth cap — how many tool results the trajectory contains. Depth counts observations, which is the work already paid for; a trajectory whose calls were never answered has depth 0 and is not runaway yet.
- ·Repeat detector — trips when the same tool has been called with the same arguments too many times. That is the stuck planner: nowhere near the depth cap, calling `query_ledger({"id": 42})` for the sixth time. Arguments are compared after re-serialising with sorted keys, so key order does not hide a repeat.
What happens at the limit: In `prod` the request is refused with a 429 whose message reads `agent loop halted: <reason>` — `depth 41 exceeds max 40`, or `identical tool call repeated 4x`. Nothing goes upstream, so the turn that would have grown the loop is never billed.
What it does not do: It keeps no state between requests. The trajectory the caller sent is the state, and a server-side copy would be a second source of truth that drifts the moment a client retries. An agent framework that resends the whole conversation on every turn — the overwhelming majority — is fully visible. One that keeps history server-side and sends only the latest turn is not, and the guard returns `allow` for it rather than guessing; that blind spot is written down here rather than papered over.
What we need from you
- The whole trajectory on each requestrequired
The guard classifies the messages array you send: prior assistant turns with their tool calls, and the tool results that answered them. That is what every mainstream agent framework already sends, on every surface (Gemini's `model` role and Anthropic's tool results inside a user turn are read as the same thing). There is no trajectory header to set and nothing to correlate across calls. If your framework holds history server-side and sends only the newest turn, the guard sees depth 0 and allows every request.
- Agent trafficrecommended
On a single-shot completion there is no tool loop to bound. The skill is inert rather than harmful, but it buys nothing.
What each mode does
| Mode | Effect on your request | What you can see |
|---|---|---|
| off | Not consulted. Every trajectory goes upstream however deep or repetitive it is. | No multi_agent_guard stage is recorded. |
| shadow | Every request is classified and nothing is refused. The trajectories that would have been halted are recorded with the limit they crossed — which is how you find out whether your agents legitimately re-poll a tool before the guard starts refusing real work. | Stage with action=would_halt and the depth; `x-ace-multi-agent-depth` on the response, and `x-ace-multi-agent-halt` carrying the reason when it would have halted. |
| prod | A trajectory that crosses a limit is refused with a 429, `agent loop halted: <reason>`, before anything is sent upstream. A 429 is what most SDKs retry, and a retry resends the same trajectory and gets the same refusal — read the reason and stop the loop rather than backing off. | Stage with action=halt, the depth and the reason; `x-ace-multi-agent-depth` on every served response the guard inspected. |
Current policy
| Depth cap | 40 tool results | Far past what a well-formed task needs — the reference agent corpus tops out at 16 by construction. `ACE_MULTI_AGENT_MAX_DEPTH` sets it per deployment. |
| Repeat threshold | 4 identical calls | Same tool, same arguments after normalisation. Two is a legitimate retry after a transient failure; four is a loop. `ACE_MULTI_AGENT_MAX_REPEATS` sets it per deployment. |
| State kept between requests | None | Deliberate. The request carries its own history; the guard reads it and forgets it. |
Worth knowing before you enable it
- ·There is no trajectory id and no session to set. If the guard appears to do nothing, check what your framework sends: a client that keeps history server-side and posts only the latest turn presents a depth-0 trajectory on every call.
- ·Depth is tool results, not turns or tokens. A long conversation with no tool use never trips it, and a token budget is not something this skill has — spend caps live on the key.
- ·Repeat detection compares call shape, so an agent legitimately polling the same endpoint with the same arguments looks like a loop. Shadow first, and read the reasons it records.
- ·The halt is a 429, and SDK retry logic treats 429 as transient. It is not: the retry resends the same trajectory. Surface the message to your orchestrator instead of letting the client back off and re-send.
- ·The guard never fails a request on its own error. A messages array it cannot read is allowed through and the failure is logged, so a cosmetic client bug does not become a refused request.
What it replaces
- ·Hand-rolled step counters in each agent, which every framework implements slightly differently.
- ·Spend alerts that fire hours after the loop started.
- ·A kill switch someone has to notice and pull.
Per-key limits and custom halt behaviour are available on the enterprise tier.
- ·Depth and repeat thresholds set per developer key, so an experimental agent cannot loosen a production one's limits.
- ·A webhook on halt, so your own orchestrator learns about it rather than discovering it in a response.