Event bus topics
The topics protoAgent's core publishes, and the rules for subscribing to them. A plugin reacts to these with registry.on or sdk.react_on, and a console view through the view bridge.
The bus is the only inter-plugin channel — plugins publish topics as their public API and never import one another (ADR 0039). It is also ephemeral: a small ring buffer lets a reconnecting subscriber catch up via since, but there is no durable log. Persist what you need to keep.
Matching
Topics are dot-namespaced. A subscription pattern may use two wildcards:
| Pattern | Matches | Doesn't match |
|---|---|---|
# | everything | — |
watch.* | watch.met | watch.a.b |
watch.# | watch, watch.met, watch.a.b | goal.changed |
watch.met | exactly that | anything else |
* matches one segment; # matches the tail. (Verified against topic_matches: watch.* vs watch.a.b → False, watch.# vs watch.a.b → True.)
Core topics
Generated by scanning every publish/emit call in the core packages, so this is what the runtime actually emits — not a list someone remembered to update. Payload keys are the union of what the emitting call sites pass; a key may be absent on a given event.
| Topic | Payload keys | Emitted from |
|---|---|---|
activity.message | context_id, error, origin, priority, role, state, stimulus, task_id, text, trigger | server/a2a.py |
background.completed | description, error, job_id, origin_session, result, status, subagent_type | background/manager.py, server/a2a.py |
background.progress | error, job_id, output, phase, task_id, tool, tool_call_id | background/manager.py, server/a2a.py |
background.started | description, job_id, origin_session, status, subagent_type | background/manager.py |
chat.progress | session_id, task_id | server/a2a.py |
chat.resumed | error, origin, session_id, state, task_id, text, trigger | server/a2a.py |
fs.changed | paths, project, source | graph/fs_changes.py |
goal.achieved / goal.failed | condition, evidence, reason, session_id, status | graph/goals/controller.py |
goal.changed | session_id | graph/goals/store.py |
goal.iteration | condition, iteration, max_iterations, reason, session_id | graph/goals/controller.py |
inbox.item | id, priority, source, text | operator_api/console_handlers.py |
media.saved | id, mime, plugin, url | graph/plugins/registry.py |
memory.hot_written | chunk_id, preview, source, source_type | knowledge/store.py |
model.fallback | fallback_index, fallback_model, primary_error | graph/agent.py |
persona.drift_detected | baseline_id, baseline_saved_at, rationale, score, signals, threshold | server/maintenance_loops.py |
persona.untooled_action_detected | count, findings, soul_revision, trigger | server/maintenance_loops.py |
plugin.changed | scope | server/settings_apply.py |
plugin.updated | by, id, reloaded, resolved_sha, version | server/maintenance_loops.py |
scheduler.completed | (varies) | server/a2a.py |
scheduler.fired | job_id, prompt, schedule | scheduler/local.py |
turn.finished | ok, origin, session_id, task_id, trigger | background/manager.py, scheduler/local.py |
turn.input_required | context_id, prompt, task_id | server/a2a.py |
turn.resumed | context_id, task_id | server/a2a.py |
turn.started | origin, session_id, trigger | background/manager.py, scheduler/local.py |
turn.usage | context_id, cost_usd, duration_ms, input_tokens, model, output_tokens, soul_rev, state, task_id | server/turn_telemetry.py |
ui.navigate | plugin, view | graph/plugins/registry.py |
watch.changed | id | graph/watches/store.py |
watch.expired | (varies) | graph/watches/controller.py |
watch.met | (varies) | graph/watches/controller.py |
watch.stalled | (varies) | graph/watches/controller.py |
watch.value_changed | (varies) | graph/watches/controller.py |
Publishing your own
registry.emit("created", {...}) publishes <your-plugin-id>.created — the namespace is forced, so a plugin can only publish under its own id. Declare your topics in the manifest (emits) so other plugins can discover the contract instead of reverse-engineering it, optionally with a payload schema (emits_schemas). Declarations are for discovery only — payloads are never validated at publish time.
Publishing is safe from any thread: an off-loop publish reroutes itself onto the bound event loop, so emitting from a sync middleware hook works. One exception — an off-loop publish before any loop is bound is dropped with a warning.
Related: Plugins ▸ Events · Plugin SDK · Lifecycle events