Configuration
The Agent Runtime Pack reads its configuration from the agent_runtime namespace in
workspace.yaml.
Enable The Pack
Section titled “Enable The Pack”Add an agent-runtime pin to the workspace pack list:
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.
Workspace Config
Section titled “Workspace Config”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:
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.
Model Profiles
Section titled “Model Profiles”Each profile declares:
| Field | Meaning |
|---|---|
provider | Adapter family used for request/response normalization |
model | Provider-specific model identifier carried into the normalized response. Not allowed on lumenflow_cloud, which names a capability_tier instead |
capability_tier | lumenflow_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_ms | lumenflow_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_env | Environment variable that holds the credential for the selected profile (the Cloud workspace key for lumenflow_cloud) |
base_url / base_url_env | Absolute 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_input | Optional boolean. Set true only for a vision-capable model; absent or false refuses any turn with an image |
caching | Optional, auto. openai_compatible: adds providerOptions.gateway.caching = "auto". messages_compatible: adds top-level cache_control plus system, tool and rolling final-message breakpoints |
stream_usage | Optional 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_affinity | Optional boolean, requires caching: auto on openai_compatible. Sends the turn session_id as the opaque x-session-affinity header |
reasoning_effort | Optional. 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 |
thinking | Optional, messages_compatible only: { type: adaptive, display?: summarized | omitted }, forwarded as thinking. budget_tokens and type: enabled are refused |
max_output_tokens | Optional 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_tokens | Optional 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_tokens | Optional prices for prompt-cache reads (default: the input price) and writes (a cache write with no price reports unknown cost) |
max_stream_events | Optional. Ceiling on parsed stream events per streamed turn (default 65536, hard maximum 1048576) |
max_stream_snapshots | Optional. Ceiling on stream deltas (live progress, not stored) per streamed turn (default 16384, hard maximum 262144) |
max_stream_bytes | Optional. 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).
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:
- Provider-settled. When the usage carries a valid
settled_costin USD,max_cost_usdandcost_limit_usdaccounting 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. - Computed. Otherwise the cost is the usage times the profile’s
*_usd_per_million_tokensprices, 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 intool_calls, and a result is a flattoolmessage (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 withPROVIDER_CAPABILITY_UNSUPPORTEDanddetails.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 listscache_hints: not_advertised. Cloud’scache_hint_appliedanswer is recorded when present. - Streaming (
stream: trueon the turn) uses the SDK stream reader when the contract advertises streaming and the reader is present: the telemetry recordsapplied.streaming: true, and the terminal usage frame supplies the usage, receipt and settledcost. 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 reasonnot_advertised, orsdk_stream_client_unavailablewhen 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.
Messages API caching, effort and thinking
Section titled “Messages API caching, effort and thinking”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:
| Provider | Protocol contract |
|---|---|
openai_compatible | Chat Completions-style messages, choices, and [DONE] streams |
openai_responses | Responses input/output items and typed semantic SSE events |
messages_compatible | Messages-style content blocks and semantic stream events |
lumenflow_cloud | LumenFlow 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.
Input URL Matching
Section titled “Input URL Matching”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.
Persisted Policy Profiles
Section titled “Persisted Policy Profiles”The policy_profiles map drives runtime-authored policy rules through the pack policy_factory:
allow_toolsdefines the only tools a task using that profile may use.approval_required_toolsis a stricter subset that pauses execution withAPPROVAL_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
Section titled “Limits”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_sessionmax_tool_calls_per_sessionmax_input_bytesmax_stream_eventsmax_stream_snapshotsmax_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.
Streamed-turn safety limits
Section titled “Streamed-turn safety limits”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:
| Limit | Default | Hard maximum |
|---|---|---|
max_stream_events | 65536 | 1048576 |
max_stream_snapshots | 16384 | 262144 |
max_stream_bytes | 16777216 (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.
Approval Classes
Section titled “Approval Classes”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.
Browser Session
Section titled “Browser Session”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.
Credentials And Network Allowlisting
Section titled “Credentials And Network Allowlisting”The pack uses two separate safeguards:
- Manifest-declared and capability-derived
required_envOnly declared environment variable names are passed into the sandboxed tool process. - 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.