@robzilla1738/agentswarm 0.3.0 → 0.5.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 (42) hide show
  1. package/README.md +28 -5
  2. package/dist/agent.js +16 -1
  3. package/dist/cli.js +18 -4
  4. package/dist/config.js +35 -5
  5. package/dist/crawltools.js +247 -0
  6. package/dist/deepseek.js +125 -10
  7. package/dist/executor.js +771 -122
  8. package/dist/hub.js +16 -3
  9. package/dist/journal.js +61 -11
  10. package/dist/memory.js +83 -0
  11. package/dist/prompts.js +109 -16
  12. package/dist/report.js +252 -0
  13. package/dist/run.js +7 -2
  14. package/dist/searchcore.js +191 -0
  15. package/dist/state.js +57 -3
  16. package/dist/tools.js +202 -12
  17. package/dist/webtools.js +191 -60
  18. package/package.json +3 -2
  19. package/ui/out/404/index.html +1 -1
  20. package/ui/out/404.html +1 -1
  21. package/ui/out/_next/static/chunks/532-35122e93f37719b9.js +1 -0
  22. package/ui/out/_next/static/chunks/677-859e8d42add1806b.js +1 -0
  23. package/ui/out/_next/static/chunks/app/page-dc9f6744d203e76c.js +1 -0
  24. package/ui/out/_next/static/chunks/app/run/page-2420c9e4c963d9b3.js +1 -0
  25. package/ui/out/_next/static/chunks/app/settings/page-092a6bf42dfde57d.js +1 -0
  26. package/ui/out/_next/static/css/9f7bd82b8e4c762c.css +3 -0
  27. package/ui/out/fonts/PlanetKosmos.ttf +0 -0
  28. package/ui/out/index.html +1 -1
  29. package/ui/out/index.txt +3 -3
  30. package/ui/out/run/index.html +1 -1
  31. package/ui/out/run/index.txt +3 -3
  32. package/ui/out/settings/index.html +1 -1
  33. package/ui/out/settings/index.txt +3 -3
  34. package/ui/out/_next/static/chunks/383-289a866b246b41cc.js +0 -1
  35. package/ui/out/_next/static/chunks/619-ba102abea3e3d0e4.js +0 -1
  36. package/ui/out/_next/static/chunks/677-7ab85a6f38c3a235.js +0 -1
  37. package/ui/out/_next/static/chunks/app/page-0fda5b8e77d90b84.js +0 -1
  38. package/ui/out/_next/static/chunks/app/run/page-07aab6b1224c3c8c.js +0 -1
  39. package/ui/out/_next/static/chunks/app/settings/page-528482d468d84cfa.js +0 -1
  40. package/ui/out/_next/static/css/e2c82b53bf4519e8.css +0 -3
  41. /package/ui/out/_next/static/{Rm5Fhkds2-wIOnVlME55J → errjtBR_bKoee8ogLp8xk}/_buildManifest.js +0 -0
  42. /package/ui/out/_next/static/{Rm5Fhkds2-wIOnVlME55J → errjtBR_bKoee8ogLp8xk}/_ssgManifest.js +0 -0
package/dist/report.js ADDED
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+ /**
3
+ * Dependency-free markdown → styled HTML rendering for final reports.
4
+ *
5
+ * Every run writes artifacts/final-report.html next to final-report.md so the
6
+ * operator always gets a readable, shareable document — even for fallback and
7
+ * failure reports. This is NOT a full CommonMark implementation; it covers the
8
+ * subset models actually emit in reports: headings, paragraphs, lists (nested),
9
+ * fenced code, inline code, bold/italic, links, images, tables, blockquotes,
10
+ * and horizontal rules. Unknown constructs degrade to escaped text — never to
11
+ * broken markup.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.mdToHtml = mdToHtml;
15
+ exports.renderFinalHtml = renderFinalHtml;
16
+ function esc(s) {
17
+ return s
18
+ .replace(/&/g, "&")
19
+ .replace(/</g, "&lt;")
20
+ .replace(/>/g, "&gt;")
21
+ .replace(/"/g, "&quot;");
22
+ }
23
+ /** Inline markdown on an already-escaped string. Code spans are opaque. */
24
+ function inline(s) {
25
+ const out = [];
26
+ // Split on code spans first so no other rule fires inside them.
27
+ const parts = s.split(/(`+[^`]*`+)/g);
28
+ for (const part of parts) {
29
+ const code = /^(`+)([^`]*)\1$/.exec(part);
30
+ if (code) {
31
+ out.push(`<code>${code[2].trim() || "`"}</code>`);
32
+ continue;
33
+ }
34
+ let t = part;
35
+ // Images before links (same bracket syntax).
36
+ t = t.replace(/!\[([^\]]*)\]\((https?:[^()\s]+)\)/g, '<img src="$2" alt="$1" loading="lazy">');
37
+ t = t.replace(/\[([^\]]+)\]\(((?:https?:|#|\.{0,2}\/)[^()\s]*)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
38
+ t = t.replace(/\*\*\*([^*]+)\*\*\*/g, "<strong><em>$1</em></strong>");
39
+ t = t.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
40
+ t = t.replace(/(^|[\s(])\*([^*\s][^*]*)\*/g, "$1<em>$2</em>");
41
+ t = t.replace(/(^|[\s(])_([^_\s][^_]*)_(?=[\s.,;:!?)]|$)/g, "$1<em>$2</em>");
42
+ // Bare URLs become links (escaped text, so no quotes can appear inside).
43
+ t = t.replace(/(^|[\s(])(https?:\/\/[^\s<)]+[^\s<).,;:!?])/g, '$1<a href="$2" target="_blank" rel="noopener">$2</a>');
44
+ out.push(t);
45
+ }
46
+ return out.join("");
47
+ }
48
+ function mdToHtml(md) {
49
+ const lines = md.replace(/\r\n/g, "\n").split("\n");
50
+ const html = [];
51
+ const lists = [];
52
+ let para = [];
53
+ let quote = [];
54
+ const closeLists = (toIndent = -1) => {
55
+ while (lists.length && lists[lists.length - 1].indent > toIndent) {
56
+ html.push(`</li></${lists.pop().tag}>`);
57
+ }
58
+ };
59
+ const flushPara = () => {
60
+ if (para.length) {
61
+ html.push(`<p>${inline(esc(para.join(" ")))}</p>`);
62
+ para = [];
63
+ }
64
+ };
65
+ const flushQuote = () => {
66
+ if (quote.length) {
67
+ html.push(`<blockquote>${mdToHtml(quote.join("\n"))}</blockquote>`);
68
+ quote = [];
69
+ }
70
+ };
71
+ const flushAll = () => {
72
+ flushPara();
73
+ flushQuote();
74
+ closeLists();
75
+ };
76
+ for (let i = 0; i < lines.length; i++) {
77
+ const line = lines[i];
78
+ // Fenced code block.
79
+ const fence = /^\s*(```|~~~)\s*(\S*)/.exec(line);
80
+ if (fence) {
81
+ flushAll();
82
+ const buf = [];
83
+ for (i++; i < lines.length && !lines[i].trim().startsWith(fence[1]); i++)
84
+ buf.push(lines[i]);
85
+ const lang = fence[2] ? ` class="lang-${esc(fence[2])}"` : "";
86
+ html.push(`<pre><code${lang}>${esc(buf.join("\n"))}</code></pre>`);
87
+ continue;
88
+ }
89
+ // Blockquote (grouped, recursively rendered).
90
+ const q = /^\s*>\s?(.*)$/.exec(line);
91
+ if (q) {
92
+ flushPara();
93
+ closeLists();
94
+ quote.push(q[1]);
95
+ continue;
96
+ }
97
+ flushQuote();
98
+ // Blank line ends the current paragraph / list run (unless the next
99
+ // non-blank line continues the list).
100
+ if (!line.trim()) {
101
+ flushPara();
102
+ if (lists.length) {
103
+ let j = i + 1;
104
+ while (j < lines.length && !lines[j].trim())
105
+ j++;
106
+ if (j >= lines.length || !/^(\s*)([-*+]|\d+[.)])\s+/.test(lines[j]))
107
+ closeLists();
108
+ }
109
+ continue;
110
+ }
111
+ // Heading.
112
+ const h = /^(#{1,6})\s+(.*)$/.exec(line.trim());
113
+ if (h) {
114
+ flushAll();
115
+ const level = h[1].length;
116
+ const text = h[2].replace(/\s+#+\s*$/, "");
117
+ html.push(`<h${level}>${inline(esc(text))}</h${level}>`);
118
+ continue;
119
+ }
120
+ // Horizontal rule.
121
+ if (/^\s*([-*_])\s*(\1\s*){2,}$/.test(line)) {
122
+ flushAll();
123
+ html.push("<hr>");
124
+ continue;
125
+ }
126
+ // Table: header row + |---| separator.
127
+ if (line.includes("|") && i + 1 < lines.length && /^\s*\|?[\s:|-]+\|[\s:|-]*$/.test(lines[i + 1]) && lines[i + 1].includes("-")) {
128
+ flushAll();
129
+ const cells = (row) => row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => inline(esc(c.trim())));
130
+ const head = cells(line);
131
+ const rows = [];
132
+ for (i += 2; i < lines.length && lines[i].includes("|") && lines[i].trim(); i++)
133
+ rows.push(cells(lines[i]));
134
+ i--;
135
+ html.push("<table><thead><tr>" +
136
+ head.map((c) => `<th>${c}</th>`).join("") +
137
+ "</tr></thead><tbody>" +
138
+ rows.map((r) => `<tr>${r.map((c) => `<td>${c}</td>`).join("")}</tr>`).join("") +
139
+ "</tbody></table>");
140
+ continue;
141
+ }
142
+ // List item (unordered or ordered, nested by indentation).
143
+ const li = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(line);
144
+ if (li) {
145
+ flushPara();
146
+ const indent = li[1].length;
147
+ const tag = /\d/.test(li[2]) ? "ol" : "ul";
148
+ const top = lists[lists.length - 1];
149
+ if (!top || indent > top.indent) {
150
+ lists.push({ indent, tag });
151
+ html.push(`<${tag}><li>${inline(esc(li[3]))}`);
152
+ }
153
+ else {
154
+ closeLists(indent);
155
+ const cur = lists[lists.length - 1];
156
+ if (cur && cur.indent === indent && cur.tag !== tag) {
157
+ html.push(`</li></${lists.pop().tag}>`);
158
+ }
159
+ if (lists.length && lists[lists.length - 1].indent === indent) {
160
+ html.push(`</li><li>${inline(esc(li[3]))}`);
161
+ }
162
+ else {
163
+ lists.push({ indent, tag });
164
+ html.push(`<${tag}><li>${inline(esc(li[3]))}`);
165
+ }
166
+ }
167
+ continue;
168
+ }
169
+ // Continuation line inside a list item.
170
+ if (lists.length && /^\s{2,}\S/.test(line)) {
171
+ html.push(` ${inline(esc(line.trim()))}`);
172
+ continue;
173
+ }
174
+ closeLists();
175
+ para.push(line.trim());
176
+ }
177
+ flushAll();
178
+ return html.join("\n");
179
+ }
180
+ const CSS = `
181
+ :root { color-scheme: light dark; }
182
+ * { box-sizing: border-box; }
183
+ body {
184
+ margin: 0; padding: 48px 24px 96px;
185
+ font: 16px/1.65 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", sans-serif;
186
+ background: #fcfcfa; color: #1c1c1a;
187
+ }
188
+ @media (prefers-color-scheme: dark) { body { background: #131312; color: #e8e6e1; } }
189
+ main { max-width: 860px; margin: 0 auto; }
190
+ header.run-meta {
191
+ max-width: 860px; margin: 0 auto 36px; padding-bottom: 20px;
192
+ border-bottom: 1px solid rgba(128,128,128,.25);
193
+ font-size: 13px; color: #6e6e68; display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: center;
194
+ }
195
+ .badge { padding: 2px 10px; border-radius: 999px; font-weight: 600; font-size: 12px; letter-spacing: .02em; }
196
+ .badge.done { background: rgba(34,160,84,.14); color: #1d8a4c; }
197
+ .badge.failed { background: rgba(214,60,60,.14); color: #c23b3b; }
198
+ .badge.cancelled { background: rgba(150,150,150,.18); color: #77756f; }
199
+ h1, h2, h3, h4 { line-height: 1.25; letter-spacing: -0.012em; }
200
+ h1 { font-size: 30px; margin: 0 0 18px; }
201
+ h2 { font-size: 22px; margin: 36px 0 12px; }
202
+ h3 { font-size: 18px; margin: 28px 0 10px; }
203
+ a { color: #2563c4; text-decoration: none; }
204
+ a:hover { text-decoration: underline; }
205
+ @media (prefers-color-scheme: dark) { a { color: #7aa7e8; } }
206
+ code {
207
+ font: 13.5px/1.5 ui-monospace, "SF Mono", Menlo, Consolas, monospace;
208
+ background: rgba(128,128,128,.13); padding: 1.5px 5px; border-radius: 4px;
209
+ }
210
+ pre {
211
+ background: rgba(128,128,128,.09); border: 1px solid rgba(128,128,128,.18);
212
+ border-radius: 8px; padding: 14px 16px; overflow-x: auto;
213
+ }
214
+ pre code { background: none; padding: 0; }
215
+ blockquote {
216
+ margin: 16px 0; padding: 2px 18px; border-left: 3px solid rgba(128,128,128,.35);
217
+ color: #6e6e68;
218
+ }
219
+ table { border-collapse: collapse; margin: 18px 0; width: 100%; font-size: 14.5px; }
220
+ th, td { border: 1px solid rgba(128,128,128,.25); padding: 7px 12px; text-align: left; vertical-align: top; }
221
+ th { background: rgba(128,128,128,.08); }
222
+ img { max-width: 100%; border-radius: 6px; }
223
+ hr { border: none; border-top: 1px solid rgba(128,128,128,.25); margin: 32px 0; }
224
+ ul, ol { padding-left: 26px; }
225
+ li { margin: 3px 0; }
226
+ `;
227
+ /** Self-contained HTML document (inline CSS, no scripts, no external fetches). */
228
+ function renderFinalHtml(o) {
229
+ const title = /^#\s+(.+)$/m.exec(o.markdown)?.[1] ?? o.mission;
230
+ const date = new Date(o.finishedAt).toISOString().replace("T", " ").slice(0, 16) + " UTC";
231
+ return `<!doctype html>
232
+ <html lang="en">
233
+ <head>
234
+ <meta charset="utf-8">
235
+ <meta name="viewport" content="width=device-width, initial-scale=1">
236
+ <title>${esc(title.slice(0, 120))}</title>
237
+ <style>${CSS}</style>
238
+ </head>
239
+ <body>
240
+ <header class="run-meta">
241
+ <span class="badge ${o.status}">${o.status}</span>
242
+ <span>run ${esc(o.runId)}</span>
243
+ <span>${esc(date)}</span>
244
+ <span title="${esc(o.mission.slice(0, 600))}">mission: ${esc(o.mission.length > 90 ? o.mission.slice(0, 90) + "…" : o.mission)}</span>
245
+ </header>
246
+ <main>
247
+ ${mdToHtml(o.markdown)}
248
+ </main>
249
+ </body>
250
+ </html>
251
+ `;
252
+ }
package/dist/run.js CHANGED
@@ -126,8 +126,13 @@ const summaryCache = new Map();
126
126
  */
127
127
  const liveCache = new Map();
128
128
  const TERMINAL_STATUSES = ["done", "failed", "cancelled"];
129
- /** Grace before a silent, pid-less run is presumed dead (engine startup, fs lag). */
130
- const STALE_AFTER_MS = 20_000;
129
+ /**
130
+ * Grace before a silent, pid-less run is presumed dead. The pid file is the
131
+ * primary live signal; this window only covers engine startup (before
132
+ * writePid) and filesystem lag — generous enough that slow disks and slow
133
+ * provider preflights never flag a healthy run as interrupted.
134
+ */
135
+ const STALE_AFTER_MS = 45_000;
131
136
  /**
132
137
  * A run whose engine process vanished without writing a terminal status
133
138
  * (kill -9, reboot) would otherwise show "running" forever. Presentation-level
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ /**
3
+ * Native search intelligence, ported from the author's SearchKit project
4
+ * (~/Code/searchkit): canonical-URL dedup, source classification, quality
5
+ * ranking, and quotable-passage extraction. Pure string/URL processing —
6
+ * no external services or processes; the engines that feed it live in
7
+ * webtools.ts.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.queryTerms = queryTerms;
11
+ exports.expandQueries = expandQueries;
12
+ exports.canonicalizeUrl = canonicalizeUrl;
13
+ exports.classifySource = classifySource;
14
+ exports.detectDate = detectDate;
15
+ exports.selectPassages = selectPassages;
16
+ exports.scorePage = scorePage;
17
+ exports.resultQualityScore = resultQualityScore;
18
+ exports.mergeCandidates = mergeCandidates;
19
+ exports.passageBonus = passageBonus;
20
+ exports.rankBonus = rankBonus;
21
+ /** Lowercased alphanumeric query tokens, stopword-ish short tokens dropped. */
22
+ function queryTerms(query) {
23
+ const m = query.toLowerCase().match(/[a-z0-9]+/g) || [];
24
+ return [...new Set(m.filter((t) => t.length > 2))];
25
+ }
26
+ /**
27
+ * Generate a few complementary query phrasings to widen source coverage in
28
+ * one search: the original, a stopword-stripped keyword core (different
29
+ * recall on most engines), and a docs/guide angle for question-shaped
30
+ * queries. Deterministic and low-noise — capped at `max`.
31
+ */
32
+ function expandQueries(query, max = 3) {
33
+ const base = query.trim();
34
+ const out = [base];
35
+ const terms = queryTerms(query);
36
+ const core = terms.join(" ");
37
+ if (core && core.length > 4 && core !== base.toLowerCase())
38
+ out.push(core);
39
+ if (/^(how|what|why|when|which|where|who|is|are|can|does|do)\b/i.test(base) && terms.length) {
40
+ out.push(`${core} guide`);
41
+ }
42
+ const seen = new Set();
43
+ return out.map((q) => q.trim()).filter((q) => q && !seen.has(q.toLowerCase()) && seen.add(q.toLowerCase())).slice(0, max);
44
+ }
45
+ const TRACKING_KEYS = new Set(["fbclid", "gclid", "mc_cid", "mc_eid"]);
46
+ /** Stable canonical form for dedup: strip tracking params, www, trailing slash; sort the query. */
47
+ function canonicalizeUrl(url) {
48
+ let u;
49
+ try {
50
+ u = new URL(url);
51
+ }
52
+ catch {
53
+ return url.toLowerCase();
54
+ }
55
+ const pairs = [...u.searchParams.entries()].filter(([k]) => !TRACKING_KEYS.has(k.toLowerCase()) && !k.toLowerCase().startsWith("utm_"));
56
+ pairs.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
57
+ const query = pairs.length ? "?" + pairs.map(([k, v]) => `${k}=${v}`).join("&") : "";
58
+ const host = u.hostname.toLowerCase().replace(/^www\./, "");
59
+ let path = u.pathname || "/";
60
+ if (path !== "/")
61
+ path = path.replace(/\/+$/, "");
62
+ return `${u.protocol.toLowerCase()}//${host}${path}${query}`;
63
+ }
64
+ function classifySource(domain) {
65
+ const d = domain.toLowerCase();
66
+ if (d.endsWith(".gov") || d.endsWith(".mil"))
67
+ return "government";
68
+ if (d.endsWith(".edu"))
69
+ return "academic";
70
+ if (["twitter.com", "x.com", "reddit.com", "facebook.com"].some((s) => d.includes(s)))
71
+ return "social";
72
+ if (d.includes("news") || d.includes("reuters.com") || d.includes("apnews.com") || d.includes("bbc."))
73
+ return "news";
74
+ return "secondary";
75
+ }
76
+ /** ISO date if present, else a bare year. */
77
+ function detectDate(text) {
78
+ const iso = /\b(20\d{2}-\d{2}-\d{2})\b/.exec(text);
79
+ if (iso)
80
+ return iso[1];
81
+ const year = /\b(20\d{2})\b/.exec(text);
82
+ return year?.[1];
83
+ }
84
+ const WINDOW_WORDS = 60;
85
+ const STRIDE = 30;
86
+ /**
87
+ * Quotable passages: slide a 60-word window (stride 30) over the text and
88
+ * score each window by the fraction of query terms it contains. Deterministic
89
+ * lexical matching — no embeddings. Falls back to the lead window so a hit
90
+ * always carries something quotable.
91
+ */
92
+ function selectPassages(text, query, maxPassages = 3) {
93
+ const body = text.trim();
94
+ if (!body)
95
+ return [];
96
+ const terms = queryTerms(query);
97
+ const tokens = [...body.matchAll(/\S+/g)];
98
+ if (!tokens.length)
99
+ return [];
100
+ const windows = [];
101
+ for (let i = 0; i < tokens.length; i += STRIDE) {
102
+ const slice = tokens.slice(i, i + WINDOW_WORDS);
103
+ const start = slice[0].index;
104
+ const last = slice[slice.length - 1];
105
+ const chunk = body.slice(start, last.index + last[0].length);
106
+ windows.push({ text: chunk, score: scoreChunk(chunk, terms) });
107
+ if (i + WINDOW_WORDS >= tokens.length)
108
+ break;
109
+ }
110
+ const scored = windows.filter((w) => w.score > 0).sort((a, b) => b.score - a.score);
111
+ const picked = (scored.length ? scored : windows).slice(0, maxPassages);
112
+ return picked.map((p) => ({ text: p.text, score: Math.round(p.score * 10_000) / 10_000 }));
113
+ }
114
+ function scoreChunk(chunk, terms) {
115
+ if (!terms.length)
116
+ return 0;
117
+ const lowered = chunk.toLowerCase();
118
+ let hits = 0;
119
+ for (const t of terms)
120
+ if (lowered.includes(t))
121
+ hits++;
122
+ return hits / terms.length;
123
+ }
124
+ /** Content-quality score for a fetched page (deep mode re-ranking). */
125
+ function scorePage(page, terms) {
126
+ let score = 0;
127
+ const domain = page.domain.toLowerCase();
128
+ const url = page.url.toLowerCase();
129
+ const title = page.title.toLowerCase();
130
+ const type = classifySource(domain);
131
+ if (type === "primary" || type === "government" || type === "academic")
132
+ score += 5;
133
+ if (domain.includes("docs") || url.includes("docs") || title.includes("documentation"))
134
+ score += 5;
135
+ if (domain === "github.com" || domain === "gitlab.com")
136
+ score += 4;
137
+ if (["pypi.org", "npmjs.com", "rubygems.org"].includes(domain))
138
+ score -= 2;
139
+ if (page.date)
140
+ score += 1;
141
+ const lowered = page.text.toLowerCase();
142
+ for (const t of terms)
143
+ if (lowered.includes(t))
144
+ score += 1;
145
+ score += Math.min(page.text.length / 4000, 1);
146
+ return score;
147
+ }
148
+ const LOW_VALUE_SNIPPET = ["copy a direct link", "file metadata"];
149
+ /** Pre-fetch quality score for one engine result (snippet-level signals only). */
150
+ function resultQualityScore(c) {
151
+ const url = c.url.toLowerCase();
152
+ const title = c.title.toLowerCase();
153
+ const snippet = c.snippet.toLowerCase();
154
+ let score = Math.max(0, 20 - c.rank);
155
+ if (title.includes("official") || snippet.includes("official"))
156
+ score += 4;
157
+ if (title.includes("documentation") || snippet.includes("documentation") || url.includes("docs"))
158
+ score += 4;
159
+ if (url.includes("github.com") || url.includes("gitlab.com"))
160
+ score += 3;
161
+ if (LOW_VALUE_SNIPPET.some((t) => snippet.includes(t)))
162
+ score -= 10;
163
+ return score;
164
+ }
165
+ /**
166
+ * Merge results from several engines: quality-rank, dedupe by canonical URL
167
+ * (first/best occurrence wins), cap at maxResults.
168
+ */
169
+ function mergeCandidates(candidates, maxResults) {
170
+ const ranked = [...candidates].sort((a, b) => resultQualityScore(b) - resultQualityScore(a));
171
+ const seen = new Set();
172
+ const out = [];
173
+ for (const c of ranked) {
174
+ const key = canonicalizeUrl(c.url);
175
+ if (seen.has(key))
176
+ continue;
177
+ seen.add(key);
178
+ out.push(c);
179
+ if (out.length >= maxResults)
180
+ break;
181
+ }
182
+ return out;
183
+ }
184
+ /** Best-passage bonus used in deep-mode composite scoring. */
185
+ function passageBonus(passages) {
186
+ return passages.length ? passages[0].score * 3 : 0;
187
+ }
188
+ /** Engine-rank decay bonus used in composite scoring. */
189
+ function rankBonus(rank, ceiling) {
190
+ return Math.max(0, ceiling - rank) * 0.2;
191
+ }
package/dist/state.js CHANGED
@@ -14,6 +14,8 @@ class RunState {
14
14
  taskOrder = [];
15
15
  agents = new Map();
16
16
  notes = [];
17
+ phases = [];
18
+ planExcerpt = "";
17
19
  conductorLog = [];
18
20
  operatorNotes = [];
19
21
  usageByModel = new Map();
@@ -29,10 +31,33 @@ class RunState {
29
31
  constructor(pricing = {}) {
30
32
  this.pricing = pricing;
31
33
  }
34
+ /** Sub-states for hierarchical teams, keyed by the owning task id. */
35
+ teams = new Map();
32
36
  apply(ev) {
33
37
  this.lastSeq = ev.seq;
34
38
  this.lastT = ev.t;
35
39
  this.updatedAt = ev.t;
40
+ // Team-stamped events reduce into their team's sub-state so a sub-swarm's
41
+ // hundred tasks never pollute the root task list. Usage still rolls up
42
+ // here — the run's budget/cost is one number.
43
+ const teamId = typeof ev.teamId === "string" ? ev.teamId : undefined;
44
+ if (teamId) {
45
+ let team = this.teams.get(teamId);
46
+ if (!team) {
47
+ team = new RunState(this.pricing);
48
+ this.teams.set(teamId, team);
49
+ }
50
+ const { teamId: _omit, ...rest } = ev;
51
+ team.apply(rest);
52
+ if (ev.type === "usage") {
53
+ const u = ev.usage;
54
+ const model = ev.model ?? "unknown";
55
+ this.usageByModel.set(model, (0, types_1.addUsage)(this.usageByModel.get(model) ?? { ...types_1.ZERO_USAGE }, u));
56
+ this.totalUsage = (0, types_1.addUsage)(this.totalUsage, u);
57
+ this.cost += (0, types_1.usageCost)(u, this.pricing[model]);
58
+ }
59
+ return;
60
+ }
36
61
  switch (ev.type) {
37
62
  case "run.created": {
38
63
  this.meta = ev.meta;
@@ -96,9 +121,21 @@ class RunState {
96
121
  t.report = ev.report;
97
122
  t.reportStatus = ev.status;
98
123
  t.artifacts = ev.artifacts ?? t.artifacts;
124
+ if (Array.isArray(ev.keyFacts))
125
+ t.keyFacts = ev.keyFacts;
126
+ if (Array.isArray(ev.openQuestions))
127
+ t.openQuestions = ev.openQuestions;
128
+ if (Array.isArray(ev.filesTouched))
129
+ t.filesTouched = ev.filesTouched;
99
130
  }
100
131
  break;
101
132
  }
133
+ case "task.checkpoint": {
134
+ const t = this.tasks.get(ev.taskId);
135
+ if (t)
136
+ t.lastCheckpoint = ev.summary;
137
+ break;
138
+ }
102
139
  case "verify.result": {
103
140
  const t = this.tasks.get(ev.taskId);
104
141
  if (t)
@@ -146,18 +183,35 @@ class RunState {
146
183
  }
147
184
  break;
148
185
  }
186
+ case "plan.updated":
187
+ this.planExcerpt = String(ev.excerpt ?? "");
188
+ break;
189
+ case "phase.set":
190
+ this.phases.push({
191
+ t: ev.t,
192
+ name: String(ev.name ?? ""),
193
+ goal: ev.goal,
194
+ exitCriteria: ev.exit_criteria,
195
+ });
196
+ break;
149
197
  case "note.added":
150
198
  this.notes.push({
151
199
  t: ev.t,
152
200
  taskId: ev.taskId,
153
201
  agentId: ev.agentId,
154
202
  key: ev.key,
203
+ kind: ev.kind,
155
204
  text: ev.text,
156
205
  });
157
206
  // Reduced state is held live by the hub and the resume seed — keep
158
- // only the tail that digests/views actually use.
159
- if (this.notes.length > 1000)
160
- this.notes.splice(0, this.notes.length - 1000);
207
+ // only the tail that digests/views actually use. Decisions are never
208
+ // dropped: they anchor the conductor's long-horizon coherence.
209
+ if (this.notes.length > 1000) {
210
+ const decisions = this.notes.filter((n) => n.kind === "decision");
211
+ const rest = this.notes.filter((n) => n.kind !== "decision");
212
+ rest.splice(0, rest.length - Math.max(0, 1000 - decisions.length));
213
+ this.notes = [...decisions, ...rest].sort((a, b) => a.t - b.t);
214
+ }
161
215
  break;
162
216
  case "conductor.say":
163
217
  this.conductorLog.push({ t: ev.t, text: ev.text });