agentfootprint 9.104.0 → 9.105.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +68 -0
  3. package/CLAUDE.md +2 -1
  4. package/README.md +1 -0
  5. package/ai-instructions/claude-code/SKILL.md +1 -1
  6. package/dist/core/Agent.js +48 -0
  7. package/dist/core/Agent.js.map +1 -1
  8. package/dist/core/agent/AgentBuilder.js +97 -1
  9. package/dist/core/agent/AgentBuilder.js.map +1 -1
  10. package/dist/core/agent/buildAgentChart.js +13 -0
  11. package/dist/core/agent/buildAgentChart.js.map +1 -1
  12. package/dist/core/agent/buildDynamicAgentChart.js +28 -0
  13. package/dist/core/agent/buildDynamicAgentChart.js.map +1 -1
  14. package/dist/core/agent/stages/callLLM.js +42 -0
  15. package/dist/core/agent/stages/callLLM.js.map +1 -1
  16. package/dist/core/agent/toolChoice/compose.js +92 -0
  17. package/dist/core/agent/toolChoice/compose.js.map +1 -0
  18. package/dist/core/agent/toolChoice/pick.js +165 -0
  19. package/dist/core/agent/toolChoice/pick.js.map +1 -0
  20. package/dist/core/agent/toolChoice/record.js +108 -0
  21. package/dist/core/agent/toolChoice/record.js.map +1 -0
  22. package/dist/core/agent/toolChoice/types.js +46 -0
  23. package/dist/core/agent/toolChoice/types.js.map +1 -0
  24. package/dist/core/slots/buildToolsSlot.js +200 -120
  25. package/dist/core/slots/buildToolsSlot.js.map +1 -1
  26. package/dist/esm/core/Agent.d.ts +7 -0
  27. package/dist/esm/core/Agent.js +48 -0
  28. package/dist/esm/core/Agent.js.map +1 -1
  29. package/dist/esm/core/agent/AgentBuilder.d.ts +49 -0
  30. package/dist/esm/core/agent/AgentBuilder.js +97 -1
  31. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  32. package/dist/esm/core/agent/buildAgentChart.d.ts +13 -0
  33. package/dist/esm/core/agent/buildAgentChart.js +13 -0
  34. package/dist/esm/core/agent/buildAgentChart.js.map +1 -1
  35. package/dist/esm/core/agent/buildDynamicAgentChart.js +28 -0
  36. package/dist/esm/core/agent/buildDynamicAgentChart.js.map +1 -1
  37. package/dist/esm/core/agent/stages/callLLM.d.ts +11 -0
  38. package/dist/esm/core/agent/stages/callLLM.js +19 -0
  39. package/dist/esm/core/agent/stages/callLLM.js.map +1 -1
  40. package/dist/esm/core/agent/toolChoice/compose.d.ts +53 -0
  41. package/dist/esm/core/agent/toolChoice/compose.js +88 -0
  42. package/dist/esm/core/agent/toolChoice/compose.js.map +1 -0
  43. package/dist/esm/core/agent/toolChoice/pick.d.ts +104 -0
  44. package/dist/esm/core/agent/toolChoice/pick.js +158 -0
  45. package/dist/esm/core/agent/toolChoice/pick.js.map +1 -0
  46. package/dist/esm/core/agent/toolChoice/record.d.ts +49 -0
  47. package/dist/esm/core/agent/toolChoice/record.js +101 -0
  48. package/dist/esm/core/agent/toolChoice/record.js.map +1 -0
  49. package/dist/esm/core/agent/toolChoice/types.d.ts +125 -0
  50. package/dist/esm/core/agent/toolChoice/types.js +43 -0
  51. package/dist/esm/core/agent/toolChoice/types.js.map +1 -0
  52. package/dist/esm/core/agent/types.d.ts +59 -0
  53. package/dist/esm/core/slots/buildToolsSlot.d.ts +21 -0
  54. package/dist/esm/core/slots/buildToolsSlot.js +177 -120
  55. package/dist/esm/core/slots/buildToolsSlot.js.map +1 -1
  56. package/dist/esm/events/payloads.d.ts +47 -0
  57. package/dist/esm/events/registry.d.ts +9 -1
  58. package/dist/esm/events/registry.js +8 -0
  59. package/dist/esm/events/registry.js.map +1 -1
  60. package/dist/esm/index.d.ts +1 -0
  61. package/dist/esm/index.js +7 -0
  62. package/dist/esm/index.js.map +1 -1
  63. package/dist/events/registry.js +8 -0
  64. package/dist/events/registry.js.map +1 -1
  65. package/dist/index.js +12 -3
  66. package/dist/index.js.map +1 -1
  67. package/dist/types/core/Agent.d.ts +7 -0
  68. package/dist/types/core/Agent.d.ts.map +1 -1
  69. package/dist/types/core/agent/AgentBuilder.d.ts +49 -0
  70. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  71. package/dist/types/core/agent/buildAgentChart.d.ts +13 -0
  72. package/dist/types/core/agent/buildAgentChart.d.ts.map +1 -1
  73. package/dist/types/core/agent/buildDynamicAgentChart.d.ts.map +1 -1
  74. package/dist/types/core/agent/stages/callLLM.d.ts +11 -0
  75. package/dist/types/core/agent/stages/callLLM.d.ts.map +1 -1
  76. package/dist/types/core/agent/toolChoice/compose.d.ts +54 -0
  77. package/dist/types/core/agent/toolChoice/compose.d.ts.map +1 -0
  78. package/dist/types/core/agent/toolChoice/pick.d.ts +105 -0
  79. package/dist/types/core/agent/toolChoice/pick.d.ts.map +1 -0
  80. package/dist/types/core/agent/toolChoice/record.d.ts +50 -0
  81. package/dist/types/core/agent/toolChoice/record.d.ts.map +1 -0
  82. package/dist/types/core/agent/toolChoice/types.d.ts +126 -0
  83. package/dist/types/core/agent/toolChoice/types.d.ts.map +1 -0
  84. package/dist/types/core/agent/types.d.ts +59 -0
  85. package/dist/types/core/agent/types.d.ts.map +1 -1
  86. package/dist/types/core/slots/buildToolsSlot.d.ts +21 -0
  87. package/dist/types/core/slots/buildToolsSlot.d.ts.map +1 -1
  88. package/dist/types/events/payloads.d.ts +47 -0
  89. package/dist/types/events/payloads.d.ts.map +1 -1
  90. package/dist/types/events/registry.d.ts +9 -1
  91. package/dist/types/events/registry.d.ts.map +1 -1
  92. package/dist/types/index.d.ts +1 -0
  93. package/dist/types/index.d.ts.map +1 -1
  94. package/package.json +2 -1
package/AGENTS.md CHANGED
@@ -544,7 +544,7 @@ files `agentfootprint.tools.coverage_declared`; with
544
544
  to the final answer, which is how a limit survives a model that would rather not
545
545
  mention it.
546
546
 
547
- ### Observability — 115 typed events across 25 domains
547
+ ### Observability — 118 typed events across 26 domains
548
548
 
549
549
  ```typescript
550
550
  agent.on('agentfootprint.context.injected', (e) =>
package/CHANGELOG.md CHANGED
@@ -5,6 +5,74 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [9.105.0] - 2026-09-17
9
+
10
+ ### Added — tool choice by classifier: a second reading beside the model's call, and a narrowing dial
11
+
12
+ - **`.toolChoice({ classifier, serve, alwaysServe })`** (`AgentOptions.toolChoice`,
13
+ `AgentBuilder.toolChoice`). At every model call the tools slot asks the
14
+ classifier ONE `choice` question — id `TOOL_CHOICE_QUESTION` (`'tool'`),
15
+ criteria the tools about to be served by name with their own descriptions
16
+ (the merged wire minus the always-served doors), state the user's message
17
+ plus the active skill id — and files a `ToolChoiceRow` under
18
+ `AgentState.toolChoices` BEFORE the call: `offered`, `ranked` (the
19
+ provider's distribution as sent, highest first, an unscored tool absent —
20
+ never padded, never renormalised), `chosen` (the provider's own pick,
21
+ absent when it named nothing offered), `confidence`, `usage`, `latencyMs`,
22
+ `served` (the names the slot committed) and `narrowed`. After the reply
23
+ `callLLM` files a `ToolChoiceOutcomeRow`: `called` (the model's tool calls
24
+ in order, empty on an answer), `firstAgrees` (`chosen === called[0]`,
25
+ absent when either is absent) and `miss` (the names called outside a
26
+ narrowed served list). A failed classifier call is a `ToolChoiceErrorRow`
27
+ (status, message, latency) and the full set is served — fail open, never
28
+ fail narrow. The model's call is the emission; the pick is a second
29
+ reading marked `source: 'classifier'`, never substituted, never merged.
30
+ - **Advisory by default** (`serve: 'all'`): the wire is byte for byte the
31
+ unarmed agent's — every request equal, every receipt equal; the record
32
+ gains the rows and nothing else.
33
+ - **The narrowing dial** (`serve: { top: N }`): the slot commits the
34
+ classifier's top-N plus the doors — `read_skill`, `list_skills`,
35
+ `skip_step`, `present` (`ALWAYS_SERVED_TOOLS`) and the app's `alwaysServe`
36
+ — in the merged wire's order, at the ONE decoration site, so
37
+ `dynamicToolSchemas`, the receipt's `tools.schemaHashes` and
38
+ `servedAt(k).tools.schemas` are the narrowed list by construction (no new
39
+ `SERVED_GAPS` kind; `toolsInjections` follows the served set). The full
40
+ wire is served with the reason on the row (`narrowedSkipped`:
41
+ `NarrowSkipReason`) when the classifier failed or scored fewer than N
42
+ (`unavailable`), fewer than N + 1 candidates were offered (`too-few`), the
43
+ previous call's outcome carried a miss (`after-miss`) or the call is the
44
+ out-of-budget wrap-up (`wrap-up`). A miss — the model naming a
45
+ narrowed-away tool — is recorded on the outcome row and answered by the
46
+ dispatcher's off-wire path exactly as before (`tools.answered_off_wire`);
47
+ the next call serves the full wire. `reactMode: 'classic'` is refused at
48
+ build (the `.findings()` precedent).
49
+ - **Three events**, `agentfootprint.tool_choice.picked` (`chosen`,
50
+ `confidence`, `offered` and `served` as counts, `narrowed`,
51
+ `narrowedSkipped`, `latencyMs`, tokens), `agentfootprint.tool_choice.outcome`
52
+ (`called`, `firstAgrees`, `missed`) and `agentfootprint.tool_choice.failed`
53
+ (`status`, `latencyMs`) — identities, enums, numbers and a boolean only
54
+ (payloads `ToolChoicePickedPayload`, `ToolChoiceOutcomePayload`,
55
+ `ToolChoiceFailedPayload`); 115 → 118 typed events, 25 → 26 domains.
56
+ - **Root exports**: `ToolChoiceRow`, `ToolChoiceErrorRow`,
57
+ `ToolChoiceOutcomeRow`, `ToolChoiceEntry`, `ToolChoiceLedger`,
58
+ `ToolChoiceScore`, `NarrowSkipReason`, `TOOL_CHOICE_QUESTION`,
59
+ `ALWAYS_SERVED_TOOLS`. The asker (`src/core/agent/toolChoice/pick.ts`) is
60
+ loaded through `import()` by the slot and is deliberately not exported.
61
+ - **`npm run bench:tool-choice`** — the mock provider, one skill of eight
62
+ tools, six scripted steps, a scripted classifier ranking the right tool
63
+ first / second / a wrong pair, under `advisory` and `top-2`: `first-agrees`,
64
+ `misses`, `extra-calls`, `tools-slot-bytes` (from the receipt's
65
+ `requestMeasurement`), `pick-tokens`, `pick-latency-ms`; the cost line
66
+ first; exits non-zero if the unarmed twin's tools-slot bytes move or an
67
+ advisory row's differ from the twin's. On 2026-09-17: 1745 bytes unarmed
68
+ and on every advisory row, 743 under `top-2` with a right ranking, 1172
69
+ under a wrong pair (three misses, the full wire on the call after each,
70
+ zero extra calls).
71
+ - Byte identity: the 18 unarmed references untouched; one new reference
72
+ `agent-tool-choice` (`test/core/tools/byte-identity.test.ts`), generated
73
+ alone. Design: `docs/design/2026-09-scored-choice.md` § Step 4; README:
74
+ `src/core/agent/toolChoice/README.md`.
75
+
8
76
  ## [9.104.0] - 2026-09-17
9
77
 
10
78
  ### Added — a calibrated judge beside the model's own standings
package/CLAUDE.md CHANGED
@@ -68,6 +68,7 @@ not in this table, search `src/index.ts` for the nearest noun before writing cod
68
68
  | subscribing to every event in one domain at once | `DomainWildcard` | `src/events/dispatcher.ts` | 9.4.0 |
69
69
  | pausing a run for a person and resuming it later | `checkInApproved` / `checkInDeclined` (`{ by, note?, value? }`; a decline is NOT an abort — the model gets a "declined by human" tool result and adapts in-loop) | `src/core/checkin.ts` | 7.5.0 |
70
70
  | a SECOND source judging every tool result — a calibrated classifier asked what a result is worth for the declared proposition (never why the tool was called), its distribution and cost on the record beside the model's own standing, never merged, never served in its place (policy A) | `.findings({ judge })` + `Classifier` / `typesafe` / `mockClassifier` (`agentfootprint/classify`) + `JudgmentRow` / `JudgmentErrorRow` + `classifierScorer` (the scored entry choice, `agentfootprint/skill-graph`) | `src/classify/` | 9.104.0 |
71
+ | a classifier's reading of WHICH TOOL beside every model call — the ranking, the pick and the cost on the record before the call, the model's call and the agreement after it; optionally the served list narrowed to the top-N plus the doors, on the committed record the receipt hashes and the served view rebuilds; a miss recorded and the full wire served next call, never a refusal, never an override | `.toolChoice({ classifier, serve, alwaysServe })` + `ToolChoiceRow` + `ToolChoiceOutcomeRow` + `ToolChoiceErrorRow` + `ALWAYS_SERVED_TOOLS` + `recordToolChoice` + `narrowServed` (events `agentfootprint.tool_choice.*`, bench `npm run bench:tool-choice`) | `src/core/agent/toolChoice/` | 9.105.0 |
71
72
 
72
73
  **One law before you add a mapping.** Any function turning caller data into a KEY,
73
74
  a namespace, a filename or an index entry must be injective, and its collision
@@ -158,7 +159,7 @@ Traps: `src/observability/` holds the finder IMPLEMENTATIONS (canonical home; `d
158
159
  - **Embedder fingerprint** (8.9.0) → `Embedder.id` (optional; every shipped embedder sets one, and NONE include dims — the store appends `@<dims>` itself, so an id carrying its own size double-stamps) + `indexDocuments` defaulting `embedderId` to it + `SqliteVectorStore.reconcileFingerprint` (the only comparison site). Rule: dimensions ALWAYS decide, model ids decide only when BOTH sides named themselves — refusing on an absent name would block the majority of callers who never pass `embedderId`.
159
160
  - **Retrieval record** (8.8.0) → FOUR stages write one object in sequence: `loadRelevant` (candidates+scores+threshold verdicts) → `pickByBudget` (re-marks admitted→over-budget/over-max-entries) → `formatDefault` (`promptFragment` + `promptPosition`) → the read mount's outputMapper lifts it to root as `retrievalEvidence_<id>`. `memoryRecallInjections` then splits ONE recall into one ActiveInjection PER CHUNK — guarded by a byte-equality check (`fragments.join('\n\n') === systemContent`) that falls back to the single injection rather than change the prompt. `rank` (score order) and `promptPosition` (picker order) are DIFFERENT and both load-bearing: joining fragments in rank order reproduces the right bytes in a sequence the model never saw.
160
161
  - **AgentState** → all 8 stages/ files, both builders' mappers, memory-wire STRING-TYPED keys ('runIdentity'/'turnNumber'/… buildAgentChart.ts:177-180 — not refactor-safe), finalizeResult's `reliabilityFail*`/`policyHalt*` reads (rename silently kills the typed errors).
161
- - **events/** → 115 typed events across 25 domains (counts anti-drift-tested against this file — update BOTH when adding events): ALL_EVENT_TYPES exhaustiveness tests, DomainWildcard hand-list, ~42 importers (recorders, strategies, stream, commentary).
162
+ - **events/** → 118 typed events across 26 domains (counts anti-drift-tested against this file — update BOTH when adding events): ALL_EVENT_TYPES exhaustiveness tests, DomainWildcard hand-list, ~42 importers (recorders, strategies, stream, commentary).
162
163
  - **Run-configuration manifest (9.41.0)** → `agentfootprint.agent.run_configured`, the JOIN KEY that turns N runs into N labelled ARMS: one event naming the adapters/strategies in play (provider+model, reactMode, each memory's declared strategy/retrieval/embedder, window, graph posture+classifier, evidence posture, artifacts-present). Composed by the PURE `core/agent/runManifest.ts`, dispatched from `Agent.emitRunManifest()` at the END of `createExecutor` — the ONE funnel `run()` AND `resume()` share, both of which mint a fresh runId. Direct `dispatcher.dispatch` with a STATED pseudo-stage (`run-configured#0`), the `emitToolSessionReport` precedent — there is no stage yet, and `minimalMeta()` would make the one joinable-by-design event unjoinable. TWO laws, both tested: NAMES ONLY (a store is reported PRESENT and unnamed rather than identified by a directory/endpoint — `MemoryStore` and `ArtifactStore` declare no id), and ABSENT means "not configured", never a guessed `'default'`. Graph presence is read off `skillGraphNextSkill`, NOT `skillGraphCascade` (a 9.16-style mount sets no cascade and would read as "no graph"). `MemoryDefinition` gained `strategy`/`retrieval`/`embedderId` for it — declared names the compiled pipeline had closed over, the `store`-in-the-open precedent.
163
164
  - **adapters/types.ts LLMMessage/LLMRequest** → 62 importers: tool_use round-trip (toolCalls.ts:115-135), wire assembly (callLLM.ts:150-160), providers, cache strategies, security/extractSequence, reliability loop.
164
165
  - **Cache** → strategy registration is a MODULE SIDE EFFECT (src/index.ts:15-17); an entry point skipping that import silently falls back to NoOp. Resolved once per Agent at construction (Agent.ts:347).
package/README.md CHANGED
@@ -932,6 +932,7 @@ The flowchart, recorders, and tests don't change between dev and prod.
932
932
  - Resilience primitives — `withRetry`, `withFallback`, `withCircuitBreaker`, `.outputFallback`, `agent.resumeOnError`
933
933
  - Context Integrity — deterministic checks at the seams where a run contradicts ITSELF: a tool parked but still on the wire, a tool offered after the results grounding it were evicted, an answer field that disagrees with the fact it claims to report (`.claims()`, requires `.outputSchema()`). Nothing is blocked or rewritten — each defect is one typed finding, and every run files a disposition ledger so "no findings" and "no check ran" stay different states. `integrityPosture: 'dev'` adds the liveness proofs (a start-of-run canary; `CheckerDeadError` instead of a green report from a checker that never ran). Read it back with `find_context_errors` over a recording — [Context Integrity](https://agentfootprint.dev/docs/monitor/context-integrity/)
934
934
  - Findings ledger — `.findings()` adds one reserved optional argument, `_findings`, to every served tool schema: the model declares why it makes each call (`basis`: `direct` or `exploratory`) and, on its next call or as a top-level key of a JSON answer, the standing of each earlier result by its tool_result id (`fact` with the assertions it stands on, `open`, `ruled-out`, `noise`). Zero extra model calls. The argument is peeled off before the tool, the middleware or the validator sees the call; the rows land as one append-only `findingsLedger` on the run's state (`agent.findings()`), with a `conflict` row whenever two stood-on readings disagree. Nothing is inferred — a call that declares nothing files nothing — and nothing is served differently yet: the ledger is a record. An agent that never calls `.findings()` is byte-identical to one built before it existed
935
+ - Tool choice by classifier — `.toolChoice({ classifier, serve })` asks a calibrated classifier (`agentfootprint/classify`) at every model call which of the tools about to be served answers the current step, and files its ranking, its pick and its cost as a `pick` row beside the model's own call, then an `outcome` row after the reply (`firstAgrees`, any `miss`). The model's call is never overridden. `serve: { top: N }` narrows the served list to the classifier's top-N plus the doors (`read_skill`, `skip_step`, `present`, your `alwaysServe`), on the committed record the receipt hashes and `servedAt` rebuilds; a failed classifier serves the full set, a miss serves the full set on the next call. `npm run bench:tool-choice` measures agreement, misses and tools-slot bytes from the record. An agent without `.toolChoice()` is byte-identical
935
936
  - 60+ typed observability events — `agent` · `composition` · `context` · `stream` · `tools` · `skill` · `memory` · `cache` · `cost` · `permission` · `eval` · `embedding` · `pause` · `error` · `fallback` · `resilience` · `reliability` · `risk`
936
937
 
937
938
  **Debugging & compliance** (`agentfootprint/observe`)
@@ -252,7 +252,7 @@ const agent = Agent.create({ provider, model })
252
252
  agent.on('agentfootprint.context.evaluated', (e) => console.log(e.payload.activeIds));
253
253
  ```
254
254
 
255
- **115 typed events across 25 domains.** Two subscription shapes and no third:
255
+ **118 typed events across 26 domains.** Two subscription shapes and no third:
256
256
  `'*'` (every event) and `'agentfootprint.<domain>.*'` (one domain). **`'agentfootprint.*'`
257
257
  is not a pattern** — TypeScript rejects it, and at runtime it would match nothing.
258
258
 
@@ -287,6 +287,13 @@ class Agent extends RunnerBase_js_1.RunnerBase {
287
287
  * armed agent with a window, where `stages/window.ts · buildWindowStage`
288
288
  * spends it as the `'ledger-fact'` pin ceiling. */
289
289
  findingsOptions;
290
+ /** Tool choice by classifier (9.105.0, `.toolChoice()`): the classifier,
291
+ * the serve dial and the app's own doors. Threaded to the tools slot (the
292
+ * pick and the narrowing), to call-llm (`toolChoice: true`, the outcome
293
+ * row) and to both chart builders (`hasToolChoice`, the mount args) on an
294
+ * armed agent only — an unarmed agent hands each stage exactly the deps it
295
+ * always did. */
296
+ toolChoiceOptions;
290
297
  /** The opt-in tool-result ceiling in characters (9.11.0). Absent → results
291
298
  * are never measured. See {@link AgentOptions.maxToolResultChars}. */
292
299
  maxToolResultChars;
@@ -661,6 +668,8 @@ class Agent extends RunnerBase_js_1.RunnerBase {
661
668
  this.toolArgValidation = opts.toolArgValidation;
662
669
  if (opts.findings !== undefined)
663
670
  this.findingsOptions = opts.findings;
671
+ if (opts.toolChoice !== undefined)
672
+ this.toolChoiceOptions = opts.toolChoice;
664
673
  // The tool-result ceiling (9.11.0). Refused HERE, naming the value, rather
665
674
  // than at the first tool call of the first run — a dial that cannot cap
666
675
  // anything is a configuration mistake, not a runtime condition.
@@ -757,6 +766,16 @@ class Agent extends RunnerBase_js_1.RunnerBase {
757
766
  }
758
767
  this.keepLedgerFacts = resolveKeepLedgerFacts(this.findingsOptions.keepLedgerFacts ?? opts.keepLedgerFacts);
759
768
  }
769
+ // Tool choice by classifier (9.105.0) needs the same per-call recomposition
770
+ // the ledger does — the pick is made where the served list is built, and
771
+ // a narrowed list committed on turn 1 would be served on every later call
772
+ // with no pick behind it. The same refusal, at the same place.
773
+ if (this.toolChoiceOptions !== undefined && this.reactMode === 'classic') {
774
+ throw new Error("Agent: .toolChoice() requires per-iteration slot recomposition — reactMode 'classic' " +
775
+ 'caches the tools slot on turn 1, so the classifier would be asked once and a narrowed ' +
776
+ "list would be served on every later call. Use the default 'dynamic' mode (or " +
777
+ "'dynamic-grouped').");
778
+ }
760
779
  // Refused at construction, never mid-run — a misspelled posture that was
761
780
  // ignored would leave the liveness theorems switched off in an agent
762
781
  // that believes they are on (the concurrency-mode precedent).
@@ -2585,6 +2604,17 @@ class Agent extends RunnerBase_js_1.RunnerBase {
2585
2604
  getRunContext: getRunCtx,
2586
2605
  }));
2587
2606
  }
2607
+ // Same wiring for `agentfootprint.tool_choice.*` (9.105.0) — the three
2608
+ // events `recordToolChoice` files on scope. Attached only under
2609
+ // `.toolChoice()`, for the same reason.
2610
+ if (this.toolChoiceOptions !== undefined) {
2611
+ attachObserver(new EmitBridge_js_1.EmitBridge({
2612
+ id: 'agentfootprint.tool-choice-bridge',
2613
+ prefix: 'agentfootprint.tool_choice.',
2614
+ dispatcher,
2615
+ getRunContext: getRunCtx,
2616
+ }));
2617
+ }
2588
2618
  for (const r of this.attachedRecorders) {
2589
2619
  // A recorder's OWN `delivery` field is more specific than the
2590
2620
  // agent-level default — footprintjs's options bag would override the
@@ -3549,6 +3579,19 @@ class Agent extends RunnerBase_js_1.RunnerBase {
3549
3579
  // The findings ledger (9.101.0): the ONE decoration site is inside this
3550
3580
  // slot; the gate rides in value-conditionally.
3551
3581
  ...(this.findingsOptions !== undefined && { findings: true }),
3582
+ // Tool choice by classifier (9.105.0): the pick and the narrowing live
3583
+ // inside this slot too, at the same site; value-conditional.
3584
+ ...(this.toolChoiceOptions !== undefined && {
3585
+ toolChoice: {
3586
+ classifier: this.toolChoiceOptions.classifier,
3587
+ ...(typeof this.toolChoiceOptions.serve === 'object' && {
3588
+ top: this.toolChoiceOptions.serve.top,
3589
+ }),
3590
+ ...(this.toolChoiceOptions.alwaysServe !== undefined && {
3591
+ alwaysServe: this.toolChoiceOptions.alwaysServe,
3592
+ }),
3593
+ },
3594
+ }),
3552
3595
  });
3553
3596
  // callLLM extracted to ./agent/stages/callLLM.ts (v2.11.2). Same
3554
3597
  // late-binding pattern as seed for toolSchemas (computed below).
@@ -3575,6 +3618,8 @@ class Agent extends RunnerBase_js_1.RunnerBase {
3575
3618
  }),
3576
3619
  }),
3577
3620
  ...(this.answerValidationConfig !== undefined && { suppressDraftTokens: true }),
3621
+ // Tool choice by classifier (9.105.0): the outcome row after the reply.
3622
+ ...(this.toolChoiceOptions !== undefined && { toolChoice: true }),
3578
3623
  // The receipt's salt (9.88.0) — read per call, like seed's own accessor.
3579
3624
  getRunId: () => this.currentRunContext?.runId,
3580
3625
  // …and its off switch. Value-conditional, so an agent on the default
@@ -3941,6 +3986,9 @@ class Agent extends RunnerBase_js_1.RunnerBase {
3941
3986
  // grammar as the slot's `findings` and callLLM's `hasFindingsLedger`,
3942
3987
  // so the three can never disagree about whether the ledger is armed.
3943
3988
  ...(this.findingsOptions !== undefined && { hasFindingsLedger: true }),
3989
+ // Tool choice by classifier (9.105.0): the mount args on the Tools
3990
+ // branch and the key across the sf-llm-call boundary, under the arm.
3991
+ ...(this.toolChoiceOptions !== undefined && { hasToolChoice: true }),
3944
3992
  // `.limitsTravelWithTheAnswer()` (this release) — value-conditional, the
3945
3993
  // `resolvedModel` precedent: absent from the deps object entirely for an
3946
3994
  // agent that did not ask, so both builders mount the final-branch stage