Three categories the model understands
- retryable — transient (timeout, 5xx upstream). Host may retry once.
- user_action_required — auth expired, missing scope, quota reached. Host surfaces to user.
- permanent — validation failure, not found, forbidden. Don't retry.
Error shape
Return { ok: false, code, message, hint? }. Keep message human; keep code machine-readable; use hint to tell the model what to try next (e.g. "ask the user to reconnect Linear").
Never leak stack traces
Audit every error
Errors are signal. Log code, tool, user_id, and input hash. A spike in user_action_required on one tool usually means a scope or token issue you can fix once for everyone.
FAQ
- Should I throw or return errors?
- Return structured errors. Throwing crashes the transport; returning gives the host a chance to surface or retry.
- What does the model need in an error?
- A short message it can show, a machine-readable code (retryable, user_action_required, permanent), and optionally a hint at what to try next.
- Retry on the server or the host?
- Transient infra errors: retry server-side with jitter, cap at 2. Auth and quota errors: never retry server-side — return and let the host handle.