Skip to main content
Typed lifecycle events — the HarnessEvent discriminated union (ADR-0001/0002, Phase 3.0). The closed, versioned union the engine emits and the cockpit renders. Unlike the sync Emitter’s raw dicts (events.py, kept for back-compat + the CLI), these are exhaustively matchable and carry a schema_version + global event_id, so the journal can round-trip them verbatim and a renderer can switch on type safely. event_id / session_id / ts are stamped by the bus at publish (see session.EventBus); a freshly built event carries placeholder defaults until then.

Classes

AgentEnd

A run has settled to a terminal outcome (the stream’s end).
Fields

AgentStart

A run has begun on goal. task_kind is the resolved kind the run opens with; mode_source records how it was decided (“override”/“classifier”/“heuristic”, or None when the core was driven directly) so a dogfood journal can distinguish a classifier miss from a classifier outage.
Fields

ApprovalController

The awaited control hook the runner consults for a gated (tier-3 ask) call. The controller announces the need (an ApprovalRequested event) and then blocks this run only until a control method resolves it — the §13 two-plane boundary.

ApprovalController.request_approval(self, approval_id: str, tool: str, reason: str, tool_input: dict) -> bool

Announce the gated call and block this run until a control method resolves it. Args: approval_id: Correlates the announcement with its resolution. tool: The tool name awaiting approval. reason: The gate’s reason, shown to the human. tool_input: The proposed call arguments. Returns: True iff the call was allowed.

ApprovalRequested

A gated (tier-3 ask) call awaits a human decision — announce only (§13).
Fields

ApprovalResolved

A pending approval (approval_id) was decided via the control plane. via records who decided: "human" answered a prompt, "grant" was auto-allowed by a session-scoped ApprovalGrant with no prompt, "auto" was auto-denied by an unattended/batch run’s deny-by-default disposition or the approval-timeout backstop (no human present). All are observable (invariant #5).
Fields

CancellationObserved

The loop observed a tripped cancellation token and is stopping (§8).
Fields

DecisionError

A malformed model reply — either recovered by an in-client retry or a lost turn (§6). Closes the observability gap where a failed decision attempt (e.g. a truncated str_replace emission) left no trace: every malformed attempt is journaled with its error and a capped excerpt of the raw reply, so a struggling run is legible live and debuggable after the fact (invariant #5).
Fields

DeclarationRequired

A greenfield edit was refused pending a declared verification contract (ADR-0038). Emitted at the investigating → editing boundary when the task is greenfield (no detected/cited/configured contract) and the model tried to edit without first calling declare_verification. nudge/max_nudges track the bounded-nudge budget: at the cap the runner stops refusing and falls back to the smoke floor.
Fields

EventBase

Fields common to every lifecycle event — the journal’s ordering/versioning keys. The type discriminator is declared on each concrete event (not here): it is per-event by nature, and a shared mutable base field can’t be narrowed to a Literal soundly. HarnessEvent is the discriminated union over the concretes.
Fields

EventSink

Where the runner publishes typed events (the bus stamps event_id/session_id/ts). Foundation publishing is fire-and-forget via publish_nowait onto an unbounded queue; the awaited emit is the frozen async interface lane 1 fills in with bounded, backpressured fan-out. Both must keep event_id monotonic.

EventSink.emit(self, draft: 'HarnessEvent') -> 'HarnessEvent'

Awaitable publish — the interface lane 1 fills in with backpressure. Args: draft: The event to publish. Returns: The stamped event.

EventSink.publish_nowait(self, draft: 'HarnessEvent') -> 'HarnessEvent'

Stamp and enqueue draft without blocking. Args: draft: The event to publish. Returns: The stamped event.

ModelDecisionEvent

The model chose an action this turn (thought + a one-line brief).
Fields

ModelUpdate

A streamed model-output delta — display only; never private chain-of-thought (ADR-0001 D6).
Fields

ModelUsage

Provider-reported token usage for one turn (in-client retries summed). The journal’s per-turn cost record — the eval harness (ADR-0004) sums these for tokens/$ per solved task; without them cost is unmeasurable (invariant #5).
Fields

PhaseChanged

The control phase advanced from old to new (§7).
Fields

TaskEscalated

The task was escalated investigate → edit mid-run (ADR-0048). Emitted when a consented switch_to_editing (model-requested, or a thrash-nudged request) flips the task kind only — deliberately not the phase, and not the frozen plan. The task becomes a normal edit task still sitting in investigating, so the standard edit-intent bootstrap runs the declaration gate and advances the phase on the next edit: escalation never jumps that gate. trigger records what caused it (model = the model asked unprompted; thrash = the harness’s thrash detector nudged it there). The transition is one-directional and once-only, so this only ever reads investigate → edit.
Fields

ToolEnd

A tool call finished — success plus its summary/content (§10).
Fields

ToolStart

A tool call (call_id) is about to execute.
Fields

TurnEnd

The current loop iteration has finished.
Fields

TurnStart

A new loop iteration (iteration) has begun.
Fields

VerificationEnd

The verifier returned a verdict — passed plus its summary (§12).
Fields

VerificationPlanFrozen

The per-session verification plan was resolved and frozen (ADR-0007). Journaled at the investigating → editing boundary, before any verification: each check carries its command and provenance, so every run’s rubric — and where each check came from — is auditable. An empty checks records that nothing was discovered (the verifier will fail legibly). change_kinds records the kinds a model-declared contract covers (ADR-0044) — the model’s stated intent, auditable against the diff; None = no declaration (tiers 1-3), including journals written before the field existed.
Fields

VerificationStart

The harness-owned verifier has begun (§12).
Fields

Functions

dump_event(event: Annotated[AgentStart | AgentEnd | TurnStart | TurnEnd | PhaseChanged | DeclarationRequired | ModelDecisionEvent | ModelUpdate | ToolStart | ToolEnd | ApprovalRequested | ApprovalResolved | DecisionError | ModelUsage | VerificationPlanFrozen | VerificationStart | VerificationEnd | CancellationObserved | TaskEscalated, FieldInfo(annotation=NoneType, required=True, discriminator='type')]) -> str

Serialize an event to one JSON line for the journal. Args: event: The event to serialize. Returns: Its compact JSON representation.

load_events(path: Path) -> list[Annotated[AgentStart | AgentEnd | TurnStart | TurnEnd | PhaseChanged | DeclarationRequired | ModelDecisionEvent | ModelUpdate | ToolStart | ToolEnd | ApprovalRequested | ApprovalResolved | DecisionError | ModelUsage | VerificationPlanFrozen | VerificationStart | VerificationEnd | CancellationObserved | TaskEscalated, FieldInfo(annotation=NoneType, required=True, discriminator='type')]]

Reload a JSONL journal back into typed events, in file order. Args: path: The JSONL journal written via dump_event / a typed EventLog. Returns: The events, one per non-blank line, validated through the union.

parse_event(data: dict | str | bytes) -> Annotated[AgentStart | AgentEnd | TurnStart | TurnEnd | PhaseChanged | DeclarationRequired | ModelDecisionEvent | ModelUpdate | ToolStart | ToolEnd | ApprovalRequested | ApprovalResolved | DecisionError | ModelUsage | VerificationPlanFrozen | VerificationStart | VerificationEnd | CancellationObserved | TaskEscalated, FieldInfo(annotation=NoneType, required=True, discriminator='type')]

Validate a dict / JSON string into the right HarnessEvent variant. Raises pydantic.ValidationError if type is unknown (the union is closed) or fields are invalid. Args: data: A mapping or JSON text carrying a known type discriminator. Returns: The validated event.