Deuz SDK 2.4 リリース — バージョン管理されたメモリとコンテキストコンパイラ、ゴールスーパーバイザーと永続スケジューラ、ローカル Docker ランナー、永続チャットストリーム。 2.4 の新機能
Deuz SDK
Core

Error Handling

The typed DeuzError taxonomy, the runtime and module errors, the never-throw streaming contract, retry semantics, the timeout layers, and the secret-redaction guarantee.

ドキュメント本文は英語です。ナビゲーション・検索・UI は選択した言語に従います。

Provider, transport, and orchestration failures use the typed DeuzError contract rather than exposing a raw Response or a provider payload. Each wire adapter normalizes its provider's error envelope into the same set of classes, so retry, routing and fallback logic branches on err.code regardless of which provider produced the failure. The provider taxonomy is exported from the package root. The errors of the agent runtime, its stores and the modules are exported from their own subpaths: see Runtime and module errors.

import { DeuzError, isDeuzError, RateLimitError, AuthenticationError } from '@deuz-sdk/core';

Prefer `isDeuzError` over `instanceof` at package boundaries

instanceof breaks across duplicate package copies and across realms (a worker, a bundled dependency that pinned its own version). isDeuzError(value) checks a Symbol.for-branded field instead, so it stays reliable. Use instanceof for the specific subclasses when you need their fields, and isDeuzError for the "is this one of ours?" question.

The taxonomy

DeuzError is the abstract base. Every error carries a stable string code; HTTP-shaped errors additionally extend APICallError and carry statusCode plus an isRetryable verdict.

APICallError and its subclasses

APICallError represents a non-2xx response from a provider (or an in-stream error envelope). Adapters map each wire's error type onto one of these.

ClasscodeStatusRetried by the SDKWhat you should do
APICallErrorapi_call_errormappedwhen >= 500Log provider / statusCode / requestId and treat as transient. Base class — also used directly for generic 5xx.
NetworkErrornetwork_error0✅DNS/TLS/transport died before an HTTP response. Nothing to fix in your request; check egress, proxies, and the base URL. Since 2.3 a batch lifecycle call throws it too, also when its answer or a results file breaks off mid-read, and wait polls again after it.
RateLimitErrorrate_limit429✅Read retryAfterMs and back off at your layer too — the SDK only retries within one call. Long term: lower concurrency or shard keys.
OverloadedErroroverloaded529✅Provider-side capacity, not you. This is the classic case for fallbackModels / withFallback to a second provider.
AuthenticationErrorauthentication401 (also 403)❌A bad, missing, expired or under-permissioned key. Never retry. Check the G1 resolution order — a deps.keyProvider outranks factory config, which outranks createClient's apiKeys.
InvalidRequestErrorinvalid_request400 / 422 / 413❌Your request is malformed — a bad message shape, an oversized payload, or a mutually exclusive option pair (prompt and messages). Fix the call; a retry sends the same bad bytes.
ModelNotFoundErrormodel_not_found404❌Wrong slug, wrong region, or the deployment does not exist (a common Azure/Vertex misconfiguration).
ContextOverflowErrorcontext_overflow400❌The history is too long. Inside the agentic loop this is auto-recovered once per step (see below); outside it, trim or compact and retry.

Every APICallError exposes the same fields:

FieldTypeDescription
statusCodenumberUpstream HTTP status (0 for NetworkError)
isRetryablebooleanWhether a retry could plausibly succeed
retryAfterMsnumber | undefinedParsed Retry-After, in milliseconds
providerstring | undefinedProvider id (anthropic, openai, xai, google, …)
requestIdstring | undefinedUpstream request id — the one thing a provider support ticket actually needs
upstreamTypestring | undefinedProvider's raw error type/code string, normalized for logging

Context overflow self-heals inside the loop (2.0)

When a step's request is rejected as too long, the agentic loop forces a compaction pass and retries that step once against the shrunk history — even if you never opted into compaction. A second overflow in the same step propagates verbatim, because compaction already gave what it could. So a ContextOverflowError reaching your catch from a loop call means compaction could not help, not that nothing was tried.

Non-HTTP errors

These extend DeuzError directly (no statusCode / isRetryable):

ClasscodeExtra fieldsWhat you should do
TimeoutErrortimeoutlayer: 'connect' | 'ttft' | 'total' | 'step' | 'tool' | 'chunk'Branch on layer: 'ttft'/'total' means the provider was slow, 'chunk' (2.2) that a stream went silent after content started, 'step' that your agent ran long. Always a failure, never retried. See the timeout layers.
AbortErroraborted—Caller-initiated cancellation. Never retried, never falls back. In a stream this normally does not reach you at all — the run resolves finishReason: 'aborted' instead.
NoObjectGeneratedErrorno_object_generatedtext?: string, finishReason? (2.3), stopDetails? (2.3)generateObject could not produce a valid object after one repair; streamObject's object promise rejects with it too. Inspect .text (the raw model output) — usually the schema is too strict or the model too small. Since 2.3 finishReason and stopDetails say how the last attempt ended: 'content_filter' with the refusal's details (Anthropic's category and explanation, or the OpenAI refusal text) when the model refused, 'length' when it was cut off. A structured batch result whose object does not parse or validate carries one as its error (2.3), without finishReason or stopDetails.
UnsupportedCapabilityErrorunsupported_capabilityprovider, capability, modelId?A model lacks a requested capability (e.g. embeddings on a chat-only provider). Thrown before any network call, so it costs nothing. Pick a different model, or override the belief with capabilities.
BreakerOpenErrorbreaker_openprovider, modelId, cooldownUntilThe per-model circuit breaker is open — the SDK is failing fast without a request. Route to a fallback model, or wait until cooldownUntil (a deps.clock.now() timestamp). See the breaker.
McpAuthorizationRequiredErrormcp_authorization_requiredserverUrl, authorizationUrl?Step one of an OAuth flow, not a dead end. Send the user to authorizationUrl, then reconnect with authorizationCode set to the ?code= you get back. See MCP.
ToolExecutionErrortool_executiontoolName, toolCallId?Usually you never see this thrown — see below.

ToolExecutionError is constructed internally but, in the agentic loop, it is not thrown. Its message is fed back to the model as an is_error tool_result so the model can self-correct. Look for isError: true on a ToolResult, not for a rejection. See The Tool Loop.

`authorizationUrl` is deliberately absent from `toJSON()`

A live authorization request carries state and code_challenge, which do not belong in every log line. Read it off the instance, not off the serialized error.

Runtime and module errors

The agent runtime, its stores and the modules raise errors of their own, each exported from the subpath that raises it. They come in two kinds:

  • DeuzError subclasses (UnpricedModelError, CheckpointNotFoundError, CheckpointVersionError, RagError, A2AError, BatchError, RealtimeError) carry a code and the secret-safe toJSON(), and isDeuzError recognizes them.
  • Plain Error subclasses (the budget, policy, persistence, lease, swarm, evolve and goal errors) carry a name and, all but SwarmConflictError and EvolveConflictError, a code. isDeuzError returns false for them: branch on instanceof, or on name and code.

A native run never rejects with these: runAgent, resumeAgent and the result of streamAgent resolve with status: 'failed' and error: { name, message, code? }, so read result.error.name and result.error.code there. The budget ledger, policy, persistence, lease, unpriced-model and checkpoint-version errors end the run even when they are raised inside a tool (an agentTool sub-agent, for example) or a compaction summary: they never become a tool error the model could retry, or a skipped compaction layer.

Agent runtime

ClassImport fromcodeRaised when
UnpricedModelError (2.3)@deuz-sdk/core, /edgeunpriced_modelThe agentic loop of generateText or streamChat, without an execution context, under budget: { usd, unknownPrice: 'reject' } is about to call a model that has no price: the loop's own model, a compaction summarizeModel, or an agentTool sub-agent's model. The run fails before that call. modelId names the model; a cause means the price lookup itself threw. Under an execution context (a native run, or an execution passed to generateText or streamChat) the same call is refused with BudgetLedgerError code unpriced_model. See Models without a price.
BudgetLedgerError/agentinvalid_budget, budget_exceeded, missing_reservation, reservation_conflict, unknown_request, invalid_snapshot, persistence_failed, admission_failed, unpriced_model (2.3)An execution context's budget ledger refused a call or could not record it: a call that does not fit the budget (budget_exceeded), a persistent BudgetStore that failed (admission_failed; since 2.3 it carries requestId and, for a settlement, settlement to replay), an unpriced model under unknownPrice: 'reject' (unpriced_model, 2.3). See How admission works and When a settlement fails.
BudgetStoreError/agentinvalid_request, reservation_conflict, window_conflictA BudgetStore refused a request: malformed input, a request id that conflicts with the hold the store recorded, or a scope key reused with another window. The ledger reports a failing store as BudgetLedgerError code admission_failed.
ExecutionPolicyError/agentinvalid_policy, invalid_context, tool_not_allowed, model_not_allowed, depth_exceeded, deadline_exceededThe execution policy refused: a tool or a model outside its allowlist, a sub-agent nested past maxDepth, a deadline that passed, or a malformed policy or context. Since 2.3 toolCallId names the refused call, and the policy decides a whole batch before any approval is asked for (One path for every tool call).
ExecutionPersistenceError/agentexecution_persistence_errorUnder session.durability: 'strict', a legacy loop could not commit a checkpoint, or (2.3) could not read the run's stored checkpoint before its first step.
AgentRunLeaseError (2.3, not exported)—lease_held, lease_lostA native run with lease found another executor holding it (lease_held, before anything is read or written), or lost the lease while it ran (lease_lost, after which it writes nothing more). It arrives as a failed result's error.name and error.code; reconcileAgentRun with lease rejects with it. See Leasing a native run.
CheckpointNotFoundError/durable, /edgecheckpoint_not_foundresumeFromCheckpoint rejects with it, and resumeStreamFromCheckpoint reports it as an error part, when the SessionStore holds no checkpoint for the runId.
CheckpointVersionError (2.3)/durable, /edgecheckpoint_version_unsupportedA stored loop checkpoint has a version other than 1, so a newer SDK probably wrote it: deserializeCheckpoint, resumeFromCheckpoint, resumeStreamFromCheckpoint and the store packs' sessions.load refuse it, and resumeAgent refuses an envelope that holds one, before anything is written. version and runId say which. See Strict checkpoints and approval resume.

Stores and executors

ClassImport fromcodeRaised when
SwarmConflictError/swarm—A swarm store refused a commit: another writer moved the run's revision, or the run already exists. swarm.resume rejects with it too when the run is not at the expectedRevision or expectedStatus (2.2) the caller passed. Since 2.3 the Postgres store raises it once its retries of a serialization failure run out, with the driver error as cause.
SwarmLeaseError/swarmheld, lostA swarm executor found the run held by another executor, or lost its lease. See Swarm leases.
EvolvePatchError/evolve, /edgeinvalid_markers, no_blocks, malformed_diff, empty_search, marker_in_replace, not_found, outside_evolve_block, ambiguous, empty_rewrite, frozen_changedA source's evolve blocks cannot be parsed, or a proposed patch cannot be applied to them. See Evolve blocks and patches.
EvolveConflictError/evolve, /edge—A population store refused a write: the run already exists, a candidate conflicts, or another writer committed the generation.
EvolveLeaseError/evolve, /edgeheld, lostAn evolve executor found the run held by another executor, or lost its lease. See Leases.

Modules

ClassImport fromcodeRaised when
A2AError (2.3)/a2aa2a_errorA failed fetchAgentCard or A2A client call, with a kind: protocol (the agent answered with an A2A error; reason, rpcCode), http, network or invalid-response. An aborted signal rejects with AbortError instead, and a URL or card the client cannot use throws InvalidRequestError. An executor of createA2AHandler throws an A2AError with a reason to answer a request with that A2A error. See A2A errors.
BatchError (2.3)/batchbatch_errorA batch the client cannot send, follow or read, with a kind: unsupported, invalid-request, invalid-job, not-ended, timeout, provider or store. Provider HTTP failures keep their own classes, and a transport failure is a NetworkError. See Batch errors.
RealtimeError (2.3)/realtimerealtime_errorA realtime session failure, with a kind (connect-failed, connect-timeout, no-websocket, provider, transport, closed, not-connected, invalid-message, tool): connect() rejects with it, and once a socket exists a failure also arrives as an error event. A send on a session that is not open (not-connected) and a sendToolResult for a call that is not waiting (tool) throw it. See Lifecycle and errors.
GoalError (2.3)/goalinvalid_goal, invalid_event, invalid_command, unsupported_version, revision_conflict, scope_mismatch; 2.4 adds delivery_conflict (supervisor.dispatch throws it for a delivery id seen with other content; the webhook handler answers it as a 409). lease_lost, stale and policy_limit are reserved and never thrown in 2.4: a lost lease and a spent limit are the supervisor's lease-lost and policy-limit events, and a stale expectedRevision is revision_conflictA goal function refused its input: a malformed record, event or command, a version from a newer build, an expectedRevision or a store save that another writer overtook, or an event for another goal. What a goal decides itself (a duplicate event, a completion without evidence) is a transition result, never an error. See Transitions.
RagError/ragrag_unsupported_legacy_doc, rag_unsupported_mime, rag_empty_text_layer, rag_parser_not_registered, rag_extension_mime_mismatchparse could not turn a document into text. mime is the detected type. See Typed errors.

Two failure channels, one rule

Which channel a failure uses is decided entirely by whether the entry point is a promise.

Entry pointFailure surfaces as
generateText, generateObject, embed, embedManyA rejected promise. Use try/catch.
streamChat, streamObjectAn error part on fullStream, a throwing textStream / partialObjectStream, and rejected usage / finishReason promises. Never a synchronous throw.

The never-throw contract for streamChat

streamChat returns a StreamChatResult synchronously and never throws — not on a missing API key, not on an invalid option pair. The network pump starts lazily on first access of any output. A failure surfaces in three coordinated ways:

  1. An { type: 'error', error } part is pushed onto fullStream.
  2. The usage promise rejects with that error.
  3. The finishReason promise rejects with that error.

Iterating textStream re-throws the error part, so a plain for await over text still lets you try/catch.

import { streamChat, isDeuzError } from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

const result = streamChat({
  model: anthropic('claude-opus-4-8'),
  messages: [{ role: 'user', content: 'Explain backpressure.' }],
});

try {
  for await (const chunk of result.textStream) {
    process.stdout.write(chunk);
  }
} catch (err) {
  // textStream re-throws the error part as a typed DeuzError.
  if (isDeuzError(err)) console.error(err.code, err.message);
}

Do not leave `usage` unhandled

The usage and finishReason promises reject on failure. If you only iterate the stream and never await them, an unhandled rejection can take a Node process down. Either await result.usage.catch(...), or do not touch them at all — an untouched promise is not "unhandled" only if you never create a floating reference to it.

Reading error parts off fullStream

If you consume fullStream directly, handle the error part yourself — it will not throw on its own. Keep a default case: StreamPart is an open union.

import { streamChat, DeuzError } from '@deuz-sdk/core';
import { createOpenAI } from '@deuz-sdk/core/openai';

const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY! });

const result = streamChat({
  model: openai('gpt-5.5'),
  messages: [{ role: 'user', content: 'hi' }],
});

for await (const part of result.fullStream) {
  switch (part.type) {
    case 'text-delta':
      process.stdout.write(part.text);
      break;
    case 'error':
      if (part.error instanceof DeuzError) {
        console.error(`[${part.error.code}]`, part.error.message);
      }
      break;
    default:
      break; // additive variants
  }
}

generateText and generateObject reject

import { generateObject, NoObjectGeneratedError } from '@deuz-sdk/core';
import { createOpenAI } from '@deuz-sdk/core/openai';
import { z } from 'zod';

const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY! });

try {
  const { object } = await generateObject({
    model: openai('gpt-5.5'),
    schema: z.object({ city: z.string(), population: z.number() }),
    messages: [{ role: 'user', content: 'Largest city in Japan?' }],
  });
  console.log(object);
} catch (err) {
  if (err instanceof NoObjectGeneratedError) {
    // The raw model text that failed to parse/validate is on `.text`.
    console.error('No valid object. Raw output:', err.text);
    // 2.3: how the last attempt ended, e.g. 'content_filter' with a refusal's category.
    console.error(err.finishReason, err.stopDetails?.category);
  }
}

generateObject makes one repair retry on a parse/validation failure before throwing. See generateObject.

Catching typed errors

Branch on instanceof for class-specific fields, or on code for a flat switch. The same classes are used across every provider, so this logic is provider-agnostic.

import {
  generateText,
  AuthenticationError,
  RateLimitError,
  ContextOverflowError,
  BreakerOpenError,
  APICallError,
} from '@deuz-sdk/core';
import { createAnthropic } from '@deuz-sdk/core/anthropic';

const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });

try {
  await generateText({
    model: anthropic('claude-opus-4-8'),
    messages: [{ role: 'user', content: 'hello' }],
  });
} catch (err) {
  if (err instanceof AuthenticationError) {
    // 401/403 — bad or insufficient credentials. Do not retry; fix the key.
  } else if (err instanceof RateLimitError) {
    console.log('retry after (ms):', err.retryAfterMs);
  } else if (err instanceof BreakerOpenError) {
    console.log('failing fast until', err.cooldownUntil); // route elsewhere
  } else if (err instanceof ContextOverflowError) {
    // Trim or compact the prompt and try again.
  } else if (err instanceof APICallError) {
    console.error(err.provider, err.statusCode, err.requestId);
  } else {
    throw err; // never swallow what you did not classify
  }
}

Checking retryability

isRetryable lives on APICallError and its subclasses. Errors that are not HTTP-shaped (TimeoutError, AbortError, NoObjectGeneratedError, UnsupportedCapabilityError, BreakerOpenError) do not carry the flag and are final.

import { APICallError } from '@deuz-sdk/core';

function isRetryable(err: unknown): boolean {
  return err instanceof APICallError && err.isRetryable;
}

Retry interplay

The pump retries only before the first byte of the response stream. Once streaming begins, a mid-stream error is final — partial output has already been emitted, so a transparent retry would either duplicate tokens or splice an unrelated completion into the middle of the first.

AspectBehaviour
WhenPre-first-byte only
BudgetmaxRetries, default 2 (per-call override on CommonCallOptions)
BackoffExponential with full jitter: random() * min(cap, base * 2^attempt), base 500ms, cap 30s
Retry-AfterHonoured when the provider sends it (capped at 30s); takes precedence over computed backoff
Which errorsThose whose isRetryable is true — NetworkError, RateLimitError (429), OverloadedError (529), and APICallError with status >= 500
Which errors neverTimeoutError and AbortError — both are re-thrown immediately. A 4xx other than 429 is never retried either.
DeterminismJitter is derived from deps.generateId() (hashed to a unit interval), never Math.random() — reproducible in tests
Aborting during backoffYour signal interrupts the wait immediately; it does not sleep out the delay first
const result = streamChat({
  model: anthropic('claude-opus-4-8'),
  messages: [{ role: 'user', content: 'hi' }],
  maxRetries: 4, // raise the pre-first-byte budget for this call
});

"Full jitter" rather than a fixed backoff matters at scale: when a provider returns 429 to a hundred of your requests at once, a deterministic delay makes all hundred retry at the same instant and produce a second thundering herd. Randomizing the whole interval spreads them.

The circuit breaker

Retries protect one call. The breaker protects the next ones from a provider that is already down.

  • Keyed per provider:modelId and stored in deps.breakerStore — so it is per-client, not global. Two createClient instances do not share a verdict.
  • Only provider-health failures count: NetworkError, TimeoutError, and retryable / >= 500 APICallErrors. A 401 or a 400 says nothing about provider health and is ignored.
  • 5 consecutive countable failures open it. It then fails fast with BreakerOpenError for a 30s cooldown instead of sending a request.
  • A successful response resets the counter (checked the moment a body arrives, before streaming) — best-effort, and only when there was something to clear.
  • A failing breaker store never blocks calls — if the store throws, the call proceeds.

fallbackModels and the withFallback middleware treat BreakerOpenError as an immediate fail-over signal, which is the point: the fast failure is what makes fail-over cheap. Full detail on Resilience.

The timeout layers

Each layer answers a different question, so each has its own scope. All are settable per call via timeout, and every timer is scheduled through deps.clock — never an ambient host timer — so tests stay deterministic.

LayerScopeDefaultFires whenCleared by
ttftMsone model call60_000the first content delta has not arrived in timethe first text-delta, reasoning-delta or tool-call-delta — a tool-call-first response counts as content
totalMsone model call300_000that whole response takes too longthe call completing
chunkMs (2.2)one model call, once content is flowingunboundedthe stream goes silent between two parts for longer than this (layer: 'chunk')every stream part re-arms it
stepMsone agentic step end-to-end — the model call plus the tool executions it triggeredunboundedthe step overrunsthe step ending
toolMsone tool execute (per call, not per step)unboundedthat execution overruns; Tool.timeoutMs overrides it for a single toolthe execution returning

Notes that save debugging time:

  • An explicit 0 disables a layer. timeout: { totalMs: 0 } is how you opt out of the 300s ceiling that a 25-second serverless budget makes meaningless.
  • ttftMs is not a total budget. A slow-but-alive stream is fine: the timer is cleared the moment any content arrives.
  • stepMs is enforced by both loops — the streaming one and the buffered generateText one arm it identically, so the same option cannot mean two things.
  • A stepMs expiry that fires while tools are running cannot abort the tools. That is toolMs / Tool.timeoutMs. The loop checks the deadline at its own step boundaries and fails there rather than feeding results into a model call it can no longer pay for.
  • A tool timeout is self-healing, not fatal. The execution is abandoned (its signal is aborted, so a well-behaved tool passing it to fetch stops working), and the model receives an is_error tool_result — Tool 'x' timed out after 30000ms and was abandoned. Every tool_use_id still gets a result. Tool timeouts do count toward the runaway-tool guard.

`layer` values you will actually see

The union declares six members. Model calls and the agentic loops construct 'ttft', 'total', 'chunk' (2.2) and 'step'. 'connect' is reserved, and a per-tool timeout in the loops reports layer: 'total' with a Tool execution exceeded …ms message rather than 'tool'. The only 'tool' timeout (2.3) is the abort reason a realtime session gives a tool's signal when the tool runs out of time. Match on the message or on the surrounding tool-result if you need to tell those apart; treat the union as open.

Abort is not a timeout

They look similar and are deliberately kept apart, because "the user changed their mind" and "the provider never answered" call for different handling.

User abort (signal.abort())Timeout
finishReasonresolves 'aborted'promise rejects
usageresolves with partial usagepromise rejects
error part on fullStreamnoneyes, carrying a TimeoutError
onUsagefires with meta.reason === 'aborted'fires with meta.reason === 'error'
Retriednevernever
Is it a failure?no — it is a clean early endyes

The SDK goes out of its way to keep this true even when a transport misbehaves: if one of our abort sources cut the request but the transport reported only a bare AbortError, the armed TimeoutError is recovered and reported instead of being misread as a user cancel.

Secret redaction (P0)

API keys must never appear in any log, error message, tracer attribute, observation event, or cause chain. This is a regression-tested invariant, not a best effort.

  • DeuzError deliberately carries no raw request headers or body, and never places a raw Request/Headers in cause.
  • error.toJSON() returns only name, code, message, and an allowlist of scalar diagnostic fields (statusCode, isRetryable, retryAfterMs, provider, requestId, upstreamType, layer, toolName, toolCallId, modelId, capability, runId, mime, serverUrl, and since 2.3 finishReason). A2AError adds its kind, reason and rpcCode, BatchError its kind and batchId, and RealtimeError its kind and closeCode. cause and model output are excluded.
  • Anything that flows into a logger, error, or span first passes through the redaction helpers. Secret-looking header values (authorization, x-api-key, x-goog-api-key, api-key) and token shapes (sk-ant-…, sk-…, AIza…, Bearer …) are masked to their last four characters — ****AB12.
  • Opt-in observation content capture passes through a second, stricter [REDACTED] profile before an event is emitted.

You never call the redaction helpers yourself; they run inside the SDK. The takeaway: it is safe to log a DeuzError verbatim — including provider, statusCode, requestId and upstreamType — without leaking credentials.

import { generateText, isDeuzError } from '@deuz-sdk/core';

try {
  await generateText({ model, prompt: 'hi' });
} catch (err) {
  // toJSON() is the secret-free projection: name, code, message + scalar details.
  console.error('inference failed', isDeuzError(err) ? err.toJSON() : { message: String(err) });
}

See also

このページの内容