Error format

The plumbing runtime uses a structured JSON error format on the wire. This document defines the format, the error categories, and the error codes in current use.

Wire format

All errors serialise as a JSON object with three required fields:

{"error": "machine_code", "category": "unavailable", "detail": "human-readable message"}

Optional extras are appended as additional fields:

{"error": "unknown_tool", "category": "not_found", "detail": "unknown tool: foo (not found in any MCP server)", "source": "foo"}

This replaced an earlier flat format ({"error": "human message"}) where the error field carried a human-readable string with no stable machine-readable code.

OCaml type

Defined in lib/runtime/error.ml:

type category =
  | Config       (* static configuration error, exit 2, not retryable *)
  | Invalid      (* bad input / validation / parse, not retryable *)
  | Not_found    (* named entity doesn't exist, not retryable *)
  | Denied       (* access denied / OITL rejection, not auto-retryable *)
  | Internal     (* runtime bug, not retryable *)
  | Unavailable  (* transient failure, retryable *)

type t = {
  code     : string;
  detail   : string;
  category : category;
  extras   : (string * Yojson.Safe.t) list;
}

val retryable : t -> bool

code is the machine-readable error code. detail is a human-readable explanation. category classifies the error for retry policy dispatch. extras carries optional context fields (e.g. source). The keys "error", "detail", and "category" are reserved and are filtered from extras at construction time to prevent collision.

Exit codes

error.ml:exit_code maps categories to Unix exit codes:

  • Config → exit 2 (configuration error)
  • all other categories → exit 1 (runtime error)

Error categories

Category Wire value Exit code Retryable Meaning
Config config 2 no Invalid configuration at startup
Invalid invalid 1 no Bad input or schema violation
Not_found not_found 1 no Named resource does not exist
Denied denied 1 no Access rejected (OITL, permissions)
Internal internal 1 no Compiler-internal or assertion failure
Unavailable unavailable 1 yes Transient — connection dead, server down

Only Unavailable errors are retryable. A retry policy can dispatch on Error.retryable or match the category directly.

Error codes

Tool execution

Produced by the tool dispatch loop when executing a tool.

Code Category Meaning
process_error Unavailable Subprocess failure — fork, exec, or non-zero exit
validation_error Invalid Input or output failed schema validation
parse_error Invalid JSON parse failure (in ParseJson morphism, or pipeline definition parse)

Tool dispatch

Produced by dispatch_tool in tool_dispatch.ml when routing an incoming tool request.

Code Category Meaning
tool_not_lowerable Invalid The backing process is filter, merge, barrier, or project — not total, cannot be used as a tool
tool_nesting_error Invalid A Tool or Lower impl was reached inside a tool dispatch context — tool nesting is not supported
internal_error Internal A compiler-internal morphism (Tagger, Log) reached tool dispatch — these are never directly callable
unknown_tool Not_found Tool name not found in the agent's plumbing bindings or any connected MCP server
tool_denied Denied Operator-in-the-loop (OITL) rejection — a human operator declined the tool call

Builtins

Produced by execute_builtin for the built-in document and system tools.

Code Category Meaning
docstore_not_found Not_found Document path does not exist
docstore_denied Denied Path traversal or permission denied
docstore_invalid Invalid Invalid glob/regex pattern
docstore_io_error Unavailable Filesystem I/O error

Pipeline calls

Produced when a tool invocation runs a pipeline subprocess (including Sys.call).

Code Category Meaning
pipeline_error Unavailable Pipeline subprocess exited with an error or produced no output
load_error Config Pipeline spec load failure — parse or validation error in the .plumb file

MCP client

Produced by lib/mcp_client/mcp_client.ml when communicating with an MCP server.

Code Category Meaning
mcp_disconnected Unavailable MCP server connection is dead — previously marked as failed
mcp_call_error Unavailable MCP tool call returned an error response from the server
mcp_transport_error Unavailable Transport-level failure — connection killed or I/O error

Configuration

Produced at startup during spec loading and validation.

Code Category Meaning
config_error Config Static configuration or spec validation failure

Agent loop

Produced by the agent conversation loop.

Code Category Meaning
output_extraction_failed Internal No JSON output found in LLM response after max retries
output_validation_failed Invalid LLM output did not match schema after max retries
max_tokens Unavailable Response exceeded max tokens

API clients

Produced by lib/client/anthropic.ml, lib/client/openai.ml, and lib/client/claude_code.ml.

Code Category Meaning
api_error Unavailable API request failed (HTTP error, unknown stop reason, missing fields)
claude_code_error Unavailable Claude Code subprocess failure (send/read error, process died)

Tool response envelope

Tool errors are returned to the agent as a tool response with is_error: true. The content field contains the serialised error JSON:

{"id": "toolu_abc123", "content": "{\"error\":\"unknown_tool\",\"category\":\"not_found\",\"detail\":\"unknown tool: foo\"}", "is_error": true}

The agent binary feeds this to the LLM as a tool_result message. The LLM observes the is_error flag and the error content and decides whether to retry or fall back.

See tools.md for the full tool dispatch protocol. See executor.md for dispatch and execution internals.