UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

5,595 lines • 325 kB
import { findingsContextBytes } from './agent/findings/context.js';
/**
 * Agent — ReAct primitive (LLM + tools + iteration loop).
 *
 * Pattern: Builder (GoF) → produces a Runner backed by a footprintjs FlowChart.
 * Role:    Layer-5 primitive (core/). Assembles the 3-slot context
 *          pipeline + callLLM + route decider + tool-calls subflow +
 *          loopTo. Composition nestable anywhere that accepts a Runner.
 * Emits:   Via internal recorders:
 *            agentfootprint.agent.turn_start / turn_end
 *            agentfootprint.agent.iteration_start / iteration_end
 *            agentfootprint.agent.route_decided
 *            agentfootprint.stream.llm_start / llm_end
 *            agentfootprint.stream.tool_start / tool_end
 *            agentfootprint.context.* (via ContextRecorder)
 */
import { FlowChartExecutor, } from 'footprintjs';
import { ReliabilityFailFastError } from '../reliability/types.js';
import { extractSequence } from '../security/extractSequence.js';
import { PolicyHaltError } from '../security/PolicyHaltError.js';
import { updateSkillHistory as updateSkillHistoryStage } from '../cache/CacheGateDecider.js';
import { cacheStrategyFor } from '../cache/cacheStrategyFor.js';
import { cacheRequestedWithoutSupport } from './agent/cacheSupport.js';
import { forcedToolChoiceFor, UnsupportedToolChoiceError, } from '../provider-contract/forcedToolChoice.js';
import { nativeOutputSchemaFor, UnsupportedNativeOutputError, } from '../provider-contract/nativeOutputSchema.js';
import { isRequestRefusal } from '../provider-contract/requestRefusal.js';
import { assistantReplayFields } from './agent/assistantReplay.js';
import { buildBrainFor, describeServingBrain } from './agent/skillBrains.js';
import { SUBFLOW_IDS } from '../lib/conventions.js';
import { readDisplayName } from '../lib/displayName.js';
import { DecisionRequiredError, ResumeDecisionError, assertDecisionIsNotStale, isPaused, pauseDemandsDecision, } from './pause.js';
import { checkDecisionAgainst, noteDecisionTakenUp, pausedRequestIdOf, processDecisionLedger, registerConsentLedger, registerConsentRules, } from './consentRequest.js';
import { assertArtifactPlacement } from '../artifacts/placement.js';
import { recordingPutInput } from '../artifacts/recordingArtifact.js';
import { recordRunWhere } from '../recorders/observability/recordRun.js';
import { CredentialConsentRequiredError } from '../identity/CredentialConsentRequiredError.js';
import { ContextRecorder } from '../recorders/core/ContextRecorder.js';
import { contextEvaluatedRecorder } from '../recorders/core/ContextEvaluatedRecorder.js';
import { streamRecorder } from '../recorders/core/StreamRecorder.js';
import { agentRecorder } from '../recorders/core/AgentRecorder.js';
import { errorBridge } from '../recorders/core/ErrorBridge.js';
import { costRecorder } from '../recorders/core/CostRecorder.js';
import { permissionRecorder } from '../recorders/core/PermissionRecorder.js';
import { evalRecorder } from '../recorders/core/EvalRecorder.js';
import { memoryRecorder } from '../recorders/core/MemoryRecorder.js';
import { embeddingRecorder } from '../recorders/core/EmbeddingRecorder.js';
import { skillRecorder } from '../recorders/core/SkillRecorder.js';
import { validationRecorder } from '../recorders/core/ValidationRecorder.js';
import { credentialRecorder } from '../recorders/core/CredentialRecorder.js';
import { mapRecorder } from '../recorders/core/MapRecorder.js';
import { integrityRecorder } from '../recorders/core/IntegrityRecorder.js';
import { artifactsRecorder } from '../recorders/core/ArtifactsRecorder.js';
import { toolsRecorder } from '../recorders/core/ToolsRecorder.js';
import { reliabilityRecorder } from '../recorders/core/ReliabilityRecorder.js';
import { resilienceRecorder } from '../recorders/core/ResilienceRecorder.js';
import { checkInEventsBridge } from '../recorders/core/CheckInRecorder.js';
import { compactionMeter } from '../recorders/core/CompactionMeter.js';
import { assertRedactionPolicy, CarriedPolicyError, carriedVocabularyOf, policyFromCarried, policyOfMarks, unionRedactionPolicies, } from '../redaction/policy.js';
import { coverageOfExecutor } from '../redaction/runRedaction.js';
import { policyOfCoverage } from '../redaction/coverage.js';
import { ResumeRedactionError } from './conversation.js';
import { declareRedaction, redactionDeclaredBy } from '../redaction/declared.js';
import { carriedRunPolicy, conversationRedaction, withCurrentVocabulary, } from '../redaction/conversation.js';
import { createEvictedTurnsHandle } from './agent/window/evictedTurns.js';
import { createReceiptDigests } from './agent/receipt/receiptDigests.js';
import { packRecording } from '../recorders/observability/recordingPack.js';
import { pendingDurableWrite } from './durabilityBarrier.js';
import { ToolSessionTier, TOOL_TEARDOWN_TIMEOUT_MS, } from '../tools/toolSessions.js';
import { buildEventMeta, eventBelongsToRun } from '../bridge/eventMeta.js';
import { assertIdentityShape, callerIdentityOf, pausedSessionOf, restoredIdentityOf, sameIdentity, } from './agent/callerIdentity.js';
import { hostedConversationKey, hostedConversationOf, registerServingConversation, sessionConversationKey, } from './agent/servingConversation.js';
import { buildRunManifest } from './agent/runManifest.js';
import { beginIntegrityRun } from '../integrity/disposition/lifecycle.js';
/**
 * The pseudo-stage a tool-teardown event is stamped with.
 *
 * `session_closed` and `session_close_failed` fire after the run's last stage
 * has committed — there is no runtimeStageId to inherit, and inventing one that
 * looked like a real stage would put a fictitious node in every consumer's step
 * strip. This says plainly where it came from, the way `'<stageId>#paused'`
 * does at the pause boundary. Their two siblings, `session_started` and
 * `session_reused`, happen inside a real stage and carry its real id.
 */
const TOOL_TEARDOWN_STAGE_ID = 'tool-teardown#0';
/**
 * The pseudo-stage a LATE artifact fact is stamped with — one that lands after
 * the run its binding was made in had ended (a tool's floating upload). Its
 * stage is long gone; like the teardown events it says plainly where it came
 * from, and carries the run it belongs to rather than the one that is live.
 */
const LATE_ARTIFACT_FACT_STAGE_ID = 'artifact-late#0';
/**
 * The pseudo-stage the run-configuration manifest is stamped with (9.41.0).
 *
 * Same reasoning as the teardown id above, at the other end of the run: the
 * manifest is dispatched before the chart starts, so there is no stage to
 * inherit and inventing a plausible one would put a node in every step strip
 * that no traversal ever visited. This says plainly where it came from.
 */
const RUN_MANIFEST_STAGE_ID = 'run-configured#0';
/**
 * Pseudo-stage for `agentfootprint.integrity.disposition` (9.60.0) — the
 * ledger is filed at the run boundary, after the last stage committed, so it
 * states where it came from exactly as the teardown id above does.
 */
const INTEGRITY_DISPOSITION_STAGE_ID = 'integrity-disposition#0';
/**
 * Pseudo-stage for `agentfootprint.skill.graph_declared` (9.50.0) — same
 * reasoning as the manifest id directly above: dispatched before the chart
 * starts, so it states where it came from instead of inheriting a stage no
 * traversal visited.
 */
const GRAPH_DECLARED_STAGE_ID = 'graph-declared#0';
import { EmitBridge } from '../recorders/core/EmitBridge.js';
import { buildWindowStage } from './agent/stages/window.js';
import { requireKeepLastToolResults } from './agent/window/options.js';
import { assertDeliverableRole, buildDeliverStage } from './agent/stages/deliver.js';
import { providerTargets } from './agent/providerTargets.js';
import { messagesContentRefusal } from '../injection-engine/messagesSlotRefusal.js';
import { CompactionUnmeasurableError } from './agent/window/errors.js';
import { resolveCheckInConfig, } from './checkin.js';
import { assertCostBudgetHasPricing, resolveCostBudget } from './cost.js';
import { causalEvidenceRecorder, } from '../memory/causal/evidenceRecorder.js';
import { buildSystemPromptSlot } from './slots/buildSystemPromptSlot.js';
import { buildMessagesSlot } from './slots/buildMessagesSlot.js';
import { buildToolsSlot, } from './slots/buildToolsSlot.js';
import { isDevMode } from 'footprintjs';
import { buildReadSkillTool } from '../injection-engine/skillTools.js';
import { foldStepPlans } from '../injection-engine/skillSteps.js';
import { buildStepNudgeStage } from './agent/stages/stepNudge.js';
import { buildEvidenceRecheckStage } from './agent/stages/evidenceRecheck.js';
import { toolWantsOf } from './agent/stagedRefs.js';
import { wrapUpStage } from './agent/stages/wrapUp.js';
import { failFastRecordOf } from '../reliability/failFastRecord.js';
import { terminalErrorOf } from './terminalVerdict.js';
import { isValidatedDelivery } from './agent/stages/breakFinal.js';
import { evidenceRefusalSentence } from './agent/evidence/gate.js';
import { UnsupportedValuesError } from './agent/evidence/errors.js';
import { checkerGoverns } from '../provider-contract/types.js';
import { skillTarget } from '../security/skillTarget.js';
import { buildInjectionEngineSubflow } from '../injection-engine/buildInjectionEngineSubflow.js';
import { makePickEntryStage } from './agent/stages/pickEntry.js';
import { makeRouteTurnStage } from './agent/stages/routeTurn.js';
import { applyOutputFallback, } from './outputFallback.js';
import { assertContinuable, buildCheckpoint, checkpointReplayHistory, retainReplayRetirements, classifyFailurePhase, RunCheckpointError, validateCheckpoint, } from './runCheckpoint.js';
import { requireVerificationOnResume } from './agent/findings/verificationCheckpoint.js';
import { NoConversationError, PendingQuestionError, ResumeIdentityConflictError, RunInFlightError, } from './conversation.js';
import { applyInputResponse, readAwaitingInput, } from '../tools/inputRequest.js';
import { applyOutputSchema, OutputSchemaError, } from '../answer-validation/outputSchema.js';
import { InvalidRunInputError, normalizeRunInput } from './runInput.js';
import { clockOf } from '../time/rows.js';
import { draftClock, readRunTime, readTimeOptions, } from '../time/clock.js';
import { buildOutputRetryStage } from './agent/stages/outputRetry.js';
import { RunnerBase, makeRunId, runSignalOf } from './RunnerBase.js';
import { clampIterations, validateMemoryIdUniqueness, validateToolNameUniqueness, } from './agent/validators.js';
import { buildRouteDeciderStage } from './agent/stages/route.js';
import { negativeCheckpointOf } from './agent/negativeClaims/checkpoint.js';
import { withNegativeProse } from './agent/negativeClaims/observe.js';
import { negativeRecheckStage } from './agent/negativeClaims/recheck.js';
import { withAnswerValidation } from './agent/stages/answerValidation.js';
import { hasOutputMiddleware } from './agent/middleware/messagePhase.js';
import { withAnswerCoverage } from './agent/stages/answerCoverage.js';
import { AnswerValidationError, resolveAnswerValidation, } from '../answer-validation/index.js';
import { buildSeedStage } from './agent/stages/seed.js';
import { buildCallLLMStage } from './agent/stages/callLLM.js';
import { buildToolCallsHandler, notServedResult } from './agent/stages/toolCalls.js';
import { buildToolResolver } from './agent/stages/toolResolver.js';
import { answeredValuesOf, declaredDefaultsFrom } from './agent/stages/route.js';
import { isRefused, rulesOf } from '../tools/declare.js';
import { honestyLayersOf, readInputsLayerOption, } from './agent/honesty/armed.js';
import { answerFoldReads } from './agent/honesty/mounts.js';
import { readsMessageFrom } from './messageFrom.js';
import { memberIdentityIn, memberRequestIn, readsParentRun } from './memberRun.js';
import { requestOf } from './requestOfRun.js';
import { carriesArgumentView } from './toolShownArgs.js';
import { assertMaxToolResultChars } from './agent/toolResultCap.js';
import { buildAgentChart } from './agent/buildAgentChart.js';
import { buildDynamicAgentChart } from './agent/buildDynamicAgentChart.js';
import { buildToolRegistry } from './agent/buildToolRegistry.js';
import { AgentBuilder } from './agent/AgentBuilder.js';
import { buildThinkingSubflow } from './slots/buildThinkingSubflow.js';
import { thinkingForProviders } from '../thinking/dispatch.js';
export { AgentBuilder };
// Public types (AgentOptions, AgentInput, AgentOutput) extracted to
// ./agent/types.ts and re-exported above (v2.11.1).
// AgentState extracted to ./agent/types.ts (v2.11.1).
/**
 * The ledger-fact hold's default ceiling (9.102.0): how many fact turns the
 * window holds beyond `keepRecentTurns` when `.findings()` and a window
 * strategy are both configured and nobody named a number. Resolved in the
 * constructor, ONCE, and threaded to `buildWindowStage` as a plain number —
 * the stage applies no default of its own (absent = unarmed = no hold), so a
 * reader has one place to look. `window/options.ts ·
 * DEFAULT_KEEP_LAST_TOOL_RESULTS` is the pin's twin of this constant.
 */
const DEFAULT_KEEP_LEDGER_FACTS = 4;
/**
 * Validate the `keepLedgerFacts` dial — `window/options.ts ·
 * requireKeepLastToolResults`'s twin: the same rule, the same moment.
 * Refused at construction, never mid-run.
 */
function requireKeepLedgerFacts(value, label) {
    if (value === false)
        return;
    if (!Number.isInteger(value) || value < 0) {
        throw new Error(`${label}: keepLedgerFacts must be a whole number >= 0, or false, got ` +
            `${String(value)}. It is how many fact turns the window holds beyond ` +
            `keepRecentTurns under .findings(); 0 and false both switch the hold off.`);
    }
}
/** `false` → `0`; a number as named; nothing named → the default. */
function resolveKeepLedgerFacts(named) {
    if (named === false)
        return 0;
    return named ?? DEFAULT_KEEP_LEDGER_FACTS;
}
export class Agent extends RunnerBase {
    name;
    id;
    provider;
    model;
    temperature;
    maxTokens;
    maxIterations;
    systemPromptValue;
    /**
     * Cache policy for the base system prompt (set via
     * `.system(text, { cache })`). Default `'always'` — base prompt is
     * stable per-turn, ideal cache anchor. CacheDecision subflow reads
     * this when computing the SystemPrompt slot's cache markers.
     */
    systemPromptCachePolicy;
    /**
     * Global cache kill switch from `Agent.create({ caching: 'off' })`.
     * Threaded into agent scope at seed-time as `scope.cachingDisabled`;
     * read by the CacheGate decider every iteration (highest-priority rule).
     */
    cachingDisabledValue;
    /**
     * The CacheStrategy this agent runs. Chosen at build time from the
     * provider's declared `promptCaching` (`cacheStrategyFor`) unless the
     * consumer passes one explicitly.
     */
    cacheStrategy;
    cacheStrategyIsOverride;
    cacheUnsupported;
    registry;
    /**
     * The Injection list — Skills, Steering, Instructions, Facts (and
     * RAG, Memory). Evaluated each iteration by the
     * InjectionEngine subflow; active set is filtered by slot subflows.
     */
    injections;
    /** Skill-graph cursor resolver (`graph.nextSkill`), set when built via
     *  `.skillGraph(graph)`. Plumbed into the Injection Engine so route triggers
     *  are `from`-gated against the persisted `currentSkillId`. */
    skillGraphNextSkill;
    /** Skill-graph reachable-set resolver (`graph.reachableSkills`), set when built
     *  via `.skillGraph(graph)`. Plumbed into the tool-calls handler so `read_skill`
     *  is gated to in-graph jumps. Undefined → gate off. */
    skillGraphReachable;
    /** Skill-graph relevance entry scorer (`graph.scoreEntries`), set when built via
     *  `.skillGraph(graph)` with `.entryByRelevance()`. Drives the PickEntry stage.
     *  Undefined → no relevance entry routing (cold-start entry as before). */
    skillGraphScoreEntries;
    /** The `to` end of every edge the mounted graph declares — which skills the graph
     *  WIRES. Empty for a graph-less agent. Read only by `openSkillIds()`. */
    skillGraphEdgeTargets;
    /** Skill-graph cursor resolver that also reports WHICH CLAUSE won
     *  (`graph.explainNextSkill`, 8.5.0). Threaded to the Injection Engine, which
     *  stamps the result on `context.evaluated` as `cursorMove`. Optional — a graph
     *  built before it existed falls back to `nextSkill` and emits no `cursorMove`. */
    skillGraphExplainNextSkill;
    /** Skill-graph suppression reporter (`graph.supersededEntries`, 8.15.0) — the
     *  conditional entries whose `when` matched while the cursor was elsewhere.
     *  Threaded to the Injection Engine, which stamps them on `context.evaluated` as
     *  `supersededIds`. Optional — a graph built before it existed routes identically
     *  and simply emits no `supersededIds`. */
    skillGraphSupersededEntries;
    /** Is the mounted graph a decision `tree()`? Derived at build time from
     *  `graph.nodes` (a tree is the only shape with `predicate` nodes) — no new
     *  public field on `SkillGraph`. Read for ONE thing: the read_skill gate's
     *  refusal has to explain that a tree has no cursor to jump (8.5.0). */
    skillGraphIsTree;
    /** The turn-start cascade wiring (SG-C, 9.17.0) — the graph's routing plan,
     *  the mount's posture/continuity, and the node-id set droppedResume checks
     *  against. Absent (every graph without the new options) → RouteTurn never
     *  mounts, the gate never postures, seed never restores a cursor, and the
     *  checkpoint shape is byte-identical. */
    /** The folded per-skill brains + escalation + decider (9.19.0) —
     *  validated at `AgentBuilder.build()`. Undefined = no brain anywhere:
     *  callLLM, the gate, seed and RouteTurn wire nothing new. */
    skillBrains;
    /**
     * The evidence gate (9.35.0) — `.namesAndNumbersFromEvidence()`, resolved by
     * the builder. Undefined for every agent that did not ask for it, and that
     * undefined is the whole zero-cost guarantee: no decider change, no branch,
     * no scope key, no event.
     */
    evidenceGate;
    /**
     * `.limitsTravelWithTheAnswer()` — whether the run's declared coverage
     * travels with the final answer: appended as a block to a prose answer, or,
     * when an output schema makes the answer TYPED, committed beside it as data
     * (`answerCoverage()`), because JSON followed by prose is not JSON. False for
     * every agent that did not ask for it; the RECORDING half of the coverage
     * primitives does not consult it.
     */
    limitsTravelWithTheAnswerValue = false;
    /**
     * `.scriptTravelsWithTheAnswer()` — the resolved arm, or undefined for every
     * agent that did not ask. With `code: 'match'` on the gate it arms the
     * script record (`scriptRecordArmed`); alone it also picks the final stage.
     */
    scriptTravel;
    skillGraphCascade;
    /** Side channel for the conversation's inherited skill cursor (SG-C) —
     *  stashed by `applyContinuation`, consumed-and-cleared by seed. */
    pendingResumeSkillCursor;
    pricingTable;
    /** Normalized at construction: a bare number is `{ usd, onExceed: 'warn' }`. */
    costBudget;
    /** Per-slot character budgets (8.11.0). Absent keys keep the slot default. */
    contextBudget;
    permissionChecker;
    toolArgValidation;
    /** See AgentOptions.findings (9.101.0). Set by `.findings()` and undefined
     *  on every other agent — the one value every findings gate below is
     *  conditioned on, so an unarmed agent hands each stage exactly the deps
     *  it always did. `serve` is threaded to seed (the run constant
     *  `findingsServe`) and to call-llm (`findingsServe` in its deps) on an
     *  armed agent only; `answerAsk` (9.103.0) rides the same two threads
     *  as `findingsAnswerAsk`, and only when it is `'quote-facts'` — the
     *  default `'none'` threads nothing. `keepLedgerFacts` is resolved once, here in the
     *  constructor (this door over `AgentOptions.keepLedgerFacts`), into the
     *  `keepLedgerFacts` field below and threaded to the window stage on an
     *  armed agent with a window, where `stages/window.ts · buildWindowStage`
     *  spends it as the `'ledger-fact'` pin ceiling. */
    findingsOptions;
    /** `.inputsLayer()` (honesty layer 2): mount the inputs layer even when no
     *  registered tool declares `askOrAssume` — for ruled tools only a
     *  ToolProvider serves, which the build cannot see. */
    inputsLayerOption;
    /** DECLARED SOURCES (honesty layer 2): `.findings({ argumentSources: true })`
     *  or `.inputsLayer({ argumentSources: true })` — resolved ONCE, in the
     *  constructor, from either door; every declared-sources gate below reads
     *  this one value, so the two doors can never arm different halves. The
     *  findings ledger's own serve stays `findingsOptions`'s: without
     *  `.findings()` the arm plants `_findings.from` on ruled tools only. */
    argumentSourcesArmed;
    /** `AgentOptions.argumentAskContext` (honesty layer 2): the host's own
     *  context for the inputs layer's batch ask, handed to the dispatch stage
     *  only when the layer is mounted. */
    argumentAskContext;
    /** Whether this agent's chart mounts the inputs layer (`sf-inputs`) —
     *  decided ONCE, in `buildChart`: a registered tool declares rules, or
     *  `.inputsLayer()` was set. Gates the findings event bridge and the
     *  widened ledger restore. */
    inputsLayerArmed = false;
    /** `.resultsLayer()` (honesty layer 3, step 7b): mount the results layer
     *  even when no registered tool declares a `ToolPeriod` — for tools a
     *  ToolProvider serves, and for tools that declare a period only on their
     *  results. */
    resultsLayerOption;
    /** Whether this agent's chart mounts the results layer (`sf-results`) —
     *  decided ONCE, in `buildChart`: a registered tool declares a `ToolPeriod`,
     *  or `.resultsLayer()` was set. Gates the findings event bridge and the
     *  widened ledger restore, as the inputs layer's flag does. */
    resultsLayerArmed = false;
    /** `.answerLayer()` (honesty layer 4): the answer's standing folded at the
     *  head of the final branch and served as data; `standingLine` — also one
     *  line appended to a prose answer, for the standings its mode names
     *  (`'always'` for `standingLine: true`). Undefined on every agent that did not
     *  ask, and that undefined is the whole zero-cost guarantee: no stage, no
     *  witness row, no stamp, no event, no bridge. */
    answerLayerOption;
    /** `.time()` (the time layer, `time/`): the fallback zone, as read.
     *  Undefined on every agent that did not ask — then no clock is read, no
     *  time row is filed and `time` on a run is refused. */
    timeOptions;
    /** The clock the CURRENT `run()`'s seed stamps — set by `run()` (a run with
     *  no zone gets an unknown one, never the server's), read by seed through `SeedStageDeps.timeClock`, and
     *  cleared when `run()` ends, so a later turn this agent did not start (its
     *  chart mounted in a composition) never stamps a clock nobody declared for
     *  it (`seedClockDraft`). */
    runClockDraft;
    /** The incoming request each of this agent's OWN scopes serves — recorded
     *  by the executor that made the scope (`createExecutor`), so a scope keeps
     *  ITS run's request for as long as it lives: a stage still running after
     *  its run was aborted can never be served the next run's request. A stage
     *  mounted in another runner's executor reads its run's request by link
     *  (`memberRun.ts`). Never written to scope. */
    scopeRequests = new WeakMap();
    /** The `time` the CURRENT resume passed, as read — TAKEN once by the first
     *  ToolCalls pass of the resumed leg (`ToolCallsHandlerDeps.time`), and
     *  cleared when `run()` or `resume()` ends. */
    resumePassedTime;
    /** Tool choice by classifier (9.105.0, `.toolChoice()`): the classifier,
     *  the serve dial and the app's own doors. Threaded to the tools slot (the
     *  pick and the narrowing), to call-llm (`toolChoice: true`, the outcome
     *  row) and to both chart builders (`hasToolChoice`, the mount args) on an
     *  armed agent only — an unarmed agent hands each stage exactly the deps it
     *  always did. */
    toolChoiceOptions;
    /** The declared ontology (9.106.0, `.ontology()`): the frozen map. Threaded
     *  to seed (the whole map, written once as the run constant `ontology`),
     *  to call-llm (`ontology: true`, the gate on every read of that key) and
     *  to both chart builders (`hasOntology`, the grouped boundary's crossing)
     *  on an armed agent only — an unarmed agent hands each stage exactly the
     *  deps it always did. The tool names its `via` edges name are checked
     *  against the registry in `buildChart`, beside `buildToolRegistry`. */
    ontology;
    /** The map's tool → declaring-skills join (9.108.0), read off the registry
     *  in `buildChart` beside the `via` check; `undefined` when no `via` name
     *  is a skill's. Seed reads it through a thunk (the stage is built first). */
    ontologyTools;
    /** The opt-in tool-result ceiling in characters (9.11.0). Absent → results
     *  are never measured. See {@link AgentOptions.maxToolResultChars}. */
    maxToolResultChars;
    /** Resolved check-in config (evidence-carrying human consent). Always
     *  present — defaults to `standard` evidence + the lexical scorer, so a tool
     *  that declares `checkIn` works even without a `.checkIn()` builder call. */
    checkInConfig;
    /** Where answered consent requests are spent (`AgentOptions.decisionLedger`).
     *  Undefined → this process's ledger, shared by every agent that names none. */
    decisionLedger;
    /** Per-run config resolver from `.configure()`. Undefined for every agent
     *  that never called it — and the chart is then built exactly as before,
     *  with no scope writes and no scope reads added. */
    runConfigFn;
    /** The agent's one window strategy. Undefined = no window stage, loop
     *  target unchanged, run byte-identical to an agent without it. */
    windowStrategy;
    /** The tool-dispatch chain (`.toolMiddleware()`), in declaration order.
     *  Empty for every agent that never called it — and then the dispatch loop
     *  never walks a chain, never writes the ledger key, and never emits. */
    toolMiddleware;
    /** The message chain (`.messageMiddleware()`), in declaration order. Empty
     *  keeps seed synchronous and prepare-final untouched. */
    messageMiddleware;
    /** The instrument the window stage reads mid-run (adapter-reported usage +
     *  per-message provenance). Only ever created alongside a strategy. */
    compactionMeterHandle;
    /** The window stage's seam to the receipt (9.93.0): what left the window at
     *  this iteration's head, read by the call-llm mint in the same iteration.
     *  Only ever created alongside a strategy, like the meter. */
    evictedTurnsHandle;
    /** Snapshot read-tracking policy (#18/#14) — forwarded to the internal
     *  executor. Agent default is `'summary'` (cheap markers), NOT
     *  footprintjs's `'full'`. See AgentOptions.readTracking. */
    readTracking;
    /** Commit-log value encoding (#13c-B) — forwarded to the internal
     *  executor. Agent default is `'delta'` (append/delete verbs; growing
     *  arrays like `history` record only their tails — lossless, linear
     *  retained memory), NOT footprintjs's `'full'`. See
     *  AgentOptions.commitValues. */
    commitValues;
    /** Per-write read provenance (#P1) — forwarded to the internal executor.
     *  Default `'off'` (footprintjs's own): recordings stay byte-identical
     *  unless a consumer opts in. See AgentOptions.writeProvenance. */
    writeProvenance;
    credentialProvider;
    /** The claim-check store (9.21.0). When set, every tool's `ctx.artifacts`
     *  is this store bound to the run's scope. See AgentOptions.artifacts. */
    artifactStore;
    /** The placement threshold (9.22.0) — the operator's ref-ing dial from the
     *  object form of AgentOptions.artifacts. Only ever set beside a store. */
    artifactPlacement;
    /** Recordings-as-artifacts (9.26.0) — the operator's dial from the object
     *  form of AgentOptions.artifacts. Only ever set beside a store; absent
     *  means no recorder is ever attached and no run is ever recorded. */
    artifactRecordings;
    /** The repeated-call nudge (9.26.0) — `false` only when the operator turned
     *  it off. See AgentOptions.repeatedCallNudge. */
    repeatedCallNudge;
    /** The out-of-budget wrap-up (9.56.0) — `false` only when the operator
     *  turned it off. See AgentOptions.wrapUpAtMaxIterations. */
    wrapUpAtMaxIterations;
    /** The last-tool-result pin (9.57.0) — set only when the operator named a
     *  value other than the default 2. See AgentOptions.keepLastToolResults. */
    keepLastToolResults;
    /** The ledger-fact hold's ceiling (9.102.0), RESOLVED — set exactly when
     *  `.findings()` is on: `findings({ keepLedgerFacts })` over
     *  `AgentOptions.keepLedgerFacts`, `false` → `0`, nothing named →
     *  `DEFAULT_KEEP_LEDGER_FACTS`. Undefined on every unarmed agent, so the
     *  thread into the window stage reads as the decision it is. See
     *  AgentOptions.keepLedgerFacts. */
    keepLedgerFacts;
    /** See AgentOptions.integrityPosture (9.60.0). Default 'observe'. */
    integrityPosture = 'observe';
    /**
     * The per-run disposition ledger, shared with the check sites by
     * REFERENCE through build-time closures (the ProviderToolCache pattern —
     * plumbing, never scope state). `run()` resets it; the run boundary
     * files its report and clears it.
     */
    integrityLedgerHolder = {
        current: undefined,
    };
    /** Set at chart build: whether any tool in the FULL declared catalog
     *  (static registry + skill-carried tools) declared `argumentsFrom`. */
    integrityDanglingPresent = false;
    /** See AgentOptions.noticeEmptyLookups (9.77.0). Default false — absent is
     *  byte-identical, save for the registered not-applicable ledger row. */
    noticeEmptyLookups = false;
    /** See AgentOptions.noticePriorTurnEvidence (9.83.0). Default false —
     *  absent is byte-identical, save for the registered not-applicable ledger
     *  row. */
    noticePriorTurnEvidence = false;
    /** Set at chart build: whether any tool in the FULL declared catalog
     *  declared `resultColumns` (9.78.0) — the other half of the column-type
     *  contract's arming. */
    integrityColumnsPresent = false;
    /** See AgentOptions.checkColumnTypes (9.78.0). Default 'off' — absent is
     *  byte-identical, save for the registered not-applicable ledger rows. */
    checkColumnTypes = 'off';
    /** See AgentOptions.externalGrounds (9.72.0). Absent = door closed,
     *  byte-identical behavior. */
    externalGrounds;
    /** What a run does when a declared credential needs 3LO consent (8.6.0).
     *  Default `'pause'`. See AgentOptions.onAuthorizationRequired. */
    onAuthorizationRequired;
    /**
     * Consent blocks outstanding in THIS run, keyed by service — the
     * `'tell-model'` honesty ledger, and the only place the authorization URL
     * lives between the block and the caller.
     *
     * It is a plain instance field rather than a scope key on purpose: tracked
     * state is the commit log, which is the snapshot, the narrative and every
     * recording, and this value is a bearer capability. Cleared at the top of
     * every `run()` / `resume()` so one run can never raise on another's block,
     * and cleared per service the moment that credential is issued.
     */
    consentOutstanding = new Map();
    /** Evidence bridge (#5) — present iff a CAUSAL memory is mounted. */
    causalEvidence;
    /** Observer delivery tier (RFC-001 Block 10). `'inline'` (default) is
     *  byte-identical to pre-10 releases; `'deferred'` routes the bridge
     *  recorders + consumer attachments through footprintjs's bounded
     *  capture queue. See AgentOptions.observerDelivery. */
    observerDelivery;
    /** Queue dials forwarded on every deferred attach (first attach
     *  configures the executor's single dispatcher). */
    observerDeliveryOptions;
    /**
     * Voice config — shared by viewers (Lens, ChatThinkKit, CLI tail).
     * `appName` is the active actor in narration ("Chatbot called…").
     * `commentaryTemplates` drives Lens's third-person panel.
     * `thinkingTemplates` drives chat-bubble first-person status.
     * Defaults to bundled English; consumer overrides via builder.
     */
    appName;
    commentaryTemplates;
    thinkingTemplates;
    currentRunContext = {
        runStartMs: 0,
        runId: 'pending',
        compositionPath: [],
    };
    /**
     * The typed ask the CURRENT run's resume answered — set by
     * `resume()` when a person's answer completed an `awaitingInput`, cleared
     * by every other run start, so a pooled agent never marks one session's
     * ask as another's re-ask. Read by the dispatch door through
     * `ToolCallsHandlerDeps.answeredAsk`. The count crosses checkpoints on the
     * awaiting-input shape itself (`repeat.count`), not here.
     */
    answeredAsk;
    // `lastExecutor` is now inherited as a protected field from RunnerBase
    // (single canonical source for footprintjs snapshot access across all
    // runners). Agent's `getLastSnapshot()` delegates to the inherited
    // implementation but is kept here for the JSDoc + clearer return type.
    // The chart is now cached on RunnerBase (`protected chart`) via
    // `initChart()` — built ONCE at constructor time. `getSpec()` returns
    // it. `createExecutor()` reuses it. The earlier `lastFlowChart`
    // field was a per-run workaround for the lazy-build pattern; both
    // are obsolete now. `getSpec()` always returns the same reference
    // the executor traces.
    /**
     * Memory subsystems registered via `.memory()`. Each definition mounts
     * its `read` subflow before the InjectionEngine on every turn; per-id
     * scope keys (`memoryInjectionKey(id)`) keep multi-memory layering
     * collision-free.
     */
    memories;
    /**
     * Optional terminal contract. Set via the builder's `.outputSchema()`.
     * When present, `agent.runTyped()` parses + validates the final
     * answer against this parser. `agent.run()` keeps returning the
     * raw string; consumers opt into typed mode explicitly.
     */
    outputSchemaParser;
    /**
     * What the LOOP enforces about the output (7.26) — resolved by the builder
     * when `.outputSchema()` was given `retries` or a `'tool-forced'` strategy.
     * Undefined otherwise, and undefined is the whole of the byte-identical
     * path: no branch is mounted, the decider is the function it always was,
     * and the request is the one 7.25 sent.
     */
    outputEnforcement;
    /**
     * Optional 3-tier degradation for output-schema validation
     * failures. Set via the builder's `.outputFallback({...})`. When
     * present, `parseOutput()` and `runTyped()` fall through:
     *   primary → fallback → canned (in order; canned guarantees no-throw).
     */
    outputFallbackCfg;
    /** Side-channel for `run({ continueFrom })` and `resumeOnError(...)` —
     *  when set, the seed function restores `scope.history` from this
     *  instead of starting fresh, and appends this turn's user entry when
     *  `appendsUserTurn` says so. Cleared on first read so subsequent runs
     *  start clean. */
    pendingResumeHistory;
    /** Retried request's repair state; never inherited by a new human turn. */
    pendingEvidenceRecovery;
    /** Its sibling for the folded spans. A restored conversation that dropped
     *  them would carry summaries nobody could unpack — the evidence would be
     *  destroyed by the act of continuing, which is the one thing retention
     *  exists to prevent. Cleared on first read, exactly like the history. */
    pendingResumeFolded;
    /** Its sibling for the findings ledger (9.101.0) — the model's standings
     *  on results this process never saw, carried by the checkpoint and
     *  restored as a stored record. Cleared on first read, like the history. */
    pendingResumeFindingsLedger;
    /** The last completed run's final answer — see `checkpoint()` for why it is
     *  kept here rather than read back from the recording. Undefined after a run
     *  that failed or paused. */
    lastRunAnswer;
    /** Last run's checkpoint owner, including explicit replay edits from a failed child stage. */
    checkpointTracker;
    /** Whether any run on this instance got past input admission — so a
     *  refused FIRST turn is reported as 'never-run', never as "continue from
     *  your earlier accepted checkpoint" when there is none. */
    hadAdmittedRun = false;
    /** The id the CONSUMER chose, or undefined when they took the default.
     *  `this.id` cannot answer that question — it is `'agent'` either way — and
     *  the stored-conversation fingerprint refuses only on ids somebody picked
     *  (see `AgentRunCheckpoint.agent`). */
    explicitId;
    /** The identity the caller gave the last run, or undefined when they gave
     *  none. Only an EXPLICIT identity is carried onto `checkpoint()`: the
     *  default is derived from a runId, and storing that would pin a whole
     *  conversation to the id of the one run that started it.
     *
     *  Written by BOTH run doors, before the executor exists: `run()` from the
     *  call and the continued conversation, `resume()` from the call and the
     *  paused run's own record (`agent/callerIdentity.ts · callerIdentityOf`).
     *  A resume that inherited it instead stored another person's identity on
     *  a shared agent and none on a rebuilt pooled one. */
    lastRunIdentity;
    /** Run ids this instance minted, most recent last (bounded) — evidence that a
     *  checkpoint's `{ conversationId: '<runId>' }` is the per-run default
     *  (`agent/callerIdentity.ts`). */
    mintedRunIds = new Set();
    /** The self-explain evidence key of the run in flight (or served last):
     *  its session, else a host's per-request key, else undefined
     *  (`agent/servingConversation.ts`). */
    servingConversation;
    /** How long ONE tool teardown may take before the runner stops waiting.
     *  See `AgentOptions.toolTeardownTimeoutMs`. */
    toolTeardownTimeoutMs;
    /** The run in flight, by id — the whole of the one-turn-at-a-time guard.
     *  Set before the executor is built and cleared in `finally`, so a run that
     *  throws does not leave the agent permanently refusing. */
    inFlightRunId;
    /** The scopes this agent's OWN executors made — see the `isOwnRun` seed dep. */
    ownScopes = new WeakSet();
    /** The question a person still owes this agent an answer to. Set when a run
     *  ends paused, cleared by `resume()`, `abandonPause()`, or a run that
     *  completes. Read by the `run()` guard — see `PendingQuestionError`. */
    pendingQuestion;
    /** The `.selfExplain()` binding, when the builder mounted one. Held so
     *  `canExplain()` can answer the same question the trace tools answer, from
     *  the same fact. Undefined on every agent that never called `.selfExplain()`. */
    selfExplainBinding;
    /**
     * Optional `ToolProvider` set via the builder's `.toolProvider()`.
     * When present, the Tools slot subflow consults it per iteration
     * (Block A5 follow-up) — the provider's tools land alongside any
     * tools registered statically via `.tool()` / `.tools()`. The
     * tool-call dispatcher also consults it for per-iteration execute
     * lookup so dynamic chains (`gatedTools`, `skillScopedTools`)
     * dispatch correctly when their visible-set changes mid-turn.
     */
    externalToolProvider;
    /**
     * Optional rules-based reliability config (v2.11.5+). Set via the
     * builder's `.reliability({...})`. When present, every CallLLM
     * execution is wrapped in a retry/fallback/fail-fast loop driven
     * by `preCheck` and `postDecide` rules. Consumed by `buildCallLLMStage`.
     */
    reliabilityConfig;
    /**
     * Resolved ThinkingHandler (v2.14+). The provider's DECLARED one
     * (`thinkingHandlerFor`) UNLESS the builder explicitly set one
     * (or `null` to opt out). When undefined, the NormalizeThinking
     * sub-subflow is NOT mounted at chart build time — zero overhead
     * for non-thinking agents.
     */
    thinkingHandler;
    /**
     * v2.14+ — request-side thinking budget. When set, every LLMRequest
     * carries `thinking: { budget }`. AnthropicProvider translates to the
     * wire format. Undefined = no thinking activation (default behavior).
     */
    thinkingBudget;
    /** Threaded to footprintjs `flowChart()` so every node the Agent
     *  builder creates is observed by these recorders at build time. Set
     *  from `opts.structureRecorders`; undefined when consumer didn't
     *  attach any. */
    structureRecorders;
    /** Per-COMPOSITION translator (L1b). Set from `opts.groupTranslator`;
     *  undefined when consumer didn't attach one. */
    agentGroupTranslator;
    /** ReAct loop mode — 'dynamic' (default, re-engineer all slots each turn,
     *  flat chart), 'classic' (engineer context once, loop→Messages only, flat
     *  chart), or 'dynamic-grouped' (dynamic semantics + LLM turn wrapped in an
     *  sf-llm-call subflow for richer Lens grouping). Set from `opts.reactMode`.
     *  See AgentOptions. */
    reactMode;
    /** The recipes `.recipe()` applied, in declaration order. Held for ONE
     *  purpose: the run manifest's `recipes` rows, so a recording can say which
     *  composition produced the agent that answered. Undefined — never `[]` — on
     *  every agent built without one, which is what keeps the manifest of an
     *  agent that uses no recipes byte-identical to the one it emitted before
     *  they existed. */
    appliedRecipes;
    /** The DECLARED skill map (9.50.0) — nodes + edges verbatim, captured by
     *  `AgentBuilder.skillGraph()`. Filed once per run as
     *  `agentfootprint.skill.graph_declared`; undefined = no graph, or a graph
     *  that could not state its map (the event then never fires). */
    skillGraphDeclared;
    /** The maps kernel's plan (9.58.0) — present only when built with `.maps()`. */
    mapsPlan;
    /** `.claims()` (9.61.0) — the declared claim contract, or undefined. */
    claimContract;
    /** `AgentOptions.recordSystemPrompt` (9.50.0) — OFF by default. When true,
     *  every `stream.llm_start` carries the assembled system prompt verbatim as
     *  `systemPromptText`. */
    recordSystemPromptValue = false;
    /** `AgentOptions.recordReceipt` (9.88.0) — ON by default. `false` declines
     *  the mint at `callLLM`; nothing else about the run changes. */
    recordReceiptValue = true;
    /** The receipt memo every call of a run mints through — one per agent,
     *  rebinding itself to each run id (`core/agent/receipt/receiptDigests.ts`). */
    receiptDigests = createReceiptDigests();
    answerValidationConfig;
    negativeClaimsConfig;
    typedNegativeClaimsConfig;
    applicationAnswerValidation;
    constructor(opts, systemPromptValue, registry, voice, injections = [], memories = [], outputSchemaParser, toolProvider, systemPromptCachePolicy = 'always', cachingDisabled = false, cacheStrategy, outputFallbackCfg, reliabilityConfig, thinkingHandlerValue, thinkingBudgetValue, skillGraphNextSkill, skillGraphReachable, skillGraphScoreEntries, checkInOptions, runConfigFn, windowStrategy, toolMiddleware, messageMiddleware, outputEnforcement, skillGraphEdgeTargets, skillGraphExplainNextSkill, skillGraphIsTree, skillGraphSupersededEntries, skillGraphCascade, skillBrains, evidenceGate, limitsTravelWithTheAnswer, recipes, skillGraphDeclared, mapsPlan, claimContract, answerValidationConfig, systemPromptCacheExplicit = false, negativeClaimsConfig, scriptTravel) {
        super();
        // Which conversation this instance's self-explain evidence belongs to —
        // held in a module registry, not on the class, so it is not public API
        // (`agent/servingConversation.ts`).
        registerServingConversation(this, () => ({
            conversation: this.servingConversation,
            runId: this.currentRunContext.runId,
            ...(this.servingConversation?.startsWith('hosted:') === true && { oneShot: true }),
        }));
        this.negativeClaimsConfig = negativeClaimsConfig;
        this.typedNegativeClaimsConfig =
            negativeClaimsConfig?.mode === 'typed' ? negativeClaimsConfig : undefined;
        this.applicationAnswerValidation = answerValidationConfig !== undefined;
        this.answerValidationConfig =
            answerValidationConfig ??
                (this.typedNegativeClaimsConfig === undefined
                    ? undefined
                    : resolveAnswerValidation({
                        id: 'negative-claims',
                        version: '1',
                        mode: 'observe',
                        validate: () => ({ checks: [] }),
                    }));
        this.provider = opts.provider;
        this.name = opts.name ?? 'Agent';
        this.id = opts.id ?? 'agent';
        if (opts.id !== undefined)
            this.explicitId = opts.id;
        this.model = opts.model;
        this.temperature = opts.temperature;
        this.maxTokens = opts.maxTokens;
        this.maxIterations = clampIterations(opts.maxIterations ?? 10);
        this.structureRecorders = opts.structureRecorders;
        this.agentGroupTranslator = opts.groupTranslator;
        this.reactMode = opts.reactMode ?? 'dynamic';
        this.systemPromptValue = systemPromptValue;
        this.systemPromptCachePolicy = systemPromptCachePolicy;
        this.cachingDisabledValue = cachingDisabled;
        // The strategy the provider's DECLARED prompt caching selects, unless the
        // caller overrides it — never one looked up by `provider.name`, which a
        // renaming decorator (`withRetry`, an app's own wrapper) rewrites. A
        // provider that declares nothing gets the no-op strategy.
        this.cacheStrategy = cacheStrategy ?? cacheStrategyFor(opts.provider, opts.model);
        this.cacheStrategyIsOverride = cacheStrategy !== undefined;
        this.cacheUnsupported = cacheRequestedWithoutSupport({
            strategy: this.cacheStrategy,
            providerName: opts.provider.name,
            systemPrompt: systemPromptValue,
            systemPolicy: systemPromptCachePolicy,
            systemExplicit: systemPromptCacheExplicit,
            modelAwareUnavailable: cacheStrategy === undefined &&
                typeof opts.provider.promptCaching === 'function' &&
                this.cacheStrategy.name === 'none',
            injections,
            disabled: cachingDisabled,
        });
        this.registry = registry;
        this.injections = injections;
        this.skillGraphNextSkill = skillGraphNextSkill;
        this.skillGraphReachable = skillGraphReachable;
        this.skillGraphScoreEntries = skillGraphScoreEntries;
        this.skillGraphEdgeTargets = new Set(skillGraphEdgeTargets ?? []);
        this.skillGraphExplainNextSkill = skillGraphExplainNextSkill;
        this.skillGraphSupersededEntries = skillGraphSupersededEntries;
        this.skillGraphIsTree = skillGraphIsTree ?? false;
        this.skillGraphCascade = skillGraphCascade;
        this.skillBrains = skillBrains;
        // Under an enforcing answer validation the gate's revision calls no tools
        // (`ResolvedEvidenceGate.toolsOffAfterRevision`): one fact on the one gate
        // every reader is already handed.
        this.evidenceGate =
            evidenceGate !== undefined &&
                evidenceGate.posture !== 'assist' &&
                (answerValidationConfig?.mode === 'enforce' || this.typedNegativeClaimsConfig !== undefined)
                ? { ...evidenceGate, toolsOffAfterRevision: true }
                : evidenceGate;
        this.limitsTravelWithTheAnswerValue = limitsTravelWithTheAnswer === true;
        this.scriptTravel = scriptTravel;
        this.appliedRecipes = recipes;
        this.skillGraphDeclared = skillGraphDeclared;
        this.mapsPlan = mapsPlan;
        this.claimContract = claimContract;
        if (opts.recordSystemPrompt === true)
            this.recordSystemPromptValue = true;
        // The receipt's OFF SWITCH (9.88.0). Default ON, so only an explicit
        // `false` is remembered and every other agent's dep bag is byte-identical.
        if (opts.recordReceipt === false)
            this.recordReceiptValue = false;
        this.memories = memories;
        this.outputSchemaParser = outputSchemaParser;
        this.outputEnforcement = outputEnforcement;
        this.outputFallbackCfg = outputFallbackCfg;
        this.externalToolProvider = toolProvider;
        // Eager validation: tool names must be unique across .tool() +
        // every Skill.inject.tools — the LLM dispatches by name. Runs in
        // constructor so `Agent.build()` throws immediately on collision,
        // not at first run().
        validateToolNameUniqueness(registry, injections);
        // Eager validation: memory ids must be unique so per-id scope keys
        // (`memoryInjection_${id}`) don't collide.
        validateMemoryIdUniqueness(memories);
        // Evidence bridge (#5): a CAUSAL memory gets a run-scoped harvest recorder
        // (decisions/toolCalls/iterations/duration/tokens). Attached per run below;
        // its `collect` is threaded into the write mount via chartDeps.
        if (memories.some((m) => m.type === 'causal')) {
            const evidence = causalEvidenceRecorder();
            this.causalEvidence = evidence;
            // A causal memory is working state: its store keeps what a later run
            // replays to the MODEL, so its tool calls are read on the real-value
            // path, never off the served events — a policy keeps a run's records
            // clean, never what the agent will compute on (`src/redaction/`). The
            // decisions it keeps come from footprintjs's flow channel, which serves
            // them under the policy (a named limit in `src/redaction/README.md`).
            this.dispatcher.onRealEvent((event) => evidence.onEmit({ name: event.type, payload: event.payload }));
        }
        // A dial without its switch — refused rather than run as a no-op (8.13.0).
        // Same policy as `observerDeliveryOptions` below; the message and the check
        // are shared with LLMCall, which takes the identical pair.
        assertCostBudgetHasPricing('Agent', opts.pricingTable, opts.costBudget);
        if (opts.pricingTable)
            this.pricingTable = opts.pricingTable;
        // Normalized once, at build: `onExceed` decides whether the Route decider
        // stops the loop, and a policy resolved per-run could differ per-run.
        const resolvedCostBudget = resolveCostBudget('Agent', opts.costBudget);
        if (resolvedCostBudget !== undefined)
            this.costBudget = resolvedCostBudget;
        if (opts.contextBudget !== undefined)
            this.contextBudget = opts.contextBudget;
        if (opts.permissionChecker)
            this.permissionChecker = opts.permissionChecker;
        if (opts.toolArgValidation !== undefined)
            this.toolArgValidation = opts.toolArgValidation;
        if (opts.findings !== undefined)
            this.findingsOptions = {
                ...opts.findings,
                maxContextBytes: findingsContextBytes(opts.findings.maxContextBytes),
            };
        // The inputs layer's door (honesty layer 2): `true`, or `{ argumentSources }`
        // — either form mounts the layer; the object form may also arm declared
        // sources without the findings ledger.
        const inputsLayer = readInputsLayerOption(opts.inputsLayer);
        if (inputsLayer !== undefined)
            this.inputsLayerOption = true;
        if (opts.resultsLayer === true)
            this.resultsLayerOption = true;
        if (opts.answerLayer === true)
            this.answerLayerOption = {};
        else if (typeof opts.answerLayer === 'object' && opts.answerLayer !== null) {
            // The builder judged the option (`AgentBuilder.ts` · `answerLayerArm`); `true` is 'always'.
            const line = opts.answerLayer.standingLine;
            this.answerLayerOption =
                line === true
                    ? { standingLine: 'always' }
                    : typeof line === 'object' && line !== null
                        ? { standingLine: line.when }
                        : {};
        }
        // The time layer's door (`.time()` / `AgentOptions.time`): `true` or the
        // options, read once; a malformed zone is refused here, at build.
        if (opts.time !== undefined) {
            const read = readTimeOptions(opts.time === true ? undefined : opts.time);
            if ('problem' in read)
                throw new Error(`Agent: time ${read.problem}.`);
            this.timeOptions = read.value;
            // The reader reads only a person's message — a composition hands the
            // `messageFrom: 'composed'` marker only to a runner that reads it.
            if (read.value.reader !== undefined)
                readsMessageFrom(this);
        }
        if (opts.findings?.argumentSources === true || inputsLayer?.argumentSources === true) {
            this.argumentSourcesArmed = true;
            // Declared sources read `AgentInput.messageFrom` — a composition hands
            // the marker only to a runner that reads it.
            readsMessageFrom(this);
        }
        // A member runs for the run that mounted it: a composition hands it that
        // run as `parentRun` (`core/memberRun.ts`).
        readsParentRun(this);
        if (opts.argumentAskContext !== undefined)
            this.argumentAskContext = opts.argumentAskContext;
        if (opts.toolChoice !== undefined)
            this.toolChoiceOptions = opts.toolChoice;
        if (opts.ontology !== undefined)
            this.ontology = opts.ontology;
        // The tool-result ceiling (9.11.0). Refused HERE, naming the value, rather
        // than at the first tool call of the first run — a dial that cannot cap
        // anything is a configuration mistake, not a runtime condition.
        assertMaxToolResultChars('Agent', opts.maxToolResultChars);
        if (opts.maxToolResultChars !== undefined)
            this.maxToolResultChars = opts.maxToolResultChars;
        // Resolve check-in config once. Always present (default: standard evidence
        // + lexical scorer) so a `checkIn`-declaring tool works even without a
        // `.checkIn()` call; the gate only fires for tools that declared `checkIn`.
        this.checkInConfig = resolveCheckInConfig(checkInOptions);
        // Refused HERE, not at the first resume: a ledger that cannot spend would
        // refuse every decision, and that is a configuration mistake.
        if (opts.decisionLedger !== undefined) {
            if (typeof opts.decisionLedger?.spend !== 'function') {
                throw new TypeError('Agent: `decisionLedger` must have a `spend(requestId)` method — see ' +
                    "`DecisionLedger`, or omit it for this process's in-memory ledger.");
            }
            this.decisionLedger = opts.decisionLedger;
        }
        if (runConfigFn !== undefined)
            this.runConfigFn = runConfigFn;
        // `.window()` / `.compaction()`: the meter is created ONCE per agent
        // (attached per run, cleared between runs like every other recorder)
        // because the window stage closes over it at chart-build time — and the
        // chart is built once.
        if (windowStrategy !== undefined) {
            this.windowStrategy = windowStrategy;
            const meter = compactionMeter();
            this.compactionMeterHandle = meter;
            this.evictedTurnsHandle = createEvictedTurnsHandle();
            // The provider's token count is a MEASUREMENT the window decides on, so
            // the meter reads it on the real-value path, never off the served event:
            // a redaction pattern that happens to match `input` must not switch
            // compaction off (`src/redaction/runRedaction.ts`).
            this.dispatcher.onRealEvent((event) => {
                if (event.type === 'agentfootprint.stream.llm_end')
                    meter.noteLlmEnd(event.payload);
            });
        }
        // The two governance chains. Empty arrays (not undefined) so every read
        // site is a plain `.length > 0` test rather than an optional dance.
        this.toolMiddleware = toolMiddleware ?? [];
        // What an ask's approval holds to, for a host that keeps this agent's
        // pauses and must tell one no answer can reach (`consentRequest.ts`).
        registerConsentRules(this, () => this.toolMiddleware.map((link) => link.name));
        // …and the ledger it spends decisions in, so that host can tell a paused
        // call that provably never ran from one whose outcome is not known.
        registerConsentLedger(this, this.decisionLedger);
        this.messageMiddleware = messageMiddleware ?? [];
        // Default 'summary' — measurement-gated (#18): stageReads values have
        // zero consumers across af/lens/eui, and 'full' clones ~18MB of unread
        // data per 200 iterations. Consumers opt into 'full' explicitly.
        this.readTracking = opts.readTracking ?? 'summary';
        // Default 'delta' — the accepted #13c-B design: agentfootprint opts in
        // immediately (the agent's history-append workload is exactly the case
        // the verb exists for; reconstruction stays lossless via commitValueAt).
        this.commitValues = opts.commitValues ?? 'delta';
        // Default 'off' — the debugging dial is opt-in: on, every write also
        // records the keys read before it, which is what upgrades `traceVariable`
        // to `coverage: 'exact'` and lets `walkToRoot` hop by recorded dataflow
        // instead of embedding similarity.
        this.writeProvenance = opts.writeProvenance ?? 'off';
        // The redaction door: refused here, where it was declared, when it is not
        // a policy footprintjs can apply — then declared for every run of this
        // agent and of any composition that mounts it (`src/redaction/declared.ts`).
        if (opts.redact !== undefined) {
            assertRedactionPolicy(opts.redact, 'Agent.create');
            declareRedaction(this, opts.redact);
        }
        // RFC-001 Block 10 — observer delivery tier. Fail fast on the dials
        // without the switch (no silently-ignored combinations; same policy
        // that merged reactMode/reactStructure in 6.0.0).
        this.observerDelivery = opts.observerDelivery ?? 'inline';
        if (opts.observerDeliveryOptions !== undefined && this.observerDelivery !== 'deferred') {
            throw new Error("Agent: observerDeliveryOptions requires observerDelivery: 'deferred' — " +
                'the dials configure the deferred capture queue and have no meaning inline.');
        }
        this.observerDeliveryOptions = opts.observerDeliveryOptions;
        if (opts.credentials)
            this.credentialProvider = opts.credentials;
        // Kept only when it says something: `undefined` and `true` are the same
        // default, and storing `true` would make the thread-when-off rule below
        // read as a coincidence.
        if (opts.repeatedCallNudge === false)
            this.repeatedCallNudge = false;
        // Same discipline (9.56.0): `undefined` and `true` are the same default, so
        // only a deliberate opt-out is stored — which keeps the conditional mount
        // below reading as the decision it is.
        if (opts.wrapUpAtMaxIterations === false)
            this.wrapUpAtMaxIterations = false;
        // Same discipline (9.57.0): the default is 2, so only a value the
        // operator actually named is stored — which keeps the value-conditional
        // thread into the window stage reading as the decision it is. Refused at
        // construction, never mid-run.
        if (opts.keepLastToolResults !== undefined) {
            requireKeepLastToolResults(opts.keepLastToolResults, 'Agent');
            this.keepLastToolResults = opts.keepLastToolResults;
        }
        // Its content-aware sibling (9.102.0). The top-level door is refused
        // HERE, whether or not `.findings()` is on — a dial that is silently
        // ignored is a configuration mistake, not a runtime condition (the
        // `.findings()` door was refused by the builder). The value is resolved
        // ONCE, under the arm only: the `.findings()` door wins when both are
        // given, `false` is `0`, nothing named is the default. Without
        // `.findings()` there is no ledger to hold facts from, so the option is
        // accepted and does nothing — the keepLastToolResults-without-a-window
        // precedent.
        if (opts.keepLedgerFacts !== undefined)
            requireKeepLedgerFacts(opts.keepLedgerFacts, 'Agent');
        if (this.findingsOptions !== undefined) {
            // Keep the supported findings execution modes explicit. This change
            // restores prefix caching; it does not add findings support to classic.
            if (this.reactMode === 'classic') {
                throw new Error("Agent: .findings() is supported in 'dynamic' and 'dynamic-grouped' modes only; reactMode 'classic' is not supported.");
            }
            this.keepLedgerFacts = resolveKeepLedgerFacts(this.findingsOptions.keepLedgerFacts ?? opts.keepLedgerFacts);
        }
        // Tool choice by classifier (9.105.0) needs the same per-call recomposition
        // the ledger does — the pick is made where the served list is built, and
        // a narrowed list committed on turn 1 would be served on every later call
        // with no pick behind it. The same refusal, at the same place.
        if (this.toolChoiceOptions !== undefined && this.reactMode === 'classic') {
            throw new Error("Agent: .toolChoice() requires per-iteration slot recomposition — reactMode 'classic' " +
                'caches the tools slot on turn 1, so the classifier would be asked once and a narrowed ' +
                "list would be served on every later call. Use the default 'dynamic' mode (or " +
                "'dynamic-grouped').");
        }
        // Refused at construction, never mid-run — a misspelled posture that was
        // ignored would leave the liveness theorems switched off in an agent
        // that believes they are on (the concurrency-mode precedent).
        if (opts.integrityPosture !== undefined) {
            if (opts.integrityPosture !== 'observe' && opts.integrityPosture !== 'dev') {
                throw new Error(`Agent: integrityPosture must be 'observe' or 'dev', got ` +
                    `${JSON.stringify(opts.integrityPosture)}.`);
            }
            this.integrityPosture = opts.integrityPosture;
        }
        // The external-ground door (9.72.0) — refused at construction, never
        // mid-run: a non-function here would otherwise fail on the first LLM call
        // of the first armed run, far from the line that caused it.
        if (opts.externalGrounds !== undefined) {
            if (typeof opts.externalGrounds !== 'function') {
                throw new Error(`Agent: externalGrounds must be a function returning {value, source} entries, got ` +
                    `${typeof opts.externalGrounds}.`);
            }
            this.externalGrounds = opts.externalGrounds;
        }
        // The write-seam advisory's dial (9.77.0) — refused at construction for
        // the same reason as the posture above: a truthy non-boolean here (a
        // string, a number) would silently arm a check the author only half
        // asked for, and the arming is what decides whether a run is
        // byte-identical to the one before it.
        if (opts.noticeEmptyLookups !== undefined) {
            if (typeof opts.noticeEmptyLookups !== 'boolean') {
                throw new Error(`Agent: noticeEmptyLookups must be a boolean, got ` +
                    `${JSON.stringify(opts.noticeEmptyLookups)}. It arms the write-seam 'empty-lookup' ` +
                    `advisory — a lookup for a value this run itself produced coming back empty. ` +
                    `Omit it (or pass false) and no such advisory is ever filed.`);
            }
            this.noticeEmptyLookups = opts.noticeEmptyLookups;
        }
        // The claim seam's recency dial (9.83.0) — refused at construction for
        // the same reason as the write-seam one above: a truthy non-boolean here
        // would silently arm a check the author only half asked for, and the
        // arming is what decides whether a run is byte-identical to the one
        // before it.
        if (opts.noticePriorTurnEvidence !== undefined) {
            if (typeof opts.noticePriorTurnEvidence !== 'boolean') {
                throw new Error(`Agent: noticePriorTurnEvidence must be a boolean, got ` +
                    `${JSON.stringify(opts.noticePriorTurnEvidence)}. It arms the claim-seam ` +
                    `'prior-turn-evidence' advisory — a final answer whose every value was last served ` +
                    `before this turn — and it needs \`.namesAndNumbersFromEvidence()\` armed beside ` +
                    `it, because that gate owns the extractor that decides which tokens are values. ` +
                    `Omit it (or pass false) and no such advisory is ever filed.`);
            }
            this.noticePriorTurnEvidence = opts.noticePriorTurnEvidence;
        }
        // The column-type contract's dial (9.78.0) — refused at construction for
        // the same reason as the postures above, and with one more: `'enforce'`
        // REFUSES tool results, so a misspelling silently downgraded to off would
        // leave an operator believing a boundary is held that nothing is holding.
        if (opts.checkColumnTypes !== undefined) {
            if (opts.checkColumnTypes !== 'off' &&
                opts.checkColumnTypes !== 'warn' &&
                opts.checkColumnTypes !== 'enforce') {
                throw new Error(`Agent: checkColumnTypes must be 'off', 'warn' or 'enforce', got ` +
                    `${JSON.stringify(opts.checkColumnTypes)}. It reads each tool's declared ` +
                    `\`resultColumns\` and judges the rows it returns: 'warn' files findings and ` +
                    `changes nothing the model reads; 'enforce' refuses the rows and hands the model a ` +
                    `teaching sentence instead. Omit it (or pass 'off') and nothing is ever measured.`);
            }
            this.checkColumnTypes = opts.checkColumnTypes;
        }
        // The claim-check seam (9.21.0). One store per agent, attached at
        // construction — idempotent by shape: there is no second door to attach a
        // competing one through, so "one per agent" is a fact of the type rather
        // than a runtime check. Since 9.22.0 the option also takes the object
        // form `{ store, placement? }` — normalized HERE so every downstream
        // reader keeps seeing exactly one store and one optional dial.
        if (opts.artifacts) {
            if ('store' in opts.artifacts) {
                // The object form. A shape with `placement` but no store cannot be
                // SPELLED in TypeScript, but plain JavaScript can hand us one — and a
                // threshold with nowhere to put what it catches is configuration
                // that lies, so it is refused by name rather than half-read.
                if (opts.artifacts.store === undefined || opts.artifacts.store === null) {
                    throw new Error('Agent: artifacts was passed in its object form without a `store`. The placement ' +
                        'threshold refs results INTO the store, so it cannot exist without one — pass ' +
                        '`artifacts: { store: inMemoryArtifacts(), placement: { maxInlineChars } }` (or ' +
                        'fileArtifacts / sqliteArtifacts / any ArtifactStore), or drop `artifacts`.');
                }
                assertArtifactPlacement('Agent', opts.artifacts.placement);
                this.artifactStore = opts.artifacts.store;
                if (opts.artifacts.placement !== undefined) {
                    this.artifactPlacement = opts.artifacts.placement;
                }
                // The recordings dial (9.26.0). `true` and `{ … }` normalize to ONE
                // shape here, so every downstream reader asks a single question
                // ("is there a recordings option?") rather than re-deriving the union.
                if (opts.artifacts.recordings === true) {
                    this.artifactRecordings = {};
                }
                else if (typeof opts.artifacts.recordings === 'object' &&
                    opts.artifacts.recordings !== null) {
                    this.artifactRecordings = opts.artifacts.recordings;
                }
            }
            else {
                this.artifactStore = opts.artifacts;
            }
        }
        // The third dial-without-its-switch (8.13.0). `onAuthorizationRequired` is
        // read at exactly one place — the tool-dispatch loop, AFTER
        // `credentials.getCredential` came back `authorization-required`. With no
        // provider that call never returns at all: the fail-closed stand-in
        // (`unconfiguredCredentialProvider`) rejects, so the branch this option
        // governs is unreachable and the setting decides nothing.
        if (opts.onAuthorizationRequired !== undefined && opts.credentials === undefined) {
            throw new Error('Agent: onAuthorizationRequired was set without a `credentials` provider, so it can ' +
                'never be reached — it decides what happens when a DECLARED credential comes back ' +
                "'authorization-required', and with no provider no credential is ever requested (the " +
                'fail-closed stand-in throws instead). Pass `credentials` — agentCoreIdentity({ ' +
                'region }), staticTokens({ … }), or any CredentialProvider from ' +
                "'agentfootprint/security' — or drop `onAuthorizationRequired`.");
        }
        // 8.6.0 — default `'pause'`: consent is work waiting on a person, and the
        // model is the one party that cannot click a link.
        this.onAuthorizationRequired = opts.onAuthorizationRequired ?? 'pause';
        // 9.7.0 — teardown is on the SIGTERM path, so it is bounded by default.
        this.toolTeardownTimeoutMs = opts.toolTeardownTimeoutMs ?? TOOL_TEARDOWN_TIMEOUT_MS;
        if (reliabilityConfig !== undefined)
            this.reliabilityConfig = reliabilityConfig;
        // v2.14 — Resolve thinking handler. Three states:
        //   - thinkingHandlerValue === undefined → the provider's DECLARED handler
        //   - thinkingHandlerValue === null      → opt out (no handler)
        //   - thinkingHandlerValue: ThinkingHandler → explicit override
        // A provider that declares none gets no subflow at chart build time.
        // Never chosen by `provider.name`: a renaming wrapper that forwards the
        // declaration keeps it.
        const handler = thinkingForProviders(opts.provider, reliabilityConfig?.providers?.map((entry) => entry.provider), thinkingHandlerValue);
        if (handler !== undefined)
            this.thinkingHandler = handler;
        if (thinkingBudgetValue !== undefined)
            this.thinkingBudget = thinkingBudgetValue;
        this.appName = voice.appName;
        this.commentaryTemplates = voice.commentaryTemplates;
        this.thinkingTemplates = voice.thinkingTemplates;
        // Eager chart construction — see `RunnerBase.initChart` JSDoc.
        // Note re Agent specifics (footprintjs inventor's review):
        // - `providerToolCache: { current: Tool[] }` is closed over by the
        //   chart's Discover + dispatch stages. It's shared across
        //   sequential runs of this Agent instance, but the Discover stage
        //   overwrites `current` at the start of every iteration (line 158
        //   of `buildToolsSlot.ts`), so stale data never reaches a tool
        //   call. Concurrent runs on the SAME Agent instance already share
        //   `currentRunContext` and the recorder dispatcher — eager build
        //   doesn't change that constraint.
        // - `currentRunContext` is read by the per-stage `getRunCtx`
        //   lambda at execution time (not at chart-build time), so a fresh
        //   value per run still flows through correctly.
        this.initChart(() => this.buildChart());
    }
    static create(opts) {
        return new AgentBuilder(opts);
    }
    /**
     * Cache policy for the base system prompt. Read by the CacheDecision
     * subflow (v2.6 Phase 4) to know how to treat the SystemPrompt slot's
     * cache markers. Exposed as a method (not direct field access) so
     * the Agent's encapsulation boundary stays clean.
     */
    getSystemPromptCachePolicy() {
        return this.systemPromptCachePolicy;
    }
    /**
     * The artifact store this agent was built with, or `undefined` when none
     * was attached (9.23.0).
     *
     * For COMPOSERS that resolve refs on the agent's behalf — the hosting
     * layer's `artifact-head` / `artifact-get` wire operations redeem a
     * screen's claim tickets against exactly this store. It is the store, not
     * a scope-bound capability: whoever calls it owns composing the resolution
     * scope (the hosting layer composes the requesting session's identity, the
     * same tuple the run's own tools resolved under). Tools never touch this —
     * `ctx.artifacts` is already bound to the run's scope, and that remains
     * their only door.
     */
    getArtifactStore() {
        return this.artifactStore;
    }
    /**
     * Start recording this run — or do nothing at all (9.26.0).
     *
     * Zero-cost when unused is not a claim here, it is the control flow: with
     * `recordings` unset this returns `undefined` before touching the agent, so
     * no listener is subscribed, no boundary recorder is attached, and the run
     * is byte-identical to every earlier release.
     *
     * It is deliberately the SAME recorder a consumer would call by hand
     * (`recordRun`'s own body, `recordRunWhere`). Nothing about this feature is a
     * second recording implementation — the three connections that a
     * hand-rolled version gets wrong (attach, subscribe, getCommitCount) are
     * wired in exactly one place in this package, and this is a caller of it.
     * The one difference is the timeline's filter: a recording filed FOR a
     * person keeps only the events of that person's run.
     */
    startRunRecording() {
        if (this.artifactRecordings === undefined || this.artifactStore === undefined)
            return undefined;
        // Only THIS run's events: on an instance serving several sessions, the
        // hosting door emits facts for other sessions while this run is in flight,
        // and a recording is redeemable by the person it was filed for.
        return recordRunWhere(this, (event) => this.ownsEvent(event));
    }
    /**
     * Does `event` belong to the run this agent is serving (or served last)? The
     * run-membership rule (`bridge/eventMeta.ts · eventBelongsToRun`) against
     * this instance's run context — asked by the per-run recording and by the
     * self-explain evidence, the two collectors that keep one run's events.
     *
     * @internal
     */
    ownsEvent(event) {
        return eventBelongsToRun(event.meta, this.currentRunContext);
    }
    /**
     * File the finished recording into the artifact store.
     *
     * ── When ────────────────────────────────────────────────────────────────
     * After the answer is composed, and only for a run that COMPLETED. A pause
     * is not a finished run (the turn continues, and the resume mints its own);
     * a throw never reaches here at all.
     *
     * ── Why it is awaited ───────────────────────────────────────────────────
     * The answer is final before this begins and this cannot change it — but
     * `run()` does return after the write rather than before, and that is a
     * choice rather than an oversight. A fire-and-forget write is a recording
     * lost whenever the process exits with the reply, which is precisely the
     * serverless deployment that wants recordings most. The cost is one store
     * write per turn, stated on the option.
     *
     * ── Why it can never fail the run ───────────────────────────────────────
     * A full store, an unserializable snapshot, a bucket that 500s — none of
     * them are facts about the ANSWER, which is already correct and already
     * paid for. Turning "your recording was not filed" into "your request
     * failed" would be the library deciding that its observability matters more
     * than the user's turn. So every failure degrades to today's path: the
     * answer is returned unchanged and the reason lands on the record as
     * `agentfootprint.artifacts.refused`, where a sink can count it.
     *
     * The recording is FROZEN before the mint, so it can never contain the
     * `artifacts.minted` event describing itself.
     */
    async fileRunRecording(recorder, outcome) {
        const store = this.artifactStore;
        if (recorder === undefined || store === undefined || this.artifactRecordings === undefined) {
            return;
        }
        if (isPaused(outcome))
            return;
        const runId = this.currentRunContext?.runId;
        let input;
        try {
            const recording = recorder.toRecording();
            input = recordingPutInput(this.artifactRecordings.packed === true ? packRecording(recording) : recording, {
                ...(runId !== undefined && { runId }),
                ...(this.artifactRecordings.label !== undefined && {
                    label: this.artifactRecordings.label,
                }),
            });
        }
        catch (err) {
            this.reportRecordingRefused(err, 'invalid-input');
            return;
        }
        try {
            const result = await store.put(this.runArtifactScope(), input);
            // Sweeps ride the put result, so retention that evicted an older
            // recording to make room says so on the record — the same law a tool's
            // own mint follows.
            for (const swept of result.swept) {
                this.emit('agentfootprint.artifacts.expired', {
                    ref: swept.ref,
                    reason: swept.reason,
                    kind: swept.kind,
                    bytes: swept.bytes,
                });
            }
            this.emit('agentfootprint.artifacts.minted', {
                ref: result.meta.ref,
                kind: result.meta.kind,
                mediaType: result.meta.mediaType,
                bytes: result.meta.bytes,
                ...(result.meta.label !== undefined && { label: result.meta.label }),
                ...(result.meta.expiresAt !== undefined && { expiresAt: result.meta.expiresAt }),
                ...(result.meta.origin !== undefined && { origin: result.meta.origin }),
            });
        }
        catch (err) {
            this.reportRecordingRefused(err, 'invalid-input');
        }
    }
    /** One refusal fact for a recording that could not be filed. The message is
     *  the store's own, which never carries a payload — only what went wrong. */
    reportRecordingRefused(err, reason) {
        this.emit('agentfootprint.artifacts.refused', {
            op: 'put',
            reason,
            detail: err instanceof Error ? err.message : String(err),
        });
    }
    /**
     * The run's artifact scope, read from the finished run's own state.
     *
     * The SAME tuple `ctx.artifacts` bound during the run (`scope.runIdentity`,
     * composed by seed from the caller's identity or derived from the session).
     * Read rather than recomposed: a second derivation here could disagree with
     * the one the run's tools used, and a recording filed in a different scope
     * from the artifacts it describes is a recording nobody can find.
     */
    runArtifactScope() {
        const identity = this.liveSnapshot()?.sharedState
            ?.runIdentity;
        if (identity === undefined) {
            // No state to read means no run happened, which this path cannot reach —
            // but a scope is required and inventing a tenant would be worse than a
            // conversation id that names the run itself.
            return { conversationId: this.currentRunContext?.runId ?? 'unknown' };
        }
        return {
            conversationId: identity.conversationId,
            ...(identity.tenant !== undefined && { tenant: identity.tenant }),
            ...(identity.principal !== undefined && { principal: identity.principal }),
        };
    }
    /**
     * The footprintjs `RuntimeSnapshot` from the most recent `run()` /
     * `resume()`. Feeds Lens's Trace tab (ExplainableShell `runtimeSnapshot`
     * prop) so consumers can scrub the execution timeline post-run without
     * threading a recorder through the call site.
     *
     * `undefined` until a run has STARTED. After that it is the most recent
     * run's snapshot — including across multiple turns of the same instance.
     *
     * **It is LIVE during a run, not a completed-runs-only view.** The executor
     * is assigned at run start, so calling this from an event listener, a tool,
     * or any other mid-run vantage point returns the IN-FLIGHT run, partially
     * filled. That is deliberate (Lens scrubs a running agent through it), and
     * it is why `.selfExplain()` captures at the terminal flush instead of
     * resolving through this: evidence that is supposed to describe a FINISHED
     * turn cannot be read from a getter that also answers about an unfinished
     * one.
     *
     * **Under `redact` it is the SERVED snapshot** — footprintjs's redacted view
     * (`RunnerBase.getLastSnapshot`): the placeholder wherever the policy
     * selected a value, and no `initialState`. The agent's own logic never reads
     * this getter; `checkpoint()`, `followUp()` and the crash checkpoint read the
     * live run, so redaction never changes what the agent continues from.
     */
    getLastSnapshot() {
        return super.getLastSnapshot();
    }
    /**
     * Structured narrative entries from the most recent run. Pairs with
     * `getLastSnapshot()` for ExplainableShell's `narrativeEntries` prop.
     * Empty array (not `undefined`) when no run has completed — matches
     * the prop's expected shape so consumers can wire it directly without
     * a defensive guard.
     *
     * Under `redact`, footprintjs wrote these lines through the run's rule: a
     * read or write of a selected key reads `[REDACTED]`, and an `[emit]` line
     * shows the event's served payload (`src/redaction/runRedaction.ts`).
     */
    getLastNarrativeEntries() {
        if (this.lastExecutor === undefined)
            return [];
        // The snapshot's terms: an executor no run of this agent opened serves
        // nothing — its narrative was written under no rule anyone knows.
        if (coverageOfExecutor(this.lastExecutor).state === 'unknown')
            return [];
        return this.lastExecutor.getNarrativeEntries();
    }
    /**
     * The FlowChart compiled for the most recent run (or a freshly-built
     * one if no run has happened yet). Feeds ExplainableShell's `spec`
     * prop. Returning the cached chart matters: the spec must match what
     * `getLastSnapshot()` traced, otherwise the Trace view's stage tree
     * desyncs from the snapshot's runtime tree.
     */
    // `getSpec()` inherited from RunnerBase — returns the cached chart
    // built once at constructor time via `initChart()`. Same reference
    // every call, same reference the executor traces.
    // ─── UI group translation (L1b) ───────────────────────────────
    getGroupTranslator() {
        return this.agentGroupTranslator;
    }
    /** Agent has no nested-runner members (tools are function executors,
     *  not Runner instances). Slot ids + tool names live in `extra` so
     *  Lens can render an Agent card with slot rows + a tool list without
     *  inspecting `buildTimeStructure`.
     *
     *  Memories are NOT included as members — they're an internal
     *  mechanism, not a composition-level concept. Consumers who need
     *  memory visibility should listen for `agentfootprint.memory.*`
     *  events at runtime. */
    buildUIGroupMetadata() {
        const toolNames = this.registry.map((r) => r.name);
        return {
            kind: 'Agent',
            id: this.id,
            name: this.name,
            members: [],
            extra: {
                slots: [SUBFLOW_IDS.SYSTEM_PROMPT, SUBFLOW_IDS.MESSAGES, SUBFLOW_IDS.TOOLS],
                toolNames,
                maxIterations: this.maxIterations,
            },
        };
    }
    /**
     * Parse + validate a raw agent answer against the agent's
     * `outputSchema` parser. Throws `OutputSchemaError` on JSON parse
     * or schema validation failure (the rawOutput is preserved on the
     * error for triage). Throws a plain `Error` if the agent has no
     * outputSchema set.
     *
     * Use this when you need to keep `agent.run()` returning the raw
     * string for logging/observability and validate at a different
     * layer; otherwise prefer `agent.runTyped()`.
     */
    parseOutput(raw) {
        if (!this.outputSchemaParser) {
            throw new Error(`Agent.parseOutput: this agent has no outputSchema. Use ` +
                `Agent.create({...}).outputSchema(parser).build() to enable typed output.`);
        }
        return applyOutputSchema(raw, this.outputSchemaParser);
    }
    /**
     * Async sister of `parseOutput()`. When the agent is configured
     * with `.outputFallback({...})`, this is the version that engages
     * the 3-tier degradation chain on validation failure (the sync
     * `parseOutput` always throws on failure for back-compat).
     *
     * Without `outputFallback`, behaves identically to `parseOutput`
     * — returns sync-style on the happy path, throws OutputSchemaError
     * on validation failure.
     */
    async parseOutputAsync(raw) {
        if (!this.outputSchemaParser) {
            throw new Error(`Agent.parseOutputAsync: this agent has no outputSchema. Use ` +
                `Agent.create({...}).outputSchema(parser).build() to enable typed output.`);
        }
        const parser = this.outputSchemaParser;
        try {
            return applyOutputSchema(raw, parser);
        }
        catch (err) {
            return this.settleTypedFailure(raw, err);
        }
    }
    /**
     * A typed answer that failed: rethrow, or — with `.outputFallback()` and an
     * `OutputSchemaError` — hand it to the 3-tier fallback. The one settle for
     * `parseOutputAsync`'s parse failure and `runTyped`'s native stop failure.
     */
    async settleTypedFailure(raw, err) {
        const parser = this.outputSchemaParser;
        if (!this.outputFallbackCfg || !(err instanceof OutputSchemaError))
            throw err;
        // Engage the 3-tier fallback. The dispatcher gives us the
        // typed-event entry; we synthesize a minimal event shape since
        // these events have no per-stage anchor.
        // Typed via `OutputFallbackEmit`, so the registry — not a grep — is what
        // decides these two names are real. The cast is now only the envelope
        // assembly (type + payload are already checked against the event map).
        const emit = (eventType, payload) => {
            try {
                this.dispatcher.dispatch({
                    type: eventType,
                    timestamp: Date.now(),
                    payload,
                });
            }
            catch {
                /* observability errors must not poison the fallback path */
            }
        };
        return applyOutputFallback(raw, parser, this.outputFallbackCfg, emit, err, this.lastRunRetriesSpent());
    }
    /**
     * Corrective re-asks the agent's LAST run paid for, read off its own ledger.
     *
     * `undefined` when there is no last run to read — `parseOutputAsync` accepts
     * any string, including one that never came from this agent, and reporting
     * `0` for "I do not know" would be an invented fact in an event payload.
     */
    lastRunRetriesSpent() {
        const state = this.liveSnapshot()?.sharedState;
        if (state === undefined)
            return undefined;
        return Math.max(0, (state.outputAttempts?.length ?? 1) - 1);
    }
    /**
     * Run the agent and return the schema-validated typed output.
     * Convenience over `parseOutputAsync(await agent.run({...}))`.
     *
     * Throws `OutputSchemaError` on parse / validation failure UNLESS
     * `.outputFallback({...})` is configured, in which case the
     * 3-tier degradation chain (primary → fallback → canned) engages.
     *
     * Throws if the agent has no outputSchema set or if the run
     * pauses (use `run()` directly when pauses are expected).
     *
     * With `.limitsTravelWithTheAnswer()`, nothing is appended to a typed
     * answer: the limits its tools declared come back as data, from
     * `agent.answerCoverage()`.
     */
    async runTyped(input, options) {
        if (!this.outputSchemaParser) {
            throw new Error(`Agent.runTyped: this agent has no outputSchema. Use ` +
                `Agent.create({...}).outputSchema(parser).build() to enable typed output.`);
        }
        const out = await this.run(input, options);
        if (typeof out !== 'string') {
            throw new Error('Agent.runTyped: run paused — typed mode does not support pauses. ' +
                'Use agent.run() + agent.parseOutput(...) after resume.');
        }
        // `strategy: 'native'`: an answer whose last attempt failed on a refusal
        // or truncation stop is refused here even when its text parses — the
        // format did not hold for it (outputEnforcement.ts · nativeStopFailure).
        // FIRST, before any delivery path. It takes the same path a parse failure
        // takes, `.outputFallback()` included.
        const stopped = this.outputEnforcement?.outputFormat !== undefined ? this.outputContractUnmet() : undefined;
        if (stopped?.stopReason !== undefined) {
            return this.settleTypedFailure(out, new OutputSchemaError(stopped.error, { rawOutput: out, stage: stopped.stage }));
        }
        if (this.answerValidationConfig !== undefined) {
            // The validator judged the schema's JSON output. Do not run a transforming
            // or stateful parser again and hand back a value the validator never saw.
            if (this.answerValidation()?.schemaAccepted !== true) {
                throw new OutputSchemaError('Answer validation could not establish a JSON schema result.', {
                    rawOutput: out,
                    stage: 'schema-validate',
                });
            }
            return applyOutputSchema(out, { parse: (value) => value });
        }
        return this.parseOutputAsync(out);
    }
    /**
     * Answer one turn.
     *
     * **`run()` is ONE turn, and it starts a new conversation every time.** The
     * chart seeds its history from this call's `message` alone, so a second
     * `run()` on the same agent does not continue the first: the model is shown
     * one user message and will honestly tell your user it has not spoken to
     * them before. That is deliberate — a primitive that quietly accumulated
     * state across calls could never be used for one-shot work, and a hidden
     * transcript is the most expensive thing an agent can carry.
     *
     * To continue a conversation, name it:
     *
     *   - `agent.followUp(message)` — continue THIS agent's own last completed
     *     run. The one-liner, and what most callers want.
     *   - `run({ message, continueFrom })` — continue a conversation you are
     *     holding: `agent.checkpoint()` from an earlier turn, persisted anywhere
     *     and handed back. Works across a restart, a deploy, or a different
     *     machine, and is what `standingAgent` uses per session.
     *
     * Passing the same `identity.conversationId` to two `run()` calls does NOT
     * continue anything — see {@link AgentInput.identity}. What a registered
     * memory adds is *recall* of prior turns into the system-prompt slot, which
     * is a different thing from the conversation itself.
     *
     * Two refusals guard the per-instance state this agent keeps; both replace
     * behavior that used to succeed while quietly being wrong (9.2.0):
     * {@link RunInFlightError} when a run is already in flight, and
     * {@link PendingQuestionError} when the last run paused to ask a person
     * something that nobody has answered.
     *
     * @example  One turn, then a follow-up
     * ```ts
     * await agent.run({ message: 'Book me a table for two.' });
     * await agent.followUp('Make it three.');       // remembers the table
     * ```
     */
    async run(input, options) {
        // Normalize or refuse BEFORE anything is created. A bare string is the
        // message; anything that is not a message is named and refused here
        // rather than becoming `content: undefined` inside the messages slot.
        const runInput = normalizeRunInput(input, 'Agent.run');
        // Who wrote the message (declared sources' one reader): one of two words,
        // or nothing — a typo would read as the person's words, so it is refused.
        const messageFrom = runInput.messageFrom;
        if (messageFrom !== undefined && messageFrom !== 'person' && messageFrom !== 'composed') {
            throw new InvalidRunInputError({
                runner: 'Agent.run',
                received: `messageFrom: ${typeof messageFrom === 'string' ? JSON.stringify(messageFrom) : typeof messageFrom}`,
                hint: "messageFrom is 'person' (the default) or 'composed' (another runner's output)",
            });
        }
        // An identity that is not one is refused HERE, where the mistake was made —
        // not at the resume of a pause it would have made impossible to resume.
        assertIdentityShape(runInput.identity, 'Agent.run');
        assertIdentityShape(options?.identity, 'Agent.run');
        // The incoming request, sealed — refused here when `requestContext` did not build it.
        const runRequest = requestOf(options, 'Agent.run');
        // A policy handed down for this run is refused here, before anything moves,
        // by the same check `Agent.create({ redact })` gets.
        if (options?.redact !== undefined)
            assertRedactionPolicy(options.redact, 'Agent.run');
        // The run's clock (the time layer): read or refused HERE, before the turn
        // starts — a run with no zone anywhere runs with its zone UNKNOWN (G15),
        // never on the server's zone.
        const clockDraft = this.clockDraftFor(runInput.time ?? options?.time, 'Agent.run');
        // The engine's options; the run's signal (and its deadline) is armed just
        // before the run starts and released in its `finally` (`runSignalOf`).
        const engineOptions = withoutTime(options);
        // Timing next, and before the executor exists: both of these refuse a call
        // that would have SUCCEEDED into corrupted per-instance state or an
        // orphaned human question. See ./conversation.ts for why they are throws.
        this.assertNotRunning('Agent.run');
        this.assertNoPendingQuestion('Agent.run');
        // A conversation handed in continues through the same side channel
        // `resumeOnError` uses — one restoration path, so the two doors cannot
        // drift about what "continue" means. This turn's message IS appended:
        // continuing a conversation adds a turn to it. Seed appends it, AFTER the
        // input middleware chain, so the entry is the chain's verdict (9.112.2).
        let continued;
        if (runInput.continueFrom !== undefined) {
            continued = validateCheckpoint(runInput.continueFrom);
            this.applyContinuation(continued, 'Agent.run({ continueFrom })', true);
        }
        // Only an EXPLICIT identity is remembered for `checkpoint()`; see the
        // field's note. `input.identity` wins over `options.identity` because the
        // input bag is where a caller looks first, and both win over the stored
        // conversation's — but the conversation's is used when neither was given,
        // so a continued turn stays in the namespace it started in.
        this.lastRunIdentity =
            runInput.identity ?? options?.identity ?? (continued ? continued.identity : undefined);
        // Recordings-as-artifacts (9.26.0). BEFORE `createExecutor`, because
        // `attach()` collects recorders for the executor that has not been built
        // yet — a recording started one line later would be missing its
        // boundaries, which is the failure mode `recordRun` exists to prevent.
        // Returns `undefined` (and touches nothing) unless the dial is on.
        const recording = this.startRunRecording();
        // (helper used in the catch block below — module-private function
        // declared at file end via hoisting)
        this.answeredAsk = undefined;
        this.resumePassedTime = undefined;
        const executor = this.createExecutor(engineOptions, runRequest);
        this.inFlightRunId = this.currentRunContext.runId;
        // One disposition ledger per run (9.60.0) — registration mirrors what
        // this agent's configuration makes applicable; dev posture runs the
        // canaries here, BEFORE any real work, so a dead check is named first.
        this.beginIntegrityLedger();
        // Auto-checkpoint at iteration boundaries — captures the latest
        // conversation history into a per-run tracker. On error, we
        // wrap the underlying error in `RunCheckpointError` carrying
        // this checkpoint so `agent.resumeOnError(checkpoint)` can
        // continue from the last good iteration.
        const tracker = {
            runId: this.currentRunContext?.runId ?? 'unknown',
            originalInput: { message: runInput.message },
            history: [],
            lastCompletedIteration: 0,
        };
        this.checkpointTracker = tracker;
        const stopTracking = this.installCheckpointTracker(tracker);
        // The answer this run produces is the only place the final assistant turn
        // exists (nothing writes it back into `scope.history`), so `checkpoint()`
        // keeps it. Cleared here so a failed or paused run cannot hand back the
        // previous run's answer.
        this.lastRunAnswer = undefined;
        // One run can never raise on another run's consent block.
        this.consentOutstanding.clear();
        // Set here, beside the `try` whose `finally` clears it, so no exit path
        // can leave it behind for a turn this `run()` did not start.
        this.runClockDraft = clockDraft;
        // Armed here, beside the `try` whose `finally` releases it: no refused
        // call above can leave a deadline running.
        const runSignal = runSignalOf(engineOptions);
        try {
            const result = await executor.run({
                input: {
                    message: runInput.message,
                    ...(this.lastRunIdentity !== undefined && { identity: this.lastRunIdentity }),
                    // A composed message reaches seed only on an agent that reads it
                    // (declared sources, or the time layer's reader) — every other run's
                    // input is the bytes it was.
                    ...(messageFrom === 'composed' &&
                        (this.argumentSourcesArmed === true || this.timeOptions?.reader !== undefined) && {
                        messageFrom: 'composed',
                    }),
                },
                // Co-engineered boundary (#16): the engine's loop-iteration limit
                // (footprintjs 9 default 1000) must never fire BELOW the agent's own
                // budget — give it headroom (×2 + 10 covers double-hop loop shapes).
                // Consumer-provided options win.
                maxIterations: this.maxIterations * 2 + 10,
                ...(runSignal.options ?? {}),
            });
            const finalized = this.finalizeResult(executor, result);
            if (typeof finalized === 'string')
                this.lastRunAnswer = finalized;
            this.recordPendingQuestion(finalized);
            await this.endRunToolSessions(finalized);
            // The answer is FINISHED before this line and cannot be changed by it.
            // The liveness theorems (9.60.0, dev posture only) — a run that would
            // return green while its registered checkers demonstrably never ran
            // fails HERE instead, before the recording is filed. A finished run is
            // itself the proof work existed: the loop cannot finish without at
            // least one model call.
            this.assertIntegrityAlive();
            // See `fileRunRecording` for why the write is awaited rather than left
            // in flight, and why it can never fail the run.
            await this.fileRunRecording(recording, finalized);
            return finalized;
        }
        catch (cause) {
            // A THROWN pause is still a pause — see `endRunToolSessions`.
            await this.endRunToolSessions(cause);
            // Wrap recoverable errors with the last-known-good checkpoint.
            // Don't wrap intentional terminal signals — let them propagate as
            // their typed shapes so callers can `instanceof` them:
            //   • PauseSignal — askHuman pause, not a failure
            //   • PolicyHaltError — policy-driven termination; resuming would
            //     immediately re-trigger the same halt (the synthetic
            //     tool_result is already in history)
            //   • ReliabilityFailFastError — finalizeResult constructs and
            //     throws this AFTER the chart returns cleanly, so it never
            //     enters this catch (kept here for documentation only)
            const isTerminalTypedError = cause instanceof Error &&
                (cause.name === 'PauseSignal' ||
                    cause instanceof PolicyHaltError ||
                    cause instanceof ReliabilityFailFastError ||
                    // A provider that reports no usage will report none on resume
                    // either — wrapping this in a checkpoint would invite the caller to
                    // retry into the same wall.
                    cause instanceof CompactionUnmeasurableError ||
                    // A forced-output capability refusal is terminal even after a
                    // correction turn: a checkpoint cannot repair this model contract.
                    isRequestRefusal(cause) ||
                    // 8.6.0 — a consent block is answered by a PERSON at the identity
                    // provider, not by replaying the run. Wrapping it in a crash
                    // checkpoint would hand the caller a retry handle for a wall, and
                    // would bury the `authorizationUrl` one `.cause` deep where the
                    // person who has to click it will not look.
                    cause instanceof CredentialConsentRequiredError ||
                    // 9.35.0 — an evidence refusal is a VERDICT, not a crash: the run
                    // finished, the loop already spent its one revision, and resuming
                    // would put the same model in front of the same evidence. Wrapping
                    // it would hand the caller a retry handle for a wall and bury the
                    // named values one `.cause` deep, where the person deciding what to
                    // do about them will not look.
                    cause instanceof UnsupportedValuesError ||
                    cause instanceof AnswerValidationError ||
                    // 9.60.0 — a dead checker is a WIRING verdict on this build, not a
                    // recoverable run state: resuming would run the same dead wiring.
                    cause.name === 'CheckerDeadError');
            if (cause instanceof Error && !isTerminalTypedError && tracker.history.length > 0) {
                // Observation beats the heuristic: if a bracket was open, it says
                // exactly what the run was doing. `classifyFailurePhase` only decides
                // when nothing was — a failure between brackets, where the error's own
                // text really is the best evidence available (8.14.0).
                const observed = tracker.inFlightPhase;
                const checkpoint = buildCheckpoint(tracker, {
                    iteration: tracker.inFlightIteration ?? tracker.lastCompletedIteration + 1,
                    phase: observed?.phase ?? classifyFailurePhase(cause),
                    ...(observed !== undefined && { stage: observed.stage }),
                }, 
                // Read from the live snapshot rather than the tracker: the tracker
                // follows `history` through iteration_end events, and a fold's spans
                // are committed state. A crash checkpoint that carried the summary
                // in its history but not the span behind it would resume into a
                // conversation whose evidence the crash had quietly eaten.
                this.foldedSpansOf(this.liveSnapshot()?.sharedState), 
                // A crash checkpoint is the same conversation carrier as
                // `checkpoint()`, so it carries the same two owner facts — otherwise
                // resuming after a crash would be the one path that still lost the
                // identity, and the memory written after the recovery would land
                // where nothing could read it.
                this.conversationOwner(), 
                // …and the same graph cursor (SG-C), from the same snapshot reader —
                // one reader, two carriers, so neither can lose what the other keeps.
                this.continuityCursorOf(this.liveSnapshot()?.sharedState), this.evidenceRecoveryOf(this.liveSnapshot()?.sharedState), 
                // …and the findings ledger (9.101.0), from the same snapshot reader
                // `checkpoint()` uses — one reader, two carriers.
                this.findingsLedgerOf(this.liveSnapshot()?.sharedState), 
                // …and the turn the failing run was on (honesty layers), so the
                // retry is stamped the same turn — one reader, two carriers.
                this.turnNumberOf(this.liveSnapshot()?.sharedState), undefined, this.negativeClaimsConfig === undefined
                    ? undefined
                    : negativeCheckpointOf(this.liveSnapshot()?.sharedState ?? {}));
                throw new RunCheckpointError(cause, checkpoint);
            }
            throw cause;
        }
        finally {
            // The run's deadline and signal links end with the run (`runSignalOf`).
            runSignal.release();
            // The time layer's per-run inputs end with the run: a stale draft would
            // be stamped by the next seed this agent did not start (a composition
            // mounting its chart) as if the app had declared it.
            this.runClockDraft = undefined;
            this.resumePassedTime = undefined;
            // The run's disposition rows, on EVERY path — success, failure, pause —
            // and BEFORE the recording stops, so a recording carries its run's
            // checker accounting (9.60.0).
            this.fileIntegrityDisposition();
            // Always released: a recording left subscribed would keep listening
            // through the next run and grow a tail nobody reads.
            this.noteAdmittedRun();
            recording?.stop();
            stopTracking();
            this.inFlightRunId = undefined;
            // `seed` consumes the restored conversation on its way past. A run that
            // died BEFORE seed never did, and a history left armed here would be
            // picked up by the next run — which would then continue a conversation
            // nobody asked it to. One run, one continuation.
            this.pendingResumeHistory = undefined;
            this.pendingResumeFolded = undefined;
            this.pendingResumeFindingsLedger = undefined;
            this.pendingResumeSkillCursor = undefined;
            this.pendingEvidenceRecovery = undefined;
        }
    }
    /**
     * Continue this agent's own last completed conversation.
     *
     * The one-liner for turn two and after. `run()` is one turn and starts a new
     * conversation each time (see {@link Agent.run}); this reads the
     * conversation off the last completed run, appends `message` as the next
     * user turn, and runs from there — so the model sees what was actually said.
     *
     * Sugar over `run({ message, continueFrom: this.checkpoint() })` and nothing
     * more: one restoration path, so the convenience cannot drift from the
     * mechanism. Reach for `run({ continueFrom })` directly when the
     * conversation comes from somewhere other than this instance's last run — a
     * store, another process, a different machine.
     *
     * Refuses rather than guessing: {@link NoConversationError} when this agent
     * has no completed run to continue (a "follow-up" that quietly became a
     * first turn would be exactly the confusion this door exists to remove),
     * and — through `run()` — {@link PendingQuestionError} when the last run
     * paused to ask a person something, because a pause has its own door:
     * `resume(checkpoint, decision)`.
     *
     * The conversation grows every turn and nothing here trims it; bounding what
     * the model is shown is `.window()` / `.compaction()` / `.memory()`, not a
     * silent cap on the way through.
     *
     * @example
     * ```ts
     * await agent.run({ message: 'Book me a table for two.' });
     * await agent.followUp('Make it three.');
     * await agent.followUp('And move it to 8pm.');
     * ```
     */
    async followUp(message, options) {
        // Refuse BEFORE the timing guards, so "there is nothing to follow up on"
        // is never reported as "a run is in flight" for an agent that has simply
        // not run yet.
        if (this.liveSnapshot() === undefined || (this.lastInputRefused() && !this.hadAdmittedRun)) {
            throw new NoConversationError('Agent.followUp', 'never-run');
        }
        const conversation = this.checkpoint();
        if (conversation === undefined || conversation.history.length === 0) {
            throw new NoConversationError('Agent.followUp', this.lastInputRefused() ? 'last-input-refused' : 'last-run-unfinished');
        }
        return this.run({ message, continueFrom: conversation }, options);
    }
    /**
     * Drop the question this agent's last run paused to ask, on the record.
     *
     * A paused run is waiting on a person. Sending a different message while one
     * is outstanding is refused ({@link PendingQuestionError}) because silently
     * discarding a pending question makes a consent gate something any later
     * message can walk around. When the question really is being dropped —
     * the user changed the subject, the session timed out, the approval is no
     * longer wanted — say so with this, and the next `run()` proceeds.
     *
     * Returns what was dropped (`undefined` when nothing was pending), so a
     * caller can log or audit the abandonment rather than perform it blind. It
     * does not touch the paused run's checkpoint: if you still hold that, it
     * remains resumable.
     */
    abandonPause() {
        const dropped = this.pendingQuestion;
        this.pendingQuestion = undefined;
        return dropped;
    }
    /**
     * Whether {@link Agent.selfExplain}'s why-questions have a run to answer
     * from right now.
     *
     * `false` for two different reasons, both honest: this agent was not built
     * with `.selfExplain()`, or it was and no turn has completed yet (evidence
     * binds at the END of a run, never to the one in flight). Either way there
     * is nothing to explain, which is what a caller routing a why-question needs
     * to know before it routes.
     *
     * The model is told the same thing by the same fact — the trace tools answer
     * "No completed run is available yet" and the skill body says to say so
     * plainly. This is that answer, for the program.
     *
     * Evidence is kept per conversation, so on an agent serving several sessions
     * this answers for the session of the run in flight — or, between runs, of
     * the run served last.
     */
    canExplain() {
        return this.selfExplainBinding?.artifacts !== undefined;
    }
    /**
     * Resume an agent run from a checkpoint produced by a prior
     * `RunCheckpointError`. Unlike `agent.resume()` (which takes a
     * `FlowchartCheckpoint` from an intentional pause), this takes
     * an `AgentRunCheckpoint` (conversation-history snapshot) and
     * replays the agent run with that history restored.
     *
     * The next iteration retries the call that originally failed —
     * with the latest provider state (circuit breaker may have
     * closed, vendor may have recovered, etc.).
     *
     * **Resume = REPLAY from the last completed iteration boundary,
     * not exact-state restore.** Only the conversation history is
     * restored; everything else re-seeds fresh:
     *
     *   - **Tool re-execution / idempotency**: tool side effects from
     *     the FAILED iteration are not in the checkpoint. The model
     *     re-decides from the restored history and may re-issue those
     *     tool calls — they WILL execute again (there is no built-in
     *     toolCallId dedup). Mutating tools (payments, emails, DB
     *     writes) must be idempotent — key on stable call content, not
     *     `ctx.toolCallId` (a re-issued call gets a new id).
     *   - **Fresh `runId`**: the resumed run's events carry a new
     *     `runId`; use `checkpoint.runId` to correlate back to the
     *     failing run.
     *   - **Iteration counter + budget reset**: the resumed run starts
     *     at iteration 1 with a full `maxIterations` budget
     *     (`checkpoint.lastCompletedIteration` is diagnostic only).
     *     Token/cost accumulators also restart at zero.
     *
     * @example
     * ```ts
     * try {
     *   const result = await agent.run({ message: 'long task' });
     * } catch (err) {
     *   if (err instanceof RunCheckpointError) {
     *     await checkpointStore.put(sessionId, err.checkpoint);
     *     // hours / restart later:
     *     const checkpoint = await checkpointStore.get(sessionId);
     *     const result = await agent.resumeOnError(checkpoint);
     *   }
     * }
     * ```
     */
    async resumeOnError(checkpoint, options) {
        const cp = validateCheckpoint(checkpoint);
        // The timing guards run HERE, not only inside `run()`, because the line
        // below writes the side channel: a refusal after that write would leave a
        // restored history armed and the NEXT run would silently continue somebody
        // else's conversation.
        this.assertNotRunning('Agent.resumeOnError');
        this.assertNoPendingQuestion('Agent.resumeOnError');
        // A request `run()` would refuse is refused HERE, before the side channel
        // below is armed — a refusal after it would leave the restored history for
        // the next, unrelated run.
        requestOf(options, 'Agent.resumeOnError');
        // Stash the checkpointed history on the side channel; the seed function
        // reads + clears it before scope.history initializes. No message is
        // appended — the failing run's message is already the last user turn in
        // that history, and adding it again would ask twice.
        this.applyContinuation(cp, 'Agent.resumeOnError', false);
        return this.run({
            message: cp.originalInput.message,
            // The conversation's own identity, unless this call named one. Until
            // 9.2.0 there was no way to pass either, so a recovered run silently
            // re-namespaced its memory under a fresh runId and wrote turn two
            // where turn three could not read it.
            ...(this.identityFor(options, cp) !== undefined && {
                identity: this.identityFor(options, cp),
            }),
        }, options);
    }
    /**
     * Which identity a continued turn runs under: the caller's if they named
     * one, otherwise the conversation's own.
     *
     * @internal
     */
    identityFor(options, cp) {
        return options?.identity ?? cp.identity;
    }
    /**
     * A run's `time`, read into the turn's clock draft (the time layer) — or
     * refused, before the turn starts: `time` on an agent without `.time()` or
     * a malformed value. No zone anywhere (the run's, else the builder's) is no
     * refusal: the zone is UNKNOWN (`zoneSource: 'unknown'`, G15) — never the
     * server's, never guessed; the person is asked it before any time they
     * wrote is read. `undefined` on an agent without the layer.
     *
     * @internal
     */
    clockDraftFor(time, runner) {
        const passed = this.passedTimeFor(time, runner);
        if (this.timeOptions === undefined)
            return undefined;
        return draftClock(passed, this.timeOptions);
    }
    /**
     * The clock draft seed stamps (the time layer): the one `run()` read — or,
     * for a turn this agent's `run()` did not start (its chart mounted in a
     * composition, which passes no `time`), the builder's fallback zone with
     * the turn's start as `now` (`zoneSource: 'builder'`, `nowSource:
     * 'default'` — the same record a `run()` with no `time` files). With no
     * fallback zone that turn's zone is unknown, as a `run()`'s is (G15): the
     * server's zone is never used.
     *
     * @internal
     */
    seedClockDraft() {
        if (this.runClockDraft !== undefined)
            return this.runClockDraft;
        return draftClock(undefined, this.timeOptions ?? {});
    }
    /**
     * A `time` passed to a run or a resume, read — or refused by name: passed to
     * an agent without `.time()` (a door that ignored it would look configured
     * and do nothing), or malformed.
     *
     * @internal
     */
    passedTimeFor(time, runner) {
        if (time === undefined)
            return undefined;
        if (this.timeOptions === undefined) {
            throw new InvalidRunInputError({
                runner,
                received: 'time',
                hint: 'this agent reads no clock — arm the time layer with .time() to pass time',
            });
        }
        const read = readRunTime(time);
        if ('problem' in read) {
            throw new InvalidRunInputError({ runner, received: 'a malformed time', hint: read.problem });
        }
        return read.value;
    }
    /**
     * Install a per-run checkpoint tracker. Listens for the agent's own
     * iteration and stream brackets and snapshots the conversation history into
     * the tracker. Returns a stop function.
     *
     * It listens on the REAL-value path (`EventDispatcher · onRealEvent`), never
     * on the served one: the crash checkpoint it builds is what
     * `resumeOnError` replays to the model, and a resume checkpoint keeps real
     * values — a redaction policy that names `history` must not turn the
     * conversation a crash resumes into placeholders (`src/redaction/`).
     *
     * @internal
     */
    installCheckpointTracker(tracker) {
        return this.dispatcher.onRealEvent((event) => {
            switch (event.type) {
                case 'agentfootprint.agent.iteration_start': {
                    const p = event.payload;
                    if (typeof p?.iterIndex === 'number')
                        tracker.inFlightIteration = p.iterIndex;
                    return;
                }
                // Phase OBSERVATION (8.14.0) — the run's own stream brackets. A crash
                // inside one of these is attributable without asking the error to
                // describe itself. `'call-llm'` is a literal and `toolName` is a name
                // the app declared: no URL, no credential, no payload ever reaches the
                // checkpoint.
                case 'agentfootprint.stream.llm_start':
                    tracker.inFlightPhase = { phase: 'llm', stage: 'call-llm' };
                    return;
                case 'agentfootprint.stream.llm_end':
                    tracker.inFlightPhase = undefined;
                    return;
                // A bracket carrying `notDispatched` (9.113.0) is a call a paused batch
                // never dispatched: nothing of it was ever in flight, so it neither
                // opens nor clears the phase. Nothing observable depends on this today
                // — the settlement emits both halves back to back with no await between
                // them (`toolCalls.ts` · `bracketSettled`), so a crash can never land
                // inside one — it keeps the phase true to the marker should that ever
                // change.
                case 'agentfootprint.stream.tool_start': {
                    const p = event.payload;
                    if (p?.notDispatched !== undefined)
                        return;
                    const name = p?.toolName;
                    tracker.inFlightPhase = {
                        phase: 'tool',
                        stage: typeof name === 'string' ? name : 'tool',
                    };
                    return;
                }
                case 'agentfootprint.stream.tool_end': {
                    const p = event.payload;
                    if (p?.notDispatched !== undefined)
                        return;
                    tracker.inFlightPhase = undefined;
                    return;
                }
                case 'agentfootprint.agent.iteration_end': {
                    const p = event.payload;
                    if (typeof p?.iterIndex === 'number')
                        tracker.lastCompletedIteration = p.iterIndex;
                    if (Array.isArray(p?.history)) {
                        tracker.history = p.history;
                    }
                    tracker.inFlightIteration = undefined;
                    tracker.inFlightPhase = undefined;
                    return;
                }
                default:
                    return;
            }
        });
    }
    /**
     * Continue a run that paused to ask a person something, with their answer.
     *
     * WHO the resumed run is for is the paused run's caller, read off the
     * checkpoint (`agent/callerIdentity.ts`) — never whatever this instance ran
     * last; which SESSION, the call's `sessionId`, else the paused run's. A
     * checkpoint names who it is for; it is not proof: a host that lets
     * checkpoints leave its trust boundary signs them or keeps them
     * server-side, and passes the identity it verified as `options.identity`.
     *
     * @throws ResumeIdentityConflictError when `options.identity` is not the
     *   paused run's caller identity — before anything runs.
     * @throws DecisionRequiredError when a consent gate is answered with
     *   something other than a decision.
     * @throws ResumeDecisionError when the decision does not answer the request
     *   this run is paused on: it names no request, another request, a call the
     *   checkpoint no longer holds (refused before anything runs), or a request
     *   a resume already answered (refused before the tool runs). The tool never
     *   runs on a refusal.
     * @throws RunInFlightError when a run is already in flight.
     *
     * @example
     * ```ts
     * const outcome = await agent.run({ message: 'refund me', identity }, { sessionId });
     * if (isPaused(outcome)) await agent.resume(outcome.checkpoint, 'yes', { identity, sessionId });
     * ```
     */
    async resume(checkpoint, input, options) {
        // Taken up, before anything can refuse: from here every use of a consent
        // decision passes through the one spend a door watching this pause sees
        // (`consentRequest.ts` · `watchDecisionUse`) — so a resume that fails
        // without reaching it, refused or aborted, used nothing.
        noteDecisionTakenUp(pausedRequestIdOf(checkpoint));
        // ── A consent gate is answered with a DECISION (8.13.0) ─────────────
        // Refused HERE, before the engine is handed the checkpoint, so nothing
        // executes and the checkpoint is unchanged — the caller can answer the same
        // one and resume again. Before this, a mis-shaped resume silently DECLINED
        // and filed `by: 'unknown'`: a consent record naming a person who was never
        // asked. Governance never silently invents a decision, for the same reason
        // it never silently drops one.
        //
        // Discriminated by the PAUSE, never by the input: a plain askHuman/pauseHere
        // answer is a value (often a string) and must stay accepted, so the only
        // sound question is "what was asked?".
        assertIdentityShape(options?.identity, 'Agent.resume');
        if (options?.redact !== undefined)
            assertRedactionPolicy(options.redact, 'Agent.resume');
        // The incoming request is the RESUMING caller's — never the paused one's:
        // a checkpoint carries no request, and a resume without one serves none.
        const resumeRequest = requestOf(options, 'Agent.resume');
        // Native guard lands with the new checkpoint protocol, before its runtime
        // opt-in is mounted. Until then no Agent is armed to consume this carrier.
        requireVerificationOnResume(checkpoint.sharedState ?? {}, false);
        if (this.negativeClaimsConfig !== undefined) {
            const prior = negativeCheckpointOf(checkpoint.sharedState ?? {});
            if (prior === undefined)
                throw new TypeError('Negative-claims pause checkpoint is missing its private evidence carrier.');
        }
        else if (negativeCheckpointOf(checkpoint.sharedState ?? {}) !== undefined) {
            throw new TypeError('Resume requires the negative-claims setting used by the paused agent.');
        }
        // The policy the paused run was covered by, carried in its own state
        // (`runRedaction`) — it covers this leg too, joined with any the caller
        // adds now. Read, or refused, before anything moves (`resumeRedactionOf`).
        const carriedRedaction = resumeRedactionOf(checkpoint, [
            redactionDeclaredBy(this),
            options?.redact,
            conversationRedaction(),
        ]);
        // A resume's `time` (the time layer) is read or refused before anything
        // moves; it is never applied — the paused turn's clock is kept, and a
        // differing value is recorded by the ToolCalls resume door.
        const passedTime = this.passedTimeFor(options?.time, 'Agent.resume');
        options = withoutTime(options);
        // One run, one identity — refused before anything moves, and before the
        // answer is read at all: a caller this run is not for learns nothing about
        // what it is waiting on (its gate, its tool, its request). The paused run's
        // caller identity is read off the checkpoint (`callerIdentity.ts`); a call
        // naming a DIFFERENT one would give the run one person's memory namespace
        // and credentials (restored, never re-seeded) and another's `ctx.identity`,
        // principal and stored ownership. A checkpoint names who it is for; it is
        // not proof — a host that lets checkpoints leave its trust boundary signs
        // them or keeps them server-side, and passes the identity it verified here.
        const pausedFor = callerIdentityOf(checkpoint.sharedState, {
            ...(this.lastRunIdentity !== undefined && { named: this.lastRunIdentity }),
            minted: this.mintedRunIds,
        });
        // The identity the run's memory namespace and credentials are restored
        // from, WHATEVER its source (a caller's, the session rung, the per-run
        // default). One run, one identity: a call that names an identity must
        // name exactly this one — an ownerless (derived) pause resumed by a named
        // person is refused too, and so is a checkpoint whose own fields disagree
        // (a session-rung marker on an identity that carries a person).
        const restored = restoredIdentityOf(checkpoint.sharedState);
        if (restored === 'malformed' ||
            (options?.identity !== undefined &&
                (restored === undefined || !sameIdentity(options.identity, restored)))) {
            throw new ResumeIdentityConflictError();
        }
        // …and the decision must be for THIS request and this exact call
        // (`core/consentRequest.ts`): refused here, before anything moves, when it
        // names no request, another request, a call the checkpoint no longer
        // holds, or — for an ask — rules this agent's chain no longer runs. The
        // dispatch loop asks again where the decision is used — and spends it
        // there, once.
        const gate = pauseDemandsDecision(checkpoint.pauseData);
        if (gate) {
            checkDecisionAgainst(checkpoint, gate, input, this.toolMiddleware.map((link) => link.name), 
            // A request minted under a ledger that can prove is spent only there.
            this.decisionLedger ?? processDecisionLedger());
        }
        // Typed data collection is not consent. Validate before changing any run state.
        const awaitingInput = gate === undefined ? readAwaitingInput(checkpoint.pauseData) : undefined;
        let answeredAsk;
        if (awaitingInput !== undefined) {
            this.assertNotRunning('Agent.resume');
            const answered = applyInputResponse(awaitingInput, input, this.timeAnswerContextOf(checkpoint.sharedState));
            if ('cancel' in answered) {
                throw new TypeError('[input request] Cancel a hosted request through its host, or abandonPause() before starting another run.');
            }
            if (answered.missing.length > 0) {
                const copy = structuredClone(checkpoint);
                const pauseData = {
                    ...copy.pauseData,
                    awaitingInput: answered,
                };
                const outcome = {
                    paused: true,
                    checkpoint: { ...copy, pauseData },
                    pauseData,
                    awaitingInput: answered,
                };
                this.recordPendingQuestion(outcome);
                return outcome;
            }
            answeredAsk = {
                id: answered.id,
                requestId: answered.requestId,
                count: (awaitingInput.repeat?.count ?? 0) + 1,
            };
            input = {
                status: 'input_received',
                requestId: answered.requestId,
                values: answered.supplied,
                origins: answered.origins,
                origin: answered.origin,
                ...(answered.context !== undefined && { context: answered.context }),
            };
        }
        // And the answer must be about the thing that was asked. Checked HERE, at
        // the same door and before any state moves, because a resume that has begun
        // is a resume that has already used the value.
        assertDecisionIsNotStale(checkpoint.pauseData, input);
        // The same one-turn-at-a-time guard `run()` carries: a resume writes the
        // same per-instance state a run does. Answering the question is what this
        // door is FOR, so it never checks `pendingQuestion` — it clears it.
        this.assertNotRunning('Agent.resume');
        // Settled the moment the answer is handed over, not when the resumed run
        // finishes: a resume that then FAILS must not leave the agent refusing
        // every later message on behalf of a question that has been answered.
        // Kept to put back when the run refuses the answer itself (a replay of a
        // request already answered): that resume answered nothing.
        const questionBefore = this.pendingQuestion;
        this.pendingQuestion = undefined;
        // WHO this resumed run is for — set BEFORE the executor exists, because the
        // run context's principal (every event's `EventMeta.principal`), a tool's
        // `ctx.identity` and `checkpoint()` all read it: the identity the PAUSED
        // run's caller named (a named `options.identity` was checked equal to the
        // restored one at the top of this method) — never the identity of whatever
        // this instance ran last, which on a shared or pooled agent is another
        // person's, or nobody's (`callerIdentity.ts`).
        this.lastRunIdentity = options?.identity ?? pausedFor;
        // …and WHICH session: the call's, else the one the paused run recorded in
        // its own state (`runSessionId`, or the session rung's) — one source, no
        // instance memory — so a host that resumes without repeating the session
        // does not file the resumed turn (its tool teardown, its events, its
        // self-explain evidence) as sessionless.
        const sessionId = options?.sessionId ?? pausedSessionOf(checkpoint.sharedState);
        const sessionResumeOptions = sessionId !== undefined && options?.sessionId === undefined
            ? { ...options, sessionId }
            : options;
        // The leg's policy — the carried one, the caller's, and the paused leg's
        // marks — comes back from the emitter and goes to THIS leg's executor
        // (`createExecutor` → `openRunRedaction`); nothing per-run is held on the instance.
        const resumeLeg = this.emitPauseResume(checkpoint, input, unionRedactionPolicies(carriedRedaction, sessionResumeOptions?.redact));
        const resumeOptions = resumeLeg === sessionResumeOptions?.redact
            ? sessionResumeOptions
            : { ...sessionResumeOptions, redact: resumeLeg };
        // Fresh executor — footprintjs 4.17.0+ seeds the runtime from
        // `checkpoint.sharedState` (and nested subflow states) automatically
        // on a fresh executor's `resume()`. No need to retain a paused
        // executor between run/resume.
        // Recorded on the same terms a fresh run is (9.26.0) — and the recording
        // covers the RESUMED run, which is what the recorder saw. A turn that
        // paused and resumed is two runs, and each mints its own recording when
        // (and only when) it completes.
        const recording = this.startRunRecording();
        this.answeredAsk = answeredAsk;
        const executor = this.createExecutor(resumeOptions, resumeRequest);
        // The leg's WHOLE policy rides its own state too (`runRedaction`), so a
        // later pause carries what this leg was covered by — names the resume
        // added included — and a third leg is never less covered than this one.
        // `createExecutor` opened this executor's run; one it did not is refused
        // before anything is held or runs — a leg whose policy nothing knows must
        // not start.
        const legCoverage = coverageOfExecutor(executor);
        if (legCoverage.state === 'unknown')
            throw new ResumeRedactionError('unreadable');
        const leg = { policy: policyOfCoverage(legCoverage) };
        this.inFlightRunId = this.currentRunContext.runId;
        // A resumed turn is two runs, and each keeps its own ledger — exactly
        // the recording's terms one comment up.
        this.beginIntegrityLedger();
        this.lastRunAnswer = undefined;
        this.checkpointTracker = {
            runId: this.currentRunContext.runId,
            originalInput: { message: '' },
            history: [],
            lastCompletedIteration: 0,
        };
        // One run can never raise on another run's consent block.
        this.consentOutstanding.clear();
        // Beside the `try` whose `finally` clears it (the fresh-run path's terms).
        this.resumePassedTime = passedTime;
        const legCheckpoint = leg.policy === undefined
            ? checkpoint
            : {
                ...checkpoint,
                sharedState: {
                    ...checkpoint.sharedState,
                    runRedaction: carriedRunPolicy(leg.policy),
                },
            };
        // A resume arms its own run signal and deadline (`runSignalOf`), released in
        // the `finally` below.
        const runSignal = runSignalOf(resumeOptions);
        try {
            const result = await executor.resume(legCheckpoint, input, runSignal.options);
            const finalized = this.finalizeResult(executor, result);
            if (typeof finalized === 'string')
                this.lastRunAnswer = finalized;
            // The question this resume answered is settled; a resume that paused
            // AGAIN has asked a new one, and that one is outstanding from here.
            this.recordPendingQuestion(finalized);
            await this.endRunToolSessions(finalized);
            // Same liveness gate the fresh-run path applies (9.60.0).
            this.assertIntegrityAlive();
            await this.fileRunRecording(recording, finalized);
            return finalized;
        }
        catch (cause) {
            // An answer the run refused ran nothing and answered nothing: the
            // question this agent was waiting on (if any) is still outstanding, and
            // no tool session ended — a replayed decision must not tear down the
            // sandbox a live pause is holding.
            if (cause instanceof ResumeDecisionError || cause instanceof DecisionRequiredError) {
                this.pendingQuestion = questionBefore;
                throw cause;
            }
            await this.endRunToolSessions(cause);
            throw cause;
        }
        finally {
            runSignal.release();
            // A resume's passed `time` ends with the resume (the fresh-run path's terms).
            this.resumePassedTime = undefined;
            // Same terms as the fresh-run path: rows on every exit, before the
            // recording stops (9.60.0).
            this.fileIntegrityDisposition();
            this.noteAdmittedRun();
            recording?.stop();
            this.inFlightRunId = undefined;
        }
    }
    /**
     * Fire `'run'`-scoped tool teardown — IF this run really ended.
     *
     * **Not on `finally`, and that is the whole point.** `finally` runs on every
     * exit including a pause, and a pause exits TWO ways: a returned
     * `RunnerPauseOutcome` and a thrown `PauseSignal`. A check-in on a
     * code-interpreter call pauses the run so a person can approve the code —
     * tearing the sandbox down there destroys the exact state the resume needs,
     * and it fails QUIETLY, as a resumed run that "just re-ran everything".
     * Both shapes are discriminated here and both are skipped.
     *
     * An error IS a terminal: the run is over, nobody is coming back, and a
     * sandbox held by a run that crashed is the clearest kind of leak. Only a
     * pause survives.
     *
     * Fired for the TURN, not for `currentRunContext.runId` — `resume()` mints a
     * fresh run id, so a pause and its resume are one turn across two runs, and
     * filtering on the id would leave everything a paused turn opened alive
     * forever. Hosted turns are scoped to their session, preserving other pauses. See `ToolSessionTier.fireRun`.
     *
     * @param outcome what `run()`/`resume()` is about to return, or about to throw.
     */
    async endRunToolSessions(outcome) {
        if (!this.toolSessionTier)
            return;
        if (isPaused(outcome))
            return;
        if (outcome instanceof Error && outcome.name === 'PauseSignal')
            return;
        if (this.currentRunContext.sessionId !== undefined)
            await this.toolSessionTier.fireSessionRuns(this.currentRunContext.sessionId);
        else
            await this.toolSessionTier.fireRun();
    }
    /**
     * The conversation this agent's LAST completed run leaves behind, packed as
     * the same `AgentRunCheckpoint` that `resumeOnError(...)` accepts. Store it,
     * hand it back next turn, and the agent continues where it left off — across
     * a restart, a deploy, or a different machine.
     *
     * Returns `undefined` before any run has completed or when the latest run
     * was refused by input middleware. A refused input is not an admitted
     * conversation. Keep an earlier accepted checkpoint to continue explicitly;
     * `followUp()` after input denial raises `NoConversationError`.
     *
     * **Read from the run's own committed state, not from a second copy.** The
     * history comes from the LIVE snapshot's `sharedState.history` — the state
     * the run actually committed, never the served record (an agent's `redact`
     * keeps the conversation out of `getLastSnapshot()`, never out of the
     * conversation it continues) — cloned on the way out so a persistence layer
     * can never mutate the live heap. The final assistant turn is appended from the answer
     * `run()` returned, because nothing ever writes it back into `history`: the
     * loop appends assistant turns only when they carry tool calls, and the turn
     * that ends the run carries none. An agent that stored this conversation
     * without that append would drop its own reply every turn and answer the next
     * one having forgotten what it just said.
     * Provider replay evidence and its normalized thinking travel with that
     * accepted answer, including a terminal proof with empty text. The answer
     * remains the delivered text, including output-policy edits; raw provider
     * text is never substituted back. Evidence comes from live state, not the
     * redacted observation. Denied, failed and paused runs append no final turn.
     *
     * Explicit thinking retirements recorded before transport also apply here:
     * a failed grouped call cannot restore reasoning omitted from its request.
     * This copies no other failed-stage state or tool effects into the history.
     * Adds no events, no scope writes and no capture: every recording is
     * byte-identical to an agent that never calls this.
     *
     * After a run that **paused**, this is the conversation as of the pause, with
     * no answer appended — a pause is unfinished work, and pause/resume has its
     * own carrier (`FlowchartCheckpoint`) that holds engine state this shape
     * cannot.
     *
     * The conversation grows every turn and nothing here trims it. Bounding what
     * the model is shown is the memory subsystem's job (`.memory(...)`), not a
     * silent cap applied on the way to storage.
     *
     * @example
     * ```ts
     * await agent.run({ message: 'Book me a table for two.' });
     * const conversation = agent.checkpoint();          // persist anywhere
     * // …a restart later, on a fresh Agent:
     * await agent.run({ message: 'Make it three.', continueFrom: conversation });
     * // `continueFrom` admits the new turn through the 'input' middleware chain.
     * // `resumeOnError` rechecks originalInput but replays stored history as-is;
     * // it does not rewrite entries manually appended to that history.
     * ```
     */
    checkpoint() {
        const snapshot = this.liveSnapshot();
        if (!snapshot)
            return undefined;
        const state = snapshot.sharedState;
        // Admission is decided by seed, before any conversation state is written.
        // Do not manufacture a blank replay carrier for an unadmitted attempt.
        if (this.lastInputRefused())
            return undefined;
        const recorded = (state?.history ?? []);
        const history = [...recorded];
        if (this.lastRunAnswer !== undefined &&
            (this.lastRunAnswer.length > 0 || state?.llmLatestReplay !== undefined)) {
            history.push({
                role: 'assistant',
                content: this.lastRunAnswer,
                ...assistantReplayFields(state ?? {}),
            });
        }
        const folded = this.foldedSpansOf(state);
        const owner = this.conversationOwner();
        const skillCursor = this.continuityCursorOf(state);
        const evidenceRecovery = this.evidenceRecoveryOf(state);
        const findingsLedger = this.findingsLedgerOf(state);
        const turnNumber = this.turnNumberOf(state);
        return {
            version: 1,
            runId: this.currentRunContext.runId,
            history: structuredClone(checkpointReplayHistory(history, this.checkpointTracker)),
            lastCompletedIteration: typeof state?.iteration === 'number' ? state.iteration : 0,
            originalInput: { message: typeof state?.userMessage === 'string' ? state.userMessage : '' },
            checkpointedAt: Date.now(),
            // Absent when nothing ever folded — a key that is always there and
            // usually empty reads like "no folds were retained", which is a
            // different claim from "there were no folds".
            ...(folded !== undefined && { folded }),
            // Who it was for and who ran it (9.2.0) — both absent unless chosen.
            ...(owner.identity !== undefined && { identity: owner.identity }),
            ...(owner.agentId !== undefined && { agent: { id: owner.agentId } }),
            // Where the graph stood (SG-C) — written ONLY under
            // `continuity: 'conversation'`, so every other checkpoint keeps its
            // exact byte shape.
            ...(skillCursor !== undefined && { skillCursor }),
            ...(evidenceRecovery !== undefined && { evidenceRecovery }),
            // The model's standings (9.101.0) — absent unless the run recorded any,
            // by the `folded` rule: an optional key, never a format change.
            ...(findingsLedger !== undefined && { findingsLedger }),
            // The turn this history ends on (honesty layers) — absent unless a
            // layer is armed, so every other checkpoint keeps its byte shape.
            ...(turnNumber !== undefined && { turnNumber }),
            ...(this.negativeClaimsConfig === undefined || state === undefined
                ? {}
                : negativeCheckpointOf(state)),
        };
    }
    /** Called as each run/resume ends: remember a run that got past input admission. */
    noteAdmittedRun() {
        if (this.liveSnapshot() !== undefined && !this.lastInputRefused()) {
            this.hadAdmittedRun = true;
        }
    }
    /**
     * Whether the evidence gate's revision calls no tools: a posture that can
     * revise (`'guard'`, `'rails'`) under an ENFORCING answer validation. There
     * the answer may yet be withheld, so a revision must not run a tool that
     * writes or sends again — and the library cannot tell one that only reads.
     */
    revisionWithholdsTools() {
        return (this.typedNegativeClaimsConfig !== undefined ||
            this.evidenceGate?.toolsOffAfterRevision === true);
    }
    /** Whether input middleware refused the last run's message — the one reading both doors ask. */
    lastInputRefused() {
        const state = this.liveSnapshot()?.sharedState;
        return state?.messageDeniedPhase === 'input';
    }
    /** Both checkpoint doors keep the repair budget separately from conversation text. */
    evidenceRecoveryOf(state) {
        if (state?.evidenceRevisionSpent !== true)
            return undefined;
        const pending = state.evidenceRecovery;
        return {
            revisionSpent: true,
            ...(pending !== undefined &&
                state.evidenceRecoveryUsed !== true && {
                pending: {
                    instruction: pending.instruction,
                    ...(pending.conclusion !== undefined && { conclusion: pending.conclusion }),
                },
            }),
        };
    }
    /**
     * The graph cursor a conversation carrier stores — the run's final
     * committed `currentSkillId`, read from the SNAPSHOT (the state the run
     * actually committed; both chart shapes map the advanced cursor onto it
     * every iteration), and only when the mounted graph declared
     * `continuity: 'conversation'`.
     *
     * ONE reader for BOTH carriers — `checkpoint()` (the conversation door
     * `followUp()` walks through) and the crash checkpoint
     * `RunCheckpointError` carries — the `foldedSpansOf`/`conversationOwner`
     * rule: a conversation must not keep its place on one path and silently
     * lose it on the other.
     *
     * @internal
     */
    continuityCursorOf(state) {
        if (this.skillGraphCascade?.continuity !== 'conversation')
            return undefined;
        const cursor = state?.currentSkillId;
        return typeof cursor === 'string' && cursor.length > 0 ? cursor : undefined;
    }
    /**
     * The folded spans this run has committed, cloned on the way out.
     *
     * One reader for both carriers — `checkpoint()` and the crash checkpoint
     * `RunCheckpointError` carries — so a conversation cannot keep its spans on
     * one path and silently lose them on the other. The clone is the same
     * promise `checkpoint()` makes about history: a persistence layer never gets
     * a reference into the live heap.
     *
     * @internal
     */
    foldedSpansOf(state) {
        const spans = state?.foldedSpans;
        if (spans === undefined || spans.length === 0)
            return undefined;
        return structuredClone(spans);
    }
    /**
     * What the resume door checks a time field's answer with (the time design
     * § 6.2, step T4): the paused turn's clock zone — the kept `clock` row, so
     * a resume that passes another `time` changes nothing — and the app's
     * catalog overrides. `undefined` without `.time()`: a `format` field is
     * then judged for shape, order and offset only, in the default words.
     *
     * @internal
     */
    timeAnswerContextOf(state) {
        if (this.timeOptions === undefined)
            return undefined;
        const ledger = state?.findingsLedger;
        const clock = clockOf(Array.isArray(ledger) ? ledger : undefined);
        // An unknown zone (G15) judges no wall time: `zone` is then only the UTC spelling.
        const zone = clock?.zoneSource === 'unknown' ? undefined : clock?.zone;
        const messages = this.timeOptions.messages;
        return {
            ...(zone !== undefined && { zone }),
            ...(messages !== undefined && { messages }),
        };
    }
    /**
     * The findings ledger for a checkpoint (9.101.0) — `foldedSpansOf`'s twin:
     * one reader for `checkpoint()` and the crash carrier, `undefined` when the
     * run recorded no rows (so the key stays absent), and a detached copy
     * otherwise, never a reference into the live heap.
     *
     * @internal
     */
    findingsLedgerOf(state) {
        const ledger = state?.findingsLedger;
        if (ledger === undefined || ledger.length === 0)
            return undefined;
        return structuredClone(ledger);
    }
    /**
     * The turn a checkpoint's history ends on — `findingsLedgerOf`'s twin, one
     * reader for `checkpoint()` and the crash carrier. Only while an honesty
     * layer is armed (the run constant `honestyLayers`, read from the same
     * snapshot): that is when every ledger row carries its `turn` and the next
     * run's stamp must continue from it (`AgentRunCheckpoint.turnNumber`).
     * `undefined` otherwise, so the key stays absent.
     *
     * @internal
     */
    turnNumberOf(state) {
        if (state?.honestyLayers === undefined && this.negativeClaimsConfig === undefined)
            return undefined;
        const turn = state?.turnNumber;
        return typeof turn === 'number' && Number.isSafeInteger(turn) && turn >= 1 ? turn : undefined;
    }
    /**
     * The two owner facts every conversation carrier stamps — who the run was
     * for, and which agent ran it (9.2.0).
     *
     * One reader for `checkpoint()` and the crash checkpoint, the same rule
     * `foldedSpansOf` follows: a fact kept on one carrier and lost on the other
     * is worse than a fact kept on neither. Both are absent unless the caller
     * chose them, which is what keeps the fingerprint refusal narrow and the
     * default `conversationId` out of storage.
     *
     * @internal
     */
    conversationOwner() {
        return {
            ...(this.lastRunIdentity !== undefined && { identity: this.lastRunIdentity }),
            ...(this.explicitId !== undefined && { agentId: this.explicitId }),
        };
    }
    /**
     * Restore a stored conversation onto the side channel `seed` reads.
     *
     * THE one restoration path — `run({ continueFrom })` and `resumeOnError()`
     * both come through here, so the conversation door and the error door cannot
     * disagree about what continuing means. It checks the agent fingerprint,
     * restores history + folded spans, and adopts the conversation's identity so
     * the continued turn writes its memory where the earlier turns are.
     *
     * `appendsUserTurn` is the difference between the two callers, and it is the
     * whole difference. Continuing a conversation ADDS this turn's user message
     * to the stored history; resuming after an error does NOT, because there the
     * message is already the last user turn in that history and appending it
     * would ask the same question twice.
     *
     * Only the FLAG is stashed, never the entry (9.112.2). The entry is written
     * by `seed.ts · historyForTurn` from the message the `'input'` middleware
     * chain let through; an entry built here, before that chain has run, is the
     * caller's raw text — which is what a continued turn served the model
     * through 9.112.1, while the record said the rewrite had happened.
     *
     * @internal
     */
    applyContinuation(cp, door, appendsUserTurn) {
        assertContinuable(cp, this.explicitId, door);
        requireVerificationOnResume(cp, false);
        if (this.negativeClaimsConfig === undefined && negativeCheckpointOf(cp) !== undefined)
            throw new TypeError('Continuation requires the negative-claims setting used by the recording agent.');
        this.pendingResumeHistory = {
            history: cp.history,
            appendsUserTurn,
            // The turn the stored history ends on (honesty layers) — seed continues
            // the turn stamp from it only while a layer is armed.
            ...(cp.turnNumber !== undefined && { turn: cp.turnNumber }),
            ...(this.negativeClaimsConfig === undefined ? {} : { negatives: negativeCheckpointOf(cp) }),
        };
        // The folded spans beside it. A conversation stored before 8.2 has none,
        // and `undefined` is the right answer there — it means "this conversation
        // recorded no folds", which is exactly true.
        this.pendingResumeFolded = cp.folded;
        // The findings ledger beside it (9.101.0). Stashed whether or not THIS
        // agent is armed: seed restores it only under `.findings()` (the deps
        // gate), and an unarmed continuation consumes and ignores it — exactly
        // like a `folded` field on an agent that never folds.
        this.pendingResumeFindingsLedger = cp.findingsLedger;
        // The conversation's skill cursor (SG-C). Stashed unconditionally —
        // whether it is HONORED is seed's `restoreSkillCursor` gate, which reads
        // the mounted graph's `continuity` declaration; a checkpoint written by a
        // continuity graph and continued on a `'turn'` one is consumed and
        // ignored, exactly like a `folded` field on an agent that never folds.
        this.pendingResumeSkillCursor = cp.skillCursor;
        // Same failed request retains its single repair budget. A new user turn
        // receives neither the rejected draft nor a spent retry allowance.
        this.pendingEvidenceRecovery =
            !appendsUserTurn && cp.evidenceRecovery !== undefined
                ? structuredClone(cp.evidenceRecovery)
                : undefined;
    }
    /** One turn at a time — see `RunInFlightError`. @internal */
    assertNotRunning(door) {
        if (this.inFlightRunId !== undefined) {
            throw new RunInFlightError(door, this.id, this.inFlightRunId);
        }
    }
    /** A person's unanswered question outranks a new message — see
     *  `PendingQuestionError`. @internal */
    assertNoPendingQuestion(door) {
        if (this.pendingQuestion !== undefined) {
            throw new PendingQuestionError(door, this.pendingQuestion);
        }
    }
    /**
     * Remember (or forget) the question this run ended on.
     *
     * A paused outcome sets it; anything else clears it, because a run that
     * reached an answer has no outstanding question by definition. Reads the
     * same `pauseData` fields `standingAgent.describePause` reads — the tool
     * name and question the dispatch loop stamped — and invents nothing.
     *
     * @internal
     */
    recordPendingQuestion(outcome) {
        if (typeof outcome === 'string') {
            this.pendingQuestion = undefined;
            return;
        }
        const data = outcome.pauseData;
        this.pendingQuestion = {
            ...(typeof data?.toolName === 'string' && { toolName: data.toolName }),
            ...(typeof data?.toolCallId === 'string' && { toolCallId: data.toolCallId }),
            ...(typeof data?.question === 'string' && { question: data.question }),
        };
    }
    /**
     * Hand the `.selfExplain()` binding to the agent that owns it.
     *
     * Called once by `AgentBuilder.build()`, immediately after `bindTo`. The
     * binding stays the tool provider's to read; the Agent holds it only so
     * `canExplain()` answers from the same fact the trace tools answer from,
     * rather than from a second guess about whether a run has completed.
     *
     * @internal
     */
    bindSelfExplain(binding) {
        this.selfExplainBinding = binding;
    }
    /**
     * Refuse, at run start, any declared messages-slot role this provider
     * cannot carry inside its message list (7.21, D2).
     *
     * Run start rather than build time because the answer depends on the
     * provider — and a decorated provider (`withFallback`, `withRetry`) is only
     * the thing it is once composed. Run start rather than delivery time
     * because a declaration that can never work should fail on the first call,
     * not three iterations into a paid run. The delivery stage re-checks each
     * message anyway, which is what catches roles that only exist at run time
     * (a hand-built memory-recall subflow's formatted output).
     *
     * Called from the ONE place `run()` and `resume()` share, so no entry point
     * can slip past it.
     */
    /**
     * The OPEN skills — the ones `read_skill` may reach from anywhere, whatever the
     * graph's cursor says (8.4.0). Two clauses, both load-bearing:
     *
     *   • `trigger.kind === 'llm-activated'` — the trigger that reads
     *     `activatedInjectionIds`, which is the ONLY thing a `read_skill` call writes.
     *     It is exactly "read_skill can really activate this", so admitting anything
     *     else (a hand-built `rule` injection, say) would replace one lie with another:
     *     the tool would answer "activated" and nothing would activate.
     *   • the graph declares no incoming edge to it — a bare model edge `.route(a, m)`
     *     is a declared, drawn, `from`-gated affordance ("from a, the model may hop to
     *     m"), and opening every such target would silently globalize it. What is left
     *     is skills the graph never mentions at all.
     *
     * That covers three shapes that were all dead before: `.selfExplain()`'s debug
     * skill under a graph, a `.skill()`/`.skills()` registration beside a graph, and a
     * skill listed in `skills[]` and wired to nothing (whose own check-up warning says
     * "it can only be reached by the model via read_skill" — true again now).
     *
     * An open pick ACTIVATES but never moves the cursor — see the tool-calls gate.
     * Computed once per chart build; the injection list is fixed at construction.
     */
    /**
     * The identity facts this run hands `tool.execute` (9.7.0).
     *
     * Read through an ACCESSOR from the chart (see `ToolCallsHandlerDeps.currentRun`)
     * because the chart is built once and this changes every run.
     *
     * `identity` is `lastRunIdentity` — what the CALLER passed — and deliberately
     * NOT `scope.runIdentity`, which is always populated and defaults to
     * `{ conversationId: '<runId>' }` (or, on a session-bound run since 9.10.0,
     * to `{ conversationId: sessionId }`). Handing a tool a synthesized
     * conversation as "the identity" would let it key an isolated session on a
     * fiction, and would make "absent" unrepresentable at exactly the layer that
     * most needs to see it. The session-derived namespace is synthesized too, and
     * is withheld here for that reason — `sessionId` beside it is the fact the
     * transport really delivered.
     */
    toolRunFacts() {
        return {
            runContext: { ...this.currentRunContext },
            runId: this.currentRunContext.runId,
            ...(this.currentRunContext.sessionId !== undefined && {
                sessionId: this.currentRunContext.sessionId,
            }),
            ...(this.lastRunIdentity !== undefined && { identity: this.lastRunIdentity }),
        };
    }
    /**
     * The teardown tier, built on FIRST registration.
     *
     * An agent whose tools never hold a session never allocates one, and its
     * terminals stay a single `undefined` check.
     */
    toolSessions() {
        if (!this.toolSessionTier) {
            this.toolSessionTier = new ToolSessionTier({
                timeoutMs: this.toolTeardownTimeoutMs,
                report: (report, origin) => this.emitToolSessionReport(report, origin),
            });
        }
        return this.toolSessionTier;
    }
    /**
     * Turn one TEARDOWN report into a typed `agentfootprint.tools.session_*` event.
     *
     * Only the two closing events come through here. A start and a reuse happen
     * inside `tool.execute`, where the dispatch loop still holds the scope, so
     * those ride the ordinary emit channel and carry the stage they really
     * happened in. These two fire after the run's last stage committed, and this
     * is the one place that has to answer "from where?" without a stage to point
     * at.
     *
     * **Built with `buildEventMeta`, never `minimalMeta()`.** `minimalMeta()`
     * hardcodes `runId: 'consumer-scope'`, and a teardown event stamped that way
     * cannot be joined to the run that OPENED the session — the exact
     * unjoinability 9.4.0 spent a release fixing for credential events. So the
     * meta comes from the registration-time run context, with a STATED pseudo-stage, the same
     * move as the `'<stageId>#paused'` stamp at the pause boundary.
     */
    emitLateFact(type, payload, runContext) {
        const dispatcher = this.getDispatcher();
        if (!dispatcher.hasListenersFor(type))
            return;
        dispatcher.dispatch({
            type,
            payload,
            meta: buildEventMeta({ runtimeStageId: LATE_ARTIFACT_FACT_STAGE_ID }, runContext),
        });
    }
    emitToolSessionReport(report, origin) {
        const type = report.kind === 'closed'
            ? 'agentfootprint.tools.session_closed'
            : 'agentfootprint.tools.session_close_failed';
        const dispatcher = this.getDispatcher();
        if (!dispatcher.hasListenersFor(type))
            return;
        const { kind: _kind, ...payload } = report;
        dispatcher.dispatch({
            type,
            payload,
            meta: buildEventMeta({ runtimeStageId: TOOL_TEARDOWN_STAGE_ID }, origin.runContext ?? this.currentRunContext),
        });
    }
    openSkillIds() {
        return this.injections
            .filter((i) => i.flavor === 'skill' &&
            i.trigger.kind === 'llm-activated' &&
            !this.skillGraphEdgeTargets.has(i.id))
            .map((i) => i.id);
    }
    // FOLD (resolver) · assembles the ReadSkillOffer the read_skill DESCRIPTION (a Lens) is built from:
    // grantable ← makeReachableSkills ∪ openSkillIds, cursor ← scope, hidden ← hiddenSkillIdsNow.
    // It assembles; it never composes a sentence. The sentence is skillToolDescriptors.describeOffer.
    /**
     * The per-iteration `read_skill` offer builder — or `undefined` to leave the tool
     * exactly as it has always been (8.5.0).
     *
     * `read_skill` enumerated every registered skill while the gate admitted only
     * `reachableSkills(cursor) ∪ open`, so under a graph the model was handed ids it
     * would be refused, every iteration, and could spend a whole run re-asking. The
     * OFFER is rebuilt here from the same two functions the gate itself calls — one
     * source of truth, so the menu cannot drift from the verdict.
     *
     * Two guards:
     *
     *   • no graph AND no per-role skill visibility → `undefined`. A plain
     *     `read_skill` agent has no cursor and no gate; every registered skill
     *     really is reachable, and the tool keeps its byte-identical description.
     *   • `reactMode: 'classic'` → `undefined` for the GRAPH menu, plus a dev-mode
     *     warning. Classic composes the tools slot on turn 1 ONLY (see the Context
     *     selector's `includeStatic`), so a cursor-scoped menu would freeze at the
     *     cold-start cursor and keep advertising it for the rest of the run — a
     *     worse lie than the honest full catalog. `.selfExplain()` refuses under
     *     classic for exactly this caching reason; here the full catalog is a
     *     correct fallback, so this warns instead of refusing.
     *
     * Per-role VISIBILITY (9.11.0) survives classic, and that is not an
     * inconsistency: a cursor moves every iteration, but who is asking does not
     * change inside one run. A filter computed on turn 1 is still exactly right
     * on turn 9.
     */
    readSkillOfferFor() {
        const skills = this.injections.filter((i) => i.flavor === 'skill');
        if (skills.length === 0)
            return undefined;
        const filtersByRole = this.governsSkillVisibility();
        if (!this.skillGraphReachable && !filtersByRole)
            return undefined;
        let graphMenu = this.skillGraphReachable;
        if (graphMenu && this.reactMode === 'classic') {
            if (isDevMode()) {
                // eslint-disable-next-line no-console
                console.warn("agentfootprint Agent: read_skill's menu is built at turn 1 under " +
                    "reactMode: 'classic', which caches the tools slot — so it lists every " +
                    'registered skill instead of the ones reachable from the cursor, and the ' +
                    "model will be offered ids the graph will refuse. Use the default 'dynamic' " +
                    "mode (or 'dynamic-grouped') for a cursor-tracking menu.");
            }
            graphMenu = undefined;
            if (!filtersByRole)
                return undefined;
        }
        const open = this.openSkillIds();
        const reachable = graphMenu;
        // A decision `tree()` keeps no cursor, so every routing pick is refused by
        // construction (9.86.0). The descriptor withholds the tool entirely when
        // nothing is left to open, and explains the tree when something is.
        const treeRouted = reachable !== undefined && this.skillGraphIsTree;
        return (args) => {
            const grantable = reachable
                ? [...new Set([...reachable(args.currentSkillId), ...open])]
                : undefined;
            // `undefined` when the offer is WITHHELD — the tools slot then drops the
            // schema from the request. (`skills` is non-empty, so an empty catalog is
            // not one of the ways this can be undefined.)
            return buildReadSkillTool(skills, {
                ...(grantable !== undefined && { grantable }),
                ...(args.hiddenSkillIds !== undefined && { hiddenIds: args.hiddenSkillIds }),
                // WHERE THE MODEL STANDS (9.84.0) — unconditional, unlike the menu below.
                // The description names it, and stops listing it as unreachable.
                ...(args.currentSkillId !== undefined && { cursorId: args.currentSkillId }),
                // The turn-start menu (SG-C) — the tools slot passes it only while the
                // verdict is outstanding; describeOffer leads with it.
                ...(args.menu !== undefined && { menu: args.menu }),
                ...(treeRouted && { treeRouted: true }),
            })?.schema;
        };
    }
    /**
     * Does the configured checker ask to decide which skills this caller sees?
     * (9.11.0)
     *
     * Absence is NO — see `PermissionChecker.governs`. This is the ONE switch:
     * false and the menu, the resolver and the activation gate are all inert, so
     * an agent with a checker that predates 9.11.0 composes the same prompt it
     * always did.
     */
    governsSkillVisibility() {
        return checkerGoverns(this.permissionChecker, 'skill_read');
    }
    // FOLD (resolver) · computes "which skill ids may this caller be told about".
    // The OWNER of the published fact is src/injection-engine/buildInjectionEngineSubflow.ts · hiddenSkillIdsOf
    // (the engine's Gather stage), which resolves it once per iteration, before the cursor and the
    // evaluator decide, and publishes it on scope; every model-facing composer reads THAT.
    // Second caller: the turn router (stages/routeTurn.ts · resolveHidden) resolves it again, with different
    // failure semantics, and publishes nothing — two sweeps of one policy question per turn.
    /**
     * Which skills the caller's role may NOT see, asked once per iteration
     * (9.11.0).
     *
     * Per iteration rather than per run because the checker is a port: a
     * hub-backed one can legitimately answer differently as a grant is revoked
     * mid-conversation, and caching the first answer would keep a withdrawn skill
     * on the menu for the rest of the run. The cost is one `check()` per skill per
     * iteration, paid only by agents that opted in.
     *
     * A skill is hidden when the checker returns anything other than `'allow'` /
     * `'gate_open'` — and when the checker THROWS, which is the fail-closed half:
     * a policy that did not answer did not say yes, and the same sentence governs
     * the tool gate.
     */
    hiddenSkillIdsNow() {
        if (!this.governsSkillVisibility())
            return undefined;
        const checker = this.permissionChecker;
        const skillIds = this.injections.filter((i) => i.flavor === 'skill').map((i) => i.id);
        if (!checker || skillIds.length === 0)
            return undefined;
        return async (scope) => {
            // The caller of THIS run: the instance's caller on its own run, the
            // member's run (never the instance's last caller) on a mount.
            const identity = this.ownScopes.has(scope) ? this.lastRunIdentity : memberIdentityIn(scope);
            const hidden = [];
            for (const id of skillIds) {
                let allowed = false;
                try {
                    const decision = await checker.check({
                        capability: 'skill_read',
                        actor: 'agent',
                        target: skillTarget(id),
                        ...(identity !== undefined && { identity }),
                    });
                    allowed = decision.result === 'allow' || decision.result === 'gate_open';
                }
                catch {
                    // Fail closed. The refusal the model would read is the dispatch
                    // gate's job; here the only decision is whether to advertise a skill
                    // whose policy is unreachable, and advertising it would be a menu
                    // built on a question nobody answered.
                    allowed = false;
                }
                if (!allowed)
                    hidden.push(id);
            }
            return hidden;
        };
    }
    assertDeliverableRoles() {
        const providers = providerTargets(this.provider, this.model, this.reliabilityConfig?.providers).map((target) => target.provider);
        for (const inj of this.injections) {
            for (const msg of inj.inject?.messages ?? []) {
                assertDeliverableRole(providers, msg.role, `Agent injection '${inj.id}'`);
                // Content, judged at the same funnel as role (8.18.0). The named
                // factories refuse empty content; a hand-built Injection reached the
                // delivery stage unchecked and put a contentless turn on the wire.
                const content = msg.content;
                if (typeof content !== 'string' || content.trim() === '') {
                    throw new Error(messagesContentRefusal({
                        site: `Agent injection '${inj.id}'`,
                        role: msg.role,
                        received: typeof content !== 'string'
                            ? content === undefined
                                ? 'missing'
                                : `a ${content === null ? 'null' : typeof content}`
                            : 'empty',
                    }));
                }
            }
        }
    }
    /**
     * Refuse, at run start, a `'tool-forced'` output strategy on a provider
     * that does not put a forced tool choice on its wire (7.26).
     *
     * Run start rather than build time for the reason `assertDeliverableRoles`
     * gives one method up: a decorated provider (`withFallback`, `withRetry`,
     * a breaker) is only the thing it is once composed, and `withFallback`
     * publishes the AND of its pair.
     *
     * Refusal rather than a quiet fall back to `'instruct'`, because the two
     * are not interchangeable: one constrains the shape at generation, the
     * other asks for it in prose. An agent that silently got the second while
     * its config said the first would be a promise the recording could not
     * even show was broken.
     */
    assertForcedToolChoiceSupported() {
        if (this.outputEnforcement?.schemaTool === undefined)
            return;
        const first = this.reliabilityConfig?.providers?.[0];
        const provider = first?.provider ?? this.provider;
        const model = first?.model ?? this.model;
        if (provider.carriesForcedToolChoice === true) {
            // Configuration resolves the actual model at seed, after this check.
            // Keep the early wire refusal, but let callLLM judge that final model.
            if ((first === undefined && this.runConfigFn !== undefined) ||
                forcedToolChoiceFor(provider, model))
                return;
            throw new UnsupportedToolChoiceError({ provider: provider.name, model });
        }
        throw new Error(`Agent: .outputSchema(parser, { strategy: 'tool-forced' }) needs a provider that puts ` +
            `a forced tool choice on its wire, and '${provider.name}' does not declare one. ` +
            `Anthropic, Bedrock, real OpenAI/Azure and the mock do; an OpenAI-COMPATIBLE endpoint ` +
            `behind a custom baseURL (Ollama, vLLM, …) deliberately does not, because what that ` +
            `server does with tool_choice is not this library's to promise. Use ` +
            `{ strategy: 'instruct', retries: N } on this provider — it works on every wire — or ` +
            `run the forced strategy on a provider that declares the capability.`);
    }
    /**
     * Refuse, at run start, a `'native'` output strategy on a provider that
     * does not put a structured-output format on its wire, or on a model that
     * does not take one — the `assertForcedToolChoiceSupported` twin, for the
     * same reasons: run start because a decorated provider is only what it is
     * once composed; a refusal because `'instruct'` asks in prose for what this
     * strategy constrains at decoding, and a quiet swap would be config that
     * lies.
     */
    assertNativeOutputSchemaSupported() {
        if (this.outputEnforcement?.outputFormat === undefined ||
            this.outputEnforcement.nativeOutputAuto === true)
            return;
        const first = this.reliabilityConfig?.providers?.[0];
        const provider = first?.provider ?? this.provider;
        const model = first?.model ?? this.model;
        if (provider.carriesNativeOutputSchema === true) {
            // A configured agent resolves its model at seed; callLLM judges it there.
            if ((first === undefined && this.runConfigFn !== undefined) ||
                nativeOutputSchemaFor(provider, model))
                return;
            throw new UnsupportedNativeOutputError({ provider: provider.name, model });
        }
        throw new Error(`Agent: .outputSchema(parser, { strategy: 'native' }) needs a provider that puts a ` +
            `structured-output format on its wire, and '${provider.name}' does not declare one. ` +
            `anthropic(), browserAnthropic() and the mock do; the Bedrock and gateway adapters do ` +
            `not in this release. Use { strategy: 'instruct', retries: N } on this provider — it ` +
            `works on every wire — or run the native strategy on a provider that declares it.`);
    }
    createExecutor(runOptions, request) {
        this.assertDeliverableRoles();
        this.assertForcedToolChoiceSupported();
        this.assertNativeOutputSchemaSupported();
        const correlationId = runOptions?.correlationId;
        const traceId = runOptions?.traceId ?? runOptions?.env?.traceId;
        const sessionId = runOptions?.sessionId;
        // The actor, for every event this run emits (9.11.0).
        //
        // `lastRunIdentity` is what the CALLER passed and nothing else — `run()`
        // sets it from `input.identity ?? options.identity ?? the conversation's`,
        // and `resume()` from `options.identity ?? the paused run's caller's`
        // (`agent/callerIdentity.ts`), both before this method is reached; it stays
        // undefined when nobody named one.
        //
        // NOT `scope.runIdentity`: that one is always populated and defaults to
        // `{ conversationId: '<runId>' }` (or, since 9.10.0, to the sessionId on a
        // session-bound run). Stamping either as the principal would publish a
        // synthesized conversation as an actor — and a caller-supplied session id
        // is exactly the string an auditor must not read as "who did this".
        // `conversationId` is deliberately not carried: it is a thread, not a
        // person, and `sessionId` beside it is the fact the transport delivered.
        const actor = runOptions?.identity ?? this.lastRunIdentity;
        // A fresh run starts with no evictions filed — the previous run's last
        // visit must not be read as this run's (`window/evictedTurns.ts`).
        this.evictedTurnsHandle?.clear();
        const runId = makeRunId();
        this.mintedRunIds.add(runId);
        boundInsertionOrder(this.mintedRunIds, MAX_REMEMBERED_RUNS);
        const hosted = hostedConversationOf(runOptions);
        this.servingConversation =
            sessionId !== undefined
                ? sessionConversationKey(sessionId)
                : hosted !== undefined
                    ? hostedConversationKey(hosted)
                    : undefined;
        this.currentRunContext = {
            runStartMs: Date.now(),
            runId,
            compositionPath: [`Agent:${this.id}`],
            ...(correlationId !== undefined && { correlationId }),
            ...(traceId !== undefined && { traceId }),
            // Session identity rides beside runId, not instead of it: one session
            // produces many runs, and an event needs to say which of each it is.
            ...(sessionId !== undefined && { sessionId }),
            ...(actor?.principal !== undefined && { principal: actor.principal }),
            ...(actor?.tenant !== undefined && { tenant: actor.tenant }),
        };
        const getRunCtx = () => this.currentRunContext;
        // The run's redaction (`src/redaction/`): this agent's `redact` joined
        // with any policy the caller handed down for this run (a tool's
        // `ctx.redact`). Opened for EVERY run — with no policy the serving is the
        // identity, but the stages' scopes still feed the real-value path the
        // crash checkpoint and the window's meter read.
        const redaction = this.openRunRedaction(runOptions?.redact, getRunCtx);
        // Reuse the cached chart built at constructor time.
        // The Agent's executor dials: readTracking (#18/#14, snapshot stageReads),
        // commitValues (#13c-B, commit-log value encoding) and writeProvenance
        // (#P1, per-write read provenance — the exact-dataflow debugging dial).
        const spec = this.getSpec();
        // Every scope THIS executor makes is marked as the agent's own, so a stage
        // can tell its own run from a mount in another runner's executor
        // (`memberRun.ts`) — per scope, never by instance state.
        const scopeFactory = redaction.scopeFactoryFor(spec);
        const ownScopes = this.ownScopes;
        const scopeRequests = this.scopeRequests;
        const executor = new FlowChartExecutor(spec, {
            readTracking: this.readTracking,
            commitValues: this.commitValues,
            writeProvenance: this.writeProvenance,
            scopeFactory: (...args) => {
                const scope = scopeFactory(...args);
                if (scope !== null && typeof scope === 'object') {
                    ownScopes.add(scope);
                    // THIS executor's request, bound to the scope for its whole life.
                    if (request !== undefined)
                        scopeRequests.set(scope, request);
                }
                return scope;
            },
        });
        redaction.applyTo(executor);
        // Enable structured narrative so `getLastNarrativeEntries()` can
        // hand a populated array to consumer Trace views (ExplainableShell).
        // Cheap when no consumer reads it; the recorder accumulates only.
        executor.enableNarrative();
        this.lastExecutor = executor;
        const dispatcher = this.getDispatcher();
        // RFC-001 Block 10 — observer delivery tier. With 'deferred', every
        // bridge below is attached onto footprintjs's bounded capture queue
        // ("one beat behind": capture inline ≈ microseconds, delivery at the
        // next microtask checkpoint, terminal flush before run()/resume()
        // returns). Default 'inline' attaches with no options bag —
        // byte-identical to every prior release.
        const deferredOpts = this.observerDelivery === 'deferred'
            ? { delivery: 'deferred', ...this.observerDeliveryOptions }
            : undefined;
        const attachObserver = (rec) => {
            if (deferredOpts)
                executor.attachCombinedRecorder(rec, deferredOpts);
            else
                executor.attachCombinedRecorder(rec);
        };
        attachObserver(new ContextRecorder({ dispatcher, getRunContext: getRunCtx, realWrites: redaction }));
        // Evidence bridge (#5): harvest decisions/toolCalls/tokens for causal snapshots.
        // ALWAYS INLINE — never routed through the deferred queue: the memory
        // write stage consumes its accumulators MID-run (`collect()` via
        // `evidenceSource`, mountMemoryPipeline). Deferred delivery would run
        // `collect()` before the queue flushed the turn's tool/token/decision
        // events, persisting an incomplete causal snapshot.
        if (this.causalEvidence) {
            // Its emits arrive on the real-value path (constructor); the executor
            // feeds it the flow channel's decisions and the per-run reset only.
            const evidence = this.causalEvidence;
            executor.attachCombinedRecorder({
                id: evidence.id,
                onDecision: (event) => evidence.onDecision(event),
                onSelected: (event) => evidence.onSelected(event),
                clear: () => evidence.clear(),
            });
        }
        // Compaction's instrument. ALWAYS INLINE, for the same reason the evidence
        // bridge is: the compaction stage reads it MID-run, at the loop head, to
        // decide whether this iteration's window is over budget. A measurement
        // delivered one beat behind would be a measurement of the wrong window.
        if (this.compactionMeterHandle) {
            executor.attachCombinedRecorder(this.compactionMeterHandle);
            // Folds speak the context vocabulary consumers already subscribe to
            // (`context.evicted` / `context.budget_pressure`) — no new event types.
            // ContextRecorder only dispatches those from writes INSIDE a slot
            // subflow, and the compaction stage is not one, so it emits them and
            // this bridge forwards them (the `contextEvaluatedRecorder` pattern).
            attachObserver(new EmitBridge({
                dispatcher,
                id: 'af-compaction-events',
                prefix: ['agentfootprint.context.evicted', 'agentfootprint.context.budget_pressure'],
                getRunContext: getRunCtx,
            }));
        }
        // The InjectionEngine typedEmits context.evaluated; this bridge forwards it
        // to the dispatcher (ContextRecorder handles the write-derived context.*).
        attachObserver(contextEvaluatedRecorder({ dispatcher, getRunContext: getRunCtx }));
        attachObserver(streamRecorder({ dispatcher, getRunContext: getRunCtx }));
        // agentRecorder feeds the run-checkpoint tracker (iteration_end →
        // history snapshot), which is read ONLY in run()'s catch — after the
        // engine's terminal flush at the reject boundary — so deferral is safe
        // (pinned by test: crash checkpoints stay complete under 'deferred').
        attachObserver(agentRecorder({ dispatcher, getRunContext: getRunCtx }));
        // Terminal-failure bridge: footprintjs onRunFailed → typed error.fatal,
        // so a thrown run clears in-flight live state + flips monitor status.
        // Deferral-safe: the reject-boundary terminal flush delivers error.fatal
        // before the rejection reaches the caller.
        attachObserver(errorBridge({ dispatcher, getRunContext: getRunCtx }));
        // The cost bridge is ALWAYS attached since 8.14.0. It used to be gated on
        // `pricingTable`, which was right while `cost.*` only ever meant money:
        // `emitCostTick` returns on its first line without a table, so the gate
        // could not hide anything. `cost.limit_hit { kind: 'max_iterations' }`
        // broke that assumption — an iteration limit has no price and fires on any
        // agent, and behind the old gate it would have reached the dispatcher only
        // for agents that happened to be costing themselves. The bridge drops
        // events with no listener, so an agent that subscribes to nothing pays
        // nothing for it.
        attachObserver(costRecorder({ dispatcher, getRunContext: getRunCtx }));
        if (this.permissionChecker) {
            attachObserver(permissionRecorder({ dispatcher, getRunContext: getRunCtx }));
        }
        // Always-on bridges for consumer-emitted domain events.
        attachObserver(evalRecorder({ dispatcher, getRunContext: getRunCtx }));
        attachObserver(memoryRecorder({ dispatcher, getRunContext: getRunCtx }));
        // Embedding cost telemetry (8.9.0). The domain had a payload, a registry
        // entry and a DomainWildcard arm since 2.x and NO bridge, so the event
        // could not have arrived however correctly it was fired. Zero-cost when
        // the agent embeds nothing.
        attachObserver(embeddingRecorder({ dispatcher, getRunContext: getRunCtx }));
        attachObserver(skillRecorder({ dispatcher, getRunContext: getRunCtx }));
        attachObserver(toolsRecorder({ dispatcher, getRunContext: getRunCtx }));
        // Tool-args validation events (#9) — always-on; zero-cost when no
        // validation event fires.
        attachObserver(validationRecorder({ dispatcher, getRunContext: getRunCtx }));
        // Credential lifecycle (9.4.0). The domain has emitted since 6.11.0 with no
        // bridge, so `agent.on('agentfootprint.credential.failed', …)` observed
        // nothing however correctly the event fired — which is how an identity
        // adapter that failed every call did so in a silence that read like health.
        // Always-on and zero-cost: the bridge drops an event nobody listens for.
        attachObserver(credentialRecorder({ dispatcher, getRunContext: getRunCtx }));
        // Map engagement (9.58.0). Shipped WITH the domain, same lesson as above.
        attachObserver(mapRecorder({ dispatcher, getRunContext: getRunCtx }));
        // Context Integrity findings (9.60.0). Same lesson, applied again.
        attachObserver(integrityRecorder({ dispatcher, getRunContext: getRunCtx }));
        // Artifact lifecycle (9.21.0). Shipped WITH the domain — the credential
        // bridge above is the record of what waiting costs. Always-on and
        // zero-cost: with no store attached nothing emits, and the bridge drops
        // an event nobody listens for.
        attachObserver(artifactsRecorder({ dispatcher, getRunContext: getRunCtx }));
        // Reliability telemetry (rules-loop fail_fast / retried / recovered).
        // Always-on, but zero-cost when no .reliability() config fires events.
        attachObserver(reliabilityRecorder({ dispatcher, getRunContext: getRunCtx }));
        // Provider-decorator telemetry (withFallback / withRetry reports,
        // translated in-run by `resilienceHooks`). Always-on, but zero-cost
        // when the configured provider is undecorated or never fails. Also
        // covers a `.reliability()` run: that path is `executeWithReliability`
        // driving `singleProviderCall` inside the SAME call-llm stage
        // (callLLM.ts), so the decorator's reports ride this bridge too. (It
        // is NOT the `buildReliabilityGateChart` subflow — the Agent never
        // mounts that chart; nothing shipped does.)
        attachObserver(resilienceRecorder({ dispatcher, getRunContext: getRunCtx }));
        // Check-in events bridge (evidence-carrying human consent). Always-on;
        // forwards checkin.request/decision $emits to the dispatcher so
        // `agent.on('agentfootprint.checkin.*')` fires. Zero-cost until a tool with
        // `checkIn` trips.
        attachObserver(checkInEventsBridge({ dispatcher, getRunContext: getRunCtx }));
        // Same wiring for `agentfootprint.middleware.*` — the governance chains'
        // one event. Attached only when a chain exists: an agent without middleware
        // gains no bridge, no listener and no per-event work.
        if (this.toolMiddleware.length > 0 || this.messageMiddleware.length > 0) {
            attachObserver(new EmitBridge({
                id: 'agentfootprint.middleware-bridge',
                prefix: 'agentfootprint.middleware.',
                dispatcher,
                getRunContext: getRunCtx,
            }));
        }
        // Same wiring for `agentfootprint.findings.*` (9.101.0) — the ledger's
        // events, filed by `recordFindings` on scope. An unarmed agent gains
        // no bridge, no listener and no
        // per-event work, and `agent.on('agentfootprint.findings.*')` can only
        // ever fire on an agent that could have filed a row.
        // Also under the inputs layer (honesty layer 2): its `argument` rows are
        // filed through the same writer and ride the same domain — and under the
        // results layer (honesty layer 3), whose `period` rows do the same.
        // Negative claims also file prose warnings, independently of those layers.
        if (this.findingsOptions !== undefined ||
            this.inputsLayerArmed ||
            this.resultsLayerArmed ||
            this.negativeClaimsConfig !== undefined) {
            attachObserver(new EmitBridge({
                id: 'agentfootprint.findings-bridge',
                prefix: 'agentfootprint.findings.',
                dispatcher,
                getRunContext: getRunCtx,
            }));
        }
        // Same wiring for `agentfootprint.answer.*` (honesty layer 4) — the one
        // event the answer layer's stage fires per answer. Attached only under
        // `.answerLayer()`, shipped WITH the domain (the credential.* lesson).
        if (this.answerLayerOption !== undefined) {
            attachObserver(new EmitBridge({
                id: 'agentfootprint.answer-bridge',
                prefix: 'agentfootprint.answer.',
                dispatcher,
                getRunContext: getRunCtx,
            }));
        }
        // Same wiring for `agentfootprint.tool_choice.*` (9.105.0) — the three
        // events `recordToolChoice` files on scope. Attached only under
        // `.toolChoice()`, for the same reason.
        if (this.toolChoiceOptions !== undefined) {
            attachObserver(new EmitBridge({
                id: 'agentfootprint.tool-choice-bridge',
                prefix: 'agentfootprint.tool_choice.',
                dispatcher,
                getRunContext: getRunCtx,
            }));
        }
        // Same wiring for `agentfootprint.ontology.*` (9.106.0) — the one event
        // `callLLM` emits when the declared map is served. Attached only under
        // `.ontology()`, for the same reason.
        if (this.ontology !== undefined) {
            attachObserver(new EmitBridge({
                id: 'agentfootprint.ontology-bridge',
                prefix: 'agentfootprint.ontology.',
                dispatcher,
                getRunContext: getRunCtx,
            }));
        }
        for (const r of this.attachedRecorders) {
            // A recorder's OWN `delivery` field is more specific than the
            // agent-level default — footprintjs's options bag would override the
            // field, so recorders that declare a tier are attached bare (their
            // field rules). This gives consumers a per-recorder escape hatch:
            // `{ id, delivery: 'inline', ...hooks }` stays inline under an
            // observerDelivery: 'deferred' agent, and vice versa.
            if (r.delivery !== undefined)
                executor.attachCombinedRecorder(r);
            else
                attachObserver(r);
        }
        // LAST, so the manifest describes a fully-wired run — and so it is the
        // first event of this runId that any listener sees.
        this.emitRunManifest();
        // …then the declared skill map (9.50.0), so a recording opens with the
        // arm's configuration and the author's topology before any stage event.
        this.emitSkillGraphDeclared();
        return executor;
    }
    /**
     * File the run-configuration manifest (9.41.0) — which adapters and
     * strategies this run is about to use, stamped with the runId every other
     * event of the run already carries.
     *
     * **Why it lives in `createExecutor` and not in `run()`.** `run()` and
     * `resume()` both come through here, and both mint a fresh runId (a resumed
     * run is a new run to every consumer joining on `meta.runId`, so a resume
     * with no manifest would be a run whose arm nobody can name). One funnel is
     * also how the next entry point cannot forget — the `beginIngress` lesson.
     *
     * **Why a direct dispatch rather than `typedEmit`.** There is no stage: the
     * chart has not started. So it is built with `buildEventMeta` and a STATED
     * pseudo-stage, exactly like the tool-teardown reports at the other end of
     * the run — never `minimalMeta()`, whose hardcoded `runId: 'consumer-scope'`
     * would make the one event whose whole job is to BE joinable the one event
     * that cannot be joined.
     *
     * **Why it is gated on a listener.** Every typed event in this library is:
     * the dispatcher drops what nobody subscribed to, and `EmitBridge` does the
     * same upstream. The gate is what keeps an unwatched agent at one map
     * lookup per run. It also means the manifest is not "always on" but "always
     * there when anything is watching" — including `recordRun`, which subscribes
     * with `'*'` before the run starts, so every recording carries one.
     */
    /** Fresh per-run ledger — see AgentOptions.integrityPosture (9.60.0). */
    beginIntegrityLedger() {
        this.integrityLedgerHolder.current = beginIntegrityRun({
            wire: true,
            composeInvariant: this.mapsPlan !== undefined,
            dangling: this.integrityDanglingPresent,
            claim: this.claimContract !== undefined,
            // TWO HALVES (9.77.0): the operator's dial AND a declaration to arm
            // on. Either alone leaves a registered `not-applicable` row.
            emptyLookup: this.noticeEmptyLookups && this.integrityDanglingPresent,
            // TWO HALVES (9.78.0), the same law: the operator's dial off `'off'`
            // AND a tool declaring `resultColumns`. Either alone leaves two
            // registered `not-applicable` rows.
            columnTypes: this.checkColumnTypes !== 'off' && this.integrityColumnsPresent,
            // TWO HALVES (9.83.0), and here the second is structural rather than
            // a policy choice: the evidence gate owns the extractor that decides
            // which tokens in an answer are values, so a dial with no gate has
            // nothing whose provenance it could read. Either alone leaves a
            // registered `not-applicable` row.
            priorTurnEvidence: this.noticePriorTurnEvidence && this.evidenceGate !== undefined,
        }, this.integrityPosture);
    }
    /**
     * Dev posture only: throw {@link CheckerDeadError} on a run whose
     * registered checkers demonstrably never ran, or whose canary went
     * uncaught. Called on the SUCCESS path before the recording files, so the
     * failure is the run's result, never a masked afterthought.
     */
    assertIntegrityAlive() {
        if (this.integrityPosture !== 'dev')
            return;
        this.integrityLedgerHolder.current?.assertAlive({
            workExisted: this.integrityWorkExisted(),
        });
    }
    /**
     * Did this run reach work a registered check should have seen?
     *
     * MEASURED, not asserted — and measured from a signal the integrity code
     * does not write. `llmLatestContent` is committed by the LLM stage's own
     * core path on every completed call, so its presence proves a call
     * happened; its absence proves the run died or paused before one, and a
     * checker that filed nothing THERE is not rot, it is a run that never got
     * started. Deriving this from the checks' own encounter counts would be
     * circular: an unhooked check would report "no work existed" and silence
     * the very alarm it should be tripping.
     */
    integrityWorkExisted() {
        const state = this.liveSnapshot()?.sharedState;
        return state !== undefined && state.llmLatestContent !== undefined;
    }
    /**
     * File the run's disposition rows as ONE `integrity.disposition` event and
     * clear the ledger. On the finally path of both run doors — every exit,
     * before the recording stops. Listener-gated like every typed event, and
     * deliberately throw-proof: accounting must never change a run's outcome.
     */
    fileIntegrityDisposition() {
        const ledger = this.integrityLedgerHolder.current;
        this.integrityLedgerHolder.current = undefined;
        if (ledger === undefined)
            return;
        try {
            const type = 'agentfootprint.integrity.disposition';
            const dispatcher = this.getDispatcher();
            if (!dispatcher.hasListenersFor(type))
                return;
            dispatcher.dispatch({
                type,
                payload: {
                    posture: this.integrityPosture,
                    workExisted: this.integrityWorkExisted(),
                    rows: ledger.report(),
                },
                meta: buildEventMeta({ runtimeStageId: INTEGRITY_DISPOSITION_STAGE_ID }, this.currentRunContext),
            });
        }
        catch {
            // Recorder/dispatcher failures never abort a run — the house rule.
        }
    }
    emitRunManifest() {
        const type = 'agentfootprint.agent.run_configured';
        const dispatcher = this.getDispatcher();
        if (!dispatcher.hasListenersFor(type))
            return;
        dispatcher.dispatch({
            type,
            payload: buildRunManifest({
                agentId: this.id,
                providerName: this.provider.name,
                model: this.model,
                hasRunConfig: this.runConfigFn !== undefined,
                hasSkillBrains: this.skillBrains !== undefined,
                ...(this.cacheUnsupported !== undefined && { cacheUnsupported: this.cacheUnsupported }),
                reactMode: this.reactMode,
                memories: this.memories,
                ...(this.windowStrategy !== undefined && {
                    windowStrategyName: this.windowStrategy.name,
                }),
                // A graph is mounted iff `.skillGraph()` handed over its cursor
                // resolver. NOT `skillGraphCascade`, which a graph mounted without the
                // turn-start cascade options never sets — reading that one would
                // report "no graph" for a graph that routes every turn.
                ...(this.skillGraphNextSkill !== undefined && {
                    skillGraph: {
                        ...(this.skillGraphCascade !== undefined && {
                            routing: this.skillGraphCascade.strictness,
                            continuity: this.skillGraphCascade.continuity,
                        }),
                        ...(this.skillGraphCascade?.turnRouting?.scorer !== undefined && {
                            scorerName: this.skillGraphCascade.turnRouting.scorer.name,
                        }),
                    },
                }),
                ...(this.evidenceGate !== undefined && {
                    evidenceGatePosture: this.evidenceGate.posture,
                }),
                // The compositions this agent was built from, in declaration order.
                // Spread value-conditionally so an agent with none passes no key at all
                // — see `RunManifestSources.recipes` for why it is absent rather than
                // an empty list.
                ...(this.appliedRecipes !== undefined && { recipes: this.appliedRecipes }),
                // The store itself is never named — see RunManifestSources.artifacts.
                ...(this.artifactStore !== undefined && {
                    artifacts: {
                        configured: true,
                        placement: this.artifactPlacement !== undefined,
                        recordings: this.artifactRecordings !== undefined,
                    },
                }),
            }),
            meta: buildEventMeta({ runtimeStageId: RUN_MANIFEST_STAGE_ID }, this.currentRunContext),
        });
    }
    /**
     * File the DECLARED skill map (9.50.0) — `agentfootprint.skill.graph_declared`,
     * once per run, right after the run-configuration manifest.
     *
     * Same funnel, same dispatch discipline, same listener gate as the manifest
     * (see `emitRunManifest` above): `run()` and `resume()` both come through
     * `createExecutor`, both mint a fresh runId, and a resumed run's consumers
     * deserve the topology under the runId they are joining on. The payload is
     * the map `AgentBuilder.skillGraph()` projected at mount — the author's
     * nodes and edges VERBATIM, never inferred from runtime hops — so a
     * recording carries the complete declared topology rather than the
     * fired-edges lower bound that `context.evaluated.routing[]` names.
     *
     * No graph, or a graph that could not state its map ⇒ no event — absent,
     * never guessed.
     */
    emitSkillGraphDeclared() {
        if (this.skillGraphDeclared === undefined)
            return;
        const type = 'agentfootprint.skill.graph_declared';
        const dispatcher = this.getDispatcher();
        if (!dispatcher.hasListenersFor(type))
            return;
        dispatcher.dispatch({
            type,
            payload: this.skillGraphDeclared,
            meta: buildEventMeta({ runtimeStageId: GRAPH_DECLARED_STAGE_ID }, this.currentRunContext),
        });
    }
    /**
     * Flush the deferred-observer backlog of the most recent run's executor,
     * then await async listener completions under a deadline (RFC-001 §11 —
     * the serverless / graceful-shutdown pattern). Resolves immediately with
     * zeros before the first run or when `observerDelivery` is `'inline'`
     * and no recorder opted into `'deferred'` itself.
     *
     * `pending === 0` means a full drain; non-zero honestly reports
     * continuations still outstanding at the deadline — never silent loss.
     *
     * @example Lambda-style handler
     * ```ts
     * export const handler = async (event) => {
     *   const reply = await agent.run({ message: event.message });
     *   // settle "one beat behind" observer work BEFORE the freeze:
     *   await agent.drainObservers({ timeoutMs: 5_000 });
     *   return reply;
     * };
     * ```
     */
    drainObservers(opts) {
        if (!this.lastExecutor)
            return Promise.resolve({ done: 0, failed: 0, pending: 0 });
        return this.lastExecutor.drainObservers(opts);
    }
    /**
     * Did the last turn stop because a LIMIT cut it short — and if so, which?
     *
     * `undefined` on every normal finish, including a turn that used its whole
     * `maxIterations` budget and then genuinely finished. It is set only when
     * the model was still asking for tools and the run refused to run them:
     * `maxIterations` was reached, or a `costBudget: { onExceed: 'halt' }` was
     * crossed.
     *
     * ## Why this is a method and not part of the answer
     *
     * `run()` resolves to a bare string. There is nowhere in a string to write
     * "…and three tool calls never ran", which is the same wall 8.6.0 hit with
     * an outstanding credential consent — and there the turn raises, because
     * handing back a plausible answer for work a tool never did is a silent
     * success. This is not that. A limit you configured firing is the limit
     * working, and the answer is sometimes real (a model can return content AND
     * tool calls). So it does not raise; it records, in committed state, where
     * it is provable after the fact — `getLastSnapshot().sharedState.stoppedEarly`
     * is the same value (unless the agent's `redact` names it: this accessor
     * reads the live run), and this is the short way to it.
     *
     * When the answer came back EMPTY the library also warns once on the
     * console, because an empty string reaching a user is indistinguishable
     * from a bug.
     *
     * @example
     * ```ts
     * const answer = await agent.run({ message: 'audit every log file' });
     * const cut = agent.stoppedEarly();
     * if (cut) {
     *   console.log(`stopped at iteration ${cut.iteration}: ${cut.reason}`);
     *   console.log(`${cut.pendingToolCalls} tool call(s) never ran`);
     * }
     * ```
     */
    stoppedEarly() {
        const state = this.liveSnapshot()?.sharedState;
        return state?.stoppedEarly;
    }
    /**
     * Did the last turn's answer FAIL this agent's `outputSchema` — and how (8.18.0)?
     *
     * `undefined` when the answer satisfied the contract, and on any agent with
     * no `.outputSchema()`. Set on every run whose final answer was judged and
     * rejected, including the default `retries: 0` case where the first answer is
     * the only one there was.
     *
     * ## Why a method, when `runTyped()` already throws
     *
     * Because `run()` does not, and `run()` is what a server, a queue worker and
     * `standingAgent` call. Before this existed, that caller received a string
     * that violated a contract they had declared, with nothing anywhere saying
     * so: the retries were billed, the ledger row was written under `retries > 0`
     * and absent under `retries: 0`, and the answer looked exactly like a good
     * one. `runTyped()` still throws `OutputSchemaError` — that is the caller
     * ASKING to be raised at, and it is unchanged.
     *
     * `brokenBy` is the case worth a dashboard: the model's answer PASSED and one
     * of your own `act({ output })` rules rewrote it into one that fails. The run
     * stops re-asking when that happens — a deterministic rule breaks the next
     * answer identically, so the retries would be bought for nothing.
     *
     * @example
     * ```ts
     * const answer = await agent.run({ message: 'summarise ticket 91' });
     * const unmet = agent.outputContractUnmet();
     * if (unmet) {
     *   log.warn({ stage: unmet.stage, error: unmet.error, brokenBy: unmet.brokenBy });
     *   return safeDefault;               // …rather than shipping `answer` as typed data
     * }
     * ```
     */
    outputContractUnmet() {
        const state = this.liveSnapshot()?.sharedState;
        return state?.outputContractUnmet;
    }
    /**
     * Did the last turn's answer state names or numbers that appear in NO tool
     * result (9.35.0)?
     *
     * `undefined` when every value was grounded — and on any agent without
     * `.namesAndNumbersFromEvidence()`. Set on a turn that shipped flagged
     * (`'assist'` / `'guard'`) and on one that was refused (`'rails'`, where
     * `run()` also raised `UnsupportedValuesError`, so this is what a caller
     * reads in the `catch`).
     *
     * `revised: true` means the model was asked once to correct the values and
     * they survived that turn — the fact worth alerting on, because it is a
     * model that cannot ground its own claims rather than one that slipped.
     *
     * Remember what the verdict does NOT say: values were invented. It cannot
     * tell you whether a claim built from real values is true.
     *
     * @example
     * ```ts
     * const answer = await agent.run({ message: 'which port is down?' });
     * const bad = agent.unsupportedValues();
     * if (bad) log.warn({ values: bad.values.map((v) => v.value), revised: bad.revised });
     * ```
     */
    unsupportedValues() {
        const state = this.liveSnapshot()?.sharedState;
        return state?.unsupportedValues;
    }
    /**
     * The last run's findings ledger (9.101.0) — the model's OWN standings on
     * its tool results, as `.findings()` recorded them: `basis` rows (what a
     * call was for, declared before it ran), `standing` rows (`fact` with the
     * assertions stood on, `open`, `ruled-out`, `noise` — the LAST row per
     * `toolCallId` is the current reading; earlier ones are quotable history)
     * and `conflict` rows (two stood-on readings that disagree, witnesses by
     * identity). Undefined when nothing filed a row — never an empty array
     * standing in for "no findings", and an id with no standing row is
     * undeclared, never `open`.
     *
     * THE INPUTS LAYER files here too (honesty layer 2): an agent whose tools
     * declare `askOrAssume` gets `argument` rows — the library's verdict on each
     * ruled argument of each call (`source: 'default'` when a tool's rule
     * assumed the value, `'model'` when the model sent one the record does not
     * trace), each stamped with its conversation `turn` — WITHOUT `.findings()`.
     * A reader that switches over every row kind must skip one it does not know.
     *
     * THE RESULTS LAYER files here too (honesty layer 3, step 7b): `period` rows
     * — one per call whose result declared the period its read covered, or whose
     * tool declares a period argument: `covered`, `partly-held`, `not-held`,
     * `unknown`, or `undeclared` (the result said nothing about its period).
     *
     * Detached from the execution record (`structuredClone`), so a caller may
     * keep or mutate it without touching the run's state.
     *
     * @example
     * ```ts
     * await agent.run({ message: 'which port is down?' });
     * const current = new Map<string, string>();
     * for (const row of agent.findings() ?? []) {
     *   if (row.kind === 'standing') current.set(row.toolCallId, row.standing);
     * }
     * ```
     */
    findings() {
        const ledger = this.liveSnapshot()?.sharedState
            ?.findingsLedger;
        return ledger === undefined ? undefined : structuredClone(ledger);
    }
    /** The library-composed negative statements and their recorded limits.
     * Detached, in-process data for the caller. Record redaction applies to the
     * snapshots and emitted events, not this caller-owned value. */
    answerNegatives() {
        const value = this.liveSnapshot()?.sharedState
            ?.answerNegatives;
        return value === undefined ? undefined : structuredClone(value);
    }
    /** The last run's answer checks, detached from its execution record.
     * Undefined means no terminal validation ran, never an implicit pass. */
    answerValidation() {
        const report = this.liveSnapshot()?.sharedState
            ?.answerValidation;
        return report === undefined ? undefined : structuredClone(report);
    }
    /**
     * The limits of the last run's TYPED answer, as data — what its tools
     * declared they `checked`, did not check (`notChecked`) and can never see
     * (`cannotCover`), folded across the run: duplicates said once, every entry
     * kept.
     *
     * Set when the agent has an output schema AND `.limitsTravelWithTheAnswer()`:
     * a typed answer is JSON, and the limits block that option appends to a
     * prose answer would stop it being JSON, so for a typed answer the limits
     * travel beside it instead — here, on `turn_end.answerCoverage`, and in
     * `getLastSnapshot().sharedState.answerCoverage` (served, under the agent's
     * `redact`), one value three ways. The
     * answer string, and so `runTyped()`, is exactly the model's.
     *
     * On an agent whose inputs layer is armed (a tool declares `askOrAssume`),
     * `assumed` lists the values a tool's rule filled this turn — the same rows,
     * read the same way, that a prose answer's "Assumed" block prints — each in
     * the tool's own argument view (`'REDACTED'` with `hidden` when that view
     * hides it).
     *
     * `undefined` when the run's tools declared no limits and nothing was
     * assumed, on a prose answer (the block is in the answer string there;
     * `sharedState.coverageDeclared` holds the raw rows either way), and before
     * the first run. Detached from the execution record, so a caller may keep
     * or mutate it.
     *
     * `periods` (honesty step 7b) — present only when a call's result declared
     * the period its read covered: one entry per declaring call, `{ toolName,
     * toolCallId, queried, held, readAt? }`, the period as the tool declared it
     * — the data twin of the prose block's `Period:` lines.
     *
     * @example
     * ```ts
     * const agent = Agent.create({ provider, model })
     *   .tool(replicationHealth) // returns coverage(verdict, { … })
     *   .outputSchema(Verdict)
     *   .limitsTravelWithTheAnswer()
     *   .build();
     *
     * const verdict = await agent.runTyped({ message: 'is replication healthy?' });
     * const limits = agent.answerCoverage();
     * for (const item of limits?.cannotCover ?? []) {
     *   console.log(`never covered: ${item.what} (${item.why})`);
     * }
     * ```
     */
    answerCoverage() {
        const limits = this.liveSnapshot()?.sharedState
            ?.answerCoverage;
        return limits === undefined ? undefined : structuredClone(limits);
    }
    /**
     * How far the last run's answer stands — folded from its COMMITTED record,
     * never from how sure the model sounded: `known` · `consistent` (checks ran,
     * none fired — never "verified") · `not-sure` (with the reasons) · `ask` (the
     * run paused on a question still waiting for a person) · `not-assessed`
     * (nothing on the record could be checked).
     *
     * The same pure fold as `assessAnswer` on `agentfootprint/observe`, over this
     * agent's last snapshot; it reads committed state, never events, so a later
     * reader of the same recording folds the same standing. (This call folds the
     * LIVE run. Under `redact` a recording holds the placeholder wherever the
     * policy selected a value, and a fold of it stands only on what the policy
     * left — the same standing unless a selected key is one the fold reads.) It resolves
     * asynchronously because the fold is loaded through `import()` on first use —
     * an agent that never asks does not carry it.
     *
     * Resolves to `undefined` when there is no answer to assess: before the first
     * run, while a run is in flight, and after a run that threw or whose answer a
     * rule refused (the typed error carries that verdict).
     *
     * `declarations` is the answer account's object, accepted as it is: the fold
     * reads `tools[name].rowsAt`, where an object result keeps its rows.
     *
     * @example
     * ```ts
     * await agent.run({ message: 'Which ports on switch A are down?' });
     * const a = await agent.assessment();
     * if (a?.standing === 'not-sure') {
     *   for (const r of a.reasons) console.log(r.reason); // e.g. 'empty-undeclared'
     * }
     * ```
     */
    async assessment(declarations) {
        const snapshot = this.liveSnapshot();
        if (snapshot === undefined)
            return undefined;
        // Settled before the fold, which cannot tell a crash from its committed state: an answer
        // this run RETURNED (`lastRunAnswer`, cleared at every run's start), or a pause — the fold
        // reads that one from the committed state itself. Read BEFORE the import resolves, so a
        // run started meanwhile cannot change which run is assessed.
        const paused = this.lastExecutor?.isPaused() === true;
        if (!paused && this.lastRunAnswer === undefined)
            return undefined;
        // Loaded on first use, off the default graph (the findings/peel.ts precedent).
        // The live state, read as values — never as a served record (`assess.ts` · `assessLive`).
        const { assessLive } = await import('./agent/assessment/assess.js');
        return assessLive(snapshot, declarations);
    }
    /**
     * @internal What a mount of this agent's chart handed back
     * (`core/terminalVerdict.ts` · `ReadsItsOwnOutcome`, read by every
     * composition): the ANSWER, or the terminal verdict the chart ended on. A
     * string, or the validated answer's `{ finalContent, answerValidationCommitted }`,
     * is the answer; a verdict is read ONLY from the chart's state — handed over
     * when the chart returned no result — with the one translation this agent's
     * own run boundary (`finalizeResult`) uses. The `'tell-model'` consent record
     * travels off tracked state, so it is raised by the run boundary only.
     */
    outcomeOf(sfOutput) {
        if (typeof sfOutput === 'string')
            return { answer: sfOutput };
        if (isValidatedDelivery(sfOutput))
            return { answer: sfOutput.finalContent };
        if (typeof sfOutput === 'object' && sfOutput !== null) {
            const state = sfOutput;
            const verdict = this.refusalVerdictOf(state) ?? this.validationVerdictOf(state);
            if (verdict !== undefined)
                return { verdict };
        }
        return { answer: sfOutput };
    }
    /** The records a run ends on in place of an answer, in the order the boundary raises them. */
    refusalVerdictOf(state) {
        // Reliability fail-fast (v2.11.5+) — the loop in callLLM writes the record
        // and calls $break (`reliability/failFastRecord.ts`).
        if (this.reliabilityConfig !== undefined) {
            const record = failFastRecordOf(state);
            if (record !== undefined)
                return { kind: 'reliability-fail-fast', record };
        }
        // Policy halt (v2.12+) — a `PermissionChecker` returned `halt`; the
        // toolCalls handler wrote a synthetic tool_result and the policyHalt*
        // fields, and called $break.
        const halt = state;
        if (halt.policyHaltReason !== undefined && halt.policyHaltTarget !== undefined) {
            const history = halt.history ?? [];
            const iteration = halt.policyHaltIteration ?? 1;
            // Sequence at halt time — derived from history. Includes the
            // proposed call (which DID land in history as the synthetic
            // tool_result for protocol compliance, but the policy denied
            // execution). Filter it out so callers see only dispatched
            // calls, then append the proposed entry as a hint.
            const sequenceWithoutProposed = extractSequence(history.slice(0, -1), iteration);
            return {
                kind: 'policy-halt',
                halt: {
                    reason: halt.policyHaltReason,
                    ...(halt.policyHaltTellLLM !== undefined && { tellLLM: halt.policyHaltTellLLM }),
                    sequence: [
                        ...sequenceWithoutProposed,
                        { name: halt.policyHaltTarget, args: halt.policyHaltArgs, iteration },
                    ],
                    iteration,
                    history,
                    proposed: { name: halt.policyHaltTarget, args: halt.policyHaltArgs ?? {} },
                    ...(halt.policyHaltCheckerId !== undefined && { checkerId: halt.policyHaltCheckerId }),
                },
            };
        }
        // Message-boundary refusal (7.18+) — a `messageMiddleware` returned
        // `deny`; a refusal must never be mistaken for an answer: at 'input' no
        // model was asked, and at 'output' the middleware declined to release it.
        if (this.messageMiddleware.length > 0 || this.findingsOptions !== undefined) {
            const denied = state;
            if (denied.messageDeniedReason !== undefined) {
                return {
                    kind: 'message-denied',
                    denial: {
                        reason: denied.messageDeniedReason,
                        phase: denied.messageDeniedPhase ?? 'output',
                        middleware: denied.messageDeniedBy ?? 'middleware',
                    },
                };
            }
        }
        // Evidence rails (9.35.0) — the answer still states values no tool result
        // carried, after the one revision the posture allows. `'assist'` and
        // `'guard'` never end here: their verdict is committed state and an event.
        if (this.evidenceGate?.posture === 'rails') {
            const verdict = state.unsupportedValues;
            if (verdict !== undefined && verdict.refused) {
                return {
                    kind: 'unsupported-values',
                    refusal: {
                        values: verdict.values,
                        candidates: verdict.candidates,
                        revised: verdict.revised,
                        message: evidenceRefusalSentence(verdict.values, 'rails', verdict.revised),
                    },
                };
            }
        }
        return undefined;
    }
    /** Answer validation's verdict on a run that reached the end (7.24): blocked, or never committed. */
    validationVerdictOf(state) {
        const config = this.answerValidationConfig;
        if (config === undefined)
            return undefined;
        const s = state;
        if (s.answerValidationBlocked === true && s.answerValidation !== undefined) {
            return { kind: 'answer-validation', report: s.answerValidation };
        }
        if (s.answerValidation === undefined ||
            s.answerValidationCommitted !== true ||
            typeof s.finalContent !== 'string') {
            return {
                kind: 'answer-validation',
                report: {
                    validatorId: config.id,
                    validatorVersion: config.version,
                    mode: config.mode,
                    status: 'unverified',
                    checked: 0,
                    failed: 0,
                    unreachable: 0,
                    notApplicable: 0,
                    checks: [],
                    resolvedRefs: [],
                    schemaAccepted: false,
                    reason: 'delivery-incomplete',
                },
            };
        }
        return undefined;
    }
    finalizeResult(executor, result) {
        const paused = this.detectPause(executor, result);
        if (paused)
            return paused;
        const snap = executor.getSnapshot();
        const state = snap.sharedState;
        const refusal = this.refusalVerdictOf(state);
        if (refusal !== undefined) {
            if (refusal.kind === 'message-denied' && refusal.denial.phase === 'output') {
                // The chart stopped cleanly, so ErrorBridge's onRunFailed cannot
                // close public streams. This API boundary owns the refusal on both
                // run and resume. Name it as a virtual stage, not a fabricated
                // engine failure, and never echo policy text into a public stream.
                this.dispatcher.dispatch({
                    type: 'agentfootprint.error.fatal',
                    payload: {
                        error: 'The output policy withheld this answer.',
                        stage: '__output_admission__',
                        scope: 'run',
                    },
                    meta: buildEventMeta({ runtimeStageId: '__output_admission__' }, this.currentRunContext),
                });
            }
            // The verdict was decoded from the LIVE state above; the snapshot a
            // fail-fast error carries is the run's audit trail — "consumers persist
            // this" (`ReliabilityFailFastError.snapshot`) — so it is the SERVED one.
            throw terminalErrorOf(refusal, this.getLastSnapshot());
        }
        // Credential-consent translation (8.6.0, `'tell-model'` only) — a tool
        // DECLARED a credential, the provider answered `authorization-required`,
        // and the model was told to route around it. The run reached the end
        // anyway, which means it is about to hand back an answer string for work a
        // tool never did.
        //
        // `AgentOutput` is a bare string, so there is nowhere to annotate "…and a
        // consent is still outstanding". An event alone would relocate the silent
        // success rather than remove it. So the turn raises, carrying the URL to
        // the caller — the party that can actually act on it. Under the default
        // `'pause'` this is unreachable for the first block (the run pauses and
        // `detectPause` returned above); it still fires if a RESUME found consent
        // ungranted and the model then finished the turn on its own.
        if (this.consentOutstanding.size > 0) {
            const [record] = [...this.consentOutstanding.values()];
            if (record) {
                this.consentOutstanding.clear();
                throw new CredentialConsentRequiredError(record);
            }
        }
        if (result instanceof Error)
            throw result;
        const invalid = this.validationVerdictOf(state);
        if (invalid !== undefined)
            throw terminalErrorOf(invalid);
        if (this.answerValidationConfig !== undefined)
            return state.finalContent;
        if (typeof result === 'string')
            return result;
        throw new Error('Agent: unexpected result shape — expected final-answer string');
    }
    // ─── Chart assembly ────────────────────────────────────────────
    buildChart() {
        const governsOutput = hasOutputMiddleware(this.messageMiddleware);
        const provider = this.provider;
        const model = this.model;
        const temperature = this.temperature;
        const maxTokens = this.maxTokens;
        const systemPromptValue = this.systemPromptValue;
        const registry = this.registry;
        // (registryByName + toolSchemas redefined below using
        // `augmentedRegistry` which adds the auto-attached `read_skill`
        // tool when Skills are registered.)
        const _legacyRegistry = registry;
        void _legacyRegistry;
        const maxIterations = this.maxIterations;
        const pricingTable = this.pricingTable;
        const costBudget = this.costBudget;
        const permissionChecker = this.permissionChecker;
        const credentialProvider = this.credentialProvider;
        const artifactStore = this.artifactStore;
        // Cache layer (v2.6) — capture for the seed + chart-build closures.
        // `systemPromptCachePolicy` is fed into the CacheDecision subflow's
        // inputMapper. `cacheStrategy` is consulted by BuildLLMRequest at
        // run-time (Phase 7+ for the actual prepareRequest call). For
        // Phase 6b the chart mounts the stages but BuildLLMRequest is a
        // pass-through; Phase 7 lights up the strategy call.
        const systemPromptCachePolicy = this.systemPromptCachePolicy;
        const cachingDisabled = this.cachingDisabledValue;
        const cacheStrategy = this.cacheStrategy;
        // ── Steps-as-data (9.18.0): fold the declared procedures ONCE ──────
        // One frozen plan per stepped skill, threaded as closures into the four
        // seams that consult it (Evaluate re-key, tools-slot narrowing,
        // tool-calls advance/skip, the Route step judge + nudge). Empty →
        // `stepPlanFor` stays undefined and not one of those seams runs a new
        // line (zero-cost-when-unused).
        const stepPlans = foldStepPlans(this.injections);
        const stepPlanFor = stepPlans.size > 0 ? (skillId) => stepPlans.get(skillId) : undefined;
        // seed extracted to ./agent/stages/seed.ts (v2.11.2). Factory takes
        // chart-build-time constants + per-run mutable accessors so the
        // resume side-channel and current run id remain dynamic.
        // toolSchemas is finalized further down; pass a getter that reads
        // the eventual const at stage-execution time.
        let toolSchemasResolved = [];
        // The `Tool.wants` declarations seed puts on the record (9.88.0), late-bound
        // for the same reason `toolSchemasResolved` is: the registry is harvested
        // further down, and seed is built here. Stays `undefined` unless the
        // evidence gate arms the staged-refs nudge — the exact condition under
        // which request assembly can compose that line at all.
        let seededToolWants;
        // The stores the conversation itself is kept in — the durable anchor seed
        // resolves the turn number against. Deduplicated (several memories over
        // one store is the common shape) and reference-stable, so the scan runs
        // once per store per run.
        const conversationStores = Array.from(new Set(this.memories
            .filter((m) => m.write !== undefined && m.corpus === undefined && m.store !== undefined)
            .map((m) => m.store)));
        // The inputs layer (honesty layer 2) — late-bound like `toolSchemas`: the
        // registry that says whether a tool is ruled is harvested further down.
        let seededRuledTools;
        let seededHonestyLayers;
        // The findings ledger's restore (9.101.0), WIDENED (honesty layer 2): wired
        // whenever the ledger OR an honesty layer is armed — an agent without
        // `.findings()` files argument rows too, and a continued conversation must
        // keep them. Negative-claim observations also persist independently of those
        // layers. Still value-conditional inside seed (only when rows exist).
        let ledgerRestoreArmed = this.findingsOptions !== undefined || this.negativeClaimsConfig !== undefined;
        const consumeResumeLedger = () => {
            const l = this.pendingResumeFindingsLedger;
            this.pendingResumeFindingsLedger = undefined;
            return l;
        };
        const seed = buildSeedStage({
            maxIterations,
            // The forced-output tool's name (9.88.0) — a build-time constant, put on
            // the record once so a rebuild can name it without reading a receipt.
            ...(this.outputEnforcement?.schemaTool !== undefined && {
                forcedOutputToolName: this.outputEnforcement.schemaTool.name,
            }),
            cachingDisabled,
            ...(costBudget !== undefined && { costBudgetOnExceed: costBudget.onExceed }),
            get toolSchemas() {
                return toolSchemasResolved;
            },
            get toolWantsByName() {
                return seededToolWants;
            },
            get ruledTools() {
                return seededRuledTools;
            },
            get honestyLayers() {
                return seededHonestyLayers;
            },
            // The time layer's clock: the draft `run()` read (refusing a run with no
            // zone), stamped by seed as the turn's one `clock` row. Absent → the
            // deps object seed always had.
            // The caller's clock belongs to the agent's OWN run: a member (another
            // runner's executor) gets the builder's fallback, never a concurrent
            // direct caller's `time` (`memberRun.ts`).
            ...(this.timeOptions !== undefined && {
                timeClock: (scope) => this.ownScopes.has(scope)
                    ? this.seedClockDraft()
                    : draftClock(undefined, this.timeOptions ?? {}),
            }),
            // The time layer's reader: seed reads the person's words once per turn,
            // after the clock. Absent → nothing is read.
            ...(this.timeOptions?.reader !== undefined &&
                this.timeOptions.policy !== undefined && {
                timeReader: { reader: this.timeOptions.reader, policy: this.timeOptions.policy },
            }),
            // Declared sources (honesty layer 2): the seed twin plants `_findings.from`
            // on ruled tools, and the run's `messageFrom` constant is written — only
            // under the arm (refused at build without the inputs layer).
            ...(this.argumentSourcesArmed === true && { argumentSources: true }),
            // The answer check's tool-call ledger — value-conditional, so an agent
            // without answer validation or negative claims keeps its prior shape.
            ...((this.answerValidationConfig !== undefined ||
                this.negativeClaimsConfig !== undefined) && { toolCallLedger: true }),
            ...(this.negativeClaimsConfig !== undefined && { negativeClaimsArmed: true }),
            get consumePendingResumeFindingsLedger() {
                return ledgerRestoreArmed ? consumeResumeLedger : undefined;
            },
            consumePendingResumeHistory: () => {
                const h = this.pendingResumeHistory;
                this.pendingResumeHistory = undefined;
                return h;
            },
            ...(this.evidenceGate !== undefined &&
                this.evidenceGate.posture !== 'assist' && {
                consumePendingEvidenceRecovery: () => {
                    const recovery = this.pendingEvidenceRecovery;
                    this.pendingEvidenceRecovery = undefined;
                    return recovery;
                },
            }),
            consumePendingResumeFolded: () => {
                const f = this.pendingResumeFolded;
                this.pendingResumeFolded = undefined;
                return f;
            },
            // The findings ledger (9.101.0): the arm, the serve mode it puts on the
            // record (the `forcedOutputToolName` precedent — a build-time constant
            // the rebuild reads instead of the receipt), and the continued
            // conversation's rows, all under the one gate — an unarmed agent hands
            // seed exactly the deps object it always did.
            ...(this.findingsOptions !== undefined && {
                findings: true,
                findingsServe: this.findingsOptions.serve ?? 'ledger-and-facts',
                findingsContextBytes: this.findingsOptions.maxContextBytes,
                // The answer-turn ask (9.103.0): VALUE-conditional inside the arm —
                // the default `'none'` threads nothing and writes no key.
                ...(this.findingsOptions.answerAsk === 'quote-facts' && {
                    findingsAnswerAsk: 'quote-facts',
                }),
            }),
            // The declared ontology (9.106.0): the whole frozen map, which seed
            // writes ONCE as the run constant `ontology` — the `findingsServe`
            // precedent, a build-time fact the rebuild reads from the RECORD. An
            // unarmed agent hands seed exactly the deps object it always did.
            ...(this.ontology !== undefined && {
                ontology: this.ontology,
                ontologyTools: () => this.ontologyTools,
            }),
            // The conversation's inherited skill cursor (SG-C). Consumed (cleared)
            // on every run; HONORED only when the mounted graph declared
            // `continuity: 'conversation'` — the same one-option-one-behavior gate
            // the write side (`checkpoint()`) applies.
            consumePendingResumeSkillCursor: () => {
                const c = this.pendingResumeSkillCursor;
                this.pendingResumeSkillCursor = undefined;
                return c;
            },
            restoreSkillCursor: this.skillGraphCascade?.continuity === 'conversation',
            // Steps (9.18.0): gate the per-run stepPointer/stepNudgeSpent reset on
            // the feature, so every other agent seeds exactly the keys it always did.
            ...(stepPlanFor !== undefined && { hasSteps: true }),
            // Escalation (9.19.0): same gate discipline — the per-run counter/flip
            // reset exists only when the policy does (de-escalation IS the seed).
            ...(this.skillBrains?.escalation !== undefined && { hasEscalation: true }),
            // The evidence gate's one bounded revision (9.35.0) — a per-turn budget,
            // reset here. Only a posture that can revise writes the key.
            ...((this.typedNegativeClaimsConfig !== undefined ||
                (this.evidenceGate !== undefined && this.evidenceGate.posture !== 'assist')) && {
                hasEvidenceRevision: true,
            }),
            // Under an enforcing answer validation the tools stay off for a run
            // resumed after its revision was asked (`toolsOffAfterRevision`).
            ...(this.revisionWithholdsTools() && { toolsOffAfterRevision: true }),
            getCurrentRunId: () => this.currentRunContext?.runId,
            // WHO this run is for, when the caller named nobody (9.10.0). Seed uses
            // it for exactly one rung of the identity ladder — see `seedFrom` — and
            // an explicit identity outranks it there. Read through an accessor for
            // the same reason the runId is: the chart is built once, and this
            // changes every run.
            getCurrentSessionId: () => this.currentRunContext?.sessionId,
            // Whether this stage runs in THIS agent's own executor, or inside another
            // runner's (a composition, a chart a caller built) — where the accessors
            // above describe some other run (`memberRun.ts`). Asked per scope.
            isOwnRun: (scope) => this.ownScopes.has(scope),
            // WHICH TURN THIS IS (9.6.0). Only memories that WRITE the conversation
            // are consulted: they are the ones whose entry ids are turn-stamped, and
            // a corpus (`.rag(...)`, which reads under its own namespace) has no
            // turns at all. Empty list → seed stays synchronous and makes no store
            // call, so an agent without memory is unchanged.
            ...(conversationStores.length > 0 && { conversationStores }),
            // The `'input'` half of the message chain, run BEFORE `userMessage` and
            // `history` are committed — see SeedStageDeps.messageMiddleware.
            ...(this.messageMiddleware.length > 0 && { messageMiddleware: this.messageMiddleware }),
            // `.configure()` — seed is where run-level facts are decided and
            // committed, so the resolver rides that commit. The closure supplies
            // the build-time defaults so a resolver can decide RELATIVE to them
            // ("upgrade the model when the message is long") instead of having to
            // restate them.
            ...(this.runConfigFn && {
                resolveRunConfig: (input) => {
                    const ctx = {
                        message: input.message,
                        ...(input.identity !== undefined && { identity: input.identity }),
                        runId: this.currentRunContext?.runId ?? 'unknown',
                        defaults: { model, instructions: systemPromptValue },
                    };
                    return this.runConfigFn?.(ctx);
                },
            }),
        });
        // Tool registry composition extracted to ./agent/buildToolRegistry.ts.
        // Composes static .tool() registry + auto-attached read_skill (+ the
        // auto-attached `present` tool when a store is attached, 9.22.0) +
        // skill-supplied tools (with autoActivate scoping); validates
        // name uniqueness; produces the dispatch map.
        const { registryByName, toolSchemas, toolDeclaringSkills, toolClaimants } = buildToolRegistry(registry, this.injections, {
            hasArtifactStore: artifactStore !== undefined,
            // The reserved-argument refusal (9.101.0) — only when the ledger is
            // armed may a registry tool's own `_findings` be refused.
            ...(this.findingsOptions !== undefined && { findings: true }),
            // …and, under declared sources (either door), a RULED tool's: the arm
            // plants `_findings.from` on it and its sentences name that argument.
            ...(this.argumentSourcesArmed === true && { argumentSources: true }),
        });
        // The declared ontology's tool edges (9.106.0): every name a node's `via`
        // names must be a tool this registry can dispatch — the static registry,
        // the framework's doors and every skill's tools, `registryByName` being
        // the ONE map that holds all of them. Refused at BUILD, naming the
        // ontology and the tool: a map that points a need at a tool nobody can
        // call is a map that lies, and only the operator can fix it. Provider-
        // served tools are only met at dispatch and cannot be named here.
        if (this.ontology !== undefined) {
            // The join (9.108.0): the same walk, collecting which skills declare
            // each `via` name — `toolDeclaringSkills` is the registry's own fact,
            // in declaration order; a name no skill declares is not in it.
            const tools = {};
            for (const [nodeId, node] of Object.entries(this.ontology.nodes)) {
                for (const held of node.sources ?? []) {
                    for (const toolName of held.via ?? []) {
                        if (!registryByName.has(toolName)) {
                            throw new Error(`Agent: ontology '${this.ontology.id}' names tool '${toolName}' that is not ` +
                                `registered (node '${nodeId}', source '${held.source}'). Register the tool ` +
                                `with .tool() or on a skill, or drop it from the node's \`via\`.`);
                        }
                        const owners = toolDeclaringSkills.get(toolName);
                        if (owners !== undefined && owners.length > 0)
                            tools[toolName] = [...owners];
                    }
                }
            }
            this.ontologyTools = Object.keys(tools).length > 0 ? Object.freeze(tools) : undefined;
        }
        // A statically registered tool that declares `wants` on an agent with no
        // store is configuration that lies: every call would be refused at
        // dispatch for a gap only the operator can close. Refused at BUILD,
        // naming the tool (provider-served tools are only met at dispatch — the
        // dispatch-time refusal covers those).
        if (artifactStore === undefined) {
            for (const [toolName, registered] of registryByName) {
                if (registered.wants !== undefined) {
                    throw new Error(`Agent: tool '${toolName}' declares artifact arguments (wants: ` +
                        `{ ${Object.entries(registered.wants)
                            .map(([arg, kind]) => `${arg}: '${kind}'`)
                            .join(', ')} }) but no artifact store is attached, so its refs could never ` +
                        `resolve and every call would be refused. Pass \`artifacts\` to Agent.create ` +
                        `(inMemoryArtifacts(), fileArtifacts({ directory }), sqliteArtifacts({ file }), ` +
                        `or any ArtifactStore), or drop the tool's \`wants\`.`);
                }
                // The same law for the typed-HITL half (9.24.0): a statically
                // registered `checkInComponent.propsRef` on a storeless agent is a
                // ref the screen could never redeem — every tripped gate would be
                // refused at raise for a gap only the operator can close.
                if (registered.checkInComponent?.propsRef !== undefined) {
                    throw new Error(`Agent: tool '${toolName}' declares checkInComponent.propsRef ` +
                        `('${registered.checkInComponent.propsRef}') but no artifact store is attached, ` +
                        `so the answering screen could never redeem it and every tripped check-in would ` +
                        `be refused at raise. Pass \`artifacts\` to Agent.create (inMemoryArtifacts(), ` +
                        `fileArtifacts({ directory }), sqliteArtifacts({ file }), or any ArtifactStore), ` +
                        `or carry the payload inline as checkInComponent.props.`);
                }
            }
        }
        // The composedOf drift gate (9.76.0), judged HERE and not at defineTool —
        // the one moment the catalog is complete. A tool that declares its
        // ingredients (`composedOf`, the runbookAsTool law) whose ingredient was
        // renamed or never registered fails the BUILD by name, instead of failing
        // its first run inside a stage. Tools delivered by a ToolProvider are
        // invisible to this check (no build-time list — the 9.72.0 caveat), so
        // with a provider configured an unmatched name is a dev-mode heads-up
        // rather than a refusal: the ingredient may genuinely arrive at dispatch.
        for (const [composedName, registered] of registryByName) {
            for (const ingredient of registered.composedOf ?? []) {
                if (registryByName.has(ingredient))
                    continue;
                if (this.externalToolProvider !== undefined) {
                    if (isDevMode()) {
                        // eslint-disable-next-line no-console
                        console.warn(`[agentfootprint] tool '${composedName}' declares composedOf ingredient ` +
                            `'${ingredient}', which is not in the static catalog. A ToolProvider is ` +
                            `configured, so it may arrive at dispatch — but provider-delivered tools ` +
                            `cannot be drift-checked at build. If '${ingredient}' is static, this is ` +
                            `the rename the check exists to catch.`);
                    }
                    continue;
                }
                throw new Error(`Agent: tool '${composedName}' declares composedOf ingredient '${ingredient}', ` +
                    `but no tool of that name is registered on this agent. The declaration names ` +
                    `the tools its procedure calls through ctx.tools, and an ingredient that is ` +
                    `not in the dispatch map fails at its first inner call — refused at build ` +
                    `instead. Register '${ingredient}' (or fix the name in composedOf).`);
            }
        }
        // Late-bind toolSchemas into the seed stage's deps (the factory was
        // built earlier with a getter; this resolves the actual value).
        toolSchemasResolved = toolSchemas;
        // ── The inputs layer (honesty layer 2) — armed ONCE, here ──────────
        // A registered tool (static, skill-carried, or an MCP tool registered on
        // the builder) that declares argument rules arms the mount — the
        // `toolGrounding` harvest precedent, over the same catalog. Tools a
        // ToolProvider serves are invisible here, so `.inputsLayer()` arms the
        // mount for them; without it their calls are refused at dispatch (fail
        // closed). Unarmed: nothing below is mounted, decorated, read or written.
        const ruledTools = new Map([...registryByName.entries()].filter(([, tool]) => tool.askOrAssume !== undefined || tool.period !== undefined));
        const inputsArmed = ruledTools.size > 0 || this.inputsLayerOption === true;
        this.inputsLayerArmed = inputsArmed;
        // DECLARED SOURCES (honesty layer 2, `.findings({ argumentSources: true })`
        // or `.inputsLayer({ argumentSources: true })`) — the model's
        // `_findings.from`, checked by the inputs layer. It has nothing to act on
        // without the layer, and configured-and-inert looks exactly like
        // configured-and-working: refused here, at build. (The `.inputsLayer()`
        // door arms the layer itself, so only the findings door can reach this.)
        const argumentSources = this.argumentSourcesArmed === true;
        if (argumentSources && !inputsArmed) {
            throw new Error('Agent: .findings({ argumentSources: true }) needs the inputs layer — the declared ' +
                'sources are read and checked on the calls of a tool that declares argument rules ' +
                '(askOrAssume), and this agent registers none. Register such a tool, or arm the ' +
                'layer for ToolProvider tools with .inputsLayer() (which can arm the sources itself: ' +
                '.inputsLayer({ argumentSources: true })).');
        }
        // ── The results layer (honesty layer 3, step 7b) — armed ONCE, here ──
        // A registered tool that declares a `ToolPeriod` (the inputs layer's
        // declaration: which argument sets the period) arms the mount, over the
        // same catalog: its results owe a period, and a result that declares none
        // is recorded as silence. `.resultsLayer()` arms it for tools the build
        // cannot see and for tools that declare a period only on their results.
        // Unarmed: nothing below is mounted, read or written.
        const resultsArmed = [...registryByName.values()].some((tool) => tool.period !== undefined) ||
            this.resultsLayerOption === true;
        this.resultsLayerArmed = resultsArmed;
        if (inputsArmed && ruledTools.size > 0)
            seededRuledTools = ruledTools;
        // ── The answer layer (honesty layer 4) — armed by its option alone ─────
        const answerArmed = this.answerLayerOption !== undefined;
        // The run constant names every armed layer; the restore is wired whenever
        // one is (adopted Q6) — still value-conditional inside seed.
        if (inputsArmed || resultsArmed || answerArmed) {
            seededHonestyLayers = honestyLayersOf({
                inputs: inputsArmed,
                argumentSources,
                results: resultsArmed,
                answer: answerArmed,
            });
            ledgerRestoreArmed = true;
        }
        // The time layer's rows ride the same ledger: a continued conversation
        // keeps its earlier turns' clocks (each turn files its own).
        if (this.timeOptions !== undefined)
            ledgerRestoreArmed = true;
        // The script record's rows too: a continued conversation keeps each
        // earlier call's facts — seed drops their text (`scripts/rows.ts`).
        const scriptRecordArmed = this.evidenceGate?.code === 'match' || this.scriptTravel !== undefined;
        if (scriptRecordArmed)
            ledgerRestoreArmed = true;
        // The gate's admissible set, for the record (9.50.0): declared hops from
        // the cursor plus the open skills — the SAME two resolvers the read_skill
        // offer and the refusal messages are built from (`readSkillOfferFor`),
        // composed once here so `context.evaluated.cursorMove.reachable` can
        // never drift from the verdicts. Open ids are fixed after build, so they
        // are computed once, exactly as the offer builder does.
        const openForReachable = this.skillGraphReachable ? this.openSkillIds() : [];
        // Per-role skill visibility (9.11.0). The RESOLVER is handed to the
        // Injection Engine, the fact's one owner: its Gather stage resolves the
        // ids once per iteration, BEFORE anything decides, so the cursor and the
        // evaluator refuse a hidden skill on every activation path; the mount
        // publishes them on scope for every model-facing composer after it.
        const hiddenSkillIdsForEngine = this.hiddenSkillIdsNow();
        const injectionEngineSubflow = buildInjectionEngineSubflow({
            injections: this.injections,
            ...(hiddenSkillIdsForEngine && { hiddenSkillIds: hiddenSkillIdsForEngine }),
            ...(this.skillGraphNextSkill && { nextSkill: this.skillGraphNextSkill }),
            ...(this.skillGraphExplainNextSkill && {
                explainNextSkill: this.skillGraphExplainNextSkill,
            }),
            ...(this.skillGraphReachable && {
                reachableSkills: (currentSkillId) => [
                    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                    ...new Set([...this.skillGraphReachable(currentSkillId), ...openForReachable]),
                ],
            }),
            ...(this.skillGraphSupersededEntries && {
                supersededEntries: this.skillGraphSupersededEntries,
            }),
            // Steps (9.18.0): the Evaluate stage owns the pointer's tenure re-key.
            ...(stepPlanFor !== undefined && { stepPlanFor }),
            // The maps kernel (9.58.0): the Evaluate stage owns the engagement
            // advance, with the same ctx the triggers gate on.
            ...(this.mapsPlan !== undefined && { engagement: this.mapsPlan }),
        });
        // The turn-start slot's occupant (SG-C). RouteTurn — the cascade — mounts
        // ONLY when the graph runs it: a classifier is configured, or the mount
        // declared `continuity: 'conversation'`. Every other graph keeps exactly
        // what it had: scorer graphs keep PickEntry byte-for-byte (no cascade, no
        // turn_routed — the zero-delta law), plain graphs keep no stage at all.
        const cascade = this.skillGraphCascade;
        const runsCascade = cascade !== undefined &&
            cascade.turnRouting !== undefined &&
            (cascade.turnRouting.scorer !== undefined || cascade.continuity === 'conversation');
        const hiddenSkillIdsForRouting = this.hiddenSkillIdsNow();
        const routeTurnStage = runsCascade
            ? makeRouteTurnStage({
                turnRouting: cascade.turnRouting,
                ...(this.skillGraphScoreEntries && { scoreEntries: this.skillGraphScoreEntries }),
                continuity: cascade.continuity,
                strictness: cascade.strictness,
                isNode: (id) => cascade.nodeIds.has(id),
                ...(hiddenSkillIdsForRouting && { hiddenSkillIds: hiddenSkillIdsForRouting }),
                // The tier-3 decider (9.19.0) — the out-of-band menu resolver.
                // `AgentBuilder.build()` already refused a decider on a mount
                // that never runs the cascade, so it always reaches its stage.
                ...(this.skillBrains?.decider !== undefined && {
                    decider: {
                        provider: this.skillBrains.decider.provider,
                        ...(this.skillBrains.decider.model !== undefined && {
                            model: this.skillBrains.decider.model,
                        }),
                        defaultModel: model,
                        runConfigured: this.runConfigFn !== undefined,
                    },
                }),
            })
            : undefined;
        // Relevance entry router — a once-per-turn stage (off the ReAct loop) that
        // picks the starting skill by embedding similarity. Only built (and mounted)
        // when the graph was created with `.entryByRelevance()` AND the cascade did
        // not subsume it.
        const pickEntryStage = !routeTurnStage && this.skillGraphScoreEntries
            ? makePickEntryStage(this.skillGraphScoreEntries)
            : undefined;
        // With `.configure()` the base prompt becomes a function of the run: the
        // slot reads the instructions seed committed, falling back to `.system()`
        // when the resolver left it alone. `reason` follows the same fork so the
        // context record names whichever one actually supplied the text.
        // Per-slot budgets (8.11.0). Each key is forwarded only when the consumer
        // set it, so an unset slot keeps its own default rather than being pinned
        // to `undefined` at the call site.
        const budget = this.contextBudget;
        const systemPromptSubflow = buildSystemPromptSlot({
            ...(this.runConfigFn
                ? {
                    prompt: (args) => args.instructions ?? systemPromptValue,
                    reason: (args) => args.instructions !== undefined ? 'Agent.configure()' : 'Agent.system()',
                }
                : {
                    prompt: systemPromptValue,
                    reason: 'Agent.system()',
                }),
            ...(budget?.systemPrompt !== undefined && { budgetCap: budget.systemPrompt }),
        });
        const messagesSubflow = buildMessagesSlot({
            ...(budget?.messages !== undefined && { budgetCap: budget.messages }),
        });
        // Cache shared between buildToolsSlot (writer, each iteration) and
        // buildToolCallsHandler (reader, same iteration). Holds the resolved
        // Tool[] from `provider.list(ctx)` so dispatch doesn't re-invoke `list()`
        // — vital for async network providers.
        //
        // LIFETIME, stated correctly (9.92.0): the chart is built ONCE, at
        // construction (`initChart`), and reused by every `run()` and `resume()`
        // of this Agent — so this cache and the record below outlive a run. They
        // are safe because one Agent runs one turn at a time and the slot
        // overwrites them every iteration before dispatch reads them. What that
        // does NOT cover is a resume in a FRESH instance, whose closure is empty
        // and whose Compose never re-runs before the resumed dispatch — which is
        // why the pause paths carry `pausedToolParty` on the checkpoint.
        const providerToolCache = { current: [] };
        // Who put each name on the wire (9.92.0) — the same closure-shared shape,
        // written by the tools slot's merge and read by dispatch so the party
        // whose contract the model read is the party that answers. `lastServed`
        // is the run's memory of the same fact, for a name that left the wire.
        const servedTools = { current: new Map(), lastServed: new Map() };
        // THE ONE DISPATCH RESOLVER, for the inputs layer (honesty layer 2): built
        // from the SAME deps objects the dispatch handler builds its own from, so
        // the layer reads a call's rules off the implementation that will answer.
        const resolveForLayer = buildToolResolver({
            registryByName,
            ...(this.externalToolProvider && { externalToolProvider: this.externalToolProvider }),
            ...(this.externalToolProvider && { providerToolCache }),
            servedTools,
            toolClaimants,
            notServed: notServedResult,
        });
        // A tool's `displayName` — the app's words for a call, read by a person — read
        // through the same resolver, so a ToolProvider's tool is read as well, and through
        // the one rule (`lib/displayName.ts` · `readDisplayName`), so a value nobody vetted
        // is never printed. Wired only where a tool can declare one: on every other agent
        // the final branch is the one it always was.
        const displayNameOf = this.externalToolProvider !== undefined ||
            [...registryByName.values()].some((tool) => tool.displayName !== undefined)
            ? (toolName) => readDisplayName(resolveForLayer(toolName).tool?.displayName)
            : undefined;
        const readSkillFor = this.readSkillOfferFor();
        // Per-role skill visibility. The Injection Engine resolves the ids once per
        // iteration and publishes them on scope (`buildInjectionEngineSubflow.ts` ·
        // `hiddenSkillIdsOf`); the tools slot reads that published set through its
        // mount. Since 9.86.0 the description is NOT the only reader — the read_skill gate's refusals, the
        // `skill.rejected` payload, the propose-transition refusal and the dispatch
        // roster all filter through that published set
        // (`stages/toolCalls.ts` · "── Skill-graph read_skill GATE" the gate, `toolCalls.ts` ·
        // "── THE REFUSAL SPEAKS WITH THE FILTERED SETS" the propose-transition refusal,
        // `toolCalls.ts` · `dispatchRoster`). Nothing is cached here: an
        // earlier draft of 9.84.0 cached the resolved ids so the gate could filter
        // the self-call notice's move offer through them; the notice no longer names
        // a destination at all (see `selfCallNotice`), so there is nothing left to
        // filter and the cache went with the clause that needed it.
        // Registration-time owner stamps (9.60.0) — the identity edges the
        // integrity checks read. Built once per chart from the registry.
        const toolOwners = new Map(registry
            .filter((r) => r.tool.owner !== undefined)
            .map((r) => [r.name, r.tool.owner]));
        // The declared argument-ground edges (9.60.0; catalog-wide since 9.72.0)
        // — read by callLLM's dangling-reference and unsupported-argument checks.
        //
        // Harvested from `registryByName` — the FULL declared catalog (static
        // `.tool()` registrations PLUS every skill-carried tool, autoActivate/
        // scoped ones included) — and not from the static registry alone, which
        // was the field bug a consumer's MCP parity work surfaced: an app whose
        // `argumentsFrom` tools all ride skills (delivered when a skill
        // activates) never armed the choice-seam pair, and its disposition rows
        // read {checked: 0, notApplicable: 1} forever while the app's own
        // comments believed the checks ran.
        //
        // ARMING IS COMPUTABLE UP FRONT, and that is a build-time fact, not a
        // hope: a skill tool's DECLARATION is known at chart build (defineSkill
        // carries the Tool object into `buildToolRegistry`'s dispatch map) even
        // though the tool reaches the model only after its skill activates — so
        // this harvest, and the `dangling` flag `beginIntegrityRun` reads at run
        // start, may see the whole catalog before any skill has fired. The
        // runtime checks stay correctly scoped on their own: dangling-reference
        // intersects this map with the tools THIS call actually served, and
        // unsupported-argument with the calls the model actually made.
        //
        // CAVEAT, stated rather than papered over: ToolProvider-DELIVERED tools
        // remain invisible here. `ToolProvider.list(ctx)` is opaque and
        // per-iteration — there is no build-time list to harvest — so a provider
        // tool declaring `argumentsFrom` arms nothing. (MCP tools are NOT in that
        // hole: `mcpClient(...).tools()` registers them statically, and 9.71.0
        // carries their declarations across the wire.)
        const toolGrounding = new Map([...registryByName.entries()]
            .filter(([, tool]) => tool.argumentsFrom !== undefined)
            .map(([name, tool]) => [name, tool.argumentsFrom]));
        this.integrityDanglingPresent = toolGrounding.size > 0;
        // The column-type contract's declared half (9.78.0) — harvested from the
        // SAME catalog and with the same ToolProvider caveat, so an MCP-carried
        // `resultColumns` (which rides `_meta`) arms the check exactly as a
        // locally-defined one does.
        // `flatMap` rather than filter-then-assert: the narrowing is real here,
        // so the twin harvest above's non-null assertion is not inherited.
        const toolColumns = new Map([...registryByName.entries()].flatMap(([name, tool]) => tool.resultColumns === undefined ? [] : [[name, tool.resultColumns]]));
        this.integrityColumnsPresent = toolColumns.size > 0;
        // The staged-refs join's other half (grounded numbers): `Tool.wants` by
        // tool name, harvested the same way and with the same ToolProvider caveat.
        // Consumed only by the evidence gate (the callLLM nudge and the recheck
        // correction), so an agent without the gate never reads it.
        const toolWants = toolWantsOf(registryByName);
        // …and onto the record (9.88.0), under the SAME arming condition callLLM
        // uses below, so the rebuild composes the nudge exactly when the run could.
        // A plain record: a `Map` written to scope round-trips to `{}`.
        if (this.evidenceGate?.nudge === true && toolWants.size > 0) {
            seededToolWants = Object.fromEntries([...toolWants].map(([name, kinds]) => [name, kinds]));
        }
        const toolsSubflow = buildToolsSlot({
            tools: toolSchemas,
            ...(toolOwners.size > 0 && { toolOwners }),
            // The kernel's map cards, for the compose-seam integrity backstop.
            ...(this.mapsPlan !== undefined && {
                mountedMaps: this.mapsPlan.maps.map((m) => ({ id: m.id, toolNames: m.toolNames })),
            }),
            integrityLedger: this.integrityLedgerHolder,
            ...(this.externalToolProvider && { toolProvider: this.externalToolProvider }),
            ...(this.externalToolProvider && { providerToolCache }),
            // The caller of THIS run, for `ToolDispatchContext.callerIdentity`: the
            // instance's caller on its own run, the member's run on a mount — never a
            // derived identity (the `hiddenSkillIdsNow` precedent).
            ...(this.externalToolProvider && {
                callerIdentity: (scope) => this.ownScopes.has(scope) ? this.lastRunIdentity : memberIdentityIn(scope),
            }),
            toolClaimants,
            servedTools,
            ...(readSkillFor && { readSkillFor }),
            ...(budget?.tools !== undefined && { budgetCap: budget.tools }),
            // Steps (9.18.0): per-step narrowing + banner + the skip_step offer.
            ...(stepPlanFor !== undefined && { stepPlanFor }),
            // The findings ledger (9.101.0): the ONE decoration site is inside this
            // slot; the gate rides in value-conditionally.
            ...(this.findingsOptions !== undefined && { findings: true }),
            // The inputs layer (honesty layer 2): the same site decorates a ruled
            // tool's schema first — value-conditional, the same grammar.
            ...(inputsArmed && { inputsLayer: true }),
            // …and, under `.time()`, the ONE served time line: the windows the reader settled this turn
            // (`.time({ reader })`, step T6b) in each served period tool's form — and the window the
            // person set in the app's time control (`time.window`), a fact about the person's turn,
            // served on every request whether or not a served tool declares a period (G14), so it
            // needs no inputs layer. `reader` (write the line every composition) only beside the
            // inputs layer, the one place a reading is asked and served.
            ...(this.timeOptions !== undefined && {
                timeWindows: {
                    ...(this.timeOptions.zone !== undefined && { appZone: this.timeOptions.zone }),
                    ...(inputsArmed && this.timeOptions.reader !== undefined && { reader: true }),
                },
            }),
            // …and, under `.time()` (step T8), the turn's time limits in the same line.
            ...(inputsArmed && this.timeOptions !== undefined && { timeLimits: true }),
            // …and, under declared sources, plants `_findings.from` on ruled tools only.
            ...(argumentSources && { argumentSources: true }),
            // Tool choice by classifier (9.105.0): the pick and the narrowing live
            // inside this slot too, at the same site; value-conditional.
            ...(this.toolChoiceOptions !== undefined && {
                toolChoice: {
                    classifier: this.toolChoiceOptions.classifier,
                    ...(typeof this.toolChoiceOptions.serve === 'object' && {
                        top: this.toolChoiceOptions.serve.top,
                    }),
                    ...(this.toolChoiceOptions.alwaysServe !== undefined && {
                        alwaysServe: this.toolChoiceOptions.alwaysServe,
                    }),
                },
            }),
        });
        // callLLM extracted to ./agent/stages/callLLM.ts (v2.11.2). Same
        // late-binding pattern as seed for toolSchemas (computed below).
        const callLLM = buildCallLLMStage({
            onReplayPrepared: (preparation) => {
                if (this.checkpointTracker !== undefined &&
                    this.inFlightRunId === this.checkpointTracker.runId)
                    retainReplayRetirements(this.checkpointTracker, preparation);
            },
            ...((this.typedNegativeClaimsConfig !== undefined ||
                (this.evidenceGate !== undefined && this.evidenceGate.posture !== 'assist')) && {
                hasEvidenceRecovery: true,
            }),
            // The revision calls no tools under an enforcing answer validation: only
            // then does request assembly read `toolsOffAfterRevision`.
            ...(this.revisionWithholdsTools() && { toolsOffAfterRevision: true }),
            // The findings ledger (9.101.0): the choice seam and `postValidate`
            // read peeled args / content under `findings`; the SERVING (step 3) —
            // the ledger piece after the recovery piece, the wire-only collapse of
            // judged results — rides `hasFindingsLedger` with its mode, the same
            // value seed records as the run constant `findingsServe`. One gate,
            // the `hasEvidenceRecovery` grammar: an unarmed agent hands the stage
            // exactly the deps it always did and reads no new key.
            ...(this.findingsOptions !== undefined && {
                findings: true,
                hasFindingsLedger: true,
                findingsServe: this.findingsOptions.serve ?? 'ledger-and-facts',
                findingsContextBytes: this.findingsOptions.maxContextBytes,
                // The answer-turn ask (9.103.0): the same value seed records, so the
                // wire and the rebuild append it from one fact; absent on `'none'`.
                ...(this.findingsOptions.answerAsk === 'quote-facts' && {
                    findingsAnswerAsk: 'quote-facts',
                }),
            }),
            // Declared sources (honesty layer 2): without the ledger, the choice
            // seam peels `_findings` where the served schema carries it (ruled
            // tools only) — value-conditional, the same grammar.
            ...(argumentSources && { argumentSources: true }),
            ...((this.answerValidationConfig !== undefined || governsOutput) && {
                suppressDraftTokens: true,
                withholdDraftContent: true,
            }),
            // Tool choice by classifier (9.105.0): the outcome row after the reply.
            ...(this.toolChoiceOptions !== undefined && { toolChoice: true }),
            // The declared ontology (9.106.0): the piece served from the run
            // constant on every call, under the one gate — an unarmed agent reads
            // no new key.
            ...(this.ontology !== undefined && { ontology: true }),
            // The time layer's served line (step T6b): read wherever the Tools mount
            // composes it — under `.time()` (G14: the control window needs no inputs
            // layer). Step T8: the same line carries the turn's time limits.
            ...(this.timeOptions !== undefined && { timeLine: true }),
            // The receipt's salt (9.88.0) — read per call, like seed's own accessor.
            getRunId: () => this.currentRunContext?.runId,
            // …and its off switch. Value-conditional, so an agent on the default
            // hands `callLLM` exactly the dep bag it always did.
            ...(this.recordReceiptValue === false && { recordReceipt: false }),
            // …and the window's evictions for `omittedForAttention` (9.93.0) — only
            // an agent with a window strategy has any, and only it hands the seam.
            ...(this.evictedTurnsHandle !== undefined && { evictedTurns: this.evictedTurnsHandle }),
            // …and the receipt memo, so a run hashes each message once rather than
            // once per call (`core/agent/receipt/receiptDigests.ts`). Only where a
            // receipt is minted at all.
            ...(this.recordReceiptValue !== false && { receiptDigests: this.receiptDigests }),
            provider,
            model,
            ...(temperature !== undefined && { temperature }),
            ...(maxTokens !== undefined && { maxTokens }),
            ...(pricingTable !== undefined && { pricingTable }),
            ...(costBudget !== undefined && { costBudget }),
            maxIterations,
            cacheStrategy,
            ...(this.cacheStrategyIsOverride && { cacheStrategyIsOverride: true }),
            get toolSchemas() {
                return toolSchemasResolved;
            },
            // The declared argument-ground edges (9.60.0) — value-conditional, so
            // an agent whose tools declare none runs the exact bytes it always did.
            ...(toolGrounding.size > 0 && { toolGrounding }),
            // The staged-refs nudge (grounded numbers) — armed only when the dial
            // is ON and a registered tool declares `wants`; otherwise the stage
            // reads nothing new and every request keeps its exact bytes.
            ...(this.evidenceGate?.nudge === true && toolWants.size > 0 && { toolWants }),
            // The external-ground door (9.72.0) — value-conditional for the same
            // reason: no provider, no key, byte-identical corpus assembly.
            ...(this.externalGrounds !== undefined && { externalGrounds: this.externalGrounds }),
            // The write-seam advisory's arming (9.77.0) — this stage owns only the
            // "no armed call this response" not-applicable note; the check itself
            // runs where the result is. Value-conditional on BOTH halves, so an
            // agent that never asked for it reads no new key.
            ...(this.noticeEmptyLookups && toolGrounding.size > 0 && { noticeEmptyLookups: true }),
            // The column-type contract's arming (9.78.0), same job and same
            // value-conditional law: this stage owns only the "no declaring tool
            // was called this response" not-applicable notes.
            ...(this.checkColumnTypes !== 'off' &&
                toolColumns.size > 0 && { columnDeclaringTools: new Set(toolColumns.keys()) }),
            integrityLedger: this.integrityLedgerHolder,
            ...(this.reliabilityConfig !== undefined && { reliability: this.reliabilityConfig }),
            ...(this.outputSchemaParser !== undefined && {
                outputSchemaParser: this.outputSchemaParser,
            }),
            // 7.26 — the synthetic tool, under `strategy: 'tool-forced'` only.
            ...(this.outputEnforcement?.schemaTool !== undefined && {
                schemaTool: this.outputEnforcement.schemaTool,
            }),
            // The native format candidate; auto selects it for the effective request.
            ...(this.outputEnforcement?.outputFormat !== undefined && {
                outputFormat: this.outputEnforcement.outputFormat,
                ...(this.outputEnforcement.nativeOutputAuto === true && {
                    nativeOutputAuto: true,
                }),
            }),
            ...(this.thinkingBudget !== undefined && { thinkingBudget: this.thinkingBudget }),
            // Only a configured agent reads `scope.resolvedModel` — see the dep's
            // JSDoc for why this is a build-time flag and not a runtime fallback.
            ...(this.runConfigFn && { runConfigured: true }),
            // Opt-in system-prompt capture (9.50.0) — value-conditional, so an agent
            // that never asked keeps byte-identical llm_start events.
            ...(this.recordSystemPromptValue && { recordSystemPrompt: true }),
            // "The cursor picks the brain" (9.19.0) — wired only when a per-skill
            // brain or an escalation exists, so every other agent's stage reads no
            // new scope key and resolves on the exact line it always did. The
            // per-brain cache strategies resolve HERE, once, where the agent's own
            // strategy (override included) is known: a brain on the agent's own
            // provider keeps it, any other provider gets the strategy its declared
            // `promptCaching` selects (markers are provider-aware — the one
            // genuinely risky seam, resolved statically).
            ...(this.skillBrains !== undefined &&
                (this.skillBrains.bySkill.size > 0 || this.skillBrains.escalation !== undefined) && {
                brainFor: buildBrainFor({
                    brains: this.skillBrains,
                    agentProvider: provider,
                    agentCacheStrategy: cacheStrategy,
                    agentModel: model,
                    ...(this.cacheStrategyIsOverride && { agentCacheStrategyIsOverride: true }),
                }),
            }),
        });
        // Window stage (7.16 as compaction; the strategy family since 7.17) —
        // built ONLY when `.window()` / `.compaction()` was called. When present
        // it becomes the ReAct loop target (see the chart builders), so it runs
        // once per iteration boundary with the previous call's adapter-reported
        // usage already measured by the meter.
        const windowStage = this.windowStrategy && this.compactionMeterHandle
            ? {
                strategyName: this.windowStrategy.name,
                run: buildWindowStage({
                    strategy: this.windowStrategy,
                    meter: this.compactionMeterHandle,
                    ...(this.evictedTurnsHandle !== undefined && {
                        evictedTurns: this.evictedTurnsHandle,
                    }),
                    agentModel: model,
                    providerName: provider.name,
                    getRunId: () => this.currentRunContext?.runId,
                    // Value-conditional (the `repeatedCallNudge` precedent): an
                    // agent on the default hands the stage exactly the deps object
                    // it always did.
                    ...(this.keepLastToolResults !== undefined && {
                        keepLastToolResults: this.keepLastToolResults,
                    }),
                    // The findings ledger (9.102.0): the arm and the RESOLVED
                    // ledger-fact ceiling, together or not at all. `keepLedgerFacts`
                    // is set exactly when `.findings()` is on, so an unarmed agent
                    // hands the stage exactly the deps object it always did; an
                    // armed agent with no window strategy threads nothing, because
                    // this whole stage does not exist for it.
                    ...(this.keepLedgerFacts !== undefined && {
                        hasFindingsLedger: true,
                        keepLedgerFacts: this.keepLedgerFacts,
                    }),
                    ...(pricingTable !== undefined && { pricingTable }),
                    ...(costBudget !== undefined && { costBudget }),
                }),
            }
            : undefined;
        // routeDecider extracted to ./agent/stages/route.ts (v2.11.2).
        // The Route decider carries the `'output'` half of the message chain when
        // one is configured — see buildRouteDeciderStage for why that seam and not
        // PrepareFinal. Without a chain this is the same function reference the
        // chart has always been handed.
        // 7.26 — the decider also judges the final answer against the schema when
        // the agent opted into enforcement, because it is the last stage that
        // still has a loop to send the answer back around. Without enforcement
        // this is the same function reference the chart has always been handed.
        // 9.18.0 — the decider also judges a would-be-final answer against the
        // active step procedure (nudge once / accept / cut-short) when a stepped
        // skill is registered. Without steps this is the same function reference
        // the chart has always been handed.
        // 9.35.0 — and the decider grounds the would-be-final answer in the
        // turn's own tool results when `.namesAndNumbersFromEvidence()` is
        // configured. Without it this is the same function reference the chart
        // has always been handed.
        // ── The out-of-budget wrap-up (9.56.0) — one build-time decision ───
        // Mounted when the agent can CALL a tool and did not opt out. The tool
        // test is not a nicety: a limit only cuts a turn short when tool calls
        // were pending (`decideBranch`'s `earlyStop`), so an agent with nothing to
        // call can never reach the branch — and mounting a node that can never run
        // would put a WrapUp box on every toolless agent's chart, in every
        // recording, forever. A `ToolProvider` counts even though its list is only
        // known per iteration: it might list one, and "might" is the whole
        // question a build-time mount can answer.
        const canCallTools = registryByName.size > 0 || this.externalToolProvider !== undefined;
        const hasWrapUp = canCallTools && this.wrapUpAtMaxIterations !== false;
        const baseRouteDecider = buildRouteDeciderStage(this.messageMiddleware, this.outputEnforcement, stepPlanFor, this.evidenceGate, hasWrapUp, this.claimContract, this.integrityLedgerHolder, 
        // THE CLAIM SEAM'S RECENCY READ (9.83.0). Value-conditional on both
        // halves, so an agent that armed neither hands the decider builder
        // exactly the arguments it always did — and `buildRouteDeciderStage`'s
        // no-judge fast path still returns the very function reference every
        // pre-9.83.0 chart was given.
        this.noticePriorTurnEvidence && this.evidenceGate !== undefined ? true : undefined, 
        // THE ANSWER TURN'S STANDINGS (9.101.0) — the same value-conditional
        // trailing positional, for the same reason: an unarmed agent hands the
        // builder exactly the arguments it always did.
        this.findingsOptions !== undefined ? true : undefined, 
        // THE INPUTS LAYER (honesty layer 2) — the same value-conditional
        // trailing positional: the rows this decider files carry the turn
        // stamp, and with the evidence gate armed too, the gate's exempt corpus
        // gains this turn's assumed values, read from the tools' declarations,
        // and the values the person gave the layer's batch ask (step 4). The
        // results layer (step 7b) arms the stamp alone — "while any layer is
        // armed, the one writer stamps the turn" — and nothing else.
        inputsArmed || resultsArmed
            ? {
                ...(inputsArmed &&
                    this.evidenceGate !== undefined && {
                    declaredDefaults: declaredDefaultsFrom((toolName, argument) => {
                        const rules = rulesOf(resolveForLayer(toolName).tool);
                        if (rules === undefined || isRefused(rules))
                            return undefined;
                        return rules.ruled.find((r) => r.argument === argument)?.assume;
                    }),
                    answeredValues: answeredValuesOf,
                    // Under `.time()` (step T7): the turn's time spellings by
                    // lineage — said ones exempt, derived ones filed, not flagged.
                    ...(this.timeOptions !== undefined && { timeLineage: true }),
                }),
            }
            : undefined, 
        // THE ANSWER LAYER (honesty layer 4) — the same value-conditional
        // trailing positional: the judges file the answer's witness rows, and
        // every row this decider files carries the turn stamp.
        answerArmed ? true : undefined);
        const validatedRouteDecider = this.answerValidationConfig === undefined
            ? baseRouteDecider
            : withAnswerValidation(baseRouteDecider, this.answerValidationConfig, this.outputSchemaParser, artifactStore, () => this.consentOutstanding.size > 0, this.outputEnforcement?.outputFormat !== undefined, 
            // Read only when `agentChecks` lists 'claims' (the agent's own rows).
            this.claimContract, this.typedNegativeClaimsConfig === undefined
                ? undefined
                : {
                    config: this.typedNegativeClaimsConfig,
                    phrases: this.typedNegativeClaimsConfig.phrases,
                    applicationValidation: this.applicationAnswerValidation,
                    runId: () => this.currentRunContext.runId,
                });
        const routeDecider = this.negativeClaimsConfig?.mode === 'prose'
            ? withNegativeProse(validatedRouteDecider, {
                runId: () => this.currentRunContext.runId,
                hasOutstandingConsent: () => this.consentOutstanding.size > 0,
                structured: this.outputSchemaParser !== undefined,
                phrases: this.negativeClaimsConfig.phrases,
            })
            : validatedRouteDecider;
        // `.limitsTravelWithTheAnswer()` on a TYPED answer — decided once, here.
        // An output schema makes the answer JSON, and the appended block would
        // make it JSON followed by prose, which `runTyped()` cannot parse. So the
        // limits travel as DATA: the Route decider's terminal decision commits the
        // fold (`withAnswerCoverage`) and the Final branch projects it onto
        // `turn_end`. A prose answer keeps the block; an agent that asked for
        // neither is handed the decider it always had.
        const limitsAsData = this.limitsTravelWithTheAnswerValue && this.outputSchemaParser !== undefined;
        // With the inputs layer armed, the typed answer's limits carry this
        // turn's assumed values too — the same rows the prose "Assumed" block
        // prints (`withAnswerCoverage`'s second argument); a rewrite by a
        // before-tool chain is read only when the agent has one.
        const terminalRouteDecider = limitsAsData
            ? withAnswerCoverage(routeDecider, inputsArmed ? { rewrites: this.toolMiddleware.length > 0 } : undefined)
            : routeDecider;
        // toolCallsHandler extracted to ./agent/stages/toolCalls.ts (v2.11.2).
        const toolCallsHandler = buildToolCallsHandler({
            registryByName,
            // WHICH SKILLS DECLARE EACH TOOL NAME (9.86.0) — read by the unknown-tool
            // roster so a role that may not see a skill is not told the names of the
            // tools that skill brought. Value-conditional: an agent whose skills carry
            // no tools hands the handler exactly the deps object it always did.
            ...(toolDeclaringSkills.size > 0 && { toolDeclaringSkills }),
            // The claim ledger accumulates only for an agent that declared a
            // contract to read it (9.61.0) — value-conditional, so every other
            // agent commits exactly what it always did.
            ...(this.claimContract !== undefined && { collectClaimFacts: true }),
            // The findings ledger (9.101.0) — the peel, the basis row and the
            // previous batch's standings all live in this handler under this gate.
            ...(this.findingsOptions !== undefined && { findings: true }),
            // The script record — `code: 'match'` or `.scriptTravelsWithTheAnswer()`:
            // one `script` row per call of a tool that declares `Tool.script`.
            ...(scriptRecordArmed && { scriptRecord: true }),
            // The inputs layer (honesty layer 2) — the fills, the refusals and the
            // note; absent → a ruled tool's call is refused (fail closed).
            ...(inputsArmed && { inputsLayer: true }),
            // Declared sources: the peel reads `_findings.from` as the layer did.
            ...(argumentSources && { argumentSources: true }),
            // The results layer (honesty layer 3, step 7b) — read in one place: a
            // period a result declares with NO layer mounted is dev-warned (nobody
            // will file its verdict). And the turn stamp, as under any layer.
            ...(resultsArmed && { resultsLayer: true }),
            // The answer layer (honesty layer 4) — only the turn stamp on the rows
            // this handler files (adopted Q6).
            ...(answerArmed && { answerLayer: true }),
            // …and the host's own context for the layer's batch ask (step 4),
            // value-conditional inside the arm.
            ...(inputsArmed &&
                this.argumentAskContext !== undefined && { argumentAskContext: this.argumentAskContext }),
            // The judge (9.104.0) — VALUE-conditional inside the arm: an armed
            // agent without one hands the handler exactly the deps it did before.
            ...(this.findingsOptions?.judge !== undefined && {
                findingsJudge: this.findingsOptions.judge,
            }),
            // THE TOWERS AT DISPATCH (9.110.0) — the contingent check's second
            // moment. VALUE-conditional on BOTH doors: the ledger (the standings)
            // and the evidence gate (the corpus and the extractor's rule); an
            // agent with one or neither hands the handler exactly the deps it did
            // before. The same resolved gate the route decider judges the answer
            // with, so the two moments read one extractor and one set of shapes.
            ...(this.findingsOptions !== undefined &&
                this.evidenceGate !== undefined && { evidenceGate: this.evidenceGate }),
            // THE WRITE SEAM (9.77.0) — `empty-lookup`. Handed the SAME harvested
            // map callLLM reads at the choice seam, so the two stages agree by
            // construction about which calls are armed. Value-conditional on both
            // halves — the operator's dial and at least one `argumentsFrom`
            // declaration — so an agent that asked for neither hands the handler
            // exactly the deps object it always did.
            ...(this.noticeEmptyLookups &&
                toolGrounding.size > 0 && {
                emptyLookupGrounding: toolGrounding,
                integrityLedger: this.integrityLedgerHolder,
            }),
            // THE WRITE SEAM'S other check (9.78.0) — the column-type contract.
            // Value-conditional on both halves for the same reason, so an agent
            // that asked for neither hands the handler exactly the deps object it
            // always did.
            ...(this.checkColumnTypes !== 'off' &&
                toolColumns.size > 0 && {
                columnDeclarations: toolColumns,
                columnCheckMode: this.checkColumnTypes,
                integrityLedger: this.integrityLedgerHolder,
            }),
            ...(this.externalToolProvider && { externalToolProvider: this.externalToolProvider }),
            ...(this.externalToolProvider && { providerToolCache }),
            servedTools,
            toolClaimants,
            ...(permissionChecker && { permissionChecker }),
            ...(credentialProvider && { credentialProvider }),
            // The request a call's credential is read for: this agent's own run's,
            // or — on a mount — the run its link names (`memberRun.ts`). An accessor,
            // because the chart is built once and the request is per run.
            ...(credentialProvider && {
                currentRequest: (scope) => this.ownScopes.has(scope) ? this.scopeRequests.get(scope) : memberRequestIn(scope),
            }),
            // The claim-check store (9.21.0). Absent → not one new line runs in
            // dispatch: `ctx.artifacts` is the fail-closed teacher and no artifact
            // event can fire (zero-cost-when-unused).
            ...(artifactStore && { artifactStore }),
            // The placement threshold (9.22.0) — only ever set beside a store (the
            // option's own shape enforces it). Absent → results are never measured
            // against it and never placed.
            ...(this.artifactPlacement !== undefined && { placement: this.artifactPlacement }),
            ...(this.toolArgValidation && { toolArgValidation: this.toolArgValidation }),
            ...(this.maxToolResultChars !== undefined && {
                maxToolResultChars: this.maxToolResultChars,
            }),
            // The repeated-call nudge (9.26.0). Threaded ONLY when switched off —
            // the VALUE-conditional pattern, so an agent on the default hands the
            // handler exactly the deps object it always did.
            ...(this.repeatedCallNudge === false && { repeatedCallNudge: false }),
            // The answer check's tool-call ledger (`context.toolCalls`) — kept ONLY
            // for answer validation or negative claims; other records stay unchanged.
            ...((this.answerValidationConfig !== undefined ||
                this.negativeClaimsConfig !== undefined) && {
                toolCallLedger: { maxCalls: this.answerValidationConfig?.limits.maxToolCalls ?? 1000 },
            }),
            ...(this.negativeClaimsConfig !== undefined && { negativeClaimsArmed: true }),
            // Skill-graph read_skill gate: bound the model's read_skill jumps to the
            // reachable set from the current cursor. Undefined → gate off (back-compat).
            ...(this.skillGraphReachable && {
                allowedSkillIds: this.skillGraphReachable,
                openSkillIds: this.openSkillIds(),
                skillGraphIsTree: this.skillGraphIsTree,
            }),
            // The mount kernel's plan (9.59.0) — the gate's THIRD admission class.
            // A `read_skill` pick of a PARKED map's member is a re-engagement, not
            // a hop: admitted even though the reachable set excludes the node the
            // cursor already stands on, and it never touches the cursor. Value-
            // conditional, so an agent without `.maps()` hands the handler exactly
            // the deps object it always did.
            ...(this.mapsPlan !== undefined && { engagementPlan: this.mapsPlan }),
            // The mount's routing posture (SG-C). `'assist'` — the default — is
            // deliberately NOT threaded: undefined keeps the gate byte-identical.
            ...(this.skillGraphCascade !== undefined &&
                this.skillGraphCascade.strictness !== 'assist' && {
                skillStrictness: this.skillGraphCascade.strictness,
            }),
            // Steps (9.18.0): advance/skip at the result boundary — the batch loop
            // AND every pausable resume path (an askHuman step advances on resume).
            ...(stepPlanFor !== undefined && { stepPlanFor }),
            // Escalate-on-evidence (9.19.0): the refusal budget + flip, wired only
            // when the policy exists. `describeFrom` resolves the event's honest
            // `from` by the same chain callLLM applies.
            ...(this.skillBrains?.escalation !== undefined && {
                escalation: {
                    afterRefusals: this.skillBrains.escalation.afterRefusals,
                    to: {
                        provider: this.skillBrains.escalation.provider.name,
                        ...(this.skillBrains.escalation.model !== undefined && {
                            model: this.skillBrains.escalation.model,
                        }),
                    },
                    describeFrom: (cursor, resolvedModel) => describeServingBrain({
                        brains: this.skillBrains,
                        cursor,
                        agentProviderName: provider.name,
                        defaultModel: model,
                        ...(resolvedModel !== undefined && { resolvedModel }),
                    }),
                },
            }),
            // The require-instruction catalog (9.19.0): every registered injection
            // id + the one delivery fact its check-up needs. Wired whenever
            // injections exist; nothing runs until a tool returns an effect.
            ...(this.injections.length > 0 && {
                leaseTargets: new Map(this.injections.map((inj) => {
                    const surfaceMode = inj.metadata?.surfaceMode;
                    return [inj.id, { ...(surfaceMode !== undefined && { surfaceMode }) }];
                })),
            }),
            // Check-in (evidence-carrying human consent). Always threaded (resolved
            // default); the gate fires only for tools that declared `checkIn`.
            checkIn: this.checkInConfig,
            // Where a decision's request is spent, once (`core/consentRequest.ts`).
            // Absent → this process's ledger, shared by every agent that names none.
            ...(this.decisionLedger !== undefined && { decisionLedger: this.decisionLedger }),
            // The governance chain. Threaded only when non-empty so an agent without
            // one produces the same handler behaviour it always did.
            ...(this.toolMiddleware.length > 0 && { toolMiddleware: this.toolMiddleware }),
            // Durable-write barrier. An ACCESSOR, never a captured value: the chart is
            // built once at construction and a session composer installs its barrier
            // later, so a direct field read here would be stale forever. Answers
            // `undefined` — no await, no microtask — until one is installed.
            awaitDurable: () => pendingDurableWrite(this),
            // 9.7.0 — run/session identity and the teardown registrar. BOTH are
            // accessors for the reason `awaitDurable` is one: the chart is built once
            // at construction, and a captured value would be run #1's forever. The
            // tier one is lazy on top of that — an agent whose tools hold no sessions
            // never allocates it.
            currentRun: () => this.toolRunFacts(),
            isOwnRun: (scope) => this.ownScopes.has(scope),
            answeredAsk: () => this.answeredAsk,
            // The time layer: each dispatched call files its `call` row, and the
            // resume door compares a resume's passed `time` with the kept clock.
            ...(this.timeOptions !== undefined && {
                time: {
                    takePassedOnResume: (scope) => {
                        // Only the agent's own resumed leg takes it — never a member's.
                        if (!this.ownScopes.has(scope))
                            return undefined;
                        const passed = this.resumePassedTime;
                        this.resumePassedTime = undefined;
                        return passed;
                    },
                    ...(this.timeOptions.zone !== undefined && { appZone: this.timeOptions.zone }),
                    // The reader (step T6b): the batch ask asks an open mention's window.
                    ...(this.timeOptions.reader !== undefined &&
                        this.timeOptions.policy !== undefined && {
                        reader: {
                            policy: this.timeOptions.policy,
                            ...(this.timeOptions.messages !== undefined && {
                                messages: this.timeOptions.messages,
                            }),
                        },
                    }),
                },
            }),
            emitForRun: (type, payload, runContext) => this.emitLateFact(type, payload, runContext),
            toolSessions: () => this.toolSessions(),
            // 8.6.0 — what a run does when a declared credential needs 3LO consent.
            onAuthorizationRequired: this.onAuthorizationRequired,
            // The `'tell-model'` consent record travels OFF tracked state (a tracked
            // write is a commit-log entry, and the URL is a bearer capability), so
            // these two callbacks are its whole route to the caller. Cleared at the
            // top of every run; read in `finalizeResult`.
            reportConsentOutstanding: (record) => {
                this.consentOutstanding.set(record.service, record);
            },
            clearConsentOutstanding: (service) => {
                this.consentOutstanding.delete(service);
            },
        });
        // v2.14 — Build the NormalizeThinking sub-subflow only when a
        // ThinkingHandler resolved (declared by the provider OR
        // explicitly set via .thinkingHandler()). Conditional mount ensures
        // zero overhead for non-thinking agents — the chart has zero extra
        // stages when undefined.
        const thinkingSubflow = this.thinkingHandler
            ? buildThinkingSubflow(this.thinkingHandler, this.provider.name)
            : undefined;
        // Chart composition extracted to ./agent/buildAgentChart.ts (v2.11.2).
        // The deps object is identical for both chart shapes — only the
        // wiring differs (flat call-llm stage vs sf-llm-call subflow).
        const chartDeps = {
            memories: this.memories,
            // Evidence bridge (#5): closure hand-off to the CAUSAL write mounts.
            ...(this.causalEvidence && { causalEvidenceSource: this.causalEvidence.collect }),
            systemPromptCachePolicy,
            maxIterations,
            seed,
            callLLM,
            routeDecider: terminalRouteDecider,
            toolCallsHandler,
            // The re-ask branch — mounted only when there are retries to spend. A
            // `'tool-forced'` agent with `retries: 0` gets the constrained shape and
            // no branch: there is nothing for a branch to do.
            ...(this.outputEnforcement !== undefined &&
                this.outputEnforcement.retries > 0 && {
                outputRetryStage: buildOutputRetryStage(this.outputEnforcement),
            }),
            // The unfinished-steps nudge branch (9.18.0) — mounted only when a
            // stepped skill exists, same conditional-mount law as the re-ask above.
            ...(stepPlanFor !== undefined && {
                hasSteps: true,
                stepNudgeStage: buildStepNudgeStage(stepPlanFor),
            }),
            // The maps kernel (9.58.0) — gates the mapEngagement alias round trip
            // through the engine boundary. Same conditional-mount law as steps.
            ...(this.mapsPlan !== undefined && { engagementPlan: this.mapsPlan }),
            // The evidence gate (9.35.0). `hasEvidenceGate` rides ANY posture (the
            // grouped chart needs the system-prompt records bubbled out to exempt
            // what the app itself supplied); the BRANCH is mounted only for a
            // posture that can revise — `'assist'` records and never loops, so it
            // gets no branch and no stage, the same conditional-mount law as above.
            ...(this.evidenceGate !== undefined && {
                hasEvidenceGate: true,
                ...(this.evidenceGate.posture !== 'assist' && {
                    evidenceRecheckStage: buildEvidenceRecheckStage(this.evidenceGate, 
                    // The staged-refs join (grounded numbers) — the correction names
                    // the refs and the spender when the run declared both. Threaded
                    // whenever a `wants` tool exists (not gated on `nudge`: a
                    // revision that cannot say HOW to compute leaves the model to
                    // head-math again); absent for every agent without one, so the
                    // correction keeps its exact bytes.
                    toolWants.size > 0
                        ? {
                            toolWants,
                            staticToolNames: () => toolSchemasResolved.map((t) => t.name),
                        }
                        : undefined),
                }),
            }),
            // Escalation (9.19.0): the grouped chart threads `skillEscalated`
            // across the sf-llm-call boundary only when the policy exists.
            ...(this.skillBrains?.escalation !== undefined && { hasEscalation: true }),
            // The findings ledger (9.102.0): both charts compute the OFFER on the
            // Tools branch's mount only under the arm — the same value-conditional
            // grammar as the slot's `findings` and callLLM's `hasFindingsLedger`,
            // so the three can never disagree about whether the ledger is armed.
            ...(this.findingsOptions !== undefined && { hasFindingsLedger: true }),
            // The inputs layer (honesty layer 2): `sf-inputs` between the LLM call
            // and Route, in both builders through one helper; absent → untouched.
            // With a before-tool chain, the final branch's "Assumed" block also reads
            // the chain's rewrites (a rewritten fill is not what the call ran with).
            ...(inputsArmed && {
                inputsLayer: {
                    ...(this.findingsOptions !== undefined && { findings: true }),
                    // Route's sixth dispatch value, read only by an agent whose revision
                    // turns the tools off (an enforcing answer validation).
                    ...(this.revisionWithholdsTools() && { toolsOffAfterRevision: true }),
                    toolOf: (toolName) => resolveForLayer(toolName).tool,
                    ...(this.toolMiddleware.length > 0 && { rewrites: true }),
                    // The time layer (step T5a): a tool whose period declares forms is filled from the
                    // turn's one window, bound to the person's window, or recorded as the model's.
                    ...(this.timeOptions !== undefined && {
                        time: {
                            ...(this.timeOptions.zone !== undefined && { appZone: this.timeOptions.zone }),
                        },
                    }),
                    // Declared sources: the layer reads `_findings.from` and checks it against
                    // the corpora the mount hands in — the app's `externalGrounds` among them.
                    ...(argumentSources && {
                        sources: {
                            ...(this.externalGrounds !== undefined && {
                                externalGrounds: this.externalGrounds,
                            }),
                            // Beside the findings ledger a declared basis files a basis row,
                            // which carries the dropped `from` entries' count; without it the
                            // count rides the call's first argument row.
                            ...(this.findingsOptions !== undefined && { basisRows: true }),
                            // A tool in reach that may hide an argument keeps every quote off the
                            // record — a quote is free text that may hold the hidden value. Decided
                            // once, here, over every party the layer's `toolOf` can resolve: the
                            // registry (each tool asked), and a ToolProvider — whatever it lists,
                            // since its list is known only per iteration and a tool it lists first
                            // AFTER a quote was filed would find that quote already on the record.
                            ...(([...registryByName.values()].some(carriesArgumentView) ||
                                this.externalToolProvider !== undefined) && {
                                argumentViews: true,
                            }),
                        },
                    }),
                },
            }),
            // The results layer (honesty layer 3, step 7b): `sf-results` at the loop
            // head, in both builders through one helper, reading each call's
            // `ToolPeriod` off the implementation that answered it; absent → untouched.
            ...(resultsArmed && {
                resultsLayer: { toolOf: (toolName) => resolveForLayer(toolName).tool },
            }),
            // The answer layer (honesty layer 4): its stage heads the final branch
            // in both builders through one helper, reading only the committed keys
            // this agent's arms can write; absent → the branch is untouched.
            ...(this.answerLayerOption !== undefined && {
                answerLayer: {
                    reads: answerFoldReads({
                        tools: canCallTools,
                        evidenceGate: this.evidenceGate !== undefined,
                        answerValidation: this.answerValidationConfig !== undefined,
                        inputs: inputsArmed,
                        toolMiddleware: this.toolMiddleware.length > 0,
                    }),
                    ...(this.answerLayerOption.standingLine !== undefined && {
                        standingLine: this.answerLayerOption.standingLine,
                    }),
                    ...(this.answerValidationConfig !== undefined && { validated: true }),
                    ...(displayNameOf !== undefined && { displayNameOf }),
                },
            }),
            // The prose limits block names an item's calls in the app's words — under the same wiring.
            ...(displayNameOf !== undefined && { displayNameOf }),
            // Tool choice by classifier (9.105.0): the mount args on the Tools
            // branch and the key across the sf-llm-call boundary, under the arm.
            ...(this.toolChoiceOptions !== undefined && { hasToolChoice: true }),
            // The declared ontology (9.106.0): the grouped chart carries the run
            // constant across the sf-llm-call boundary, under the arm.
            ...(this.ontology !== undefined && { hasOntology: true }),
            // `.limitsTravelWithTheAnswer()` — value-conditional, the
            // `resolvedModel` precedent: absent from the deps object entirely for an
            // agent that did not ask, so both builders mount the final-branch stage
            // function they have always mounted. ONE of the two, never both: a prose
            // answer gets the appended block, a TYPED one (`limitsAsData`, above)
            // gets the stage that appends nothing and carries the limits as data.
            ...(this.limitsTravelWithTheAnswerValue && !limitsAsData && { attachCoverageLimits: true }),
            ...(limitsAsData && { coverageLimitsAsData: true }),
            // `.scriptTravelsWithTheAnswer()` — the recorded programs travel with the
            // answer: data always, text on a prose answer only. Absent → the final
            // stage this agent always mounted.
            ...(this.scriptTravel !== undefined && {
                scriptTravel: {
                    ...this.scriptTravel,
                    ...(this.outputSchemaParser !== undefined && { typed: true }),
                },
            }),
            // The time layer: the prose limits block renders its `Period:` lines in
            // the run's clock zone. Absent → the final stage it always mounted.
            ...(this.timeOptions !== undefined && { timeLayer: true }),
            // The Tools mount hands the slot the turn's windows — the reader's (step T6b) and the
            // app's control window (G14) — under `.time()`.
            ...(this.timeOptions !== undefined && { timeWindows: true }),
            ...(inputsArmed && this.timeOptions !== undefined && { timeLimits: true }),
            ...(this.answerValidationConfig !== undefined && { hasAnswerValidation: true }),
            ...(this.typedNegativeClaimsConfig !== undefined && {
                negativeRecheckStage: negativeRecheckStage,
            }),
            ...(governsOutput && { releaseOutputTokens: true }),
            ...(this.evidenceGate?.posture === 'rails' && { evidenceRails: true }),
            // The out-of-budget wrap-up branch (9.56.0) — the conditional-mount law
            // above, decided once beside the Route decider that routes to it so the
            // two can never disagree about whether the branch exists.
            ...(hasWrapUp && { wrapUpStage: wrapUpStage }),
            injectionEngineSubflow,
            ...(pickEntryStage && { pickEntryStage }),
            ...(routeTurnStage && { routeTurnStage: routeTurnStage }),
            // Messages-slot delivery (7.21) — mounted ONLY when something could
            // target the slot. A registered injection declaring `inject.messages`
            // is the obvious case; a `.memory()` is the other, because a hand-built
            // read subflow may format its recall as a non-system role and that
            // recall only exists at run time. An agent with neither gets no stage
            // and no write, so its chart and its commit log are what they were.
            ...((this.injections.some((i) => (i.inject?.messages?.length ?? 0) > 0) ||
                this.memories.length > 0) && {
                deliverStage: buildDeliverStage({
                    provider: this.provider,
                    ...(this.reliabilityConfig?.providers !== undefined && {
                        providers: providerTargets(this.provider, this.model, this.reliabilityConfig.providers).map((target) => target.provider),
                    }),
                    memoryIds: this.memories.map((m) => m.id),
                }),
            }),
            systemPromptSubflow,
            messagesSubflow,
            toolsSubflow,
            ...(thinkingSubflow !== undefined && { thinkingSubflow }),
            updateSkillHistoryStage,
            ...(windowStage !== undefined && { windowStage }),
            // Gate the UpdateSkillHistory stage on skills being registered —
            // same idiom buildToolRegistry uses to auto-attach `read_skill`.
            hasSkills: this.injections.some((i) => i.flavor === 'skill'),
            // Builders only branch on classic-vs-dynamic SEMANTICS; the grouped
            // chart shape is selected below by choosing buildDynamicAgentChart.
            reactMode: (this.reactMode === 'classic' ? 'classic' : 'dynamic'),
            ...(this.structureRecorders !== undefined && {
                structureRecorders: [...this.structureRecorders],
            }),
        };
        // `'dynamic-grouped'` wraps the whole LLM turn in an `sf-llm-call` subflow —
        // the same boundary LLMCall produces — so Lens / explainable-ui render it as
        // an LLM group with its slots inside. `'classic'` and `'dynamic'` use the
        // flat chart; they differ only in `chartDeps.reactMode` (whether the Context
        // selector re-engineers the static slots each turn). Grouping is dynamic-only
        // (it re-seeds context every turn by design), so there is no classic-grouped.
        return this.reactMode === 'dynamic-grouped'
            ? buildDynamicAgentChart(chartDeps)
            : buildAgentChart(chartDeps);
    }
}
// AgentBuilder extracted to ./agent/AgentBuilder.ts (v2.11.2).
// Re-export so the 28+ existing import sites continue to work unchanged.
// Validators + helpers extracted to ./agent/validators.ts (v2.11.1).
/** How many run ids one instance remembers. */
const MAX_REMEMBERED_RUNS = 256;
/** Drop the oldest entries of an insertion-ordered Set or Map past `max`. */
function boundInsertionOrder(held, max) {
    while (held.size > max) {
        const oldest = held.keys().next();
        if (oldest.done === true)
            return;
        held.delete(oldest.value);
    }
}
/**
 * The run options without the time layer's `time` — what the engine is handed.
 * The SAME object when it carries no `time` key, so a run that passed none
 * hands the executor exactly the options it always did.
 */
function withoutTime(options) {
    if (options === undefined || !('time' in options))
        return options;
    const { time: _time, ...rest } = options;
    void _time;
    return rest;
}
/**
 * The policy a paused agent run was covered by, read off its own checkpoint
 * (`AgentState.runRedaction`, committed by seed and by every resumed leg) —
 * FAIL CLOSED: a value this library did not write, or a checkpoint whose run
 * kept names out of its records (`redactionMarks`) but carries no policy,
 * refuses the resume (`ResumeRedactionError`) before anything moves. A leg its
 * policy could not keep covered must never start.
 */
function resumeRedactionOf(checkpoint, trusted) {
    const state = checkpoint.sharedState;
    let carried;
    try {
        // A carried pattern is a reference to one the resuming side holds — never
        // compiled from the checkpoint (`redaction/policy.ts` · `policyFromCarried`).
        carried = policyFromCarried(state?.runRedaction, 'Agent.resume', trusted);
        // A policy built under ANOTHER vocabulary version is joined with the
        // current list — the leg is never covered by an older list alone.
        carried = withCurrentVocabulary(carried, carriedVocabularyOf(state?.runRedaction));
    }
    catch (refusal) {
        throw new ResumeRedactionError(refusal instanceof CarriedPolicyError && refusal.refusal === 'unknown-pattern'
            ? 'unknown-pattern'
            : 'unreadable');
    }
    // Every leg of a covered run writes its policy into its checkpoint's state
    // (seed, and `resume` for a resumed leg), so a checkpoint whose run kept
    // names out but carries none was altered — refused even when the resume
    // names a policy, which cannot stand in for the one the run was covered by.
    if (carried === undefined && policyOfMarks(checkpoint.redactionMarks) !== undefined) {
        throw new ResumeRedactionError('missing');
    }
    return carried;
}
//# sourceMappingURL=Agent.js.map