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

ai-error.ts
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.status is aborted. Check caller cancellation / timeouts.
AGENT_FAILED
Rust agent loop failed (maxSteps, unexpected outcome). Inspect details.runId and prior events.
AGENT_MISSING
getAgent / handoff / agentNode could 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
callTool hit a name with no local execute registry 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_route with a missing condition handler, or no matching predicate and no unconditional fallback edge.
GRAPH_RESUME_INVALID
resume() called when status is not waitingInterrupt / 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() without compile({ checkpointer }).
CHECKPOINT_NOT_FOUND
No blob for that threadId in the checkpointer.
DEF_HASH_MISMATCH
Checkpoint or in-flight run defHash does not match the compiled graph (definition changed). Start a new threadId or keep the old definition registered.
GRAPH_ALREADY_REGISTERED
compile() without replace: true when 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 }. MCP mcpTools defaults 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
generateObject got 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/fetch failures talking to an OpenAI-compatible host (not HTTP status — that is OPENAI_HTTP).
SCHEMA_UNSUPPORTED
zodToIr hit ZodEffects or an unsupported check/type.

MCP

MCP_CONNECT
mcpStdio / mcpHttp could not connect (spawn, Streamable HTTP, or SSE). Verify command/URL, headers, and transport. Failed Streamable sessions are closed before SSE fallback in auto mode.
MCP_TOOL_MISSING
Requested MCP tool name not in listTools.
MCP_TOOL_ERROR
Remote tool returned isError / failed payload.

HTTP mapping (BYO)

map-error.ts
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.

FAQ