Skip to content

Tools

Tool system for AI function calling. See the Tool Calling & Policies guide and MCP Tool Provider guide for usage examples.

ToolPolicy

Bases: BaseModel

Per-agent allow/deny rules for tool access.

Rules use :func:fnmatch.fnmatch glob patterns (e.g. "mcp_*", "search_*").

Resolution order:

  1. Empty allow and empty deny → permit all (backward compatible).
  2. If the tool name matches any deny pattern → blocked.
  3. If allow is non-empty and the tool name matches no allow pattern → blocked.
  4. Otherwise → permitted.

In short: deny always wins, and a non-empty allow list is a whitelist.

Role overrides ~~~~~~~~~~~~~~

role_overrides maps :class:~roomkit.models.enums.ParticipantRole string values (e.g. "observer", "member") to :class:RoleOverride instances.

Call :meth:resolve with a role to obtain an effective ToolPolicy that merges the base rules with the role-specific override.

resolve

resolve(role=None)

Return an effective ToolPolicy for the given role.

If role is None or has no override entry, returns self unchanged (backward compatible).

is_allowed

is_allowed(tool_name)

Return True if tool_name passes the policy.

as_filter

as_filter()

Return a callable (tool_name) -> bool suitable for :func:filter.

RoleOverride

Bases: BaseModel

Per-role tool policy override.

mode controls how the override combines with the base policy:

  • "restrict" (default): deny lists are unioned, allow lists are intersected (a tool must pass both the base and override allow lists).
  • "replace": the override completely replaces the base policy.

MCPToolProvider

MCPToolProvider(url=None, *, transport='streamable_http', tool_filter=None, headers=None, command=None, args=(), env=None, cwd=None, discover=True)

Discover and invoke tools from an MCP server.

Three transports: streamable_http (default) and sse for a server reached by URL, and stdio for a server RoomKit starts as a subprocess and talks to over its stdin/stdout — the way most MCP servers are run.

Usage::

async with MCPToolProvider.from_url("http://localhost:8000/mcp") as mcp:
    tools = mcp.get_tools()          # list[AITool]
    handler = mcp.as_tool_handler()   # ToolHandler for AIChannel

async with MCPToolProvider.from_command("uvx", ["mcp-server-time"]) as mcp:
    ...

The tools are listed once, on entry; no call lists them again (RFC §21.2). discover=False connects without listing, for a host that only reads the server's resources.

Enter and exit it in the same task (async with): the MCP SDK's transports hold anyio cancel scopes that must close where they opened.

connected property

connected

Whether the provider holds a live connection: entered, not yet exited.

tool_names property

tool_names

Return the names of all discovered tools.

server_extensions property

server_extensions

The extensions the server declared when it connected, by identifier, with their settings (io.modelcontextprotocol/skills among them); empty for a server that declared none (RFC §21.2).

instructions property

instructions

What the server published, when it connected, on how to use its tools (InitializeResult.instructions); None when it published nothing (RFC §21.2).

The server's text, as it sent it: hand it to a channel's server_instructions, which sets it apart and bounds it, rather than into a prompt directly.

server_info property

server_info

What the server said of itself when it connected (serverInfo: name, version...), as it sent it (RFC §21.2).

The server's own words, not a name for it: a host names a server by a label of its own (server_instructions, add_mcp_server).

from_url classmethod

from_url(url, *, transport='streamable_http', tool_filter=None, headers=None, discover=True)

Create an MCPToolProvider for the given URL.

The provider is not connected until used as an async context manager.

Parameters:

Name Type Description Default
url str

MCP server URL.

required
transport str

"streamable_http" (default) or "sse".

'streamable_http'
tool_filter Callable[[str], bool] | None

Optional predicate to include only matching tool names.

None
headers dict[str, str] | None

Optional HTTP headers sent with every request.

None
discover bool

List the server's tools on entry (the default). False connects without listing, for a host that only reads resources: :meth:get_tools and :meth:tool_meta are then empty, and no call is checked against an output schema.

True

Returns:

Type Description
MCPToolProvider

An MCPToolProvider instance (not yet connected).

from_command classmethod

from_command(command, args=(), *, env=None, cwd=None, tool_filter=None, discover=True)

Create an MCPToolProvider for a server started as a subprocess (stdio).

Entering the provider starts command with args; exiting stops it.

Parameters:

Name Type Description Default
command str

Executable of the MCP server ("uvx", "npx", a path).

required
args Sequence[str]

Its arguments, passed as a list: no shell is involved.

()
env dict[str, str] | None

Extra environment variables for the server. The MCP SDK starts it with a minimal environment (HOME, PATH, USER…), not the whole of this process's: pass what the server needs, an API key say, here.

None
cwd str | None

Working directory of the server.

None
tool_filter Callable[[str], bool] | None

Optional predicate to include only matching tool names.

None
discover bool

List the server's tools on entry (the default); see :meth:from_url.

True

Returns:

Type Description
MCPToolProvider

An MCPToolProvider instance (not yet connected).

get_tools

get_tools()

Return discovered tools as RoomKit AITool instances.

get_tools_as_dicts

get_tools_as_dicts()

Return discovered tools as plain dicts (for binding metadata).

tool_meta

tool_meta()

The _meta each discovered tool was listed with, by tool name.

Read from the listing made at connection, so no second tools/list: an MCP App's ui (resourceUri, csp) among the rest. A tool listed without one, or not discovered (tool_filter, a name no provider accepts, discover=False), is absent.

read_resource async

read_resource(uri, *, timeout=_DEFAULT_CALL_TIMEOUT)

The server's ReadResourceResult for uri (an MCP App's HTML, say).

Raises:

Type Description
McpError

The server refused the read (an unknown resource).

TimeoutError

No answer within timeout seconds.

request async

request(method, params=None, *, timeout=_DEFAULT_CALL_TIMEOUT)

Send method, a request the SDK does not model (skills/list, say), and return its result as the server sent it (RFC §21.2).

Raises:

Type Description
McpError

The server answered with an error.

TimeoutError

No answer within timeout seconds.

call_tool_result async

call_tool_result(name, arguments, *, timeout=_DEFAULT_CALL_TIMEOUT)

Call the tool and return the server's CallToolResult as it is.

A failure is the result's isError, not an exception: :meth:call_tool renders it into its error envelope, the tool handler raises it (:class:~roomkit.core.exceptions.ToolFailedError). For a host relaying the raw result (an MCP App's frame calling its server). A successful result's structuredContent is also published to the tool call in progress, when there is one (the model's own calls).

Like :meth:call_tool, it calls any tool the server has: tool_filter shapes what discovery offers a model, not what the host may call (an app-only tool the frame calls, say). The host authorizes the call.

A successful result of a tool listed with an output schema is checked against it, and one that breaks it raises RuntimeError, as the MCP SDK does. A tool the listing did not name is called as it is: no call lists the server's tools (RFC §21.2).

call_tool async

call_tool(name, arguments, *, timeout=_DEFAULT_CALL_TIMEOUT)

Call a tool on the MCP server and return the result as a string.

Parameters:

Name Type Description Default
name str

Tool name.

required
arguments dict[str, Any]

Tool arguments dict.

required
timeout float

Maximum seconds to wait for a response.

_DEFAULT_CALL_TIMEOUT

Returns:

Type Description
str

Result string. Single TextContent → plain text; multi-part → JSON array;

str

error results → {"error": "..."}.

The error envelope is this method's contract and does not change. A tool loop reads :meth:as_tool_handler instead, which raises :class:~roomkit.core.exceptions.ToolFailedError so the outcome does not have to be recognised in the body.

as_tool_handler

as_tool_handler(*, gate_discovery=True)

Return a ToolHandler suitable for AIChannel(tool_handler=...).

A tool that is not from this MCP server raises :class:~roomkit.core.exceptions.UnservedToolCallError, which hands the call to the next handler of a compose_tool_handlers chain and reads as served by nothing on a channel (RFC §21.4).

gate_discovery=False forwards every name to the server instead. A gateway that routes by name prefix and authenticates the caller per call serves tools this connection never listed — a server whose tools/list answers only behind the caller's own credential, say — and a host with its own allow-list in front has already decided what the model may call. Such a handler never raises UnservedToolCallError, so it sits last in a compose_tool_handlers chain: nothing after it would be reached. A provider connected with discover=False discovered nothing, so its gated handler serves no name: every call raises UnservedToolCallError.

A tool whose result says isError raises :class:~roomkit.core.exceptions.ToolFailedError either way: the tool ran and failed, so the tool loop marks the call failed (not refused) and hands the server's message to the model unchanged.

A result that carries an image (PNG, JPEG, GIF or WebP, with a payload that decodes) comes back as content parts (:class:~roomkit.providers.ai.base.AITextPart and :class:~roomkit.providers.ai.base.AIImagePart), so the model sees the image; any other result is the string :meth:call_tool returns. Binary content the model cannot take (another image format, audio, a blob resource) becomes a one-line note rather than its base64.

compose_tool_handlers

compose_tool_handlers(*handlers)

Chain multiple ToolHandlers so the first one that handles a tool wins.

Each handler is tried in order. A handler that declines the call, by raising :class:~roomkit.core.exceptions.UnservedToolCallError or by answering the {"error": "Unknown tool: ..."} envelope (RFC §21.4), hands it to the next one. The last handler's answer is the composition's, a decline included: the channel then reads the call as served by nothing.

Parameters:

Name Type Description Default
*handlers ToolHandler

Two or more ToolHandler callables.

()

Returns:

Type Description
ToolHandler

A single ToolHandler that dispatches to the first matching handler.

Raises:

Type Description
ValueError

If fewer than two handlers are provided.

Per-Call Context

A tool handler receives only (name, arguments). These accessors read the turn it is running under from a contextvar — see What a handler knows about the call.

current_tool_room_id

current_tool_room_id()

Room id of the tool loop the caller is executing under.

Returns None when called outside a tool loop.

current_tool_room

current_tool_room()

The :class:~roomkit.models.room.Room of the turn the caller is executing under.

The room as the store loaded it when the turn began: the same object RoomContext.room holds for that turn's hooks, memory provider and config provider, so a handler deciding whom a call acts for reads the room's organization_id, metadata or status here instead of re-reading the room by :func:current_tool_room_id on every call. On a realtime tool call, which runs no turn, it is the room as the store loaded it for that call, shared with the BEFORE_TOOL_USE gate's context when a hook made the channel build one. A patch written to the store during the turn, by this handler or another, is not in it; re-read the room when the turn's own writes matter. Do not mutate it: the object is shared with the whole turn, a room changes through the store, and a write on this object would be read by the rest of the turn (the agent-response policy, the delivery plan) as if the room had changed.

Like :func:current_tool_actor_id, it names the turn and authenticates nothing. Which organization the room belongs to is a fact of the room; whether the caller may act for it is the host's rule, applied by the host.

None outside a tool loop, and None for a loop started without a turn above it.

current_tool_actor_id

current_tool_actor_id()

Participant id of whoever's turn the caller is executing under.

The author of the event that woke the channel this round. Read it rather than the identity a handler captured when it was built — one channel object serves every room and every speaker, so a captured identity is whoever happened to attach it.

It names the turn; it does not authenticate it. The value is a room Participant.id, and the inbound pipeline only substitutes the resolved Identity.id for it once identification succeeds — a turn still pending, ambiguous or unknown carries whatever the channel supplied, or a synthetic pending-…, and reads back just as non-None. A handler that reaches a person's data with it resolves it first: load the participant, require Participant.identification to be IDENTIFIED, and take Participant.identity_id as the principal.

The author need not be human, either. In a multi-agent room the waking event may be another agent's, whose participant id reads back the same way — compare the participant's role against ParticipantRole.AGENT when that distinction matters.

None outside a tool loop, and None when the turn has no participant behind it (a system injection, a webhook, a scheduled run). A caller that needs a person then decides for itself — refuse, or fall back to a principal it configured on purpose — rather than borrow whoever spoke last.

current_tool_allowed_names

current_tool_allowed_names()

Names of every tool in the current turn's resolved toolset that its tool policy admits, with what a round declared beyond it (the re-read of a stored result, from the round that declared it), nothing withdrawn.

_build_context stamps the turn's full toolset (config-provider result plus channel-injected tools) into the loop context; a host's tool handler can validate an incoming call against it instead of an attach-time snapshot that goes stale on shared channels. A tool the policy denies the turn's actor is left out, on every channel: the gate refuses it before any handler. Includes skill-gated tools whose visibility is filtered per round — gating is presentation, not an execution boundary.

On a realtime tool call (a voice session, a conference) it is every tool the session declares that its policy admits: its catalogue, what orchestration set up, the channel's own (RFC §21.4); None when the session declares no catalogue: it names no list, and its gate, policy included, still judges each call.

Returns None outside a tool loop or before context build, so hosts can fall back to their own allowlist.

current_tool_call

current_tool_call()

The per-call context of the tool call the caller is executing under.

What _run_one set before invoking the handler — the call's id, its room, its channel — and the reverse channel the handler may fill: structured_content, the MCP structured result the tool-call events carry for UI surfaces, as it is but for binary payloads past the event's bound. A host that rewrites a result before the model reads it (a provider's private address turned into its own relay link, say) reaches the structured copy here, so the persisted event does not keep what the text no longer says.

None outside a tool call.

ToolCallContext dataclass

ToolCallContext(room_id='', tool_call_id='', channel_id='', structured_content=None)

Contextvar payload carrying tool-call metadata.

The ToolHandler protocol is (name, arguments) → result — it does not receive room_id, tool_call_id or channel_id. This payload bridges the gap: _ai_tools._run_one() sets it before calling the handler, and a handler that needs the call's origin reads it. Safe with :func:asyncio.gather, which creates Tasks with copied contexts.

structured_content is the reverse channel: the ToolHandler contract returns only text or content parts, but MCP tools can produce a structured result (CallToolResult.structuredContent) that UI surfaces need as it is — the LLM-facing string may be truncated/evicted when large. A handler that has one sets it here; _run_one() reads it back after the call and carries it on the tool-call events untouched by eviction, its binary payloads bounded like the result's.

current_response_metadata

current_response_metadata()

The response-metadata record of the turn the caller is executing under.

The one mapping RoomKit merges into every MESSAGE event the turn produces (see :mod:roomkit.models.response_metadata): a memory provider writing it during context build, a BEFORE_AI_GENERATION hook writing event.ai_context.response_metadata, and a tool handler or a BEFORE_TOOL_USE hook writing here (the hook runs under the call's turn) all reach the same object, the one InboundResult.response_metadata hands the caller, the turn answered or failed. A tool handler is the case this exists for — the ToolHandler protocol hands it nothing but (name, arguments), and a document it read is a fact about the turn, not about the tool's string result.

Returns None when no loop context is set (a direct call) and on a realtime tool call, where no turn merges the record: the guard if record is not None then skips a write nothing would carry. A loop started without a turn — no handle_event above it — carries a record of its own that no MESSAGE event is built from; writes to it are harmless and go nowhere.

tool_turn_context

tool_turn_context(*, room_id=None, room=None, actor_id=None, tools=None, chain_depth=0, call=None, requester=None)

Run the enclosed code as a tool call of a turn described by the arguments.

What a test calling a tool handler directly needs: inside the block the accessors of this module answer for the turn described here, as they do for a call the tool loop makes, and on the way out (an exception included) they answer what they answered before. A handler called outside a tool loop otherwise reads None everywhere.

Parameters:

Name Type Description Default
room_id str | None

The turn's room id, read by :func:current_tool_room_id.

None
room Room | None

The turn's :class:~roomkit.models.room.Room, read by :func:current_tool_room; its id is the turn's room id, so room_id may be left out. Both given must name the same room.

None
actor_id str | None

Whose turn it is, read by :func:current_tool_actor_id; None for a turn with no author (a system injection, a webhook).

None
tools Iterable[AITool] | None

The turn's resolved toolset, read by :func:current_tool_allowed_names. None stands for a turn whose toolset was not resolved (the accessor answers None); an empty list for a resolved, empty one.

None
chain_depth int

The chain depth of the response the turn produces (RFC §8.3), which a result delivered later on its behalf inherits. 0, the default, is what outside a turn reads (RFC §21.4); an AI channel's turn answering a human's message runs at 1.

0
requester str | None

Who asked, read by :func:current_tool_requester; None for a one-to-one conversation.

None
call ToolCallContext | None

The per-call record :func:current_tool_call answers; a handler writing its structured_content writes this object. Its room_id is the turn's, as the tool loop builds it ("" for a turn without a room). None leaves the block outside any call record.

None

The turn carries a fresh response-metadata record, which :func:current_response_metadata answers inside the block.

Raises:

Type Description
ValueError

The arguments name more than one room: room and room_id, or the turn's room and call's room_id.

Turn Response Metadata

The record every writer of a turn shares — see What a handler can tell the turn.

ResponseMetadata

ResponseMetadata(initial=None)

Bases: MutableMapping[str, Any]

Turn-level metadata merged into every MESSAGE event the turn produces.

Behaves as a dict for every reader and writer ([...], .get, .update, **, dict(...), ==); the only thing it adds is that Pydantic keeps the instance instead of copying it, so one turn has one record. A bare mapping passed where this type is expected is wrapped — the caller's dict is then a snapshot, which is what passing a literal means.

Each MESSAGE event carries the record as it stands when the event is created: a streamed segment persisted before a tool round shows what was known then, the final answer shows everything the turn learned; the non-streaming path builds all its events at the end, so they read alike.

coerce classmethod

coerce(value)

The instance itself, or a bare mapping wrapped — anything else is refused.

Human-in-the-Loop

HumanInputHandler

HumanInputHandler(*, retention=128)

Manages pending human input requests.

Core lifecycle::

pending = await handler.create("AskUser", args, room_id="r1", ...)
# → request is answerable from here on; the
#   ON_USER_INPUT_REQUIRED notification runs alongside
result  = await handler.wait(pending.pending_id, timeout=300)
# → blocks until resolve() / reject() / timeout

Two invariants the caller can rely on:

  • The notification never gates the answer. create() arms the request and returns; the _on_input_required callback runs in a background task. A human who answers while that callback is still running — a slow WebSocket broadcast, a hook burning its 30 s budget — is answering a request that is already listening. A denial coming back from the callback rejects the request, and wait() reports it.
  • A recorded outcome stays readable. A request settling — answered, rejected, timed out — is kept in a bounded retention (retention entries, newest kept), and wait() replays it once the request has left the active set. Only a genuinely unknown id raises ValueError, so neither a second read nor a host that keeps its own bookkeeping can turn an answer that arrived into a hard failure.
  • A channel scope belongs to a channel object, not to its id. A host that rebuilds the channel serving an id — the same agent re-attached to a second room — hands the same shared handler a succession of owners. Registering re-opens the scope, and the departing owner's close() is a no-op once a newer one has taken over, so a predecessor being torn down cannot silence its live successor.

The _on_input_required callback is injected by the framework (via register_channel hook builder) or set by the application directly.

pending property

pending

Active pending requests (read-only snapshot).

close async

close(*, channel_id=None, registration=None)

Stop notifications and settle requests owned by one channel.

When channel_id is omitted, all work owned by this handler is stopped. Channel-scoped closing lets a handler be shared safely by multiple :class:~roomkit.channels.ai.AIChannel instances.

registration is the token :meth:_set_on_input_required handed the closing channel. Passing it makes the close belong to that channel object rather than to the id it used: a channel displaced from the registry and torn down later closes nothing, because the id is already serving its replacement. Omitting it closes the scope unconditionally, which is what a lone owner and a manual host call both want.

create async

create(tool_name, arguments, *, room_id='', tool_call_id='', channel_id='', channel_type=AI, actor_id=None)

Register a new pending input request and schedule the callback.

Returns as soon as the request is answerable — the _on_input_required callback runs in a background task and a denial from it rejects the request wherever wait() has got to.

wait() owns this request's cleanup; for a request no one will wait on, use :meth:create_detached.

actor_id names whose turn raised the request, so a notification layer can ask that person rather than the whole room. The native :class:HumanInputToolHandler fills it from the tool loop; a caller driving its own loop passes what it knows.

create_detached async

create_detached(tool_name, arguments, *, room_id='', tool_call_id='', channel_id='', channel_type=AI, actor_id=None)

Register a pending request that no one will :meth:wait on.

For runtimes that own their own tool loop — a Claude Code sandbox, say — where create() exists to raise the request and the answer travels back another way. Nothing here retires the request, so its creator MUST call :meth:release when done with it; otherwise the entry lives as long as the handler.

wait async

wait(pending_id, *, timeout=300)

Block until the request is resolved, rejected, or times out.

An outcome already reached and consumed is replayed from the retention, so waiting twice — or waiting after someone else dropped their own record of the request — reports what happened rather than an error.

Returns:

Type Description
str

The result string on resolution.

Raises:

Type Description
TimeoutError

If the timeout expires, or if a retained request had timed out.

HumanInputRejectedError

If the request was rejected, by a human or an ON_USER_INPUT_REQUIRED hook (a :class:RuntimeError).

RuntimeError

If the handler gave the request up before an answer (its close, a release), or is closed.

ValueError

If pending_id is unknown — never seen, or retired long enough ago to have been evicted from the retention.

release

release(pending_id)

Drop a request whose cleanup the caller owns.

The counterpart of :meth:create_detached. A request still unanswered is rejected on the way out, so a stray waiter unblocks instead of hanging; the outcome goes to the retention either way and stays readable by :meth:wait.

Returns True if an active request was dropped.

resolve

resolve(pending_id, result)

Resolve a pending request with a result.

Returns True if the request was found and resolved.

reject

reject(pending_id, reason='')

Reject a pending request.

Returns True if the request was found and rejected.

HumanInputToolHandler

HumanInputToolHandler(tool_names, timeout=300, handler=None, tool_definitions=None)

ToolHandler wrapper that blocks on human input for specified tools.

Composes with other handlers via :func:~roomkit.tools.compose.compose_tool_handlers. Declines a non-matching tool name by raising :class:~roomkit.core.exceptions.UnservedToolCallError, so the compose chain continues to the next handler (RFC §21.4).

Pass this as human_input_handler= to an :class:~roomkit.channels.ai.AIChannel, a :class:~roomkit.channels.realtime_voice.RealtimeVoiceChannel or a :class:~roomkit.conference.models.ConferenceRealtimeConfig: the channel declares :attr:tools and serves :attr:tool_names before the host's handler, on every door, under :attr:timeout rather than its default call bound, and the framework injects the ON_USER_INPUT_REQUIRED hook callback at registration time (RFC §9.3, §21.6). Given as a plain tool_handler it is served as any other handler, without those rules.

handler property

handler

The underlying :class:HumanInputHandler for resolve/reject access.

tools property

tools

Tool definitions to inject into the AI context.

__call__ async

__call__(name, arguments)

ToolHandler protocol: :meth:ask on an AI channel's door.

ask async

ask(name, arguments, *, channel_type=AI)

Block on a person's answer to the call name; decline any other.

channel_type is the door the call came through, which the request and its ON_USER_INPUT_REQUIRED event name. Raises :class:~roomkit.core.exceptions.UnservedToolCallError for a name outside :attr:tool_names, so a compose chain goes on to the next handler (RFC §21.4).

Raises :class:~roomkit.core.exceptions.ToolRefusedError when the request is rejected (a human, an ON_USER_INPUT_REQUIRED hook), the model reading why, and :class:~roomkit.core.exceptions.ToolFailedError when nobody answers within :attr:timeout: the tool ran and got no answer. Any other error, the handler giving the request up included, takes the generic failure path, its message withheld from the model (RFC §9.3).

PendingInput dataclass

PendingInput(pending_id, tool_name, arguments, room_id, tool_call_id, channel_id, status=PENDING, result=None, reject_reason=None, detached=False, created_at=_utcnow(), actor_id=None, _event=Event(), _withdrawn=False)

A pending human input request.

Mutable — transitions from PENDING to RESOLVED/REJECTED/TIMED_OUT when the application calls :meth:HumanInputHandler.resolve or :meth:HumanInputHandler.reject.

detached class-attribute instance-attribute

detached = False

No one will call wait() on this request — its creator frees it with :meth:HumanInputHandler.release. wait() owns the cleanup of every other request.

actor_id class-attribute instance-attribute

actor_id = None

Participant whose turn raised this request, when the tool loop knew one.

A request that names nobody is a request a notification layer has to broadcast, and an answer it cannot attribute. None when the turn had no author (a system injection, a webhook, a scheduled run) or when the creator runs its own tool loop and did not supply one. It names the turn without authenticating it — resolve it against the room's roster before treating it as a principal, as current_tool_actor_id() documents.

PendingInputEvent dataclass

PendingInputEvent(pending_id, tool_name, arguments, room_id, tool_call_id, channel_id, channel_type, timestamp=_utcnow(), actor_id=None)

Event fired through ON_USER_INPUT_REQUIRED hooks.

Carries the pending request details so notification layers (WebSocket, REST, etc.) can inform the user.

pending_id instance-attribute

pending_id

Handler-generated ID for resolving this request.

tool_name instance-attribute

tool_name

Name of the tool that requires human input.

arguments instance-attribute

arguments

Tool arguments (e.g. questions, options).

room_id instance-attribute

room_id

Room where the tool call originated.

tool_call_id instance-attribute

tool_call_id

Provider-assigned tool call ID.

channel_id instance-attribute

channel_id

Channel that triggered the tool call.

channel_type instance-attribute

channel_type

Type of the originating channel.

timestamp class-attribute instance-attribute

timestamp = field(default_factory=_utcnow)

When the pending request was created.

actor_id class-attribute instance-attribute

actor_id = None

Participant whose turn raised the request, when the tool loop knew one.

What lets a notification layer ask the person who asked rather than everyone in the room. Appended rather than grouped with the origin fields above so existing positional construction keeps working. None when the turn had no author.

PendingInputStatus

Bases: StrEnum

Status of a pending human input request.