@robzilla1738/agentswarm 0.5.0 → 0.7.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/README.md +29 -12
- package/dist/agent.js +6 -15
- package/dist/cli.js +31 -4
- package/dist/config.js +44 -1
- package/dist/crawltools.js +3 -22
- package/dist/executor.js +276 -60
- package/dist/hub.js +67 -3
- package/dist/journal.js +39 -5
- package/dist/memory.js +17 -11
- package/dist/pdftext.js +211 -0
- package/dist/prompts.js +23 -15
- package/dist/report.js +39 -1
- package/dist/run.js +8 -0
- package/dist/sandbox.js +11 -0
- package/dist/searchcore.js +55 -2
- package/dist/state.js +67 -17
- package/dist/tools.js +208 -19
- package/dist/util.js +117 -3
- package/dist/webtools.js +185 -32
- package/package.json +1 -1
- package/ui/out/404/index.html +1 -1
- package/ui/out/404.html +1 -1
- package/ui/out/_next/static/chunks/677-a62d486d6734bcf3.js +1 -0
- package/ui/out/_next/static/chunks/app/run/page-c29f95c51af08c60.js +1 -0
- package/ui/out/_next/static/chunks/app/settings/page-41a5d8ba43ecfd4a.js +1 -0
- package/ui/out/_next/static/css/{9f7bd82b8e4c762c.css → d95c2ba395730031.css} +1 -1
- package/ui/out/index.html +1 -1
- package/ui/out/index.txt +3 -3
- package/ui/out/run/index.html +1 -1
- package/ui/out/run/index.txt +3 -3
- package/ui/out/settings/index.html +1 -1
- package/ui/out/settings/index.txt +3 -3
- package/ui/out/_next/static/chunks/677-859e8d42add1806b.js +0 -1
- package/ui/out/_next/static/chunks/app/run/page-2420c9e4c963d9b3.js +0 -1
- package/ui/out/_next/static/chunks/app/settings/page-092a6bf42dfde57d.js +0 -1
- /package/ui/out/_next/static/{errjtBR_bKoee8ogLp8xk → JFkx5KtNi0DYyqm_THzbY}/_buildManifest.js +0 -0
- /package/ui/out/_next/static/{errjtBR_bKoee8ogLp8xk → JFkx5KtNi0DYyqm_THzbY}/_ssgManifest.js +0 -0
package/dist/hub.js
CHANGED
|
@@ -43,6 +43,7 @@ const url_1 = require("url");
|
|
|
43
43
|
const config_1 = require("./config");
|
|
44
44
|
const control_1 = require("./control");
|
|
45
45
|
const crawltools_1 = require("./crawltools");
|
|
46
|
+
const webtools_1 = require("./webtools");
|
|
46
47
|
const deepseek_1 = require("./deepseek");
|
|
47
48
|
const providers_1 = require("./providers");
|
|
48
49
|
const journal_1 = require("./journal");
|
|
@@ -83,9 +84,16 @@ function startHub(opts) {
|
|
|
83
84
|
async function handle(req, res, opts) {
|
|
84
85
|
const url = new url_1.URL(req.url || "/", `http://localhost:${opts.port}`);
|
|
85
86
|
const p = url.pathname;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
// Localhost-only CORS. The hub launches runs and reads reports with the
|
|
88
|
+
// operator's keys — a random website's JS must never get a readable
|
|
89
|
+
// response. The dev UI on another localhost port is the one legitimate
|
|
90
|
+
// cross-origin client; everyone else gets no CORS headers at all.
|
|
91
|
+
const origin = String(req.headers.origin || "");
|
|
92
|
+
if (/^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?$/.test(origin)) {
|
|
93
|
+
res.setHeader("access-control-allow-origin", origin);
|
|
94
|
+
res.setHeader("access-control-allow-methods", "GET, POST, DELETE, OPTIONS");
|
|
95
|
+
res.setHeader("access-control-allow-headers", "content-type");
|
|
96
|
+
}
|
|
89
97
|
if (req.method === "OPTIONS") {
|
|
90
98
|
res.writeHead(204);
|
|
91
99
|
res.end();
|
|
@@ -159,6 +167,49 @@ async function api(req, res, url, opts) {
|
|
|
159
167
|
const r = await (0, sandbox_1.testSandbox)(cfg, kind);
|
|
160
168
|
return sendJson(res, 200, { kind, ...r });
|
|
161
169
|
}
|
|
170
|
+
// Settings diagnostics: prove the search engines / crawl backend actually
|
|
171
|
+
// work with the saved keys before a mission depends on them.
|
|
172
|
+
if (p === "/api/search/test" && method === "POST") {
|
|
173
|
+
const q = "open source vector database";
|
|
174
|
+
const probe = async (engine, fn) => {
|
|
175
|
+
try {
|
|
176
|
+
const hits = await fn();
|
|
177
|
+
return { engine, ok: hits.length > 0, detail: `${hits.length} result(s)` };
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
return { engine, ok: false, detail: (0, util_1.errMsg)(e) };
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const checks = (0, webtools_1.searchEngines)(cfg).map((e) => probe(e.name, () => e.search(q, 3)));
|
|
184
|
+
const engines = await Promise.all(checks);
|
|
185
|
+
return sendJson(res, 200, { ok: engines.some((e) => e.ok), engines });
|
|
186
|
+
}
|
|
187
|
+
if (p === "/api/crawl/test" && method === "POST") {
|
|
188
|
+
const backend = (0, crawltools_1.resolveCrawlBackend)(cfg);
|
|
189
|
+
if (!backend) {
|
|
190
|
+
return sendJson(res, 200, { ok: false, backend: null, detail: "no crawl backend configured — add a key first" });
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
if ((0, crawltools_1.hasScrapeBackend)(cfg)) {
|
|
194
|
+
const text = await (0, crawltools_1.scrapeUrl)(cfg, "https://example.com/");
|
|
195
|
+
return sendJson(res, 200, {
|
|
196
|
+
ok: Boolean(text && text.length > 50),
|
|
197
|
+
backend,
|
|
198
|
+
detail: text ? `scraped ${text.length} chars` : "empty scrape result",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// deepcrawl has no single-page scrape — smoke a 1-page crawl instead.
|
|
202
|
+
const out = await (0, crawltools_1.crawlSite)(cfg, { url: "https://example.com/", maxPages: 1 });
|
|
203
|
+
return sendJson(res, 200, {
|
|
204
|
+
ok: out.pages.length > 0,
|
|
205
|
+
backend,
|
|
206
|
+
detail: out.pages.length ? `crawled ${out.pages.length} page(s)` : out.warnings.join("; ") || "no pages",
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
catch (e) {
|
|
210
|
+
return sendJson(res, 200, { ok: false, backend, detail: (0, util_1.errMsg)(e) });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
162
213
|
if (p === "/api/models" && method === "GET") {
|
|
163
214
|
try {
|
|
164
215
|
const models = await (0, deepseek_1.listModels)(cfg);
|
|
@@ -297,6 +348,14 @@ async function api(req, res, url, opts) {
|
|
|
297
348
|
res.end(fs.readFileSync(file));
|
|
298
349
|
return;
|
|
299
350
|
}
|
|
351
|
+
if (sub === "/plan" && method === "GET") {
|
|
352
|
+
const file = path.join((0, config_1.runDir)(id), "artifacts", "mission-plan.md");
|
|
353
|
+
if (!fs.existsSync(file))
|
|
354
|
+
return sendJson(res, 404, { error: "no plan yet" });
|
|
355
|
+
res.writeHead(200, { "content-type": "text/markdown; charset=utf-8" });
|
|
356
|
+
res.end(fs.readFileSync(file));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
300
359
|
if (sub === "/artifacts" && method === "GET") {
|
|
301
360
|
return sendJson(res, 200, { artifacts: listArtifactFiles(id) });
|
|
302
361
|
}
|
|
@@ -429,6 +488,9 @@ function publicConfig(cfg) {
|
|
|
429
488
|
reasoningEffort: cfg.reasoningEffort,
|
|
430
489
|
safeMode: cfg.safeMode,
|
|
431
490
|
contextTokenLimit: cfg.contextTokenLimit,
|
|
491
|
+
contextWindows: cfg.contextWindows,
|
|
492
|
+
cheapModel: cfg.cheapModel,
|
|
493
|
+
strongModel: cfg.strongModel,
|
|
432
494
|
knownModels,
|
|
433
495
|
pricing: cfg.pricing,
|
|
434
496
|
};
|
|
@@ -482,6 +544,8 @@ function snapshot(state, id) {
|
|
|
482
544
|
operatorNotes: state.operatorNotes,
|
|
483
545
|
usageByModel: Object.fromEntries(state.usageByModel),
|
|
484
546
|
cost: state.cost,
|
|
547
|
+
budgetSeries: state.budgetSeries,
|
|
548
|
+
planExcerpt: state.planExcerpt,
|
|
485
549
|
finalSummary: state.finalSummary,
|
|
486
550
|
finalReportPath: state.finalReportPath,
|
|
487
551
|
live: (0, run_1.isRunLive)(id),
|
package/dist/journal.js
CHANGED
|
@@ -88,11 +88,14 @@ class Journal {
|
|
|
88
88
|
}
|
|
89
89
|
return ev;
|
|
90
90
|
}
|
|
91
|
+
/** The chunk an async drain is writing right now — flushSync must see it. */
|
|
92
|
+
inFlight = "";
|
|
91
93
|
async drain() {
|
|
92
94
|
if (!this.buf)
|
|
93
95
|
return;
|
|
94
96
|
const chunk = this.buf;
|
|
95
97
|
this.buf = "";
|
|
98
|
+
this.inFlight = chunk;
|
|
96
99
|
try {
|
|
97
100
|
await fs.promises.appendFile(this.file, chunk, "utf8");
|
|
98
101
|
this.failures = 0;
|
|
@@ -107,16 +110,26 @@ class Journal {
|
|
|
107
110
|
process.stderr.write(`agentswarm: journal writes are failing (${String(e)}); run state is no longer durable\n`);
|
|
108
111
|
}
|
|
109
112
|
}
|
|
113
|
+
finally {
|
|
114
|
+
this.inFlight = "";
|
|
115
|
+
}
|
|
110
116
|
}
|
|
111
117
|
flush() {
|
|
112
118
|
return this.chain.then(() => this.drain());
|
|
113
119
|
}
|
|
114
|
-
/**
|
|
120
|
+
/**
|
|
121
|
+
* Last-gasp synchronous flush for signal handlers and exit paths. Includes
|
|
122
|
+
* any chunk a pending async drain holds: process.exit would abandon that
|
|
123
|
+
* write, silently losing just-settled events. If the abandoned write did
|
|
124
|
+
* land first, the chunk appears twice — readers dedupe by seq.
|
|
125
|
+
*/
|
|
115
126
|
flushSync() {
|
|
116
|
-
|
|
127
|
+
const pending = this.inFlight + this.buf;
|
|
128
|
+
if (!pending)
|
|
117
129
|
return;
|
|
118
130
|
try {
|
|
119
|
-
fs.appendFileSync(this.file,
|
|
131
|
+
fs.appendFileSync(this.file, pending, "utf8");
|
|
132
|
+
this.inFlight = "";
|
|
120
133
|
this.buf = "";
|
|
121
134
|
}
|
|
122
135
|
catch {
|
|
@@ -136,7 +149,24 @@ function readEvents(runDirPath) {
|
|
|
136
149
|
catch {
|
|
137
150
|
return [];
|
|
138
151
|
}
|
|
139
|
-
return parseLines(raw).events;
|
|
152
|
+
return dedupeBySeq(parseLines(raw).events);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Seq is strictly increasing in a healthy journal; a chunk can appear twice
|
|
156
|
+
* when a signal-handler flushSync raced an in-flight async append. Replays of
|
|
157
|
+
* already-seen seqs are dropped.
|
|
158
|
+
*/
|
|
159
|
+
function dedupeBySeq(events, lastSeq = 0) {
|
|
160
|
+
let max = lastSeq;
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const ev of events) {
|
|
163
|
+
if (typeof ev.seq === "number" && ev.seq <= max)
|
|
164
|
+
continue;
|
|
165
|
+
if (typeof ev.seq === "number")
|
|
166
|
+
max = ev.seq;
|
|
167
|
+
out.push(ev);
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
140
170
|
}
|
|
141
171
|
function lastSeq(runDirPath) {
|
|
142
172
|
const evs = readEvents(runDirPath);
|
|
@@ -155,6 +185,7 @@ function readNewEvents(file, state) {
|
|
|
155
185
|
// Truncated/rewritten (should not happen) — start over.
|
|
156
186
|
state.offset = 0;
|
|
157
187
|
state.carry = "";
|
|
188
|
+
state.lastSeq = 0;
|
|
158
189
|
}
|
|
159
190
|
if (stat.size === state.offset)
|
|
160
191
|
return [];
|
|
@@ -172,7 +203,10 @@ function readNewEvents(file, state) {
|
|
|
172
203
|
const text = state.carry + buf.toString("utf8", 0, n);
|
|
173
204
|
const parsed = parseLines(text, true);
|
|
174
205
|
state.carry = parsed.carry;
|
|
175
|
-
|
|
206
|
+
const fresh = dedupeBySeq(parsed.events, state.lastSeq ?? 0);
|
|
207
|
+
if (fresh.length)
|
|
208
|
+
state.lastSeq = fresh[fresh.length - 1].seq;
|
|
209
|
+
out.push(...fresh);
|
|
176
210
|
}
|
|
177
211
|
state.offset += read;
|
|
178
212
|
return out;
|
package/dist/memory.js
CHANGED
|
@@ -38,7 +38,6 @@ exports.loadMemory = loadMemory;
|
|
|
38
38
|
exports.appendMemory = appendMemory;
|
|
39
39
|
exports.memoryBlock = memoryBlock;
|
|
40
40
|
const crypto = __importStar(require("crypto"));
|
|
41
|
-
const fs = __importStar(require("fs"));
|
|
42
41
|
const path = __importStar(require("path"));
|
|
43
42
|
const config_1 = require("./config");
|
|
44
43
|
const util_1 = require("./util");
|
|
@@ -48,20 +47,27 @@ function memoryFile(cwd) {
|
|
|
48
47
|
return path.join((0, config_1.home)(), "memory", `${hash}.json`);
|
|
49
48
|
}
|
|
50
49
|
function loadMemory(cwd) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return Array.isArray(raw?.entries) ? raw.entries : [];
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
50
|
+
const raw = (0, util_1.readJson)(memoryFile(cwd), {});
|
|
51
|
+
if (!Array.isArray(raw.entries))
|
|
56
52
|
return [];
|
|
57
|
-
|
|
53
|
+
// Memory is best-effort and the file is user-editable: one malformed entry
|
|
54
|
+
// must degrade to "forgotten", never crash a run at startup.
|
|
55
|
+
return raw.entries.filter((e) => !!e &&
|
|
56
|
+
typeof e === "object" &&
|
|
57
|
+
typeof e.mission === "string" &&
|
|
58
|
+
typeof e.summary === "string" &&
|
|
59
|
+
typeof e.status === "string" &&
|
|
60
|
+
Number.isFinite(e.finishedAt) &&
|
|
61
|
+
Array.isArray(e.keyDecisions) &&
|
|
62
|
+
e.keyDecisions.every((d) => typeof d === "string"));
|
|
58
63
|
}
|
|
59
64
|
function appendMemory(cwd, entry) {
|
|
60
65
|
try {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
66
|
+
// Same-run entries replace (interim → final); writeJson is temp+rename so
|
|
67
|
+
// a crash mid-write never loses the prior history.
|
|
68
|
+
const prior = loadMemory(cwd).filter((e) => !(entry.runId && e.runId === entry.runId));
|
|
69
|
+
const entries = [...prior, entry].slice(-MAX_ENTRIES);
|
|
70
|
+
(0, util_1.writeJson)(memoryFile(cwd), { cwd: path.resolve(cwd), entries });
|
|
65
71
|
}
|
|
66
72
|
catch {
|
|
67
73
|
/* memory is best-effort */
|
package/dist/pdftext.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.extractPdfText = extractPdfText;
|
|
37
|
+
const zlib = __importStar(require("zlib"));
|
|
38
|
+
/**
|
|
39
|
+
* Minimal zero-dependency PDF text extraction: inflate FlateDecode content
|
|
40
|
+
* streams (Node's built-in zlib) and interpret the text-showing operators
|
|
41
|
+
* (Tj / TJ / ' / "). Good enough for most digitally-produced text PDFs;
|
|
42
|
+
* returns null for scanned, encrypted, or exotic-encoding documents so the
|
|
43
|
+
* caller can tell the agent to find an HTML source instead.
|
|
44
|
+
*/
|
|
45
|
+
function extractPdfText(buf) {
|
|
46
|
+
if (buf.subarray(0, 5).toString("latin1") !== "%PDF-")
|
|
47
|
+
return null;
|
|
48
|
+
// latin1 preserves bytes 1:1, so stream offsets in the string match the buffer.
|
|
49
|
+
const raw = buf.toString("latin1");
|
|
50
|
+
const pages = (raw.match(/\/Type\s*\/Pages?\b/g) || []).filter((m) => !/Pages/.test(m)).length || 1;
|
|
51
|
+
let text = "";
|
|
52
|
+
const streamRe = /<<([\s\S]{0,2000}?)>>\s*stream\r?\n/g;
|
|
53
|
+
let m;
|
|
54
|
+
while ((m = streamRe.exec(raw))) {
|
|
55
|
+
const dict = m[1];
|
|
56
|
+
const start = m.index + m[0].length;
|
|
57
|
+
const end = raw.indexOf("endstream", start);
|
|
58
|
+
if (end < 0)
|
|
59
|
+
continue;
|
|
60
|
+
streamRe.lastIndex = end;
|
|
61
|
+
// Only plain or Flate-compressed streams are supported.
|
|
62
|
+
if (/\/Filter/.test(dict) && !/FlateDecode/.test(dict))
|
|
63
|
+
continue;
|
|
64
|
+
let len = end;
|
|
65
|
+
while (len > start && (raw[len - 1] === "\n" || raw[len - 1] === "\r"))
|
|
66
|
+
len--;
|
|
67
|
+
let data = buf.subarray(start, len);
|
|
68
|
+
if (/FlateDecode/.test(dict)) {
|
|
69
|
+
try {
|
|
70
|
+
data = zlib.inflateSync(data);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const content = data.toString("latin1");
|
|
77
|
+
if (!/\bBT\b/.test(content))
|
|
78
|
+
continue; // not a text content stream
|
|
79
|
+
const extracted = extractFromContent(content);
|
|
80
|
+
if (extracted.trim())
|
|
81
|
+
text += extracted + "\n";
|
|
82
|
+
}
|
|
83
|
+
const cleaned = text
|
|
84
|
+
.replace(/[^\S\n]+/g, " ")
|
|
85
|
+
.replace(/ ?\n ?/g, "\n")
|
|
86
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
87
|
+
.trim();
|
|
88
|
+
// CID/Type0 fonts yield glyph-index garbage; require a body of real characters.
|
|
89
|
+
const printable = cleaned.replace(/[^\x20-\x7E\n -]/g, "");
|
|
90
|
+
if (printable.replace(/\s/g, "").length < 40)
|
|
91
|
+
return null;
|
|
92
|
+
return { text: printable, pages };
|
|
93
|
+
}
|
|
94
|
+
/** Walk a content stream, collecting strings shown by Tj/TJ/'/" with newline heuristics. */
|
|
95
|
+
function extractFromContent(src) {
|
|
96
|
+
let out = "";
|
|
97
|
+
let pending = [];
|
|
98
|
+
const n = src.length;
|
|
99
|
+
let i = 0;
|
|
100
|
+
while (i < n) {
|
|
101
|
+
const ch = src[i];
|
|
102
|
+
if (ch === "(") {
|
|
103
|
+
const [s, next] = parseLiteralString(src, i);
|
|
104
|
+
pending.push(s);
|
|
105
|
+
i = next;
|
|
106
|
+
}
|
|
107
|
+
else if (ch === "<" && src[i + 1] !== "<") {
|
|
108
|
+
const close = src.indexOf(">", i + 1);
|
|
109
|
+
if (close < 0)
|
|
110
|
+
break;
|
|
111
|
+
pending.push(decodeHexString(src.slice(i + 1, close)));
|
|
112
|
+
i = close + 1;
|
|
113
|
+
}
|
|
114
|
+
else if (ch === "%") {
|
|
115
|
+
// comment to end of line
|
|
116
|
+
while (i < n && src[i] !== "\n" && src[i] !== "\r")
|
|
117
|
+
i++;
|
|
118
|
+
}
|
|
119
|
+
else if (/[A-Za-z'"*]/.test(ch)) {
|
|
120
|
+
let j = i;
|
|
121
|
+
while (j < n && /[A-Za-z'"*]/.test(src[j]))
|
|
122
|
+
j++;
|
|
123
|
+
const op = src.slice(i, j);
|
|
124
|
+
if (op === "Tj" || op === "TJ") {
|
|
125
|
+
out += pending.join("");
|
|
126
|
+
}
|
|
127
|
+
else if (op === "'" || op === '"') {
|
|
128
|
+
out += "\n" + pending.join("");
|
|
129
|
+
}
|
|
130
|
+
else if (op === "Td" || op === "TD" || op === "T*" || op === "Tm" || op === "ET") {
|
|
131
|
+
if (pending.length)
|
|
132
|
+
out += pending.join("");
|
|
133
|
+
if (!out.endsWith("\n"))
|
|
134
|
+
out += "\n";
|
|
135
|
+
}
|
|
136
|
+
pending = [];
|
|
137
|
+
i = j;
|
|
138
|
+
}
|
|
139
|
+
else if (ch === "-" || (ch >= "0" && ch <= "9") || ch === ".") {
|
|
140
|
+
let j = i + 1;
|
|
141
|
+
while (j < n && /[0-9.]/.test(src[j]))
|
|
142
|
+
j++;
|
|
143
|
+
// Large negative kerning inside a TJ array is a word gap.
|
|
144
|
+
const num = parseFloat(src.slice(i, j));
|
|
145
|
+
if (num <= -180 && pending.length && !pending[pending.length - 1].endsWith(" "))
|
|
146
|
+
pending.push(" ");
|
|
147
|
+
i = j;
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
i++;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/** PDF literal string: balanced parens, backslash escapes, octal codes. */
|
|
156
|
+
function parseLiteralString(src, start) {
|
|
157
|
+
let out = "";
|
|
158
|
+
let depth = 0;
|
|
159
|
+
let i = start;
|
|
160
|
+
for (; i < src.length; i++) {
|
|
161
|
+
const ch = src[i];
|
|
162
|
+
if (ch === "\\") {
|
|
163
|
+
const next = src[i + 1];
|
|
164
|
+
if (next >= "0" && next <= "7") {
|
|
165
|
+
let oct = "";
|
|
166
|
+
for (let k = 1; k <= 3 && src[i + k] >= "0" && src[i + k] <= "7"; k++)
|
|
167
|
+
oct += src[i + k];
|
|
168
|
+
out += String.fromCharCode(parseInt(oct, 8));
|
|
169
|
+
i += oct.length;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const map = { n: "\n", r: "\r", t: "\t", b: "\b", f: "\f", "(": "(", ")": ")", "\\": "\\" };
|
|
173
|
+
out += map[next] ?? next ?? "";
|
|
174
|
+
i++;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else if (ch === "(") {
|
|
178
|
+
depth++;
|
|
179
|
+
if (depth > 1)
|
|
180
|
+
out += ch;
|
|
181
|
+
}
|
|
182
|
+
else if (ch === ")") {
|
|
183
|
+
depth--;
|
|
184
|
+
if (depth === 0) {
|
|
185
|
+
i++;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
out += ch;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
out += ch;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return [out, i];
|
|
195
|
+
}
|
|
196
|
+
/** PDF hex string: byte pairs; a UTF-16BE BOM switches to two-byte chars. */
|
|
197
|
+
function decodeHexString(hex) {
|
|
198
|
+
const clean = hex.replace(/[^0-9a-fA-F]/g, "");
|
|
199
|
+
const bytes = [];
|
|
200
|
+
for (let i = 0; i + 1 < clean.length; i += 2)
|
|
201
|
+
bytes.push(parseInt(clean.slice(i, i + 2), 16));
|
|
202
|
+
if (clean.length % 2)
|
|
203
|
+
bytes.push(parseInt(clean[clean.length - 1] + "0", 16));
|
|
204
|
+
if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
205
|
+
let s = "";
|
|
206
|
+
for (let i = 2; i + 1 < bytes.length; i += 2)
|
|
207
|
+
s += String.fromCharCode((bytes[i] << 8) | bytes[i + 1]);
|
|
208
|
+
return s;
|
|
209
|
+
}
|
|
210
|
+
return bytes.map((b) => String.fromCharCode(b)).join("");
|
|
211
|
+
}
|
package/dist/prompts.js
CHANGED
|
@@ -122,7 +122,7 @@ function taskTable(tasks) {
|
|
|
122
122
|
return "(no tasks yet)";
|
|
123
123
|
const line = (t) => {
|
|
124
124
|
const deps = t.deps.length ? ` deps:[${t.deps.join(",")}]` : "";
|
|
125
|
-
const extra = t.status === "failed" && t.error ? ` — ${(0, util_1.clip)(t.error,
|
|
125
|
+
const extra = (t.status === "failed" || t.status === "blocked") && t.error ? ` — ${(0, util_1.clip)(t.error, 120)}` : "";
|
|
126
126
|
return `${t.id} [${t.status}${t.attempt > 1 ? ` a${t.attempt}` : ""}] (${t.role})${deps} ${(0, util_1.clip)(t.title, 70)}${extra}`;
|
|
127
127
|
};
|
|
128
128
|
const settled = tasks.filter((t) => ["done", "failed", "blocked"].includes(t.status));
|
|
@@ -146,6 +146,13 @@ function taskTable(tasks) {
|
|
|
146
146
|
}
|
|
147
147
|
return out.join("\n");
|
|
148
148
|
}
|
|
149
|
+
function sourcesLine(t, max = 6) {
|
|
150
|
+
if (!t.sources?.length)
|
|
151
|
+
return "";
|
|
152
|
+
const shown = t.sources.slice(0, max).map((s) => s.url);
|
|
153
|
+
const more = t.sources.length > max ? ` (+${t.sources.length - max} more)` : "";
|
|
154
|
+
return `\nsources: ${shown.join(" · ")}${more}`;
|
|
155
|
+
}
|
|
149
156
|
function reportBlock(t) {
|
|
150
157
|
const head = `── ${t.id} (${t.role}) "${(0, util_1.clip)(t.title, 60)}" → ${t.status.toUpperCase()}${t.attempt > 1 ? ` (attempt ${t.attempt})` : ""}`;
|
|
151
158
|
const body = t.report ? (0, util_1.clip)(t.report, 1600) : t.error ? `error: ${(0, util_1.clip)(t.error, 400)}` : "(no report)";
|
|
@@ -154,7 +161,7 @@ function reportBlock(t) {
|
|
|
154
161
|
const files = t.filesTouched?.length ? `\nfiles touched: ${t.filesTouched.join(", ")}` : "";
|
|
155
162
|
const arts = t.artifacts.length ? `\nartifacts: ${t.artifacts.join(", ")}` : "";
|
|
156
163
|
const fb = t.feedback ? `\nverifier: ${(0, util_1.clip)(t.feedback, 300)}` : "";
|
|
157
|
-
return `${head}\n${body}${facts}${open}${files}${arts}${fb}`;
|
|
164
|
+
return `${head}\n${body}${facts}${open}${files}${arts}${sourcesLine(t)}${fb}`;
|
|
158
165
|
}
|
|
159
166
|
/**
|
|
160
167
|
* Compact dependency context for a downstream worker: structured handoff
|
|
@@ -168,11 +175,12 @@ function depReportBlock(t) {
|
|
|
168
175
|
const arts = t.artifacts.length ? `\nartifacts: ${t.artifacts.join(", ")}` : "";
|
|
169
176
|
const full = (t.report ?? "").length > 1200 ? `\n(excerpt — full text: read_report("${t.id}"))` : "";
|
|
170
177
|
const body = t.report ? (0, util_1.clip)(t.report, 1200) : t.error ? `error: ${(0, util_1.clip)(t.error, 400)}` : "(no report)";
|
|
171
|
-
return `${head}\n${body}${facts}${files}${arts}${full}`;
|
|
178
|
+
return `${head}\n${body}${facts}${files}${arts}${sourcesLine(t)}${full}`;
|
|
172
179
|
}
|
|
173
180
|
// ============================================================ workers
|
|
174
181
|
const ROLE_HINTS = {
|
|
175
|
-
researcher: "Research craft: be exhaustive. Run deep web_search (deep=true, high count) across several distinct phrasings — pull DOZENS of sources for your sub-question, not three. Triangulate across independent sources; prefer primary docs and official sources over blog spam; capture exact figures, dates, and URLs, and keep the quotable passages the search returns. Record key findings as blackboard notes (with
|
|
182
|
+
researcher: "Research craft: be exhaustive. Run deep web_search (deep=true, high count) across several distinct phrasings — pull DOZENS of sources for your sub-question, not three. Triangulate across independent sources; prefer primary docs and official sources over blog spam; capture exact figures, dates, and URLs, and keep the quotable passages the search returns. Record key findings as blackboard notes (with url=<source>) and save a structured markdown file of your sources+findings as an artifact so the synthesizer can build on it. " +
|
|
183
|
+
"A finding without a source is an opinion: list EVERY source your findings rest on in report(...)'s `sources` field (url + what it supports) — only sources reported there can be cited in the final deliverable. When independent sources disagree on a material fact, post note(kind:'conflict') naming both sources and the discrepancy — never silently pick one. For scientific or technical questions, also run academic_search (arXiv + Crossref) — peer-reviewed beats blog posts. " +
|
|
176
184
|
"If a crawl_site tool is available, use it to ingest whole documentation sites or multi-page sources into local markdown files, then read the saved files — far cheaper and broader than fetching pages one by one.",
|
|
177
185
|
coder: "Engineering craft: read existing code before changing it; match its conventions; build/run/test after every meaningful change and include the command + result in your report. Leave the tree compiling.",
|
|
178
186
|
analyst: "Analysis craft: quantify wherever possible; state assumptions explicitly; separate observation from interpretation; sanity-check numbers twice.",
|
|
@@ -209,7 +217,7 @@ OPERATING PROTOCOL
|
|
|
209
217
|
- You are fully autonomous. Never ask questions; decide and act.
|
|
210
218
|
- Plan briefly, then act in small verified steps: after changing anything, prove it worked (run it, read it back, test it).
|
|
211
219
|
- Evidence over assumption: read before you edit; check outputs; cite concrete paths, commands and numbers.
|
|
212
|
-
- Be token-lean: targeted reads (line ranges,
|
|
220
|
+
- Be token-lean: targeted reads (line ranges, grep_files) over wholesale dumps; don't re-read unchanged files. Several edits to one file → one replace_in_file call with edits[].
|
|
213
221
|
- Post durable discoveries other agents will need to the blackboard with note(...) — facts only, used sparingly.
|
|
214
222
|
- Editing files other tasks might also touch? First search_notes for claims, then post note(kind:"claim", key:"<path>") before editing. Claims are advisory — coordinate, don't fight.
|
|
215
223
|
- Save deliverable files with save_artifact so the operator sees them. Pick the format that genuinely fits the deliverable — structured data as .csv/.json, polished documents as self-contained .html, code as runnable files — not everything is a markdown report.
|
|
@@ -217,7 +225,7 @@ OPERATING PROTOCOL
|
|
|
217
225
|
- Genuinely impossible / missing prerequisite → report(status:"blocked", …) early instead of thrashing.
|
|
218
226
|
- You have at most ${opts.maxSteps} tool steps. Budget them.
|
|
219
227
|
- Dependency reports above are excerpts; use read_report(task_id) for full text, and search_notes(query) to find facts posted earlier in the run.
|
|
220
|
-
- ALWAYS end by calling report(...). The conductor sees ONLY that report — it is the entire value of your work. Specific beats vague: what you did, what you verified, key findings, exact paths. Fill key_facts (standalone facts downstream tasks need), open_questions, and files_touched — they are handed verbatim to dependent tasks.
|
|
228
|
+
- ALWAYS end by calling report(...). The conductor sees ONLY that report — it is the entire value of your work. Specific beats vague: what you did, what you verified, key findings, exact paths. Fill key_facts (standalone facts downstream tasks need), open_questions, and files_touched — they are handed verbatim to dependent tasks. If your work drew on the web, fill sources (url + what it supports): only sources reported there can be cited in the final deliverable.
|
|
221
229
|
${roleHint ? "\n" + roleHint : ""}`;
|
|
222
230
|
}
|
|
223
231
|
exports.WORKER_KICKOFF = "Begin now. Work the task to completion, then call report(...).";
|
|
@@ -227,7 +235,7 @@ function forcedFinal(reason) {
|
|
|
227
235
|
return `${reason} Stop working and call your terminal tool RIGHT NOW with your best honest account: what you completed, what you verified, what remains.`;
|
|
228
236
|
}
|
|
229
237
|
// ============================================================ verifier
|
|
230
|
-
function verifierSystem(meta, task) {
|
|
238
|
+
function verifierSystem(meta, task, depReports = "") {
|
|
231
239
|
return `You are an adversarial verification agent. A worker claims it completed this task — your job is to try to falsify that claim with evidence.
|
|
232
240
|
|
|
233
241
|
MISSION (for context): ${(0, util_1.clip)(meta.mission, 400)}
|
|
@@ -238,18 +246,18 @@ ${task.context ? `Context: ${(0, util_1.clip)(task.context, 600)}` : ""}
|
|
|
238
246
|
Worker's report:
|
|
239
247
|
${(0, util_1.clip)(task.report ?? "", 2400)}
|
|
240
248
|
${task.artifacts.length ? `Claimed artifacts: ${task.artifacts.join(", ")}` : ""}
|
|
241
|
-
|
|
249
|
+
${depReports ? `\nUPSTREAM INPUTS (settled dependency reports — what this task had to build on; judge completeness against them):\n${depReports}\n` : ""}
|
|
242
250
|
Working directory: ${meta.cwd}
|
|
243
251
|
|
|
244
252
|
PROTOCOL
|
|
245
253
|
- Do NOT trust the report. Verify concretely with tools: read the files it claims to have written, run the build/tests/commands, fetch the URLs, check the numbers. You see only the worker's CLAIMS — gather your own evidence; do not assume shared context.
|
|
246
254
|
- RUBRIC — fail unless all hold:
|
|
247
|
-
1. Completeness: every part of the objective and its "Done when" criteria is addressed.
|
|
255
|
+
1. Completeness: every part of the objective and its "Done when" criteria is addressed${depReports ? " (including everything the upstream inputs handed over)" : ""}.
|
|
248
256
|
2. Evidence: each substantive claim in the report is backed by something you verified yourself.
|
|
249
257
|
3. Deliverables: claimed files/artifacts exist, are non-trivial (not stubs/placeholders), and match what the report says about them.
|
|
250
258
|
4. Correctness: commands/builds/tests the task implies actually succeed when you run them.
|
|
251
259
|
- Spot-check depth over exhaustive breadth; ~5-12 tool steps.
|
|
252
|
-
- Then call verdict(pass, feedback). On fail,
|
|
260
|
+
- Then call verdict(pass, feedback, issues). On fail, ALSO fill issues — one entry per concrete problem with the evidence you gathered and the exact change needed; the worker's retry sees them verbatim. On pass, feedback is one line citing the evidence you checked.`;
|
|
253
261
|
}
|
|
254
262
|
exports.VERIFIER_KICKOFF = "Verify now, then call verdict(...).";
|
|
255
263
|
// ============================================================ synthesizer
|
|
@@ -265,13 +273,13 @@ Conductor's closing notes: ${opts.finishNotes || "(none)"}
|
|
|
265
273
|
ALL TASK REPORTS
|
|
266
274
|
${opts.reports}
|
|
267
275
|
|
|
268
|
-
${opts.blackboard ? `BLACKBOARD\n${opts.blackboard}\n` : ""}${opts.artifactList ? `ARTIFACTS ON DISK\n${opts.artifactList}\n` : ""}
|
|
276
|
+
${opts.sources ? `SOURCES (numbered, deduplicated from the task reports — the only sources that exist)\n${opts.sources}\n\n` : ""}${opts.blackboard ? `BLACKBOARD\n${opts.blackboard}\n` : ""}${opts.artifactList ? `ARTIFACTS ON DISK\n${opts.artifactList}\n` : ""}
|
|
269
277
|
Working directory: ${opts.meta.cwd}
|
|
270
278
|
|
|
271
279
|
PROTOCOL
|
|
272
280
|
- You may read files (read_file / list_dir) to confirm specifics before writing — verify key claims you repeat.
|
|
273
281
|
- The mission's PRIMARY deliverable should exist in the format that serves it best, not only as prose. If the task reports produced data, comparisons, or rankings that the artifacts don't already capture in a structured form, save them now with save_artifact (e.g. data/results.csv, data/findings.json) before submitting. Don't duplicate artifacts that already exist — point to them.
|
|
274
|
-
- Then call submit_final with:
|
|
282
|
+
${opts.sources ? `- CITE YOUR SOURCES: where a claim rests on a numbered source, cite it inline as [n]. End report_markdown with a \`## Sources\` section listing each number you actually cited as a markdown link ([n] [title](url)). Never invent a source or cite a number not in the list. Where sources conflict, present both positions with their citations — do not silently pick one.\n` : ""}- Then call submit_final with:
|
|
275
283
|
• report_markdown — the deliverable document. Structure: # title; **Outcome** first (did the mission succeed, headline results); then What was built/found with evidence and exact paths; How to use/run it (if applicable); Open issues & recommended next steps. Write for the operator: complete, concrete, zero filler. Use real markdown tables for tabular findings. (A styled HTML rendering is generated automatically — do not hand-write one.)
|
|
276
284
|
• summary — ≤8 sentences for the console.
|
|
277
285
|
- The report stands alone: a reader who saw nothing else must understand what happened and where everything is.`;
|
|
@@ -292,7 +300,7 @@ ${reports}
|
|
|
292
300
|
|
|
293
301
|
Reply with EXACTLY "COMPLETE" if the mission's requirements are genuinely covered. Otherwise reply with a short numbered list of concrete gaps (max 5), each one actionable enough to become a task. Do not invent nice-to-haves — only true gaps against the stated mission.`;
|
|
294
302
|
}
|
|
295
|
-
function synthCheckPrompt(mission, reports, finalReport) {
|
|
303
|
+
function synthCheckPrompt(mission, reports, finalReport, sources) {
|
|
296
304
|
return `You are checking a final mission report for faithfulness before delivery. Compare it against the underlying task reports.
|
|
297
305
|
|
|
298
306
|
MISSION
|
|
@@ -301,10 +309,10 @@ ${mission}
|
|
|
301
309
|
TASK REPORTS (ground truth)
|
|
302
310
|
${reports}
|
|
303
311
|
|
|
304
|
-
FINAL REPORT (to check)
|
|
312
|
+
${sources ? `SOURCE LIST (the only citable sources)\n${sources}\n\n` : ""}FINAL REPORT (to check)
|
|
305
313
|
${finalReport}
|
|
306
314
|
|
|
307
|
-
Reply with EXACTLY "OK" if the final report's claims are supported by the task reports and nothing material is misrepresented or fabricated. Otherwise list the specific discrepancies (max 5), each citing what the final report says vs what the task reports support.`;
|
|
315
|
+
Reply with EXACTLY "OK" if the final report's claims are supported by the task reports and nothing material is misrepresented or fabricated${sources ? ", its inline [n] citations all reference numbers that exist in the source list, and no key web-derived factual claim is left uncited" : ""}. Otherwise list the specific discrepancies (max 5), each citing what the final report says vs what the task reports support.`;
|
|
308
316
|
}
|
|
309
317
|
// ============================================================ compaction
|
|
310
318
|
function compactorPrompt(serialized) {
|
package/dist/report.js
CHANGED
|
@@ -11,8 +11,46 @@
|
|
|
11
11
|
* broken markup.
|
|
12
12
|
*/
|
|
13
13
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.aggregateSources = aggregateSources;
|
|
15
|
+
exports.sourcesBlock = sourcesBlock;
|
|
14
16
|
exports.mdToHtml = mdToHtml;
|
|
15
17
|
exports.renderFinalHtml = renderFinalHtml;
|
|
18
|
+
const searchcore_1 = require("./searchcore");
|
|
19
|
+
const util_1 = require("./util");
|
|
20
|
+
/**
|
|
21
|
+
* Dedupe every task's reported sources (by canonical URL) into one numbered
|
|
22
|
+
* bibliography for the synthesizer. First occurrence wins the number; later
|
|
23
|
+
* tasks fill in missing titles/dates.
|
|
24
|
+
*/
|
|
25
|
+
function aggregateSources(tasks) {
|
|
26
|
+
const byKey = new Map();
|
|
27
|
+
for (const t of tasks) {
|
|
28
|
+
for (const s of t.sources ?? []) {
|
|
29
|
+
const key = (0, searchcore_1.canonicalizeUrl)(s.url);
|
|
30
|
+
const cur = byKey.get(key);
|
|
31
|
+
if (cur) {
|
|
32
|
+
if (!cur.taskIds.includes(t.id))
|
|
33
|
+
cur.taskIds.push(t.id);
|
|
34
|
+
if (!cur.title && s.title)
|
|
35
|
+
cur.title = s.title;
|
|
36
|
+
if (!cur.date && s.date)
|
|
37
|
+
cur.date = s.date;
|
|
38
|
+
if (!cur.note && s.note)
|
|
39
|
+
cur.note = s.note;
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
byKey.set(key, { ...s, n: byKey.size + 1, taskIds: [t.id] });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return [...byKey.values()];
|
|
47
|
+
}
|
|
48
|
+
/** Render the numbered source list for prompts (one line per source). */
|
|
49
|
+
function sourcesBlock(sources) {
|
|
50
|
+
return sources
|
|
51
|
+
.map((s) => `[${s.n}] ${s.title ? `${s.title} — ` : ""}${s.url}${s.date ? ` (${s.date})` : ""}${s.note ? ` — ${s.note}` : ""} [cited by ${s.taskIds.join(",")}]`)
|
|
52
|
+
.join("\n");
|
|
53
|
+
}
|
|
16
54
|
function esc(s) {
|
|
17
55
|
return s
|
|
18
56
|
.replace(/&/g, "&")
|
|
@@ -241,7 +279,7 @@ function renderFinalHtml(o) {
|
|
|
241
279
|
<span class="badge ${o.status}">${o.status}</span>
|
|
242
280
|
<span>run ${esc(o.runId)}</span>
|
|
243
281
|
<span>${esc(date)}</span>
|
|
244
|
-
<span title="${esc(o.mission.slice(0, 600))}">mission: ${esc(
|
|
282
|
+
<span title="${esc(o.mission.slice(0, 600))}">mission: ${esc((0, util_1.clip)(o.mission, 90))}</span>
|
|
245
283
|
</header>
|
|
246
284
|
<main>
|
|
247
285
|
${mdToHtml(o.markdown)}
|
package/dist/run.js
CHANGED
|
@@ -198,6 +198,14 @@ function listRuns(pricing) {
|
|
|
198
198
|
s.pid = readPid(id);
|
|
199
199
|
out.push(applyLiveness(s));
|
|
200
200
|
}
|
|
201
|
+
// Deleted runs must not pin their reduced state in a long-lived hub forever.
|
|
202
|
+
const live = new Set(ids);
|
|
203
|
+
for (const key of summaryCache.keys())
|
|
204
|
+
if (!live.has(key))
|
|
205
|
+
summaryCache.delete(key);
|
|
206
|
+
for (const key of liveCache.keys())
|
|
207
|
+
if (!live.has(key))
|
|
208
|
+
liveCache.delete(key);
|
|
201
209
|
out.sort((a, b) => b.createdAt - a.createdAt);
|
|
202
210
|
return out;
|
|
203
211
|
}
|