Errors & failure modes
Operators need to know what fails, how it surfaces, and how to recover. AiError is the stable surface: match on code, not message text.
AiError shape
import { AiError } from "@monorch/ai";
try {
await bot.run(input, { signal });
} catch (e) {
if (e instanceof AiError) {
// e.code: string — stable catalog below
// e.message: human-readable
// e.details?: { runId?, error?, text?, ... }
}
throw e;
}Prefer e instanceof AiError then branch on e.code. Do not parse message strings for control flow.
Failure modes
These are the paths that show up in production. Related guides: agents, tools, graphs, checkpoints, abort recipe.
Abort / cancellation
Pass AbortSignal on agent.run / stream. The loop checks abort between steps and forwards the signal to the provider. You get run_end with status: "aborted" and an AiError with code ABORTED. OpenAI-compatible fetch aborts map to the same code.
Recover: treat as a clean cancel. Do not retry automatically unless the caller intends a new run. See the abort recipe.
Tool prepare / permissions
Every callTool (including the agent loop) goes through Rust prepare: authorization + schema parse. Denied roles, { type: "deny" }, or invalid input never reach execute. That surfaces as TOOL_PREPARE_FAILED with details.error from the engine. Missing local executor → TOOL_MISSING.
The agent loop calls tools with { roles: ["agent"] } by default. Align tool permission.roles with that (or pass your own caller when invoking manually).
Recover: fix roles / schema and retry the call. Do not catch and pretend the tool succeeded — the model needs an honest tool_result or a failed run.
Graph interrupt → resume
An interrupt node stops the run at waitingInterrupt, persists a checkpoint when a checkpointer + threadId are set, and returns the handle. Call resume(decision) when the human or system decides. Calling drive() again while waiting is idempotent (re-emits wait).
Resume when status is not waitingInterrupt / waitingHuman → GRAPH_RESUME_INVALID. Restore without a checkpointer → CHECKPOINT_MISSING. Missing blob → CHECKPOINT_NOT_FOUND (start a new run for that thread).
Recover: only call resume after confirming wait status (or after a successful restore). Persist threadId in your HTTP session so the next request can restore.
Def-hash mismatch (hot reload)
Checkpoint blobs carry a defHash of the compiled graph. After compile({ replace: true }), in-flight runs and stored checkpoints whose hash no longer matches fail instead of continuing on a new definition. That usually surfaces as GRAPH_FAILED on resume, or a restore error whose message mentions def_hash / definition mismatch.
Recover: start a new run for that thread (or keep the old definition registered until waiting runs finish). See hot-reload and checkpoints.
Max steps / routing / missing nodes
Agent maxSteps exhaustion → AGENT_FAILED (with details.runId). Graph cycle / advance failures → GRAPH_FAILED. No matching conditional edge and no unconditional fallback → GRAPH_ROUTE. Engine asked for a node id with no TypeScript handler → NODE_MISSING.
Recover: raise limits, fix edge predicates, or register the missing node / agent before start. Inspect prior events for the last successful step.
Code catalog
Treat codes as part of the public surface for 0.x. Removals and renames are changelogged.
Agents & handoffs
- ABORTED
- AbortSignal fired (agent loop or provider fetch).
run_end.statusisaborted. Check caller cancellation / timeouts. - AGENT_FAILED
- Rust agent loop failed (maxSteps, unexpected outcome). Inspect
details.runIdand prior events. - AGENT_MISSING
getAgent/ handoff /agentNodecould not find the named agent. Register before use; keep names unique.- HANDOFF_DENIED
- Target not listed in
handoffs: [...]. Add the agent to the array. - HANDOFF_MIXED
- Model returned
handoff_to_*with other tool calls in the same turn. Fix prompting or provider tool choice. - HANDOFF_FAILED
- Forced handoff did not complete in the engine.
Tools
- TOOL_MISSING
callToolhit a name with no localexecuteregistry entry.- TOOL_PREPARE_FAILED
- Rust prepare failed (auth or schema). Align permission roles and Zod / IR input. See
details.error.
Graphs & checkpoints
- GRAPH_EMPTY
compile()with no nodes.- GRAPH_FAILED
- Advance failed, node threw, resume after def-hash mismatch, or unexpected engine outcome. See message /
details.runId. - GRAPH_ROUTE
need_routewith a missing condition handler, or no matching predicate and no unconditional fallback edge.- GRAPH_RESUME_INVALID
resume()called when status is notwaitingInterrupt/waitingHuman.- GRAPH_BUSY
- Concurrent
drive()/stream()/resume()on the same handle. - NODE_MISSING
- Engine asked for a node id with no TypeScript handler map entry.
- CHECKPOINT_MISSING
restore()withoutcompile({ checkpointer }).- CHECKPOINT_NOT_FOUND
- No blob for that
threadIdin the checkpointer. - DEF_HASH_MISMATCH
- Checkpoint or in-flight run
defHashdoes not match the compiled graph (definition changed). Start a newthreadIdor keep the old definition registered. - GRAPH_ALREADY_REGISTERED
compile()withoutreplace: truewhen the graph name exists.- GRAPH_NOT_REGISTERED
- Restore/advance referenced a graph name that is not compiled in this process.
- TOOL_ALREADY_REGISTERED
- Second
tool()with the same name without{ replace: true }. MCPmcpToolsdefaults to replace. - ENGINE_ERROR
- Other Rust/N-API failures remapped from a plain native
Error. Prefer matching more specific codes when present; message carries the engine reason.
Model / structured output
- EMPTY_OUTPUT
generateObjectgot empty text.- INVALID_JSON
- Model text was not parseable JSON.
- VALIDATION_FAILED
- JSON failed Rust schema validation against Zod IR.
- OPENAI_AUTH
- API key missing for
openai(). - OPENAI_HTTP
- Provider returned a non-OK HTTP status from chat completions. Check baseUrl, model id, quotas, and the response body in the message.
- OPENAI_STREAM
- SSE body missing or stream error.
- OPENAI_NETWORK
- TCP/DNS/
fetchfailures talking to an OpenAI-compatible host (not HTTP status — that isOPENAI_HTTP). - SCHEMA_UNSUPPORTED
zodToIrhitZodEffectsor an unsupported check/type.
MCP
- MCP_CONNECT
mcpStdio/mcpHttpcould not connect (spawn, Streamable HTTP, or SSE). Verify command/URL, headers, and transport. Failed Streamable sessions are closed before SSE fallback inautomode.- MCP_TOOL_MISSING
- Requested MCP tool name not in listTools.
- MCP_TOOL_ERROR
- Remote tool returned isError / failed payload.
HTTP mapping (BYO)
function statusFor(err: unknown): number {
if (!(err instanceof AiError)) return 500;
switch (err.code) {
case "ABORTED":
return 499; // or 408 — client cancelled
case "OPENAI_AUTH":
return 401;
case "TOOL_PREPARE_FAILED":
case "HANDOFF_DENIED":
case "GRAPH_RESUME_INVALID":
return 400;
case "CHECKPOINT_NOT_FOUND":
case "AGENT_MISSING":
case "TOOL_MISSING":
case "GRAPH_NOT_REGISTERED":
return 404;
case "DEF_HASH_MISMATCH":
case "GRAPH_ALREADY_REGISTERED":
case "TOOL_ALREADY_REGISTERED":
return 409;
default:
return 500;
}
}This is illustrative — pick status codes that match your API conventions. See the Fastify recipe for SSE + interrupt patterns.