dialt-sdk
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.
dialt-sdk replaces the deprecated converse-sdk distribution. New code imports from dialt.
Existing applications can install the final converse-sdk compatibility release while migrating imports from converse_sdk to 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, expected_duration="instant")
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.registeruses defaults. Bound async methods work too.- Parameter annotations support
str,int,float,bool,None,Literalvalues (strings, integers, booleans, null), unions,list[T]anddict[str, T], recursively. Future annotations are resolved with Python'sget_type_hints; referenced types must be resolvable there. - The cleaned docstring is the tool description, including any parameter explanations. Override
nameordescriptionif needed. Required parameters have no Python default:str | Nonestill 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.
callrejects 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_permissionordeferredpass through unchanged. Safety and duration are never inferred from the function name or annotation. tools.definitionsreturns an independent JSON-serializable manifest, usable at session start, withset_tools, or in eval cases. Custom JSON Schemas still useToolDefinitiondirectly; a registry'sparameterscannot 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.
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, durable 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.
DialtMode(turn_end_threshold=0.2) opts one voice session into faster, less conservative
Ink finalization. The accepted range is 0.05 through 0.5; lower values are more conservative.
Leave it unset for the broker's deployment default. Use an override only when the application's
pause distribution has been evaluated, because faster finalization can increase mid-thought ends.
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). 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.
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://dialt.com/ws",
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: the farewell is spoken, session_end_requested arrives with the
farewell, and the server closes after a short grace unless the user speaks. 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.
Mid-session agent handoff, 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 persona 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.
For an atomic same-session handoff, use await session.handoff_agent(instructions=..., tools=..., voice=..., context=..., 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.
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)
elif event.type == "done" and handoff_pending:
await session.handoff_agent(
instructions=SPECIALIST_INSTRUCTIONS, tools=SPECIALIST_TOOLS,
voice="southern_us_female")
await session.inject_context(
f"The caller {name} has just been passed to you by intake; reason: {reason}.",
role="context", reply=True)
request_wrap_up(reason) asks the model for a graceful sign-off in persona, 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.
A policy agent beside the call
DialtMode(policy=...) declares rules the broker's judge watches the transcript for, each with
the instruction it injects when the rule applies and how it lands: speak_now makes the agent
reply at once, next_turn lets it act at its next turn. The judge sees only the policy and the
transcript, each rule is raised once per session, and every raised rule arrives as a
policy_flag event (rule, action, evidence, delivered). The agent's instructions and
tools are untouched; the injected context is the whole effect, so a flag never invalidates the
prompt cache. Contract and timing: docs/policy-agent.md.
mode = DialtMode(instructions=..., tools=..., policy={
"subject": "a clinic appointment call",
"rules": [
{"id": "emergency", "action": "speak_now",
"when": "The caller says they have, right now, symptoms that may need urgent care.",
"do": "Tell the caller to hang up and call emergency services now, then end the call."},
{"id": "clinical_advice", "action": "next_turn",
"when": "The caller asks whether symptoms are serious or whether to change a medicine.",
"do": "Do not give medical advice. Say a clinician has to answer that; offer a callback."},
],
})
async for event in session.events():
if event.type == "policy_flag":
log(event.data["rule"], event.data["delivered"])
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.
converse-recipes ships a converse-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 converse-recipes' converse-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() # line noise flows from now on, like a live line
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. Both relays take
on_error to surface background failures.
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.
Policy occurrences and protected tools
mode.policy supports optional recheck_corrections, batch_guidance, report_checks,
wait_for_check and block_on_error. Rules accept frequency (once_per_session by default,
once_per_turn for each matching source) and block_tools. Existing defaults are unchanged.
Both SDKs forward policy_flag, nonfatal policy_error, policy_check and policy_settled
through their ordinary event interface. Tracked transcript events include utterance identity,
revision and policy version; reduce flag updates/retractions by occurrence identity.
Tool restrictions apply before dispatch, including after application approval. A flag's
delivered means its guidance entered context, not that the agent complied. Completion events
are opt-in; see the policy contract.
Release files for dialt-sdk 0.28.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dialt_sdk-0.28.0.tar.gz | 62.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dialt_sdk-0.28.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 110.9 kB
Release files / dialt_sdk-0.28.0.tar.gz
| Download URL | dialt_sdk-0.28.0.tar.gz |
|---|---|
| Size | 62.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a0e007c366173db97a46f7b0aac6860a976bfab6fb39c91844236ec3ea80b385
|
|
BLAKE2b-256 checksum How to use checksums |
81fbe45a9cd1b044bee9441c30aa24abd9ff1bc3b9221a5477835106f738e2da
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","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.28.0-py3-none-any.whl
| Download URL | dialt_sdk-0.28.0-py3-none-any.whl |
|---|---|
| Size | 48.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e71533a3f82113d56c873d4e1a6c27ed2dd5f21af6241e11cb635faef06acb25
|
|
BLAKE2b-256 checksum How to use checksums |
d4371ae7d056bcb9557675e9051fe6786e3d22ea787a2b7e8fc0e3937d3bbfc9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","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}
|