Error Contract

August 5, 2026 · View on GitHub

labby_runtime::error::ToolError is the canonical surface-neutral error type. CLI, MCP, and HTTP must preserve its stable kind vocabulary and human-readable message rather than inventing surface-specific error shapes.

Agent Error Envelope

Every agent-facing error object carries the versioned recovery contract in addition to kind and message. The required fields are:

  • contract_version — currently 1; additive changes keep the version.
  • kind — stable machine-readable tag (this document's vocabulary).
  • message — human/model-readable diagnosis, useful on its own.
  • origin — where the failure arose: validation, policy, budget, discovery, tool_execution, upstream_transport, bridge, code_mode, or runtime.
  • recovery — advice object with required action (revise_and_retry, retry_later, reauthenticate, confirm, rediscover, reduce_work, start_dependency, inspect_and_escalate, do_not_retry), same_arguments (safe, conditional, discouraged, never), and guidance (free text), plus optional retry_after_ms.
  • side_effectsnone_expected, possible, or unknown.

Optional context fields (service, action, tool, upstream, command, prompt, resource, cause, original_kind, safety, evidence) are additive.

The full normative contract, surface rules, and published JSON Schemas are owned by ../contracts/agent-error-contract.md and ../contracts/code-mode-tool-errors.md (schemas: docs/contracts/schemas/agent-error.schema.json, docs/contracts/schemas/code-mode-call-error.schema.json). The shared Rust implementation is crates/labby-runtime/src/agent_error.rs; drift between the emitted values and the published schemas is locked by crates/labby-runtime/tests/agent_error_schema.rs and crates/labby-codemode/tests/code_mode_error_schema.rs.

Unknown-kind catch-all

A kind outside the classification tables is still a valid envelope: it classifies as origin: runtime, side_effects: unknown, and recovery.action: inspect_and_escalate with same_arguments: discouraged. Consumers must treat unknown kinds as opaque and fall back on recovery and message rather than failing.

Core Dispatcher Kinds

  • unknown_action: action is not registered; includes valid choices and an optional hint.
  • missing_param: required parameter is absent.
  • invalid_param: parameter type or value is invalid.
  • unknown_instance: named instance is not configured.
  • ambiguous_tool: an upstream tool name requires qualification.
  • confirmation_required: a destructive action lacks explicit confirmation.
  • conflict: the requested identifier already exists.
  • forbidden: caller lacks required scopes; includes required_scopes.

SDK and subsystem errors use ToolError::Sdk to promote their stable kind to the same top-level envelope:

{ "kind": "auth_failed", "message": "..." }

Common Subsystem Kinds

Supported code may emit additional stable kinds, including:

  • auth/OAuth: auth_failed, auth_required, permission_denied, oauth_needs_reauth, oauth_state_invalid, oauth_resource_mismatch, oauth_issuer_mismatch, oauth_unsupported_method;
  • routing/upstreams: not_found, unknown_upstream, unknown_tool, upstream_error, bad_gateway, network_error, service_unavailable, not_connected, connection_error, timeout, cancelled (the upstream reported the proxied call was cancelled; not automatically retryable), unexpected_response;
  • relay/bridge: bridge_transport_error (the stdio bridge could not reach the canonical daemon), relay_invalid_target, relay_forwarder_init_failed;
  • validation/security: validation_failed, invalid_hint, ssrf_blocked, path_traversal, symlink_rejected, content_too_large, invalid_encoding;
  • payload limits: response_too_large — gateway cap on upstream MCP response bytes (distinct from content_too_large's request/content limits);
  • Code Mode: timeout (wall-clock expiry — the historical code_mode_timeout/code_mode_fuel_exhausted kinds are retired and must not be reintroduced), invalid_code_mode_id, call_budget_exceeded, snippet_budget_exceeded, snippet_resolve_limit, snippet_not_found, artifact_too_large, result_too_large;
  • providers: provider_unavailable, provider_timeout, invalid_provider_output;
  • concurrency/state: rate_limited, queue_saturated, budget_exceeded, quota_exceeded, restart_required, stale_suggestion, merge_write_conflict, workspace_not_configured;
  • internal failures: internal_error, server_error, decode_error.

The emitting subsystem owns the precise remediation text. New stable kinds require API mapping tests and documentation.

oauth_needs_reauth vs auth_failed — two classifier vocabularies

Two classifiers look at raw upstream transport failures, on purpose:

  • classify_upstream_error (crates/labby-gateway/src/upstream/pool/helpers.rs) feeds the circuit breaker, backoff, and operator logs with auth_failed / auth_required / timeout / dns_error / connection_refused / connection_error.
  • upstream_failure_kind (crates/labby/src/mcp/call_tool_upstream.rs) runs on the live MCP call path and emits the model-facing kind. oauth_needs_reauth is a deliberate refinement of auth_failed there: an authorization-shaped transport failure (401 as a standalone token, unauthorized, invalid_token, OAuth wording, …) becomes an envelope that carries recovery.action: reauthenticate and points the agent at gateway.oauth.start for that upstream instead of a generic auth failure.

Keep the two auth heuristics aligned when either changes; both sites carry cross-referencing comments.

Circuit Breaker And Completed Tool Errors

A completed MCP result with isError: true proves the upstream protocol connection worked. Such results are enriched for the model (tool_execution origin) but never count toward the upstream circuit breaker or health state. Only the absence of a completed result — a transport failure — records a breaker failure.

HTTP Mapping

ApiError is the local axum wrapper around ToolError. Broad mapping rules:

  • authentication failure: 401;
  • forbidden scope/action: 403;
  • unknown resource: 404;
  • conflict/restart/stale state: 409;
  • invalid input, confirmation, SSRF, or path validation: 422;
  • payload limits: 413;
  • rate/queue limits: 429;
  • upstream gateway failure: 502;
  • service unavailable: 503;
  • timeouts: 504;
  • unknown/internal kind: 500.

MCP and CLI retain the same serialized error envelope even when HTTP assigns a status code.

Logging And Redaction

Caller-fixable errors log at WARN. Internal failures requiring operator action log at ERROR. Error messages must not include bearer tokens, OAuth codes, provider credentials, full secret environment values, or unredacted sensitive paths.

Upstream-controlled error text is sanitized (control/bidi characters stripped, prompt-injection markers removed, secret-like segments redacted, length bounded) before it enters any envelope, log, or the Code Mode sandbox; capped text ends with a …[truncated] marker. See labby_runtime::agent_error::sanitize_error_text.

Removed Error Vocabularies

Error kinds used only by ACP, Registry installers, Marketplace artifacts, Fleet/node transport, Deploy-product, or Stash must be removed with those features, including stale status-mapping tests. Their historical definitions are available in ../references/retired-labby/current-docs/dev/ERRORS.md.