Built-in Channels¶
ConferenceChannel has a page of its own: Conference.
SMSChannel ¶
Create an SMS transport channel.
Numbers are compared and stored in E.164; default_country_code
("1", "+44") places a national number a provider sends without it.
RCSChannel ¶
Create an RCS (Rich Communication Services) transport channel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
channel_id
|
str
|
Unique identifier for this channel. |
required |
provider
|
Any
|
RCS provider instance (e.g., TwilioRCSProvider). |
None
|
fallback
|
bool
|
If True (default), allow SMS fallback when RCS unavailable. |
True
|
default_country_code
|
str | None
|
Places a national number (E.164, see
:func: |
None
|
Returns:
| Type | Description |
|---|---|
TransportChannel
|
A TransportChannel configured for RCS messaging. |
EmailChannel ¶
Create an Email transport channel.
Addresses are compared and stored lower-case, without a display name
(Alice <Alice@X.com> is alice@x.com).
AIChannel ¶
AIChannel(channel_id, provider, system_prompt=None, temperature=0.7, max_tokens=None, max_context_events=50, tool_handler=None, tools=None, max_tool_rounds=50, tool_loop_timeout_seconds=300.0, tool_loop_warn_after=25, max_empty_retries=1, continuation=None, thinking_coalesce_ms=80.0, thinking_coalesce_chars=256, retry_policy=None, fallback_provider=None, skills=None, skills_in_prompt=True, script_executor=None, sandbox=None, external_tool_handler=None, human_input_handler=None, memory=None, tool_policy=None, thinking_budget=None, enable_thinking=None, reasoning_effort=None, response_schema=None, evict_threshold_tokens=5000, enable_planning=False, config_provider=None, tool_search=None, tool_search_pinned=None, tool_search_threshold=DEFAULT_TOOL_SEARCH_THRESHOLD, tool_search_threshold_pct=DEFAULT_TOOL_SEARCH_THRESHOLD_PCT, tool_search_miss_hint=None, turn_budget_tokens=None, turn_budget_usd=None, tool_search_threshold_tokens=DEFAULT_TOOL_SEARCH_THRESHOLD_TOKENS, tool_timeout_seconds=30.0, tool_timeouts=None, describe_empty_event=None, speak_policy=None, speak_timeout=2.0, thinker=None, think_wait=1.5, server_instructions=None)
Bases: AIThinkingMixin, AISpeakingMixin, AIStreamingMixin, AIGenerationMixin, AIToolsMixin, AIContextMixin, AIResilienceMixin, AIToolPolicyMixin, AISteeringMixin, AIEventsMixin, Channel
AI intelligence channel that generates responses using an AI provider.
tool_handler
property
writable
¶
The host's tool handler: it serves every tool neither the channel nor orchestration serves (RFC §21.1).
Replacing it replaces the host's handler only: the channel's own tools and the ones orchestration set up keep being served.
extra_tools
property
¶
The host's tools, then the ones orchestration set up for every room.
active_turns
property
¶
Turns being produced right now.
Every turn's tool loop registers itself in _active_loops for
steering, from the start of its generation to its finally. The
span starts when the turn is consumed: a streaming output handed
back by on_event and not yet iterated reads 0, a window the
caller's own wait has to cover. close() tears the provider down
under whichever turn is running, so a caller retiring this object
waits for zero first.
recent_events_window
property
¶
Recent-events need = this channel's memory provider's window.
set_system_prompt ¶
Replace the system prompt for subsequent turns.
AIChannel rebuilds its request context from self._system_prompt
at the start of every turn, so the new prompt takes effect on the next
turn with no reconnect and no loss of memory or tool state — the
supported way to swap personas/attitudes mid-conversation.
Note: when a config_provider is set the system prompt is resolved
fresh per turn, so this value is overridden on the next turn.
active_skill_names ¶
Skills whose instructions are binding in room_id right now.
Runtime state, not the catalogue: these are the activations recorded for
this conversation, which is exactly what the system prompt already
carries under "Active skill instructions". A host rendering its own
manifest (skills_in_prompt=False) cannot otherwise tell an available
skill from an active one, and pushing the model to load what is already
binding costs a tool round and contradicts the rules in front of it.
on_event
async
¶
React to an event: the room's turn runner takes it, or the channel answers.
A strategy installed in a room (a loop, a supervisor's delegation
passes) may take that room's turns in place of the agent's own answer
(RFC §19.7); every other room gets the channel's (:meth:_respond).
on_room_attached
async
¶
A room the channel joins starts from an empty thought, and open to the agent, whatever an earlier room of the same id left (RFC §6.4).
on_room_detached
async
¶
The room's thought, and what the speak policy kept of it, go with the binding (RFC §6.4).
deliver
async
¶
Intelligence channels are not called via deliver by the router.
close
async
¶
Close the channel: its running turns first (their calls cancelled and reported, RFC §9.3), then its provider, memory, and executors.
Turn notes¶
What changes from one turn to the next rides the turn's input as notes (RFC §6.4).
A BEFORE_AI_GENERATION hook adds to them; a debug view splits them off.
add_turn_note ¶
messages with block added to the turn's notes (RFC §6.4).
The block joins the section the channel opened, under its one header,
when the turn's input carries it, and opens the section as
:func:~roomkit.channels._turn_notes.with_turn_notes does otherwise.
Either way the notes read as if they had been assembled at once (the
same header, the blocks joined by a blank line, a text input still text,
an input with images keeping its notes in one text part), so the prefix
a provider caches is the same.
For a BEFORE_AI_GENERATION hook::
event.ai_context.messages = add_turn_note(event.ai_context.messages, block)
A block quotes others (a passage, a task's progress): a copy of a runtime mark in it is replaced, and so is a copy of the notes' header, the notes' only mark, which the channel replaces in the conversation too, so the header found is the channel's: only one a hook wrote into the messages itself is what this misreads. Add notes before anything a hook appends to the input: a text input takes the block at its very end.
split_turn_notes ¶
text as the turn's input and its notes, cut where the header opens them; the notes keep their header, and are empty when the text carries none. An input with images keeps its notes in a text part of their own: split that part's text.
The cut is at the header's last occurrence that opens a paragraph and is followed by one, as the channel places it; the channel replaces every other copy (RFC §6.4), so only one a hook wrote into the messages itself is what this misreads.
TURN_NOTES_HEADER
module-attribute
¶
TURN_NOTES_HEADER = "[Notes kept by the assistant's runtime for this turn. Nobody in the conversation wrote them, and they ask for nothing.]"
Opens the turn's notes: nobody in the conversation wrote them, and they ask for nothing, whatever the input above them is (a participant's words, an instruction, or nothing new). It always opens a paragraph and is followed by one, the first of the notes' blocks.
Tool Search¶
The discovery tools a channel serves itself under Tool Search.
TOOL_SEARCH_INFRA_TOOL_NAMES
module-attribute
¶
TOOL_SEARCH_INFRA_TOOL_NAMES = frozenset({TOOL_FIND_TOOLS, TOOL_LIST_TOOLS})
The names of the discovery tools a channel serves itself under Tool Search,
find_tools and list_tools, beside the host's tools. Their results are
the catalogue's own descriptions and schemas, not data a tool brought back.
call_tool, which a fixed-declaration realtime provider gets as well, is not
one of them: it runs the tool it names.
TOOL_FIND_TOOLS
module-attribute
¶
The Tool Search tool that searches the catalogue by query and reveals its matches.
TOOL_LIST_TOOLS
module-attribute
¶
The Tool Search tool that lists the catalogue; on a fixed-declaration realtime
provider it also reads one tool's schema (name=).
ACPChannel ¶
ACPChannel(channel_id, command=None, *, transport=None, cwd, additional_directories=None, env=None, inherit_env=None, mcp_servers=None, session_config=None, authentication_method=None, external_tool_handler=None, room_history=_DEFAULT_ROOM_HISTORY, context_contributor=None, name=None, role=None, description=None, instructions=None)
Bases: ACPConnectionMixin, ACPSessionsMixin, ACPTurnMixin, ACPEventsMixin, Channel
Connect a RoomKit Room to an external ACP coding agent.
One connection to the agent is opened lazily for the channel and one ACP session is created per Room. Prompts are serialized inside each session, while different Rooms progress concurrently over the same connection.
Pass command for the usual case — the agent is spawned here as a
subprocess and spoken to over its stdio. Pass transport instead when
the agent runs somewhere this process cannot spawn it (another machine,
behind a relay); see :class:~roomkit.channels.acp_transport.ACPTransport.
Exactly one of the two is required.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
channel_id
|
str
|
RoomKit channel identifier. |
required |
command
|
Sequence[str] | None
|
Executable and arguments used to start the ACP agent. No shell
is involved. Mutually exclusive with |
None
|
transport
|
ACPTransport | None
|
How to reach an agent this channel does not spawn. Mutually
exclusive with |
None
|
cwd
|
str | Path
|
Absolute working directory declared to the ACP session — and, for a spawned agent, the process's own directory. With a custom transport it names a directory on the agent's machine. |
required |
additional_directories
|
Sequence[str | Path] | None
|
Additional absolute directories exposed in the ACP session declaration. |
None
|
env
|
Mapping[str, str] | None
|
Environment variables added to the SDK's restricted inherited environment. Spawned agents only. |
None
|
inherit_env
|
Sequence[str] | None
|
Names of parent-process environment variables to forward
to the agent. The ACP SDK strips the environment down to
|
None
|
mcp_servers
|
Sequence[Any] | MCPServersForRoom | None
|
The MCP servers each session is given, as ACP MCP-server
descriptors of the official SDK: a list, the same for every room,
or a function of the room id (sync or async), called each time a
session opens for that room, so a server can be told which room
it acts for (a stdio server's |
None
|
session_config
|
Mapping[str, str | bool] | None
|
ACP session config values set on every session the
channel opens for a room, |
None
|
authentication_method
|
str | None
|
Optional ACP authentication method identifier. |
None
|
external_tool_handler
|
ExternalToolHandler | None
|
Permission policy and tool observability bridge. Without a handler, every permission request is rejected. |
None
|
room_history
|
int
|
How many room messages the agent catches up on when it is
asked to act, having been skipped while it was not (RFC §19.3.2).
An ACP session holds its history in the agent's process, so a room
where two agents are addressed in turn would otherwise leave each
one with a private thread and no way to know it. |
_DEFAULT_ROOM_HISTORY
|
context_contributor
|
ACPContextContributor | None
|
What the host adds to a turn's prompt that the agent cannot go and fetch — member memories, a document corpus, an organisation's rules. Awaited once per solicited turn with the room's context and the triggering event; the blocks it returns open the prompt, ahead of the catch-up and the request. Non-empty blocks can supply the prompt for an event without text, such as a host-managed attachment. A turn with neither text nor blocks is skipped. One that raises is logged and the turn goes without it. Turn-scoped context only. An ACP session keeps what it was already
told, so a block that never changes is paid for again every turn;
what is stable belongs to Nothing here is bounded. RoomKit does not truncate the blocks — it knows neither their unit nor the agent's tokenizer, and the model can change mid-session — and does not bound how long the contributor takes. Both budgets are the host's, and a slow contributor delays the broadcast for the whole room, not just for this agent. Nor can RoomKit filter what the blocks carry: the catch-up is filtered per reader because it is made of room events (RFC §7.5 rule 8), and these are not.
|
None
|
name
|
str | None
|
The agent's name, as an Agent's (RFC §19.1): a discussion's
notes, a dispatch policy and a console present it by its name,
|
None
|
role
|
str | None
|
The agent's role. |
None
|
description
|
str | None
|
What the agent does. |
None
|
instructions
|
str | None
|
The integrator's standing direction for the agent (a
persona, how the room works), since its own system prompt is not
RoomKit's to set: sent in the first prompt of each session the
channel opens for a room, a session opened again included, as the
application's block ahead of the catch-up, and not again while
the session keeps it (RFC §6.4). What changes from turn to turn
belongs to |
None
|
active_turns
property
¶
Turns in flight: registered by _prompt_stream once it holds the
room, while its session is opened, dropped when its stream closes. The
whole of the turn as the consumer sees it, not only while the agent is
answering.
recent_events_window
property
¶
Room tail this channel reads — the catch-up window (RFC §19.3.2).
The framework sizes RoomContext.recent_events to the largest window
any bound channel declares, under a floor it keeps for hooks (50
events, while one is registered). So a room_history under that
floor reads a tail that was loaded anyway, one above it grows the tail
to match, and on a room with no hook the declaration is what loads the
tail at all: declaring the window is what keeps the two in step.
session_config ¶
Current ACP session config values for room_id, keyed by config id.
Agents publish their tunables through this one list — model,
mode, effort, vendor switches. Empty until the room's session
exists (sessions open on the first prompt).
Tracks what the agent announces. A switch made inside the agent
with its own slash command may not be announced at all — the ACP
bridge for Claude Code relays /model output as plain text and
sends no config update — so drive changes through
:meth:set_config_option when the value must stay observable.
config_options ¶
The agent's session tunables for room_id, as ACP describes them.
Full descriptors — id, name, current value, available choices — for
surfaces that let a user pick one (a model picker). Empty until the
session exists. :meth:session_config is the values-only shortcut.
set_config_option
async
¶
Set one session config option — set_config_option(room, "model", "opus").
Returns the full config mapping the agent reports back, so the caller sees the value it landed on (agents resolve aliases) plus any option the change invalidated. Opens the room's session if the first prompt has not yet done so, which connects to the agent.
The value belongs to the room, not only to this session: the room's
next session (after a recovery or an agent restart) is set to it too,
over session_config, until :meth:close_session. A call that opens
the room's session sets its value in place of the room's earlier one,
so a value the agent has come to refuse is replaced, not replayed.
on_event
async
¶
Create a lazy ACP prompt stream for a Room event.
close_session
async
¶
Forget one Room's ACP session, and close it where the agent can.
Forget is the whole of it: every map keyed by the session, and the
room's turn lock once no session is left behind it, is dropped here.
A long-lived channel cycling sessions (one per conversation, one per
reconnect) would otherwise carry every dead session's config options
until the channel itself closed. session/close goes only to an
agent that announces it; one that does not keeps the session until the
connection closes. Never raises for a close the agent refuses: returns
True once the session is forgotten, False when there was none.
The config values the host set on the room are forgotten too: its
next session opens on session_config alone.
close
async
¶
Cancel turns, close sessions where the agent can, and close the transport.
Shutdown is bounded: the graceful ACP round trips share
_SHUTDOWN_TIMEOUT, and the transport teardown runs even when
they time out, fail, or the caller is cancelled mid-close (a second
Ctrl-C landing on close_session). An agent that has stopped
answering must not outlive — or hang — the process that started it.
WebSocketChannel ¶
Bases: Channel
WebSocket transport channel with connection registry.
Args: channel_id: Channel identifier. send_timeout: Seconds a single send may take before the connection is treated as failing. Broadcast runs under the room lock and is unbounded by design (RFC §10.1), so an unbounded send would let one unresponsive socket freeze the room.
supports_streaming_delivery
property
¶
Whether any connected client supports streaming text delivery.
register_connection ¶
Register a WebSocket connection and subscribe it to a room.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
connection_id
|
str
|
Unique connection identifier. |
required |
send_fn
|
SendFn
|
Callback for delivering complete events. |
required |
room_id
|
str
|
The room this connection is for. Required: a channel
instance can be attached to several rooms, and without this the
channel has no way to tell which of its sockets belongs to the
room it is delivering — it would have to send everything to
everyone. Use :meth: |
required |
stream_send_fn
|
StreamSendFn | None
|
Optional callback for delivering streaming messages.
When provided, this connection receives progressive text delivery
via the |
None
|
subscribe ¶
Also deliver room_id's events to an existing connection.
For the client that holds several conversations open on one socket.
unregister_connection ¶
Unregister a WebSocket connection and drop its subscriptions.
supports_streaming_delivery_for ¶
Whether any client in this room speaks the streaming protocol.
The channel-wide property answers for every socket the channel holds, which is the wrong question once connections are scoped: a room whose clients are all non-streaming would otherwise take the streaming path and fall back at the end, having set up a stream nobody reads.
deliver_stream
async
¶
Deliver a streaming response with interleaved events to clients.
The stream yields str for text deltas and RoomEvent for
persisted events (text segments, tool calls). Streaming-capable
connections receive:
stream_start— streaming beginsstream_chunk— text delta (drives the live bubble)event— persisted event (tool call, text segment)stream_end— streaming complete
Non-streaming connections receive all persisted events via
the regular send_fn.
VoiceChannel ¶
VoiceChannel(channel_id, *, stt=None, tts=None, backend=None, pipeline=None, streaming=True, enable_barge_in=True, barge_in_threshold_ms=200, interruption=None, batch_mode=False, voice_map=None, max_audio_frames_per_second=None, tts_filter=None, max_sentences=None, bridge=None, recording=None, close_providers=True, stt_language_lock=None, tts_context=None, pipeline_speakers=False)
Bases: VoiceSTTMixin, VoiceTTSMixin, VoiceHooksMixin, VoiceRecordingHooksMixin, VoiceTurnMixin, VoicePipelineMixin, FrameworkAwareChannel, Channel
Real-time voice communication channel.
Supports three STT modes:
- VAD mode (default): VAD segments speech, streaming STT during speech
with batch fallback on SPEECH_END.
- Continuous mode: No VAD + streaming STT provider — all audio streamed,
provider handles endpointing.
- Batch mode (batch_mode=True): No VAD, audio accumulates post-pipeline.
Caller controls when to transcribe via :meth:flush_stt. Useful for
dictation, voicemail, and audio-file transcription with offline models.
When a VoiceBackend and AudioPipelineConfig are configured, the channel: - Registers for raw audio frames from the backend via on_audio_received - Routes frames through the AudioPipeline inbound chain: [Resampler] -> [Recorder] -> [AEC] -> [AGC] -> [Denoiser] -> VAD -> [Diarization] + [DTMF] - Fires hooks based on pipeline events (speech, silence, DTMF, recording, etc.) - Transcribes speech using the STT provider - Optionally evaluates turn completion via TurnDetector - Synthesizes AI responses using TTS and streams to the client
When no pipeline is configured, the channel operates without VAD — the backend must handle speech detection externally.
The STT language can be chosen per session at runtime with
:meth:set_stt_language; it applies from the session's next stream.
stt_language_lock installs an
:class:~roomkit.voice.stt.language.STTLanguageLock that starts every
session detecting (Deepgram multi), pins it to the language the
speaker uses, and releases it when the results stop fitting.
A TTS provider whose context_level is not NONE receives the dialogue
of the session on every streaming call (RFC §12.2.2): the channel keeps it
per session, as tts_context bounds it, and releases it when the
session is unbound.
Who spoke (RFC §12.2.3): with a diarizing STT in continuous mode each
segment becomes its own message carrying speaker_label,
speaker_epoch and sender_name. pipeline_speakers=True does the
same from the pipeline's DiarizationProvider when the STT labels
nothing: each transcript takes the speaker the stage heard the longest
over it (refused in batch mode, where a flush may hold several). Opt-in,
because it changes the name every message carries.
max_sentences caps how long the agent speaks (RFC §12.2 step 12s.e):
once that many sentences are said, a streamed reply that goes on stops at
its first word past them, as a barge-in stops it (nothing more generated,
no tool call it would make next started, the text produced so far stored,
marked cancelled); a text delivered whole is spoken to the budget. A
sentence BEFORE_TTS drops does not count, and :meth:say has no budget.
supports_streaming_delivery
property
¶
Whether this channel can accept streaming text delivery.
add_backend ¶
Serve sessions from another transport on this channel (RFC §12.7.3).
A bridged room often mixes transports — phone callers on SIP beside browser participants on WebRTC. The added backend's inbound audio enters this channel's pipeline, and its session-ready and client-disconnected signals drive the channel's session lifecycle, as for the backend given at construction, and so do its barge-in detection and out-of-band DTMF. Everything addressed to one of its sessions — bridged audio, TTS, assistant transcriptions, the playback cancel of an interruption, DTMF — goes out through it.
The pipeline is built once, for the primary backend's capabilities,
so the channel must have been constructed with a backend. A session
is matched to its transport by kit.join(..., backend=), or else
by asking each added backend whether it holds the session.
Adding a backend already served does nothing. Closing the channel
closes the added backends too.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
The channel has no primary backend or pipeline. |
set_bridge_filter ¶
Set a synchronous filter for bridged audio frames.
The filter runs in the audio callback thread before each frame
is forwarded. It receives (source_session, frame) and
returns the frame (possibly modified) or None to drop it.
This is the synchronous equivalent of BEFORE_BRIDGE_AUDIO
— use it for fast operations like per-session muting or gain.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
fn
|
BridgeFrameFilter | None
|
Filter function, or |
required |
set_framework ¶
Set the framework reference for inbound routing.
Called automatically when the channel is registered with RoomKit.
on_trace ¶
Register a trace observer and bridge to the backend.
bind_session ¶
Bind a voice session to a room for message routing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The voice session to bind. |
required |
room_id
|
str
|
Target room ID. |
required |
binding
|
ChannelBinding
|
Channel binding descriptor. |
required |
backend
|
VoiceBackend | None
|
Override backend for the bridge. When bridging sessions from different transports (e.g. SIP + WebRTC), pass the session's own backend so the bridge sends audio through the correct transport. |
None
|
connect_session
async
¶
Accept a voice session via process_inbound.
Delegates to :meth:bind_session which handles pipeline
activation and framework events.
disconnect_session
async
¶
Clean up a voice session on remote disconnect.
update_binding ¶
Update cached bindings for all sessions in a room.
Called by the framework after mute/unmute/set_access so the
audio gate in _on_audio_received sees the new state.
add_media_tap ¶
Register a tap on processed inbound audio frames (for room recording).
Delegates to the pipeline's on_processed_frame callback list.
add_outbound_media_tap ¶
Register a tap on outbound TTS audio (for room recording).
The callback receives (session, pcm_data, sample_rate) for
every outbound chunk after pipeline processing.
update_voice_map ¶
Merge entries into the per-agent voice map.
Called by :meth:ConversationPipeline.install to auto-wire
voice IDs from :class:Agent instances.
set_stt_language ¶
Choose the STT language for one session, from its next stream on.
None returns the session to the provider's configured language.
A streaming STT fixes its language when the stream opens, so the
choice lands on the next stream:
- VAD mode — the next utterance. A stream that is open stays open; restarting it would cut the utterance in progress in two.
- Continuous mode — right away: the current cycle is ended and the loop reconnects with the new language. Audio arriving in the gap is kept, as on every reconnect.
- Batch mode — the next :meth:
flush_stt.
The typical caller is an ON_TRANSCRIPTION hook reading
event.language from a detecting stream (Deepgram multi) and
pinning the session to what it heard;
:class:~roomkit.voice.stt.language.STTLanguageLock packages that
loop.
Raises:
| Type | Description |
|---|---|
RuntimeError
|
No STT provider, or one whose
|
get_stt_language ¶
The STT language chosen for a session, None for the provider's default.
send_dtmf ¶
Send a DTMF digit to the remote party via the voice backend.
The digit is sent as an RFC 4733 telephone-event (out-of-band).
Requires a backend with DTMF_SIGNALING capability (SIP, RTP).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active voice session. |
required |
digit
|
str
|
DTMF digit ('0'-'9', '*', '#', 'A'-'D'). |
required |
duration_ms
|
int
|
Tone duration in milliseconds (default 160). |
160
|
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If no backend is configured or session is ended. |
ValueError
|
If digit or duration_ms is invalid. |
interrupt
async
¶
Interrupt ongoing TTS playback for a session.
interrupt_all
async
¶
Interrupt all active TTS playback in a room.
Returns:
| Type | Description |
|---|---|
int
|
Number of sessions that were interrupted. |
wait_playback_done
async
¶
Wait until active TTS playback finishes for all sessions in room_id.
Returns immediately if no playback is in progress. Uses per-session
events that are set when send_audio() returns (before the echo
drain delay), so callers don't wait for the 2-second drain window.
RealtimeVoiceChannel ¶
RealtimeVoiceChannel(channel_id, *, provider, transport, owns_transport=True, system_prompt=None, voice=None, tools=None, temperature=None, input_sample_rate=16000, output_sample_rate=24000, transport_sample_rate=None, emit_transcription_events=True, tool_handler=None, human_input_handler=None, mute_on_tool_call=False, tool_result_max_length=16384, tool_timeout_seconds=10.0, tool_timeouts=None, pipeline=None, recording=None, skills=None, script_executor=None, skill_delivery_mode=None, server_instructions=None, tool_recovery=True, tool_search=None, tool_search_pinned=None, tool_search_threshold=20, reasoning_backend=None, reasoning_timeout_s=120.0, tool_policy=None)
Bases: RealtimeToolRecoveryMixin, RealtimeToolsMixin, RealtimeToolGateMixin, RealtimeDelegationMixin, RealtimeTranscriptionMixin, RealtimeSpeechMixin, RealtimeAudioMixin, RealtimeResponseMixin, VoiceRecordingHooksMixin, VoicePipelineMixin, RealtimeModelHost, FrameworkAwareChannel, Channel
Real-time voice channel using speech-to-speech AI providers.
Wraps APIs like OpenAI Realtime and Gemini Live as a first-class RoomKit channel. Audio flows directly between the user's browser and the provider; transcriptions are emitted into the Room so other channels (supervisor dashboards, logging) see the conversation.
Category is TRANSPORT so that:
- on_event() receives broadcasts: another channel's text enters the
sessions as its author's quoted words (RFC §12.4)
- deliver() is called but returns empty (customer is on voice)
Example
from roomkit.voice.realtime.mock import MockRealtimeProvider, MockRealtimeTransport
provider = MockRealtimeProvider() transport = MockRealtimeTransport()
channel = RealtimeVoiceChannel( "realtime-1", provider=provider, transport=transport, system_prompt="You are a helpful agent.", ) kit.register_channel(channel)
Initialize realtime voice channel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
channel_id
|
str
|
Unique channel identifier. |
required |
provider
|
RealtimeVoiceProvider
|
The realtime voice provider (OpenAI, Gemini, etc.). |
required |
transport
|
VoiceBackend
|
The audio transport (WebSocket, etc.). |
required |
owns_transport
|
bool
|
Close the transport with this channel (default True). Set False for a shared transport with unsubscribe support, such as FastRTC. Only this channel's sessions and callbacks close. |
True
|
system_prompt
|
str | None
|
Default system prompt for the AI. |
None
|
voice
|
str | None
|
Default voice ID for audio output. |
None
|
tools
|
list[dict[str, Any] | Any] | None
|
Tool definitions as dicts, or Tool objects with
|
None
|
temperature
|
float | None
|
Default sampling temperature. |
None
|
input_sample_rate
|
int
|
Default input audio sample rate (Hz). |
16000
|
output_sample_rate
|
int
|
Default output audio sample rate (Hz). |
24000
|
transport_sample_rate
|
int | None
|
Sample rate of audio from the transport (Hz).
When set and different from provider rates, enables automatic
resampling. Transports can override it in session metadata:
|
None
|
emit_transcription_events
|
bool
|
If True, emit final transcriptions as RoomEvents so other channels see them. |
True
|
tool_handler
|
ToolHandler | None
|
Async callable to execute tool calls.
Signature: |
None
|
human_input_handler
|
HumanInputToolHandler | None
|
The tools that ask a person, as on an
|
None
|
mute_on_tool_call
|
bool
|
If True, mute the transport microphone during
tool execution to prevent barge-in that causes providers
(e.g. Gemini) to silently drop the tool result. Defaults
to False — use |
False
|
tool_result_max_length
|
int
|
Maximum character length of tool results before truncation. Large results (e.g. SVG payloads) can overflow the provider's context window. Defaults to 16384. |
16384
|
tool_timeout_seconds
|
float | None
|
How long one tool call may take before its
handler is cancelled and the call fails (RFC §21.6), so a
handler that never answers cannot leave the caller in silence.
Defaults to 10 s; |
10.0
|
tool_timeouts
|
Mapping[str, float | None] | None
|
A bound per tool name, above the default ( |
None
|
tool_policy
|
ToolPolicy | None
|
Allow/deny rules for the session's tools, with the
meaning they have on an |
None
|
pipeline
|
AudioPipelineConfig | None
|
Optional |
None
|
recording
|
Any | None
|
Optional |
None
|
skills
|
SkillRegistry | None
|
Optional |
None
|
script_executor
|
ScriptExecutor | None
|
Optional |
None
|
skill_delivery_mode
|
str | None
|
How skill bodies reach the model.
|
None
|
server_instructions
|
Mapping[str, str | None] | None
|
What the MCP servers whose tools this
channel serves published for the model
(:attr: |
None
|
tool_recovery
|
bool
|
If True, detect tool calls that the model speaks
as text (e.g. |
True
|
tool_search
|
bool | None
|
Auto-enable Tool Search when the catalogue is
large enough to exceed the realtime model's reliable
tool-selection window (Google: 10–20 active tools on
Gemini Live). Pass |
None
|
tool_search_pinned
|
list[str] | None
|
Tool names that should ALWAYS be
visible (never hidden behind search). Use for tools
the agent calls reflexively — e.g. |
None
|
tool_search_threshold
|
int
|
Auto-activation threshold and the cap on how many tools may be live at once. Defaults to 20 to match Google's published recommendation. |
20
|
reasoning_backend
|
ReasoningBackend | None
|
Serves the integrator-side reasoning delegations of a full-duplex provider (RFC §12.4.1): when the model hands work over, the backend receives the transcript recorded since the previous delegation and its outputs return to the model as spoken or silent context. Its tool calls pass the same pre-execution gate as any realtime tool call. Only a full-duplex provider delegates; on any other provider the backend never runs. |
None
|
reasoning_timeout_s
|
float
|
Bound on one delegation's run. A backend that exceeds it is abandoned and the model is told so. Defaults to 120 seconds. |
120.0
|
wait_idle
async
¶
Wait until the selected sessions in the room are idle (not speaking).
session_ids=None includes every session. A proactive delivery can
restrict the wait to its pinned destinations without waiting on others.
An idle session has submitted its tool results, finished the provider response that follows them, and all audio has been forwarded to the transport. A queued transport such as SIP may still be playing that audio.
set_framework ¶
Set the framework reference for event routing.
Called automatically when the channel is registered with RoomKit.
on_trace ¶
Register a trace observer and bridge to the transport.
configure ¶
Update channel defaults for future sessions.
Active sessions are not affected — use reconfigure_session
for those. A tool under a name the channel, its reasoning backend or
orchestration serves, or given twice, is refused (RFC §21.1,
§12.4.1), and so is a name no vendor accepts (RFC §6.7).
start_audio_stream
async
¶
Open the realtime audio path on the provider.
Low-level escape hatch for opening the audio stream without
injecting any text. Most callers should use
inject_text(..., start_audio_stream=True) instead — that
composes the open + inject in a single call. No-op on providers
that don't need it.
on_room_attached
async
¶
Compile the patterns that find a copy of a runtime mark as the channel joins a room, off the event loop: the first injection of a session (a greeting) then does not wait for them (RFC §6.4).
inject_text
async
¶
Inject a text turn into the provider session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active voice session. |
required |
text
|
str
|
Text to inject. |
required |
role
|
str
|
The intent (RFC §12.4) — |
'user'
|
silent
|
bool
|
If True, add to conversation context without requesting a response. The agent sees the text on its next turn but does not react immediately. |
False
|
start_audio_stream
|
bool
|
If True, open the realtime audio path on the provider before sending the text. Set this on the first inject in outbound flows where the app speaks first (e.g. SIP dial greetings); no-op on providers that don't need priming (OpenAI, xAI). |
False
|
chain_depth
|
int
|
The chain depth of what the text stands for; the model's answer to it is one deeper (RFC §12.4). 0, the default, for text that opens a chain. |
0
|
A copy of a runtime mark in text is replaced before the provider takes it (RFC §6.4).
A session the channel no longer serves is not sent to:
not_sent / realtime_session_gone, as on a conference.
inject_image
async
¶
Inject an image into the provider session for multimodal analysis.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active voice session. |
required |
image_data
|
bytes
|
Raw image bytes. |
required |
mime_type
|
str
|
MIME type of the image. |
'image/png'
|
prompt
|
str
|
Optional text prompt accompanying the image, a copy of a runtime mark in it replaced (RFC §6.4). |
''
|
silent
|
bool
|
If True, add to context without requesting a response. |
False
|
start_session
async
¶
Start a new realtime voice session.
Connects both the transport (client audio) and the provider (AI service), then fires a framework event.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
room_id
|
str
|
The room to join. |
required |
participant_id
|
str
|
The participant's ID. |
required |
connection
|
Any
|
Protocol-specific connection (e.g. WebSocket), or an
awaitable resolving to it. With an awaitable, provider setup
starts while the connection is pending (e.g. SIP ringing).
Cancel the start task if the connection will never arrive.
Both branches belong to this session and are rolled back on
failure. The channel publishes the session only when both are ready.
Provider callbacks wait for publication in a bounded startup
queue; exceeding 2 MiB of audio or 500 events aborts startup.
Monotonic timestamps are written to metadata's
|
required |
metadata
|
dict[str, Any] | None
|
Optional session metadata. May include overrides for system_prompt, voice, tools, temperature. |
None
|
organization_id
|
str | None
|
The organization the caller acts for (RFC §17.2). The room is read scoped to it before the session exists: another organization's room is not found, and none of its recordings is told of the session. Left unset, the room is not read. |
None
|
Returns:
| Type | Description |
|---|---|
VoiceSession
|
The created VoiceSession. |
Raises:
| Type | Description |
|---|---|
RoomNotFoundError
|
organization_id is set and the room is missing, another organization's, or unreadable because the channel is not registered with a framework. |
end_session
async
¶
End a realtime voice session.
Disconnects both provider and transport, fires framework event.
Concurrent callers share one teardown: the first owns it, and a caller
that arrives while it runs (close(), the transport's disconnect
callback, a hangup tool) waits for it instead of tearing the session
down a second time. A joiner learns that the teardown ended, not how:
the owner's exception is the owner's. The work stays in the owner's
task, so a tool that is the first to end its own session is still the
current task the tool sweep spares; a joiner cancelled while waiting
leaves the owner's teardown untouched; and a call re-entered from the
teardown itself (a handler of the ended event ending the session it is
told about) returns at once instead of waiting on its own task. A
subclass's own teardown rides _before_session_teardown and is
covered by the same arbitration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The session to end. |
required |
reconfigure_session
async
¶
reconfigure_session(session, *, system_prompt=None, voice=None, tools=None, temperature=None, provider_config=None)
Reconfigure an active session with new agent parameters.
Used during agent handoff to switch the AI personality, voice, and tools. Providers with session resumption (e.g. Gemini Live) preserve conversation history across the reconfiguration.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession
|
The active session to reconfigure. |
required |
system_prompt
|
str | None
|
New system instructions for the AI. |
None
|
voice
|
str | None
|
New voice ID for audio output. |
None
|
tools
|
list[dict[str, Any]] | None
|
New tool/function definitions; one under a name no vendor
accepts raises |
None
|
temperature
|
float | None
|
New sampling temperature. |
None
|
provider_config
|
dict[str, Any] | None
|
Provider-specific configuration overrides. |
None
|
connect_session
async
¶
Accept a realtime voice session via process_inbound.
Delegates to :meth:start_session which handles provider/transport
connection, resampling, and framework events.
disconnect_session
async
¶
Clean up realtime sessions on remote disconnect.
update_binding ¶
Update cached bindings for all sessions in a room.
Called by the framework after mute/unmute/set_access so the
audio gate in _pipeline_on_audio_received (pipeline path)
or _forward_client_audio (direct path) sees the new state.
handle_inbound
async
¶
Not used directly — audio flows via start_session.
on_event
async
¶
React to events from other channels — TEXT INJECTION.
A text another channel broadcast enters each session of the room as
content its author wrote, quoted after their name, never as the
application's instruction, whatever the event carries (RFC §12.4).
The application directs the model with inject_text and the
system intent, or kit.deliver(..., instruction=True). Skips
events from this channel (self-loop prevention).
deliver
async
¶
No-op for realtime voice — content is injected via kit.deliver()'s
_deliver_to_realtime_voice path, which calls inject_text
directly. Events broadcast through channel.deliver() would double-
feed the session.
WhatsAppChannel ¶
Create a WhatsApp transport channel.
Numbers are compared and stored in E.164 (see :func:SMSChannel).
MessengerChannel ¶
Create a Facebook Messenger transport channel.
TeamsChannel ¶
Create a Microsoft Teams transport channel.
TelegramChannel ¶
Create a Telegram Bot transport channel.
WhatsAppPersonalChannel ¶
Create a WhatsApp Personal transport channel (neonize).
Numbers are compared and stored in E.164 (see :func:SMSChannel). A
private chat is its sender's conversation; a group is the group's,
answered in the group (its JID under phone_number).
TransportChannel ¶
TransportChannel(channel_id, channel_type, *, provider=None, capabilities=None, recipient_key='recipient_id', defaults=None, address_normalizer=None, replies_to_sender=False, reply_metadata_key=None, requires_recipient=True)
Bases: Channel
Generic transport channel driven by configuration rather than subclassing.
All transport channels (SMS, Email, WhatsApp, Messenger, HTTP) share the
same inbound/deliver logic. The only differences are data: which
ChannelType, which ChannelCapabilities, which metadata key holds the
recipient address, and which extra kwargs to pass to the provider's
send() method.
Use the factory functions (SMSChannel, EmailChannel, …) in
roomkit.channels for convenient construction.
Initialise a transport channel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
channel_id
|
str
|
Unique identifier for this channel instance. |
required |
channel_type
|
ChannelType
|
The channel type (SMS, email, etc.). |
required |
provider
|
Any
|
Provider that handles external delivery (e.g. ElasticEmailProvider). |
None
|
capabilities
|
ChannelCapabilities | None
|
Media and feature capabilities for this channel. |
None
|
recipient_key
|
str
|
Binding metadata key that holds the recipient address. |
'recipient_id'
|
defaults
|
dict[str, Any] | None
|
Default kwargs passed to |
None
|
address_normalizer
|
Callable[[str], str] | None
|
How this channel writes an address (E.164 for the channels addressed by phone number); unchanged if omitted. |
None
|
replies_to_sender
|
bool
|
Whether a reply goes to the address the correspondent writes from (a phone number, an email address), rather than to a chat or a URL. Then the framework records an inbound sender as the binding's recipient, and a binding's recipient names its correspondent: the router admits no one else through it (RFC §10.4). |
False
|
reply_metadata_key
|
str | None
|
On a channel that replies to a chat or a
conversation rather than to the sender, the inbound
message's metadata key naming it ( |
None
|
requires_recipient
|
bool
|
Whether a delivery needs a recipient in the
binding's metadata. A channel whose provider delivers to a
destination of its own (a webhook's configured URL) passes
|
True
|
recipient_metadata ¶
{recipient_key: address} when the recipient is a conversation's address.
handle_inbound
async
¶
Convert an inbound message into a room event.
deliver
async
¶
Deliver an event to the external recipient via the provider.
The recipient address is read from binding.metadata[recipient_key].
Extra kwargs are built from defaults: fixed values are passed as-is,
None defaults are resolved from binding metadata at delivery time.
WebSocket Streaming¶
StreamStart ¶
Bases: BaseModel
Sent when a streaming response begins.
StreamChunk ¶
Bases: BaseModel
Sent for each text delta during streaming.
StreamEnd ¶
Bases: BaseModel
Sent when a streaming response completes.
StreamMessage
module-attribute
¶
StreamMessage = StreamStart | StreamChunk | StreamEnd | StreamError
StreamSendFn
module-attribute
¶
StreamSendFn = Callable[[str, StreamMessage], Coroutine[Any, Any, None]]