Skip to content

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.

Use agent-runtime:execute-turn for one normalized turn, then call the requested tool through the same authenticated kernel facade.

const selector = {
  run_id: 'run-agent-session-1',
  task_id: 'task-agent-session-1',
  session_id: 'session-agent-session-1',
};

const toolCatalog = await buildGovernedToolCatalog({
  runtime: execution,
  executionSelector: selector,
});

const turn = await execution.executeTool(
  'agent-runtime:execute-turn',
  {
    session_id: selector.session_id,
    model_profile: 'operations',
    url: 'https://models.internal.example/',
    messages: [{ role: 'user', content: 'Reschedule the weekly review for tomorrow morning.' }],
    tool_catalog: toolCatalog,
  },
  selector,
);

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:

const result = await runGovernedAgentLoop({
  runtime: execution,
  executionSelector: selector,
  executeTurnInput: {
    session_id: selector.session_id,
    model_profile: 'operations',
    url: 'https://models.internal.example/',
    messages,
    tool_catalog: toolCatalog,
  },
});

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.

const result = await runGovernedAgentLoop({
  runtime: execution,
  executionSelector: selector,
  executeTurnInput: { session_id, model_profile: 'builder', url, messages, tool_catalog },
  systemMessages: 'hoist',
  toolNameCodec: { toWire: (name) => name.replaceAll(':', '_'), toGoverned },
  signal: run.signal,
  transformHistory: ({ messages }) => compact(messages),
  checkLimits: ({ phase, tool_call_count }) =>
    phase === 'before_tool_call' && tool_call_count >= limit
      ? { action: 'stop', code: 'RUN_TOOL_CALL_LIMIT', message: 'Progress was kept.' }
      : undefined,
  onLimitStop: (cursor) => saveCheckpoint(cursor),
  retryPolicy: ({ attempt, failure, request }) =>
    isRetryable(failure) && attempt <= 3
      ? { retry: true, wait_ms: backoff(attempt), request: withHostNote(request) }
      : { retry: false },
  onTurnComplete: (report) => meter.add(report.usage, report.request_bytes),
  verifyTerminal: () =>
    checksPass() ? { accept: true } : { accept: false, note: 'Run the checks.' },
});
HookRunsWhat it can do
transformHistorybefore every turn, with a copy of the working historyreturn a replacement history; it replaces the working history, so a compaction stays compacted. An exception propagates
checkLimitsbefore 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
onLimitStopwhen checkLimits (or the signal) stops the loopreceives the cursor (messages, history, counters) to persist before the result returns. A failing checkpoint fails the caller
retryPolicyafter 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
onTurnCompleteafter every turn and its tool callsobserve usage, attempts, request bytes, duration and tool outputs. Its exceptions are contained
verifyTerminalon a terminal (non-paused) reply{ accept: false, note } appends the reply and the note and asks again, bounded by maxTurns
systemMessageswhen 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)
toolNameCodecwhen the request is built and when the answer is readtoWire for the catalog and the calls and results in the request; toGoverned for what the model called. The stored history keeps governed names
signalevery runtime call, retry wait and turn startaborting 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.

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.

Hosts discover the tool set through the sealed facade before they build a turn prompt:

const toolCatalog = await buildGovernedToolCatalog({
  runtime: execution,
  executionSelector: selector,
  excludeToolNames: ['agent-runtime:execute-turn'],
});

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.

Set stream: true when you want the provider adapter to capture streaming partials:

const turn = await execution.executeTool(
  'agent-runtime:execute-turn',
  {
    session_id: selector.session_id,
    model_profile: 'operations',
    url: 'https://models.internal.example/',
    stream: true,
    messages: [{ role: 'user', content: 'Draft the handoff summary.' }],
  },
  selector,
);

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.

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:

{
  "runnable": false,
  "assistant_message": "I will check both sources.",
  "assistant_message_truncated": false,
  "finish_reason": "tool_calls",
  "usage": { "input_tokens": 20, "output_tokens": 12 },
  "tool_calls": [
    {
      "call_id": "call-weather",
      "name": "weather:get",
      "raw_arguments": "{invalid",
      "argument_bytes": 8,
      "truncated": false,
      "malformed": true
    }
  ]
}

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.

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:

const reviewer = await kernelRuntime.authenticateApprovalCaller(reviewerCredential);
const resolution = await reviewer.resolveApproval({
  request_id: pendingRequestId,
  task_id: pendingTaskId,
  decision: 'approved',
  reason: 'Exact request and blast radius reviewed.',
});

const approvalMessage = createApprovalResolutionMessage({
  requestId: resolution.request_id,
  approved: resolution.decision === 'approved',
  approvedBy: resolution.resolver,
  toolName: 'calendar:delete-event',
});

const resumed = await resumeGovernedAgentSession({
  runtime: execution,
  approvalRuntime: execution,
  workflowStateStore,
  sessionId: selector.session_id,
  executionSelector: selector,
  continuationMessages: [approvalMessage],
});

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.

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.

The orchestration helpers keep long-running flows inside the same agent-session task model.

  1. 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_effect transition 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.

const workflow = await startGovernedAgentWorkflow({
  runtime: execution,
  workflowStateStore,
  executionSelector: selector,
  workflow: {
    session_id: selector.session_id,
    nodes: [
      {
        id: 'collect-context',
        execute_turn_input: {
          session_id: selector.session_id,
          model_profile: 'operations',
          url: 'https://models.internal.example/',
          messages: [{ role: 'user', content: 'Collect the scheduling constraints.' }],
          tool_catalog: toolCatalog,
        },
      },
      {
        id: 'scheduled-follow-up',
        depends_on: ['collect-context'],
        wake_at: '2026-03-13T09:00:00.000Z',
        execute_turn_input: {
          session_id: selector.session_id,
          model_profile: 'operations',
          url: 'https://models.internal.example/',
          messages: [{ role: 'user', content: 'Run the approved follow-up.' }],
          tool_catalog: toolCatalog,
        },
      },
    ],
  },
});

Later, resume the same workflow:

await resumeGovernedAgentWorkflow({
  runtime: execution,
  workflowStateStore,
  sessionId: selector.session_id,
  executionSelector: selector,
});

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.