Appearance
07 — Error Model
NOTE
Status: Draft (normative). Errors are part of the protocol: agents plan around them, adapters translate them, tests assert them. Vague errors are spec bugs.
TIP
Errors here are not failures to apologize for — they are how the runtime talks back to the agent. CAPABILITY_NOT_AVAILABLE with a reason teaches the model the enabling step; CONFIRMATION_REQUIRED is a normal protocol turn; every code carries a machine-actionable retry hint. Implementers: the per-code table is the contract. Adapter authors: read Principles and the mapping table.
Principles
- Results, not exceptions, at the boundary.
invokereturns a discriminated union (03 §invocation);status: "error"carries a serializable payload. JS exceptions are reserved for programmer misuse (structural definition defects, use-after-dispose). - Typed and closed. Agent-facing codes come from one closed enum. Adding a code is a spec change (
INVOCATION_CONFLICTwas added by 18-spec-corrections-rfc.md). The enum, per-code production phase, retry category, and cacheability are cross-validated against the implementation from one machine-readable source:spec/error-matrix.json(directive §4.4). - Two audiences, two channels. Every error has an agent-safe projection (code, message, retry, details) and an operator channel (cause, stack, internal context → audit/logs). The two never mix.
- Actionable. Each code defines retry semantics an agent loop can act on mechanically.
Shapes
ts
export type AgentCapabilityErrorCode =
| "CAPABILITY_NOT_FOUND"
| "CAPABILITY_NOT_AVAILABLE"
| "AMBIGUOUS_INSTANCE"
| "COMPONENT_UNMOUNTED"
| "STALE_CAPABILITY"
| "INVOCATION_CONFLICT"
| "INVALID_INPUT"
| "NOT_AUTHENTICATED"
| "NOT_AUTHORIZED"
| "PRECONDITION_FAILED"
| "CONFIRMATION_REQUIRED"
| "CONFIRMATION_INVALID"
| "RATE_LIMITED"
| "TIMEOUT"
| "CANCELLED"
| "EXECUTION_FAILED";
export type AgentErrorRetry =
| "no" // don't retry this request as-is
| "yes" // safe to retry unchanged (idempotency window applies)
| "after-refresh" // re-snapshot, re-resolve target, then retry
| "after-delay" // retry after details.retryAfterMs
| "with-confirmation" // retry with confirmationId once approved
| "with-changes"; // fix input per details, then retry
export interface AgentCapabilityErrorPayload {
code: AgentCapabilityErrorCode;
/** Agent-safe, imperative, ≤ 300 chars. */
message: string;
retry: AgentErrorRetry;
/** Code-specific, agent-safe, JsonValue only. */
details?: Record<string, JsonValue>;
}
/** Thrown form used inside policies/handlers; serialized at the boundary. */
export class AgentSurfaceError extends Error {
constructor(payload: AgentCapabilityErrorPayload, opts?: { cause?: unknown });
readonly payload: AgentCapabilityErrorPayload;
}
export function isAgentSurfaceError(e: unknown): e is AgentSurfaceError;
/** Structural defects at registration time. Always thrown, never agent-facing. */
export class AgentSurfaceDefinitionError extends Error {
readonly code:
| "INVALID_ID" | "INVALID_DEFINITION" | "UNSUPPORTED_SCHEMA"
| "PLANE_VIOLATION" | "DUPLICATE_CAPABILITY" | "LIMIT_EXCEEDED";
}Code-by-code specification
For each: meaning · produced when · retry · agent-visible details · log-only info · adapter duty.
CAPABILITY_NOT_FOUND
- Meaning: the id does not exist in this consumer's surface — never registered, long gone, or hidden by authority (indistinguishable on purpose, requirement 12 in 06).
- When: resolution finds nothing and no tombstone matches.
- Retry:
after-refresh. Details: none (no existence leaks). Log-only: whether a hidden registration actually matched. Adapter: refresh snapshot before the model's next step.
CAPABILITY_NOT_AVAILABLE
- Meaning: exists and is visible, but currently disabled (state,
enabled: false, or a policydisable). - When: phase 3, or a discovery re-check downgrading to disabled.
- Retry:
after-refresh(typically: perform the enabling step first). Details:{ reason }— same string as the descriptor'sunavailableReason. Adapter: pass the reason through; it is planning fuel.
AMBIGUOUS_INSTANCE
- Meaning: >1 live instance (or procedure reference) matches and none was specified.
- Retry:
with-changes. Details:{ instances: [{ instanceId, registrationId, description? }] }(for procedures:contextinstead of description). Adapter: re-issue with an explicit target.
COMPONENT_UNMOUNTED
- Meaning: the target registration existed recently and is gone (tombstone hit), or unmounted mid-execution.
- Retry:
after-refresh. Details:{ phase: "resolve" | "mid-flight" }. Log-only: tombstone timestamps, late-settlement info. Adapter: refresh; ifmid-flighton a non-idempotent action, surface uncertainty to the model ("the view changed while acting; verify state before repeating").
STALE_CAPABILITY
- Meaning: the invocation references a superseded snapshot:
registrationIdmismatch with a live replacement,surfaceIdfrom another page load, or version mismatch on adestructive/external-side-effectcall. - Retry:
after-refresh. Details:{ reason: "registration-replaced" | "surface-reloaded" | "surface-version-mismatch", liveRegistrationId? }. Adapter: MUST refresh and re-resolve; MUST NOT strip the staleness tokens to force the call through.
INVOCATION_CONFLICT
- Meaning: the
invocationIdwas already used by this consumer for a different request (different capability, target, input, or evidence) within the idempotency window. Reuse never silently returns another request's result (D22). - When: phase 1, comparing the request fingerprint against the stored in-flight/terminal record for
(consumerKey, invocationId). - Retry:
with-changes(use a freshinvocationIdif the new request is intentional). Details:{ reason: "id-reused-with-different-request" }— never the prior request's capability, input, or fingerprint material. Log-only: both fingerprints. Adapter: treat as an adapter/provider id-collision bug signal; mint a namespaced id and retry the new request. Never cached as terminal.
INVALID_INPUT
- Meaning: schema parse failed, or a locked bound field was supplied (05 §binding).
- Retry:
with-changes. Details:{ issues: [{ path, message }], lockedFields? }— issue messages come from the schema layer and MUST be safe (field-level, no values echoed for fields marked sensitive via schemaformat/meta). Log-only: offending raw input (dev only). Adapter: give issues to the model verbatim.
NOT_AUTHENTICATED
- Meaning: no authenticated context where one is required.
- Retry:
no(until the user signs in — never the agent's job; credential entry is out of scope for agents). Details: none. Adapter: tell the model to inform the user.
NOT_AUTHORIZED
- Meaning: authenticated but not permitted (client policy or mapped server authz error).
- Retry:
no. Details:{ origin: "client" | "server" }only — no permission names, no policy internals. Log-only: failing policy name, required permission. Adapter: do not loop; report.
PRECONDITION_FAILED
- Meaning: input is schema-valid but state-invalid (
precondition), or a procedure binding failed to evaluate. - Retry:
with-changes(orafter-refreshwhendetails.reason: "binding-failed"). Details: author-provided{ message, …details }; binding failures add{ reason: "binding-failed" }. Adapter: pass through.
CONFIRMATION_REQUIRED
- Meaning: a user approval gate is active; not a failure (06 §confirmation).
- Retry:
with-confirmation. Details:{ confirmationId, summary, expiresAt, effect, origin: "client" | "server" }. Adapter: inwaitmode handle internally; intwo-phasemode relay to the model with instructions to retry withconfirmationIdafter approval. Never cached as terminal in the dedupe window.
CONFIRMATION_INVALID
- Meaning: evidence rejected.
- Retry:
reason: "expired"→with-confirmation(a fresh cycle);"denied" | "consumed" | "mismatch"→no. Details:{ reason }. Log-only: mismatch diff. Adapter: MUST NOT auto-retry a denial; a denial is a user decision the model must respect.
RATE_LIMITED
- Meaning: client-side advisory limit or full action queue.
- Retry:
after-delay. Details:{ reason: "rate" | "queue-full", retryAfterMs }. Never cached as terminal. Adapter: honor the delay; do not tight-loop.
TIMEOUT
- Meaning: the handler/procedure exceeded its budget; the signal was aborted; side effects may or may not have occurred.
- Retry:
nofor non-idempotent capabilities (verify state first — say so in the message),yesfor idempotent ones. Details:{ timeoutMs, idempotent }. Log-only: whether a late settlement arrived. Adapter: for non-idempotent, prompt a verification observation before any repeat. Cached as terminal (a retry needs a new invocationId — deliberate friction).
CANCELLED
- Meaning: the host/adapter aborted (
InvokeOptions.signal) or the registry was disposed. - Retry:
yes(new invocationId) if still relevant. Details: none.
EXECUTION_FAILED
- Meaning: handler threw, output failed validation/serialization/size, or a transport/server error not mappable to a more precise code.
- Retry:
noby default;details.transient: trueMAY mark network-ish failures asafter-delay. Details:{ reason?: "handler-error" | "output-invalid" | "output-too-large" | "transport" , transient? }plus a sanitized message. Log-only: full cause chain, stack, raw server error. Sanitization is normative: the agent-facing message MUST NOT contain stack frames, SQL/queries, internal URLs, header values, or environment details; implementations MUST build it from a allowlisted template, never fromerror.messagepass-through of unknown causes.
Adapter mapping guidance
| Adapter | ok | error |
|---|---|---|
| Embedded toolset | tool result = output (JSON) | tool result = the payload, plus isError: true where the provider supports it; retry hints kept in-band |
| WebMCP / MCP | content: JSON text of output | MCP tool error content with the payload JSON; MUST NOT use protocol-level errors for capability errors (those are for transport faults) |
| Testing | asserted directly | asserted directly (expectError(result, "STALE_CAPABILITY")) |
Adapters MUST preserve code, retry, and details losslessly; they MAY prepend a one-line natural-language rendering for weaker models.
Registration-time errors
AgentSurfaceDefinitionError cases (always thrown, environment-independent — these are code bugs):
| Code | Trigger |
|---|---|
INVALID_ID | grammar violation in type/capability name/instanceId |
INVALID_DEFINITION | missing/empty description, unknown fields, non-JsonValue meta |
UNSUPPORTED_SCHEMA | JSON Schema outside the D19 subset, too deep, too large |
PLANE_VIOLATION | view action declaring a server effect; procedure binding without executor path |
DUPLICATE_CAPABILITY | two capabilities with the same name in one definition |
LIMIT_EXCEEDED | description/meta size caps |
Runtime rejection cases (dead handle + event, never thrown): duplicate (type, instanceId) under "reject", guard rejection — see 03 §registry.