Status: Living
Last updated: 2026-10-16
- Related: llm-provider-seam.md, tool-registry.md, run-plan.md, built-in-tools.md, ../contracts/sse-event-schema.md, ../../decisions/0038-agentrunner-llm-call-boundary.md, ../../decisions/0039-same-provider-reasoning-replay.md, ../../decisions/0036-run-loop-substrate-event-bus-and-execution-host.md, ../../decisions/0037-engine-tool-execution-boundary.md, ../../standards/error-handling.md
The AgentRunner is the single dispatching NodeExecutor (ADR-0036) the run loop holds. It runs an agent vertex's LLM turn(s) end to end against the @relavium/llm seam, through the ToolRegistry, and returns one NodeOutcome. This page is the canonical home for its injection boundary and turn contract; the decision is ADR-0038.
| Layer | Where | Concern |
|---|---|---|
| Turn core | internal (engine/agent-turn.ts) |
A correlation-key-agnostic driver: assemble → chain.stream → fold the stream into agent:* events → tool-call loop → settle. It takes messages + tools + the fallback plan + emit + signal + nodeId + the registry + limits, and emits envelope-less event bodies. No NodeExecContext, no runId/sessionId. AgentSession (1.V) reuses it unchanged (ADR-0024/0025/0026) — its parameter shape is a frozen internal contract, not on the public surface. |
| Dispatching adapter | exported (engine/agent-runner.ts) |
createAgentNodeExecutor(deps) → the NodeExecutor. Switches on ctx.vertex.type: an agent vertex runs the turn core; every non-agent type is a loud, typed failed stub (internal) until the 1.P handlers land. Owns the run-path concerns the core excludes (below). |
The host injects only platform capabilities; the credential is threaded opaquely and is never stored, inspected, logged, persisted, or sent to the frontend by @relavium/core (ADR-0038, rule 6).
resolveProvider(providerId): LlmProvider | undefined— the one genuinely-new capability: the authoredAgent.provider/fallback_chain[].providerare provider-id strings, but aFallbackPlanEntry.provideris a concrete adapter instance, which the engine cannot construct (vendor SDK +@types/nodebreak engine purity).undefined⇒ a host-wiring gap → aNodeFailure{ code: 'internal' }.resolveMediaSurface?(model): MediaSurface | undefined— the catalog projection ofmodel_catalog.media_surface(1.AG Section C, ADR-0045 §1) that selects the inline-vs-generative dispatch.'generative'routes the node to the separate-endpointgenerateMedia;'chat'— the default, and the value when this dep is absent or returnsundefined— uses the normal turn. The engine is platform-pure (no DB), so the host injects the lookup; the production catalog wiring is deferred to 1.AH (until then every model is'chat'— no generative model is runtime-reachable).registry+tools— the sharedToolRegistry(for dispatch) and itsToolDefs (the source of the LLM-visible schema + descriptions for the granted tools).keyFor/sleep/now?/onAuthError?— forwarded into the per-nodeFallbackChain(the existingFallbackChainOptionsseam — not re-declared as a parallel credential surface).onAuthError(the single out-of-band credential refresh) is host-owned.resolverCapabilities?(theread_filefilter for a prompt),fsScope?(default'sandboxed'),limits?,preEgress?.
The engine's execution context supplies continueReceipt, from which the runner derives a
call-local producer-lifetime hook. Preparation grants no receipt authority. The chat turn passes
that hook separately from ChainCapabilities and request data; the separate-endpoint generative
path uses it before invoking generateMedia. Both retain the raw producer before the existing
deadline race. A refused registration preserves host-error identity and engages no provider;
proven pre-egress generative admission is released. Raw completion creates no new provider
attempt, media pin, save or ordinary execution restart. Lifetime-only completion cannot change
accounting. Token accounting refinement requires the exact registered receipt authority described
in Usage evidence and durable coverage; it can refine that
invocation once without reopening its public result. See the
canonical chain lifetime contract.
The chain and separate media submission pass that authority through the provider's optional
invocation argument, outside request data. Each engine-owned media poll creates a new retained
raw scope, including its key resolution, and forwards a poll-local invocation hook; it never reuses
the settled submission context. Poll status and binary-download transport children remain owed
through that invocation's independent aggregate. Existing bounded outcomes and once-only media
accounting are preserved. Custom providers also receive invocation-local authority that retires
before bounded completion, fallback or consumer return, while admitted descendants remain joined.
Invocation transfer is itself a synchronous host boundary. The runner/chain recheck caller and
merged-deadline cancellation after it, before marking an attempt invoked or calling the captured
provider method. Proven pre-egress refusal retires the aggregate and refunds unused admission.
The engine's departure contract
owns the complete actor and final-health barrier; the runner does not certify host closure itself.
The runner owns the cost path itself — one CostTracker per node execution and its own onAttempt→cost:updated — never a host-supplied (shared) tracker, because the executor is shared across concurrent runs.
- Resolve the agent. An absent
resolvedAgent(run-plan.md §AgentPlanConfig) ⇒NodeFailure{ code: 'validation' }naming theagent_ref(an authoring error — distinct from an unresolved provider id, which isinternal). Never a raw throw. - Build the fallback plan. The primary uses
node.model ?? agent.model. With an authorednode.retry ?? agent.retrybudget it has one within-chain attempt; without one it has the shared runner's unauthored two-attempt default. Above-chain node retry does not multiply within-chain primary attempts (ADR-0040). Eachfallback_chainentry keeps its ownmax_attempts. OneFallbackChainper node execution, reused across the tool loop so per-provider cooldown and the ADR-0039 strip-latch survive.- Generative fork (1.AG Section C, ADR-0045 §1/§6). After the plan + the resolved prompt, the primary model's surface is read via
resolveMediaSurface?.(primary.model) ?? 'chat'. A'generative'result dispatches to the separate-endpointgenerateMedia(one provider, no chain failover — a generative call is provider-bound) and returns early: an empty-prompt / multi-modality node failsvalidation; a pre-egress budget gate runs first (gate-only — the ADR-0028 pre-egress governor; the token estimate is pinned to zero for a token-free generative call, see the code comment); the SYNC{ media }becomes the{ text: '', media: [part] }node output (de-inlined to amedia://handle like the inline path); exactly one realizedcost:updatedis emitted (request volume × per-model media rate) — unless a rate is missing, which since ADR-0089 §4 is unpriced, not zero: under the non-strict default the numeric total may still floor at 0 but the event carriespriced: falseand the modality is counted inunpriced_calls, and under the authoredstrict_cost_capthe call is refused before provider egress, so no realized event is emitted at all. (This clause said "degrade-to-0" until 2026-09-02; that behaviour is exactly theCR-55defect — a0the governor cannot tell from "nothing to charge".) AjobId(async LRO) is Section D. A'chat'model continues to the turn core below.
- Generative fork (1.AG Section C, ADR-0045 §1/§6). After the plan + the resolved prompt, the primary model's surface is read via
- Narrow the tool grant.
node.toolsmust be a subset ofagent.tools— a widening attempt ⇒validation(ADR-0029). - Assemble messages.
system= authored text ONLY (agent.system_prompt+node.system_prompt_append), concatenated verbatim — 1.O does not interpolate the system role, so an untrusted{{ run.outputs }}/read_filereference can never resolve intosystem(it ships as literal authored text). The parser still collects a{{ … }}reference insystem_prompt_appendfor the parse-time secret-taint scan (catching a stray{{ secrets.* }}), but does not promise dispatch-time resolution of system fields. Only the resolvedprompt_template— which may draw on untrustedrun.outputs/read_file— lands in auserposition, neversystem(security-review.md §Prompt-injection, the structural placement guarantee; public prompt resolution also enforces the qualified context boundary). (A future parse-time gate that admits trusted{{ inputs }}/{{ ctx }}in system fields while rejecting untrusted sources is a recorded follow-up.) output_schema(node override wins over the agent default). Lowered toLlmRequest.responseFormat(a request-side hint), and validated node-side: the seam'sresponseFormatdoes not guarantee a schema-conformant response (DeepSeek degrades to barejson_object), so the runner parses the output and a non-JSON result ⇒validation(ADR-0038, error-handling.md). The check is two steps: the output is parsed as JSON, then validated against the compiledoutput_schema— a conformance miss isvalidation(retryable: false), exactly as ADR-0038 required. Compilation uses the allowlist-strict JSON-Schema subset (ADR-0092; the subset is a written contract, and a schema outside it is refused at parse rather than silently unenforced). (This clause read "parse-as-JSON; deep conformance deferred… needs a validator dependency" until 2026-09-02 — the dependency premise was false, the compiler was already in-house.)- Run the turn core, map its result to
NodeOutcome.completed(output= the parsed structured value or the assistant text;tokensUsed = { input, output, model }), or map a classifiedAgentTurnErrortoNodeOutcome.failed.
Ordinary execution and budget preparation forward the identical node signal to public prompt
resolution and recheck cancellation after its final await, before prompt-error mapping or further
admission. Literal/empty prompts share that boundary. A cancelled resolver settlement returns the
existing non-retryable cancelled failure without provider/key/effect work; pure plan lookup may have
already occurred. Human-gate message/assignee resolution also rechecks after its final await, so
cancellation there cannot become a newly published pause.
The core streams one turn (emitting agent:token per text delta, accumulating text / tool-call / reasoning parts), and on a tool_use stop appends the assistant turn (including its reasoning ContentPart for the same-provider replay, ADR-0039), dispatches each tool call, appends the results, and continues — bounded by a runner-default max-tool-turns cap (a DoS guard; the authored hard cap + the loud turn_limit surfacing is the 1.V session knob).
The error mapping to the closed ErrorCode (error-handling.md) — cancel wins over all others:
| Source | ErrorCode |
Retryable | Note |
|---|---|---|---|
abort (ctx.signal) / ToolCancelledError / chain cancelled |
cancelled |
false | precedence over every other classification |
ToolPolicyError |
tool_denied |
false | not fed back as a correctable result (re-asking a denied tool burns budget) — EXCEPT media_scope_denied, a Step-14 recoverable SCOPE denial fed back on the recoverToolFailures surfaces (chat / Home / one-shot agent run); see the recoverable note in tool-registry.md §error taxonomy |
UnknownToolError / ToolArgsInvalidError |
(model-correctable) | — | converted to an isError tool result fed back, within a bounded correction budget; after it ⇒ tool_failed |
ToolExecutionError |
tool_failed |
true | |
absent host capability (ToolUnavailableError) |
tool_unavailable |
false | a host/config gap — names the unwired arm actionably, never a bare internal (EA1, ADR-0055) |
official LlmError.context_overflow |
context_overflow |
no | fixed engine-authored message names the actual attempted model and its authoritative window (or size unknown), and whether tools ran; never provider text |
chain-exhausted LlmError |
provider_auth / provider_rate_limit / provider_unavailable / content_filter (content_filter, 1.AG/ADR-0045 §6) / validation (bad_request) / internal (unknown) |
per LlmError.retryable |
classified from error.kind, never error.message |
| max-tool-turns hit | turn_limit |
false |
The runner emits, per sse-event-schema.md (envelope-less; the bus stamps runId/timestamp/sequenceNumber):
agent:token{ nodeId, token, model }—modelis the active attempt's model (see the model-attribution note inagent-turn.ts; per-attempt model identity is carried by usage-coverage dispositions and complete-evidencecost:updated).agent:reasoning{ nodeId, text, model }— a streaming reasoning ("thinking") delta (EA6, 2.5.H — the reasoning counterpart ofagent:token, one perreasoning_delta);modelfollows the same mid-stream attribution note. Never carries the ephemeral same-providersignature(ADR-0030).agent:tool_call{ nodeId, model, toolId, toolInput, attemptNumber? }andagent:tool_result{ nodeId, toolId, success, outputSummary, attemptNumber? }— assembled from the registry's partialevents.call/events.result(the runner addstype+nodeId+model); the registry does not carry them.cost:updated{ nodeId, model, inputTokens, outputTokens, costMicrocents, cumulativeCostMicrocents, attemptNumber? }— on the agent-turn path, for provider-engaged attempts with complete usage evidence; incomplete evidence follows the acknowledged usage-coverage path without inventing an actual-money event.attemptNumbercounts non-skipped records.cumulativeCostMicrocentsis a placeholder the engine overwrites authoritatively (it owns the run-wide total).
The runner emits no budget:* / run_timeout (run-level, not in the in-node set). Its
host-owned pre-egress hook gates each actual provider attempt, including retries, failovers and
later tool rounds; it runs before credential resolution and provider invocation.
PreEgressHook(info) receives the required PreEgressInfo union and may return an admission
lease, synchronously or asynchronously. A refusal prevents that attempt's egress. The hook has
process-local access to the current owned construction through allowanceQuoteContext; the request
body and provider options never enter the durable allowance quote or run events, and no credential is
passed to this hook. Controlled request ownership is specified once in the
LLM seam. Core prepares its first round before measurement
and quotes and executes that exact factory request with only the live execution signal overlaid.
prepareAgentTurnRequest returns { request, inputTokensEstimate }; execution accepts that exact
request through preparedRequest, refusing a borrowed spread. Incoming preparedOutputCaps remains
supported and is validated by the same ownership/cap authority. The chain's cap projection has its
canonical home in the
LLM seam.
| Route | Required fields |
|---|---|
text |
The complete PreAttemptInfo: model, provider, actual endpoint, maxTokens, providerOptions, outputCapPlan; plus current-round pre-strip inputTokensEstimate and maxTokensEstimate |
generative-media |
model, provider, actual endpoint; literal zero inputTokensEstimate, maxTokens and outputTokensEstimate |
Both routes may carry outputModalities and disjoint mediaUnitsEstimate. Text maxTokens,
providerOptions and maxTokensEstimate have required keys allowing undefined. The turn core
computes input from the owned current request's system text, messages, advertised tools
and JSON response format/output schema before dialect-specific stripping. The pre-attempt hook closes
over that request and estimate before money durability can await; it never rebuilds from caller data
afterward. A real tool result creates a fresh owned round and estimate from owned static fields and
working history rather than provider usage. The current core input surface does not carry native
provider options, tool choice or stop sequences; no heterogeneous native-serializer continuation
guarantee is claimed for its later tool-round reconstruction. Local ownership/cap refusals are fixed,
nonretryable validation outcomes; existing observer/money/cancellation provenance remains intact.
All governor evaluation, admission and commitment-restoration entry points consume the whole
object. The host captures max_tokens_estimate once and forwards it through workflow,
fresh/resumed/reseated chat and one-shot session construction, including sessions without a
money governor. NodeExecContext.maxTokensEstimate supplies the workflow value; a directly
constructed runner can receive it through AgentRunnerDeps.maxTokensEstimate.
The governor inserts an accepted priced reservation synchronously after evaluation and before any host notice or warning write. A reentrant partial-pricing notice therefore sees that live reservation. Non-strict fail/pause verdicts reserve nothing and still announce their partial-pricing gap. Warning admissions await their shared durable warning; a failed write releases each admission.
Before invoking the hook, the turn joins the run's shared realized/conservative money barrier, including turns without a budget hook. A pending sibling write blocks admission, credential resolution and provider egress; its failure retains the sibling's attribution. Both chains drain their current tails, including writes appended during the wait. The shared barrier rechecks the realized tail after conservative flushing so a cross-chain append also blocks continuation. Its stable point covers charges already recorded; later charges enter the next barrier. Cancellation is checked after that join before the no-hook return, and again after any awaited admission; a newly acquired admission is released before cancellation propagates.
An admission settles from accountable usage. A usage-less failed attempt releases only with
positive pre-provider proof, or an actual official endpoint's classified pre-content
context_overflow, or its pre-content HTTP status in
429, 400, 401, 402, 403, 404, 413, 422. A returned generation, even empty, and every forwarded
non-stop/non-error stream chunk establish processing evidence. A rejected generated response with
accountable usage also proves processing; its actual quantities settle once and are never refunded. Custom routes and uncertain
failures retain the reserved estimate. Chain-owned facts cannot be replaced by an accounting
or observer exception; observers run once outside provider error normalization.
The turn and generative runner also attach process-local allowanceQuoteContext: the paused raw
construction and eligible plan, current input estimate, actual lowered tools, configured output
fallback and turn limit. The generative fork supplies only its primary and authored media volume.
The governor freezes scalar AllowanceQuoteResult onto BudgetPauseError/GateRequest; raw
requests, native options, cap plans, provider objects and closures never enter that quote.
AgentTurnError.recoverableOverflow is diagnostic, not recovery authority. Session recovery requires
private evidence joined by exact error identity to the current failed, invoked official provider
attempt, before any tool round or received content. Admission/credential faults and an earlier
capture cannot supply that evidence. It is not an event field; the session owns the once-only
compaction and fresh retry. A later tool-round overflow ends
the turn without redispatching tools. Generative media paths never acquire session recovery;
their overflow mapping also uses fixed facts and preserves custom-endpoint authority.
ADR-0097 sizes the
frozen amount as calls × eligible chain attempts × largest current E. Advertised tools give
maxToolTurns + 1 calls; no lowered tools or inline media gives one. Generative media has one
primary call/attempt and zero text tokens. Node retry does not multiply A; cooldown remains in
the eligible plan. Capability/streaming skips and unknown pricing are structured exclusions.
Non-strict partial media pricing retains its priced part; strict gaps are excluded/refused.
Priced zero, wholly unpriced and unrepresentable results are distinct. The full price/eligibility
comparison covers quantities, rates, tiers, route, calls, attempts, endpoint and every priced and
excluded candidate; equal A alone is insufficient, including at zero volume.
The trusted governor API activates an opaque, process-local dispatch token with a frozen safe amount and a host lifetime predicate. Tokens belong to one governor/node/dispatch; stale dispatch identities cannot replace newer owners. Evaluation, owner debit and global reservation have no callback or await between them. Owned calls debit under the cap too, including an unbounded configured cap. One in-flight slot prevents another call before settlement reconciles realized under-spend/overrun; proven release refunds, uncertain settlement retains. Global money changes before the settlement's host predicate runs. Late actuals remain global truth but cannot refund or revive a replacement owner. Lifetime is rechecked after pricing, durability/legacy holds, notices and warning awaits; failed warning writes release the admission.
A rejected unsafe actual text settlement retains safe E with its attempt attribution before propagating the accounting error, so the admission cannot remain orphaned in flight. Generative settlement carries the priced flag: unknown model/modality pricing retains E while publishing the existing unpriced result; a known zero reconciles as genuinely free. Post-provider pricing/outcome exceptions retain any still-owned admission. Already settled event-sink failures do not duplicate conservative spend; proven pre-egress key/cancel failures refund, and async jobs transfer their held admission. The engine's parked-job consumer applies the same distinction on done, failed, deadline and cancellation: pricing gaps retain E, and accounting faults finish the transferred admission before the poll backstop handles them. Its early exactly-once marker prevents callback reentry from billing twice. Terminal accounting guards each job independently so one fault cannot skip the remaining jobs or timer/lease/stream cleanup; cancellation and an earlier failure retain precedence. Conservative commitments remain separate from actual cost. The exact submitted admission is captured before fallible park-time clock/date work; until the parked record owns it, failure conserves E. The poll backstop similarly conserves an unsettled admission before removing its job. Park-time cancellation prevents registration after terminal cleanup, and timer installation disposes a handle if its callback has already cleared that job. Immediate cancellation and the early exactly-once marker remain, while terminal accounting joins any active cost fold before capturing the durable total.
Native generation prices the authored output volume through the existing rate-only
estimateMediaCost kernel rather than constructing provider-reported Usage. Image counts remain
non-negative safe integers; audio/video seconds may be fractional. The kernel rounds the charge
once to integer micro-cents and refuses malformed volume or unsafe cost before the engine folds it.
Both synchronous completion and parked-job settlement use this path; the integer Usage contract
and priced-versus-gap settlement above are unchanged.
A consumed positive allowance, a prospective E larger than the remainder, an unsafe E or a real
overdraw closes later admission with budget_exceeded; it cannot produce a new pause or retry.
An originally zero allowance still admits a genuinely free call and ordinary non-strict unpriced
usage until positive actual spend or forced exhaustion closes it. Unknown E has lifecycle ownership
without a fabricated global reservation; a later known actual debits that same owner. A known zero
has an explicit zero reservation and cannot manufacture a positive commitment event. Strict-cost
refusals are never overridden.
The optional NodeExecutor.prepareBudgetDispatch(NodePreparationContext) capability prepares the
actual next request without events, credentials, provider calls, tool dispatch, monetary admission or
host notices. Its context contains only the vertex, admitted inputs, completed outputs, resolved
context, secret names, tool policy, signal and configured output estimate. The standard runner and
node dispatcher provide it through the same prompt/tool/plan construction used by ordinary execution.
An executor without this capability can reject a frozen gate; it cannot approve its allowance.
A cross-process host may first compare the frozen quote's recorded priced quantities with current rates before secret input, credential-resolver construction or MCP connection. This price-only refusal uses the same rate comparison as full validation; a match grants no dispatch authority. The CLI reads existing catalog/overlay rows without seeding them for this check and reuses the resume command's database connection when constructing its resolvers. The engine still performs the complete current request, eligibility, exclusion and price comparison described below.
A successful preparation retains ephemeral quote(...) and execute(ctx) capabilities. Text retains
the actual request and candidate-specific cap plans; generative media retains its primary request and
authored volume. The current price overlay is read at validation. The full quote must match the frozen
scalar evidence before claiming the gate, and the prepared request is used for the first approved
attempt so a second prompt/file read cannot replace the request just approved. Generation notices
remain deferred until actual execution. Raw options, provider instances and closures stay process-local.
Aggregate pause waits for all in-flight gate pause publications, including each budget authority and its companions, before handing back a waiting-only run's lease. Parallel budget gates and ordinary human siblings therefore cannot reacquire and retain ownership after aggregate handoff. A claimed gate keeps the lease while its decision work is in flight. An ordinary human gate also stays visible while its payload is pinned, until its vertex can be completed; that preparation is not a stalled run. Media-job polling keeps its existing ownership.
The engine observes durable authority and companion acknowledgements before handing the approved
safe amount to a new dispatch token. The pre-egress hook remains installed on every attempt,
fallback, tool round and above-chain node retry. One dispatch token spans that node's retry/backoff
lifetime and closes when it completes, fails, parks, is cancelled, abandoned or loses ownership.
Exhaustion is fatal budget_exceeded; it neither retries nor creates a fresh budget pause.
A cancellation request for a known cross-process resume also covers passive context, quote and effect admission before execution registration. It carries the preparation signal into the retained request builder, latches cancellation without scheduling or arming a grace timer, and renews the exact acquired fence before settling a cancelled outcome. A successor or unconfirmable owner receives no terminal from the abandoned preparation. Command signals continue to the active execution and the returned handle; a late acknowledged decision remains durable and cannot grant post-cancellation egress.
The Node CLI host yields across a poll turn after a successful decision append, before exposing its acknowledgement to the engine, so an OS signal queued during native SQLite contention can reach this cancellation path before dispatch. The row is already committed and is not undone. This host-specific scheduling belongs outside Core. A delivered terminal with uncertain durability uses the terminal-outbox outcome, not the nonterminal ownership-refusal path; the canonical CLI exit codes distinguish them.
Cross-process amount/kind/request validation occurs before execution registration. Constructor seeding
is passive; context and effect-journal admission precede timers, media polling and dispatch. After an
asynchronous preflight, the engine rechecks the whole current quote. It then atomically renews
the exact acquired owner/generation fence before any registration or activation. This check also
precedes a refused admission's settlement. A takeover or unconfirmable lease causes a
transient, safe run_owned_elsewhere refusal before registration, timers, key resolution or
polling; cleanup releases only the original fence. An expired claim with no successor renews
without minting another generation. Renewal confirms ownership at the store operation. An
unbounded asynchronous host ACK can outlive that renewal's TTL before activation; it is not a
universal freshness guarantee. Later loss is observed through the existing liveness and write
fences. The local SQLite lease operation is synchronous. Every pre-activation refusal and already-terminal no-op
attempts to release its exact acquired fence. A rejected cleanup preserves the primary typed
refusal or closed handle and leaves the existing bounded TTL; it cannot release a successor
or surface private cleanup text. A supplied target decision is
applied before its timer could be armed; surviving gates keep their absolute remaining deadlines.
Effect refusal retains priority over authorization. A reconstructed approval does not restore a
token or pretend the agent completed: an eligible kick re-runs under the current budget checks.
Legacy approval likewise grants no allowance. The durable protocol and join rules have one home in
sse-event-schema.md.
W7 status, 2026-10-04: Step 10's workflow approval protocol is accepted after eight independent review rounds. Step 11's safe CLI surface is accepted after three independent review rounds; automatic session pre-send/summary handoff remains Step 8. See the W7 execution plan.
The earlier dated W7 status is historical. Measured request reuse, atomic multi-pass compaction, pre-send and single pre-content overflow recovery, finite frozen allowances and the CLI resume surface are implemented with scoped independent acceptance. The closing register tracks W7's final acceptance and approved limits. The canonical roadmap owns subsequent W8 status.
Every potentially billable token invocation acknowledges pending usage through the money port before it invokes the provider, with or without a cost cap. The single evidence value can be complete, partial or missing. Only complete quantities enter actual pricing; partial/missing evidence remains observable and becomes explicit unknown coverage. Proven non-invocation and official pre-content refusal resolve possibility without inventing a charge. Correlation has no budget authority. Exact retained late receipts can refine uncertainty once under their original fence; host closure joins those writes. The LLM seam owns the evidence contract and the event contract owns coverage transitions.
Workflow checkpoint reconstruction preserves explicitly unpriced historical attempt rows even when they predate the cumulative missing-price envelope. Reconcile their count with absolute envelope snapshots without adding already included observations twice; separate-endpoint native media can supply a snapshot without a token-attempt row. Resume carries this known price gap into the fresh live reader independently of legacy/unverified usage coverage. Neither kind of uncertainty changes the known actual-money sum or fabricates historical calls.
An exact late complete receipt refines usage coverage and records realized money once. It does not implicitly release an earlier retained conservative commitment; the existing explicit estimate-release policy remains separate from actual receipt accounting.
Historical receipts retain their original within-chain diagnostic attempt and refine only their own local unknown-call contribution once. They cannot change the current response's model, overflow or terminal-observation ownership. A complete receipt joined before outcome construction can refine that outcome's usage coverage; a later receipt cannot mutate the already published result. Exact ephemeral pre-attempt metadata retires at the final receipt, without a completed-ID history.
Every accounting port entry requires its producer's active receipt scope, even when it carries a previously issued invocation identity. The runner explicitly transfers a dedicated accounting child before entering provider work. That child joins the exact registered producer and receipt promises; it does not extend the original executor's ports or borrow an unrelated sibling's lifetime. Late cleanup settles only the admission associated with its exact ephemeral pre-attempt identity.
TurnMoneyPort is a behavioral interface. Its optional capabilities retain their presence and
the original receiver through turn capture, including class/prototype and non-enumerable method
implementations. A decorator must not silently turn a persisted host into a volatile host by
dropping beginUsageInvocation, claimUsage, usageDisposition, accountingFailure or emitCost.
The pending/claim/disposition triple is one all-or-none coverage feature: an incomplete set is
refused with a fixed AgentTurnError (internal, non-retryable) before key resolution, pending
allocation or provider entry. A port supplying none retains volatile accounting. The independent
accountingFailure and emitCost capabilities remain optional. MoneyDurability similarly
requires both pending and disposition callbacks, or neither, at construction.
Post-provider claimUsage, usageDisposition, record and optional emitCost retain the
observer-failure origin, including native predecessor notifications. An observer's throwable
keeps its exact identity but cannot authorize a budget pause, paid retry or arbitrary provider
diagnosis. Physical engagement, complete observed quantities and exact live late-receipt metadata
are captured before these callbacks can throw. Observation alone grants no realized-write
authority. An explicit false claim restores only that callback's provisional observation and
receipt contribution; it neither erases prior observations nor permits a duplicate charge.
Mandatory pending admission and the join barrier retain their existing precedence; inert
request data restrictions do not restrict implementations of this host port.
Conservative admission cleanup preserves an established exact primary throwable, including an opaque or falsy value. This applies both when actual settlement falls back to retaining the estimate and when turn teardown retries an outstanding obligation. Secondary cleanup failures are retained through the existing accounting-health port when attached; a failing health callback cannot replace the primary. A cleanup failure with no earlier primary remains a failure. None of these paths authorizes another provider call, releases an uncertain admission or fabricates a successful settlement. The supported host admission callback is the failure boundary; a distinct second synchronous cleanup error is not claimed for the current built-in governor.
Accepted implementation — 2026-10-16. Step 3 implements ADR-0113. The acceptance register joins two NEW complete cumulative clean reviews, the separate clean security sitting and exact-source evidence; whole Wave 3 remains open. Exact projection, public-template refusal, VM/input compatibility, result transport and finite observed-access semantics belong to the expression specification.
WorkflowEngine immediately qualifies trusted raw resolveContext output at fresh start
and resume, before storing context or invoking execution/preparation callbacks. Context is
re-resolved on resume because it is not checkpointed. The admitted workflow supplies one
owned provenance authority shared by execution and budgetPreparationContext. Standalone
cross-process budget preparation creates that authority, qualifies the resolved context and
passes both through resumePreparation.context / resumePreparation.provenance; the resumed
execution reuses them rather than deriving a second mutable secret-name set.
Context analysis and resolution use the authority's same owned definition snapshot, so caller
edits after admission or during an awaited context capability cannot change the analysed source.
Qualification refuses a resolved key absent from that owned declaration snapshot before reading
its value; an undeclared key never acquires an implicit clean classification.
NodeExecContext and NodePreparationContext require the same context projection and owned
secretInputNames membership; the preparation Pick includes both. Agent prompt resolution
and human-gate message_template / assignee resolution use resolvePublicTemplate.
Budget preparation uses the same prompt consumer, without a VM-only metadata field. Denied
context and direct declared secret-input references refuse before any filters, defaults or
read capabilities, including on hand-constructed workflows that bypass the parser. Clean
fields retain their existing resolution and user-role placement. Authored system text is
still concatenated verbatim, not interpolated.
An absent public template still resolves the empty default through the provenance boundary;
it does not waive required context or secret-input membership metadata. Optional assignee
presence and existing cancellation/preparation outcomes remain unchanged.
Condition/transform/custom-merge handlers use buildExpressionScope with the required
projection and explicit denied input names. Direct supplied secret inputs become VM-owned
denied accessors; omitted optional inputs remain absent. maskSecretInputs retains its
separate run-start/input-node/display marker duty. The exported raw resolver and trusted raw
NodeExecContext.inputs remain host APIs; this is not a confidentiality boundary against a
custom trusted executor or a host that fabricates provenance.
Historical persisted output and private error data retain the at-rest/recovery limit. Optional external tool-argument provenance has its separate canonical boundary.