agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
1,389 lines • 71.1 kB
TypeScript
/**
* provider-contract/types — the Ports of the hexagonal architecture.
*
* Pattern: Adapter (GoF, Design Patterns ch. 4) + Ports-and-Adapters
* (Cockburn, 2005).
* Role: Contracts for every external dependency the library reaches for:
* LLM providers, memory stores, permission checkers, pricing tables.
* Emits: N/A (interfaces only).
*
* Concrete adapters (AnthropicProvider, PineconeStore, PermissionPolicy,
* ...) implement these contracts. `core/` and `core-flow/` depend only on
* these interfaces — never on concrete adapters.
*/
import type { ContextRole, ContextSource } from '../events/types.js';
import type { ThinkingBlock } from '../thinking/types.js';
export interface LLMMessage {
readonly role: ContextRole;
readonly content: string;
/** For `role: 'tool'` — the tool_use id this result corresponds to. */
readonly toolCallId?: string;
/** For `role: 'tool'` — the tool name this result corresponds to. */
readonly toolName?: string;
/**
* For a refused `role: 'tool'` result only: the same finite argument failure
* carried by its attempted call. Anthropic projects this pair as recovery
* text, never as a successful parse of substituted empty input.
*/
readonly toolArgumentsFailure?: LLMResponse['toolCalls'][number]['argumentsFailure'];
/**
* Producer-owned notice origin, recorded only under findings admission.
* `not-entered` means the producer refused before tool.execute; an error
* thrown inside execute remains a result. `input-cancelled` is the host's
* notice, not a completed result, and says nothing about prior tool entry.
* `outcome-unknown` is the host's notice for a call left open by a run that
* stopped after a person's decision was used (`hosting/answeredPause.ts`):
* whether it entered is not known, and the notice is not its result.
* Absent on old history: no missing origin is reconstructed from text.
* JSON/checkpoint safe; stripFrameworkFields removes it before transport.
*/
readonly toolResultOrigin?: 'not-entered' | 'input-cancelled' | 'outcome-unknown';
/**
* For `role: 'assistant'` only — the tool calls the LLM requested in this
* turn. Required for providers (Anthropic, OpenAI) that need to round-trip
* tool_use blocks across iterations: when the next `complete()` includes
* a `role: 'tool'` message, the provider reconstructs the matching
* `tool_use` block on the previous assistant turn from this field.
* Empty array on text-only turns; undefined for non-assistant roles.
*
* `providerMeta` (9.29.0) rides back UNCHANGED — it is the same bag the
* response put there, and for Gemini it holds the `thoughtSignature` without
* which the model refuses the turn after a tool call. See
* {@link LLMResponse}'s `toolCalls[].providerMeta`. Unlike `injectedBy`, it
* is NOT stripped on the way to a provider: it exists to be sent.
*/
readonly toolCalls?: readonly {
readonly id: string;
readonly name: string;
readonly args: Readonly<Record<string, unknown>>;
/** Stream integrity evidence retained with the attempted call; never tool input. */
readonly argumentsFailure?: LLMResponse['toolCalls'][number]['argumentsFailure'];
readonly providerMeta?: Readonly<Record<string, unknown>>;
}[];
/**
* v2.14 — Thinking blocks emitted by the LLM on assistant turns.
*
* Required for Anthropic extended-thinking + tool-use flows: signed
* blocks MUST be echoed BYTE-EXACT in subsequent assistant turns or
* Anthropic's API rejects with 400. The framework persists blocks
* here so the AnthropicProvider's serializer (Phase 4b) can restore
* them on the next request.
*
* **Persistence model — DIFFERENT from `ephemeral`:**
* - `ephemeral` messages: NOT persisted to scope.history
* - `thinkingBlocks`: PERSISTED (required for signature round-trip)
*
* Visible to recorders + audit by default. Use
* `RedactionPolicy.thinkingPatterns` (Phase 3) to scrub sensitive
* reasoning content before audit-log adapters fire.
*
* Empty array OR undefined when no thinking is present (most calls).
*/
readonly thinkingBlocks?: readonly ThinkingBlock[];
/** Replay layout and local consistency evidence (including known loss); JSON/checkpoint safe. */
readonly replay?: import('../thinking/replay.js').MessageReplay;
/**
* v2.13 — PERSISTENCE flag (NOT a visibility flag). When `true`:
* • The message IS sent to the LLM as part of the next request
* (visible to the model, counts toward its context window).
* • The message is OBSERVABLE via narrative/recorders/audit log
* (visible to humans for debugging + forensics).
* • The message is NOT persisted to `scope.history` after the gate
* loop that produced it completes — long-term memory writes,
* `getNarrative()` snapshots, and downstream consumers see only
* non-ephemeral messages.
*
* Use case: Instructor-style schema retry. The reliability gate
* appends `{ role: 'user', content: feedbackForLLM, ephemeral: true }`
* before retry — the LLM sees the validation feedback for the next
* call, but the conversation history (and any memory persistence
* downstream) sees only the final accepted exchange.
*
* Audit-trail safety: ephemeral DOES NOT mean invisible to security
* review. `getNarrative()`, recorders, and the typed-event stream all
* see ephemeral messages; only the persistent conversation log filters
* them out. An attacker cannot use the ephemeral marker to construct
* audit-invisible prompts.
*/
readonly ephemeral?: boolean;
/**
* v7.21 — WHO let this message into the window.
*
* Stamped by the agent's `Deliver` stage on a message that came from a
* `slot: 'messages'` Injection rather than from the conversation. It is the
* stable marker the messages slot reads to attribute the message to its
* injection (source / sourceId / reason) instead of inferring a baseline
* source from the role — so one wire message produces exactly one
* `context.injected` record, naming whoever put it there.
*
* **Never reaches a provider.** `callLLM` strips this field from every
* message before the request is handed to `provider.complete()` / `stream()`,
* so no adapter — first-party or consumer-authored — can leak framework
* metadata onto a wire, even one that serializes a message wholesale.
* Stripping removes a field, never a message, so wire indices are unchanged
* (which is what lets a `CacheMarker{field:'messages'}` name a real position).
*
* Absent on every message that came from the conversation itself.
*/
readonly injectedBy?: {
/** The `Injection.id` that produced this message. */
readonly injectionId: string;
/** The injection's flavor — the `source` the slot records. */
readonly flavor: ContextSource;
/** The injection's description, when it had one. */
readonly reason?: string;
/** The ReAct iteration whose boundary delivered it. */
readonly iteration: number;
};
/**
* 9.113.0 — this `role: 'tool'` message answers a call that was NEVER
* DISPATCHED. The library wrote it; no gate judged the call and no tool ran.
*
* Stamped by the batch settlement (`core/agent/stages/toolCalls.ts` ·
* `settleBatch`, which takes each copy from `notDispatchedMarker`). The
* model proposed several calls in one turn, one of them paused the run, and
* the resume answered each call after it with a fixed sentence
* (`notDispatchedResult`) instead of running it. The sentence is what the
* MODEL reads. This field is the same fact as data, for the readers that ask
* "did this call run?" or "is this message a tool's result?":
* `security/extractSequence.ts` · `extractSequence`, which builds the
* `sequence` a permission policy judges (pairing each marker with the ONE
* proposal it answers, by position — a provider may reuse an id); the
* empty-lookup check's producer corpus (`toolCalls.ts` ·
* `producerCorpusOf`); the check-in evidence trail
* (`core/checkin.ts` · `CheckInTrail` — its `toolCalls` are the calls
* already completed); the window's result naming
* (`core/agent/window/toolNames.ts` · `toolNameOfMessage` — the
* last-tool-result pin, the drop notice, the dangling-reference check); and
* the trace toolpack's `inspect_tool_call`
* (`trace-toolpack/traceToolpack.ts` · `notDispatchedOf`); and, under
* `.findings()`, the offer, the identity source, the piece's `undeclared:`
* line, the collapse, the window's `droppedStandings` and a turn's standing
* (`core/agent/findings/offer.ts`, `findings/serve.ts` · `collapseJudged`,
* `stages/window.ts`, `core/agent/window/ledgerFactPins.ts` ·
* `turnStandingOf` — the `'ledger-fact'` pin and
* `WindowStrategyInput.standingOf`) — a settled message is not a result to
* judge. None of them parses the sentence.
*
* **One definition, two carriers.** The settled call's two brackets on the
* event stream carry the same fact under the same name, typed off THIS field
* (`events/payloads.ts` · `ToolStartPayload.notDispatched`,
* `ToolEndPayload.notDispatched`), so a reader of the stream alone never has
* to guess from a `durationMs: 0` bracket whether the call ran.
*
* **Never reaches a provider.** `core/agent/composeRequest.ts` ·
* `stripFrameworkFields` removes it with `injectedBy` before a request
* exists, so the wire carries the sentence and nothing else.
*
* Absent on every other message.
*/
readonly notDispatched?: {
/** The call in the same batch the run paused on — the one the sentence names. */
readonly pausedCall: {
readonly toolCallId: string;
readonly toolName: string;
};
};
/**
* THE TOOL-BYTES BOUNDARY (honesty layer 2) — on a `role: 'tool'` message
* whose `content` the library annotated with a note that carries a VALUE
* (the inputs layer's "the call ran with "2h", the value the tool's rule
* assumes"), the length of the tool's OWN delivered text: `content` up to
* this index is the tool's; everything after it is the library's.
*
* Every reader that treats a result's content as the TOOL's words reads
* through the cut, and through ONE owner of it (`lib/toolBytes.ts` ·
* `toolBytesOf`): the evidence index (`core/agent/evidence/evidenceIndex.ts`
* · `evidenceFromHistory`), so a value that sits only in the note grounds no
* answer — without it, the next call could cite this result as the source of
* "2h" and an assumption would become an observed value; the answer's
* standing (`core/agent/assessment/assess.ts` · `assessAnswer`) and the
* answer account's in-view results (`answer-account/facts/inView.ts`),
* so the note never breaks the one emptiness reader's parse and hides an
* empty or absent result; the unsupported-argument seam's grounded corpus
* (`core/agent/stages/callLLM.ts`, cut on the committed twin — the wire copy
* has lost this field) and the empty-lookup seam's producer corpus
* (`core/agent/stages/toolCalls.ts` · `producerCorpusOf`), so the note never
* vouches for an argument. The model still reads the whole `content`: it
* needs the value to reason about what ran.
*
* **Never reaches a provider.** `core/agent/composeRequest.ts` ·
* `stripFrameworkFields` removes it with `injectedBy` and `notDispatched`.
* Present only on a message the layer annotated, so every other run indexes
* exactly what it always did.
*/
readonly toolChars?: number;
/**
* THE EXEMPTION LINEAGE OF A FOLD — on a compaction summary frame
* (`core/agent/window/summarize.ts` · `buildSummaryMessage`), the lookup
* forms the EXEMPT corpus held for the messages this summary folded away:
* the person's and the app's user/system turns (and the lineage of any
* earlier summary inside the span), taken from those ORIGINAL messages at
* fold time by `core/agent/evidence/evidenceIndex.ts` · `exemptLineageOf`.
*
* The summary's own TEXT is a model's claim, so it exempts nothing
* (`evidenceIndex.ts` · `exemptFromRun` skips the frame's content and reads
* this field instead). Without the field a value the person gave in a
* folded turn would lose its exemption; without skipping the text, a value
* the summarizer invented would gain one.
*
* Rides the summary wherever the history goes (the conversation checkpoint
* included), so a restored summary keeps the lineage it was folded with; a
* summary frame WITHOUT it (hand-built, or folded before this field
* existed) exempts nothing. Absent when the fold carried no exempt value.
*
* **Never reaches a provider.** `core/agent/composeRequest.ts` ·
* `stripFrameworkFields` removes it with the other framework fields.
*/
readonly foldedExempt?: readonly string[];
}
/**
* The roles a provider can carry INSIDE the `messages` array.
*
* `'tool'` is deliberately absent: a tool message is an answer to a specific
* `tool_use` id, so it cannot be authored by an injection — there is no call
* for it to answer. See `LLMProvider.carriesInMessages`.
*/
export type WireRole = 'system' | 'user' | 'assistant';
/**
* The floor every known wire supports. Used for any provider that does not
* declare `carriesInMessages` — a third-party adapter is assumed to carry
* only what all of them do, never more.
*/
export declare const DEFAULT_CARRIES_IN_MESSAGES: readonly WireRole[];
export interface LLMToolSchema {
readonly name: string;
readonly description: string;
readonly inputSchema: Readonly<Record<string, unknown>>;
}
export interface LLMRequest {
readonly systemPrompt?: string;
readonly messages: readonly LLMMessage[];
readonly tools?: readonly LLMToolSchema[];
readonly model: string;
readonly temperature?: number;
readonly maxTokens?: number;
readonly stop?: readonly string[];
readonly signal?: AbortSignal;
/**
* Cache markers (v2.6+) — provider-agnostic prefix-cache hints
* populated by `CacheStrategy.prepareRequest` after the agent's
* CacheGate decider routes to `apply-markers`. Each marker
* identifies a cacheable prefix in `system` / `tools` / `messages`.
*
* Providers that support caching (Anthropic, Bedrock-Claude) read
* this field and translate to their wire format. Providers without
* cache support (OpenAI auto-cache, Mock, NoOp) ignore it.
*/
readonly cacheMarkers?: readonly import('../cache/types.js').CacheMarker[];
/**
* v2.14 — request the LLM emit reasoning/thinking content on this call.
*
* Activation: presence of this field tells the provider to ASK for
* thinking. It is the INTENT, never a wire shape: each adapter maps it to
* what the model takes. The Anthropic adapters ask their declared
* `thinkingMode(model)` — a `'budget'` model gets `thinking: { type:
* 'enabled', budget_tokens: budget }`, an `'adaptive'` model (Claude 4.7
* and later) gets `thinking: { type: 'adaptive', display: 'summarized' }`
* with no budget, and a `'none'` model is refused with an
* `UnsupportedThinkingError` before anything is sent. OpenAI ignores it
* (o1/o3 thinking is selected at the model id level, not per-request).
*
* `budget` is the maximum reasoning tokens the model may spend where the
* model takes a budget (Anthropic: a whole number of at least 1024). On
* an adaptive model it is not sent; it only raises `max_tokens` to at
* least `budget + 1024`, so a long think has room to finish.
*
* Independent from `LLMMessage.thinkingBlocks` (the response side):
* - `request.thinking` = activation (consumer ASKS for thinking)
* - `message.thinkingBlocks` = round-trip (consumer ECHOES prior
* assistant turn's signed blocks back to the model)
*
* Set via `AgentBuilder.thinking({ budget })` — applied to every
* LLM call the agent makes. Leave undefined to call without thinking
* (the v2.13 default).
*/
readonly thinking?: {
readonly budget: number;
};
/**
* v7.26 — force the model to answer through one named tool.
*
* One arm, because one arm is what the library needs and can keep a
* promise about: `.outputSchema(parser, { strategy: 'tool-forced' })`
* presents the schema as a synthetic tool and forces the choice, so the
* shape is constrained at generation instead of requested in prose.
* Anthropic spells it `{type:'tool',name}`, OpenAI
* `{type:'function',function:{name}}`, Bedrock Converse
* `toolConfig.toolChoice.tool.name` — the field is the one word all three
* agree on, and each adapter writes its own dialect.
*
* If `forcedToolChoiceFor(provider, model)` answers false, the agent never
* sends this field: it refuses at
* run start instead, naming the provider and the model. Silently sending it
* to a wire that ignores it would turn a guarantee into a suggestion with
* nothing in the recording to say so — and a model that rejects it (Claude
* Opus 5.5, Sonnet 5.5, Fable 5.1, Mythos 5.1) is refused by the Anthropic
* adapters before sending, with an `UnsupportedToolChoiceError`.
*/
readonly toolChoice?: {
readonly type: 'tool';
readonly name: string;
};
/**
* Constrain the model's TEXT answer to a JSON Schema with the provider's
* own structured output — `.outputSchema(parser, { strategy: 'native' })`.
* The Anthropic adapters write it as `output_config: { format }`
* (https://platform.claude.com/docs/en/build-with-claude/structured-outputs).
*
* Same rule as {@link toolChoice}: when `nativeOutputSchemaFor(provider,
* model)` answers false the agent refuses at run start, naming both, and an
* adapter that cannot put it on its wire refuses with an
* `UnsupportedNativeOutputError` before anything is sent — never drops it.
* The schema is the wire copy `nativeOutputFormat` built; send it as-is.
*/
readonly outputFormat?: {
readonly type: 'json_schema';
readonly schema: Readonly<Record<string, unknown>>;
};
}
export interface LLMResponse {
/** Ordered replay evidence, retained with the assistant message. */
readonly replay?: import('../thinking/replay.js').MessageReplay;
/** Leaf-side omissions, also available to standalone callers without hooks. */
readonly replayOmissions?: readonly import('../thinking/replay.js').ThinkingReplayOmission[];
readonly content: string;
readonly toolCalls: readonly {
readonly id: string;
readonly name: string;
readonly args: Readonly<Record<string, unknown>>;
/**
* The provider observed malformed or unfinished streamed tool arguments.
* The call must not execute, even with schema validation disabled. `args`
* is only a placeholder on this branch, not evidence of valid empty input.
* Raw argument fragments are deliberately not retained here.
*/
readonly argumentsFailure?: {
readonly reason: 'invalid-json' | 'invalid-argument-shape' | 'incomplete-stream';
};
/**
* 9.29.0 — what the PROVIDER needs back with this call, opaque to
* everything in between.
*
* Same escape-hatch shape and the same rules as
* `ThinkingBlock.providerMeta`: plain data only, nothing
* identity-bearing beyond what the wire itself demands, and no framework
* behaviour keys on it. The framework's whole job is to keep it attached
* to its call — through `scope.llmLatestToolCalls`, into the assistant
* turn in `history`, across a checkpoint — and hand it back to the same
* adapter on the next request.
*
* Why it exists: Gemini signs the reasoning behind a function call and
* REFUSES the following turn when the signature does not come back
* (`thought_signature`, HTTP 400 — measured in the field, after the tool
* had already run). The signature belongs to the tool call, not to the
* message and not to a thinking block, so it needed a home on the call.
* Providers that sign nothing leave this undefined and cost nothing.
*/
readonly providerMeta?: Readonly<Record<string, unknown>>;
}[];
readonly usage: {
readonly input: number;
readonly output: number;
readonly cacheRead?: number;
readonly cacheWrite?: number;
/**
* v2.14 — count of reasoning/thinking tokens used by the model.
* Distinct from `output` (which is visible-content tokens).
*
* Semantics:
* - `undefined` — provider doesn't expose / no thinking enabled
* on this call / call without extended thinking
* - `0` — thinking enabled but model produced no
* thinking tokens this call
* - `>0` — actual reasoning token count (billing-relevant
* for both Anthropic extended thinking and
* OpenAI o1/o3 reasoning_tokens)
*
* Cost dashboards reading `cost.tick` events should track this
* separately from `output` — pricing differs (Anthropic charges
* extended thinking at output rates; OpenAI o1/o3 reasoning tokens
* are billed as a separate line item).
*/
readonly thinking?: number;
};
readonly stopReason: string;
readonly providerRef?: string;
/**
* v2.14 — Provider-specific raw thinking data, opaque to the
* framework. Providers that support extended thinking populate this
* with their native shape (Anthropic: array of `{type, thinking,
* signature}` blocks; OpenAI: `reasoning_summary` value; custom:
* whatever the provider emits). The framework hands this to a
* configured `ThinkingHandler.normalize(rawThinking)` to produce
* the normalized `ThinkingBlock[]` that lands on
* `LLMMessage.thinkingBlocks`.
*
* Undefined when the provider has no thinking content for this call
* — most calls (gpt-4o, claude without extended thinking enabled,
* etc.). The thinking subflow's stage early-returns in this case.
*/
readonly rawThinking?: unknown;
/**
* 9.60.0 — what this request ACTUALLY carried, read back from the
* serialized body after every transform (see adapters/llm/wireManifest.ts,
* which builds it; the SHAPE lives here because it is part of the provider
* port, and the skill-graph fence rightly refuses this file reaching
* anything past the port).
*
* The wire seam's evidence: the recorded defect was an adapter still
* serializing four tool schemas after the internal frame said they were
* removed — invisible to any check that reads only the pre-serialization
* IR. Absent when the adapter states no manifest, and the wire check then
* treats the request as INCOMPARABLE (never as "nothing crossed"); an
* empty `toolNames` is the opposite — a stated zero.
*/
readonly wireManifest?: WireToolManifest;
}
/** The structural record of what one request actually carried on the wire. */
export interface WireToolManifest {
/** Tool names as they appear in the serialized body, in wire order. */
readonly toolNames: readonly string[];
/**
* Present only when the serialized body carried a structured-output format
* (Anthropic's `output_config.format`) — its `type`, read back from the
* body. Absent means none crossed. A `strategy: 'native'` agent refuses a
* stated manifest without it: the provider declared the capability and the
* request went out unconstrained.
*/
readonly outputFormat?: 'json_schema';
}
export interface LLMChunk {
readonly tokenIndex: number;
/** Token text. Empty for the terminal chunk (`done: true`). */
readonly content: string;
/** True only for the final chunk in a stream. */
readonly done: boolean;
/**
* Authoritative response payload, populated ONLY on the final chunk
* (`done: true`). Carries `toolCalls`, `usage`, `stopReason` — the
* fields that drive the ReAct loop. The `content` mirrors the
* concatenation of all non-terminal chunks; consumers can use
* either source.
*
* Streaming providers SHOULD populate this. Older providers that
* yield only text and end with `done: true` (no `response`) are
* still supported — Agent falls back to `complete()` for the
* authoritative payload in that case.
*/
readonly response?: LLMResponse;
/**
* v2.14 — streaming thinking-content tokens. Parallel to `content`
* but for the model's reasoning chain rather than visible output.
* Set on chunks that carry thinking deltas (Anthropic emits these
* via `content_block_delta` events with `delta.type === 'thinking_delta'`);
* undefined or empty on chunks that carry only visible-content tokens.
*
* Frameworks: this field drives `agentfootprint.stream.thinking_delta`
* events when a `ThinkingHandler.parseChunk()` returns one. Consumers
* who want to render thinking-as-it-streams subscribe to that event.
*
* Default consumer behavior: thinking tokens are not surfaced to end
* users unless a consumer explicitly subscribes to the
* `agentfootprint.stream.thinking_delta` event (or renders it through a
* live-status strategy).
*/
readonly thinkingDelta?: string;
}
/**
* v7.8 — what a resilience decorator DID during one provider call.
*
* Plain data only (strings + numbers), so a report drops straight into a
* typed event payload and survives `structuredClone`. Field names are
* deliberately 1:1 with the declared event payloads
* (`FallbackTriggeredPayload`, `ErrorRetriedPayload`,
* `ErrorRecoveredPayload`) so the in-run call site maps a report to an
* event without renaming or synthesizing anything.
*
* Produced by exactly one decorator per `kind`:
* • `'fell-back'` ← `withFallback`
* • `'retried'` / `'recovered'` ← `withRetry`
* • `'circuit-changed'` ← `withCircuitBreaker` (9.32.0)
*/
export type ResilienceReport = {
readonly kind: 'fell-back';
/** Provider tried first, which failed. */
readonly primary: string;
/** Provider called instead — the one that actually served. */
readonly fallback: string;
/** Message of the error that triggered the fallback. */
readonly reason: string;
} | {
readonly kind: 'retried';
/** 1-based number of the attempt ABOUT TO START (2 = first retry). */
readonly attempt: number;
readonly maxAttempts: number;
/** Message of the error that caused this retry. */
readonly lastError: string;
/** How long the decorator waits before the attempt — the stated wait when one won. */
readonly backoffMs: number;
/**
* The wait the failure itself STATED (a `Retry-After` header, a
* gateway's "try again in N seconds"), in ms, as the adapter declared
* it on `retryAfterMs`. Absent when it stated none. `backoffMs` is
* max(the schedule, this), capped by the policy's `maxDelayMs` — so a
* reader sees both what was asked and what was done.
*/
readonly statedWaitMs?: number;
/**
* Classification OF THE ERROR — **not** of the predicate's
* reasoning. `shouldRetry` returns a bare boolean, so when a custom
* predicate is in force the decorator cannot know *why* it said yes;
* this field reports what the error looked like instead, derived
* from the same `status`/`statusCode` fields `defaultShouldRetry`
* inspects. One of: `'http-429'` | `'http-5xx'` | `'http-4xx'` |
* `` `http-${code}` `` | `'no-status'`.
*/
readonly reason: string;
} | {
readonly kind: 'recovered';
/** 1-based attempt that finally succeeded. Always >= 2. */
readonly attempt: number;
readonly totalDurationMs: number;
} | {
/**
* v9.32 — the breaker moved between `closed` / `open` / `half-open`.
*
* Until this arm existed `withCircuitBreaker` reported NOTHING through
* the in-run channel: `onStateChange` was the only way to see a trip,
* and that hook fires at consumer level where the run ids are
* synthetic. An independent reviewer (2026-08-13, on a local harness of
* scripted failures) watched a breaker open after two failures, serve
* from fallback, half-open after cooldown and close after two probes —
* all correct, and all invisible
* to the typed stream. So the same trip is now on the record with the
* run's real correlation ids, because every transition happens INSIDE a
* call and there is nothing to synthesize.
*
* `onStateChange` is unchanged and still fires beside this: it is the
* consumer's own hook, and a Redis-backed counter built on it must not
* start depending on whether a run happened to be in flight.
*/
readonly kind: 'circuit-changed';
/** The state entered. Transitions only — a no-op re-entry never reports. */
readonly state: 'closed' | 'open' | 'half-open';
/** WHY, in the breaker's own words (`'3 consecutive failures'`,
* `'cooldown elapsed'`, `'half-open probe failed'`). Never an error's
* message — the failure that tripped it is reported by whoever threw. */
readonly reason: string;
/** WHICH provider this breaker wraps. A stack of breakers under one
* fallback produces one stream, and this is what tells them apart. */
readonly providerName: string;
};
/**
* v7.8 — optional per-call hooks the CALLER hands a provider.
*
* Lets a resilience decorator report what it did to whoever invoked it,
* without the decorator knowing anything about runs, scopes, or events.
* The channel rides the CALL (not the factory) because decorators are
* constructed by the consumer before any run exists.
*
* Passed by agentfootprint's in-run LLM call sites, which translate each
* report into an already-declared typed event with real correlation ids.
* Outside a run nothing passes hooks, so `hooks` is `undefined` and every
* report site short-circuits — standalone decorator behaviour is
* unchanged.
*
* ⚠ **IF YOU WRITE A PROVIDER WRAPPER, FORWARD THIS PARAMETER.** It is the
* one silent-failure trap in the design. A wrapper that declares
* `complete(req)` and calls `inner.complete(req)` still type-checks
* perfectly — `hooks` is optional, and TypeScript has never rejected an
* implementation for taking FEWER parameters than its signature — so
* dropping it produces no compile error, no runtime error, and no test
* failure. What it produces is a decorated provider that goes DARK the
* moment it is placed underneath: the reports still happen, and nothing
* receives them. Every wrapper shipped in this library forwards (the three
* `src/resilience/` decorators, and EVERY class-form / Azure-shaped wrapper
* in `src/adapters/llm/` — `foundry()` and `foundryLocal()` included; the
* count is deliberately not written down, because a stale count reads as an
* unaudited wrapper), so the trap can only be sprung by a
* consumer-authored wrapper — `myWrapper(withRetry(p))`. There is no way to
* police it from here; the only defence is this note and the one in the
* resilience guide's "honest limits".
*/
export interface LLMCallHooks {
/**
* The actual leaf provider omitted eligible completed-turn thinking before
* transport. Wrappers forward this channel unchanged, like onResilience.
* The run records metadata and keeps the permitted omission in its history.
*/
readonly onReplayPrepared?: (preparation: import('../thinking/replay.js').ReplayPreparation) => void;
/**
* Called once per resilience decision (a fallback, a retry, a
* recovery). Decorators forward this hook inward unchanged, so a
* stack of decorators produces one concatenated report stream with no
* duplication.
*/
readonly onResilience?: (report: ResilienceReport) => void;
}
export interface LLMProvider {
readonly name: string;
/**
* How this provider's wire caches a repeated prompt prefix (`PromptCaching`,
* from `agentfootprint/cache`). The agent picks its cache strategy from
* this, never from `name`. Absent = no caching the agent drives or meters.
* A wrapper must forward it, like `carriesInMessages`.
* A model-aware declaration returns undefined for an unknown/unsupported
* model. `cacheStrategyFor(provider, model)` resolves either form; adapters
* must consult the same rule before putting cache markers on the wire.
*/
readonly promptCaching?: import('../cache/types.js').PromptCaching | ((model: string) => import('../cache/types.js').PromptCaching | undefined);
/**
* The handler that normalizes this wire's thinking (`rawThinking`). The
* agent uses it unless `.thinkingHandler()` overrides it; absent = no
* thinking stage. Never chosen by `name` — a wrapper must forward it.
*/
readonly thinkingHandler?: import('../thinking/types.js').ThinkingHandler;
/**
* Which thinking request each model takes on this wire — `'budget'`,
* `'adaptive'` or `'none'` (`ThinkingMode`, from `agentfootprint/providers`),
* asked with the model id a request names (the adapter resolves its own
* shorthand, e.g. `'anthropic'`). The adapter maps `LLMRequest.thinking` to
* the right wire shape for that model; the agent reads this at build to
* refuse `.thinking()` for a model that cannot think and to say, in dev
* mode, when `'adaptive'` will not send the budget.
*
* Absent = no per-model claim: requests carry `thinking: { budget }` and the
* adapter decides. A plain function of the model id — it must not use
* `this`, because a wrapper forwards it as a value, like `carriesInMessages`;
* `withFallback` answers for its pair.
*/
readonly thinkingMode?: (model: string) => import('../thinking/types.js').ThinkingMode;
/**
* v7.21 — which roles this provider carries INSIDE the `messages` array.
*
* The wires disagree, and the disagreement is invisible from the outside:
* the Anthropic-family adapters (Anthropic, Bedrock, Browser Anthropic) DROP
* a `role: 'system'` message inside `messages` because system rides a
* separate top-level field, while the OpenAI-family adapters carry it. So a
* `slot: 'messages'` injection with `role: 'system'` would arrive on one
* provider and vanish on another — and nothing in the recording would tell
* the two apart. Declaring the capability is what lets the engine refuse at
* run start instead, naming the provider and the roles it does carry.
*
* Consulted at DELIVERY time by the agent's `Deliver` stage. A role that is
* not listed is REFUSED, never silently re-roled: changing who appears to
* speak is a meaning change the app must make, not the library.
*
* **Optional, and absence is not "carries everything"** — a provider that
* omits it is treated as `['user', 'assistant']`
* ({@link DEFAULT_CARRIES_IN_MESSAGES}), the floor every known wire
* supports. Declare it if your adapter carries more.
*
* A WRAPPER must forward it (the three `src/resilience/` decorators do);
* `withFallback` publishes the INTERSECTION of the two providers it holds,
* because a role only one of them carries is a role the call might drop.
*/
readonly carriesInMessages?: readonly WireRole[];
/**
* v7.26 — whether this adapter puts {@link LLMRequest.toolChoice} on its
* wire as a forced choice of one named tool.
*
* **Absence means NO, not "probably".** That is the opposite of
* `carriesInMessages`, whose absence means the floor every wire supports,
* and the difference is what the two capabilities are for. A role that
* quietly vanishes costs a message; a tool choice that quietly vanishes
* costs the guarantee the consumer selected the strategy FOR — the model
* would answer in whatever shape it liked while the config said the shape
* was constrained. So an agent using `strategy: 'tool-forced'` on a
* provider that has not declared this refuses at run start, by name.
*
* Declare it only where it is true of the endpoint, not of the SDK: the
* OpenAI adapter declares it for real OpenAI and Azure and NOT behind a
* custom `baseURL` (Ollama, vLLM, Together, …), because what an
* OpenAI-compatible server does with `tool_choice` is that server's
* business and this library does not get to promise it.
*
* A WRAPPER must forward it; `withFallback` publishes the AND of the two
* providers it holds. Model-specific restrictions are declared separately
* by `supportsForcedToolChoice`; this existing wire flag remains boolean.
*/
readonly carriesForcedToolChoice?: boolean;
/**
* Optional model-specific restriction on the forced-choice wire capability.
* Read with `forcedToolChoiceFor(provider, model)`: the wire flag must be
* true AND this predicate (when present) must answer true. Absence preserves
* the wire flag's existing behaviour. The adapter resolves its shorthand.
*
* Like `thinkingMode`, this function must not depend on `this`; wrappers
* forward it as a value. `withFallback` intersects both sides per model.
*/
readonly supportsForcedToolChoice?: (model: string) => boolean;
/**
* Whether this adapter puts {@link LLMRequest.outputFormat} on its wire —
* the provider's own structured output, `strategy: 'native'`. **Absence
* means NO**, for the reason `carriesForcedToolChoice` gives: a format that
* quietly vanishes costs the guarantee the strategy was chosen for. Declared
* by `anthropic()`, `browserAnthropic()` and the mock; read with
* `nativeOutputSchemaFor(provider, model)`. A WRAPPER must forward it;
* `withFallback` publishes the AND of its pair.
*/
readonly carriesNativeOutputSchema?: boolean;
/**
* Optional per-model restriction on {@link carriesNativeOutputSchema}, the
* `supportsForcedToolChoice` twin: both must answer true. A plain function
* of the model id (no `this`); the adapter resolves its own shorthand.
*/
readonly supportsNativeOutputSchema?: (model: string) => boolean;
/**
* Positive model knowledge for automatic native output. Unlike explicit
* native support, this must return false for unknown or opaque aliases.
* Auto requires both native wire support and this predicate to return true;
* absence keeps instruction-based generation. It must not depend on `this`.
* Wrappers forward it; fallback pairs require both sides to know the model.
*/
readonly supportsAutoNativeOutputSchema?: (model: string) => boolean;
/**
* `hooks` (v7.8) is optional and additive — implementations may declare
* `complete(req)` with no second parameter and stay assignable. A LEAF
* provider (one that talks to a vendor) may ignore it. A WRAPPER must
* forward it, or everything it wraps goes silently dark — see the
* `LLMCallHooks` docs above.
*/
complete(req: LLMRequest, hooks?: LLMCallHooks): Promise<LLMResponse>;
stream?(req: LLMRequest, hooks?: LLMCallHooks): AsyncIterable<LLMChunk>;
}
/**
* One entry in the in-flight tool-call sequence delivered to
* `PermissionChecker.check()` since v2.12. Lets sequence-aware
* policies (exfil chain detection, idempotency limits, cost guards)
* inspect what the agent has already dispatched this run.
*
* Derived from `scope.history` at check time — single source of truth,
* survives `agent.resumeOnError(checkpoint)` correctly.
*/
export interface ToolCallEntry {
/** Tool name dispatched. */
readonly name: string;
/** Tool args passed to `tool.execute(args, ctx)`. */
readonly args: Readonly<Record<string, unknown>> | undefined;
/** ReAct iteration the call was dispatched on. */
readonly iteration: number;
/**
* Optional source identifier — `'local'` for tools registered via
* `.tool(...)` / `staticTools(...)`, or the `ToolProvider.id` for
* tools resolved through a `discoveryProvider`. Lets cross-hub
* exfil rules match on origin, not just name.
*/
readonly providerId?: string;
}
/**
* What a tool DECLARES it touches (9.11.0).
*
* The four values a tool can honestly say about itself. Deliberately a subset
* of {@link PermissionCapability}: `'tool_call'` is the framework's own word for
* "a tool was dispatched" and `'skill_read'` is the framework's word for "a
* skill was activated" — neither is something a tool declares about its own
* behaviour.
*
* **The framework never infers these.** A tool's capabilities are not knowable
* from its name, its schema or its description; classifying them by guess would
* put a policy decision on a heuristic. Declared or absent — see `Tool.capabilities`.
*/
export type ToolCapability = 'memory_read' | 'memory_write' | 'external_net' | 'user_data';
/**
* The full vocabulary a {@link PermissionRequest} can carry.
*
* ## What is actually ENFORCED, said plainly
*
* `'tool_call'` has been enforced since v2.4: every tool dispatch asks the
* checker before `tool.execute`. The rest are enforced **only when both sides
* speak** (9.11.0):
*
* - a tool DECLARES `Tool.capabilities`, and
* - the checker DECLARES {@link PermissionChecker.governs}.
*
* With either side silent, nothing extra is asked and nothing extra is refused
* — byte-identical to every earlier release. This is deliberate: a framework
* that started sending `'memory_write'` to existing fail-closed allowlists would
* deny work those deployments have always permitted.
*
* **What is still NOT gated by this port, and is not pretended to be:** the
* agent's own memory pipeline. Recall and write stages are scoped by
* `MemoryIdentity` (tenant / principal / conversation), which is a
* different mechanism from a permission check, and no memory stage builds a
* `PermissionRequest`. So `'memory_read'` / `'memory_write'` reach a checker
* only for a TOOL that declared them.
*/
export type PermissionCapability = ToolCapability | 'tool_call' | 'skill_read';
export interface PermissionRequest {
/**
* What kind of operation is being asked about. See
* {@link PermissionCapability} for which values the framework actually sends
* and when.
*/
readonly capability: PermissionCapability;
readonly actor: string;
/**
* What is being asked about, in the vocabulary of the capability:
*
* - `'tool_call'` and every {@link ToolCapability} — the TOOL NAME.
* - `'skill_read'` — `skill:<id>` (9.11.0). Prefixed so a skill and a tool of
* the same name are two different subjects to a policy that lists ids.
*/
readonly target?: string;
readonly context?: Readonly<Record<string, unknown>>;
/**
* v2.12 — Sequence of tool calls already dispatched this run, in
* call order. EMPTY for non-`tool_call` capabilities. Sequence-aware
* policies (forbidden chains, idempotency limits) read this to make
* decisions that single-call governance cannot.
*/
readonly sequence?: readonly ToolCallEntry[];
/**
* v2.12 — Full conversation history at check time. Lets policies
* inspect prior assistant content / tool results without maintaining
* parallel state via event subscription.
*/
readonly history?: readonly LLMMessage[];
/**
* v2.12 — Current ReAct iteration (1-based). Lets policies fire
* different rules per iteration without external counters.
*/
readonly iteration?: number;
/**
* v2.12 — Caller identity from `agent.run({ identity })`. Permission
* predicates can role-check on `identity.principal` / `identity.tenant`.
*/
readonly identity?: {
readonly tenant?: string;
readonly principal?: string;
readonly conversationId: string;
};
/**
* v2.12 — Optional abort signal propagated from `agent.run({ env: { signal } })`.
* Async checkers (Redis lookups, hub-backed allowlists) MUST honor this
* — when the agent run is cancelled, in-flight checks should abort.
*/
readonly signal?: AbortSignal;
}
/**
* v2.12 — content shape mirroring `LLMMessage.content`. Future-compatible
* with multi-modal `tool_result` blocks once `LLMMessage` widens.
*/
export type ToolResultContent = string;
export interface PermissionDecision {
/**
* v2.12 — `'halt'` is NEW. Terminates the run cleanly with a typed
* `PolicyHaltError`. The framework writes a synthetic `tool_result`
* (using `tellLLM`) to `scope.history` BEFORE throwing, so:
* • Anthropic / OpenAI tool_use ↔ tool_result pairing is satisfied
* • The conversation history is consistent for `resumeOnError`
* • Lens / `getNarrative()` shows what the LLM was told
*
* `'deny'` keeps existing semantics: synthetic tool_result + LLM
* continues and can pick differently.
*/
readonly result: 'allow' | 'deny' | 'halt' | 'gate_open';
readonly policyRuleId?: string;
readonly rationale?: string;
readonly gateId?: string;
/**
* v2.12 — telemetry tag (machine-readable, stable across versions).
* Surfaces on `agentfootprint.permission.halt.reason` for routing
* alerts (e.g. `'security:exfiltration'` → PagerDuty,
* `'cost:context-bloat'` → Slack channel).
*/
readonly reason?: string;
/**
* v2.12 — content delivered to the LLM as the synthetic `tool_result`
* on `'deny'` and `'halt'`. When omitted, defaults to a deliberately
* generic `"Tool '${name}' is not available in this context."` —
* NEVER falls back to `reason` (which is telemetry, not user-facing).
*/
readonly tellLLM?: ToolResultContent;
}
export interface PermissionChecker {
readonly name: string;
check(request: PermissionRequest): Promise<PermissionDecision> | PermissionDecision;
/**
* Which capabilities BEYOND `'tool_call'` this checker asks to be consulted
* about (9.11.0). Optional and feature-detected — **absence is NO**.
*
* `'tool_call'` is always asked and needs no declaration. Everything else is
* asked only when it appears here AND the other side declares it too:
*
* - a {@link ToolCapability} — asked once per declared capability, per
* dispatch of a tool whose `Tool.capabilities` names it, right after
* the `'tool_call'` check allows.
* - `'skill_read'` — asked once per skill when the `read_skill` menu is
* composed (a refused skill's row disappears from what the model is
* offered) and again when the model activates one (a refused activation
* lands as the policy's own message, which the model reads and adapts to).
*
* The reason this exists rather than "just send everything": a checker
* written before these values were sent is fail-closed by design, and would
* deny a capability it has no rule for. Silence keeps such a checker doing
* exactly what it does today. Declare it and the framework starts asking.
*
* @example a checker that also governs which skills a role may activate
* const checker: PermissionChecker = {
* name: 'my-policy',
* governs: ['skill_read'],
* check: (req) =>
* req.capability === 'skill_read' && req.target === 'skill:payroll'
* ? { result: 'deny', rationale: 'payroll is HR-only' }
* : { result: 'allow' },
* };
*/
readonly governs?: readonly PermissionCapability[];
}
/**
* Does this checker ask to be consulted about `capability`? (9.11.0)
*
* `'tool_call'` is always true — it has been enforced since v2.4 and needs no
* declaration. Everything else is true only when the checker named it.
* Absence is NO.
*/
export declare function checkerGoverns(checker: PermissionChecker | undefined, capability: PermissionCapability): boolean;
export type TokenKind = 'input' | 'output' | 'cacheRead' | 'cacheWrite';
export interface PricingTable {
readonly name: string;
/** USD per ONE token for the given model+kind. */
pricePerToken(model: string, kind: TokenKind): number;
}
/**
* A service that runs code in an isolated session — a managed code
* interpreter, a container pool, a subprocess.
*
* **The shape is Start → Execute ×N → Stop**, because that is what every real
* one is, and the middle is the part a framework has to make possible. Paying
* session start-up on every call is the honest cheap version; holding the
* session in a module-level map is the fast version that hands one live sandbox
* to whoever calls next. `ctx.onTeardown` + {@link ToolExecutionContext.runId}
* are what let a tool do neither.
*
* ── Why this port exists at all: "summarize prose, compute data" ────────────
* A tool that returns 40MB of rows does not need a bigger context window; it
* needs to not put the rows in one. The motivating failure is a real production
* request of 879,073 tokens — a tool result pasted straight into the prompt.
* With a code runner, the model writes the aggregation, the RUNNER holds the
* data, and what comes back is the answer. Prose gets summarized; data gets
* computed. `CodeResult.truncated` exists so the second half of that promise
* cannot quietly break.
*
* Implement it for your own backend; ship it to `codeRunnerTool({ runner })`.
*/
/** One screenshot a browser session took. */
export interface BrowserShot {
/** Image bytes. */
readonly data: Uint8Array;
/** Image format, lower-case (`'png'`). */
readonly format: string;
}
/** Who is driving a browser session right now. */
export type BrowserDriver = 'agent' | 'person';
/**
* A browser an agent can drive — the PORT (9.68.0).
*
* Deliberately small, and deliberately not a browser automation API. Page-level
* work (navigate, find an element, fill a form) is what CDP and Playwright
* already do far better than any interface here would; a session exposes
* {@link BrowserSession.automationEndpoint} so those attach directly. What this
* port carries is what an automation library CANNOT do on its own: open and
* release a managed session, reach the operating system above the page, and —
* the one that matters — hand the controls to a person and take them back.
*/
export interface BrowserRunner {
/** Stable id — reported on every tool-session event, so a row names its backend. */
readonly id: string;
/**
* Open a session.
*
* `key` is the ISOLATION key the caller derived. An adapter may use it to
* name the remote session; it must never widen it.
*/
start(req: {
readonly key: string;
readonly signal?: AbortSignal;
}): Promise<BrowserSession>;
}
/** One open browser session. */
export interface BrowserSession {
/** The backend's own id for this session. */
readonly id: string;
/**
* Where an automation client attaches — a CDP WebSocket, for Playwright and
* friends. Absent on a backend that offers no such channel.
*
* This library does not drive the page for you and does not depend on
* Playwright; it hands you the endpoint and stays out of the way.
*/
readonly automationEndpoint?: string;
/** Where a PERSON can watch this session, when the backend offers a view. */
readonly liveViewEndpoint?: string;
/** Click at a point, in the operating system rather than in the page. */
click(req: {
readonly x: number;
readonly y: number;
readonly button?: 'left' | 'middle' | 'right';
readonly clicks?: number;
}): Promise<void>;
/** Type text, as a keyboard would. */
type(text: string): Promise<void>;
/** Press a named key, optionally more than once. */
press(req: {
readonly key: string;
readonly times?: number;
}): Promise<void>;
/** Take a screenshot of the session as it is now. */
screenshot(): Promise<BrowserShot>;
/**
* Hand the controls to a person, or take them back (optional).
*
* This is the seam a human-in-the-loop browsing flow is built on: the agent
* stops driving, a person finishes the step that needed them — a login, a
* consent screen, a CAPTCHA — and the agent resumes on the page they left.
* Absent on a backend with no such notion; feature-detect before offering it.
*/
handControlTo?(driver: BrowserDriver): Promise<void>;
/**
* Release the session.
*
* Must tolerate a session the far side already reaped — an idle timeout is
* the reality on every managed backend, and a stop on a dead session is a
* no-op, not an error.
*/
stop(): Promise<void>;
}
export interface CodeRunner {
/** Stable id — reported on every `agentfootprint.tools.session_*` event so a
* row names its backend, not just its tool. */
readonly id: string;
/**
* Open a session.
*
* `key` is the ISOLATION key the caller derived (see `toolSessionKey`). An
* adapter may use it to name the remote session; it must never widen it.
*/
start(req: {
readonly key: string;
readonly language?: string;
readonly signal?: AbortSignal;
}): Promise<CodeSession>;
}
/**
* The environment variable an executing snippet reads its staged inputs from —
* a JSON object mapping each input's NAME to the path it landed at.
*
* Part of {@link CodeSession.stageInputs}'s contract rather than one adapter's
* convention, and named here so an adapter uses the constant instead of
* retyping the string. It is what makes model-written code portable across
* backends: the code reads one variable, and every adapter that stages inputs
* fills it the same way.
*/
export declare const STAGED_INPUTS_ENV = "AF_STAGED_INPUTS";
/**
* One payload staged INTO a code session before code runs (9.26.0).
*
* `name` is the file name the caller wants it under — the tool derives it from
* the declared argument (`dataset` → `dataset.json`), so the model can be told
* the name in a static description. An adapter may sanitize it (a name is
* caller data landing in a filesystem) but must not rename it beyond
* recognition, because the manifest is keyed by what it was ASKED for.
*/
export interface CodeInput {
/**
* The MANIFEST KEY — what the executing code looks this input up by. The
* tool uses the declared argument name (`dataset`), so a static description
* can tell the model exactly what to look up before any session exists.
*/
readonly name: string;
/**
* The file name to write it under, when it should differ from `name` — the
* tool derives one from the artifact's media type (`dataset` +
* `application/json` → `dataset.json`) so an interpreter's own loader sees a
* familiar extension. Defaults to `name`.
*
* A separate field precisely so the manifest KEY and the on-disk NAME cannot
* drift: the code looks up what it was told to look up, whatever the file
* ended up being called.
*/
readonly fileName?: string;
/** The bytes. A string is written as UTF-8 text; a `Uint8Array` verbatim. */
readonly data: string | Uint8Array;
/** The producer's own statement about the payload, when it has one. */
readonly mediaType?: string;
}
/** Where one staged input actually landed. */
export interface StagedCodeInput {
/** The name it was asked for — the manifest key the code looks up. */
readonly name: string;
/** The path the executing code opens. Absolute, or relative to the session's
* working directory: whichever it is, it is what the manifest carries and
* what the code should use verbatim. */
readonly path: string;
/** How many bytes landed. */
readonly bytes: number;
}
/** One live session. `stop()` is idempotent and tolerates "already gone". */
export interface CodeSession {
/** The backend's own id for this session, when it has one. */
readonly id: string;
execute(req: {
readonly code: string;
readonly language?: string;
readonly timeoutMs?: number;
readonly signal?: AbortSignal;
}): Promise<CodeResult>;
/**
* OPTIONAL (9.26.0) — put payloads INTO the session, so code can read data
* that never travelled through the context window.
*
* ── Why it is a new member rather than an argument to `execute` ──────────
* The port's only input was the code STRING, and 9.22.0 stated the honest
* consequence rather than working around it: pushing a resolved artifact
* through that door would mean inlining megabytes into an argv, in
* language-specific quoting, past operating-system argument limits. This is
* the session file-write verb that note said it was waiting for.
*
* ── The contract, which is two promises not one ─────────────────────────
* 1. The payloads are written where the session's code can read them, and
* the returned {@link StagedCodeInput.path} is what the code opens.
* 2. Every later `execute` on this session exposes the manifest as the
* {@link STAGED_INPUTS_ENV} environment variable — a JSON object of
* `name → path`. That second promise is what makes the model's code
* portable: it reads one variable, on every backend that stages.
*
* Staged inputs live as long as the SESSION and are released by `stop()`.
*
* ── Absent, never faked ─────────────────────────────────────────────────
* A backend that cannot write into its own session LEAVES THIS ABSENT.
* Feature-detect with `canStageCodeInputs(session)`; `codeRunnerTool`
* refuses by name when a tool declares artifact inputs and the runner cannot
* carry them, because running the code without the data it declared would be
* the accepted-and-silently-wrong failure.
*/
stageInputs?(inputs: readonly CodeInput[]): Promise<readonly StagedCodeInput[]>;
/**
* OPTIONAL — read ONE file back out of the session, whole or not at all.
*
* `path` is relative to the session's working directory and may not leave
* it. An absolute path, a `..` that climbs above the working directory, a
* backslash, a leading `~` or a NUL is refused by name before anything is
* sent (`CodeSandboxError`, reason `'path-refused'`) — one rule,
* `sandboxPath`, shared by every adapter.
*
* The answer is the file the BACKEND returned for that path, paired to it
* by what the backend said it returned. A file the adapter cannot pair with
* the path it asked for is refused, never guessed at — a backend that
* answers with another file, or with two, has not answered this read.
*
* `maxBytes` is required: a read names its bound. A file over it is refused
* (reason `'file-too-large'`), never cut — a truncated file handed back as
* the file is the silent success this port refuses everywhere else.
*
* Feature-detect with `canReadCodeFiles(session)`.
*/
readFile?(path: string, options: CodeReadOptions): Promise<CodeFile>;
/**
* OPTIONAL — run ONE shell command in the session.
*
* It runs arbitrary shell, so an adapter offers it only when the runner was
* built with `commands: true`; otherwise the member is ABSENT. Feature-detect
* with `canRunCodeCommands(session)`. The command runs in the session's
* working directory with the environment `execute` gets, staged-input
* manifest included.
*
* The result says only what the backend reported. The exit status comes
* from the backend or not at all: when it reported none, `exitCode` is
* absent — unknown, never inferred from an error flag and never defaulted.
* stdout and stderr are bounded, and a cut is stated on `truncated`.
*/
runCommand?(command: string, options?: CodeCommandOptions): Promise<CodeCommandResult>;
/**
* Release the session.
*
* Must tolerate a session the far side already reaped — an idle timeout is
* the reality on every managed backend, and a `Stop` on a dead session is a
* no-op, not an error.
*/
stop(): Promise<void>;
}
/** Can this session accept staged inputs? The feature-detection law: read the
* member, never assume it from the adapter's name. */
export declare function canStageCodeInputs(session: CodeSession): session is CodeSession & Required<Pick<CodeSession, 'stageInputs'>>;
/** Can this session read files back out? Read the member, never the adapter's name. */
export declare function canReadCodeFiles(session: CodeSession): session is CodeSession & Required<Pick<CodeSession, 'readFile'>>;
/** Can this session run shell commands? Absent unless the runner was built
* with `commands: true`. Read the member, never the adapter's name. */
export declare function canRunCodeCommands(session: CodeSession): session is CodeSession & Required<Pick<CodeSession, 'runCommand'>>;
/** How one {@link CodeSession.readFile} is bounded. */
export interface CodeReadOptions {
/**
* The most bytes this read may return. Required, and a positive whole
* number: a file larger than this is refused, never cut.
*/
readonly maxBytes: number;
/** Stop waiting for the read. */
readonly signal?: AbortSignal;
}
/** One file read back out of a code session — exactly as the backend returned it. */
export interface CodeFile {
/** The path that was read, in the canonical form the backend was asked for
* (`./out//a.csv` → `out/a.csv`). */
readonly path: string;
/**
* The contents. A string when the backend returned TEXT, bytes when it
* returned BYTES — decided by which of the two the backend filled, never by
* the file's name or extension. A backend that draws no such line
* (`localCodeRunner`: a file on disk is bytes) returns bytes, and never
* guesses an encoding.
*/
readonly data: string | Uint8Array;
/** The size of `data` in bytes (a string's UTF-8 length). */
readonly bytes: number;
/** The backend's own statement of the media type, when it made one. Never guessed. */
readonly mediaType?: string;
}
/** How one {@link CodeSession.runCommand} is bounded. */
export interface CodeCommandOptions {
/**
* Stop waiting after this many milliseconds. What happens to the command
* depends on the backend, and the error says which: a local child process is
* killed; a managed sandbox may keep running it.
*/
readonly timeoutMs?: number;
/** Stop waiting when this fires. */
readonly signal?: AbortSignal;
/** Per-stream ceiling for this command's output, in characters. The
* runner's own `maxOutputChars` when absent. A cut is stated on `truncated`. */
readonly maxOutputChars?: number;
/**
* A line the command prints when it reaches a point you care about — for
* example `make && echo STEP_DONE` with `checkpoint: 'STEP_DONE'`. The
* result then says, on `checkpointSeen`, whether stdout held that exact line,
* read BEFORE any cut, so a marker printed past `maxOutputChars` is still
* found.
*
* It proves the command reached that line and nothing more: not that it
* exited cleanly, and not that anything after it ran. Its absence proves
* nothing about why. One line, no line breaks.
*/
readonly checkpoint?: string;
}
/** What one shell command produced — only what the backend reported. */
export interface CodeCommandResult {
readonly stdout: string;
readonly stderr: string;
/**
* The exit status the backend reported. ABSENT when it reported none: the
* outcome is unknown. Never inferred from {@link CodeCommandResult.isError},
* never defaulted, never derived from the output.
*/
readonly exitCode?: number;
/**
* The backend's own error flag, as it reported it (AgentCore's `isError`);
* absent when it reported none. It is NOT an exit status — a command can
* fail without it, and it can be set with no exit status at all.
*/
readonly isError?: boolean;
/** The signal that ended the command before it exited, when the runner knows
* it (`'SIGKILL'`). There is no `exitCode` then, and none is made up. */
readonly terminatedBy?: string;
/** Present IFF output was cut — see {@link CodeTruncation}. */
readonly truncated?: CodeTruncation;
/** Present IFF a `checkpoint` was asked for: whether stdout held that line. */
readonly checkpointSeen?: boolean;
}
/**
* Present IFF output was cut, and then it says by how much.
*
* Load-bearing, not politeness. A runner exists so big data is computed
* outside the context window instead of pasted into it; a runner that
* quietly slices its own output to fit is the same bug wearing a different
* hat, and the model would go on to reason over a truncated table it was
* never told was truncated. An unstated slice is a silent success.
*/
export interface CodeTruncation {
readonly stdout?: boolean;
readonly stderr?: boolean;
/** The pre-truncation length, in characters, of whichever stream was cut. */
readonly ofChars?: number;
}
/** What one execution produced. */
export interface CodeResult {
/**
* Did the code run to completion without an error exit? WHAT THE RUNNER
* REPORTED, nothing more.
*
* `true` or `false` only when the backend said so: an exit status, or its
* own error verdict. ABSENT when it reported neither — the outcome is
* unknown, and an absent `ok` is never success. (Before this was optional,
* `agentCoreCodeRunner` filled `true` from silence.) A `false` is told
* apart by the fields below: a program that exited non-zero (`exitCode`), a
* backend error with no exit status (`serviceError`, where the run is
* unconfirmed), and a process ended before it exited (`terminatedBy`).
*/
readonly ok?: boolean;
readonly stdout: string;
readonly stderr: string;
/** The exit status — exactly when the runner reported one. Never inferred
* from an error flag, never defaulted. */
readonly exitCode?: number;
/**
* `true` when the backend flagged its answer as an error and reported NO
* exit status: whether the code ran at all is not confirmed. That is not a
* program that failed — a program that ran and failed has an `exitCode`.
* `ok` is `false` alongside it.
*/
readonly serviceError?: true;
/**
* The signal that ended the process before it exited, when the runner knows
* it (`'SIGKILL'`): there is no exit status then, and none is made up. A
* local child the operating system ended, or one the runner ended because
* its output passed the buffer ceiling. `ok` is `false` alongside it.
*/
readonly terminatedBy?: string;
/** Files the run produced, described rather than inlined — the whole point is
* that big data does not enter the window. All fields beyond the original
* `{ name, bytes, uri? }` are ADDITIVE (9.22.0) and honest about absence:
* a runner that only knows the file exists states exactly what it always
* did.
*
* **ABSENT and `[]` are different facts, and only one of them is about the
* code.** `[]` says this execution produced no files. ABSENT says this
* RUNNER does not report produced files at all — it has no declared output
* location to collect from, so "the code produced nothing" is a claim it is
* not in a position to make. `localCodeRunner` is in the second camp today
* (see its module header): it runs the code in a child process whose cwd is
* the caller's own working directory, and treating every file that appeared
* there as an output would be a guess about which files mattered, made on
* the machine least able to afford it. A runner that WANTS to produce
* output artifacts owes three things: a declared output location the code
* is told about, a bounded read-back of what landed there, and a `bytes`
* that is the payload's real length — then `codeRunnerTool` mints every
* data-carrying entry with no further wiring.
*
* `data` is the in-band payload, present ONLY when the adapter can hand
* the bytes back (a local runner reading its own working directory, a
* managed sandbox that returns file contents). When a store is attached,
* `codeRunnerTool` mints every data-carrying entry into the artifact
* store and the model's result names the ref — entries without `data`
* stay described-only, because minting needs bytes and inventing them is
* worse than stating the gap. `mediaType` is the adapter's own statement
* when it knows one; `ref` is stamped by whoever minted the file into an
* artifact store (today: `codeRunnerTool`'s mint-on-output). */
readonly artifacts?: readonly {
readonly name: string;
readonly bytes: number;
readonly uri?: string;
readonly mediaType?: string;
readonly data?: string | Uint8Array;
readonly ref?: string;
}[];
/** Present IFF output was cut — see {@link CodeTruncation}. An unstated
* slice is a silent success. */
readonly truncated?: CodeTruncation;
}
//# sourceMappingURL=types.d.ts.map