@sema-agent/core 5.17.0-pre.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 (44) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/dist/agents/subagent.d.ts +8 -0
  3. package/dist/agents/subagent.js +69 -2
  4. package/dist/core/ask-question.js +6 -1
  5. package/dist/core/canonical-json.js +176 -14
  6. package/dist/core/checkpoint-store.d.ts +14 -0
  7. package/dist/core/checkpoint-store.js +73 -0
  8. package/dist/core/hooks.d.ts +4 -1
  9. package/dist/core/hooks.js +24 -6
  10. package/dist/core/mcp.d.ts +1 -0
  11. package/dist/core/mcp.js +15 -3
  12. package/dist/core/runner/prepare-task.d.ts +1 -0
  13. package/dist/core/runner/prepare-task.js +78 -22
  14. package/dist/core/runner/runtask.js +3 -1
  15. package/dist/core/runner/turn-attachments.d.ts +2 -1
  16. package/dist/core/runner/turn-attachments.js +9 -6
  17. package/dist/core/session-reconcile.js +19 -1
  18. package/dist/core/task-registry-agent.js +1 -0
  19. package/dist/core/tool-policy.d.ts +9 -0
  20. package/dist/core/tool-policy.js +28 -8
  21. package/dist/core/types.d.ts +1 -0
  22. package/dist/core/wiring-manifest.js +2 -2
  23. package/dist/engine/llm/validation.js +121 -5
  24. package/dist/engine/loop/agent-loop.d.ts +2 -0
  25. package/dist/engine/loop/agent-loop.js +17 -4
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +1 -0
  28. package/dist/prompts/supervisor.d.ts +1 -1
  29. package/dist/prompts/supervisor.js +1 -1
  30. package/dist/stores/file/checkpoint-store.d.ts +1 -0
  31. package/dist/stores/file/checkpoint-store.js +1 -0
  32. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  33. package/dist/tools/fs/bash-readonly-classifier.js +11 -10
  34. package/dist/tools/fs/fs-bash.js +6 -6
  35. package/dist/tools/fs/fs-read.d.ts +1 -1
  36. package/dist/tools/fs/fs-read.js +4 -3
  37. package/dist/tools/fs/fs-shared.d.ts +3 -0
  38. package/dist/tools/fs/fs-shared.js +8 -1
  39. package/dist/tools/fs/fs-write.js +8 -8
  40. package/dist/tools/fs/index.d.ts +1 -0
  41. package/dist/tools/fs/index.js +1 -1
  42. package/dist/tools/fs/safety.d.ts +1 -0
  43. package/dist/tools/fs/safety.js +9 -2
  44. package/package.json +1 -1
package/dist/core/mcp.js CHANGED
@@ -69,6 +69,18 @@ export function gateMcpOutput(content, limitTokens = mcpMaxOutputTokens()) {
69
69
  }
70
70
  const MCP_ERROR_HEAD_CHARS = 8_000;
71
71
  const MCP_ERROR_TAIL_CHARS = 2_000;
72
+ const STRUCTURED_CONTENT_DEDUP_MIN_CHARS = 32;
73
+ export function structuredContentErrorLine(structuredContent, collectedText) {
74
+ if (structuredContent === undefined)
75
+ return undefined;
76
+ const json = JSON.stringify(structuredContent);
77
+ if (json === undefined)
78
+ return undefined;
79
+ const distinctive = json.length >= STRUCTURED_CONTENT_DEDUP_MIN_CHARS && (json.startsWith("{") || json.startsWith("["));
80
+ if (distinctive && truncateMcpErrorText(collectedText.trim()).includes(json))
81
+ return undefined;
82
+ return `[structuredContent] ${truncateMcpErrorText(json)}`;
83
+ }
72
84
  export function truncateMcpErrorText(s) {
73
85
  const max = MCP_ERROR_HEAD_CHARS + MCP_ERROR_TAIL_CHARS;
74
86
  if (s.length <= max)
@@ -1198,9 +1210,9 @@ function intakeListedTools(listed, spec, client, health, imageResizer) {
1198
1210
  else
1199
1211
  parts.push(`[${block.type} block omitted from error result]`);
1200
1212
  }
1201
- const errSc = res.structuredContent;
1202
- if (errSc !== undefined)
1203
- parts.push(`[structuredContent] ${truncateMcpErrorText(JSON.stringify(errSc))}`);
1213
+ const errScLine = structuredContentErrorLine(res.structuredContent, parts.join("\n"));
1214
+ if (errScLine !== undefined)
1215
+ parts.push(errScLine);
1204
1216
  const body = truncateMcpErrorText(parts.join("\n").trim());
1205
1217
  const msg = body
1206
1218
  ? `MCP tool ${inlineUntrusted(remoteName)} reported an error. The server's error content follows as external/untrusted data:\n${delimitUntrusted(`${spec.name} tool error`, body)}`
@@ -127,6 +127,7 @@ export interface Prepared {
127
127
  activeTools: Set<string>;
128
128
  deferredToolNames?: ReadonlySet<string>;
129
129
  toolMaterializeStatic: boolean;
130
+ deferDirectCall: boolean;
130
131
  staticFaceFor?: (name: string) => boolean;
131
132
  memoryEngineSession?: {
132
133
  engine: MemoryEngine;
@@ -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";
@@ -77,7 +77,7 @@ import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-
77
77
  import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
78
78
  import { resolveKey } from "../../tools/fs/safety.js";
79
79
  import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
80
- 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";
81
81
  import { boundInputHashOf } from "../canonical-json.js";
82
82
  import { countElicitOptIns, deriveAskEffective, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
83
83
  import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
@@ -168,6 +168,9 @@ export function resolveEnvLifetimeExpiry(env, observedAt) {
168
168
  return { unanchored: true };
169
169
  return { expiresAt: anchor + lifetimeMs };
170
170
  }
171
+ function isLiveApproverSeat(onAsk) {
172
+ return typeof onAsk === "function";
173
+ }
171
174
  const DEFAULT_UNATTENDED_APPROVAL_TTL_MS = 30 * 24 * 60 * 60 * 1000;
172
175
  function sanitizedTtlMs(ttlMs) {
173
176
  if (ttlMs === undefined)
@@ -182,6 +185,12 @@ export const DEFAULT_IRREVERSIBLE_SCOPE = "irreversible";
182
185
  export function checkpointScopeOf(spec) {
183
186
  return spec.durableApproval?.scope || spec.principal || DEFAULT_IRREVERSIBLE_SCOPE;
184
187
  }
188
+ class ParkRefusal extends Error {
189
+ constructor(message, options) {
190
+ super(message, options);
191
+ this.name = "ParkRefusal";
192
+ }
193
+ }
185
194
  export { resolveCheckpointStore } from "../checkpoint-store.js";
186
195
  export function isFableFamilyModelId(id) {
187
196
  const tail = id.toLowerCase().split("/").pop() ?? "";
@@ -1050,7 +1059,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1050
1059
  ...(ownSessionRulesRef.current !== undefined ? [ownSessionRulesRef.current] : []),
1051
1060
  ];
1052
1061
  const ownCallerPolicy = lockedPreflight.toolPolicy;
1053
- const durableMandate = runtimeCaps?.forceDurableGate === true || (spec.durableApproval !== undefined && frozenOnAsk === undefined);
1062
+ const durableMandate = runtimeCaps?.forceDurableGate === true ||
1063
+ (spec.durableApproval !== undefined && !isLiveApproverSeat(frozenOnAsk));
1054
1064
  const parentConstraints = [
1055
1065
  ...(inheritedParentConstraints ?? []),
1056
1066
  ...(ownCallerPolicy !== undefined
@@ -1595,6 +1605,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1595
1605
  ...(deps.hands?.commitCoAuthor !== undefined ? { commitCoAuthor: deps.hands.commitCoAuthor } : {}),
1596
1606
  ...(deps.hands?.readImageDownsampler !== undefined ? { readImageDownsampler: deps.hands.readImageDownsampler } : {}),
1597
1607
  ...(deps.hands?.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: deps.hands.autoBackgroundOnTimeout } : {}),
1608
+ ...(deps.hands?.readCyberReminder !== undefined ? { readCyberReminder: deps.hands.readCyberReminder } : {}),
1598
1609
  beforeWrite: async (w) => {
1599
1610
  const deploymentGate = deps.hands?.beforeWrite;
1600
1611
  if (deploymentGate) {
@@ -2358,6 +2369,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2358
2369
  .map((s) => ({ name: inlineUntrusted(s.name, 160), ...(s.error !== undefined ? { error: inlineUntrusted(s.error, 240) } : {}) }));
2359
2370
  let toolsDeltaRef;
2360
2371
  let toolMaterializeStatic = false;
2372
+ let deferDirectCall = false;
2361
2373
  const staticFaceForRef = {};
2362
2374
  if (deferred.size > 0 || failedMcpServers.length > 0) {
2363
2375
  toolsDeltaRef = { pending: [], pendingRemoved: [], pendingReadded: [], pendingFailed: failedMcpServers };
@@ -2380,6 +2392,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2380
2392
  const laneDegrade = requestedStrategy === "static" && spec.deferSelfResolve === false;
2381
2393
  const materializeStatic = requestedStrategy === "static" && !laneDegrade;
2382
2394
  toolMaterializeStatic = materializeStatic;
2395
+ deferDirectCall = spec.deferSelfResolve !== false;
2383
2396
  promptManifest.toolDisclosure = {
2384
2397
  deferredTools: deferred.size,
2385
2398
  strategy: materializeStatic ? "static" : "swap",
@@ -3403,15 +3416,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3403
3416
  });
3404
3417
  const commitSuspendSaga = async (token, cp, remoteEnv, remoteHandle) => {
3405
3418
  if (!checkpointStore)
3406
- return false;
3419
+ return { ok: false };
3407
3420
  if (memoryEngineSession)
3408
3421
  await memoryEngineSession.harvest("checkpoint");
3409
3422
  try {
3410
3423
  await checkpointStore.put(token, cp);
3411
- return true;
3424
+ return { ok: true };
3412
3425
  }
3413
3426
  catch (putErr) {
3414
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)";
3415
3429
  if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
3416
3430
  const { outcome: back, attempts: backAttempts } = await restoreWorkspaceWithRetry(remoteEnv, remoteHandle.snapshotId, {
3417
3431
  abortSignal: abortController.signal,
@@ -3419,7 +3433,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3419
3433
  if (back.ok) {
3420
3434
  const init = await remoteEnv.postResumeInit();
3421
3435
  if (init.ok)
3422
- return false;
3436
+ return { ok: false, reason };
3423
3437
  remoteEnvFailures.push(remoteEnvFailureNote("postResumeInit", init.error, 1));
3424
3438
  deps.onError?.(init.error, { phase: "config", sessionId });
3425
3439
  }
@@ -3429,9 +3443,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3429
3443
  }
3430
3444
  abortController.abort();
3431
3445
  void harness.abort();
3432
- return false;
3446
+ return { ok: false, reason };
3433
3447
  }
3434
- return false;
3448
+ return { ok: false, reason };
3435
3449
  }
3436
3450
  };
3437
3451
  const publishCommittedSuspend = (token, gate, scope, remoteHandle) => {
@@ -3519,7 +3533,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3519
3533
  resourceLedger: resourceLedgerOut,
3520
3534
  ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
3521
3535
  };
3522
- if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)))
3536
+ if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
3523
3537
  return false;
3524
3538
  publishCommittedSuspend(token, gate, scope, remoteHandle);
3525
3539
  try {
@@ -3604,7 +3618,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3604
3618
  ...(spec.principal ? { principal: spec.principal } : {}),
3605
3619
  ...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
3606
3620
  };
3607
- if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)))
3621
+ if (!(await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).ok)
3608
3622
  return false;
3609
3623
  publishCommittedSuspend(token, gate, scope, remoteHandle);
3610
3624
  try {
@@ -3700,7 +3714,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3700
3714
  };
3701
3715
  const suspendAsk = parkLaneArmed && checkpointStore !== undefined
3702
3716
  ? async (req, postHookArgs, safety, liveFaceUnavailable) => {
3703
- const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : typeof onAsk === "function";
3717
+ const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
3704
3718
  if (syncFirstEligible &&
3705
3719
  runtimeCaps?.forceDurableGate !== true &&
3706
3720
  liveFaceUnavailable !== true &&
@@ -3713,13 +3727,50 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3713
3727
  let remoteHandle;
3714
3728
  const remoteEnv = suspendableEnv;
3715
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
+ }
3716
3767
  if (offloadStore && isVolatileOffloadStore(offloadStore) && !offloadStore.isEmpty()) {
3717
- 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 " +
3718
3769
  "holds offloaded results that are lost across a cross-replica resume); inject " +
3719
3770
  "RunnerDeps.toolResultStore or disable offload.");
3720
3771
  }
3721
3772
  if (ownedEnv !== undefined && remoteEnv === undefined && parkOnlyRemoteEnv === undefined) {
3722
- throw new Error(incompleteSuspendAdapter !== undefined
3773
+ throw new ParkRefusal(incompleteSuspendAdapter !== undefined
3723
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).`
3724
3775
  : "durable suspend is not supported with a non-remote per-task executionEnvFactory env: the " +
3725
3776
  "minted env is destroyed on suspend, so a resumed file/shell tool would act on a fresh " +
@@ -3728,7 +3779,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3728
3779
  }
3729
3780
  const leafId = await session.getLeafId();
3730
3781
  if (!leafId) {
3731
- 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");
3732
3783
  }
3733
3784
  const { messages } = await session.buildContext();
3734
3785
  const { batchToolCallIds, completedCallIds } = batchContextAt(messages, req.toolCallId);
@@ -3755,7 +3806,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3755
3806
  token = mintCheckpointToken();
3756
3807
  const riskDescriptor = buildRiskDescriptor({
3757
3808
  toolName: req.toolName,
3758
- args: postHookArgs,
3809
+ args: parkedArgs,
3759
3810
  safety,
3760
3811
  shellGated: (req.toolName === "Bash" && shellGatedBash) || (req.toolName === "Monitor" && shellGatedMonitor),
3761
3812
  ...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
@@ -3798,12 +3849,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3798
3849
  kind: "tool_approval",
3799
3850
  toolCallId: req.toolCallId,
3800
3851
  toolName: req.toolName,
3801
- args: postHookArgs,
3852
+ args: parkedArgs,
3802
3853
  ...(() => {
3803
- const preview = approvalPreviewOf(req.toolName, postHookArgs);
3854
+ const preview = approvalPreviewOf(req.toolName, parkedArgs);
3804
3855
  return preview !== undefined ? { preview } : {};
3805
3856
  })(),
3806
- boundInputHash: boundInputHashOf(postHookArgs),
3857
+ boundInputHash: boundInputHashOf(parkedArgs),
3807
3858
  batchToolCallIds,
3808
3859
  completedCallIds,
3809
3860
  },
@@ -3828,10 +3879,15 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3828
3879
  }
3829
3880
  catch (err) {
3830
3881
  deps.onError?.(err, { phase: "config", sessionId });
3831
- 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
+ };
3832
3887
  }
3833
- if (!(await commitSuspendSaga(token, cp, remoteEnv, remoteHandle))) {
3834
- return undefined;
3888
+ const committed = await commitSuspendSaga(token, cp, remoteEnv, remoteHandle);
3889
+ if (!committed.ok) {
3890
+ return committed.reason !== undefined ? { parkFailed: committed.reason } : undefined;
3835
3891
  }
3836
3892
  publishCommittedSuspend(token, gate, cp.scope, remoteHandle);
3837
3893
  try {
@@ -4206,7 +4262,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4206
4262
  : undefined;
4207
4263
  overheadState.promptChars = systemPrompt.length;
4208
4264
  const preparedHolder = {};
4209
- 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 } : {}) });
4210
4266
  const prepared = buildPrepared();
4211
4267
  preparedHolder.current = prepared;
4212
4268
  return prepared;
@@ -577,7 +577,9 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
577
577
  skillsListing: rs.attach.skillsListingOn,
578
578
  mcpInstructions: mcpInstructionsOn,
579
579
  },
580
- ...(undiscoveredTools !== undefined && undiscoveredTools.length > 0 ? { undiscoveredTools } : {}),
580
+ ...(undiscoveredTools !== undefined && undiscoveredTools.length > 0
581
+ ? { undiscoveredTools, deferDirectCall: prepared.deferDirectCall }
582
+ : {}),
581
583
  ...(changed !== undefined ? { changedFiles: changed } : {}),
582
584
  ...(budgetUsdOn && rs.budget.maxCostMicroUsd !== undefined
583
585
  ? { budgetUsd: { used: stats.costMicroUsd / 1e6, total: rs.budget.maxCostMicroUsd / 1e6 } }
@@ -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",
@@ -982,6 +982,7 @@ export async function reviveBackgroundAgentLane(core, id, access, abort) {
982
982
  handle.errorRetryAfterMs = undefined;
983
983
  handle.resultIsPartial = undefined;
984
984
  handle.stopSource = undefined;
985
+ handle.stoppedBy = undefined;
985
986
  handle.completionId = undefined;
986
987
  handle.terminalNotified = undefined;
987
988
  handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
@@ -100,6 +100,15 @@ export type AskOutcome = boolean | "unavailable" | {
100
100
  };
101
101
  export declare function withDelegationProvenance(onAsk: OnAsk, delegation: AskDelegationProvenance): OnAsk;
102
102
  export declare function askApproverIdentity(onAsk: unknown): unknown;
103
+ export declare function tryCloneArgs<T>(v: T): {
104
+ ok: true;
105
+ value: T;
106
+ } | {
107
+ ok: false;
108
+ reason: string;
109
+ cause?: unknown;
110
+ };
111
+ export declare function describeThrown(err: unknown): string;
103
112
  export type ResolvedAsk = PermissionResult & {
104
113
  approverUnavailable?: true;
105
114
  presentedInput?: unknown;
@@ -2,6 +2,7 @@ import { homedir } from "node:os";
2
2
  import { isAbsolute, join, normalize as normalizePath, sep } from "node:path";
3
3
  import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
4
4
  import { boundInputHashOf } from "./canonical-json.js";
5
+ import { inlineUntrusted } from "./untrusted-text.js";
5
6
  import { writeTargetPath } from "../tools/fs/safety.js";
6
7
  export function decisionText(d) {
7
8
  return d.message;
@@ -90,7 +91,7 @@ export function createApprovalPolicy(opts) {
90
91
  catch (err) {
91
92
  return {
92
93
  action: "deny",
93
- message: `approval errored for "${req.toolName}": ${err instanceof Error ? err.message : String(err)}`,
94
+ message: `approval errored for "${req.toolName}": ${describeThrown(err)}`,
94
95
  };
95
96
  }
96
97
  const okRaw = ok;
@@ -528,17 +529,36 @@ export function withDelegationProvenance(onAsk, delegation) {
528
529
  export function askApproverIdentity(onAsk) {
529
530
  return typeof onAsk === "function" ? (delegatedApproverRoot.get(onAsk) ?? onAsk) : onAsk;
530
531
  }
531
- function tryCloneArgs(v) {
532
+ export function tryCloneArgs(v) {
532
533
  try {
533
534
  const value = structuredClone(v);
534
535
  if (containsSharedMemory(value))
535
- return { ok: false };
536
+ return { ok: false, reason: "the value carries shared memory (SharedArrayBuffer), which a clone still aliases" };
536
537
  return { ok: true, value };
537
538
  }
539
+ catch (err) {
540
+ return { ok: false, reason: describeThrown(err), cause: err };
541
+ }
542
+ }
543
+ export function describeThrown(err) {
544
+ try {
545
+ if (typeof err === "string")
546
+ return containThrownText(err);
547
+ if (err instanceof Error) {
548
+ const message = err.message;
549
+ if (typeof message === "string" && message !== "")
550
+ return containThrownText(message);
551
+ }
552
+ return `a non-Error value was thrown (${err === null ? "null" : typeof err})`;
553
+ }
538
554
  catch {
539
- return { ok: false };
555
+ return "the value could not be described";
540
556
  }
541
557
  }
558
+ const THROWN_TEXT_MAX = 300;
559
+ function containThrownText(text) {
560
+ return inlineUntrusted(text, THROWN_TEXT_MAX);
561
+ }
542
562
  function containsSharedMemory(v, seen = new Set()) {
543
563
  if (typeof SharedArrayBuffer !== "undefined" && v instanceof SharedArrayBuffer)
544
564
  return true;
@@ -593,7 +613,7 @@ export async function resolveAsk(req, onAsk, signal) {
593
613
  if (!presented.ok) {
594
614
  return {
595
615
  action: "deny",
596
- message: `approval for "${req.toolName}" could not present the args safely (unclonable value) — denied fail-closed`,
616
+ message: `approval for "${req.toolName}" could not present the args safely (unclonable value: ${presented.reason}) — denied fail-closed`,
597
617
  decisionReason: "mode",
598
618
  };
599
619
  }
@@ -603,7 +623,7 @@ export async function resolveAsk(req, onAsk, signal) {
603
623
  if (!approverView.ok) {
604
624
  return {
605
625
  action: "deny",
606
- message: `approval for "${req.toolName}" could not present the args safely (unclonable value) — denied fail-closed`,
626
+ message: `approval for "${req.toolName}" could not present the args safely (unclonable value: ${approverView.reason}) — denied fail-closed`,
607
627
  decisionReason: "mode",
608
628
  };
609
629
  }
@@ -612,7 +632,7 @@ export async function resolveAsk(req, onAsk, signal) {
612
632
  catch (err) {
613
633
  return {
614
634
  action: "deny",
615
- message: `approval errored for "${req.toolName}": ${err instanceof Error ? err.message : String(err)}`,
635
+ message: `approval errored for "${req.toolName}": ${describeThrown(err)}`,
616
636
  decisionReason: "mode",
617
637
  };
618
638
  }
@@ -635,7 +655,7 @@ export async function resolveAsk(req, onAsk, signal) {
635
655
  if (!edit.ok) {
636
656
  return {
637
657
  action: "deny",
638
- message: `the approved edit for "${req.toolName}" is not safely clonable — denied fail-closed`,
658
+ message: `the approved edit for "${req.toolName}" is not safely clonable (${edit.reason}) — denied fail-closed`,
639
659
  decisionReason: "mode",
640
660
  };
641
661
  }
@@ -174,6 +174,7 @@ export interface HandsBandOptions {
174
174
  readImageDownsampler?: ((input: Buffer, mimeType: string) => Promise<import("./mcp.js").DownsampledImage | undefined>) | false;
175
175
  beforeWrite?: BeforeWriteHook;
176
176
  autoBackgroundOnTimeout?: boolean;
177
+ readCyberReminder?: boolean;
177
178
  }
178
179
  export interface AgentDefinition {
179
180
  name: string;
@@ -19,9 +19,9 @@ export function deriveAskEffective(form, parkEffective) {
19
19
  case "callback":
20
20
  return "human_reachable";
21
21
  case "allow":
22
- return "auto_allow";
22
+ return parkEffective === true ? "park_only" : parkEffective === "unresolved" ? "unresolved" : "auto_allow";
23
23
  case "deny":
24
- return "auto_deny";
24
+ return parkEffective === true ? "park_only" : parkEffective === "unresolved" ? "unresolved" : "auto_deny";
25
25
  case "absent":
26
26
  return parkEffective === true ? "park_only" : parkEffective === "unresolved" ? "unresolved" : "auto_deny";
27
27
  default: {