@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.
- package/dist/agents/observer.js +8 -3
- package/dist/agents/send-message-tool.js +112 -81
- package/dist/agents/subagent.d.ts +10 -4
- package/dist/agents/subagent.js +103 -53
- package/dist/core/memory-engine/dual-root.js +2 -0
- package/dist/core/memory-engine/engine.d.ts +4 -0
- package/dist/core/memory-engine/engine.js +6 -1
- package/dist/core/runner/prepare-memory.d.ts +4 -0
- package/dist/core/runner/prepare-memory.js +4 -1
- package/dist/core/runner/prepare-task.d.ts +8 -2
- package/dist/core/runner/prepare-task.js +39 -8
- package/dist/core/runner/runtask.js +910 -865
- package/dist/core/runner/turn-attachments.d.ts +15 -1
- package/dist/core/runner/turn-attachments.js +68 -9
- package/dist/core/tool-result-budget.js +2 -2
- package/dist/core/tool-result-store.d.ts +2 -0
- package/dist/core/tool-result-store.js +27 -2
- package/dist/core/tools.js +1 -0
- package/dist/core/types.d.ts +1 -1
- package/dist/core/workflow-journal-store.d.ts +13 -0
- package/dist/engine/loop/agent-loop.d.ts +1 -1
- package/dist/engine/loop/agent-loop.js +10 -9
- package/dist/engine/session/import-validate.js +29 -0
- package/dist/orchestration/workflow.js +28 -1
- package/dist/prompt-assembly/event-registry.js +1 -1
- package/dist/stores/cc/task-list-store.js +3 -3
- package/dist/stores/file/mailbox-store.js +33 -2
- package/dist/stores/file/memory-store.d.ts +3 -0
- package/dist/stores/file/memory-store.js +39 -12
- package/dist/stores/file/tool-result-store.js +16 -2
- package/dist/stores/file/workflow-journal-store.d.ts +17 -0
- package/dist/stores/file/workflow-journal-store.js +102 -2
- package/dist/tools/fs/bash-readonly-classifier.js +1 -1
- package/dist/tools/fs/fs-read.js +2 -2
- package/dist/tools/fs/fs-search-tools.js +3 -1
- package/dist/tools/fs/fs-shared.d.ts +2 -1
- package/dist/tools/fs/fs-shared.js +7 -3
- package/dist/tools/fs/fs-write.js +9 -11
- package/dist/tools/fs/safety.d.ts +3 -0
- package/dist/tools/fs/safety.js +20 -7
- package/dist/tools/fs/search.d.ts +1 -0
- package/dist/tools/fs/search.js +21 -3
- package/dist/tools/task-list.d.ts +1 -0
- package/dist/tools/task-list.js +13 -2
- package/dist/tools/web.js +79 -11
- package/package.json +1 -1
package/dist/tools/task-list.js
CHANGED
|
@@ -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: "
|
|
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
|
-
|
|
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
|
|
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
|
|
694
|
-
|
|
695
|
-
|
|
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 =
|
|
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
|
|
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: {
|