Skip to content

Configuration

The Agent Runtime Pack reads its configuration from the agent_runtime namespace in workspace.yaml.

Add an agent-runtime pin to the workspace pack list:

packs:
  - id: agent-runtime
    version: 0.1.0
    integrity: dev
    source: local

This example shows the monorepo or local-pack development path. The same pack manifest contract works with other pack sources once you publish and pin a verifiable integrity hash.

integrity: dev is not accepted by normal production loading. Published binaries remain fail-closed. For a non-loopback registry, provide a sha256: hash obtained independently from the registry serving the pack.

When the namespace is absent, initialize it in one validated operation with the complete required object. Nested writes such as setting default_model first still fail until the required models map is present:

pnpm config:set --key agent_runtime --json-value \
  '{"default_model":"default","models":{"default":{"provider":"openai_responses","model":"operator-configured","api_key_env":"OPERATOR_MODEL_KEY"}}}'

This replaces the complete consumer configuration; omitted existing settings and profiles are removed. It does not create workspace or lane scope grants, grant provider access, or infer permissions. Configure those separately through their owning surfaces.

agent_runtime:
  default_model: operations
  models:
    operations:
      provider: messages_compatible
      model: ops-assistant
      api_key_env: AGENT_RUNTIME_API_KEY
      base_url: https://models.internal.example/
    planning:
      provider: openai_compatible
      model: planner
      api_key_env: PLANNER_API_KEY
      base_url_env: PLANNER_BASE_URL
    responses:
      provider: openai_responses
      model: reasoning-agent
      api_key_env: OPENAI_API_KEY
      base_url: https://api.openai.com/v1/responses
  policy_profiles:
    scheduling-agent:
      description: Scheduling tools for a persisted agent task
      allow_tools:
        - calendar:create-event
        - calendar:reschedule-event
      approval_required_tools:
        - calendar:delete-event
  connected_compute:
    policy_profile: scheduling-agent
    # Optional nonnegative USD ceiling for each Connected Compute assignment.
    # cost_limit_usd: 2.00
  limits:
    max_turns_per_session: 12
    max_tool_calls_per_session: 6
    max_input_bytes: 65536

Each profile declares:

FieldMeaning
providerAdapter family used for request/response normalization
modelProvider-specific model identifier carried into the normalized response. Not allowed on lumenflow_cloud, which names a capability_tier instead
capability_tierlumenflow_cloud only, required there. The vendor-neutral tier LumenFlow Cloud resolves to a route. Never a model or provider name
turn_timeout_ms, stream_idle_timeout_ms, turn_ceiling_mslumenflow_cloud only, optional integers (1000 to 3600000 ms). Whole-call limit of a non-streamed turn (default 120000), longest quiet gap between the chunks of a streamed turn (default 60000), and the longest a streamed turn may stay open (default 600000). A long builder turn raises them; any other provider kind refuses them
api_key_envEnvironment variable that holds the credential for the selected profile (the Cloud workspace key for lumenflow_cloud)
base_url / base_url_envAbsolute provider base URL used both for outbound requests and network allowlist derivation A base URL with userinfo (user:token@host) is refused; put the credential in api_key_env. A base URL or turn url that carries a credential-shaped parameter in its query or path (for example key, api_key, x-goog-api-key, subscription-key, token, access_token, sig, secret, password, code, or a name derived from api_key_env; case-insensitive), or any fragment, is refused before any call and before the input is persisted; use the auth header via api_key_env or the secret configuration. A provider that accepts its key only as a query parameter is not supported. A non-credential query (for example api-version) is sent as configured but every error, detail, metadata field and event shows only origin plus path.
image_inputOptional boolean. Set true only for a vision-capable model; absent or false refuses any turn with an image
cachingOptional, auto. openai_compatible: adds providerOptions.gateway.caching = "auto". messages_compatible: adds top-level cache_control plus system, tool and rolling final-message breakpoints
stream_usageOptional boolean, openai_compatible streamed turns. Default on: the body carries stream_options: { include_usage: true } so the endpoint reports token usage. Set false for an endpoint that rejects the field
session_affinityOptional boolean, requires caching: auto on openai_compatible. Sends the turn session_id as the opaque x-session-affinity header
reasoning_effortOptional. openai_compatible: none, minimal, low, medium, high, xhigh or max, sent as top-level reasoning: { effort }. messages_compatible: low to max, sent as output_config.effort
thinkingOptional, messages_compatible only: { type: adaptive, display?: summarized | omitted }, forwarded as thinking. budget_tokens and type: enabled are refused
max_output_tokensOptional integer (1 to 1048576). Ceiling on output tokens per turn, sent as max_tokens on openai_compatible and messages_compatible
input_usd_per_million_tokens / output_usd_per_million_tokensOptional nonnegative USD prices per million tokens. Only workspace config supplies a price; an assignment payload cannot. Both are needed to enforce cost_limit_usd
cached_input_usd_per_million_tokens / cache_write_usd_per_million_tokensOptional prices for prompt-cache reads (default: the input price) and writes (a cache write with no price reports unknown cost)
max_stream_eventsOptional. Ceiling on parsed stream events per streamed turn (default 65536, hard maximum 1048576)
max_stream_snapshotsOptional. Ceiling on stream deltas (live progress, not stored) per streamed turn (default 16384, hard maximum 262144)
max_stream_bytesOptional. Ceiling on streamed response bytes per turn (default 16777216, hard maximum 67108864)

Prompt caching through an OpenAI-compatible gateway

Section titled “Prompt caching through an OpenAI-compatible gateway”

Set caching: auto on an openai_compatible profile that points at a gateway that supports automatic caching to turn it on. The chat/completions body then carries a top-level providerOptions: { gateway: { caching: 'auto' } }; Anthropic requires it for any caching, and cache reads cost 0.1x base input. session_affinity: true also sends the turn session_id (only when it is 1 to 128 characters of letters, digits, ., _, : or -) as x-session-affinity, which the gateway forwards to preserve provider cache locality; it never changes routing and does not guarantee a hit. Both options are off by default, and a profile without them sends exactly the body and headers it sent before. Setting caching on another provider family, or session_affinity without caching, is a configuration error. Cache hits appear in usage.cached_input_tokens and cache writes in usage.cache_write_input_tokens (see the execute-turn output schema).

On every provider route usage.input_tokens is the whole prompt, cache reads and writes included, so cached_input_tokens and cache_write_input_tokens are a breakdown of it and never exceed it. Uncached input is input_tokens - cached_input_tokens - cache_write_input_tokens. The Messages API reports its own input_tokens without the cache counts, so a messages_compatible turn adds the provider’s cache_read_input_tokens and cache_creation_input_tokens to it (once, for streamed turns too) and sets total_tokens to input_tokens + output_tokens when the provider gives no total. Chat Completions and Responses usage is passed through unchanged. A consumer that added the cache counts itself on the Messages route must stop, or it will count them twice.

A text content block may carry cache_control: { type: 'ephemeral' } with an optional ttl of 5m or 1h; anything else is rejected. It is forwarded on the message object on the Chat Completions route (as the gateway documents), as a content part on the Messages routes and ignored on the Responses route.

execute-turn also accepts an optional max_output_tokens per turn on openai_compatible and messages_compatible profiles. It is sent as max_tokens, can only lower the profile’s max_output_tokens ceiling (a larger value is refused with INVALID_INPUT before any provider call), and is a no-op on openai_compatible when neither is set. On messages_compatible, where max_tokens is required, an unset cap defaults to 16000 for a non-streaming turn and 64000 for a streaming one, never above the profile ceiling.

Governed inference through LumenFlow Cloud

Section titled “Governed inference through LumenFlow Cloud”

A lumenflow_cloud profile sends the turn to LumenFlow Cloud governed inference (POST /api/v1/inference), so Cloud routes, meters, limits and receipts it. The profile names a vendor-neutral capability_tier and a reference to a Cloud workspace key. It never names a model and never holds a provider key; Cloud resolves the tier to a route and holds the provider credentials (for example a workspace’s own key registered with Cloud).

agent_runtime:
  default_model: planner
  models:
    planner:
      provider: lumenflow_cloud
      capability_tier: standard
      api_key_env: LUMENFLOW_CLOUD_WORKSPACE_KEY
      base_url: https://cloud.example.com
      max_output_tokens: 4096
      caching: auto

api_key_env names the environment variable holding the Cloud workspace key, which is sent as the bearer credential and nowhere else; base_url or base_url_env is the Cloud origin, and the turn url must match it. reasoning_effort, thinking, session_affinity and image_input: true are configuration errors on this kind. A turn’s max_output_tokens may only lower the profile ceiling; Cloud requires a cap, so an unset one defaults to 16000. The request goes through the published control-plane SDK inference client; the engine builds no HTTP of its own.

What the turn returns. The normalized output is the same shape as other kinds, with provider.model carrying the capability tier. usage.input_tokens is the whole prompt, with cached_input_tokens and cache_write_input_tokens reported when Cloud reports them. The extra governed_route object carries what served the turn as Cloud reports it: capability_tier, provider_kind (Cloud’s own label, openai-compatible or hosted), provider_family and the evidence receipt (id, recorded, signed). Cloud meters the turn. A USD ceiling that needs a trusted per-request bound before dispatch is not provided by this kind, so rely on Cloud’s own limits and caps for that; the cost of a finished turn is covered under “Cost sources and precedence” below.

Cost sources and precedence. Cloud settles a per-call cost and reports it on the reply (and on the terminal usage event of a stream) as cost: an amount decimal string with six fractional digits, a currency and a basis (managed, byok_estimate or byok_declared, the last priced from prices the route declared; a basis this build does not know is kept as unknown_basis), or null when the call is unpriced. The turn surfaces it as usage.settled_cost (amount, currency, basis, source: provider). The cost of a turn comes from one of two sources, in this order:

  1. Provider-settled. When the usage carries a valid settled_cost in USD, max_cost_usd and cost_limit_usd accounting use it. The amount is read as an integer count of micro-USD and summed in exact micro-units, never with float arithmetic. This needs no price in the profile, so a caller that names only a capability tier still has a cost.
  2. Computed. Otherwise the cost is the usage times the profile’s *_usd_per_million_tokens prices, as for every other provider kind.

An unknown basis does not discard the cost: the amount and currency are the provider’s figure, so caps use them, and the telemetry says cost: provider_settled_unknown_basis. A null, absent or malformed settled cost (including a malformed amount), and any currency other than USD, is not used: the turn falls back to the computed cost, and with no price either the cost stays unknown (null). A cap treats an unknown cost as exhausted. A cost is never read as zero unless the provider settled exactly zero. The source of each model step is named: cost_source: provider | computed on the agent task step record, and cost: provider_settled | provider_settled_unknown_basis | unpriced in the lumenflow_cloud turn telemetry. Other provider kinds never carry settled_cost; their usage is priced from configuration as before.

Caller reference. The turn input accepts an optional opaque caller_reference (1 to 128 characters), sent to Cloud and echoed as governed_route.caller_reference, so a caller can group per-call costs by run. It is not part of Cloud’s idempotency key: a replayed call returns the original reference and cost. Any provider kind other than lumenflow_cloud refuses a turn that carries one.

Trace context. The turn input accepts an optional trace_context (traceparent, and tracestate when given), validated against the W3C Trace Context grammar before any call. On a lumenflow_cloud turn it is forwarded unchanged as the traceparent and tracestate request headers of the inference call, and the W3C trace id (32 hex) is recorded in the turn evidence as governed_route.trace_id, so local evidence and Cloud’s records join on it. When the turn carries a trace context and no caller_reference, the trace id is sent as the caller_reference; a caller-supplied caller_reference always wins and is never overwritten. Without a trace_context no id is invented. Any provider kind other than lumenflow_cloud refuses a turn that carries one.

Optional capabilities are used only when Cloud advertises them. The engine reads what the Cloud contract it carries advertises (tool turns, cache hints, streaming) and the turn telemetry (lumenflow_cloud in the tool metadata) records capabilities, what was applied, and every degradations entry with its reason:

  • Tool turns (tool definitions, assistant tool calls, tool results) cannot be dropped silently. The tool definitions go in tools, an assistant call in tool_calls, and a result is a flat tool message (tool_call_id, content, is_error). If the contract does not advertise them, a turn that carries a tool catalog or tool history fails before any network call with PROVIDER_CAPABILITY_UNSUPPORTED and details.capability: tools.
  • Cache hints (caching: auto) mark the last system message, the last tool definition and the final cacheable message of the request (a rolling breakpoint, so each turn reads the previous turn’s whole prefix; at most 3 markers) when advertised. Otherwise the turn is sent without hints and the telemetry lists cache_hints: not_advertised. Cloud’s cache_hint_applied answer is recorded when present.
  • Streaming (stream: true on the turn) uses the SDK stream reader when the contract advertises streaming and the reader is present: the telemetry records applied.streaming: true, and the terminal usage frame supplies the usage, receipt and settled cost. A call that fails after the first byte is a typed failure, never a partial reply; the reader bounds every event, the assembled text, the tool calls, the quiet time between reads and the whole stream, and an abort closes the connection. Otherwise the turn degrades to one non-streaming call, with the reason not_advertised, or sdk_stream_client_unavailable when the contract advertises streaming but no reader is present.

Refusals are typed and final. A Cloud refusal (no_route, routing_policy_refused, residency_violation, platform_ceiling_reached, limit_exceeded, insufficient_credit, insufficient_scope, endpoint_not_enabled, managed_not_enabled) fails the turn with PROVIDER_GOVERNED_REFUSAL, with details.cloud_code and a details.refusal_class of routing, residency, ceiling, limit, credit, access or size. The engine never retries it, never tries another route or provider, and never falls back to a different profile. 401 is PROVIDER_AUTHENTICATION_FAILED, 429 without a code is PROVIDER_RATE_LIMITED, and an unreachable Cloud is PROVIDER_TRANSPORT_ERROR.

provider_rate_limited. Cloud answers this code when the upstream provider chosen for the call returned HTTP 429. It is not a Cloud limit (limit_exceeded): Cloud made exactly one provider attempt and did not fail over. As a JSON 429 it carries Retry-After only when the provider gave a valid hint; on a stream that has already opened, it arrives as the terminal error event with an optional retry_after_seconds (an integer from 1 to 3600; any other value is dropped, never coerced). Both shapes become PROVIDER_RATE_LIMITED with details.cloud_code, details.request_id and, when sent, details.retry_after_seconds. The message says the upstream provider rate limited the call and it was not retried or routed elsewhere. The engine never retries it. After model output has started, the code stays provider_error and carries no hint.

Refusal classes. ceiling is platform_ceiling_reached only, the platform’s own ceiling. limit is limit_exceeded: a per-workspace, per-key or per-subject limit that an operator can raise, so a host never mistakes it for the platform ceiling. A limit refusal’s message names the limit, its scope and value, and when it resets if Cloud says so. Every refusal’s details keep the hosted fields that Cloud sent, each only when present and well formed and never invented: cloud_code, limit, scope, limit_value and resets_at (an RFC 3339 instant). The other classes are routing (no_route, routing_policy_refused), residency (residency_violation), credit (insufficient_credit), access (insufficient_scope, endpoint_not_enabled, managed_not_enabled) and size (below).

Finish reason and tool call count. A lumenflow_cloud turn result carries finish_reason, as Cloud reported it (lower-cased, with - as _), and tool_call_count, the number of tool calls the model requested in that turn. Both are present on streaming and non-streaming turns.

Size limits. Hosted caps what one turn may carry, and names the cap in the refusal: messages (4096 per request, counted on the wire, where the governed system instruction takes one slot), message_chars, request_body_bytes (default 1 MiB) and tokens_per_call (default 16,000 input tokens). A 400, 413 or 429 that names one of these fails the turn with PROVIDER_GOVERNED_REFUSAL, details.refusal_class: size, and details.limit and details.limit_value (the effective cap), plus details.scope when Cloud names one. It is never retried and never falls back to another route. The byte and token caps are registry limits, so an operator raises them for a workspace or key; the message count is a fixed backstop. The engine’s SDK client validates a request against the pinned contract before sending it, so a request over the pinned message count is refused locally with no call.

On a messages_compatible profile, caching: auto sends top-level cache_control: { type: "ephemeral" } (automatic caching) and puts a breakpoint on the last system text block, on the last tool definition and, as a rolling breakpoint, on the last cacheable content block of the final message (never a thinking block or an empty text block), so turn N+1 reads turn N’s whole prefix; system is then sent as an array of text blocks. A text block of an embedded system message and a tool_catalog entry may carry their own cache_control: { type: "ephemeral", ttl?: "5m" | "1h" }, which is forwarded unchanged; other shapes are refused. The Messages API allows 4 breakpoints per request and the automatic one takes a slot, so surplus markers are dropped, message-level markers first and oldest first. reasoning_effort is sent as output_config.effort and thinking as thinking. The request never carries temperature, top_p, top_k or a forced tool_choice, which current models refuse. Cache hits and writes come back as usage.cached_input_tokens and usage.cache_write_input_tokens, for streamed turns too. Without these options the system field stays a plain string.

default_model selects the profile a host should use when it does not supply a different model_profile.

The provider value selects the wire protocol:

ProviderProtocol contract
openai_compatibleChat Completions-style messages, choices, and [DONE] streams
openai_responsesResponses input/output items and typed semantic SSE events
messages_compatibleMessages-style content blocks and semantic stream events
lumenflow_cloudLumenFlow Cloud governed inference (POST /api/v1/inference) through the control-plane SDK client

The Responses adapter owns stateless continuation history: it sends store: false, requests function tools with strict: false, and replays the complete ordered output history on the next turn. OpenAI returns encrypted reasoning for this stateless mode; the adapter preserves it as opaque provider state. strict: false is deliberate because governed tool schemas may use JSON Schema features that do not satisfy OpenAI’s narrower strict structured-output requirements.

agent-runtime:execute-turn still receives a url field on each call. That url must match the resolved base URL for the selected model profile. This keeps the requested endpoint aligned with the profile-derived network allowlist and avoids drift between host input and pack config.

The policy_profiles map drives runtime-authored policy rules through the pack policy_factory:

  • allow_tools defines the only tools a task using that profile may use.
  • approval_required_tools is a stricter subset that pauses execution with APPROVAL_REQUIRED.

The trusted task spec selects one profile with execution_policy_profile. The kernel projects that value from the immutable task; request and model payloads cannot replace it. When profiles are configured, a task with a missing or unknown selection fails closed before discovery or execution.

The kernel file tools (fs:read, fs:write, fs:list, fs:edit and fs:search) and the opt-in code:run_command are kernel built-ins, not agent-runtime tools. A profile admits them only by listing their names in allow_tools, so a task whose profile omits fs:list, fs:edit, fs:search or code:run_command is denied them. List fs:edit or code:run_command in approval_required_tools to pause each call for approval. buildCodeWorkToolCatalog returns the catalog of exactly these six tools that a task is allowed to use. See Tool Execution for their parameters and error codes.

agent_runtime.connected_compute.policy_profile selects the configured profile that a trusted Connected Compute task persists for code assignments. The runner does not accept a profile from the assignment payload. An optional finite, nonnegative cost_limit_usd sets a USD ceiling for the assignment. When that ceiling is configured, a trusted per-request cost bound is required before a provider request is dispatched. The runner computes it from the profile’s prices (input bytes times the input price, plus max_output_tokens times the output price), so the profile needs both prices and an openai_compatible max_output_tokens ceiling; otherwise execution waits with cost_bound_unknown. A dispatch over the remaining bound waits with budget_exhausted and sends no provider request, and the reservation survives a restart. Actual cost is provider usage (cached tokens included) times the configured price; with an unknown price or usage it remains null rather than being inferred.

limits bounds host-driven loops and pack-owned orchestration:

  • max_steps, max_cost_usd, max_duration_s (ceilings of a governed agent task; see Agent Task Contract. A task may only tighten them.)
  • max_turns_per_session
  • max_tool_calls_per_session
  • max_input_bytes
  • max_stream_events
  • max_stream_snapshots
  • max_stream_bytes

Hosts can pass per-call limits only to tighten these ceilings. Missing, non-finite, non-positive, or widening values are rejected or clamped to the configured host bound.

A streamed turn emits one stream event per provider chunk and one snapshot for roughly every three output tokens, so a turn that writes a large file produces thousands of events. The built-in defaults admit that turn in one call:

LimitDefaultHard maximum
max_stream_events655361048576
max_stream_snapshots16384262144
max_stream_bytes16777216 (16 MiB)67108864 (64 MiB)

Exceeding a limit fails the turn with PROVIDER_LIMIT_EXCEEDED. max_response_bytes and the other provider bounds are unchanged.

The ceiling for a turn comes from the model profile (agent_runtime.models.<name>.max_stream_*), which is trusted workspace configuration and may raise or lower the defaults up to the hard maximum. agent_runtime.limits.max_stream_* and the per-turn limits.max_stream_* input can only tighten that ceiling. A value that is not a positive integer or exceeds the hard maximum is refused (CONFIGURATION_ERROR for a profile, INVALID_INPUT for turn input) before any provider call.

agent_runtime:
  models:
    builder:
      provider: messages_compatible
      model: builder-model
      api_key_env: BUILDER_API_KEY
      base_url: https://provider.example/v1/messages
      max_stream_events: 262144
      max_stream_snapshots: 65536
      max_stream_bytes: 33554432

agent_runtime.approval_classes maps a class name to its policy. A class is only a name that a tool or connector manifest declares; the pack knows no class name. Each class needs default_decision (allow, ask or deny), auto_approve_allowed and expiry_s. A class with auto_approve_allowed: false cannot default to allow and no narrower policy can auto-approve it. A class a tool names that is not configured denies. See the Agent Task Contract.

agent_runtime:
  approval_classes:
    publish:
      default_decision: ask
      auto_approve_allowed: false
      expiry_s: 900

agent_runtime.browser_session holds every value of the browser session tool contract; the pack has no default for any of them and a browser tool with the section missing is refused. See the Browser Session Contract.

agent_runtime:
  browser_session:
    idle_timeout_s: 300
    handoff_classes: [sign-in, payment] # names are the host classifier's, not the pack's
    page_text: { max_nodes: 500, max_depth: 12, max_text_chars: 20000 }
    redaction_rules:
      - { rule_id: passwords, match: input_type, value: password }

The pack uses two separate safeguards:

  1. Manifest-declared and capability-derived required_env Only declared environment variable names are passed into the sandboxed tool process.
  2. Capability-derived network allowlists Provider hosts are derived from the configured model profiles and intersected with workspace and lane policy before the turn runs.

This means a valid profile must declare both:

  • how the pack authenticates
  • which provider hosts the sandbox may contact

Connected Compute additionally requires the agent-runtime pack pin plus explicit workspace and lane grants for .agent-runtime/** read/write and the canonical provider host:port allowlist entry derived from the configured host default model. A payload URL, model profile, scope, or tool catalog cannot broaden those grants.