@sema-agent/core 1.451.0 → 2.0.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 (191) 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/observer.js +3 -0
  5. package/dist/agents/repair-loop.d.ts +1 -1
  6. package/dist/agents/repair-loop.js +1 -1
  7. package/dist/agents/retain-ledger.d.ts +60 -0
  8. package/dist/agents/retain-ledger.js +225 -0
  9. package/dist/agents/roster-store.d.ts +1 -0
  10. package/dist/agents/roster-store.js +9 -0
  11. package/dist/agents/send-message-tool.d.ts +41 -0
  12. package/dist/agents/send-message-tool.js +469 -0
  13. package/dist/agents/subagent.d.ts +33 -108
  14. package/dist/agents/subagent.js +40 -813
  15. package/dist/agents/teacher.d.ts +0 -1
  16. package/dist/agents/teacher.js +0 -9
  17. package/dist/bin/sema-tb.js +3 -3
  18. package/dist/brain/anthropic.js +15 -40
  19. package/dist/brain/openai.js +15 -40
  20. package/dist/brain/reasoning.d.ts +1 -1
  21. package/dist/brain/stream-engine.js +2 -2
  22. package/dist/brain/stream-shared.d.ts +17 -0
  23. package/dist/brain/stream-shared.js +50 -0
  24. package/dist/brain/timeout.d.ts +1 -1
  25. package/dist/brain/timeout.js +1 -1
  26. package/dist/config/catalog.js +1 -3
  27. package/dist/config/defaults.d.ts +6 -0
  28. package/dist/config/defaults.js +6 -0
  29. package/dist/core/ask-question.d.ts +1 -1
  30. package/dist/core/ask-question.js +5 -5
  31. package/dist/core/background-agent-store.d.ts +2 -0
  32. package/dist/core/background-agent-store.js +5 -1
  33. package/dist/core/background-shell.d.ts +1 -1
  34. package/dist/core/checkpoint-store.d.ts +0 -1
  35. package/dist/core/exec-gate.d.ts +1 -1
  36. package/dist/core/exec-output-tail.d.ts +19 -1
  37. package/dist/core/exec-output-tail.js +44 -5
  38. package/dist/core/file-snapshot-store.d.ts +1 -1
  39. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  40. package/dist/core/git-worktree-env.d.ts +1 -1
  41. package/dist/core/hooks.d.ts +1 -1
  42. package/dist/core/image-downsample.d.ts +44 -0
  43. package/dist/core/image-downsample.js +97 -0
  44. package/dist/core/lsp-session.d.ts +1 -0
  45. package/dist/core/lsp-session.js +24 -6
  46. package/dist/core/lsp.d.ts +11 -1
  47. package/dist/core/lsp.js +63 -6
  48. package/dist/core/mailbox-store.d.ts +3 -2
  49. package/dist/core/mailbox-store.js +19 -4
  50. package/dist/core/mcp.d.ts +3 -45
  51. package/dist/core/mcp.js +2 -97
  52. package/dist/core/media-byte-cap.d.ts +1 -1
  53. package/dist/core/memory-recall.d.ts +0 -12
  54. package/dist/core/memory-recall.js +0 -222
  55. package/dist/core/memory.d.ts +0 -6
  56. package/dist/core/memory.js +0 -88
  57. package/dist/core/message-utils.d.ts +1 -1
  58. package/dist/core/oracle-isolation.d.ts +1 -1
  59. package/dist/core/remote-env.d.ts +1 -1
  60. package/dist/core/roles.d.ts +1 -1
  61. package/dist/core/runner/active-skill-scope.d.ts +1 -1
  62. package/dist/core/runner/assemble-result.d.ts +0 -2
  63. package/dist/core/runner/memory-consolidation.d.ts +1 -1
  64. package/dist/core/runner/prepare-memory.d.ts +16 -0
  65. package/dist/core/runner/prepare-memory.js +191 -0
  66. package/dist/core/runner/prepare-task.d.ts +4 -28
  67. package/dist/core/runner/prepare-task.js +29 -208
  68. package/dist/core/runner/prompt-suggestions.d.ts +1 -1
  69. package/dist/core/runner/runtask.d.ts +2 -3
  70. package/dist/core/runner/runtask.js +337 -529
  71. package/dist/core/runner/session-rule-policy.d.ts +1 -1
  72. package/dist/core/runner/synthetic-tools.d.ts +5 -5
  73. package/dist/core/runner/synthetic-tools.js +4 -4
  74. package/dist/core/runner/tool-disclosure.d.ts +3 -3
  75. package/dist/core/runner/tool-disclosure.js +2 -2
  76. package/dist/core/runner/tool-output-projection.d.ts +6 -0
  77. package/dist/core/runner/tool-output-projection.js +81 -0
  78. package/dist/core/runtime.d.ts +1 -1
  79. package/dist/core/scheduler.d.ts +1 -1
  80. package/dist/core/sensitive-path-policy.d.ts +1 -1
  81. package/dist/core/session-reconcile.d.ts +1 -1
  82. package/dist/core/session-reconcile.js +5 -2
  83. package/dist/core/session-store.d.ts +2 -1
  84. package/dist/core/session-store.js +2 -1
  85. package/dist/core/session.d.ts +0 -1
  86. package/dist/core/task-notification.d.ts +1 -0
  87. package/dist/core/task-registry-agent.d.ts +115 -0
  88. package/dist/core/task-registry-agent.js +1163 -0
  89. package/dist/core/task-registry-monitor.d.ts +13 -0
  90. package/dist/core/task-registry-monitor.js +347 -0
  91. package/dist/core/task-registry-shared.d.ts +294 -0
  92. package/dist/core/task-registry-shared.js +271 -0
  93. package/dist/core/task-registry-workflow.d.ts +5 -0
  94. package/dist/core/task-registry-workflow.js +143 -0
  95. package/dist/core/task-registry.d.ts +54 -213
  96. package/dist/core/task-registry.js +190 -1766
  97. package/dist/core/task-tool-shape.d.ts +32 -0
  98. package/dist/core/task-tool-shape.js +83 -0
  99. package/dist/core/tool-policy.d.ts +1 -2
  100. package/dist/core/tool-result-budget.d.ts +1 -1
  101. package/dist/core/tool-result-store.d.ts +2 -2
  102. package/dist/core/tool-result-store.js +3 -3
  103. package/dist/core/tools.d.ts +7 -3
  104. package/dist/core/tools.js +3 -8
  105. package/dist/core/trace.d.ts +2 -2
  106. package/dist/core/types.d.ts +1 -29
  107. package/dist/core/workflow-journal-store.d.ts +2 -0
  108. package/dist/core/workflow-journal-store.js +14 -0
  109. package/dist/core/workflow-run-store-contract.d.ts +1 -1
  110. package/dist/core/workflow-run-store-contract.js +19 -19
  111. package/dist/engine/execution-env/node-execution-env.d.ts +14 -0
  112. package/dist/engine/execution-env/node-execution-env.js +184 -44
  113. package/dist/engine/lsp/frame-decoder.d.ts +1 -1
  114. package/dist/engine/lsp/frame-decoder.js +1 -1
  115. package/dist/engine/lsp/node-lsp-manager.d.ts +4 -2
  116. package/dist/engine/lsp/node-lsp-manager.js +22 -5
  117. package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
  118. package/dist/engine/lsp/stdio-lsp-transport.js +19 -8
  119. package/dist/index.d.ts +18 -16
  120. package/dist/index.js +15 -13
  121. package/dist/internal/harness-types.d.ts +7 -0
  122. package/dist/internal/harness-types.js +1 -0
  123. package/dist/internal/harness.d.ts +1 -7
  124. package/dist/internal/harness.js +1 -0
  125. package/dist/orchestration/goal.js +2 -2
  126. package/dist/orchestration/run-workflow-tool.d.ts +2 -1
  127. package/dist/orchestration/run-workflow-tool.js +29 -6
  128. package/dist/orchestration/workflow-types.d.ts +193 -0
  129. package/dist/orchestration/workflow-types.js +54 -0
  130. package/dist/orchestration/workflow.d.ts +3 -184
  131. package/dist/orchestration/workflow.js +87 -64
  132. package/dist/prompt-assembly/assemble.d.ts +3 -3
  133. package/dist/prompt-assembly/assemble.js +0 -16
  134. package/dist/prompt-assembly/explain.d.ts +1 -1
  135. package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
  136. package/dist/prompts/default.d.ts +1 -6
  137. package/dist/prompts/default.js +3 -11
  138. package/dist/scenarios/env.d.ts +1 -1
  139. package/dist/scenarios/full-body.d.ts +0 -3
  140. package/dist/scenarios/full-body.js +0 -2
  141. package/dist/stores/cc/mailbox-store.js +6 -1
  142. package/dist/stores/file/background-agent-store.d.ts +2 -15
  143. package/dist/stores/file/background-agent-store.js +33 -102
  144. package/dist/stores/file/checkpoint-store.d.ts +1 -14
  145. package/dist/stores/file/checkpoint-store.js +47 -119
  146. package/dist/stores/file/file-snapshot-store.d.ts +1 -1
  147. package/dist/stores/file/mailbox-store.d.ts +1 -1
  148. package/dist/stores/file/mailbox-store.js +9 -7
  149. package/dist/stores/file/shared-ledger.d.ts +35 -0
  150. package/dist/stores/file/shared-ledger.js +94 -0
  151. package/dist/stores/file/workflow-run-store.d.ts +1 -14
  152. package/dist/stores/file/workflow-run-store.js +24 -93
  153. package/dist/tools/fs/bash-readonly-classifier.d.ts +8 -0
  154. package/dist/tools/fs/bash-readonly-classifier.js +166 -0
  155. package/dist/tools/fs/encoding.d.ts +5 -0
  156. package/dist/tools/fs/encoding.js +6 -0
  157. package/dist/tools/fs/fs-bash.d.ts +31 -0
  158. package/dist/tools/fs/fs-bash.js +672 -0
  159. package/dist/tools/fs/fs-pdf.d.ts +22 -0
  160. package/dist/tools/fs/fs-pdf.js +236 -0
  161. package/dist/tools/fs/fs-read.d.ts +8 -0
  162. package/dist/tools/fs/fs-read.js +286 -0
  163. package/dist/tools/fs/fs-search-tools.d.ts +5 -0
  164. package/dist/tools/fs/fs-search-tools.js +173 -0
  165. package/dist/tools/fs/fs-shared.d.ts +55 -0
  166. package/dist/tools/fs/fs-shared.js +144 -0
  167. package/dist/tools/fs/fs-write.d.ts +8 -0
  168. package/dist/tools/fs/fs-write.js +423 -0
  169. package/dist/tools/fs/index.d.ts +12 -65
  170. package/dist/tools/fs/index.js +26 -2026
  171. package/dist/tools/fs/notebook.d.ts +43 -0
  172. package/dist/tools/fs/notebook.js +141 -0
  173. package/dist/tools/fs/pdf.d.ts +1 -1
  174. package/dist/tools/fs/repo-map.d.ts +3 -3
  175. package/dist/tools/fs/repo-map.js +3 -3
  176. package/dist/tools/fs/safety.d.ts +1 -1
  177. package/dist/tools/fs/search.d.ts +1 -1
  178. package/dist/tools/fs/search.js +141 -12
  179. package/dist/tools/gitea-issue.js +4 -2
  180. package/dist/tools/monitor.d.ts +1 -1
  181. package/dist/tools/monitor.js +12 -8
  182. package/dist/tools/scheduler-tools.d.ts +1 -1
  183. package/dist/tools/scheduler-tools.js +16 -16
  184. package/dist/tools/task-list.js +34 -12
  185. package/dist/tools/web.d.ts +4 -2
  186. package/dist/tools/web.js +106 -20
  187. package/dist/tools/worktree.d.ts +1 -1
  188. package/dist/tools/worktree.js +14 -14
  189. package/package.json +1 -1
  190. package/dist/core/dynamic-recall.d.ts +0 -32
  191. package/dist/core/dynamic-recall.js +0 -76
@@ -1,5 +1,5 @@
1
1
  import { Type } from "typebox";
2
- import { defineTool } from "../core/tools.js";
2
+ import { defineTool, errorResult } from "../core/tools.js";
3
3
  import { hasScheduler, isValidCronExpr, } from "../core/scheduler.js";
4
4
  export const SCHEDULE_WAKEUP_TOOL_NAME = "ScheduleWakeup";
5
5
  export const AUTONOMOUS_LOOP_SENTINEL = "<<autonomous-loop>>";
@@ -210,33 +210,33 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
210
210
  execute: async (args) => {
211
211
  const a = args;
212
212
  if (a.schedule !== undefined && a.cron !== undefined) {
213
- return "Error (CronCreate): pass exactly one of `schedule` (object form) or `cron` (string form), not both.";
213
+ return errorResult("Error (CronCreate): pass exactly one of `schedule` (object form) or `cron` (string form), not both.");
214
214
  }
215
215
  if (a.schedule === undefined && a.cron === undefined) {
216
- return "Error (CronCreate): a schedule is required — pass `schedule` (kinds cron/at/delay) or the shorthand `cron` string.";
216
+ return errorResult("Error (CronCreate): a schedule is required — pass `schedule` (kinds cron/at/delay) or the shorthand `cron` string.");
217
217
  }
218
218
  if (a.prompt === undefined) {
219
- return "Error (CronCreate): `prompt` is required — the task runs unattended, so it needs self-contained instructions.";
219
+ return errorResult("Error (CronCreate): `prompt` is required — the task runs unattended, so it needs self-contained instructions.");
220
220
  }
221
221
  const when = a.schedule ?? { kind: "cron", expr: a.cron };
222
222
  if (when.kind === "cron") {
223
223
  if (!isValidCronExpr(when.expr)) {
224
- return `Error (CronCreate): invalid cron expression "${when.expr}" — only 5/6 space-separated cron fields (digits and * / , -) are allowed.`;
224
+ return errorResult(`Error (CronCreate): invalid cron expression "${when.expr}" — only 5/6 space-separated cron fields (digits and * / , -) are allowed.`);
225
225
  }
226
226
  const deepErr = cronScheduleError(when.expr);
227
227
  if (deepErr)
228
- return `Error (CronCreate): cron expression "${when.expr}" — ${deepErr}`;
228
+ return errorResult(`Error (CronCreate): cron expression "${when.expr}" — ${deepErr}`);
229
229
  }
230
230
  else if (a.recurring !== undefined) {
231
- return `Error (CronCreate): \`recurring\` only applies to cron schedules — "${when.kind}" fires once by nature; omit \`recurring\`.`;
231
+ return errorResult(`Error (CronCreate): \`recurring\` only applies to cron schedules — "${when.kind}" fires once by nature; omit \`recurring\`.`);
232
232
  }
233
233
  const durable = a.durable ?? a.schedule !== undefined;
234
234
  if (!durable) {
235
235
  if (sched.schedulerCapabilities.supportsSessionLifetime !== true) {
236
- return "Error (CronCreate): this scheduler backend does not support session-scoped jobs (they could not be cleaned up when the session ends). Pass `durable: true` to schedule a persistent job instead.";
236
+ return errorResult("Error (CronCreate): this scheduler backend does not support session-scoped jobs (they could not be cleaned up when the session ends). Pass `durable: true` to schedule a persistent job instead.");
237
237
  }
238
238
  if (ctx.sessionId === undefined) {
239
- return "Error (CronCreate): session-scoped jobs need a session to bind to, and this run has none. Pass `durable: true` to schedule a persistent job instead.";
239
+ return errorResult("Error (CronCreate): session-scoped jobs need a session to bind to, and this run has none. Pass `durable: true` to schedule a persistent job instead.");
240
240
  }
241
241
  }
242
242
  const intent = {
@@ -248,7 +248,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
248
248
  };
249
249
  const r = await sched.schedule(intent, schedCtx);
250
250
  if (!r.ok)
251
- return `Error (CronCreate): ${r.error.message}`;
251
+ return errorResult(`Error (CronCreate): ${r.error.message}`);
252
252
  const humanSchedule = when.kind === "cron"
253
253
  ? cronToHuman(when.expr)
254
254
  : when.kind === "delay"
@@ -278,7 +278,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
278
278
  if (!r.ok) {
279
279
  if (r.error.code === "not_found")
280
280
  return `CronDelete: no scheduled task "${id}" (already gone or not yours).`;
281
- return `Error (CronDelete): ${r.error.message}`;
281
+ return errorResult(`Error (CronDelete): ${r.error.message}`);
282
282
  }
283
283
  return { content: `Cancelled scheduled task ${id}.`, details: { type: "cron-delete", id } };
284
284
  },
@@ -292,7 +292,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
292
292
  execute: async () => {
293
293
  const r = await sched.list(schedCtx);
294
294
  if (!r.ok)
295
- return `Error (CronList): ${r.error.message}`;
295
+ return errorResult(`Error (CronList): ${r.error.message}`);
296
296
  if (r.value.length === 0) {
297
297
  return { content: "No scheduled tasks.", details: { type: "cron-list", jobs: [] } };
298
298
  }
@@ -367,7 +367,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
367
367
  if (a.stop === true) {
368
368
  const pending = await listPendingWakeups();
369
369
  if ("err" in pending)
370
- return `Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${pending.err}`;
370
+ return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${pending.err}`);
371
371
  const cancelledWakeups = await cancelWakeups(pending.ids);
372
372
  return {
373
373
  content: `Loop stopped. Cancelled ${cancelledWakeups} pending wakeup(s); recurring cron tasks are unaffected.`,
@@ -375,10 +375,10 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
375
375
  };
376
376
  }
377
377
  if (a.delaySeconds === undefined || a.reason === undefined || a.prompt === undefined) {
378
- return `Error (${SCHEDULE_WAKEUP_TOOL_NAME}): delaySeconds, reason and prompt are required unless \`stop\` is true.`;
378
+ return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): delaySeconds, reason and prompt are required unless \`stop\` is true.`);
379
379
  }
380
380
  if (sched.schedulerCapabilities.supportsSessionWakeup === false) {
381
- return `Error (${SCHEDULE_WAKEUP_TOOL_NAME}): this environment has no resident scheduler that can honor a session wakeup — the wakeup would never fire. Wait in the foreground instead, or start the work in a self-detaching form.`;
381
+ return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): this environment has no resident scheduler that can honor a session wakeup — the wakeup would never fire. Wait in the foreground instead, or start the work in a self-detaching form.`);
382
382
  }
383
383
  const clampedDelaySeconds = Math.min(3600, Math.max(60, Math.round(a.delaySeconds)));
384
384
  const wasClamped = clampedDelaySeconds !== a.delaySeconds;
@@ -387,7 +387,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
387
387
  await cancelWakeups(pending.ids);
388
388
  const r = await sched.schedule({ prompt: a.prompt, when: { kind: "delay", delaySec: clampedDelaySeconds }, label: WAKEUP_LABEL, mode: "session-wakeup" }, schedCtx);
389
389
  if (!r.ok)
390
- return `Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${r.error.message}`;
390
+ return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${r.error.message}`);
391
391
  const scheduledFor = Date.now() + clampedDelaySeconds * 1000;
392
392
  const hhmmss = new Date(scheduledFor).toTimeString().slice(0, 8);
393
393
  const clampNote = wasClamped ? ` (clamped to ${clampedDelaySeconds}s from your requested value)` : "";
@@ -326,22 +326,41 @@ export function createTaskListTools(store) {
326
326
  t.metadata[k] = v;
327
327
  }
328
328
  }
329
+ const ignoredDeps = [];
330
+ let linkedBlocks = false;
331
+ let linkedBlockedBy = false;
329
332
  for (const b of a.addBlocks ?? []) {
330
- if (!t.blocks.includes(b))
331
- t.blocks.push(b);
332
333
  const other = b === id ? t : await tx.get(b);
333
- if (other && !other.blockedBy.includes(id)) {
334
+ if (!other) {
335
+ if (!ignoredDeps.includes(b))
336
+ ignoredDeps.push(b);
337
+ continue;
338
+ }
339
+ if (!t.blocks.includes(b)) {
340
+ t.blocks.push(b);
341
+ linkedBlocks = true;
342
+ }
343
+ if (!other.blockedBy.includes(id)) {
334
344
  other.blockedBy.push(id);
345
+ linkedBlocks = true;
335
346
  if (other !== t)
336
347
  await tx.set(b, other);
337
348
  }
338
349
  }
339
350
  for (const b of a.addBlockedBy ?? []) {
340
- if (!t.blockedBy.includes(b))
341
- t.blockedBy.push(b);
342
351
  const other = b === id ? t : await tx.get(b);
343
- if (other && !other.blocks.includes(id)) {
352
+ if (!other) {
353
+ if (!ignoredDeps.includes(b))
354
+ ignoredDeps.push(b);
355
+ continue;
356
+ }
357
+ if (!t.blockedBy.includes(b)) {
358
+ t.blockedBy.push(b);
359
+ linkedBlockedBy = true;
360
+ }
361
+ if (!other.blocks.includes(id)) {
344
362
  other.blocks.push(id);
363
+ linkedBlockedBy = true;
345
364
  if (other !== t)
346
365
  await tx.set(b, other);
347
366
  }
@@ -354,10 +373,13 @@ export function createTaskListTools(store) {
354
373
  a.activeForm !== undefined ? "activeForm" : "",
355
374
  a.owner !== undefined ? "owner" : "",
356
375
  a.metadata ? "metadata" : "",
357
- a.addBlocks?.length ? "blocks" : "",
358
- a.addBlockedBy?.length ? "blockedBy" : "",
376
+ linkedBlocks ? "blocks" : "",
377
+ linkedBlockedBy ? "blockedBy" : "",
359
378
  ].filter(Boolean);
360
- return { content: `Updated task #${id}${changed.length ? ` ${changed.join(", ")}` : ""}`, details: { type: "task", task: snapTask(t) } };
379
+ const ignoredNote = ignoredDeps.length
380
+ ? ` (ignored: no such task ${ignoredDeps.map((x) => `#${x}`).join(", ")})`
381
+ : "";
382
+ return { content: `Updated task #${id}${changed.length ? ` ${changed.join(", ")}` : ""}${ignoredNote}`, details: { type: "task", task: snapTask(t) } };
361
383
  }),
362
384
  };
363
385
  const taskList = {
@@ -393,12 +415,12 @@ export function createTaskListTools(store) {
393
415
  if (all.length === 0) {
394
416
  return { content: "The task list is empty.", details: { type: "task-list", tasks: [] } };
395
417
  }
396
- const completed = new Set(all.filter((t) => t.status === "completed").map((t) => t.id));
418
+ const openIds = new Set(all.filter((x) => x.status !== "completed").map((x) => x.id));
397
419
  const lines = all.map((t) => {
398
- const liveBlockers = t.blockedBy.filter((b) => !completed.has(b));
420
+ const liveBlockers = t.blockedBy.filter((b) => openIds.has(b));
399
421
  return `#${t.id} [${t.status}] ${t.subject}${t.owner ? ` (owner: ${t.owner})` : ""}${liveBlockers.length ? ` (blocked by: ${liveBlockers.join(", ")})` : ""}`;
400
422
  });
401
- const open = all.filter((t) => t.status !== "completed").length;
423
+ const open = openIds.size;
402
424
  return {
403
425
  content: `${lines.join("\n")}\n(${all.length} total, ${open} open)`,
404
426
  details: { type: "task-list", tasks: all.map(snapTask) },
@@ -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[];
@@ -25,5 +25,7 @@ export interface WebSearchConfig {
25
25
  snippet: string;
26
26
  }>>;
27
27
  maxResults?: number;
28
+ timeoutMs?: number;
28
29
  }
30
+ export declare function clipCodePoints(s: string, max: number): string;
29
31
  export declare function createWebSearchTool(config: WebSearchConfig): ToolSpec;
package/dist/tools/web.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Type } from "typebox";
2
- import { defineTool } from "../core/tools.js";
2
+ import { defineTool, errorResult } from "../core/tools.js";
3
3
  import { delimitUntrusted, inlineUntrusted } from "../core/untrusted-text.js";
4
+ import { redactSecrets } from "../core/untrusted-egress.js";
4
5
  import { isPrivateHost } from "../core/runner/image.js";
5
6
  const MAX_REDIRECTS = 10;
6
7
  const MAX_URL_LENGTH = 2048;
@@ -229,10 +230,7 @@ export function webFetchToolSpec(config = {}) {
229
230
  const { url, prompt } = args;
230
231
  const startedAt = Date.now();
231
232
  let urlForDetails = url.length > MAX_URL_LENGTH ? url.slice(0, MAX_URL_LENGTH) : url;
232
- const failure = (message, extra = {}) => ({
233
- content: message,
234
- details: { type: "web-fetch", url: urlForDetails, bytes: 0, durationMs: Date.now() - startedAt, ...extra },
235
- });
233
+ const failure = (message, extra = {}) => errorResult(message, { type: "web-fetch", url: urlForDetails, bytes: 0, durationMs: Date.now() - startedAt, ...extra });
236
234
  if (url.length > MAX_URL_LENGTH) {
237
235
  return failure(`Error (WebFetch): URL is ${url.length} characters — the limit is ${MAX_URL_LENGTH}.`);
238
236
  }
@@ -381,6 +379,7 @@ export function webFetchToolSpec(config = {}) {
381
379
  return {
382
380
  content: `Error (WebFetch): the request was interrupted (aborted) before completion${finalResponse ? " while reading the response body" : ""}. No content was retrieved.`,
383
381
  details: { ...errDetails, aborted: true },
382
+ isError: true,
384
383
  };
385
384
  }
386
385
  if (timedOut) {
@@ -391,11 +390,13 @@ export function webFetchToolSpec(config = {}) {
391
390
  ? `Error (WebFetch): the server responded (HTTP ${connectedMeta.status}) to a redirect hop, but the request stalled while following the redirect — timed out after ${timeoutMs}ms. The host IS reachable; retry only if the stall was likely transient, or fetch the redirect target directly.`
392
391
  : `Error (WebFetch): timed out after ${timeoutMs}ms while connecting — the host did not respond or the connection stalled before any response arrived. Do not retry the same URL; continue without it or try a different source.`,
393
392
  details: { ...errDetails, timedOut: true },
393
+ isError: true,
394
394
  };
395
395
  }
396
396
  return {
397
397
  content: `Error (WebFetch): request failed: ${e instanceof Error ? e.message : String(e)}`,
398
398
  details: errDetails,
399
+ isError: true,
399
400
  };
400
401
  }
401
402
  finally {
@@ -447,6 +448,7 @@ export function webFetchToolSpec(config = {}) {
447
448
  durationMs: Date.now() - startedAt,
448
449
  ...transferStateDetails,
449
450
  },
451
+ isError: true,
450
452
  };
451
453
  }
452
454
  if (bodyBytes) {
@@ -474,6 +476,7 @@ export function webFetchToolSpec(config = {}) {
474
476
  durationMs: Date.now() - startedAt,
475
477
  ...transferStateDetails,
476
478
  },
479
+ isError: true,
477
480
  };
478
481
  }
479
482
  }
@@ -547,7 +550,7 @@ export const WEBFETCH_SUMMARY_GUIDELINES = `Provide a concise response based onl
547
550
  ` - Use quotation marks for exact language from articles; any language outside of the quotation should never be word-for-word the same.\n` +
548
551
  ` - You are not a lawyer and never comment on the legality of your own prompts and responses.\n` +
549
552
  ` - Never produce or reproduce exact song lyrics.\n`;
550
- export function makeWebFetchSummarizer(brain, model) {
553
+ export function createWebFetchSummarizer(brain, model) {
551
554
  return async (content, prompt, signal) => {
552
555
  const truncated = content.length > WEBFETCH_SUMMARY_MAX_CONTENT
553
556
  ? content.slice(0, WEBFETCH_SUMMARY_MAX_CONTENT) + "\n\n[Content truncated due to length...]"
@@ -571,20 +574,36 @@ export function makeWebFetchSummarizer(brain, model) {
571
574
  return text;
572
575
  };
573
576
  }
577
+ const DEFAULT_SEARCH_TIMEOUT_MS = 30_000;
578
+ const SEARCH_ERROR_EXCERPT_CHARS = 2048;
579
+ const SEARCH_TITLE_MAX_CHARS = 300;
580
+ const SEARCH_SNIPPET_MAX_CHARS = 1_000;
581
+ const SEARCH_BODY_MAX_CHARS = 100_000;
582
+ export function clipCodePoints(s, max) {
583
+ const cps = [...s];
584
+ return cps.length > max ? cps.slice(0, max).join("") + "…" : s;
585
+ }
574
586
  function hostMatchesDomain(host, domain) {
575
- const d = domain.toLowerCase().replace(/^\*\./, "").replace(/^\.+|\.+$/g, "");
576
- if (!d)
587
+ const norm = (s) => s.toLowerCase().replace(/^\*\./, "").replace(/^\.+|\.+$/g, "");
588
+ const h = norm(host);
589
+ const d = norm(domain);
590
+ if (!d || !h)
577
591
  return false;
578
- return host === d || host.endsWith("." + d);
592
+ return h === d || h.endsWith("." + d);
579
593
  }
580
594
  function webSearchResultAllowed(url, allowed, blocked) {
581
595
  let host;
582
596
  try {
583
- host = new URL(url).hostname.toLowerCase();
597
+ const u = new URL(url);
598
+ if (u.protocol !== "http:" && u.protocol !== "https:")
599
+ return false;
600
+ host = u.hostname.toLowerCase();
584
601
  }
585
602
  catch {
586
- return !(allowed && allowed.length);
603
+ return false;
587
604
  }
605
+ if (!host)
606
+ return false;
588
607
  if (blocked && blocked.some((d) => hostMatchesDomain(host, d)))
589
608
  return false;
590
609
  if (allowed && allowed.length && !allowed.some((d) => hostMatchesDomain(host, d)))
@@ -612,27 +631,94 @@ export function createWebSearchTool(config) {
612
631
  effect: "read",
613
632
  execute: async (args, ctx) => {
614
633
  const { query, allowed_domains, blocked_domains } = args;
634
+ const startedAt = Date.now();
635
+ const failCard = (extra = {}) => ({
636
+ type: "web-search",
637
+ query,
638
+ results: [],
639
+ durationSeconds: (Date.now() - startedAt) / 1000,
640
+ searchCount: 0,
641
+ ...extra,
642
+ });
615
643
  if (allowed_domains?.length && blocked_domains?.length) {
616
- return "Error: Cannot specify both allowed_domains and blocked_domains in the same request";
644
+ return errorResult("Error: Cannot specify both allowed_domains and blocked_domains in the same request", failCard());
617
645
  }
618
- const startedAt = Date.now();
646
+ const timeoutMs = config.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS;
647
+ const ac = new AbortController();
648
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
649
+ const onOuterAbort = () => ac.abort();
650
+ if (ctx.signal?.aborted)
651
+ ac.abort();
652
+ else
653
+ ctx.signal?.addEventListener("abort", onOuterAbort, { once: true });
654
+ 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 }));
619
657
  let results;
620
658
  try {
621
- results = await config.search(query, ctx.signal, { allowedDomains: allowed_domains, blockedDomains: blocked_domains });
659
+ const work = Promise.resolve(config.search(query, ac.signal, { allowedDomains: allowed_domains, blockedDomains: blocked_domains }));
660
+ work.catch(() => { });
661
+ let fireDeadline;
662
+ const deadline = new Promise((res) => {
663
+ fireDeadline = () => res(TIMED_OUT);
664
+ });
665
+ const deadlineTimer = setTimeout(() => fireDeadline(), timeoutMs);
666
+ deadlineTimer.unref?.();
667
+ try {
668
+ const raced = await Promise.race([work, deadline]);
669
+ if (raced === TIMED_OUT) {
670
+ return ctx.signal?.aborted ? abortedFrame() : timedOutFrame();
671
+ }
672
+ results = raced;
673
+ }
674
+ finally {
675
+ clearTimeout(deadlineTimer);
676
+ }
622
677
  }
623
678
  catch (e) {
624
- return `Error (WebSearch): ${e instanceof Error ? e.message : String(e)}`;
679
+ if (ctx.signal?.aborted)
680
+ return abortedFrame();
681
+ if (ac.signal.aborted)
682
+ return timedOutFrame();
683
+ const msg = redactSecrets(e instanceof Error ? e.message : String(e)).trim();
684
+ const headline = "Error (WebSearch): the search backend failed.";
685
+ return errorResult(msg
686
+ ? `${headline} The backend's error text follows:\n\n${delimitUntrusted("WebSearch backend error", msg, SEARCH_ERROR_EXCERPT_CHARS)}`
687
+ : headline, failCard());
625
688
  }
626
- results = results.filter((r) => webSearchResultAllowed(r.url, allowed_domains, blocked_domains)).slice(0, max);
627
- const body = results.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`).join("\n");
628
- const fenced = delimitUntrusted(`WebSearch ${JSON.stringify(query)}`, body || "(no results)");
629
- const modelText = `${fenced}\n\nREMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks.`;
689
+ finally {
690
+ clearTimeout(timer);
691
+ ctx.signal?.removeEventListener("abort", onOuterAbort);
692
+ }
693
+ const usable = results.filter((r) => webSearchResultAllowed(r.url));
694
+ const droppedUnusable = results.length - usable.length;
695
+ results = usable.filter((r) => webSearchResultAllowed(r.url, allowed_domains, blocked_domains)).slice(0, max);
696
+ let clipped = false;
697
+ const shown = results.map((r) => {
698
+ const title = clipCodePoints(r.title, SEARCH_TITLE_MAX_CHARS);
699
+ const url = clipCodePoints(r.url, MAX_URL_LENGTH);
700
+ const snippet = clipCodePoints(r.snippet, SEARCH_SNIPPET_MAX_CHARS);
701
+ if (title !== r.title || url !== r.url || snippet !== r.snippet)
702
+ clipped = true;
703
+ return { title, url, snippet };
704
+ });
705
+ const body = shown.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`).join("\n");
706
+ if (body.length > SEARCH_BODY_MAX_CHARS)
707
+ clipped = true;
708
+ const fenced = delimitUntrusted(`WebSearch ${JSON.stringify(query)}`, body || "(no results)", SEARCH_BODY_MAX_CHARS);
709
+ const dropNote = droppedUnusable > 0
710
+ ? `\n\n[WebSearch: ${droppedUnusable} result(s) returned by the backend were dropped — their URL was not a usable http(s) link]`
711
+ : "";
712
+ const clipNote = clipped
713
+ ? "\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
+ : "";
715
+ const modelText = `${fenced}${dropNote}${clipNote}\n\nREMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks.`;
630
716
  return {
631
717
  content: modelText,
632
718
  details: {
633
719
  type: "web-search",
634
720
  query,
635
- results: [{ tool_use_id: ctx.toolCallId, content: results.map((r) => ({ title: r.title, url: r.url })) }],
721
+ results: [{ tool_use_id: ctx.toolCallId, content: shown.map((r) => ({ title: r.title, url: r.url })) }],
636
722
  durationSeconds: (Date.now() - startedAt) / 1000,
637
723
  searchCount: 1,
638
724
  },
@@ -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;
@@ -1,6 +1,6 @@
1
1
  import { resolve } from "node:path";
2
2
  import { Type } from "typebox";
3
- import { defineTool } from "../core/tools.js";
3
+ import { defineTool, errorResult } from "../core/tools.js";
4
4
  import { addWorktree, pruneWorktrees, WORKTREE_PARENT } from "../core/git-worktree-env.js";
5
5
  const ENTER_DESCRIPTION = "Use this tool ONLY when explicitly instructed to work in a worktree — either by the user directly, or by " +
6
6
  "project instructions. This tool creates an isolated git worktree and switches the current session into it.\n" +
@@ -113,20 +113,20 @@ export function createWorktreeTools(env, opts) {
113
113
  execute: async (args) => {
114
114
  const { name, path, baseRef } = args;
115
115
  if (path !== undefined && name !== undefined) {
116
- return "Error (EnterWorktree): `name` and `path` are mutually exclusive — `path` enters an existing worktree, `name` creates a new one.";
116
+ return errorResult("Error (EnterWorktree): `name` and `path` are mutually exclusive — `path` enters an existing worktree, `name` creates a new one.");
117
117
  }
118
118
  if (path !== undefined && baseRef !== undefined) {
119
- return "Error (EnterWorktree): `baseRef` only applies when creating a new worktree — an existing worktree entered via `path` keeps whatever state it has.";
119
+ return errorResult("Error (EnterWorktree): `baseRef` only applies when creating a new worktree — an existing worktree entered via `path` keeps whatever state it has.");
120
120
  }
121
121
  if (session.current && path === undefined) {
122
- return (`Error (EnterWorktree): already in a worktree session (${session.current.worktreeDir}). ` +
122
+ return errorResult(`Error (EnterWorktree): already in a worktree session (${session.current.worktreeDir}). ` +
123
123
  "Use ExitWorktree to leave it before creating a new worktree, or pass `path` to switch into an " +
124
124
  "existing worktree of this repository.");
125
125
  }
126
126
  const probe = await env.exec("git rev-parse --is-inside-work-tree", { cwd: repoRoot });
127
127
  if (!probe.ok || probe.value.exitCode !== 0) {
128
128
  const detail = probe.ok ? (probe.value.stderr || probe.value.stdout).trim().split("\n")[0] : String(probe.error);
129
- return (`Error (EnterWorktree): the working directory ${repoRoot} is not a git repository` +
129
+ return errorResult(`Error (EnterWorktree): the working directory ${repoRoot} is not a git repository` +
130
130
  `${detail ? ` (${detail})` : ""}. This deployment has no VCS-agnostic worktree extension point, ` +
131
131
  "so worktree isolation is unavailable here.");
132
132
  }
@@ -134,31 +134,31 @@ export function createWorktreeTools(env, opts) {
134
134
  const candidate = resolve(cwdRef.current, path);
135
135
  const top = await env.exec("git rev-parse --show-toplevel", { cwd: candidate });
136
136
  if (!top.ok || top.value.exitCode !== 0) {
137
- return `Error (EnterWorktree): ${candidate} is not an accessible git worktree (cannot resolve its top-level directory).`;
137
+ return errorResult(`Error (EnterWorktree): ${candidate} is not an accessible git worktree (cannot resolve its top-level directory).`);
138
138
  }
139
139
  const target = top.value.stdout.trim();
140
140
  const list = await env.exec("git worktree list --porcelain", { cwd: repoRoot });
141
141
  if (!list.ok || list.value.exitCode !== 0) {
142
- return "Error (EnterWorktree): could not read `git worktree list` for this repository.";
142
+ return errorResult("Error (EnterWorktree): could not read `git worktree list` for this repository.");
143
143
  }
144
144
  const registered = list.value.stdout
145
145
  .split("\n")
146
146
  .filter((l) => l.startsWith("worktree "))
147
147
  .map((l) => l.slice("worktree ".length).trim());
148
148
  if (!registered.includes(target)) {
149
- return (`Error (EnterWorktree): ${target} is not a registered worktree of this repository ` +
149
+ return errorResult(`Error (EnterWorktree): ${target} is not a registered worktree of this repository ` +
150
150
  "(it does not appear in `git worktree list`).");
151
151
  }
152
152
  if (target === registered[0]) {
153
- return `Error (EnterWorktree): ${target} is the main working tree, not a linked worktree — nothing to enter.`;
153
+ return errorResult(`Error (EnterWorktree): ${target} is the main working tree, not a linked worktree — nothing to enter.`);
154
154
  }
155
155
  if (target !== repoRoot && !target.startsWith(repoRoot.endsWith("/") ? repoRoot : `${repoRoot}/`)) {
156
- return (`Error (EnterWorktree): the worktree ${target} lives outside the task root ${repoRoot}; ` +
156
+ return errorResult(`Error (EnterWorktree): the worktree ${target} lives outside the task root ${repoRoot}; ` +
157
157
  "file containment stays anchored at the task root, so it cannot be entered from this session.");
158
158
  }
159
159
  const head = await env.exec("git rev-parse HEAD", { cwd: target });
160
160
  if (!head.ok || head.value.exitCode !== 0) {
161
- return `Error (EnterWorktree): cannot resolve HEAD of the worktree ${target}.`;
161
+ return errorResult(`Error (EnterWorktree): cannot resolve HEAD of the worktree ${target}.`);
162
162
  }
163
163
  const previous = session.current;
164
164
  const originalCwd = previous ? previous.originalCwd : cwdRef.current;
@@ -172,7 +172,7 @@ export function createWorktreeTools(env, opts) {
172
172
  }
173
173
  const base = await resolveBaseCommit(env, repoRoot, baseRef ?? "fresh");
174
174
  if ("error" in base)
175
- return `Error (EnterWorktree): ${base.error}.`;
175
+ return errorResult(`Error (EnterWorktree): ${base.error}.`);
176
176
  let createdDir;
177
177
  try {
178
178
  const { worktreeDir } = await addWorktree(env, {
@@ -187,7 +187,7 @@ export function createWorktreeTools(env, opts) {
187
187
  createdDir = worktreeDir;
188
188
  }
189
189
  catch (e) {
190
- return `Error (EnterWorktree): ${e instanceof Error ? e.message : String(e)}`;
190
+ return errorResult(`Error (EnterWorktree): ${e instanceof Error ? e.message : String(e)}`);
191
191
  }
192
192
  session.current = {
193
193
  worktreeDir: createdDir,
@@ -227,7 +227,7 @@ export function createWorktreeTools(env, opts) {
227
227
  const discardChanges = raw.discardChanges ?? raw.discard_changes;
228
228
  const wantsRemove = raw.action === "remove";
229
229
  if (keep === true && discardChanges === true) {
230
- return "Error (ExitWorktree): `keep: true` and `discardChanges: true` contradict each other — pick one.";
230
+ return errorResult("Error (ExitWorktree): `keep: true` and `discardChanges: true` contradict each other — pick one.");
231
231
  }
232
232
  if (!session.current) {
233
233
  return ("No-op: there is no active EnterWorktree session to exit. This tool only operates on the " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "1.451.0",
3
+ "version": "2.0.0",
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 };