@sema-agent/core 2.4.0 → 2.5.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 (34) hide show
  1. package/dist/agents/subagent.js +17 -14
  2. package/dist/core/auto-compaction.d.ts +2 -0
  3. package/dist/core/auto-compaction.js +2 -1
  4. package/dist/core/mcp.js +4 -1
  5. package/dist/core/runner/prepare-task.d.ts +5 -0
  6. package/dist/core/runner/prepare-task.js +21 -1
  7. package/dist/core/runner/runtask.js +39 -9
  8. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  9. package/dist/core/runner/tool-disclosure.js +16 -5
  10. package/dist/core/session-reconcile.d.ts +1 -0
  11. package/dist/core/session-reconcile.js +40 -20
  12. package/dist/core/task-registry-agent.d.ts +2 -0
  13. package/dist/core/task-registry-agent.js +11 -1
  14. package/dist/core/task-registry-shared.d.ts +1 -0
  15. package/dist/core/task-registry.d.ts +2 -0
  16. package/dist/core/task-registry.js +11 -0
  17. package/dist/core/types.d.ts +10 -3
  18. package/dist/engine/compaction/compaction.d.ts +5 -0
  19. package/dist/engine/compaction/compaction.js +68 -2
  20. package/dist/engine/compaction/utils.d.ts +6 -0
  21. package/dist/engine/compaction/utils.js +53 -3
  22. package/dist/engine/harness/messages.d.ts +1 -1
  23. package/dist/engine/harness/messages.js +11 -3
  24. package/dist/engine/loop/types.d.ts +2 -0
  25. package/dist/engine/session/import-validate.js +30 -1
  26. package/dist/engine/session/session.js +7 -5
  27. package/dist/index.d.ts +7 -0
  28. package/dist/index.js +7 -0
  29. package/dist/internal/harness.d.ts +1 -1
  30. package/dist/internal/harness.js +1 -1
  31. package/dist/orchestration/workflow-types.d.ts +1 -0
  32. package/dist/orchestration/workflow.js +10 -1
  33. package/dist/tools/fs/fs-bash.js +11 -5
  34. package/package.json +1 -1
@@ -706,6 +706,8 @@ export function settleBackgroundAgentLane(core, id, outcome) {
706
706
  handle.errorCode = outcome.errorCode;
707
707
  if (outcome.retryable !== undefined)
708
708
  handle.errorRetryable = outcome.retryable;
709
+ if (outcome.errorKind !== undefined)
710
+ handle.errorKind = outcome.errorKind;
709
711
  }
710
712
  handle.updatedAt = Date.now();
711
713
  if (outcome.seq !== undefined)
@@ -844,6 +846,9 @@ export function reviveBackgroundAgentLane(core, id, access, abort) {
844
846
  handle.resultFull = undefined;
845
847
  handle.spillRef = undefined;
846
848
  handle.error = undefined;
849
+ handle.errorCode = undefined;
850
+ handle.errorRetryable = undefined;
851
+ handle.errorKind = undefined;
847
852
  handle.resultIsPartial = undefined;
848
853
  handle.stopSource = undefined;
849
854
  handle.completionId = undefined;
@@ -1030,6 +1035,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1030
1035
  ...(row.resultIsPartial ? { partial_result: true } : {}),
1031
1036
  ...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
1032
1037
  },
1038
+ ...(row.status === "failed" ? { isError: true } : {}),
1033
1039
  };
1034
1040
  }
1035
1041
  export async function spillClippedAgentResult(handle, full, clipped, store, sessionId) {
@@ -1065,6 +1071,9 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
1065
1071
  }
1066
1072
  const fullResult = handle.result ? (handle.resultFull ?? handle.result) : undefined;
1067
1073
  const resultText = fullResult !== undefined ? await spillClippedAgentResult(handle, fullResult, clipTaskOutput(fullResult, handle.outputFile), store, sessionId) : undefined;
1074
+ const kindClause = handle.status === "failed" && handle.errorKind !== undefined && handle.errorRetryable !== undefined
1075
+ ? ` (error_kind: ${handle.errorKind}, retryable: ${handle.errorRetryable})`
1076
+ : "";
1068
1077
  const body = running
1069
1078
  ? oneShot === true
1070
1079
  ? `status: running
@@ -1072,7 +1081,7 @@ This is a ONE-SHOT submission — there is no later turn for a background notifi
1072
1081
  : `status: running
1073
1082
  The agent is still working — you will be notified when it completes.`
1074
1083
  : `status: ${handle.status}
1075
- ${handle.error ? `error: ${handle.error}
1084
+ ${handle.error ? `error: ${handle.error}${kindClause}
1076
1085
  ` : ""}${handle.result ? `--- result${handle.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
1077
1086
  ${resultText}` : "(no result text)"}`;
1078
1087
  return {
@@ -1092,6 +1101,7 @@ ${resultText}` : "(no result text)"}`;
1092
1101
  ...(handle.resultIsPartial ? { partial_result: true } : {}),
1093
1102
  ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1094
1103
  },
1104
+ ...(handle.status === "failed" ? { isError: true } : {}),
1095
1105
  };
1096
1106
  }
1097
1107
  export async function stopBackgroundAgentLane(core, handle) {
@@ -139,6 +139,7 @@ export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
139
139
  error?: string;
140
140
  errorCode?: string;
141
141
  errorRetryable?: boolean;
142
+ errorKind?: string;
142
143
  resultIsPartial?: boolean;
143
144
  stopSource?: StopSource;
144
145
  stoppedBy?: StopSource;
@@ -135,6 +135,7 @@ export declare class TaskRegistry {
135
135
  error?: string;
136
136
  errorCode?: string;
137
137
  retryable?: boolean;
138
+ errorKind?: string;
138
139
  stoppedBy?: StopSource;
139
140
  seq?: number;
140
141
  }): "completed" | "failed" | "killed" | undefined;
@@ -170,6 +171,7 @@ export declare class TaskRegistry {
170
171
  error?: string;
171
172
  errorCode?: string;
172
173
  retryable?: boolean;
174
+ errorKind?: string;
173
175
  }): "completed" | "failed" | "killed" | undefined;
174
176
  unmarkRetainedContinuation(id: string): void;
175
177
  attachAgentNotify(id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void;
@@ -368,6 +368,17 @@ export class TaskRegistry {
368
368
  async settleKilledForOwner(access, opts) {
369
369
  const source = opts?.source ?? "parent";
370
370
  const clause = (by) => by === "user" ? "stopped by user" : by === "parent" ? "its parent run ended" : `stopped by ${by}`;
371
+ if (source === "user") {
372
+ for (const handle of this.handles.values()) {
373
+ if (handle.type !== "background_agent" || handle.status !== "running")
374
+ continue;
375
+ if (!canAccess(handle, { owner: access.owner, scope: access.scope }))
376
+ continue;
377
+ if (opts?.skipSessionScoped && handle.sessionScoped)
378
+ continue;
379
+ this.markStopSource(handle.id, source);
380
+ }
381
+ }
371
382
  let settled = 0;
372
383
  for (const handle of this.handles.values()) {
373
384
  if (handle.type !== "background_bash" && handle.type !== "monitor")
@@ -47,6 +47,7 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
47
47
  offload?: boolean;
48
48
  offloadThresholdChars?: number;
49
49
  defer?: boolean;
50
+ alwaysLoad?: boolean;
50
51
  contract?: {
51
52
  contractId: string;
52
53
  implementationRevision: string;
@@ -256,6 +257,7 @@ export interface TaskSpec {
256
257
  tools?: ToolSpec[];
257
258
  excludeTools?: string[];
258
259
  deferTools?: string[];
260
+ alwaysLoadTools?: string[];
259
261
  promptProfile?: "simple" | "classic";
260
262
  agents?: AgentDefinition[];
261
263
  toolPolicy?: import("./tool-policy.js").ToolPolicy;
@@ -514,9 +516,9 @@ export type TaskEvent = ({
514
516
  };
515
517
  usageMissing?: true;
516
518
  stopReason?: string;
517
- } & TaskEventIdentity) | {
519
+ } & TaskEventIdentity) | ({
518
520
  type: "compacted";
519
- trigger: "auto" | "manual";
521
+ trigger: "auto" | "manual" | "forced";
520
522
  tokensBefore: number;
521
523
  tokensAfter?: number;
522
524
  triggerTokensBefore?: number;
@@ -534,7 +536,12 @@ export type TaskEvent = ({
534
536
  clampedRatio?: number;
535
537
  clampReason?: "budget" | "walltime" | "tolerance";
536
538
  phaseDurations?: import("./auto-compaction.js").CompactionPhaseDurations;
537
- } | ({
539
+ } & TaskEventIdentity) | ({
540
+ type: "compaction_outcome";
541
+ outcome: Exclude<CompactOutcome, "compacted"> | "suppressed";
542
+ trigger: "auto" | "manual" | "forced";
543
+ reason?: string;
544
+ } & TaskEventIdentity) | ({
538
545
  type: "steering_injected";
539
546
  source: "deadline_nudge" | "finalize" | "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "budget_usd" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
540
547
  preview: string;
@@ -8,6 +8,9 @@ export interface CompactionDetails {
8
8
  modifiedFiles: string[];
9
9
  modifiedFilesByRecency?: string[];
10
10
  invokedSkills?: InvokedSkillRetention[];
11
+ elidedMessages?: number;
12
+ persistedOutputRefs?: string[];
13
+ activeTools?: string[];
11
14
  }
12
15
  export interface CompactionResult<T = unknown> {
13
16
  summary: string;
@@ -68,6 +71,8 @@ export interface CompactionPreparation {
68
71
  previousSummary?: string;
69
72
  fileOps: FileOperations;
70
73
  invokedSkills: InvokedSkillRetention[];
74
+ persistedOutputRefs?: string[];
75
+ elidedMessages?: number;
71
76
  settings: CompactionSettings;
72
77
  }
73
78
  export declare function prepareCompaction(pathEntries: SessionTreeEntry[], settings: CompactionSettings, charsPerToken?: number, windowTokens?: number): Result<CompactionPreparation | undefined, CompactionError>;
@@ -2,7 +2,7 @@ import { resolveAgentCoreCompleteFn, } from "../loop/runtime-deps.js";
2
2
  import { asAgentMessage, convertToLlm, createCompactionSummaryMessage, createCustomMessage, } from "../harness/messages.js";
3
3
  import { buildSessionContext } from "../session/session.js";
4
4
  import { CompactionError, err, ok, } from "../harness/types.js";
5
- import { budgetInvokedSkillsRetention, computeFileLists, createFileOps, extractFileOpsFromMessage, extractInvokedSkills, formatFileOperations, readRetainedInvokedSkills, replaceInvokedSkillBodiesForSummary, serializeConversation, stripFileOperationsFooter, } from "./utils.js";
5
+ import { budgetInvokedSkillsRetention, computeFileLists, createFileOps, extractFileOpsFromMessage, extractInvokedSkills, extractPersistedOutputRefs, formatFileOperations, formatPersistedOutputRefs, PERSISTED_OUTPUT_REFS_MAX_ENTRIES, readElidedMessages, readPersistedOutputRefs, readRetainedInvokedSkills, replaceInvokedSkillBodiesForSummary, serializeConversation, stripFileOperationsFooter, } from "./utils.js";
6
6
  function safeJsonStringify(value) {
7
7
  try {
8
8
  return JSON.stringify(value) ?? "undefined";
@@ -256,6 +256,51 @@ export function findTurnStartIndex(entries, entryIndex, startIndex) {
256
256
  }
257
257
  return -1;
258
258
  }
259
+ function enforceToolPairContainment(entries, startIndex, endIndex, cutIndex) {
260
+ const callSites = new Map();
261
+ for (let i = startIndex; i < endIndex; i++) {
262
+ const entry = entries[i];
263
+ if (entry.type !== "message")
264
+ continue;
265
+ const msg = entry.message;
266
+ if (msg.role !== "assistant" || !Array.isArray(msg.content))
267
+ continue;
268
+ for (const block of msg.content) {
269
+ if (block.type === "toolCall" && typeof block.id === "string") {
270
+ const at = callSites.get(block.id);
271
+ if (at === undefined)
272
+ callSites.set(block.id, [i]);
273
+ else
274
+ at.push(i);
275
+ }
276
+ }
277
+ }
278
+ if (callSites.size === 0)
279
+ return cutIndex;
280
+ for (let i = cutIndex; i < endIndex; i++) {
281
+ const entry = entries[i];
282
+ if (entry.type !== "message")
283
+ continue;
284
+ const msg = entry.message;
285
+ if (msg.role !== "toolResult")
286
+ continue;
287
+ const sites = callSites.get(msg.toolCallId);
288
+ if (sites === undefined)
289
+ continue;
290
+ let site = -1;
291
+ for (const s of sites) {
292
+ if (s < i)
293
+ site = s;
294
+ else
295
+ break;
296
+ }
297
+ if (site === -1 || site >= cutIndex)
298
+ continue;
299
+ cutIndex = site;
300
+ i = site;
301
+ }
302
+ return cutIndex;
303
+ }
259
304
  export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens, charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
260
305
  const cutPoints = findValidCutPoints(entries, startIndex, endIndex);
261
306
  if (cutPoints.length === 0) {
@@ -290,6 +335,7 @@ export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens, ch
290
335
  break;
291
336
  }
292
337
  }
338
+ cutIndex = enforceToolPairContainment(entries, startIndex, endIndex, cutIndex);
293
339
  const cutEntry = entries[cutIndex];
294
340
  const isUserMessage = cutEntry.type === "message" && cutEntry.message.role === "user";
295
341
  const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
@@ -721,6 +767,21 @@ export function prepareCompaction(pathEntries, settings, charsPerToken = DEFAULT
721
767
  ? Math.max(0, windowTokens - settings.reserveTokens - settings.keepRecentTokens) * charsPerToken
722
768
  : undefined;
723
769
  const invokedSkills = budgetInvokedSkillsRetention(extractInvokedSkills([...messagesToSummarize, ...turnPrefixMessages], prevRetainedSkills), retentionBudgetChars);
770
+ const refSet = new Set();
771
+ if (prevCompactionIndex >= 0 && !pathEntries[prevCompactionIndex].fromHook) {
772
+ for (const r of readPersistedOutputRefs(pathEntries[prevCompactionIndex].details)) {
773
+ refSet.add(r);
774
+ }
775
+ }
776
+ extractPersistedOutputRefs(messagesToSummarize, refSet);
777
+ if (cutPoint.isSplitTurn) {
778
+ extractPersistedOutputRefs(turnPrefixMessages, refSet);
779
+ }
780
+ const persistedOutputRefs = [...refSet].slice(-PERSISTED_OUTPUT_REFS_MAX_ENTRIES);
781
+ const prevElided = prevCompactionIndex >= 0 && !pathEntries[prevCompactionIndex].fromHook
782
+ ? readElidedMessages(pathEntries[prevCompactionIndex].details)
783
+ : undefined;
784
+ const elidedMessages = (prevElided ?? 0) + messagesToSummarize.length + turnPrefixMessages.length;
724
785
  const [summarizeReady, prefixReady] = replaceInvokedSkillBodiesForSummary([messagesToSummarize, turnPrefixMessages], new Set(invokedSkills.map((s) => s.name)));
725
786
  return ok({
726
787
  firstKeptEntryId,
@@ -731,6 +792,8 @@ export function prepareCompaction(pathEntries, settings, charsPerToken = DEFAULT
731
792
  previousSummary,
732
793
  fileOps,
733
794
  invokedSkills,
795
+ persistedOutputRefs,
796
+ elidedMessages,
734
797
  settings,
735
798
  });
736
799
  }
@@ -750,7 +813,7 @@ Summarize the prefix to provide context for the retained suffix:
750
813
  Be concise. Focus on what's needed to understand the kept suffix.`;
751
814
  export { computeFileLists, serializeConversation } from "./utils.js";
752
815
  export async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry) {
753
- const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, invokedSkills, settings, } = preparation;
816
+ const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, invokedSkills, persistedOutputRefs, elidedMessages, settings, } = preparation;
754
817
  if (!firstKeptEntryId) {
755
818
  return err(new CompactionError("invalid_session", "First kept entry has no UUID - session may need migration"));
756
819
  }
@@ -784,6 +847,7 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
784
847
  }
785
848
  const { readFiles, modifiedFiles, modifiedFilesByRecency } = computeFileLists(fileOps);
786
849
  summary += formatFileOperations(readFiles, modifiedFiles);
850
+ summary += formatPersistedOutputRefs(persistedOutputRefs ?? []);
787
851
  return ok({
788
852
  summary,
789
853
  firstKeptEntryId,
@@ -793,6 +857,8 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
793
857
  modifiedFiles,
794
858
  modifiedFilesByRecency,
795
859
  ...(invokedSkills !== undefined && invokedSkills.length > 0 ? { invokedSkills } : {}),
860
+ ...(persistedOutputRefs !== undefined && persistedOutputRefs.length > 0 ? { persistedOutputRefs } : {}),
861
+ ...(elidedMessages !== undefined && elidedMessages > 0 ? { elidedMessages } : {}),
796
862
  },
797
863
  });
798
864
  }
@@ -21,6 +21,12 @@ export declare function budgetInvokedSkillsRetention(newestFirst: InvokedSkillRe
21
21
  export declare function replaceInvokedSkillBodiesForSummary(groups: AgentMessage[][], retainedNames: ReadonlySet<string>): AgentMessage[][];
22
22
  export declare function readRetainedInvokedSkills(details: unknown): InvokedSkillRetention[];
23
23
  export declare function renderInvokedSkillsRetention(skills: InvokedSkillRetention[]): string;
24
+ export declare const PERSISTED_OUTPUT_REFS_MAX_ENTRIES = 50;
25
+ export declare function extractPersistedOutputRefs(messages: AgentMessage[], into: Set<string>): void;
26
+ export declare function readElidedMessages(details: unknown): number | undefined;
27
+ export declare function readCompactionActiveTools(details: unknown): string[];
28
+ export declare function readPersistedOutputRefs(details: unknown): string[];
29
+ export declare function formatPersistedOutputRefs(refs: string[]): string;
24
30
  export declare function computeFileLists(fileOps: FileOperations): {
25
31
  readFiles: string[];
26
32
  modifiedFiles: string[];
@@ -182,6 +182,52 @@ export function renderInvokedSkillsRetention(skills) {
182
182
  `If a skill's content was truncated, invoke the Skill tool again for the full text.\n\n` +
183
183
  `${sections.join("\n\n")}\n</invoked-skills>`);
184
184
  }
185
+ const PERSISTED_OUTPUT_REF_RE = /<persisted-output ref="([^"]+)"/g;
186
+ const OFFLOAD_TOOL_NAME_LITERAL = "ReadToolResult";
187
+ export const PERSISTED_OUTPUT_REFS_MAX_ENTRIES = 50;
188
+ export function extractPersistedOutputRefs(messages, into) {
189
+ for (const msg of messages) {
190
+ if (msg.role !== "toolResult" || !Array.isArray(msg.content))
191
+ continue;
192
+ for (const block of msg.content) {
193
+ if (block.type !== "text" || typeof block.text !== "string")
194
+ continue;
195
+ for (const m of block.text.matchAll(PERSISTED_OUTPUT_REF_RE)) {
196
+ into.add(m[1]);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ export function readElidedMessages(details) {
202
+ if (details === null || typeof details !== "object")
203
+ return undefined;
204
+ const raw = details.elidedMessages;
205
+ return typeof raw === "number" && Number.isSafeInteger(raw) && raw >= 0 ? raw : undefined;
206
+ }
207
+ export function readCompactionActiveTools(details) {
208
+ if (details === null || typeof details !== "object")
209
+ return [];
210
+ const raw = details.activeTools;
211
+ if (!Array.isArray(raw))
212
+ return [];
213
+ return raw.filter((n) => typeof n === "string" && n.length > 0);
214
+ }
215
+ export function readPersistedOutputRefs(details) {
216
+ if (details === null || typeof details !== "object")
217
+ return [];
218
+ const raw = details.persistedOutputRefs;
219
+ if (!Array.isArray(raw))
220
+ return [];
221
+ return raw.filter((r) => typeof r === "string" && r.length > 0);
222
+ }
223
+ export function formatPersistedOutputRefs(refs) {
224
+ if (refs.length === 0)
225
+ return "";
226
+ return (`\n\n<persisted-tool-outputs>\n` +
227
+ `Tool outputs elided by compaction were persisted in full and remain retrievable — call ` +
228
+ `${OFFLOAD_TOOL_NAME_LITERAL} with a ref to read one back:\n` +
229
+ `${capList(refs)}\n</persisted-tool-outputs>`);
230
+ }
185
231
  function touchModified(fileOps, path) {
186
232
  fileOps.modifiedOrder.delete(path);
187
233
  fileOps.modifiedOrder.add(path);
@@ -217,9 +263,13 @@ export function formatFileOperations(readFiles, modifiedFiles) {
217
263
  return `\n\n${sections.join("\n\n")}`;
218
264
  }
219
265
  export function stripFileOperationsFooter(summary) {
220
- return summary
221
- .replace(/\n\n<read-files>\n[\s\S]*?\n<\/read-files>(\n\n<modified-files>\n[\s\S]*?\n<\/modified-files>)?\s*$/, "")
222
- .replace(/\n\n<modified-files>\n[\s\S]*?\n<\/modified-files>\s*$/, "");
266
+ let out = summary;
267
+ for (;;) {
268
+ const next = out.replace(/\n\n<(read-files|modified-files|persisted-tool-outputs)>\n[\s\S]*?\n<\/\1>\s*$/, "");
269
+ if (next === out)
270
+ return out;
271
+ out = next;
272
+ }
223
273
  }
224
274
  const TOOL_RESULT_MAX_CHARS = 2000;
225
275
  function safeJsonStringify(value) {
@@ -6,7 +6,7 @@ export declare function asAgentMessage(message: HarnessMessage): AgentMessage;
6
6
  export declare const COMPACTION_SUMMARY_PREFIX = "This session is being continued from a previous conversation that ran out of context. The summary below covers the earlier portion of the conversation.\n\n<summary>\n";
7
7
  export declare const COMPACTION_SUMMARY_SUFFIX = "\n</summary>\n\nRecent messages are preserved verbatim below. Continue the conversation from where it left off without asking the user any further questions. Resume directly \u2014 do not acknowledge the summary, do not recap what was happening, do not preface with \"I'll continue\" or similar. Pick up the last task as if the break never happened.";
8
8
  export declare function bashExecutionToText(msg: BashExecutionMessage): string;
9
- export declare function createCompactionSummaryMessage(summary: string, tokensBefore: number, timestamp: string): CompactionSummaryMessage;
9
+ export declare function createCompactionSummaryMessage(summary: string, tokensBefore: number, timestamp: string, elidedMessages?: number): CompactionSummaryMessage;
10
10
  export declare function createCustomMessage(customType: string, content: string | (TextContent | ImageContent)[], display: boolean, details: unknown, timestamp: string): CustomMessage;
11
11
  export declare const NORMALIZED_CONTENT_PREFIX = "[invalid content block normalized to text]";
12
12
  export declare function normalizeLlmMessageContent<T extends Message>(message: T): T;
@@ -36,12 +36,13 @@ export function bashExecutionToText(msg) {
36
36
  }
37
37
  return text;
38
38
  }
39
- export function createCompactionSummaryMessage(summary, tokensBefore, timestamp) {
39
+ export function createCompactionSummaryMessage(summary, tokensBefore, timestamp, elidedMessages) {
40
40
  return {
41
41
  role: "compactionSummary",
42
42
  summary,
43
43
  tokensBefore,
44
44
  timestamp: requireSessionTimestampMs(timestamp, "compaction summary timestamp"),
45
+ ...(elidedMessages !== undefined ? { elidedMessages } : {}),
45
46
  };
46
47
  }
47
48
  export function createCustomMessage(customType, content, display, details, timestamp) {
@@ -128,17 +129,24 @@ export function convertToLlm(messages) {
128
129
  ...(message.provenance === "engine-note" ? { provenance: "engine-note" } : {}),
129
130
  });
130
131
  }
131
- case "compactionSummary":
132
+ case "compactionSummary": {
133
+ const n = message.elidedMessages;
134
+ const disclosure = n !== undefined && n > 0
135
+ ? `\n\n(For scale: this summary stands in for ${n} earlier message${n === 1 ? "" : "s"}${message.tokensBefore > 0
136
+ ? ` — approximately ${message.tokensBefore} tokens of context before compaction`
137
+ : ""}.)`
138
+ : "";
132
139
  return {
133
140
  role: "user",
134
141
  content: [
135
142
  {
136
143
  type: "text",
137
- text: COMPACTION_SUMMARY_PREFIX + message.summary + COMPACTION_SUMMARY_SUFFIX,
144
+ text: COMPACTION_SUMMARY_PREFIX + message.summary + COMPACTION_SUMMARY_SUFFIX + disclosure,
138
145
  },
139
146
  ],
140
147
  timestamp: normalizeCompactionSummaryTimestamp(message.timestamp),
141
148
  };
149
+ }
142
150
  case "user":
143
151
  case "assistant":
144
152
  case "toolResult":
@@ -129,6 +129,7 @@ export interface CompactionSummaryMessage {
129
129
  summary: string;
130
130
  tokensBefore: number;
131
131
  timestamp: number | string;
132
+ elidedMessages?: number;
132
133
  tokensAfter?: number;
133
134
  firstKeptEntryId?: string;
134
135
  details?: unknown;
@@ -161,6 +162,7 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = unk
161
162
  execute: (toolCallId: string, params: Static<TParameters>, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<TDetails>) => Promise<AgentToolResult<TDetails>>;
162
163
  executionMode?: ToolExecutionMode;
163
164
  mcpMaxResultSizeChars?: number;
165
+ mcpAlwaysLoad?: boolean;
164
166
  isConcurrencySafe?: (args: unknown) => boolean;
165
167
  }
166
168
  export interface AgentContext {
@@ -3,11 +3,14 @@ import { leafIdAfterEntry } from "./storage-base.js";
3
3
  import { parseSessionTimestampMs } from "./timestamps.js";
4
4
  import { flattenableUserText, normalizeEngineSegments } from "../../core/untrusted-text.js";
5
5
  import { normalizePromptEpoch } from "../../prompt-assembly/epoch.js";
6
- import { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, } from "../compaction/utils.js";
6
+ import { PERSISTED_OUTPUT_REFS_MAX_ENTRIES, SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, } from "../compaction/utils.js";
7
7
  const PATH_LIST_MAX_CHARS = 4096;
8
8
  const PATH_LIST_MAX_ENTRIES = 1000;
9
9
  const INVOKED_SKILLS_MAX_ENTRIES = 1000;
10
10
  const INVOKED_SKILL_NAME_MAX_CHARS = 1024;
11
+ const PERSISTED_REF_MAX_CHARS = 256;
12
+ const ACTIVE_TOOL_NAME_MAX_CHARS = 1024;
13
+ const ACTIVE_TOOLS_MAX_ENTRIES = 1000;
11
14
  export class StreamingImportValidator {
12
15
  seen = new Set();
13
16
  parentOf = new Map();
@@ -157,6 +160,32 @@ export class StreamingImportValidator {
157
160
  throw new SessionError("invalid_session", `compaction "${e.id}" carries an invokedSkills area of ${totalChars} chars (max ${SKILL_RETENTION_TOTAL_MAX_CHARS})`);
158
161
  }
159
162
  }
163
+ const elided = e.details?.elidedMessages;
164
+ if (elided !== undefined && !(typeof elided === "number" && Number.isSafeInteger(elided) && elided >= 0)) {
165
+ throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid elidedMessages count (non-negative integer)`);
166
+ }
167
+ const refs = e.details?.persistedOutputRefs;
168
+ if (refs !== undefined) {
169
+ if (!Array.isArray(refs) || refs.length > PERSISTED_OUTPUT_REFS_MAX_ENTRIES) {
170
+ throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid persistedOutputRefs list (array of at most ${PERSISTED_OUTPUT_REFS_MAX_ENTRIES} refs)`);
171
+ }
172
+ for (const r of refs) {
173
+ if (typeof r !== "string" || r.length === 0 || r.length > PERSISTED_REF_MAX_CHARS) {
174
+ throw new SessionError("invalid_session", `compaction "${e.id}" carries a persistedOutputRefs entry that is not a non-empty string of at most ${PERSISTED_REF_MAX_CHARS} chars`);
175
+ }
176
+ }
177
+ }
178
+ const activeToolsCarrier = e.details?.activeTools;
179
+ if (activeToolsCarrier !== undefined) {
180
+ if (!Array.isArray(activeToolsCarrier) || activeToolsCarrier.length > ACTIVE_TOOLS_MAX_ENTRIES) {
181
+ throw new SessionError("invalid_session", `compaction "${e.id}" carries a structurally invalid activeTools list (array of at most ${ACTIVE_TOOLS_MAX_ENTRIES} names)`);
182
+ }
183
+ for (const n of activeToolsCarrier) {
184
+ if (typeof n !== "string" || n.length === 0 || n.length > ACTIVE_TOOL_NAME_MAX_CHARS) {
185
+ throw new SessionError("invalid_session", `compaction "${e.id}" carries an activeTools entry that is not a non-empty string of at most ${ACTIVE_TOOL_NAME_MAX_CHARS} chars`);
186
+ }
187
+ }
188
+ }
160
189
  }
161
190
  this.parentOf.set(e.id, e.parentId);
162
191
  this.seen.add(e.id);
@@ -1,7 +1,7 @@
1
1
  import { asAgentMessage, createCompactionSummaryMessage, createCustomMessage, } from "../harness/messages.js";
2
- import { SessionError, normalizeAnnouncedListing, normalizeCompactionStateCarrier, normalizeWorkspaceState } from "../harness/types.js";
2
+ import { SessionError, isValidModelChange, normalizeAnnouncedListing, normalizeCompactionStateCarrier, normalizeWorkspaceState } from "../harness/types.js";
3
3
  import { normalizePromptEpoch } from "../../prompt-assembly/epoch.js";
4
- import { budgetInvokedSkillsRetention, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../compaction/utils.js";
4
+ import { budgetInvokedSkillsRetention, readElidedMessages, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../compaction/utils.js";
5
5
  const RETENTION_CLAMP_DEFAULT_CHARS_PER_TOKEN = 4;
6
6
  const RETENTION_CLAMP_WINDOW_FRACTION = 0.5;
7
7
  export function buildSessionContext(pathEntries, opts) {
@@ -25,7 +25,9 @@ export function buildSessionContext(pathEntries, opts) {
25
25
  model = { provider: entry.provider, modelId: entry.modelId };
26
26
  }
27
27
  else if (entry.type === "message" && entry.message.role === "assistant") {
28
- model = { provider: entry.message.provider, modelId: entry.message.model };
28
+ if (isValidModelChange({ provider: entry.message.provider, modelId: entry.message.model })) {
29
+ model = { provider: entry.message.provider, modelId: entry.message.model };
30
+ }
29
31
  }
30
32
  else if (entry.type === "compaction") {
31
33
  compaction = entry;
@@ -64,7 +66,7 @@ export function buildSessionContext(pathEntries, opts) {
64
66
  retainedSkills = budgetInvokedSkillsRetention(retainedSkills, budgetChars);
65
67
  }
66
68
  const retainedSkillsBlock = renderInvokedSkillsRetention(retainedSkills);
67
- messages.push(asAgentMessage(createCompactionSummaryMessage(compaction.summary + retainedSkillsBlock, compaction.tokensBefore, compaction.timestamp)));
69
+ messages.push(asAgentMessage(createCompactionSummaryMessage(compaction.summary + retainedSkillsBlock, compaction.tokensBefore, compaction.timestamp, readElidedMessages(compaction.details))));
68
70
  const compactionIdx = pathEntries.findIndex((e) => e.type === "compaction" && e.id === compaction.id);
69
71
  let foundFirstKept = false;
70
72
  for (let i = 0; i < compactionIdx; i++) {
@@ -242,7 +244,7 @@ export class StoredSession {
242
244
  const carried = await (async () => {
243
245
  try {
244
246
  const ctx = buildSessionContext(await this.getBranch());
245
- return { thinkingLevel: ctx.thinkingLevel, model: ctx.model };
247
+ return { thinkingLevel: ctx.thinkingLevel, ...(ctx.model !== null ? { model: ctx.model } : {}) };
246
248
  }
247
249
  catch {
248
250
  return undefined;
package/dist/index.d.ts CHANGED
@@ -151,6 +151,13 @@ export { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "./core/presen
151
151
  export { type WorkflowRunStore, type WorkflowRunSummary, summarizeWorkflowRun, isTerminalWorkflowStatus, WorkflowRunStoreError, InMemoryWorkflowRunStore, } from "./core/workflow-run-store.js";
152
152
  export { FileWorkflowRunStore, type FileWorkflowRunStoreOptions } from "./stores/file/workflow-run-store.js";
153
153
  export { workflowRunStoreContract } from "./core/workflow-run-store-contract.js";
154
+ export { CONTRACT_KIT_ENGINE_VERSION } from "./core/store-contracts/contract-kit-version.js";
155
+ export { type ContractAssertionRunner } from "./core/store-contracts/contract-harness.js";
156
+ export { checkpointStoreContract } from "./core/store-contracts/checkpoint-store-contract.js";
157
+ export { sessionRepoContract } from "./core/store-contracts/session-repo-contract.js";
158
+ export { toolResultStoreContract } from "./core/store-contracts/tool-result-store-contract.js";
159
+ export { fileSnapshotStoreContract } from "./core/store-contracts/file-snapshot-store-contract.js";
160
+ export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, } from "./core/store-contracts/mailbox-store-contract.js";
154
161
  export { type BackgroundAgentStore, type BackgroundAgentRecord, type BackgroundAgentRowSummary, type BackgroundAgentUsage, canAccessAgentRecord, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, } from "./core/background-agent-store.js";
155
162
  export { FileBackgroundAgentStore, type FileBackgroundAgentStoreOptions } from "./stores/file/background-agent-store.js";
156
163
  export { type WorkflowJournalStore, type WorkflowJournalEntry, InMemoryWorkflowJournalStore, callKeyOrdinal, JOURNAL_OVERSIZE_ERROR_CODE, } from "./core/workflow-journal-store.js";
package/dist/index.js CHANGED
@@ -138,6 +138,13 @@ export { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "./core/presen
138
138
  export { summarizeWorkflowRun, isTerminalWorkflowStatus, WorkflowRunStoreError, InMemoryWorkflowRunStore, } from "./core/workflow-run-store.js";
139
139
  export { FileWorkflowRunStore } from "./stores/file/workflow-run-store.js";
140
140
  export { workflowRunStoreContract } from "./core/workflow-run-store-contract.js";
141
+ export { CONTRACT_KIT_ENGINE_VERSION } from "./core/store-contracts/contract-kit-version.js";
142
+ export {} from "./core/store-contracts/contract-harness.js";
143
+ export { checkpointStoreContract } from "./core/store-contracts/checkpoint-store-contract.js";
144
+ export { sessionRepoContract } from "./core/store-contracts/session-repo-contract.js";
145
+ export { toolResultStoreContract } from "./core/store-contracts/tool-result-store-contract.js";
146
+ export { fileSnapshotStoreContract } from "./core/store-contracts/file-snapshot-store-contract.js";
147
+ export { MAILBOX_CONTRACT_SCOPE, mailboxStoreContract, mailboxAckOwnershipContract, mailboxBundledOnlyContract, } from "./core/store-contracts/mailbox-store-contract.js";
141
148
  export { canAccessAgentRecord, BackgroundAgentStoreError, InMemoryBackgroundAgentStore, reconcileParkedAgents, } from "./core/background-agent-store.js";
142
149
  export { FileBackgroundAgentStore } from "./stores/file/background-agent-store.js";
143
150
  export { InMemoryWorkflowJournalStore, callKeyOrdinal, JOURNAL_OVERSIZE_ERROR_CODE, } from "./core/workflow-journal-store.js";
@@ -2,7 +2,7 @@ export * from "./harness-types.js";
2
2
  export { AgentHarness } from "../engine/harness/agent-harness.js";
3
3
  export { CompactionError, ExecutionError, FileError, ok, err } from "../engine/harness/types.js";
4
4
  export { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, summaryOutputBudgetTokens, } from "../engine/compaction/compaction.js";
5
- export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
5
+ export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
6
6
  export { NodeExecutionEnv } from "../engine/execution-env/node-execution-env.js";
7
7
  export { StoredSession, buildSessionContext } from "../engine/session/session.js";
8
8
  export { getEntriesToFork } from "../engine/session/repo-utils.js";
@@ -2,7 +2,7 @@ export * from "./harness-types.js";
2
2
  export { AgentHarness } from "../engine/harness/agent-harness.js";
3
3
  export { CompactionError, ExecutionError, FileError, ok, err } from "../engine/harness/types.js";
4
4
  export { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, summaryOutputBudgetTokens, } from "../engine/compaction/compaction.js";
5
- export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
5
+ export { SKILL_RETENTION_PER_SKILL_MAX_CHARS, SKILL_RETENTION_TOTAL_MAX_CHARS, SKILL_RETENTION_TRUNCATION_MARKER, readCompactionActiveTools, readRetainedInvokedSkills, renderInvokedSkillsRetention, } from "../engine/compaction/utils.js";
6
6
  export { NodeExecutionEnv } from "../engine/execution-env/node-execution-env.js";
7
7
  export { StoredSession, buildSessionContext } from "../engine/session/session.js";
8
8
  export { getEntriesToFork } from "../engine/session/repo-utils.js";
@@ -43,6 +43,7 @@ export interface WorkflowAgentRun {
43
43
  attempts?: number;
44
44
  lastAttemptReason?: string;
45
45
  sessionId?: string;
46
+ worktreeDir?: string;
46
47
  }
47
48
  export interface WorkflowRunStats {
48
49
  tokens: number;
@@ -641,6 +641,14 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
641
641
  };
642
642
  return { tail, onActivity };
643
643
  };
644
+ const createWorkspaceObserver = (rec) => (workspace) => {
645
+ if (finalized)
646
+ return;
647
+ if (!workspace.isolated || rec.worktreeDir === workspace.cwd)
648
+ return;
649
+ rec.worktreeDir = workspace.cwd;
650
+ void persist("update");
651
+ };
644
652
  let currentPhase;
645
653
  let openMarkerPhase;
646
654
  let currentGroup;
@@ -815,7 +823,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
815
823
  opts.onForwardEvent(e.type === "task_progress" ? { ...e, workflowRunId: runId, workflowAgentLabel: label } : e);
816
824
  }
817
825
  : undefined;
818
- const baseInternals = { ...(agentOpts.isolation ? { isolation: agentOpts.isolation } : {}), ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd } : {}), ...spawnAttribution, ...(enrichedForward !== undefined ? { onForwardEvent: enrichedForward } : {}), agentName: label };
826
+ const baseInternals = { ...(agentOpts.isolation ? { isolation: agentOpts.isolation } : {}), ...(opts.parentCwd !== undefined ? { parentCwd: opts.parentCwd } : {}), ...spawnAttribution, ...(enrichedForward !== undefined ? { onForwardEvent: enrichedForward } : {}), agentName: label, onWorkspaceResolved: createWorkspaceObserver(rec) };
819
827
  let attempts = 0;
820
828
  let throttleRetried = false;
821
829
  let lastAttemptReason;
@@ -1160,6 +1168,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1160
1168
  ...(enrichedForwardS !== undefined ? { onForwardEvent: enrichedForwardS } : {}),
1161
1169
  agentName: label,
1162
1170
  onActivity,
1171
+ onWorkspaceResolved: createWorkspaceObserver(rec),
1163
1172
  ...(bceSink !== undefined
1164
1173
  ? {
1165
1174
  onForwardEvent: (e) => {