# Capabilities — what already ships

**Search this file before you design anything.** Grep it for the noun YOU would
use (it is keyed by what you would call the thing, not by its name here), then
for the nearest symbol in `src/index.ts`. Every row below already ships.

A row's **Since** column says `unreleased` until the release that ships it —
the release fills in the version (see `.changes/README.md`). Never write a
version number here by hand.

**Read this table before proposing any new capability.** Everything below already
ships. The failure it exists to stop is real and expensive: a reader searches for
the words THEY would use, finds nothing, and designs a feature the library has had
for releases. That has happened repeatedly — the typed-HITL ask, element bindings
by role and name, and the artifact-kind renderer were each re-proposed after they
shipped.

Keyed by **what you would call it**, not by what it is called here. If your idea is
not in this table, search `src/index.ts` for the nearest noun before writing code.

| If you are about to build… | It is | Where | Since |
|---|---|---|---|
| moving files in and out of a code sandbox exactly and bounded, and running shell commands in it — staging text as text and bytes as bytes (AgentCore: one `writeFiles` call, `blob` for bytes, refused over the 100 MB per-call limit, `AF_STAGED_INPUTS` filled as the local runner fills it); reading ONE file back whole or not at all (paired to its path by the returned URI, a missing or different one refused; text or bytes by the field the backend filled, never the extension; over `maxBytes` refused, never cut); a shell command with bounded output, the cut stated, a checkpoint read before the cut, and an exit status only when the backend reported one — off unless the runner opts in; one path rule for both runners; a gone AgentCore session as one typed error; an outcome nobody reported kept unknown (`CodeResult.ok` absent, never success), and a failed program (`exitCode`) told apart from an unconfirmed run (`serviceError`) and a stopped process (`terminatedBy`) in the result and in the `tools.code_run` record | `CodeSession.readFile` + `CodeSession.runCommand` (runner option `commands: true`) + `canReadCodeFiles` / `canRunCodeCommands` + `CodeSandboxError` + `SessionExpiredError` + `sandboxPath` (`agentfootprint`); `agentCoreCodeRunner` / `localCodeRunner` (`agentfootprint/providers`) | `src/adapters/code/` | 12.0.0 |
| keeping an MCP client per conversation for a stateful server (AgentCore Runtime `'reuse'`, or any server that holds per-session state) — an app-side `Map<sessionId, client>` is keyed by caller data, unbounded, blind to calls in flight, and never ends the remote sessions: one client per authenticated conversation keyed by the identity the CALLER named (`ToolDispatchContext.callerIdentity`, never a derived one; principal required by default, tenant on request), one open per key under the pool's own signal and `initTimeoutMs`, three budgets each charged until the work under it SETTLED (open clients, endings, operations on any client — a timed-out call stuck in a credential vend stays charged) with `PoolFullError` at once and no queue, a busy client never ended, idle clients swept lazily (no timer), definitions per client incarnation unless the app declares a share checked against the transport's configuration (tenant always in the context), every bound tool re-checked after each wait and at the send gate (before every attempt, after a credential arrives, after SigV4 signing) so a revoked or stale tool — or one listed under an authorization name `share` no longer gives — is never sent, a discovered list kept only under the names it was listed for (asked at its send and again before it is kept), a pooled request with no guard refused unsent and a transport whose `send` cannot be watched never opened, a just-opened client held for its waiting callers, a lost handle's local close charged until it settles, `terminate()` answered by an elapsed-time deadline even when a local close hangs or the clock moves, a server's 404 for its own session (`SessionExpiredError`) kept apart from a handle this process lost (`LocalSessionHandleLostError`), and every use recorded with ids and outcomes only | `mcpClientPool` + `PoolFullError` + `McpPoolRefusedError` (`agentfootprint/providers`): options `name`, `transport`, `maxClients`, `maxClientsPerPrincipal`, `maxEnding`, `maxOperations`, `initTimeoutMs`, `idleMs`, `maxLeaseMs`, `definitionTtlMs`, `definitions`, `requirePrincipal`, `requireTenant`, `onEvent`, `replay`; the pool's `toolProvider()`, `acquire(identity)` (an `AsyncDisposable` lease), `terminate(scope)`, `close()`, `settled()`, `permissionsChanged(scope)`, `toolsChanged()`, `sweep()`, `stats()`; the event `agentfootprint.tools.mcp_client`; `LocalSessionHandleLostError` on `agentfootprint`. Docs: the "A client per conversation" page; example `examples/features/120-mcp-client-pool.ts` | `src/mcp/pool/` | 12.2.0 |
| ending an MCP client's REMOTE session, and knowing whether it ended — `close()` only aborts the local requests, so a `'reuse'` runtime session ran until the platform's idle timeout and nothing said so: `terminate()` sends the transport's own session termination (the Streamable HTTP `DELETE`), reads the answer itself (the SDK reports a 405 as success), closes locally whatever happened, bounded at 5 s, at most once; `resolved` only for `terminated` / `already-gone` / `no-session`; under AgentCore Runtime `'reuse'` it is `StopRuntimeSession` for that client's runtime session (SigV4 command or bearer HTTPS, the handle forgotten first, a 409 retried in the series) | `canTerminate` + `McpTerminateOutcome` + `McpClient.terminate?()` (`agentfootprint/providers`); `McpHttpTransport.settings` (a transport's comparable settings as data) | `src/mcp/terminate.ts` | 12.2.0 |
| tuning how an MCP call waits before asking again — the first wait, how it grows, jitter, a cap on the scheduled wait, a deadline for the whole call — and keeping a 429 retry and AgentCore Runtime's session-busy retry from multiplying requests: each call is ONE footprintjs `attempt()` series, the 429 rule JOINS a transport's own series instead of wrapping it (one attempt count, one waiting total, one deadline), a `Retry-After` is waited whole (never cut by the cap), and `onRetry` reports the refusal's `kind` but never arguments or answers | `retryOnThrottle` / `agentCoreRuntimeMcpTransport({ retry })` / `retryingFetch(fetch, options)`: `baseDelayMs`, `multiplier`, `jitter` (none, equal or full), `maxDelayMs`, `maxElapsedMs` beside `maxAttempts`, `maxWaitMs`, `onRetry` (`agentfootprint/providers`); AgentCore default 500 ms doubling, 5 attempts = 3.75–7.5 s of waiting | `src/mcp/throttleRetry.ts` | 11.2.0 |
| reading an MCP server's whole tool list when it pages (`nextCursor`) — every page, at most 100 `tools/list` requests; a list that does not end (`'page-limit'`) or loops (`'cursor-repeated'`) is refused with what was listed on the error, never returned cut short; the signal checked before each request and after each answer; and every listed tool keeps the SDK `Client`'s call checks whatever `mcpClient` lists later (required task refused before sending, `structuredContent` required by and matched against `outputSchema`, the same error class or shape; left to the SDK only for a one-page list still its latest answer, so a paged list, a failed or successful `refresh()`, a held tool and a call during a listing all stay checked), with `McpOutputUncheckedError` (`sent: false`, or `sent: true` when a list was read while the call was out) only where no JSON Schema validator is reachable — ONLY while discovery runs through `tools()`/`refresh()` (any `mcpClient` over the client): a `listTools` call of your own, the SDK client's automatic refresh (`listChanged.tools`, `autoRefresh` on by default) or any other unseen cache replacement can bypass both the output and the required-task checks, validator or not, so on a `connection` set `autoRefresh: false` and route `onChanged` to `refresh()`, or give `mcpClient` its own connection, which still needs that automatic refresh off | `mcpClient(...).tools()` / `.refresh()` + `McpToolListIncompleteError`, `jsonSchemaValidator` (`McpJsonSchemaValidator`) and `McpOutputUncheckedError` (`agentfootprint/providers`) | `src/mcp/` | 12.1.0 |
| deciding which MCP tool calls are sent again after a 429 or a lost connection — permission declared by the APP per (server, tool) at registration, never by the model, a tool description or server annotations (`readOnlyHint`); a generic 429 is not proof nothing ran, so `'app-authorized'` (opt-in) sends a call again only on the kinds its entry names (`throttle`, `network`) and otherwise returns the refusal (`not-permitted`); today's default stays as the NAMED `'compatibility'` policy, every retry recording its assumption (`permission: { by: 'compatibility', assumes: 'refused-before-execution' }`); the adapter you chose retries its own refusal (AgentCore session-busy) as its POLICY, recording `permission: { by: 'adapter', assumes: 'refused-before-execution' }` — never proof: a busy-shaped reply can come from a server that ran the call, so every attempt sent that did not complete is `unknown` and `none` is only a request never sent; `retryOnThrottle: false` turns off the 429 rule only — a declared `network` repeat keeps its series, budget and record; the side-effect guard — a tool declared `changes: true` is never sent again unless the server is declared to deduplicate by a key the call carries (`changes-without-key`); each attempt records failure, effect (`none`/`unknown`/`applied`, footprintjs's law) and permission apart, plus `durationMs`; the end counts the requests sent and keeps uncertainty (a later refusal or success never resolves an earlier `unknown`; `applied` = the call completed, never proof of a write); under `'app-authorized'` a received refusal or error (429, 503, JSON-RPC or tool error — or a chain of them) keeps the server's error, effect `unknown`, while a call that genuinely lacked an answer (an attempt with no response, or a request accepted over HTTP — 202, a stream — whose JSON-RPC answer never came: the ledger watches it by id) and then fails, fails as "may have run" (footprintjs `UnknownEffectError`, its final error the cause, `mayHaveRun: true`); `'compatibility'` keeps today's errors; `'app-authorized'` never silently weakens: it needs each request matched to its call (a library-private `_meta` marker, stripped before sending, so a copying SDK still matches) and each answer seen (checked when the `initialize` answer passes the observer), or the call is refused BEFORE it is sent with `McpReplayUnavailableError` (`sent: false`, `reason`: `answers-unobserved` for an SDK whose `connect` replaces `onmessage`, as 1.10's does; `request-unmatched` for one that drops `_meta`; `requests-unwatched` for stdio); an answer's watch ends by request id (answer, SDK cancellation, call settled, client closed), never with the HTTP send alone; a repeat re-sends the same arguments, server, session and identity | `mcpClient({ replay: { policy, tools: { [name]: { on, changes } }, deduplicates: { argument } } })` + `McpReplayOptions` + `McpReplayUnavailableError` (`agentfootprint/providers`); `effect` / `permission` / `durationMs` on `agentfootprint.tools.request_attempts` rows; `attempts` / `effect` / `mayHaveRun` on its `end` | `src/mcp/replay.ts` | 11.5.0 |
| saving what an MCP call's retries did into the run that made the call — every attempt, why the series went on or stopped after it, each wait and any stated `Retry-After`, and how the request ended, a cancellation DURING A WAIT included — so a saved and re-read recording gives it back; concurrent calls and runs on one connection never mix; past 100 attempts the rest are counted (`omitted`), never dropped; a 503 received is `answered` with its status, never success and never retried; allow-listed metadata only (a thrown error is a fixed sentence and its category; the endpoint is the declared `endpointLabel`, else its origin; a filing is read as plain data). Every request sent is a row or counted; one cut in flight is `cut`, `remote: 'unknown'` (never recorded as not run), and only a request never sent has no row. Filed ONCE per request by the call's own footprintjs `attempt()` series (no second loop) through an internal per-dispatch port (`REQUEST_EVIDENCE`, not public API) and a per-connection ledger: the call's private `_meta` marker (taken out before sending) → JSON-RPC id → series. An MCP tool call honours its `ctx.signal` (the SDK request is cancelled, `notifications/cancelled` sent), its series stops with it, a wait included, filed the moment it lands, and a call that settles first cuts its series inside the call — never filed after its run, nothing re-sent | `agentfootprint.tools.request_attempts` / `SeriesEvidence` + `RequestLedger` | `src/mcp/` | 11.3.0 |
| refusing invalid memory list and search request sizes uniformly before backend reads or empty-result shortcuts, while keeping optional defaults, adapter upper caps and list continuation intact | `ListOptions` + `SearchOptions` | `src/memory/store/types.ts` | 10.8.1 |
| reading AgentCore memory with a large portable page request while obeying AWS's 100-event wire bound and keeping every continuation cursor; validating its configured page size before SDK construction; bundled hosts supplying the SDK as a public typed option, with extraction writes refused when an older serializer cannot carry them | `AgentCoreStore` + `AgentCoreStoreOptions.sdk` + `BedrockAgentCoreSdkModule` + `ListResult.cursor` (`agentfootprint/memory`) | `src/adapters/memory/agentcore.ts` | 10.8.1 |
| giving a tool the credential that arrived WITH the incoming request (an encrypted per-request token, a session key) — sealed per run so no copy, log or state carries it, read only by the run that carried it (its composition members included, at any depth; a resume serves the resume's request), failing closed by name when absent; strategies for bearer, API-key header and decrypt-then-read; through a host, the app seals each request's credential from its headers (`sealRequest`) and the host hands it to that request's run only — a handle returned twice refused, a sealer's throw refused in fixed words | `requestContext` + `requestCredentials` + `asBearer` / `asApiKey` / `decodedWith` + `RequestCredentialError` (`agentfootprint/security`), `run(input, { request })`; `sealRequest` + `RequestSealer` on `standingAgent` / `streamHandler` (`agentfootprint/hosting`) | `src/identity/requestCredentials.ts` | 10.5.0 |
| hiding a credential or identity ARGUMENT of a tool (an MCP server's `token` field) from the model — removed from the schema the model sees, a model-sent value refused by name, filled from `ctx.credential` / `ctx.identity` inside `execute`, echoes of the credential scrubbed from results and errors | `hostFilled` + `fromCredential` / `fromIdentity` + `HostFilledArgumentError` | `src/tools/hostFilled.ts` | 10.5.0 |
| running the same composition (Sequence / Parallel / Conditional / Loop / workflow / graph, nested) for many users at once without their member agents sharing memory or identity — the caller's identity passed once to the composition and handed to every member at any depth; with none, each run is its own conversation; a mounted agent no run reaches refused by name, never run under a placeholder or its instance's last caller | `Sequence.run({ message, identity })` (and every composition's input) + `MemberIdentityError` | `src/core/memberRun.ts` | 10.4.0 |
| keeping a memory write out of a managed service's long-term extraction, or filing what it derives under one tenant's namespace — vendor-neutral write options on the `MemoryStore` port (`put`/`putMany`/`putIfVersion`), declared per store and REFUSED BY NAME where not honoured (never dropped), bound by `defineMemory` to every write AND search of a memory (the scope comes only from the run's identity; a per-call scope, an empty one, a search namespace that does not hold it segment for segment, or one that omits the person's actor is refused; `requireNamespaceScope` refuses unscoped access) and checked before the first turn; AgentCore maps them to `CreateEvent`'s `extractionMode: 'SKIP'` and `extractionConfig.namespaceVariables` only when its client and SDK can carry them, checks AgentCore's variable domain first, and ships a one-to-one tenant encoder into its alphabet | `MemoryWriteOptions` (`extraction: 'skip'`, `namespaceScope`) + `MemoryStore.writeOptions` + `defineMemory({ writeOptions, namespaceScope })` + `SearchOptions.namespaceScope` + `UnsupportedMemoryWriteOptionError` + `refuseUnhonouredWriteOptions` + `agentCoreNamespaceValue` (`agentfootprint/memory`) | `src/memory/store/writeOptions.ts` | 10.4.0 |
| calling an MCP server somebody deployed TO AWS Bedrock AgentCore Runtime (`--protocol MCP`) — through `InvokeAgentRuntime` with SigV4, or a bearer token over the documented HTTPS endpoint (the SDK has no bearer scheme); a fresh runtime session per call by default (`'per-call'` carries no session id, so one client can serve every user) or one per client (`'reuse'` keys each client's connection — its `runtimeSessionId` and the server's `Mcp-Session-Id` — by a handle the transport issues); what the MCP client reads of a failure rewritten from fixed sentences and safe ids (status, a plain error type, a JSON-RPC code), never the service's text (which can quote the signed request); a bearer token vended as a machine token only; the runtime's "Session operation in progress" refusal (JSON-RPC -32005 inside an HTTP 200, which MCP clients never retry; or the API's 409 `RetryableConflictException`) retried with bounded backoff by the SAME loop as the 429 retry, one small decision function — and nothing else re-sent: the SDK client's own retry is off, so a 5xx or a dropped connection is never replayed | `agentCoreRuntimeMcpTransport` + `agentCoreRuntimeUrl` + `AGENTCORE_RUNTIME_SESSION_HEADER` (`agentfootprint/providers`) | `src/adapters/mcp/agentcoreRuntime/` | 10.4.0 |
| bundling an app that speaks MCP (esbuild, webpack, a Lambda zip without `node_modules`) — the MCP and AWS SDKs load through a lazy `require` a bundler cannot follow, so the caller hands over the modules it imported statically | `mcpClient({ sdk: { Client, StreamableHTTPClientTransport } })` · `agentCoreRuntimeMcpTransport({ sdk })` (SigV4 only; `_sdk` = the older name, both-different refused, `sdk` beside `{ bearer }` refused); a missing or wrong SDK is refused when the client/transport is BUILT, in words naming `sdk`, never at the first tool call. Docs: mcp-client "Bundling (esbuild, webpack)". Fenced by `test/mcp/bundling.test.ts` | `src/mcp/` | 10.5.0 |
| serving agents to AG-UI frontends (CopilotKit, the AG-UI Dojo, an `@ag-ui/client` app) from any framework with no glue code — POST a `RunAgentInput`, stream AG-UI events back; the request is untrusted browser input: only the newest user message is conversation (the page's copy is never read), a system/developer message refused, an answer taken only for the pause the server keeps, frontend tools only by the app's name allow-list with its own contract (they run in the page, no server privilege), `state`/`context` only through the app's validators, no identity/credential/redaction from a request; a fresh runner per request, the thread bound to the caller the app authenticates, the conversation and paused runs kept server-side, one turn per thread, door guard, body ceiling, SSE keep-alive | `streamHandler` + `ProtocolAdapter` + `verifiedThreads` (`agentfootprint/hosting`) + `agUi` (`agentfootprint/ag-ui`) | `src/hosting/streamHandler.ts` | 10.4.0 |
| serving an agent behind a GATEWAY that speaks AG-UI in its own request body and its own output envelope (`{"eventType":"…Event","event":{…,"eventSequence":n}}`) — `agUi()` as three replaceable strategies: a reader (the gateway's body → the turn), the translation (AG-UI vocabulary, unchanged) and the framing (the envelope; each frame's number in its own stream handed in by the host, never a shared counter); every untrusted-input rule stays in the core whichever reader ran (shape and ids, no system/developer message, frontend tools by allow-list, state/context by validators, answers only for the kept pause; each value read once) | `agUi({ reader, translation, framing })` + `AgUiRequestReader` / `AgUiTurnRequest` / `AgUiTranslationStrategy` (`agentfootprint/ag-ui`), `SseFraming.frame(event, { sequence })` + `SseFramePosition` (`agentfootprint/observe`) | `src/ag-ui/adapter.ts` | 10.5.0 |
| checking forced tool choice per model without changing the existing boolean wire declaration; explicit requests refuse before transport, classifiers keep their strict parse alternative, wrappers preserve and fallback pairs intersect | `LLMProvider.supportsForcedToolChoice` + `forcedToolChoiceFor` + `UnsupportedToolChoiceError` (`agentfootprint/providers`) | `src/provider-contract/forcedToolChoice.ts` | 10.1.0 |
| a typed answer constrained by the PROVIDER'S OWN structured output instead of asked for in prose — the schema sent as Anthropic's `output_config.format`, the agent keeping its tools; one function builds the API-compatible wire copy (objects closed, unsupported constraints moved into the description, recursion / external `$ref` / complex enums / open values / open-keyed maps refused at build by node), the parser still judges; explicit-native per-model capability with unknown ids read as the newest generation, refused by name at run start and before transport; a `'refusal'` / `'max_tokens'` stop fails the attempt by name even when it parses; `_findings` allowed on the format when the ledger is armed | `.outputSchema(parser, { strategy: 'native', jsonSchema })` + `LLMProvider.carriesNativeOutputSchema` / `supportsNativeOutputSchema` + `nativeOutputSchemaFor` + `UnsupportedNativeOutputError` (`agentfootprint/providers`) + `answerGuarantee: 'native'` + `OutputAttempt.stopReason` | `src/provider-contract/nativeOutputSchema.ts` | 10.3.0 |
| choosing native structured output automatically only for positively known supported models (`supportsAutoNativeOutputSchema`) when a safely lowered JSON Schema is available; otherwise keeping instruction plus local validation, with an accurate format guarantee; accepting exactly one whole-answer JSON or unlabelled code fence before the unchanged JSON/schema parser | `.outputSchema(parser, { strategy: 'auto', jsonSchema? })` (default) + `agent.parseOutput` / `runTyped` | `src/core/agent/AgentBuilder.ts` | 11.0.0 |
| replaying Anthropic thinking in its original content order; refusing known unsafe active-tool replay, observing each permitted completed-turn omission before sending, and retaining that decision across checkpoints | `LLMMessage.replay` + `UnsafeThinkingReplayError` + `LLMCallHooks.onReplayPrepared` + `agentfootprint.agent.thinking_omitted` | `src/adapters/llm/anthropicThinkingReplay.ts` | 10.1.0 |
| keeping personal data out of an agent's saved records — snapshot, narrative, every event, recordings, bug reports, answer accounts, the exporters — while the model, the tools and the caller keep the real values; the library's own list of the names the conversation travels under (proven per feature), joined with the fields your tools name; it reaches compositions (members' policies adopted) and runs a tool starts (`ctx.redact`), and readers of the record say "kept out", never "not recorded" or "no tool ran" | `Agent.create({ redact })` (a footprintjs `RedactionPolicy`) + `conversationRedaction` + `ToolExecutionContext.redact` | `src/redaction/` | 9.140.0 |
| prompt caching that keeps hitting behind `withRetry`, `withFallback` or your own provider wrapper — the cache strategy chosen from what the adapter DECLARES (explicit breakpoints / automatic / none), never from its name, plus a moving breakpoint on the conversation tail so each call reads back what the last one sent | `LLMProvider.promptCaching` (`PromptCaching`) + `cacheStrategyFor` + `BreakpointCacheStrategy` / `AutomaticCacheStrategy` / `NoOpCacheStrategy` | `src/cache/cacheStrategyFor.ts` | 9.138.0 |
| findings with stable tool contracts and immutable instructions, bounded late DATA outside cached history, unchanged citation admission, and call-scoped saved/resumed layout evidence | `.findings({ maxContextBytes })` + `findingsContext` + `servedAt` / `findings-layout` | `src/core/agent/findings/context.ts` | 10.10.0 |
| Bedrock Converse prompt caching for exact verified models and geography profiles, field/TTL restrictions and four-checkpoint limit; cache read/write counts on both transports; model-aware capabilities through resilience decorators; one build warning and fixed run metadata when requested caching is unavailable; the meter accepts real engine events from `.watch()` | `bedrock` + `BedrockProvider` | `src/adapters/llm/BedrockProvider.ts` | 10.8.0 |
| reading a static or model-aware prompt-cache capability before inspecting its mode, with unknown models failing closed | `promptCachingFor` | `src/cache/cacheStrategyFor.ts` | 10.8.0 |
| asking a model to think in the shape THAT model takes — budget tokens on Claude 4.6 and earlier, adaptive on 4.7 and later (and on any id the table does not know), a build-time refusal for a model that cannot think; one table per wire, an adapter option for an id the table cannot read, forwarded by every wrapper, answered per side by `withFallback` | `.thinking({ budget })` + `LLMProvider.thinkingMode` + the adapters' `thinkingMode` option + `ThinkingMode` + `UnsupportedThinkingError` | `src/adapters/llm/anthropicModels.ts` | 9.141.0 |
| a thinking handler that survives `withRetry`, `withFallback` or an app's renaming wrapper — DECLARED by the adapter, never matched by provider name, so the signed thinking blocks still round-trip | `LLMProvider.thinkingHandler` + `thinkingHandlerFor` | `src/thinking/thinkingHandlerFor.ts` | 9.138.0 |
| observing selected middleware, permission and credential facts at runtime with bounded content-minimized metadata, explicit loss counters and optional source positions; never inferring approval, redaction or complete coverage from silence | `trustBoundaryRecorder` + `TrustBoundaryRecorder` | `src/trust-boundaries/` | 9.137.0 |
| keeping a queued event's emission time and supplied engine log coordinates, without guessing a root index or confusing agent and engine run identities; absent when the engine supplies no usable position | `EventSourcePosition` + `projectEventSourcePosition` | `src/bridge/eventSourcePosition.ts` | 9.137.0 |
| identifying which same-name tool call a permission check or credential event describes — captured at dispatch, including resume and inner pull failures; qualify with run identity, missing fields stay unknown | `ToolCallEventIdentity` + `bindToolSecurityEvents` | `src/core/agent/toolSecurityEvents.ts` | 9.137.0 |
| guiding an evidence repair to ask for missing context without inventing values | `recoveryInstruction` + `EvidenceRecoveryContext` | `src/core/agent/evidence/` | 9.96.0 |
| checking a typed final answer against scoped evidence before streaming or memory delivery — optional host-authored checks with enforce/observe modes, canonical JSON and bounded read-only artifacts | `answerValidation` + `AnswerValidationError` | `src/answer-validation/` | 9.95.0 |
| an answer the agent's own checks flagged still being DELIVERED — a typed field that contradicts the fact a tool settled (`.claims()`) or a value no tool result carried (`.namesAndNumbersFromEvidence()`) withheld instead of only recorded: each listed check is a row of the enforcing answer validation, judged on the exact JSON delivered after any output transform (draft tokens held, `AnswerValidationError` on refusal, recorded only under `mode: 'observe'`), after the gate's one repair turn under `'guard'` (served no tools while the validation enforces, so a tool that writes or sends never runs twice); claims compare after a declared normalization (text in NFC, trimmed, case agreeing only between the same letters in another case in a word of more than three letters — `ß` never `ss`, and a rule that cannot know where case carries meaning, so `Polish` agrees with `polish`; a number and a plain decimal string by value; a unit exactly; a currency, separators, a dot-grouped number, a date or a time only as written), so a different spelling — an accent dropped included — is never a contradiction; a reading can only withhold — a field contradicting any value read fails its row, and readings that differ where the field contradicts none, a reading that settled only doubt beside a stated value, values that cannot be compared safely (a list with a hole included), a malformed `Claim`, or a doubt agreeing with a tool's doubt is `unreachable` and withheld; an agreeing claim, a fact no tool settled and a clean gate pass are `not-applicable` — the agent's rows withhold and never verify, so only the app's own checks pass a report; rows carry declared names and counts, never values; opt-in, defaults unchanged | `answerValidation({ agentChecks })` + `AgentCheckName` | `src/answer-validation/` | 11.0.0 |
| blocking a declared claim when its required relationship evidence is missing or unresolved, while independently checked measurements remain usable — host-owned policies over Foottrace requirement records, composed with output middleware and enforcing answer validation | `checkClaimRequirements` + `ClaimRequirementPolicy` + `ClaimRequirementAssessment` | `src/answer-validation/requirements.ts` | 10.6.0 |
| an output guard that sees which tools ran — fail the run when the answer presents code but no code-execution tool ran; this run's tool calls (name, call id, outcome ok/error/not-run; never arguments or results) handed to the enforce check, per run, bounded | `AnswerValidationContext.toolCalls` + `AnswerToolCallsView` + `limits.maxToolCalls` | `src/answer-validation/toolLedger.ts` | 10.5.0 |
| turning a written operational procedure (a runbook, a triage playbook) into ONE agent tool whose every answer is EVIDENCE — coverage folded up from the inner tools it calls, rule name+version, verdict rows with GENERATED meanings, and the recorded walk as an artifact ticket, never bytes | `runbookAsTool` — dials: `procedure` (factory, invoked per call with `ctx.tools`) + `resultKind` (`'verdict/*'` arms the rowset projection; anything else ships spine + the chart's `report`) + `rules {name, version}` (default absent ⇒ `rule_version: 'undeclared'`) + `verdicts {decider, maxRows}` (default 50; `verdict_meanings` = statically declared branches + this run's rule labels + the DEFAULT branch's label, which reaches evidence only when the chart calls `decide(s, rules, {branch, label})` — fp ≥9.16.1, the default is chosen by NO rule so nothing else can name it, and inside a generated fan-out branch the static walk is blind too; an undeclared or blank label stays ABSENT from the map, never invented from a branch id, and there is deliberately NO caller-supplied meanings map) + `presentation` (`'prose'` default = ship `table` + `VERDICT_RENDER_NOTE` "output it VERBATIM"; `'panel'` = the HOST renders the rowset, so NO `table` key at all + `PANEL_RENDER_NOTE` "the rows are already on the reader's screen — do not reproduce them"; the rowset half is byte-identical across modes, `table` stays RESERVED in both, an unknown value THROWS at definition) + `walk {cap, recording}` (cap default 500; over-cap ⇒ control-flow projection, declared · `recording: true` or `{label, maxBytes}`, default OFF — ALSO files the inner chart's own `{snapshot, events, structure}` under `recording/run` and puts its ref on the spine as `walk.recording_ref`, because the ROW projection cannot be drawn; snapshot read from the REDACTED mirror so one `redact` means the same for both; over `maxBytes` (`DEFAULT_RECORDING_MAX_BYTES` = 5,000,000) it is REFUSED not truncated; every absence — no store / over size / unserializable / store threw — is STATED in `walk.recording_note`, and the four `recording_*` fields are absent entirely when the dial is off) + `composedOf` (drift-checked at agent BUILD) + kept `recorders`/`keepRecord`/`keepRecordLimit`/`redact`; reserved state keys `verdicts`/`coverage`/`report` (the `report` bag lands BESIDE the spine, never over it — a report field spelling `af_coverage`/`af_provenance`/`rule_version`/`walk`/`report_note` or a live projection key is discarded and NAMED in `result.report_note`; precedence is explicit in `report.ts`, never spread order); inner `absent()` passes through VERBATIM unless the call said `allowAbsent`; walk kind `recording/chart-walk` + `walk_segment` discriminant | `src/core/runbook/` | 9.76.0, recording 9.79.0 |
| speaking MCP from a BROWSER — the SDK is browser-clean (its client + streamableHttp bundle at `platform:'browser'` with zero `node:` edges and never pull in `client/stdio.js`); the one barrier was that `lazyRequire` gets CALLED there, so the fix is to let the caller supply what the loader would have found | `mcpClient({ sdk })` (`McpSdk` = the two SDK modules you imported statically; the library STILL builds the transport, so headers/fetch/gateway vending/`retryOnThrottle`/`_meta` all keep working) · `mcpClient({ connection })` (`McpConnection` = listTools/callTool/close, deliberately NO `connect` — you already did; the only arm that reaches the SDK's `jsonSchemaValidator`, i.e. CSP) + `McpConnectionOptions`; `refuseConflictingOptions` REFUSES at construction every option a transport would have consumed, naming where it moved (a knob naming a behaviour that no longer happens is the defect class); `transportUrl` resolves a relative `url` against `globalThis.location.href` (absolute takes the identical branch, Node refuses by name); `sdkLoadFailure` classifies the seven load sites so a browser is never told to install a package it has (resolution failure ⇒ the site's own absent-package message, which names the `sdk` hook); `retryingFetch` + `ThrottleFetch` promoted out of `@internal` so the connection arm keeps its 429 handling. stdio keeps `lazyRequire` FOREVER — it spawns a subprocess. ZERO packaging change: no subpath, no `browser` condition (TypeScript is blind to it), peer stays optional (no literal dynamic `import()`). Fenced by `test/mcp/browserGraph.test.ts` | `src/mcp/` | 9.81.0 |
| calling ANOTHER registered tool from inside a tool's `execute` — composition over the agent's own dispatch map instead of importing the module and building a second query stack | `agentToolDispatch` + `ctx.tools` (ToolDispatch has/call) — sees static and skill-carried tools, NEVER ToolProvider-delivered ones (no build-time list, the 9.72.0 caveat); inner calls get the outer facts with hasArtifacts false and a derived toolCallId, `needs` resolved fail-closed non-interactively, `checkIn` and `wants` tools refused by name, no nested dispatch; declare ingredients via `composedOf` (and `gates` for a pausing procedure) — both travel MCP `_meta` | `src/core/agent/toolDispatch.ts` | 9.76.0 |
| a tool returning numbers WITH the caveats that make them honest — interval/aggregation grain, is-it-a-counter, when the world was measured, which ground was NOT covered — as typed data the model reads compactly and the record keeps whole | `describedResult()` + `tools.semantics_declared` (model sees `semanticsForModel` projection; `coverage` field absorbed by the coverage()/absent() channel; the old `semantic()` declaration door was removed in 10.0.0) | `src/semantics/` | 9.53.0 |
| the same typed tool result declared in ONE spelling — camelCase `measuredAt`/`isCounter`/`filterNote`, respelled to the unchanged snake_case wire — with a missing source a COMPILE error; which result helper to return (rows → describedResult, a value with limits → coverage, nothing → absent); an EMPTY data list refused with the branch to write ("if nothing matched, return absent({ what, checked }) instead"), the declaration door and the gate | `describedResult()` + `DescribedResultDeclaration` (`provenance` required whenever `series`/`facts` is present; the snake_case spelling refused, naming the camelCase one) | `src/semantics/described.ts` | 9.119.0, empty-list refusal 9.125.0 |
| a build gate that refuses a triage/inventory tool that forgot its caveats, by tool name and field name | `checkSemantics` + `defineTool({ resultClass })` (`'triage'`/`'inventory'`, the closed set) + bin `agentfootprint-check-semantics` | `src/semantics/check.ts` | 9.53.0 |
| an agent that runs out of `maxIterations` mid-task handing back a HALF-SENTENCE as its answer — and nothing saying the budget ran out | `wrapUpAtMaxIterations` (default ON) + `WRAP_UP_INSTRUCTION` + `agent.budget_exhausted` + `stoppedEarly.wrappedUp` | `src/core/agent/stages/wrapUp.ts` | 9.56.0 |
| the conversation outgrowing the context window — dropping, summarizing or token-budgeting old turns, every removal on the record (the three refusal rows below all guard this window) | `.window()` + `slidingWindow` / `summarizeOldest` (= `.compaction()`) / `tokenBudget` + `keepRecentTurns` (default 6) + `thresholdTokens` (required, deliberately no default) + `retain: 'conversation'` (default) / `'discard'` | `src/core/agent/window/` | 7.17.0 |
| keeping the user's own request in context while the window shrinks — the task's anchor, un-droppable by every window strategy including one you wrote | `'current-request'` refusal + `currentRequestIndexOf` + `WindowStrategyInput.currentRequestIndex` | `src/core/agent/window/currentRequest.ts` | 9.55.0 |
| the window evicting the EVIDENCE while keeping the task — the tool result carrying the only valid ids leaves, and the model invents one that has never existed | `keepLastToolResults` (default 2; `0`/`false` disables) + `'last-tool-result'` refusal + `toolResultPinsOf` + `WindowRecord.observations` (what was kept, and its exact char cost) | `src/core/agent/window/lastToolResult.ts` | 9.57.0 |
| telling the model WHICH tools' results a drop took, so it re-calls instead of reconstructing from memory — and filing the same fact on the record even when no notice was authored | `droppedToolNames` + the notice's tool sentence + `WindowRecord.droppedObservations` | `src/core/agent/window/toolNames.ts` | 9.57.0 |
| an instruction that SAYS a run-time number rather than only gating on it ("you are on action 25 of 30") — with the library owning absence, so never "23 of undefined" and never a fabricated zero | `promptTemplate` + the closed `TEMPLATE_FACTS` vocabulary (`action`/`actionBudget`/`actionsRemaining`) + `skipped: 'unknown-fact'` + `Injection.templated` | `src/injection-engine/promptTemplate.ts` | 9.57.0 |
| an injection predicate that can see how much of the action budget is left | `InjectionContext.maxIterations` / `.iterationsRemaining` + `iterationsRemainingOf` (the ONE denominator, shared with the cache decision) | `src/lib/iterationBudget.ts` | 9.57.0 |
| report progress from inside a tool — "hop 3 of 12 done", said mid-call while a long-running `execute` is still working (the record was otherwise atomic: tool_start, silence, tool_end) | `ctx.progress` + `agentfootprint.stream.tool_progress` | `src/tools/tools.ts` | 9.52.0 |
| show tool progress to the user — a mid-call report reaching the live status line / chat bubble, not just the record (`message` shown verbatim, capped at 120 chars with the cut stated; otherwise an honest generic line, never a payload dump) | `selectStatus` + `progressMessageOf` + `tool.progress` templates | `src/recorders/observability/status/statusTemplates.ts` | 9.54.0 |
| declaring which skills connect — the SkillMap as one named thing (the agent that mounts it is the SkillWalker; there is deliberately no walker class) | `defineSkillMap` + `SkillMap` (permanent reference-equal aliases of `skillGraph`/`SkillGraph`) | `src/injection-engine/skillGraph.ts` | 9.51.0 |
| guarding a skill transition on state or a tool result's fields — a route condition as DATA (comparable, drawable, evidence-recorded), not an opaque predicate | `guard:` + `compileGuard` (ops `'eq'`/`'ne'`/`'gt'`/`'gte'`/`'lt'`/`'lte'`/`'in'`/`'notIn'`) + `guard-unsatisfiable` | `src/injection-engine/skillGuard.ts` | 9.51.0 |
| seeing the skill map in a recording — the author's nodes + edges (guards included) as DATA, never parsed from prose or inferred from fired hops | `skill.graph_declared` + `buildSkillGraphDeclared` | `src/core/agent/skillGraphDeclared.ts` | 9.50.0 |
| "where could the run go next?" — the reachable skill set at every cursor move, typed on the move itself | `cursorMove.reachable` + `reachableSkills` | `src/injection-engine/buildInjectionEngineSubflow.ts` | 9.50.0 |
| putting the ASSEMBLED system prompt in the recording — the exact string the model read (opt-in; default OFF is a privacy decision) | `recordSystemPrompt` | `src/core/agent/types.ts` | 9.50.0 |
| a typed HITL prompt — let a person pick from a list, choose a range, use a real control instead of typing prose | `AskComponent` + `componentId` (consumer-registered vocabulary, opaque here) + inline `props` + `propsRef` (artifact claim ticket for the big half) | `src/tools/askComponent.ts` | 9.24.0 |
| carrying WHAT the person chose back, with what they could see | `DecisionValue` (`kind` / `value` / `from?` + `coverage {seen, total, filter?}`) | `src/core/checkin.ts` | 9.47.0 |
| archiving a finished run — filing it, attaching it to a bug report, feeding it to an analysis tool | `persistRecording` + `RecordingEnvelope` + `run.complete` (required) + `privacy.mode` (`'full'` today; `'structure-only'`/`'redacted'` refuse by name) | `src/recorders/observability/recordingEnvelope.ts` | 9.48.0 |
| saving a LONG run's recording — one that is too big to write as JSON (plain recordings grow with the square of the iterations and pass JSON's string limit before 80 iterations at 1,000-row results), or a recording file a reader must read whether it was packed or not | `packRecording` / `unpackRecording` (+ `isPackedRecording`, `PACKED_RECORDING_FORMAT`, `PackedRecordingError`) on `agentfootprint/observe`; an Agent's artifact mint via `artifacts: { recordings: { packed: true } }` | `src/recorders/observability/recordingPack.ts` | 9.139.0 |
| packaging a run for a HUMAN to file — a zip whose evidence is the archive envelope, plus host facts and a readable transcript | `exportBugReport` + `include` (the consent seam; left-out units are counted in `manifest.excluded`) + `warnOverBytes` (default 20 MB) | `src/bug-report/build.ts` | — |
| writing archived runs somewhere — a directory, one JSON file per run | `fileRecordingSink` + `RecordingSink` | `src/recorders/observability/fileRecordingSink.ts` | 9.48.0 |
| declaring an agent's whole setup as one named, versioned thing — a preset, a template, a blueprint, "the support agent we all use" | `defineAgentRecipe` + `AgentBuilder.recipe()` | `src/recipes/` | 9.48.0 |
| naming your own branches and nodes — which prefix is reserved, and telling framework plumbing from consumer structure in a trace | `RESERVED_SUBFLOW_PREFIX` + `isReservedSubflowSegment` | `src/lib/conventions.ts` | 9.49.0 |
| keeping a large tool result out of the model's context | `artifacts` (bare store, or `{ store, placement: { maxInlineChars }, recordings }` — `placement` omitted ⇒ never placed; `recordings: true` / `{ label }`, default off) + `wants` + `placement` | `src/artifacts/` | 9.21.0 |
| opening selected JSON from a host-admitted artifact without loading its whole payload into model context | `artifactDetailTool` + `ArtifactDetailError` — fresh host admission per call, small values, paginated child actions, whole-scalar omission and bounded context; no automatic store access, source read or claim authorization | `src/tools/artifactDetailTool.ts` | 10.7.0 |
| the ticket placement just minted being REFUSED by your own `wants` argument as a kind mismatch — because the mint says `tool-result/<toolName>` and your consumer says `dataset/rows` | `Tool.resultKind` + `placedResultKind(toolName, declared?)` (the mint speaks the consumer's vocabulary; the exact-match matcher is untouched) | `src/artifacts/placement.ts` | 9.70.0 |
| a viewer guessing which column of a dataset is time (epoch seconds under `ts`, wall-clock strings, a `bucket` column) and opening a series as a table | `timeAxis` on `PutArtifactInput` / `ArtifactMeta` (column · `'iso'`/`'epoch-s'`/`'epoch-ms'` · zone · interval · aggregate) + `readTimeAxis` + `describeTimeAxis` + `timeAxisIssues` (judged at mint, never repaired) | `src/time/axis.ts` | 9.128.0 |
| a chart or check comparing a dataset's time values as spellings — epoch seconds beside ISO strings, `+02:00` beside `Z`, a wall-clock string with no offset silently read as UTC, two `01:30` rows in the hour the clocks go back; a READ-SIDE view (the stored rows are never rewritten): UTC instants at one precision, sorted, each with its row; an offset-less value under a zone-less axis is counted (`status: 'naive-values'`) or refused (`naive: 'refuse'`), never read as UTC; a fall-back wall time placed by row order and noted, else counted `dstAmbiguous`; a skipped one `dstGap` (the axis is re-exported from `src/artifacts/`) | `normaliseInstants` + `NormalisedAxis` | `src/time/axis.ts` | 9.129.0 |
| a second parser for an instant, a look-back like `30m`/`24h`, a time zone or a `from..to` range inside the library — three separately written grammars used to disagree (an `iso-range` argument took 30 February, a declared period took a lower-case `t`); ONE owner now, with two named instant profiles (`lenient` for a period a result declares, `strict` for a value sent to a tool), a duration grammar with the unit set named per use (look-back `mhdw`, no digit cap; `s` only by opt-in), IANA zones through `Intl` with abbreviations and bare offsets refused, Temporal's four DST words, and one conversion per boundary a half-open range crosses; internal until the run clock ships | `instantOf` + `durationMs` / `spellDuration` + `isZoneName` / `wallToInstant` + `parseRange` / `spellRange` + `fromInclusive` / `toInclusive` | `src/time/` | 9.129.0 |
| a run that reads "now" and the person's time zone from the SERVER — "since 8" answered in the host's zone, a default nobody chose, a resume half an hour later silently re-anchoring the question — the clock as a declared, recorded input: the zone per run (`run({ time: { zone } })`), `.time({ zone })` an optional fallback, a run with neither REFUSED before the turn starts; `now` the app's (the message's time) else the turn's start, recorded `nowSource: 'default'`; one `clock` row per turn on the ledger (with a UI-set `time.window`, `source: 'control'`), one `call` row per dispatched call with `dispatchedAt`, a resume's differing `time` kept out and recorded as `clock-on-resume`; the limits block's `Period:` lines rendered in the run's zone with the zone named (`2026-10-09 08:00–08:40 America/Los_Angeles (UTC-07:00)`) while the typed record keeps the instants; nothing served to the model | `AgentBuilder.time()` + `AgentInput.time` / `AgentRunOptions.time` (`RunTime`) + `TimeClock` + `ClockRow` / `ClockOnResumeRow` / `CallRow` | `src/time/clock.ts` | 9.129.0 |
| an app's date parser silently choosing MDY, reading `8:40` as morning, guessing a year, taking `PST` as a zone, or re-reading a message differently on a resume — the person's words read ONLY through a reader the app arms (`.time({ reader })`, no default), which returns zone-less PARTS and a verbatim quote (checked: an out-of-text quote is refused and keeps no text); the library resolves every candidate window against the run clock (three date orders, am and pm, both instants of a DST overlap or gap, a day word, a look-back, a range read to the end of its grain — an o'clock end ends AT the hour, "8 AM to 9 AM" is one hour; a place named with `time` read as the one zone the tz database names for it; an abbreviation asked unless the app maps it with `policy.abbreviations`, and then offered as its zone AND its letters' offset when they disagree); a v1 policy (`dateOrder`, `year`, both `'ask'` by default) removes readings and the removal is recorded; one `time-reading` row per mention (or one `mentions: 0` row), once per message — only a message a person wrote, never a composed run's — with the reader's id, version, kind and locale and the tz database version; a resume or retry reads the row, never the reader; a `kind: 'model'` reader's window is never `said` and waits for the person's confirmation; nothing served to the model | `TimeReader` + `TimeParts` + `TimePolicy` + `TimeCandidate` / `ReadingChoice` + `TimeReadingRow` (`.time({ reader, policy })`) | `src/time/resolve.ts` | 9.129.0 |
| every app re-checking a time answer the person typed into an ask — a window written backwards, a date-time with no offset, `PST` for a zone, a wall time the clocks skip — and re-asking in silence; a `requestInput` field declares a `format` — `'instant'`, `'time-range'` or `'zone'` — (refused unless `type: 'string'`; a malformed choice or supplied value refused at definition) and the resume door judges the answer with the time layer's one judge: a refused one is NOT taken — the same ask returns with `refused: { answer, reason }` and `repeat: { count }`, the field missing again, nothing run; the reason is a catalog sentence the app can reword (`.time({ messages })`), and only a check the app armed produces one (the DST check needs `.time()`'s zone); `labels` per choice, free entry open unless `strict`; over MCP a range travels as two `date-time` elicitation properties | `InputField.format` / `labels` / `strict` + `defaultTimeAskMessages` + `elicitationOf` / `answerFromElicitation` | `src/time/ask.ts` | 9.129.0 |
| a tool whose period is two epoch-millisecond arguments (or a joined range, a date, a look-back in seconds) being sent the person's window in the wrong shape, rounded silently, or overwritten — and every host tool re-parsing its own argument to say what its read covered; the tool DECLARES every shape its period takes (`forms`: `bounds` · `joined` · `object` · `day` · `lookback` with `units`; today's `{ argument, spelling }` is shorthand, plus `accepts`, `wall-range` + `zoneArgument`) and facts about its source (`direction`, `retention`, `maxRange`, `granularity`, `filtersToAsked`, `wallZone`), judged at definition, dispatch and MCP ingest (`_meta.agentfootprint.period`); under `.time()` a period the model left out is filled from the turn's ONE window of the person's (a settled reading, or a UI `time.window`) into the first form that holds it exactly, filed as theirs (`said` + `matched: 'mention'`; a model reader's as a reading; a UI window as `app`); two mentions fill nothing; a sent window is bound by quote or value, or runs as sent and is recorded `model-chosen` (standing "not sure"); one `call-window` row per call; the tool is handed `ctx.time` (`asked`, `zone`, `now`, `dispatchedAt`; over MCP in the call's `_meta.agentfootprint.time`); the tool's facts join the time answer's re-check | `ToolPeriod.forms` / `PeriodForm` / `PeriodFacts` + `ToolExecutionContext.time` (`TimeContext`) + `CallWindowRow` | `src/time/convert.ts` | 9.129.0 |
| a tool whose period cannot hold the person's window exactly being sent a narrower window, a silently wider one, or a window its source cannot hold — a look-back tool asked for "yesterday", a `day` tool asked for two days, a log store asked for tomorrow, a look-back re-read half an hour later after a pause; under `.time()` the fill takes the first exact form, else the first that holds MORE (a whole day; the covering look-back from now, never over `maxRange`) and records it (`call-window` `sent` + `differs.extra`, or `trimmedByTool` under `filtersToAsked`; the note says wider); a window outside `direction`, wholly beyond `retention`, over `maxRange`, spanning days for a `day`-only tool, or a sent wall time the zone skips is REFUSED before dispatch with a past-tense reason naming the declared fact (`how: 'refused'`); a window partly beyond `retention` runs, marked; after drift past the tool's step the library's own look-back fill is redrawn into an absolute form and the model's runs as sent (`call` row `drift`: `redrawn` or `shifted`) | `PeriodFacts` + `convertWidened` + `driftAtDispatch` + `CallWindowRow` + `CallRow.drift` | `src/time/` | 9.129.0 |
| reading a person's time words with a hand-rolled parser that guesses — `10/09/26` silently MDY, `PST` taken as a zone, "yesterday" read out of "yesterday morning" — then making the model re-derive the window from words, and asking about dates nobody needed; the library's careful English reader (no dependency, `kind: 'rule'`) TOKENIZES a small, closed set (ISO dates and instants, numeric dates with the order left open, clock times, a range of two, a zone written after them or after a day word — `yesterday London time` included, today / yesterday / tomorrow, "last 40 minutes") and says "unreadable" — one mention, the whole phrase — for every other time phrase it recognises; a reading only PROPOSES (owner decision "Always confirm": nothing typed in chat is filed as the person's words) — when a tool that declares a period is about to be called, it is offered as a pre-filled, editable one-click confirmation naming its window AND its zone (the zone asked first for an abbreviation, the readings the tool's facts allow as labelled choices), written into the tool's own arguments and filed `answered` with a `time-answer` row (`confirmed` click or `edited`); from then on the turn's later calls are filled from it and it is served on every tool that declares a period, in that tool's own form, with its source; when every reading breaks the tool's facts the call is refused, nothing asked | `englishTimeReader` + the window field of the batch ask + the `time-answer` row + the served time sentence | `src/time/readers/english.ts` | 9.131.0 |
| an answer about the person's time window marked "not traced" for `08:41`, `-07:00`, `PDT` or the epoch the call ran with — or, the other way, the model's echo of the library's served time line passing as the person's words; under `.time()` beside the evidence gate, ONE owner says which spelling of a time is whose: the parts the person wrote, at the grain they wrote them ("8 AM" is also `8:00`/`08:00`; a confirmed `10/09/26` is `2026-10-09`), and every part of a window they typed or set in a UI, are theirs (exempt); what the library produced from a reading (an implied year, the abbreviation in effect, offsets, UTC and epoch spellings, the end-of-grain minute, a look-back's duration, the values it filled, the served time line, any part of a `model` reading) is filed as ONE `time-derived` row and folds `derived-from-reading` — "not sure", never "known", never called invented; a time no reading produced is still flagged; the gate's private clock table is retired into the same owner | `timeFormsOf` + `TimeDerivedRow` + `derived-from-reading` + `EvidenceVerdict.derived` | `src/time/forms.ts` | 9.131.0 |
| an answer standing on a read that is not the window the person asked about — a store that clamped 30 days to 7, a covering look-back that read two days for one, a look-back re-read half an hour late after a pause, a model's drill-down reported as the person's window, a window older than the source keeps — while the answer reads as if it covered the ask; under `.time()` the results layer compares each call's READ (the result's declared `period.queried`, read back with the tool's `granularity`; else the widened fill; else the look-back shifted by its drift) with what it ASKED (for a window the model chose, with the person's): `missing` (asked, not read) and `extra` (read, not asked) on the call's `period` row (`differs`, plus `shifted`, `beyondRetention`, `partlyBeyondRetention`), the event names the checks (never a range), the standing folds `period-differs-from-asked` (TQ8: a wider read too, unless the result declares it read exactly what was asked) and `period-beyond-retention` — "not sure" — and the limits block says each in the person's zone; a dataset whose declared axis names a zone files a `source-clock` row and a `Clocks` line (two zones: the clocks differ — a label only); a period's offset is never read as a clock | `PeriodRow.differs` / `shifted` / `beyondRetention` + `period-differs-from-asked` + `period-beyond-retention` + `SourceClockRow` | `src/time/check.ts` | 9.131.0 |
| an app keeping its own copy of the library's period conversion — to preview what a tool would read for the person's window, or to build a call itself — and drifting from what the fill, the time ask and the served line send; the same question the time layer asks of each tool, over the tool's own `period` and a clock the app passes: a fact the window breaks, else the first form that holds it EXACTLY, else the first that reads MORE within `maxRange` (with the range it reads and what it adds, marked when partly older than `retention`), else `multi-day` / `no-form-holds` (`convertForTool`, the forms and step read the run's way by `sugarForms` / `granularityMsOf`); the steps alone too (`convertExact`, `convertWidened`, `periodFactProblem`) — no run, no record, no `.time()` | `convertForTool` + `sugarForms` + `granularityMsOf` (+ `convertExact` / `convertWidened` / `widestMsOf` / `periodFactProblem`) (`agentfootprint/time`) | `src/doors/time.ts` | 9.132.0 |
| letting the UI draw an artifact without the model naming a component | `registerArtifactComponent` (in the `agentfootprint-lens` package) | — | — |
| finding which tools the model keeps writing by hand | `agentfootprint.tools.code_run` + `codeShape` | `src/core/codeRunnerTool.ts` | 9.46.0 |
| showing the script an answer was computed with — the program a code-running call SUBMITTED attached under the answer from the run record (text after `---` on a prose answer, data on `turn_end.scripts` always), labelled only with what the record proves (an explicit exit code, a runner's bare verdict, an unknown outcome, a call that returned with no runner facts); and a fenced block the model wrote read as script content only when it equals, byte for byte after removing the Markdown wrapper, a program this turn's call submitted — no value candidates, never evidence — while any other block stays prose, labelled `unmatched` (`script-unmatched` under the answer layer). Blocks are read where a CommonMark renderer shows them, and reading stops where a renderer could pair fences another way, so no part of a shown program can render as an unchecked sentence. The audit chain's bounded mode keeps each attached program out (`'[N chars]'`). The attachment never renders inside a block the answer left open (a fence it closes first; the programs kept to the data after a line it cannot vouch for), names and ids are inert code, and a row attests the exact dispatch: the program read before the tool ran, the runner's facts of that call alone. A tool is code-running because it declares so, never by name | `Tool.script` (`defineTool`; `codeRunnerTool` declares it) + `.scriptTravelsWithTheAnswer({ which, maxBytes })` + `.namesAndNumbersFromEvidence({ code: 'match' })` + `evidence_checked.scripts` / `turn_end.scripts` + `ScriptRow` on the findings ledger | `src/core/agent/scripts/` | 12.3.0 |
| a skill wrongly activated by a keyword staying loaded for the whole turn — suspending/parking a map's prompt + tools when its contribution goes unused, WITHOUT touching its cursor, and re-engaging it on evidence | `.maps({ renewalGrace })` (default 3) + `MountedMap.nonParkable` + `advanceEngagement` + `agentfootprint.map.engaged/parked` + skip reason `'parked'` | `src/maps/` | 9.58.0 |
| a value that must say how it knows itself — an unknown count that can never render as zero | `Claim<T>` (`known`/`unknown`/`notApplicable`) | `src/lib/claim/claim.ts` | 9.58.0 |
| asking a debugging model "what did this run contradict itself about, and why?" — the Context Integrity findings, joined to the step that filed them | `find_context_errors` | `src/trace-toolpack/traceToolpack.ts` | 9.61.0 |
| reading abnormal nodes, recorded dependency roots and downstream impact with bounded public Foottrace queries, scoped proof IDs and explicit unknown coverage; optional host-authorized storage graph, including inner tool and subflow questions | `attention_list` / `root_cause` / `impact` + `TraceToolpackArtifacts.cause` + `TraceToolpackOptions.cause` | `src/trace-toolpack/causeTools.ts` | 10.7.0 |
| admitting a completed native root-log capture for read-only cause tools without a custom graph; partial, legacy, child and resumed coverage stays uncertain with fixed reasons | `assessCapture` | `src/trace-toolpack/causeCapture.ts` | 10.10.0 |
| loading one authorized, acknowledged saved root history and asking the existing bounded cause questions without a synthetic snapshot; separate native/storage assessments, privately bound graph identity, missing-value observations preserved; unproven source ownership stays unknown | `readStoredCauseArtifacts` + `traceCauseTools` (`/observe`) | `src/trace-toolpack/storedCause.ts` | 11.1.0 |
| the model inventing an id or a reading that no tool ever returned — a deterministic fabrication detector on the final answer, with a posture for how hard it pushes back | `.namesAndNumbersFromEvidence()` + `posture` (`'assist'` record+flag, the default / `'guard'` one revision then ship flagged / `'rails'` refuse instead) + `shapes` + `exempt` + `minDigits` (default 4) + `nudge` (default off) | `src/core/agent/evidence/` | 9.35.0 |
| plain decimal readings honestly rounded at the written precision (opt-in; one carried number only, no new unitless arithmetic), and exactly-two numeric range endpoints checked after declared ID shapes; short bare endpoints retain minDigits | `AgentBuilder.namesAndNumbersFromEvidence` + `NamesAndNumbersOptions` + `EvidenceVerdict` + `ComputedFigure` | `src/core/agent/evidence/` | 10.8.0 |
| a script's stdout printing `1.12e-02` and the answer's correct `0.0112` flagged as invented — the same number in scientific notation grounding the decimal (and the reverse) at the answer's precision under `decimalRounding`, in exact decimal arithmetic, never across magnitude; hashes and versions (`9f3e10ab`, `1.2e3-rc`) never read as numbers | `decimalRounding` + `scientificDecimalOf` | `src/core/agent/evidence/` | 11.3.0 |
| counting a HUMAN-VERIFIED value as ground for the choice-seam check — the person clicked a row, the app verified the cells against the artifact, and the id the model takes from that selection is not fabricated; the source label travels onto the record | `externalGrounds` + `ExternalGround` + `external_ground_used` | `src/integrity/unsupported-argument/check.ts` | 9.72.0 |
| following exact read versions in an acknowledged saved root history using independently collected native namespace and invocation ownership; missing ownership stays unknown, partial capture keeps gaps, and recorded reads never prove output derivation | `readStoredCauseArtifacts` + `traceCauseTools` (`/observe`) | `src/trace-toolpack/storedCause.ts` | 11.1.0 |
| a lookup tool that answers "nothing found" for EVERY id because a filter broke, and an agent reporting that absence as fact — the run itself produced the id, the lookup came back empty, and that PAIR is worth a look | `noticeEmptyLookups` + `readLookupResult` + `EMPTY_LOOKUP_CEILING` (dial default OFF and needs a tool declaring `argumentsFrom` too — two halves; kind `empty-lookup` at seam `write`, always `advisory: true`; empty = a zero-length ARRAY or an `absent()` envelope, every other shape files `not-applicable` and no finding; the ceiling is quoted verbatim into every message) | `src/integrity/empty-lookup/` | 9.77.0 |
| an agent answering a NEW question out of an OLD turn's tool results — grounded, four turns stale, and the evidence gate approving it (`Tool calls 0` and "all 7 values were found in what the tools returned"). ALSO the bug half: the gate's two sentences claimed the flagged values "appear in no tool result FROM THIS TURN" while its index walked every `role:'tool'` turn — a boundary asserted and never measured; both now say "no tool result this run read" | `noticePriorTurnEvidence` + `PRIOR_TURN_EVIDENCE_CEILING` + `priorTurnEvidenceOf` + `AnswerGroundingReading` (dial default OFF and needs `.namesAndNumbersFromEvidence()` too — two halves, and the second is structural: the gate owns the extractor that decides which tokens are values; kind `prior-turn-evidence` at seam `claim`, always `advisory: true`; `EvidenceCorpus.values` became a `Map<form, turn>` stamped in the walk that was already happening, a TURN = each `role:'user'` message `isLibraryAuthoredTurn` did NOT write (counting the gate's own correction would file against every revised answer); fires when ≥1 value is grounded and NOT ONE came from the turn being answered — ONE current-turn value files nothing, which is what keeps an honest follow-up quiet and is the design's falsification test; the zero-tool-call turn is the SAME kind with a stronger witness, not a second kind; corpus is the LIVE WINDOW so ordinals are window-relative and the distance is a FLOOR, while the boundary stays exact (`'current-request'` is un-droppable); memory/RAG values are exempt from grounding and invisible — it can under-report, never over-report; the ceiling is quoted verbatim into every message) | `src/integrity/prior-turn-evidence/` | 9.83.0 |
| a tool's ROWS quietly disagreeing with what the tool promised — a LUN 0 stored as `""` because it is falsy, a numeric column arriving as quoted strings, a declared column present in no row at all | `Tool.resultColumns` + `checkColumnTypes` + `COLUMN_TYPE_CEILING` + `readRowset` (dial `'off'` (default) / `'warn'` (file findings, model reads the rows unchanged) / `'enforce'` (refuse the rows, `resultCeiling`'s teaching-sentence idiom, delivered status `'invalid'`) — the `toolArgsValidation` trio, this seam's mirror; needs a tool declaring `resultColumns` too — two halves; types `number`/`string`/`boolean`/`date`, bare word or `{type, nullable}`, deliberately NO `'unknown'`; OPEN — unlisted columns allowed and unjudged; TWO kinds at seam `write` — `column-type-mismatch` (there, wrong type) vs `missing-column` (declared, in no row); rowset = an ARRAY OF PLAIN OBJECTS with ≥1 row, everything else incl. the ZERO-ROW result files `not-applicable` (empty is `empty-lookup`'s subject); travels MCP `_meta`; the ceiling is quoted verbatim into every message) | `src/integrity/column-types/` | 9.78.0 |
| verifying the model-selected negative kind against cited native reads, normalizing only none/not-found over declared absence; optional subject phrases must include the recorded populations, and delivered subjects come from the searches; complete requires an explicit claim and completeness evidence, with visible absence-only fallback when it fails                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | `.negativeClaims({ field, tools })` + `NegativeClaimCitation`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | `src/core/agent/negativeClaims/`                        | 11.0.0                          |
| observing data-anchored negative prose without an output schema, citation field or answer validator; native read evidence only, unchanged delivery and normal streaming, no correction or withholding. An eligible grammar match with no dispatched native read recorded for the current turn files one no-read note per answer; earlier-turn reads do not suppress it, but recorded failed or kept-out reads do. This presence check proves neither that no read occurred nor that evidence is usable. The observer filters exact normalized ordinary clauses None / No action is needed / Tests: 0 failed; added words or separate negative clauses remain eligible. Current-turn findings make the assessment not-sure; no finding never means verified. Bounded English heuristic with code/language/limit notes and optional literal phrase extensions; prior detector failed every free-replay benchmark gate, revised data-anchored path remains unmeasured. App validation stays independent; prose-only plus outputFallback is refused | `.negativeClaims({ prose: true, phrases? })` + `NegativeClaimsOptions` + `agent.findings()` + `agentfootprint.findings.negative_claim` (bounded locations, codes, phrase version and native references; no answer text or tool values) | `src/core/agent/negativeClaims/` | 11.0.0 |
| deriving a typed negative's kind and searched subject from the cited native records; model supplies only result handles, legacy kind/about never steer checks, and advisory display text remains unchecked; other searches are compared using recorded populations | `.negativeClaims({ field, tools })` + `NegativeClaimCitation` | `src/core/agent/negativeClaims/` | 11.0.0 |
| checking an agent's "there are none", "not found" or "that is the full list" against what its searches were actually made of — by code, never a model: a TYPED claim cites the library-minted result identities (or served handles) of the reads it rests on and is established only from those reads (this turn, read and returned, a local composed row, the kind fitting the row), withheld when rows on the same, a narrower or a wider search contradict it, a same-search call fails after it, or any call not classified as a read runs after it; the library writes the statement, attributing every text to the tool, the model or the result and listing every gap as "not checked". The typed path is UNMEASURED. A PROSE claim found by a fixed, versioned and hashed English grammar is only NOTED (`scope-wider`, `coverage-gap`, `entity-unresolved` …); on a first free replay against model-rater labels the prose grammar FAILED every gate it could be read on | `judgeTypedNegatives` + `judgeProseNegatives` + `composeStatement` + `NEGATIVE_CLAIM_PHRASES` (internal; typed checker mounted by `.negativeClaims()`, prose checker mounted observe-only with whole-answer limits) | `src/integrity/negative-claim/` | 11.0.0 |
| a model head-mathing a total from tool-result numbers while a compute tool sits unused on the wire — a LATE line each iteration naming the staged refs and the `wants` tool that spends them (recency working FOR the instruction), plus the revise correction naming the same route | `nudge: true` (default off) + `stagedRefsNudgeLine` + `findStagedRefs` + `grounding_nudged` | `src/core/agent/stagedRefs.ts` | 9.75.0 |
| a tool answering "I looked and found nothing", routably | `absent` + `looked_for` + `checked`/`not_checked`/`cannot_cover` + `tryInstead` (delivered status `'absent'`, routable by `onToolStatus`) | `src/semantics/coverage/absent.ts` | 9.43.0 |
| a lookup that checked a name against ONE known list and found nothing — a named miss over the names it read, never "it does not exist": the names facet in `checked` (dated when `measuredAt` is an instant), the gap "names outside <source>" (`{ kind: 'scope', relevance: 'bears', bears: 'source' }`) the author cannot remove except by `authoritative: { why }` — and, for `partial: true`, also "not every name in <source> was read" (`bears: 'completeness'`), which nothing removes — and `provenance` composed; `names` read only to check `count` (`names-count-mismatch`) and never served | `absent({ what, among })` + `AmongDeclaration` + `namesFacetOf` | `src/semantics/coverage/among.ts` | 11.0.0 |
| a build gate for declarations that claim more than was read — a refusal that says what does not exist, an absence that calls the input wrong, every `authoritative` names list for review | `checkSemantics(entries, { strict?, rules?: ['overclaim'] })` + catalog `refusals` (strings, values, or `{ text, argument?, value? }`) + codes `refusal-asserts-absence` / `absence-asserts-wrongness` / `authoritative-names` (warnings, only for samples using the new fields, and only on a sentence that names the looked-up argument or value; a format, range, permission or transport sentence exempts a wrongness phrase, never a non-existence one; `--strict` or `--rules=overclaim` make the first two errors) | `src/semantics/overclaim.ts` | 11.0.0 |
| stating what a clean answer does NOT rule out | `coverage` + the declaration `{ checked, not_checked?, cannot_cover? }` | `src/semantics/coverage/ledger.ts` | 9.43.0 |
| a tool's "nothing found" stating exactly what ONE call searched — declared once as typed data (every argument classified by role, defaults, built-in qualifiers, blind spots each bearing on a facet) and composed by the library from the arguments the call ran with, so it can only under-claim relative to a true declaration; a remote declaration establishes nothing; the row addressed by a library-minted result identity | `Tool.search` (`defineTool({ search })`: `source`, `subject`, `fixed`, `arguments` with `role` (incl. `query` / `match`) + `whenOmitted` (a value or `{ any: true }`), `complete`, `gaps` with `bears`, `limits.cap` / `limits.byCredential`, `ranking: 'approximate'`; judged at `defineTool` and again at registration) + `effects: 'read'` (local, `defineTool` only) + `absent({ checked: 'declared' })` / `coverage(rows, { checked: 'declared' })` + `resolved` / `paging` on a result + `ComposedCoverage` / `isComposedCoverage` (the `coverageDeclared` row: typed `checked`, `gaps`, `complete`, `composed: 'local' or 'remote'`, `resultId`, `handle`); events carry references, never a composed value; `check:semantics` lists `source-said` / `said-overlap` | `src/semantics/coverage/search/` | 11.0.0 |
| a TYPED answer (`.outputSchema()`) carrying the limits its tools declared WITHOUT breaking its JSON — the same limits `.limitsTravelWithTheAnswer()` appends to a prose answer, as data beside it (checked / not checked / cannot cover, duplicates said once, every entry kept); `runTyped()` parses the model's answer untouched | `agent.answerCoverage()` + `turn_end.answerCoverage` + `AgentState.answerCoverage` (with `.outputSchema()` + `.limitsTravelWithTheAnswer()`; absent when no tool declared a limit) | `src/core/agent/stages/answerCoverage.ts` | 9.121.0 |
| a person-readable short form of a coverage item, and whether an unchecked item is about EXISTENCE (does the thing asked about exist at all) or SCOPE (outside what the tool reaches) — on the record for a report, never in the model's request, never evidence. `short`: at most 80 chars, one line, never longer than `what`. `kind`: 'existence' or 'scope', on notChecked and cannotCover only. Refused at `absent()` and `coverage()`, read-never-repaired on a foreign envelope; carried by `tools.absent`, `tools.coverage_declared` and `coverageDeclared`; stripped from every served envelope, with `tool_end.modelResult` stamped where it differs | `CoverageItem` + `absent` + `coverage` | `src/semantics/coverage/` | 9.116.0 |
| a skill's plain name for a person ("array estate report" for `array-inventory`) on the recording, without changing what the model reads or what a graph draws — at most 60 chars, one line of plain visible text, not the id; carried as `skill.graph_declared` `nodes[].title` on a skill-graph agent, absent when not declared | `defineSkill` + `title` | `src/injection-engine/factories/defineSkill.ts` | 9.116.0 |
| how many of an answer's values the evidence check actually LOOKED UP — `candidates` also counts values the person's message or the app's instructions already held, which are never looked up; found plus not found, before the 12-value slice, on every `evidence_checked` | `lookedUp` + `EvidenceVerdict` | `src/core/agent/evidence/` | 9.116.0 |
| explaining ONE answer to a person in plain words — what was asked, how it was routed, what each tool checked and did not check, what it found, how sure, and anything wrong — every line from the record by a fixed versioned template (never a model), each with its template id and who vouches for it (person / library / tool / model / app), "not recorded" instead of a guess, own-run events only, three checks with the ones that could not run said aloud | `accountForAnswer` + `AnswerAccount` | `src/answer-account/` | 9.116.0 |
| a tool whose default NOBODY CHOSE (a log search that quietly defaults to 2 hours) — the LIBRARY fills a value the call left out from the tool's declared rule, never the tool; files one `argument` row per ruled argument per call on the findings ledger (`source: 'default'` filled or echoed — `proposed` tells them apart — or `'model'`), stamped with the conversation `turn`; tells the model in a past-tense note (only for a value the call ran with) behind a `toolChars` boundary every reader of a result reads through, so the note grounds nothing and hides no empty result; the answer's standing reads "not sure — assumed"; an "Assumed" block under `.limitsTravelWithTheAnswer()`; carried over MCP `_meta`; a ruled tool on an agent without the layer is REFUSED before any middleware asks, never run unruled; `{ ask }` is the batch ask (next row) | `Tool.askOrAssume` + `Tool.period` (`ToolPeriod`) + `ArgumentRow` + `agentfootprint.findings.argument` + `AgentBuilder.inputsLayer()` + `allow(value, why, { from })` | `src/core/agent/arguments/` | 9.122.0 |
| a value the PERSON must give (a look-back period, a site, a row limit) that the model left out — the library asks ONCE for everything the batch needs, before anything in it runs, through the typed ask `requestInput` uses (`isInputPause`, `agent.resume(cp, { requestId, values })`, `standingAgent`): one field per distinct (tool, argument), positional ids, the author's question and choices, `context.agentfootprint` naming each field's calls, one period asked once across spellings (`24h` ↔ `-24h`); the answer re-checked against the property's own schema and asked again (at most three times) when it does not fit; `asked` then `answered` rows; the standing reads "ask" while it waits (`argument-asked`); the resume adds no model call; one human question per resume — a call refused a second pause in the batch that asked keeps the person's answer, so proposed again it runs with it and pauses for its own step (the person is asked once); the resume event carries the reply's shape, never a value | `askOrAssume: { x: { ask, choices? } }` + `argumentAskContext` + `ArgumentRow` (`asked`, `answered`, `free`) | `src/core/agent/arguments/` | 9.123.0 |
| a typed ask asked AGAIN after your app refused the person's answer, with the person seeing the identical question and never learning why (a time window whose year the app turned down, a reason told only to the model) — the re-ask declares the refused answer and the app's OWN reason and it rides the awaiting-input shape the person receives (pause outcome, checkpoint, `pause.request` event); with no declaration the runtime still marks a re-ask of the same `id` in the same turn: how many times it was answered, and the previous answer as the record holds it (after the result rules, redaction first) — facts only, never a reason the library wrote | `InputRefusal` + `InputRepeat` (declare `refused: { answer?, reason }`; read `awaitingInput.refused` / `awaitingInput.repeat`) | `src/tools/inputRequest.ts` | 9.127.0 |
| where each argument value CAME FROM — the model says so (`_findings.from`: the person's words as a `quote`, a result `id`, an earlier answer, the app, or `'assumed'`) and the library CHECKS each claim before the batch runs: the quote in the person's own messages (never a composed step's message), a phrase the author declared on a choice (`said`), the value in the named result's own bytes (numbers and booleans in compact JSON found; a library note never counts), the person's earlier answer (`24h` ↔ `-24h`), the app's text and `externalGrounds`; a failed claim is filed as written, a library lookup is only a hint (`coincides`); an untraced value on an `ask` argument is ASKED (a reading shows the person their own words, `quoted` — no quote is shown on a row or an ask while a tool in reach can hide arguments: a registered argument view, or any ToolProvider); the standing reads `argument-read` / `value-contingent` / `argument-unverified`, and checked values read "consistent with the record" — never "known"; compositions mark another runner's output (`AgentInput.messageFrom: 'composed'`); `from` rides ruled tools only (first and required), explained once in its own property, with or without the findings ledger | `.inputsLayer({ argumentSources: true })` (or `.findings({ argumentSources: true })` beside the ledger) + `ArgumentRow` (`claimed`, `matched`, `quote`, `failed`, …) + `AgentInput.messageFrom` | `src/core/agent/arguments/` | 9.126.0 |
| a result that does not say WHAT TIME its read covered — "no failed backups in the last hour" answered from an export that ends at 02:00 — the result declares its period on the doors it already returns (`absent()`, `coverage()`, `describedResult()`: `period: { queried, held, readAt? }` with `held` a range or `'unknown'`, ISO 8601 instants with a zone, never parsed durations) and an absence its source and time (`provenance`, `describedResult()`'s shape); one rule set refuses a malformed one at the mint and reads a foreign one without repair; the coverage channel carries it (`coverageDeclared`, `tools.absent` / `tools.coverage_declared`); the results layer (`src/core/agent/results/`) at the loop head files ONE verdict per call — covered · partly-held · not-held · unknown · undeclared (a tool with a period argument whose result said nothing) — and the answer's standing reads "not sure" for all but covered, joined to the inputs layer's row for who chose the period; a `Period:` line under `.limitsTravelWithTheAnswer()` and `periods` in a typed answer's limits; `canonical-notes.json` publishes the wire spelling for foreign minters | `period` on `absent`/`coverage`/`describedResult` + `DeclaredPeriod` + `periodVerdict` + `PeriodRow` + `PERIOD_WIRE` + `AgentBuilder.resultsLayer()` + `agentfootprint.findings.period` | `src/semantics/coverage/period.ts` | 9.125.0 |
| a tool that counts a vendor's IN-FLIGHT state as a failure — a backup still running read as unprotected, 96 sessions still synchronizing read as "non-OK" — because its test was equality with the success value: a vendor state machine has three outcomes (settled-ok, in progress, failed), and the TOOL declares which of its items are still running on the `coverage()` it already returns (`inProgress`: a list of strings or `{ what, why?, short?, count? }`, needs `checked` beside it; the library never reads a state name); one rule set refuses a malformed list at the mint and reads a foreign `in_progress` without repair; the model reads `in_progress` plus one static clause after the note (served only when declared); `tools.coverage_declared` and `coverageDeclared` carry it; an `In progress (outcome not known yet):` section under `.limitsTravelWithTheAnswer()` and `answerCoverage.inProgress` in a typed answer's limits; the answer's standing is NOT changed (a label, not a reason); `canonical-notes.json` publishes `IN_PROGRESS_WIRE` | `inProgress` on `coverage` + `InProgressItem` + `IN_PROGRESS_WIRE` + `IN_PROGRESS_CLAUSE` + `IN_PROGRESS_SECTION_LABEL` + `AnswerInProgress` | `src/semantics/coverage/inProgress.ts` | 9.126.0 |
| how far an answer STANDS — known · consistent with the record · not sure (with the reasons) · ask · not assessed — folded from the run's COMMITTED record (never from the model's confidence, never from events, never "known" from silence: only a passed enforce answer check supports it); each reason names the row it rests on, `checked` counts what ran, this turn only; a pause of any kind reads `ask` from the recording alone, a run with no answer has no standing; one emptiness reader shared with the answer account (`agent/emptiness.ts`: a `coverage()` envelope read through, a described result counted, an undeclared empty rowset said aloud) | `assessAnswer` + `AnswerAssessment` (on the observe door), `agent.assessment()` | `src/core/agent/assessment/` | 9.120.0 |
| the answer's standing line on EVERY reply turning into boilerplate readers skip — and "not assessed" reading as doubt on a correct answer: say the line only when it matters (`'unless-consistent'` leaves "consistent" unsaid, `'not-sure-or-ask'` also "not assessed" and "known"; `ask` always said), while the standing still travels as data in every mode, the limits block stays, memory keeps exactly what was delivered, and no separator is added when nothing follows; no line is never a check | `AgentBuilder.answerLayer({ standingLine: { when } })` + `AgentOptions.answerLayer` | `src/core/agent/AgentBuilder.ts` | 11.4.0 |
| knowing WHICH calls a "not sure" rests on — or that it rests on the answer itself or on the run as a whole — and naming those calls to a person in the app's own words, never by their registered tool name (the time-layer lines and the answer account still print registered names — a follow-up): each reason carries its source (the kind declared per reason, the calls read off the reason's own rows; names and ids only, a call the record cannot name counted, never dropped); a tool declares `displayName` (one plain line, at most 60 characters, refused with a control, separator or bidi character) and the standing line names a reason's calls only by it, the declared ones named and the rest counted, while the limits block marks each item's declaring calls the same way; no `displayName` → byte-identical; the standing never moves | `AnswerAssessment.reasons[].source` + `defineTool({ displayName })` + `Tool.displayName` + `turn_end.standingLineNames` | `src/core/agent/assessment/sources.ts` | 11.4.0 |
| the answer's standing INSIDE the run, the moment the answer exists — the same fold as `agent.assessment()`, run as the first stage of the final branch and served as DATA (`turn_end` and one event: the value, its word, the reason kinds, the checks that ran — never a value or a quote); the evidence gate's clean pass and an answer given before a skill's declared steps finished committed as witness rows (so a clean gate reads "consistent", never "known"); an opt-in line appended to a PROSE answer (refused beside `.answerValidation()` and `.outputSchema()`); the in-run standing equals the fold of the saved recording; a typed answer's limits carry the values a tool's rule assumed | `AgentBuilder.answerLayer({ standingLine? })` + `turn_end.answerAssessment` + `agentfootprint.answer.assessed` + `GroundedRow` / `StepsUnfinishedRow` + `AnswerCoverage.assumed` | `src/core/agent/assessment/` | 9.124.0 |
| serving that explanation to a browser without the recording: one opt-in wire op returns the account plus allow-listed "show me" leaves, owner-only on the `artifact-get` path, lane-free, cached and single-flight, a stated size ceiling, and a swept record answering "not available" even when cached | `standingAgent({ answerAccounts })` + `{ op: 'answer-account', ref }` + `answerAccountPointerKey` | `src/hosting/answerAccounts.ts` | 9.116.0 |
| bounding what one session can ask of the artifact door at once: `artifact-head`, `artifact-get` and `answer-account` counted together per session, after the ownership check, refused 429 by name while every other session is served | `standingAgent({ artifactOpsPerSession })` + `ArtifactOpsBusyError` | `src/hosting/standingAgent.ts` | 9.116.0 |
| minting one of those shapes from a tool that is NOT JavaScript — the canonical note sentences and reserved marker keys as DATA, so a Python/Go/Rust sidecar reads a file instead of regex-scraping `dist/esm` (which a consumer really did) | `canonical-notes.json` at the package root + the `./canonical-notes.json` exports entry, GENERATED from the built barrel by `scripts/gen-canonical-notes.mjs` | `scripts/gen-canonical-notes.mjs` | 9.70.0 |
| proving a session store really honours the port | `runSessionLifecycleConformance` + `declared` skips (by case name WITH the reason; a declared case still RUNS) + outcomes `'passed'`/`'not-applicable'`/`'declared'`/`'failed'` | `src/hosting/conformance/` | 9.37.0 |
| deciding who owns a contested session write | `resolveSessionOwner` | `src/hosting/sessionOwnership.ts` | 9.37.0 |
| a session list that badges only what still waits on a person — an answered pause, or one no answer can reach any more, lists as a conversation; the one reading every store that lists reports (the conformance battery holds a store you wrote to it) | `listedFormatOf` + `SessionSummary.format` | `src/hosting/answeredPause.ts` | 11.0.0 |
| a host filing its OWN per-turn artifacts (a story, the person's clicks) where `artifact-get` will look for the same caller — never composing a scope itself; awaited inside the turn, bounded (`turnArtifactsTimeoutMs`, default `TURN_ARTIFACTS_TIMEOUT_MS`), revoked after it (`TurnArtifactsExpiredError`), failures on the stream (`artifacts.hand_over_failed`) | `HostReply.turnArtifacts` + `TurnArtifacts` | `src/hosting/turnArtifacts.ts` | 9.115.0 |
| a host reading or filing artifacts in the scope a redemption by the same caller reads, OUTSIDE a turn (a read by ref before a run, an app-owned route) — the redemption door's verifier (the person a token or a sign-in key proves — a `HostRequest` goes in as it is — or at an open door the session id and claim), session-id check, ownership rule and ONE scope composer; refusals as values (`unverified` / `unavailable` / `no-session` / `invalid-session` / `not-found` / `no-store`); never builds or evicts an instance, never the unscoped store; revoked with its instance | `handle.artifactsForRequest` + `ArtifactsForRequestResult` | `src/hosting/standingAgent.ts` | 9.117.0 |
| refusing a forged cross-site request, DNS rebinding or an oversized session id at the hosting door — before any handler, in every identity mode; the same rule for an app's own routes beside the door | `doorGuard` + `checkSessionId` + `MAX_SESSION_ID_LENGTH` + `CrossSiteOptions` (`allowedOrigins` / `allowedHosts` / `requireJsonContentType` on `httpHost`, `nodeHost` and the hosted-runtime adapters, which default the browser rules OFF on a non-loopback bind; a loopback bind defaults `allowedHosts` to the loopback names) + `AgentHost.onRefusal` (feeds the ingress record) | `src/hosting/doorGuard.ts` | 9.115.0 |
| choosing how callers prove who they are from CONFIG at install time — one strategy per deployment, a wrong or missing setting refused at boot by name (production must name its strategy, even `open`; a typo or a key a later release reads is never ignored), an unreachable IdP started with a banner warning and 503s, a boot banner with no secrets | `identityFromConfig` + `identityConfigFromEnv` + `IdentityConfigError` + `IdentityChoice` (strategies `open`, `oidc-token`, `local-password`, `directory-password`, `proxy-token`) | `src/adapters/identity/strategies/` | 9.117.0 |
| refusing an application's own token posing as a PERSON at the door — verifying an access token from AD FS, Entra ID, Keycloak or Okta by discovery, with AD FS's `access_token_issuer`, a required id claim (no `sub` default), `exp` required, the person test (required scope, `idtyp: app`, `oid == sub`, roles-without-scope, the obtaining client) and group overage refused as unknown | `oidcIdentity` + `IdentityFailureClass` `'not-a-user-token'` / `'wrong-client'` / `'roles-unknown'` | `src/adapters/identity/oidc.ts` | 9.117.0 |
| keeping a browser's sign-in cookie away from every handler and log line — the transport strips it and passes only its SHA-256 key; one credential per request (a token and a sign-in together refused); a WebSocket's sign-in checked before the 101, re-checked before every inbound frame, and the socket closed when the sign-in ends | `signIn` on `nodeHost`/`httpHost` + `HostRequest.signInKey` + `readSignIn` + `withoutCredentials` + `signInSource` + `SignInStore` + `IdentityVerificationOptions.signIn` / `tokenHeader` + `'two-credentials'` | `src/hosting/signin/` | 9.117.0 |
| a browser sign-in with a password for development, tests and demos — `/auth/config`, `/auth/me`, `/auth/login`, `/auth/logout`; an HttpOnly `__Host-Http-` cookie, no IdP token in the browser; the door guard on every login, one answer for every wrong credential after a minimum time, attempt limits with growing delay, a bounded per-process store; scrypt-hashed password lists only, refused in production | `signInDoor` + `memorySignIns` + `PasswordChecker` + `localPasswords` + `hashPassword` + `IDENTITY_STRATEGY=local-password` (`identityFromConfig` → `signInDoor` / `hostSignIn`) | `src/hosting/signin/door.ts` | 9.117.0 |
| signing people in through the company's identity provider in the browser (OIDC authorization code + PKCE) — PENDING INDEPENDENT REVIEW: a sealed per-attempt transaction cookie (nothing on the server), `returnTo` pinned to the public origin, the person read from an ACCESS token by the strategy's own verify (one id for browser and bearer), private-key or secret client authentication, AD FS `resource`, 303 with no-referrer, end-session | `oidcSignIn` + `signInDoor({ redirect })` + `safeReturnTo` + `IDENTITY_CLIENT_ID` / `IDENTITY_CLIENT_KEY_FILE` / `IDENTITY_PKCE` (`identityFromConfig` → mode `redirect`) | `src/adapters/identity/oidcSignIn.ts` | 9.117.0 |
| signing people in with their Windows password against plain Active Directory (no federation server) — LDAPS only, the empty password refused before the bind, RFC 4532 Who-am-I then exactly one entry (the rename collision closed), the id = objectGUID, a nested required group, one answer for every wrong credential, limits from AD's lockout threshold (a third per ACCOUNT via `PasswordChecker.budgetKey`, reset from the LAST failure; threshold 1–2 refused unless overridden), the MFA bypass stated — PENDING INDEPENDENT REVIEW | `directoryPasswords` + `ldapDirectory` + `IDENTITY_STRATEGY=directory-password` (`IDENTITY_LDAP_*`) | `src/adapters/identity/directory/` | 9.117.0 |
| labelling a password sign-in form for the password it takes — "Windows username" for the company directory, "Username" for a local list — from the door's own answer, not a guess: `GET /auth/config` adds `passwordKind` (`'directory'` or `'local'`) beside `mode: 'password'`, the checker's declared `kind` (`directoryPasswords` declares `'directory'`, `localPasswords` `'local'`); a fact, the words stay the page's; no key when the checker declares none | `PasswordKind` + `PasswordChecker` (its `kind`) | `src/hosting/signin/door.ts` | 9.118.0 |
| an audit trail of every sign-in — who tried, from which address, when, and why it was refused (AD's bind sub-code and its name on `directory-password`), success included; one JSON log line per outcome with no code, a typed sink for your own store; never a password, cookie or token | `signInDoor({ onAudit, log })` + `SignInAuditRecord` + `PasswordChecker.check`'s `note` + `adSubCodeName` + `identityFromConfig(…, { onSignInAudit, signInLog })` | `src/hosting/signin/audit.ts` | 9.135.0 |
| trusting a DC certificate a PUBLIC CA issued, from Node's own store instead of a CA file (explicit, never unset-means-system), and refusing — not only delaying — a client address after N failed sign-ins, opt-in | `systemCa` + `IDENTITY_LDAP_CA_FILE=system` + `refuseAddressAfter` + `IDENTITY_SIGN_IN_ADDRESS_REFUSE_AFTER` | `src/adapters/identity/directory/ldapDirectory.ts` | 9.135.0 |
| trusting a company login proxy (oauth2-proxy, mod_auth_openidc, Pomerium) only as far as the signed ACCESS token it forwards — verified with a literal issuer and a JWKS URL (no discovery, no redirects), the person test and the proxy's client check; an ID token refused; the door guard required because the proxy's cookie rides along on forged requests | `proxyTokenChoice` + `IDENTITY_PROXY_TOKEN` + `IDENTITY_PROXY_HEADER` (strategy `proxy-token` of `identityFromConfig`, mode `proxy`) | `src/adapters/identity/strategies/proxyChoice.ts` | 9.117.0 |
| making a caller-supplied id safe for a backend | `encodeIdentityField` | `src/memory/identity/encode.ts` | 9.37.0 |
| scoping a skill's tools so they reach the model only while it is active | `toolsFromActiveSkill` + a no-arg posture (default off) + the stamp `autoActivate: 'currentSkill'` (a default, never an override) | `src/core/agent/toolsFromActiveSkill.ts` | 9.36.0 |
| subscribing to every event in one domain at once | `DomainWildcard` | `src/events/dispatcher.ts` | 9.4.0 |
| pausing a run for a person and resuming it later | `checkInApproved` / `checkInDeclined` (`{ request, by, note?, value? }`; a decline is NOT an abort — the model gets a "declined by human" tool result and adapts in-loop) | `src/core/checkin.ts` | 7.5.0 |
| a person's approval used ONCE, for the exact call they were shown — refused for another paused request (a later pause of the run, a parallel sibling, another conversation), for a checkpoint whose call changed after the ask, for an ask whose later rules changed or whose arguments a later rule rewrote, or on a second resume; once per ledger — one per process by default, one shared store across processes; every pre-execute callback and event (a `checkIn` predicate, the evidence assembler, the permission checker, `tool_start`, and the history they are handed) sees one shared frozen copy of the arguments (its own copy when they hold a `Map`, `Set`, `Date` or typed array), never the ones the tool runs with, and an approved call is held to its request again at `execute`; a closed pause's call is recorded as not run only when the ledger the request was minted under spends it (a tombstone, so nothing can run it later), else as possibly run — proof needs a shared ledger with a `storeId` (unique per backing store, never copied across environments, changed on migration) and a permanent `spend` across processes, and a retried close keeps its verdict (`closed-proven:` marker; a late answer is refused `'closed-by-host'`; a decision is spent only in the proving ledger its request was minted under — `'ledger-changed'` otherwise, so a ledger migration means pending approvals are asked again); this process's default ledger proves only for pauses kept in `memorySessions()`, a restart never; switching ledgers leaves earlier pauses unprovable | `CheckInRequest.id` / `MiddlewareAsk.id` (the request id, bound to the call) + `CheckInDecision.requestId` + `ResumeDecisionError` (`reason`) + `Agent.create({ decisionLedger })` / `DecisionLedger` / `inMemoryDecisionLedger` | `src/core/consentRequest.ts` | 11.0.0 |
| refusing unserved, ambiguous, malformed or known not-dispatched findings citations before host callbacks and tool execution, with request-local accepted-attempt evidence, bounded diagnostics and JSON-answer admission; no additional model call or cache-layout change                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          | `captureFindingsRequest` + `findingsReferenceRefusal` (internal; enabled by `.findings()`)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | `src/core/agent/findings/admission.ts`                  | 10.9.0                          |
| a SECOND source judging every tool result — a calibrated classifier asked what a result is worth for the declared proposition (never why the tool was called), its distribution and cost on the record beside the model's own standing, never merged, never served in its place (policy A) | `.findings({ judge })` + `Classifier` / `typesafe` / `mockClassifier` (`agentfootprint/classify`) + `JudgmentRow` / `JudgmentErrorRow` + `classifierScorer` (the scored entry choice, `agentfootprint/skill-graph`) | `src/classify/` | 9.104.0 |
| a classifier's reading of WHICH TOOL beside every model call — the ranking, the pick and the cost on the record before the call, the model's call and the agreement after it; optionally the served list narrowed to the top-N plus the doors, on the committed record the receipt hashes and the served view rebuilds; a miss recorded and the full wire served next call, never a refusal, never an override | `.toolChoice({ classifier, serve, alwaysServe })` + `ToolChoiceRow` + `ToolChoiceOutcomeRow` + `ToolChoiceErrorRow` + `ALWAYS_SERVED_TOOLS` + `recordToolChoice` + `narrowServed` (events `agentfootprint.tool_choice.*`, bench `npm run bench:tool-choice`) | `src/core/agent/toolChoice/` | 9.105.0 |
| a MAP of the domain the model can reason over when a result holds nothing — what each term IS (meaning, unit), how terms RELATE, which SOURCE holds a term (with the author's coverage sentence) and which registered TOOL reads it from there; declared once, validated against the tool registry at build, on the record as one run constant, served as one request-only system piece per call, never a fetch path and never inferred from | `defineOntology` + `.ontology(map)` (`AgentOptions.ontology`) + `AgentState.ontology` + `ontologyPiece` / `ONTOLOGY_INSTRUCTION` (`agentfootprint/ontology`) + `agentfootprint.ontology.served` | `src/ontology/` | 9.106.0 |
| a value the model USED — in its answer, or as an argument of a later call — that came ONLY from results it had itself declared open, noise or ruled-out (a theorem tower on a set-aside lemma): declared standings joined to the evidence corpus's carriers, no inference, no judge; one row per value per moment through the one writer; served back under `contingent (read off the record):`; cache reads summed on the record beside the input tokens | `ContingentRow` / `ContingentCarrier` + `agentfootprint.findings.contingent` + `findingsInstructionFor` (`FINDINGS_CONTINGENT_LINE`, registered under BOTH `.findings()` and `.namesAndNumbersFromEvidence()`, never one) + `EvidenceCorpus.carriers` / `EvidenceVerdict.grounded` + `AgentState.totalCacheReadTokens` | `src/core/agent/findings/contingent.ts` | 9.110.0 |
| a model's ruling-out that rests only on "nothing found" — a `ruled-out` standing on a result the dispatch door recorded as an absence (`absent()`, bare or bounded by `coverage()`: status `'absent'`, a `coverageDeclared` row; never a second reading of a return) AND the model was served as one (a result an after-tool rule denied, a ceiling's refusal, a placed ticket or a summary files nothing) — marked by a row BESIDE the model's own, never a rewrite of it, carrying what the SERVED envelope said it did not check and its `try_instead` byte for byte; served back under `unsettled by absence (read off the record):`; no event, no instruction line | `UnsettledByAbsenceRow` + `agent.findings()` / the checkpoint's `findingsLedger` (the fold's `unsettled` map and the rule — `withUnsettledRows` / `unsettledRowOf` — are internal) | `src/core/agent/findings/unsettled.ts` | 9.113.0 |
| an evaluation platform grading your agent from its OpenTelemetry traces — scores as the GenAI `gen_ai.evaluation.result` event, the conversation (as a digest), declared absence and coverage, the evidence verdict, findings standings and actionable checker accounting as content-free span events, and tool arguments/results only under their own opt-in, after your middleware rules | `otelObservability` options `captureToolContent` / `maxContentChars` / `conversationId` / `checkAccounting` (+ `agentCoreEvaluationSpans`, which turns tool content on) | `src/adapters/observability/otel.ts` | 9.115.0 |
| retrying a STREAMING call that fails before any token arrives — a 429, a 5xx or a reset at connect — without hand-rolling retries inside your provider, and without ever replaying tokens the caller has already seen — `stream()` is re-opened under the same policy as `complete()` (maxAttempts, shouldRetry, backoff, `req.signal`) until its first chunk arrives, reported as `error.retried` / `error.recovered`; after the first chunk nothing retries, because `openStream` owns the only retry loop and returns with that chunk | `withRetry` + `openStream` | `src/resilience/withRetry.ts` | 9.126.0 |
| reaching Anthropic models through your organisation's gateway that forwards the Bedrock InvokeModel operation over plain HTTPS with an API-key header (not SigV4, not Converse — so neither `bedrock()` nor `providerFromEnv()` reaches it): `complete()` + `stream()` (SSE the gateway already decoded; tool arguments reassembled), a key per model id (map or function, re-read per request), typed errors that name the model (a 403 says the key is scoped away from it), NO retry loop of its own — compose `withRetry`, whose default predicate now skips an error declaring `retryable: false` (a refusal before any request, a 2xx it could not read) | `invokeModelGateway` + `InvokeModelGatewayError` (`reason`, `status`, `retryable`, `modelId`, `retryAfterSeconds`) + `createProvider({ kind: 'invoke-model-gateway' })` | `src/adapters/llm/InvokeModelGatewayProvider.ts` | 9.126.0 |
| retrying AFTER the wait a failure stated — a `Retry-After` header, or a gateway's "Try again in 4 seconds" — instead of spending short backoffs inside it: the wait is max(the schedule, the stated wait), capped by `maxDelayMs` (a hostile header cannot stall a run), `req.signal` still ends it, and `error.retried` carries `statedWaitMs` beside `backoffMs` (absent when nothing was stated — byte-identical). The resilience layer never parses prose: an error DECLARES `retryAfterMs` (`retryAfterSeconds` read as fallback) through ONE reader; `anthropic()` / `openai()` / `bedrock()` declare it from `retry-after-ms` / `Retry-After`, `invokeModelGateway()` also from its gateway's body wording | `statedRetryAfterMs` + `withRetry` (`retryAfterMs` on the error, `statedWaitMs` on the event) | `src/resilience/statedWait.ts` | 9.130.0 |
| a per-request deadline on your gateway provider, so a gateway that never answers (or a stream that goes silent) fails typed instead of hanging the run: bounds the response headers, a `complete()` body and EACH stream read (first chunk, then every gap — a stream that keeps talking is never cut off); a miss aborts the request and raises `reason: 'timeout'`, `retryable: true` (withRetry re-opens it while no chunk reached the caller); the caller's `req.signal` still wins; omitted = the fetch gets exactly `req.signal` as before | `timeoutMs` + `invokeModelGateway` + `InvokeModelGatewayError` (`reason: 'timeout'`) | `src/adapters/llm/InvokeModelGatewayProvider.ts` | 9.130.0 |
| Replay a saved complete domain-event batch with the same fork grouping and nested drill projection as live run steps | `ingestDomainEvents` + `getSteps` + `clear` | `src/recorders/observability/RunStepRecorder.ts` | 10.0.0 |
| experimental, not yet validated in real use: additive saved-record shapes for scoped historical verification observations; paired checkpoint readers and writers validate retained evidence and counters without judging old observations again, and native continuation refuses an unarmed evidence carrier; protocol only — no runtime verifier, model declaration parser or answer badge is enabled | `FindingsRow` + optional `AgentRunCheckpoint.claimVerification` | `src/core/` | 10.2.0 |
| an AG-UI frontend (`@ag-ui/client`, CopilotKit, the AG-UI Dojo) reading an agentfootprint run with no glue code — the run's served events translated into AG-UI 1.0 events: streamed text, tool calls with arguments and results, reasoning, ReAct iterations and compositions as steps, a nested agent (a `Sequence` step, a `Parallel` branch) as a sub-agent with its parent links, a pause as an interrupt, a failure as `RUN_ERROR` in the record's words (an answer `.answerValidation()` withheld as `RUN_ERROR { code: 'ERR_ANSWER_VALIDATION' }`, a refusal not a crash — `RunSettlement.refusal` / `RunRefusal`); `CUSTOM` an allow-list of events with projected fields (`AG_UI_CUSTOM_EVENTS`), reasoning only on request; ids minted from the record's address under the request's run id (unique across a thread when branches reuse a provider's tool-call id); nothing a redaction policy hides on the wire, at any depth; framed by the official `@ag-ui/encoder` (optional peer, loaded only when a framing is built) | `agUiSerializer` + `agUiSse` + `AG_UI_CUSTOM_EVENTS` (`agentfootprint/ag-ui`) + `RunTranslation` / `RunSettlement` (the port, `agentfootprint/hosting`) | `src/ag-ui/` | 10.4.0 |
| writing a run's Server-Sent Events in a different frame shape — the framing is a strategy: `toSSE({ framing })`, default the frames it always wrote | `SseFraming` + `agentfootprintSse` (`agentfootprint/observe`) | `src/hosting/stream.ts` | 10.4.0 |

**Maintaining this table:** a new public capability adds a row, in the same
change that adds the capability. `test/docs/capability-index.test.ts` fails if a
row names a file or symbol that does not exist, so the table can go out of date
by OMISSION but can never lie about what it names.
