Exception hierarchy
Public Preview

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.ValidationError when you construct DualeConfig directly. When DualeSDK() creates an invalid default config from the environment, it wraps that failure in RuntimeError.
  • Missing attachment paths raise FileNotFoundError. Invalid Library update or polling arguments raise ValueError.
  • sdk.libraries.wait_for_document() raises the built-in TimeoutError when the whole polling operation exceeds its client-side timeout.
  • A cached @activity that exceeds job_timeout also raises the built-in TimeoutError.
  • Local file and object-store upload failures raise LibraryUploadError. Its attachment_key and optional part_number identify the failed input; completed_receipts contains receipts that finished earlier in the same batch.
ExceptionRaised when
DualeErrorBase class. A platform task failure is raised as DualeError with problem details attached, except TaskStoppedError.
ValidationErrorA result does not match the response model passed through res=.
DualeAuthErrorThe server rejected the token or the caller lacks the required permission.
BusinessErrorA platform request was valid HTTP but failed a domain rule, including Library 4xx errors other than 401 and 403.
LibraryUploadErrorA local file or object-store upload failed.
ConfigurationErrorA Library operation needs configuration, such as DUALE_TENANT_ID, that is missing.
MessagingErrorBase class for SDK transport failures.
DualeConnectionErrorA task stream, network request, or Library 5xx response failed (subclass of MessagingError).
TaskStoppedErrorAn agent stopped the task before it produced a result. Read exc.reason for the reason that agent gave.

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.

FieldMeaning
error_codeStable wire identifier, for example TASK_DEADLINE_EXCEEDED, AUTHORIZATION_FAILED, BILLING_LIMIT_EXCEEDED, CONTENT_FILTERED. Branch on this.
detailHuman-readable description of the failure.
retryableWhether a client retry can succeed. Check this before retrying.
retry_after_secondsSuggested wait before a retry, when present.
owner_action_requiredThe failure needs an account or configuration change, not a retry.
error_categoryOne of transient, upstream, integrity, or config.
errors[]Per-error details, each with a field path and optional ai_hints for automated handling. ai_hints is not an envelope-level field.
  • 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, or config.
  • Field
    errors[]
    Meaning
    Per-error details, each with a field path and optional ai_hints for automated handling. ai_hints is 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.

error_codeWhat happenedYour next action
FORMAT_UNSUPPORTEDThe platform does not support this file type.Convert the file to a supported type, then upload the result.
CORRUPT_DOCUMENTThe file is damaged and cannot be parsed.Re-export the file from its source application.
ENCRYPTED_DOCUMENTThe file is an encrypted legacy .doc or .ppt, or an encrypted .epub.Remove the password, then upload the file again. A protected .docx, .xlsx, or .pptx reaches ready instead—see Troubleshoot.
DOCUMENT_TOO_LARGEThe file passed the 500 MiB upload limit but tripped a scanning or extraction limit.Upload a smaller or simpler source. Splitting helps only when plain size was the cause.
MALWARE_DETECTEDThe upload scan matched a malware signature.Do not upload it again. The platform keeps the scanned file with the failed document. Investigate the file at its source.
HASH_MISMATCH, SIZE_MISMATCHThe stored bytes did not match what the upload declared.Upload the file again from an unchanged local copy.
EXPIRED_UPLOADThe upload session expired before the file finished uploading.Start a new upload and finish it within the session.
LIBRARY_DELETEDThe Library was deleted while the document was still being processed.Upload into a Library that is not deleted.
PERMISSION_DRIFTThe uploading agent lost library:write on that Library while the document was being processed.Grant library:write back to that agent, then upload again.
EXTRACTION_FAILEDThe file produced neither searchable text nor a searchable picture, or extraction failed for a cause detail names.Read detail: it separates an empty document from a parse failure. Check that the file opens and contains readable text or a picture the platform can decode. The same file rarely gives a different result.
PROCESSING_TIMEOUTExtraction ran past its deadline.Upload a smaller or simpler source.
INDEX_WRITE_FAILED, SEGMENT_LOSTProcessing failed inside the platform.Upload the same file again. Report it with the request_id if it repeats.

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:
        raise

CONTENT_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, and 504, 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.

SymptomCauseFix
await response.model() has not returned when expectedThe task has not reached a terminal event.model() has no separate timeout. Keep response.task_id and compare elapsed time with the deadline (30 minutes by default). Follow Tasks and results to stop the platform task or only cancel the local wait.
ValidationError from model()The result did not match the response model.Align the model with the task, or drop res and read the raw completion.
DualeAuthError with status 401The server rejected the token.Ask the workspace administrator to reissue or rotate it. A missing or malformed token raises pydantic.ValidationError from direct DualeConfig() construction, or RuntimeError when DualeSDK() builds the config. Set DUALE_TOKEN to a duale_ value of 10–256 characters.
DualeAuthError with status 403The token’s caller lacks the required Library grant.Find the action the operation checks in Library actions, which also names who grants it, then repeat the operation.
DualeConnectionError during a task streamThe task-stream connection dropped and did not recover.The task transport retries automatically; if it persists, check DUALE_ENDPOINT and network egress.
BusinessError during a Library callThe Library rejected the operation with a domain 4xx.Read problem_details.error_code and correct the request or resource state.
DualeConnectionError during a Library callThe network failed or the Library returned 5xx.Check DUALE_ENDPOINT and network egress. Retry only when the problem details mark the failure retryable.
LibraryUploadErrorA local read or object-store part failed.Read attachment_key and part_number; preserve completed_receipts, then retry or reconcile only unfinished files.
BusinessError with error_code DOCUMENT_UPLOAD_TOO_LARGE (status 413)A document exceeds the 500 MiB upload limit.Reduce or split the document before preparing and uploading it again.
A ready document preview shows only an encryption type or rights labelA password or a rights label protects the file.The platform made only that protection metadata searchable. Remove the protection, upload the file again, and confirm that the preview now contains the source text.
A ready scan or figure gives no visual answerNo configured route carried the picture, or a route limit dropped it.Configure an image-capable target whose limits admit the pictures, then repeat a question that only one picture answers. Images in a document covers the setup.
Built-in TimeoutError from an @activityThe activity exceeded the SDK’s job_timeout.Treat the side effect as uncertain. Cancellation cannot roll back an external action that already completed.
error_code is CONTENT_FILTEREDThe agent boundary rejected the task; non-retryable.Do not retry. Inspect problem_details and adjust the request.
A submission raises RuntimeErrorThe task dependency circuit is open.Wait for the reported recovery delay, then allow one probe task.
Serving tools raises an error about agent_idNo agent identifier is configured.Set DUALE_AGENT_ID to a provisioned agent before sdk.serve().

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.