Skip to main content
PathDocs

Listen to Events

In one sentence: ctx.on(event, handler) lets a plugin hook into the session/tool/agent lifecycle: the most common ones are the tool pipeline (tools/pre-execute gate, tools/post-execute rewrite). This is the entry point for the vast majority of "behavior extensions".

1. Listening basics

import { Context } from '@deepseek-ai/cordis'

export const name = 'event-watcher'

export function apply(ctx: Context) {
// session lifecycle
ctx.on('session/created', (session) => {
console.log('New session:', session.id)
})

// tool gate (can veto): signature (execution, next), returns a PreToolDecision
ctx.on('tools/pre-execute', async (execution, next) => {
if (execution.name === 'forbidden_tool') {
return { kind: 'deny', reason: 'This tool is forbidden by policy' }
}
return next() // allow through to subsequent listeners
})

// tool result rewrite
ctx.on('tools/post-execute', async (execution, result, next) => {
const decision = await next()
// inspect the result, attach extra information, or change the decision
return decision
})
}

2. Event hook patterns

PatternPurposeExample
Listen/notifyobserve onlyctx.on('agent/error', ...) logging
Gateintercept/allowtools/pre-execute returns deny
Aroundwrap a layertools/execute adds timeout/metrics
Rewritechange the resulttools/post-execute appends context

3. Five dispatch modes

ModeSemanticsTypical
emitsync trigger, no waitagent/error
parallelasync in parallel, await allsession/flush
serialserial in sequencesome agent/*
bailstops at the first bail
waterfallchained into a next() chaintools/pre-execute, agent/request

waterfall must call next(): skipping it short-circuits the whole chain. On the publish side use ctx.emit()/ctx.parallel()/ctx.waterfall().

4. The observable event catalog (excerpt)

DomainEvents
agent lifecycleagent/created /disposed /status /session-start; waterfall's /pre-step /request /request-error; /turn-stopping, agent/inbox/*
sessionsession/created /disposed /event /flush
tool pipelinetools/pre-execute execute post-execute (waterfall), tools/code-dispatch-log, tools/result (emit)
cross-domainfs/edit-intent fs/write-intent, llm/stream, approval/request, goal/changed, settings/document-updated, credentials/updated, commands/change

The full matrix is in $SRC/docs/event-producer-consumer.md (including each event's dispatch mode).

5. Dual event planes (don't mix them up)

Plane A: Cordis in-processPlane B: SessionEvent persisted log
When to usetransient signals, plugin communicationfacts that need to be persisted/recoverable
Publishctx.emit('my/event', ...)session.append('my/thing-happened', {...})
Subscribectx.on('my/event', handler)only observable via session/event

tools/result (host in-process notification) ≠ tool/result (session log event).

Custom persisted events must first be declared (augment into SessionEventMap), otherwise session.append rejects unknown types:

declare module '@deepseek-ai/dsh-session/types' {
interface SessionEventMap {
'my-plugin/thing-happened': { type: 'my-plugin/thing-happened'; detail: number }
}
}

Downstream listening goes through session/event, and you cannot directly ctx.on('my-plugin/thing-happened'):

ctx.on('session/event', (session, event) => {
if (event.type === 'my-plugin/thing-happened') {
console.log('thing:', event.detail)
}
})

6. In practice: a "policy gate" plugin

Scenario: an environment forbids the model from calling web_fetch, but allows web_search.

export function apply(ctx: Context) {
ctx.on('tools/pre-execute', async (execution, next) => {
if (execution.name === 'web_fetch') {
return { kind: 'deny', reason: 'This environment\'s policy forbids web_fetch' }
}
return next()
})
}

The packages/guard package is not this gate: it is a loop-hygiene guard family (repeat-tool-reminder / timeout-policy), with a different responsibility. allow/deny/ask is implemented through the PreToolDecision of tools/pre-execute.

7. Verification

# All event streams are in the session JSONL (default zstd, two-level directories)
zstdcat ~/.dsh/sessions/*/*/session.jsonl.zstd | jq -r '.type' | sort | uniq -c

Next steps