Architecture

View as Markdown

AgentDuet is a connector client. The application process maintains one control connection, opens short-lived sessions per subscriber, and drives per-call media. Speech models run outside the SDK and exchange PCM with each Call.

on_incoming_call on_incoming_message open_session open_session process_call / make_call send_message answer / dial / connect /whisper / barge / spy caller / callee audio_stream send_audio / clear_send_audio_buffer SessionManager Call notification IncomingMessage Session Call WhatsApp Call control PCM in PCM out Your realtime model

Three objects

1. SessionManager

One persistent WebSocket to the AgentDuet connector for the life of your process.

  • Authenticates (API key + connector UUID, or mTLS)
  • Heartbeats and reconnects with exponential backoff
  • Delivers inbound call and WhatsApp notifications
  • Buffers notifications that arrive between connect and start() / run_forever(), so nothing is dropped while you register handlers
async with SessionManager(config) as sm:
@sm.on_incoming_call
async def on_call(noti): ...
@sm.on_incoming_message
async def on_message(msg): ...
await sm.run_forever()

2. Session

Ephemeral, per-subscriber handle from sm.open_session(session_id, subscriber).

You need to…Call
Attach an inbound callawait session.process_call(noti)Call
Place an outbound callawait session.make_call(Address.telco(...))Call
Send WhatsAppawait session.send_message(SendWAMessage(...))

open_session is get-or-create. Reuse a session_id to continue a conversation; create a new one (new_session_id()) to start fresh. Never share one session id across different subscribers.

Server sessions have a ~30-minute sliding idle TTL. The next process_call / make_call / send_message transparently re-opens if needed.

3. Call (voice connection)

Each Call owns a lazy voice WebSocket. The SDK opens media the first time you need audio (answer, dial, connect, send_audio, …) and tears it down when the call ends. You never manage that socket yourself.

Audio is always per party: call.caller.audio_stream() and call.callee.audio_stream(). The agent speaks with call.send_audio(...).

What AgentDuet owns vs what you own

AgentDuetYour application
Connector auth and reconnectAPI keys for Gemini / OpenAI / etc.
Call routing to your processBusiness rules, CRM, calendars
Call commands and PCM transportModel sessions and tool execution
WhatsApp inbound delivery and outbound sendPayload parsing and reply text
Outgoing audio buffer + clear_send_audio_bufferWhen to clear (on model interrupt)
At-least-once notification deliveryDedup on call_id / IncomingMessage.id

Delivery model

Inbound notifications are connector-wide competing-consumer: they may arrive concurrently and out of order. Correlate by (subscriber, participant) yourself. Established voice calls are unaffected when the control link reconnects; if the voice link drops, you get hangup + CallClosedError.

Next Step