Skip to content
Book a demoSign in
All docs
Agent SDK

Errors and recovery

The seven typed errors the SDK raises, what each means operationally, and the right recovery for each.

Updated May 29, 2026

The SDK raises seven typed errors. Six map to marketing-spec refusals. The seventh, NotLiveYetError, is the v0.1.0 surface: every HttpTransport method raises it until the auth server is live. All seven carry enough state on the error object that a well-written agent can recover without a second round-trip. The auth layer enforces the refusals, not the prompt layer.

The catalog

ErrorHTTPRetryableSurfaces
NotLiveYetErrorn/anoevery HttpTransport call in v0.1.0
AuthError401noevery call
ScopeError403noevery call
RateLimitError429yes, after retryAfterSecondsevery call
CommitNotAllowedError403noonly if an agent attempts a clinician-only commit path
ClinicianRejectedDraftError403nocommit calls on a draft a clinician already rejected
InstrumentFrozenError409noany mutation of PHQ-9 or GAD-7

There is no generic NyraError to catch everything. The base class exists for instanceof, but the SDK never raises it directly. Catch the typed subclass, or rethrow.

NotLiveYetError is the one error you can stop catching once the auth server ships. The other six are permanent surface.

Anatomy

Every typed error carries the same base fields:

type NyraErrorBase = {
  name: string;          // matches the class name verbatim
  message: string;       // one-line, human-readable
  requestId: string;     // server-issued; quote it when filing support
  auditId?: string;      // present when the refusal landed in the audit log
};

Subclasses extend with the state the recovery path needs:

type NotLiveYetError     = NyraErrorBase & { method: string; sdkVersion: string };
type RateLimitError      = NyraErrorBase & { bucket: "evidence_map" | "simulation" | "commit"; retryAfterSeconds: number };
type ScopeError          = NyraErrorBase & { missingScope: string };
type CommitNotAllowedError = NyraErrorBase & { attemptedAction: string };
type ClinicianRejectedDraftError = NyraErrorBase & { rejectedDraftId: string; rejectedAt: string };
type InstrumentFrozenError = NyraErrorBase & { instrument: "phq9" | "gad7"; instrumentVersion: string };

Python mirrors the same field set on the equivalent classes, in snake_case. A golden fixture in both test suites pins the field set so cross-language drift fails in CI.

Recovery, by error

ErrorWhat to do
NotLiveYetErrorwire the shape; defer the call until the auth server ships; cite sdkVersion if you log it
AuthErrorrefresh the token; if refresh fails, re-authenticate end-to-end
ScopeErrorsurface missingScope to the operator; do not silently retry
RateLimitErrorsleep retryAfterSeconds against the bucket, then retry the exact call
CommitNotAllowedErrornever; the agent must hand the draft to a clinician
ClinicianRejectedDraftErrordrop the draft; do not re-propose verbatim; cite rejectedDraftId in the next proposal
InstrumentFrozenErrorabandon the mutation; PHQ-9 and GAD-7 are version-pinned and immutable from the SDK

The two errors an agent will see most often are RateLimitError and CommitNotAllowedError. Both have runnable examples below.

Backing off a rate limit

import { RateLimitError } from "@humyn/nyra";

async function runWithBackoff<T>(call: () => Promise<T>, maxAttempts = 3): Promise<T> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await call();
    } catch (err) {
      if (err instanceof RateLimitError && attempt < maxAttempts) {
        await new Promise((r) => setTimeout(r, err.retryAfterSeconds * 1000));
        continue;
      }
      throw err;
    }
  }
  throw new Error("unreachable");
}
from humyn_nyra import RateLimitError

async def run_with_backoff(call, max_attempts: int = 3):
    for attempt in range(1, max_attempts + 1):
        try:
            return await call()
        except RateLimitError as err:
            if attempt == max_attempts:
                raise
            await asyncio.sleep(err.retry_after_seconds)

The backoff is per-bucket. If your agent reads the evidence map and runs simulations concurrently, hold one limiter per bucket so a simulation backoff does not stall an unrelated read.

Catching a commit refusal

import { CommitNotAllowedError } from "@humyn/nyra";

try {
  await client.createDraft({ patientId, body });
} catch (err) {
  if (err instanceof CommitNotAllowedError) {
    // Hand the draft state to the clinician's surface. Do not retry from the agent.
    return { status: "needs_clinician", attemptedAction: err.attemptedAction };
  }
  throw err;
}
from humyn_nyra import CommitNotAllowedError

try:
    await client.create_draft(patient_id=patient_id, body=body)
except CommitNotAllowedError as err:
    return {"status": "needs_clinician", "attempted_action": err.attempted_action}

The agent's correct response to CommitNotAllowedError is structural, not behavioral. Hand the artifact off. A retry loop here is a bug.

What never throws

A few situations look like errors but are not. The SDK returns an empty or default-shaped value instead, so the agent does not need to branch on exceptions for routine state.

  • An evidence map with no signal yet returns the full shape with empty arrays and a confidence.overall of 0. It does not throw.
  • A sandbox with no drafts returns [] from listDrafts. It does not throw.
  • A reflection with no transcript yet (the patient is mid-write) returns null from the read. It does not throw.
  • A simulation that the surrogate cannot bound returns a confidence band of { low: 0, high: 1 } and confidence.reason: "unbounded". It does not throw.

Anything else that fails raises one of the six typed errors above. There is no untyped Error path inside the SDK.

Next: Webhooks and realtime events.