Runtime engine
Status: Draft
1. One interpreter
The framework has one semantic Run interpreter. Convenience methods reduce the same event stream:
AgentRuntime.stream(agentBinding, input): Stream<RunEvent, RunFailure, R>
AgentRuntime.run(agentBinding, input): Effect<AgentResult, RunFailure, R | Scope>2
run MUST NOT implement a separate loop. Golden tests compare its result to reducing stream.
2. Run lifecycle
Ephemeral lifecycle:
created → preparing → running → completed | failed | interrupted | suspendedDurable lifecycle adds Submission and Attempt state described in the durability specification.
One Run owns:
- decoded input;
- Agent Binding, its Agent Definition, and version digest;
- Conversation projection;
- current Turn state;
- cumulative budgets and usage;
- owned Scope;
- semantic event sequence;
- steering/follow-up queues;
- child fibers and capabilities.
3. Turn state machine
Normative sequence:
- Decode Run input.
- Evaluate instructions and initial context.
- Prepare Turn Plan.
- Check budgets and abort/interruption.
- Materialize the Effect AI Prompt and model request, including the model-visible final-output contract (RUN-028).
- Stream Effect AI Response parts.
- Reduce them into one complete assistant response.
- Reject malformed or incomplete provider sequences.
- If the assistant response is final, decode Agent output.
- Otherwise validate the complete Tool Call batch.
- Apply approval policy.
- Re-evaluate optional host Tool authorization at the action boundary.
- Apply scheduling policy and execute Tool Handlers.
- Encode outcomes and construct deterministic Tool result messages.
- Advance the Conversation.
- Drain steering commands.
- Evaluate Stop Policy.
- If otherwise complete, drain follow-ups.
- Begin another Turn or emit one terminal Run event.
The transition reducer should be pure wherever possible. Effects interpret decisions; they do not hide transition rules.
Stop Policy evaluation (step 16) enforces the finite Agent Policy at the Turn seam, before the next model request. Turn and Tool Call limits bound the work itself: work that would exceed them never starts. How exhaustion resolves is policy-selected via onExhaustion:
"fail"fails the Run typed (AgentPolicyError) at the seam before any declared application Handler starts."final-answer"(the default) gives the model one constrained settlement opportunity. An over-budget declared Tool batch never executes a handler and is never durably declared as a pending batch; every open call settles synthetically as a model-visible failed result carrying the encoded policy failure, exempt from repeated-failure folding. Every subsequent model request either forbids tool use (toolChoice: "none") or advertises only the Definition-owned completion Tool. Turn exhaustion admits exactly one grace Turn pastmaxTurnsunder the same constraint. A Run that settles this way completes withfinishReason: "budget-exhausted"(RUN-011), and a model that declares a Tool Call under the constraint fails the Run typed (ModelProtocolError, RUN-020).
A Run may carry tightening-only allowances (toolCallAllowance, turnAllowance, RUN-021): the effective limit is min(policy bound, max(1, floor(allowance))), so an allowance can never widen the Definition's ceiling, and the onExhaustion resolution keys off the effective limits. This is the budget-extension seam: an orchestrator grants a delegated child more budget by re-invoking the delegation with a larger allowance below the child Definition's policy.
Duration, cost, and hierarchical budget-hook bounds are hard limits regardless of onExhaustion. The token dimension originally shipped as a hard limit and now participates in the onExhaustion resolution with a one-shot bound (RUN-025; at most one grace call). Repeated-failure enforcement is Run-level: each completed Turn's terminal Tool Call outcomes fold into one consecutive-failure counter in declaration order, any terminal Tool Call success resets it, and reaching repeatedFailureLimit fails the Run with the typed policy failure (limit: "repeated-failures"). A repeatedFailureLimit of 0 disables the bound. Budget-rejected synthetic settlements neither advance nor reset that counter.
An Agent Definition may designate one Toolkit member as its completion Tool and provide a pure, typed projector from the Tool's decoded parameters and successful result to the Agent output (RUN-032). The completion call must be the only call in its batch. After authorization and the ordinary durable Tool protocol succeed, the engine Schema-validates the projected output and completes immediately: it does not ask the model for a private summary. A completion batch may run after a token, Turn, or Tool-call limit is crossed under onExhaustion: "final-answer", and its completion preserves the honest budget-exhausted metadata. Fail mode rejects the batch before its Handler starts. Mixed batches fail before any Handler starts.
maxDuration bounds wall clock for one logical Run, not one process Attempt and not cumulative worker-active time. In the durable assemblies the coordinator appends one canonical RunStarted record before the first engine execution. Its envelope timestamp is the logical start and its payload fixes the Run ID and admitted duration allowance. Admission, queue delay, and binding-free input recovery precede the Run clock, while process loss, recovery gaps, approval suspension, unknown-outcome suspension, and waitingForChild suspension do not reset or pause it. The coordinator derives one absolute deadline from that timestamp plus the stored allowance and supplies it to every replacement Attempt; the engine accepts only a deadline that preserves or tightens its fresh policy allowance (RUN-030). An ordinary replacement Attempt whose deadline is already expired fails before subscribing to unresolved Tool or model execution; deadline interruption then handles only future expiry. The coordinator also supplies the RunStarted record timestamp separately from the deadline, so elapsed status reports actual wall-clock age when a deadline is tightened or an Attempt is replaced. Before invoking an expired Run, the coordinator may use its canonical attachment and Settlement evidence to commit mandatory settled-child joins and release their reservations. That cleanup happens outside the interpreter. The engine has no past-deadline execution capability and always blocks ordinary Tools, new children, and model continuation after expiry (SUB-019).
Note on durable Attempts: the batch-resume seam counts Tool Calls from the resumed batch onward, so maxToolCalls is enforced per Attempt under the durable coordinator. This is existing, documented behavior; cumulative cross-Attempt accounting would require persisted counters and is deliberately out of scope for RUN-018.
4. Effect AI response reduction
The reducer accepts Effect AI Response.StreamPart values. It validates:
- exactly one response start and completion;
- monotonic part identities;
- Tool Call ID uniqueness;
- complete Tool arguments before declaration;
- usage monotonicity/consistency;
- provider-declared stop reason compatibility;
- no content after terminal completion.
Malformed output remains an Effect AI error or becomes a framework protocol failure when it violates an Agent-loop invariant. It is not silently repaired.
In ephemeral mode, text/reasoning deltas are live events. In durable mode, returned text/reasoning deltas may be appended canonically, while partial Tool arguments remain live-only. Only a completed response may execute Tools or enter the next Effect AI Prompt.
5. Tool Call processing
Preflight
Before any Handler starts:
- the assistant message must be complete;
- stop reason must permit Tool execution;
- all Tool names must resolve;
- every parameter value must decode;
- budgets and authorization must pass;
- approval decisions must be known or represented as suspension.
A length-truncated response never executes Tool Calls.
Host action authorization
RunOptions.toolAuthorization, when present, runs for every still-executable model-declared call in an application Tool batch after complete-batch validation and approval, but before durable preparation, semaphore acquisition, or any Handler. Every decision in the batch completes in declaration order before the first Handler may start. The request carries canonical ConversationId, RunId, TurnId, Turn number, the Agent-Schema encoded Run input, the selected call's stable ID/name/encoded parameters/execution class. A durable host receives the exact admitted canonical Submission input. Policy remains host-owned; the library assigns no mutation meaning to Tool names. Programmatic ToolBroker calls are outside this hook.
A durable batch resume reconstructs input authority from the canonical Submission and calls from the pending response record, then invokes the same hook again with the same Run, Turn, input, and call identity. Already-settled calls are omitted because no Handler can start for them. A denied decision emits ToolCallFailed and fails the Run with AgentToolAuthorizationDenied before the denied Attempt starts any Handler or creates a new side effect. A fresh denial precedes preparation; a resumed denial may retain the prior Attempt's prepared record and historical effects but writes no new preparation. DN/DC then record the ordinary bounded failed Submission settlement, which is terminal and cannot select the call for another retry. A hook failure remains typed in the Run error channel and is likewise fail-closed.
Before that resume can start input hooks, Handlers, or model execution, the engine Schema-decodes the prior-Attempt usage seed and every settled-call envelope. Settled results must be bounded canonical JSON and are copied into history only from an owned snapshot. Usage counts and microdollars must be non-negative safe integers, and each last-call token count must not exceed its cumulative total. Invalid recovery input fails with ModelProtocolError; the engine neither repairs it nor continues with weaker budget accounting.
Scheduling
The default is bounded parallel execution owned by the engine.
- the complete batch passes preflight before any Handler starts;
- the engine requests unresolved Effect AI Tool Calls;
- Effect AI Toolkit handlers retain their native validation, success, failure, and requirements;
- the engine wraps Handler effects in Effect
Semaphorepermits and runs them as scoped child fibers; - per-run concurrency is finite, and outer global, Agent, or Tool-group semaphores may impose stricter limits;
- a Run or Tool may require sequential execution;
- calls start in declaration order where scheduling permits;
- progress and completion may be observed in actual completion order;
- final Tool results commit in original declaration order;
- the next model request never sees a partial Tool batch.
Completion
When a declared Tool Call settles, its call-level terminal classification is:
- success;
- failure returned as a terminal Tool value;
- typed failure in the Effect error channel;
- infrastructure failure;
- denied;
- interrupted.
The DN and DC assemblies add Unknown Outcome. Approval suspension and unresolved Tool Calls are nonterminal and may remain unresolved indefinitely; the runtime does not replay an ordinary unresolved call merely to force settlement. Denial happens during preflight, before any application Handler starts. Provider-executed calls likewise have no application-handler attempt.
An in-memory application-handler attempt therefore starts only for a native call that passed preflight. Its complete lifecycle classification set is:
- A successful attempt is terminal.
- A failed attempt is terminal. Failures include returned failures, typed or infrastructure Causes, and post-terminal anomalies.
- An interrupted attempt is terminal and remains classified as interrupted.
- A waiting attempt is nonterminal and may last indefinitely.
Only success and failure receive a bounded terminal telemetry outcome. Interruption produces no terminal outcome log. Denied and provider-executed calls remain solely call-level classifications: neither creates an application-handler attempt.
Tool failure observation
Invocation kind identifies how an application Tool is called: model for a model-declared call, or programmatic for an inner ToolBroker invocation. It is independent of execution class, whose values are readonly, idempotent, and uncertain. Unannotated Tools default to uncertain; this observation interface grants no new replay authority.
CurrentToolFailureObserver is one engine-owned, default-none Context.Reference. toolFailureObserverLayer installs a closed RunToolFailureObserver, whose observe method returns Effect<void> with no typed error or requirements. Applications capture their reporting dependencies before installation. There is no RunOptions installation path or precedence rule. With no observer installed, outcomes, events, canonical records, logs, and spans remain unchanged.
The engine captures the reference once per Run and passes that value to direct Tool execution and the broker. DurableAgentRuntime captures it once at Layer acquisition and explicitly re-provides that value, including absence, around each interpreter invocation. Caller context cannot replace the captured host choice. Node and Cloudflare accept the same closed toolFailureObserver option. Omitting that platform option explicitly provides absence, masking an observer in the surrounding Layer-acquisition context. Direct users of DurableAgentRuntime.layer install the engine Layer at acquisition instead. Adapter certification uses the default-none observer.
Observations are plain readonly interfaces, deliberately not persisted or transported Schemas. Every observation carries agentId, conversationId, runId, turnId, toolName, kind, and tag. Identity fields are raw, unconditionally. The telemetry ID filter does not apply to this trusted local service. Fresh provider responses still pass the ordinary protocol ID validation.
| Variant | Extra identity | Execution class | Allowed kinds |
|---|---|---|---|
ModelToolFailure | Raw toolCallId | Always | declared-failure |
ProgrammaticToolFailure | toolCallId = parentToolCallId + "#" + sequenceIndex, raw parentToolCallId, sequenceIndex | Always | All four kinds |
ProgrammaticPreflightFailure | Raw parentToolCallId only | Only when the Tool resolved | infrastructure, protocol |
Presence of sequenceIndex means the Handler started, budget was consumed, and side effects may exist. A preflight observation has neither an inner Tool Call ID nor an index. The engine never fabricates either.
The following taxonomy is normative (RUN-036).
| Source | Variant | Kind | Cause |
|---|---|---|---|
Direct returned failure, commitTerminalResult failure branch | Model | declared-failure | Never |
ProgrammaticCallFailure after a started handler | Programmatic | declared-failure | Never |
| Started handler's typed error | Programmatic | handler-error | Always, original and uncollapsed |
| Result byte-bound rejection | Programmatic | infrastructure | No |
| JSON-surface / missing / duplicate terminal result | Programmatic | protocol | No |
| Budget-hook typed failure | Preflight | infrastructure | Yes, captured before projection |
| Unknown Tool, approval-unsupported, policy limit, call already in flight, pass retained past the outer call | Preflight | infrastructure | No |
Invalid encoded arguments, SchemaError | Preflight | protocol | Yes |
tag uses the existing bounded error-tag projection, with UnknownError as fallback. Only infrastructure and protocol carry message, bounded to 4096 UTF-8 bytes without splitting a code point. handler-error carries no message. A Cause-backed observation retains the exact Effect Cause held by the engine, with its Reasons and annotations; source-less outcomes never acquire a fabricated Cause. The public types encode these guarantees through kind: handler errors require cause, infrastructure/protocol observations require message, and declared failures allow neither.
Declared failures expose only their tag, never their message, Cause, decoded result, or encoded payload. For Code Mode this follows CAP-016. For direct Tools it is a deliberate uniform policy, not a claim that their declared values are secret: those values already belong to the ordinary model-visible Tool result. The observer is not an implicit Code Mode output channel.
Raw Causes are sensitive live diagnostics. The engine MUST NOT automatically serialize, persist, log, attach them to spans or public events, or send them to the model. Only the explicitly installed observer receives them; the application owns any further reporting and redaction policy (SEC-008).
Direct observation runs after the failure event and in-memory result are authoritative, before ordinary continuation. Telemetry and observation share one delivery owner across the next-pull and early-close race. Delivery runs inline under that call's own batch permit, so a slow observer occupies one toolConcurrency slot. Started broker calls use the outer call's already-held permit. Their outcome and terminal telemetry are fixed before delivery, which finishes before pass.invoke returns. The original Cause is retained before the broker's diagnostic projection. Preflight rejections also deliver inline. They share one reporting Semaphore permit per broker, across all its passes, including passes retained after close. This bounds simultaneous reporting even though rejections consume no execution budget. Waiting callers remain interruptible in their own structured fibers and acquire no additional Tool-execution permit. There is no observation queue, background fiber, or daemon.
Observer defects go to ErrorReporter; reporter defects are isolated too. Neither may replace the authoritative Tool result or alter the Run outcome. External interruption of observation or reporting remains interruption and may stop the Run, but never rolls back or reclassifies an already-authoritative Tool terminal value. Observation is independent of Logger/Tracer telemetry. An observer MUST NOT reenter ToolBroker, emit through RunEventSink, start nested Agent execution, or intentionally self-interrupt. This is a documented application contract, not a type-enforced capability restriction.
Delivery is at most once per in-memory attempt. An interrupted delivery is never retried in that attempt. Across replacement Attempts one logical call may be observed zero, one, or multiple times. Synthetic inner IDs collide across Attempts because the sequence counter restarts, so IDs alone cannot deduplicate incidents. Inner and outer Code Mode observations correlate through parentToolCallId; the engine does not infer causal attribution or incident deduplication.
No observation is a durable fact. There is no happened-before guarantee with a later canonical record commit, no journal or ledger field, no checkpoint data, and no observation replay. This adds no durable mutation or mutation failpoint and makes no exactly-once delivery claim.
The following exclusions are normative; none fires an observation.
- Direct typed handler errors, defects, and direct protocol errors propagate to the ordinary Run failure boundary with their Cause. Observing them again would duplicate that channel.
- Inner programmatic defects and interruption escape
pass.invokethrough the outer call. They are nothandler-errorobservations. - Direct approval/authorization denial and direct preflight failure start no application attempt.
- Synthetic budget rejection starts no application attempt even though it produces a Tool result.
- Interruption, waiting, and provider-executed results have no eligible application failure.
- Resume-injected settled calls run no Handler and possess no live Cause.
openPasserrors,ToolBrokerUnavailableErrorandToolBrokerConfigurationError, describe zero inner calls. They may be visible only through the outer declared failure Code Mode returns.
Provider-executed Tools
A provider-hosted built-in Tool does not run an application Handler. Its complete Effect AI call and result parts retain providerExecuted: true; the runtime emits declaration and terminal events with that provenance, but no ToolCallStarted event. These calls still consume Tool Call and Turn budgets. When another Turn is required, provider results remain assistant content rather than becoming application Tool output messages. Their authorization, recovery, and source-trust limits belong to the explicit provider capability that enabled them.
A response containing only provider-executed calls may finish with stop and final text in the same Turn, because no application Handler remains unresolved. A response containing any application Tool Call still requires the Tool-compatible finish path and another Turn.
6. Steering and follow-ups
Steering and follow-up are typed Run commands delivered through Effect queues.
- Steering is drained at Run start and after a complete assistant/Tool Turn, before the next model request.
- Steering never cancels an in-flight response or skips its active Tool Calls.
- Follow-up is drained only when there are no pending Tool Calls or steering messages and the Run would otherwise complete.
- Queue drain policy is
oneorall, withoneas the initial default. - Durable input delivery uses claimed
joiningandjoinedSubmissions at the same safe boundaries.
7. Interruption and abort
Ephemeral interruption
Interrupting the consumer fiber closes the Run Scope:
- stop starting new work;
- interrupt the Effect AI Response stream;
- interrupt active Tool/Subagent fibers;
- run finalizers;
- emit no false success;
- preserve any already materialized result available to the caller.
Durable abort
Durable abort is a persisted intent. Interrupting a local await or observation fiber only detaches that caller. It does not abort accepted work. A separate runtime operation requests durable abort.
This distinction must be visible in naming and documentation.
8. Retry policy
Retries are classified and local:
- Model transient failure may retry before a canonical terminal response.
- A provider context-length rejection is classified into a typed overflow. With a configured
contextTokenLimitthe engine compacts (§9) and issues at most one framework-level retry (transport ambiguity may still duplicate the external model execution); otherwise, or on a second overflow, the Run fails withContextOverflowErrorrather than an opaque provider error. - Tool handler failures do not automatically retry.
- Tool infrastructure retry is opt-in and must respect idempotency/durability annotations.
- Output encoding failure never retries as an external Tool call.
- Defects are not retried by default.
Effect's retry operator is an implementation mechanism, not the product policy. Retrying an effectful Tool without durable evidence is forbidden.
9. Context and compaction
Context preparation:
- Reconstruct canonical/ephemeral Conversation Messages and append stable instructions/input.
- On a durable resume, replace the re-evaluated history prefix with the canonical run-journal projection.
- Apply the optional host
RunContextPreparationservice to the resulting immutable source. - Calculate window/budget over that model-visible result.
- Compact with the engine-native policy if required.
- Append derived run status and the final-output contract and produce normalized Model Input.
Official prior history remains canonical and append-only. Each first response marks the leading messages contributed only by that Run's evaluated instructions and wake input. Recovery of the owning Run retains that prefix, while every later Run omits it from model input and keeps the assistant/tool conversational remainder (RUN-033). Records without provenance retain their full legacy projection. Current instructions and decoded input append after the projected prior history; no projection rewrites or reorders canonical records.
RunContextPreparation is a generic Effect service, not a Conversation store. Its optional prompt hook receives stable Run/Turn identities, the immutable source Prompt, and the rendered output-contract text when available; it returns only the prompt for the next model call. Its optional toolAuthorization hook is the closed durable-host form of the action boundary in §5. An absent hook is the original pass-through behavior. DurableAgentRuntime.layerWithContext exposes the service in its Layer requirement; DurableAgentRuntime.layer explicitly supplies RunContextPreparationPassthrough for compatibility. Coordinators always apply journal reconstruction before a prompt hook and rebuild action-authorization authority from canonical records before a resumed Handler, so an ownership retry or a new host incarnation cannot bypass either boundary. The returned prompt is never assigned to official history and cannot enter canonical records.
Window and budget calculation
Step 4 is implemented from policy plus observed usage. The engine tracks the most recent model call's provider-reported input and output tokens as the live-context estimate, and estimates the next call's context as that value plus a chars/4 approximation of parts appended since (the whole prompt on the first call). Before provider I/O it also accounts for the derived run-status and output-contract messages and withholds AgentPolicy.completionReserveTokens from research. The admission condition is therefore estimated next call + completion reserve <= remaining token budget (RUN-034). If it does not hold, the engine compacts or enters final-answer mode before the research call; onExhaustion: "fail" fails typed at that seam. Cache-read and cache-write input tokens remain distinct but both count toward gross token use. AgentPolicy.contextTokenLimit, when present, bounds one call's live context; tokenBudget remains the cumulative runaway stop; spend belongs to costBudgetMicrousd.
Tool result bounds
Every application Tool result, including MCP results, is bounded once at the settle boundary by AgentPolicy.toolResultBounds (default 50 KiB) before it enters records or prompts, so both carry the same value. An oversized encoded result becomes the canonical TruncatedToolResult envelope preserving head, tail, and original byte size. A durable resume rechecks the recorded settled result against the same bound in the DN and DC assemblies and rejects non-JSON or hostile values instead of changing them. Provider-executed results are exempt.
Run-status message
With AgentPolicy.runStatus: "appended" (the default), each outgoing model request ends with a derived run-status message showing Turns, Tool Calls, tokens against budget, last-call context, and elapsed time, with a wrap-up warning once any tracked dimension reaches 80%. The message is derived at prompt-assembly time and is never persisted as canonical history.
Compaction
Step 5 runs at the pre-Turn seam, synchronously, when either the next context exceeds contextTokenLimit or estimated next call + completion reserve exceeds remaining tokenBudget, per AgentPolicy.compaction:
- Prune (
clear-tool-results): application Tool results older than the protectedkeepRecentTokenstail are replaced with"[tool result cleared by compaction]", preserving message structure and call/result pairing. - Summarize, if still over and the mode allows. One metered model call on the Run's bound model summarizes the goal, constraints, progress, decisions, next steps, and critical context. The call's usage counts like any other. The rebuilt prompt is the instruction prefix, the summary message, and the kept tail.
Cut points never split an assistant Tool call from its result, and prepared-unsettled Tool records are always in the kept tail. In the DN and DC assemblies each compaction appends a canonical CompactionCreated record (kind, coversThrough, optional summary) inside the epoch-fenced log it covers. The run-journal projection folds it, and covered records render as the summary or with cleared Tool results. The projection ignores an invalid range and keeps the full history authoritative. The session selects coversThrough itself, walks its own records with the shared estimator, and limits coverage to prior Runs. The owning Run's records are never covered, because its first response record carries the evaluated instructions and input. The engine's in-memory rebuild is therefore a view that may cover more than the record; the record is canonical. A threshold compaction with no prior-Run records to cover commits no record and applies only in-view. After pruning or summarizing, the engine re-estimates the complete outgoing prompt. If the target remains unreachable it fails locally with ContextBudgetError and never sends a request it already knows cannot fit (RUN-034). Compaction appends or emits a summary representation and preserves the source history; failed compaction leaves the original history authoritative. Host-supplied, digest-bound compaction artifacts remain available through the capabilities layer (capabilities §6). They shape each model request through RunContextPreparation; unlike CompactionCreated, the supplied artifact itself is not canonical and is recomputed after restart from the canonical source.
Budget warnings and the token soft landing
Crossing 80% of a configured budget dimension emits a BudgetWarning Run Event once per dimension (RUN-025). Turn and Tool Call exhaustion resolve through the Stop Policy's onExhaustion machinery in section 3 and RUN-018 through RUN-020. Token exhaustion is a pre-call reserve and post-response gross check, and joins the same resolution. Under the default "final-answer", a breaching response that already carries a decodable final answer at a stop completes the Run directly with that answer and no extra call, and otherwise the Run takes at most one constrained grace Turn (toolChoice: "none", or only the Definition-owned completion Tool; its usage is consumed once and exempt from re-triggering breach). Either way the Run settles as RunCompleted with finishReason: "budget-exhausted" and exhausted: "tokens". A grace-Turn response that declares any Tool call other than the permitted singleton completion Tool fails typed (ModelProtocolError, RUN-020). Under onExhaustion: "fail", a token breach remains fatal and no declared application Handler starts. A maxDuration breach always fails because a grace call would extend wall clock past the contract.
After these steps the engine appends the model-visible final-output contract to the produced Model Input (RUN-028): one framework-owned system message carrying the JSON Schema derived from the Agent's output Schema, inserted immediately after the request's last system message and never entered into official history. Context transforms receive the exact contract text (RunContextRequest.outputContract) so a limit-targeting adapter can reserve its overhead, and no transform can remove or alter it. A Definition whose output Schema cannot render to JSON Schema produces no contract. The field is absent and the request is unchanged, with one Turn-1 diagnostic per Attempt.
When a Definition designates a completion Tool, the directive distinguishes that Tool's parameter Schema from the private Agent output Schema. A model calling the completion Tool follows its parameter Schema and never places the private output JSON in a Tool argument; the engine projects the successful Tool result into the Agent output. A final assistant response without that Tool call must still be JSON conforming to the Agent output Schema. The output Schema therefore remains in the request even for Definitions with a completion Tool. Definitions without a completion Tool retain the original final-text directive. This guidance changes neither Tool execution nor canonical history.
10. Event interface
Every semantic Run Event carries:
- event version;
- Run ID;
- Conversation ID;
- monotonic sequence;
- timestamp from Effect Clock;
- Agent ID;
- optional Turn ID;
- optional Tool Call ID;
- typed payload.
ToolCallDeclared carries the complete JSON parameters and whether execution belongs to the provider. Tool progress and terminal events repeat that execution provenance so transports and UIs do not infer an application Handler where none ran.
Terminal events are exactly one of:
RunCompleted;RunFailed;RunInterrupted;RunSuspended.
An Agent Definition may declare an application run-disposition Schema plus a pure selector from decoded output. At an ordinary completion seam the engine selects, Schema-encodes, and JSON-checks that value before adding it to RunCompleted.runDisposition; undefined means absent. Invalid selection fails typed with AgentRunDispositionError. A final-answer budget completion never evaluates or carries the selector result. Reducers fail closed if a budget-completed event carries a disposition or if an event carries one without a Definition-owned Schema. No runtime path parses output prose or infers disposition from Tool events. When the application selector throws, the typed error retains the original value in its Schema-safe diagnostic cause; the terminal event uses a fixed non-sensitive message and never serializes that foreign cause. The public AgentResultSchema independently rejects a disposition on finishReason: "budget-exhausted", so untrusted serialized results cannot bypass the event-reducer invariant. Durable recovery re-applies the selector and Schema to the reconstructed output and rejects a completion marker whose encoded disposition differs or appears where the live seam would omit it.
Raw provider chunks are never mixed into the stable event union.
RunFailed covers expected failures. The engine keeps defects as defects: a defect fails the event stream with its full Cause and is never converted into a typed failure or a successful stream end. Host boundaries that forward Run Events to a UI or transport may opt in to the exported withTerminalDefectEvent combinator, whose contract is:
- typed failures and interruptions pass through untouched because their terminal event was already emitted;
- a cause carrying a defect first appends one bounded terminal
RunFailed { errorTag: "Defect" }with a bounded string rendering rather than the raw defect value, then rethrows the original cause unchanged; - identity fields come from the last event already streamed; a defect before the first event is rethrown without an event, because the helper never fabricates Run identities.
In the DN and DC assemblies, engine-owned Durable Step and durable-Subagent infrastructure errors use fixed, bounded public messages. A mapped error preserves the foreign hook failure in its Schema-safe diagnostic cause for the live Effect, while Tool and Run events retain only the fixed message. Reporting that diagnostic is a composition-edge decision; the engine does not send it to ambient error reporters.
11. Backpressure
Local stream uses a bounded queue:
- default strategy: suspend producer at semantic event seams;
- text delta coalescing MAY reduce volume;
- semantic terminal events are never dropped;
- disconnecting/interrupting the sole ephemeral consumer interrupts the Run unless execution was explicitly detached.
Every Run also has tightening-only RunOptions.bufferLimits. Engine defaults cap one model call at 16,384 decoded response parts and an 8 MiB conservative retained-memory estimate, one Run at 65,536 semantic events including a reserved terminal slot, and one Tool batch at 1,024 queued Subagent events. Non-finite or wider overrides retain the engine ceiling; smaller positive values tighten it. Crossing a model-response or Run-event ceiling fails with ModelProtocolError. Compaction model calls use the same per-response limits as ordinary Turns.
AgentRuntime.start sizes its non-blocking multicast and replay storage from the effective Run-event ceiling plus one settlement marker. events and observe therefore preserve a complete replay without allowing a slow observer to backpressure execution or retain an unbounded trace.
Durable transports observe from the journal/projection and do not own execution liveness.
12. S1 Subagent execution seam
This section documents the S1 APIs for attached ephemeral Subagents (Subagent specification §4.3, §10.1). The engine owns the one interpreter and exposes delegation through execution options and two engine-provided services; it implements no second child loop and no delegation policy.
Execution options
RunOptions accepts preallocated identity and non-model-visible lineage:
conversationIdreuses an existing ephemeral Conversation identity;runIdis used instead ofIdGeneratorwhen supplied, so a delegating handler can know the intended child identity;parentLinkcarries the coreSubagentParentLinkfor a delegated child Run. It never enters the model prompt or event payloads; the engine uses it only to fix the Run's delegation depth (parentLink.depthfor a child,0when absent), and future durable work persists it as child lineage.
Run event sink
RunEventSink is an engine-owned Context.Service provided locally to Tool handlers so a delegation handler can emit the seven Subagent lifecycle events into the parent Run's semantic stream. emit accepts a pre-base payload (the core event minus eventVersion, runId, conversationId, agentId, sequence, timestamp, and turnId); the engine stamps those fields through the same eventBase path as every other event, so the base identity and the emitting batch's Turn are authoritative and the Run sequence stays monotonic. Each Tool batch owns one sink backed by a bounded queue drained by the Run's own stream. When a handler burst fills that queue, structured backpressure suspends the handler until the internal Run stream drains capacity. A detached external observer cannot backpressure the batch. Sink events appear inside the batch, and the batch settles, including on failure, only after already-emitted events have surfaced in the Run stream. Emission after the batch settled, or outside any Tool batch, fails closed with the typed RunEventSinkClosedError.
AgentSpawner
AgentSpawner is the engine-owned service contract through which a declared delegation Tool runs an Attached Child on the same interpreter. The engine provides it locally to every Run, bound to a narrow immutable parent value (agentId, conversationId, runId) and the Run's delegation depth (0 for a root Run); it exposes neither the engine's mutable Run state nor a root Layer Context. spawn(binding, input, delegation, options?) allocates a fresh child ConversationId/RunId through IdGenerator (no Conversation reuse), constructs the immutable Parent Link at depth + 1, and starts the child eagerly inside the caller-provided Scope, so parent interruption reaches the child and its finalizers (SUB-011). It returns the child identity and Parent Link plus the DetachedRun observation API (await, events, observe) with the child's full E/R visible. Depth exposure is the seam the delegation preflight uses to reject nested delegation (SUB-029); the engine itself enforces no delegation policy.
Both services are excluded from AgentRuntimeRequirements because the interpreter supplies them itself; an application Layer must not provide them.
12.1 Code Mode programmatic invocation seam
Programmatic calls emit no Run events and create no inner-call Canonical Records.
This section documents the engine API for Code Mode programmatic Tool invocation (capability specification §9.1). The engine owns a broker seam in the same pattern as AgentSpawner and DurableStep: provided locally by the interpreter, bound per outer Tool Call, and excluded from AgentRuntimeRequirements. The live native Toolkit handlers, engine policy context, and parent Tool Call identity are capabilities bound when the per-outer-call broker service is constructed. Per-call input from generated code contains only the namespace, method, and encoded arguments. The broker allocates each call's sequence index from its own monotonic per-pass state: generated code never supplies the authoritative index, and a transport-carried index is validated against the broker's state, failing typed on mismatch. A caller inside business execution can never substitute handlers or policy.
A programmatic call shares the existing per-call execution path for Tool lookup, parameter handling, approval preflight, scoped handler execution, typed failure handling, success and failure encoding, and per-call telemetry. It executes under the parent Tool Call's already-held scheduling permit and never acquires Tool Batch permits of its own: the batch semaphore is created per batch, so re-entrant acquisition would deadlock at toolConcurrency: 1, and a second batch path would let inner calls escape the declared concurrency bound. Calls are strictly sequential. A host call issued while another call from the same pass is unsettled fails with a typed concurrency error. Each call's identity derives from the outer ToolCallId plus the broker-owned zero-based sequence index; neither the model nor generated code can choose or forge it.
Two behaviors are specific to the broker path. Tool-call and duration budgets are consumed and checked before every inner invocation, so budget exhaustion prevents the next call mid-pass; direct model-declared calls keep their Turn-boundary accounting unchanged. Result size bounds and redaction at the sandbox boundary are broker-owned; openPass requires a positive finite maxResultBytes, and no such stage is added to the direct path. An inner call that would require approval fails with a typed policy failure and never suspends in the ephemeral slice. Per-Tool authorization remains application- and handler-owned; the engine contributes approval policy, scheduling, budgets, encoding, and telemetry only.
13. Runtime invariants
- RUN-001:
runandstreamshare one interpreter. - RUN-002: A Run settles its state exactly once, and any complete observed event trace contains at most one terminal event. An interrupted observer is not guaranteed to receive that event.
- RUN-003: A Tool Handler never sees undecoded parameters.
- RUN-004: Truncated Tool arguments never execute.
- RUN-005: The next Model Request sees a complete Tool Batch or none.
- RUN-006: Canonical/materialized Tool result order follows declaration order.
- RUN-007: Every owned child fiber belongs to the Run Scope.
- RUN-008: Interruption propagates to all attached children.
- RUN-009: Concurrency is bounded.
- RUN-010: Slow detached observers cannot determine durable liveness.
- RUN-011: Budget exhaustion cannot masquerade as success, and its dimension survives durably typed. A Run that settles through final-answer resolution after Turn, Tool Call, or token exhaustion carries
finishReason: "budget-exhausted"and theexhausteddimension marker on the live terminal event and durableSubmissionSettledrecord, never"model-stop".onExhaustion: "fail"fails typed before any exhausted application Handler starts. A Run failed byAgentPolicyErrorsettles with the typedlimitpreserved as the durable record'spolicyLimit. Consumers never reconstruct either dimension from message text. The metadata is family-bound fail-closed:finishReason: "budget-exhausted"andexhaustedmust either both be present on acompletedsettlement or both be absent;policyLimitis valid only on afailedsettlement whose recorded failure projection is theAgentPolicyErrorit names, and histories persisted before the dimensions became durable decode with the metadata absent. - RUN-012: Provider SDK types do not enter Conversation records; Effect AI Prompt and Response values remain the model-facing boundary.
- RUN-013: No retry policy can blindly repeat an uncertain external effect.
- RUN-014: Steering is delivered only before a model request and never mutates an in-flight response or Tool Batch.
- RUN-015: Follow-up input is delivered only when the Agent would otherwise stop.
- RUN-016: Programmatic Tool calls execute only through the engine-owned broker seam, under the parent Tool Call's already-held permit, strictly sequentially, with identities derived from the outer
ToolCallIdand a broker-owned zero-based index that generated code cannot supply; a concurrent host call fails typed. - RUN-017: Every programmatic Tool call consumes the Run's Tool-call and duration budgets before its handler is invoked; exhaustion prevents the next call mid-pass while direct calls keep their Turn-boundary accounting.
- RUN-018: Under
onExhaustion: "final-answer"(the default), an over-budget declared Tool batch never executes a handler and is never durably declared: every open call settles synthetically as a model-visible failed result exempt from repeated-failure folding, subsequent model requests forbid tool use, and the Run completes withfinishReason: "budget-exhausted". Under"fail"the Run fails typed before the batch starts. - RUN-019: Turn exhaustion under
"final-answer"admits exactly one grace Turn pastmaxTurns, with Tool use limited to the permitted singleton completion Tool; the pending batch at the final permitted Turn executes normally, and no second grace Turn exists. Under"fail"the Run fails typed at the seam. - RUN-020: Final-answer Turns are fail-closed: a model that declares any Tool Call under a
toolChoice: "none"request, or any call other than the permitted singleton completion Tool when that Tool is constrained, fails the Run typed. - RUN-021: Per-Run allowances are tightening-only: the effective Turn/Tool-Call limit is the minimum of the Agent Policy bound and the normalized allowance, never more, and the
onExhaustionresolution applies at the effective limit. - RUN-022: Every application Tool result, including MCP results, is bounded by policy
toolResultBoundsonce at the settle seam before entering records or prompts; an oversized result becomes the canonicalTruncatedToolResultenvelope preserving head, tail, and original byte size, and provider-executed results are exempt. - RUN-023: The engine accounts cache-read and cache-write input tokens distinctly from uncached input and tracks the most recent call's input/output tokens as the live-context estimate, both visible through the budget hook. Every present provider token field and each derived or cumulative total must be a non-negative safe integer; malformed usage or live aggregation overflow fails with
ModelProtocolErrorbefore cost estimation or budget consumption. A recovery usage seed obeys the same integer bound, with last-call token counts no greater than cumulative totals; invalid seeds fail before Run input hooks or external execution. A reported provider total must not be smaller than its explicit components and must equal them when every component is present; only genuinely omitted components may receive an inferred remainder. When a provider reports cache-write tokens both separately and inside uncached input, the engine separates that overlap before validating and persisting the canonical additive components while preserving the raw provider usage for cost estimation. - RUN-024: With policy
runStatus: "appended", every outgoing model request carries a derived run-status message showing Turns, Tool Calls, tokens, and elapsed time; the message is never persisted as canonical history. - RUN-025: Crossing 80% of a configured budget emits
BudgetWarningonce per dimension. The token dimension participates in theonExhaustionresolution: under"final-answer", a token-breaching response that already carries decodable final output settles directly, and otherwise the Run takes at most one constrained grace Turn (toolChoice: "none", or only the Definition-owned completion Tool; its usage consumed once and exempt from re-triggering breach), completing withfinishReason: "budget-exhausted"andexhausted: "tokens"; under"fail"a token breach stays fatal and no application Handler starts. Duration and cost remain hard rails in both modes. - RUN-026: When the estimated next model-call context exceeds policy
contextTokenLimit, including the reserved size of the model-visible output contract the engine appends after preparation under RUN-028, the engine compacts at the pre-Turn boundary. It prunes old Tool results and then summarizes through one metered model call. Compaction never splits an assistant Tool call from its result and always keeps prepared-unsettled Tool records. In the DN and DC assemblies, compaction appends a canonicalCompactionCreatedrecord that projections fold, and source history is never erased. - RUN-027: A provider context-length rejection is classified typed; with compaction configured the engine compacts and issues at most one framework-level retry (transport ambiguity may still duplicate the external model execution); otherwise, or on a second overflow, the Run fails with
ContextOverflowError. - RUN-028: Every model request of a Run whose Agent Definition declares an output Schema carries a model-visible representation of that Schema derived by Effect AI's JSON-Schema derivation, applied after context preparation and never entered into official history; a Definition whose output Schema cannot be derived runs with the documented fallback and a diagnostic, never a silent difference.
- RUN-029: An Agent Definition may declare an application-owned run-disposition Schema and decoded-output selector. Only an ordinary completed Run may Schema-validate, emit, and durably persist the selected value. Failed, interrupted, aborted, incomplete, run-less, and budget-exhausted Runs carry none; invalid values fail typed, and consumers never infer a disposition from prose or Tool output.
- RUN-030:
maxDurationis one wall-clock allowance per logical Run. DN and DC derive its absolute deadline from one canonicalRunStartedrecord whose timestamp fixes the logical start and whose payload stores the admitted duration allowance. They preserve that deadline across Attempt replacement and every durable suspension; admission, queue delay, and binding-free input recovery are excluded, and no Run option may widen the Definition's fresh duration allowance. The record timestamp separately fixes elapsed-time reporting, so a tightened deadline does not fabricate elapsed time. The coordinator commits verified settled-child joins before invoking an expired Run. The engine exposes no past-deadline capability and authorizes no model, ordinary Tool, or child execution after expiry. - RUN-031: Optional host Tool authorization runs for each model-declared call in the complete still-executable application batch before durable preparation or any Handler. Fresh and durable-resumed calls present the same canonical Run/Turn/input authority and stable call descriptor; denial fails typed and settles accepted work terminally without the denied Attempt starting a Handler or creating a new side effect. Fresh denial writes no prepared record, and resumed denial writes no new preparation.
- RUN-032: An Agent Definition may designate one singleton completion Tool and a typed output projector. A successful authorized call Schema-validates and completes the Run immediately, including after budget exhaustion under
onExhaustion: "final-answer", without a private-summary model turn. Fail mode rejects exhausted delivery before its Handler starts, mixed completion batches fail before any Handler, and durable recovery never repeats a canonically settled call. Recovery re-decodes the canonical parameters/result, reapplies the projector, output Schema, and any ordinary run-disposition selector, counts provider-executed calls when enforcing the singleton batch, and requires the reconstructed durable values to match their marker. Durable no-tool completion likewise commits its final response and completion marker atomically, so recovery re-decodes and matches that response before terminalizing without a duplicate model request. - RUN-033: A first response marks its evaluated instruction/wake prefix as Run-scoped. Recovery of that Run retains it, while later Runs omit only that marked prefix and preserve the remaining assistant/tool conversation. The recorded prefix length must be smaller than the recorded message count, every prefixed message must be system or user input, and at least the response must remain. Invalid provenance fails Schema decoding, while unmarked legacy records retain their full projection.
- RUN-034: Before provider I/O the engine reserves
completionReserveTokensand admits a research call only when its estimated complete prompt plus that reserve fits the remaining gross token budget. Context or token pressure invokes target-aware compaction; an unreachable target fails locally withContextBudgetError, and token pressure otherwise enters finalization early. - RUN-035: Each completed model call normalizes and durably records provider, model, optional service tier/pricing version, uncached/cache-read/cache-write input, text/reasoning output, and estimated microdollar cost. Every canonical input or output total exactly equals its component sum. Cached tokens remain part of gross token budgeting, durable hosts expose the estimator,
costBudgetMicrousdis enforced across Attempts, and every Run settlement carries an aggregate usage summary without double-counting joined Submissions. Any component, token, model-call, or cost aggregation that would exceed safe-integer accounting fails typed before settlement. Summary pricing identities are unique, and every top-level total exactly equals the checked sum of its per-model groups. - RUN-036: The opt-in trusted Tool failure observer receives exactly the non-propagating failures classified in §5, with raw correlation identities, original Causes wherever held, tag-only declared failures, and byte-bounded infrastructure/protocol messages. Delivery is inline and at most once per in-memory attempt; observer/reporter defects are isolated and external interruption remains effective. Durable hosts capture the same default-none service at composition and never observe replay-injected settled calls. Observations add no automatic export, public event, canonical record, durable state, or replay guarantee.