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 under plugin:<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 under plugin:<pluginId>:. Desktop sessions and other plugins' sessions are filtered out.
  • With scope: 'all', every session is forwarded. For non-plugin scopes the sessionId field is an opaque single-element tuple — treat it as a routing handle, not as parseable data.
  • For the plugin's own sessions, the sessionId field is the tuple your plugin chose when delivering the inbound message, not the internal scopeKey.

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

EventExtra fieldsNotes
session:startreason: 'startup' | 'reload' | 'new' | 'resume' | 'fork', previousSessionFile?: stringFired when the Pi session is constructed or reloaded for this scope.
session:shutdownreason: '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.

EventExtra fields
compact:startpromptId?: string
compact:endpromptId?: string, aborted?: boolean, errorMessage?: string, willRetry?: boolean

Agent loop

EventExtra fieldsNotes
agent:startThe agent loop has begun servicing a prompt.
agent:endThe 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

EventExtra fields
turn:startturnIndex: number, timestamp: number
turn:endturnIndex: 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.

EventExtra fieldsNotes
message:startA new assistant message has started streaming.
message:updatedeltaText: stringEmitted once per text delta with the new chunk. Non-text deltas (thinking, tool-use) are not forwarded.
message:endtext: 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

EventExtra fieldsWhen
tool:calltoolName: 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:resulttoolName: string, toolCallId: string, input, content: PluginToolContent[], details: unknown, isErrorThe 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.

EventExtra fields
tool:execution_starttoolName, toolCallId, input: Record<string, unknown>
tool:execution_updatetoolName, toolCallId, partial: unknown
tool:execution_endtoolName, 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 — only assistant messages 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.

On this page