SDK exception hierarchy, problem details, and retry behavior
The Duale AI SDK exception hierarchy and RFC 9457 problem details envelope define how to classify, retry, and report task and Library failures.
Typed SDK exceptions, RFC 9457 problem details, document ingestion failure codes, retry ownership, and a troubleshooting table for Duale AI task and Library errors.
- DualeError is the base class; catching it catches every typed SDK exception subclass.
- Branch on error_code from the RFC 9457 envelope; check retryable before retrying.
- Document ingestion failures carry stable error codes that determine whether re-uploading the same bytes helps.
- A process-local circuit breaker opens after ten consecutive retryable terminal failures; one probe closes it.
- Task streams retry up to ten times; Library REST requests run once per call.
Summaries were generated by AI. Generative AI is experimental.
Task failures and LibrariesClient HTTP failures that the SDK translates use typed exceptions. Local input, filesystem, polling, object-store upload, and task-scoped attachment transport failures can raise other types. Platform problem details carry a machine-readable error code; branch on that code, not on a message string.
Exception hierarchy
Every exception in the table below is importable from duale. DualeError is the base of the SDK’s typed errors; catching it catches every subclass below. Common paths outside that hierarchy include:
- A submission rejected by the open task circuit breaker raises
RuntimeError. - Invalid client configuration raises
pydantic.ValidationErrorwhen you constructDualeConfigdirectly. WhenDualeSDK()creates an invalid default config from the environment, it wraps that failure inRuntimeError. - Missing attachment paths raise
FileNotFoundError. Invalid Library update or polling arguments raiseValueError. sdk.libraries.wait_for_document()raises the built-inTimeoutErrorwhen the whole polling operation exceeds its client-side timeout.- A cached
@activitythat exceedsjob_timeoutalso raises the built-inTimeoutError. - Local file and object-store upload failures raise
LibraryUploadError. Itsattachment_keyand optionalpart_numberidentify the failed input;completed_receiptscontains receipts that finished earlier in the same batch.
Catch MessagingError to also catch DualeConnectionError.
TaskStoppedError carries no problem details: a stop is a decision an agent made, not a failure of the task. It reports reason and task_id, and nothing else.
RoutingError, TaskTimeoutError, ActivityTimeoutError, TaskSubmissionError, AgentRegistrationError, CacheError, CacheConnectionError, and CacheSerializationError remain importable from duale for compatibility. Current task, activity, registration, and cache paths do not raise those classes, so do not use them to classify current runtime failures.
Problem details
Platform failures use one machine-readable envelope. Its seven decision fields tell your handler how to classify, retry, or report the failure.
When the platform ends a task with a failure, await response.model() raises DualeError and attaches the RFC 9457
envelope on exc.problem_details. Library HTTP failures attach the same envelope to DualeAuthError, BusinessError,
or DualeConnectionError. Client-side errors such as local validation, configuration, or file reads have no server
envelope, so problem_details is None.
- Field
error_code- Meaning
- Stable wire identifier, for example
TASK_DEADLINE_EXCEEDED,AUTHORIZATION_FAILED,BILLING_LIMIT_EXCEEDED,CONTENT_FILTERED. Branch on this.
- Field
detail- Meaning
- Human-readable description of the failure.
- Field
retryable- Meaning
- Whether a client retry can succeed. Check this before retrying.
- Field
retry_after_seconds- Meaning
- Suggested wait before a retry, when present.
- Field
owner_action_required- Meaning
- The failure needs an account or configuration change, not a retry.
- Field
error_category- Meaning
- One of
transient,upstream,integrity, orconfig.
- Field
errors[]- Meaning
- Per-error details, each with a field path and optional
ai_hintsfor automated handling.ai_hintsis not an envelope-level field.
The envelope also carries the standard RFC 9457 members type, title, status, and instance, plus the correlation
identifiers request_id, trace_id, and span_id.
Branch on error_code. Before you retry, require retryable and honor retry_after_seconds when present. If you report
the failure to support, include the correlation identifiers.
Document ingestion failures
A document can fail after upload, and its stable error code tells you which recovery decision to make. Use the table below before you upload the same bytes again.
wait_for_document() returns a failed document; it does not raise. document.failure carries an
RFC 9457 envelope, but not the retryable, retry_after_seconds, or error_category fields above,
so read this table to decide whether to retry. error_code is normally one of the values below;
treat an unlisted code as an unknown terminal cause.
The state is terminal: the document never becomes ready on its own. Upload again only when the
cause is one a new upload can change.
A failed document still occupies its Library until you delete it.
Handle a failed task
Catch DualeError, read the code, and decide from the envelope (response and the handlers are your code):
from duale import DualeError
try:
result = await response.model()
except DualeError as exc:
details = exc.problem_details
if details is not None and details.error_code == "CONTENT_FILTERED":
# Agent-boundary rejection. The platform marks it non-retryable.
handle_rejected_task(details)
elif details is not None and details.retryable:
await schedule_retry(after=details.retry_after_seconds)
else:
raiseCONTENT_FILTERED does not expose the classifier’s reasoning. Always check problem_details.retryable before applying your own retry logic.
Retries and timeouts
Retry ownership depends on the operation:
- Task stream. The HTTP transport makes at most ten stream attempts. It reconnects a dropped event stream with exponential backoff and jitter, resuming from the last received event.
- Task runtime. The runtime retries a failing task within its policy and returns a terminal result.
- Library REST. Each management request runs once.
wait_for_document()repeats successful non-terminal reads, but a failed read ends the poll. - Presigned upload. Each part of a file upload retries network and timeout failures plus HTTP
500,502,503, and504, for at most five attempts.
Every task carries an absolute deadline. Without an explicit deadline, the SDK sets one 1800 seconds (30 minutes)
ahead. A task that passes its deadline ends as a failure with error_code TASK_DEADLINE_EXCEEDED.
The SDK also runs a process-local circuit breaker. Ten consecutive retryable terminal failures open it; 30 seconds later it admits one task as a recovery probe, and only that task’s success closes it. Cancellation, validation, business, user-tool, and explicitly non-retryable failures are neutral and do not open the breaker.
Task circuit and activity capacity
Task submissions start immediately unless the process-local dependency circuit is open. The SDK does not put tasks through a local queue or token bucket. max_jobs and the activity fields in BackpressureConfig bound cached activities; the same config also sets the task circuit’s failure threshold and recovery delay. See the API reference.
Troubleshoot
Confirm any fix by re-running the failing call: recovery means the symptom is gone—model() returns a result, a submission returns an AgentResponse, or serve() keeps running.
If the repeated call fails with the same symptom, use Problem details to branch on its stable code and collect the correlation identifiers for support.