all docs

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

ModeEffect on your requestWhat you can see
offNot consulted. Every trajectory goes upstream however deep or repetitive it is.No multi_agent_guard stage is recorded.
shadowEvery 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.
prodA 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 cap40 tool resultsFar 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 threshold4 identical callsSame 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 requestsNoneDeliberate. 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.
team@acefleet.dev →