Plugin Session Events
Observe Cola agent execution from a plugin through the typed runtime.events surface.
runtime.events is the typed event surface a plugin uses to observe what is happening inside the Cola agent. By default it covers only the plugin's own sessions; opting into scope: 'all' extends it to every session in Cola — desktop, CLI, and other plugins — for cross-channel automation and global observability. The surface is intentionally read-only: events are notifications, handlers return void, and nothing a handler returns or throws influences agent behavior.
For the broader plugin contract, see Plugin SDK.
Subscribe
Register listeners during Plugin.start (or gateway.start for channel plugins) and tear them down by calling the returned unsubscribe function:
import { definePlugin } from '@marswave/cola-plugin-sdk'
export default definePlugin({
id: 'example',
meta: { label: 'Example', description: 'Example plugin' },
async start({ runtime }) {
const offTurnEnd = runtime.events.on('turn:end', (event) => {
runtime.logger.info(`turn ${event.turnIndex} ended (origin: ${event.origin.kind})`)
})
const offToolCall = runtime.events.on('tool:call', (event) => {
runtime.logger.info(`call ${event.toolName} #${event.toolCallId}`)
})
return async () => {
offTurnEnd()
offToolCall()
}
}
})Subscription scope
runtime.events.on(type, handler, options?) accepts an optional options.scope:
'self'(default) — only sessions underplugin:<thisPluginId>:*reach the handler.'all'— every session, including desktop, CLI, and other plugins. Use this for cross-channel automation and global observability.
Every event payload carries an origin field:
type PluginSessionEventOrigin =
| { kind: 'plugin'; pluginId: string }
| { kind: 'desktop' }
| { kind: 'cli'; user?: string }
| { kind: 'other'; scopeKey: string }origin is informational for 'self' subscribers and load-bearing for 'all' subscribers.
Filtering rules
The host filters events before dispatch:
- With
scope: 'self', a plugin only receives events for scopes underplugin:<pluginId>:. Desktop sessions and other plugins' sessions are filtered out. - With
scope: 'all', every session is forwarded. For non-plugin scopes thesessionIdfield is an opaque single-element tuple — treat it as a routing handle, not as parseable data. - For the plugin's own sessions, the
sessionIdfield is the tuple your plugin chose when delivering the inbound message, not the internalscopeKey.
Reliability
- Handlers are scheduled asynchronously, so a slow listener cannot hold an agent turn hostage.
- Each handler call is bounded by a 2 s timeout. Handlers that overrun or throw are logged through the plugin logger and ignored by the agent path.
- Events are best-effort notifications. Do not treat them as a transactional log.
Event Catalogue
All events carry sessionId: SessionId and origin: PluginSessionEventOrigin. Per-variant fields are listed below.
Session lifecycle
| Event | Extra fields | Notes |
|---|---|---|
session:start | reason: 'startup' | 'reload' | 'new' | 'resume' | 'fork', previousSessionFile?: string | Fired when the Pi session is constructed or reloaded for this scope. |
session:shutdown | reason: 'quit' | 'reload' | 'new' | 'resume' | 'fork' | Fired when the session is torn down. Pair with session:start to track scope tenancy. |
Compaction
Compaction events carry the promptId they belong to so handlers can correlate them to the agent turn that triggered compaction.
| Event | Extra fields |
|---|---|
compact:start | promptId?: string |
compact:end | promptId?: string, aborted?: boolean, errorMessage?: string, willRetry?: boolean |
Agent loop
| Event | Extra fields | Notes |
|---|---|---|
agent:start | — | The agent loop has begun servicing a prompt. |
agent:end | — | The agent loop has finished. The host stripts the full assistant messages array from this event so plugins do not re-receive the LLM transcript. |
Turn
| Event | Extra fields |
|---|---|
turn:start | turnIndex: number, timestamp: number |
turn:end | turnIndex: number |
Assistant message stream
Only assistant-role messages are reported here. User prompts come in through your plugin itself, system prompts are host-internal, and tool-result messages travel through tool:result.
| Event | Extra fields | Notes |
|---|---|---|
message:start | — | A new assistant message has started streaming. |
message:update | deltaText: string | Emitted once per text delta with the new chunk. Non-text deltas (thinking, tool-use) are not forwarded. |
message:end | text: string, stopReason?: 'stop' | 'length' | 'toolUse' | 'error' | 'aborted' | text is the concatenated text-content of the assistant message; thinking and tool-call segments are excluded. |
Tool boundary
| Event | Extra fields | When |
|---|---|---|
tool:call | toolName: string, toolCallId: string, input: Record<string, unknown> | The agent is about to invoke a tool. toolName is the plugin-facing original name — the host-side plugin__<pluginId>__ prefix is stripped before dispatch. |
tool:result | toolName: string, toolCallId: string, input, content: PluginToolContent[], details: unknown, isError | The tool result has been recorded against the call. |
PluginToolContent is { type: 'text'; text } | { type: 'image'; source: { type: 'base64'; mediaType; data } }. Other content variants are normalized into one of these two shapes.
Tool execution (runtime)
Distinct from tool:call / tool:result: these fire around the actual runtime execution of the tool handler, not the extension-API boundary. Useful when you want to observe live progress of a long-running tool.
| Event | Extra fields |
|---|---|
tool:execution_start | toolName, toolCallId, input: Record<string, unknown> |
tool:execution_update | toolName, toolCallId, partial: unknown |
tool:execution_end | toolName, toolCallId, isError: boolean |
toolName here is also the unprefixed original name.
What is Not Forwarded
Treat the following as host-internal and never relayed to plugins:
- The full system prompt.
- The full assistant
messages: AgentMessage[]array from agent end. - User and system messages on
message:start/message:end— onlyassistantmessages are reported. - Non-text assistant deltas (thinking deltas, tool-use deltas).
- Host-internal types and tool-content variants that have no SDK equivalent.
If you find yourself needing one of these, it is a signal that the work belongs inside Cola, not inside the plugin.