Extension API Reference

This page is the reference companion to Writing extensions and Events. It documents the objects an extension’s register(mb) function receives and the types it works with.

register(mb) is called once at startup with an ExtensionContext built from the module’s [extensions.config.<module>] section. Import and registration failures are fatal — a module that cannot load stops startup rather than leaving the agent silently unable to use its tools.

ExtensionContext

class minibot.app.extensions.ExtensionContext(name, config, settings, event_bus, logger, entrypoint='daemon', vault=None, agent_registry=None, agent_management=None, refresh_tools=None, tools=<factory>, tool_derivers=<factory>, tool_wrappers=<factory>, subscriptions=<factory>, services=<factory>, routes=<factory>, prompt_fragments=<factory>, pages=<factory>)[source]

Handed to an extension’s register(mb) function.

Deliberately narrow: the container is a class-level singleton, and passing it whole would make every one of its fields public API.

entrypoint is "daemon", "console", or "worker". Channel extensions must contribute nothing outside the daemon: those entrypoints do not drive channel services.

vault is the unlocked credential vault, or None when [vault] enabled is false. Resolve a secret only into a destination the admin configured; never expose one to the LLM.

Parameters:
  • name (str)

  • config (dict[str, Any])

  • settings (Settings)

  • event_bus (EventBus)

  • logger (logging.Logger)

  • entrypoint (ExtensionEntrypoint)

  • vault (SecretVault | None)

  • agent_registry (AgentRegistry | None)

  • agent_management (AgentManagementService | None)

  • refresh_tools (Callable[[], Awaitable[None]] | None)

  • tools (list[ToolBinding])

  • tool_derivers (list[ToolDeriver])

  • tool_wrappers (list[ToolWrapper])

  • subscriptions (list[tuple[type[BaseEvent], EventHandler]])

  • services (list[ExtensionService])

  • routes (list[RouteSpec])

  • prompt_fragments (list[_PromptFragment])

  • pages (list[tuple[str, str, str]])

name: str
config: dict[str, Any]
settings: Settings
event_bus: EventBus
logger: logging.Logger
entrypoint: ExtensionEntrypoint = 'daemon'
vault: SecretVault | None = None
agent_registry: AgentRegistry | None = None
agent_management: AgentManagementService | None = None
refresh_tools: Callable[[], Awaitable[None]] | None = None
tools: list[ToolBinding]
tool_derivers: list[ToolDeriver]
tool_wrappers: list[ToolWrapper]
subscriptions: list[tuple[type[BaseEvent], EventHandler]]
services: list[ExtensionService]
routes: list[RouteSpec]
prompt_fragments: list[_PromptFragment]
pages: list[tuple[str, str, str]]
on(event_type, handler=None)[source]

Subscribe to event_type. Usable directly or as a decorator.

Parameters:
Return type:

Any

tool(func)[source]

Register func as a tool: name from __name__, description from its docstring, parameters from its first argument’s pydantic model.

For anything this does not cover — a name that is not a Python identifier, a hand-written schema, a description loaded from a file — build the ToolBinding yourself and pass it to add_tool.

Parameters:

func (Callable[[Any, ToolContext], Awaitable[Any]])

Return type:

Callable[[Any, ToolContext], Awaitable[Any]]

add_tool(bindings)[source]
Parameters:

bindings (ToolBinding | Sequence[ToolBinding])

Return type:

None

add_tool_deriver(deriver)[source]

Contribute tools built from the tool list an agent ends up with.

add_tool registers before any agent exists, so a tool that must act on other tools (call them, describe them) cannot be built there. deriver runs once per agent, with the final list: after approval, event and output-spill wrapping, tools_allow/tools_deny and the removal of the delegation tools. It returns the extra bindings, which are then approval- and event-wrapped like any tool and filtered by the same agent policy. The input list does not contain them, and it is rebuilt whenever the tool list is, so a deriver must be a pure function of its argument. Runs in the daemon, the console and task workers.

A deriver may return a DerivedTool instead of a plain binding to take over input tools: while the derived tool survives the agent’s policy, the tools it replaces are no longer shown to the model.

Parameters:

deriver (ToolDeriver)

Return type:

None

add_tool_wrapper(wrapper)[source]

Wrap the handlers of every tool an agent can call, to check or change calls and results.

wrapper gets the bindings and an Approver that asks the user in the chat the call came from (denied where the channel has no approval buttons), and returns one binding per input binding, in the same order and with the same tool names; keep spill_after_chars (dataclasses.replace). It covers core, extension, MCP and derived tools alike, in the daemon, the console and task workers, and runs again whenever the tool list is rebuilt, so it must be a pure function of its arguments. Wrappers sit innermost: they see the raw result before output spill, while [tools.approval] and tool-call events stay outside them. Calls nested in a codemode script go through them too, with context.parent_call_id set.

Parameters:

wrapper (ToolWrapper)

Return type:

None

add_service(service)[source]
Parameters:

service (ExtensionService)

Return type:

None

add_route(path, handler, methods=('GET',))[source]

Serve handler at path on the built-in HTTP server ([http] enabled).

The handler takes a starlette.requests.Request and returns a Response. Routes are collected once every extension has registered, so ordering between extensions is irrelevant.

Parameters:
Return type:

None

add_prompt_fragment(text, *, tool_names=())[source]

Append text to the main agent’s system prompt.

For guidance the model needs before it picks a tool and that no single tool description can carry – an MCP server’s instructions, a backend’s conventions. Tool-specific wording belongs in that tool’s description, not here: this is paid for on every single turn. When tool_names is set, the fragment is included only if one of those tools is attached.

Parameters:
Return type:

None

add_page(path, label, handler, methods=('GET',), *, icon='file')[source]

Like add_route, plus an entry in the web UI’s navigation menu under label.

icon is a Lucide icon name (https://lucide.dev/icons), shown next to the label. An extension that bails out of register() never calls this, so a disabled extension drops out of the menu on its own — the menu needs no separate notion of what is enabled.

Parameters:
Return type:

None

ExtensionService

Objects passed to mb.add_service implement this protocol. They are started when the entrypoint starts and stopped on shutdown.

class minibot.app.extensions.ExtensionService(*args, **kwargs)[source]
async start()[source]
Return type:

None

async stop()[source]
Return type:

None

Tools

class minibot.llm.tools.base.ToolBinding(tool, handler, spill_after_chars=None)[source]

A tool schema and its handler.

spill_after_chars lets a tool keep larger results inline than [tools.tool_output_spill] spill_after_chars (it never lowers it). Where spill cannot run, the tool’s results are cut to it.

Parameters:
tool: Tool
handler: Callable[[dict[str, Any], ToolContext], Awaitable[ToolResult | Any]]
spill_after_chars: int | None = None
class minibot.llm.tools.base.ToolContext(owner_id: 'str | None' = None, channel: 'str | None' = None, channel_capabilities: 'ChannelCapabilities' = <factory>, chat_id: 'int | None' = None, user_id: 'int | None' = None, turn_id: 'str | None' = None, task_handoff_callback: 'Callable[[str], Awaitable[None]] | None' = None, task_chain_depth: 'int' = 0, claim_task_continuation: 'Callable[[], bool] | None' = None, release_task_continuation: 'Callable[[], None] | None' = None, call_id: 'str | None' = None, parent_call_id: 'str | None' = None)[source]
Parameters:
  • owner_id (str | None)

  • channel (str | None)

  • channel_capabilities (ChannelCapabilities)

  • chat_id (int | None)

  • user_id (int | None)

  • turn_id (str | None)

  • task_handoff_callback (Callable[[str], Awaitable[None]] | None)

  • task_chain_depth (int)

  • claim_task_continuation (Callable[[], bool] | None)

  • release_task_continuation (Callable[[], None] | None)

  • call_id (str | None)

  • parent_call_id (str | None)

owner_id: str | None = None
channel: str | None = None
channel_capabilities: ChannelCapabilities
chat_id: int | None = None
user_id: int | None = None
turn_id: str | None = None
task_handoff_callback: Callable[[str], Awaitable[None]] | None = None
task_chain_depth: int = 0
claim_task_continuation: Callable[[], bool] | None = None
release_task_continuation: Callable[[], None] | None = None
call_id: str | None = None
parent_call_id: str | None = None

Events

Subscribe to any of these with @mb.on(EventType) or mb.on(EventType, handler). See Events for when each one fires and what its payload carries. Handlers are async and lossy: a slow handler drops events rather than blocking the pipeline.

class minibot.core.events.EventPublisher(*args, **kwargs)[source]
async publish(event)[source]
Parameters:

event (BaseEvent)

Return type:

None

class minibot.core.events.MessageEvent(*, event_id=<factory>, event_type='message', message)[source]
Parameters:
  • event_id (str)

  • event_type (str)

  • message (ChannelMessage)

event_type: str
message: ChannelMessage
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class minibot.core.events.OutboundEvent(*, event_id=<factory>, event_type='outbound', response)[source]
Parameters:
  • event_id (str)

  • event_type (str)

  • response (ChannelResponse)

event_type: str
response: ChannelResponse
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class minibot.core.events.OutboundFileEvent(*, event_id=<factory>, event_type='outbound_file', response)[source]
Parameters:
  • event_id (str)

  • event_type (str)

  • response (ChannelFileResponse)

event_type: str
response: ChannelFileResponse
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class minibot.core.events.OutboundFormatRepairEvent(*, event_id=<factory>, event_type='outbound_format_repair', response, parse_error, attempt=1, chat_id, channel, user_id=None, capabilities=<factory>)[source]
Parameters:
  • event_id (str)

  • event_type (str)

  • response (ChannelResponse)

  • parse_error (str)

  • attempt (int)

  • chat_id (int)

  • channel (str)

  • user_id (int | None)

  • capabilities (ChannelCapabilities)

event_type: str
response: ChannelResponse
parse_error: str
attempt: int
chat_id: int
channel: str
user_id: int | None
capabilities: ChannelCapabilities
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class minibot.core.events.SystemEvent(*, event_id=<factory>, event_type='system', payload=None)[source]
Parameters:
  • event_id (str)

  • event_type (str)

  • payload (dict | None)

event_type: str
payload: dict | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class minibot.core.events.TurnStartedEvent(*, event_id=<factory>, event_type='turn_started', turn_id, channel, chat_id=None, user_id=None, available_tools=<factory>)[source]

Emitted when the dispatcher begins processing an inbound message.

Parameters:
  • event_id (str)

  • event_type (str)

  • turn_id (str)

  • channel (str)

  • chat_id (int | None)

  • user_id (int | None)

  • available_tools (list[str])

event_type: str
turn_id: str
channel: str
chat_id: int | None
user_id: int | None
available_tools: list[str]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class minibot.core.events.TurnInputPreparedEvent(*, event_id=<factory>, event_type='turn_input_prepared', turn_id, channel, chat_id=None, text='', available_tools=<factory>)[source]

Emitted once the turn’s opening message is ready for the model.

text is the message text followed by the automatic transcriptions of its own audio. Messages folded into the running turn and audio transcribed later by a tool are not included. available_tools names the tools the main agent can use for this turn.

Parameters:
event_type: str
turn_id: str
channel: str
chat_id: int | None
text: str
available_tools: list[str]
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class minibot.core.events.TurnCompletedEvent(*, event_id=<factory>, event_type='turn_completed', turn_id, channel, chat_id=None, should_reply=True, llm_provider=None, llm_model=None, token_trace=<factory>, compaction_performed=None, tools_used=<factory>, task_handoff=False)[source]

Emitted once a turn is fully done.

Fires after the response has been dispatched to the channel, or deliberately suppressed when should_reply is false — never before.

Parameters:
  • event_id (str)

  • event_type (str)

  • turn_id (str)

  • channel (str)

  • chat_id (int | None)

  • should_reply (bool)

  • llm_provider (str | None)

  • llm_model (str | None)

  • token_trace (dict[str, Any])

  • compaction_performed (bool | None)

  • tools_used (list[str])

  • task_handoff (bool)

event_type: str
turn_id: str
channel: str
chat_id: int | None
should_reply: bool
llm_provider: str | None
llm_model: str | None
token_trace: dict[str, Any]
compaction_performed: bool | None
tools_used: list[str]
task_handoff: bool
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class minibot.core.events.TurnFailedEvent(*, event_id=<factory>, event_type='turn_failed', turn_id, channel, chat_id=None, error)[source]

Emitted when a turn raised before producing a response.

Parameters:
  • event_id (str)

  • event_type (str)

  • turn_id (str)

  • channel (str)

  • chat_id (int | None)

  • error (str)

event_type: str
turn_id: str
channel: str
chat_id: int | None
error: str
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class minibot.core.events.TurnStopRequestedEvent(*, event_id=<factory>, event_type='turn_stop_requested', channel, chat_id, user_id=None)[source]

Emitted when the owner asks to stop the turn running in a chat.

Parameters:
  • event_id (str)

  • event_type (str)

  • channel (str)

  • chat_id (int)

  • user_id (int | None)

event_type: str
channel: str
chat_id: int
user_id: int | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class minibot.core.events.ReasoningEvent(*, event_id=<factory>, event_type='reasoning', text, step, turn_id=None, owner_id=None, channel=None, chat_id=None)[source]

Emitted as soon as one provider step returns reasoning, before the turn finishes.

Reasoning is also attached to the final response metadata; this event exists so a channel can show thinking while the turn is still running instead of only after it completes.

Parameters:
  • event_id (str)

  • event_type (str)

  • text (str)

  • step (int)

  • turn_id (str | None)

  • owner_id (str | None)

  • channel (str | None)

  • chat_id (int | None)

event_type: str
text: str
step: int
turn_id: str | None
owner_id: str | None
channel: str | None
chat_id: int | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class minibot.core.events.ToolCallEvent(*, event_id=<factory>, event_type='tool_call', phase, call_id=<factory>, tool_name, turn_id=None, owner_id=None, channel=None, chat_id=None, detail='', error=None, parent_call_id=None)[source]

Emitted around every tool handler invocation.

detail is already redacted and clipped by minibot/llm/tools/tool_call_display.py before this event is published — the raw argument payload (which can hold credentials, e.g. http_request headers, bash env, python_execute code) never leaves the tool-execution layer. Safe to log, persist, or forward to any subscriber, including extensions.

parent_call_id is the call_id of the tool call that made this one, for tools that call other tools (run_tools_script); None for calls the model made directly.

Parameters:
  • event_id (str)

  • event_type (str)

  • phase (Literal['started', 'completed', 'failed'])

  • call_id (str)

  • tool_name (str)

  • turn_id (str | None)

  • owner_id (str | None)

  • channel (str | None)

  • chat_id (int | None)

  • detail (str)

  • error (str | None)

  • parent_call_id (str | None)

event_type: str
phase: Literal['started', 'completed', 'failed']
call_id: str
tool_name: str
turn_id: str | None
owner_id: str | None
channel: str | None
chat_id: int | None
detail: str
error: str | None
parent_call_id: str | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class minibot.core.events.ToolApprovalRequestedEvent(*, event_id=<factory>, event_type='tool_approval_requested', approval_id, tool_name, channel, chat_id, detail='', requester_user_id=None)[source]

A dangerous tool call waits for the user. detail is redacted like ToolCallEvent’s.

Parameters:
  • event_id (str)

  • event_type (str)

  • approval_id (str)

  • tool_name (str)

  • channel (str)

  • chat_id (int)

  • detail (str)

  • requester_user_id (int | None)

event_type: str
approval_id: str
tool_name: str
channel: str
chat_id: int
detail: str
requester_user_id: int | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

class minibot.core.events.ToolApprovalResolvedEvent(*, event_id=<factory>, event_type='tool_approval_resolved', approval_id, approved, user_id=None)[source]
Parameters:
  • event_id (str)

  • event_type (str)

  • approval_id (str)

  • approved (bool)

  • user_id (int | None)

event_type: str
approval_id: str
approved: bool
user_id: int | None
model_config = {}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].