@sema-agent/core 5.16.0 → 5.17.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 (70) hide show
  1. package/CHANGELOG.md +224 -0
  2. package/dist/agents/peer-admission.d.ts +58 -0
  3. package/dist/agents/peer-admission.js +175 -0
  4. package/dist/agents/retain-ledger.d.ts +1 -1
  5. package/dist/agents/retain-ledger.js +9 -1
  6. package/dist/agents/send-message-tool.d.ts +8 -0
  7. package/dist/agents/send-message-tool.js +171 -21
  8. package/dist/agents/subagent.d.ts +13 -0
  9. package/dist/agents/subagent.js +90 -5
  10. package/dist/core/ask-question.js +16 -1
  11. package/dist/core/canonical-json.js +176 -14
  12. package/dist/core/checkpoint-store.d.ts +14 -0
  13. package/dist/core/checkpoint-store.js +73 -0
  14. package/dist/core/hooks.d.ts +4 -1
  15. package/dist/core/hooks.js +24 -6
  16. package/dist/core/mailbox-store.d.ts +2 -0
  17. package/dist/core/mailbox-store.js +2 -2
  18. package/dist/core/mcp.d.ts +1 -0
  19. package/dist/core/mcp.js +15 -3
  20. package/dist/core/runner/prepare-task.d.ts +4 -0
  21. package/dist/core/runner/prepare-task.js +169 -46
  22. package/dist/core/runner/runtask.js +13 -1
  23. package/dist/core/runner/turn-attachments.d.ts +2 -1
  24. package/dist/core/runner/turn-attachments.js +9 -6
  25. package/dist/core/session-reconcile.js +19 -1
  26. package/dist/core/shared-memory/contract.d.ts +17 -0
  27. package/dist/core/shared-memory/contract.js +138 -0
  28. package/dist/core/shared-memory/normalize.d.ts +73 -0
  29. package/dist/core/shared-memory/normalize.js +259 -0
  30. package/dist/core/shared-memory/tools.d.ts +7 -0
  31. package/dist/core/shared-memory/tools.js +289 -0
  32. package/dist/core/shared-memory/types.d.ts +95 -0
  33. package/dist/core/shared-memory/types.js +18 -0
  34. package/dist/core/task-notification.d.ts +3 -0
  35. package/dist/core/task-registry-agent.d.ts +1 -1
  36. package/dist/core/task-registry-agent.js +2 -1
  37. package/dist/core/task-registry.d.ts +1 -1
  38. package/dist/core/task-registry.js +2 -0
  39. package/dist/core/tool-policy.d.ts +9 -0
  40. package/dist/core/tool-policy.js +28 -8
  41. package/dist/core/types.d.ts +8 -0
  42. package/dist/core/untrusted-text.d.ts +1 -0
  43. package/dist/core/untrusted-text.js +10 -0
  44. package/dist/core/wiring-manifest.js +2 -2
  45. package/dist/engine/harness/agent-harness.d.ts +1 -0
  46. package/dist/engine/harness/agent-harness.js +21 -2
  47. package/dist/engine/llm/validation.js +121 -5
  48. package/dist/engine/loop/agent-loop.d.ts +2 -0
  49. package/dist/engine/loop/agent-loop.js +17 -4
  50. package/dist/index.d.ts +4 -0
  51. package/dist/index.js +4 -0
  52. package/dist/prompts/supervisor.d.ts +1 -1
  53. package/dist/prompts/supervisor.js +1 -1
  54. package/dist/stores/cc/mailbox-store.js +4 -0
  55. package/dist/stores/file/checkpoint-store.d.ts +1 -0
  56. package/dist/stores/file/checkpoint-store.js +1 -0
  57. package/dist/stores/file/mailbox-store.js +2 -2
  58. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  59. package/dist/tools/fs/bash-readonly-classifier.js +11 -10
  60. package/dist/tools/fs/fs-bash.js +6 -6
  61. package/dist/tools/fs/fs-read.d.ts +1 -1
  62. package/dist/tools/fs/fs-read.js +4 -3
  63. package/dist/tools/fs/fs-shared.d.ts +3 -0
  64. package/dist/tools/fs/fs-shared.js +8 -1
  65. package/dist/tools/fs/fs-write.js +8 -8
  66. package/dist/tools/fs/index.d.ts +1 -0
  67. package/dist/tools/fs/index.js +1 -1
  68. package/dist/tools/fs/safety.d.ts +1 -0
  69. package/dist/tools/fs/safety.js +9 -2
  70. package/package.json +1 -1
@@ -20,7 +20,7 @@ import { createSubagentWorktreeHelper, forkGovernanceDenial } from "../../agents
20
20
  import { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME } from "../../agents/agent-transcript-tool.js";
21
21
  import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send-message-tool.js";
22
22
  import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
23
- import { askApproverIdentity, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets } from "../tool-policy.js";
23
+ import { askApproverIdentity, combinePolicies, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
24
24
  import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
25
25
  import { CHANGED_FILES_MTIME_EPS_MS, fenceMcpServerInstructions, renderAgentListingDelta } from "./turn-attachments.js";
26
26
  import { inlineUntrusted } from "../untrusted-text.js";
@@ -39,6 +39,8 @@ import { DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, createOffloadPersist, firstPartyOf
39
39
  import { OUTPUT_TOOL_NAME, REPORT_FINDINGS_TOOL_NAME, SKILL_CONTENT_MAX_CHARS, SKILL_TOOL_NAME, createOutputTool, createReportBlockedTool, createReportFindingsTool, createSkillTool, normalizeSkills } from "./synthetic-tools.js";
40
40
  import { compileOutputSchema } from "./strict-output-schema.js";
41
41
  import { TOOL_SEARCH_NAME, buildDeferredRegistry, classifyDeferred, extractDiscoveredToolNames, createPlaceholderTool, createToolSearchTool, staticSchemaRenderable, } from "./tool-disclosure.js";
42
+ import { createSharedMemoryTools } from "../shared-memory/tools.js";
43
+ import { SHARED_MEMORY_TOOL_NAMES } from "../shared-memory/types.js";
42
44
  import { composeMemoryBlock } from "../memory.js";
43
45
  import { preflightLockedConfig } from "../locked-config.js";
44
46
  import { COMPLIANCE_CAPABILITIES, WEB_FETCH_TOOL_NAME, complianceCallDenial, resolveComplianceDenies } from "../compliance.js";
@@ -75,7 +77,7 @@ import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-
75
77
  import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
76
78
  import { resolveKey } from "../../tools/fs/safety.js";
77
79
  import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
78
- import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, remainingBudgetMicroUsd, resolveCheckpointStore, } from "../checkpoint-store.js";
80
+ import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
79
81
  import { boundInputHashOf } from "../canonical-json.js";
80
82
  import { countElicitOptIns, deriveAskEffective, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
81
83
  import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
@@ -166,6 +168,9 @@ export function resolveEnvLifetimeExpiry(env, observedAt) {
166
168
  return { unanchored: true };
167
169
  return { expiresAt: anchor + lifetimeMs };
168
170
  }
171
+ function isLiveApproverSeat(onAsk) {
172
+ return typeof onAsk === "function";
173
+ }
169
174
  const DEFAULT_UNATTENDED_APPROVAL_TTL_MS = 30 * 24 * 60 * 60 * 1000;
170
175
  function sanitizedTtlMs(ttlMs) {
171
176
  if (ttlMs === undefined)
@@ -180,6 +185,12 @@ export const DEFAULT_IRREVERSIBLE_SCOPE = "irreversible";
180
185
  export function checkpointScopeOf(spec) {
181
186
  return spec.durableApproval?.scope || spec.principal || DEFAULT_IRREVERSIBLE_SCOPE;
182
187
  }
188
+ class ParkRefusal extends Error {
189
+ constructor(message, options) {
190
+ super(message, options);
191
+ this.name = "ParkRefusal";
192
+ }
193
+ }
183
194
  export { resolveCheckpointStore } from "../checkpoint-store.js";
184
195
  export function isFableFamilyModelId(id) {
185
196
  const tail = id.toLowerCase().split("/").pop() ?? "";
@@ -619,6 +630,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
619
630
  }
620
631
  }
621
632
  const taskScope = internals?.registryScope ?? spec.principal ?? "default";
633
+ internals?.peerSelfRef?.addAxis("s", sessionId);
634
+ internals?.peerSelfRef?.addAxis("t", hostTaskId);
622
635
  const offloadScope = spec.principal ?? "default";
623
636
  if (internals?.registryScope !== undefined && internals.registryScope !== offloadScope) {
624
637
  deps.onError?.(new Error(`offload/tool-result namespace note: this run mounts in registry domain "${internals.registryScope}" but its offloaded tool results live under the TRUST identity "${offloadScope}" (principal${spec.principal === undefined ? " absent → default" : ""}). Before 2.13.0 the registry domain named this namespace too; if a deployment used registryScope as its ref-tenancy boundary, that boundary is now the principal — pass one, or read refs under "${offloadScope}".`), { phase: "config", sessionId });
@@ -1046,7 +1059,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1046
1059
  ...(ownSessionRulesRef.current !== undefined ? [ownSessionRulesRef.current] : []),
1047
1060
  ];
1048
1061
  const ownCallerPolicy = lockedPreflight.toolPolicy;
1049
- const durableMandate = runtimeCaps?.forceDurableGate === true || (spec.durableApproval !== undefined && frozenOnAsk === undefined);
1062
+ const durableMandate = runtimeCaps?.forceDurableGate === true ||
1063
+ (spec.durableApproval !== undefined && !isLiveApproverSeat(frozenOnAsk));
1050
1064
  const parentConstraints = [
1051
1065
  ...(inheritedParentConstraints ?? []),
1052
1066
  ...(ownCallerPolicy !== undefined
@@ -1103,6 +1117,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1103
1117
  ...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}),
1104
1118
  ...(internals?.rootSessionId !== undefined ? { rootSessionId: internals.rootSessionId } : {}),
1105
1119
  ...(internals?.explicitAgentName !== undefined ? { spawnedAgentName: internals.explicitAgentName } : {}),
1120
+ ...(internals?.peerSelfRef !== undefined ? { peerSelfRef: internals.peerSelfRef } : {}),
1121
+ ...(internals?.peerInboundChainRef !== undefined ? { peerInboundChainRef: internals.peerInboundChainRef } : {}),
1106
1122
  ...(deps.rosterStore !== undefined ? { roster: deps.rosterStore } : {}),
1107
1123
  ...(sessions.fork !== undefined
1108
1124
  ? {
@@ -1589,6 +1605,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1589
1605
  ...(deps.hands?.commitCoAuthor !== undefined ? { commitCoAuthor: deps.hands.commitCoAuthor } : {}),
1590
1606
  ...(deps.hands?.readImageDownsampler !== undefined ? { readImageDownsampler: deps.hands.readImageDownsampler } : {}),
1591
1607
  ...(deps.hands?.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: deps.hands.autoBackgroundOnTimeout } : {}),
1608
+ ...(deps.hands?.readCyberReminder !== undefined ? { readCyberReminder: deps.hands.readCyberReminder } : {}),
1592
1609
  beforeWrite: async (w) => {
1593
1610
  const deploymentGate = deps.hands?.beforeWrite;
1594
1611
  if (deploymentGate) {
@@ -1650,7 +1667,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1650
1667
  ...(req.row.agentType !== undefined ? { subagent_type: req.row.agentType } : {}),
1651
1668
  ...(req.row.name !== undefined ? { name: req.row.name } : {}),
1652
1669
  ...(req.row.model !== undefined ? { model: req.row.model } : {}),
1653
- }, enrichSpecToolCtx({ toolCallId: `rv-${randomBytes(8).toString("hex")}`, reviveClaim: { row: req.row, rev: req.rev } }));
1670
+ }, enrichSpecToolCtx({ toolCallId: `rv-${randomBytes(8).toString("hex")}`, reviveClaim: { row: req.row, rev: req.rev, ...(req.peerSeed !== undefined ? { peerSeed: req.peerSeed } : {}) } }));
1654
1671
  const o = (typeof out === "string" ? { content: out } : out);
1655
1672
  return {
1656
1673
  content: typeof o.content === "string" ? o.content : JSON.stringify(o.content),
@@ -1668,13 +1685,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1668
1685
  ...(sessionId !== undefined ? { sessionId } : {}),
1669
1686
  ...(internals?.onTaskNotification !== undefined ? { notify: internals.onTaskNotification } : {}),
1670
1687
  ...(internals?.onSubagentSpawn !== undefined ? { sink: internals.onSubagentSpawn } : {}),
1671
- ...(internals?.parentNotify !== undefined ? { uplink: internals.parentNotify } : {}),
1688
+ ...(internals?.parentNotify !== undefined
1689
+ ? { uplink: internals.parentNotify, ...(internals.parentPeerRef !== undefined ? { uplinkRecipient: internals.parentPeerRef } : {}) }
1690
+ : {}),
1672
1691
  ...(internals?.explicitAgentName !== undefined ? { senderName: internals.explicitAgentName } : {}),
1673
1692
  ...(internals?.parentRetainLedger !== undefined ? { siblingRetain: internals.parentRetainLedger } : {}),
1674
1693
  ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
1675
1694
  ...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}),
1676
1695
  enrichCtx: enrichSpecToolCtx,
1677
1696
  ...(deps.rosterStore !== undefined ? { roster: deps.rosterStore } : {}),
1697
+ ...(deps.peerAdmission !== undefined ? { admission: deps.peerAdmission } : {}),
1698
+ ...(internals?.peerSelfRef !== undefined ? { peerSelf: internals.peerSelfRef } : {}),
1699
+ ...(internals?.peerInboundChainRef !== undefined ? { peerInbound: internals.peerInboundChainRef } : {}),
1678
1700
  ...(deps.onBackgroundChildEvent ? { onBackgroundChildEvent: deps.onBackgroundChildEvent } : {}),
1679
1701
  ...(deps.backgroundAgentStore !== undefined ? { agentStore: deps.backgroundAgentStore } : {}),
1680
1702
  ...(deps.mailboxStore !== undefined ? { mailbox: deps.mailboxStore } : {}),
@@ -1963,6 +1985,33 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1963
1985
  }
1964
1986
  tools.push(createSkillTool(skillSpecs, skillScope));
1965
1987
  }
1988
+ const sharedMemoryProvider = deps.sharedMemoryStores;
1989
+ let sharedMemoryPairMounted = false;
1990
+ if (sharedMemoryProvider !== undefined) {
1991
+ const excluded = new Set(toolFaceSnapshot.exclude ?? []);
1992
+ const mountedNameDomain = new Set();
1993
+ for (const t of tools) {
1994
+ mountedNameDomain.add(t.name);
1995
+ for (const alias of t.aliases ?? [])
1996
+ mountedNameDomain.add(alias);
1997
+ }
1998
+ const excludedName = SHARED_MEMORY_TOOL_NAMES.find((n) => excluded.has(n));
1999
+ const occupiedName = SHARED_MEMORY_TOOL_NAMES.find((n) => mountedNameDomain.has(n));
2000
+ if (excludedName !== undefined || occupiedName !== undefined) {
2001
+ deps.onError?.(new Error(`Shared memory tools ${SHARED_MEMORY_TOOL_NAMES.join("/")} were NOT mounted: ` +
2002
+ (excludedName !== undefined
2003
+ ? `excludeTools removes "${excludedName}"`
2004
+ : `this task already declares a tool named "${occupiedName}" (the built-in yields to it)`) +
2005
+ " — the pair mounts together or not at all."), { phase: "config", sessionId, classification: "shared-memory-not-mounted" });
2006
+ }
2007
+ else {
2008
+ tools.push(...createSharedMemoryTools({
2009
+ provider: sharedMemoryProvider,
2010
+ context: { sessionId, taskId: hostTaskId, ...(spec.principal !== undefined ? { principal: spec.principal } : {}) },
2011
+ }).map((s) => defineTool(s)));
2012
+ sharedMemoryPairMounted = true;
2013
+ }
2014
+ }
1966
2015
  const skillsListing = skillSpecs.length > 0
1967
2016
  ? {
1968
2017
  entries: skillSpecs.map((s) => ({
@@ -2142,6 +2191,56 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2142
2191
  tools[i] = { ...t, description: t.descriptionClassic };
2143
2192
  }
2144
2193
  }
2194
+ const userToolNames = (spec.tools ?? []).map((t) => t.name);
2195
+ const protocolToolNames = [...mcp.tools.map((t) => t.name), ...a2a.tools.map((t) => t.name)];
2196
+ const mcpAlwaysLoadNames = mcp.tools
2197
+ .filter((t) => t.mcpAlwaysLoad === true && !(toolFaceSnapshot.defer ?? []).includes(t.name))
2198
+ .map((t) => t.name);
2199
+ const classifyDeferredOverFace = (face, builtinDeferNames) => {
2200
+ const deferredSet = classifyDeferred({
2201
+ specs: spec.tools ?? [],
2202
+ protocolToolNames,
2203
+ fullTools: face.filter((t) => userToolNames.includes(t.name) || protocolToolNames.includes(t.name)),
2204
+ deferMode: deps.deferMode,
2205
+ model,
2206
+ deferNames: [...(toolFaceSnapshot.defer ?? []), ...builtinDeferNames].filter((n) => face.some((t) => t.name === n)),
2207
+ alwaysLoadNames: [
2208
+ ASK_USER_QUESTION_TOOL_NAME,
2209
+ ...(toolFaceSnapshot.alwaysLoad ?? []),
2210
+ ...mcpAlwaysLoadNames,
2211
+ ],
2212
+ });
2213
+ for (const n of [...deferredSet]) {
2214
+ if (!face.some((t) => t.name === n))
2215
+ deferredSet.delete(n);
2216
+ }
2217
+ return deferredSet;
2218
+ };
2219
+ const sharedMemoryPair = SHARED_MEMORY_TOOL_NAMES.filter((n) => tools.some((t) => t.name === n));
2220
+ let deferred;
2221
+ if (sharedMemoryPairMounted && sharedMemoryPair.length > 0) {
2222
+ const withoutPair = tools.filter((t) => !sharedMemoryPair.some((n) => n === t.name));
2223
+ const d1 = classifyDeferredOverFace(tools, sharedMemoryPair);
2224
+ const d0 = classifyDeferredOverFace(withoutPair, []);
2225
+ const soleCause = d0.size === 0 && d1.size > 0;
2226
+ const supportNameTaken = tools.some((t) => t.name === TOOL_SEARCH_NAME || (t.aliases ?? []).includes(TOOL_SEARCH_NAME));
2227
+ if (soleCause && supportNameTaken) {
2228
+ for (let i = tools.length - 1; i >= 0; i--) {
2229
+ if (sharedMemoryPair.some((n) => n === tools[i].name))
2230
+ tools.splice(i, 1);
2231
+ }
2232
+ sharedMemoryPairMounted = false;
2233
+ deps.onError?.(new Error(`Shared memory tools ${SHARED_MEMORY_TOOL_NAMES.join("/")} were NOT mounted: mounting them would inject ` +
2234
+ `the "${TOOL_SEARCH_NAME}" tool, whose name this task already declares — the pair mounts together or not at all.`), { phase: "config", sessionId, classification: "shared-memory-not-mounted" });
2235
+ deferred = d0;
2236
+ }
2237
+ else {
2238
+ deferred = d1;
2239
+ }
2240
+ }
2241
+ else {
2242
+ deferred = classifyDeferredOverFace(tools, []);
2243
+ }
2145
2244
  let centerAdoption;
2146
2245
  let centerAdoptionFresh = false;
2147
2246
  {
@@ -2181,7 +2280,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2181
2280
  isDefaultProvider: provider === defaultPromptProvider,
2182
2281
  userSystemPrompt,
2183
2282
  userAppendSystemPrompt,
2184
- tools,
2283
+ tools: tools.map((t) => (t.aliases === undefined ? { ...t } : { ...t, aliases: [...t.aliases] })),
2185
2284
  facts: {
2186
2285
  supervisorEnabled: false,
2187
2286
  teammateEnabled: internals?.explicitAgentName !== undefined && tools.some((t) => t.name === SEND_MESSAGE_TOOL_NAME),
@@ -2261,27 +2360,6 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2261
2360
  });
2262
2361
  }
2263
2362
  }
2264
- const userToolNames = (spec.tools ?? []).map((t) => t.name);
2265
- const protocolToolNames = [...mcp.tools.map((t) => t.name), ...a2a.tools.map((t) => t.name)];
2266
- const deferred = classifyDeferred({
2267
- specs: spec.tools ?? [],
2268
- protocolToolNames,
2269
- fullTools: tools.filter((t) => userToolNames.includes(t.name) || protocolToolNames.includes(t.name)),
2270
- deferMode: deps.deferMode,
2271
- model,
2272
- deferNames: (toolFaceSnapshot.defer ?? []).filter((n) => tools.some((t) => t.name === n)),
2273
- alwaysLoadNames: [
2274
- ASK_USER_QUESTION_TOOL_NAME,
2275
- ...(toolFaceSnapshot.alwaysLoad ?? []),
2276
- ...mcp.tools
2277
- .filter((t) => t.mcpAlwaysLoad === true && !(toolFaceSnapshot.defer ?? []).includes(t.name))
2278
- .map((t) => t.name),
2279
- ],
2280
- });
2281
- for (const n of [...deferred]) {
2282
- if (!tools.some((t) => t.name === n))
2283
- deferred.delete(n);
2284
- }
2285
2363
  const activeTools = new Set();
2286
2364
  const fpRef = {};
2287
2365
  const turnSnapshotRef = {};
@@ -2291,6 +2369,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2291
2369
  .map((s) => ({ name: inlineUntrusted(s.name, 160), ...(s.error !== undefined ? { error: inlineUntrusted(s.error, 240) } : {}) }));
2292
2370
  let toolsDeltaRef;
2293
2371
  let toolMaterializeStatic = false;
2372
+ let deferDirectCall = false;
2294
2373
  const staticFaceForRef = {};
2295
2374
  if (deferred.size > 0 || failedMcpServers.length > 0) {
2296
2375
  toolsDeltaRef = { pending: [], pendingRemoved: [], pendingReadded: [], pendingFailed: failedMcpServers };
@@ -2313,6 +2392,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2313
2392
  const laneDegrade = requestedStrategy === "static" && spec.deferSelfResolve === false;
2314
2393
  const materializeStatic = requestedStrategy === "static" && !laneDegrade;
2315
2394
  toolMaterializeStatic = materializeStatic;
2395
+ deferDirectCall = spec.deferSelfResolve !== false;
2316
2396
  promptManifest.toolDisclosure = {
2317
2397
  deferredTools: deferred.size,
2318
2398
  strategy: materializeStatic ? "static" : "swap",
@@ -3336,15 +3416,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3336
3416
  });
3337
3417
  const commitSuspendSaga = async (token, cp, remoteEnv, remoteHandle) => {
3338
3418
  if (!checkpointStore)
3339
- return false;
3419
+ return { ok: false };
3340
3420
  if (memoryEngineSession)
3341
3421
  await memoryEngineSession.harvest("checkpoint");
3342
3422
  try {
3343
3423
  await checkpointStore.put(token, cp);
3344
- return true;
3424
+ return { ok: true };
3345
3425
  }
3346
3426
  catch (putErr) {
3347
3427
  deps.onError?.(putErr, { phase: "config", sessionId });
3428
+ const reason = "the approval checkpoint could not be persisted (the store rejected the write; the deployment's error face carries the store's own message)";
3348
3429
  if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
3349
3430
  const { outcome: back, attempts: backAttempts } = await restoreWorkspaceWithRetry(remoteEnv, remoteHandle.snapshotId, {
3350
3431
  abortSignal: abortController.signal,
@@ -3352,7 +3433,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3352
3433
  if (back.ok) {
3353
3434
  const init = await remoteEnv.postResumeInit();
3354
3435
  if (init.ok)
3355
- return false;
3436
+ return { ok: false, reason };
3356
3437
  remoteEnvFailures.push(remoteEnvFailureNote("postResumeInit", init.error, 1));
3357
3438
  deps.onError?.(init.error, { phase: "config", sessionId });
3358
3439
  }
@@ -3362,9 +3443,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3362
3443
  }
3363
3444
  abortController.abort();
3364
3445
  void harness.abort();
3365
- return false;
3446
+ return { ok: false, reason };
3366
3447
  }
3367
- return false;
3448
+ return { ok: false, reason };
3368
3449
  }
3369
3450
  };
3370
3451
  const publishCommittedSuspend = (token, gate, scope, remoteHandle) => {
@@ -3452,7 +3533,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3452
3533
  resourceLedger: resourceLedgerOut,
3453
3534
  ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
3454
3535
  };
3455
- if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)))
3536
+ if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
3456
3537
  return false;
3457
3538
  publishCommittedSuspend(token, gate, scope, remoteHandle);
3458
3539
  try {
@@ -3537,7 +3618,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3537
3618
  ...(spec.principal ? { principal: spec.principal } : {}),
3538
3619
  ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
3539
3620
  };
3540
- if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)))
3621
+ if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
3541
3622
  return false;
3542
3623
  publishCommittedSuspend(token, gate, scope, remoteHandle);
3543
3624
  try {
@@ -3633,7 +3714,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3633
3714
  };
3634
3715
  const suspendAsk = parkLaneArmed && checkpointStore !== undefined
3635
3716
  ? async (req, postHookArgs, safety, liveFaceUnavailable) => {
3636
- const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : onAsk !== undefined;
3717
+ const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
3637
3718
  if (syncFirstEligible &&
3638
3719
  runtimeCaps?.forceDurableGate !== true &&
3639
3720
  liveFaceUnavailable !== true &&
@@ -3646,13 +3727,50 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3646
3727
  let remoteHandle;
3647
3728
  const remoteEnv = suspendableEnv;
3648
3729
  try {
3730
+ const parked = tryCloneArgs(postHookArgs);
3731
+ if (!parked.ok) {
3732
+ throw new ParkRefusal(`the arguments of "${req.toolName}" cannot be captured onto an approval checkpoint (unclonable value: ${parked.reason}) — ` +
3733
+ `a durable approval row must hold an inert snapshot of exactly what a human approves and a resume executes`, { cause: parked.cause });
3734
+ }
3735
+ let backendFidelity;
3736
+ try {
3737
+ backendFidelity = resolveDeclaredFidelity(checkpointStore, "checkpointStore");
3738
+ }
3739
+ catch (err) {
3740
+ throw new ParkRefusal(`the checkpoint backend declares an unrecognized encoding fidelity, so the width of the snapshot this row may hold is unknown (${describeThrown(err)}) — ` +
3741
+ `refusing to park rather than guess which values would survive it`, { cause: err });
3742
+ }
3743
+ const captured = encodeAtFidelity(backendFidelity, parked.value);
3744
+ if (!captured.ok) {
3745
+ throw new ParkRefusal(`the arguments of "${req.toolName}" cannot be encoded by this checkpoint backend (${describeThrown(captured.cause)}) — ` +
3746
+ `a durable approval row must hold an inert snapshot of exactly what a human approves and a resume executes`, { cause: captured.cause });
3747
+ }
3748
+ const parkedArgs = captured.value;
3749
+ const projectionMovedArgs = backendFidelity !== "structured-clone" && !samePlainValue(parked.value, parkedArgs);
3750
+ if (projectionMovedArgs && basePolicyForResumeEdit !== undefined) {
3751
+ const presented = encodeAtFidelity(backendFidelity, parkedArgs);
3752
+ if (!presented.ok) {
3753
+ throw new ParkRefusal(`the stored form of "${req.toolName}"'s arguments could not be presented for re-adjudication (${describeThrown(presented.cause)})`, { cause: presented.cause });
3754
+ }
3755
+ const reprojected = refuseOutOfContractDecision(await basePolicyForResumeEdit.check({ toolName: req.toolName, args: presented.value, toolCallId: req.toolCallId }, abortController.signal));
3756
+ const demanded = reprojected.updatedInput !== undefined ? tryCloneArgs(reprojected.updatedInput) : undefined;
3757
+ const rewroteTheFiledValue = reprojected.updatedInput !== undefined &&
3758
+ !(demanded?.ok === true &&
3759
+ samePlainValue(demanded.value, parkedArgs) &&
3760
+ samePlainValue(reprojected.updatedInput, parkedArgs));
3761
+ if (reprojected.action === "deny" || rewroteTheFiledValue) {
3762
+ throw new ParkRefusal(`the arguments of "${req.toolName}" change shape when stored by this checkpoint backend, and the deployment's tool policy does not accept the stored shape` +
3763
+ `${reprojected.action === "deny" && reprojected.message ? `: ${reprojected.message}` : ""} — ` +
3764
+ `refusing to file a row whose executable form nothing adjudicated`);
3765
+ }
3766
+ }
3649
3767
  if (offloadStore && isVolatileOffloadStore(offloadStore) && !offloadStore.isEmpty()) {
3650
- throw new Error("durable suspend requires a durable toolResultStore (the default InMemoryToolResultStore " +
3768
+ throw new ParkRefusal("durable suspend requires a durable toolResultStore (the default InMemoryToolResultStore " +
3651
3769
  "holds offloaded results that are lost across a cross-replica resume); inject " +
3652
3770
  "RunnerDeps.toolResultStore or disable offload.");
3653
3771
  }
3654
3772
  if (ownedEnv !== undefined && remoteEnv === undefined && parkOnlyRemoteEnv === undefined) {
3655
- throw new Error(incompleteSuspendAdapter !== undefined
3773
+ throw new ParkRefusal(incompleteSuspendAdapter !== undefined
3656
3774
  ? `durable suspend is not supported with this per-task executionEnvFactory env: it declares capabilities.suspendable but its adapter does not implement ${incompleteSuspendAdapter.join(" or ")}, so a snapshot taken now could never be restored. Implement the full RemoteExecutionEnv restore surface, or declare capabilities.suspendable:false if the workspace is externally durable (the park-only lane).`
3657
3775
  : "durable suspend is not supported with a non-remote per-task executionEnvFactory env: the " +
3658
3776
  "minted env is destroyed on suspend, so a resumed file/shell tool would act on a fresh " +
@@ -3661,7 +3779,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3661
3779
  }
3662
3780
  const leafId = await session.getLeafId();
3663
3781
  if (!leafId) {
3664
- throw new Error("cannot suspend: session has no committed leaf to resume from");
3782
+ throw new ParkRefusal("cannot suspend: session has no committed leaf to resume from");
3665
3783
  }
3666
3784
  const { messages } = await session.buildContext();
3667
3785
  const { batchToolCallIds, completedCallIds } = batchContextAt(messages, req.toolCallId);
@@ -3688,7 +3806,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3688
3806
  token = mintCheckpointToken();
3689
3807
  const riskDescriptor = buildRiskDescriptor({
3690
3808
  toolName: req.toolName,
3691
- args: postHookArgs,
3809
+ args: parkedArgs,
3692
3810
  safety,
3693
3811
  shellGated: (req.toolName === "Bash" && shellGatedBash) || (req.toolName === "Monitor" && shellGatedMonitor),
3694
3812
  ...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
@@ -3731,12 +3849,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3731
3849
  kind: "tool_approval",
3732
3850
  toolCallId: req.toolCallId,
3733
3851
  toolName: req.toolName,
3734
- args: postHookArgs,
3852
+ args: parkedArgs,
3735
3853
  ...(() => {
3736
- const preview = approvalPreviewOf(req.toolName, postHookArgs);
3854
+ const preview = approvalPreviewOf(req.toolName, parkedArgs);
3737
3855
  return preview !== undefined ? { preview } : {};
3738
3856
  })(),
3739
- boundInputHash: boundInputHashOf(postHookArgs),
3857
+ boundInputHash: boundInputHashOf(parkedArgs),
3740
3858
  batchToolCallIds,
3741
3859
  completedCallIds,
3742
3860
  },
@@ -3761,10 +3879,15 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3761
3879
  }
3762
3880
  catch (err) {
3763
3881
  deps.onError?.(err, { phase: "config", sessionId });
3764
- return undefined;
3882
+ return {
3883
+ parkFailed: err instanceof ParkRefusal
3884
+ ? err.message
3885
+ : "the durable approval park could not be prepared (the deployment's error face carries the exception)",
3886
+ };
3765
3887
  }
3766
- if (!(await commitSuspendSaga(token, cp, remoteEnv, remoteHandle))) {
3767
- return undefined;
3888
+ const committed = await commitSuspendSaga(token, cp, remoteEnv, remoteHandle);
3889
+ if (!committed.ok) {
3890
+ return committed.reason !== undefined ? { parkFailed: committed.reason } : undefined;
3768
3891
  }
3769
3892
  publishCommittedSuspend(token, gate, cp.scope, remoteHandle);
3770
3893
  try {
@@ -4139,7 +4262,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4139
4262
  : undefined;
4140
4263
  overheadState.promptChars = systemPrompt.length;
4141
4264
  const preparedHolder = {};
4142
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4265
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4143
4266
  const prepared = buildPrepared();
4144
4267
  preparedHolder.current = prepared;
4145
4268
  return prepared;
@@ -42,6 +42,7 @@ import { refuseOutOfContractDecision, toolPolicyNameSets } from "../tool-policy.
42
42
  import { defaultTaskRegistry } from "../task-registry.js";
43
43
  import { discloseDroppedPending, isDelegatedAgentTerminal, PendingSessionNotifications, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
44
44
  import { ToolDetachHub } from "../tool-detach.js";
45
+ import { createPeerInboundChainRef, createPeerSelfRef } from "../../agents/peer-admission.js";
45
46
  import { workflowSizeGuidelineChangeNotice } from "../../orchestration/workflow-size-guideline.js";
46
47
  import { structuredFrom, toolOutputFrom } from "./tool-output-projection.js";
47
48
  function createRunState() {
@@ -576,7 +577,9 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
576
577
  skillsListing: rs.attach.skillsListingOn,
577
578
  mcpInstructions: mcpInstructionsOn,
578
579
  },
579
- ...(undiscoveredTools !== undefined && undiscoveredTools.length > 0 ? { undiscoveredTools } : {}),
580
+ ...(undiscoveredTools !== undefined && undiscoveredTools.length > 0
581
+ ? { undiscoveredTools, deferDirectCall: prepared.deferDirectCall }
582
+ : {}),
580
583
  ...(changed !== undefined ? { changedFiles: changed } : {}),
581
584
  ...(budgetUsdOn && rs.budget.maxCostMicroUsd !== undefined
582
585
  ? { budgetUsd: { used: stats.costMicroUsd / 1e6, total: rs.budget.maxCostMicroUsd / 1e6 } }
@@ -1821,8 +1824,12 @@ export class Runner {
1821
1824
  resolve("queued");
1822
1825
  });
1823
1826
  };
1827
+ const peerSelfRef = internals?.peerSelfRef ?? createPeerSelfRef(internals?.registryScope ?? spec.principal ?? "default");
1828
+ const peerInboundChainRef = internals?.peerInboundChainRef ?? createPeerInboundChainRef();
1824
1829
  const prepared = await prepareTask(spec, this.deps, this.sessions, prepareResume, {
1825
1830
  ...(internals ?? {}),
1831
+ peerSelfRef,
1832
+ peerInboundChainRef,
1826
1833
  detachHub,
1827
1834
  onTaskNotification: (notification, opts) => {
1828
1835
  try {
@@ -1849,6 +1856,11 @@ export class Runner {
1849
1856
  for (const p of payloads)
1850
1857
  this.pendingSessionNotifications.pend(notificationSessionId, p);
1851
1858
  };
1859
+ prepared.harness.onEngineNoteConsumed = (p) => {
1860
+ const peer = p?.peer;
1861
+ if (peer !== undefined && Array.isArray(peer.hopChain))
1862
+ peerInboundChainRef.current = [...peer.hopChain];
1863
+ };
1852
1864
  if (notifyRef)
1853
1865
  notifyRef.inject = injectTaskNotification;
1854
1866
  try {
@@ -94,6 +94,7 @@ export interface AttachmentInputs {
94
94
  mcpInstructions?: boolean;
95
95
  };
96
96
  undiscoveredTools?: readonly string[];
97
+ deferDirectCall?: boolean;
97
98
  changedFiles?: ReadonlyArray<{
98
99
  path: string;
99
100
  mtimeMs: number;
@@ -118,7 +119,7 @@ export interface AttachmentInputs {
118
119
  mcpDroppedTools?: ReadonlyArray<McpDroppedTool>;
119
120
  }
120
121
  export declare function collectDueAttachments(state: AttachmentState, inp: AttachmentInputs): readonly TurnAttachment[];
121
- export declare function renderToolSearchUsageReminder(undiscovered: readonly string[]): string;
122
+ export declare function renderToolSearchUsageReminder(undiscovered: readonly string[], directCallEnabled?: boolean): string;
122
123
  export declare function renderDateChange(newDate: string): string;
123
124
  export declare function collectDateChange(state: DateChangeState, today: string): TurnAttachment | undefined;
124
125
  export declare function collectInstructionsChange(state: InstructionsChangeState, probed: ReadonlyArray<{
@@ -145,7 +145,7 @@ export function collectDueAttachments(state, inp) {
145
145
  const sinceUse = state.toolSearchLastUseTurn === undefined ? Number.POSITIVE_INFINITY : cadenceNow - state.toolSearchLastUseTurn;
146
146
  if (sinceUse >= TOOL_SEARCH_REMINDER_CONFIG.EVERY_N_TURNS && cadenceNow - state.lastToolSearchReminderTurn >= TOOL_SEARCH_REMINDER_CONFIG.EVERY_N_TURNS) {
147
147
  state.lastToolSearchReminderTurn = cadenceNow;
148
- (out ??= []).push({ source: "tool_search_usage_reminder", body: renderToolSearchUsageReminder(inp.undiscoveredTools) });
148
+ (out ??= []).push({ source: "tool_search_usage_reminder", body: renderToolSearchUsageReminder(inp.undiscoveredTools, inp.deferDirectCall !== false) });
149
149
  }
150
150
  }
151
151
  if (inp.config.planModeReminder && inp.planActive) {
@@ -248,14 +248,17 @@ function renderTaskReminder(state) {
248
248
  }
249
249
  return message;
250
250
  }
251
- export function renderToolSearchUsageReminder(undiscovered) {
251
+ export function renderToolSearchUsageReminder(undiscovered, directCallEnabled = true) {
252
252
  const shown = undiscovered.slice(0, TOOL_SEARCH_REMINDER_CONFIG.MAX_NAMES);
253
253
  const more = undiscovered.length - shown.length;
254
254
  const names = `${shown.join(", ")}${more > 0 ? ` (+${more} more)` : ""}`;
255
255
  return (`Some available tools' schemas are not loaded in this conversation yet: ${names}. Before concluding a ` +
256
256
  `capability is missing or building a workaround, use ${TOOL_SEARCH_TOOL_NAME} to find and load relevant tools — ` +
257
- `keywords to search, or query "select:<name>[,<name>...]" for specific tools. Calling a tool before its schema ` +
258
- `is loaded will fail. This is just a gentle reminder - ignore if not applicable to the current work.`);
257
+ `keywords to search, or query "select:<name>[,<name>...]" for specific tools. ` +
258
+ (directCallEnabled
259
+ ? `You do not have these tools' parameters, so activate one rather than guessing its arguments. `
260
+ : `Calling a tool before its schema is loaded will fail. `) +
261
+ `This is just a gentle reminder - ignore if not applicable to the current work.`);
259
262
  }
260
263
  export function renderDateChange(newDate) {
261
264
  return `The date has changed. Today's date is now ${newDate}. DO NOT mention this to the user explicitly because they are already aware.`;
@@ -400,8 +403,8 @@ export function renderToolsDelta(input) {
400
403
  if (added.length > 0) {
401
404
  const swapped = input.staticFace === true ? (input.swappedUnderStatic ?? []).filter((n) => added.includes(n)) : [];
402
405
  blocks.push((input.staticFace === true
403
- ? "The following deferred tools are now active — call them directly. Their parameter schemas were " +
404
- "provided in the ToolSearch result (the tools list itself keeps compact placeholder entries):\n"
406
+ ? "The following deferred tools are now active — call them directly. The tools list keeps compact " +
407
+ 'placeholder entries, so re-run ToolSearch ("select:<name>") if you need a parameter schema:\n'
405
408
  : "The following deferred tools are now available. Their full schemas are loaded — call them " +
406
409
  "directly like any other tool:\n") +
407
410
  added.map((n) => `- ${n}`).join("\n") +
@@ -10,6 +10,20 @@ const INTERRUPTED_IDEMPOTENT = "[INTERRUPTED] The previous run ended before this
10
10
  "what you intended. If you decide not to re-issue it, verify the current state first.";
11
11
  const INTERRUPTED_NEVER_STARTED = "[INTERRUPTED] The run was aborted before this tool call started. It was never executed and had " +
12
12
  "no side effects — it is safe to re-issue this call if you still need it.";
13
+ const INTERRUPTED_REPEAT_UNKNOWN = "[INTERRUPTED] Same as an earlier interrupted call in this batch: no result was recorded and the outcome is " +
14
+ "UNKNOWN — verify before relying on it.";
15
+ const INTERRUPTED_REPEAT_SAFE = "[INTERRUPTED] Same as an earlier interrupted call in this batch: this tool is read-only/idempotent, so just " +
16
+ "call it again if you still need the result.";
17
+ const INTERRUPTED_REPEAT_IDEMPOTENT = "[INTERRUPTED] Same as an earlier interrupted call in this batch: it is safe to REPLAY — re-issue it, or verify " +
18
+ "the current state if you decide not to.";
19
+ const INTERRUPTED_REPEAT_NEVER_STARTED = "[INTERRUPTED] Same as an earlier interrupted call in this batch: it never started and had no side effects — " +
20
+ "safe to re-issue.";
21
+ const REPEAT_TEXT = {
22
+ never_started: INTERRUPTED_REPEAT_NEVER_STARTED,
23
+ read: INTERRUPTED_REPEAT_SAFE,
24
+ idempotent: INTERRUPTED_REPEAT_IDEMPOTENT,
25
+ unknown: INTERRUPTED_REPEAT_UNKNOWN,
26
+ };
13
27
  export function findOrphanToolCalls(messages, suspendedBatch) {
14
28
  const resolvedAt = new Map();
15
29
  const callSitesAt = new Map();
@@ -70,11 +84,15 @@ export async function reconcileInterruptedSession(session, toolEffects, suspende
70
84
  const { messages } = await session.buildContext();
71
85
  const orphans = findOrphanToolCalls(messages, suspendedBatch).filter((o) => o.kind !== "result");
72
86
  const recovered = [];
87
+ const statedInFull = new Set();
73
88
  for (const orphan of orphans) {
74
89
  const effect = toolEffects?.get(orphan.toolName) ?? "write";
75
90
  const effectText = effect === "read" ? INTERRUPTED_SAFE : effect === "idempotent" ? INTERRUPTED_IDEMPOTENT : INTERRUPTED_UNKNOWN;
76
91
  const neverStarted = startedToolCallIds !== undefined && !startedToolCallIds.has(orphan.toolCallId);
77
- const text = neverStarted ? INTERRUPTED_NEVER_STARTED : effectText;
92
+ const fullText = neverStarted ? INTERRUPTED_NEVER_STARTED : effectText;
93
+ const cls = neverStarted ? "never_started" : effect === "read" ? "read" : effect === "idempotent" ? "idempotent" : "unknown";
94
+ const text = statedInFull.has(cls) ? REPEAT_TEXT[cls] : fullText;
95
+ statedInFull.add(cls);
78
96
  const errorKind = neverStarted ? "interrupted_never_started" : "interrupted_outcome_unknown";
79
97
  const entryId = await session.appendMessage({
80
98
  role: "toolResult",
@@ -0,0 +1,17 @@
1
+ import type { SharedMemorySnapshot, SharedMemoryStoreInfo, SharedMemoryStoreProvider } from "./types.js";
2
+ export interface SharedMemoryFixture {
3
+ state: SharedMemorySnapshot["state"];
4
+ stores: Array<{
5
+ info: SharedMemoryStoreInfo;
6
+ documents: Array<{
7
+ path: string;
8
+ content: string;
9
+ updatedAt?: string;
10
+ }>;
11
+ }>;
12
+ }
13
+ export interface SharedMemoryStoreContractHooks {
14
+ make: (fixture: SharedMemoryFixture) => Promise<SharedMemoryStoreProvider> | SharedMemoryStoreProvider;
15
+ runAssertion?: (name: string, fn: () => Promise<void>) => void | Promise<void>;
16
+ }
17
+ export declare function sharedMemoryStoreContract(hooks: SharedMemoryStoreContractHooks): Promise<void>;