agentfootprint 9.91.0 → 9.93.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 (98) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +236 -0
  3. package/CLAUDE.md +1 -1
  4. package/ai-instructions/claude-code/SKILL.md +1 -1
  5. package/dist/core/Agent.js +38 -7
  6. package/dist/core/Agent.js.map +1 -1
  7. package/dist/core/LLMCall.js +6 -4
  8. package/dist/core/LLMCall.js.map +1 -1
  9. package/dist/core/agent/buildAgentMessageApiChart.js +10 -0
  10. package/dist/core/agent/buildAgentMessageApiChart.js.map +1 -1
  11. package/dist/core/agent/buildToolRegistry.js +90 -33
  12. package/dist/core/agent/buildToolRegistry.js.map +1 -1
  13. package/dist/core/agent/messageApiReceipt.js +1 -0
  14. package/dist/core/agent/messageApiReceipt.js.map +1 -1
  15. package/dist/core/agent/stages/callLLM.js +18 -7
  16. package/dist/core/agent/stages/callLLM.js.map +1 -1
  17. package/dist/core/agent/stages/toolCalls.js +176 -28
  18. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  19. package/dist/core/agent/stages/window.js +12 -2
  20. package/dist/core/agent/stages/window.js.map +1 -1
  21. package/dist/core/agent/window/evictedTurns.js +60 -0
  22. package/dist/core/agent/window/evictedTurns.js.map +1 -0
  23. package/dist/core/slots/buildToolsSlot.js +209 -62
  24. package/dist/core/slots/buildToolsSlot.js.map +1 -1
  25. package/dist/esm/core/Agent.d.ts +4 -0
  26. package/dist/esm/core/Agent.js +39 -8
  27. package/dist/esm/core/Agent.js.map +1 -1
  28. package/dist/esm/core/LLMCall.js +6 -4
  29. package/dist/esm/core/LLMCall.js.map +1 -1
  30. package/dist/esm/core/agent/buildAgentMessageApiChart.js +10 -0
  31. package/dist/esm/core/agent/buildAgentMessageApiChart.js.map +1 -1
  32. package/dist/esm/core/agent/buildToolRegistry.d.ts +80 -31
  33. package/dist/esm/core/agent/buildToolRegistry.js +90 -33
  34. package/dist/esm/core/agent/buildToolRegistry.js.map +1 -1
  35. package/dist/esm/core/agent/messageApiReceipt.d.ts +3 -1
  36. package/dist/esm/core/agent/messageApiReceipt.js +1 -0
  37. package/dist/esm/core/agent/messageApiReceipt.js.map +1 -1
  38. package/dist/esm/core/agent/stages/callLLM.d.ts +9 -0
  39. package/dist/esm/core/agent/stages/callLLM.js +18 -7
  40. package/dist/esm/core/agent/stages/callLLM.js.map +1 -1
  41. package/dist/esm/core/agent/stages/toolCalls.d.ts +43 -5
  42. package/dist/esm/core/agent/stages/toolCalls.js +174 -27
  43. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  44. package/dist/esm/core/agent/stages/window.d.ts +8 -0
  45. package/dist/esm/core/agent/stages/window.js +12 -2
  46. package/dist/esm/core/agent/stages/window.js.map +1 -1
  47. package/dist/esm/core/agent/types.d.ts +15 -0
  48. package/dist/esm/core/agent/window/evictedTurns.d.ts +52 -0
  49. package/dist/esm/core/agent/window/evictedTurns.js +56 -0
  50. package/dist/esm/core/agent/window/evictedTurns.js.map +1 -0
  51. package/dist/esm/core/slots/buildToolsSlot.d.ts +100 -0
  52. package/dist/esm/core/slots/buildToolsSlot.js +207 -61
  53. package/dist/esm/core/slots/buildToolsSlot.js.map +1 -1
  54. package/dist/esm/events/payloads.d.ts +92 -18
  55. package/dist/esm/events/registry.d.ts +5 -1
  56. package/dist/esm/events/registry.js +4 -0
  57. package/dist/esm/events/registry.js.map +1 -1
  58. package/dist/esm/lib/time-travel/receipt.d.ts +71 -28
  59. package/dist/esm/lib/time-travel/receipt.js +33 -17
  60. package/dist/esm/lib/time-travel/receipt.js.map +1 -1
  61. package/dist/esm/lib/time-travel/servedView.d.ts +31 -22
  62. package/dist/esm/lib/time-travel/servedView.js +78 -43
  63. package/dist/esm/lib/time-travel/servedView.js.map +1 -1
  64. package/dist/events/registry.js +4 -0
  65. package/dist/events/registry.js.map +1 -1
  66. package/dist/lib/time-travel/receipt.js +33 -17
  67. package/dist/lib/time-travel/receipt.js.map +1 -1
  68. package/dist/lib/time-travel/servedView.js +78 -43
  69. package/dist/lib/time-travel/servedView.js.map +1 -1
  70. package/dist/types/core/Agent.d.ts +4 -0
  71. package/dist/types/core/Agent.d.ts.map +1 -1
  72. package/dist/types/core/LLMCall.d.ts.map +1 -1
  73. package/dist/types/core/agent/buildAgentMessageApiChart.d.ts.map +1 -1
  74. package/dist/types/core/agent/buildToolRegistry.d.ts +80 -31
  75. package/dist/types/core/agent/buildToolRegistry.d.ts.map +1 -1
  76. package/dist/types/core/agent/messageApiReceipt.d.ts +3 -1
  77. package/dist/types/core/agent/messageApiReceipt.d.ts.map +1 -1
  78. package/dist/types/core/agent/stages/callLLM.d.ts +9 -0
  79. package/dist/types/core/agent/stages/callLLM.d.ts.map +1 -1
  80. package/dist/types/core/agent/stages/toolCalls.d.ts +43 -5
  81. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  82. package/dist/types/core/agent/stages/window.d.ts +8 -0
  83. package/dist/types/core/agent/stages/window.d.ts.map +1 -1
  84. package/dist/types/core/agent/types.d.ts +15 -0
  85. package/dist/types/core/agent/types.d.ts.map +1 -1
  86. package/dist/types/core/agent/window/evictedTurns.d.ts +53 -0
  87. package/dist/types/core/agent/window/evictedTurns.d.ts.map +1 -0
  88. package/dist/types/core/slots/buildToolsSlot.d.ts +100 -0
  89. package/dist/types/core/slots/buildToolsSlot.d.ts.map +1 -1
  90. package/dist/types/events/payloads.d.ts +92 -18
  91. package/dist/types/events/payloads.d.ts.map +1 -1
  92. package/dist/types/events/registry.d.ts +5 -1
  93. package/dist/types/events/registry.d.ts.map +1 -1
  94. package/dist/types/lib/time-travel/receipt.d.ts +71 -28
  95. package/dist/types/lib/time-travel/receipt.d.ts.map +1 -1
  96. package/dist/types/lib/time-travel/servedView.d.ts +31 -22
  97. package/dist/types/lib/time-travel/servedView.d.ts.map +1 -1
  98. package/package.json +1 -1
@@ -11,10 +11,14 @@
11
11
  * Treats that answer as the paused tool's result, appends to
12
12
  * history, then continues the ReAct iteration loop.
13
13
  *
14
- * Dispatch resolution order:
15
- * 1. Static registry built at chart-build time (registryByName).
16
- * 2. External `ToolProvider.list(ctx).find(...)` if a `.toolProvider()`
17
- * was wired and the tool isn't in the static registry.
14
+ * Dispatch resolution order (9.92.0 — DISPATCH FOLLOWS THE OFFER):
15
+ * 0. A name on THIS epoch's wire resolves to the party whose contract the
16
+ * model read (`servedTools`, written by the tools slot from the same
17
+ * merge that built the wire): a provider's schema → the provider's tool
18
+ * from `providerToolCache`; anything else → `registryByName`.
19
+ * 1. A name NOT on the wire this epoch — held out by a step, a park or a
20
+ * scoping, or named from a restored transcript — resolves as it always
21
+ * did: `registryByName` first, then the provider cache.
18
22
  *
19
23
  * Permission gate (when `permissionChecker` is configured) runs BEFORE
20
24
  * `tool.execute`. Deny → tool not executed; result is a synthetic
@@ -53,7 +57,8 @@ import type { ArtifactStore } from '../../../artifacts/types.js';
53
57
  import { type ArtifactPlacement } from '../../../artifacts/placement.js';
54
58
  import type { AuthorizationRequiredMode } from '../../../identity/consent.js';
55
59
  import { type ResolvedCheckInConfig } from '../../checkin.js';
56
- import type { ProviderToolCache } from '../../slots/buildToolsSlot.js';
60
+ import type { ProviderToolCache, ServedToolParties } from '../../slots/buildToolsSlot.js';
61
+ import type { ToolClaim } from '../buildToolRegistry.js';
57
62
  import type { Tool } from '../../tools.js';
58
63
  import type { MemoryIdentity } from '../../../memory/identity/types.js';
59
64
  import type { ToolSessionTier } from '../../toolSessions.js';
@@ -143,6 +148,21 @@ export interface ToolCallsHandlerDeps {
143
148
  * within one chart build.
144
149
  */
145
150
  readonly providerToolCache?: ProviderToolCache;
151
+ /**
152
+ * Who put each name on this epoch's wire (9.92.0) — the tools slot's own
153
+ * record of its merge, shared by reference like `providerToolCache`. The
154
+ * first thing `lookupTool` consults, so the party whose contract the model
155
+ * read is the party that answers. Absent (a chart without the slot's
156
+ * record) → resolution order 1 alone, exactly as before 9.92.0.
157
+ */
158
+ readonly servedTools?: ServedToolParties;
159
+ /**
160
+ * Every build-time claimant per tool name (9.92.0) — `buildToolRegistry`'s
161
+ * `toolClaimants`. Read for one thing: which party a registry-routed tool
162
+ * belongs to, so an off-wire dispatch can be REPORTED with its answering
163
+ * party (`tools.answered_off_wire`). Absent → `'registry'`.
164
+ */
165
+ readonly toolClaimants?: ReadonlyMap<string, readonly ToolClaim[]>;
146
166
  /** Optional permission gate. When present, every tool dispatch
147
167
  * awaits `check({capability: 'tool_call', ...})` BEFORE executing.
148
168
  * Throwing checkers are treated as deny-by-default. */
@@ -495,6 +515,24 @@ export declare function atMountedCursor(args: {
495
515
  * Exported for the producer registry, and used by BOTH dispatch doors so they
496
516
  * cannot drift.
497
517
  */
518
+ /**
519
+ * What a call gets back when the party whose contract the model read under
520
+ * this name cannot answer it, and no other party may (9.92.0).
521
+ *
522
+ * Two doors reach here. A name that LEFT the wire — the model read a
523
+ * provider's contract on an earlier call and the provider withdrew the name —
524
+ * whose only remaining holder is a party the model was never shown under that
525
+ * name (a scoped skill that never activated): dispatching that holder would
526
+ * hand the model's belief to a stranger's function, which is the silent
527
+ * identity swap this release closed. And a resume in a fresh Agent instance
528
+ * of a provider-served call, where the served party is on the checkpoint but
529
+ * the provider's list has not been resolved on this instance.
530
+ *
531
+ * It names no roster on purpose: `unknownToolResult`'s roster is the DISPATCH
532
+ * map, and the map here holds exactly the implementation this sentence is
533
+ * refusing. Anchored past tense, one clause, like every persistent result.
534
+ */
535
+ export declare function notServedResult(toolName: string): string;
498
536
  export declare function unknownToolResult(toolName: string, dispatchable: SpokenIds): string;
499
537
  /**
500
538
  * THE ONE COMPOSER of every `read_skill` refusal (9.86.0) — reachability,
@@ -11,10 +11,14 @@
11
11
  * Treats that answer as the paused tool's result, appends to
12
12
  * history, then continues the ReAct iteration loop.
13
13
  *
14
- * Dispatch resolution order:
15
- * 1. Static registry built at chart-build time (registryByName).
16
- * 2. External `ToolProvider.list(ctx).find(...)` if a `.toolProvider()`
17
- * was wired and the tool isn't in the static registry.
14
+ * Dispatch resolution order (9.92.0 — DISPATCH FOLLOWS THE OFFER):
15
+ * 0. A name on THIS epoch's wire resolves to the party whose contract the
16
+ * model read (`servedTools`, written by the tools slot from the same
17
+ * merge that built the wire): a provider's schema → the provider's tool
18
+ * from `providerToolCache`; anything else → `registryByName`.
19
+ * 1. A name NOT on the wire this epoch — held out by a step, a park or a
20
+ * scoping, or named from a restored transcript — resolves as it always
21
+ * did: `registryByName` first, then the provider cache.
18
22
  *
19
23
  * Permission gate (when `permissionChecker` is configured) runs BEFORE
20
24
  * `tool.execute`. Deny → tool not executed; result is a synthetic
@@ -216,6 +220,27 @@ export function atMountedCursor(args) {
216
220
  * Exported for the producer registry, and used by BOTH dispatch doors so they
217
221
  * cannot drift.
218
222
  */
223
+ /**
224
+ * What a call gets back when the party whose contract the model read under
225
+ * this name cannot answer it, and no other party may (9.92.0).
226
+ *
227
+ * Two doors reach here. A name that LEFT the wire — the model read a
228
+ * provider's contract on an earlier call and the provider withdrew the name —
229
+ * whose only remaining holder is a party the model was never shown under that
230
+ * name (a scoped skill that never activated): dispatching that holder would
231
+ * hand the model's belief to a stranger's function, which is the silent
232
+ * identity swap this release closed. And a resume in a fresh Agent instance
233
+ * of a provider-served call, where the served party is on the checkpoint but
234
+ * the provider's list has not been resolved on this instance.
235
+ *
236
+ * It names no roster on purpose: `unknownToolResult`'s roster is the DISPATCH
237
+ * map, and the map here holds exactly the implementation this sentence is
238
+ * refusing. Anchored past tense, one clause, like every persistent result.
239
+ */
240
+ export function notServedResult(toolName) {
241
+ return (`Tool '${toolName}' did not run on that call: the implementation whose contract was ` +
242
+ `offered under that name was not available to answer it.`);
243
+ }
219
244
  export function unknownToolResult(toolName, dispatchable) {
220
245
  const head = `Unknown tool '${toolName}' on that call.`;
221
246
  if (dispatchable.named.length > 0) {
@@ -422,7 +447,7 @@ function appendBatchResult(scope, entry) {
422
447
  */
423
448
  const UNSCOPED_RUN = '#no-run-id';
424
449
  export function buildToolCallsHandler(deps) {
425
- const { registryByName, externalToolProvider, providerToolCache, permissionChecker } = deps;
450
+ const { registryByName, externalToolProvider, providerToolCache, servedTools, toolClaimants, permissionChecker, } = deps;
426
451
  const toolArgValidation = deps.toolArgValidation ?? 'enforce';
427
452
  // 8.6.0 — default `'pause'`. Consent is work waiting on a person, and every
428
453
  // other place this library needs a person stops the run and asks the caller.
@@ -1686,15 +1711,91 @@ export function buildToolCallsHandler(deps) {
1686
1711
  // one resolver. The Tools slot already invoked `provider.list(ctx)` this
1687
1712
  // iteration and cached the resolved Tool[] in `providerToolCache` — read from
1688
1713
  // there to avoid a second discovery call (vital for async network providers).
1689
- const lookupTool = (toolName) => {
1690
- const fromRegistry = registryByName.get(toolName);
1691
- if (fromRegistry)
1692
- return fromRegistry;
1693
- if (!externalToolProvider)
1694
- return undefined;
1695
- const cached = providerToolCache?.current ?? [];
1696
- return cached.find((t) => t.schema.name === toolName);
1714
+ //
1715
+ // DISPATCH FOLLOWS THE OFFER (9.92.0). A name on this epoch's wire resolves
1716
+ // to the implementation of the party whose contract the model read — the
1717
+ // slot's own record of its merge (`servedTools`), never a re-derivation.
1718
+ // Before this, `registryByName` was consulted first for every name, so a
1719
+ // provider's contract on the wire could be answered by a skill's `execute`
1720
+ // (an INACTIVE skill's, even) and a provider's `skip_step` by the framework's
1721
+ // — recorded-not-built entries 1 and 3.
1722
+ //
1723
+ // A name NOT on the wire keeps the capability law's held-out clause — a
1724
+ // later step's tool, a parked map's tool, a scoped tool named from a
1725
+ // restored transcript still run — under ONE condition: the party that
1726
+ // answers is the party the model LAST READ the name under (`lastServed`), or
1727
+ // the name's only holder when it was never served this run. A name whose
1728
+ // last-served party can no longer answer (a provider withdrew it) is REFUSED
1729
+ // as a recorded tool result rather than handed to a party the model was
1730
+ // never shown under that name; a never-served name held by two parties is
1731
+ // refused the same way. Every off-wire dispatch that DOES happen is put on
1732
+ // the record by `tools.answered_off_wire`.
1733
+ const fromProviderCache = (toolName) => externalToolProvider
1734
+ ? (providerToolCache?.current ?? []).find((t) => t.schema.name === toolName)
1735
+ : undefined;
1736
+ const providerParty = {
1737
+ channel: 'provider',
1738
+ ...(externalToolProvider?.id !== undefined && { id: externalToolProvider.id }),
1739
+ };
1740
+ /** The party a registry-routed name belongs to — its first build-time claimant. */
1741
+ const registryPartyOf = (toolName) => {
1742
+ const first = toolClaimants?.get(toolName)?.[0];
1743
+ return first === undefined
1744
+ ? { channel: 'registry' }
1745
+ : { channel: first.channel, ...(first.id !== undefined && { id: first.id }) };
1746
+ };
1747
+ const resolveTool = (toolName, pinned) => {
1748
+ const wireParty = pinned ?? servedTools?.current.get(toolName);
1749
+ if (wireParty !== undefined) {
1750
+ const owner = wireParty.channel === 'provider'
1751
+ ? fromProviderCache(toolName)
1752
+ : registryByName.get(toolName);
1753
+ return owner !== undefined
1754
+ ? { tool: owner, party: wireParty }
1755
+ : { refusal: notServedResult(toolName) };
1756
+ }
1757
+ const registryHolder = registryByName.get(toolName);
1758
+ const providerHolder = fromProviderCache(toolName);
1759
+ if (registryHolder === undefined && providerHolder === undefined)
1760
+ return {};
1761
+ const last = servedTools?.lastServed.get(toolName);
1762
+ if (last !== undefined) {
1763
+ const owner = last.channel === 'provider' ? providerHolder : registryHolder;
1764
+ return owner !== undefined
1765
+ ? { tool: owner, party: last, offWire: true }
1766
+ : { refusal: notServedResult(toolName) };
1767
+ }
1768
+ if (registryHolder !== undefined && providerHolder !== undefined) {
1769
+ return { refusal: notServedResult(toolName) };
1770
+ }
1771
+ return registryHolder !== undefined
1772
+ ? { tool: registryHolder, party: registryPartyOf(toolName), offWire: true }
1773
+ : // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1774
+ { tool: providerHolder, party: providerParty, offWire: true };
1697
1775
  };
1776
+ const lookupTool = (toolName, pinned) => resolveTool(toolName, pinned).tool;
1777
+ /** The record of an off-wire dispatch — once per such call, before it runs. */
1778
+ const noteOffWire = (scope, resolved, call) => {
1779
+ if (resolved.offWire !== true || resolved.party === undefined)
1780
+ return;
1781
+ typedEmit(scope, 'agentfootprint.tools.answered_off_wire', {
1782
+ toolName: call.toolName,
1783
+ toolCallId: call.toolCallId,
1784
+ iteration: call.iteration,
1785
+ answeredBy: resolved.party.channel,
1786
+ ...(resolved.party.id !== undefined && { answeredById: resolved.party.id }),
1787
+ });
1788
+ };
1789
+ // The framework's own `skip_step` — `buildToolRegistry` installs it in the
1790
+ // dispatch map whenever a skill declares steps. The step bookkeeping below
1791
+ // keys on THIS instance having answered, not on the name: since 9.92.0 a
1792
+ // provider's `skip_step` on the wire is the provider's to answer, and a
1793
+ // procedure must not advance on a call whose contract the model read from
1794
+ // somebody else (recorded-not-built, entry 3).
1795
+ const frameworkSkipStep = deps.stepPlanFor ? registryByName.get(SKIP_STEP_TOOL_NAME) : undefined;
1796
+ const frameworkSkipStepAnswered = (name, answered) => frameworkSkipStep !== undefined &&
1797
+ name === SKIP_STEP_TOOL_NAME &&
1798
+ answered === frameworkSkipStep;
1698
1799
  /**
1699
1800
  * Every member id of every map the kernel currently holds PARKED (9.86.0
1700
1801
  * fix pass) — asked of `parkedMemberIds`, the kernel's own owner of the fact.
@@ -1809,9 +1910,17 @@ export function buildToolCallsHandler(deps) {
1809
1910
  // runs now. Fail-closed: a blocked/failed credential surfaces to the model
1810
1911
  // and the tool does NOT run. A tool that pauses again during an approved
1811
1912
  // resume can't re-pause (resume returns void), so that is surfaced as an error.
1812
- const resolveCredentialAndExecute = async (scope, tool, toolName, args, toolCallId, iteration, env) => {
1813
- if (!tool)
1814
- return { result: unknownToolResult(toolName, dispatchRoster(scope)), error: true };
1913
+ const resolveCredentialAndExecute = async (scope, resolved, toolName, args, toolCallId, iteration, env) => {
1914
+ const tool = resolved.tool;
1915
+ // The two refusals share one door here too (9.92.0): a name nothing holds,
1916
+ // and a name whose offered party cannot answer on this resume.
1917
+ if (!tool) {
1918
+ return {
1919
+ result: resolved.refusal ?? unknownToolResult(toolName, dispatchRoster(scope)),
1920
+ error: true,
1921
+ };
1922
+ }
1923
+ noteOffWire(scope, resolved, { toolName, toolCallId, iteration });
1815
1924
  // Declared artifact arguments (9.22.0) — the same resolution the batch
1816
1925
  // loop applies, at this door: a resumed call's refs are judged exactly
1817
1926
  // as an inline call's, BEFORE credentials, and a refusal means the tool
@@ -2118,7 +2227,8 @@ export function buildToolCallsHandler(deps) {
2118
2227
  const runIdentity = scope.runIdentity;
2119
2228
  const env = scope.$getEnv();
2120
2229
  for (const tc of toolCalls) {
2121
- const tool = lookupTool(tc.name);
2230
+ const resolved = resolveTool(tc.name);
2231
+ const tool = resolved.tool;
2122
2232
  typedEmit(scope, 'agentfootprint.stream.tool_start', {
2123
2233
  toolName: tc.name,
2124
2234
  toolCallId: tc.id,
@@ -2389,6 +2499,11 @@ export function buildToolCallsHandler(deps) {
2389
2499
  scope.pausedAskArgs = chain.args;
2390
2500
  scope.pausedAskIndex = chain.index;
2391
2501
  scope.pausedAskMiddleware = chain.middleware;
2502
+ // The party this call was resolved against (9.92.0) — so a resume
2503
+ // in a FRESH instance dispatches to the same party, never to the
2504
+ // build-time map's first holder. Pause-path-only.
2505
+ if (resolved.party !== undefined)
2506
+ scope.pausedToolParty = resolved.party;
2392
2507
  // Which surface will collect the decision — carried so the
2393
2508
  // resume-side ledger rows can say which component ANSWERED.
2394
2509
  // Written only when one exists: a component-less ask's
@@ -2544,6 +2659,9 @@ export function buildToolCallsHandler(deps) {
2544
2659
  scope.pausedToolStartMs = startMs;
2545
2660
  scope.pausedCheckIn = true;
2546
2661
  scope.pausedCheckInArgs = callArgs;
2662
+ // Same carrier as the middleware-ask pause (9.92.0).
2663
+ if (resolved.party !== undefined)
2664
+ scope.pausedToolParty = resolved.party;
2547
2665
  // Which surface will collect the decision — read back on resume
2548
2666
  // so `checkin.decision` says which component ANSWERED. Written
2549
2667
  // only when one exists (byte-identical otherwise).
@@ -2637,6 +2755,9 @@ export function buildToolCallsHandler(deps) {
2637
2755
  scope.pausedCredential = true;
2638
2756
  scope.pausedCredentialArgs = callArgs;
2639
2757
  scope.pausedCredentialService = need.credential;
2758
+ // Same carrier as the middleware-ask pause (9.92.0).
2759
+ if (resolved.party !== undefined)
2760
+ scope.pausedToolParty = resolved.party;
2640
2761
  // A defined return value triggers the footprintjs pause; this
2641
2762
  // object becomes the checkpoint's pauseData, and standingAgent's
2642
2763
  // describePause hands it to the caller as `PendingAsk`.
@@ -2683,8 +2804,13 @@ export function buildToolCallsHandler(deps) {
2683
2804
  }
2684
2805
  if (!credentialBlocked && !wantsBlocked) {
2685
2806
  try {
2686
- if (!tool)
2687
- throw new Error(unknownToolResult(tc.name, dispatchRoster(scope)));
2807
+ // The two refusals share one door: a name nothing holds, and a
2808
+ // name whose offered party cannot answer (9.92.0). Both land as
2809
+ // an errored tool result the model reads, on the same path.
2810
+ if (!tool) {
2811
+ throw new Error(resolved.refusal ?? unknownToolResult(tc.name, dispatchRoster(scope)));
2812
+ }
2813
+ noteOffWire(scope, resolved, { toolName: tc.name, toolCallId: tc.id, iteration });
2688
2814
  // Set BEFORE the await: a tool that throws has still run, and a
2689
2815
  // tool that does not exist has not. This flag is the entire
2690
2816
  // precondition of the after-tool moment below.
@@ -3077,7 +3203,7 @@ export function buildToolCallsHandler(deps) {
3077
3203
  // Postures never gate it — skipping is judgment inside a step, not
3078
3204
  // routing. Zero-cost gate: `stepPlanFor` is undefined on every agent
3079
3205
  // without a stepped skill.
3080
- if (deps.stepPlanFor && tc.name === SKIP_STEP_TOOL_NAME && !error && !denied) {
3206
+ if (frameworkSkipStepAnswered(tc.name, tool) && !error && !denied) {
3081
3207
  result = applySkipStep(scope, { args: callArgs, toolCallId: tc.id, iteration });
3082
3208
  }
3083
3209
  // ── present bookkeeping (9.22.0) ───────────────────────────────
@@ -3453,7 +3579,12 @@ export function buildToolCallsHandler(deps) {
3453
3579
  // continue here must see the same `toolSource` the links before the
3454
3580
  // ask saw. A chain that changed its mind about where a tool came from
3455
3581
  // halfway through one dispatch would be worse than not knowing.
3456
- const tool = lookupTool(toolName);
3582
+ //
3583
+ // Resolved against the party the CHECKPOINT carries (9.92.0): a
3584
+ // fresh instance has an empty served record and Compose has not
3585
+ // re-run, so without it the build-time map's first holder answered.
3586
+ const resolved = resolveTool(toolName, scope.pausedToolParty);
3587
+ const tool = resolved.tool;
3457
3588
  const rest = await runToolChain(deps.toolMiddleware ?? [], {
3458
3589
  toolName,
3459
3590
  ...(tool?.source !== undefined && { toolSource: tool.source }),
@@ -3516,7 +3647,7 @@ export function buildToolCallsHandler(deps) {
3516
3647
  }
3517
3648
  else {
3518
3649
  const env = scope.$getEnv();
3519
- const dispatched = await resolveCredentialAndExecute(scope, tool, toolName, rest.args, toolCallId, iteration, env);
3650
+ const dispatched = await resolveCredentialAndExecute(scope, resolved, toolName, rest.args, toolCallId, iteration, env);
3520
3651
  result = dispatched.result;
3521
3652
  error = dispatched.error;
3522
3653
  // A ceiling-refused call RAN but must not advance a step (9.20.0);
@@ -3531,7 +3662,7 @@ export function buildToolCallsHandler(deps) {
3531
3662
  // placeholder just landed — replace it with the authoritative
3532
3663
  // sentence BEFORE the chain's last word, the execute loop's
3533
3664
  // composition kept.
3534
- if (deps.stepPlanFor && toolName === SKIP_STEP_TOOL_NAME && stepToolRan) {
3665
+ if (frameworkSkipStepAnswered(toolName, tool) && stepToolRan) {
3535
3666
  result = applySkipStep(scope, { args: rest.args, toolCallId, iteration });
3536
3667
  }
3537
3668
  // present behind a middleware ask, approved (9.22.0): same
@@ -3635,6 +3766,11 @@ export function buildToolCallsHandler(deps) {
3635
3766
  // carried one must not gain the key (byte-identical).
3636
3767
  if (answeredVia !== undefined)
3637
3768
  scope.pausedComponentId = undefined;
3769
+ // The served party the checkpoint carried is consumed with it (9.92.0);
3770
+ // written back only when it was there, so an older checkpoint's resume
3771
+ // commits exactly what it always did.
3772
+ if (scope.pausedToolParty !== undefined)
3773
+ scope.pausedToolParty = undefined;
3638
3774
  return;
3639
3775
  }
3640
3776
  // ── Check-in decision path ───────────────────────────────────────
@@ -3677,8 +3813,10 @@ export function buildToolCallsHandler(deps) {
3677
3813
  let resumeEnvelope;
3678
3814
  if (decision.approved) {
3679
3815
  const env = scope.$getEnv();
3680
- const tool = lookupTool(toolName);
3681
- const dispatched = await resolveCredentialAndExecute(scope, tool, toolName, args, toolCallId, iteration, env);
3816
+ // Resolved against the party the checkpoint carries (9.92.0).
3817
+ const resolved = resolveTool(toolName, scope.pausedToolParty);
3818
+ const tool = resolved.tool;
3819
+ const dispatched = await resolveCredentialAndExecute(scope, resolved, toolName, args, toolCallId, iteration, env);
3682
3820
  result = dispatched.result;
3683
3821
  error = dispatched.error;
3684
3822
  // Same ceiling law as the ask path: ran ≠ completed a step (9.20.0).
@@ -3766,6 +3904,11 @@ export function buildToolCallsHandler(deps) {
3766
3904
  // Cleared ONLY when it was set — see the ask path's twin note.
3767
3905
  if (answeredVia !== undefined)
3768
3906
  scope.pausedComponentId = undefined;
3907
+ // The served party the checkpoint carried is consumed with it (9.92.0);
3908
+ // written back only when it was there, so an older checkpoint's resume
3909
+ // commits exactly what it always did.
3910
+ if (scope.pausedToolParty !== undefined)
3911
+ scope.pausedToolParty = undefined;
3769
3912
  return;
3770
3913
  }
3771
3914
  // ── Credential-consent decision path (8.6.0) ─────────────────────
@@ -3785,8 +3928,10 @@ export function buildToolCallsHandler(deps) {
3785
3928
  const iteration = scope.iteration;
3786
3929
  const args = (scope.pausedCredentialArgs ?? {});
3787
3930
  const env = scope.$getEnv();
3788
- const tool = lookupTool(toolName);
3789
- const dispatched = await resolveCredentialAndExecute(scope, tool, toolName, args, toolCallId, iteration, env);
3931
+ // Resolved against the party the checkpoint carries (9.92.0).
3932
+ const resolved = resolveTool(toolName, scope.pausedToolParty);
3933
+ const tool = resolved.tool;
3934
+ const dispatched = await resolveCredentialAndExecute(scope, resolved, toolName, args, toolCallId, iteration, env);
3790
3935
  const result = dispatched.result;
3791
3936
  const error = dispatched.error;
3792
3937
  let modelResult;
@@ -3865,6 +4010,8 @@ export function buildToolCallsHandler(deps) {
3865
4010
  scope.pausedCredential = false;
3866
4011
  scope.pausedCredentialArgs = undefined;
3867
4012
  scope.pausedCredentialService = undefined;
4013
+ if (scope.pausedToolParty !== undefined)
4014
+ scope.pausedToolParty = undefined;
3868
4015
  return;
3869
4016
  }
3870
4017
  // ── The `pauseHere` / `askHuman` path ────────────────────────────