Skip to main content

dialt-sdk

Version 0.39.0 includes breaking alpha API cleanup. Read the migration guide before upgrading.

Version 0.41.0 requires DialtMode.instructions. Dialt's platform prompt gives the agent no role, so your instructions say who the agent is, whose side it is on, what it knows and what it should do on the call. DialtMode() without them raises ValueError, and the server refuses such a session with missing_instructions. To keep the previous default behaviour, pass instructions="You are a helpful voice assistant on a phone call. If a request is missing a key detail, ask one concise clarifying question."

The headless Python SDK for the Dialt realtime voice API. Dialt manages speech recognition, turn-taking, interruption handling, speech generation, reasoning and tool orchestration through a single API; this SDK connects telephony bridges, services, evaluations and custom devices to it.

Caller input is mono 16 kHz. Set output_sample_rate on DialtSession.connect to 8, 16 (the default) or 24 kHz assistant audio. Each audio event reports the selected rate in audio_sample_rate; the samples remain mono Float32.

Install the dialt-sdk distribution and import its public API from dialt.

uv add dialt-sdk

Register a Python tool

Define an async function once. ToolRegistry derives its name, description and JSON Schema; pass the generated definitions to the ordinary session configuration. The decorator leaves the function callable from your application.

from dialt import DialtMode, ToolRegistry

tools = ToolRegistry()

@tools.register(read_only=True, recorded_bridge="auto")
async def lookup_order(order_id: str) -> dict:
    """Look up the current order status.

    Args:
        order_id: The customer's order identifier.
    """
    return await order_store.lookup(order_id)

mode = DialtMode(instructions="Help callers with their orders.", tools=tools.definitions)
# In your application's handler for a broker-dispatched tool_call:
result = await tools.call(event.data["name"], event.data["args"])
# Set verified=True only after your application has established successful completion.
await session.send_tool_result(event.data["id"], result, outcome="succeeded", verified=True)

order_store, session and event above belong to your application. For a complete text-session example with tracked tasks, cancellation and quiet progress, see tool_updates.py. Run it with DIALT_API_KEY in the environment:

uv run --project sdk/python python -u sdk/python/examples/tool_updates.py

Keep the event consumer running while tools execute so audio, cancellation and other calls can still be handled. The registry does not consume events, schedule tasks, retry operations, infer success or grant permission. Your host owns these decisions and sends exactly one terminal result per dispatched call. Only execute a broker-dispatched tool_call; declaring a permissioned tool does not authorize running it directly.

Registration contract

  • tools.register(function, **options) and @tools.register(**options) register async functions; @tools.register uses defaults. Bound async methods work too.
  • Parameter annotations support str, int, float, bool, None, Literal values (strings, integers, booleans, null), unions, list[T] and dict[str, T], recursively. Future annotations are resolved with Python's get_type_hints; referenced types must be resolvable there.
  • The cleaned docstring is the tool description, including any parameter explanations. Override name or description if needed. Required parameters have no Python default: str | None still requires an argument unless a default is provided. Return annotations do not define the result envelope.
  • Registration rejects duplicate names, missing/unsupported parameter types, non-async functions, positional-only parameters, variadic parameters and invalid defaults. call rejects missing, extra or incorrectly typed arguments before execution, without coercion; booleans are not numbers and non-finite floats are rejected. Each invocation receives its own argument copies.
  • Existing tool policy options such as requires_permission or deferred pass through unchanged. Safety and duration are never inferred from the function name or annotation.
  • tools.definitions returns an independent JSON-serializable manifest, usable at session start, with set_tools, or in eval cases. Custom JSON Schemas still use ToolDefinition directly; a registry's parameters cannot be overridden. No broker frame or dependency is added.

Choose what the caller hears

Routine updates should normally stay quiet. Use speech for information that helps the caller at that moment, and send the final result once the operation has finished.

Intent Python API Delivery
Update status quietly send_tool_progress(id, note) Context only; no new speech.
Add structured facts quietly send_tool_partial_result(id, content) Context only; no new speech.
Offer a useful spoken milestone send_tool_partial_result(id, content, reply=True) Best effort when no reply or caller speech is in flight. Skipped speech is not queued; facts stay in context.
Finish the operation send_tool_result(id, content, outcome=..., verified=...) Ends the call and informs its answer. For background completion, notify_on_complete=True requests delivery when the floor is free.

Progress and partials never complete the call. Do not send a spoken partial and then repeat the same information as a terminal result. reply must be a boolean, so a string such as "false" cannot accidentally request speech. A request to speak is not a playback receipt.

Send outcome and verified with the terminal result whenever you can establish them. outcome="succeeded", verified=True lets the assistant confirm the action; send it when you hold proof, such as a confirmation ID. outcome="unknown" (the default) suits queued or fire-and-forget work, and the assistant says it cannot confirm yet. Failures send outcome="failed". See report the outcome.

For a deferred tool, notify_on_complete=False keeps its final result in context without a proactive completion wake. It does not suppress an ordinary call's answer. A partial with interaction is a separate, correlated request for a caller decision; use it only when the work needs an answer. See the background tool guide for deferral, decision interactions and their acknowledgements.

A string greeting is the conversation's first assistant turn, not presentation-only audio. It enters model context and is bargeable in voice mode; when interrupted, playback stops and only the heard prefix remains in context. Use greeting=False to disable it.

deferred: True permits a tool call to become a background job after the host sends send_tool_deferred: the call can outlive the voice turn, the conversation carries on while the host works, and with notify_on_complete the agent delivers the result when the floor is free, even if the caller has moved on to another topic. For work that should release the voice turn immediately, call send_tool_deferred(id, handle, status_label=...) before continuing it in the background. The handle identifies that call, not your worker: mint a fresh one per call (e.g. f"job-{call_id}") and check the returned acknowledgement; re-using a live handle is rejected as handle_in_use, and a rejected defer leaves the call on the ordinary tool timeout, so it expires while you think it is backgrounded. Keep a handle-to-worker map if several calls should feed one long-running worker. For proactive host announcements, call await session.inject_context(text, role="context", reply=True, message_id="job-42"). It returns the broker's authoritative acknowledgement (accepted plus optional retryable/detail). Context is appended even while busy; reply_started: false, queued: true means its reply waits for the next idle boundary. Waiting context updates coalesce into one reply. For a typed user turn, the same message_id and input_source="text" appear on the final ASR event; spoken input uses input_source="voice". The role defaults to "context", reply defaults to False, and omitted message IDs are generated by the SDK. Text is limited to 1–2000 characters. An accepted message_id is an idempotency key for the logical session. After an acknowledgement loss, retry the identical payload and ID (including after resume) to replay delivery proof without injecting a duplicate turn. Reusing an accepted ID with different content is rejected; the broker retains up to 512 accepted receipts per logical session.

Save session.resume_token from the connected session; after a transport loss, pass it as resume_token=... to the replacement DialtSession.connect(...) call so pending jobs are re-announced as tool_deferred_resume events within the broker's bounded resume window.

Automatic session recovery

DialtSession.connect(..., auto_reconnect=True) (the default) handles that transport loss for you: on any abnormal close (a WS close code other than 1000, e.g. 1006 loss, 1011 upstream lost, 1012/1013 broker drain/handoff), the SDK redials the same URL with the retained start-frame configuration (voice, instructions, tools, mode, user, timezone, capabilities) plus the latest resume_token, so the broker (possibly a different instance) restores the session from its Postgres stash. Redials back off exponentially (0.5s base, capped at 5s, up to 12 attempts by default; tune with reconnect_base_s, reconnect_max_s, max_reconnect_attempts), and resume_token keeps rotating on every accepted ready, including a resumed one.

session.state reports "connecting", "live", "reconnecting", or "closed", and session.events() surfaces the recovery itself:

  • reconnecting ({"attempt": 1, "code": <close code>}): the connection dropped and a redial is under way. events() keeps running, it does not end here.
  • reconnected ({"attempt": <n>}): the redial succeeded and the session is live again.
  • resume_failed ({"code": "resume_failed", "detail": ..., "retryable": False}): the broker rejected the resume (the window closed, or the token was already consumed). Terminal: resume_token is cleared and events() ends right after, with no session_end.
  • error ({"code": "reconnect_failed", "detail": ...}): every redial attempt failed and the chain gave up. Terminal, events() ends right after.
  • A clean server close (WS code 1000, e.g. an idle sign-off or end_call) still ends the session with session_end and never reconnects, same as before.

While state == "reconnecting", send_audio/stream_audio silently drop the frame instead of raising or buffering it (buffered stale audio must never flush into the resumed session once it comes back), and every other control method (send_text, inject_context, set_voice, set_tools, handoff_agent, and the rest) raises DialtError(code="reconnecting", retryable=True) so a caller can retry once reconnected arrives instead of getting the ambiguous connection_closed. set_voice, set_instructions, and set_tools fold their change into the retained mode so a later reconnect replays it; set_tool_choice folds only a non-one_shot choice, and only when the retained mode already has a non-empty tools list (matching set_tools resetting tool_choice to "auto" on the wire). Pass auto_reconnect=False to get the pre-recovery behavior instead: any drop just ends events().

The SDK deliberately does not own capture, playback, pacing or echo cancellation. Live playback integrations must implement the playback contract.

For a text session, keep the same conversation configuration and replace the media loop with committed turns:

async with await DialtSession.connect(
    "wss://api.dialt.com/v1/realtime",
    api_key=os.environ["DIALT_API_KEY"],
    mode=DialtMode(modality="text", instructions="Help customers with their orders."),
) as session:
    await session.send_text("Where is order A123?")
    async for event in session.events():
        if event.type == "utterance":
            print(event.data["text"])

Text mode is WebSocket-only and emits no audio. Model behavior, tools, greeting, history and conversation lifecycle events remain the same.

DialtMode(end_call=True) lets the agent end the session itself through the managed end_call(farewell) tool: one goodbye is sent, then session_end_requested arrives with the farewell argument. Reply text in that turn that ends a sentence is the goodbye, and the farewell is not sent after it; reply text that stops mid-sentence is followed by the farewell, which completes it; with no reply text the farewell is the goodbye. Text the agent streamed before calling end_call follows the same rule, and the farewell is sent if none of that text was heard. That advisory does not confirm that the peer heard or acted on the farewell. Stop automatic new-turn injection but keep any local output already queued; a client that needs post-close audio drain owns that policy. The server waits a short, cancellable grace for a real user turn before it closes. Off by default; the host then ends the session with wrap_up or by closing. DialtMode(end_call_when="the user asks to end the call") states your own ending condition instead of Dialt's default (the caller asks you to end the call); the condition is declared to the agent as part of the tool.

Continue with another agent, instructions, voice switch, tool swap and wrap-up

set_instructions(text) replaces the session instructions from the next reply on; a reply already in flight, tool answer included, finishes under the old ones, and the platform prompt above them is untouched. set_voice(key) switches the roster voice from the next reply on, confirmed by a voice event. set_tools(tools) replaces the client tool manifest for an agent whose capabilities change by call phase.

To initialize another agent at a safe same-session boundary, use await session.continue_with_agent(instructions=..., tools=..., voice=..., context=..., tool_choice=..., tool_choice_one_shot=..., operation_id=...). The SDK generates operation_id when omitted; it only correlates this live request and is not an idempotency or retry token. The call waits through an optional queued acknowledgement for the final applied or rejected acknowledgement. On timeout or disconnect its outcome is uncertain and it is never retried. Await it before a mode setter. After an applied handoff, the caller recipe may use its separately acknowledged inject_context(..., reply=True) call to make the new agent respond first.

When a tool result is the boundary, prefer one atomic operation: send_tool_result(..., continue_with={"instructions": ..., "tools": ...}). Dialt records the result, retires the old agent's plan, initializes the next agent and starts its reply as one transaction. The old handoff_agent method and handoff result option remain compatibility aliases. Use tool_choice with tool_choice_one_shot=True in the continuation when the incoming agent must call a grounding tool before its first reply; the broker applies the constraint before starting that reply and restores auto afterward.

The individual setters remain available when you need a narrower change. Send them between replies (after done); with new_speaker the server refuses set_instructions (instructions_busy, retryable) from the moment a turn is accepted until its reply's done. Passing the caller to a human is a different operation: a permission-gated client tool whose host moves the call leg, then request_wrap_up.

async for event in session.events():
    if event.type == "tool_call" and event.data["name"] == "handoff_to_agent":
        await session.send_tool_result(
            event.data["id"], {"handoff_complete": True},
            outcome="succeeded", verified=True,
            continue_with={
                "instructions": SPECIALIST_INSTRUCTIONS,
                "tools": SPECIALIST_TOOLS,
                "voice": "us_female",
                "context": f"Caller: {name}. Reason: {reason}.",
                "tool_choice": {"tool": "lookup_patient"},
                "tool_choice_one_shot": True,
            },
        )

request_wrap_up(reason) asks the model for a graceful sign-off in the voice's register, for a host-enforced time limit or a transfer the host is about to make. It never cuts a reply in flight; the session closes with session_end once the sign-off has played.

See the complete Python reference and the Twilio bridge quickstart.

Policy guidance

Define each rule once in mode.policy with id, when, and do:

{"include_instructions": true, "background_guidance": true,
 "rules": [{"id": "supervisor", "when": "The caller requests a supervisor.",
            "do": "Call request_supervisor and explain the tool result to the caller."}]}

Both switches default true and work independently. Declare and handle the tool normally; existing permission checks still apply. Background checks quietly inject guidance about the reviewed caller or assistant turn without requesting another reply or executing tools. Source revisions, flag updates/retractions, errors and completion events are reported through the ordinary event interface. Delivery is not proof the agent complied.

Policy rules use id, when and do; legacy action configurations are rejected. See the policy contract.

Hosted evals

dialt.evals creates cases and starts runs on the Dialt evals dashboard with your account API key (dk_...; existing ck_... keys remain valid). Cases are JSON files in your repository; upsert_case keeps the hosted copy in step by name, so a re-push updates rather than duplicates.

from dialt import EvalsClient, load_cases

evals = EvalsClient(api_key=os.environ["DIALT_API_KEY"])
cases = evals.upsert_cases(load_cases("evals/"))          # one case per *.json file
run = evals.start_run([c["id"] for c in cases], modality="text")
print(evals.dashboard_url(run["id"]))
result = evals.wait(run["id"])                            # polls until terminal
print(result["status"], [a["status"] for a in result["attempts"]])

A case has name, starter (or target.greeting, when the agent opens the call), target and simulator (each a session mode document, the shape DialtMode.to_wire() produces, so every session option is a case option), fixtures, checks and limits; the field reference is in the evals guide. Both modes require instructions: target.instructions is the agent under test and simulator.instructions is the simulated caller's role and goal. simulator.voice (a GET /v1/voices key) sets the simulated caller's voice, language and register. New run snapshots pin omitted or null values to US Male (us_male); historical snapshots retain their original caller. Set the voice explicitly for your real callers' language and dialect, and rebaseline results after changing it.

start_run(targets=[...]) selects the models under test (dialt, dialt-smart, openai-live, gemini-live); list_targets() shows what each supports in capabilities. Pass dry_run=True first to check every case against every target: it returns the plan, including ignored-setting warnings and the max_external_charge_micros to accept for external models, and starts nothing. Then pass that max_external_charge_micros, and an idempotency_key to retry safely after a lost response. replace_unassessable=True gives an attempt lost to an infrastructure or harness failure, or to a simulated caller that broke script, one fresh replacement within max_replacements, so the run ends with the assessed attempts you asked for. A rejected request raises EvalsError: code is the error code, details["reason"] explains it and a failed preflight lists every problem in details["errors"].

dialt-recipes ships a dialt-evals push evals/ command built on this client.

Relaying two sessions (simulations)

dialt.relay cross-pipes two sessions so one can play the user for the other: the building blocks behind dialt-recipes' dialt-sim and the hosted evals in the Dialt webapp.

from dialt.relay import TextTurnRelay, VoiceTurnRelay

# text: forward each committed target utterance to the simulator once tool work has settled
relay = TextTurnRelay(simulator.send_text)
relay.utterance(event.data["text"])      # on the target's `utterance`
relay.working(active)                    # on `working`
relay.done()                             # on `done`: forwards after TURN_RELAY_SETTLE_S

# voice: the simulator's virtual microphone, a paced stream that never stops
relay = VoiceTurnRelay(simulator)
relay.start()                            # silent frames keep the receiving audio clock running
await relay.audio(event.audio)           # on each target `audio` event: queued, played at real time
await target.send_client_event(          # on the target's `interrupted`: what the simulator
    "playback_stopped", **relay.interrupted(event.data))   # never heard, as a real client reports it
relay.canceled()                         # on the target's `canceled`: drop the rescinded audio
await relay.close()                      # at the end; a line that hangs up first just stops the mic

The broker measures turn-end silence in received audio, so a simulated caller's mic must keep streaming between turns and after an interrupted clip, not only while someone speaks; the receiving side's own endpointer then closes each turn on the trailing silence. Idle frames contain exact zeros. A silent_mic advisory after prolonged idle is expected for this generated audio; it does not stop processing or close the session. Both relays take on_error to surface background failures. An eval harness that expects a simulated caller to make a decision must leave that caller an opportunity to respond before ending its relay. That is an eval-harness decision, not a general done or session_end_requested API guarantee.

WebRTC transport (experimental)

Experimental: the API is stable, but this transport is newly shipped and still being hardened on real networks; ws remains the default and recommended fallback.

Pass transport="webrtc" to DialtSession.connect(...) to carry the session over WebRTC (UDP) instead of the default WebSocket. Requires uv add "dialt-sdk[webrtc]". ws remains the default; most headless callers are fine on it. See the Python guide's WebRTC section.

Licensed under the Apache License 2.0. This license applies to the SDK, not to the hosted Dialt service, its models, or its server-side implementation. Runtime dependencies remain under their own licenses.

Application tool approval

The permission source vocabulary is caller (the default) or application; the old conversation and external values are not accepted.

Set requires_permission: True and permission_source: "application" on a tool to require approval from your application. Omit the source for caller consent verified by AI. Forward permission_pending events whose source is "application" to your approval application, displaying the exact tool, arguments, status_label and expires_at. Keep the session event consumer running while the reviewer decides. In your authenticated approval callback:

await session.resolve_tool_permission(request_id, "approve")  # or "decline"

The authenticated session host is trusted to submit decisions; authenticate the reviewer in your application. Observe permission_resolution in session.events() and check event.data["accepted"] and any rejection reason. Sending is not acceptance or completion. An accepted approval releases the exact stored tool call; handle tool_call and return its result as usual. Caller consent does not satisfy application approval, and the assistant does not ask the caller for it.

Voice selection is optional and uses the server roster from GET /v1/voices. An explicit voice takes precedence over a saved preference, then US Female (us_female) is the shared default. The voice route is independent of Dialt Fast or Dialt Smart. Irish Male keeps the stable key classic, and Circuit (circuit) remains selectable. The regional keys are us_female, us_male, british_male, british_female, mexican_female and brazilian_female. British Female and Mexican Spanish Female retain their keys with new speaking identities. southern_us_female is an accepted alias of us_female, changing the accent from Southern US to general US English. brazilian_male is an accepted alias of brazilian_female, changing the speaker from male to female, including feminine self-reference. Aliases are absent as duplicate picker options; use canonical keys in new integrations. Customer voice conditioning and VoicesClient.create()/delete() are removed. Replace saved conditioned-voice keys with a stock key from GET /v1/voices, available through VoicesClient.list(). Resumed sessions retain their confirmed stock voice identity unless explicitly changed. Irish Male, Chime, Servo, Circuit, Arabic voices and provisional Hinglish Male retain their identities. See the catalogue and migration guide. Empty keys are rejected locally; unknown, retired or unavailable keys return the server's nonretryable invalid_voice error. A rejected mid-session switch leaves the confirmed voice unchanged, including on reconnect. Retired keys must be replaced explicitly.

Capture timing for queued microphone audio

For WebSocket integrations that retain source capture timestamps, enable the existing capture clock. Carry the first sample's monotonic capture timestamp with each PCM frame:

session = await DialtSession.connect(
    api_key=api_key, capture_clock=True,
    mode=DialtMode(instructions="You are the front desk of a dental clinic."))
# Captured at the microphone source, before application/network queues:
await session.send_audio(pcm_frame, capture_ms=first_sample_monotonic_s * 1000)

Supply a timestamp for every frame, including silence. Do not replace it with the current send time after dequeueing. Missing/invalid timestamps fail locally. stream_audio has no capture timestamps, so use send_audio for this option. Default untimed sends remain compatible. WebRTC uses its media clock instead.

The server can then distinguish capture spacing from delivery bursts in the recording. The first-frame arrival anchor remains approximate; this does not measure absolute one-way network delay or physical speaker playback.

Metadata

Release files for dialt-sdk 0.45.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dialt-sdk 0.45.0
File Size Uploaded Provenance
dialt_sdk-0.45.0.tar.gz 94.7 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for dialt-sdk 0.45.0
File Interpreter ABI Platform
dialt_sdk-0.45.0-py3-none-any.whl Python 3 none any Details

Total release size: 160.7 kB

Release files / dialt_sdk-0.45.0.tar.gz

Download URL dialt_sdk-0.45.0.tar.gz
Size 94.7 kB
Tags Source
SHA-256 checksum
How to use checksums
30c170a753656cd845d28e2fecdb6d8359532fd68266ed1840943781e87d9588
BLAKE2b-256 checksum
How to use checksums
5b27be9f83b29613da2e2a5b1b407ed7f5aa950c64b9b35e1f01f8dc9319285e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / dialt_sdk-0.45.0-py3-none-any.whl

Download URL dialt_sdk-0.45.0-py3-none-any.whl
Size 66.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
9f310e5a3cae2943e35629fc9d3a918e98a44ff7e05efd5a8b6c75d291d3b502
BLAKE2b-256 checksum
How to use checksums
31a4ac43ea9c195ef40aee3a6c62d92208eeaa6002ebc0c54ebab5516389d8b1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for dialt-sdk 0.45.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Release history Release notifications | RSS feed

This release

0.45.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page