Run guards
Long-running agent loops occasionally get stuck — the model keeps re-issuing the same (toolName, args) batch, or burns its turn budget hammering one expensive tool. MANTYX ships two opt-in run guards that intervene before either failure mode runs the run into the ground:
- Loop detection — fingerprints every assistant turn that emits tool calls, soft-nudges the model to pivot once it repeats itself, and forces a clean tools-disabled finalise turn if it keeps looping.
- Tool budgets — per-tool call caps enforced over the lifetime of the run; calls past the cap are intercepted before execution and replaced with a synthetic “budget exceeded — pivot or finalize” tool result.
- Supervisor — an optional platform LLM judge that periodically reviews the transcript and may inject a steering message or force a tools-disabled finalise turn.
Both loop detection and tool budgets have runtime defaults that always apply (so SDK-driven runs and platform-driven runs behave identically). The supervisor is enabled by default on ephemeral API runs; pass supervisor: false to opt out. You only ever need to touch these when you want to tune thresholds, attach a budget to a custom tool, or change supervisor behaviour.
Loop detection
Section titled “Loop detection”The pipeline tracks a canonical order-invariant (toolName, args) signature for every assistant turn that emits one or more tool calls. When the same signature repeats consecutively the guard fires.
| Threshold | Default | What happens |
|---|---|---|
consecutiveThreshold |
3 |
The pipeline skips the duplicate batch with a synthetic “you’ve made this exact call before” tool result and prepends a user-style steering nudge (“either deliver a final answer or change strategy”). The model gets the nudge before its next turn and either finalises or pivots. |
hardCutoffThreshold |
6 |
The pipeline forces a tools-disabled finalise turn so the run lands cleanly instead of churning forever. |
hardCutoffThreshold must be strictly greater than consecutiveThreshold so the soft nudge always gets a chance to land.
Tuning the thresholds
Section titled “Tuning the thresholds”// TypeScriptimport { MantyxClient } from "@mantyx/sdk";
const result = await client.runAgent({ systemPrompt: "...", prompt: "...", loopDetection: { consecutiveThreshold: 2, // nudge after 2 identical batches hardCutoffThreshold: 4, // force finalise after 4 },});# Pythonresult = client.run_agent( system_prompt="...", prompt="...", loop_detection={ "consecutiveThreshold": 2, "hardCutoffThreshold": 4, },)// Goresult, err := client.RunAgent(ctx, mantyx.RunSpec{ SystemPrompt: "...", Prompt: "...", LoopDetection: mantyx.LoopDetectionThresholds(2, 4),})Disabling the guard for a single run
Section titled “Disabling the guard for a single run”Pass the literal false (TypeScript / Python) or the LoopDetectionDisabled() sentinel (Go):
await client.runAgent({ systemPrompt: "...", prompt: "Iterate freely until you converge.", loopDetection: false, // opt out for this run only});client.run_agent( system_prompt="...", prompt="Iterate freely until you converge.", loop_detection=False,)client.RunAgent(ctx, mantyx.RunSpec{ SystemPrompt: "...", Prompt: "Iterate freely until you converge.", LoopDetection: mantyx.LoopDetectionDisabled(),})Tool budgets
Section titled “Tool budgets”toolBudgets caps how many times each tool may execute over the lifetime of the run (across every LLM turn). Calls under the cap run normally; calls past the cap are intercepted before execution and the model receives a synthetic “budget exceeded — pivot or finalize” tool result.
Budgets are per-tool, not pooled: hive_search_deals: { maxCalls: 5 } and hive_search_meetings: { maxCalls: 5 } give the agent five of each, not five between them. maxCalls: 0 disables the tool entirely (every attempt returns the synthetic body on the first try) — useful for ad-hoc denylists without rebuilding the agent’s tool surface.
Default research-tool surface
Section titled “Default research-tool surface”When toolBudgets is omitted MANTYX layers its runtime defaults on top of the spec:
| Tool | Default maxCalls |
|---|---|
recall (workspace memory hybrid search) |
4 |
traverse (memory graph BFS) |
3 |
hive_consult_ontology (per-hive ontology read) |
4 |
hive_search_deals / _meetings / _companies / _people (Sales Hive general search) |
5 |
hive_search_tickets / _conversations / _accounts (Customer Hive general search) |
5 |
hive_search_releases / _issues (Product Hive general search) |
5 |
hive_list_* and hive_get_* are intentionally not capped — agents legitimately call them once per entity-of-interest, which can easily exceed any small cap during normal multi-entity reads. The loop-detection guard catches the pathological “same (name, args) batch over and over” case for that family without needing per-tool caps.
Adding a budget for a custom tool
Section titled “Adding a budget for a custom tool”Caller overrides layer on top of the runtime defaults; when both specify a budget for the same tool, the caller’s value wins.
await client.runAgent({ systemPrompt: "...", prompt: "...", toolBudgets: { recall: { maxCalls: 8 }, // raise the default expensive_api: { maxCalls: 2 }, // cap a custom tool scary_tool: { maxCalls: 0 }, // disable a tool for this run },});client.run_agent( system_prompt="...", prompt="...", tool_budgets={ "recall": {"maxCalls": 8}, "expensive_api": {"maxCalls": 2}, "scary_tool": {"maxCalls": 0}, },)client.RunAgent(ctx, mantyx.RunSpec{ SystemPrompt: "...", Prompt: "...", ToolBudgets: mantyx.ToolBudgets{ "recall": {MaxCalls: 8}, "expensive_api": {MaxCalls: 2}, "scary_tool": {MaxCalls: 0}, },})Clearing the runtime defaults
Section titled “Clearing the runtime defaults”Pass an empty (but non-nil) toolBudgets object to start from a clean slate — useful for runs that intentionally want unbounded research:
await client.runAgent({ systemPrompt: "...", prompt: "Do a deep dive on this customer.", toolBudgets: {}, // no defaults; no caps});client.run_agent( system_prompt="...", prompt="Do a deep dive on this customer.", tool_budgets={},)client.RunAgent(ctx, mantyx.RunSpec{ SystemPrompt: "...", Prompt: "Do a deep dive on this customer.", ToolBudgets: mantyx.ToolBudgets{}, // empty (non-nil) map})Supervisor
Section titled “Supervisor”The optional run supervisor is a platform LLM judge that periodically reviews the agent’s transcript (reasoning, tool calls, tool results, visible text) and may steer the run:
| Verdict | What happens |
|---|---|
on_track |
No-op — the run continues. |
redirect |
A steering user message is injected; tools stay available on the next turn. |
finalize |
The next turn is forced tools-disabled so the run lands a clean final answer. |
Reviews fire on two triggers:
- Cadence — every
intervalLLM calls (default 5 when enabled) at tool-round boundaries (phase: "turn_boundary"). - Mid-turn reasoning — while a single turn is still streaming reasoning, once the span crosses 3000 characters or 30s (whichever first), a
phase: "reasoning"review runs. Aredirect/finalizeverdict aborts the in-flight turn. Enabled by default; tune or disable withreasoningTrigger.
await client.runAgent({ systemPrompt: "...", prompt: "...", supervisor: { interval: 10, reasoningTrigger: { chars: 5000, ms: 60000 }, },});
// only review at tool-round boundaries:await client.runAgent({ systemPrompt: "...", prompt: "...", supervisor: { interval: 10, reasoningTrigger: false },});
// opt out for this run:await client.runAgent({ systemPrompt: "...", prompt: "...", supervisor: false });client.run_agent( system_prompt="...", prompt="...", supervisor={"interval": 10, "reasoningTrigger": {"chars": 5000, "ms": 60000}},)client.run_agent( system_prompt="...", prompt="...", supervisor={"interval": 10, "reasoningTrigger": False},)client.run_agent(system_prompt="...", prompt="...", supervisor=False)client.RunAgent(ctx, mantyx.RunSpec{ SystemPrompt: "...", Prompt: "...", Supervisor: mantyx.SupervisorInterval(10).DisableReasoningTrigger(),})// custom mid-turn trigger:client.RunAgent(ctx, mantyx.RunSpec{ SystemPrompt: "...", Prompt: "...", Supervisor: &mantyx.Supervisor{ Interval: 10, ReasoningTrigger: &mantyx.ReasoningTrigger{Chars: 5000, Ms: 60000}, },})// opt out:client.RunAgent(ctx, mantyx.RunSpec{..., Supervisor: mantyx.SupervisorDisabled()})Defaults and inheritance
Section titled “Defaults and inheritance”For session-scoped runs the inheritance rules are the same for all three fields:
client.createSession({ loopDetection, toolBudgets, supervisor })(TS) /client.create_session(loop_detection=..., tool_budgets=..., supervisor=...)(Python) /mantyx.SessionSpec{LoopDetection: ..., ToolBudgets: ..., Supervisor: ...}(Go) — sets the session-default applied to every subsequent message run.session.send(prompt, { loopDetection, toolBudgets, supervisor })(TS) /session.send(prompt, loop_detection=..., tool_budgets=..., supervisor=...)(Python) /session.Send(ctx, prompt, mantyx.WithLoopDetection(...), mantyx.WithToolBudgets(...), mantyx.WithSupervisor(...))(Go) — optional per-message override; applies to that one run only and does not mutate the session’s stored value.
All three fields are additive: omitting them keeps MANTYX’s runtime defaults; passing the disable sentinel (loopDetection: false or supervisor: false) opts out; passing entries layers caller overrides on top of the defaults.
Observability events
Section titled “Observability events”Every intervention emits a dedicated SSE event so the SDK can render status notes. The synthetic skip + steering nudge / tool-result already ride the normal tool_result and assistant_delta channels — you don’t need to act on these events for the agent loop to keep running. Treat them as observability surface.
// loop-detection guard fired{ "seq": 9, "type": "loop_detected", "data": { "consecutiveCount": 3, // length of the identical-batch streak "hardCutoff": false, // false = soft nudge round; true = forced finalise "tools": ["recall"] // names of the tool calls in the looping batch }}
// per-tool budget exceeded{ "seq": 10, "type": "tool_budget_exceeded", "data": { "tool": "recall", // logical tool name "maxCalls": 4, // configured cap "callIndex": 5 // which call (1-indexed) tripped the cap }}
// run-supervisor review (fired on every check — on_track included){ "seq": 11, "type": "supervisor", "data": { "action": "redirect", // on_track | redirect | finalize "phase": "turn_boundary", // or "reasoning" for mid-turn reviews "reason": "Stuck re-querying.", "redirect": "Answer from the data you already have.", // present when action=redirect "llmCalls": 10 }}A single run may emit any number of these events: zero (well-behaved agents), one or more tool_budget_exceeded events as the model keeps reaching for capped tools, a loop_detected (hardCutoff: false) followed by a second loop_detected (hardCutoff: true) if the model keeps looping past the soft nudge, or periodic supervisor events as the judge reviews the transcript.
await client.runAgent({ systemPrompt: "...", prompt: "...", onEvent: (ev) => { if (ev.type === "loop_detected") { console.warn(`looping on ${ev.tools.join(", ")} (×${ev.consecutiveCount})`); } else if (ev.type === "tool_budget_exceeded") { console.warn(`tool ${ev.tool} hit cap ${ev.maxCalls} on call #${ev.callIndex}`); } else if (ev.type === "supervisor") { const phase = ev.phase ?? "turn_boundary"; console.warn(`supervisor [${phase}] ${ev.action}: ${ev.reason}`); } },});Limits
Section titled “Limits”| Constraint | Limit |
|---|---|
loopDetection.consecutiveThreshold |
2 ≤ n ≤ 100 |
loopDetection.hardCutoffThreshold |
3 ≤ n ≤ 100, must be > consecutiveThreshold |
toolBudgets max entries |
32 |
toolBudgets[<name>] key length |
1..120 chars |
toolBudgets[<name>].maxCalls |
0 ≤ n ≤ 1000 (functionally unlimited; maxToolTurns: 100 fires first) |
supervisor.interval |
1 ≤ n ≤ 100 (default 5 when enabled and omitted) |
supervisor.reasoningTrigger.chars |
1 ≤ n ≤ 50000 (default 3000 when enabled and omitted) |
supervisor.reasoningTrigger.ms |
1 ≤ n ≤ 600000 (default 30000 when enabled and omitted) |
The reference SDKs mirror these checks locally so callers see an early typed error rather than a server round-trip.
See also
Section titled “See also”- Streaming — the full SSE event vocabulary, including the
loop_detected,tool_budget_exceeded, andsupervisorobservability events. - Wire protocol §8 — canonical spec for the wire shapes (with subsections 8.1
loopDetectionand 8.2toolBudgets). - Task planning — opt-in
planfield andtask_planSSE events (complements supervisor reviews during planned runs). - Agent-runs protocol §4.6 and §4.7 — server-side validation contract and inheritance rules for sessions.
