events() (an async stream that can never block or
redirect the run); control flows IN via resolve_approval() and cancel(). An
event may announce that approval is needed, but only the control method decides
it (§13). The cockpit binds to exactly this surface; the engine stays unchanged.
The EventBus fan-out + the privileged JsonlEventJournal live in bus.py/journal.py
(Lane 1); this module owns the session boundary and approval/grant control.
Classes
ApprovalGrant
A session-scoped standing approval: auto-allow one tool’s calls sharing a program.
Stored when a human approves a tier-3 call with [a] always. Scoped to a (tool, prefix, tier) triple — never global (an empty prefix matches nothing) and never
tier-4 (destructive actions always re-prompt).
ApprovalGrant.matches(self, tool: str, program: str, tier: int) -> bool
Whether this grant auto-allows a call to tool/program at tier.
Args:
tool: The tool name of the incoming call.
program: The incoming call’s program prefix (see _grant_prefix).
tier: The incoming call’s permission tier.
Returns:
True iff the grant covers the call (same tool + program, at or below the
granted tier, and below the tier-4 ceiling). A blank prefix never matches,
and an ungrantable tool (alter_verification) never matches — a contract
amendment is ratified by a human every time.
Session
A live, interruptible task run exposing the two-plane API (§13, §23).
Args:
runner: The AgentRunner to drive (its event_sink/approval_controller
are wired to this session for the duration of run).
state: The task state to execute.
session_id: Stable id stamped on events; generated if omitted.
journal: The privileged write-ahead sink for this run’s events; None (default)
keeps the in-memory history only. run() closes it when the run ends.
grants: The standing-approval list to consult and append to. Pass the multi-turn
SessionState.grants (by reference) so a [a] always granted in one task
persists to later tasks in the conversation; omit for a fresh per-run list.
unattended: When True (batch/eval/autonomous wrappers), an ask is auto-denied
immediately rather than awaiting a human — no resolve_approval will ever come,
so awaiting one deadlocks the run. The deny is still announced + recorded
(ApprovalResolved(via="auto")). False (default) keeps the interactive path.
approval_timeout: A backstop, in seconds, on a blocking (attended) approval: if no
resolve_approval arrives within it, the call is auto-denied so a run can’t hang
forever inside the gate (the wall-clock budget can’t preempt an awaited approval).
None (default) waits indefinitely — the right shape for a human at a REPL.
amendment_policy: The unattended disposition for an alter_verification amendment
(ADR-0039). "deny" (default) keeps ADR-0016’s deny-only posture; "approve" lets
an autonomous run self-ratify that one action (never any other ask).
escalation_policy: The unattended disposition for a switch_to_editing escalation
(ADR-0048). "deny" (default) refuses the mid-run investigate→edit switch with no
human; "approve" lets an autonomous run self-ratify that one action — the same
vocabulary as amendment_policy, since the two knobs share their semantics.
Session.cancel(self, reason: str = 'cancelled') -> None
Control plane: trip the cancellation token; the loop observes it at the next turn.
Also denies any in-flight approval so a run blocked on the gate can reach the
cancellation checkpoint rather than hanging.
Args:
reason: Why the run is being cancelled (retained for display; the loop records
its own cancellation feedback when it observes the token).
Session.events(self) -> AsyncIterator[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')]]
The observation plane: an independent async stream of typed events (§13).
Subscribes eagerly (at call time, not first iteration), so a consumer created
before run() never misses early events. Each call yields a fresh, independent
stream — many observers can watch the same run concurrently.
Returns:
An async iterator over this run’s HarnessEvents, ending on agent_end.
Session.request_approval(self, approval_id: str, tool: str, reason: str, tool_input: dict) -> bool
Announce a gated call (observation) and await the human’s decision (control).
Called by the runner on a tier-3 ask. Emits ApprovalRequested, then disposes of
it per mode: an attended session blocks this run only until resolve_approval
completes the future (decision never returns through the event stream, §13); an
unattended session auto-denies immediately (no human will come); and an
approval_timeout denies a blocking wait that no human answers in time. Every deny
is recorded as ApprovalResolved(via="auto").
A standing ApprovalGrant from an earlier [a] always short-circuits the human:
the call is auto-allowed and recorded as ApprovalResolved(via="grant") with no
ApprovalRequested (that event means “a human must decide”; a grant skips the human).
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.
Session.resolve_approval(self, approval_id: str, *, allow: bool, remember: bool = False) -> None
Control plane: resolve a pending approval (the decision the event announced).
Tolerant of an unknown id (resolves the sole pending request if there is exactly
one) so a caller that didn’t capture the id can still unblock the run. With
allow and remember both set (the [a] always choice), stores a session-scoped
ApprovalGrant for the call’s program prefix so matching calls auto-allow later.
remember is ignored on a denial (there is no “always deny”), on a blank prefix,
and on an ungrantable tool (alter_verification — a contract amendment is ratified
by a human every time; remember degrades to allow-once).
Args:
approval_id: The id from the ApprovalRequested event.
allow: Whether to permit the gated call.
remember: Whether to store a standing grant (only meaningful with allow).
Session.run(self) -> TaskState
Drive the run with this session as the event sink + approval controller.
Returns:
The terminal TaskState.