Skip to content

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 InMemoryStore.

None
identity_resolver IdentityResolver | None

Optional resolver for identifying inbound senders. Without one, an event's participant_id is whatever the channel stamped on it (RFC §11.6) — so it is only as trustworthy as the transport that supplied it, and the edit/delete author check (RFC §10.3) is decided from it. Derive the sender id from an authenticated session rather than a client-supplied field, or install a resolver. See the Security Hardening guide.

None
identity_channel_types set[ChannelType] | None

Restrict identity resolution to specific channel types. If None (default), resolution runs for all channels. Set to e.g. {ChannelType.SMS} to only resolve identity for SMS. Applies wherever resolution runs, the conference arrival path included — see :meth:identity_enabled_for.

None
inbound_router InboundRoomRouter | None

Strategy for routing inbound messages to rooms. Defaults to DefaultInboundRoomRouter.

None
lock_manager RoomLockManager | None

Per-room locking backend. Defaults to InMemoryLockManager. For multi-process deployments, supply a distributed implementation (e.g. Redis-backed).

None
realtime RealtimeBackend | None

Realtime backend for ephemeral events (typing, presence). Defaults to InMemoryRealtime. For multi-process deployments, supply a distributed implementation (e.g. Redis pub/sub).

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 InMemoryTaskRunner.

None
delivery_strategy DeliveryStrategy | str | None

Controls when proactive content is delivered, a background task's result included: the result is handed back through deliver() after the ON_TASK_COMPLETED hook (RFC §23.3). deliver() takes a strategy per call.

None
delivery_backend DeliveryBackend | None

Persistent delivery backend. When set, kit.deliver() enqueues items instead of executing in-process, and a worker loop dequeues and executes them. Defaults to None (in-process delivery).

None
status_bus StatusBus | None

Shared status bus for multi-agent coordination. Defaults to a StatusBus with InMemoryStatusBackend. Access via kit.status_bus.

None
telemetry TelemetryConfig | TelemetryProvider | None

Optional telemetry provider or config for span/metric collection. Accepts a TelemetryProvider instance or a TelemetryConfig. Defaults to NoopTelemetryProvider.

None
inbound_rate_limit RateLimit | None

Optional rate limit applied to all inbound messages before any processing. Messages exceeding the limit are dropped with reason="rate_limited". Keyed per channel_id.

None
orchestration Orchestration | None

Default orchestration strategy applied to rooms created via create_room() unless overridden per-room.

None
persistence_policy PersistencePolicy | None

Controls which event types are persisted. When None (default), all events are persisted. Use PersistencePolicy(exclude_types={...}) to skip specific types or PersistencePolicy(persist_types={...}) to whitelist.

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 delivery_skipped framework event (RFC §10.2). The skip bounds the loss to the crash window; it never covers an index this process holds a plan for.

30.0
delivery_claim_lock_manager RoomLockManager | None

Lock manager used for the per-room delivery claims. Defaults to lock_manager. Supply a separate PostgresAdvisoryLockManager (own pool) in multi-process deployments: a claim is held for the length of a delivery set — provider round trips included — and must not starve the room-lock pool that commits depend on.

None

store property

store

The backing conversation store.

hook_engine property

hook_engine

The hook engine used for sync/async hook pipelines.

realtime property

realtime

The realtime backend for ephemeral events.

status_bus property

status_bus

Shared status bus for multi-agent coordination.

create_room instance-attribute

create_room

get_room instance-attribute

get_room

register_channel instance-attribute

register_channel

attach_channel instance-attribute

attach_channel

detach_channel instance-attribute

detach_channel

join instance-attribute

join

deliver instance-attribute

deliver

close_room async

close_room(room_id, *, organization_id=None)

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_room(room_id, *, organization_id=None)

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_room_timers(room_id, *, organization_id=None)

Check and apply timer-based transitions for a single room.

Returns the room (possibly transitioned to PAUSED or CLOSED).

check_all_timers async

check_all_timers(*, organization_id=None)

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(room_id, metadata, *, organization_id=None)

Update room metadata.

set_agent_response_policy async

set_agent_response_policy(room_id, policy, *, organization_id=None)

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.

mute async

mute(room_id, channel_id, *, organization_id=None)

Mute a channel in a room.

unmute async

unmute(room_id, channel_id, *, organization_id=None)

Unmute a channel in a room.

set_visibility async

set_visibility(room_id, channel_id, visibility, *, organization_id=None)

Set visibility for a channel in a room.

set_access async

set_access(room_id, channel_id, access, *, organization_id=None)

Set access level for a channel in a room.

update_binding_metadata async

update_binding_metadata(room_id, channel_id, metadata, *, organization_id=None)

Update metadata on a channel binding.

get_channel

get_channel(channel_id)

Get a registered channel by ID.

list_channels

list_channels()

List all registered channels.

get_binding async

get_binding(room_id, channel_id, *, organization_id=None)

Get a channel binding. Raises ChannelNotFoundError if missing.

list_bindings async

list_bindings(room_id, *, organization_id=None)

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 newest_first is set.

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 index > after_index.

None
before_index int | None

Return events with index < before_index.

None
newest_first bool

In offset-based mode, return the most recent limit events instead of the oldest. Ignored when a cursor is supplied.

False
include_blocked bool

Serve the rows the room refused too (status BLOCKED), for audit.

False

list_tasks async

list_tasks(room_id, status=None, *, organization_id=None)

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 True, run the agent inline and return a pre-completed :class:DelegatedTask. If False (default), submit as a background task.

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 (DelegatedTaskResult) -> None.

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 output is then the tool's JSON payload.

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 submit_result (:data:~roomkit.orchestration.result.SUBMIT_RESULT).

None
post_status bool

Follow the task on :attr:status_bus (RFC §23.3): pending once announced, then completed or failed. False for a caller that posts its tasks there itself (an orchestration strategy's worker run), so none shows twice.

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", the notified channel is told it (step 8), or "post", it is posted as it is, as a message of the notified agent, without a turn of its model. "post" needs notify (or agent_id) to name an intelligence channel attached to the room, and a background task.

'relay'

Returns:

Name Type Description
A DelegatedTask

class:DelegatedTask handle. When wait is True,

DelegatedTask

the result is already set. When False, call .wait()

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 "post" the notified channel cannot take, or a negative conversation.

cancel_task async

cancel_task(task_id)

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

True when the task was running and is now cancelled.

list_observations async

list_observations(room_id, *, organization_id=None)

List observations for a room.

leave async

leave(session)

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_inbound(message, *, room_id=None, defer_delivery=False, organization_id=None)

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 result.delivery holds a :class:DeliveryHandle on the rest of the turn: delivery execution, reentry passes (an AI reply included) and streamed responses, all following in the room's lane. For a caller that must answer with the committed event while the agent's turn runs on (an HTTP route returning 200, say); delivery_results is backfilled by delivery.wait().

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 [] so the stored message solicits nobody, and one talking to a specific agent in a room that holds several names it. None leaves the event unaddressed — the router decides, or every eligible intelligence channel acts.

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:_resolve_thread_root.

None
idempotency_key str | None

Stable de-duplication key. When set, the locked pipeline's idempotency check (backed by the unique events(room_id, idempotency_key) index) skips a re-send that carries the same key — so a caller that may replay a publish (an outbox dispatcher redelivering after a crash) gets at-most-once persistence for a given key. None keeps the prior behaviour (no de-duplication), matching inbound events that carry no key.

None
standalone bool

For an INSTRUCTION only: the turn it opens reads nothing of the room — no history, no memory provider call (RFC §10.1.1 step 7). Raises on any other event type.

False

commit_event async

commit_event(room_id, event, *, organization_id=None)

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_room_recording(room_id, recorders, *, organization_id=None)

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

room_recordings(room_id, *, organization_id=None)

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

add_room_recording_track(room_id, track, *, organization_id=None)

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_room_recording(room_id, *, organization_id=None)

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

regenerate_response(room_id, *, trigger_id=None)

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

regenerate_target(room_id)

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

ensure_participant(room_id, channel_id, participant_id, display_name=None, *, organization_id=None)

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_verification(request_id, secret, *, method=None, organization_id=None)

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

record_failed_attempt(request_id, *, organization_id=None)

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

revoke_verification(identity_id, *, reason='revoked', organization_id=None)

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

verification_of(identity_id, *, organization_id=None)

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

sender_verification(event, context)

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

check_verifications()

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

connect_websocket(channel_id, connection_id, send_fn, *, room_id, stream_send_fn=None)

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

disconnect_websocket(channel_id, connection_id)

Unregister a WebSocket connection and emit framework event.

mark_read async

mark_read(room_id, channel_id, event_id, *, organization_id=None)

Mark an event as read for a channel.

mark_all_read async

mark_all_read(room_id, channel_id, *, organization_id=None)

Mark all events as read for a channel.

publish_typing async

publish_typing(room_id, user_id, is_typing=True, data=None)

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_presence(room_id, user_id, status)

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_read_receipt(room_id, user_id, event_id)

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_room(room_id, callback)

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_room(subscription_id)

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 (ValueError on an ASYNC hook, which cannot block) — a timeout, an exception or an unusable result blocks the payload instead of letting it through, with reason="hook_timeout:<name>" / hook_error:<name> / hook_invalid_result:<name> (RFC §9.3). For content checks.

False
needs_lock bool

SYNC BEFORE_BROADCAST only — False runs the hook before the room lock is taken, so its I/O does not hold the room; arrival order is kept by a per-room admission ticket (RFC §9.5.1). Only for a hook that reads the event, not the room's state. Raises ValueError on another trigger, or when a locked hook would be ordered before it by priority.

True

on

on(event_type)

Decorator to register a framework event handler filtered by type.

identity_hook

identity_hook(trigger, channel_types=None, channel_ids=None, directions=None)

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.

remove_room_hook

remove_room_hook(room_id, name)

Remove a room hook by name.

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

locked(room_id)

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

close()

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

InMemoryLockManager(max_locks=1024)

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.

size property

size

Return the number of locks currently held.

locked async

locked(room_id)

Acquire the lock for a room (reentrant via ContextVar).

AuthCallback module-attribute

AuthCallback = Callable[[Any], Awaitable[dict[str, Any] | None]]

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.