Workflows And Examples
The Agent Runtime Pack supports both host-driven loops and pack-owned agent-session
orchestration. Both require a sealed AuthenticatedExecutionRuntime and the strict three-field
ExecutionSelector for a persisted active run. A selector locates state; it does not carry caller,
scope, lane, workspace, pack, or policy authority.
Routing is governed by the versioned universal harness capability contract, independent of provider or client names. A harness must declare methodology, WU, lifecycle, initiative, delegation, model/effort-control, and application-attestation capabilities; missing or unknown declarations fail safe. Model control, effort control, and application attestation produce separate outcomes, so a model-only or effort-only attestation cannot imply an unobserved success for the other control. Requested, resolved, applied, unattested, fallback, blocked, and deviation are distinct states. Only host/provider attestation can establish applied model or effort. The release-evidence-indexed contract digest and lifecycle-issued brief/WU/workspace/HEAD evidence must all match before routing is verified; host memory/cache is advisory context and cannot self-assert expected evidence, synthesize a brief, or grant authority.
Host-Driven Turn Loop
Section titled “Host-Driven Turn Loop”Use agent-runtime:execute-turn for one normalized turn, then call the requested tool through the same
authenticated kernel facade.
If the governed turn returns tool_request, use runGovernedAgentLoop() when possible. It
preserves the assistant call turn, executes every requested tool in provider order, and pairs each
result to the exact provider-issued call ID. It also rejects a requested name that is absent from
the exact host catalog before any requested tool executes:
For a custom loop, the assistant message containing turn.data.content_blocks must precede its
result messages. Do not flatten reasoning or tool blocks, and do not generate a substitute call ID.
OpenAI and Anthropic both require results to reference the exact corresponding call ID. The
Responses adapter emits one function_call_output item for every parallel call; Anthropic
additionally requires parallel results to immediately follow the assistant tool-use turn. See the
official OpenAI function-calling
guide and Anthropic parallel
tool-use guide.
runGovernedAgentLoop() validates the complete ordered batch before execution and checks the
remaining tool-call budget atomically. A rejected batch executes nothing, but still records one
paired limit result per call. In this invocation-local loop, a thrown tool receives an error
result and the unexecuted tail receives paired errors. Durable session/workflow helpers instead
retain an uncertain invocation as a typed recovery wait before advancing its saved cursor.
Running A Whole Agent Loop On The Governed Loop
Section titled “Running A Whole Agent Loop On The Governed Loop”A host that needs more than the stock loop does not have to fork it. runGovernedAgentLoop()
accepts optional hooks. Each one is absent by default, and with none given the loop behaves
exactly as above. runtime stays the only way anything reaches the kernel, so a host-side
refusal, a host-only tool, a tool-output size bound or a receipt capture is a thin wrapper around
runtime.executeTool, not a hook. The loop runs over any kernel-bound runtime; the refusal some
hosts see when the local host route starts a provider turn with a network allowlist is a property
of that route’s posture, not of the loop.
| Hook | Runs | What it can do |
|---|---|---|
transformHistory | before every turn, with a copy of the working history | return a replacement history; it replaces the working history, so a compaction stays compacted. An exception propagates |
checkLimits | before every turn and before every tool call | { action: 'stop', code, message } ends the loop with kind: 'error', stage: 'loop_limit'. Unrun calls of the batch are answered AGENT_RUNTIME_TOOL_NOT_EXECUTED |
onLimitStop | when checkLimits (or the signal) stops the loop | receives the cursor (messages, history, counters) to persist before the result returns. A failing checkpoint fails the caller |
retryPolicy | after each failed or thrown provider turn | { retry: true, wait_ms?, request? }. The replacement request applies to this turn only and is never stored in the history |
onTurnComplete | after every turn and its tool calls | observe usage, attempts, request bytes, duration and tool outputs. Its exceptions are contained |
verifyTerminal | on a terminal (non-paused) reply | { accept: false, note } appends the reply and the note and asks again, bounded by maxTurns |
systemMessages | when the request is built | 'hoist' leads with every system message that precedes the first reply and turns later ones into user messages, in the request only (leadWithSystemMessages) |
toolNameCodec | when the request is built and when the answer is read | toWire for the catalog and the calls and results in the request; toGoverned for what the model called. The stored history keeps governed names |
signal | every runtime call, retry wait and turn start | aborting ends retry waits and stops the loop with AGENT_RUNTIME_LOOP_ABORTED |
The report passed to onTurnComplete has an outcome of tool_calls, terminal,
terminal_vetoed, paused, approval_required, halted or failed. A failed report carries
the failure (code, message, http_status, thrown) and every failed attempt.
The durable startGovernedAgentSession() refuses these hooks with a typed error, except the
onTurnComplete observer: its replay journal cannot replay a history the host rewrote. The four
durable session and workflow entry points accept onTurnComplete. A turn replayed from the journal
is reported again by the invocation that replays it.
Built-in retry of an empty stream failure
Section titled “Built-in retry of an empty stream failure”Without a retryPolicy, and in every durable session and workflow, the loop retries one failure by
itself: a provider turn whose stream opened with a 2xx status and then ended in a terminal provider
error before any output arrived. It retries exactly once, after a short jittered wait (500 to 1000
ms) that the loop signal ends early. The turn report shows attempts: 2 and the failed attempt
in attempt_errors; a second empty failure fails the turn after exactly two attempts.
It applies only when all of these hold: the response status was 2xx; the provider’s own terminal
error event ended the stream (a Cloud provider_error frame, an OpenAI-compatible or Responses
server_error, a Messages overloaded_error or api_error); none of these had arrived first: a
content, reasoning, refusal or tool-call delta, an output item or content block, or usage showing
generated tokens (a role-only chunk is not output); no partial turn came back; no upstream status
was observed; and an upstream code, if present, is overloaded or provider_internal. The decision
comes from what the library observed on the stream, never from the provider’s own JSON. It never retries after any output, an observed 4xx or upstream status, a rejected
credential, a rate limit, a malformed request, an exceeded limit, a malformed stream, a stop by the
kernel, or an aborted signal. An abort during the retry wait stops the loop; this also holds for a
host retryPolicy, which no longer gets one more attempt after the abort. A retry is a second provider request and spends host budget like one.
A turn with attempts > 1 carries extra spend of unknown size: after any retry a job’s
actualCostUsd is null, because the failed attempt reports no cost and none is invented. The
onTurnComplete report of a durable entry point also names the workflow node_id and carries
replayed: true for a turn replayed from the journal; count usage once per
(node_id, turn_index), because a replayed: true report may or may not repeat an earlier one
(an invocation interrupted during a turn’s tool calls never reports that turn). In a durable session the failed attempt is recorded in the journal, a resume never repeats
or skips it and sends at most one further attempt. When you pass a retryPolicy it decides alone; call
isRetryableEmptyStreamFailure(failure) inside it to reuse the same test. Retrying any other
failure stays with you.
Governed Tool Auto-Discovery
Section titled “Governed Tool Auto-Discovery”Hosts discover the tool set through the sealed facade before they build a turn prompt:
The helper delegates to AuthenticatedExecutionRuntime.listGovernedTools(selector), removes
out-of-scope and explicitly excluded tools, and preserves approval_required decisions. The model
cannot add tools by returning a different catalog or by naming another registered capability.
Streaming Turns
Section titled “Streaming Turns”Set stream: true when you want the provider adapter to capture streaming partials:
The adapter assembles partial text, reasoning, and indexed tool-call arguments before producing the final normalized turn. Reads and argument accounting are incremental and bounded.
Stream deltas are live, non-durable progress, the same on every streaming provider (including
lumenflow_cloud). A host that calls the tool in process passes onProgress in the tool execution
options and receives each delta in order as it arrives: { sequence, data, dropped_before }. Delivery
is in memory and bounded. A subscriber that returns quickly sees every delta; a subscriber that
returns a promise is busy until it settles, later deltas wait in a ring of 256 events with the oldest
dropped when it is full, and dropped_before reports the gap. The turn never waits on a subscriber,
and a subscriber that throws cannot fail it. Nothing is written per delta: the kernel evidence store
records the assembled reply and usage, with one final tool_call_progress entry (plus the
tool_call_finished receipt) for a streamed turn, however many deltas it carried.
After a provider terminal marker, the adapter drains until natural EOF or one absolute configured deadline, rejects every non-empty post-terminal chunk observed before that deadline, and then requests best-effort cancellation of a reader that did not close without waiting for transport cleanup. Bytes sent only after the deadline are not observable. Post-header cancellation, malformed chunks, provider errors, and reader disconnects each end the adapter once. No incomplete stream is converted into a runnable tool request.
Completion requires the provider’s full lifecycle, not only its outer marker. Chat Completions
must emit a finish reason before [DONE] or EOF. Responses consumes typed item, text, reasoning, and
function-argument events and must end with one completed, incomplete, failed, cancelled, or error
event. Messages-compatible streams must stop every started content block and emit a
message_delta stop reason before message_stop. Canonical non-streaming tool-only responses
remain valid without synthetic text or an intent catalog. Tool calls require provider-native
function/tool calls and a successful terminal outcome. Truncated, refused, failed, cancelled, or
unknown terminal states never become runnable.
Empty or whitespace-only tool-call arguments mean the model called the tool with no arguments, as
models routinely do for a tool whose schema has no properties. Every provider path, streamed or
not, normalizes them to {}, including a streamed call that receives no argument deltas. Any other
argument text must still encode one JSON object; invalid JSON or a non-object value is refused with
PROVIDER_MALFORMED_RESPONSE or PROVIDER_MALFORMED_STREAM.
Recovering A Malformed Tool Turn
Section titled “Recovering A Malformed Tool Turn”When a native tool call has malformed arguments, the turn still fails with its existing
PROVIDER_MALFORMED_RESPONSE or PROVIDER_MALFORMED_STREAM code and message. Direct
agent-runtime:execute-turn and provider-adapter callers also receive
error.details.partial_turn. This is recovery information, never an executable turn:
The payload preserves observed assistant text, tool-call order, exact IDs (or null when
absent), argument text and usage when reported. Native object inputs are represented as serialized JSON. finish_reason is null when no terminal
reason was observed. details.retryable: true only advises that a fresh turn might succeed;
it does not schedule a retry or establish that an uncertain external effect is safe to replay.
Assistant text is capped in UTF-8 bytes by max_response_bytes. Each call reports its
argument_bytes from the observed representation or retained incremental safety measurement; an argument over
max_tool_argument_bytes omits raw_arguments and sets
truncated: true. The failure retains PROVIDER_LIMIT_EXCEEDED and the existing byte-limit
details. Early stream refusals return only what was observed before refusal, without reading
on to invent terminal data or usage.
No failed turn produces requested_tools. Neither a malformed call nor its valid siblings
are dispatched. A custom host can use an observed malformed call ID to construct its own tool
error result; it must never execute that call or convert this recovery payload into tool requests.
Missing IDs remain missing rather than being replaced with synthetic ones.
Recovery content is returned to the invoking host, including an explicit host retryPolicy
that handles its failed request. LumenFlow removes partial_turn from terminal onEvent
notifications, failed and recovered onTurnComplete attempt reports, and automatic nested-tool
history/result messages. These projections preserve other error fields and do not mutate the
caller’s error. The standard kernel failure trace stores no recovery content. A caller still
controls any content it deliberately logs or retains. The governed loop’s final failure result
keeps its existing code/message shape.
A Chat Completions stream that closes at EOF after a finish reason is complete, with or without a trailing usage-only chunk (empty choices) and without a [DONE] line; unknown extra fields on chunks, choices and deltas are ignored. EOF or [DONE] before any finish reason is truncation and fails as PROVIDER_MALFORMED_STREAM. Every malformed-stream error from this parser carries details.check, a stable identifier of the failed check (for example eof_before_finish_reason, done_before_finish_reason, tool_calls_wrong_finish_reason, tool_call_missing_id_or_name, event_not_json), without stream content or credentials in the check identifier. Caller-only malformed-tool recovery is described above.
Chat Completions streams identify each reasoning detail by index. The adapter joins the fragments
that share an index and a type into one detail, in stream order. A gateway that fronts several
upstream protocols can change the type it sends at an index part-way through a stream; that is
not an error and does not cost the turn its text, tool calls, usage, or finish reason. The new type
starts a separate detail at the same index. Text and signatures are never merged across types, no
fragment is dropped or reordered, and the details replay in stream order, so a continued tool-use
request never carries a hybrid of two types. A reasoning type the runtime does not use is kept
verbatim as opaque provider state and never aborts the stream. Single-type streams assemble and
replay exactly as before. A reasoning detail that is not an object, an index that is not a
nonnegative integer, and a chunk with neither choices[0] nor usage are still refused with
PROVIDER_MALFORMED_STREAM, and the content-block limit still bounds the number of details.
When Responses emits incremental text or function arguments, the matching text/content or argument
done substages must arrive before response.output_item.done. Late and duplicate substage events are
malformed. An incomplete call remains opaque, but it still counts against all item/call limits and
its arguments must remain byte-bounded, parseable JSON objects.
Responses sends store: false and continuation history preserves item IDs, optional item statuses,
exact call IDs, and encrypted reasoning metadata. The application treats that state as opaque
provider data; it does not turn reasoning state into policy authority. Function tools use
strict: false so governed caller schemas are not falsely claimed to meet OpenAI strict-schema
constraints. Protocol choice remains explicit in the model profile and is never inferred from the
endpoint URL.
Approval-Driven Continuation
Section titled “Approval-Driven Continuation”When policy returns APPROVAL_REQUIRED, bind an authenticated and authorized reviewer and resolve
the durable request. For a persisted session, give the resume helper the authenticated approval
runtime so it reserves and executes the exact approved attempt itself:
The narration message is optional. The durable helper binds the pending request to its exact
accepted node/turn/call ordinal, ID, name, and input, and replaces one matching native result with
the committed approved output. A cached approved output is reused after restart. For a custom
invocation-local loop, the host calls execution.executeApprovedTool() directly and pairs its
output to the original native call before requesting another provider turn.
When a turn has multiple approval-required calls, the helper checkpoints each resolved action and
retains unresolved approvals. It cannot dispatch an unresolved action from text. A canonical
provider narration has a nonempty request_id, a boolean approved, and a nonempty approved_by;
those fields report the kernel result and never authorize it. An uncertain approved execution
returns a reconciliation wait and cannot be retried by another narration message.
On resume, continuation messages that carry approval narration are screened before anything
changes. Narration is a message whose whole content is a JSON object with an approval field.
A malformed, duplicate or unmatched resolution makes resumeGovernedAgentSession and
resumeGovernedAgentWorkflow throw, and so does a set that resolves some pending approvals but
not all of them; the checkpoint stays exactly as it was. A continuation with no resolution, such as
a fresh brief on lease adoption, is left alone and the authenticated approval runtime resolves the
pending approvals. Narration never grants or denies anything: only the kernel does. When the host
supplies inspectApproval, narration that says approved: false for a request the kernel has
approved is refused with no state change, so record the denial in the kernel; approved: true for a
request the kernel has not approved is not refused and the resume waits (host_policy_wait) because
the kernel will not execute it. A message that parses as a JSON object with a top-level approval
key counts as narration in any key order; log lines, NDJSON and host context objects without that
key do not.
Provider-Owned Calls and Paused Turns
Section titled “Provider-Owned Calls and Paused Turns”A turn can contain provider-owned calls (Anthropic server_tool_use, an OpenAI Responses hosted
tool item) alongside — or instead of — client-owned ones. turn.data.requested_tools only ever
contains the client/kernel-owned calls a host should execute; provider-owned calls never appear
there and must never be executed or emulated locally. See Execution Ownership & Caller
Graphs for the full
execution_owner/caller_id/container_id contract.
When Anthropic pauses a long-running provider-owned turn (terminal_state: "paused"),
runGovernedAgentLoop() returns suspended, and the session/workflow helpers persist that state
with the exact assistant content already appended to messages. Resume with
resumeGovernedAgentSession() / resumeGovernedAgentWorkflow() exactly as you would after a
turn-budget suspension — the host never needs to special-case a paused turn beyond calling resume.
Pack-Owned Workflow Helpers
Section titled “Pack-Owned Workflow Helpers”The orchestration helpers keep long-running flows inside the same agent-session task model.
- Start a workflow with one or more nodes. 2. Let ready nodes execute governed turns. 3. Persist
waiting, scheduled, approval, and completion continuations through the RunAggregate-backed
workflow authority (a fenced
record_effecttransition against the task’s own history plus a immutable payload bound to the committed checkpoint reference). 4. Resume the same session when a wake time arrives or approval is resolved.
New session/workflow checkpoints use schema v4, including the operation journal and monotonic
acquisition revision. The bounded v1-v3 importer remains readable and normalizes persisted turn
fields without parsing assistant text as control data. An invalid legacy approval identity is
reported as non-resumable; a nonterminal legacy state returns reconciliation_required with
legacy_state and never dispatches from an ordinary resume. Completed sessions remain terminal.
The legacy importer rejects v4 rather than dropping its journal. Provide a workflowStateStore backed by
RunAggregateAgentWorkflowAuthority (exported from the pack’s run-aggregate module) plus a
payload port such as ProtectedStateRunAggregateWorkflowPayloads — the legacy
.agent-runtime/workflow/ directory is now read-only, wired solely into
createLegacyAgentRuntimeWorkflowImportSource() for one-way migration, and cannot become lifecycle
authority.
Later, resume the same workflow:
Checkpoint Recovery
Section titled “Checkpoint Recovery”Workflow and runtime-session authorities publish detached JSON snapshots immutably before committing their fenced RunAggregate checkpoint references. The reference binds content and workspace/task/run/session/workflow identity with SHA-256. Restart selects only committed references, verifies the selected payload, and reports missing, corrupt, or mismatched content as a recovery error.
A publication failure creates no accepted checkpoint. An interruption between publication and append leaves an unselected orphan; a committed append can be replayed by a fresh authority. A paused old owner cannot overwrite the newer owner’s immutable payload, and stale ownership or an event-version conflict refuses its append. Exact correlation/effect retries verify the original checkpoint without moving selection; different identity or content under the same key is rejected. The durable helpers use a persisted monotonic checkpoint revision and fresh host invocation owner, so multiple saves with the same timestamp remain distinct.
Initial session/graph acceptance commits before provider execution. Each provider request, client
action, and approved action commits a pending reservation before dispatch and its exact output
before further dispatch. Only a newly committed acquisition grants execution: replayed saves and
conflicts return checkpoint_conflict with checkpoint_not_committed. A DAG node’s completion
commits before dependents unlock. Cold recovery retains exact native call IDs, full ordered content,
opaque provider state, operation fingerprints, and known outputs; completed operations are not
redispatched and a cached provider response does not consume another invocation turn.
Pending requests or actions return reconciliation_required with unknown_outcome. Failures,
cancellation, and synthetic local crash traces do not prove that an external effect did not happen.
The host’s reconciliation.reconcile port can return unknown, a completed exact output, or
definitive not_executed. The latter two must bind the operation ID and fingerprint and cite
verified evidence; nonexecution also requires old_executor_cannot_complete: true. The helper
checkpoints that resolution before consuming it or reserving a fresh attempt. Model text,
caller-supplied JSON, and approval narration cannot replace this trusted host port.
These rules prevent blind replay, not arbitrary external exactly-once effects. A pending outcome needs an external idempotency/reconciliation contract. Real regression fixtures kill only their own child at reservation, provider acceptance, client effect, and committed-output boundaries, then reopen SQLite and protected payloads in fresh processes. Competing cold resumes acquire one successor, and frozen timestamps do not collide with monotonic revisions.
Legacy effect markers without bound references cannot certify mutable checkpoint files. Restoration reports an explicit compatibility boundary until the host commits a new bound workflow checkpoint from trusted live state with a fresh correlation. Runtime legacy sessions require a new session ID initialized from trusted state; the old unbound session remains refused. Workflow v1/v2 envelope migration does not itself establish checkpoint authority.
Protected payload stores allow 16 MiB per snapshot and retain at most 512 files / 128 MiB per identity/kind/session scope, including orphan and interrupted staging files. Exclusive publication locks serialize quota admission. Full scopes still permit exact immutable retries. Quota exhaustion or a stranded lock reports a visible recovery error. Stop all publishers, reconcile against all committed references, preserve payloads needed for replay/retries, then remove only proven orphans or the exact stranded lock. Append failure alone never authorizes deletion.
These guarantees use the existing protected-state port on every supported host. Hosts must wire the durable helper and trusted recovery ports into their execution path; production assignment adoption and supervisor orchestration are separate integrations.