Skip to content

Hooks & Side Effects

HookResult

Bases: BaseModel

Result returned by a sync hook.

event class-attribute instance-attribute

event = None

The replacement payload when action is "modify".

Typed loosely because a sync hook receives whatever its trigger passes, and only one of them passes a RoomEvent: BEFORE_TTS receives a string, the bridge triggers receive frames, the tool and generation triggers receive their own event types. Restricting this to RoomEvent made "modify" impossible for all of them — the hook raised, the engine logged and carried on, and the unmodified payload continued on its way.

Consumers must check the type they expect before using it.

allow classmethod

allow(injected=None)

Allow the event to proceed.

allow_unverified classmethod

allow_unverified()

On ON_VERIFICATION_REQUIRED: let an unverified sender's message through.

It goes on labelled with the sender's verification; without this, the message is held from the room's agents (RFC §11.7.3). A plain :meth:allow does not pass it: a hook that only watches never lets a message through by accident.

block classmethod

block(reason, injected=None, tasks=None, observations=None)

Block the event from proceeding.

modify classmethod

modify(event, injected=None)

Replace the payload before it proceeds.

Takes whatever the trigger passed the hook — a string for BEFORE_TTS, a RoomEvent for BEFORE_BROADCAST — because restricting it to RoomEvent left every other sync trigger unable to use its own constructor.

InjectedEvent

Bases: BaseModel

An event injected by a hook as a side effect.

AI Generation Hooks

What ON_AI_RESPONSE carries once a turn of intelligence completes, and the record its declared_tools is made of. See the hook table in Features.

AIResponseEvent dataclass

AIResponseEvent(channel_id, response_content, room_id=None, tool_calls_count=0, usage=dict(), thinking='', round_count=0, loop_end_reason=None, latency_ms=0, streaming=False, timestamp=_utcnow(), segments=list(), usage_metadata=dict(), declared_tools=list())

Emitted through ON_AI_RESPONSE hooks after AI generation completes.

Provides response content, usage metrics, and timing for evaluation and scoring integrations.

channel_id instance-attribute

channel_id

ID of the AI channel that generated the response.

response_content instance-attribute

response_content

Everything the model said in the turn, as one readable transcript.

The turn's :attr:segments joined with :data:RESPONSE_SEGMENT_SEPARATOR — a blank line at every tool-call boundary, nothing inside a segment. A turn without a tool call is its single segment, verbatim.

room_id class-attribute instance-attribute

room_id = None

Room where the response was generated.

tool_calls_count class-attribute instance-attribute

tool_calls_count = 0

Number of tool calls executed during generation.

usage class-attribute instance-attribute

usage = field(default_factory=dict)

Token usage from the provider (input_tokens, output_tokens).

thinking class-attribute instance-attribute

thinking = ''

Extended thinking/reasoning text (if supported by provider).

round_count class-attribute instance-attribute

round_count = 0

Number of tool execution rounds.

loop_end_reason class-attribute instance-attribute

loop_end_reason = None

Which of the tool loop's rules ended the turn, or None if unreported.

The loop names it on its :class:LoopEndMarker, and this event carries it: without it a hook could see that a turn ended and how much work it did, never whether it finished or was cut off. Counting tool calls does not answer it — :attr:tool_calls_count reports the calls the turn ran, so a healthy multi-round answer and one guillotined by the round cap both report a positive count. Read this instead: "completed" is a turn that ended on its own terms, and "max_rounds", "timeout", "budget_exceeded", "cancelled", "force_stopped", "truncated", "empty_response", "unfinished" and "error" each name the rule that stopped it ("unfinished": the channel's continuation policy still asked to go on once its tries had run out; "error": the provider interrupted the turn after a tool round and the turn was delivered once its loop ended, RFC §6.4). An ACP agent's turn carries its stop reason: "completed" for end_turn, else the agent's own ("max_tokens", "max_turn_requests", "refusal", "cancelled").

A turn whose loop did not reach its end fires no event at all: one that raised (a streamed turn the provider interrupted included) or whose stream was closed first (a barge-in, a transport that stopped reading, a task cancelled from outside). Its llm.generate span carries what its rounds used.

None means the path that fired the hook reported no reason, not that the turn completed.

latency_ms class-attribute instance-attribute

latency_ms = 0

Total generation time in milliseconds.

streaming class-attribute instance-attribute

streaming = False

Whether the response was streamed.

timestamp class-attribute instance-attribute

timestamp = field(default_factory=_utcnow)

When the response was generated.

segments class-attribute instance-attribute

segments = field(default_factory=list)

The turn's text, one entry per stretch between tool calls, in order.

Empty stretches are dropped, so :attr:response_content is exactly these joined with :data:RESPONSE_SEGMENT_SEPARATOR, and segments[-1] is the text that followed the last tool call — the answer, for a consumer that wants it without the narration before it. Empty when the turn produced no text.

usage_metadata class-attribute instance-attribute

usage_metadata = field(default_factory=dict)

Optional provenance and scope of the provider's usage observations.

ACP includes the native session, prompt source and session usage report. These are observations, not a price or a claim that generation succeeded. Existing providers and consumers can leave this empty.

declared_tools class-attribute instance-attribute

declared_tools = field(default_factory=list)

The tools the provider received this turn, over every generation round.

BEFORE_AI_GENERATION sees the turn's toolset once, as the turn starts: under Tool Search the whole catalogue, not what any round declares. A tool find_tools reveals only enters the declaration of the next round, so a host recording "what the model was offered" reads it here. This is the union of the toolsets handed to the provider on each round of the turn, in first declaration order, one entry per name: a tool declared on several rounds keeps its first entry, and since the reveal window slides from one find_tools to the next, the union is the only reading that keeps a tool revealed earlier in the turn. A turn without Tool Search reports its one declaration here too, every entry "always": one reading for the host, whatever the turn's mode.

Empty when the turn declared no tool, and for a channel whose toolset is not RoomKit's to declare (an external ACP agent).

DeclaredTool dataclass

DeclaredTool(name, description, parameters, origin='always')

One tool as the provider received it, and why it was there.

The record :attr:AIResponseEvent.declared_tools is made of. name, description and parameters are the AITool fields RoomKit handed the provider: its declaration, not the provider's wire form of it. The schema is that object, not a copy, so a consumer reads it and never writes it.

from_tool classmethod

from_tool(tool, origin='always')

The record for tool, declared for the reason origin says.

Task

Bases: BaseModel

A task assigned within a room.

Observation

Bases: BaseModel

An observation produced by an intelligence channel.