PluginPort interface and uses the PluginRegistryApi to register its capabilities. The hook system runs at every lifecycle point — 5 modifying hooks (can change behavior) and 8 void hooks (observe only).
PluginPort Interface
A plugin is any object that satisfies thePluginPort interface. The registry calls register() once during bootstrap, passing a PluginRegistryApi facade scoped to the plugin’s ID.
id— Unique identifier for the plugin (e.g.,"audit-logger","webhook-forwarder")name— Human-readable name displayed in diagnosticsregister()— Called once during plugin loading. Use theregistryparameter to register hooks, tools, routes, and config schemas. Must be synchronous.activate()— Optional async initialization (connect to external services, warm caches)deactivate()— Optional async cleanup (close connections, flush buffers)
Registration Methods
ThePluginRegistryApi exposes four methods for extending Comis. Each method is called within register() and takes effect immediately.
registerHook
hookName must be a valid hook name from the HookName type. Options include priority (number, default 0, higher runs first, range -100 to 100).
registerTool
execute function. Plugin-provided tools appear alongside built-in tools and skill-provided tools — agents see them identically.
registerHttpRoute
GET, POST, PUT, DELETE, PATCH), path (must start with /), and an async handler function.
registerConfigSchema
Lifecycle Hooks
The hook system is the primary extension mechanism for customizing Comis behavior. Every hook is organized into one of six domains: Agent, Tool, Compaction, Delivery, Session, and Gateway.| Hook | Type | When It Runs | Can Modify |
|---|---|---|---|
before_agent_start | Modifying | Before agent processes a message | systemPrompt, prependContext |
before_tool_call | Modifying | Before a tool executes | params, block (boolean), blockReason |
tool_result_persist | Modifying (SYNC) | When tool result is saved to session transcript | result |
before_compaction | Modifying | Before session compaction runs | cancel (boolean), cancelReason |
before_delivery | Modifying | Before a message is delivered to a channel | text, cancel (boolean), cancelReason, metadata |
agent_end | Void | After agent finishes processing | — |
after_tool_call | Void | After a tool completes | — |
after_compaction | Void | After session compaction completes | — |
after_delivery | Void | After a message is delivered to a channel | — |
session_start | Void | When a new session begins | — |
session_end | Void | When a session ends | — |
gateway_start | Void | When the HTTP gateway starts | — |
gateway_stop | Void | When the HTTP gateway stops | — |
Modifying Hooks
Modifying hooks run sequentially in priority order (higher priority first). Each handler can return an object with fields to modify. Results are merged using last-writer-wins for each field — if two hooks both setsystemPrompt, the higher-priority hook’s value is used.
Return values are validated against Zod schemas before merging. Invalid returns are logged and ignored, protecting the system from malformed hook output.
Void Hooks
Void hooks run in parallel (fire-and-forget). Handlers observe events but cannot change them. Errors in void hooks are caught and logged without affecting the system.Priority Ordering
Higher priority numbers run first (range-100 to 100, default 0). For example, a hook with priority 10 runs before a hook with priority 0. This applies to modifying hooks where execution order determines which values take precedence.
Hook Details
before_agent_start
before_agent_start
Fires before an agent processes a message. Use this to inject context, modify the system prompt, or add preamble text.Event payload:
systemPrompt(string) — The current system promptmessages(unknown[]) — Message history
agentId(string) — The agent processing the messagesessionKey(SessionKey) — Current session identifierworkspaceDir(string) — Workspace directory pathisFirstMessageInSession(boolean) — Whether this is the first user message
systemPrompt(string) — Override the system promptprependContext(string) — Text prepended to the agent’s context
before_tool_call
before_tool_call
Fires before a tool executes. Use this to modify parameters, block dangerous tools, or enforce security policies.Event payload:
toolName(string) — Name of the tool being calledparams(Record<string, unknown>) — Tool parameters
agentId(string) — The agent calling the toolsessionKey(SessionKey) — Current session identifier
params(Record<string, unknown>) — Modified parametersblock(boolean) — Set totrueto prevent the tool from executingblockReason(string) — Reason for blocking (logged and returned to agent)
tool_result_persist
tool_result_persist
Fires when a tool result is saved to the session transcript. Use this to redact sensitive data or transform tool output before it is persisted.Event payload:
toolName(string) — Name of the tool that executedresult(string) — The tool’s result text
agentId(string) — The agent that called the toolsessionKey(SessionKey) — Current session identifier
result(string) — Modified result text to persist
before_compaction
before_compaction
Fires before the context engine’s LLM compaction layer runs (the last-resort summarization step). Use this to cancel compaction for specific sessions or log compaction decisions. See Compaction for how compaction fits into the 10-layer context engine.Event payload:
sessionKey(SessionKey) — Session being compactedmessageCount(number) — Current message countestimatedTokens(number) — Estimated token count (optional)
agentId(string) — The agent whose session is being compacted
cancel(boolean) — Set totrueto prevent compactioncancelReason(string) — Reason for cancellation
agent_end
agent_end
Fires after an agent finishes processing a message. Use this for logging, metrics collection, or post-processing.Event payload:
durationMs(number) — Total processing timetokenUsage(object) — Token counts:prompt,completion,total(optional)success(boolean) — Whether processing succeedederror(string) — Error message if failed (optional)
agentId(string) — The agent that processed the messagesessionKey(SessionKey) — Current session identifier
after_tool_call
after_tool_call
Fires after a tool completes execution. Use this for audit logging, metrics, or triggering side effects.Event payload:
toolName(string) — Name of the tool that executedparams(Record<string, unknown>) — Parameters that were passedresult(unknown) — The tool’s return valuedurationMs(number) — Execution timesuccess(boolean) — Whether execution succeeded
agentId(string) — The agent that called the toolsessionKey(SessionKey) — Current session identifier
after_compaction
after_compaction
Fires after the context engine’s LLM compaction layer completes. Use this for logging compaction results or triggering downstream actions like notifying monitoring systems.Event payload:
sessionKey(SessionKey) — Session that was compactedremovedCount(number) — Messages removedretainedCount(number) — Messages retaineddurationMs(number) — Compaction duration
agentId(string) — The agent whose session was compacted
before_delivery
before_delivery
Fires before a message is delivered to a channel. Use this to modify the outgoing text, cancel delivery, or attach metadata for downstream processing. See Delivery for how delivery hooks fit into the message pipeline.Event payload:
text(string) — The message text about to be deliveredchannelType(string) — Target channel type (e.g., “telegram”, “discord”)channelId(string) — Target channel identifieroptions(Record<string, unknown>) — Platform-specific delivery optionsorigin(string) — Where the message originated (e.g., “agent”, “system”)
sessionKey(string) — Current session identifier (optional)agentId(string) — The agent sending the message (optional)traceId(string) — Request trace identifier (optional)
text(string) — Modified message textcancel(boolean) — Set totrueto prevent deliverycancelReason(string) — Reason for cancellation (logged)metadata(Record<string, unknown>) — Arbitrary metadata passed to the delivery layer
after_delivery
after_delivery
Fires after a message has been delivered to a channel. Use this for delivery tracking, analytics, or triggering follow-up actions.Event payload:
text(string) — The message text that was deliveredchannelType(string) — Channel type the message was delivered tochannelId(string) — Channel identifierresult(unknown) — Platform-specific delivery resultdurationMs(number) — Delivery duration in millisecondsorigin(string) — Where the message originated
sessionKey(string) — Current session identifier (optional)agentId(string) — The agent that sent the message (optional)traceId(string) — Request trace identifier (optional)
session_start
session_start
Fires when a new session begins. Use this for session tracking, initializing per-session state, or logging.Event payload:
sessionKey(SessionKey) — The new session identifierisNew(boolean) — Whether this is a brand-new session or a resumed one
agentId(string) — The agent for this session (optional)
session_end
session_end
Fires when a session ends. Use this for cleanup, final logging, or triggering follow-up actions.Event payload:
sessionKey(SessionKey) — The session that endedreason(string) — Why the session endeddurationMs(number) — Total session duration (optional)
agentId(string) — The agent for this session (optional)
gateway_start
gateway_start
Fires when the HTTP gateway starts. Use this for initialization that depends on the gateway being ready.Event payload:
port(number) — The port the gateway is listening onhost(string) — The host addresstls(boolean) — Whether TLS is enabled
gateway_stop
gateway_stop
Fires when the HTTP gateway stops. Use this for cleanup or graceful shutdown coordination.Event payload:
reason(string) — Why the gateway is stopping
Complete Plugin Example
This audit logger plugin demonstrates multiple registration types: lifecycle hooks (both observing and modifying), a custom HTTP route, and priority ordering. This is a non-channel plugin — for channel adapter plugins, see Custom Adapters.PluginRegistry Lifecycle
ThePluginRegistry manages plugin state through the application lifecycle:
register(plugin)— Adds the plugin to the registry and callsplugin.register(api)with a scoped facade. Returnserr()if the plugin ID is empty or already registered.activateAll()— Callsactivate()on all plugins that define it (after all plugins are registered). Collects and reports activation errors.deactivateAll()— Callsdeactivate()on all plugins during graceful shutdown. Runs in registration order.getPlugin(id)/getPlugins()— Query registered plugins by ID or get all plugins.unregister(id)— Remove a plugin and all its registered hooks.
createPluginRegistry() and wired in the composition root (bootstrap.ts). Plugins are registered during the bootstrap phase before the daemon starts accepting connections.
Custom Adapters
Channel adapter plugins (ChannelPluginPort)
Event Bus
Events that trigger hooks
Custom Skills
registerTool for agent tools
Architecture
PluginPort in the port system
