@sema-agent/core 2.0.0 → 2.1.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 (46) hide show
  1. package/dist/agents/observer.js +8 -3
  2. package/dist/agents/send-message-tool.js +112 -81
  3. package/dist/agents/subagent.d.ts +10 -4
  4. package/dist/agents/subagent.js +103 -53
  5. package/dist/core/memory-engine/dual-root.js +2 -0
  6. package/dist/core/memory-engine/engine.d.ts +4 -0
  7. package/dist/core/memory-engine/engine.js +6 -1
  8. package/dist/core/runner/prepare-memory.d.ts +4 -0
  9. package/dist/core/runner/prepare-memory.js +4 -1
  10. package/dist/core/runner/prepare-task.d.ts +8 -2
  11. package/dist/core/runner/prepare-task.js +39 -8
  12. package/dist/core/runner/runtask.js +910 -865
  13. package/dist/core/runner/turn-attachments.d.ts +15 -1
  14. package/dist/core/runner/turn-attachments.js +68 -9
  15. package/dist/core/tool-result-budget.js +2 -2
  16. package/dist/core/tool-result-store.d.ts +2 -0
  17. package/dist/core/tool-result-store.js +27 -2
  18. package/dist/core/tools.js +1 -0
  19. package/dist/core/types.d.ts +1 -1
  20. package/dist/core/workflow-journal-store.d.ts +13 -0
  21. package/dist/engine/loop/agent-loop.d.ts +1 -1
  22. package/dist/engine/loop/agent-loop.js +10 -9
  23. package/dist/engine/session/import-validate.js +29 -0
  24. package/dist/orchestration/workflow.js +28 -1
  25. package/dist/prompt-assembly/event-registry.js +1 -1
  26. package/dist/stores/cc/task-list-store.js +3 -3
  27. package/dist/stores/file/mailbox-store.js +33 -2
  28. package/dist/stores/file/memory-store.d.ts +3 -0
  29. package/dist/stores/file/memory-store.js +39 -12
  30. package/dist/stores/file/tool-result-store.js +16 -2
  31. package/dist/stores/file/workflow-journal-store.d.ts +17 -0
  32. package/dist/stores/file/workflow-journal-store.js +102 -2
  33. package/dist/tools/fs/bash-readonly-classifier.js +1 -1
  34. package/dist/tools/fs/fs-read.js +2 -2
  35. package/dist/tools/fs/fs-search-tools.js +3 -1
  36. package/dist/tools/fs/fs-shared.d.ts +2 -1
  37. package/dist/tools/fs/fs-shared.js +7 -3
  38. package/dist/tools/fs/fs-write.js +9 -11
  39. package/dist/tools/fs/safety.d.ts +3 -0
  40. package/dist/tools/fs/safety.js +20 -7
  41. package/dist/tools/fs/search.d.ts +1 -0
  42. package/dist/tools/fs/search.js +21 -3
  43. package/dist/tools/task-list.d.ts +1 -0
  44. package/dist/tools/task-list.js +13 -2
  45. package/dist/tools/web.js +79 -11
  46. package/package.json +1 -1
@@ -55,6 +55,17 @@ export function assertJsonMetadata(value, path = "metadata") {
55
55
  }
56
56
  throw new Error(`Task ${path} must be JSON-serializable (found ${t === "object" ? "non-plain object" : t}).`);
57
57
  }
58
+ export function compareTaskIds(a, b) {
59
+ const num = (id) => (/^\d+$/.test(id) ? Number.parseInt(id, 10) : Number.NaN);
60
+ const [na, nb] = [num(a), num(b)];
61
+ if (Number.isFinite(na) && Number.isFinite(nb))
62
+ return na - nb || a.localeCompare(b);
63
+ if (Number.isFinite(na))
64
+ return -1;
65
+ if (Number.isFinite(nb))
66
+ return 1;
67
+ return a.localeCompare(b);
68
+ }
58
69
  export function createMemoryTaskListStore() {
59
70
  const tasks = new Map();
60
71
  let nextId = 1;
@@ -69,7 +80,7 @@ export function createMemoryTaskListStore() {
69
80
  tasks.set(id, snapTask(item));
70
81
  },
71
82
  delete: (id) => tasks.delete(id),
72
- list: () => [...tasks.values()].map(snapTask),
83
+ list: () => [...tasks.values()].sort((a, b) => compareTaskIds(a.id, b.id)).map(snapTask),
73
84
  allocateId: () => String(nextId++),
74
85
  };
75
86
  }
@@ -133,7 +144,7 @@ export function createTaskListTools(store) {
133
144
  activeForm: Type.Optional(Type.String({ description: 'Present continuous form shown when in_progress (e.g., "Running tests")' })),
134
145
  metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Arbitrary metadata to attach to the task" })),
135
146
  }),
136
- effect: "idempotent",
147
+ effect: "write",
137
148
  execute: (args) => serialized(tasks, async (tx) => {
138
149
  const a = args;
139
150
  if (a.metadata)
package/dist/tools/web.js CHANGED
@@ -576,6 +576,41 @@ export function createWebFetchSummarizer(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
+ const DETAILS_QUERY_ECHO_MAX_CHARS = 500;
581
+ function clipQueryEcho(query) {
582
+ const cps = [...query];
583
+ return cps.length > DETAILS_QUERY_ECHO_MAX_CHARS
584
+ ? cps.slice(0, DETAILS_QUERY_ECHO_MAX_CHARS).join("") + `…[${cps.length} chars total]`
585
+ : query;
586
+ }
587
+ function classifySearchFailure(message) {
588
+ const m = message.toLowerCase();
589
+ const status = /(?:\bhttp\b|\bstatus\b|\bcode\b|\berror\b)\D{0,12}?(\d{3})\b/.exec(m)?.[1];
590
+ const code = status === undefined ? undefined : Number(status);
591
+ if (code === 429 || /\brate[- ]?limit/.test(m)) {
592
+ return { retryable: true, hint: "The backend rate-limited this request — retry after a pause, not immediately." };
593
+ }
594
+ if (code === 408 || /\btimed? ?out\b|\betimedout\b/.test(m)) {
595
+ return { retryable: true, hint: "This looks like a timeout — a retry may succeed; a narrower query tends to return faster." };
596
+ }
597
+ if (/\becconnrefused\b|\benotfound\b|\beai_again\b|\becconnreset\b|\bepipe\b|\bfetch failed\b|socket hang up|\bnetwork\b|\bdns\b/.test(m)) {
598
+ 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." };
599
+ }
600
+ if (code !== undefined && code >= 500 && code <= 599) {
601
+ return { retryable: true, hint: `The backend reported a server-side fault (HTTP ${code}) — retry once; if it repeats, continue without search results.` };
602
+ }
603
+ if (code !== undefined && code >= 400 && code <= 499) {
604
+ return {
605
+ retryable: false,
606
+ 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.`,
607
+ };
608
+ }
609
+ return {
610
+ retryable: "unknown",
611
+ hint: "Whether a retry helps cannot be determined from the backend's message — at most one retry, then continue without search results.",
612
+ };
613
+ }
579
614
  const SEARCH_TITLE_MAX_CHARS = 300;
580
615
  const SEARCH_SNIPPET_MAX_CHARS = 1_000;
581
616
  const SEARCH_BODY_MAX_CHARS = 100_000;
@@ -591,6 +626,18 @@ function hostMatchesDomain(host, domain) {
591
626
  return false;
592
627
  return h === d || h.endsWith("." + d);
593
628
  }
629
+ const URL_SCHEME_PREFIX = /^([a-z][a-z0-9+.-]*):/i;
630
+ function normalizeResultUrl(raw) {
631
+ const s = raw.trim();
632
+ if (!s)
633
+ return { url: raw, schemeAssumed: false };
634
+ if (s.startsWith("//"))
635
+ return { url: "https:" + s, schemeAssumed: true };
636
+ const scheme = URL_SCHEME_PREFIX.exec(s)?.[1];
637
+ if (scheme !== undefined && !scheme.includes("."))
638
+ return { url: s, schemeAssumed: false };
639
+ return { url: "https://" + s, schemeAssumed: true };
640
+ }
594
641
  function webSearchResultAllowed(url, allowed, blocked) {
595
642
  let host;
596
643
  try {
@@ -634,14 +681,18 @@ export function createWebSearchTool(config) {
634
681
  const startedAt = Date.now();
635
682
  const failCard = (extra = {}) => ({
636
683
  type: "web-search",
637
- query,
684
+ query: clipQueryEcho(query),
638
685
  results: [],
639
686
  durationSeconds: (Date.now() - startedAt) / 1000,
640
687
  searchCount: 0,
641
688
  ...extra,
642
689
  });
643
690
  if (allowed_domains?.length && blocked_domains?.length) {
644
- return errorResult("Error: Cannot specify both allowed_domains and blocked_domains in the same request", failCard());
691
+ return errorResult("Error: Cannot specify both allowed_domains and blocked_domains in the same request", failCard({ retryable: false }));
692
+ }
693
+ const queryChars = [...query].length;
694
+ if (queryChars > SEARCH_QUERY_MAX_CHARS) {
695
+ 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
696
  }
646
697
  const timeoutMs = config.timeoutMs ?? DEFAULT_SEARCH_TIMEOUT_MS;
647
698
  const ac = new AbortController();
@@ -652,8 +703,8 @@ export function createWebSearchTool(config) {
652
703
  else
653
704
  ctx.signal?.addEventListener("abort", onOuterAbort, { once: true });
654
705
  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 }));
706
+ 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 }));
707
+ 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
708
  let results;
658
709
  try {
659
710
  const work = Promise.resolve(config.search(query, ac.signal, { allowedDomains: allowed_domains, blockedDomains: blocked_domains }));
@@ -682,19 +733,27 @@ export function createWebSearchTool(config) {
682
733
  return timedOutFrame();
683
734
  const msg = redactSecrets(e instanceof Error ? e.message : String(e)).trim();
684
735
  const headline = "Error (WebSearch): the search backend failed.";
685
- return errorResult(msg
736
+ const verdict = classifySearchFailure(msg);
737
+ return errorResult((msg
686
738
  ? `${headline} The backend's error text follows:\n\n${delimitUntrusted("WebSearch backend error", msg, SEARCH_ERROR_EXCERPT_CHARS)}`
687
- : headline, failCard());
739
+ : headline) + `\n\n${verdict.hint}`, failCard({ retryable: verdict.retryable }));
688
740
  }
689
741
  finally {
690
742
  clearTimeout(timer);
691
743
  ctx.signal?.removeEventListener("abort", onOuterAbort);
692
744
  }
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);
745
+ const normalized = results.map((r) => {
746
+ const n = normalizeResultUrl(r.url);
747
+ return { ...r, url: n.url, schemeAssumed: n.schemeAssumed };
748
+ });
749
+ const usable = normalized.filter((r) => webSearchResultAllowed(r.url));
750
+ const droppedUnusable = normalized.length - usable.length;
751
+ const domainFiltered = usable.filter((r) => webSearchResultAllowed(r.url, allowed_domains, blocked_domains));
752
+ const droppedByDomain = usable.length - domainFiltered.length;
753
+ const kept = domainFiltered.slice(0, max);
754
+ const schemeAssumed = kept.filter((r) => r.schemeAssumed).length;
696
755
  let clipped = false;
697
- const shown = results.map((r) => {
756
+ const shown = kept.map((r) => {
698
757
  const title = clipCodePoints(r.title, SEARCH_TITLE_MAX_CHARS);
699
758
  const url = clipCodePoints(r.url, MAX_URL_LENGTH);
700
759
  const snippet = clipCodePoints(r.snippet, SEARCH_SNIPPET_MAX_CHARS);
@@ -709,10 +768,19 @@ export function createWebSearchTool(config) {
709
768
  const dropNote = droppedUnusable > 0
710
769
  ? `\n\n[WebSearch: ${droppedUnusable} result(s) returned by the backend were dropped — their URL was not a usable http(s) link]`
711
770
  : "";
771
+ const domainNote = droppedByDomain > 0
772
+ ? `\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.]`
773
+ : "";
712
774
  const clipNote = clipped
713
775
  ? "\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
776
  : "";
715
- const modelText = `${fenced}${dropNote}${clipNote}\n\nREMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks.`;
777
+ const schemeNote = schemeAssumed > 0
778
+ ? `\n\n[WebSearch: ${schemeAssumed} result(s) came back with no URL scheme (a bare domain or a protocol-relative URL) and are shown normalized to https:// — that scheme is this tool's assumption, not the backend's claim.]`
779
+ : "";
780
+ const tailReminder = shown.length > 0
781
+ ? "REMINDER: You MUST include the sources above in your response to the user using markdown hyperlinks."
782
+ : "REMINDER: this search returned ZERO usable sources — there is nothing above to cite. Do not fabricate sources, URLs or citations; tell the user the search returned no usable results (the notes above say why, when there is a why) and answer from what you already know, or try a different query.";
783
+ const modelText = `${fenced}${dropNote}${domainNote}${schemeNote}${clipNote}\n\n${tailReminder}`;
716
784
  return {
717
785
  content: modelText,
718
786
  details: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",