@botconnector/bccli 0.4.0-beta.3 → 0.4.0-beta.30
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 +71 -1
- package/dist/args-XQYEBOKB.js +12 -0
- package/dist/{chunk-OSWQPSA4.js → chunk-22CEHQVR.js} +44 -4
- package/dist/{chunk-NGLKANMD.js → chunk-44MXWCFN.js} +4 -3
- package/dist/{chunk-UESQNU4A.js → chunk-BEXQSEBI.js} +11 -10
- package/dist/{chunk-L7L4RGKG.js → chunk-DJR4EAVL.js} +2 -2
- package/dist/chunk-EO7IVVX4.js +443 -0
- package/dist/{chunk-U7DLMVPZ.js → chunk-K5JNR3HL.js} +2 -1
- package/dist/chunk-ON6JYPE7.js +103 -0
- package/dist/chunk-SN3ODJZN.js +63 -0
- package/dist/chunk-WWQVALA5.js +952 -0
- package/dist/cli.js +657 -233
- package/dist/{config-NPSYFAFD.js → config-PBEZ3IGD.js} +2 -2
- package/dist/integrationCli-ZTZSZLO7.js +586 -0
- package/dist/{login-KHU7ONSR.js → login-HUPT4247.js} +1 -1
- package/dist/{mcpCli-Q2H63ISP.js → mcpCli-J43FHF2H.js} +11 -10
- package/dist/providerCli-II7TQDYQ.js +12 -0
- package/dist/{ui-A32PTF4F.js → ui-HO3Z5LYD.js} +792 -238
- package/package.json +20 -3
- package/dist/args-FCLPQ7TO.js +0 -11
- package/dist/chunk-2JBRLGKD.js +0 -266
- package/dist/chunk-RZUJYAQV.js +0 -112
- package/dist/chunk-UN7QXPCB.js +0 -82
- package/dist/providerCli-G3X7KENO.js +0 -11
package/dist/cli.js
CHANGED
|
@@ -1,16 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
HELP_TEXT,
|
|
4
|
-
parseCliArgs
|
|
5
|
-
} from "./chunk-UN7QXPCB.js";
|
|
6
|
-
import {
|
|
7
|
-
addProviderKey,
|
|
8
|
-
createProvider,
|
|
9
|
-
listAllModels
|
|
10
|
-
} from "./chunk-2JBRLGKD.js";
|
|
11
|
-
import {
|
|
12
|
-
runLogin
|
|
13
|
-
} from "./chunk-L7L4RGKG.js";
|
|
14
2
|
import {
|
|
15
3
|
BUILTIN_AGENTS,
|
|
16
4
|
buildSystemPrompt,
|
|
@@ -18,15 +6,33 @@ import {
|
|
|
18
6
|
loadAgentDefs,
|
|
19
7
|
loadCommands,
|
|
20
8
|
loadSkills,
|
|
9
|
+
normalizeGlobPath,
|
|
21
10
|
parseFrontmatter,
|
|
22
|
-
runPrint
|
|
23
|
-
|
|
11
|
+
runPrint,
|
|
12
|
+
validateGlobPattern
|
|
13
|
+
} from "./chunk-22CEHQVR.js";
|
|
14
|
+
import {
|
|
15
|
+
helpText,
|
|
16
|
+
parseCliArgs
|
|
17
|
+
} from "./chunk-ON6JYPE7.js";
|
|
24
18
|
import {
|
|
25
19
|
Permissions
|
|
26
20
|
} from "./chunk-ELML4KVQ.js";
|
|
27
21
|
import {
|
|
28
|
-
|
|
29
|
-
|
|
22
|
+
addProviderKey,
|
|
23
|
+
completionAssistantMessage,
|
|
24
|
+
createProvider,
|
|
25
|
+
hasPendingProgrammaticReplay,
|
|
26
|
+
listAllModels
|
|
27
|
+
} from "./chunk-WWQVALA5.js";
|
|
28
|
+
import {
|
|
29
|
+
providerName,
|
|
30
|
+
readGlobalConfigFile
|
|
31
|
+
} from "./chunk-K5JNR3HL.js";
|
|
32
|
+
import "./chunk-SN3ODJZN.js";
|
|
33
|
+
import {
|
|
34
|
+
runLogin
|
|
35
|
+
} from "./chunk-DJR4EAVL.js";
|
|
30
36
|
import {
|
|
31
37
|
Session,
|
|
32
38
|
pruneSessions
|
|
@@ -35,8 +41,76 @@ import {
|
|
|
35
41
|
ConfigError,
|
|
36
42
|
bccliHome,
|
|
37
43
|
loadConfig,
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
resolveLanguage,
|
|
45
|
+
resolveModel,
|
|
46
|
+
setLanguage,
|
|
47
|
+
t
|
|
48
|
+
} from "./chunk-EO7IVVX4.js";
|
|
49
|
+
|
|
50
|
+
// src/textToolCalls.ts
|
|
51
|
+
var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
|
|
52
|
+
var CALL_START = /<tool_call>|\n?[^\n]*\bTool call list\b[^\n]*|\n\s*\d+\.\s*Tool:/g;
|
|
53
|
+
function coerce(value, type) {
|
|
54
|
+
const types = Array.isArray(type) ? type : [type];
|
|
55
|
+
const v = value.trim();
|
|
56
|
+
if ((types.includes("integer") || types.includes("number")) && v !== "" && !Number.isNaN(Number(v))) return Number(v);
|
|
57
|
+
if (types.includes("boolean") && /^(true|false)$/i.test(v)) return v.toLowerCase() === "true";
|
|
58
|
+
if ((types.includes("object") || types.includes("array")) && /^[[{]/.test(v)) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(v);
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return value.replace(/^\n+|\n+$/g, "");
|
|
65
|
+
}
|
|
66
|
+
function pickTool(name, keys, tools) {
|
|
67
|
+
const named = name && tools.find((t2) => t2.function.name === name);
|
|
68
|
+
if (named) return named;
|
|
69
|
+
let best;
|
|
70
|
+
let bestScore = 0;
|
|
71
|
+
let tie = false;
|
|
72
|
+
for (const t2 of tools) {
|
|
73
|
+
const params = t2.function.parameters;
|
|
74
|
+
const props = Object.keys(params.properties ?? {});
|
|
75
|
+
if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
|
|
76
|
+
const score = keys.filter((k) => props.includes(k)).length;
|
|
77
|
+
if (score > bestScore) [best, bestScore, tie] = [t2, score, false];
|
|
78
|
+
else if (score === bestScore && score > 0) tie = true;
|
|
79
|
+
}
|
|
80
|
+
return tie ? void 0 : best;
|
|
81
|
+
}
|
|
82
|
+
function recoverTextToolCalls(text, tools) {
|
|
83
|
+
if (!/<\/arg_value>|<tool_call>/.test(text)) return null;
|
|
84
|
+
const starts = [...text.matchAll(CALL_START)].map((m) => m.index ?? 0);
|
|
85
|
+
const firstPair = text.search(PAIR);
|
|
86
|
+
const cut = Math.min(...[...starts, firstPair].filter((i) => i >= 0));
|
|
87
|
+
const bounds = starts.filter((i) => i >= cut);
|
|
88
|
+
if (!bounds.length || bounds[0] > cut) bounds.unshift(cut);
|
|
89
|
+
const toolCalls = [];
|
|
90
|
+
bounds.forEach((start, n) => {
|
|
91
|
+
const segment = text.slice(start, bounds[n + 1] ?? text.length);
|
|
92
|
+
const name = /<tool_call>\s*([A-Za-z_][\w.-]*)/.exec(segment)?.[1] ?? /Tool:\s*`?([A-Za-z_][\w.-]*)`?/.exec(segment)?.[1];
|
|
93
|
+
let args = [];
|
|
94
|
+
const flush = () => {
|
|
95
|
+
if (!args.length) return;
|
|
96
|
+
const keys = args.map(([k]) => k);
|
|
97
|
+
const tool = pickTool(name, keys, tools);
|
|
98
|
+
if (tool) {
|
|
99
|
+
const props = tool.function.parameters.properties ?? {};
|
|
100
|
+
const input = Object.fromEntries(args.filter(([k]) => k in props).map(([k, v]) => [k, coerce(v, props[k].type)]));
|
|
101
|
+
toolCalls.push({ id: `text_call_${toolCalls.length}`, name: tool.function.name, arguments: JSON.stringify(input) });
|
|
102
|
+
}
|
|
103
|
+
args = [];
|
|
104
|
+
};
|
|
105
|
+
for (const m of segment.matchAll(PAIR)) {
|
|
106
|
+
if (args.some(([k]) => k === m[1])) flush();
|
|
107
|
+
args.push([m[1], m[2]]);
|
|
108
|
+
}
|
|
109
|
+
flush();
|
|
110
|
+
});
|
|
111
|
+
if (!toolCalls.length) return null;
|
|
112
|
+
return { text: text.slice(0, cut).trim(), toolCalls };
|
|
113
|
+
}
|
|
40
114
|
|
|
41
115
|
// src/tools/index.ts
|
|
42
116
|
import { z as z8 } from "zod";
|
|
@@ -136,7 +210,7 @@ function runCommand(command, opts) {
|
|
|
136
210
|
clearTimeout(timer);
|
|
137
211
|
opts.signal.removeEventListener("abort", onAbort);
|
|
138
212
|
const output = dropped ? `${head}
|
|
139
|
-
\u2026 [${dropped}
|
|
213
|
+
\u2026 [${dropped} characters truncated] \u2026
|
|
140
214
|
${tail}` : head + tail;
|
|
141
215
|
resolve2({ output: output + extra, exitCode, timedOut, aborted });
|
|
142
216
|
};
|
|
@@ -155,7 +229,7 @@ var bashTool = defineTool({
|
|
|
155
229
|
target: (input) => input.command,
|
|
156
230
|
async run(input, ctx) {
|
|
157
231
|
const r = await runCommand(input.command, { cwd: ctx.cwd, timeoutMs: input.timeout_ms ?? 12e4, signal: ctx.signal });
|
|
158
|
-
const status = r.timedOut ? `[
|
|
232
|
+
const status = r.timedOut ? `[stopped: timeout after ${Math.round((input.timeout_ms ?? 12e4) / 1e3)} seconds]` : r.aborted ? "[cancelled by the user]" : `[exit code ${r.exitCode}]`;
|
|
159
233
|
return { output: `${r.output.trimEnd()}
|
|
160
234
|
${status}`.trimStart(), isError: r.exitCode !== 0 };
|
|
161
235
|
}
|
|
@@ -214,7 +288,7 @@ function countOccurrences(haystack, needle) {
|
|
|
214
288
|
}
|
|
215
289
|
function applyEdit(content, input) {
|
|
216
290
|
if (input.old_string === input.new_string) {
|
|
217
|
-
return { output: "old_string
|
|
291
|
+
return { output: "old_string and new_string are identical; nothing to change.", isError: true };
|
|
218
292
|
}
|
|
219
293
|
const eol = content.includes("\r\n") ? "\r\n" : "\n";
|
|
220
294
|
const toEol = (s) => s.replace(/\r?\n/g, eol);
|
|
@@ -225,13 +299,13 @@ function applyEdit(content, input) {
|
|
|
225
299
|
if (count === 0) continue;
|
|
226
300
|
if (count > 1 && !input.replace_all) {
|
|
227
301
|
return {
|
|
228
|
-
output: `old_string
|
|
302
|
+
output: `old_string appears ${count} times in ${input.path}. Add more context to make it unique, or use replace_all.`,
|
|
229
303
|
isError: true
|
|
230
304
|
};
|
|
231
305
|
}
|
|
232
306
|
return input.replace_all ? content.split(oldText).join(newText) : content.replace(oldText, () => newText);
|
|
233
307
|
}
|
|
234
|
-
return { output: `old_string
|
|
308
|
+
return { output: `old_string not found in ${input.path}. Re-read the file.`, isError: true };
|
|
235
309
|
}
|
|
236
310
|
var editTool = defineTool({
|
|
237
311
|
name: "edit",
|
|
@@ -241,12 +315,12 @@ var editTool = defineTool({
|
|
|
241
315
|
target: (input) => input.path,
|
|
242
316
|
async validate(input, ctx) {
|
|
243
317
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
244
|
-
if (!ctx.readFiles.has(abs)) return `
|
|
318
|
+
if (!ctx.readFiles.has(abs)) return `Read ${input.path} with read before editing it.`;
|
|
245
319
|
let content;
|
|
246
320
|
try {
|
|
247
321
|
content = await readFile(abs, "utf8");
|
|
248
322
|
} catch (error) {
|
|
249
|
-
return `
|
|
323
|
+
return `Cannot read ${input.path}: ${errorCode(error)}`;
|
|
250
324
|
}
|
|
251
325
|
const next = applyEdit(content, input);
|
|
252
326
|
return typeof next === "string" ? void 0 : next.output;
|
|
@@ -260,25 +334,30 @@ var editTool = defineTool({
|
|
|
260
334
|
async run(input, ctx) {
|
|
261
335
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
262
336
|
if (!ctx.readFiles.has(abs)) {
|
|
263
|
-
return { output: `
|
|
337
|
+
return { output: `Read ${input.path} with read before editing it.`, isError: true };
|
|
264
338
|
}
|
|
265
339
|
let content;
|
|
266
340
|
try {
|
|
267
341
|
content = await readFile(abs, "utf8");
|
|
268
342
|
} catch (error) {
|
|
269
|
-
return { output: `
|
|
343
|
+
return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
|
|
270
344
|
}
|
|
271
345
|
const next = applyEdit(content, input);
|
|
272
346
|
if (typeof next !== "string") return next;
|
|
273
347
|
await ctx.checkpoint?.(abs);
|
|
274
348
|
await writeFile(abs, next);
|
|
275
|
-
return { output: `
|
|
349
|
+
return { output: `Edited ${input.path}.`, display: formatDiff(diffLines(content, next)) };
|
|
276
350
|
}
|
|
277
351
|
});
|
|
278
352
|
|
|
279
353
|
// src/tools/fetch.ts
|
|
280
354
|
import { z as z3 } from "zod";
|
|
281
|
-
var MAX_CHARS =
|
|
355
|
+
var MAX_CHARS = 12e3;
|
|
356
|
+
var FOCUS_CHUNKS = 3;
|
|
357
|
+
var FOCUS_SIZE = 800;
|
|
358
|
+
var CACHE_MAX = 16;
|
|
359
|
+
var CACHE_TTL_MS = 15 * 6e4;
|
|
360
|
+
var CACHE_MAX_CHARS = 2e6;
|
|
282
361
|
var MAX_BYTES = 5 * 1024 * 1024;
|
|
283
362
|
async function readCapped(res) {
|
|
284
363
|
const reader = res.body?.getReader();
|
|
@@ -300,49 +379,202 @@ async function readCapped(res) {
|
|
|
300
379
|
function htmlToText(html) {
|
|
301
380
|
return html.replace(/<(script|style|noscript)[\s\S]*?<\/\1>/gi, "").replace(/<(br|\/p|\/div|\/h[1-6]|\/li|\/tr)\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/'/g, "'").replace(/&/g, "&").split("\n").map((l) => l.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n");
|
|
302
381
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
382
|
+
function describeFetchError(error) {
|
|
383
|
+
const cause = error.cause;
|
|
384
|
+
const code = cause?.code ?? "";
|
|
385
|
+
const detail = cause?.message ?? error.message;
|
|
386
|
+
if (error.name === "TimeoutError") return "no response within 30 seconds";
|
|
387
|
+
if (code === "ENOTFOUND" || code === "EAI_AGAIN") return `${detail} \u2014 DNS could not find the host (typo, offline, or blocked by DNS/ISP)`;
|
|
388
|
+
if (/CERT|SELF_SIGNED|UNABLE_TO/.test(code)) {
|
|
389
|
+
return `${detail} \u2014 TLS certificate rejected; often caused by an antivirus or proxy that inspects HTTPS. Try running with NODE_OPTIONS=--use-system-ca`;
|
|
390
|
+
}
|
|
391
|
+
if (/ECONNRESET|ECONNREFUSED|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|UND_ERR_CONNECT_TIMEOUT|UND_ERR_SOCKET/.test(code)) {
|
|
392
|
+
return `${detail} \u2014 connection failed (firewall, proxy, or the site is blocked by the network)`;
|
|
393
|
+
}
|
|
394
|
+
return code ? `${detail} (${code})` : detail;
|
|
395
|
+
}
|
|
396
|
+
var tokensOf = (text) => Math.ceil(text.length / 4);
|
|
397
|
+
function isPrivateHost(host) {
|
|
398
|
+
const h = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
399
|
+
if (h === "localhost" || /\.(local|internal|lan|localdomain|home\.arpa)$/.test(h)) return true;
|
|
400
|
+
if (h.includes(":")) return /^(::1?$|f[cd]|fe[89ab]|::ffff:)/.test(h);
|
|
401
|
+
const v4 = h.match(/^(\d+)\.(\d+)\.\d+\.\d+$/);
|
|
402
|
+
if (v4) {
|
|
403
|
+
const [a, b] = [Number(v4[1]), Number(v4[2])];
|
|
404
|
+
return a === 0 || a === 10 || a === 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 100 && b >= 64 && b <= 127;
|
|
405
|
+
}
|
|
406
|
+
return !h.includes(".");
|
|
407
|
+
}
|
|
408
|
+
var words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [];
|
|
409
|
+
function pickRelevant(text, query, count = FOCUS_CHUNKS, size = FOCUS_SIZE) {
|
|
410
|
+
const chunks = [];
|
|
411
|
+
let cur = "";
|
|
412
|
+
for (const line of text.split("\n")) {
|
|
413
|
+
for (let i = 0; i < Math.max(line.length, 1); i += size) {
|
|
414
|
+
cur += `${line.slice(i, i + size)}
|
|
415
|
+
`;
|
|
416
|
+
if (cur.length >= size) {
|
|
417
|
+
chunks.push(cur.trimEnd());
|
|
418
|
+
cur = "";
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (cur.trim()) chunks.push(cur.trimEnd());
|
|
423
|
+
const q = [...new Set(words(query))];
|
|
424
|
+
const docs = chunks.map(words);
|
|
425
|
+
const avg = docs.reduce((n, d) => n + d.length, 0) / (docs.length || 1) || 1;
|
|
426
|
+
const idf = q.map((t2) => {
|
|
427
|
+
const df = docs.filter((d) => d.includes(t2)).length;
|
|
428
|
+
return Math.log(1 + (docs.length - df + 0.5) / (df + 0.5));
|
|
429
|
+
});
|
|
430
|
+
const scored = docs.map((d, i) => ({
|
|
431
|
+
i,
|
|
432
|
+
score: q.reduce((sum, t2, k) => {
|
|
433
|
+
const tf = d.filter((w) => w === t2).length;
|
|
434
|
+
return sum + idf[k] * tf * 2.2 / (tf + 1.2 * (0.25 + 0.75 * d.length / avg));
|
|
435
|
+
}, 0)
|
|
436
|
+
})).filter((c) => c.score > 0).sort((a, b) => b.score - a.score).slice(0, count).sort((a, b) => a.i - b.i);
|
|
437
|
+
return scored.length ? scored.map((c) => chunks[c.i]).join("\n\u2026\n") : null;
|
|
438
|
+
}
|
|
439
|
+
var cache = /* @__PURE__ */ new Map();
|
|
440
|
+
function remember(key, value, size) {
|
|
441
|
+
if (size > CACHE_MAX_CHARS) return;
|
|
442
|
+
cache.delete(key);
|
|
443
|
+
cache.set(key, value);
|
|
444
|
+
if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
|
|
445
|
+
}
|
|
446
|
+
function recall(key) {
|
|
447
|
+
const hit = cache.get(key);
|
|
448
|
+
if (typeof hit === "object" && Date.now() - hit.at > CACHE_TTL_MS) {
|
|
449
|
+
cache.delete(key);
|
|
450
|
+
return void 0;
|
|
451
|
+
}
|
|
452
|
+
return hit;
|
|
453
|
+
}
|
|
454
|
+
var ACCEPT = "text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.5";
|
|
455
|
+
var withCost = (text, what) => `${what} \xB7 ${t("~{n} tokens", { n: tokensOf(text) })}`;
|
|
456
|
+
function createFetchTool(opts = {}) {
|
|
457
|
+
const keenableURL = opts.keenableURL === void 0 ? "https://api.keenable.ai/v1/fetch/public" : opts.keenableURL;
|
|
458
|
+
const isPrivate = opts.isPrivate ?? isPrivateHost;
|
|
459
|
+
return defineTool({
|
|
460
|
+
name: "fetch",
|
|
461
|
+
description: `Fetch a URL (http/https) as text/markdown. Costs the user tokens, so ask for as little as you need:
|
|
462
|
+
- Pass \`prompt\` (what you want to know from the page) to get only the relevant excerpt instead of the whole page. Prefer this whenever you are looking for specific facts.
|
|
463
|
+
- Without \`prompt\` you get the first ${MAX_CHARS} characters; the reply tells you the \`offset\` to continue from.
|
|
464
|
+
- Fetching a page that is already in this conversation is wasteful; reuse what you have.`,
|
|
465
|
+
schema: z3.object({
|
|
466
|
+
url: z3.string().describe("Absolute http(s) URL"),
|
|
467
|
+
prompt: z3.string().max(2e3).optional().describe("What to look for in the page; returns only the relevant excerpt"),
|
|
468
|
+
offset: z3.number().int().min(0).optional().describe("Start reading at this character (to continue a truncated page)")
|
|
469
|
+
}),
|
|
470
|
+
kind: "fetch",
|
|
471
|
+
target: (input) => input.url,
|
|
472
|
+
async run(input, ctx) {
|
|
473
|
+
if (!/^https?:\/\//i.test(input.url)) return { output: "URL must start with http:// or https://", isError: true };
|
|
474
|
+
const target = new URL(input.url);
|
|
475
|
+
const prompt = input.prompt?.trim();
|
|
476
|
+
const askKeenable = !!prompt && !input.offset && !!keenableURL && !target.username && !target.password && !isPrivate(target.hostname);
|
|
477
|
+
if (askKeenable) {
|
|
478
|
+
const key = `x
|
|
479
|
+
${input.url}
|
|
480
|
+
${prompt}`;
|
|
481
|
+
const hit = recall(key);
|
|
482
|
+
if (hit) {
|
|
483
|
+
ctx.refundFetch?.();
|
|
484
|
+
return { output: hit, display: withCost(hit, t("focused excerpt \xB7 cache")) };
|
|
325
485
|
}
|
|
486
|
+
const out = await extractWithKeenable(keenableURL, input.url, prompt, ctx.signal);
|
|
487
|
+
if (out) {
|
|
488
|
+
const output2 = `[Excerpt from ${input.url} for "${prompt}" (extracted by Keenable). If it says the answer is not there, try fetch without a prompt.]
|
|
489
|
+
${out}`;
|
|
490
|
+
remember(key, output2, output2.length);
|
|
491
|
+
return { output: output2, display: withCost(output2, t("focused excerpt (Keenable)")) };
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
let page = recall(`p
|
|
495
|
+
${input.url}`);
|
|
496
|
+
const cached = !!page;
|
|
497
|
+
if (cached) ctx.refundFetch?.();
|
|
498
|
+
if (!page) {
|
|
499
|
+
const loaded = await loadPage(input.url, ctx.signal);
|
|
500
|
+
if ("error" in loaded) return { output: loaded.error, isError: true };
|
|
501
|
+
page = loaded;
|
|
502
|
+
remember(`p
|
|
503
|
+
${input.url}`, page, page.text.length);
|
|
504
|
+
}
|
|
505
|
+
const { text, truncated } = page;
|
|
506
|
+
const via = cached ? " \xB7 cache" : "";
|
|
507
|
+
if (prompt && !input.offset) {
|
|
508
|
+
const picked = pickRelevant(text, prompt);
|
|
509
|
+
if (picked) {
|
|
510
|
+
const output2 = `[Most relevant excerpt from ${input.url} for "${prompt}" (${text.length} characters total). Full page: fetch without a prompt.]
|
|
511
|
+
${picked}`;
|
|
512
|
+
return { output: output2, display: withCost(output2, `${t("focused excerpt (local)")}${via}`) };
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
const offset = input.offset ?? 0;
|
|
516
|
+
if (offset >= text.length && text.length > 0) {
|
|
517
|
+
return { output: `offset ${offset} is past the end of the page (${text.length} characters).`, isError: true };
|
|
518
|
+
}
|
|
519
|
+
const end = Math.min(offset + MAX_CHARS, text.length);
|
|
520
|
+
let output = text.slice(offset, end);
|
|
521
|
+
if (end < text.length || truncated) {
|
|
522
|
+
const total = truncated ? `${text.length}+` : text.length;
|
|
523
|
+
output += `
|
|
524
|
+
\u2026 [truncated: characters ${offset}-${end} of ${total}. Continue with offset=${end}, or use prompt for a focused excerpt.]`;
|
|
525
|
+
}
|
|
526
|
+
const size = truncated ? t("over 5 MB, truncated") : t("{n} characters", { n: text.length });
|
|
527
|
+
return { output, display: withCost(output, `${size}${via}`) };
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
async function extractWithKeenable(endpoint, url, prompt, signal) {
|
|
532
|
+
try {
|
|
533
|
+
const u = new URL(endpoint);
|
|
534
|
+
u.searchParams.set("url", url);
|
|
535
|
+
u.searchParams.set("prompt", prompt);
|
|
536
|
+
u.searchParams.set("live", "true");
|
|
537
|
+
u.searchParams.set("max_chars", String(MAX_CHARS));
|
|
538
|
+
const res = await fetch(u, { headers: { "X-Keenable-Title": "BotConnector" }, signal: AbortSignal.any([signal, AbortSignal.timeout(25e3)]) });
|
|
539
|
+
if (!res.ok) return void 0;
|
|
540
|
+
const content = (await res.json()).content;
|
|
541
|
+
return typeof content === "string" && content.trim() ? content.slice(0, MAX_CHARS) : void 0;
|
|
542
|
+
} catch {
|
|
543
|
+
return void 0;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async function loadPage(inputUrl, signal) {
|
|
547
|
+
let url = new URL(inputUrl);
|
|
548
|
+
let res;
|
|
549
|
+
try {
|
|
550
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
551
|
+
res = await fetch(url, { headers: { accept: ACCEPT }, signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)]), redirect: "manual" });
|
|
552
|
+
const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
|
|
553
|
+
if (!location) break;
|
|
554
|
+
const next = new URL(location, url);
|
|
555
|
+
if (next.host !== url.host) {
|
|
326
556
|
await res.body?.cancel();
|
|
327
|
-
|
|
557
|
+
return { error: `Redirect to another host: ${next.href}. Call fetch again with that URL if needed (permission will be requested).` };
|
|
328
558
|
}
|
|
329
|
-
|
|
330
|
-
|
|
559
|
+
await res.body?.cancel();
|
|
560
|
+
url = next;
|
|
331
561
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (!res.ok) return { output: `HTTP ${res.status} dari ${input.url}: ${body.slice(0, 500)}`, isError: true };
|
|
335
|
-
const text = (res.headers.get("content-type") ?? "").includes("html") ? htmlToText(body) : body;
|
|
336
|
-
const clipped = text.length > MAX_CHARS || truncated ? `${text.slice(0, MAX_CHARS)}
|
|
337
|
-
\u2026 [dipotong]` : text;
|
|
338
|
-
const display = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
|
|
339
|
-
return { output: clipped, display };
|
|
562
|
+
} catch (error) {
|
|
563
|
+
return { error: `Failed to fetch ${inputUrl}: ${describeFetchError(error)}` };
|
|
340
564
|
}
|
|
341
|
-
}
|
|
565
|
+
if (!res) return { error: `Failed to fetch ${inputUrl}` };
|
|
566
|
+
const { text: body, truncated } = await readCapped(res);
|
|
567
|
+
const type = res.headers.get("content-type") ?? "";
|
|
568
|
+
const text = type.includes("html") ? htmlToText(body) : body;
|
|
569
|
+
if (!res.ok) return { error: `HTTP ${res.status} from ${inputUrl}: ${text.slice(0, 500)}` };
|
|
570
|
+
return { text, truncated, at: Date.now() };
|
|
571
|
+
}
|
|
572
|
+
var fetchTool = createFetchTool();
|
|
342
573
|
|
|
343
574
|
// src/tools/glob.ts
|
|
344
|
-
import
|
|
575
|
+
import { glob as nativeGlob } from "fs/promises";
|
|
345
576
|
import { z as z4 } from "zod";
|
|
577
|
+
var MAX_OUTPUT_CHARS = 24e3;
|
|
346
578
|
var globTool = defineTool({
|
|
347
579
|
name: "glob",
|
|
348
580
|
description: 'Find files by glob pattern, e.g. "src/**/*.ts". Respects .gitignore. Max 500 results.',
|
|
@@ -353,12 +585,28 @@ var globTool = defineTool({
|
|
|
353
585
|
kind: "read",
|
|
354
586
|
target: (input) => input.pattern,
|
|
355
587
|
async run(input, ctx) {
|
|
588
|
+
const invalid = validateGlobPattern(input.pattern);
|
|
589
|
+
if (invalid) return { output: invalid, isError: true };
|
|
356
590
|
const root = resolvePath(ctx.cwd, input.path ?? ".");
|
|
357
|
-
const files =
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
591
|
+
const files = [];
|
|
592
|
+
for await (const file of nativeGlob(input.pattern, { cwd: root, exclude: ignorePatterns(root) })) {
|
|
593
|
+
files.push(normalizeGlobPath(file));
|
|
594
|
+
}
|
|
595
|
+
files.sort();
|
|
596
|
+
if (!files.length) return { output: "No matching files." };
|
|
597
|
+
const first = files.slice(0, 500);
|
|
598
|
+
const shown = [];
|
|
599
|
+
let size = 0;
|
|
600
|
+
for (const file of first) {
|
|
601
|
+
const extra = file.length + (shown.length ? 1 : 0);
|
|
602
|
+
if (shown.length && size + extra > MAX_OUTPUT_CHARS) break;
|
|
603
|
+
shown.push(file);
|
|
604
|
+
size += extra;
|
|
605
|
+
if (size >= MAX_OUTPUT_CHARS) break;
|
|
606
|
+
}
|
|
607
|
+
const omitted = files.length - shown.length;
|
|
608
|
+
const more = omitted > 0 ? `
|
|
609
|
+
\u2026 ${omitted} more files omitted (narrow pattern/path)` : "";
|
|
362
610
|
return { output: shown.join("\n") + more, display: `${files.length} file` };
|
|
363
611
|
}
|
|
364
612
|
});
|
|
@@ -366,12 +614,16 @@ var globTool = defineTool({
|
|
|
366
614
|
// src/tools/grep.ts
|
|
367
615
|
import { execFile } from "child_process";
|
|
368
616
|
import { readFile as readFile2 } from "fs/promises";
|
|
369
|
-
import
|
|
617
|
+
import { glob as nativeGlob2 } from "fs/promises";
|
|
370
618
|
import { z as z5 } from "zod";
|
|
371
619
|
var MAX_MATCHES = 200;
|
|
620
|
+
var MAX_OUTPUT_CHARS2 = 24e3;
|
|
372
621
|
async function grepJs(pattern, opts) {
|
|
373
622
|
const regex = new RegExp(pattern, opts.ignoreCase ? "i" : "");
|
|
374
|
-
const files =
|
|
623
|
+
const files = [];
|
|
624
|
+
for await (const file of nativeGlob2(opts.glob ?? "**/*", { cwd: opts.root, exclude: ignorePatterns(opts.root) })) {
|
|
625
|
+
files.push(normalizeGlobPath(file));
|
|
626
|
+
}
|
|
375
627
|
const out = [];
|
|
376
628
|
for (const file of files.sort()) {
|
|
377
629
|
let text;
|
|
@@ -416,15 +668,31 @@ var grepTool = defineTool({
|
|
|
416
668
|
kind: "read",
|
|
417
669
|
target: (input) => input.pattern,
|
|
418
670
|
async run(input, ctx) {
|
|
671
|
+
if (input.glob) {
|
|
672
|
+
const invalid = validateGlobPattern(input.glob);
|
|
673
|
+
if (invalid) return { output: invalid, isError: true };
|
|
674
|
+
}
|
|
419
675
|
const opts = { root: resolvePath(ctx.cwd, input.path ?? "."), glob: input.glob, ignoreCase: input.ignore_case };
|
|
420
676
|
try {
|
|
421
677
|
new RegExp(input.pattern);
|
|
422
678
|
} catch (error) {
|
|
423
|
-
return { output: `
|
|
679
|
+
return { output: `Invalid regex: ${error.message}`, isError: true };
|
|
424
680
|
}
|
|
425
681
|
const matches = await grepRg(input.pattern, opts) ?? await grepJs(input.pattern, opts);
|
|
426
|
-
if (!matches.length) return { output: "
|
|
427
|
-
|
|
682
|
+
if (!matches.length) return { output: "No matches." };
|
|
683
|
+
const shown = [];
|
|
684
|
+
let size = 0;
|
|
685
|
+
for (const match of matches) {
|
|
686
|
+
const extra = match.length + (shown.length ? 1 : 0);
|
|
687
|
+
if (shown.length && size + extra > MAX_OUTPUT_CHARS2) break;
|
|
688
|
+
shown.push(match);
|
|
689
|
+
size += extra;
|
|
690
|
+
if (size >= MAX_OUTPUT_CHARS2) break;
|
|
691
|
+
}
|
|
692
|
+
const omitted = matches.length - shown.length;
|
|
693
|
+
const suffix = omitted > 0 ? `
|
|
694
|
+
\u2026 ${omitted} more matches omitted (narrow pattern/path/glob)` : "";
|
|
695
|
+
return { output: shown.join("\n") + suffix, display: t("{n} results", { n: matches.length }) };
|
|
428
696
|
}
|
|
429
697
|
});
|
|
430
698
|
|
|
@@ -432,6 +700,7 @@ var grepTool = defineTool({
|
|
|
432
700
|
import { readFile as readFile3 } from "fs/promises";
|
|
433
701
|
import { z as z6 } from "zod";
|
|
434
702
|
var MAX_LINE = 2e3;
|
|
703
|
+
var MAX_OUTPUT_CHARS3 = 4e4;
|
|
435
704
|
var readTool = defineTool({
|
|
436
705
|
name: "read",
|
|
437
706
|
description: "Read a text file. Returns numbered lines (default first 2000). Use offset/limit for large files. Always read a file before editing it.",
|
|
@@ -448,25 +717,37 @@ var readTool = defineTool({
|
|
|
448
717
|
try {
|
|
449
718
|
buffer = await readFile3(abs);
|
|
450
719
|
} catch (error) {
|
|
451
|
-
return { output: `
|
|
720
|
+
return { output: `Cannot read ${input.path}: ${errorCode(error)}`, isError: true };
|
|
452
721
|
}
|
|
453
722
|
if (buffer.subarray(0, 8e3).includes(0)) {
|
|
454
|
-
return { output: `${input.path}
|
|
723
|
+
return { output: `${input.path} is a binary file; not read.`, isError: true };
|
|
455
724
|
}
|
|
456
725
|
const lines = buffer.toString("utf8").split(/\r?\n/);
|
|
457
726
|
const start = (input.offset ?? 1) - 1;
|
|
458
727
|
const limit = input.limit ?? 2e3;
|
|
459
|
-
const
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
728
|
+
const requested = lines.slice(start, start + limit);
|
|
729
|
+
const shown = [];
|
|
730
|
+
let size = 0;
|
|
731
|
+
for (let i = 0; i < requested.length; i++) {
|
|
732
|
+
const line = requested[i];
|
|
733
|
+
const text = line.length > MAX_LINE ? `${line.slice(0, MAX_LINE)}\u2026 [line truncated]` : line;
|
|
734
|
+
const rendered = `${String(start + i + 1).padStart(6)} ${text}`;
|
|
735
|
+
const extra = rendered.length + (shown.length ? 1 : 0);
|
|
736
|
+
if (shown.length && size + extra > MAX_OUTPUT_CHARS3) break;
|
|
737
|
+
shown.push(rendered);
|
|
738
|
+
size += extra;
|
|
739
|
+
if (size >= MAX_OUTPUT_CHARS3) break;
|
|
740
|
+
}
|
|
741
|
+
const body = shown.join("\n");
|
|
742
|
+
const remaining = lines.length - start - shown.length;
|
|
743
|
+
const tokenBudgetHit = shown.length < requested.length;
|
|
744
|
+
const nextOffset = start + shown.length + 1;
|
|
465
745
|
ctx.readFiles.add(abs);
|
|
466
746
|
return {
|
|
467
|
-
output: remaining > 0 ? `${body}
|
|
468
|
-
\u2026 ${remaining}
|
|
469
|
-
|
|
747
|
+
output: remaining > 0 ? tokenBudgetHit ? `${body}
|
|
748
|
+
\u2026 ${remaining} more lines (token budget reached; continue with offset=${nextOffset})` : `${body}
|
|
749
|
+
\u2026 ${remaining} more lines (use offset)` : body,
|
|
750
|
+
display: t("{n} lines", { n: shown.length })
|
|
470
751
|
};
|
|
471
752
|
}
|
|
472
753
|
});
|
|
@@ -487,7 +768,7 @@ var writeTool = defineTool({
|
|
|
487
768
|
target: (input) => input.path,
|
|
488
769
|
async validate(input, ctx) {
|
|
489
770
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
490
|
-
return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path}
|
|
771
|
+
return existsSync(abs) && !ctx.readFiles.has(abs) ? `${input.path} already exists. Read it with read before overwriting it.` : void 0;
|
|
491
772
|
},
|
|
492
773
|
async preview(input, ctx) {
|
|
493
774
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
@@ -497,18 +778,18 @@ var writeTool = defineTool({
|
|
|
497
778
|
async run(input, ctx) {
|
|
498
779
|
const abs = resolvePath(ctx.cwd, input.path);
|
|
499
780
|
if (existsSync(abs) && !ctx.readFiles.has(abs)) {
|
|
500
|
-
return { output: `${input.path}
|
|
781
|
+
return { output: `${input.path} already exists. Read it with read before overwriting it.`, isError: true };
|
|
501
782
|
}
|
|
502
783
|
try {
|
|
503
784
|
await ctx.checkpoint?.(abs);
|
|
504
785
|
await mkdir(dirname(abs), { recursive: true });
|
|
505
786
|
await writeFile2(abs, input.content);
|
|
506
787
|
} catch (error) {
|
|
507
|
-
return { output: `
|
|
788
|
+
return { output: `Failed to write ${input.path}: ${errorCode(error)}`, isError: true };
|
|
508
789
|
}
|
|
509
790
|
ctx.readFiles.add(abs);
|
|
510
791
|
const lines = input.content.split("\n").length;
|
|
511
|
-
return { output: `
|
|
792
|
+
return { output: `Wrote ${input.path} (${lines} lines).`, display: t("{lines} lines written", { lines }) };
|
|
512
793
|
}
|
|
513
794
|
});
|
|
514
795
|
|
|
@@ -517,74 +798,83 @@ var ALL_TOOLS = [readTool, writeTool, editTool, bashTool, grepTool, globTool, fe
|
|
|
517
798
|
function toolDefinitions(tools) {
|
|
518
799
|
return tools.map((tool) => {
|
|
519
800
|
const { $schema: _ignored, ...parameters } = tool.jsonSchema ?? z8.toJSONSchema(tool.schema);
|
|
520
|
-
return { type: "function", function: { name: tool.name, description: tool.description, parameters } };
|
|
801
|
+
return { type: "function", function: { name: tool.name, description: tool.description, parameters, ...tool.outputJsonSchema ? { output_schema: tool.outputJsonSchema } : {} } };
|
|
521
802
|
});
|
|
522
803
|
}
|
|
523
804
|
|
|
524
|
-
// src/
|
|
525
|
-
var
|
|
526
|
-
var
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
805
|
+
// src/webBudget.ts
|
|
806
|
+
var KEEP_TOKENS = 4e4;
|
|
807
|
+
var PRUNE_MIN_TOKENS = 2e4;
|
|
808
|
+
var STUB = "[old tool result removed:";
|
|
809
|
+
var tokens = (text) => Math.ceil(text.length / 4);
|
|
810
|
+
var isStub = (content) => content.startsWith(STUB);
|
|
811
|
+
function pruneOldFetches(messages) {
|
|
812
|
+
const urls = /* @__PURE__ */ new Map();
|
|
813
|
+
for (const m of messages) {
|
|
814
|
+
if (m.role !== "assistant") continue;
|
|
815
|
+
for (const c of m.tool_calls ?? []) {
|
|
816
|
+
if (c.function.name !== "fetch") continue;
|
|
817
|
+
try {
|
|
818
|
+
urls.set(c.id, String(JSON.parse(c.function.arguments).url ?? ""));
|
|
819
|
+
} catch {
|
|
820
|
+
urls.set(c.id, "");
|
|
821
|
+
}
|
|
536
822
|
}
|
|
537
823
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
|
|
550
|
-
const score = keys.filter((k) => props.includes(k)).length;
|
|
551
|
-
if (score > bestScore) [best, bestScore, tie] = [t, score, false];
|
|
552
|
-
else if (score === bestScore && score > 0) tie = true;
|
|
824
|
+
let kept = 0;
|
|
825
|
+
let oldTokens = 0;
|
|
826
|
+
const old = [];
|
|
827
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
828
|
+
const m = messages[i];
|
|
829
|
+
if (m.role !== "tool" || !urls.has(m.tool_call_id) || isStub(m.content)) continue;
|
|
830
|
+
kept += tokens(m.content);
|
|
831
|
+
if (kept > KEEP_TOKENS) {
|
|
832
|
+
old.push(i);
|
|
833
|
+
oldTokens += tokens(m.content);
|
|
834
|
+
}
|
|
553
835
|
}
|
|
554
|
-
|
|
836
|
+
if (oldTokens <= PRUNE_MIN_TOKENS) return false;
|
|
837
|
+
for (const i of old) {
|
|
838
|
+
const m = messages[i];
|
|
839
|
+
messages[i] = { ...m, content: `${STUB} fetch ${urls.get(m.tool_call_id)}. Fetch it again (use prompt for a focused excerpt) if still needed.]` };
|
|
840
|
+
}
|
|
841
|
+
return true;
|
|
555
842
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
if (args.some(([k]) => k === m[1])) flush();
|
|
581
|
-
args.push([m[1], m[2]]);
|
|
843
|
+
var WebBudget = class {
|
|
844
|
+
fetches = 0;
|
|
845
|
+
chars = 0;
|
|
846
|
+
perTurn;
|
|
847
|
+
sessionChars;
|
|
848
|
+
constructor(limits = {}) {
|
|
849
|
+
this.perTurn = limits.perTurn ?? 8;
|
|
850
|
+
this.sessionChars = limits.sessionChars ?? 3e5;
|
|
851
|
+
}
|
|
852
|
+
startTurn() {
|
|
853
|
+
this.fetches = 0;
|
|
854
|
+
}
|
|
855
|
+
/** Gives back a reservation, e.g. when the fetch was served from cache and cost nothing. */
|
|
856
|
+
refund() {
|
|
857
|
+
if (this.fetches > 0) this.fetches--;
|
|
858
|
+
}
|
|
859
|
+
reset() {
|
|
860
|
+
this.fetches = 0;
|
|
861
|
+
this.chars = 0;
|
|
862
|
+
}
|
|
863
|
+
/** Reserves one fetch; returns the refusal to show the model when over budget. */
|
|
864
|
+
take() {
|
|
865
|
+
if (this.fetches >= this.perTurn) {
|
|
866
|
+
return `Limit of ${this.perTurn} fetches per question reached (each fetch uses the user's tokens). Answer from the results you already have; if that is not enough, tell the user what is still missing.`;
|
|
582
867
|
}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
868
|
+
if (this.chars >= this.sessionChars) {
|
|
869
|
+
return `Total web page limit for this session reached (~${Math.round(this.sessionChars / 4e3)}k tokens). Answer from the results you already have and ask the user whether to search more.`;
|
|
870
|
+
}
|
|
871
|
+
this.fetches++;
|
|
872
|
+
return void 0;
|
|
873
|
+
}
|
|
874
|
+
record(chars) {
|
|
875
|
+
this.chars += chars;
|
|
876
|
+
}
|
|
877
|
+
};
|
|
588
878
|
|
|
589
879
|
// src/agent.ts
|
|
590
880
|
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
@@ -602,6 +892,7 @@ var Agent = class {
|
|
|
602
892
|
this.maxSteps = opts.maxSteps ?? 50;
|
|
603
893
|
this.contextWindow = opts.contextWindow ?? 128e3;
|
|
604
894
|
this.systemPrompt = opts.systemPrompt;
|
|
895
|
+
this.reasoning = opts.reasoning ?? "auto";
|
|
605
896
|
}
|
|
606
897
|
opts;
|
|
607
898
|
provider;
|
|
@@ -611,6 +902,7 @@ var Agent = class {
|
|
|
611
902
|
lastInputTokens = 0;
|
|
612
903
|
onEvent = () => {
|
|
613
904
|
};
|
|
905
|
+
reasoning;
|
|
614
906
|
askPermission = async () => "no";
|
|
615
907
|
tools;
|
|
616
908
|
definitions;
|
|
@@ -618,6 +910,7 @@ var Agent = class {
|
|
|
618
910
|
turnTools;
|
|
619
911
|
turnDefinitions;
|
|
620
912
|
readFiles = /* @__PURE__ */ new Set();
|
|
913
|
+
webBudget = new WebBudget();
|
|
621
914
|
maxSteps;
|
|
622
915
|
// ponytail: one window for every model; read it from /v1/models metadata when providers expose it.
|
|
623
916
|
contextWindow;
|
|
@@ -630,7 +923,7 @@ var Agent = class {
|
|
|
630
923
|
this.definitions = toolDefinitions(tools);
|
|
631
924
|
}
|
|
632
925
|
isParallelSafe(call) {
|
|
633
|
-
const tool = this.turnTools.find((
|
|
926
|
+
const tool = this.turnTools.find((t2) => t2.name === call.name);
|
|
634
927
|
if (!tool?.parallelSafe) return false;
|
|
635
928
|
try {
|
|
636
929
|
const parsed = tool.schema.safeParse(JSON.parse(call.arguments || "{}"));
|
|
@@ -647,11 +940,13 @@ var Agent = class {
|
|
|
647
940
|
load(messages) {
|
|
648
941
|
this.messages = messages;
|
|
649
942
|
this.readFiles.clear();
|
|
943
|
+
this.webBudget.reset();
|
|
650
944
|
this.lastInputTokens = 0;
|
|
651
945
|
}
|
|
652
946
|
clear() {
|
|
653
947
|
this.messages = [];
|
|
654
948
|
this.readFiles.clear();
|
|
949
|
+
this.webBudget.reset();
|
|
655
950
|
this.lastInputTokens = 0;
|
|
656
951
|
this.opts.onReset?.();
|
|
657
952
|
}
|
|
@@ -660,31 +955,37 @@ var Agent = class {
|
|
|
660
955
|
messages: [{ role: "system", content: this.systemPrompt }, ...this.messages, { role: "user", content: COMPACT_PROMPT }],
|
|
661
956
|
// Some gateways reject tool_calls in history when no tools are declared.
|
|
662
957
|
tools: this.definitions,
|
|
663
|
-
signal
|
|
958
|
+
signal,
|
|
959
|
+
reasoning: this.reasoning
|
|
664
960
|
});
|
|
665
961
|
this.clear();
|
|
666
|
-
this.push({ role: "user", content: `
|
|
962
|
+
this.push({ role: "user", content: `Summary of the previous conversation:
|
|
667
963
|
${completion.text}` });
|
|
668
|
-
this.push({ role: "assistant", content: "
|
|
964
|
+
this.push({ role: "assistant", content: "OK, I will continue from this summary." });
|
|
669
965
|
this.onEvent({ type: "compacted" });
|
|
670
966
|
}
|
|
671
967
|
async run(text, signal) {
|
|
672
968
|
this.turnTools = this.tools;
|
|
673
969
|
this.turnDefinitions = this.definitions;
|
|
674
970
|
this.opts.onTurnStart?.();
|
|
971
|
+
this.webBudget.startTurn();
|
|
675
972
|
try {
|
|
676
|
-
if (this.lastInputTokens > this.contextWindow * 0.8) await this.compact(signal);
|
|
973
|
+
if (this.lastInputTokens > this.contextWindow * 0.8 && !hasPendingProgrammaticReplay(this.messages)) await this.compact(signal);
|
|
677
974
|
this.push({ role: "user", content: text });
|
|
678
975
|
for (let step = 0; step < this.maxSteps; step++) {
|
|
679
|
-
if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8) {
|
|
976
|
+
if (step > 0 && this.lastInputTokens > this.contextWindow * 0.8 && !hasPendingProgrammaticReplay(this.messages)) {
|
|
680
977
|
await this.compact(signal);
|
|
681
|
-
this.push({ role: "user", content: `
|
|
978
|
+
this.push({ role: "user", content: `Continue this task using the summary above: ${text}` });
|
|
682
979
|
}
|
|
980
|
+
pruneOldFetches(this.messages);
|
|
683
981
|
let completion = await this.provider.chat({
|
|
684
982
|
messages: [{ role: "system", content: this.systemPrompt }, ...this.messages],
|
|
685
983
|
tools: this.turnDefinitions,
|
|
984
|
+
programmaticToolNames: this.turnTools.filter((tool) => tool.programmaticSafe).map((tool) => tool.name),
|
|
686
985
|
signal,
|
|
687
|
-
onText: (delta) => this.onEvent({ type: "text", delta })
|
|
986
|
+
onText: (delta) => this.onEvent({ type: "text", delta }),
|
|
987
|
+
onThinking: (delta) => this.onEvent({ type: "thinking", delta }),
|
|
988
|
+
reasoning: this.reasoning
|
|
688
989
|
});
|
|
689
990
|
if (!completion.toolCalls.length && completion.finishReason !== "repetition") {
|
|
690
991
|
const recovered = recoverTextToolCalls(completion.text, this.turnDefinitions);
|
|
@@ -701,26 +1002,17 @@ ${completion.text}` });
|
|
|
701
1002
|
this.totalUsage.outputTokens += usage.outputTokens;
|
|
702
1003
|
this.lastInputTokens = usage.inputTokens;
|
|
703
1004
|
this.onEvent({ type: "usage", ...this.totalUsage });
|
|
704
|
-
this.push(
|
|
705
|
-
role: "assistant",
|
|
706
|
-
content: completion.text || null,
|
|
707
|
-
...completion.toolCalls.length ? {
|
|
708
|
-
tool_calls: completion.toolCalls.map((c) => ({
|
|
709
|
-
id: c.id,
|
|
710
|
-
type: "function",
|
|
711
|
-
function: { name: c.name, arguments: c.arguments }
|
|
712
|
-
}))
|
|
713
|
-
} : {}
|
|
714
|
-
});
|
|
1005
|
+
this.push(completionAssistantMessage(completion));
|
|
715
1006
|
if (completion.finishReason === "repetition") {
|
|
716
1007
|
this.onEvent({ type: "textReplace", text: completion.text });
|
|
717
1008
|
this.onEvent({
|
|
718
1009
|
type: "error",
|
|
719
|
-
message: "
|
|
1010
|
+
message: t("The model got stuck repeating text, so the answer was stopped. Try again or switch models with /model.")
|
|
720
1011
|
});
|
|
721
1012
|
return;
|
|
722
1013
|
}
|
|
723
1014
|
if (!completion.toolCalls.length) {
|
|
1015
|
+
if (completion.finishReason === "continue") continue;
|
|
724
1016
|
this.onEvent({ type: "done" });
|
|
725
1017
|
return;
|
|
726
1018
|
}
|
|
@@ -728,7 +1020,13 @@ ${completion.text}` });
|
|
|
728
1020
|
let i = 0;
|
|
729
1021
|
while (i < calls.length) {
|
|
730
1022
|
if (signal.aborted) {
|
|
731
|
-
for (const pending of calls.slice(i))
|
|
1023
|
+
for (const pending of calls.slice(i))
|
|
1024
|
+
this.push({
|
|
1025
|
+
role: "tool",
|
|
1026
|
+
tool_call_id: pending.id,
|
|
1027
|
+
content: "Cancelled by the user.",
|
|
1028
|
+
...pending.caller ? { responses_caller: pending.caller } : {}
|
|
1029
|
+
});
|
|
732
1030
|
this.onEvent({ type: "aborted" });
|
|
733
1031
|
return;
|
|
734
1032
|
}
|
|
@@ -737,7 +1035,12 @@ ${completion.text}` });
|
|
|
737
1035
|
const batch = calls.slice(i, j);
|
|
738
1036
|
const results = await Promise.all(batch.map((c) => this.runTool(c, signal)));
|
|
739
1037
|
batch.forEach((c, k) => {
|
|
740
|
-
this.push({
|
|
1038
|
+
this.push({
|
|
1039
|
+
role: "tool",
|
|
1040
|
+
tool_call_id: c.id,
|
|
1041
|
+
content: results[k],
|
|
1042
|
+
...c.caller ? { responses_caller: c.caller } : {}
|
|
1043
|
+
});
|
|
741
1044
|
});
|
|
742
1045
|
i = j;
|
|
743
1046
|
}
|
|
@@ -757,7 +1060,7 @@ ${completion.text}` });
|
|
|
757
1060
|
this.onEvent({
|
|
758
1061
|
type: "error",
|
|
759
1062
|
message: noTools ? `${message}
|
|
760
|
-
|
|
1063
|
+
${t("This model may not support tool calling. Try another model with /model.")}` : message
|
|
761
1064
|
});
|
|
762
1065
|
}
|
|
763
1066
|
}
|
|
@@ -767,20 +1070,22 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
767
1070
|
this.onEvent({ type: "toolEnd", id: call.id, tool: call.name, output, isError: true });
|
|
768
1071
|
return output;
|
|
769
1072
|
};
|
|
770
|
-
const tool = this.turnTools.find((
|
|
771
|
-
if (!tool) return fail(`
|
|
1073
|
+
const tool = this.turnTools.find((t2) => t2.name === call.name);
|
|
1074
|
+
if (!tool) return fail(`Tool "${call.name}" does not exist. Available tools: ${this.turnTools.map((t2) => t2.name).join(", ")}`);
|
|
772
1075
|
let args;
|
|
773
1076
|
try {
|
|
774
1077
|
args = JSON.parse(call.arguments || "{}");
|
|
775
1078
|
} catch {
|
|
776
|
-
return fail(`
|
|
1079
|
+
return fail(`Arguments are not valid JSON: ${call.arguments.slice(0, 200)}`);
|
|
777
1080
|
}
|
|
778
1081
|
const parsed = tool.schema.safeParse(args);
|
|
779
1082
|
if (!parsed.success) {
|
|
780
|
-
return fail(`
|
|
1083
|
+
return fail(`Invalid arguments: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
781
1084
|
}
|
|
782
1085
|
const input = parsed.data;
|
|
783
1086
|
const target = tool.target(input);
|
|
1087
|
+
const overBudget = tool.name === "fetch" ? this.webBudget.take() : void 0;
|
|
1088
|
+
if (overBudget) return fail(overBudget, target);
|
|
784
1089
|
const ctx = {
|
|
785
1090
|
cwd: this.opts.cwd,
|
|
786
1091
|
signal,
|
|
@@ -789,6 +1094,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
789
1094
|
emit: (event) => this.onEvent(event),
|
|
790
1095
|
ask: (req) => this.askPermission(req),
|
|
791
1096
|
checkpoint: this.opts.checkpoint,
|
|
1097
|
+
refundFetch: () => this.webBudget.refund(),
|
|
792
1098
|
addUsage: (u) => {
|
|
793
1099
|
this.totalUsage.inputTokens += u.inputTokens;
|
|
794
1100
|
this.totalUsage.outputTokens += u.outputTokens;
|
|
@@ -816,7 +1122,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
816
1122
|
decision = answer === "no" ? "deny" : "allow";
|
|
817
1123
|
}
|
|
818
1124
|
if (decision === "deny") {
|
|
819
|
-
const output = this.permissions.mode === "plan" ? "
|
|
1125
|
+
const output = this.permissions.mode === "plan" ? "Denied: plan mode only allows reading and searching. Write the plan for the user without changing anything." : "The user declined to run this tool. Ask the user what they want, or try another approach.";
|
|
820
1126
|
this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output, isError: true });
|
|
821
1127
|
return output;
|
|
822
1128
|
}
|
|
@@ -826,6 +1132,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
826
1132
|
} catch (error) {
|
|
827
1133
|
result = { output: `Error: ${error.message}`, isError: true };
|
|
828
1134
|
}
|
|
1135
|
+
if (tool.name === "fetch" && !result.isError) this.webBudget.record(result.output.length);
|
|
829
1136
|
this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
|
|
830
1137
|
return result.output;
|
|
831
1138
|
}
|
|
@@ -855,7 +1162,7 @@ var CheckpointStore = class {
|
|
|
855
1162
|
while (this.turns() > MAX_TURNS) this.history.shift();
|
|
856
1163
|
}
|
|
857
1164
|
turns() {
|
|
858
|
-
return this.history.filter((
|
|
1165
|
+
return this.history.filter((t2) => t2.size).length;
|
|
859
1166
|
}
|
|
860
1167
|
async undo() {
|
|
861
1168
|
while (this.history.length && !this.history.at(-1)?.size) this.history.pop();
|
|
@@ -890,9 +1197,10 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
|
890
1197
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
891
1198
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
892
1199
|
import { z as z9 } from "zod";
|
|
1200
|
+
var MCP_OUTPUT_MAX_CHARS = 32e3;
|
|
893
1201
|
function mcpToolName(server, tool, taken) {
|
|
894
|
-
const
|
|
895
|
-
const base = `mcp__${
|
|
1202
|
+
const clean2 = (s) => s.replace(/[^A-Za-z0-9_-]+/g, "_");
|
|
1203
|
+
const base = `mcp__${clean2(server)}__${clean2(tool)}`.slice(0, 64);
|
|
896
1204
|
let name = base;
|
|
897
1205
|
for (let n = 2; taken.has(name); n++) name = `${base.slice(0, 64 - String(n).length - 1)}_${n}`;
|
|
898
1206
|
taken.add(name);
|
|
@@ -901,7 +1209,7 @@ function mcpToolName(server, tool, taken) {
|
|
|
901
1209
|
function withTimeout(promise, ms) {
|
|
902
1210
|
let timer;
|
|
903
1211
|
const timeout = new Promise((_, reject) => {
|
|
904
|
-
timer = setTimeout(() => reject(new Error(
|
|
1212
|
+
timer = setTimeout(() => reject(new Error(t("timeout after {n} seconds", { n: Math.round(ms / 1e3) }))), ms);
|
|
905
1213
|
});
|
|
906
1214
|
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
|
907
1215
|
}
|
|
@@ -938,7 +1246,7 @@ var McpManager = class {
|
|
|
938
1246
|
});
|
|
939
1247
|
return;
|
|
940
1248
|
}
|
|
941
|
-
entry.tools = tools.map((
|
|
1249
|
+
entry.tools = tools.map((t2) => this.wrap(spec.name, client, t2));
|
|
942
1250
|
entry.state = { ...entry.state, status: "ready", tools: entry.tools.length };
|
|
943
1251
|
} catch (error) {
|
|
944
1252
|
entry.state = { ...entry.state, status: "error", error: error.message };
|
|
@@ -954,7 +1262,9 @@ var McpManager = class {
|
|
|
954
1262
|
description: `[MCP ${server}] ${tool.description ?? tool.name}`.slice(0, 1024),
|
|
955
1263
|
schema: z9.record(z9.string(), z9.unknown()),
|
|
956
1264
|
jsonSchema: tool.inputSchema,
|
|
1265
|
+
...tool.outputSchema ? { outputJsonSchema: tool.outputSchema } : {},
|
|
957
1266
|
kind: "mcp",
|
|
1267
|
+
programmaticSafe: tool.annotations?.readOnlyHint === true,
|
|
958
1268
|
target: (input) => {
|
|
959
1269
|
const text = JSON.stringify(input);
|
|
960
1270
|
return text.length > 80 ? `${text.slice(0, 77)}\u2026` : text;
|
|
@@ -962,9 +1272,12 @@ var McpManager = class {
|
|
|
962
1272
|
async run(input, ctx) {
|
|
963
1273
|
try {
|
|
964
1274
|
const result = await client.callTool({ name: tool.name, arguments: input }, void 0, { signal: ctx.signal });
|
|
965
|
-
const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(
|
|
966
|
-
return {
|
|
967
|
-
|
|
1275
|
+
const output = (result.content ?? []).map((c) => c.type === "text" ? c.text ?? "" : `[${c.type}]`).join("\n") || "(empty)";
|
|
1276
|
+
return {
|
|
1277
|
+
output: output.length > MCP_OUTPUT_MAX_CHARS ? `${output.slice(0, MCP_OUTPUT_MAX_CHARS)}
|
|
1278
|
+
\u2026 [truncated at ${MCP_OUTPUT_MAX_CHARS} characters; narrow the MCP request if possible]` : output,
|
|
1279
|
+
isError: !!result.isError
|
|
1280
|
+
};
|
|
968
1281
|
} catch (error) {
|
|
969
1282
|
return { output: `Server MCP ${server} error: ${error.message}`, isError: true };
|
|
970
1283
|
}
|
|
@@ -975,14 +1288,14 @@ var McpManager = class {
|
|
|
975
1288
|
const entry = this.servers.get(name);
|
|
976
1289
|
if (!entry) return;
|
|
977
1290
|
this.servers.delete(name);
|
|
978
|
-
for (const
|
|
979
|
-
if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((
|
|
1291
|
+
for (const t2 of entry.tools) this.taken.delete(t2.name);
|
|
1292
|
+
if (entry.tools.length) this.opts.onToolsRemoved?.(entry.tools.map((t2) => t2.name));
|
|
980
1293
|
await entry.client?.close().catch(() => {
|
|
981
1294
|
});
|
|
982
1295
|
this.opts.onChange?.();
|
|
983
1296
|
}
|
|
984
1297
|
tools() {
|
|
985
|
-
return [...this.servers.values()].flatMap((s) => s.tools);
|
|
1298
|
+
return [...this.servers.values()].flatMap((s) => s.tools).sort((a, b) => a.name.localeCompare(b.name));
|
|
986
1299
|
}
|
|
987
1300
|
states() {
|
|
988
1301
|
return [...this.servers.values()].map((s) => s.state);
|
|
@@ -1003,15 +1316,15 @@ function createExitPlanTool(opts) {
|
|
|
1003
1316
|
description: "Plan mode only: present the finished plan to the user for approval. If approved you may start implementing it.",
|
|
1004
1317
|
schema: z10.object({ plan: z10.string().describe("The plan, in markdown") }),
|
|
1005
1318
|
kind: "read",
|
|
1006
|
-
target: () => "
|
|
1319
|
+
target: () => t("plan"),
|
|
1007
1320
|
async run(input) {
|
|
1008
|
-
if (opts.permissions.mode !== "plan") return { output: "exit_plan
|
|
1321
|
+
if (opts.permissions.mode !== "plan") return { output: "exit_plan is only used in plan mode.", isError: true };
|
|
1009
1322
|
const decision = await opts.interaction.approvePlan(input.plan);
|
|
1010
1323
|
if (decision === "no") {
|
|
1011
|
-
return { output: "
|
|
1324
|
+
return { output: "The user has not approved yet. Ask what needs to change, revise the plan, then call exit_plan again." };
|
|
1012
1325
|
}
|
|
1013
1326
|
opts.permissions.mode = decision;
|
|
1014
|
-
return { output: `
|
|
1327
|
+
return { output: `Plan approved. The mode is now ${decision}. Start carrying out the plan now.` };
|
|
1015
1328
|
}
|
|
1016
1329
|
});
|
|
1017
1330
|
}
|
|
@@ -1028,7 +1341,7 @@ function createSkillTool(skills) {
|
|
|
1028
1341
|
target: (input) => input.name,
|
|
1029
1342
|
async run(input) {
|
|
1030
1343
|
const skill = skills.find((s) => s.name === input.name);
|
|
1031
|
-
if (!skill) return { output: `Skill "${input.name}"
|
|
1344
|
+
if (!skill) return { output: `Skill "${input.name}" does not exist. Available: ${skills.map((s) => s.name).join(", ") || "(none)"}`, isError: true };
|
|
1032
1345
|
const { body } = parseFrontmatter(readFileSync(skill.file, "utf8"));
|
|
1033
1346
|
return { output: `Base directory for this skill: ${skill.dir}
|
|
1034
1347
|
|
|
@@ -1037,8 +1350,69 @@ ${body}`, display: skill.description.slice(0, 60) };
|
|
|
1037
1350
|
});
|
|
1038
1351
|
}
|
|
1039
1352
|
|
|
1040
|
-
// src/tools/
|
|
1353
|
+
// src/tools/websearch.ts
|
|
1041
1354
|
import { z as z12 } from "zod";
|
|
1355
|
+
var clean = (items, max) => (Array.isArray(items) ? items : []).filter((r) => typeof r?.url === "string" && /^https?:\/\//.test(r.url)).slice(0, max).map((r) => ({
|
|
1356
|
+
title: String(r.title || r.url).slice(0, 200),
|
|
1357
|
+
url: r.url,
|
|
1358
|
+
snippet: String(r.snippet || r.description || "").replace(/\s+/g, " ").trim().slice(0, 600)
|
|
1359
|
+
}));
|
|
1360
|
+
async function postJson(url, body, headers, signal) {
|
|
1361
|
+
const res = await fetch(url, {
|
|
1362
|
+
method: "POST",
|
|
1363
|
+
headers: { "content-type": "application/json", ...headers },
|
|
1364
|
+
body: JSON.stringify(body),
|
|
1365
|
+
signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)])
|
|
1366
|
+
});
|
|
1367
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
1368
|
+
return res.json();
|
|
1369
|
+
}
|
|
1370
|
+
function createWebSearchTool(opts) {
|
|
1371
|
+
const keenableURL = opts.keenableURL ?? "https://api.keenable.ai/v1/search/public";
|
|
1372
|
+
return defineTool({
|
|
1373
|
+
name: "web_search",
|
|
1374
|
+
description: "Search the web. Returns titles, URLs and short snippets. Use it to find the right pages instead of guessing URLs; then fetch a result URL when you need the full page.",
|
|
1375
|
+
schema: z12.object({
|
|
1376
|
+
query: z12.string().min(1).max(500).describe("Search query"),
|
|
1377
|
+
max_results: z12.number().int().min(1).max(10).optional().describe("Number of results (default 6)")
|
|
1378
|
+
}),
|
|
1379
|
+
kind: "read",
|
|
1380
|
+
target: (input) => input.query,
|
|
1381
|
+
parallelSafe: () => true,
|
|
1382
|
+
async run(input, ctx) {
|
|
1383
|
+
const max = input.max_results ?? 6;
|
|
1384
|
+
const errors = [];
|
|
1385
|
+
let results = [];
|
|
1386
|
+
const bc = opts.botconnector();
|
|
1387
|
+
if (bc) {
|
|
1388
|
+
try {
|
|
1389
|
+
const data = await postJson(`${bc.baseURL.replace(/\/+$/, "")}/web/search`, { query: input.query, max_results: max }, { authorization: `Bearer ${bc.apiKey}` }, ctx.signal);
|
|
1390
|
+
results = clean(data.results, max);
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
errors.push(`BotConnector: ${error.message}`);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
if (!results.length) {
|
|
1396
|
+
try {
|
|
1397
|
+
const data = await postJson(keenableURL, { query: input.query }, { "x-keenable-title": "BotConnector" }, ctx.signal);
|
|
1398
|
+
results = clean(data.results, max);
|
|
1399
|
+
} catch (error) {
|
|
1400
|
+
errors.push(`Keenable: ${error.message}`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (!results.length) {
|
|
1404
|
+
return { output: errors.length ? `Web search failed (${errors.join("; ")}).` : "No results.", isError: errors.length > 0 };
|
|
1405
|
+
}
|
|
1406
|
+
const output = results.map((r, i) => `${i + 1}. ${r.title}
|
|
1407
|
+
${r.url}${r.snippet ? `
|
|
1408
|
+
${r.snippet}` : ""}`).join("\n");
|
|
1409
|
+
return { output, display: t("{n} results", { n: results.length }) };
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// src/tools/task.ts
|
|
1415
|
+
import { z as z13 } from "zod";
|
|
1042
1416
|
function createTaskTool(opts) {
|
|
1043
1417
|
const byName = new Map(opts.agents.map((a) => [a.name, a]));
|
|
1044
1418
|
const list = opts.agents.map((a) => `- ${a.name}: ${a.description}`).join("\n");
|
|
@@ -1046,10 +1420,10 @@ function createTaskTool(opts) {
|
|
|
1046
1420
|
name: "task",
|
|
1047
1421
|
description: `Delegate a self-contained sub-task to a subagent. It starts with an empty context and returns only its final report. Available agents:
|
|
1048
1422
|
${list}`,
|
|
1049
|
-
schema:
|
|
1050
|
-
agent:
|
|
1051
|
-
description:
|
|
1052
|
-
prompt:
|
|
1423
|
+
schema: z13.object({
|
|
1424
|
+
agent: z13.string().describe("Agent name"),
|
|
1425
|
+
description: z13.string().describe("3-6 word summary shown to the user"),
|
|
1426
|
+
prompt: z13.string().describe("Full instructions; the subagent sees nothing else")
|
|
1053
1427
|
}),
|
|
1054
1428
|
kind: "read",
|
|
1055
1429
|
target: (input) => `[${input.agent}] ${input.description}`,
|
|
@@ -1058,16 +1432,16 @@ ${list}`,
|
|
|
1058
1432
|
const allowed = byName.get(input.agent)?.tools;
|
|
1059
1433
|
if (!allowed) return false;
|
|
1060
1434
|
const base = opts.baseTools();
|
|
1061
|
-
return allowed.every((name) => base.find((
|
|
1435
|
+
return allowed.every((name) => base.find((t2) => t2.name === name)?.kind === "read");
|
|
1062
1436
|
},
|
|
1063
1437
|
async run(input, ctx) {
|
|
1064
1438
|
const def = byName.get(input.agent);
|
|
1065
|
-
if (!def) return { output: `Agent "${input.agent}"
|
|
1066
|
-
const base = opts.baseTools().filter((
|
|
1067
|
-
const unknown = (def.tools ?? []).filter((n) => !base.some((
|
|
1068
|
-
if (unknown.length) return { output: `Agent "${def.name}":
|
|
1439
|
+
if (!def) return { output: `Agent "${input.agent}" does not exist. Available: ${[...byName.keys()].join(", ")}`, isError: true };
|
|
1440
|
+
const base = opts.baseTools().filter((t2) => t2.name !== "task" && t2.name !== "exit_plan");
|
|
1441
|
+
const unknown = (def.tools ?? []).filter((n) => !base.some((t2) => t2.name === n));
|
|
1442
|
+
if (unknown.length) return { output: `Agent "${def.name}": unknown tools: ${unknown.join(", ")}`, isError: true };
|
|
1069
1443
|
const allowed = def.tools;
|
|
1070
|
-
const tools = allowed ? base.filter((
|
|
1444
|
+
const tools = allowed ? base.filter((t2) => allowed.includes(t2.name)) : base;
|
|
1071
1445
|
let provider;
|
|
1072
1446
|
try {
|
|
1073
1447
|
provider = def.model ? opts.providerFor(def.model) : opts.provider();
|
|
@@ -1085,6 +1459,7 @@ ${def.prompt}`,
|
|
|
1085
1459
|
cwd: opts.cwd,
|
|
1086
1460
|
maxSteps: def.maxSteps ?? 50,
|
|
1087
1461
|
label: def.name,
|
|
1462
|
+
reasoning: opts.reasoning?.() ?? "auto",
|
|
1088
1463
|
// Subagent edits belong to the parent's turn so /undo reverts them too.
|
|
1089
1464
|
checkpoint: ctx.checkpoint
|
|
1090
1465
|
});
|
|
@@ -1098,22 +1473,22 @@ ${def.prompt}`,
|
|
|
1098
1473
|
if (ctx.ask) child.askPermission = ctx.ask;
|
|
1099
1474
|
await child.run(input.prompt, ctx.signal);
|
|
1100
1475
|
ctx.addUsage?.(child.totalUsage);
|
|
1101
|
-
const
|
|
1102
|
-
const display =
|
|
1103
|
-
if (outcome?.type === "error") return { output: `Subagent ${def.name}
|
|
1104
|
-
if (outcome?.type === "aborted") return { output: `Subagent ${def.name}
|
|
1476
|
+
const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
|
|
1477
|
+
const display = t("{steps} steps \xB7 {tokens} tokens", { steps, tokens: tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2 });
|
|
1478
|
+
if (outcome?.type === "error") return { output: `Subagent ${def.name} failed: ${outcome.message}`, isError: true, display };
|
|
1479
|
+
if (outcome?.type === "aborted") return { output: `Subagent ${def.name} was cancelled.`, isError: true, display };
|
|
1105
1480
|
const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
|
|
1106
|
-
const report = last && typeof last.content === "string" ? last.content : "(
|
|
1481
|
+
const report = last && typeof last.content === "string" ? last.content : "(no report)";
|
|
1107
1482
|
const suffix = outcome?.type === "stepLimit" ? `
|
|
1108
1483
|
|
|
1109
|
-
(
|
|
1484
|
+
(The subagent stopped at its ${def.maxSteps ?? 50}-step limit.)` : "";
|
|
1110
1485
|
return { output: report + suffix, display };
|
|
1111
1486
|
}
|
|
1112
1487
|
});
|
|
1113
1488
|
}
|
|
1114
1489
|
|
|
1115
1490
|
// src/tools/todo.ts
|
|
1116
|
-
import { z as
|
|
1491
|
+
import { z as z14 } from "zod";
|
|
1117
1492
|
var TodoStore = class {
|
|
1118
1493
|
items = [];
|
|
1119
1494
|
listeners = /* @__PURE__ */ new Set();
|
|
@@ -1130,36 +1505,47 @@ function createTodoTool(store) {
|
|
|
1130
1505
|
return defineTool({
|
|
1131
1506
|
name: "todo_write",
|
|
1132
1507
|
description: "Write the task list for multi-step work (3+ steps). Send the whole list every time; exactly one item in_progress while working. Mark items completed as soon as they are done.",
|
|
1133
|
-
schema:
|
|
1134
|
-
todos:
|
|
1508
|
+
schema: z14.object({
|
|
1509
|
+
todos: z14.array(z14.object({ content: z14.string(), status: z14.enum(["pending", "in_progress", "completed"]) }))
|
|
1135
1510
|
}),
|
|
1136
1511
|
kind: "read",
|
|
1137
1512
|
target: (input) => `${input.todos.length} item`,
|
|
1138
1513
|
async run(input) {
|
|
1139
|
-
if (input.todos.filter((
|
|
1140
|
-
return { output: "
|
|
1514
|
+
if (input.todos.filter((t2) => t2.status === "in_progress").length > 1) {
|
|
1515
|
+
return { output: "Only one item may be in_progress.", isError: true };
|
|
1141
1516
|
}
|
|
1142
1517
|
store.set(input.todos);
|
|
1143
|
-
const done = input.todos.filter((
|
|
1144
|
-
return { output: `Todo
|
|
1518
|
+
const done = input.todos.filter((t2) => t2.status === "completed").length;
|
|
1519
|
+
return { output: `Todo updated: ${done}/${input.todos.length} completed.` };
|
|
1145
1520
|
}
|
|
1146
1521
|
});
|
|
1147
1522
|
}
|
|
1148
1523
|
|
|
1149
1524
|
// src/setup.ts
|
|
1150
1525
|
var TOOL_RULES = { bash: "bash", edit: "edit", write: "edit", fetch: "fetch" };
|
|
1526
|
+
function lunaPtcAutoEnabled(env) {
|
|
1527
|
+
const value = String(env.BCCLI_LUNA_PTC ?? "").trim().toLowerCase();
|
|
1528
|
+
return !["0", "false", "off", "no"].includes(value);
|
|
1529
|
+
}
|
|
1151
1530
|
function createRuntime(opts) {
|
|
1152
1531
|
const env = opts.env ?? process.env;
|
|
1153
1532
|
const home = bccliHome(env);
|
|
1154
1533
|
let config = loadConfig(opts.cwd, env);
|
|
1155
1534
|
let modelRef = opts.args.model ?? config.model;
|
|
1535
|
+
let reasoning = opts.args.reasoning ?? config.reasoning;
|
|
1156
1536
|
const makeProvider = (ref) => {
|
|
1157
1537
|
const resolved = resolveModel(config, ref, env);
|
|
1158
|
-
return opts.provider ?? createProvider({
|
|
1538
|
+
return opts.provider ?? createProvider({
|
|
1539
|
+
baseURL: resolved.baseURL,
|
|
1540
|
+
apiKey: resolved.apiKey,
|
|
1541
|
+
model: resolved.model,
|
|
1542
|
+
providerId: resolved.providerId,
|
|
1543
|
+
enableProgrammaticToolCalling: lunaPtcAutoEnabled(env)
|
|
1544
|
+
});
|
|
1159
1545
|
};
|
|
1160
1546
|
const provider = makeProvider(modelRef);
|
|
1161
1547
|
const mode = opts.args.allowAll ? "allowAll" : opts.args.permissionMode ?? config.permissionMode;
|
|
1162
|
-
const rules = [...config.allow, ...opts.args.allowedTools.map((
|
|
1548
|
+
const rules = [...config.allow, ...opts.args.allowedTools.map((t2) => TOOL_RULES[t2] ?? t2)];
|
|
1163
1549
|
const permissions = new Permissions(mode, rules, opts.cwd);
|
|
1164
1550
|
const roots = { cwd: opts.cwd, home, userHome: opts.userHome ?? homedir() };
|
|
1165
1551
|
const skills = loadSkills(roots);
|
|
@@ -1179,16 +1565,27 @@ function createRuntime(opts) {
|
|
|
1179
1565
|
}
|
|
1180
1566
|
}
|
|
1181
1567
|
const systemPrompt = buildSystemPrompt({ cwd: opts.cwd, home, model: modelRef, skills });
|
|
1182
|
-
const
|
|
1568
|
+
const webSearch = createWebSearchTool({
|
|
1569
|
+
botconnector: () => {
|
|
1570
|
+
try {
|
|
1571
|
+
const bc = resolveModel(config, "bc-cloud/search", env);
|
|
1572
|
+
return bc.apiKey ? { baseURL: bc.baseURL, apiKey: bc.apiKey } : void 0;
|
|
1573
|
+
} catch {
|
|
1574
|
+
return void 0;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
});
|
|
1578
|
+
const baseTools = [...ALL_TOOLS, webSearch, createSkillTool(skills), createTodoTool(todos), createExitPlanTool({ permissions, interaction })];
|
|
1183
1579
|
const task = createTaskTool({
|
|
1184
1580
|
agents: [...BUILTIN_AGENTS, ...agentDefs],
|
|
1185
1581
|
// Subagents get whatever the main agent has right now (incl. MCP tools), minus task itself.
|
|
1186
|
-
baseTools: () => agent.tools.filter((
|
|
1582
|
+
baseTools: () => agent.tools.filter((t2) => t2.name !== "task"),
|
|
1187
1583
|
permissions,
|
|
1188
1584
|
provider: () => agent.provider,
|
|
1189
1585
|
providerFor: makeProvider,
|
|
1190
1586
|
systemPrompt,
|
|
1191
|
-
cwd: opts.cwd
|
|
1587
|
+
cwd: opts.cwd,
|
|
1588
|
+
reasoning: () => reasoning
|
|
1192
1589
|
});
|
|
1193
1590
|
const agent = new Agent({
|
|
1194
1591
|
provider,
|
|
@@ -1197,6 +1594,7 @@ function createRuntime(opts) {
|
|
|
1197
1594
|
systemPrompt,
|
|
1198
1595
|
cwd: opts.cwd,
|
|
1199
1596
|
history,
|
|
1597
|
+
reasoning,
|
|
1200
1598
|
onMessage: (m) => session.append(m),
|
|
1201
1599
|
onReset: () => session.reset(),
|
|
1202
1600
|
onTurnStart: () => checkpoints.beginTurn(),
|
|
@@ -1238,6 +1636,9 @@ function createRuntime(opts) {
|
|
|
1238
1636
|
get modelRef() {
|
|
1239
1637
|
return modelRef;
|
|
1240
1638
|
},
|
|
1639
|
+
get reasoning() {
|
|
1640
|
+
return reasoning;
|
|
1641
|
+
},
|
|
1241
1642
|
agent,
|
|
1242
1643
|
get session() {
|
|
1243
1644
|
return session;
|
|
@@ -1267,36 +1668,59 @@ function createRuntime(opts) {
|
|
|
1267
1668
|
setModel(ref) {
|
|
1268
1669
|
agent.provider = makeProvider(ref);
|
|
1269
1670
|
modelRef = ref;
|
|
1671
|
+
},
|
|
1672
|
+
setReasoning(level) {
|
|
1673
|
+
reasoning = level;
|
|
1674
|
+
agent.reasoning = level;
|
|
1270
1675
|
}
|
|
1271
1676
|
};
|
|
1272
1677
|
}
|
|
1273
1678
|
|
|
1274
1679
|
// src/version.ts
|
|
1275
|
-
var VERSION = true ? "0.4.0-beta.
|
|
1680
|
+
var VERSION = true ? "0.4.0-beta.30" : "0.0.0-dev";
|
|
1276
1681
|
|
|
1277
1682
|
// src/cli.ts
|
|
1683
|
+
function chooseLanguage(argv) {
|
|
1684
|
+
let fromConfig;
|
|
1685
|
+
try {
|
|
1686
|
+
fromConfig = readGlobalConfigFile(process.env).language;
|
|
1687
|
+
} catch {
|
|
1688
|
+
}
|
|
1689
|
+
setLanguage(resolveLanguage(argv, process.env, fromConfig));
|
|
1690
|
+
}
|
|
1278
1691
|
async function main() {
|
|
1692
|
+
chooseLanguage(process.argv.slice(2));
|
|
1279
1693
|
const args = parseCliArgs(process.argv.slice(2));
|
|
1280
1694
|
if (args.version) {
|
|
1281
1695
|
console.log(VERSION);
|
|
1282
1696
|
return 0;
|
|
1283
1697
|
}
|
|
1284
1698
|
if (args.help) {
|
|
1285
|
-
console.log(
|
|
1699
|
+
console.log(helpText());
|
|
1286
1700
|
return 0;
|
|
1287
1701
|
}
|
|
1288
1702
|
const cwd = process.cwd();
|
|
1289
1703
|
if (args.command === "login") return runLogin(args.loginProvider ?? "bc-cloud", cwd);
|
|
1290
1704
|
if (args.command === "mcp") {
|
|
1291
|
-
const { runMcpCommand } = await import("./mcpCli-
|
|
1292
|
-
const { readSecret } = await import("./login-
|
|
1705
|
+
const { runMcpCommand } = await import("./mcpCli-J43FHF2H.js");
|
|
1706
|
+
const { readSecret } = await import("./login-HUPT4247.js");
|
|
1293
1707
|
return runMcpCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
|
|
1294
1708
|
}
|
|
1295
1709
|
if (args.command === "provider") {
|
|
1296
|
-
const { runProviderCommand } = await import("./providerCli-
|
|
1297
|
-
const { readSecret } = await import("./login-
|
|
1710
|
+
const { runProviderCommand } = await import("./providerCli-II7TQDYQ.js");
|
|
1711
|
+
const { readSecret } = await import("./login-HUPT4247.js");
|
|
1298
1712
|
return runProviderCommand(args, { env: process.env, cwd, out: (s) => console.log(s), err: (s) => console.error(s), readSecret });
|
|
1299
1713
|
}
|
|
1714
|
+
if (args.command === "connect" || args.command === "disconnect" || args.command === "integrations") {
|
|
1715
|
+
const { runIntegrationCommand } = await import("./integrationCli-ZTZSZLO7.js");
|
|
1716
|
+
return runIntegrationCommand(args, {
|
|
1717
|
+
env: process.env,
|
|
1718
|
+
cwd,
|
|
1719
|
+
out: (s) => console.log(s),
|
|
1720
|
+
err: (s) => console.error(s),
|
|
1721
|
+
fetch: globalThis.fetch
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1300
1724
|
if (args.command === "models") {
|
|
1301
1725
|
const config = loadConfig(cwd);
|
|
1302
1726
|
const resolved = resolveModel(config, args.model ?? config.model);
|
|
@@ -1310,11 +1734,11 @@ async function main() {
|
|
|
1310
1734
|
} catch {
|
|
1311
1735
|
}
|
|
1312
1736
|
if (args.print) {
|
|
1313
|
-
if (!args.prompt) throw new ConfigError('
|
|
1314
|
-
const { serversToStart } = await import("./config-
|
|
1737
|
+
if (!args.prompt) throw new ConfigError(t('-p mode needs a task, e.g. bccli -p "explain this repo"'));
|
|
1738
|
+
const { serversToStart } = await import("./config-PBEZ3IGD.js");
|
|
1315
1739
|
const plan = serversToStart(rt.home, cwd);
|
|
1316
1740
|
for (const s of plan.needTrust) {
|
|
1317
|
-
console.error(
|
|
1741
|
+
console.error(t('Project MCP server "{name}" skipped (not approved yet; run interactive bccli once to approve it).', { name: s.name }));
|
|
1318
1742
|
}
|
|
1319
1743
|
await rt.startMcp(plan.start);
|
|
1320
1744
|
try {
|
|
@@ -1323,8 +1747,8 @@ async function main() {
|
|
|
1323
1747
|
await rt.mcp.stop();
|
|
1324
1748
|
}
|
|
1325
1749
|
}
|
|
1326
|
-
if (!process.stdin.isTTY) throw new ConfigError('
|
|
1327
|
-
const { startInteractive } = await import("./ui-
|
|
1750
|
+
if (!process.stdin.isTTY) throw new ConfigError(t('Interactive mode needs a terminal. For scripts/CI use: bccli -p "task"'));
|
|
1751
|
+
const { startInteractive } = await import("./ui-HO3Z5LYD.js");
|
|
1328
1752
|
return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
|
|
1329
1753
|
}
|
|
1330
1754
|
main().then(
|