Skip to content
ask
Public Preview

Python SDK reference: ask, next, AgentResponse

Reference for the Duale AI Python SDK's ask, next, and AgentResponse APIs, covering task submission, continuation tasks, and response observation.

This reference covers task submission, continuation, response handling, SDK configuration, routing policies, and tool authoring for the Duale AI Python SDK.

  • Task submission and continuation via ask and next.
  • AgentResponse members and stop_task behavior.
  • DualeAISDK client, create_sdk helper, and DualeAIConfig.
  • PriorityClass, RoutingPolicy, ResponseFormat, and Capability.
  • Tool decorators, tool context fields, and streaming events.

Summaries were generated by AI.

Import common interfaces from dualeai; specialized module paths are shown below. In signatures, * starts the keyword-only arguments. These APIs are in Public Preview.

ask

Submit a Task and return an AgentResponse once the SDK’s circuit breaker allows the submission. Duale AI has not necessarily accepted the request when the call returns.

async def ask(
    action: str,
    capabilities: list[Capability] | None = None,
    res: type | None = None,
    response_format: ResponseFormat | None = None,
    routing: RoutingPolicy | None = None,
    *,
    priority: PriorityClass = PriorityClass.standard,
    streaming: bool = False,
    deadline: datetime | None = None,
    attachments: list[PreparedAttachment] | None = None,
    task_id: str | None = None,
    sdk: DualeAISDK,
) -> AgentResponse
ParameterMeaning
actionThe Task instruction. Required, non-empty, at most 100,000 characters.
resA Pydantic model (or basic type) to validate the result against. Sets the return type of response.model().
routingA RoutingPolicy of model-selection preferences. Takes precedence over capabilities; the two are not merged.
priorityThe Task’s scheduling class: PriorityClass.standard by default, or PriorityClass.fast. See PriorityClass for billing and continuation behavior.
capabilitiesWithout routing, a non-empty list creates a policy containing only soft required_capabilities. None or an empty list adds no caller capability preference.
response_formatA ResponseFormat that constrains the output shape.
streamingWhen True, response.stream() yields the content events of a free-text answer as they arrive, in no guaranteed order. An answer streams only without res or response_format, or with the text or markdown format.
deadlineA timezone-aware absolute datetime. Defaults to 30 minutes from submission. A value without timezone information raises TypeError before submission.
attachmentsPrepared attachment references to include with the task. Upload the files first with sdk.upload_attachments; see Attach documents.
task_idThe Task identifier, chosen by the caller: 10–128 ASCII letters, digits, _, or -. Defaults to a generated identifier. Reuse it only for an exact retry of the same request. While a Task with this identifier exists for the Agent Identity, the same identifier names that Task: the request streams it again, without comparing payloads. This also applies to continuation Tasks.
sdkThe DualeAISDK instance. Required and keyword-only.

next

Start a continuation Task that inherits the Task history of an earlier Task, while that Task runs or after its outcome.

async def next(
    self,
    message: str,
    *,
    priority: PriorityClass = PriorityClass.standard,
    res: type[T] | None = None,
    deadline: datetime | None = None,
    response_format: ResponseFormat | None = None,
    streaming: bool | None = None,
    routing: RoutingPolicy | None = None,
    attachments: list[PreparedAttachment] | None = None,
    task_id: str | None = None,
) -> AgentResponse[T]
ParameterMeaning
messageThe next user message. Required and at most 100,000 characters; it cannot contain NUL. Empty and whitespace-only strings are accepted.
priorityThis continuation Task’s own scheduling class. Defaults to PriorityClass.standard, including when the parent used PriorityClass.fast.
resA Pydantic model or basic type for local validation of the continuation’s result. Not inherited from the parent.
deadlineA timezone-aware absolute datetime. A value without timezone information raises ValueError. Defaults to 30 minutes from submission. This is the continuation Task’s own deadline; the parent’s deadline does not bound it.
response_formatThe continuation’s output shape. Omit it to keep the parent’s format, unless res supplies a JSON Schema. Pass text for free text after a structured parent.
streamingNone keeps the parent’s streaming choice, including local buffering for stream().
routingSupplies routing preferences for the continuation. Omitted fields inherit from the parent. Duale AI still applies its own needs for each part of the work, and inferred capabilities come from the continuation’s own message. See RoutingPolicy.
attachmentsPrepared documents to add; the parent’s documents stay available.
task_idThe continuation Task’s identifier, with the same rules as ask(task_id=...). It must differ from the parent’s identifier; Duale AI rejects a self-continuation with 422. Upload its documents under this identifier first.

None preserves the parent’s response format and streaming choice. The deadline defaults to 30 minutes from submission, and priority defaults independently to standard. res= is not inherited: when response_format is omitted, res= sends its JSON Schema as the continuation’s format. With neither, the continuation keeps the parent’s format and performs no local validation.

next() sends the continuation as soon as Duale AI has accepted the parent’s request. It does not wait for the parent’s outcome or validate the parent’s result. Call await response.model() first if you need that result. The returned AgentResponse.task_id is the task_id argument, or a new identifier generated before submission. The parent’s identifier stays unchanged. See Continuation Tasks.

The annotations still use the parent’s AgentResponse[T], so they do not reflect these runtime type changes. The method never raises the parent’s outcome. A permission refusal is an immediate HTTP answer; awaiting the returned response’s model() raises DualeAIAuthError with AUTHORIZATION_FAILED.

If the parent is not ready yet, the continuation waits. When a continuation waits or fails states the rules, and Task outcomes lists PARENT_TASK_NOT_FOUND and PARENT_TASK_UNAVAILABLE, which model() raises as DualeAIError. Cancelling a caller waiting in next() before it sends leaves the parent running.

AgentResponse

Returned by ask() and response.next().

MemberBehavior
task_idThe client-generated submission identifier, available as soon as the call returns. If Duale AI accepts the submission, this becomes the durable Task identifier.
await model()Wait for the terminal outcome and validate a completed result. Raises outcome, schema, or observation errors, including DualeAIConnectionError if the SDK cannot observe the outcome because of a transport failure.
stream()Iterate content deltas and reset events in arrival order; order them by generation and sequence. Yields events only for a streamed free-text answer. Call model() afterward to check the outcome.
await next(message, ...)Start a continuation Task at once, while this Task runs or after any outcome, without validating this Task (next). Pass res= to validate the continuation result; the default performs no local validation.
await stop(reason)Request a Stop for this Task. It needs agent:stop_task on the Task. Keep observing the outcome; acceptance does not prove that the Task stopped. See Stop the Task you intend to stop.
await cache_hit()Returns False for every completed Task: Duale AI currently produces each final answer fresh. Returns None on a failed or stopped Task, or an observation failure.
await llm_metrics()Return the LLMResult: completion, validated data, and requested Tool Calls. Returns None on failure, Stop, or observation failure. Contains no provider, model, token, cost, or latency analytics.
  • Member
    task_id
    Behavior
    The client-generated submission identifier, available as soon as the call returns. If Duale AI accepts the submission, this becomes the durable Task identifier.
  • Member
    await model()
    Behavior
    Wait for the terminal outcome and validate a completed result. Raises outcome, schema, or observation errors, including DualeAIConnectionError if the SDK cannot observe the outcome because of a transport failure.
  • Member
    stream()
    Behavior
    Iterate content deltas and reset events in arrival order; order them by generation and sequence. Yields events only for a streamed free-text answer. Call model() afterward to check the outcome.
  • Member
    await next(message, ...)
    Behavior
    Start a continuation Task at once, while this Task runs or after any outcome, without validating this Task (next). Pass res= to validate the continuation result; the default performs no local validation.
  • Member
    await stop(reason)
    Behavior
    Request a Stop for this Task. It needs agent:stop_task on the Task. Keep observing the outcome; acceptance does not prove that the Task stopped. See Stop the Task you intend to stop.
  • Member
    await cache_hit()
    Behavior
    Returns False for every completed Task: Duale AI currently produces each final answer fresh. Returns None on a failed or stopped Task, or an observation failure.
  • Member
    await llm_metrics()
    Behavior
    Return the LLMResult: completion, validated data, and requested Tool Calls. Returns None on failure, Stop, or observation failure. Contains no provider, model, token, cost, or latency analytics.

response.task is an asyncio.Task that submits the request and observes events locally. Cancelling it, or a caller waiting in model(), ends that observation without sending a Stop. To pause waiting, keep a task for model(), protect it with asyncio.shield(), and await it later. Cancellation can also propagate through cache_hit() and llm_metrics(). Check success with model(); the metadata methods are not outcome checks.

stop_task

Request a Stop for a Task. Duale AI processes it asynchronously.

async def stop_task(self, task_id: str, reason: str) -> TaskStopAccepted
ParameterMeaning
task_idThe task to stop.
reasonWhy the caller requests the Stop. Required, at most 500 characters, and not only spaces. It is returned only if the Stop takes effect. TaskStopAccepted comes from dualeai.models.task_stop.
  • Parameter
    task_id
    Meaning
    The task to stop.
  • Parameter
    reason
    Meaning
    Why the caller requests the Stop. Required, at most 500 characters, and not only spaces. It is returned only if the Stop takes effect. TaskStopAccepted comes from dualeai.models.task_stop.

TaskStopAccepted confirms that the caller has permission to stop the Task; it does not prove that the Task exists or that it stopped. Keep observing the original response: model() raises TaskStoppedError with your reason if the Stop takes effect. Otherwise, the Task keeps the outcome it reached first. stream() yields no separate terminal Stop event.

from dualeai import TaskStoppedError

response = await ask("Analyse this contract", sdk=sdk)
await response.stop(reason="Wrong document supplied")

try:
    await response.model()
except TaskStoppedError as stopped:
    print(stopped.reason)

Use response.stop(reason) for a response you hold, or sdk.stop_task(task_id, reason) with a saved ID, including one submitted by another process. An Agent Identity has permission to stop its own Tasks by default, so these calls work with its API token out of the box. Without agent:stop_task on the Task, the call immediately raises DualeAIAuthError (AUTHORIZATION_FAILED, 403). An allowed request receives acceptance (202), even if the Task ended or does not exist. A Stop sent before the Task starts, including to a continuation still waiting for its parent, is applied only if the Task starts within a limited period. Confirm the Stop through the Task outcome.

Stop the Task you intend to stop explains which work a Stop covers, how it affects continuations, and what it cannot undo.

Task actions

SDK operationRequired actionResource
askagent:create_taskagent:<agent_id>
askagent:read_taskThe new task:<agent_id>/<task_id>
nextagent:continue_task, agent:read_taskThe parent’s task:<agent_id>/<task_id>
nextagent:read_taskThe new task:<agent_id>/<task_id>
model(), stream()agent:read_tasktask:<agent_id>/<task_id>
stop, stop_taskagent:stop_tasktask:<agent_id>/<task_id>
Tool Result submissionagent:continue_task, agent:read_tasktask:<agent_id>/<task_id>
serve() registrationagent:register_toolsagent:<agent_id>
Tool Host heartbeatagent:heartbeatagent:<agent_id>
Tool Host shutdownagent:deregister_processagent:<agent_id>

A call answered with a Task’s event stream (ask, next, or Tool Result submission) also needs agent:read_task on that Task. A permission refusal is answered at once with AUTHORIZATION_FAILED. See Tenant access policies for resource patterns and {self}, and Default policies for the supplied grants.

DualeAISDK

The client. Use it as an async context manager, or call serve() to publish tools.

class DualeAISDK:
    def __init__(
        self,
        config: DualeAIConfig | None = None,
        *,
        agent_id: str | None = None,
        max_jobs: int = 100,
        job_timeout: int = 1800,
        auto_start: bool = True,
        backpressure_config: BackpressureConfig | None = None,
        max_concurrent_tools: int | None = None,
        graceful_shutdown_timeout: float = 0.0,
        transport: HTTPTransportProtocol | None = None,
    ) -> None: ...

    async def serve(self, *, stop_event: asyncio.Event | None = None) -> None: ...

config defaults to a DualeAIConfig() built from the environment. max_jobs sets the default concurrency for cached activities and registered tools. job_timeout limits the wait for scheduled activity work; see Activity caching for its scope. max_concurrent_tools overrides only the registered-tool concurrency and otherwise defaults to max_jobs.

On shutdown, graceful_shutdown_timeout allows that many seconds for in-flight Tool Results. The 0.0 default cancels the SDK waits immediately; a synchronous callable already running in a worker thread can continue. auto_start defaults to True on the constructor. The create_sdk() helper and MockSDK default it to False.

For Task submissions, use async with create_sdk() as sdk:. To publish registered Tools for agent_id, call serve(). It maintains heartbeats and blocks until shutdown. Use stop_event= for cooperative shutdown.

Tests can pass transport= to inject an HTTPTransportProtocol. dualeai.testing provides the simpler keyed-response and in-memory Library helpers.

Close the SDK after its work finishes

Keep the async context open until you have consumed every response. On exit, await sdk.cleanup() cancels Task observers, stops Tool handling and heartbeats, and closes network and cache resources. Request a Stop explicitly if needed; cleanup does not send one.

await sdk.start() registers the current Tools and starts heartbeats. Repeated calls do not start another lifecycle. To control startup, construct with auto_start=False, declare all Tools, then call start() or serve(). serve() also runs cleanup when it exits. See Authoring tools for the complete serving example.

create_sdk

create_sdk(**kwargs) builds a DualeAISDK from keyword arguments, falling back to DUALEAI_ environment variables. It defaults to auto_start=False.

from dualeai import create_sdk

sdk = create_sdk(agent_id="agent_security_operations")

Accepted keys: token, endpoint, tenant_id, agent_id, redis_url, sqlite_path, debug, max_jobs, job_timeout, auto_start.

Use DualeAISDK(...) directly for transport, backpressure_config, max_concurrent_tools, or graceful_shutdown_timeout. Pass a DualeAIConfig for nested observability settings. These are not create_sdk() keywords.

DualeAIConfig

DualeAIConfig reads DUALEAI_ environment variables; explicit values override them. SDK configuration lists every variable, its default, and its rules, and also covers logging, telemetry export, and local health.

PriorityClass

Import PriorityClass from dualeai. ask() and response.next() accept the keyword-only priority argument. Its values are PriorityClass.standard and PriorityClass.fast; omitting it selects standard. None and integers are not accepted classes.

Standard favors throughput; fast favors lower waiting time for the same work and uses the Fast Task price. Model selection, Tool behavior, and the Task deadline keep their own contracts. Fast does not guarantee an end-to-end response time.

A Task keeps the class it started with across retries. Duale AI charges a Task once, at its class’s price, when it starts working on it. A request that ends before work starts has no Task charge, and retries or work inside the Task add none. Each continuation is a separate Task with its own class and charge; omitting priority selects standard even after a fast parent.

Excerpt inside an open SDK context:

from dualeai import PriorityClass, ask

response = await ask("Summarize the supplied report", priority=PriorityClass.fast, sdk=sdk)
await response.model()
follow_up = await response.next("List the unresolved questions", priority=PriorityClass.fast)
await follow_up.model()

Omit priority from next() to request a standard continuation. Scheduling class is separate from the model-selection preferences in RoutingPolicy.

RoutingPolicy

All fields are optional, soft preferences. Even required_capabilities allows fallback when no model meets the full request.

FieldAccepted values
target_accuracy0.0–1.0 or None
target_permissiveness0.0–1.0 or None
cost_sensitivity0.0–1.0 or None
speed_preference0.0–1.0 or None
required_capabilitieslist of Capability
preferred_capabilitieslist of Capability
  • Field
    target_accuracy
    Accepted values
    0.0–1.0 or None
  • Field
    target_permissiveness
    Accepted values
    0.0–1.0 or None
  • Field
    cost_sensitivity
    Accepted values
    0.0–1.0 or None
  • Field
    speed_preference
    Accepted values
    0.0–1.0 or None
  • Field
    required_capabilities
    Accepted values
    list of Capability
  • Field
    preferred_capabilities
    Accepted values
    list of Capability

Omitting a field leaves it out of the submitted policy. For a new Task, Duale AI resolves omitted cost_sensitivity to 0.1. Set it to None or 0 to remove the general preference for lower model prices, or choose another value from 0 to 1. Duale AI applies its own defaults to the other omitted fields. A retry keeps the policy the Task started with.

Routing contract and limits defines what these values mean, how routing handles omitted preferences, and which controls to use for hard requirements.

An omitted field reads back as None on the RoutingPolicy instance. Explicit None is accepted only for the four nullable numeric fields; omit capability lists instead of setting them to None.

For a continuation, each omitted field inherits from the parent, even when you pass RoutingPolicy() or a partial policy. Set a nullable numeric field to None to clear the inherited preference, or a capability list to [] to clear the inherited list. Duale AI’s own needs for each part of the work still apply, so these overrides do not guarantee the same effective preferences. What a continuation keeps explains the scope of inheritance.

ResponseFormat

Use a predefined string—text, json, markdown, html, xml, yaml, or csv—or an object with a json_schema and an optional description. Pass a predefined format as its string, for example response_format="json". For validation against a strict schema, construct the object form:

from dualeai.models.response_format import JsonSchemaResponseFormat

response_format = JsonSchemaResponseFormat(
    json_schema={"type": "object", "properties": {"total": {"type": "number"}}, "required": ["total"]},
    description="Invoice totals",
)

Capability

problem_solving, math, code, agentic, long_context, finance, cyber_defensive, cyber_offensive, medical, legal.

A capability describes the work a Task needs. Routing uses available benchmark evidence to inform that preference; coverage and evaluation conditions vary by model and benchmark. Task capabilities and benchmark evidence defines each value, gives examples, and names its associated benchmarks.

Use these values in RoutingPolicy. They are best-effort preferences, not safety controls or permission to perform a use prohibited by the terms of use.

Decorators: @tool and @activity

Use these signatures to declare Tools and cached activities:

@tool(*, sdk: DualeAISDK, description: str, timeout: timedelta,
      retries: int = 0, error_transform: Callable[[Exception], str] | None = None)

@activity(cache_ttl: timedelta | None = None, max_retries: int = 3, *, sdk: DualeAISDK)

@tool registers a function that a Task can call; see Authoring tools. @activity caches an async function called by your application; it does not publish a Tool. Both decorators require sdk= when the function is declared. Activity caching explains storage, expiry, and retries.

Tool context fields

Inside a Tool Call, current_tool_context() returns a ToolContext with tool_call_id, attempt, task_id, and deadline_at. It also provides remaining_seconds() and is_expiring(). Outside a Tool Call, the function returns None. See Authoring tools.

Streaming events

response.stream() yields content events in arrival order, which is not always the answer’s order.

EventFieldsMeaning
BridgeContentDeltaResponsedelta, generation, sequenceOne chunk of the answer text, placed by sequence within its generation.
BridgeContentResetResponsegenerationStarts a new generation; everything shown from lower generations is replaced.
  • Event
    BridgeContentDeltaResponse
    Fields
    delta, generation, sequence
    Meaning
    One chunk of the answer text, placed by sequence within its generation.
  • Event
    BridgeContentResetResponse
    Fields
    generation
    Meaning
    Starts a new generation; everything shown from lower generations is replaced.

Show only the highest generation, with its deltas sorted by sequence; Stream an answer shows the rule. Take the final result from await response.model(), not from the streamed text.

Attachments

sdk.prepare_attachments(files) accepts a list of (Path, description) tuples and returns PreparedAttachment references for non-empty regular files. It reads their sizes, not their contents. Descriptions have at most 500 characters. Keep each file unchanged between preparation and upload.

await sdk.upload_attachments(task_id, attachments, agent_id=None) uploads into the task’s Library and returns dict[str, LibraryDocumentCreateResponse], keyed by PreparedAttachment.key. Omit agent_id to use sdk.agent_id, configured by the constructor or DUALEAI_AGENT_ID. A non-empty upload without either identifier raises ValueError; an empty attachment list returns {} without a request.

Pass the same prepared references to ask(attachments=...). Upload batches can partially succeed; follow Attach documents and its recovery link before retrying.

LibrariesClient

sdk.libraries manages persistent Library metadata and documents. Before any operation, set tenant_id through DualeAIConfig(tenant_id=...) or DUALEAI_TENANT_ID. The server identifies the caller from DUALEAI_TOKEN and decides each operation from that caller’s access to the Library’s path.

class LibrariesClient:
    async def create(self, request: LibraryCreateRequest) -> LibraryWithRevision: ...
    async def list(self) -> LibraryListResponse: ...
    async def get(self, request: LibraryGetRequest) -> LibraryWithRevision: ...
    async def update(self, request: LibraryUpdateRequest) -> LibraryWithRevision: ...
    async def delete(self, request: LibraryDeleteRequest) -> None: ...

    async def upload(
        self,
        library_id: str | UUID,
        attachments: list[PreparedAttachment],
    ) -> dict[str, LibraryDocumentCreateResponse]: ...
    async def list_documents(
        self,
        request: LibraryDocumentListRequest,
    ) -> LibraryDocumentPage: ...
    async def get_document(
        self,
        request: LibraryDocumentGetRequest,
    ) -> PublicIndexedDocument: ...
    async def wait_for_document(
        self,
        request: LibraryDocumentGetRequest,
        *,
        timeout: float = 360.0,
    ) -> PublicIndexedDocument: ...
    async def delete_document(
        self,
        request: LibraryDocumentDeleteRequest,
    ) -> None: ...
MethodContract
create(request)Requires library:write on the path. Returns the active Library at that path, or creates one; it adds no Grant. Keep the returned Library ID. See creation and recovery.
list()Returns the readable, non-deleted Libraries in LibraryListResponse.libraries. When Duale AI cannot resolve read access for the whole list, for example when a broad allow meets a refusal on one Library, it raises BusinessError with HTTP 409 instead of returning a partial list. Use get() with known IDs. See Library limits.
get(request)Returns an active Library’s stable id, current path and tags, revision actor and time, and creation time. A deleted Library returns 410.
update(request)The nested LibraryPatchRequest changes path, replaces the complete tags map, or both. tags={} clears all tags. Changing the path keeps the Library id, requires library:write on both paths, and moves the Library to the new path’s access. While the Library is busy, for example while documents are being written or processed, a path change returns 503 with retryable true and changes nothing; repeat it shortly.
delete(request)Requires library:delete and valid aal3 authentication; use the Dashboard because API-token sessions are aal2. Documents become unrecoverable. Security and data handling explains the effects.
upload(library_id, attachments)Uploads files concurrently and returns queued receipts by attachment key. A batch can partly succeed. On LibraryUploadError, save completed_receipts. After another error, list documents to reconcile prior successes.
list_documents(request)Requires limit from 1 to 500 and nullable cursor. Returns LibraryDocumentPage.documents and next_cursor. Pass the cursor unchanged until None; see pagination. Excludes Recently deleted documents.
get_document(request)Reads one document status without polling.
wait_for_document(request)Polls every five seconds until ready or failed. Positive timeout bounds the entire wait and defaults to 360 seconds.
delete_document(request)Moves one document into the 30-day Recently deleted window. Repeated calls succeed. The core Python client does not expose restore.

The request and response models in the signatures above are importable from dualeai. Ignore the receipt’s location field, and use its library_id and document_id to build every document request. For processing failures, wait_for_document returns a failed document instead of raising an exception. Its document.failure contains RFC 9457 details, or None if the cause is unavailable. Document ingestion failures lists the known causes.

Limits and ceilings

Library limits covers file size, path, tags, access-list limits, upload and deletion windows, and the model’s Library Tool ceilings. The table above covers method arguments.

Capabilities the client does not wrap

Use the Dashboard for these capabilities; the core client provides no methods for them:

CapabilityWhat it does
GrantsAdd and revoke access on one Library’s path. Needs library:admin on that path.
Document previewRead the opening of a document’s extracted text. The only content read-back path.
Document tagsChange a document’s tags after upload. upload() sets none.
Recently deletedList the tenant’s deleted documents.
RestoreReturn a deleted document to its Library. Needs library:write, within 30 days.
  • Capability
    Grants
    What it does
    Add and revoke access on one Library’s path. Needs library:admin on that path.
  • Capability
    Document preview
    What it does
    Read the opening of a document’s extracted text. The only content read-back path.
  • Capability
    Document tags
    What it does
    Change a document’s tags after upload. upload() sets none.
  • Capability
    Recently deleted
    What it does
    List the tenant’s deleted documents.
  • Capability
    Restore
    What it does
    Return a deleted document to its Library. Needs library:write, within 30 days.

See Manage libraries for the supported workflow and SDK errors for DualeAIAuthError and DualeAIConnectionError.

Library actions

library:upload applies to the Tenant; the other actions apply to the Library’s path, library:<path>. Access to one path does not imply access to others. Use these identifiers when requesting a grant; the Dashboard uses plain-language labels.

ActionScopeOperations that check it
library:uploadTenantStarting a document upload in upload() and upload_attachments()
library:readLibrary pathget(), list_documents(), get_document(), wait_for_document()
library:writeLibrary pathcreate() on the requested path; update(), on both paths when the path changes; upload(), tag changes, restore
library:deleteLibrary pathdelete(), delete_document()
library:adminLibrary pathReading and changing the Grants on that path

Scope

library:uploadlibrary:readlibrary:writelibrary:deletelibrary:admin
TenantLibrary pathLibrary pathLibrary pathLibrary path

Operations that check it

library:uploadlibrary:readlibrary:writelibrary:deletelibrary:admin
Starting a document upload in upload() and upload_attachments()get(), list_documents(), get_document(), wait_for_document()create() on the requested path; update(), on both paths when the path changes; upload(), tag changes, restoredelete(), delete_document()Reading and changing the Grants on that path

An upload requires both Tenant-scoped library:upload and library:write on the Library’s path. Creation requires only library:write on the requested path. See LibrariesClient for path reuse and list resolution.

Access and isolation states who grants each action, how deny takes precedence over allow, and which boundaries a Grant does not draw.

BackpressureConfig

Pass backpressure_config= to DualeAISDK to tune cached-activity capacity and the process-local task circuit breaker.

from dualeai.backpressure import BackpressureConfig

config = BackpressureConfig(
    max_concurrent_activities=100,
    max_pending_activities=500,
    failure_threshold=10,
    recovery_timeout_seconds=30.0,
)

Task submission does not use the activity scheduler. A Task submission raises RuntimeError only while the circuit breaker is open.

MockSDK

MockSDK includes keyed task responses and an in-memory sdk.libraries client:

import asyncio

from dualeai import LibraryCreateRequest
from dualeai.testing import MockSDK


async def main() -> None:
    async with MockSDK() as mock:
        mock.set_mock_response("Summarize invoice", {"result": "data"})
        result = await mock.mock_ask("Summarize invoice")
        print(result)

        library = await mock.libraries.create(LibraryCreateRequest(path="tests/invoices"))
        print(library.id)


asyncio.run(main())

mock.libraries runs the core Library workflow in memory; wait_for_document() completes queued documents. mock_ask() looks up a saved response without running ask(), streaming, Tool dispatch, or terminal errors. To exercise ask() without a live connection, inject a mock through transport=.

The in-memory Library client does not enforce permissions or simulate file storage, asynchronous processing, or retention. Its delete methods remove records immediately. Use it to test application flow, then verify those service boundaries separately.

Versioning and status

The SDK is in Public Preview. A feature page states its own release stage when that stage differs from the rest of the SDK. To report the version installed in your Python environment, run:

python -m pip show dualeai