@sema-agent/core 5.9.0 → 5.11.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 (62) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/agents/roster-store.d.ts +1 -0
  3. package/dist/agents/send-message-tool.js +6 -0
  4. package/dist/agents/subagent.d.ts +31 -1
  5. package/dist/agents/subagent.js +67 -21
  6. package/dist/agents/teacher.js +15 -3
  7. package/dist/agents/team.js +10 -0
  8. package/dist/agents/verify.js +7 -0
  9. package/dist/brain/anthropic.js +27 -10
  10. package/dist/brain/open-responses.js +19 -4
  11. package/dist/brain/openai.js +32 -5
  12. package/dist/core/a2a.js +1 -1
  13. package/dist/core/background-agent-store.d.ts +2 -1
  14. package/dist/core/background-agent-store.js +1 -0
  15. package/dist/core/checkpoint-store.d.ts +2 -0
  16. package/dist/core/checkpoint-store.js +1 -0
  17. package/dist/core/memory-recall.js +8 -3
  18. package/dist/core/memory.d.ts +5 -0
  19. package/dist/core/memory.js +6 -4
  20. package/dist/core/runner/assemble-result.js +9 -0
  21. package/dist/core/runner/prepare-task.d.ts +12 -1
  22. package/dist/core/runner/prepare-task.js +110 -38
  23. package/dist/core/runner/runtask.d.ts +12 -0
  24. package/dist/core/runner/runtask.js +169 -37
  25. package/dist/core/runner/session-file-state-replay.d.ts +7 -0
  26. package/dist/core/runner/session-file-state-replay.js +56 -0
  27. package/dist/core/runner/synthetic-tools.js +1 -1
  28. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  29. package/dist/core/runner/tool-disclosure.js +24 -9
  30. package/dist/core/runner/tool-output-projection.js +5 -4
  31. package/dist/core/runner/turn-attachments.d.ts +2 -0
  32. package/dist/core/runner/turn-attachments.js +14 -5
  33. package/dist/core/session-reconcile.d.ts +7 -3
  34. package/dist/core/session-reconcile.js +3 -2
  35. package/dist/core/strategy-store.d.ts +1 -1
  36. package/dist/core/strategy-store.js +27 -4
  37. package/dist/core/task-registry-agent.d.ts +3 -0
  38. package/dist/core/task-registry-agent.js +9 -2
  39. package/dist/core/task-registry-shared.d.ts +1 -0
  40. package/dist/core/task-registry.d.ts +2 -0
  41. package/dist/core/tools.js +9 -1
  42. package/dist/core/trace.d.ts +1 -0
  43. package/dist/core/types.d.ts +8 -1
  44. package/dist/engine/loop/agent-loop.js +168 -22
  45. package/dist/orchestration/run-workflow-tool.js +1 -1
  46. package/dist/orchestration/workflow-governance.js +19 -0
  47. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  48. package/dist/orchestration/workflow-primitives.js +4 -1
  49. package/dist/orchestration/workflow.js +1 -1
  50. package/dist/prompts/coordinator.d.ts +1 -1
  51. package/dist/prompts/coordinator.js +1 -1
  52. package/dist/prompts/default.d.ts +1 -0
  53. package/dist/prompts/default.js +3 -0
  54. package/dist/stores/file/memory-store.js +3 -7
  55. package/dist/tools/fs/fs-bash.js +7 -4
  56. package/dist/tools/fs/fs-shared.d.ts +1 -0
  57. package/dist/tools/fs/fs-shared.js +4 -0
  58. package/dist/tools/fs/index.d.ts +1 -0
  59. package/dist/tools/fs/index.js +7 -4
  60. package/dist/tools/web.d.ts +21 -1
  61. package/dist/tools/web.js +126 -11
  62. package/package.json +2 -2
@@ -24,7 +24,7 @@ import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
24
24
  import { assembleResult, errorCodeOf } from "./assemble-result.js";
25
25
  import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, renderOrphanedBackgroundTasks, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta, stampWriteAnchor } from "./turn-attachments.js";
26
26
  import { buildWorkingFileAttachments, centerAdoptionOption, emitInputTruncated } from "./compaction-call-options.js";
27
- import { prepareTask } from "./prepare-task.js";
27
+ import { prepareTask, resolveCheckpointStore } from "./prepare-task.js";
28
28
  import { settleTeardownLeg } from "./teardown-bounded.js";
29
29
  import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
30
30
  import { hasVerifiableStructureSignal } from "./grounding-signal.js";
@@ -123,6 +123,9 @@ function toolEndBodyFrom(result, isError) {
123
123
  ...(typeof code === "string" ? { errorCode: code } : {}),
124
124
  };
125
125
  }
126
+ export function reconciledToolEndBody(orphan) {
127
+ return toolEndBodyFrom({ content: orphan.text, details: { code: orphan.errorKind } }, true);
128
+ }
126
129
  function deepJsonEqual(a, b) {
127
130
  if (a === b)
128
131
  return true;
@@ -240,6 +243,42 @@ function resumeContinuation(resume) {
240
243
  `conversation above. Do NOT restart the task or re-run any tool you already ran; continue from this ` +
241
244
  `exact point, building on the existing results, and finish the remaining work.`);
242
245
  }
246
+ const ENV_DUE_GOVERNANCE_READ_BUDGET_MS = 5_000;
247
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
248
+ const DEADLINE_TICK = Symbol("deadline-tick");
249
+ export const GOVERNANCE_READ_STALLED = Symbol("governance-read-stalled");
250
+ const CHARGE_SETTLE_DISCLOSE_MS = 10_000;
251
+ export async function awaitChargeWithSlowDisclosure(charge, onSlow, discloseAfterMs = CHARGE_SETTLE_DISCLOSE_MS) {
252
+ let timer = setTimeout(() => {
253
+ timer = undefined;
254
+ try {
255
+ onSlow();
256
+ }
257
+ catch {
258
+ }
259
+ }, discloseAfterMs);
260
+ try {
261
+ return await charge;
262
+ }
263
+ finally {
264
+ if (timer !== undefined)
265
+ clearTimeout(timer);
266
+ }
267
+ }
268
+ export async function raceUntilDeadline(p, deadline) {
269
+ for (let firstPass = true;; firstPass = false) {
270
+ const remaining = deadline - Date.now();
271
+ if (remaining <= 0 && !firstPass)
272
+ return GOVERNANCE_READ_STALLED;
273
+ let timer;
274
+ const tick = new Promise((res) => {
275
+ timer = setTimeout(() => res(DEADLINE_TICK), Math.max(0, Math.min(remaining, MAX_TIMER_DELAY_MS)));
276
+ });
277
+ const out = await Promise.race([p, tick]).finally(() => clearTimeout(timer));
278
+ if (out !== DEADLINE_TICK)
279
+ return out;
280
+ }
281
+ }
243
282
  function platformLimitTerminal(reason, retryAfterMs, moment = "turn_boundary") {
244
283
  const message = moment === "entry"
245
284
  ? `a deployment usage window is exhausted (RunnerDeps.usageWindows), so the task was refused before its first model call — nothing ran and nothing was spent. There is no checkpoint to suspend into at this point, so the caller re-submits after the window frees, in ${String(retryAfterMs ?? 0)}ms.`
@@ -267,17 +306,39 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
267
306
  let platformRetryAfterMs;
268
307
  const preemptWillSuspend = preemptHit && prepared.suspendForResource !== undefined;
269
308
  if (!preemptWillSuspend && prepared.suspendRef.token === undefined && !prepared.abortController.signal.aborted) {
270
- if (prepared.envLifetimeSuspendAt !== undefined && Date.now() >= prepared.envLifetimeSuspendAt) {
271
- platformCause = "env_lifetime";
272
- }
273
- else if (prepared.usageGovernance !== undefined) {
309
+ const ledgerReadDeadline = prepared.envLifetimeSuspendAt !== undefined ? prepared.envLifetimeSuspendAt + ENV_DUE_GOVERNANCE_READ_BUDGET_MS : undefined;
310
+ if (prepared.usageGovernance !== undefined) {
274
311
  const governance = prepared.usageGovernance;
275
312
  try {
276
313
  const at = Date.now();
277
- await governance.commit(stats.tokens, at);
278
- platformRetryAfterMs = await governance.check(at);
279
- if (platformRetryAfterMs !== undefined)
280
- platformCause = "usage_window";
314
+ await awaitChargeWithSlowDisclosure(governance.commit(stats.tokens, at), () => runnerHooks.onError?.(new Error("the usage ledger CHARGE has not settled after 10s — still waiting (a charge is never abandoned: walking away would double-charge on the end-of-task flush). A wedged ledger store wedges this boundary, now visibly."), { phase: "config", sessionId: prepared.sessionId }));
315
+ const windowRead = governance.check(at);
316
+ let answer;
317
+ if (ledgerReadDeadline !== undefined) {
318
+ answer = await raceUntilDeadline(windowRead, ledgerReadDeadline);
319
+ if (answer === GOVERNANCE_READ_STALLED) {
320
+ void windowRead.catch((lateErr) => {
321
+ try {
322
+ runnerHooks.onError?.(lateErr instanceof Error ? lateErr : new Error(String(lateErr)), { phase: "config", sessionId: prepared.sessionId });
323
+ }
324
+ catch {
325
+ }
326
+ });
327
+ try {
328
+ runnerHooks.onError?.(new Error("the usage ledger did not answer before the execution environment's suspend deadline — stopping for the environment, the cause this run can still prove. The window state for this boundary is UNKNOWN and was not enforced."), { phase: "config", sessionId: prepared.sessionId });
329
+ }
330
+ catch {
331
+ }
332
+ }
333
+ }
334
+ else {
335
+ answer = await windowRead;
336
+ }
337
+ if (answer !== GOVERNANCE_READ_STALLED) {
338
+ platformRetryAfterMs = answer;
339
+ if (platformRetryAfterMs !== undefined)
340
+ platformCause = "usage_window";
341
+ }
281
342
  }
282
343
  catch (govErr) {
283
344
  rs.limits.platformTerminal = govErr instanceof Error ? govErr : new Error(String(govErr));
@@ -286,6 +347,9 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
286
347
  return undefined;
287
348
  }
288
349
  }
350
+ if (platformCause === undefined && prepared.envLifetimeSuspendAt !== undefined && Date.now() >= prepared.envLifetimeSuspendAt) {
351
+ platformCause = "env_lifetime";
352
+ }
289
353
  }
290
354
  if (platformCause !== undefined) {
291
355
  const platformSpend = { costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) };
@@ -477,6 +541,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
477
541
  : {}),
478
542
  ...(bgTasks !== undefined ? { backgroundTasks: bgTasks } : {}),
479
543
  ...(pendingTools !== undefined ? { newTools: pendingTools } : {}),
544
+ ...(prepared.toolMaterializeStatic ? { newToolsStaticFace: true } : {}),
480
545
  ...(mcpToolsDelta !== undefined ? { mcpToolsDelta } : {}),
481
546
  ...(rs.attach.agentListingOn && prepared.agentListing !== undefined
482
547
  ? {
@@ -492,7 +557,11 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
492
557
  ...(mcpDropped !== undefined ? { mcpDroppedTools: mcpDropped } : {}),
493
558
  }));
494
559
  if (pendingTools !== undefined || mcpToolsDelta !== undefined) {
495
- const exact = renderToolsDelta({ ...(pendingTools !== undefined ? { added: pendingTools } : {}), ...(mcpToolsDelta ?? {}) });
560
+ const exact = renderToolsDelta({
561
+ ...(pendingTools !== undefined ? { added: pendingTools } : {}),
562
+ ...(prepared.toolMaterializeStatic ? { staticFace: true } : {}),
563
+ ...(mcpToolsDelta ?? {}),
564
+ });
496
565
  if (exact !== undefined && due.some((a) => a.source === "tools_delta" && a.body === exact)) {
497
566
  const ref = prepared.toolsDeltaRef;
498
567
  if (pendingTools !== undefined)
@@ -923,7 +992,8 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
923
992
  : {}),
924
993
  ts: callEndAt,
925
994
  }));
926
- if (prepared.cacheBreakDetector && prepared.cacheFingerprint) {
995
+ const cacheRowUnknown = m.usageMissing === true || m.stopReason === "aborted";
996
+ if (prepared.cacheBreakDetector && prepared.cacheFingerprint && !cacheRowUnknown) {
927
997
  const finding = prepared.cacheBreakDetector.observe({
928
998
  turn: stats.turns + 1,
929
999
  systemPrompt: prepared.cacheFingerprint.systemPrompt,
@@ -1273,6 +1343,7 @@ export class Runner {
1273
1343
  return Promise.race([p, timeout]).finally(() => clearTimeout(t));
1274
1344
  };
1275
1345
  let reapHandle;
1346
+ let steerChain = Promise.resolve();
1276
1347
  const notifyRef = {};
1277
1348
  const manualCompactRef = { requested: false, waiters: [] };
1278
1349
  const drainManualCompactWaiters = (outcome) => {
@@ -1313,6 +1384,10 @@ export class Runner {
1313
1384
  finally {
1314
1385
  releaseLock?.();
1315
1386
  publishReady(handle);
1387
+ manualCompactRef.closed = true;
1388
+ if (manualCompactRef.waiters.length > 0)
1389
+ manualCompactRef.emitMooted?.("task_ending");
1390
+ manualCompactRef.emitMooted = undefined;
1316
1391
  drainManualCompactWaiters("mooted");
1317
1392
  }
1318
1393
  })();
@@ -1432,24 +1507,41 @@ export class Runner {
1432
1507
  return suggestionsDone.catch(() => []);
1433
1508
  },
1434
1509
  steer: async (text, options) => {
1435
- if (resultValue)
1436
- throw steeringError("the task has already finished");
1437
- const h = handle ?? (await orTimeout(ready));
1438
- if (!h)
1439
- throw steeringError("the task is not running");
1440
1510
  if (options?.trusted && sanitizeUntrustedText(text) !== text) {
1441
1511
  throw steeringError("trusted steering text must not contain a </system-reminder> tag", "steering.invalid_content");
1442
1512
  }
1443
1513
  const payload = options?.trusted ? formatHookFeedback(text) : text;
1444
- try {
1445
- await h.harness.steer(payload, { provenance: "engine-note" });
1446
- }
1447
- catch (e) {
1448
- if (e instanceof Error && e.code === "invalid_state") {
1449
- throw steeringError("the task is no longer running");
1514
+ const deliver = async () => {
1515
+ if (resultValue)
1516
+ throw steeringError("the task has already finished");
1517
+ const h = handle ?? (await orTimeout(ready));
1518
+ if (!h)
1519
+ throw steeringError("the task is not running");
1520
+ try {
1521
+ await h.harness.steer(payload, { provenance: "engine-note" });
1522
+ return;
1450
1523
  }
1451
- throw e;
1452
- }
1524
+ catch (e) {
1525
+ if (!(e instanceof Error && e.code === "invalid_state"))
1526
+ throw e;
1527
+ }
1528
+ const birthDeadline = Date.now() + READY_TIMEOUT_MS;
1529
+ while (resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) {
1530
+ try {
1531
+ await h.harness.steer(payload, { provenance: "engine-note" });
1532
+ return;
1533
+ }
1534
+ catch (e2) {
1535
+ if (!(e2 instanceof Error && e2.code === "invalid_state"))
1536
+ throw e2;
1537
+ }
1538
+ await new Promise((r) => setTimeout(r, 10));
1539
+ }
1540
+ throw steeringError("the task is no longer running");
1541
+ };
1542
+ const p = steerChain.then(deliver);
1543
+ steerChain = p.then(() => undefined, () => undefined);
1544
+ return p;
1453
1545
  },
1454
1546
  notify: async (input, opts) => {
1455
1547
  const notifyError = (msg, code) => {
@@ -1501,6 +1593,8 @@ export class Runner {
1501
1593
  throw steeringError("the task is not running");
1502
1594
  if (opts?.signal?.aborted)
1503
1595
  return "mooted";
1596
+ if (manualCompactRef.closed)
1597
+ return "mooted";
1504
1598
  return new Promise((resolve) => {
1505
1599
  const signal = opts?.signal;
1506
1600
  const entry = {
@@ -1516,6 +1610,7 @@ export class Runner {
1516
1610
  if (manualCompactRef.waiters.length === 0) {
1517
1611
  manualCompactRef.requested = false;
1518
1612
  }
1613
+ manualCompactRef.emitMooted?.("cancelled");
1519
1614
  entry.resolve("mooted");
1520
1615
  }
1521
1616
  };
@@ -1649,6 +1744,13 @@ export class Runner {
1649
1744
  }, this);
1650
1745
  notificationHarness = prepared.harness;
1651
1746
  notificationSessionId = prepared.sessionId;
1747
+ const runSourceTaskId = spec.taskId ?? prepared.sessionId;
1748
+ const parentToolCallId = internals?.parentToolCallId;
1749
+ const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
1750
+ notificationIdent = ident;
1751
+ manualCompactRef.emitMooted = (reason) => {
1752
+ queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason, ...ident() });
1753
+ };
1652
1754
  prepared.harness.onUndrainedEngineNotes = (payloads) => {
1653
1755
  if (notificationSessionId === undefined)
1654
1756
  return;
@@ -1667,14 +1769,15 @@ export class Runner {
1667
1769
  if (pendingIdle !== undefined) {
1668
1770
  for (const payload of discloseDroppedPending(pendingIdle)) {
1669
1771
  deliveredAtTurnOpen.add(taskNotificationDedupKey(payload));
1670
- queue.push({ type: "task_notification", notification: payload });
1772
+ queue.push({ type: "task_notification", notification: payload, ...ident() });
1671
1773
  void prepared.harness.nextTurn(renderTaskNotificationXml(payload), { provenance: "engine-note", enginePayload: payload }).catch(() => {
1672
1774
  this.pendingSessionNotifications.pend(prepared.sessionId, payload);
1673
1775
  });
1674
1776
  }
1675
1777
  }
1676
1778
  }
1677
- onReady({ harness: prepared.harness, abortController: prepared.abortController });
1779
+ const loopLatch = { ended: false };
1780
+ onReady({ harness: prepared.harness, abortController: prepared.abortController, loop: loopLatch });
1678
1781
  const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
1679
1782
  prepared.liveSpendRef.get = () => ({ costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) });
1680
1783
  if (resume &&
@@ -1716,7 +1819,7 @@ export class Runner {
1716
1819
  rs.limits.outputRetryCap = resolveOutputRetries(spec.outputRetries);
1717
1820
  rs.limits.effectiveMaxTurns = resolveMaxTurns(spec.limits);
1718
1821
  rs.telemetry.tracer = spec.tracer ?? this.deps.tracer;
1719
- rs.telemetry.taskId = spec.taskId ?? prepared.sessionId;
1822
+ rs.telemetry.taskId = runSourceTaskId;
1720
1823
  if (taskIdRef) {
1721
1824
  taskIdRef.current = rs.telemetry.taskId;
1722
1825
  taskIdRef.sessionId = prepared.sessionId;
@@ -2064,9 +2167,6 @@ export class Runner {
2064
2167
  const walltimeMonotonicDeadline = effectiveTimeoutMs !== undefined ? rs.telemetry.taskStartMonotonic + effectiveTimeoutMs : undefined;
2065
2168
  rs.counters.walltimeSyncBackstopFired = false;
2066
2169
  const timeout = startTimeout(prepared.harness, prepared.abortController, walltimeMonotonicDeadline !== undefined ? walltimeMonotonicDeadline - performance.now() : undefined, prepared.suspendForResource !== undefined);
2067
- const parentToolCallId = internals?.parentToolCallId;
2068
- const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: rs.telemetry.taskId } : { eventId: uuidv7() };
2069
- notificationIdent = ident;
2070
2170
  const pushContent = (e) => {
2071
2171
  queue.push(e);
2072
2172
  if (parentToolCallId !== undefined && internals?.onForwardEvent) {
@@ -2114,6 +2214,10 @@ export class Runner {
2114
2214
  };
2115
2215
  const withBrainSinks = (fn) => runWithStatusSink(statusEmit, () => runWithBrainTelemetry(telemetryEmit, fn));
2116
2216
  const toolLabels = new Map(prepared.tools.flatMap((t) => (t.label !== undefined && t.label !== t.name ? [[t.name, t.label]] : [])));
2217
+ for (const orphan of prepared.wakeRecovered) {
2218
+ queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan), ...ident() });
2219
+ emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId);
2220
+ }
2117
2221
  const postToolBatchHook = (spec.hooks ?? this.deps.hooks)?.postToolBatch;
2118
2222
  const batchArgs = postToolBatchHook ? new Map() : undefined;
2119
2223
  rs.turn.toolBatch = [];
@@ -2625,9 +2729,11 @@ export class Runner {
2625
2729
  }));
2626
2730
  }
2627
2731
  }
2732
+ loopLatch.ended = true;
2628
2733
  abortedLive = prepared.abortController.signal.aborted;
2629
2734
  }
2630
2735
  catch (err) {
2736
+ loopLatch.ended = true;
2631
2737
  if (errorCodeOf(err) === "resume.tool_unavailable") {
2632
2738
  await settleTeardownLeg(() => prepared.mcp.dispose(), "mcp.dispose (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
2633
2739
  await settleTeardownLeg(() => prepared.a2a?.dispose(), "a2a.dispose (tool_unavailable leg)", (e) => this.deps.onError?.(e, { phase: "config", sessionId: prepared.sessionId }));
@@ -2660,7 +2766,7 @@ export class Runner {
2660
2766
  }
2661
2767
  if (prepared.usageGovernance !== undefined) {
2662
2768
  try {
2663
- await prepared.usageGovernance.commit(stats.tokens, Date.now());
2769
+ await awaitChargeWithSlowDisclosure(prepared.usageGovernance.commit(stats.tokens, Date.now()), () => this.deps.onError?.(new Error("the usage ledger's FINAL charge has not settled after 10s — still waiting (a charge is never abandoned: walking away would double-charge on a later flush). A wedged ledger store wedges this teardown, now visibly."), { phase: "config", sessionId: prepared.sessionId }));
2664
2770
  }
2665
2771
  catch (flushErr) {
2666
2772
  this.deps.onError?.(flushErr, { phase: "config", sessionId: prepared.sessionId });
@@ -2675,7 +2781,7 @@ export class Runner {
2675
2781
  try {
2676
2782
  const report = await reconcileInterruptedSession(prepared.session, prepared.toolEffects, undefined, startedToolCallIds);
2677
2783
  for (const orphan of report.recovered) {
2678
- queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...ident() });
2784
+ queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan), ...ident() });
2679
2785
  emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId);
2680
2786
  }
2681
2787
  }
@@ -2873,7 +2979,7 @@ export class Runner {
2873
2979
  const committedToken = prepared.suspendRef.token ?? prepared.reviewRef.token;
2874
2980
  const committedScope = prepared.suspendRef.scope ?? prepared.reviewRef.scope;
2875
2981
  if (committedToken !== undefined) {
2876
- const store = spec.checkpointStore ?? this.deps.checkpointStore;
2982
+ const store = resolveCheckpointStore(spec, this.deps);
2877
2983
  if (store && committedScope !== undefined) {
2878
2984
  onSuspend({
2879
2985
  env: prepared.ownedEnv,
@@ -2929,6 +3035,11 @@ export class Runner {
2929
3035
  catch {
2930
3036
  }
2931
3037
  }
3038
+ if (manualCompactRef.waiters.length > 0) {
3039
+ queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason: "task_ending", ...ident() });
3040
+ drainManualCompact("mooted");
3041
+ }
3042
+ manualCompactRef.emitMooted = undefined;
2932
3043
  queue.push({ type: "done", result });
2933
3044
  queue.close();
2934
3045
  if (spec.rewindFiles && result.status === "completed") {
@@ -3117,7 +3228,7 @@ export class Runner {
3117
3228
  return stream.result();
3118
3229
  }
3119
3230
  async resumeStream(token, outcome, taskConfig, internals) {
3120
- const store = taskConfig.checkpointStore ?? this.deps.checkpointStore;
3231
+ const store = resolveCheckpointStore(taskConfig, this.deps);
3121
3232
  if (!store) {
3122
3233
  throw new CheckpointError("checkpoint.not_found", "no CheckpointStore wired — cannot resume (set RunnerDeps.checkpointStore or taskConfig.checkpointStore)");
3123
3234
  }
@@ -3352,13 +3463,16 @@ export class Runner {
3352
3463
  const completed = new Set(pendingAction.completedCallIds);
3353
3464
  const deferredIds = pendingAction.batchToolCallIds.filter((id) => id !== pendingAction.toolCallId && !completed.has(id));
3354
3465
  const names = new Map();
3466
+ const deferredArgs = new Map();
3355
3467
  if (deferredIds.length > 0) {
3356
3468
  const { messages } = await prepared.session.buildContext();
3357
3469
  for (const m of messages) {
3358
3470
  if (m.role === "assistant") {
3359
3471
  for (const c of m.content) {
3360
- if (c.type === "toolCall")
3472
+ if (c.type === "toolCall") {
3361
3473
  names.set(c.id, c.name);
3474
+ deferredArgs.set(c.id, c.arguments);
3475
+ }
3362
3476
  }
3363
3477
  }
3364
3478
  }
@@ -3369,7 +3483,12 @@ export class Runner {
3369
3483
  }
3370
3484
  else if (!completed.has(id)) {
3371
3485
  const name = names.get(id) ?? "unknown";
3372
- emit({ type: "tool_end", toolCallId: id, toolName: name, ...((() => { const l = prepared.tools.find((t) => t.name === name)?.label; return l !== undefined && l !== name ? { label: l } : {}; })()), isError: true, ...toolEndBodyFrom({ content: formatHookFeedback(DEFERRED_REISSUE) }, true) });
3486
+ const displayLabel = (() => {
3487
+ const l = prepared.tools.find((t) => t.name === name)?.label;
3488
+ return l !== undefined && l !== name ? { label: l } : {};
3489
+ })();
3490
+ emit({ type: "tool_start", toolCallId: id, toolName: name, ...displayLabel, args: deferredArgs.get(id) ?? {} });
3491
+ emit({ type: "tool_end", toolCallId: id, toolName: name, ...displayLabel, isError: true, ...toolEndBodyFrom({ content: formatHookFeedback(DEFERRED_REISSUE) }, true) });
3373
3492
  const eid = await prepared.session.appendMessage(toolResultMsg(id, name, formatHookFeedback(DEFERRED_REISSUE), true));
3374
3493
  emitCommitted(eid, "toolResult", id);
3375
3494
  }
@@ -3419,13 +3538,26 @@ export class Runner {
3419
3538
  }
3420
3539
  const args = resolvedArgs;
3421
3540
  onExecuteStart?.(pendingAction.toolCallId);
3541
+ prepared.suspendProgressRef.executedApproved = true;
3422
3542
  let res;
3423
3543
  try {
3424
3544
  res = await tool.execute(pendingAction.toolCallId, args, prepared.abortController.signal);
3425
3545
  }
3426
3546
  catch (err) {
3427
3547
  const execError = `Error: ${err instanceof Error ? err.message : String(err)}`;
3428
- emitEnd(true, { content: execError });
3548
+ const marks = (() => {
3549
+ if (err === null || typeof err !== "object")
3550
+ return undefined;
3551
+ const src = err;
3552
+ let d;
3553
+ if (src.details !== null && typeof src.details === "object" && !Array.isArray(src.details)) {
3554
+ d = { ...src.details };
3555
+ }
3556
+ if (typeof src.errorKind === "string")
3557
+ d = { ...(d ?? {}), errorKind: src.errorKind };
3558
+ return d;
3559
+ })();
3560
+ emitEnd(true, { content: execError, ...(marks !== undefined ? { details: marks } : {}) });
3429
3561
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, execError, true));
3430
3562
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3431
3563
  return;
@@ -0,0 +1,7 @@
1
+ import type { AgentMessage } from "../../internal/harness.js";
2
+ export interface TranscriptFileRecord {
3
+ path: string;
4
+ content: string;
5
+ at: number;
6
+ }
7
+ export declare function wholeFileRecordsFromTranscript(messages: readonly AgentMessage[]): TranscriptFileRecord[];
@@ -0,0 +1,56 @@
1
+ import { isAbsolutePathForm } from "../../tools/fs/safety.js";
2
+ const READ_TOOL = "Read";
3
+ const WRITE_TOOL = "Write";
4
+ const RETRACTING_RESULTS = [
5
+ { toolName: "Edit", type: "edit", pathField: "filePath" },
6
+ { toolName: "NotebookEdit", type: "notebook-edit", pathField: "notebookPath" },
7
+ ];
8
+ function cardOf(details) {
9
+ if (typeof details !== "object" || details === null)
10
+ return undefined;
11
+ const rest = details;
12
+ return typeof rest.type === "string" ? { type: rest.type, rest } : undefined;
13
+ }
14
+ function wholeFileFromReadCard(rest) {
15
+ const file = rest.file;
16
+ if (typeof file !== "object" || file === null)
17
+ return undefined;
18
+ const f = file;
19
+ if (typeof f.content !== "string" || f.truncatedByTokenCap === true)
20
+ return undefined;
21
+ if (f.startLine !== 1 || typeof f.numLines !== "number" || f.numLines !== f.totalLines)
22
+ return undefined;
23
+ return f.content;
24
+ }
25
+ export function wholeFileRecordsFromTranscript(messages) {
26
+ const byPath = new Map();
27
+ for (const m of messages) {
28
+ if (m.role !== "toolResult" || m.isError)
29
+ continue;
30
+ const card = cardOf(m.details);
31
+ if (card === undefined)
32
+ continue;
33
+ const { type, rest } = card;
34
+ const retracts = RETRACTING_RESULTS.find((r) => r.toolName === m.toolName && r.type === type);
35
+ if (retracts !== undefined) {
36
+ const changed = rest[retracts.pathField];
37
+ if (typeof changed === "string")
38
+ byPath.delete(changed);
39
+ continue;
40
+ }
41
+ const isRead = m.toolName === READ_TOOL && type === "text";
42
+ const isWrite = m.toolName === WRITE_TOOL && (type === "create" || type === "update");
43
+ if (!isRead && !isWrite)
44
+ continue;
45
+ const nested = rest.file;
46
+ const holder = isRead ? (typeof nested === "object" && nested !== null ? nested : undefined) : rest;
47
+ const filePath = holder?.filePath;
48
+ if (typeof filePath !== "string" || !isAbsolutePathForm(filePath))
49
+ continue;
50
+ const content = isRead ? wholeFileFromReadCard(rest) : typeof rest.content === "string" ? rest.content : undefined;
51
+ if (content === undefined)
52
+ continue;
53
+ byPath.set(filePath, { path: filePath, content, at: m.timestamp });
54
+ }
55
+ return [...byPath.values()];
56
+ }
@@ -78,7 +78,7 @@ export function createReportFindingsTool() {
78
78
  const count = findings.length;
79
79
  return {
80
80
  content: count === 0 ? "No findings reported." : `${count} finding${count === 1 ? "" : "s"} reported.`,
81
- details: { count, ...(level !== undefined ? { level } : {}), findings },
81
+ details: { type: "report-findings", count, ...(level !== undefined ? { level } : {}), findings },
82
82
  };
83
83
  },
84
84
  });
@@ -54,4 +54,5 @@ export declare function createToolSearchTool(opts: {
54
54
  mountedNames?: () => ReadonlySet<string>;
55
55
  directCallEnabled?: boolean;
56
56
  serializeActivation?: <T>(section: () => Promise<T>) => Promise<T>;
57
+ staticSchemaFor?: (name: string) => TSchema | undefined;
57
58
  }): AgentTool;
@@ -80,7 +80,7 @@ export function createPlaceholderTool(info, direct) {
80
80
  };
81
81
  const invalidArgumentsRejection = (target, params, schemaJson, ride) => {
82
82
  const text = `Invalid arguments for \`${sn}\`: ${formatZodValidationError(target.parameters, params)} ` +
83
- `\`${sn}\` is now active — its full parameter schema is below (and rides the next request). ` +
83
+ `\`${sn}\` is now active — its full parameter schema is below; use it for this and later calls. ` +
84
84
  `Call \`${sn}\` again with arguments matching it.\nParameter schema: ${schemaJson}`;
85
85
  return {
86
86
  content: ride === undefined || ride === "" ? [{ type: "text", text }] : [{ type: "text", text }, { type: "text", text: ride }],
@@ -236,11 +236,15 @@ export function extractDiscoveredToolNames(messages, registry) {
236
236
  return [...names];
237
237
  }
238
238
  export function createToolSearchTool(opts) {
239
- const { registry, active, rematerialize, listingRide, mountedNames } = opts;
239
+ const { registry, active, rematerialize, listingRide, mountedNames, staticSchemaFor } = opts;
240
240
  const directCallEnabled = opts.directCallEnabled !== false;
241
241
  const activationPosture = directCallEnabled
242
- ? "Most tools start as name-only placeholders to keep requests small; activating one here loads its full " +
243
- "parameter schema. Until you have that schema you cannot reliably form a call, so activate a tool rather " +
242
+ ? (staticSchemaFor !== undefined
243
+ ? "Most tools start as name-only placeholders to keep requests small; activating one here returns its full " +
244
+ "parameter schema in the result (the tools list keeps the compact placeholder entry). "
245
+ : "Most tools start as name-only placeholders to keep requests small; activating one here loads its full " +
246
+ "parameter schema. ") +
247
+ "Until you have that schema you cannot reliably form a call, so activate a tool rather " +
244
248
  "than guessing its arguments — a call that does match the real schema executes and activates the tool. " +
245
249
  "When any instruction, reminder, or another tool's description names a deferred tool, activate it here " +
246
250
  'with query "select:<name>". '
@@ -265,7 +269,9 @@ export function createToolSearchTool(opts) {
265
269
  "a bare tool name — activates that tool directly. " +
266
270
  "Activate every tool you expect to need in one call (select accepts a comma-separated list) " +
267
271
  "rather than one at a time. " +
268
- "Activated tools become callable with their full parameters on your next turn.",
272
+ (staticSchemaFor !== undefined
273
+ ? "Activation returns each tool's full parameter schema in this result — call the tool directly with arguments matching it."
274
+ : "Activated tools become callable with their full parameters on your next turn."),
269
275
  parameters: Type.Object({
270
276
  query: Type.Optional(Type.String({
271
277
  description: 'Query to find deferred tools. Use "select:<tool_name>" for direct selection, or keywords to search.',
@@ -329,11 +335,20 @@ export function createToolSearchTool(opts) {
329
335
  const lines = matched.map((n) => {
330
336
  const info = registry.get(n);
331
337
  const tag = newly.includes(n) ? "activated" : "already active";
332
- return `- ${safeName(n)} (${tag})${info ? ` — ${info.hint}` : ""}`;
338
+ const base = `- ${safeName(n)} (${tag})${info ? ` — ${info.hint}` : ""}`;
339
+ if (staticSchemaFor === undefined)
340
+ return base;
341
+ const schema = staticSchemaFor(n);
342
+ const json = schema === undefined ? undefined : renderSchemaForModel(schema);
343
+ return json === undefined ? base : `${base}\n parameters: ${json}`;
333
344
  });
334
- const head = newly.length > 0
335
- ? `Activated ${newly.length} tool(s); they are now available with full parameters — call them directly:`
336
- : "These tools are already active — call them directly:";
345
+ const head = staticSchemaFor !== undefined
346
+ ? newly.length > 0
347
+ ? `Activated ${newly.length} tool(s) — call them directly with arguments matching the parameter schemas below (the tools list keeps compact placeholder entries):`
348
+ : "These tools are already active — call them directly; their parameter schemas are repeated below:"
349
+ : newly.length > 0
350
+ ? `Activated ${newly.length} tool(s); they are now available with full parameters — call them directly:`
351
+ : "These tools are already active — call them directly:";
337
352
  return {
338
353
  content: `${head}\n${lines.join("\n")}${missingNote}${ride !== undefined ? `\n\n${ride}` : ""}`,
339
354
  details: {
@@ -57,11 +57,12 @@ export const toolOutputFrom = (result) => {
57
57
  return { output: raw, truncated: true, totalChars };
58
58
  };
59
59
  const CC_DETAIL_TYPES = new Set([
60
- "edit", "multiedit", "create", "update", "bash", "notebook-edit", "notebook", "file_unchanged", "worktree", "text", "grep", "glob", "mcp",
61
- "agent", "task", "task-list", "task-output", "memory-saved", "workflow-run",
60
+ "edit", "create", "update", "bash", "notebook-edit", "notebook", "file_unchanged", "worktree", "text", "grep", "glob", "mcp",
61
+ "agent", "task", "task-list", "task-output", "workflow-run",
62
62
  "web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",
63
- "task-stop", "tool-search", "memory-recall", "repo-map", "fork", "enter-plan-mode", "exit-plan-mode",
64
- "monitor-start", "path_not_in_root",
63
+ "task-stop", "tool-search", "repo-map", "fork", "enter-plan-mode", "exit-plan-mode",
64
+ "monitor-start", "path_not_in_root", "readonly_out_of_root",
65
+ "report-findings", "schedule-wakeup", "send-message", "agent-transcript", "a2a", "document",
65
66
  ]);
66
67
  export const structuredFrom = (result) => {
67
68
  const details = result !== null && typeof result === "object" ? result.details : undefined;
@@ -100,6 +100,7 @@ export interface AttachmentInputs {
100
100
  }>;
101
101
  backgroundTasks?: ReadonlyArray<BackgroundTaskSnapshot>;
102
102
  newTools?: readonly string[];
103
+ newToolsStaticFace?: boolean;
103
104
  mcpToolsDelta?: McpToolsDeltaFacts;
104
105
  agentListing?: ReadonlyArray<AgentListingEntry>;
105
106
  agentToolName?: string;
@@ -148,6 +149,7 @@ export interface McpToolsDeltaFacts {
148
149
  }
149
150
  export declare function renderToolsDelta(input: {
150
151
  added?: readonly string[];
152
+ staticFace?: boolean;
151
153
  } & McpToolsDeltaFacts): string | undefined;
152
154
  export declare const AGENT_TOOLS_NOTE_DEFAULT = "All tools";
153
155
  export declare const AGENT_CONCURRENCY_NOTE = "When you launch multiple agents for independent work, send them in a single message with multiple tool uses so they run concurrently.";
@@ -168,7 +168,11 @@ export function collectDueAttachments(state, inp) {
168
168
  }
169
169
  }
170
170
  if (inp.config.toolsDelta) {
171
- const body = renderToolsDelta({ ...(inp.newTools !== undefined ? { added: inp.newTools } : {}), ...(inp.mcpToolsDelta ?? {}) });
171
+ const body = renderToolsDelta({
172
+ ...(inp.newTools !== undefined ? { added: inp.newTools } : {}),
173
+ ...(inp.newToolsStaticFace === true ? { staticFace: true } : {}),
174
+ ...(inp.mcpToolsDelta ?? {}),
175
+ });
172
176
  if (body !== undefined)
173
177
  (out ??= []).push({ source: "tools_delta", body });
174
178
  }
@@ -386,15 +390,20 @@ export function renderToolsDelta(input) {
386
390
  const blocks = [];
387
391
  const added = input.added ?? [];
388
392
  if (added.length > 0) {
389
- blocks.push("The following deferred tools are now available. Their full schemas are loaded — call them " +
390
- "directly like any other tool:\n" +
393
+ blocks.push((input.staticFace === true
394
+ ? "The following deferred tools are now active — call them directly. Their parameter schemas were " +
395
+ "provided in the ToolSearch result (the tools list itself keeps compact placeholder entries):\n"
396
+ : "The following deferred tools are now available. Their full schemas are loaded — call them " +
397
+ "directly like any other tool:\n") +
391
398
  added.map((n) => `- ${n}`).join("\n"));
392
399
  }
393
400
  const readded = input.readded ?? [];
394
401
  if (readded.length > 0) {
395
402
  blocks.push(`${readded.length} deferred tool${readded.length === 1 ? " is" : "s are"} available again (MCP server reconnected — ` +
396
- `names announced earlier in this conversation): ${groupByMcpServer(readded)}. Their schemas are loaded again — ` +
397
- `call them directly.`);
403
+ `names announced earlier in this conversation): ${groupByMcpServer(readded)}. ` +
404
+ (input.staticFace === true
405
+ ? `The tools list keeps compact placeholder entries — re-run ToolSearch ("select:<name>") if you need their current parameter schemas.`
406
+ : `Their schemas are loaded again — call them directly.`));
398
407
  }
399
408
  const removed = input.removed ?? [];
400
409
  if (removed.length > 0) {