Skip to content

Handle errors

A refusal arrives as a normal HTTP 200 whose tool result carries isError: true. The server answered; the answer was a refusal. Only a protocol-level problem uses the JSON-RPC error channel.

This matters for retries: a refusal re-earns itself. Retrying a validation fault gets you the same fault having spent a round trip to prove it. Repair the call instead — field names what to fix and details carries the allowed values when the server can enumerate them.

CategoryExampleWhat to do
Authunauthenticated, forbiddenRefresh or fix the credential. Never retry in a loop — that attacks your own auth service
ValidationvalidationThe request is wrong. Fix the call site; retrying cannot help
Fault, retryablerate_limited, dependency_unavailable, internalThe SDK already retried. Reaching you means exhaustion
Transportconnection refused, timeoutThe platform did not answer. Not a refusal
switch {
case errors.Is(err, vectadyne.ErrUnauthenticated):
case vectadyne.IsFaultCode(err, wire.FaultCodeValidation):
case errors.Is(err, vectadyne.ErrFault):
case err != nil: // transport
}

“The platform said no” is not “the platform did not answer.”

The SDK never synthesizes a verdict. If the control plane is unreachable you get a typed transport error, not an invented allow.

That is a deliberate cost, and worth stating plainly rather than dressing up:

  • You are coupled to the outage. There is no local cache and no last-known-good context.
  • You wait first. Up to the total timeout, including retries and backoff, before finding out.
  • The decision is yours, and has to be written down. Stop, queue, degrade a feature, or proceed and record that you did — the SDK will not choose.

Why not a fallback verdict? Because a synthesized allow is an ungoverned decision wearing the platform’s name, and it would be indistinguishable in your logs from a real one. Moving “proceed on outage” into your application code does not make it governed — but it makes it visible and reviewable, which is the honest version.

The SDK retries on transport outcome only. It never reads decision, degraded or any governed payload to make that call.

OutcomeRetried?
transport error before a responseyes
429 rate limitedyes, after Retry-After
503, 500yes, with backoff
400 validationno
401, 403no
404, 409, 410no
413 payload too largeno — shrink the batch
any verdict, any decisionno — it is a successful answer

A block producing two requests is a bug. Assert the request count.

RateLimitedError carries retry_after typed, because it is the one field that says what to do next. The SDK waits the server’s figure rather than applying its own backoff — the server knows when the bucket refills.

One asymmetry to know about: a 429 from the token endpoint uses the RFC 6749 OAuth error shape, not our fault envelope, and puts its correlation id only in the X-Request-Id header. The SDKs normalise both into the same typed error so you do not parse two shapes.

DefaultWhy both
Per-attempt30sWithout it, one hung connection eats the whole budget
Total60sWithout it, N retries multiply into an unbounded wait

Your own context or cancellation deadline always wins over both. Backoff sleeps consume the total budget — a client that sleeps outside its own deadline has no deadline.

Do not confuse either with latency_budget_ms, which is a server-side budget on context.assemble that degrades rather than failing.

Every response and every fault carries one. It is what makes a bug report actionable, and it is safe to log — unlike prompt_block or memory content, which no SDK logs at any level.