Skip to content

Tool provider — conformance#

Error taxonomy#

Distinct from model/conformance.md#error-taxonomy's ModelError — a tool's failure modes are a different domain (no rate_limited/context_length_exceeded, which are model-vendor concepts) — but follows the same shape and the same non-negotiable principle: a plugin MUST classify every failure, MUST NOT collapse them into one generic error, for the same reason the model-provider protocol cites (undifferentiated errors are undebuggable after the fact).

ToolError {
  category    ToolErrorCategory  // MUST
  message     string             // MUST — human-readable
  retryable   bool               // MUST
  details     JSON               // MAY — provider-specific structured detail
}

ToolErrorCategory = enum {
  invalid_arguments      // input failed input_schema validation
  not_found              // target of the operation doesn't exist (path, URL, symbol, ...)
  permission_denied      // OS/policy denied the underlying operation
  execution_failed       // the operation ran but failed on its own terms (non-zero exit,
                          // compiler error, HTTP 4xx/5xx) — not a plugin bug
  timeout                // exceeded a plugin- or kernel-enforced deadline
  concurrency_conflict   // provider detected a conflicting concurrent call it could not
                          // serialize itself (see data-types.md#concurrencyspec) — signals
                          // the kernel to retry serialized
  cancelled              // stream was cancelled per README.md#transport--lifecycle — not
                          // "an error" in the failure sense; MUST be distinguished from
                          // other categories so the kernel doesn't surface it to the model
                          // as a tool failure when the whole turn is being aborted anyway
  process_crashed        // the plugin subprocess itself died mid-Invoke (transport error,
                          // not a graceful error event the plugin chose to emit) — MUST be
                          // kernel-synthesized, never something a plugin emits about itself
  unknown                // anything else; MUST include the raw underlying error in `details`
}

process_crashed exists because a tool subprocess dying mid-call (a segfault, an OOM kill, a panic that takes the process down) is a distinct, detectable failure mode from any graceful error event a plugin chooses to emit about its own operation — collapsing it into unknown would hide from the kernel (and from policy/circuit-breaker logic) that the plugin itself misbehaved, as opposed to the operation it was asked to perform. Because the plugin process is, by definition, no longer running to emit this event itself, the kernel MUST synthesize process_crashed from the transport-level failure (a broken gRPC connection, a hashicorp/go-plugin health-check failure) — a plugin author never constructs one directly.

Kernel's expected reaction per category:

Category Reaction
invalid_arguments MUST NOT retry as-is; feed back to the model as a tool_result error so it can correct arguments
not_found Surface to the model; no retry
permission_denied Surface to the model; MUST NOT silently retry with escalated privilege
execution_failed Ordinary tool_result content, not a protocol-level failure — this is the common case for e.g. a failing test run
timeout Cancel per README.md; retryable at kernel's discretion
concurrency_conflict Retry serialized against the same key (see data-types.md#concurrencyspec)
cancelled Not surfaced as a model-visible failure unless the turn itself is being aborted
process_crashed Surfaced to the model as an ordinary tool_result error (same observe-and-adapt principle as a denial, agent-loop/plan-apply-gate.md); SHOULD trip the same circuit breaker as repeated denials if it recurs
unknown Non-retryable by default; log details for debugging

On the wire, process_crashed maps to codes.Unavailable — the same code used for a transient, retriable unavailability elsewhere in the system, since a crashed subprocess is exactly that from the kernel's point of view: the service became unavailable, not that the request itself was invalid.

The idempotent / retry interaction#

ToolSchema.idempotent (per protocol.md#getschema) is the gate on top of the category-reaction table above for exactly one row: a retryable ToolError returned for a TOOL_KIND_RESOURCE operation. The kernel MAY auto-retry such a failure — without surfacing it to the model as a failed call first — only when that operation's ToolSchema.idempotent is true; when it's false (or unset — proto3's zero value for bool is false, so an operation MUST explicitly declare idempotent: true to opt in, never rely on an implicit default), the kernel MUST treat the failure as terminal for this attempt and surface it, exactly as the category-reaction table already prescribes. TOOL_KIND_DATA_SOURCE operations are exempt from this gate entirely — they're implicitly safe to retry regardless of idempotent, since by definition they cannot mutate anything. TOOL_KIND_INTERACTIVE calls are never auto-retried (per protocol.md#kind-interactive, a human's answer isn't something a kernel can safely redo unprompted).

This interacts with, but is distinct from, concurrency_conflict's existing "retry serialized against the same key" reaction: that retry is about serialization ordering, not about whether re-running the operation is safe at all, so it applies independent of idempotent.

Required vs. optional support — summary matrix#

Capability Level Notes
GetSchema / Configure / Invoke RPCs MUST the whole protocol surface
Describe RPC MUST protocol.md#describe; needed for dev_overrides plugin identity per configuration/lock-file.md
Streaming RPC shape for Invoke MUST see README.md / protocol.md#invoke — applies even to non-streaming operations
ToolCall.call_context MUST be set by the kernel, every Invoke call protocol.md#invoke; working_directory is what makes process-backed operations usable at all
input_schema/output_schema in the common JSON-Schema subset MUST model/data-types.md#tool-schema
kind (resource / data_source / interactive) MUST, per operation drives the plan/apply gate; protocol.md#kind-interactive
risk classification MUST, per operation see data-types.md#riskclass; read_only for data_source and interactive alike
ConcurrencySpec.safe MUST, per operation except interactive absent/unset MUST be treated as false; MUST NOT be declared for interactive
ConcurrencySpec.key_fields MAY, per operation only meaningful under safe: true
default_timeout SHOULD, per operation protocol.md#getschema; absent means the kernel's global default applies
idempotent MUST, per operation protocol.md#getschema; gates kernel auto-retry, see above
terminates_turn MAY, per operation data-types.md#terminates_turn; resource-only; opts into agent-loop/turn-algorithm.md#done-detection's explicit terminal-tool path, absent/false otherwise
supported_hook_points MAY protocol.md#getschema; empty means this provider subscribes no hook{} blocks
exit_status event MUST for process-backed (exec-family) operations; MUST NOT otherwise
output_chunk / progress / partial_result events MAY only for operations with streaming: true
Structured ToolError taxonomy, including process_crashed MUST
Strict output_schema enforcement MUST protocol.md#invoke
Best-effort partial-mutation report on cancellation MUST, for resource operations see protocol.md#invoke
Render MAY generic fallback exists; RenderRequest.schema_version per ../frontend/render-tree.md#schema-versioning-for-opaque-emit-payloads
Preview MAY protocol.md#preview; kernel MUST fall back to raw arguments when absent; MUST NOT mutate anything when implemented

Open questions#

  • OS-level sandboxing (bubblewrap/Seatbelt/Landlock — increasingly common in production coding harnesses, not just research tools) isn't modeled by this protocol at all; it's presumably a Configure-time concern per provider (see protocol.md#configure) or a kernel-level policy applied uniformly regardless of provider, but which is genuinely unresolved.
  • Whether key_fields (see data-types.md#concurrencyspec) needs to support derived/composite keys beyond "the literal value of named input fields" — e.g. a filesystem provider wanting to serialize on a resolved absolute path rather than the raw (possibly relative, possibly symlinked) path argument as given.