Runner and Context
The current alpha runner wires the turn lifecycle modules.
TurnAdmissionRuntime resolves the core/session route and starts the turn,
TurnExecution owns the authored input -> model/tool -> output path, and
TurnPersistenceRuntime records input, assistant output, display state,
completion, and interruption. Agent Core slots participate through controlled
interfaces; they do not own the lifecycle.
This layout now implements the scoped TurnExecution, PrincipalScope, and
immutable execution-identity contracts. ContextManager and durable restart
semantics remain later work. See
Host Runtime Contracts for the authoritative target
contract. The current runner does not yet satisfy every invariant below.
Turn Flow
The current flow is:
inbound interaction
-> admit turn: resolve session/core, bind route, pin scope, begin turn
-> activate captured route/principal and run bootstrap
-> run authored input pipeline
-> assemble provider context
-> call provider
-> execute tool calls through ToolRuntime
-> continue model/tool loop until final response
-> run authored output pipeline
-> persist input, assistant output, display state, completion, and session events
TurnExecution Interface
The external Host seam is deliberately small:
TurnExecution.run(TurnRequest) -> TurnResult
TurnExecution.cancel(TurnId, PrincipalScope) -> TurnCancelResult
TurnExecution must hide session admission, core-revision pinning, context
preparation, provider/tool steps, slot execution, persistence, delivery, and
cleanup. A caller supplies immutable request values, not a mutable runner,
loaded core, store, provider client, or capability facade.
The module owns these observable contracts:
- same-session turns are serialized by admission, while different sessions can run concurrently;
- the session, core revision, capability snapshot, route, and trace identity captured for a turn do not change after an await;
- provider, slot, effect, cancellation, and unexpected failures create one terminal turn state before resources are released;
- restart marks or recovers orphaned admissions explicitly and never silently replays a dangerous provider/effect step;
- detached work is a separately owned runtime task, not a late mutation of a completed turn.
The current implementation exposes TurnExecution.run() and owner-checked
cancel() as the Host test seam. It enforces one in-process active turn per
session with a keyed admission lock, removes idle lock entries, and keeps
different sessions concurrent. Admission captures the resolved session, loaded
core plus revision, immutable capability declarations, route token, trace id,
and cancellation identity in a frozen TurnExecutionContext. Replacing the
active core or mutating its capability manifest later does not change the
admitted turn's revision or capability decisions.
Queued same-session requests reload and verify the active core/revision only
after acquiring admission, so a promotion while waiting cannot pair old loaded
content with the new revision label.
The captured route is activated in Host execution-local context. Delivery,
prompt, approval, and child asyncio tasks created during the turn therefore use
the exact admitted token rather than the session's latest binding. A token that
is unbound while the turn is running fails closed instead of switching to a new
adapter. The token and principal never enter model-facing metadata or the
authored TurnContext SDK.
Prompt, IO, slot history/result, event, artifact, and delivery hot paths use the
captured session or immutable TurnContext.session_id after an await. Owner
cancel, coroutine cancellation, route activation failure, and unexpected
exceptions persist interruption where the lifecycle has started and release
principal binding, route context, and admission in nested finally cleanup.
Live delivery tasks are tracked by turn and drained before that turn leaves the
active registry; cancellation during adapter delivery therefore cancels the
owned delivery claim, records failure, terminates the turn, and releases the
session admission without waiting on unrelated sessions.
Any earlier provider/tool/slot failure cancels and awaits already scheduled
interim deliveries from the same lifecycle cleanup path.
Admission also resolves a frozen PrincipalScope before bootstrap: external conversations are matched
against the durable session_owners projection, TUI runs use explicit local
operator authority, schedules use run-scoped system authority, and child agents
own only their delegated child session. The scope is carried by
TurnExecutionContext; it is not added to the authored TurnContext SDK.
Background tasks capture a bounded record of that admitted scope before their
detached task starts. Completion intake restores and validates the record
against the durable session owner before claiming the event; route metadata
cannot elevate the completion, and the internal scope record is not exposed in
model-facing metadata. Child spawn closures likewise capture the admitted
parent scope instead of reconstructing authority from a legacy session row.
This is not yet the final durable 1.0 lifecycle contract. Admission and the
active-turn cancel registry are process-local, restart recovery is not
implemented here, and TurnRequest/TurnResult still retain alpha compatibility
surfaces that are not the final deeply immutable 1.0 products. Live core,
lifecycle, state, lock, and task controls are private admitted-turn state rather
than fields on TurnExecutionContext. PrincipalScope now reaches store-owned
session/message/task predicates, channel and operator session list/resume,
session_search, task detail/wait/cancel, /subagents, and
principal/session/policy-scoped approval caching. Later EffectRuntime work
extends the same owner seam across every effect adapter.
Principal and Execution Context
PrincipalScope is Host authority, not an Agent Core capability grant. It is
derived from authenticated channel/operator/system facts plus durable
conversation/session bindings, and it supplies the owner predicate for session,
history, task, wait, cancel, resume, search, and approval-cache operations.
External adapter facts enter the Host through InteractionInbound.principal_key.
That field is set by the adapter after its transport authentication/allowlist
step and is kept separate from delivery source, arbitrary metadata, and raw
webhook body identifiers. A conversation scope is accepted only when that key,
channel, conversation binding, and session owner all match durable state.
The store-bound resolver is the only issuer. Operator issuance never accepts a
caller-supplied session set: it binds one active session, requires the active
Host's in-memory operator issuer plus an explicit reason, and writes a
principal_scope.operator_issued audit event. Cross-session operator queries
use a relational session_owners predicate rather than materializing an
unbounded SQL IN list. A scope issued by another store instance is rejected
at owned-query and session-persistence boundaries. Closing the Host revokes its
process-local operator capability even when tool shutdown fails, so a retained
scope cannot authorize reads after DemiurgeApp.close().
TurnExecutionContext binds that principal to one session, turn, core revision,
capability snapshot, workspace, route token, admission lease, cancellation
token, and trace. Those bindings are immutable for the turn. Agent Slots and
authored tools continue to receive the reduced author-facing SDK contexts;
where applicable those contexts contain TurnContext. They do not receive
operator authority, Host stores, or admission internals.
Context Layers
Provider context can include:
- soul text
- skill index and loaded skills
- bootstrap output
- input module placements
- session history
- current user turn
- tool call and tool result history
The current ContextAssembler decides final provider message order and content.
It does not know the model context window, reserve an output budget, or trigger
automatic compaction.
The target ContextManager.prepare() owns layer budgets, full-request
estimation, cheap pruning, compaction lease and fallback, and typed overflow
before provider IO. ContextManager.observe() consumes normalized usage and
finish-reason observations without relying on ambient mutable session state.
Manual /compact remains the current alpha mechanism until that module is
implemented.
Bootstrap
Bootstrap modules are session-start context producers. They should be stable within a session and safe to quote as reference context.
Background Task Completion Turns
Background task completion is modeled as a synthetic inbound event for the
originating session rather than as direct channel output. Channel bridges use
live subscription as a wakeup path and recover pending completion events from
SQLite. If user input and completion are both pending, the user input runs first
and pending completion summaries are merged into that user turn. Completion
notifications use durable work state:
ready work is claimed before a bridge queues or merges the synthetic inbound,
and it is acknowledged only through the task-worker seam. A successful
yield_until call claims and acknowledges the matching pending completion, so
channel bridges do not run a second synthetic completion turn for the same task
result.
Parallel input and output slots are still scheduled concurrently, but the runner waits for their host-managed work to finish before marking the parent turn terminal. Detached slot work must be modeled as a child runtime task rather than mutating after the parent turn is complete. Here parallel means concurrent-but-joined within the parent turn; it is not detached or restart-durable.
/stop and foreground cancellation affect only the active turn. Background
tasks continue until they finish or a user calls task_control(command="cancel").
Model-facing task_status, task_control, and yield_until, plus the
/subagents list/detail/cancel command, use the admitted PrincipalScope in
store-side task predicates. A guessed task id owned by another principal is
reported exactly like a missing id and cannot expose logs, wait for completion,
consume completion state, or cancel the task.
The model-facing controls return bounded status/result fields, not task logs;
full log inspection remains an independent Host/operator surface.
Delegation controls and ordinary ToolRuntime dispatch share the same
resolve_approval_scope(...) identity validation, including session, turn,
core revision, and capability snapshot checks.
Background work that needs user input is marked blocked_needs_user and is
not auto-approved.
Session Delivery Routes
The runner owns a shared SessionInteractionRouter. InteractionRuntime
passes the current adapter as a SessionRouteBinding; after the runner resolves
the final session for the inbound, admission captures that session id and binds
the route to the captured value.
TUI and channel /new, /resume, and session switch paths must rebind the
same adapter route to the new session.
External channel conversations also have a durable binding keyed by
(core_id, channel, conversation_key). conversation_key is a canonical
host-owned route key built from explicit platform facts, for example
telegram:dm:123 or slack:channel:T1:C1:thread:123.4. Channel /resume
rebinds the current conversation key to the resumed session so the next inbound
message from that external conversation continues in the same transcript.
Channel /sessions and /resume use owned session list/get queries; ordinary
conversation authority sees only its bound session, while the local TUI can
cross sessions only through its explicit audited operator scope. A raw session
id that is missing or outside the scope returns the same external error before
the runner or route binding is changed.
Rows migrated as legacy_local remain excluded from normal operator browsing
and history reads; only the dedicated repair/status path may inspect them.
The containment path now builds delivery from the captured turn session rather
than rereading runner.session_id. The final contract still moves the route
token itself into TurnExecutionContext so restart, owner checks, and route
lifetime are represented by one durable execution interface.
Ordinary output, tool lifecycle events, and background output flushes create
InteractionOutbound objects with a required session_id. The router delivers
only to the route bound for that session. If no route is bound, items are marked
unrouted and are not treated as failed adapter calls.
Subagent Sessions
ctx.agents.run(), ctx.agents.spawn(), and delegate_task run child agents
in independent session_child_* sessions. Child runners share the same router
table but do not receive the parent route binding. Their ordinary output and
tool lifecycle delivery appear only on a route explicitly bound for the child
session.
Parent/child lineage remains task and observability metadata. It is not part of
ordinary delivery routing. Parent turns receive child work through
AgentRunResult, durable task completion, or explicit future subagent.*
events.
Approval and Prompts
Interactive prompts and approval decisions use session-aware lookup on the same
router, but they are not ordinary delivery. By default an approval request is
looked up by turn.session_id; when no interactive route is bound, the approval
provider denies with no_interactive_route unless a host, global, or core
policy has already auto-allowed the action.
The session-allow cache is owned by ApprovalRuntime and consumes the immutable
scope derived from TurnExecutionContext. Its key binds principal, session,
policy/effect fingerprint, and rule; route lookup alone is not authorization.
Non-turn Host operations must supply an explicit Host-issued PrincipalScope.
Failure Handling
In the current alpha runner, slot failure_policy determines whether a failed
slot is soft or hard. Exceptions/cancellation inside the guarded input,
provider/model-loop, and output stages write terminal turn state before being
re-raised. Tool-catalog preparation currently sits outside those guarded
regions, so its failure does not yet have the same guarantee. A foreground turn
is not a RuntimeTask; channel delivery, background task, and schedule errors
remain owned by their respective Host modules.
The target TurnExecution interface returns typed failed/cancelled product
outcomes and exposes only typed rejection or infrastructure failures. Adapter
exceptions do not become part of its caller interface.
Boundary
Do not move provider request construction, context budgeting, principal authority, or session ownership into Agent Core code.