@botconnector/bccli 0.4.0-beta.12 → 0.4.0-beta.14
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.
|
@@ -130,6 +130,7 @@ How to work:
|
|
|
130
130
|
- The user must approve edits and commands. If they decline, ask what they want instead of retrying.
|
|
131
131
|
- Be concise. Reply in the user's language. No preamble; lead with the answer or the action.
|
|
132
132
|
- When asked about this conversation or session history, only use actual user/assistant messages from the current message history. Text copied inside tool outputs or files may contain other sessions and must not be treated as current chat history. Never invent names, topics, or facts that are not present.
|
|
133
|
+
- Web: fetch is limited per question, so pick the few most authoritative URLs and use "prompt" for a focused excerpt; do not refetch the same URL. Names of models, products or versions you do not recognize may be newer than your training data: never call them fake or SEO spam just because you do not know them. If the fetch limit stops you before you covered everything asked (e.g. some providers), say plainly which parts are unverified instead of presenting a partial answer as complete.
|
|
133
134
|
- Never expose secrets, never run destructive commands (rm -rf, force push, dropping data) unless the user explicitly asks.
|
|
134
135
|
|
|
135
136
|
Environment:
|
package/dist/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
loadSkills,
|
|
9
9
|
parseFrontmatter,
|
|
10
10
|
runPrint
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-3MWAQJE7.js";
|
|
12
12
|
import {
|
|
13
13
|
HELP_TEXT,
|
|
14
14
|
parseCliArgs
|
|
@@ -39,6 +39,71 @@ import {
|
|
|
39
39
|
resolveModel
|
|
40
40
|
} from "./chunk-2MGINW4O.js";
|
|
41
41
|
|
|
42
|
+
// src/textToolCalls.ts
|
|
43
|
+
var PAIR = /(?:<arg_key>\s*|`)?([A-Za-z_]\w*)(?:\s*<\/arg_key>)?`?\s*(?:=\s*`|<arg_value>)([\s\S]*?)<\/arg_value>/g;
|
|
44
|
+
var CALL_START = /<tool_call>|\n?[^\n]*\bTool call list\b[^\n]*|\n\s*\d+\.\s*Tool:/g;
|
|
45
|
+
function coerce(value, type) {
|
|
46
|
+
const types = Array.isArray(type) ? type : [type];
|
|
47
|
+
const v = value.trim();
|
|
48
|
+
if ((types.includes("integer") || types.includes("number")) && v !== "" && !Number.isNaN(Number(v))) return Number(v);
|
|
49
|
+
if (types.includes("boolean") && /^(true|false)$/i.test(v)) return v.toLowerCase() === "true";
|
|
50
|
+
if ((types.includes("object") || types.includes("array")) && /^[[{]/.test(v)) {
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(v);
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return value.replace(/^\n+|\n+$/g, "");
|
|
57
|
+
}
|
|
58
|
+
function pickTool(name, keys, tools) {
|
|
59
|
+
const named = name && tools.find((t) => t.function.name === name);
|
|
60
|
+
if (named) return named;
|
|
61
|
+
let best;
|
|
62
|
+
let bestScore = 0;
|
|
63
|
+
let tie = false;
|
|
64
|
+
for (const t of tools) {
|
|
65
|
+
const params = t.function.parameters;
|
|
66
|
+
const props = Object.keys(params.properties ?? {});
|
|
67
|
+
if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
|
|
68
|
+
const score = keys.filter((k) => props.includes(k)).length;
|
|
69
|
+
if (score > bestScore) [best, bestScore, tie] = [t, score, false];
|
|
70
|
+
else if (score === bestScore && score > 0) tie = true;
|
|
71
|
+
}
|
|
72
|
+
return tie ? void 0 : best;
|
|
73
|
+
}
|
|
74
|
+
function recoverTextToolCalls(text, tools) {
|
|
75
|
+
if (!/<\/arg_value>|<tool_call>/.test(text)) return null;
|
|
76
|
+
const starts = [...text.matchAll(CALL_START)].map((m) => m.index ?? 0);
|
|
77
|
+
const firstPair = text.search(PAIR);
|
|
78
|
+
const cut = Math.min(...[...starts, firstPair].filter((i) => i >= 0));
|
|
79
|
+
const bounds = starts.filter((i) => i >= cut);
|
|
80
|
+
if (!bounds.length || bounds[0] > cut) bounds.unshift(cut);
|
|
81
|
+
const toolCalls = [];
|
|
82
|
+
bounds.forEach((start, n) => {
|
|
83
|
+
const segment = text.slice(start, bounds[n + 1] ?? text.length);
|
|
84
|
+
const name = /<tool_call>\s*([A-Za-z_][\w.-]*)/.exec(segment)?.[1] ?? /Tool:\s*`?([A-Za-z_][\w.-]*)`?/.exec(segment)?.[1];
|
|
85
|
+
let args = [];
|
|
86
|
+
const flush = () => {
|
|
87
|
+
if (!args.length) return;
|
|
88
|
+
const keys = args.map(([k]) => k);
|
|
89
|
+
const tool = pickTool(name, keys, tools);
|
|
90
|
+
if (tool) {
|
|
91
|
+
const props = tool.function.parameters.properties ?? {};
|
|
92
|
+
const input = Object.fromEntries(args.filter(([k]) => k in props).map(([k, v]) => [k, coerce(v, props[k].type)]));
|
|
93
|
+
toolCalls.push({ id: `text_call_${toolCalls.length}`, name: tool.function.name, arguments: JSON.stringify(input) });
|
|
94
|
+
}
|
|
95
|
+
args = [];
|
|
96
|
+
};
|
|
97
|
+
for (const m of segment.matchAll(PAIR)) {
|
|
98
|
+
if (args.some(([k]) => k === m[1])) flush();
|
|
99
|
+
args.push([m[1], m[2]]);
|
|
100
|
+
}
|
|
101
|
+
flush();
|
|
102
|
+
});
|
|
103
|
+
if (!toolCalls.length) return null;
|
|
104
|
+
return { text: text.slice(0, cut).trim(), toolCalls };
|
|
105
|
+
}
|
|
106
|
+
|
|
42
107
|
// src/tools/index.ts
|
|
43
108
|
import { z as z8 } from "zod";
|
|
44
109
|
|
|
@@ -279,7 +344,12 @@ var editTool = defineTool({
|
|
|
279
344
|
|
|
280
345
|
// src/tools/fetch.ts
|
|
281
346
|
import { z as z3 } from "zod";
|
|
282
|
-
var MAX_CHARS =
|
|
347
|
+
var MAX_CHARS = 12e3;
|
|
348
|
+
var FOCUS_CHUNKS = 3;
|
|
349
|
+
var FOCUS_SIZE = 800;
|
|
350
|
+
var CACHE_MAX = 16;
|
|
351
|
+
var CACHE_TTL_MS = 15 * 6e4;
|
|
352
|
+
var CACHE_MAX_CHARS = 2e6;
|
|
283
353
|
var MAX_BYTES = 5 * 1024 * 1024;
|
|
284
354
|
async function readCapped(res) {
|
|
285
355
|
const reader = res.body?.getReader();
|
|
@@ -315,45 +385,183 @@ function describeFetchError(error) {
|
|
|
315
385
|
}
|
|
316
386
|
return code ? `${detail} (${code})` : detail;
|
|
317
387
|
}
|
|
318
|
-
var
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
388
|
+
var tokensOf = (text) => Math.ceil(text.length / 4);
|
|
389
|
+
function isPrivateHost(host) {
|
|
390
|
+
const h = host.toLowerCase().replace(/^\[|\]$/g, "");
|
|
391
|
+
if (h === "localhost" || /\.(local|internal|lan|localdomain|home\.arpa)$/.test(h)) return true;
|
|
392
|
+
if (h.includes(":")) return /^(::1?$|f[cd]|fe[89ab]|::ffff:)/.test(h);
|
|
393
|
+
const v4 = h.match(/^(\d+)\.(\d+)\.\d+\.\d+$/);
|
|
394
|
+
if (v4) {
|
|
395
|
+
const [a, b] = [Number(v4[1]), Number(v4[2])];
|
|
396
|
+
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;
|
|
397
|
+
}
|
|
398
|
+
return !h.includes(".");
|
|
399
|
+
}
|
|
400
|
+
var words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]{2,}/gu) ?? [];
|
|
401
|
+
function pickRelevant(text, query, count = FOCUS_CHUNKS, size = FOCUS_SIZE) {
|
|
402
|
+
const chunks = [];
|
|
403
|
+
let cur = "";
|
|
404
|
+
for (const line of text.split("\n")) {
|
|
405
|
+
for (let i = 0; i < Math.max(line.length, 1); i += size) {
|
|
406
|
+
cur += `${line.slice(i, i + size)}
|
|
407
|
+
`;
|
|
408
|
+
if (cur.length >= size) {
|
|
409
|
+
chunks.push(cur.trimEnd());
|
|
410
|
+
cur = "";
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (cur.trim()) chunks.push(cur.trimEnd());
|
|
415
|
+
const q = [...new Set(words(query))];
|
|
416
|
+
const docs = chunks.map(words);
|
|
417
|
+
const avg = docs.reduce((n, d) => n + d.length, 0) / (docs.length || 1) || 1;
|
|
418
|
+
const idf = q.map((t) => {
|
|
419
|
+
const df = docs.filter((d) => d.includes(t)).length;
|
|
420
|
+
return Math.log(1 + (docs.length - df + 0.5) / (df + 0.5));
|
|
421
|
+
});
|
|
422
|
+
const scored = docs.map((d, i) => ({
|
|
423
|
+
i,
|
|
424
|
+
score: q.reduce((sum, t, k) => {
|
|
425
|
+
const tf = d.filter((w) => w === t).length;
|
|
426
|
+
return sum + idf[k] * tf * 2.2 / (tf + 1.2 * (0.25 + 0.75 * d.length / avg));
|
|
427
|
+
}, 0)
|
|
428
|
+
})).filter((c) => c.score > 0).sort((a, b) => b.score - a.score).slice(0, count).sort((a, b) => a.i - b.i);
|
|
429
|
+
return scored.length ? scored.map((c) => chunks[c.i]).join("\n\u2026\n") : null;
|
|
430
|
+
}
|
|
431
|
+
var cache = /* @__PURE__ */ new Map();
|
|
432
|
+
function remember(key, value, size) {
|
|
433
|
+
if (size > CACHE_MAX_CHARS) return;
|
|
434
|
+
cache.delete(key);
|
|
435
|
+
cache.set(key, value);
|
|
436
|
+
if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
|
|
437
|
+
}
|
|
438
|
+
function recall(key) {
|
|
439
|
+
const hit = cache.get(key);
|
|
440
|
+
if (typeof hit === "object" && Date.now() - hit.at > CACHE_TTL_MS) {
|
|
441
|
+
cache.delete(key);
|
|
442
|
+
return void 0;
|
|
443
|
+
}
|
|
444
|
+
return hit;
|
|
445
|
+
}
|
|
446
|
+
var ACCEPT = "text/markdown, text/html;q=0.9, text/plain;q=0.8, */*;q=0.5";
|
|
447
|
+
var withCost = (text, what) => `${what} \xB7 ~${tokensOf(text)} token`;
|
|
448
|
+
function createFetchTool(opts = {}) {
|
|
449
|
+
const keenableURL = opts.keenableURL === void 0 ? "https://api.keenable.ai/v1/fetch/public" : opts.keenableURL;
|
|
450
|
+
const isPrivate = opts.isPrivate ?? isPrivateHost;
|
|
451
|
+
return defineTool({
|
|
452
|
+
name: "fetch",
|
|
453
|
+
description: `Fetch a URL (http/https) as text/markdown. Costs the user tokens, so ask for as little as you need:
|
|
454
|
+
- 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.
|
|
455
|
+
- Without \`prompt\` you get the first ${MAX_CHARS} characters; the reply tells you the \`offset\` to continue from.
|
|
456
|
+
- Fetching a page that is already in this conversation is wasteful; reuse what you have.`,
|
|
457
|
+
schema: z3.object({
|
|
458
|
+
url: z3.string().describe("Absolute http(s) URL"),
|
|
459
|
+
prompt: z3.string().max(2e3).optional().describe("What to look for in the page; returns only the relevant excerpt"),
|
|
460
|
+
offset: z3.number().int().min(0).optional().describe("Start reading at this character (to continue a truncated page)")
|
|
461
|
+
}),
|
|
462
|
+
kind: "fetch",
|
|
463
|
+
target: (input) => input.url,
|
|
464
|
+
async run(input, ctx) {
|
|
465
|
+
if (!/^https?:\/\//i.test(input.url)) return { output: "URL harus diawali http:// atau https://", isError: true };
|
|
466
|
+
const target = new URL(input.url);
|
|
467
|
+
const prompt = input.prompt?.trim();
|
|
468
|
+
const askKeenable = !!prompt && !input.offset && !!keenableURL && !target.username && !target.password && !isPrivate(target.hostname);
|
|
469
|
+
if (askKeenable) {
|
|
470
|
+
const key = `x
|
|
471
|
+
${input.url}
|
|
472
|
+
${prompt}`;
|
|
473
|
+
const hit = recall(key);
|
|
474
|
+
if (hit) {
|
|
475
|
+
ctx.refundFetch?.();
|
|
476
|
+
return { output: hit, display: withCost(hit, "kutipan fokus \xB7 cache") };
|
|
340
477
|
}
|
|
478
|
+
const out = await extractWithKeenable(keenableURL, input.url, prompt, ctx.signal);
|
|
479
|
+
if (out) {
|
|
480
|
+
const output2 = `[Kutipan dari ${input.url} untuk "${prompt}" (diekstrak Keenable). Kalau jawabannya "tidak ada", coba fetch tanpa prompt.]
|
|
481
|
+
${out}`;
|
|
482
|
+
remember(key, output2, output2.length);
|
|
483
|
+
return { output: output2, display: withCost(output2, "kutipan fokus (Keenable)") };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
let page = recall(`p
|
|
487
|
+
${input.url}`);
|
|
488
|
+
const cached = !!page;
|
|
489
|
+
if (cached) ctx.refundFetch?.();
|
|
490
|
+
if (!page) {
|
|
491
|
+
const loaded = await loadPage(input.url, ctx.signal);
|
|
492
|
+
if ("error" in loaded) return { output: loaded.error, isError: true };
|
|
493
|
+
page = loaded;
|
|
494
|
+
remember(`p
|
|
495
|
+
${input.url}`, page, page.text.length);
|
|
496
|
+
}
|
|
497
|
+
const { text, truncated } = page;
|
|
498
|
+
const via = cached ? " \xB7 cache" : "";
|
|
499
|
+
if (prompt && !input.offset) {
|
|
500
|
+
const picked = pickRelevant(text, prompt);
|
|
501
|
+
if (picked) {
|
|
502
|
+
const output2 = `[Kutipan paling relevan dari ${input.url} untuk "${prompt}" (${text.length} karakter total). Halaman penuh: fetch tanpa prompt.]
|
|
503
|
+
${picked}`;
|
|
504
|
+
return { output: output2, display: withCost(output2, `kutipan fokus (lokal)${via}`) };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const offset = input.offset ?? 0;
|
|
508
|
+
if (offset >= text.length && text.length > 0) {
|
|
509
|
+
return { output: `offset ${offset} melewati akhir halaman (${text.length} karakter).`, isError: true };
|
|
510
|
+
}
|
|
511
|
+
const end = Math.min(offset + MAX_CHARS, text.length);
|
|
512
|
+
let output = text.slice(offset, end);
|
|
513
|
+
if (end < text.length || truncated) {
|
|
514
|
+
const total = truncated ? `${text.length}+` : text.length;
|
|
515
|
+
output += `
|
|
516
|
+
\u2026 [dipotong: karakter ${offset}-${end} dari ${total}. Lanjut dengan offset=${end}, atau pakai prompt untuk kutipan terfokus.]`;
|
|
517
|
+
}
|
|
518
|
+
const size = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
|
|
519
|
+
return { output, display: withCost(output, `${size}${via}`) };
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
async function extractWithKeenable(endpoint, url, prompt, signal) {
|
|
524
|
+
try {
|
|
525
|
+
const u = new URL(endpoint);
|
|
526
|
+
u.searchParams.set("url", url);
|
|
527
|
+
u.searchParams.set("prompt", prompt);
|
|
528
|
+
u.searchParams.set("live", "true");
|
|
529
|
+
u.searchParams.set("max_chars", String(MAX_CHARS));
|
|
530
|
+
const res = await fetch(u, { headers: { "X-Keenable-Title": "BotConnector" }, signal: AbortSignal.any([signal, AbortSignal.timeout(25e3)]) });
|
|
531
|
+
if (!res.ok) return void 0;
|
|
532
|
+
const content = (await res.json()).content;
|
|
533
|
+
return typeof content === "string" && content.trim() ? content.slice(0, MAX_CHARS) : void 0;
|
|
534
|
+
} catch {
|
|
535
|
+
return void 0;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
async function loadPage(inputUrl, signal) {
|
|
539
|
+
let url = new URL(inputUrl);
|
|
540
|
+
let res;
|
|
541
|
+
try {
|
|
542
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
543
|
+
res = await fetch(url, { headers: { accept: ACCEPT }, signal: AbortSignal.any([signal, AbortSignal.timeout(3e4)]), redirect: "manual" });
|
|
544
|
+
const location = res.status >= 300 && res.status < 400 ? res.headers.get("location") : null;
|
|
545
|
+
if (!location) break;
|
|
546
|
+
const next = new URL(location, url);
|
|
547
|
+
if (next.host !== url.host) {
|
|
341
548
|
await res.body?.cancel();
|
|
342
|
-
|
|
549
|
+
return { error: `Redirect ke host lain: ${next.href}. Panggil fetch lagi dengan URL itu kalau memang perlu (akan diminta izin).` };
|
|
343
550
|
}
|
|
344
|
-
|
|
345
|
-
|
|
551
|
+
await res.body?.cancel();
|
|
552
|
+
url = next;
|
|
346
553
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
const text = (res.headers.get("content-type") ?? "").includes("html") ? htmlToText(body) : body;
|
|
350
|
-
if (!res.ok) return { output: `HTTP ${res.status} dari ${input.url}: ${text.slice(0, 500)}`, isError: true };
|
|
351
|
-
const clipped = text.length > MAX_CHARS || truncated ? `${text.slice(0, MAX_CHARS)}
|
|
352
|
-
\u2026 [dipotong]` : text;
|
|
353
|
-
const display = truncated ? "lebih dari 5 MB, dipotong" : `${text.length} karakter`;
|
|
354
|
-
return { output: clipped, display };
|
|
554
|
+
} catch (error) {
|
|
555
|
+
return { error: `Gagal mengambil ${inputUrl}: ${describeFetchError(error)}` };
|
|
355
556
|
}
|
|
356
|
-
}
|
|
557
|
+
if (!res) return { error: `Gagal mengambil ${inputUrl}` };
|
|
558
|
+
const { text: body, truncated } = await readCapped(res);
|
|
559
|
+
const type = res.headers.get("content-type") ?? "";
|
|
560
|
+
const text = type.includes("html") ? htmlToText(body) : body;
|
|
561
|
+
if (!res.ok) return { error: `HTTP ${res.status} dari ${inputUrl}: ${text.slice(0, 500)}` };
|
|
562
|
+
return { text, truncated, at: Date.now() };
|
|
563
|
+
}
|
|
564
|
+
var fetchTool = createFetchTool();
|
|
357
565
|
|
|
358
566
|
// src/tools/glob.ts
|
|
359
567
|
import fg from "fast-glob";
|
|
@@ -536,70 +744,79 @@ function toolDefinitions(tools) {
|
|
|
536
744
|
});
|
|
537
745
|
}
|
|
538
746
|
|
|
539
|
-
// src/
|
|
540
|
-
var
|
|
541
|
-
var
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
747
|
+
// src/webBudget.ts
|
|
748
|
+
var KEEP_TOKENS = 4e4;
|
|
749
|
+
var PRUNE_MIN_TOKENS = 2e4;
|
|
750
|
+
var STUB = "[hasil tool lama dihapus:";
|
|
751
|
+
var tokens = (text) => Math.ceil(text.length / 4);
|
|
752
|
+
var isStub = (content) => content.startsWith(STUB);
|
|
753
|
+
function pruneOldFetches(messages) {
|
|
754
|
+
const urls = /* @__PURE__ */ new Map();
|
|
755
|
+
for (const m of messages) {
|
|
756
|
+
if (m.role !== "assistant") continue;
|
|
757
|
+
for (const c of m.tool_calls ?? []) {
|
|
758
|
+
if (c.function.name !== "fetch") continue;
|
|
759
|
+
try {
|
|
760
|
+
urls.set(c.id, String(JSON.parse(c.function.arguments).url ?? ""));
|
|
761
|
+
} catch {
|
|
762
|
+
urls.set(c.id, "");
|
|
763
|
+
}
|
|
551
764
|
}
|
|
552
765
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if (!(params.required ?? []).every((r) => keys.includes(r))) continue;
|
|
565
|
-
const score = keys.filter((k) => props.includes(k)).length;
|
|
566
|
-
if (score > bestScore) [best, bestScore, tie] = [t, score, false];
|
|
567
|
-
else if (score === bestScore && score > 0) tie = true;
|
|
766
|
+
let kept = 0;
|
|
767
|
+
let oldTokens = 0;
|
|
768
|
+
const old = [];
|
|
769
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
770
|
+
const m = messages[i];
|
|
771
|
+
if (m.role !== "tool" || !urls.has(m.tool_call_id) || isStub(m.content)) continue;
|
|
772
|
+
kept += tokens(m.content);
|
|
773
|
+
if (kept > KEEP_TOKENS) {
|
|
774
|
+
old.push(i);
|
|
775
|
+
oldTokens += tokens(m.content);
|
|
776
|
+
}
|
|
568
777
|
}
|
|
569
|
-
|
|
778
|
+
if (oldTokens <= PRUNE_MIN_TOKENS) return false;
|
|
779
|
+
for (const i of old) {
|
|
780
|
+
const m = messages[i];
|
|
781
|
+
messages[i] = { ...m, content: `${STUB} fetch ${urls.get(m.tool_call_id)}. Ambil lagi dengan fetch (pakai prompt untuk kutipan terfokus) kalau masih perlu.]` };
|
|
782
|
+
}
|
|
783
|
+
return true;
|
|
570
784
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (args.some(([k]) => k === m[1])) flush();
|
|
596
|
-
args.push([m[1], m[2]]);
|
|
785
|
+
var WebBudget = class {
|
|
786
|
+
fetches = 0;
|
|
787
|
+
chars = 0;
|
|
788
|
+
perTurn;
|
|
789
|
+
sessionChars;
|
|
790
|
+
constructor(limits = {}) {
|
|
791
|
+
this.perTurn = limits.perTurn ?? 8;
|
|
792
|
+
this.sessionChars = limits.sessionChars ?? 3e5;
|
|
793
|
+
}
|
|
794
|
+
startTurn() {
|
|
795
|
+
this.fetches = 0;
|
|
796
|
+
}
|
|
797
|
+
/** Gives back a reservation, e.g. when the fetch was served from cache and cost nothing. */
|
|
798
|
+
refund() {
|
|
799
|
+
if (this.fetches > 0) this.fetches--;
|
|
800
|
+
}
|
|
801
|
+
reset() {
|
|
802
|
+
this.fetches = 0;
|
|
803
|
+
this.chars = 0;
|
|
804
|
+
}
|
|
805
|
+
/** Reserves one fetch; returns the refusal to show the model when over budget. */
|
|
806
|
+
take() {
|
|
807
|
+
if (this.fetches >= this.perTurn) {
|
|
808
|
+
return `Batas ${this.perTurn} fetch per pertanyaan tercapai (tiap fetch memakai token user). Rangkum jawaban dari hasil yang sudah ada; kalau memang kurang, jelaskan ke user apa yang belum ketemu.`;
|
|
597
809
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
810
|
+
if (this.chars >= this.sessionChars) {
|
|
811
|
+
return `Batas total halaman web sesi ini tercapai (~${Math.round(this.sessionChars / 4e3)}k token). Rangkum dari hasil yang sudah ada dan tanyakan ke user apakah perlu mencari lebih banyak.`;
|
|
812
|
+
}
|
|
813
|
+
this.fetches++;
|
|
814
|
+
return void 0;
|
|
815
|
+
}
|
|
816
|
+
record(chars) {
|
|
817
|
+
this.chars += chars;
|
|
818
|
+
}
|
|
819
|
+
};
|
|
603
820
|
|
|
604
821
|
// src/agent.ts
|
|
605
822
|
var estimateTokens = (text) => Math.ceil(text.length / 4);
|
|
@@ -635,6 +852,7 @@ var Agent = class {
|
|
|
635
852
|
turnTools;
|
|
636
853
|
turnDefinitions;
|
|
637
854
|
readFiles = /* @__PURE__ */ new Set();
|
|
855
|
+
webBudget = new WebBudget();
|
|
638
856
|
maxSteps;
|
|
639
857
|
// ponytail: one window for every model; read it from /v1/models metadata when providers expose it.
|
|
640
858
|
contextWindow;
|
|
@@ -664,11 +882,13 @@ var Agent = class {
|
|
|
664
882
|
load(messages) {
|
|
665
883
|
this.messages = messages;
|
|
666
884
|
this.readFiles.clear();
|
|
885
|
+
this.webBudget.reset();
|
|
667
886
|
this.lastInputTokens = 0;
|
|
668
887
|
}
|
|
669
888
|
clear() {
|
|
670
889
|
this.messages = [];
|
|
671
890
|
this.readFiles.clear();
|
|
891
|
+
this.webBudget.reset();
|
|
672
892
|
this.lastInputTokens = 0;
|
|
673
893
|
this.opts.onReset?.();
|
|
674
894
|
}
|
|
@@ -690,6 +910,7 @@ ${completion.text}` });
|
|
|
690
910
|
this.turnTools = this.tools;
|
|
691
911
|
this.turnDefinitions = this.definitions;
|
|
692
912
|
this.opts.onTurnStart?.();
|
|
913
|
+
this.webBudget.startTurn();
|
|
693
914
|
try {
|
|
694
915
|
if (this.lastInputTokens > this.contextWindow * 0.8) await this.compact(signal);
|
|
695
916
|
this.push({ role: "user", content: text });
|
|
@@ -698,6 +919,7 @@ ${completion.text}` });
|
|
|
698
919
|
await this.compact(signal);
|
|
699
920
|
this.push({ role: "user", content: `Lanjutkan tugas ini sesuai ringkasan di atas: ${text}` });
|
|
700
921
|
}
|
|
922
|
+
pruneOldFetches(this.messages);
|
|
701
923
|
let completion = await this.provider.chat({
|
|
702
924
|
messages: [{ role: "system", content: this.systemPrompt }, ...this.messages],
|
|
703
925
|
tools: this.turnDefinitions,
|
|
@@ -801,6 +1023,8 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
801
1023
|
}
|
|
802
1024
|
const input = parsed.data;
|
|
803
1025
|
const target = tool.target(input);
|
|
1026
|
+
const overBudget = tool.name === "fetch" ? this.webBudget.take() : void 0;
|
|
1027
|
+
if (overBudget) return fail(overBudget, target);
|
|
804
1028
|
const ctx = {
|
|
805
1029
|
cwd: this.opts.cwd,
|
|
806
1030
|
signal,
|
|
@@ -809,6 +1033,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
809
1033
|
emit: (event) => this.onEvent(event),
|
|
810
1034
|
ask: (req) => this.askPermission(req),
|
|
811
1035
|
checkpoint: this.opts.checkpoint,
|
|
1036
|
+
refundFetch: () => this.webBudget.refund(),
|
|
812
1037
|
addUsage: (u) => {
|
|
813
1038
|
this.totalUsage.inputTokens += u.inputTokens;
|
|
814
1039
|
this.totalUsage.outputTokens += u.outputTokens;
|
|
@@ -846,6 +1071,7 @@ Model ini sepertinya tidak mendukung tool calling. Coba model lain dengan /model
|
|
|
846
1071
|
} catch (error) {
|
|
847
1072
|
result = { output: `Error: ${error.message}`, isError: true };
|
|
848
1073
|
}
|
|
1074
|
+
if (tool.name === "fetch" && !result.isError) this.webBudget.record(result.output.length);
|
|
849
1075
|
this.onEvent({ type: "toolEnd", id: call.id, tool: tool.name, output: result.output, display: result.display, isError: !!result.isError });
|
|
850
1076
|
return result.output;
|
|
851
1077
|
}
|
|
@@ -1180,8 +1406,8 @@ ${def.prompt}`,
|
|
|
1180
1406
|
if (ctx.ask) child.askPermission = ctx.ask;
|
|
1181
1407
|
await child.run(input.prompt, ctx.signal);
|
|
1182
1408
|
ctx.addUsage?.(child.totalUsage);
|
|
1183
|
-
const
|
|
1184
|
-
const display = `${steps} langkah \xB7 ${
|
|
1409
|
+
const tokens2 = child.totalUsage.inputTokens + child.totalUsage.outputTokens;
|
|
1410
|
+
const display = `${steps} langkah \xB7 ${tokens2 >= 1e3 ? `${Math.round(tokens2 / 1e3)}k` : tokens2} token`;
|
|
1185
1411
|
if (outcome?.type === "error") return { output: `Subagent ${def.name} gagal: ${outcome.message}`, isError: true, display };
|
|
1186
1412
|
if (outcome?.type === "aborted") return { output: `Subagent ${def.name} dibatalkan.`, isError: true, display };
|
|
1187
1413
|
const last = [...child.messages].reverse().find((m) => m.role === "assistant" && m.content);
|
|
@@ -1374,7 +1600,7 @@ function createRuntime(opts) {
|
|
|
1374
1600
|
}
|
|
1375
1601
|
|
|
1376
1602
|
// src/version.ts
|
|
1377
|
-
var VERSION = true ? "0.4.0-beta.
|
|
1603
|
+
var VERSION = true ? "0.4.0-beta.14" : "0.0.0-dev";
|
|
1378
1604
|
|
|
1379
1605
|
// src/cli.ts
|
|
1380
1606
|
async function main() {
|
|
@@ -1436,7 +1662,7 @@ async function main() {
|
|
|
1436
1662
|
}
|
|
1437
1663
|
}
|
|
1438
1664
|
if (!process.stdin.isTTY) throw new ConfigError('Mode interaktif butuh terminal. Untuk skrip/CI pakai: bccli -p "tugas"');
|
|
1439
|
-
const { startInteractive } = await import("./ui-
|
|
1665
|
+
const { startInteractive } = await import("./ui-5BJA3UXH.js");
|
|
1440
1666
|
return startInteractive(rt, { initialPrompt: args.prompt, resume: args.resume, version: VERSION });
|
|
1441
1667
|
}
|
|
1442
1668
|
main().then(
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
instructionPaths,
|
|
6
6
|
parseFrontmatter,
|
|
7
7
|
toolLabel
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-3MWAQJE7.js";
|
|
9
9
|
import {
|
|
10
10
|
nextMode
|
|
11
11
|
} from "./chunk-ELML4KVQ.js";
|
|
@@ -248,6 +248,11 @@ function Markdown({ text }) {
|
|
|
248
248
|
// src/ui/LinePrompt.tsx
|
|
249
249
|
import { Box as Box2, Text as Text2, useInput } from "ink";
|
|
250
250
|
import { useState } from "react";
|
|
251
|
+
|
|
252
|
+
// src/ui/focusReport.ts
|
|
253
|
+
var isFocusReport = (input) => input === "[I" || input === "[O";
|
|
254
|
+
|
|
255
|
+
// src/ui/LinePrompt.tsx
|
|
251
256
|
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
252
257
|
function LinePrompt({ label, mask, onSubmit, onCancel }) {
|
|
253
258
|
const [value, setValue] = useState("");
|
|
@@ -255,7 +260,7 @@ function LinePrompt({ label, mask, onSubmit, onCancel }) {
|
|
|
255
260
|
if (key.return) onSubmit(value.trim());
|
|
256
261
|
else if (key.escape) onCancel();
|
|
257
262
|
else if (key.backspace || key.delete) setValue((v) => v.slice(0, -1));
|
|
258
|
-
else if (!key.ctrl && !key.meta && !key.tab && !key.upArrow && !key.downArrow) setValue((v) => v + input.replace(/[\r\n]/g, ""));
|
|
263
|
+
else if (!key.ctrl && !key.meta && !key.tab && !key.upArrow && !key.downArrow && !isFocusReport(input)) setValue((v) => v + input.replace(/[\r\n]/g, ""));
|
|
259
264
|
});
|
|
260
265
|
const shown = mask ? "\u2022".repeat(value.length) : value;
|
|
261
266
|
return /* @__PURE__ */ jsx2(Box2, { borderStyle: "round", borderColor: color("cyan"), paddingX: 1, children: /* @__PURE__ */ jsxs(Text2, { children: [
|
|
@@ -284,7 +289,7 @@ function ListPicker({ title, items, onPick }) {
|
|
|
284
289
|
else if (key.backspace || key.delete) {
|
|
285
290
|
setFilter((f) => f.slice(0, -1));
|
|
286
291
|
setIndex(0);
|
|
287
|
-
} else if (!key.ctrl && !key.meta && !key.tab && input) {
|
|
292
|
+
} else if (!key.ctrl && !key.meta && !key.tab && input && !isFocusReport(input)) {
|
|
288
293
|
setFilter((f) => f + input);
|
|
289
294
|
setIndex(0);
|
|
290
295
|
}
|
|
@@ -329,7 +334,7 @@ function ModelPicker({ groups, current, onPick }) {
|
|
|
329
334
|
else if (key.backspace || key.delete) {
|
|
330
335
|
setFilter((f) => f.slice(0, -1));
|
|
331
336
|
setIndex(0);
|
|
332
|
-
} else if (!key.ctrl && !key.meta && !key.tab && input) {
|
|
337
|
+
} else if (!key.ctrl && !key.meta && !key.tab && input && !isFocusReport(input)) {
|
|
333
338
|
setFilter((f) => f + input);
|
|
334
339
|
setIndex(0);
|
|
335
340
|
}
|
|
@@ -498,7 +503,7 @@ function PromptInput({ disabled, history, cwd, onSubmit, extraCommands }) {
|
|
|
498
503
|
edit((v) => completeFile(v, cwd));
|
|
499
504
|
return;
|
|
500
505
|
}
|
|
501
|
-
if (key.ctrl || key.meta || key.escape || key.tab || key.leftArrow || key.rightArrow) return;
|
|
506
|
+
if (key.ctrl || key.meta || key.escape || key.tab || key.leftArrow || key.rightArrow || isFocusReport(input)) return;
|
|
502
507
|
edit((v) => v + input.replace(/\r/g, "\n"));
|
|
503
508
|
},
|
|
504
509
|
{ isActive: !disabled }
|