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).
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.
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).
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).
CancellationObserved
The loop observed a tripped cancellation token and is stopping (§8).
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).
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.
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.
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).
ModelUpdate
A streamed model-output delta — display only; never private chain-of-thought (ADR-0001 D6).
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).
PhaseChanged
The control phase advanced from old to new (§7).
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.
ToolEnd
A tool call finished — success plus its summary/content (§10).
ToolStart
A tool call (call_id) is about to execute.
TurnEnd
The current loop iteration has finished.
TurnStart
A new loop iteration (iteration) has begun.
VerificationEnd
The verifier returned a verdict — passed plus its summary (§12).
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.
VerificationStart
The harness-owned verifier has begun (§12).
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.