@sema-agent/core 5.10.0 → 5.12.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 (40) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/agents/subagent.d.ts +1 -1
  3. package/dist/agents/subagent.js +6 -2
  4. package/dist/brain/anthropic.js +1 -1
  5. package/dist/brain/openai.js +1 -1
  6. package/dist/core/auto-compaction.d.ts +12 -1
  7. package/dist/core/auto-compaction.js +3 -1
  8. package/dist/core/background-agent-store.d.ts +2 -1
  9. package/dist/core/background-agent-store.js +1 -0
  10. package/dist/core/checkpoint-store.d.ts +2 -0
  11. package/dist/core/checkpoint-store.js +1 -0
  12. package/dist/core/exec-gate.js +12 -1
  13. package/dist/core/runner/assemble-result.js +9 -0
  14. package/dist/core/runner/compaction-call-options.d.ts +13 -1
  15. package/dist/core/runner/compaction-call-options.js +85 -0
  16. package/dist/core/runner/prepare-task.d.ts +6 -0
  17. package/dist/core/runner/prepare-task.js +118 -36
  18. package/dist/core/runner/runtask.js +71 -21
  19. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  20. package/dist/core/runner/tool-disclosure.js +24 -9
  21. package/dist/core/runner/turn-attachments.d.ts +2 -0
  22. package/dist/core/runner/turn-attachments.js +17 -6
  23. package/dist/core/task-registry-agent.d.ts +3 -0
  24. package/dist/core/task-registry-agent.js +9 -2
  25. package/dist/core/task-registry-shared.d.ts +1 -0
  26. package/dist/core/task-registry.d.ts +2 -0
  27. package/dist/core/trace.d.ts +1 -0
  28. package/dist/core/types.d.ts +9 -1
  29. package/dist/engine/compaction/compaction.d.ts +11 -2
  30. package/dist/engine/compaction/compaction.js +87 -9
  31. package/dist/index.d.ts +1 -1
  32. package/dist/prompt-assembly/event-registry.js +1 -1
  33. package/dist/prompts/default.d.ts +1 -0
  34. package/dist/prompts/default.js +3 -0
  35. package/dist/tools/fs/fs-bash.js +4 -1
  36. package/dist/tools/fs/index.d.ts +1 -0
  37. package/dist/tools/fs/index.js +7 -4
  38. package/dist/tools/web.d.ts +21 -1
  39. package/dist/tools/web.js +126 -10
  40. package/package.json +1 -1
@@ -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
  }
@@ -363,7 +367,9 @@ function renderBackgroundTasks(tasks) {
363
367
  ? "stopped"
364
368
  : t.status === "running"
365
369
  ? "still running in background"
366
- : t.status;
370
+ : t.status === "parked"
371
+ ? "parked awaiting an out-of-band approval — do NOT re-issue the gated call"
372
+ : t.status;
367
373
  return `- [${t.id}] Task "${(t.description ?? "background task").slice(0, PROJECTION_CONTENT_MAX)}" ${phrase}`;
368
374
  });
369
375
  return ("Context was compacted. Background tasks from before the compaction (check output with TaskOutput, " +
@@ -386,15 +392,20 @@ export function renderToolsDelta(input) {
386
392
  const blocks = [];
387
393
  const added = input.added ?? [];
388
394
  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" +
395
+ blocks.push((input.staticFace === true
396
+ ? "The following deferred tools are now active — call them directly. Their parameter schemas were " +
397
+ "provided in the ToolSearch result (the tools list itself keeps compact placeholder entries):\n"
398
+ : "The following deferred tools are now available. Their full schemas are loaded — call them " +
399
+ "directly like any other tool:\n") +
391
400
  added.map((n) => `- ${n}`).join("\n"));
392
401
  }
393
402
  const readded = input.readded ?? [];
394
403
  if (readded.length > 0) {
395
404
  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.`);
405
+ `names announced earlier in this conversation): ${groupByMcpServer(readded)}. ` +
406
+ (input.staticFace === true
407
+ ? `The tools list keeps compact placeholder entries — re-run ToolSearch ("select:<name>") if you need their current parameter schemas.`
408
+ : `Their schemas are loaded again — call them directly.`));
398
409
  }
399
410
  const removed = input.removed ?? [];
400
411
  if (removed.length > 0) {
@@ -69,6 +69,7 @@ export declare function settleBackgroundAgentLane(core: DurableAgentCore, id: st
69
69
  errorCode?: string;
70
70
  retryable?: boolean;
71
71
  errorKind?: string;
72
+ retryAfterMs?: number;
72
73
  stoppedBy?: StopSource;
73
74
  seq?: number;
74
75
  cycle?: number;
@@ -105,6 +106,7 @@ export declare function settleRevivedAgentLane(core: DurableAgentCore, id: strin
105
106
  errorCode?: string;
106
107
  retryable?: boolean;
107
108
  errorKind?: string;
109
+ retryAfterMs?: number;
108
110
  }): "completed" | "failed" | "killed" | undefined;
109
111
  export declare function noteBackgroundAgentActivityLane(core: DurableAgentCore, id: string, now?: number): void;
110
112
  export declare function reapStaleSessionBackgroundAgentsLane(core: DurableAgentCore, staleMs: number, now?: number, onTerminal?: (note: () => void) => void): number;
@@ -137,6 +139,7 @@ export interface AgentPollDetailsInput {
137
139
  error?: string;
138
140
  errorCode?: string;
139
141
  errorRetryable?: boolean;
142
+ errorRetryAfterMs?: number;
140
143
  resultIsPartial?: boolean;
141
144
  completionId?: string;
142
145
  }
@@ -713,6 +713,8 @@ export function settleBackgroundAgentLane(core, id, outcome) {
713
713
  handle.errorRetryable = outcome.retryable;
714
714
  if (outcome.errorKind !== undefined)
715
715
  handle.errorKind = outcome.errorKind;
716
+ if (outcome.retryAfterMs !== undefined)
717
+ handle.errorRetryAfterMs = outcome.retryAfterMs;
716
718
  }
717
719
  handle.updatedAt = Date.now();
718
720
  if (outcome.seq !== undefined)
@@ -731,6 +733,7 @@ export function settleBackgroundAgentLane(core, id, outcome) {
731
733
  ...(handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
732
734
  ...(handle.errorRetryable !== undefined ? { errorRetryable: handle.errorRetryable } : {}),
733
735
  ...(handle.errorKind !== undefined ? { errorKind: handle.errorKind } : {}),
736
+ ...(handle.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: handle.errorRetryAfterMs } : {}),
734
737
  }, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
735
738
  return outcome.status;
736
739
  }
@@ -857,6 +860,7 @@ export function reviveBackgroundAgentLane(core, id, access, abort) {
857
860
  handle.errorCode = undefined;
858
861
  handle.errorRetryable = undefined;
859
862
  handle.errorKind = undefined;
863
+ handle.errorRetryAfterMs = undefined;
860
864
  handle.resultIsPartial = undefined;
861
865
  handle.stopSource = undefined;
862
866
  handle.completionId = undefined;
@@ -1052,6 +1056,7 @@ export function buildAgentPollDetails(input) {
1052
1056
  ...(failed && input.error !== undefined ? { error: delimitUntrusted("agent error", boundedRedactedSummary(input.error, 300)) } : {}),
1053
1057
  ...(failed && input.errorCode !== undefined ? { errorCode: input.errorCode } : {}),
1054
1058
  ...(failed && input.errorRetryable !== undefined ? { retryable: input.errorRetryable } : {}),
1059
+ ...(failed && input.errorRetryAfterMs !== undefined ? { retryAfterMs: input.errorRetryAfterMs } : {}),
1055
1060
  ...(input.resultIsPartial === true ? { partial_result: true } : {}),
1056
1061
  ...(input.completionId !== undefined ? { completionId: input.completionId } : {}),
1057
1062
  };
@@ -1070,7 +1075,7 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
1070
1075
  };
1071
1076
  }
1072
1077
  const kindClause = row.status === "failed" && row.errorKind !== undefined && row.errorRetryable !== undefined
1073
- ? ` (error_kind: ${row.errorKind}, retryable: ${row.errorRetryable})`
1078
+ ? ` (error_kind: ${row.errorKind}, retryable: ${row.errorRetryable}${row.errorRetryAfterMs !== undefined ? `, retry_after_ms: ${row.errorRetryAfterMs}` : ""})`
1074
1079
  : "";
1075
1080
  const body = `status: ${row.status}
1076
1081
  ${row.error ? `error: ${row.error}${kindClause}
@@ -1087,6 +1092,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1087
1092
  ...(row.error !== undefined ? { error: row.error } : {}),
1088
1093
  ...(row.errorCode !== undefined ? { errorCode: row.errorCode } : {}),
1089
1094
  ...(row.errorRetryable !== undefined ? { errorRetryable: row.errorRetryable } : {}),
1095
+ ...(row.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: row.errorRetryAfterMs } : {}),
1090
1096
  ...(row.resultIsPartial === true ? { resultIsPartial: true } : {}),
1091
1097
  ...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
1092
1098
  }),
@@ -1126,7 +1132,7 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
1126
1132
  const fullResult = handle.result ? (handle.resultFull ?? handle.result) : undefined;
1127
1133
  const resultText = fullResult !== undefined ? await spillClippedAgentResult(handle, fullResult, clipTaskOutput(fullResult, handle.outputFile), store, sessionId) : undefined;
1128
1134
  const kindClause = handle.status === "failed" && handle.errorKind !== undefined && handle.errorRetryable !== undefined
1129
- ? ` (error_kind: ${handle.errorKind}, retryable: ${handle.errorRetryable})`
1135
+ ? ` (error_kind: ${handle.errorKind}, retryable: ${handle.errorRetryable}${handle.errorRetryAfterMs !== undefined ? `, retry_after_ms: ${handle.errorRetryAfterMs}` : ""})`
1130
1136
  : "";
1131
1137
  const body = running
1132
1138
  ? oneShot === true
@@ -1149,6 +1155,7 @@ ${resultText}` : "(no result text)"}`;
1149
1155
  ...(handle.error !== undefined ? { error: handle.error } : {}),
1150
1156
  ...(handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
1151
1157
  ...(handle.errorRetryable !== undefined ? { errorRetryable: handle.errorRetryable } : {}),
1158
+ ...(handle.errorRetryAfterMs !== undefined ? { errorRetryAfterMs: handle.errorRetryAfterMs } : {}),
1152
1159
  ...(handle.resultIsPartial === true ? { resultIsPartial: true } : {}),
1153
1160
  ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1154
1161
  }),
@@ -139,6 +139,7 @@ export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
139
139
  errorCode?: string;
140
140
  errorRetryable?: boolean;
141
141
  errorKind?: string;
142
+ errorRetryAfterMs?: number;
142
143
  resultIsPartial?: boolean;
143
144
  stopSource?: StopSource;
144
145
  stoppedBy?: StopSource;
@@ -141,6 +141,7 @@ export declare class TaskRegistry {
141
141
  errorCode?: string;
142
142
  retryable?: boolean;
143
143
  errorKind?: string;
144
+ retryAfterMs?: number;
144
145
  stoppedBy?: StopSource;
145
146
  seq?: number;
146
147
  cycle?: number;
@@ -180,6 +181,7 @@ export declare class TaskRegistry {
180
181
  errorCode?: string;
181
182
  retryable?: boolean;
182
183
  errorKind?: string;
184
+ retryAfterMs?: number;
183
185
  }): "completed" | "failed" | "killed" | undefined;
184
186
  unmarkRetainedContinuation(id: string): void;
185
187
  attachAgentNotify(id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void;
@@ -94,6 +94,7 @@ export type TraceEvent = {
94
94
  kind: "config.additional_directory_skipped";
95
95
  version: 1;
96
96
  taskId: string;
97
+ field?: "additionalReadDirectories";
97
98
  entry: string;
98
99
  reason: string;
99
100
  ts: number;
@@ -3,6 +3,10 @@ import type { AgentTool, ThinkingLevel } from "../internal/harness.js";
3
3
  import type { CompleteSimpleFn, DocumentContent, ImageContent, Model, ResilienceOptions, StreamFn, TextContent } from "../internal/llm.js";
4
4
  import type { TaskNotificationPayload } from "./task-notification.js";
5
5
  export type ModelRef = string | Model;
6
+ export interface StaleToolResultOffloadOptions {
7
+ keepRecentPerTool?: number;
8
+ minSavingsChars?: number;
9
+ }
6
10
  export type ModelRole = "default" | "summarize" | "subagent" | "team" | "synthesize" | "advisor" | "verifier" | "classifier";
7
11
  export type RoleSpec = ModelRef | {
8
12
  model?: ModelRef;
@@ -109,6 +113,7 @@ export interface ToolExecuteContext {
109
113
  alwaysLoadTools?: readonly string[];
110
114
  promptProfile?: "simple" | "classic";
111
115
  additionalDirectories?: readonly string[];
116
+ additionalReadDirectories?: readonly string[];
112
117
  envFacts?: TaskSpec["envFacts"];
113
118
  getApiKeyAndHeaders?: TaskSpec["getApiKeyAndHeaders"];
114
119
  activeSkillScope?: () => readonly unknown[];
@@ -297,6 +302,7 @@ export interface TaskSpec {
297
302
  tools?: ToolSpec[];
298
303
  excludeTools?: string[];
299
304
  deferTools?: string[];
305
+ toolMaterializeStrategy?: "swap" | "static";
300
306
  alwaysLoadTools?: string[];
301
307
  deferSelfResolve?: boolean;
302
308
  promptProfile?: "simple" | "classic";
@@ -325,6 +331,7 @@ export interface TaskSpec {
325
331
  checkpointStore?: import("./checkpoint-store.js").CheckpointStore | null;
326
332
  handsReadOnly?: boolean;
327
333
  additionalDirectories?: string[];
334
+ additionalReadDirectories?: string[];
328
335
  enablePlanMode?: boolean;
329
336
  interactiveTools?: boolean;
330
337
  enableFork?: boolean;
@@ -368,6 +375,7 @@ export interface TaskSpec {
368
375
  maxFiles?: number;
369
376
  maxCharsPerFile?: number;
370
377
  };
378
+ staleToolResultOffload?: StaleToolResultOffloadOptions;
371
379
  clampTolerance?: number;
372
380
  };
373
381
  attachments?: {
@@ -379,7 +387,7 @@ export interface TaskSpec {
379
387
  };
380
388
  planModeReminder?: true;
381
389
  budgetUsd?: true;
382
- backgroundTasks?: true;
390
+ backgroundTasks?: boolean;
383
391
  toolsDelta?: true;
384
392
  agentListing?: boolean;
385
393
  skillsListing?: boolean;
@@ -1,4 +1,4 @@
1
- import type { Model, StreamFn, Usage } from "../llm/index.js";
1
+ import type { Context, Message, Model, StreamFn, Tool, Usage } from "../llm/index.js";
2
2
  import { type AgentCoreCompletionRuntimeDeps } from "../loop/runtime-deps.js";
3
3
  import type { AgentMessage, ThinkingLevel } from "../loop/types.js";
4
4
  import { CompactionError, type Result, type SessionTreeEntry } from "../harness/types.js";
@@ -47,6 +47,15 @@ export interface CutPointResult {
47
47
  }
48
48
  export declare function findCutPoint(entries: SessionTreeEntry[], startIndex: number, endIndex: number, keepRecentTokens: number, charsPerToken?: number): CutPointResult;
49
49
  export declare const SUMMARIZATION_SYSTEM_PROMPT = "You are a context summarization assistant. Your task is to read a conversation between a user and an AI coding assistant, then produce a structured summary following the exact format specified.\n\nDo NOT continue the conversation. Do NOT respond to any questions in the conversation. ONLY output the structured summary.";
50
+ export interface CompactionForkContext {
51
+ systemPrompt?: string;
52
+ systemBlocks?: Context["systemBlocks"];
53
+ messages: Message[];
54
+ tools?: Tool[];
55
+ modelId?: string;
56
+ }
57
+ export declare function extractForkSummaryEnvelope(text: string): string | undefined;
58
+ export declare function forkSummarizationInstruction(customInstructions?: string): string;
50
59
  export declare function summaryOutputBudgetTokens(model: Model, settings: CompactionSettings): number;
51
60
  export interface SummarizationInputTruncation {
52
61
  label: "history" | "turn_prefix";
@@ -77,5 +86,5 @@ export interface CompactionPreparation {
77
86
  }
78
87
  export declare function prepareCompaction(pathEntries: SessionTreeEntry[], settings: CompactionSettings, charsPerToken?: number, windowTokens?: number): Result<CompactionPreparation | undefined, CompactionError>;
79
88
  export { computeFileLists, serializeConversation } from "./utils.js";
80
- export declare function compact(preparation: CompactionPreparation, model: Model, apiKey: string | undefined, headers?: Record<string, string>, customInstructions?: string, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, streamFn?: StreamFn, runtime?: AgentCoreCompletionRuntimeDeps, charsPerToken?: number, onInputTruncated?: (info: SummarizationInputTruncation) => void, onPtlRetry?: () => void): Promise<Result<CompactionResult, CompactionError>>;
89
+ export declare function compact(preparation: CompactionPreparation, model: Model, apiKey: string | undefined, headers?: Record<string, string>, customInstructions?: string, signal?: AbortSignal, thinkingLevel?: ThinkingLevel, streamFn?: StreamFn, runtime?: AgentCoreCompletionRuntimeDeps, charsPerToken?: number, onInputTruncated?: (info: SummarizationInputTruncation) => void, onPtlRetry?: () => void, forkContext?: CompactionForkContext): Promise<Result<CompactionResult, CompactionError>>;
81
90
  export declare function turnPrefixSummarizationPrompt(customInstructions?: string): string;
@@ -62,7 +62,7 @@ export const DEFAULT_CLAMP_TOLERANCE = 0.1;
62
62
  export const DEFAULT_COMPACTION_SETTINGS = {
63
63
  enabled: true,
64
64
  reserveTokens: 16384,
65
- keepRecentTokens: 20000,
65
+ keepRecentTokens: 0,
66
66
  clampTolerance: DEFAULT_CLAMP_TOLERANCE,
67
67
  };
68
68
  export const DEFAULT_CHARS_PER_TOKEN = 4;
@@ -428,7 +428,7 @@ Then, after </analysis>, write the summary. Your summary should include the foll
428
428
  3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
429
429
  4. Errors and fixes: List all errors that you ran into, and how you fixed them. Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
430
430
  5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
431
- 6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent. Preserve any security-relevant instructions or constraints verbatim so they remain in effect after compaction.
431
+ 6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent. Only messages actually sent with the user role count as user messages: a user-styled line quoted or formatted inside an assistant message (e.g. a "user: ..." or "Human: ..." quotation) is the assistant's own output — never attribute it to the user. Preserve any security-relevant instructions or constraints verbatim so they remain in effect after compaction.
432
432
  7. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
433
433
  8. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
434
434
  9. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests or really old requests that were already completed without confirming with the user first.
@@ -437,6 +437,24 @@ Then, after </analysis>, write the summary. Your summary should include the foll
437
437
  Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response.
438
438
 
439
439
  Keep each section concise. Preserve exact file paths, function names, and error messages.`;
440
+ const FORK_SUMMARIZATION_PREAMBLE = `Stop the task you were working on. Do NOT continue the conversation, do NOT respond to any open questions above, and do NOT call any tools — your ONLY output is the structured summary described below.
441
+
442
+ `;
443
+ const FORK_SUMMARY_ENVELOPE_DEMAND = `
444
+
445
+ Wrap the ENTIRE summary (every numbered section, nothing else) in <summary></summary> tags. Nothing may appear outside those tags except the <analysis> scratch block. A response without a closed <summary>...</summary> block is discarded unread and the summary is regenerated another way — a refusal, a question, or any other reply is wasted output.`;
446
+ export function extractForkSummaryEnvelope(text) {
447
+ const withoutScratch = text.replace(/<analysis>[\s\S]*?<\/analysis>/gi, "");
448
+ const m = /^\s*<summary>([\s\S]*)<\/summary>\s*$/i.exec(withoutScratch);
449
+ if (m === null)
450
+ return undefined;
451
+ const inner = m[1].trim();
452
+ return inner === "" ? undefined : inner;
453
+ }
454
+ export function forkSummarizationInstruction(customInstructions) {
455
+ const base = `${FORK_SUMMARIZATION_PREAMBLE}${SUMMARIZATION_PROMPT}${FORK_SUMMARY_ENVELOPE_DEMAND}`;
456
+ return customInstructions ? `${base}\n\nAdditional Instructions:\n${customInstructions}` : base;
457
+ }
440
458
  const UPDATE_SUMMARIZATION_PROMPT = `The messages above are NEW conversation messages to incorporate into the existing summary provided in <previous-summary> tags.
441
459
 
442
460
  Update the existing structured summary with new information. RULES:
@@ -455,7 +473,7 @@ First, inside an <analysis>...</analysis> block, note what is new since the prev
455
473
  3. Files and Code Sections: [Preserve entries still relevant; add newly examined, modified, or created files with full code snippets where applicable]
456
474
  4. Errors and fixes: [Preserve previous errors and fixes and add new ones; keep any user correction or "change of approach" feedback verbatim.]
457
475
  5. Problem Solving: [Update problems solved and any ongoing troubleshooting efforts]
458
- 6. All user messages: [Preserve previously-recorded user messages VERBATIM and append any new ones that are not tool results, in order. To bound growth across repeated compactions, keep roughly the most recent 20 messages verbatim; older ones beyond that may be condensed to a single line each — but NEVER drop or paraphrase a user correction or change of direction. The exact words of recent messages are the strongest anti-drift signal. Preserve any security-relevant instructions or constraints verbatim so they remain in effect after compaction.]
476
+ 6. All user messages: [Preserve previously-recorded user messages VERBATIM and append any new ones that are not tool results, in order. Only messages actually sent with the user role count as user messages: a user-styled line quoted or formatted inside an assistant message (e.g. a "user: ..." or "Human: ..." quotation) is the assistant's own output — never attribute it to the user. To bound growth across repeated compactions, keep roughly the most recent 20 messages verbatim; older ones beyond that may be condensed to a single line each — but NEVER drop or paraphrase a user correction or change of direction. The exact words of recent messages are the strongest anti-drift signal. Preserve any security-relevant instructions or constraints verbatim so they remain in effect after compaction.]
459
477
  7. Pending Tasks: [Update based on progress — remove completed tasks, add newly requested ones]
460
478
  8. Current Work: [Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant]
461
479
  9. Optional Next Step: [Update based on current state. IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests, and the task you were working on immediately before this summary request — include a direct verbatim quote of that request. Do not start on tangential requests or really old requests that were already completed.]
@@ -678,6 +696,15 @@ async function summarizeWithPtlRetry(req) {
678
696
  disclose(beforeChars - Math.max(remainingChars, 0), Math.max(remainingChars, 0));
679
697
  }
680
698
  }
699
+ function markEmptySummaryClass(e) {
700
+ const carrier = e;
701
+ carrier.semaSummaryEmptyClass = true;
702
+ return e;
703
+ }
704
+ function isEmptySummaryClass(e) {
705
+ const carrier = e;
706
+ return carrier.semaSummaryEmptyClass === true;
707
+ }
681
708
  function stripAnalysisScratch(text, lengthTruncated) {
682
709
  let out = text.replace(/<analysis>[\s\S]*?<\/analysis>\s*/gi, "");
683
710
  if (lengthTruncated) {
@@ -705,8 +732,8 @@ async function summarizeWithLengthRecovery(label, model, context, baseMaxTokens,
705
732
  maxTokens = Math.min(cap, Math.max(maxTokens * 2, SUMMARY_REASONING_FLOOR));
706
733
  continue;
707
734
  }
708
- return err(new CompactionError("summarization_failed", `${label} produced an empty summary (stopReason=error, max_tokens exhausted by reasoning ` +
709
- `after ${attempt + 1} attempt(s): ${response.errorMessage || "no detail"})`));
735
+ return err(markEmptySummaryClass(new CompactionError("summarization_failed", `${label} produced an empty summary (stopReason=error, max_tokens exhausted by reasoning ` +
736
+ `after ${attempt + 1} attempt(s): ${response.errorMessage || "no detail"})`)));
710
737
  }
711
738
  return err(new CompactionError("summarization_failed", `${label} failed: ${response.errorMessage || "Unknown error"}`));
712
739
  }
@@ -723,10 +750,43 @@ async function summarizeWithLengthRecovery(label, model, context, baseMaxTokens,
723
750
  maxTokens = Math.min(cap, Math.max(maxTokens * 2, SUMMARY_REASONING_FLOOR));
724
751
  continue;
725
752
  }
726
- return err(new CompactionError("summarization_failed", `${label} produced an empty summary (stopReason=${response.stopReason}` +
727
- `${lengthTruncated ? ", the output was analysis scratch cut at max_tokens" : ""})`));
753
+ return err(markEmptySummaryClass(new CompactionError("summarization_failed", `${label} produced an empty summary (stopReason=${response.stopReason}` +
754
+ `${lengthTruncated ? ", the output was analysis scratch cut at max_tokens" : ""})`)));
728
755
  }
729
756
  }
757
+ async function forkSummarize(fork, model, baseMaxTokens, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn, runtime, charsPerToken) {
758
+ const instruction = forkSummarizationInstruction(customInstructions);
759
+ const context = {
760
+ ...(fork.systemPrompt !== undefined ? { systemPrompt: fork.systemPrompt } : {}),
761
+ ...(fork.systemBlocks !== undefined ? { systemBlocks: fork.systemBlocks } : {}),
762
+ ...(fork.tools !== undefined && fork.tools.length > 0 ? { tools: fork.tools } : {}),
763
+ messages: [
764
+ ...fork.messages,
765
+ { role: "user", content: [{ type: "text", text: instruction }], timestamp: Date.now() },
766
+ ],
767
+ };
768
+ let result;
769
+ try {
770
+ result = await summarizeWithLengthRecovery("Summarization", model, context, baseMaxTokens, apiKey, headers, signal, thinkingLevel, streamFn, runtime, charsPerToken);
771
+ }
772
+ catch (e) {
773
+ const msg = e instanceof Error ? e.message : String(e);
774
+ if (parsePromptTooLong(msg).isPtl)
775
+ return { kind: "fallback", detail: msg };
776
+ throw e;
777
+ }
778
+ if (!result.ok) {
779
+ if (result.error.code === "summarization_failed" && (isEmptySummaryClass(result.error) || parsePromptTooLong(result.error.message).isPtl)) {
780
+ return { kind: "fallback", detail: result.error.message };
781
+ }
782
+ return { kind: "err", error: result.error };
783
+ }
784
+ const enveloped = extractForkSummaryEnvelope(result.value);
785
+ if (enveloped === undefined) {
786
+ return { kind: "fallback", detail: "fork response lacked a closed <summary> envelope (non-conforming output)" };
787
+ }
788
+ return { kind: "ok", summary: enveloped };
789
+ }
730
790
  export async function generateSummary(currentMessages, model, summaryBudgetTokens, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry) {
731
791
  let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
732
792
  if (customInstructions) {
@@ -866,7 +926,7 @@ Summarize the prefix to provide context for the retained suffix:
866
926
 
867
927
  Be concise. Focus on what's needed to understand the kept suffix.`;
868
928
  export { computeFileLists, serializeConversation } from "./utils.js";
869
- export async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry) {
929
+ export async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry, forkContext) {
870
930
  const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, invokedSkills, persistedOutputRefs, elidedMessages, settings, } = preparation;
871
931
  if (!firstKeptEntryId) {
872
932
  return err(new CompactionError("invalid_session", "First kept entry has no UUID - session may need migration"));
@@ -876,7 +936,25 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
876
936
  }
877
937
  let summary;
878
938
  const summaryBudget = summaryOutputBudgetTokens(model, settings);
879
- if (isSplitTurn && turnPrefixMessages.length > 0) {
939
+ if (forkContext !== undefined && forkContext.messages.length > 0) {
940
+ const forked = await forkSummarize(forkContext, model, Math.floor(0.8 * summaryBudget), apiKey, headers, customInstructions, signal, thinkingLevel, streamFn, runtime, charsPerToken);
941
+ if (forked.kind === "ok") {
942
+ summary = forked.summary;
943
+ }
944
+ else if (forked.kind === "err") {
945
+ return err(forked.error);
946
+ }
947
+ else {
948
+ try {
949
+ onPtlRetry?.();
950
+ }
951
+ catch {
952
+ }
953
+ }
954
+ }
955
+ if (summary !== undefined) {
956
+ }
957
+ else if (isSplitTurn && turnPrefixMessages.length > 0) {
880
958
  const [historyResult, turnPrefixResult] = await Promise.all([
881
959
  messagesToSummarize.length > 0
882
960
  ? generateSummary(messagesToSummarize, model, summaryBudget, apiKey, headers, signal, customInstructions, previousSummary, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry)
package/dist/index.d.ts CHANGED
@@ -206,7 +206,7 @@ export { retryBackoffMs, parseRetryAfter } from "./brain/retry.js";
206
206
  export { type BrainTimeoutConfig } from "./brain/timeout.js";
207
207
  export { createAssistantMessageEventStream } from "./internal/llm.js";
208
208
  export type { AssistantMessage, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
209
- export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, A2aServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, RuntimeCaps, BackgroundChildEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskLimits, TaskResult, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolEffect, WorkflowGovernanceBaseline, } from "./core/types.js";
209
+ export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, A2aServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, RuntimeCaps, BackgroundChildEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskLimits, StaleToolResultOffloadOptions, TaskResult, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolEffect, WorkflowGovernanceBaseline, } from "./core/types.js";
210
210
  export { Type } from "typebox";
211
211
  export type { TSchema, Static } from "typebox";
212
212
  export { explainPromptAssembly, describeDefaultPack, type DefaultPackDescription, type ExplainInput } from "./prompt-assembly/explain.js";
@@ -7,7 +7,7 @@ export const EVENT_PROMPT_REGISTRY = new Map([
7
7
  { kind: "instructions_change", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", maxBytes: 512, defaultPolicy: "always", rendererRef: "turn-attachments.ts#collectInstructionsChange" },
8
8
  { kind: "workflow_size_guideline_change", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "always", rendererRef: "runtask.ts#workflowSizeGuidelineChangeNotice" },
9
9
  { kind: "budget_usd", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderBudgetUsd" },
10
- { kind: "background_tasks", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderBackgroundTasks" },
10
+ { kind: "background_tasks", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "on", rendererRef: "turn-attachments.ts#renderBackgroundTasks" },
11
11
  { kind: "tools_delta", carrier: "message.user-prefix", trust: "external", dedupe: "replace-by-key", defaultPolicy: "off", rendererRef: "turn-attachments.ts#renderToolsDelta" },
12
12
  { kind: "agent_listing", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "on", rendererRef: "turn-attachments.ts#renderAgentListingDelta" },
13
13
  { kind: "skills_listing", carrier: "message.user-prefix", trust: "operator", dedupe: "replace-by-key", defaultPolicy: "on", rendererRef: "turn-attachments.ts#renderSkillsListingDelta" },
@@ -27,6 +27,7 @@ export interface EnvironmentFacts {
27
27
  gitWorktreeRoot?: string;
28
28
  isLinkedWorktree?: boolean;
29
29
  additionalDirectories?: readonly string[];
30
+ additionalReadDirectories?: readonly string[];
30
31
  platform?: string;
31
32
  osVersion?: string;
32
33
  shell?: string;
@@ -237,6 +237,9 @@ export function buildEnvironmentContext(facts) {
237
237
  if (facts.additionalDirectories && facts.additionalDirectories.length > 0) {
238
238
  lines.push(`Additional working directories: ${facts.additionalDirectories.map((d) => inlineUntrusted(d)).join(", ")}`);
239
239
  }
240
+ if (facts.additionalReadDirectories && facts.additionalReadDirectories.length > 0) {
241
+ lines.push(`Additional read-only directories: ${facts.additionalReadDirectories.map((d) => inlineUntrusted(d)).join(", ")}`);
242
+ }
240
243
  if (facts.isGitRepo !== undefined)
241
244
  lines.push(`Is a git repository: ${facts.isGitRepo ? "yes" : "no"}`);
242
245
  if (facts.gitBranch)
@@ -296,9 +296,12 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
296
296
  const captured = (pStdout ? `--- partial stdout ---\n${pStdout}` : "") +
297
297
  (pStderr ? `${pStdout ? "\n" : ""}--- partial stderr ---\n${pStderr}` : "");
298
298
  const zeroOutput = captured.length === 0;
299
+ const zeroOutputHint = res.error.code === "timeout" && timeout >= 60
300
+ ? ` — the process ran the full ${timeout}s without writing to its stdio; it may have been stalled before producing output, or holding it in a block buffer`
301
+ : "";
299
302
  const body = !zeroOutput
300
303
  ? `\n${delimitUntrusted("partial command output", captured)}`
301
- : `\n(no output was produced before the cutoff)`;
304
+ : `\n(no output was produced before the cutoff${zeroOutputHint})`;
302
305
  const overflowNote = cutOverflowFile !== undefined ? shellRecoveryHint(cutOverflowFile, readOnly) : "";
303
306
  return {
304
307
  content: `Error (${toolName}): ${headline}${body}${overflowNote}`,
@@ -13,6 +13,7 @@ export * from "./fs-bash.js";
13
13
  import { type CwdRef, type ReadImageDownsamplerOption } from "./fs-shared.js";
14
14
  export interface HandsToolkitOptions {
15
15
  additionalRoots?: readonly string[];
16
+ additionalReadRoots?: readonly string[];
16
17
  includeShell?: boolean;
17
18
  readOnly?: boolean;
18
19
  bashReadonlyAllow?: readonly string[];
@@ -16,7 +16,10 @@ import { createEditFileTool, createWriteFileTool, createNotebookEditTool } from
16
16
  import { createGrepTool, createGlobTool } from "./fs-search-tools.js";
17
17
  import { createBashTool, createBashReadonlyTool, createEnvTaskOutputTool, createEnvTaskStopTool } from "./fs-bash.js";
18
18
  export function createHandsToolkit(env, readFileState, rootCanonical, opts = {}) {
19
- const { includeShell = false, readOnly = false, bashReadonlyAllow, commitCoAuthor = false, mountBackgroundTaskTools = true, additionalRoots, } = opts;
19
+ const { includeShell = false, readOnly = false, bashReadonlyAllow, commitCoAuthor = false, mountBackgroundTaskTools = true, additionalRoots, additionalReadRoots, } = opts;
20
+ const readFaceRoots = additionalReadRoots === undefined || additionalReadRoots.length === 0
21
+ ? additionalRoots
22
+ : [...(additionalRoots ?? []), ...additionalReadRoots];
20
23
  const cwdRef = opts.cwdRef ?? { current: rootCanonical };
21
24
  const bgReadRegistry = opts.taskRegistry;
22
25
  const bgOutputReadExemption = bgReadRegistry === undefined
@@ -27,18 +30,18 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
27
30
  ...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
28
31
  }, env);
29
32
  const tools = [
30
- createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, additionalRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption),
33
+ createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption),
31
34
  ];
32
35
  if (!readOnly) {
33
36
  tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite), createWriteFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite), createNotebookEditTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite));
34
37
  }
35
- tools.push(createGrepTool(env, rootCanonical, additionalRoots), createGlobTool(env, rootCanonical, additionalRoots), createRepoMapTool(env, rootCanonical, additionalRoots));
38
+ tools.push(createGrepTool(env, rootCanonical, readFaceRoots), createGlobTool(env, rootCanonical, readFaceRoots), createRepoMapTool(env, rootCanonical, readFaceRoots));
36
39
  if (includeShell) {
37
40
  tools.push(readOnly
38
41
  ? createBashReadonlyTool(env, rootCanonical, new Set(bashReadonlyAllow ?? BASH_READONLY_DEFAULT_ALLOW), {
39
42
  ...(opts.bashDefaultTimeoutMs !== undefined ? { bashDefaultTimeoutMs: opts.bashDefaultTimeoutMs } : {}),
40
43
  ...(opts.bashMaxTimeoutMs !== undefined ? { bashMaxTimeoutMs: opts.bashMaxTimeoutMs } : {}),
41
- ...(additionalRoots !== undefined ? { additionalRoots } : {}),
44
+ ...(readFaceRoots !== undefined ? { additionalRoots: readFaceRoots } : {}),
42
45
  })
43
46
  : createBashTool(env, rootCanonical, commitCoAuthor, cwdRef, {
44
47
  taskRegistry: opts.taskRegistry,
@@ -9,17 +9,37 @@ export interface WebFetchConfig {
9
9
  summarize?: (content: string, prompt: string, signal?: AbortSignal) => Promise<string | {
10
10
  text: string;
11
11
  truncated?: boolean;
12
+ inputTruncated?: boolean;
13
+ inputChars?: number;
14
+ usedChars?: number;
12
15
  }>;
13
16
  userAgent?: string;
14
17
  }
18
+ export declare const WEBFETCH_GROUNDING_MIN_TEXT_CHARS = 200;
19
+ export interface WebFetchGrounding {
20
+ level: "ok" | "low";
21
+ textChars: number;
22
+ bytes: number;
23
+ textRatio: number;
24
+ }
15
25
  export declare function htmlToText(html: string): string;
16
26
  export declare function webFetchToolSpec(config?: WebFetchConfig): ToolSpec;
17
27
  export declare function createWebFetchTool(config?: WebFetchConfig): AgentTool;
18
28
  export declare const WEBFETCH_SUMMARY_MAX_CONTENT = 100000;
19
29
  export declare const WEBFETCH_SUMMARY_GUIDELINES: string;
20
- export declare function createWebFetchSummarizer(brain: Brain, model: Model): (content: string, prompt: string, signal?: AbortSignal) => Promise<string | {
30
+ export declare const WEBFETCH_SUMMARY_GROUNDING_CLAUSE: string;
31
+ export declare const WEBFETCH_SUMMARY_INPUT_HEADROOM = 0.8;
32
+ export declare const WEBFETCH_SUMMARY_MIN_CONTENT = 4000;
33
+ export declare function resolveSummaryInputChars(model: Model, override?: number): number;
34
+ export interface WebFetchSummarizerOptions {
35
+ maxContentChars?: number;
36
+ }
37
+ export declare function createWebFetchSummarizer(brain: Brain, model: Model, options?: WebFetchSummarizerOptions): (content: string, prompt: string, signal?: AbortSignal) => Promise<string | {
21
38
  text: string;
22
39
  truncated?: boolean;
40
+ inputTruncated?: boolean;
41
+ inputChars?: number;
42
+ usedChars?: number;
23
43
  }>;
24
44
  export interface WebSearchConfig {
25
45
  search: (query: string, signal?: AbortSignal, opts?: {