Skip to main content
The Connectivity half of the SDK. An AgentRunner is the long-lived process that receives calls; a Session is your control surface for one live call. This page is the reference for both.

Install

Source: unpod-ai/unpod-python-sdk.
The Quickstart documents every environment variable.

The runner

AgentRunner holds a WebSocket connection to the Unpod orchestrator. When a call is dispatched to your agent, the runner invokes your entrypoint with a CallContext.
Animated AgentRunner dispatch diagram showing the Unpod orchestrator connected to AgentRunner over WSS, heartbeats and capacity reporting, dispatch into entrypoint CallContext, and the Session run loop.
agent_id is the runner agent ID - a short string you choose, matching the agent_id in your Speech Pipe config. Not the pipe’s UUID. See IDs You’ll Meet.

Constructor

Runner lifecycle hooks

React to runner-level events (distinct from per-session hooks):
This is the runner-level call_end (signature (ctx, final_state)), fired once per call on the runner. The session-level call_end (@ctx.session.on("call_end"), signature (final_state)) fires inside a single call and is where you read the telephony end_reason via ctx.session.data.get("end_reason"). See Call Lifecycle.

CallContext

Every call to your entrypoint receives a CallContext:
ctx.agent_id is the agent the call was dispatched for (carried on call.started); ctx.runner_id is the agent_id this runner process was constructed with. They match for a single-agent runner and differ for a multi-tenant one — route on ctx.agent_id, not ctx.runner_id.

The Session

ctx.session is your interface to the live call: speak, interrupt, transfer, record, end - all from inside your entrypoint.

Speaking

Interrupting

Transferring

A cold transfer drops your session the moment it is initiated. For a warm handoff, use the out-of-band client.sessions.transfer(..., mode="warm") - see below.

Ending

Common reasons: "completed", "no_response", "error", "transferred", "max_duration".

Recording control

Pause/resume requires recording to be enabled on the Speech Pipe (recording=True); otherwise these calls are ignored.

Per-call data

session.data is a plain dict scoped to the current call:

The main loop - run()

session.run() keeps the call alive. It reads bridge events, fires your hooks, routes each transcribed user turn to your dialog adapter’s stream(), and pipes the reply tokens to TTS.

Live metrics

Latency/cost/token fields populate only if you feed the tracker via metrics.record_turn(...) (e.g. from the turn_complete hook). Out of the box only duration_s is meaningful — for real per-turn numbers use the llm_call / turn_complete hooks. See Metrics, Cost & Observability.

Session API reference

Out-of-band session control

Act on a live session from outside the call - your backend, an ops tool - via the Management SDK, targeting it by session ID:

Running in production

Monitoring

Graceful shutdown

Send SIGTERM (standard for containers and systemd). The runner stops accepting dispatches, waits up to drain_timeout_s for active calls, then exits. Or call await runner.shutdown() yourself.

Multiple runners

Run multiple AgentRunner processes with the same agent_id across machines. The orchestrator load-balances on reported capacity - no shared state needed.

Next steps

Bring Your Agent

Plug your existing brain into session.dialog_machine.

Hooks & Events

React to every turn, interruption, silence, and lifecycle event.