TypedEventBus for cross-module communication. Instead of packages importing each other directly, they emit and subscribe to typed events. The bus provides compile-time safety — TypeScript enforces that you emit events with the correct payload shape and subscribe to events that actually exist.
How It Works
TheTypedEventBus wraps Node.js EventEmitter with a type-safe generic interface. All typed events are defined in the EventMap interface, which is composed from 5 domain-specific sub-interfaces (MessagingEvents, AgentEvents, ChannelEvents, InfraEvents, TerminalEvents).
API methods include emit(), on(), off(), once(), removeAllListeners(), listenerCount(), and setMaxListeners(). All methods are constrained by EventMap — you cannot emit an event name that does not exist in the map, and every payload is type-checked at compile time.
Event Naming Convention
Events follow asubsystem:action naming pattern (for example, message:received, tool:executed, graph:started). Events are organized into 5 subsystems:
Most Commonly Used Events
A quick reference for the events developers are most likely to need when building plugins or adapters:Complete Event Reference
MessagingEvents (38 events)
MessagingEvents (38 events)
Message lifecycle, session management, subagent lifecycle, compaction, context engine lifecycle, context engine metrics, response filtering, execution control, and dead-letter queue events.
The
context:* events provide per-turn observability into context engine decisions:context:evictedfires when context history is dropped to fit the token budget — in pipeline mode when the dead content evictor removes superseded tool results, and in DAG mode when the LCD assembler evicts older history under budget (categories.lcd_historycarries the dropped count). Counts and ids only; never message content.context:rereadfires when the re-read detector identifies duplicate tool calls in the session (pipeline mode).context:pipelinefires after every pipeline run with a full metrics snapshot (pipeline mode). This is always emitted, even when no optimization was needed.context:pipeline:cachefires post-LLM to patch cache-specific metrics (cache hit/write/miss tokens) once the API response is available.context:dag_compactedfires after a DAG compaction pass creates new summaries (DAG mode).context:integrityfires when the DAG integrity checker detects and repairs structural issues (DAG mode).
AgentEvents (47 events)
AgentEvents (47 events)
Skill lifecycle, tool execution, audit logging, observability (tokens and latency), cache break detection, model failover, security, execution graph, per-node sub-agent budget breaches, pipeline authoring telemetry, small-model graph authoring (repair + intent synthesis), provider health, SEP (step-execution planning), and exec command blocking events.
The
graph:driver_lifecycle event fires at each phase transition during typed node execution. The phase field indicates which stage the driver has reached:Subscribe to this event for driver-level monitoring, logging, or triggering downstream actions based on typed node progress. See the Pipelines: Driver Lifecycle documentation for the full execution flow.Per-node token budgets:
subagent:budget_exceeded fires when a graph node’s sub-agent exceeds its per-node token budget. The payload is counts and ids only — tokenBudget is the cap that was breached, tokensUsed is the node’s actual spend, and agentId is the node’s child agent (never task text or sub-agent output). The breaching node fails terminally (honoring the graph’s on_failure); see Execution Graphs: Token budgets. For completed nodes, graph:node_updated carries the optional tokensUsed and cost fields so per-node spend is observable from the bus alone; both are absent when the underlying run did not report them.Per-subagent cost rollup: the graph:completed payload carries an optional nodeTokenSpend (nodeId → tokens) and nodeCost (nodeId → corrected dollars) ledger, present only when at least one node recorded the respective metric. nodeCost accumulates each node’s graph:node_updated cost delta — the same corrected dollars that feed the graph-wide total — so a per-subagent corrected-$ rollup (a node plus all its descendants) is computable from the bus alone. Both maps are counts/ids only (never task text or sub-agent output).Pipeline authoring telemetry: pipeline:authored fires once per pipeline authoring invocation — graph.define and graph.execute — carrying the action (define or execute), the calling model’s server-resolved capabilityClass tier (frontier / mid / small / nano, or unknown when the tier cannot be resolved), whether the call parsed and validated against the graph schema (schemaValid, the real verdict — false is still emitted, and the user-facing validation error still throws), and repaired. A present-but-malformed authoring call that fails schema either at the request-contract parse or at the graph parse/validate step counts as schemaValid: false; only the bespoke pre-checks (a define call with no nodes, or an execute call rejected by the agent-to-agent policy gate) emit nothing — an empty/garbage call or a policy rejection is not an authoring attempt. The emit is best-effort: a telemetry failure (e.g. an observability-buffer write error) is logged and swallowed, never surfaced as the graph.define/graph.execute result. The payload is counts, ids, and enum labels only — never a pipeline body, a node type_config value, a task or label string, or any secret. The tier is resolved daemon-side from the authoring agent, never tool-supplied. repaired is false unless the small-model graph repair producer (orchestration.authoring.repairProducer, default true) repaired the call — opted out (false), it stays false. The fleet aggregate of this event — the small-model pipeline-authoring failure rate and the pre-committed build/defer gate — is surfaced by obs.fleet.health.Small-model graph authoring: graph:repaired and graph:synthesized_from_intent are the audit events for the small-model DAG-authoring layer (on by default — see orchestration.authoring). Both are counts/ids/enums-only — the payload carries the matched/requested canonical pattern (research-fanout / debate / vote / map-reduce, a closed enum), the resulting nodeCount, and correlation ids; never a graph body, a node type_config value, a task or label string, or the one-line intent text. Both emits are best-effort (a telemetry failure is logged and swallowed, never surfaced as the operation result), and both fire after the synthesized/repaired graph passes the same validation a hand-authored graph runs — so the event always reflects a governed graph.graph:repairedfires when the daemon repairs a weak-tier model’s schema-invalid graph to a canonical template. It additionally carries the callingcapabilityClasstier (frontier/mid/small/nano, orunknown), resolved daemon-side. A conservative match is required: an ambiguous shape returns a structured did-you-mean instead, and no event is emitted.graph:synthesized_from_intentfires when thepipelinetool’sfrom_intentaction synthesizes a graph from a one-line intent. It omits the tier (the synthesizer is tier-agnostic). The one-line intent text is the highest-risk leak and is intentionally absent from the payload.
subagent:killed fires at the runner’s kill chokepoint for every force-killed sub-agent run, naming who initiated it via the closed killedBy union (parent | health_monitor | operator | system). A daemon health-monitor stuck-kill carries the idleMs/thresholdMs telemetry; the free-text kill reason never rides the bus (it stays on the run’s failure record and the WARN log). sessionKey is the killed child’s key, so the trajectory record lands in the child’s own file and comis explain on the child yields the subagent_stuck_killed verdict.Self-healing completion delivery: subagent:delivery_retried and subagent:delivery_deadlettered track the retry/dead-letter routing when a sub-agent completion delivery falls back to a direct channel send and that send fails. Like subagent:budget_exceeded, both payloads are counts and ids only — never the announcement text, the sub-agent output, or the error string.subagent:delivery_retriedfires once per retry of a transient delivery failure.runIdis the sub-agent run,channelTypeis the target channel,attemptis the 1-based retry number, andtransientis alwaystrue(only transient failures are retried).subagent:delivery_deadletteredfires when a delivery is finally dead-lettered — either after exhaustingsecurity.agentToAgent.delivery.maxRetriesretries for a transient failure, or immediately for a permanent one.attemptis the number of retries that were exhausted (0for an immediate permanent dead-letter), andtransientrecords whether the dead-letter followed an exhausted transient retry (true) or a permanent failure (false).
subagent:steered fires when subagent.steer injects a steering message into a running child — i.e. only when security.agentToAgent.steerInject is true (default false); the flag-off kill+respawn path does not emit it. Like the other subagent:* events, the payload is counts/ids/mode only — runId is the steered run (the work is preserved; the same run continues), agentId is the owning agent, and mode is the closed union "steer" | "followup" recording which path landed the inject (steer when the child was streaming, followup when it was idle). The steering message body is never included — no message, text, or task string is ever on the bus.See Resilience: Self-healing delivery retries for the transient/permanent classification and the backoff schedule.Sandbox no-downgrade refusal: security:sandbox_downgrade_refused fires when the fail-closed no-downgrade invariant refuses a sub-agent spawn because the child’s resolved sandbox posture would be less confined than its spawner’s. parentAgentId/childAgentId are agent ids; violatedDimensions lists the offending dimensions (exec / filesystem / network / uid); parentPosture and childPosture carry each dimension as a closed enum label (e.g. exec always/never). The payload is enum labels and ids only — never filesystem paths, network hosts, uid numbers, or any credential value, so it does not leak the operator’s sandbox topology. Gated by security.agentToAgent.sandboxNoDowngrade (default on).Cache cost fields: The observability:token_usage event includes prompt caching data when the provider supports it. cost.cacheRead and cost.cacheWrite are dollar costs for cached token reads/writes. savedVsUncached is the net savings compared to uncached pricing (positive = money saved). cacheEligible indicates whether the model supports prompt caching. sessionKey identifies the conversation session for per-session cost aggregation.ChannelEvents (44 events)
ChannelEvents (44 events)
Channel registration, sender blocking, command queuing, streaming delivery, typing indicators, auto-reply, send policies, debounce, group history, follow-up chains, priority lanes, elevated routing, retry engine, ack reactions, inbound reaction capture, steer lifecycle, block coalescing, unified delivery pipeline, delivery queue, channel health monitoring, delivery hooks, and sub-agent proxy typing events.
channel:reaction_received fires when someone adds a reaction to one of the agent’s outbound messages on Discord, Slack, or Telegram (emitted by the channel-manager via the optional adapter.onReaction fanout). The payload is content-free — ids, the emoji, and a timestamp only, never a message body or a sender display name. The daemon consumes it as a corroborating Verified Learning signal: it resolves the messageId to the originating trajectory (and ignores the event if it does not map to an agent-authored outbound message) and observes a reaction-source outcome. It is distinct from the outbound ack-reaction lifecycle (ack:reaction_sent, reaction:phase_changed, reaction:terminal, and friends), which tracks reactions the agent itself posts as progress indicators.InfraEvents (55 events)
InfraEvents (55 events)
Approval gates, config patches, plugin lifecycle, hook execution, auth token rotation, diagnostics, media file extraction, scheduler (cron, heartbeat, tasks), process metrics, observability admin, agent hot-add/remove, MCP server connection lifecycle, notifications, background task lifecycle, system lifecycle, secret management, security warnings, and lifecycle reaction events.
TerminalEvents (4 events)
TerminalEvents (4 events)
Terminal session lifecycle, spawn failures, keystroke forwarding, and session pool eviction events.
Source:
packages/core/src/event-bus/events-terminal.tsSubscribing to Events
Plugins subscribe to events via lifecycle hooks, not directly on the event bus. The hook system provides priority ordering and result merging.Plugins subscribe to events via lifecycle hooks (see Plugins), not directly on the event bus. The hook system provides priority ordering and result merging. Direct
eventBus.on() is available for internal subsystem wiring in the composition root.Related
Architecture
TypedEventBus in the hexagonal architecture
Plugins
Hook into events via the plugin system
Custom Adapters
Adapters emit channel events
Packages
Which package owns which events
