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.