// sdk/event-stream/types.ts — typed event envelope + pre-registered // event shapes, per Plan 20-06 (SDK-08). // // The event stream is the Phase 20+ observability primitive that every // downstream consumer (Plan 20-05 MCP tool handlers, Plan 20-13 hooks) // builds on. A single append-only JSONL file at // `.design/telemetry/events.jsonl` holds the persisted form; an in-process // `EventEmitter` bus (see `./emitter.ts`) broadcasts the same events // live to subscribers within the same Node process. // // Envelope invariants (also encoded in `reference/schemas/events.schema.json`): // * `type` — required, string, free-form. Pre-registered subtypes // below are merely the seeded set; unknown types are // allowed (validation is structural, not a closed enum). // * `timestamp` — required, ISO-8601 (`date-time` format). // * `sessionId` — required, stable per GDD pipeline run. // * `stage` — optional, narrow `Stage` union. // * `cycle` — optional, free-form string identifier. // * `payload` — required, opaque object bag. // * `_meta` — optional, writer-injected `{ pid, host, source }`. // * `_truncated` — optional, writer-set when a payload exceeds // `maxLineBytes` and has been replaced by a placeholder. // // Plan 20-04 owns the error taxonomy that feeds `ErrorEvent.payload`: // `{ code, message, kind }` mirrors `toToolError(err)` output. import type { Stage } from '../state/types.ts'; /** Writer-injected metadata. Never populated by callers. */ export interface EventMeta { pid: number; host: string; /** * Free-form identifier for the module that produced the event. * Defaults to `"event-stream"` when `appendEvent()` fills the field * itself; callers that wrap `appendEvent()` in a module-specific helper * should overwrite this before calling. */ source: string; } /** * Canonical event envelope. All persisted and in-process events share * this shape. Concrete subtypes narrow `type` + `payload` but add no * additional top-level fields. */ export interface BaseEvent { type: string; timestamp: string; sessionId: string; stage?: Stage; cycle?: string; payload: Record; _meta?: EventMeta; /** * Set to `true` by the writer when the serialized event exceeded * `maxLineBytes` and the payload has been replaced with a placeholder. * Never set by callers. */ _truncated?: boolean; } /** * Emitted by Plan 20-05's MCP tool handlers after a successful * `mutate()` / `transition()` call. `diff` is an opaque structural * description of the change; consumers (Phase 22 dashboard) render it. */ export type StateMutationEvent = BaseEvent & { type: 'state.mutation'; payload: { tool: string; diff: unknown }; }; /** * Emitted by Plan 20-05 wrapping `transition()`. `pass=false` means * the gate blocked the advance; `blockers` carries the same list the * transition's `TransitionGateFailed` would expose. */ export type StateTransitionEvent = BaseEvent & { type: 'state.transition'; payload: { from: Stage; to: Stage; blockers: string[]; pass: boolean }; }; /** Lifecycle hook emitted when a pipeline stage begins execution. */ export type StageEnteredEvent = BaseEvent & { type: 'stage.entered'; payload: { stage: Stage }; }; /** * Lifecycle hook emitted when a pipeline stage finishes. `duration_ms` * measures wall-clock time from `stage.entered`. `outcome` mirrors the * stage's terminal state. */ export type StageExitedEvent = BaseEvent & { type: 'stage.exited'; payload: { stage: Stage; duration_ms: number; outcome: 'pass' | 'fail' | 'halted' }; }; /** Emitted by Plan 20-13 hook consumers when a hook dispatches a decision. */ export type HookFiredEvent = BaseEvent & { type: 'hook.fired'; payload: { hook: string; decision: string }; }; /** * Emitted whenever a `GDDError` is surfaced to the user or returned from * a tool handler. `kind` mirrors `classify(err).kind`; `code` + * `message` mirror the error's `code` + `message`. */ export type ErrorEvent = BaseEvent & { type: 'error'; payload: { code: string; message: string; kind: string }; }; // --------------------------------------------------------------------------- // Phase 22 — pre-registered subtypes expansion (Plan 22-01) // --------------------------------------------------------------------------- /** Wave orchestration — Plan 21 parallel-mapper / wave execution. */ export type WaveStartedEvent = BaseEvent & { type: 'wave.started'; payload: { wave: string; plan_count: number }; }; export type WaveCompletedEvent = BaseEvent & { type: 'wave.completed'; payload: { wave: string; duration_ms: number; outcome: 'pass' | 'fail' }; }; /** STATE.md mutation lifecycle (Plan 20-03). */ export type BlockerAddedEvent = BaseEvent & { type: 'blocker.added'; payload: { id: string; summary: string; source: string }; }; export type DecisionAddedEvent = BaseEvent & { type: 'decision.added'; payload: { id: string; summary: string; source: string }; }; export type MustHaveAddedEvent = BaseEvent & { type: 'must_have.added'; payload: { id: string; summary: string; source: string }; }; /** Parallelism decision engine output — Plan 21 explore-parallel-runner. */ export type ParallelismVerdictEvent = BaseEvent & { type: 'parallelism.verdict'; payload: { task_ids: string[]; verdict: 'parallel' | 'sequential'; reason: string }; }; /** * Phase 10.1 cost-telemetry event-stream sink. * * Phase 33.6 / Plan 33.6-03 (SC#6) extension — additive/back-compat: the * payload gains an OPTIONAL `provider?: string`, set to `'openrouter'` when the * model for this cost row was resolved via the OpenRouter tier-resolver adapter * (`scripts/lib/tier-resolver-openrouter.cjs`). Absent on every pre-33.6 event * (and on native-resolution rows) — exactly the same additive discipline as the * Phase-27 `runtime_role`/`peer_id` extension documented above. The cost-row * emit site that threads it is * `scripts/lib/budget-enforcer.cjs#buildCostEventPayload`. */ export type CostUpdateEvent = BaseEvent & { type: 'cost.update'; payload: { agent: string; tier: string; usd: number; tokens_in: number; tokens_out: number; /** * Phase 33.6 SC#6. Set to `'openrouter'` when the model was resolved via the * OpenRouter adapter; omitted otherwise (native-resolution + pre-33.6 rows). */ provider?: string; }; }; /** Rate-guard / backoff stream (Plan 20-10, 20-11). */ export type RateLimitEvent = BaseEvent & { type: 'rate_limit'; payload: { provider: string; reset_at: string; remaining: number }; }; export type ApiRetryEvent = BaseEvent & { type: 'api.retry'; payload: { provider: string; attempt: number; delay_ms: number; reason: string }; }; /** Context-window churn; emitted by `hooks/context-exhaustion.ts`. */ export type CompactBoundaryEvent = BaseEvent & { type: 'compact.boundary'; payload: { tokens_before: number; tokens_after: number }; }; /** MCP liveness probe from connection-probe primitive (Plan 22-08). */ export type McpProbeEvent = BaseEvent & { type: 'mcp.probe'; payload: { name: string; status: 'ok' | 'degraded' | 'down'; latency_ms?: number }; }; /** Reflector proposal (Phase 11 post-cycle reflector → event stream). */ export type ReflectionProposedEvent = BaseEvent & { type: 'reflection.proposed'; payload: { kind: string; target_file: string; summary: string }; }; /** Connection state transitions emitted by `connection-probe` (Plan 22-08). */ export type ConnectionStatusChangeEvent = BaseEvent & { type: 'connection.status_change'; payload: { name: string; from: string; to: string }; }; /** Per-tool-call trajectory (Plan 22-03). */ export type ToolCallStartedEvent = BaseEvent & { type: 'tool_call.started'; payload: { tool: string; args_hash: string }; }; export type ToolCallCompletedEvent = BaseEvent & { type: 'tool_call.completed'; payload: { tool: string; args_hash: string; result_hash: string; latency_ms: number; status: 'ok' | 'error'; }; }; /** Agent-level lifecycle (Plan 21 pipeline-runner / subagent spawn). */ export type AgentSpawnEvent = BaseEvent & { type: 'agent.spawn'; payload: { agent: string; task_id?: string; tier?: string }; }; export type AgentOutcomeEvent = BaseEvent & { type: 'agent.outcome'; payload: { agent: string; task_id?: string; outcome: 'pass' | 'fail' | 'halted'; duration_ms: number; cost_usd?: number; }; }; // --------------------------------------------------------------------------- // Phase 27 — peer-CLI delegation lifecycle (Plan 27-08, D-09) // --------------------------------------------------------------------------- // // Additive extension. Every event keeps existing fields. Peer-call events // gain two payload tags: // // * `runtime_role: "host" | "peer"` — defaults to `"host"` if absent at // read time (so all pre-Phase-27 events continue to read as host-mode). // Only the three `peer_call_*` events below MUST carry it as `"peer"`. // * `peer_id` — the peer-CLI ID (`"gemini"`, `"codex"`, `"cursor"`, // `"copilot"`, `"qwen"`, …) — set when `runtime_role === "peer"`. // // `costs.jsonl` cost rows (`cost_recorded` / `cost.update`) gain the same // two tags so Phase 26's reflector cross-runtime arbitrage continues to // roll up correctly with mixed-role data. See // `scripts/lib/budget-enforcer.cjs#buildCostEventPayload` for the cost-row // extension. // // Plan 27-06 owns the actual emission call sites in session-runner; this // file provides the shape + symbolic constants so 27-06 can import the // type names without redefining them. /** * Narrow union for the runtime-role tag. Pre-Phase-27 events do not carry * this field; readers MUST default to `"host"` when absent. */ export type RuntimeRole = 'host' | 'peer'; /** * Emitted by session-runner (Plan 27-06) when a peer-CLI delegation is * about to start. `latency_ms` is captured on the corresponding * `peer_call_complete` / `peer_call_failed` event; this event marks the * boundary so chain-walkers can pair started/complete via shared * sessionId + peer_id + role. */ export type PeerCallStartedEvent = BaseEvent & { type: 'peer_call_started'; payload: { runtime_role: 'peer'; peer_id: string; role: string; }; }; /** * Emitted by session-runner on successful peer-CLI delegation. * `cost_usd` is computed via the shared cost backend (Plan 26-05) * extended with `runtime_role` + `peer_id` tags so the cost-aggregator * rolls up peer spend correctly. */ export type PeerCallCompleteEvent = BaseEvent & { type: 'peer_call_complete'; payload: { runtime_role: 'peer'; peer_id: string; role: string; latency_ms: number; tokens_in: number; tokens_out: number; cost_usd: number | null; }; }; /** * Emitted by session-runner when peer-CLI delegation fails (peer-absent, * peer-error, timeout). D-07: failure is transparent — session-runner * falls back to the local Anthropic call — so this event is purely a * measurement signal for the reflector. `error_class` mirrors * Plan 20-04's `classify(err).kind`. */ export type PeerCallFailedEvent = BaseEvent & { type: 'peer_call_failed'; payload: { runtime_role: 'peer'; peer_id: string; role: string; error_class: string; }; }; /** * Union of all pre-registered event types. Not a closed enum at the * envelope level — callers can emit unknown types — but downstream * consumers use this to drive typed `switch` statements with exhaustive * checks for the subset they care about. */ export type KnownEvent = | StateMutationEvent | StateTransitionEvent | StageEnteredEvent | StageExitedEvent | HookFiredEvent | ErrorEvent | WaveStartedEvent | WaveCompletedEvent | BlockerAddedEvent | DecisionAddedEvent | MustHaveAddedEvent | ParallelismVerdictEvent | CostUpdateEvent | RateLimitEvent | ApiRetryEvent | CompactBoundaryEvent | McpProbeEvent | ReflectionProposedEvent | ConnectionStatusChangeEvent | ToolCallStartedEvent | ToolCallCompletedEvent | AgentSpawnEvent | AgentOutcomeEvent | PeerCallStartedEvent | PeerCallCompleteEvent | PeerCallFailedEvent; /** * Runtime list of all pre-registered event `type` strings. Used by the * Phase 22 baseline test and the CLI transport's `--list-types` * subcommand. */ export const KNOWN_EVENT_TYPES: readonly string[] = [ 'state.mutation', 'state.transition', 'stage.entered', 'stage.exited', 'hook.fired', 'error', 'wave.started', 'wave.completed', 'blocker.added', 'decision.added', 'must_have.added', 'parallelism.verdict', 'cost.update', 'rate_limit', 'api.retry', 'compact.boundary', 'mcp.probe', 'reflection.proposed', 'connection.status_change', 'tool_call.started', 'tool_call.completed', 'agent.spawn', 'agent.outcome', // Phase 27 / Plan 27-08 — peer-CLI delegation lifecycle (D-09). 'peer_call_started', 'peer_call_complete', 'peer_call_failed', ] as const; // --------------------------------------------------------------------------- // Phase 27 / Plan 27-08 — symbolic constants for peer-CLI event names. // --------------------------------------------------------------------------- // // Plan 27-06 (session-runner) imports these by name rather than copying // string literals so a downstream rename is a single-source-of-truth edit. // All three are also present in `KNOWN_EVENT_TYPES` above for the registry // test in `tests/event-types-registry.test.ts`. /** Event type emitted when a peer-CLI delegation starts. See `PeerCallStartedEvent`. */ export const PEER_CALL_STARTED = 'peer_call_started' as const; /** Event type emitted when a peer-CLI delegation succeeds. See `PeerCallCompleteEvent`. */ export const PEER_CALL_COMPLETE = 'peer_call_complete' as const; /** Event type emitted when a peer-CLI delegation fails (D-07: transparent fallback). See `PeerCallFailedEvent`. */ export const PEER_CALL_FAILED = 'peer_call_failed' as const; /** * Frozen set of all three peer-call event names. Convenient for * downstream code that wants to gate "is this a peer-call event?" checks * (e.g. the cost-aggregator's mixed-role roll-up). */ export const PEER_CALL_EVENT_TYPES: readonly string[] = [ PEER_CALL_STARTED, PEER_CALL_COMPLETE, PEER_CALL_FAILED, ] as const; /** * Default runtime-role tag for events that pre-date Phase 27. Readers * SHOULD substitute this when `payload.runtime_role` is absent so legacy * event-stream consumers continue to read uniformly. Stamping at write * time on every emission would be a much larger surface change — see * Plan 27-08 deviation notes for rationale. */ export const DEFAULT_RUNTIME_ROLE: RuntimeRole = 'host';