@sema-agent/core 2.8.0 → 2.10.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/dist/agents/send-message-tool.js +37 -29
  2. package/dist/agents/subagent.js +91 -4
  3. package/dist/brain/circuit-breaker.js +18 -8
  4. package/dist/brain/retry.d.ts +1 -0
  5. package/dist/brain/retry.js +29 -7
  6. package/dist/brain/stream-engine.d.ts +1 -0
  7. package/dist/brain/stream-engine.js +74 -12
  8. package/dist/config/defaults.d.ts +1 -0
  9. package/dist/config/defaults.js +1 -0
  10. package/dist/core/auto-compaction.js +9 -1
  11. package/dist/core/background-agent-store.d.ts +2 -0
  12. package/dist/core/background-agent-store.js +20 -0
  13. package/dist/core/mcp.js +8 -5
  14. package/dist/core/runner/assemble-result.d.ts +1 -0
  15. package/dist/core/runner/assemble-result.js +1 -1
  16. package/dist/core/runner/prepare-task.d.ts +2 -1
  17. package/dist/core/runner/prepare-task.js +61 -20
  18. package/dist/core/runner/runtask.js +38 -12
  19. package/dist/core/runner/tool-disclosure.d.ts +8 -3
  20. package/dist/core/runner/tool-disclosure.js +39 -10
  21. package/dist/core/skills-directory.d.ts +1 -1
  22. package/dist/core/skills-directory.js +257 -28
  23. package/dist/core/task-registry-agent.d.ts +2 -1
  24. package/dist/core/task-registry-agent.js +50 -54
  25. package/dist/core/task-registry.d.ts +1 -0
  26. package/dist/core/task-registry.js +1 -1
  27. package/dist/core/types.d.ts +9 -1
  28. package/dist/engine/compaction/compaction.js +71 -20
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/internal/harness-types.d.ts +1 -1
  32. package/dist/internal/harness.d.ts +1 -1
  33. package/dist/internal/harness.js +1 -1
  34. package/dist/orchestration/run-workflow-tool.d.ts +1 -0
  35. package/dist/orchestration/run-workflow-tool.js +4 -1
  36. package/dist/orchestration/workflow.d.ts +1 -1
  37. package/dist/orchestration/workflow.js +20 -12
  38. package/dist/tools/fs/bash-readonly-classifier.js +83 -17
  39. package/dist/tools/fs/fs-bash.js +17 -11
  40. package/dist/tools/fs/fs-shared.d.ts +1 -0
  41. package/dist/tools/fs/fs-shared.js +44 -2
  42. package/dist/tools/web.d.ts +15 -0
  43. package/dist/tools/web.js +42 -0
  44. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, CompactionError, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, } from "../internal/harness.js";
1
+ import { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, CompactionError, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, formatPersistedOutputRefs, prepareCompaction, shouldCompact, } from "../internal/harness.js";
2
2
  import { fileArgPath } from "../tools/fs/safety.js";
3
3
  import { contextEditFrontier } from "./context-edit.js";
4
4
  import { selectCompactionEpoch } from "../prompt-assembly/epoch.js";
@@ -155,6 +155,14 @@ export async function maybeCompact(opts) {
155
155
  if (prep.value.invokedSkills.length > 0) {
156
156
  details.invokedSkills = prep.value.invokedSkills;
157
157
  }
158
+ const reusedRefs = prep.value.persistedOutputRefs ?? [];
159
+ if (reusedRefs.length > 0) {
160
+ details.persistedOutputRefs = reusedRefs;
161
+ summary += formatPersistedOutputRefs(reusedRefs);
162
+ }
163
+ if (prep.value.elidedMessages !== undefined && prep.value.elidedMessages > 0) {
164
+ details.elidedMessages = prep.value.elidedMessages;
165
+ }
158
166
  }
159
167
  else {
160
168
  let summaryModel = opts.compactionModel ?? opts.model;
@@ -46,6 +46,8 @@ export interface BackgroundAgentRecord {
46
46
  usage?: BackgroundAgentUsage;
47
47
  rev: number;
48
48
  }
49
+ export declare const REVIVED_ROW_CLEARED_FIELDS: readonly ["settledAt", "stoppedBy", "completionId", "finalOutput", "finalOutputFull", "error", "errorCode", "errorRetryable", "errorKind", "resultIsPartial", "summary", "recentSteps", "editedFiles", "usage"];
50
+ export declare function clearRevivedRowTerminalPayload(record: BackgroundAgentRecord): void;
49
51
  export interface BackgroundAgentRowSummary {
50
52
  handle: string;
51
53
  owner: string;
@@ -1,4 +1,24 @@
1
1
  import { uuidv7 } from "../internal/harness.js";
2
+ export const REVIVED_ROW_CLEARED_FIELDS = [
3
+ "settledAt",
4
+ "stoppedBy",
5
+ "completionId",
6
+ "finalOutput",
7
+ "finalOutputFull",
8
+ "error",
9
+ "errorCode",
10
+ "errorRetryable",
11
+ "errorKind",
12
+ "resultIsPartial",
13
+ "summary",
14
+ "recentSteps",
15
+ "editedFiles",
16
+ "usage",
17
+ ];
18
+ export function clearRevivedRowTerminalPayload(record) {
19
+ for (const field of REVIVED_ROW_CLEARED_FIELDS)
20
+ delete record[field];
21
+ }
2
22
  export class BackgroundAgentStoreError extends Error {
3
23
  code;
4
24
  constructor(code, message) {
package/dist/core/mcp.js CHANGED
@@ -145,6 +145,11 @@ const MCP_SPEC_ERROR_CODE_NAMES = new Map([
145
145
  export function describeMcpSpecErrorCode(code) {
146
146
  return typeof code === "number" ? MCP_SPEC_ERROR_CODE_NAMES.get(code) : undefined;
147
147
  }
148
+ function namedMcpFailureText(err) {
149
+ const detail = err instanceof Error ? err.message : String(err);
150
+ const condition = err instanceof McpError ? describeMcpSpecErrorCode(err.code) : undefined;
151
+ return condition !== undefined ? `${condition}: ${detail}` : detail;
152
+ }
148
153
  function isTransportLost(err) {
149
154
  if (err instanceof McpError && err.code === ErrorCode.ConnectionClosed)
150
155
  return true;
@@ -555,7 +560,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
555
560
  }
556
561
  else {
557
562
  warnings.push(asServerWarning(spec, r.reason));
558
- statuses.push({ name: spec.name, status: "failed", error: r.reason instanceof Error ? r.reason.message : String(r.reason) });
563
+ statuses.push({ name: spec.name, status: "failed", error: namedMcpFailureText(r.reason) });
559
564
  }
560
565
  }
561
566
  const resourceTools = buildResourceTools(resourceServers);
@@ -614,7 +619,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
614
619
  toolCount: h.toolNames.length,
615
620
  added: [],
616
621
  removed: [],
617
- error: inlineUntrusted(err instanceof Error ? err.message : String(err), 240),
622
+ error: inlineUntrusted(namedMcpFailureText(err), 240),
618
623
  });
619
624
  }
620
625
  }
@@ -1155,9 +1160,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer) {
1155
1160
  return { serverTools, serverAxes, dropped };
1156
1161
  }
1157
1162
  function asServerWarning(spec, err) {
1158
- const detail = err instanceof Error ? err.message : String(err);
1159
- const condition = err instanceof McpError ? describeMcpSpecErrorCode(err.code) : undefined;
1160
- const warning = new Error(`mcp: server "${spec.name}" failed to connect — skipped (${condition !== undefined ? `${condition}: ` : ""}${detail})`, { cause: err });
1163
+ const warning = new Error(`mcp: server "${spec.name}" failed to connect — skipped (${namedMcpFailureText(err)})`, { cause: err });
1161
1164
  warning.code = "mcp.server_unavailable";
1162
1165
  return warning;
1163
1166
  }
@@ -69,6 +69,7 @@ export interface ResultFlags {
69
69
  threw: unknown;
70
70
  model?: string;
71
71
  unpricedSpend?: boolean;
72
+ rewindNotes?: TaskResult["rewindNotes"];
72
73
  abortedForTimeout: boolean;
73
74
  abortedForTurns: boolean;
74
75
  abortedLive?: boolean;
@@ -143,5 +143,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
143
143
  void _internalCompaction;
144
144
  if (flags.unpricedSpend)
145
145
  delete publicStats.costMicroUsd;
146
- return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, checkpointToken, checkpointGate, stats: publicStats };
146
+ return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, checkpointToken, checkpointGate, ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), stats: publicStats };
147
147
  }
@@ -17,7 +17,7 @@ import { type CwdRef } from "../../tools/fs/index.js";
17
17
  import type { Runner } from "./runtask.js";
18
18
  import { type CheckpointGate, type CheckpointState, type CheckpointToken, type ResourceLedger, type ResourceLimitReason } from "../checkpoint-store.js";
19
19
  import type { AgentMessage, AgentTool, ExecutionEnv } from "../../internal/harness.js";
20
- import type { RunnerDeps, TaskEvent, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
20
+ import type { RunnerDeps, TaskEvent, TaskResult, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
21
21
  import type { RepairBundle } from "../../agents/repair-loop.js";
22
22
  export declare const DEFAULT_IRREVERSIBLE_SCOPE = "irreversible";
23
23
  export declare function checkpointScopeOf(spec: {
@@ -48,6 +48,7 @@ export interface Prepared {
48
48
  tasks: number;
49
49
  costMicroUsd: number;
50
50
  };
51
+ rewindNotes?: NonNullable<TaskResult["rewindNotes"]>;
51
52
  cwdRef?: CwdRef;
52
53
  worktreeSessionRef?: {
53
54
  current?: {
@@ -616,8 +616,37 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
616
616
  catch {
617
617
  }
618
618
  }
619
- let rewindTarget = spec.resumeAt !== undefined ? (spec.rewindFiles ? spec.resumeAt : undefined) : spec.rewindFilesTo;
619
+ const rewindNotes = [];
620
+ const rewindCaptureRequested = spec.rewindFiles === true;
621
+ let rewindTarget = spec.resumeAt !== undefined ? (rewindCaptureRequested ? spec.resumeAt : undefined) : spec.rewindFilesTo;
620
622
  const rewindBefore = rewindTarget !== undefined && spec.resumeAt !== undefined && spec.resumeAtMode === "before";
623
+ if (spec.resumeAt !== undefined && !rewindCaptureRequested) {
624
+ rewindNotes.push({
625
+ code: "conversation_only",
626
+ message: `the conversation was branched at entry "${spec.resumeAt}" but the working tree was NOT rewound — this task set resumeAt without rewindFiles, so files remain at their current state`,
627
+ });
628
+ }
629
+ if (!deps.fileSnapshotStore) {
630
+ if (rewindTarget !== undefined) {
631
+ const e = new Error(`rewind-files: restoring the working tree to entry "${rewindTarget}" requires a snapshot backend, but this deployment wired no RunnerDeps.fileSnapshotStore — no snapshot was ever captured, so the files were NOT rewound`);
632
+ e.code = "rewind.store_unconfigured";
633
+ throw e;
634
+ }
635
+ if (rewindCaptureRequested) {
636
+ rewindNotes.push({
637
+ code: "snapshot_store_unconfigured",
638
+ message: "rewindFiles was requested but no RunnerDeps.fileSnapshotStore is wired — no working-tree snapshot was captured for this turn, so it cannot be rewound to later",
639
+ });
640
+ }
641
+ }
642
+ else if (!handsEnabled) {
643
+ if (rewindTarget !== undefined || rewindCaptureRequested) {
644
+ rewindNotes.push({
645
+ code: "files_env_unsupported",
646
+ message: "the file side of rewind was inert: this deployment mounts no filesystem-capable ExecutionEnv, so no working-tree snapshot was captured or restored",
647
+ });
648
+ }
649
+ }
621
650
  if (rewindTarget !== undefined && deps.fileSnapshotStore && handsEnabled) {
622
651
  if (rewindBefore) {
623
652
  let anchor;
@@ -642,18 +671,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
642
671
  const restoreSignal = spec.signal ? AbortSignal.any([abortController.signal, spec.signal]) : abortController.signal;
643
672
  const restored = await deps.fileSnapshotStore.restore(sessionId, rewindTarget, executionEnv, restoreRoot, restoreSignal);
644
673
  if (!restored.ok) {
645
- if (restored.error.code === "not_found" && rewindBefore) {
646
- const e = new Error(`rewind-files: the resolved "before" snapshot anchor "${rewindTarget}" disappeared before restore — files were NOT rewound`);
674
+ if (restored.error.code === "not_found") {
675
+ const e = new Error(rewindBefore
676
+ ? `rewind-files: the resolved "before" snapshot anchor "${rewindTarget}" disappeared before restore — files were NOT rewound`
677
+ : `rewind-files: no file snapshot exists for entry "${rewindTarget}" on session "${sessionId}" — the working tree was NOT rewound (only a COMPLETED turn that ran with rewindFiles is snapshotted, and snapshots are keyed by that turn's END leaf; earlier turns that ran without rewindFiles, mid-turn entries, and reaped snapshots have none)`);
647
678
  e.code = "rewind_snapshot.unresolvable";
648
679
  throw e;
649
680
  }
650
- if (restored.error.code === "not_found") {
651
- try {
652
- deps.onError?.(new Error(`rewind-files: no snapshot for entry "${rewindTarget}" — files left unchanged`), { phase: "rewind", sessionId });
653
- }
654
- catch {
655
- }
656
- }
657
681
  else {
658
682
  const e = new Error(`rewind-files restore failed (${restored.error.code}): ${restored.error.message}`);
659
683
  e.code = "rewind.restore_failed";
@@ -758,6 +782,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
758
782
  clientContext: spec.clientContext,
759
783
  excludeTools: toolFaceSnapshot.exclude,
760
784
  deferTools: toolFaceSnapshot.defer,
785
+ alwaysLoadTools: toolFaceSnapshot.alwaysLoad,
761
786
  promptProfile,
762
787
  ...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
763
788
  ...(spec.envFacts !== undefined ? { envFacts: { ...spec.envFacts } } : {}),
@@ -885,6 +910,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
885
910
  governanceBaseline: deps.workflowGovernanceBaseline,
886
911
  parentExcludeTools: toolFaceSnapshot.exclude,
887
912
  parentDeferTools: toolFaceSnapshot.defer,
913
+ parentAlwaysLoadTools: toolFaceSnapshot.alwaysLoad,
888
914
  parentPromptProfile: promptProfile,
889
915
  models: deps.models,
890
916
  agents: deps.agents,
@@ -1796,7 +1822,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1796
1822
  deferNames: (toolFaceSnapshot.defer ?? []).filter((n) => tools.some((t) => t.name === n)),
1797
1823
  alwaysLoadNames: [
1798
1824
  ...(toolFaceSnapshot.alwaysLoad ?? []),
1799
- ...mcp.tools.filter((t) => t.mcpAlwaysLoad === true).map((t) => t.name),
1825
+ ...mcp.tools
1826
+ .filter((t) => t.mcpAlwaysLoad === true && !(toolFaceSnapshot.defer ?? []).includes(t.name))
1827
+ .map((t) => t.name),
1800
1828
  ],
1801
1829
  });
1802
1830
  for (const n of [...deferred]) {
@@ -1831,21 +1859,33 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1831
1859
  throw e;
1832
1860
  }
1833
1861
  const registry = buildDeferredRegistry(deferred, tools);
1834
- const realByName = new Map(tools.map((t) => [t.name, t]));
1835
1862
  const directCallFor = (name) => {
1836
1863
  if (spec.deferSelfResolve === false)
1837
1864
  return undefined;
1838
- const real = realByName.get(name);
1839
- if (real === undefined)
1840
- return undefined;
1865
+ const executionMode = tools.find((t) => t.name === name)?.executionMode;
1841
1866
  return {
1842
- parameters: real.parameters,
1843
- invoke: (toolCallId, params, signal) => real.execute(toolCallId, params, signal),
1867
+ resolveReal: () => {
1868
+ const real = tools.find((t) => t.name === name);
1869
+ if (real === undefined)
1870
+ return undefined;
1871
+ return {
1872
+ parameters: real.parameters,
1873
+ invoke: (toolCallId, params, signal, onUpdate) => real.execute(toolCallId, params, signal, onUpdate),
1874
+ };
1875
+ },
1876
+ ...(executionMode !== undefined ? { executionMode } : {}),
1844
1877
  activate: async () => {
1845
1878
  if (activeTools.has(name))
1846
- return;
1879
+ return undefined;
1847
1880
  activeTools.add(name);
1848
- await rematerialize(activeTools);
1881
+ try {
1882
+ await rematerialize(activeTools);
1883
+ }
1884
+ catch (e) {
1885
+ activeTools.delete(name);
1886
+ throw e;
1887
+ }
1888
+ return listingRideRef.current?.([name]);
1849
1889
  },
1850
1890
  };
1851
1891
  };
@@ -1918,6 +1958,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1918
1958
  rematerialize,
1919
1959
  listingRide: (newly) => listingRideRef.current?.(newly),
1920
1960
  mountedNames: () => new Set(buildToolList(activeTools).map((t) => t.name).filter((n) => !registry.has(n) || activeTools.has(n))),
1961
+ directCallEnabled: spec.deferSelfResolve !== false,
1921
1962
  });
1922
1963
  harnessTools = buildToolList(activeTools);
1923
1964
  }
@@ -3341,7 +3382,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3341
3382
  : undefined;
3342
3383
  overheadState.promptChars = systemPrompt.length;
3343
3384
  const preparedHolder = {};
3344
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, reviewRequestRef, suspendLoopRef, suspendForResource, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
3385
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, reviewRequestRef, suspendLoopRef, suspendForResource, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
3345
3386
  const prepared = buildPrepared();
3346
3387
  preparedHolder.current = prepared;
3347
3388
  return prepared;
@@ -688,13 +688,6 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
688
688
  queue.push({ type: "compaction_outcome", outcome, trigger: passTrigger, ...ident() });
689
689
  }
690
690
  }
691
- if (comp.compacted && !forceManual) {
692
- if (recordCompactionAndCheckRapidRefill(rapidRefill, stats.turns)) {
693
- compactionBreaker.failures = MAX_CONSECUTIVE_COMPACTION_FAILURES;
694
- runnerHooks.onError?.(new Error("compaction.rapid_refill: the context refilled within <3 turns of compaction 3 times in a row — compaction disabled for the rest of this task (thrash spiral; the transcript is dominated by incompressible content)"), { phase: "compaction", sessionId: prepared.sessionId });
695
- queue.push({ type: "compaction_outcome", outcome: "disabled", trigger: passTrigger, reason: "rapid_refill: compaction disabled for the rest of this task", ...ident() });
696
- }
697
- }
698
691
  runnerHooks.recordCompactionReuse(prepared, comp);
699
692
  if (comp.compacted) {
700
693
  queue.push({
@@ -713,6 +706,11 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
713
706
  ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
714
707
  ...ident(),
715
708
  });
709
+ if (!forceManual && recordCompactionAndCheckRapidRefill(rapidRefill, stats.turns)) {
710
+ compactionBreaker.failures = MAX_CONSECUTIVE_COMPACTION_FAILURES;
711
+ queue.push({ type: "compaction_outcome", outcome: "disabled", trigger: passTrigger, reason: "rapid_refill: compaction disabled for the rest of this task", ...ident() });
712
+ runnerHooks.onError?.(new Error("compaction.rapid_refill: the context refilled within <3 turns of compaction 3 times in a row — compaction disabled for the rest of this task (thrash spiral; the transcript is dominated by incompressible content)"), { phase: "compaction", sessionId: prepared.sessionId });
713
+ }
716
714
  if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
717
715
  const pd = comp.phaseDurations;
718
716
  const pdDur = comp.durationMs;
@@ -2074,6 +2072,9 @@ export class Runner {
2074
2072
  phase: s.phase,
2075
2073
  ...(s.detail !== undefined ? { detail: s.detail } : {}),
2076
2074
  ...(s.retryInSec !== undefined ? { retryInSec: s.retryInSec } : {}),
2075
+ ...(s.retryInMs !== undefined ? { retryInMs: s.retryInMs } : {}),
2076
+ ...(s.attempt !== undefined ? { attempt: s.attempt } : {}),
2077
+ ...(s.maxRetries !== undefined ? { maxRetries: s.maxRetries } : {}),
2077
2078
  ...ident(),
2078
2079
  });
2079
2080
  };
@@ -2675,6 +2676,7 @@ export class Runner {
2675
2676
  minTokens: rs.counters.compactionFloor,
2676
2677
  brain: compactionBrain,
2677
2678
  windowSafety: windowSafetyOptions(prepared.model),
2679
+ onCompactionFailed: (reason) => queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "auto", reason, ...ident() }),
2678
2680
  });
2679
2681
  try {
2680
2682
  if (comp?.compacted) {
@@ -2762,6 +2764,7 @@ export class Runner {
2762
2764
  threw,
2763
2765
  model: prepared.model.id,
2764
2766
  unpricedSpend: rs.telemetry.unpricedSpend,
2767
+ rewindNotes: prepared.rewindNotes,
2765
2768
  abortedForTimeout: timeout.fired,
2766
2769
  abortedForTurns: rs.limits.turnsExceeded,
2767
2770
  abortedLive,
@@ -3043,10 +3046,19 @@ export class Runner {
3043
3046
  if (!leafId)
3044
3047
  return;
3045
3048
  const root = prepared.taskRootPath;
3046
- const refusedAt = this.snapshotTooLargeRoots.get(root);
3047
- if (refusedAt !== undefined) {
3048
- if (Date.now() - refusedAt < SNAPSHOT_TOO_LARGE_TTL_MS)
3049
+ const refusal = this.snapshotTooLargeRoots.get(root);
3050
+ if (refusal !== undefined) {
3051
+ if (Date.now() - refusal.refusedAt < SNAPSHOT_TOO_LARGE_TTL_MS) {
3052
+ if (!refusal.skipAnnounced) {
3053
+ refusal.skipAnnounced = true;
3054
+ try {
3055
+ this.deps.onError?.(new Error(`rewind-files snapshot skipped for this working-tree root: a too_large refusal is still inside its ${SNAPSHOT_TOO_LARGE_TTL_MS / 60_000}-minute cooldown (re-probed at ${new Date(refusal.refusedAt + SNAPSHOT_TOO_LARGE_TTL_MS).toISOString()}) — turns completed during the cooldown capture no snapshot and cannot be rewound to. Announced once per cooldown window.`), { phase: "rewind", sessionId: prepared.sessionId, classification: "too_large" });
3056
+ }
3057
+ catch {
3058
+ }
3059
+ }
3049
3060
  return;
3061
+ }
3050
3062
  this.snapshotTooLargeRoots.delete(root);
3051
3063
  }
3052
3064
  const ac = new AbortController();
@@ -3069,11 +3081,14 @@ export class Runner {
3069
3081
  }
3070
3082
  if (!r.ok) {
3071
3083
  const tooLarge = r.error.code === "too_large";
3084
+ const refusedAt = Date.now();
3072
3085
  if (tooLarge)
3073
- this.snapshotTooLargeRoots.set(root, Date.now());
3086
+ this.snapshotTooLargeRoots.set(root, { refusedAt, skipAnnounced: false });
3074
3087
  try {
3075
3088
  this.deps.onError?.(new Error(`rewind-files snapshot failed (${r.error.code}): ${r.error.message}` +
3076
- (tooLarge ? " — skipping further snapshots for this root (this process); raise the store's snapshotBounds or shrink the tree to re-enable rewind" : "")), { phase: "rewind", sessionId: prepared.sessionId, ...(tooLarge ? { classification: "too_large" } : {}) });
3089
+ (tooLarge
3090
+ ? ` — skipping further snapshots for this root (this process) during a ${SNAPSHOT_TOO_LARGE_TTL_MS / 60_000}-minute cooldown, until ${new Date(refusedAt + SNAPSHOT_TOO_LARGE_TTL_MS).toISOString()}, when the tree is re-probed automatically. Raise the store's snapshotBounds to accept a tree this size; shrinking the tree also re-enables rewind, but only at that re-probe — not immediately`
3091
+ : "")), { phase: "rewind", sessionId: prepared.sessionId, ...(tooLarge ? { classification: "too_large" } : {}) });
3077
3092
  }
3078
3093
  catch {
3079
3094
  }
@@ -3516,6 +3531,17 @@ export class Runner {
3516
3531
  comp = finishComp;
3517
3532
  }
3518
3533
  catch (err) {
3534
+ const failMsg = String(err instanceof Error ? err.message : err);
3535
+ const failReason = failMsg.length > 512 ? `${failMsg.slice(0, 512)}…` : failMsg;
3536
+ emitTrace(spec.tracer ?? this.deps.tracer, () => ({
3537
+ kind: "compaction.failed",
3538
+ version: 1,
3539
+ taskId: spec.taskId ?? prepared.sessionId,
3540
+ trigger: "auto",
3541
+ reason: failReason,
3542
+ ts: Date.now(),
3543
+ }));
3544
+ opts?.onCompactionFailed?.(failReason);
3519
3545
  this.deps.onError?.(err, { phase: "compaction", sessionId: prepared.sessionId });
3520
3546
  }
3521
3547
  }
@@ -1,5 +1,5 @@
1
1
  import { type TSchema } from "typebox";
2
- import type { AgentMessage, AgentTool, AgentToolResult } from "../../internal/harness-types.js";
2
+ import type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ToolExecutionMode } from "../../internal/harness-types.js";
3
3
  import type { Model } from "../../internal/llm.js";
4
4
  import type { ToolSpec } from "../types.js";
5
5
  export declare const TOOL_SEARCH_NAME = "ToolSearch";
@@ -29,9 +29,13 @@ export declare function buildDeferredRegistry(deferred: ReadonlySet<string>, too
29
29
  description: string;
30
30
  }>): Map<string, DeferredToolInfo>;
31
31
  export interface PlaceholderDirectCall {
32
+ resolveReal: () => PlaceholderDirectTarget | undefined;
33
+ executionMode?: ToolExecutionMode;
34
+ activate: () => Promise<string | undefined>;
35
+ }
36
+ export interface PlaceholderDirectTarget {
32
37
  parameters: TSchema;
33
- invoke: (toolCallId: string, params: unknown, signal?: AbortSignal) => Promise<AgentToolResult<unknown>>;
34
- activate: () => Promise<void>;
38
+ invoke: (toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<unknown>) => Promise<AgentToolResult<unknown>>;
35
39
  }
36
40
  export declare function createPlaceholderTool(info: DeferredToolInfo, direct?: PlaceholderDirectCall): AgentTool;
37
41
  export declare function scoreToolMatch(query: string, info: DeferredToolInfo): number;
@@ -52,4 +56,5 @@ export declare function createToolSearchTool(opts: {
52
56
  rematerialize: (active: ReadonlySet<string>) => Promise<void>;
53
57
  listingRide?: (newlyActivated: readonly string[]) => string | undefined;
54
58
  mountedNames?: () => ReadonlySet<string>;
59
+ directCallEnabled?: boolean;
55
60
  }): AgentTool;
@@ -43,7 +43,8 @@ export function classifyDeferred(opts) {
43
43
  deferred.add(name);
44
44
  if (opts.deferMode === "auto") {
45
45
  const candidates = opts.fullTools.filter((t) => !deferred.has(t.name) && !pinned.has(t.name));
46
- const total = candidates.reduce((n, t) => n + inlinedChars(t), 0);
46
+ const inlineFace = opts.fullTools.filter((t) => !deferred.has(t.name));
47
+ const total = inlineFace.reduce((n, t) => n + inlinedChars(t), 0);
47
48
  const window = (opts.model.contextTokens ?? opts.model.contextWindow ?? 0) * CHARS_PER_TOKEN;
48
49
  if (window > 0 && total > DEFER_AUTO_FRACTION * window) {
49
50
  for (const t of candidates)
@@ -70,12 +71,19 @@ export function createPlaceholderTool(info, direct) {
70
71
  return {
71
72
  name: info.name,
72
73
  label: info.name,
73
- description: `${info.hint} — deferred: call ${TOOL_SEARCH_NAME}({"query":"select:${sn}"}) to load its parameters before use.`,
74
+ description: `${info.hint} — deferred: its parameters are not listed here. Call ` +
75
+ `${TOOL_SEARCH_NAME}({"query":"select:${sn}"}) to load them; a call that already matches this ` +
76
+ `tool's real parameters runs directly and activates it.`,
74
77
  parameters: EMPTY_PARAMS,
75
- execute: async (toolCallId, params, signal) => {
76
- if (Value.Check(direct.parameters, params)) {
77
- await direct.activate();
78
- return direct.invoke(toolCallId, params, signal);
78
+ ...(direct.executionMode !== undefined ? { executionMode: direct.executionMode } : {}),
79
+ execute: async (toolCallId, params, signal, onUpdate) => {
80
+ const real = direct.resolveReal();
81
+ if (real !== undefined && Value.Check(real.parameters, params)) {
82
+ const ride = await direct.activate();
83
+ const result = await real.invoke(toolCallId, params, signal, onUpdate);
84
+ if (ride === undefined || ride === "")
85
+ return result;
86
+ return { ...result, content: [...result.content, { type: "text", text: ride }] };
79
87
  }
80
88
  return teachingRejection();
81
89
  },
@@ -163,28 +171,49 @@ export function resolveToolSearch(args, registry) {
163
171
  }
164
172
  export function extractDiscoveredToolNames(messages, registry) {
165
173
  const names = new Set();
174
+ const pendingDirect = new Map();
166
175
  for (const m of messages) {
176
+ if (m.role === "toolResult") {
177
+ const name = pendingDirect.get(m.toolCallId);
178
+ if (name !== undefined && m.isError !== true)
179
+ names.add(name);
180
+ continue;
181
+ }
167
182
  if (m.role !== "assistant")
168
183
  continue;
169
184
  for (const part of m.content) {
170
- if (part.type === "toolCall" && part.name === TOOL_SEARCH_NAME) {
185
+ if (part.type !== "toolCall")
186
+ continue;
187
+ if (part.name === TOOL_SEARCH_NAME) {
171
188
  for (const n of resolveToolSearch(part.arguments, registry)) {
172
189
  names.add(n);
173
190
  }
174
191
  }
192
+ else if (registry.has(part.name)) {
193
+ pendingDirect.set(part.id, part.name);
194
+ }
175
195
  }
176
196
  }
177
197
  return [...names];
178
198
  }
179
199
  export function createToolSearchTool(opts) {
180
200
  const { registry, active, rematerialize, listingRide, mountedNames } = opts;
201
+ const directCallEnabled = opts.directCallEnabled !== false;
202
+ const activationPosture = directCallEnabled
203
+ ? "Most tools start as name-only placeholders to keep requests small; activating one here loads its full " +
204
+ "parameter schema. Until you have that schema you cannot reliably form a call, so activate a tool rather " +
205
+ "than guessing its arguments — a call that does match the real schema executes and activates the tool. " +
206
+ "When any instruction, reminder, or another tool's description names a deferred tool, activate it here " +
207
+ 'with query "select:<name>". '
208
+ : "Most tools start as name-only placeholders to keep requests small; to USE one you must activate it here " +
209
+ "first. When any instruction, reminder, or another tool's description names a deferred tool, activate it " +
210
+ 'with query "select:<name>" before calling it. ';
181
211
  let activationChain = Promise.resolve();
182
212
  return defineTool({
183
213
  name: TOOL_SEARCH_NAME,
184
214
  contract: { contractId: "core.tool_search@1", implementationRevision: "1" },
185
- description: "Discover and activate deferred tools. Most tools start as name-only placeholders to keep requests " +
186
- "small; to USE one you must activate it here first. When any instruction, reminder, or another " +
187
- 'tool\'s description names a deferred tool, activate it with query "select:<name>" before calling it. ' +
215
+ description: "Discover and activate deferred tools. " +
216
+ activationPosture +
188
217
  "Query forms: " +
189
218
  '"select:ToolA,ToolB" — activate these exact tools by name; ' +
190
219
  '"notebook jupyter" — keyword search, up to max_results best matches; ' +
@@ -1,5 +1,5 @@
1
1
  import type { SkillSpec } from "./types.js";
2
- export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "description_too_long" | "allowed_tool_not_mounted" | "attachment_skipped" | "read_failed";
2
+ export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "description_too_long" | "allowed_tool_not_mounted" | "disallowed_tools_unenforced" | "attachment_skipped" | "read_failed";
3
3
  export interface SkillsDirectoryWarning {
4
4
  code: SkillsDirectoryWarningCode;
5
5
  skill: string;