@robzilla1738/agentswarm 0.3.0 → 0.6.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 +51 -11
- package/dist/agent.js +18 -2
- package/dist/cli.js +39 -8
- package/dist/config.js +62 -6
- package/dist/crawltools.js +247 -0
- package/dist/deepseek.js +125 -10
- package/dist/executor.js +993 -144
- package/dist/hub.js +85 -6
- package/dist/journal.js +61 -11
- package/dist/memory.js +84 -0
- package/dist/pdftext.js +211 -0
- package/dist/prompts.js +124 -23
- package/dist/report.js +289 -0
- package/dist/run.js +15 -2
- package/dist/sandbox.js +11 -0
- package/dist/searchcore.js +244 -0
- package/dist/state.js +85 -3
- package/dist/tools.js +392 -25
- package/dist/util.js +85 -0
- package/dist/webtools.js +327 -66
- package/package.json +3 -2
- package/ui/out/404/index.html +1 -1
- package/ui/out/404.html +1 -1
- package/ui/out/_next/static/chunks/532-35122e93f37719b9.js +1 -0
- package/ui/out/_next/static/chunks/677-721ce1c8b7a6a317.js +1 -0
- package/ui/out/_next/static/chunks/app/page-dc9f6744d203e76c.js +1 -0
- package/ui/out/_next/static/chunks/app/run/page-3674e103981703a2.js +1 -0
- package/ui/out/_next/static/chunks/app/settings/page-41a5d8ba43ecfd4a.js +1 -0
- package/ui/out/_next/static/css/d95c2ba395730031.css +3 -0
- package/ui/out/fonts/PlanetKosmos.ttf +0 -0
- 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/383-289a866b246b41cc.js +0 -1
- package/ui/out/_next/static/chunks/619-ba102abea3e3d0e4.js +0 -1
- package/ui/out/_next/static/chunks/677-7ab85a6f38c3a235.js +0 -1
- package/ui/out/_next/static/chunks/app/page-0fda5b8e77d90b84.js +0 -1
- package/ui/out/_next/static/chunks/app/run/page-07aab6b1224c3c8c.js +0 -1
- package/ui/out/_next/static/chunks/app/settings/page-528482d468d84cfa.js +0 -1
- package/ui/out/_next/static/css/e2c82b53bf4519e8.css +0 -3
- /package/ui/out/_next/static/{Rm5Fhkds2-wIOnVlME55J → 7_pihFubDGD40BCy2ynlr}/_buildManifest.js +0 -0
- /package/ui/out/_next/static/{Rm5Fhkds2-wIOnVlME55J → 7_pihFubDGD40BCy2ynlr}/_ssgManifest.js +0 -0
package/dist/tools.js
CHANGED
|
@@ -33,12 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.FINISH_TOOL = exports.WAIT_TOOL = exports.SPAWN_TASKS_TOOL = exports.SUBMIT_FINAL_TOOL = exports.VERDICT_TOOL = exports.REPORT_TOOL = void 0;
|
|
36
|
+
exports.FINISH_TOOL = exports.WAIT_TOOL = exports.SET_PHASE_TOOL = exports.UPDATE_PLAN_TOOL = exports.CONDUCTOR_READ_REPORT_TOOL = exports.SPAWN_TASKS_TOOL = exports.SUBMIT_FINAL_TOOL = exports.VERDICT_TOOL = exports.REPORT_TOOL = void 0;
|
|
37
37
|
exports.workerToolset = workerToolset;
|
|
38
38
|
exports.verifierToolset = verifierToolset;
|
|
39
39
|
exports.synthToolset = synthToolset;
|
|
40
40
|
const fs = __importStar(require("fs"));
|
|
41
41
|
const path = __importStar(require("path"));
|
|
42
|
+
const crawltools_1 = require("./crawltools");
|
|
43
|
+
const searchcore_1 = require("./searchcore");
|
|
42
44
|
const util_1 = require("./util");
|
|
43
45
|
const webtools_1 = require("./webtools");
|
|
44
46
|
// ---------- safety ----------
|
|
@@ -61,9 +63,48 @@ function checkCommand(cmd, cfg) {
|
|
|
61
63
|
function resolveRead(p, ctx) {
|
|
62
64
|
return path.resolve(ctx.workdir, p);
|
|
63
65
|
}
|
|
66
|
+
/** Single-quote a string for sh. */
|
|
67
|
+
function shq(s) {
|
|
68
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Where a write actually lands: realpath of the deepest existing ancestor plus
|
|
72
|
+
* the not-yet-created remainder. Confinement checks must use this, or a
|
|
73
|
+
* symlink inside the workdir smuggles writes anywhere on the host.
|
|
74
|
+
*/
|
|
75
|
+
function realDestination(abs) {
|
|
76
|
+
let dir = abs;
|
|
77
|
+
const tail = [];
|
|
78
|
+
while (!fs.existsSync(dir)) {
|
|
79
|
+
tail.unshift(path.basename(dir));
|
|
80
|
+
const parent = path.dirname(dir);
|
|
81
|
+
if (parent === dir)
|
|
82
|
+
break;
|
|
83
|
+
dir = parent;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
dir = fs.realpathSync(dir);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
/* races/permissions: keep the lexical path */
|
|
90
|
+
}
|
|
91
|
+
return path.join(dir, ...tail);
|
|
92
|
+
}
|
|
93
|
+
function realBase(base) {
|
|
94
|
+
try {
|
|
95
|
+
return fs.realpathSync(base);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return base;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
64
101
|
function resolveWrite(p, ctx) {
|
|
65
102
|
const abs = path.resolve(ctx.workdir, p);
|
|
66
|
-
|
|
103
|
+
// Remote sandboxes own their filesystem — host-side realpath is meaningless there.
|
|
104
|
+
const real = ctx.sandbox.localFs ? realDestination(abs) : abs;
|
|
105
|
+
const ok = (0, util_1.pathInside)(realBase(ctx.workdir), real) ||
|
|
106
|
+
(0, util_1.pathInside)(realBase(ctx.runDirPath), real) ||
|
|
107
|
+
!ctx.cfg.safeMode;
|
|
67
108
|
if (!ok) {
|
|
68
109
|
throw new Error(`safeMode: writes are restricted to the working directory (${ctx.workdir}). ` +
|
|
69
110
|
`Use a relative path, or save deliverables with save_artifact.`);
|
|
@@ -84,7 +125,7 @@ async function writeFileVia(ctx, abs, content) {
|
|
|
84
125
|
}
|
|
85
126
|
}
|
|
86
127
|
// ---------- tool definitions ----------
|
|
87
|
-
function workerToolset() {
|
|
128
|
+
function workerToolset(cfg) {
|
|
88
129
|
const tools = {};
|
|
89
130
|
tools.shell = {
|
|
90
131
|
schema: {
|
|
@@ -163,13 +204,14 @@ function workerToolset() {
|
|
|
163
204
|
if (content.length > 5_000_000)
|
|
164
205
|
throw new Error("content too large (>5MB)");
|
|
165
206
|
await writeFileVia(ctx, abs, content);
|
|
166
|
-
|
|
207
|
+
const warn = ctx.checkClaim?.(String(args.path));
|
|
208
|
+
return `wrote ${abs} (${content.length} chars)${warn ? `\n${warn}` : ""}`;
|
|
167
209
|
},
|
|
168
210
|
};
|
|
169
211
|
tools.replace_in_file = {
|
|
170
212
|
schema: {
|
|
171
213
|
name: "replace_in_file",
|
|
172
|
-
description: "Exact string replacement in a file. `find` must match exactly (including whitespace). Fails if not found, or if ambiguous when all=false.",
|
|
214
|
+
description: "Exact string replacement in a file. `find` must match exactly (including whitespace). Fails if not found, or if ambiguous when all=false. For several edits to the same file, pass `edits` — they apply in order, all-or-nothing, in one call.",
|
|
173
215
|
parameters: {
|
|
174
216
|
type: "object",
|
|
175
217
|
properties: {
|
|
@@ -177,24 +219,96 @@ function workerToolset() {
|
|
|
177
219
|
find: { type: "string" },
|
|
178
220
|
replace: { type: "string" },
|
|
179
221
|
all: { type: "boolean", description: "Replace every occurrence (default false)" },
|
|
222
|
+
edits: {
|
|
223
|
+
type: "array",
|
|
224
|
+
description: "Batch mode: multiple find/replace pairs applied in order, atomically (replaces top-level find/replace)",
|
|
225
|
+
items: {
|
|
226
|
+
type: "object",
|
|
227
|
+
properties: {
|
|
228
|
+
find: { type: "string" },
|
|
229
|
+
replace: { type: "string" },
|
|
230
|
+
all: { type: "boolean" },
|
|
231
|
+
},
|
|
232
|
+
required: ["find", "replace"],
|
|
233
|
+
},
|
|
234
|
+
},
|
|
180
235
|
},
|
|
181
|
-
required: ["path"
|
|
236
|
+
required: ["path"],
|
|
182
237
|
},
|
|
183
238
|
},
|
|
184
239
|
run: async (args, ctx) => {
|
|
185
240
|
const abs = resolveWrite(String(args.path), ctx);
|
|
186
241
|
const raw = await readFileVia(ctx, abs);
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
242
|
+
const edits = Array.isArray(args.edits) && args.edits.length
|
|
243
|
+
? args.edits.map((e) => ({
|
|
244
|
+
find: String(e.find ?? ""),
|
|
245
|
+
replace: String(e.replace ?? ""),
|
|
246
|
+
all: Boolean(e.all),
|
|
247
|
+
}))
|
|
248
|
+
: args.find !== undefined && args.replace !== undefined
|
|
249
|
+
? [{ find: String(args.find), replace: String(args.replace), all: Boolean(args.all) }]
|
|
250
|
+
: null;
|
|
251
|
+
if (!edits)
|
|
252
|
+
throw new Error("provide find+replace, or an edits array");
|
|
253
|
+
// Validate-then-apply against the progressively edited content:
|
|
254
|
+
// any failing edit aborts the whole batch with nothing written.
|
|
255
|
+
let next = raw;
|
|
256
|
+
let total = 0;
|
|
257
|
+
const at = (i) => (edits.length > 1 ? `edit ${i + 1}: ` : "");
|
|
258
|
+
for (let i = 0; i < edits.length; i++) {
|
|
259
|
+
const { find, replace, all } = edits[i];
|
|
260
|
+
if (!find)
|
|
261
|
+
throw new Error(`${at(i)}find must not be empty`);
|
|
262
|
+
const count = next.split(find).length - 1;
|
|
263
|
+
if (count === 0) {
|
|
264
|
+
throw new Error(`${at(i)}find string not found in file${edits.length > 1 ? " — no edits were applied" : ""}`);
|
|
265
|
+
}
|
|
266
|
+
if (count > 1 && !all) {
|
|
267
|
+
throw new Error(`${at(i)}find string matches ${count} times; provide more context or set all=true${edits.length > 1 ? " — no edits were applied" : ""}`);
|
|
268
|
+
}
|
|
269
|
+
next = all ? next.split(find).join(replace) : next.replace(find, replace);
|
|
270
|
+
total += all ? count : 1;
|
|
194
271
|
}
|
|
195
|
-
const next = args.all ? raw.split(find).join(replace) : raw.replace(find, replace);
|
|
196
272
|
await writeFileVia(ctx, abs, next);
|
|
197
|
-
|
|
273
|
+
const warn = ctx.checkClaim?.(String(args.path));
|
|
274
|
+
return `replaced ${total} occurrence(s) via ${edits.length} edit(s) in ${abs}${warn ? `\n${warn}` : ""}`;
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
tools.grep_files = {
|
|
278
|
+
schema: {
|
|
279
|
+
name: "grep_files",
|
|
280
|
+
description: "Search file contents with a regex (grep -E syntax). Returns matching lines as path:line:text. Use this to locate code or text instead of shell grep pipelines — one round-trip, works identically in remote sandboxes, skips node_modules/.git/build output.",
|
|
281
|
+
parameters: {
|
|
282
|
+
type: "object",
|
|
283
|
+
properties: {
|
|
284
|
+
pattern: { type: "string", description: "Extended regex (grep -E)" },
|
|
285
|
+
path: { type: "string", description: "Directory or file to search (default: working directory)" },
|
|
286
|
+
glob: { type: "string", description: "Filename filter, e.g. *.ts" },
|
|
287
|
+
ignore_case: { type: "boolean" },
|
|
288
|
+
max_results: { type: "number", description: "Default 50, max 200" },
|
|
289
|
+
},
|
|
290
|
+
required: ["pattern"],
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
run: async (args, ctx) => {
|
|
294
|
+
const pattern = String(args.pattern ?? "");
|
|
295
|
+
if (!pattern.trim())
|
|
296
|
+
throw new Error("pattern is required");
|
|
297
|
+
const root = args.path ? resolveRead(String(args.path), ctx) : ctx.workdir;
|
|
298
|
+
const max = Math.min(Math.max(Number(args.max_results) || 50, 1), 200);
|
|
299
|
+
const flags = `-rnE${args.ignore_case ? "i" : ""}`;
|
|
300
|
+
const include = args.glob ? ` --include=${shq(String(args.glob))}` : "";
|
|
301
|
+
const excludes = ["node_modules", ".git", "dist", ".next", "out", "build", "target", "__pycache__", ".venv"]
|
|
302
|
+
.map((d) => ` --exclude-dir=${d}`)
|
|
303
|
+
.join("");
|
|
304
|
+
const cmd = `grep ${flags}${include}${excludes} -e ${shq(pattern)} ${shq(root)} | head -n ${max + 1}`;
|
|
305
|
+
const r = await ctx.sandbox.exec(cmd, { cwd: ctx.workdir, timeoutSec: 60, signal: ctx.signal });
|
|
306
|
+
const lines = r.out.split("\n").filter(Boolean);
|
|
307
|
+
if (!lines.length)
|
|
308
|
+
return "no matches";
|
|
309
|
+
const shown = lines.slice(0, max);
|
|
310
|
+
const more = lines.length > max ? `\n…more matches truncated (raise max_results or narrow the pattern)` : "";
|
|
311
|
+
return shown.join("\n") + more;
|
|
198
312
|
},
|
|
199
313
|
};
|
|
200
314
|
tools.list_dir = {
|
|
@@ -266,20 +380,20 @@ function workerToolset() {
|
|
|
266
380
|
tools.web_search = {
|
|
267
381
|
schema: {
|
|
268
382
|
name: "web_search",
|
|
269
|
-
description: "Search the web. Returns ranked results with title, URL and snippet. " +
|
|
270
|
-
"Set deep=true to
|
|
383
|
+
description: "Search the web. Fans out across multiple engines (DuckDuckGo, Bing, +TinyFish if configured), merges and quality-ranks results, and dedupes by canonical URL. Returns ranked results with title, URL and snippet. " +
|
|
384
|
+
"Set deep=true to widen the query into complementary phrasings, fetch the top pages, and return quotable passages with publication dates — use for thorough research and any claim that needs grounding. Raise count (up to 25) to pull more sources per call.",
|
|
271
385
|
parameters: {
|
|
272
386
|
type: "object",
|
|
273
387
|
properties: {
|
|
274
388
|
query: { type: "string" },
|
|
275
|
-
count: { type: "number", description: "Max results, default
|
|
276
|
-
deep: { type: "boolean", description: "
|
|
389
|
+
count: { type: "number", description: "Max results, default 8, max 25" },
|
|
390
|
+
deep: { type: "boolean", description: "Multi-phrasing sweep + fetch pages for quotable passages" },
|
|
277
391
|
},
|
|
278
392
|
required: ["query"],
|
|
279
393
|
},
|
|
280
394
|
},
|
|
281
395
|
run: async (args, ctx) => {
|
|
282
|
-
const count = Math.min(Math.max(Number(args.count) ||
|
|
396
|
+
const count = Math.min(Math.max(Number(args.count) || 8, 1), 25);
|
|
283
397
|
const hits = await (0, webtools_1.webSearch)(ctx.cfg, String(args.query), count, ctx.signal, Boolean(args.deep), (msg) => ctx.log?.("warn", msg));
|
|
284
398
|
if (!hits.length)
|
|
285
399
|
return "no results";
|
|
@@ -292,6 +406,39 @@ function workerToolset() {
|
|
|
292
406
|
.join("\n");
|
|
293
407
|
},
|
|
294
408
|
};
|
|
409
|
+
tools.academic_search = {
|
|
410
|
+
schema: {
|
|
411
|
+
name: "academic_search",
|
|
412
|
+
description: "Search scholarly sources: arXiv preprints and Crossref journal/conference metadata (keyless APIs). Returns papers with title, link (arXiv/DOI), abstract snippet, and date. Use for scientific or technical questions where peer-reviewed and preprint sources beat the open web.",
|
|
413
|
+
parameters: {
|
|
414
|
+
type: "object",
|
|
415
|
+
properties: {
|
|
416
|
+
query: { type: "string" },
|
|
417
|
+
count: { type: "number", description: "Max results, default 8, max 20" },
|
|
418
|
+
},
|
|
419
|
+
required: ["query"],
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
run: async (args, ctx) => {
|
|
423
|
+
const count = Math.min(Math.max(Number(args.count) || 8, 1), 20);
|
|
424
|
+
const q = String(args.query);
|
|
425
|
+
const settled = await Promise.allSettled([
|
|
426
|
+
(0, webtools_1.arxivSearch)(q, count, ctx.signal),
|
|
427
|
+
(0, webtools_1.crossrefSearch)(q, count, ctx.signal),
|
|
428
|
+
]);
|
|
429
|
+
const candidates = settled.flatMap((s) => (s.status === "fulfilled" ? s.value : []));
|
|
430
|
+
if (!candidates.length) {
|
|
431
|
+
const err = settled.find((s) => s.status === "rejected");
|
|
432
|
+
if (err)
|
|
433
|
+
throw err.reason;
|
|
434
|
+
return "no results";
|
|
435
|
+
}
|
|
436
|
+
const merged = (0, searchcore_1.mergeCandidates)(candidates, count);
|
|
437
|
+
return merged
|
|
438
|
+
.map((h, i) => `${i + 1}. ${h.title}${h.date ? ` (${h.date})` : ""} [${h.engine}]\n ${h.url}\n ${h.snippet}`)
|
|
439
|
+
.join("\n");
|
|
440
|
+
},
|
|
441
|
+
};
|
|
295
442
|
tools.fetch_url = {
|
|
296
443
|
schema: {
|
|
297
444
|
name: "fetch_url",
|
|
@@ -315,25 +462,94 @@ function workerToolset() {
|
|
|
315
462
|
tools.note = {
|
|
316
463
|
schema: {
|
|
317
464
|
name: "note",
|
|
318
|
-
description: "Post a durable fact/discovery to the swarm's shared blackboard so the conductor and other agents can see it. Use sparingly — facts other tasks need, not progress chatter.",
|
|
465
|
+
description: "Post a durable fact/discovery to the swarm's shared blackboard so the conductor and other agents can see it. Use sparingly — facts other tasks need, not progress chatter. Mark kind='decision' for choices the rest of the mission must respect, and kind='conflict' when independent sources disagree on a material fact (both are never trimmed from digests).",
|
|
319
466
|
parameters: {
|
|
320
467
|
type: "object",
|
|
321
468
|
properties: {
|
|
322
469
|
text: { type: "string" },
|
|
323
470
|
key: { type: "string", description: "Optional short label" },
|
|
471
|
+
kind: {
|
|
472
|
+
type: "string",
|
|
473
|
+
enum: ["finding", "decision", "conflict", "open-question", "handoff", "claim"],
|
|
474
|
+
description: "Category (default finding). kind='conflict' flags sources that disagree — name both. kind='claim' with key=<file path> advertises you are editing that file",
|
|
475
|
+
},
|
|
476
|
+
url: { type: "string", description: "Source URL backing this note, when it came from the web" },
|
|
324
477
|
},
|
|
325
478
|
required: ["text"],
|
|
326
479
|
},
|
|
327
480
|
},
|
|
328
481
|
run: async (args, ctx) => {
|
|
329
|
-
|
|
482
|
+
const kind = ["finding", "decision", "conflict", "open-question", "handoff", "claim"].includes(String(args.kind))
|
|
483
|
+
? String(args.kind)
|
|
484
|
+
: undefined;
|
|
485
|
+
const url = /^https?:\/\//.test(String(args.url ?? "")) ? String(args.url) : undefined;
|
|
486
|
+
ctx.addNote(String(args.text), args.key ? String(args.key) : undefined, kind, url);
|
|
330
487
|
return "noted on the blackboard";
|
|
331
488
|
},
|
|
332
489
|
};
|
|
490
|
+
tools.search_notes = {
|
|
491
|
+
schema: {
|
|
492
|
+
name: "search_notes",
|
|
493
|
+
description: "Keyword-search the ENTIRE blackboard history (the digest in your prompt only shows the recent tail). Use when you need a fact another agent may have posted earlier in the run.",
|
|
494
|
+
parameters: {
|
|
495
|
+
type: "object",
|
|
496
|
+
properties: {
|
|
497
|
+
query: { type: "string", description: "Keywords to match against note text/labels" },
|
|
498
|
+
},
|
|
499
|
+
required: ["query"],
|
|
500
|
+
},
|
|
501
|
+
},
|
|
502
|
+
run: async (args, ctx) => {
|
|
503
|
+
if (!ctx.searchNotes)
|
|
504
|
+
return "note search is unavailable in this context";
|
|
505
|
+
return ctx.searchNotes(String(args.query ?? ""));
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
tools.read_report = {
|
|
509
|
+
schema: {
|
|
510
|
+
name: "read_report",
|
|
511
|
+
description: "Read the FULL report of a settled task (dependency reports in your prompt are excerpts). Use when an excerpt cuts off details you need.",
|
|
512
|
+
parameters: {
|
|
513
|
+
type: "object",
|
|
514
|
+
properties: {
|
|
515
|
+
task_id: { type: "string", description: "e.g. T3" },
|
|
516
|
+
},
|
|
517
|
+
required: ["task_id"],
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
run: async (args, ctx) => {
|
|
521
|
+
if (!ctx.readReport)
|
|
522
|
+
return "report lookup is unavailable in this context";
|
|
523
|
+
return ctx.readReport(String(args.task_id ?? ""));
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
tools.checkpoint = {
|
|
527
|
+
schema: {
|
|
528
|
+
name: "checkpoint",
|
|
529
|
+
description: "Journal a durable progress checkpoint: a dense summary of what you've completed, key findings, and what remains. If the run is interrupted, the next attempt resumes from your latest checkpoint instead of starting over. Use after completing each major chunk of a long task.",
|
|
530
|
+
parameters: {
|
|
531
|
+
type: "object",
|
|
532
|
+
properties: {
|
|
533
|
+
summary: {
|
|
534
|
+
type: "string",
|
|
535
|
+
description: "Completed work (exact paths/commands), key findings, and remaining steps",
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
required: ["summary"],
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
run: async (args, ctx) => {
|
|
542
|
+
const summary = String(args.summary ?? "").trim();
|
|
543
|
+
if (!summary)
|
|
544
|
+
throw new Error("summary is required");
|
|
545
|
+
ctx.addCheckpoint?.(summary);
|
|
546
|
+
return "checkpoint saved";
|
|
547
|
+
},
|
|
548
|
+
};
|
|
333
549
|
tools.save_artifact = {
|
|
334
550
|
schema: {
|
|
335
551
|
name: "save_artifact",
|
|
336
|
-
description: "Save a deliverable into the run's artifacts folder (shown prominently to the operator). Provide content, or from_path to copy an existing file.",
|
|
552
|
+
description: "Save a deliverable into the run's artifacts folder (shown prominently to the operator). Provide content, or from_path to copy an existing file. Any file type works — save deliverables in the format that fits them (.csv/.json for data, .html for documents, runnable code files), not just markdown.",
|
|
337
553
|
parameters: {
|
|
338
554
|
type: "object",
|
|
339
555
|
properties: {
|
|
@@ -346,8 +562,12 @@ function workerToolset() {
|
|
|
346
562
|
},
|
|
347
563
|
run: async (args, ctx) => {
|
|
348
564
|
const name = String(args.name).replace(/^\/+/, "");
|
|
349
|
-
const
|
|
350
|
-
|
|
565
|
+
const artifactsRoot = path.join(ctx.runDirPath, "artifacts");
|
|
566
|
+
(0, util_1.ensureDir)(artifactsRoot);
|
|
567
|
+
const dest = path.join(artifactsRoot, name);
|
|
568
|
+
// Realpath-based: neither ../ traversal nor a planted symlink may move
|
|
569
|
+
// the artifact outside the run's artifacts folder.
|
|
570
|
+
if (!(0, util_1.pathInside)(realBase(artifactsRoot), realDestination(dest))) {
|
|
351
571
|
throw new Error("artifact name must stay inside the artifacts folder");
|
|
352
572
|
}
|
|
353
573
|
(0, util_1.ensureDir)(path.dirname(dest));
|
|
@@ -366,6 +586,64 @@ function workerToolset() {
|
|
|
366
586
|
return `saved artifacts/${name}`;
|
|
367
587
|
},
|
|
368
588
|
};
|
|
589
|
+
// Only offered when a crawl backend (Firecrawl / context.dev / deepcrawl)
|
|
590
|
+
// is configured — there is no free fallback for whole-site crawls.
|
|
591
|
+
if (cfg && (0, crawltools_1.resolveCrawlBackend)(cfg)) {
|
|
592
|
+
tools.crawl_site = {
|
|
593
|
+
schema: {
|
|
594
|
+
name: "crawl_site",
|
|
595
|
+
description: "Crawl a website (JS-rendered, clean markdown) and save every discovered page as a markdown file under crawl/<host>/ in the working directory. Returns an index of the saved files — read individual pages afterwards with read_file. Use for ingesting documentation sites or multi-page content; use fetch_url for single pages.",
|
|
596
|
+
parameters: {
|
|
597
|
+
type: "object",
|
|
598
|
+
properties: {
|
|
599
|
+
url: { type: "string", description: "Starting URL to crawl" },
|
|
600
|
+
max_pages: { type: "number", description: "Page limit (default 15, max 50)" },
|
|
601
|
+
include_paths: {
|
|
602
|
+
type: "array",
|
|
603
|
+
items: { type: "string" },
|
|
604
|
+
description: "Limit the crawl to URL path prefixes/globs, e.g. /docs/*",
|
|
605
|
+
},
|
|
606
|
+
},
|
|
607
|
+
required: ["url"],
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
run: async (args, ctx) => {
|
|
611
|
+
const url = String(args.url ?? "");
|
|
612
|
+
if (!/^https?:\/\//.test(url))
|
|
613
|
+
throw new Error("only http(s) URLs are supported");
|
|
614
|
+
const maxPages = Math.min(Math.max(Number(args.max_pages) || 15, 1), 50);
|
|
615
|
+
const includePaths = Array.isArray(args.include_paths)
|
|
616
|
+
? args.include_paths.map(String).filter(Boolean)
|
|
617
|
+
: undefined;
|
|
618
|
+
const out = await (0, crawltools_1.crawlSite)(ctx.cfg, { url, maxPages, includePaths, signal: ctx.signal });
|
|
619
|
+
if (!out.pages.length) {
|
|
620
|
+
return `crawled ${url} via ${out.backend}: no pages with content${out.warnings.length ? `\nwarnings: ${out.warnings.join("; ")}` : ""}`;
|
|
621
|
+
}
|
|
622
|
+
const used = new Set();
|
|
623
|
+
const lines = [];
|
|
624
|
+
for (const page of out.pages) {
|
|
625
|
+
const { host, slug } = (0, crawltools_1.slugForUrl)(page.url || url);
|
|
626
|
+
let rel = `crawl/${host}/${slug}.md`;
|
|
627
|
+
for (let n = 2; used.has(rel); n++)
|
|
628
|
+
rel = `crawl/${host}/${slug}-${n}.md`;
|
|
629
|
+
used.add(rel);
|
|
630
|
+
const abs = resolveWrite(rel, ctx);
|
|
631
|
+
const header = `# ${page.title || page.url || "untitled"}\n\nSource: ${page.url || url}\n\n`;
|
|
632
|
+
await writeFileVia(ctx, abs, header + page.markdown);
|
|
633
|
+
if (lines.length < 50) {
|
|
634
|
+
lines.push(` ${rel} — "${page.title || "untitled"}" (${page.markdown.length.toLocaleString()} chars)`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const hidden = out.pages.length - lines.length;
|
|
638
|
+
return [
|
|
639
|
+
`crawled ${url} via ${out.backend}: ${out.pages.length} page${out.pages.length > 1 ? "s" : ""} saved`,
|
|
640
|
+
...lines,
|
|
641
|
+
...(hidden > 0 ? [` …and ${hidden} more (list crawl/ to see all)`] : []),
|
|
642
|
+
...(out.warnings.length ? [`warnings: ${out.warnings.join("; ")}`] : []),
|
|
643
|
+
].join("\n");
|
|
644
|
+
},
|
|
645
|
+
};
|
|
646
|
+
}
|
|
369
647
|
return tools;
|
|
370
648
|
}
|
|
371
649
|
function verifierToolset() {
|
|
@@ -383,6 +661,7 @@ function synthToolset() {
|
|
|
383
661
|
return {
|
|
384
662
|
read_file: all.read_file,
|
|
385
663
|
list_dir: all.list_dir,
|
|
664
|
+
save_artifact: all.save_artifact,
|
|
386
665
|
};
|
|
387
666
|
}
|
|
388
667
|
// ---------- terminal tool schemas (handled by the agent loop, not executed) ----------
|
|
@@ -402,6 +681,35 @@ exports.REPORT_TOOL = {
|
|
|
402
681
|
items: { type: "string" },
|
|
403
682
|
description: "Paths of files you created/changed that matter",
|
|
404
683
|
},
|
|
684
|
+
key_facts: {
|
|
685
|
+
type: "array",
|
|
686
|
+
items: { type: "string" },
|
|
687
|
+
description: "3-8 standalone facts downstream tasks need (figures, paths, URLs, decisions)",
|
|
688
|
+
},
|
|
689
|
+
open_questions: {
|
|
690
|
+
type: "array",
|
|
691
|
+
items: { type: "string" },
|
|
692
|
+
description: "Unresolved questions or risks the conductor should know about",
|
|
693
|
+
},
|
|
694
|
+
files_touched: {
|
|
695
|
+
type: "array",
|
|
696
|
+
items: { type: "string" },
|
|
697
|
+
description: "Every file you created or modified (exact paths)",
|
|
698
|
+
},
|
|
699
|
+
sources: {
|
|
700
|
+
type: "array",
|
|
701
|
+
description: "Web sources your findings rely on — REQUIRED whenever your work drew on the web. They flow into the final report's bibliography; a web-sourced claim without an entry here cannot be cited.",
|
|
702
|
+
items: {
|
|
703
|
+
type: "object",
|
|
704
|
+
properties: {
|
|
705
|
+
url: { type: "string" },
|
|
706
|
+
title: { type: "string" },
|
|
707
|
+
date: { type: "string", description: "Publication date if known (ISO or year)" },
|
|
708
|
+
note: { type: "string", description: "What this source supports" },
|
|
709
|
+
},
|
|
710
|
+
required: ["url"],
|
|
711
|
+
},
|
|
712
|
+
},
|
|
405
713
|
},
|
|
406
714
|
required: ["status", "report"],
|
|
407
715
|
},
|
|
@@ -417,6 +725,19 @@ exports.VERDICT_TOOL = {
|
|
|
417
725
|
type: "string",
|
|
418
726
|
description: "If fail: exactly what is wrong and where. If pass: one-line confirmation of the evidence.",
|
|
419
727
|
},
|
|
728
|
+
issues: {
|
|
729
|
+
type: "array",
|
|
730
|
+
description: "On fail: one entry per concrete problem. The worker's retry sees these verbatim — make each actionable.",
|
|
731
|
+
items: {
|
|
732
|
+
type: "object",
|
|
733
|
+
properties: {
|
|
734
|
+
problem: { type: "string", description: "What is wrong" },
|
|
735
|
+
evidence: { type: "string", description: "What you observed that proves it (command output, file content, URL)" },
|
|
736
|
+
fix: { type: "string", description: "The exact change that would resolve it" },
|
|
737
|
+
},
|
|
738
|
+
required: ["problem"],
|
|
739
|
+
},
|
|
740
|
+
},
|
|
420
741
|
},
|
|
421
742
|
required: ["pass", "feedback"],
|
|
422
743
|
},
|
|
@@ -460,6 +781,17 @@ exports.SPAWN_TASKS_TOOL = {
|
|
|
460
781
|
},
|
|
461
782
|
verify: { type: "boolean", description: "Adversarially verify this task's result before accepting it" },
|
|
462
783
|
context: { type: "string", description: "Facts, paths, URLs, constraints the worker needs inlined" },
|
|
784
|
+
model: {
|
|
785
|
+
type: "string",
|
|
786
|
+
enum: ["cheap", "default", "strong"],
|
|
787
|
+
description: "Model tier: cheap for scouts/bulk extraction, strong for leads, integration, and verified deliverables",
|
|
788
|
+
},
|
|
789
|
+
team: {
|
|
790
|
+
type: "boolean",
|
|
791
|
+
description: "Run as a sub-swarm: this task gets its own conductor that decomposes it into parallel sub-tasks and reports one consolidated result. Use for coherent multi-task subsystems (e.g. 'build the backend'). Teams cannot spawn teams.",
|
|
792
|
+
},
|
|
793
|
+
team_max_workers: { type: "number", description: "Parallelism inside the team (default: half the run's)" },
|
|
794
|
+
team_budget_tokens: { type: "number", description: "Token slice for the team (default: a quarter of what remains)" },
|
|
463
795
|
},
|
|
464
796
|
required: ["title", "objective"],
|
|
465
797
|
},
|
|
@@ -468,6 +800,41 @@ exports.SPAWN_TASKS_TOOL = {
|
|
|
468
800
|
required: ["tasks"],
|
|
469
801
|
},
|
|
470
802
|
};
|
|
803
|
+
exports.CONDUCTOR_READ_REPORT_TOOL = {
|
|
804
|
+
name: "read_report",
|
|
805
|
+
description: "Read the full report of any settled task. Updates show one-line summaries once many tasks settle — use this when a summary isn't enough to plan from.",
|
|
806
|
+
parameters: {
|
|
807
|
+
type: "object",
|
|
808
|
+
properties: {
|
|
809
|
+
task_id: { type: "string", description: "e.g. T17" },
|
|
810
|
+
},
|
|
811
|
+
required: ["task_id"],
|
|
812
|
+
},
|
|
813
|
+
};
|
|
814
|
+
exports.UPDATE_PLAN_TOOL = {
|
|
815
|
+
name: "update_plan",
|
|
816
|
+
description: "Maintain the mission's living plan document (artifacts/mission-plan.md, full overwrite). On missions beyond ~20 tasks, keep it current: approach, phases, what's done, what's next, open risks. Its head is pinned into every update you receive, surviving history trimming and restarts.",
|
|
817
|
+
parameters: {
|
|
818
|
+
type: "object",
|
|
819
|
+
properties: {
|
|
820
|
+
markdown: { type: "string", description: "The complete plan document (markdown)" },
|
|
821
|
+
},
|
|
822
|
+
required: ["markdown"],
|
|
823
|
+
},
|
|
824
|
+
};
|
|
825
|
+
exports.SET_PHASE_TOOL = {
|
|
826
|
+
name: "set_phase",
|
|
827
|
+
description: "Declare the mission's current phase/milestone. Use on long missions to structure the work (e.g. 'discovery' → 'build' → 'integrate' → 'polish'). The phase and its exit criteria are pinned into every update you receive, surviving history trimming.",
|
|
828
|
+
parameters: {
|
|
829
|
+
type: "object",
|
|
830
|
+
properties: {
|
|
831
|
+
name: { type: "string", description: "Short phase name" },
|
|
832
|
+
goal: { type: "string", description: "What this phase accomplishes" },
|
|
833
|
+
exit_criteria: { type: "string", description: "Concrete conditions that end this phase" },
|
|
834
|
+
},
|
|
835
|
+
required: ["name"],
|
|
836
|
+
},
|
|
837
|
+
};
|
|
471
838
|
exports.WAIT_TOOL = {
|
|
472
839
|
name: "wait",
|
|
473
840
|
description: "Do nothing for now; wake again when running tasks report.",
|
package/dist/util.js
CHANGED
|
@@ -48,6 +48,7 @@ exports.ensureDir = ensureDir;
|
|
|
48
48
|
exports.readJson = readJson;
|
|
49
49
|
exports.writeJson = writeJson;
|
|
50
50
|
exports.pathInside = pathInside;
|
|
51
|
+
exports.validateArtifactFormat = validateArtifactFormat;
|
|
51
52
|
exports.decodeEntities = decodeEntities;
|
|
52
53
|
exports.htmlToText = htmlToText;
|
|
53
54
|
const fs = __importStar(require("fs"));
|
|
@@ -147,6 +148,90 @@ function pathInside(parent, child) {
|
|
|
147
148
|
const rel = path.relative(path.resolve(parent), path.resolve(child));
|
|
148
149
|
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
149
150
|
}
|
|
151
|
+
// ---------- artifact validation ----------
|
|
152
|
+
/**
|
|
153
|
+
* Cheap structural checks for common deliverable formats — catches a worker
|
|
154
|
+
* shipping malformed JSON/CSV/stub HTML before an LLM verifier spends tokens
|
|
155
|
+
* on it. Returns a problem description, or null when the file looks sound
|
|
156
|
+
* (or is a format we don't check).
|
|
157
|
+
*/
|
|
158
|
+
function validateArtifactFormat(absPath) {
|
|
159
|
+
const ext = path.extname(absPath).toLowerCase();
|
|
160
|
+
if (![".json", ".csv", ".html", ".htm"].includes(ext))
|
|
161
|
+
return null;
|
|
162
|
+
let raw;
|
|
163
|
+
try {
|
|
164
|
+
raw = fs.readFileSync(absPath, "utf8");
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null; // existence/size is the caller's check
|
|
168
|
+
}
|
|
169
|
+
if (ext === ".json") {
|
|
170
|
+
try {
|
|
171
|
+
JSON.parse(raw);
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
return `not valid JSON (${errMsg(e)})`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (ext === ".csv") {
|
|
179
|
+
const counts = csvFieldCounts(raw, 50);
|
|
180
|
+
if (!counts.length)
|
|
181
|
+
return "CSV has no records";
|
|
182
|
+
const expect = counts[0];
|
|
183
|
+
const bad = counts.findIndex((c) => c !== expect);
|
|
184
|
+
if (bad > 0)
|
|
185
|
+
return `inconsistent CSV: record 1 has ${expect} field(s), record ${bad + 1} has ${counts[bad]}`;
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
// .html / .htm — catch empty shells and plain text passed off as HTML.
|
|
189
|
+
if (raw.length < 200 || !/<[a-z!][^>]*>/i.test(raw) || !/<\/[a-z][a-z0-9]*>/i.test(raw)) {
|
|
190
|
+
return "HTML looks like a stub (too short or no real markup)";
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
/** Field count per CSV record (quote-aware, handles newlines inside quotes). */
|
|
195
|
+
function csvFieldCounts(raw, maxRecords) {
|
|
196
|
+
const counts = [];
|
|
197
|
+
let fields = 1;
|
|
198
|
+
let chars = 0; // non-separator chars seen in the current record
|
|
199
|
+
let inQ = false;
|
|
200
|
+
for (let i = 0; i < raw.length && counts.length < maxRecords; i++) {
|
|
201
|
+
const ch = raw[i];
|
|
202
|
+
if (inQ) {
|
|
203
|
+
if (ch === '"') {
|
|
204
|
+
if (raw[i + 1] === '"')
|
|
205
|
+
i++;
|
|
206
|
+
else
|
|
207
|
+
inQ = false;
|
|
208
|
+
}
|
|
209
|
+
chars++;
|
|
210
|
+
}
|
|
211
|
+
else if (ch === '"') {
|
|
212
|
+
inQ = true;
|
|
213
|
+
chars++;
|
|
214
|
+
}
|
|
215
|
+
else if (ch === ",") {
|
|
216
|
+
fields++;
|
|
217
|
+
chars++;
|
|
218
|
+
}
|
|
219
|
+
else if (ch === "\n" || ch === "\r") {
|
|
220
|
+
if (ch === "\r" && raw[i + 1] === "\n")
|
|
221
|
+
i++;
|
|
222
|
+
if (chars > 0)
|
|
223
|
+
counts.push(fields); // skip blank lines
|
|
224
|
+
fields = 1;
|
|
225
|
+
chars = 0;
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
chars++;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (chars > 0 && counts.length < maxRecords)
|
|
232
|
+
counts.push(fields);
|
|
233
|
+
return counts;
|
|
234
|
+
}
|
|
150
235
|
// ---------- html ----------
|
|
151
236
|
const ENTITIES = {
|
|
152
237
|
amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " ",
|