RoomKit¶
RoomKit ¶
RoomKit(store=None, identity_resolver=None, identity_channel_types=None, inbound_router=None, lock_manager=None, realtime=None, max_chain_depth=5, agent_response_policy=AGENT_CHAIN, identity_timeout=10.0, process_timeout=30.0, stt=None, tts=None, voice=None, task_runner=None, delivery_strategy=None, delivery_backend=None, status_bus=None, telemetry=None, inbound_rate_limit=None, orchestration=None, persistence_policy=None, delivery_gap_timeout=30.0, delivery_claim_lock_manager=None, verification_policy=None)
Central orchestrator tying rooms, channels, hooks, and storage.
Initialise the RoomKit orchestrator.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
store
|
ConversationStore | None
|
Persistent storage backend. Defaults to |
None
|
identity_resolver
|
IdentityResolver | None
|
Optional resolver for identifying inbound senders.
Without one, an event's |
None
|
identity_channel_types
|
set[ChannelType] | None
|
Restrict identity resolution to specific channel
types. If |
None
|
inbound_router
|
InboundRoomRouter | None
|
Strategy for routing inbound messages to rooms.
Defaults to |
None
|
lock_manager
|
RoomLockManager | None
|
Per-room locking backend. Defaults to
|
None
|
realtime
|
RealtimeBackend | None
|
Realtime backend for ephemeral events (typing, presence).
Defaults to |
None
|
max_chain_depth
|
int
|
Maximum reentry chain depth to prevent infinite loops. |
5
|
identity_timeout
|
float
|
Timeout in seconds for identity resolution calls. |
10.0
|
verification_policy
|
VerificationPolicy | None
|
How long an identity verification and its requests last, and how many attempts a request accepts (RFC §11.7). Passing one also turns on the inbound side: an identified sender's messages are labelled with their verification and, while it is not verified, held from the room's agents. Omitted, the API works with the defaults and the pipeline is unchanged. |
None
|
process_timeout
|
float
|
Timeout in seconds for the locked processing phase. |
30.0
|
stt
|
STTProvider | None
|
Optional speech-to-text provider for transcription. |
None
|
tts
|
TTSProvider | None
|
Optional text-to-speech provider for synthesis. |
None
|
voice
|
VoiceBackend | None
|
Optional voice backend for real-time audio transport. |
None
|
task_runner
|
TaskRunner | None
|
Pluggable backend for delegated background tasks.
Defaults to |
None
|
delivery_strategy
|
DeliveryStrategy | str | None
|
Controls when proactive content is delivered,
a background task's result included: the result is handed back
through |
None
|
delivery_backend
|
DeliveryBackend | None
|
Persistent delivery backend. When set,
|
None
|
status_bus
|
StatusBus | None
|
Shared status bus for multi-agent coordination.
Defaults to a |
None
|
telemetry
|
TelemetryConfig | TelemetryProvider | None
|
Optional telemetry provider or config for span/metric
collection. Accepts a |
None
|
inbound_rate_limit
|
RateLimit | None
|
Optional rate limit applied to all inbound
messages before any processing. Messages exceeding the limit
are dropped with |
None
|
orchestration
|
Orchestration | None
|
Default orchestration strategy applied to rooms
created via |
None
|
persistence_policy
|
PersistencePolicy | None
|
Controls which event types are persisted.
When |
None
|
delivery_gap_timeout
|
float
|
Seconds a delivery lane waits on a cursor
hole owned by an absent worker (one that committed events and
crashed before delivering them) before skipping over it with
a |
30.0
|
delivery_claim_lock_manager
|
RoomLockManager | None
|
Lock manager used for the per-room
delivery claims. Defaults to |
None
|
close_room
async
¶
Close a room.
organization_id scopes the operation to one tenant (RFC §17.2); a room belonging to another organization is reported as not found.
archive_room
async
¶
Archive a room — the terminal storage state (RFC §5.1).
ARCHIVED refuses new events exactly as CLOSED does; the difference is intent. A closed room MAY be reopened by returning it to ACTIVE; an archived one is done. History, participants and bindings stay readable at every status, archived included.
This is the only path to the status, and therefore the only source of
the room_archived framework event (§8.2).
check_room_timers
async
¶
Check and apply timer-based transitions for a single room.
Returns the room (possibly transitioned to PAUSED or CLOSED).
check_all_timers
async
¶
Check one tenant's active/paused room timers.
With no organization scope, preserves the single-tenant/all-rooms behaviour.
update_room_metadata
async
¶
Update room metadata.
set_agent_response_policy
async
¶
Set what an agent's own output solicits in this room (RFC §19.3.1).
The policy is chosen at creation, but a room rarely knows then how many
agents it will end up holding: one that gains a second agent is a
different room, because under AGENT_CHAIN the first answer solicits
the other agent, whose answer comes back, down to max_chain_depth.
Switching to ADDRESSED_ONLY at that moment is what keeps a room of
independent agents from answering itself.
Applies to events processed after it — an event already broadcast is not reconsidered. No-op when the room already holds policy.
set_visibility
async
¶
Set visibility for a channel in a room.
set_access
async
¶
Set access level for a channel in a room.
update_binding_metadata
async
¶
Update metadata on a channel binding.
get_binding
async
¶
Get a channel binding. Raises ChannelNotFoundError if missing.
list_bindings
async
¶
List all channel bindings for a room.
get_timeline
async
¶
get_timeline(room_id, offset=0, limit=50, visibility_filter=None, *, after_index=None, before_index=None, newest_first=False, include_blocked=False, organization_id=None)
Query the event timeline for a room.
Supports offset-based (offset/limit) and cursor-based
(after_index/before_index) pagination. When a cursor
parameter is set, offset is ignored.
The offset-based default is the oldest limit events — page 1 of
a log reads from the beginning. Pass newest_first=True for the most
recent limit instead (still ascending), which is the shape a
reconnect snapshot wants: what was just said, not how the room opened.
The page holds what the room received: a row stored BLOCKED is
served only when include_blocked asks for it, the audit read
(RFC §14.1).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
room_id
|
str
|
Room to query. |
required |
offset
|
int
|
Number of events to skip (offset-based mode), counted from
the newest end when |
0
|
limit
|
int
|
Maximum number of events to return. |
50
|
visibility_filter
|
str | None
|
Optional visibility value to filter by. |
None
|
after_index
|
int | None
|
Return events with |
None
|
before_index
|
int | None
|
Return events with |
None
|
newest_first
|
bool
|
In offset-based mode, return the most recent
|
False
|
include_blocked
|
bool
|
Serve the rows the room refused too (status
|
False
|
list_tasks
async
¶
List tasks for a room, optionally filtered by status.
delegate
async
¶
delegate(room_id, agent_id, task, *, wait=False, context=None, share_channels=None, notify=None, on_complete=None, require_structured_result=False, max_result_retries=3, result_tool=None, post_status=True, brief=None, conversation=0, hand_back='relay')
Delegate a task to an agent in a child room.
Creates a child room linked to room_id, attaches the agent and any shared channels, then either runs the agent inline or submits the task for background execution.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
room_id
|
str
|
Parent room ID. |
required |
agent_id
|
str
|
Channel ID of the agent to run the task. |
required |
task
|
str
|
What was asked: the agent reads it unless brief is given, and a hand-back, the StatusBus and a turn's notes name the task by it (RFC §23.3 step 4). |
required |
wait
|
bool
|
If |
False
|
context
|
dict[str, Any] | None
|
Optional context dict passed to the agent. |
None
|
share_channels
|
list[str] | None
|
Channel IDs from the parent to share. |
None
|
notify
|
str | None
|
Channel the result is handed to when a background task completes (RFC §23.3): an agent receives it, bounded, as an instruction and answers through the room's transport; a transport receives it as a delivery. Defaults to agent_id. |
None
|
on_complete
|
Any | None
|
Optional async callback |
None
|
require_structured_result
|
bool
|
Inline runs only: the agent must hand its
work back by calling a result tool, re-prompted up to
max_result_retries times; the result's |
False
|
max_result_retries
|
int
|
How many times a turn ending without the call is re-prompted. |
3
|
result_tool
|
ResultTool | None
|
The tool to force, when not |
None
|
post_status
|
bool
|
Follow the task on :attr: |
True
|
brief
|
str | None
|
What the agent is told instead of task, when it says more (how to work, what to report); it never reaches the parent room. |
None
|
conversation
|
int
|
How many of the parent room's last messages the agent is handed, those the notified channel may read, quoted as data (0: none). Its child room starts empty. |
0
|
hand_back
|
str
|
How a background task's result reaches the room:
|
'relay'
|
Returns:
| Name | Type | Description |
|---|---|---|
A |
DelegatedTask
|
class: |
DelegatedTask
|
the result is already set. When |
|
DelegatedTask
|
to block for the result, or let it run fire-and-forget. |
Raises:
| Type | Description |
|---|---|
RoomNotFoundError
|
If the parent room doesn't exist. |
ChannelNotRegisteredError
|
If the agent channel isn't registered. |
ValueError
|
An unknown hand_back, a |
cancel_task
async
¶
Cancel a delegated task still running in the background (RFC §23.3).
The task ends cancelled, as any task cancelled from outside:
ON_TASK_COMPLETED fires, the StatusBus posts its end, and the
channel its result was going to is told it was cancelled. A task that
already ended, or that the framework's task runner does not hold (an
inline delegation), is left as it stands.
Returns:
| Type | Description |
|---|---|
bool
|
|
list_observations
async
¶
List observations for a room.
leave
async
¶
Remove a participant from a room.
Stops listening, unbinds the session, removes recording tracks, and disconnects the backend.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
VoiceSession | VideoSession
|
The session to leave. |
required |
process_inbound
async
¶
Process an inbound message through the full pipeline.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
InboundMessage
|
The inbound message to process. |
required |
room_id
|
str | None
|
Explicit room to route to, bypassing the inbound router. Useful for shared channels attached to multiple rooms. |
None
|
defer_delivery
|
bool
|
Return at the commit instead of waiting for the
delivery set (RFC §10.1 step 18 detached completion). The
result carries the committed event — a hook refusal is still
decided under the room lock and reported synchronously — and
|
False
|
organization_id
|
str | None
|
The organization the caller acts for (RFC §17.2). The room the message would land in is checked against it before any event is committed or any channel auto-attached: a room belonging to another organization is reported as not found, whether the caller named it or the router picked it (the router itself is not organization aware, so on the routed path the refusal is the outcome, not a room of the caller's own); a room the caller names that does not exist yet is created under it. That auto-create makes a miss observable, so a scoped caller passes room ids it resolved itself, never one it was handed. Left unset, the read is unscoped and behaves as it always has. A library has no caller or auth context of its own, so the scope has to come from the caller: the row that mapped an external identity (a chat, a number) to a room is where it usually lives. |
None
|
Cancelling an awaited call cancels only its delivery cascade and
drains owned generation, tools and streams before propagating
CancelledError. Cleanup has a five-second budget; TimeoutError
reports that work is still unwinding and its resources must be kept.
Deferred callers explicitly use result.delivery.cancel();
cancelling a handle's wait() cancels only that wait. Committed
timeline events are retained in either case.
send_event
async
¶
send_event(room_id, channel_id, content, event_type=MESSAGE, chain_depth=0, participant_id=None, metadata=None, visibility=ALL, provider=None, response_visibility=None, created_at=None, parent_event_id=None, idempotency_key=None, addressed_to=None, organization_id=None, standalone=False)
Send an event directly into a room from a channel.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
room_id
|
str
|
Target room ID |
required |
channel_id
|
str
|
Source channel ID |
required |
content
|
Any
|
Event content (TextContent, RichContent, etc.) |
required |
event_type
|
EventType
|
Type of event (default MESSAGE) |
MESSAGE
|
chain_depth
|
int
|
Depth in response chain (for loop prevention) |
0
|
participant_id
|
str | None
|
Optional participant/sender ID for the event source |
None
|
metadata
|
dict[str, Any] | None
|
Optional event metadata |
None
|
visibility
|
str
|
Event visibility ("all" or "internal") |
ALL
|
provider
|
str | None
|
Optional provider/backend name for event attribution |
None
|
response_visibility
|
str | None
|
Controls where the AI's response is delivered. Uses the same vocabulary as visibility. None means no restriction. |
None
|
addressed_to
|
list[str] | None
|
The intelligence channels this event asks to act
(RFC §19.3). An address is set by a sender, and direct injection
has a sender like any other entry point: a caller storing a
message whose answer it triggers separately passes |
None
|
parent_event_id
|
str | None
|
In-app thread parent. The locked pipeline normalises
it to the thread root (flat two-level model); see
:meth: |
None
|
idempotency_key
|
str | None
|
Stable de-duplication key. When set, the locked
pipeline's idempotency check (backed by the unique
|
None
|
standalone
|
bool
|
For an |
False
|
commit_event
async
¶
Commit a record no member receives to the room's timeline (RFC §10.5).
For a trace, a display snapshot, the copy a branched conversation starts
from. The record takes the room's next index, which its delivery lane
counts as delivered at once, so the room's next event never waits on
it; no delivery set, no hook and no broadcast follow. It is stored as
given, whatever the persistence policy, but for the record of its
author, which the room fixes when the event names one
(metadata["author"], RFC §10.1 step 12, any value given dropped);
the source write rule (RFC §7.5 rule 2) does not apply to it. An event
the members must receive goes through :meth:send_event.
The room is read under its lock, which a hook already holding it re-enters, scoped to organization_id (RFC §17.2).
Raises:
| Type | Description |
|---|---|
ValueError
|
event names another room; nothing is read or written. |
RoomNotFoundError
|
the room is missing, or another organization's. |
RoomClosedError
|
the room's status refuses new events (RFC §5.1); nothing is written. |
start_room_recording
async
¶
Start recorders on an existing room, all or nothing, and announce each.
The room is read under its lock, scoped to organization_id (RFC §17.2). ON_RECORDING_STARTED fires for each recording, under the lock, before it joins the room's media: it is told the room's tracks, then filed, so media already flowing reaches it after its consent point and in the format its track declares (RFC §12.11, §17.6). It captures the room's declared tracks: a session that joined while the room recorded nothing declared none, and is recorded once it joins again. A recording resumed after a restart is started here.
Raises:
| Type | Description |
|---|---|
RoomNotFoundError
|
the room is missing, or another organization's. |
RoomClosedError
|
the room's status refuses new events (RFC §5.1). |
Exception
|
a recorder refused to start; the ones already started for this call are stopped. |
room_recordings
async
¶
The recordings room_id runs, in the order they started; empty when none.
The room is read scoped to organization_id, as a stop reads it (RFC §12.11): a room whose row is gone is listed while it still records.
Raises:
| Type | Description |
|---|---|
RoomNotFoundError
|
the room is another organization's, or it is missing and records nothing. |
add_room_recording_track
async
¶
Declare track to the room's recordings; the feed its media goes through.
For a source the framework does not wire itself (a channel that joins
the room while it records wires its own). The track describes the
media it will carry (RFC §12.11): the feed hands it as declared, and a
recording started later is told it before any of its media. The room
is read scoped to organization_id, as a stop reads it. None when
the room records nothing.
Raises:
| Type | Description |
|---|---|
RoomNotFoundError
|
the room is another organization's, or it is missing and records nothing. |
stop_room_recording
async
¶
Stop the room's recordings, each announced with its result (RFC §12.11).
The room is read under its lock, scoped to organization_id: a call refused for another organization stops nothing. A room whose row is gone still has its recordings stopped: they run in memory, and a file nothing stops is never finalized; with no room left to read a context from, their end is logged rather than announced. Returns the results of the recordings that stopped; empty when the room recorded nothing.
Raises:
| Type | Description |
|---|---|
RoomNotFoundError
|
the room is another organization's, or it is missing and records nothing, as one would be told not found. |
regenerate_response
async
¶
Re-run the room's intelligence channel on the last inbound message.
Produces a fresh response to the most recent transport (human) message
without ingesting a new inbound event — the triggering message keeps
its identity, index, and timestamp. The re-broadcast runs in the
room's delivery lane, off the room lock and unbounded, as an inbound
event's broadcast does (RFC §13.5, §13.6): a strategy that works in
on_event (a Loop, a Supervisor's delegation) goes to its end, and
the room takes messages meanwhile. The new response re-enters like a
first-time turn's (RFC §10.1 step 14): its BEFORE_BROADCAST hooks run,
its source's right to write is checked, it counts against the reentry
budget, it is persisted, streamed, and runs its AFTER_BROADCAST hooks,
and it comes back in response_events. The trigger message's own
hooks are not re-run, nor its delivery report or event_processed.
Replacement semantics are the caller's concern: any responses already
present after the last inbound message should be removed before calling
this (the method only generates — it does not delete the prior answer).
:meth:regenerate_target names the message this call would re-run on,
so the caller can key that removal on it.
trigger_id makes the call a compare-and-regenerate. The target is
read outside the room lock, so a message can land between that read
and this call: the pipeline has answered it already, and a regenerate
that re-selects under the lock would answer it a second time, with
the earlier answer gone. Naming the trigger closes that window: when
the selection under the lock is no longer that event — another
message moved in, or nothing qualifies any more — the call returns
InboundResult(blocked=True, reason="trigger_moved") without
running the agent and without writing anything. None (the
default) regenerates whatever the selection is. A closed room is
refused first: room_closed wins over trigger_moved. The
compare is on the trigger, not on the answers that follow it: two
concurrent calls naming the same trigger both pass it, and an answer
landing between them does not move a transport selection.
Returns the :class:InboundResult for the regenerated turn, or None
when there is no inbound message to regenerate (no transport message, or
its source binding can no longer write) — with trigger_id set, that
same state is a moved trigger and comes back blocked instead of
None. process_timeout bounds only the wait for the room lock
and the choice of the trigger: past it, the call returns
InboundResult(blocked=True, reason="process_timeout") without
running the agent. The call waits behind the deliveries already in
the room's lane; called from inside that lane or under the room lock,
it returns at once and the regeneration follows in lane order. A
caller cancelled cuts the regenerated turn. A room whose status
refuses new events (RFC §5.1), at the call or while the regeneration
waits in the lane, is refused before the agent runs, with
InboundResult(blocked=True, reason="room_closed") and a
room_refused_event framework event — exactly as
:meth:process_inbound refuses — rather than after a generation whose
answer nothing could commit.
The re-broadcast is scoped to visibility="intelligence" so only the
agent reacts — transports never receive the user message again (no
duplicate bubble, no echo to other participants). Targets the single
intelligence-channel path; orchestrated rooms (routing installed as
BEFORE_BROADCAST hooks) are not re-routed here.
In a room a discussion holds (RFC §19.7.5 rule 7), each agent that
answered the trigger (when no answer is left, each agent the trigger
asked for) is queued at the front for a turn of its own, and the call
returns once they are queued, with no answer in its result: the
answers come in those turns. A discussion that is over refuses it,
InboundResult(blocked=True, reason="discussion_over").
regenerate_target
async
¶
The event :meth:regenerate_response would re-run the agent on.
The primitive's own choice, from the primitive's own read: the newest message a transport binding wrote and the room accepted (a message a hook blocked is never replayed), in the history window the room's channels derive, whose source binding can still write. A host that must act on the trigger before regenerating — delete the answer the new one replaces, refuse a trigger it recognises as a runner's prompt rather than a person's question — asks here instead of re-implementing the selection off a window of its own.
Returns None when a regenerate would do nothing: no transport
message in the window, or its source binding can no longer write.
Raises :class:RoomClosedError when the room's status refuses new
events (RFC §5.1): :meth:regenerate_response would refuse, and an
accessor that returns an event has no way to hand back that refusal —
the same reasoning as :meth:send_event. Raises
:class:RoomNotFoundError for an unknown room.
A read, taken outside the room lock, so the answer can be stale by
the time a regenerate acts on it — the caveat of any answer taken
before the lock (RFC §10.1 step 6): the regenerating call re-selects
under the lock, and a message that lands in between becomes the
trigger there. A host that must not answer that message twice hands
this event's id back as regenerate_response(trigger_id=...),
which refuses a selection that moved instead of regenerating it.
ensure_participant
async
¶
Get an existing participant or create one.
A participant is one record per (room, id), so channel_id names the
channel a record being created is primarily reached on — it is not a
filter. A participant the room already has is returned as they stand,
primary channel included, even when that channel is not the one asked
for: the same person reached by SMS and then by email is one participant,
not two (RFC §5.5). The channel asked for is recorded in
connected_via, and a channel that is not the record's primary one is
logged, because nothing in the returned record would otherwise say so —
a caller keeping a lifecycle or a status on it is keeping it on a record
another channel also drives. Use :meth:add_member for a deliberate join.
Bookkeeping only: no PARTICIPANT_UPDATED event, no
ON_PARTICIPANT_UPDATED hook. Runs under the room lock — recording a
channel is a read-modify-write, and add_member / remove_member
hold that lock while they write the same record.
resolve_participant
async
¶
resolve_participant(room_id, participant_id, identity_id, resolved_by='manual', *, organization_id=None)
Resolve a pending participant to a known identity (RFC 7.4).
Called by an advisor or automated process when a pending/ambiguous participant has been identified.
request_verification
async
¶
request_verification(identity_id, *, room_id, channel_id, method=METHOD_LINK, window_start=None, organization_id=None)
Open a verification request for an identity; return it and its secret.
The secret is returned once and never stored: put it in the link to
your page, or send it as a code (method="code": digits the person
replies with). Opening a request cancels the identity's earlier open
one. An identity that is not verified becomes PENDING; one already
verified stays so until this request completes or its verification
ends. window_start is when the message that called for it was
received (now by default): the held messages released on completion
are those from then on (RFC §11.7.6).
The request is scoped to the room's organization, the one whose
messages it verifies. organization_id is the organization the
caller acts for (RFC §17.2): a room of another raises
:class:RoomNotFoundError.
complete_verification
async
¶
Complete a request once your own check passed; return the verification.
Raises :class:VerificationRefusedError when the request is unknown
(or another organization's than organization_id, when given), no
longer open, expired, or the secret does not match (a mismatch counts
as an attempt; the last one fails it).
method records how the person proved it, the request's by default.
record_failed_attempt
async
¶
Count a failed check of your own method against a request; return it.
Your page's wrong PIN or refused login is an attempt like a wrong
secret: the request's bound applies to it, so a PIN cannot be guessed
through your page. The request comes back OPEN with one attempt more,
or FAILED when that was the last one (which ends it as a wrong secret
does, a verification in force included). Raises
:class:VerificationRefusedError when the request is unknown (or
another organization's than organization_id, when given), no longer
open, or expired.
revoke_verification
async
¶
End an identity's verification before its time; None if none was in force.
Its open request is cancelled in every case, verified or still pending: a link sent before the revocation can never verify the person after it (RFC §11.7.7).
verification_of
async
¶
The identity's verification as it stands now: UNVERIFIED when it has none.
A verification whose time has passed reads EXPIRED, and an open
request past its expiry reads as ended (no request_id, a PENDING
identity back to UNVERIFIED), whether or not the end has been
recorded yet.
sender_verification
async
¶
The verification of event's sender, in its room's organization.
None when the pipeline did not label the event (its sender is not
identified, or the kit has no verification policy). What an
ON_VERIFICATION_REQUIRED hook reads before opening a request: whose
verification it is (identity_id) and whether a request is open
already (request_id). The sender's participant may not be in
context yet on their first message; this reads it all the same.
check_verifications
async
¶
End the verifications whose time passed; arm the timers of the others.
A verification ends when its time runs out (RFC §11.7.5), by a timer
this process arms when it completes. Call this when the process
starts (async with RoomKit(...) does): it ends what expired while
nothing was running and re-arms the rest. Returns the ones it ended.
connect_websocket
async
¶
Register a WebSocket connection for a room and emit framework event.
room_id says which conversation this socket belongs to. It is
required because one channel instance can serve several rooms, and a
connection the channel cannot place would otherwise have to receive
every room's events. Add more rooms to the same socket with
:meth:subscribe_websocket.
disconnect_websocket
async
¶
Unregister a WebSocket connection and emit framework event.
mark_read
async
¶
Mark an event as read for a channel.
mark_all_read
async
¶
Mark all events as read for a channel.
publish_typing
async
¶
Publish a typing indicator for a user in a room.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
room_id
|
str
|
The room to publish the typing event in. |
required |
user_id
|
str
|
The user who is typing. |
required |
is_typing
|
bool
|
True for typing_start, False for typing_stop. |
True
|
data
|
dict[str, Any] | None
|
Optional additional data (e.g., {"name": "User Name"}). |
None
|
publish_presence
async
¶
Publish a presence update for a user in a room.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
room_id
|
str
|
The room to publish the presence event in. |
required |
user_id
|
str
|
The user whose presence changed. |
required |
status
|
str
|
One of "online", "away", or "offline". |
required |
publish_read_receipt
async
¶
Publish a read receipt for a user in a room.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
room_id
|
str
|
The room to publish the read receipt in. |
required |
user_id
|
str
|
The user who read the message. |
required |
event_id
|
str
|
The ID of the event that was read. |
required |
subscribe_room
async
¶
Subscribe to ephemeral events for a room.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
room_id
|
str
|
The room to subscribe to. |
required |
callback
|
EphemeralCallback
|
Async callback invoked for each ephemeral event. |
required |
Returns:
| Type | Description |
|---|---|
str
|
A subscription ID that can be used to unsubscribe. |
unsubscribe_room
async
¶
Unsubscribe from ephemeral events.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subscription_id
|
str
|
The subscription ID returned by subscribe_room. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the subscription existed and was removed. |
hook ¶
hook(trigger, execution=SYNC, priority=0, name='', timeout=30.0, channel_types=None, channel_ids=None, directions=None, event_types=None, fail_closed=False, needs_lock=True)
Decorator to register a global hook.
A filtered-out hook is not called at all — neither its body nor its
timeout budget. A trigger that fires for every event a turn produces
(BEFORE_BROADCAST runs once per text segment and once per
tool-call event) is where event_types pays: a hook that only
shapes tool calls declares it instead of guarding on event.type
and being invoked on every segment of every turn.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trigger
|
HookTrigger
|
When the hook fires (BEFORE_BROADCAST, AFTER_BROADCAST, etc.) |
required |
execution
|
HookExecution
|
SYNC (can block/modify) or ASYNC (fire-and-forget) |
SYNC
|
priority
|
int
|
Lower numbers run first (default: 0) |
0
|
name
|
str
|
Optional name for logging and removal |
''
|
timeout
|
float
|
Max execution time in seconds (default: 30.0) |
30.0
|
channel_types
|
set[ChannelType] | None
|
Only run for events from these channel types (None = all) |
None
|
channel_ids
|
set[str] | None
|
Only run for events from these channel IDs (None = all) |
None
|
directions
|
set[ChannelDirection] | None
|
Only run for events with these directions (None = all) |
None
|
event_types
|
set[EventType] | None
|
Only run for events of these types (None = all) |
None
|
fail_closed
|
bool
|
SYNC only ( |
False
|
needs_lock
|
bool
|
SYNC |
True
|
identity_hook ¶
Decorator to register an identity-resolution hook.
The decorated function receives (event, context, id_result) and
returns an IdentityHookResult or None.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
trigger
|
HookTrigger
|
When the hook fires (ON_IDENTITY_AMBIGUOUS, ON_IDENTITY_UNKNOWN). |
required |
channel_types
|
set[ChannelType] | None
|
Only run for events from these channel types (None = all). |
None
|
channel_ids
|
set[str] | None
|
Only run for events from these channel IDs (None = all). |
None
|
directions
|
set[ChannelDirection] | None
|
Only run for events with these directions (None = all). |
None
|
add_room_hook ¶
add_room_hook(room_id, trigger, execution, fn, priority=0, name='', *, fail_closed=False, needs_lock=True)
Add a hook for a specific room.
fail_closed and needs_lock have the meaning they have on
:meth:hook.
Exceptions¶
RoomNotFoundError ¶
Bases: RoomKitError
Room does not exist.
ChannelNotFoundError ¶
Bases: RoomKitError
Channel binding not found in room.
ChannelNotRegisteredError ¶
Bases: RoomKitError
Channel type not registered.
Infrastructure¶
RoomLockManager ¶
Bases: ABC
Abstract base for per-room locking.
Implement this to plug in any locking backend (Redis, Postgres
advisory locks, etc.). The library ships with InMemoryLockManager
for single-process deployments.
Implementations should be reentrant within the same execution
context (including child tasks spawned by asyncio.gather): if
a coroutine already holds the lock for a room and awaits code that
tries to acquire the same room lock, the inner acquisition must
succeed without deadlocking. This is required because tool handlers
(e.g. handoff) may update room state while the inbound pipeline
already holds the room lock.
locked
abstractmethod
async
¶
Acquire an exclusive lock for room_id.
Acquisition MUST be cancellation-safe: the inbound pipeline bounds the
wait with process_timeout (RFC §13.6), so a caller that gives up
queueing is cancelled here. An implementation that leaves the lock
taken, or a reference held, on cancellation strands the room — every
later event for it queues behind a lock nobody owns.
close
async
¶
Release any resources held by the lock manager (e.g. a connection
pool). Called by RoomKit.close(). Default is a no-op; overrides
MUST be idempotent.
InMemoryLockManager ¶
Bases: RoomLockManager
In-process per-room asyncio locks with LRU eviction.
Reentrant within the same execution context: if the current context
already holds the lock for a given room (including child tasks
spawned by asyncio.gather), locked() yields immediately
instead of deadlocking.
Suitable for single-process deployments. For multi-process or
distributed setups, provide a custom RoomLockManager backed by
Redis, Postgres advisory locks, or similar.
AuthCallback
module-attribute
¶
Async callback for transport authentication.
Receives the connection context (e.g. WebSocket, HTTP request) and returns
a metadata dict on success or None to reject the connection.