@sema-agent/core 1.452.0 → 2.0.1

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 (167) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +15 -0
  2. package/dist/agents/agent-transcript-tool.js +122 -0
  3. package/dist/agents/cascade.js +5 -8
  4. package/dist/agents/repair-loop.d.ts +1 -1
  5. package/dist/agents/repair-loop.js +1 -1
  6. package/dist/agents/retain-ledger.d.ts +60 -0
  7. package/dist/agents/retain-ledger.js +225 -0
  8. package/dist/agents/roster-store.d.ts +1 -0
  9. package/dist/agents/roster-store.js +9 -0
  10. package/dist/agents/send-message-tool.d.ts +41 -0
  11. package/dist/agents/send-message-tool.js +469 -0
  12. package/dist/agents/subagent.d.ts +33 -108
  13. package/dist/agents/subagent.js +22 -834
  14. package/dist/agents/teacher.d.ts +0 -1
  15. package/dist/agents/teacher.js +0 -9
  16. package/dist/bin/sema-tb.js +3 -3
  17. package/dist/brain/anthropic.js +15 -40
  18. package/dist/brain/openai.js +15 -40
  19. package/dist/brain/reasoning.d.ts +1 -1
  20. package/dist/brain/stream-engine.js +2 -2
  21. package/dist/brain/stream-shared.d.ts +17 -0
  22. package/dist/brain/stream-shared.js +50 -0
  23. package/dist/brain/timeout.d.ts +1 -1
  24. package/dist/brain/timeout.js +1 -1
  25. package/dist/config/catalog.js +1 -3
  26. package/dist/config/defaults.d.ts +6 -0
  27. package/dist/config/defaults.js +6 -0
  28. package/dist/core/ask-question.d.ts +1 -1
  29. package/dist/core/background-shell.d.ts +1 -1
  30. package/dist/core/checkpoint-store.d.ts +0 -1
  31. package/dist/core/exec-gate.d.ts +1 -1
  32. package/dist/core/exec-output-tail.d.ts +1 -0
  33. package/dist/core/exec-output-tail.js +6 -0
  34. package/dist/core/file-snapshot-store.d.ts +1 -1
  35. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  36. package/dist/core/git-worktree-env.d.ts +1 -1
  37. package/dist/core/hooks.d.ts +1 -1
  38. package/dist/core/image-downsample.d.ts +44 -0
  39. package/dist/core/image-downsample.js +97 -0
  40. package/dist/core/lsp.d.ts +1 -1
  41. package/dist/core/mcp.d.ts +3 -45
  42. package/dist/core/mcp.js +2 -97
  43. package/dist/core/media-byte-cap.d.ts +1 -1
  44. package/dist/core/memory-recall.d.ts +0 -12
  45. package/dist/core/memory-recall.js +0 -222
  46. package/dist/core/memory.d.ts +0 -6
  47. package/dist/core/memory.js +0 -88
  48. package/dist/core/message-utils.d.ts +1 -1
  49. package/dist/core/oracle-isolation.d.ts +1 -1
  50. package/dist/core/remote-env.d.ts +1 -1
  51. package/dist/core/roles.d.ts +1 -1
  52. package/dist/core/runner/active-skill-scope.d.ts +1 -1
  53. package/dist/core/runner/assemble-result.d.ts +0 -2
  54. package/dist/core/runner/memory-consolidation.d.ts +1 -1
  55. package/dist/core/runner/prepare-memory.d.ts +16 -0
  56. package/dist/core/runner/prepare-memory.js +191 -0
  57. package/dist/core/runner/prepare-task.d.ts +4 -28
  58. package/dist/core/runner/prepare-task.js +19 -207
  59. package/dist/core/runner/prompt-suggestions.d.ts +1 -1
  60. package/dist/core/runner/runtask.d.ts +2 -3
  61. package/dist/core/runner/runtask.js +337 -529
  62. package/dist/core/runner/session-rule-policy.d.ts +1 -1
  63. package/dist/core/runner/synthetic-tools.d.ts +5 -5
  64. package/dist/core/runner/synthetic-tools.js +4 -4
  65. package/dist/core/runner/tool-disclosure.d.ts +3 -3
  66. package/dist/core/runner/tool-disclosure.js +2 -2
  67. package/dist/core/runner/tool-output-projection.d.ts +6 -0
  68. package/dist/core/runner/tool-output-projection.js +81 -0
  69. package/dist/core/runtime.d.ts +1 -1
  70. package/dist/core/scheduler.d.ts +1 -1
  71. package/dist/core/sensitive-path-policy.d.ts +1 -1
  72. package/dist/core/session-reconcile.d.ts +1 -1
  73. package/dist/core/session-store.d.ts +2 -1
  74. package/dist/core/session-store.js +2 -1
  75. package/dist/core/session.d.ts +0 -1
  76. package/dist/core/task-registry-agent.d.ts +115 -0
  77. package/dist/core/task-registry-agent.js +1163 -0
  78. package/dist/core/task-registry-monitor.d.ts +13 -0
  79. package/dist/core/task-registry-monitor.js +347 -0
  80. package/dist/core/task-registry-shared.d.ts +294 -0
  81. package/dist/core/task-registry-shared.js +271 -0
  82. package/dist/core/task-registry-workflow.d.ts +5 -0
  83. package/dist/core/task-registry-workflow.js +143 -0
  84. package/dist/core/task-registry.d.ts +51 -216
  85. package/dist/core/task-registry.js +127 -1940
  86. package/dist/core/task-tool-shape.d.ts +32 -0
  87. package/dist/core/task-tool-shape.js +83 -0
  88. package/dist/core/tool-policy.d.ts +1 -2
  89. package/dist/core/tool-result-budget.d.ts +1 -1
  90. package/dist/core/tool-result-store.d.ts +2 -2
  91. package/dist/core/tool-result-store.js +1 -1
  92. package/dist/core/tools.d.ts +2 -3
  93. package/dist/core/tools.js +1 -8
  94. package/dist/core/trace.d.ts +2 -2
  95. package/dist/core/types.d.ts +0 -29
  96. package/dist/core/workflow-run-store-contract.d.ts +1 -1
  97. package/dist/core/workflow-run-store-contract.js +19 -19
  98. package/dist/engine/execution-env/node-execution-env.d.ts +13 -0
  99. package/dist/engine/execution-env/node-execution-env.js +55 -25
  100. package/dist/engine/loop/agent-loop.d.ts +1 -1
  101. package/dist/engine/loop/agent-loop.js +10 -9
  102. package/dist/engine/lsp/frame-decoder.d.ts +1 -1
  103. package/dist/engine/lsp/frame-decoder.js +1 -1
  104. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  105. package/dist/engine/lsp/stdio-lsp-transport.js +2 -2
  106. package/dist/index.d.ts +16 -14
  107. package/dist/index.js +13 -11
  108. package/dist/internal/harness-types.d.ts +7 -0
  109. package/dist/internal/harness-types.js +1 -0
  110. package/dist/internal/harness.d.ts +1 -7
  111. package/dist/internal/harness.js +1 -0
  112. package/dist/orchestration/goal.js +2 -2
  113. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  114. package/dist/orchestration/workflow-types.d.ts +193 -0
  115. package/dist/orchestration/workflow-types.js +54 -0
  116. package/dist/orchestration/workflow.d.ts +3 -193
  117. package/dist/orchestration/workflow.js +7 -59
  118. package/dist/prompt-assembly/assemble.d.ts +3 -3
  119. package/dist/prompt-assembly/assemble.js +0 -16
  120. package/dist/prompt-assembly/explain.d.ts +1 -1
  121. package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
  122. package/dist/prompts/default.d.ts +1 -6
  123. package/dist/prompts/default.js +3 -11
  124. package/dist/scenarios/env.d.ts +1 -1
  125. package/dist/scenarios/full-body.d.ts +0 -3
  126. package/dist/scenarios/full-body.js +0 -2
  127. package/dist/stores/file/background-agent-store.d.ts +2 -15
  128. package/dist/stores/file/background-agent-store.js +30 -100
  129. package/dist/stores/file/checkpoint-store.d.ts +1 -14
  130. package/dist/stores/file/checkpoint-store.js +47 -119
  131. package/dist/stores/file/file-snapshot-store.d.ts +1 -1
  132. package/dist/stores/file/mailbox-store.js +33 -2
  133. package/dist/stores/file/shared-ledger.d.ts +35 -0
  134. package/dist/stores/file/shared-ledger.js +94 -0
  135. package/dist/stores/file/workflow-run-store.d.ts +1 -14
  136. package/dist/stores/file/workflow-run-store.js +24 -93
  137. package/dist/tools/fs/bash-readonly-classifier.d.ts +8 -0
  138. package/dist/tools/fs/bash-readonly-classifier.js +166 -0
  139. package/dist/tools/fs/fs-bash.d.ts +31 -0
  140. package/dist/tools/fs/fs-bash.js +672 -0
  141. package/dist/tools/fs/fs-pdf.d.ts +22 -0
  142. package/dist/tools/fs/fs-pdf.js +236 -0
  143. package/dist/tools/fs/fs-read.d.ts +8 -0
  144. package/dist/tools/fs/fs-read.js +286 -0
  145. package/dist/tools/fs/fs-search-tools.d.ts +5 -0
  146. package/dist/tools/fs/fs-search-tools.js +175 -0
  147. package/dist/tools/fs/fs-shared.d.ts +56 -0
  148. package/dist/tools/fs/fs-shared.js +148 -0
  149. package/dist/tools/fs/fs-write.d.ts +8 -0
  150. package/dist/tools/fs/fs-write.js +421 -0
  151. package/dist/tools/fs/index.d.ts +12 -65
  152. package/dist/tools/fs/index.js +26 -2090
  153. package/dist/tools/fs/pdf.d.ts +1 -1
  154. package/dist/tools/fs/repo-map.d.ts +3 -3
  155. package/dist/tools/fs/repo-map.js +1 -1
  156. package/dist/tools/fs/safety.d.ts +4 -1
  157. package/dist/tools/fs/safety.js +7 -1
  158. package/dist/tools/fs/search.d.ts +2 -1
  159. package/dist/tools/fs/search.js +21 -3
  160. package/dist/tools/monitor.d.ts +1 -1
  161. package/dist/tools/scheduler-tools.d.ts +1 -1
  162. package/dist/tools/web.d.ts +2 -2
  163. package/dist/tools/web.js +46 -8
  164. package/dist/tools/worktree.d.ts +1 -1
  165. package/package.json +1 -1
  166. package/dist/core/dynamic-recall.d.ts +0 -32
  167. package/dist/core/dynamic-recall.js +0 -76
@@ -1,4 +1,4 @@
1
- import type { ExecutionEnv } from "../../internal/harness.js";
1
+ import type { ExecutionEnv } from "../../internal/harness-types.js";
2
2
  export declare const PDF_MAX_PAGES_PER_READ = 20;
3
3
  export declare const PDF_INLINE_PAGE_THRESHOLD = 10;
4
4
  export declare const PDF_TARGET_RAW_SIZE: number;
@@ -1,4 +1,4 @@
1
- import type { AgentTool } from "../../internal/harness.js";
2
- import type { ExecutionEnv } from "../../internal/harness.js";
1
+ import type { AgentTool } from "../../internal/harness-types.js";
2
+ import type { ExecutionEnv } from "../../internal/harness-types.js";
3
3
  export declare function extractSymbols(text: string): string[];
4
- export declare function makeRepoMapTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[]): AgentTool;
4
+ export declare function createRepoMapTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[]): AgentTool;
@@ -79,7 +79,7 @@ function rankEntries(a, b) {
79
79
  return b.symbols.length - a.symbols.length;
80
80
  return a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0;
81
81
  }
82
- export function makeRepoMapTool(env, rootCanonical, additionalRoots) {
82
+ export function createRepoMapTool(env, rootCanonical, additionalRoots) {
83
83
  return defineTool({
84
84
  name: "RepoMap",
85
85
  contract: { contractId: "core.repo_map@1", implementationRevision: "1" },
@@ -1,4 +1,4 @@
1
- import type { ExecutionEnv } from "../../internal/harness.js";
1
+ import type { ExecutionEnv } from "../../internal/harness-types.js";
2
2
  export declare function fileArgPath(args: unknown): string | undefined;
3
3
  export declare function writeTargetPath(canonicalName: string, args: unknown): string | undefined;
4
4
  export interface ReadEntry {
@@ -17,6 +17,7 @@ export type ReadFileState = Map<string, ReadEntry>;
17
17
  export declare function sha256(content: string): string;
18
18
  export interface FsViolation {
19
19
  code: "path_not_in_root" | "not_read" | "stale" | "ambiguous_edit" | "invalid";
20
+ partialView?: true;
20
21
  message: string;
21
22
  }
22
23
  export declare function isBlockedDevicePath(key: string): boolean;
@@ -44,6 +45,8 @@ export declare function canonicalizeTarget(env: ExecutionEnv, path: string, sign
44
45
  export declare function violationText(toolName: string, v: FsViolation): string;
45
46
  export declare function requireRead(state: ReadFileState, key: string): FsViolation | undefined;
46
47
  export declare const OVERSIZE_READ_ESCAPE_HINT = "(This file is over the Read tool's whole-file byte cap, so a default Read is refused \u2014 read it in slices with explicit offset/limit to satisfy the read-first rule, or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
48
+ export declare const PARTIAL_VIEW_READ_ESCAPE_HINT = "(Your last Read of this file returned only a PARTIAL view \u2014 the output token cap paginated it, so a default Read will keep returning the same page. Re-read it with explicit offset/limit (start from the page marker's next-page hint) until you have seen the part you are about to change; an explicit slice that fits satisfies the read-first rule. Or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
49
+ export declare const WRITE_ENCODING_DEADLOCK_ESCAPE_HINT = "(If the Read tool refuses this file (binary/unknown encoding), overwrite or convert it with bash instead \u2014 e.g. `rm` + rewrite, or `iconv`.)";
47
50
  export declare function checkNoChange(oldString: string, newString: string): FsViolation | undefined;
48
51
  export declare function checkStale(entry: ReadEntry, currentHash: string): FsViolation | undefined;
49
52
  export declare function countOccurrences(haystack: string, needle: string): number;
@@ -230,11 +230,17 @@ export function violationText(toolName, v) {
230
230
  export function requireRead(state, key) {
231
231
  const entry = state.get(key);
232
232
  if (entry === undefined || entry.isPartialView) {
233
- return { code: "not_read", message: "File has not been read yet. Read it first before writing to it." };
233
+ return {
234
+ code: "not_read",
235
+ message: "File has not been read yet. Read it first before writing to it.",
236
+ ...(entry?.isPartialView ? { partialView: true } : {}),
237
+ };
234
238
  }
235
239
  return undefined;
236
240
  }
237
241
  export const OVERSIZE_READ_ESCAPE_HINT = "(This file is over the Read tool's whole-file byte cap, so a default Read is refused — read it in slices with explicit offset/limit to satisfy the read-first rule, or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
242
+ export const PARTIAL_VIEW_READ_ESCAPE_HINT = "(Your last Read of this file returned only a PARTIAL view — the output token cap paginated it, so a default Read will keep returning the same page. Re-read it with explicit offset/limit (start from the page marker's next-page hint) until you have seen the part you are about to change; an explicit slice that fits satisfies the read-first rule. Or inspect/transform it with bash (e.g. `sed -n`, `grep`) instead.)";
243
+ export const WRITE_ENCODING_DEADLOCK_ESCAPE_HINT = "(If the Read tool refuses this file (binary/unknown encoding), overwrite or convert it with bash instead — e.g. `rm` + rewrite, or `iconv`.)";
238
244
  export function checkNoChange(oldString, newString) {
239
245
  if (oldString === newString) {
240
246
  return { code: "invalid", message: "No changes to make: old_string and new_string are exactly the same." };
@@ -1,4 +1,4 @@
1
- import type { ExecutionEnv } from "../../internal/harness.js";
1
+ import type { ExecutionEnv } from "../../internal/harness-types.js";
2
2
  export declare const DEFAULT_IGNORE_DIRS: ReadonlySet<string>;
3
3
  export interface GrepParams {
4
4
  pattern: string;
@@ -69,4 +69,5 @@ export declare function runGlobDetailed(env: ExecutionEnv, root: string, pattern
69
69
  durationMs: number;
70
70
  totalMatches: number;
71
71
  countIsComplete: boolean;
72
+ error?: string;
72
73
  }>;
@@ -1089,6 +1089,13 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
1089
1089
  const baseIgnore = await buildIgnore(env, root, signal);
1090
1090
  const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
1091
1091
  const start = opts.path ? (opts.path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(opts.path) ? opts.path : `${rootPrefix}${opts.path}`) : root;
1092
+ if (opts.path !== undefined) {
1093
+ const probe = await env.fileInfo(start, signal);
1094
+ if (!probe.ok && probe.error.code === "not_found") {
1095
+ const error = `Error (Glob): path ${JSON.stringify(opts.path)} does not exist — check the path, or omit \`path\` to search the whole root.`;
1096
+ return { text: error, error, filenames: [], numFiles: 0, truncated: false, durationMs: Date.now() - t0, totalMatches: 0, countIsComplete: false };
1097
+ }
1098
+ }
1092
1099
  const pat = normalizeGlobToken(pattern);
1093
1100
  const anchored = globIsAnchored(pattern);
1094
1101
  const re = globTokenToRegExp(pat, anchored, true);
@@ -1131,6 +1138,7 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
1131
1138
  return k < starStarIdx ? literalAt[k] === segs[k] : literalAfterStarStar.has(segs[k]);
1132
1139
  };
1133
1140
  let ignoredDirs = 0;
1141
+ let ignoredFiles = 0;
1134
1142
  const rescued = [];
1135
1143
  const ignore = (relPath, isDir) => {
1136
1144
  const sr = toStartRel(relPath);
@@ -1144,8 +1152,12 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
1144
1152
  if (rescued.length > 0 && rescued.some((r) => sr.startsWith(r)))
1145
1153
  return false;
1146
1154
  const ig = baseIgnore(relPath, isDir);
1147
- if (ig && isDir)
1148
- ignoredDirs++;
1155
+ if (ig) {
1156
+ if (isDir)
1157
+ ignoredDirs++;
1158
+ else
1159
+ ignoredFiles++;
1160
+ }
1149
1161
  return ig;
1150
1162
  };
1151
1163
  const walked = await walk(env, root, start, ignore, signal);
@@ -1173,6 +1185,12 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
1173
1185
  const ignoreNote = matched.length === 0 && ignoredDirs > 0
1174
1186
  ? `\n[note: ${ignoredDirs} ignored director${ignoredDirs === 1 ? "y was" : "ies were"} not searched (dependency/build/VCS trees and .gitignore) — name a directory in the pattern (e.g. "dist/**") or pass \`path\` to include it]`
1175
1187
  : "";
1176
- const text = matched.length === 0 ? "No files matched." + ignoreNote + caveat : matched.join("\n") + capNote + caveat;
1188
+ const ignoredFileNote = matched.length === 0 && ignoredFiles > 0
1189
+ ? `\n[note: ${ignoredFiles} file(s) matching an ignore rule (.gitignore) were skipped — an empty result here is NOT proof the file is absent; name it literally with a root-anchored path (e.g. "/notes.secret", "logs/app.log") to include it, or read it directly]`
1190
+ : "";
1191
+ const budgetNote = matched.length === 0 && walked.incomplete
1192
+ ? `\n[note: the file-walk budget (${WALK_MAX_FILES} files / ${WALK_MAX_DEPTH} directory levels) ran out before the whole tree was scanned — this empty result is NOT proof the file is absent; scope the search with \`path\`, or use a root-anchored pattern (e.g. "src/**/*.ts") so the walk is pruned toward it, then retry]`
1193
+ : "";
1194
+ const text = matched.length === 0 ? "No files matched." + ignoreNote + ignoredFileNote + budgetNote + caveat : matched.join("\n") + capNote + caveat;
1177
1195
  return { text, filenames: matched, numFiles: matched.length, truncated, durationMs: Date.now() - t0, totalMatches, countIsComplete };
1178
1196
  }
@@ -1,4 +1,4 @@
1
- import type { AgentTool, ExecutionEnv } from "../internal/harness.js";
1
+ import type { AgentTool, ExecutionEnv } from "../internal/harness-types.js";
2
2
  import { type MonitorTimers, type TaskRegistry } from "../core/task-registry.js";
3
3
  import type { TaskNotificationPayload } from "../core/task-notification.js";
4
4
  export interface MonitorToolOptions {
@@ -1,4 +1,4 @@
1
- import type { AgentTool, ExecutionEnv } from "../internal/harness.js";
1
+ import type { AgentTool, ExecutionEnv } from "../internal/harness-types.js";
2
2
  export declare const SCHEDULE_WAKEUP_TOOL_NAME = "ScheduleWakeup";
3
3
  export declare const AUTONOMOUS_LOOP_SENTINEL = "<<autonomous-loop>>";
4
4
  export declare const AUTONOMOUS_LOOP_DYNAMIC_SENTINEL = "<<autonomous-loop-dynamic>>";
@@ -1,4 +1,4 @@
1
- import type { AgentTool } from "../internal/harness.js";
1
+ import type { AgentTool } from "../internal/harness-types.js";
2
2
  import type { Brain, ToolSpec } from "../core/types.js";
3
3
  import type { Model } from "../internal/llm.js";
4
4
  export interface WebFetchConfig {
@@ -14,7 +14,7 @@ export declare function webFetchToolSpec(config?: WebFetchConfig): ToolSpec;
14
14
  export declare function createWebFetchTool(config?: WebFetchConfig): AgentTool;
15
15
  export declare const WEBFETCH_SUMMARY_MAX_CONTENT = 100000;
16
16
  export declare const WEBFETCH_SUMMARY_GUIDELINES: string;
17
- export declare function makeWebFetchSummarizer(brain: Brain, model: Model): (content: string, prompt: string, signal?: AbortSignal) => Promise<string>;
17
+ export declare function createWebFetchSummarizer(brain: Brain, model: Model): (content: string, prompt: string, signal?: AbortSignal) => Promise<string>;
18
18
  export interface WebSearchConfig {
19
19
  search: (query: string, signal?: AbortSignal, opts?: {
20
20
  allowedDomains?: string[];
package/dist/tools/web.js CHANGED
@@ -550,7 +550,7 @@ export const WEBFETCH_SUMMARY_GUIDELINES = `Provide a concise response based onl
550
550
  ` - Use quotation marks for exact language from articles; any language outside of the quotation should never be word-for-word the same.\n` +
551
551
  ` - You are not a lawyer and never comment on the legality of your own prompts and responses.\n` +
552
552
  ` - Never produce or reproduce exact song lyrics.\n`;
553
- export function makeWebFetchSummarizer(brain, model) {
553
+ export function createWebFetchSummarizer(brain, model) {
554
554
  return async (content, prompt, signal) => {
555
555
  const truncated = content.length > WEBFETCH_SUMMARY_MAX_CONTENT
556
556
  ? content.slice(0, WEBFETCH_SUMMARY_MAX_CONTENT) + "\n\n[Content truncated due to length...]"
@@ -576,6 +576,34 @@ export function makeWebFetchSummarizer(brain, model) {
576
576
  }
577
577
  const DEFAULT_SEARCH_TIMEOUT_MS = 30_000;
578
578
  const SEARCH_ERROR_EXCERPT_CHARS = 2048;
579
+ const SEARCH_QUERY_MAX_CHARS = 2_000;
580
+ function classifySearchFailure(message) {
581
+ const m = message.toLowerCase();
582
+ const status = /(?:\bhttp\b|\bstatus\b|\bcode\b|\berror\b)\D{0,12}?(\d{3})\b/.exec(m)?.[1];
583
+ const code = status === undefined ? undefined : Number(status);
584
+ if (code === 429 || /\brate[- ]?limit/.test(m)) {
585
+ return { retryable: true, hint: "The backend rate-limited this request — retry after a pause, not immediately." };
586
+ }
587
+ if (code === 408 || /\btimed? ?out\b|\betimedout\b/.test(m)) {
588
+ return { retryable: true, hint: "This looks like a timeout — a retry may succeed; a narrower query tends to return faster." };
589
+ }
590
+ if (/\becconnrefused\b|\benotfound\b|\beai_again\b|\becconnreset\b|\bepipe\b|\bfetch failed\b|socket hang up|\bnetwork\b|\bdns\b/.test(m)) {
591
+ return { retryable: true, hint: "This is a network/transport fault, not a rejection of the query — one retry is reasonable; if it repeats, treat the search backend as unavailable and continue without it." };
592
+ }
593
+ if (code !== undefined && code >= 500 && code <= 599) {
594
+ return { retryable: true, hint: `The backend reported a server-side fault (HTTP ${code}) — retry once; if it repeats, continue without search results.` };
595
+ }
596
+ if (code !== undefined && code >= 400 && code <= 499) {
597
+ return {
598
+ retryable: false,
599
+ hint: `The backend REJECTED the request (HTTP ${code}) — an identical retry will fail the same way. Change the query or the domain filters, or report that the search backend's configuration/credentials need attention.`,
600
+ };
601
+ }
602
+ return {
603
+ retryable: "unknown",
604
+ hint: "Whether a retry helps cannot be determined from the backend's message — at most one retry, then continue without search results.",
605
+ };
606
+ }
579
607
  const SEARCH_TITLE_MAX_CHARS = 300;
580
608
  const SEARCH_SNIPPET_MAX_CHARS = 1_000;
581
609
  const SEARCH_BODY_MAX_CHARS = 100_000;
@@ -641,7 +669,11 @@ export function createWebSearchTool(config) {
641
669
  ...extra,
642
670
  });
643
671
  if (allowed_domains?.length && blocked_domains?.length) {
644
- return errorResult("Error: Cannot specify both allowed_domains and blocked_domains in the same request", failCard());
672
+ return errorResult("Error: Cannot specify both allowed_domains and blocked_domains in the same request", failCard({ retryable: false }));
673
+ }
674
+ const queryChars = [...query].length;
675
+ if (queryChars > SEARCH_QUERY_MAX_CHARS) {
676
+ return errorResult(`Error (WebSearch): the query is ${queryChars} characters, over the ${SEARCH_QUERY_MAX_CHARS}-character limit. Shorten it to the terms that matter — an identical retry will fail the same way.`, failCard({ retryable: false }));
645
677
  }
646
678
  const timeoutMs = config.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS;
647
679
  const ac = new AbortController();
@@ -652,8 +684,8 @@ export function createWebSearchTool(config) {
652
684
  else
653
685
  ctx.signal?.addEventListener("abort", onOuterAbort, { once: true });
654
686
  const TIMED_OUT = Symbol("websearch-timeout");
655
- const abortedFrame = () => errorResult("Error (WebSearch): the search was interrupted (aborted) before completion. No results were retrieved.", failCard({ aborted: true }));
656
- const timedOutFrame = () => errorResult(`Error (WebSearch): the search backend did not respond within ${timeoutMs}ms. The tool stopped waiting; the backend may still be executing the request.`, failCard({ timedOut: true }));
687
+ const abortedFrame = () => errorResult("Error (WebSearch): the search was interrupted (aborted) before completion. No results were retrieved. The interruption came from the caller, not the backend — do not retry on your own initiative.", failCard({ aborted: true, retryable: false }));
688
+ const timedOutFrame = () => errorResult(`Error (WebSearch): the search backend did not respond within ${timeoutMs}ms. The tool stopped waiting; the backend may still be executing the request. A retry may succeed — a narrower query, or fewer domain filters, tends to return faster.`, failCard({ timedOut: true, retryable: true }));
657
689
  let results;
658
690
  try {
659
691
  const work = Promise.resolve(config.search(query, ac.signal, { allowedDomains: allowed_domains, blockedDomains: blocked_domains }));
@@ -682,9 +714,10 @@ export function createWebSearchTool(config) {
682
714
  return timedOutFrame();
683
715
  const msg = redactSecrets(e instanceof Error ? e.message : String(e)).trim();
684
716
  const headline = "Error (WebSearch): the search backend failed.";
685
- return errorResult(msg
717
+ const verdict = classifySearchFailure(msg);
718
+ return errorResult((msg
686
719
  ? `${headline} The backend's error text follows:\n\n${delimitUntrusted("WebSearch backend error", msg, SEARCH_ERROR_EXCERPT_CHARS)}`
687
- : headline, failCard());
720
+ : headline) + `\n\n${verdict.hint}`, failCard({ retryable: verdict.retryable }));
688
721
  }
689
722
  finally {
690
723
  clearTimeout(timer);
@@ -692,7 +725,9 @@ export function createWebSearchTool(config) {
692
725
  }
693
726
  const usable = results.filter((r) => webSearchResultAllowed(r.url));
694
727
  const droppedUnusable = results.length - usable.length;
695
- results = usable.filter((r) => webSearchResultAllowed(r.url, allowed_domains, blocked_domains)).slice(0, max);
728
+ const domainFiltered = usable.filter((r) => webSearchResultAllowed(r.url, allowed_domains, blocked_domains));
729
+ const droppedByDomain = usable.length - domainFiltered.length;
730
+ results = domainFiltered.slice(0, max);
696
731
  let clipped = false;
697
732
  const shown = results.map((r) => {
698
733
  const title = clipCodePoints(r.title, SEARCH_TITLE_MAX_CHARS);
@@ -709,10 +744,13 @@ export function createWebSearchTool(config) {
709
744
  const dropNote = droppedUnusable > 0
710
745
  ? `\n\n[WebSearch: ${droppedUnusable} result(s) returned by the backend were dropped — their URL was not a usable http(s) link]`
711
746
  : "";
747
+ const domainNote = droppedByDomain > 0
748
+ ? `\n\n[WebSearch: ${droppedByDomain} result(s) were dropped by the ${allowed_domains?.length ? "allowed_domains" : "blocked_domains"} filter passed with this call — the backend returned them, this tool removed them. Relax or drop the filter to see them.]`
749
+ : "";
712
750
  const clipNote = clipped
713
751
  ? "\n\n[WebSearch: one or more result fields exceeded the display limits and were truncated — treat the text above as an excerpt, not the backend's full result.]"
714
752
  : "";
715
- const modelText = `${fenced}${dropNote}${clipNote}\n\nREMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks.`;
753
+ const modelText = `${fenced}${dropNote}${domainNote}${clipNote}\n\nREMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks.`;
716
754
  return {
717
755
  content: modelText,
718
756
  details: {
@@ -1,4 +1,4 @@
1
- import type { AgentTool, ExecutionEnv } from "../internal/harness.js";
1
+ import type { AgentTool, ExecutionEnv } from "../internal/harness-types.js";
2
2
  import type { CwdRef } from "./fs/index.js";
3
3
  export interface ActiveWorktreeSession {
4
4
  worktreeDir: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "1.452.0",
3
+ "version": "2.0.1",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -1,32 +0,0 @@
1
- import type { AgentMessage } from "../internal/harness.js";
2
- import { type MemorySelector, encodeSurfacedKey } from "./memory-recall.js";
3
- import type { MemoryStore } from "./memory.js";
4
- export interface DynamicRecallConfig {
5
- enabled?: boolean;
6
- recentMessageWindow?: number;
7
- maxQueryChars?: number;
8
- }
9
- export declare const DEFAULT_RECENT_MESSAGE_WINDOW = 6;
10
- export declare const DEFAULT_MAX_QUERY_CHARS = 4000;
11
- export declare function buildRecallQuery(messages: readonly AgentMessage[], window?: number, maxChars?: number): string;
12
- export interface DynamicRecallOutcome {
13
- block: string | undefined;
14
- surfacedIds: readonly string[];
15
- surfacedKeys: readonly string[];
16
- }
17
- export interface DynamicRecallInput {
18
- store: MemoryStore;
19
- scopes: readonly string[];
20
- selector: MemorySelector;
21
- query: string;
22
- recentTools?: string[];
23
- maxSelected?: number;
24
- maxLinked?: number;
25
- timeoutMs?: number;
26
- signal?: AbortSignal;
27
- nowMs?: number;
28
- priorSurfacedIds?: ReadonlySet<string>;
29
- priorSurfacedKeys?: ReadonlySet<string>;
30
- }
31
- export declare function runDynamicRecall(input: DynamicRecallInput): Promise<DynamicRecallOutcome>;
32
- export { encodeSurfacedKey };
@@ -1,76 +0,0 @@
1
- import { encodeSurfacedKey, selectAndComposeLayeredMemory, selectAndComposeMemory, } from "./memory-recall.js";
2
- export const DEFAULT_RECENT_MESSAGE_WINDOW = 6;
3
- export const DEFAULT_MAX_QUERY_CHARS = 4_000;
4
- export function buildRecallQuery(messages, window = DEFAULT_RECENT_MESSAGE_WINDOW, maxChars = DEFAULT_MAX_QUERY_CHARS) {
5
- if (window <= 0 || maxChars <= 0)
6
- return "";
7
- const recent = messages.slice(-window);
8
- const parts = [];
9
- for (const m of recent) {
10
- const text = messageText(m);
11
- if (text)
12
- parts.push(text);
13
- }
14
- const joined = parts.join("\n").trim();
15
- if (joined.length <= maxChars)
16
- return joined;
17
- return joined.slice(joined.length - maxChars);
18
- }
19
- function messageText(m) {
20
- const content = m.content;
21
- if (typeof content === "string")
22
- return content.trim();
23
- if (Array.isArray(content)) {
24
- const out = [];
25
- for (const c of content) {
26
- if (typeof c.text === "string" && c.text.length > 0)
27
- out.push(c.text);
28
- }
29
- return out.join(" ").trim();
30
- }
31
- return "";
32
- }
33
- const EMPTY_OUTCOME = { block: undefined, surfacedIds: [], surfacedKeys: [] };
34
- export async function runDynamicRecall(input) {
35
- if (input.scopes.length === 0 || input.query.trim().length === 0)
36
- return EMPTY_OUTCOME;
37
- if (input.scopes.length === 1) {
38
- const res = await selectAndComposeMemory({
39
- store: input.store,
40
- scope: input.scopes[0],
41
- objective: input.query,
42
- selector: input.selector,
43
- recentTools: input.recentTools,
44
- maxSelected: input.maxSelected,
45
- maxLinked: input.maxLinked,
46
- timeoutMs: input.timeoutMs,
47
- signal: input.signal,
48
- nowMs: input.nowMs,
49
- priorSurfacedIds: input.priorSurfacedIds,
50
- });
51
- if ("degrade" in res)
52
- return EMPTY_OUTCOME;
53
- if (res.block === undefined || res.surfacedIds.length === 0)
54
- return EMPTY_OUTCOME;
55
- return { block: res.block, surfacedIds: res.surfacedIds, surfacedKeys: [] };
56
- }
57
- const res = await selectAndComposeLayeredMemory({
58
- store: input.store,
59
- scopes: [...input.scopes],
60
- objective: input.query,
61
- selector: input.selector,
62
- recentTools: input.recentTools,
63
- maxSelected: input.maxSelected,
64
- maxLinked: input.maxLinked,
65
- timeoutMs: input.timeoutMs,
66
- signal: input.signal,
67
- nowMs: input.nowMs,
68
- priorSurfacedKeys: input.priorSurfacedKeys,
69
- });
70
- if ("degrade" in res)
71
- return EMPTY_OUTCOME;
72
- if (res.block === undefined || res.surfacedKeys.length === 0)
73
- return EMPTY_OUTCOME;
74
- return { block: res.block, surfacedIds: [], surfacedKeys: res.surfacedKeys };
75
- }
76
- export { encodeSurfacedKey };