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.

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. If an invoked tool throws, the failed call and every unexecuted remainder call receive explicit error results before the session is persisted or resumed.

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; progress snapshots contain only the latest delta. The final tool output remains normalized, but the kernel evidence store also records tool_call_progress entries for the intermediate snapshots before the final tool_call_finished receipt.

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]. 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.

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, resolve the durable request, execute the exact approved attempt, then feed a narration message into the next provider turn:

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 output = await execution.executeApprovedTool(
  resolution.request_id,
  'calendar:delete-event',
  exactOriginalInput,
  selector,
);

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

Append approvalMessage to the next turn or resume call so the agent-session state remains

When a turn has multiple approval-required calls, resolve every entry in pending_approvals and append one resolution message per request_id. Resume fails closed if any pending request is unresolved, malformed, or duplicated, leaving the persisted approval set intact. 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.

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() and the session/workflow helpers persist the turn as suspended rather than completed, 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 content-addressed payload sink). 4. Resume the same session when a wake time arrives or approval is resolved.

Workflow files use schema v3. The bounded v1/v2 read path migrates already-persisted turn fields to canonical ordered content without parsing assistant text as control data. A legacy pending approval without an exact provider identity is reported as non-resumable instead of being silently rewritten. Every new write uses v3. 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,
});