@moikapy/lich 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -1
- package/README.md +38 -19
- package/dist/{chunk-QOTECFCN.js → chunk-4N3S6ACI.js} +2 -2
- package/dist/{chunk-EDRUZF22.js → chunk-KOEZQUIX.js} +1207 -512
- package/dist/chunk-KOEZQUIX.js.map +1 -0
- package/dist/{chunk-W6JXZBUE.js → chunk-P2YEHNM4.js} +2 -2
- package/dist/chunk-P2YEHNM4.js.map +1 -0
- package/dist/{chunk-VKEOHUCB.js → chunk-TYRKVBWQ.js} +26 -8
- package/dist/chunk-TYRKVBWQ.js.map +1 -0
- package/dist/cli.js +82 -44
- package/dist/cli.js.map +1 -1
- package/dist/{gateway-WU5G4ODO.js → gateway-OSB7254F.js} +209 -47
- package/dist/gateway-OSB7254F.js.map +1 -0
- package/dist/index.d.ts +105 -60
- package/dist/index.js +12 -5
- package/dist/{store-COOLBAHB.js → store-YYLEB7JG.js} +2 -2
- package/dist/{tui-4BP3TI7J.js → tui-XXIU4W7K.js} +17 -5
- package/dist/tui-XXIU4W7K.js.map +1 -0
- package/docs/architecture/overview.md +2 -2
- package/docs/architecture/tools.md +3 -1
- package/docs/getting-started.md +6 -6
- package/docs/index.md +1 -1
- package/docs/user-guide/cli.md +9 -7
- package/docs/user-guide/games.md +2 -1
- package/docs/user-guide/library.md +1 -1
- package/docs/user-guide/redot.md +2 -2
- package/docs/user-guide/tui.md +2 -2
- package/package.json +1 -1
- package/dist/chunk-EDRUZF22.js.map +0 -1
- package/dist/chunk-VKEOHUCB.js.map +0 -1
- package/dist/chunk-W6JXZBUE.js.map +0 -1
- package/dist/gateway-WU5G4ODO.js.map +0 -1
- package/dist/tui-4BP3TI7J.js.map +0 -1
- /package/dist/{chunk-QOTECFCN.js.map → chunk-4N3S6ACI.js.map} +0 -0
- /package/dist/{store-COOLBAHB.js.map → store-YYLEB7JG.js.map} +0 -0
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
safe_json_parse,
|
|
4
4
|
safe_stringify,
|
|
5
5
|
truncate_text
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-TYRKVBWQ.js";
|
|
7
7
|
|
|
8
8
|
// src/util/log.ts
|
|
9
9
|
var level_order = {
|
|
@@ -186,6 +186,38 @@ async function capture_errors(run) {
|
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// src/tools/read_clamped.ts
|
|
190
|
+
async function read_clamped_text(response, max_bytes) {
|
|
191
|
+
if (response.body === null) {
|
|
192
|
+
const text = await response.text();
|
|
193
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
194
|
+
if (bytes > max_bytes) {
|
|
195
|
+
return { text: text.slice(0, max_bytes), bytes_read: max_bytes, truncated: true };
|
|
196
|
+
}
|
|
197
|
+
return { text, bytes_read: bytes, truncated: false };
|
|
198
|
+
}
|
|
199
|
+
return read_stream(response.body.getReader(), max_bytes);
|
|
200
|
+
}
|
|
201
|
+
async function read_stream(reader, max_bytes) {
|
|
202
|
+
const chunks = [];
|
|
203
|
+
let bytes_read = 0;
|
|
204
|
+
let truncated = false;
|
|
205
|
+
while (bytes_read < max_bytes) {
|
|
206
|
+
const { done, value } = await reader.read();
|
|
207
|
+
if (done === true || value === void 0) break;
|
|
208
|
+
const room = max_bytes - bytes_read;
|
|
209
|
+
const take = value.byteLength > room ? value.subarray(0, room) : value;
|
|
210
|
+
chunks.push(Buffer.from(take));
|
|
211
|
+
bytes_read += take.byteLength;
|
|
212
|
+
if (value.byteLength > room) {
|
|
213
|
+
truncated = true;
|
|
214
|
+
await reader.cancel().catch(() => void 0);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { text: Buffer.concat(chunks).toString("utf8"), bytes_read, truncated };
|
|
219
|
+
}
|
|
220
|
+
|
|
189
221
|
// src/tools/url_guard.ts
|
|
190
222
|
import dns from "dns/promises";
|
|
191
223
|
import http from "http";
|
|
@@ -418,6 +450,7 @@ var DEFAULT_MAX_CHARS = 2e4;
|
|
|
418
450
|
var MAX_MAX_CHARS = 1e5;
|
|
419
451
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
420
452
|
var MAX_TIMEOUT_MS = 6e4;
|
|
453
|
+
var MAX_BODY_BYTES = MAX_MAX_CHARS * 4;
|
|
421
454
|
var USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36";
|
|
422
455
|
var parameters = {
|
|
423
456
|
type: "object",
|
|
@@ -455,16 +488,17 @@ async function run_fetch_url(args, external) {
|
|
|
455
488
|
if (content_type.startsWith("image/") === true || content_type.startsWith("application/octet-stream") === true) {
|
|
456
489
|
return { ok: false, output: "", error: `unsupported_content_type: ${content_type}` };
|
|
457
490
|
}
|
|
458
|
-
const
|
|
491
|
+
const byte_budget = Math.min(MAX_BODY_BYTES, max_chars * 4);
|
|
492
|
+
const clamped = await read_clamped_text(response, byte_budget);
|
|
459
493
|
const marker = content_type.toLowerCase().includes("text/html") === true ? "[html content]\n" : "";
|
|
460
|
-
const body = clamp_output(`${marker}${text}`, max_chars);
|
|
461
|
-
return { ok: true, output: `${header_line(response,
|
|
494
|
+
const body = clamp_output(`${marker}${clamped.text}`, max_chars);
|
|
495
|
+
return { ok: true, output: `${header_line(response, clamped.bytes_read)}
|
|
462
496
|
${body}` };
|
|
463
497
|
}
|
|
464
|
-
function header_line(response,
|
|
498
|
+
function header_line(response, bytes_read) {
|
|
465
499
|
const content_type = response.headers.get("content-type") ?? "unknown";
|
|
466
500
|
const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
|
|
467
|
-
const bytes = Number.isFinite(declared) === true ? declared :
|
|
501
|
+
const bytes = Number.isFinite(declared) === true ? declared : bytes_read;
|
|
468
502
|
return `# ${response.status} ${content_type} (${bytes} bytes)`;
|
|
469
503
|
}
|
|
470
504
|
var fetch_url_tool = {
|
|
@@ -1023,17 +1057,21 @@ var env_get_tool = {
|
|
|
1023
1057
|
// src/tools/builtin/grep_files.ts
|
|
1024
1058
|
import { readFile as readFile2, readdir as readdir2, stat } from "fs/promises";
|
|
1025
1059
|
import path5 from "path";
|
|
1060
|
+
import vm from "vm";
|
|
1026
1061
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".lich", ".cursor"]);
|
|
1027
1062
|
var MAX_FILE_BYTES = 1e6;
|
|
1028
1063
|
var SNIFF_BYTES = 1e3;
|
|
1029
1064
|
var DEFAULT_MAX_RESULTS2 = 200;
|
|
1065
|
+
var MAX_MAX_RESULTS = 2e3;
|
|
1066
|
+
var MAX_LINE_CHARS = 4e3;
|
|
1067
|
+
var REGEX_TIMEOUT_MS = 50;
|
|
1030
1068
|
var parameters7 = {
|
|
1031
1069
|
type: "object",
|
|
1032
1070
|
properties: {
|
|
1033
1071
|
pattern: { type: "string", description: "Regular expression source to match against each line" },
|
|
1034
1072
|
path: { type: "string", description: "Directory or file to search, relative to the working directory (default .)" },
|
|
1035
1073
|
glob: { type: "string", description: "Simple filename filter like *.ts (suffix match only)" },
|
|
1036
|
-
max_results: { type: "number", description: "Stop after this many matches (default 200)" }
|
|
1074
|
+
max_results: { type: "number", description: "Stop after this many matches (default 200, max 2000)" }
|
|
1037
1075
|
},
|
|
1038
1076
|
required: ["pattern"],
|
|
1039
1077
|
additionalProperties: false
|
|
@@ -1070,12 +1108,46 @@ async function read_if_text(file_path, size) {
|
|
|
1070
1108
|
return void 0;
|
|
1071
1109
|
}
|
|
1072
1110
|
}
|
|
1073
|
-
|
|
1111
|
+
var regex_sandbox = {
|
|
1112
|
+
re: /$^/,
|
|
1113
|
+
line: "",
|
|
1114
|
+
matched: false
|
|
1115
|
+
};
|
|
1116
|
+
vm.createContext(regex_sandbox);
|
|
1117
|
+
var regex_script = new vm.Script("matched = re.test(line)");
|
|
1118
|
+
function safe_regex_test(regex, line) {
|
|
1119
|
+
regex_sandbox.re = regex;
|
|
1120
|
+
regex_sandbox.line = line;
|
|
1121
|
+
try {
|
|
1122
|
+
regex_script.runInContext(regex_sandbox, { timeout: REGEX_TIMEOUT_MS });
|
|
1123
|
+
} catch {
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1126
|
+
return Boolean(regex_sandbox.matched);
|
|
1127
|
+
}
|
|
1128
|
+
function has_nested_quantifiers(pattern) {
|
|
1129
|
+
return /(\([^)]*[+*][^)]*\)[+*])/.test(pattern) === true;
|
|
1130
|
+
}
|
|
1131
|
+
function is_literal_pattern(pattern) {
|
|
1132
|
+
return /^[A-Za-z0-9_./:@-]+$/.test(pattern) === true;
|
|
1133
|
+
}
|
|
1134
|
+
function line_matches(regex, pattern, line) {
|
|
1135
|
+
const capped = line.length > MAX_LINE_CHARS ? line.slice(0, MAX_LINE_CHARS) : line;
|
|
1136
|
+
if (is_literal_pattern(pattern) === true) {
|
|
1137
|
+
return capped.includes(pattern);
|
|
1138
|
+
}
|
|
1139
|
+
return safe_regex_test(regex, capped);
|
|
1140
|
+
}
|
|
1141
|
+
function match_lines(lines, regex, pattern) {
|
|
1074
1142
|
const hits = [];
|
|
1075
1143
|
for (let index = 0; index < lines.length; index += 1) {
|
|
1076
1144
|
const line = lines[index];
|
|
1077
|
-
if (line
|
|
1078
|
-
|
|
1145
|
+
if (line === void 0) {
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
const capped = line.length > MAX_LINE_CHARS ? line.slice(0, MAX_LINE_CHARS) : line;
|
|
1149
|
+
if (line_matches(regex, pattern, capped) === true) {
|
|
1150
|
+
hits.push({ line_no: index + 1, text: capped.trim() });
|
|
1079
1151
|
}
|
|
1080
1152
|
}
|
|
1081
1153
|
return hits;
|
|
@@ -1083,7 +1155,6 @@ function match_lines(lines, regex) {
|
|
|
1083
1155
|
async function safe_readdir(dir) {
|
|
1084
1156
|
try {
|
|
1085
1157
|
return await readdir2(dir, { withFileTypes: true });
|
|
1086
|
-
abort_marker: ;
|
|
1087
1158
|
} catch {
|
|
1088
1159
|
return void 0;
|
|
1089
1160
|
}
|
|
@@ -1122,7 +1193,13 @@ function guard_grep_target(work_dir, absolute) {
|
|
|
1122
1193
|
assert_file_tool_access(work_dir, safe, "read");
|
|
1123
1194
|
return safe;
|
|
1124
1195
|
}
|
|
1125
|
-
|
|
1196
|
+
function throw_if_aborted(signal) {
|
|
1197
|
+
if (signal?.aborted === true) {
|
|
1198
|
+
throw new Error("cancelled");
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
async function search_file(frame, work_dir, relative_root, regex, pattern, collected, max_results, signal) {
|
|
1202
|
+
throw_if_aborted(signal);
|
|
1126
1203
|
let safe;
|
|
1127
1204
|
try {
|
|
1128
1205
|
safe = guard_grep_target(work_dir, frame.dir);
|
|
@@ -1135,7 +1212,7 @@ async function search_file(frame, work_dir, relative_root, regex, collected, max
|
|
|
1135
1212
|
return false;
|
|
1136
1213
|
}
|
|
1137
1214
|
const relative = path5.relative(relative_root, safe);
|
|
1138
|
-
for (const hit of match_lines(lines, regex)) {
|
|
1215
|
+
for (const hit of match_lines(lines, regex, pattern)) {
|
|
1139
1216
|
collected.push(`${relative}:${hit.line_no}: ${hit.text}`);
|
|
1140
1217
|
if (collected.length >= max_results) {
|
|
1141
1218
|
return true;
|
|
@@ -1143,17 +1220,18 @@ async function search_file(frame, work_dir, relative_root, regex, collected, max
|
|
|
1143
1220
|
}
|
|
1144
1221
|
return false;
|
|
1145
1222
|
}
|
|
1146
|
-
async function search_tree(root, work_dir, regex, matcher, max_results) {
|
|
1223
|
+
async function search_tree(root, work_dir, regex, pattern, matcher, max_results, signal) {
|
|
1147
1224
|
const collected = [];
|
|
1148
1225
|
const stack = [{ dir: root, name: root }];
|
|
1149
1226
|
while (stack.length > 0 && collected.length < max_results) {
|
|
1227
|
+
throw_if_aborted(signal);
|
|
1150
1228
|
const frame = stack.pop();
|
|
1151
1229
|
if (frame === void 0) {
|
|
1152
1230
|
break;
|
|
1153
1231
|
}
|
|
1154
1232
|
const found = await scan_dir(frame, matcher);
|
|
1155
1233
|
for (const file of found.files) {
|
|
1156
|
-
const hit_cap = await search_file(file, work_dir, root, regex, collected, max_results);
|
|
1234
|
+
const hit_cap = await search_file(file, work_dir, root, regex, pattern, collected, max_results, signal);
|
|
1157
1235
|
if (hit_cap === true) {
|
|
1158
1236
|
break;
|
|
1159
1237
|
}
|
|
@@ -1173,27 +1251,32 @@ function finalize_output(matches, max_results) {
|
|
|
1173
1251
|
}
|
|
1174
1252
|
return matches.join("\n");
|
|
1175
1253
|
}
|
|
1176
|
-
function search_file_direct(file_path, work_dir, regex, collected, max_results) {
|
|
1254
|
+
function search_file_direct(file_path, work_dir, regex, pattern, collected, max_results, signal) {
|
|
1177
1255
|
return search_file(
|
|
1178
1256
|
{ dir: file_path, name: path5.basename(file_path) },
|
|
1179
1257
|
work_dir,
|
|
1180
1258
|
path5.dirname(file_path),
|
|
1181
1259
|
regex,
|
|
1260
|
+
pattern,
|
|
1182
1261
|
collected,
|
|
1183
|
-
max_results
|
|
1262
|
+
max_results,
|
|
1263
|
+
signal
|
|
1184
1264
|
);
|
|
1185
1265
|
}
|
|
1186
|
-
async function collect_file_matches(root, work_dir, regex, matcher, max_results) {
|
|
1266
|
+
async function collect_file_matches(root, work_dir, regex, pattern, matcher, max_results, signal) {
|
|
1187
1267
|
const collected = [];
|
|
1188
1268
|
if (matcher(path5.basename(root)) === true) {
|
|
1189
|
-
await search_file_direct(root, work_dir, regex, collected, max_results);
|
|
1269
|
+
await search_file_direct(root, work_dir, regex, pattern, collected, max_results, signal);
|
|
1190
1270
|
}
|
|
1191
1271
|
return collected;
|
|
1192
1272
|
}
|
|
1193
|
-
|
|
1273
|
+
function clamp_max_results(raw) {
|
|
1274
|
+
return Math.min(MAX_MAX_RESULTS, Math.max(1, Math.floor(raw)));
|
|
1275
|
+
}
|
|
1276
|
+
async function run_grep(args, work_dir, signal) {
|
|
1194
1277
|
const pattern = require_string_arg(args, "pattern");
|
|
1195
1278
|
const target = optional_string_arg(args, "path", ".");
|
|
1196
|
-
const max_results =
|
|
1279
|
+
const max_results = clamp_max_results(optional_number_arg(args, "max_results", DEFAULT_MAX_RESULTS2));
|
|
1197
1280
|
const glob = optional_string_arg(args, "glob", "");
|
|
1198
1281
|
let regex;
|
|
1199
1282
|
try {
|
|
@@ -1201,12 +1284,15 @@ async function run_grep(args, work_dir) {
|
|
|
1201
1284
|
} catch {
|
|
1202
1285
|
throw new Error(`invalid_regex: ${pattern}`);
|
|
1203
1286
|
}
|
|
1287
|
+
if (has_nested_quantifiers(pattern) === true) {
|
|
1288
|
+
throw new Error(`unsafe_regex: nested quantifiers are not supported (${pattern})`);
|
|
1289
|
+
}
|
|
1204
1290
|
const matcher = glob.length > 0 ? glob_matcher(glob) : () => true;
|
|
1205
1291
|
const root = resolve_safe_path(work_dir, target);
|
|
1206
1292
|
assert_file_tool_access(work_dir, root, "read");
|
|
1207
1293
|
const root_stat = await stat(root);
|
|
1208
1294
|
const cap = max_results + 1;
|
|
1209
|
-
const matches = root_stat.isDirectory() === true ? await search_tree(root, work_dir, regex, matcher, cap) : await collect_file_matches(root, work_dir, regex, matcher, cap);
|
|
1295
|
+
const matches = root_stat.isDirectory() === true ? await search_tree(root, work_dir, regex, pattern, matcher, cap, signal) : await collect_file_matches(root, work_dir, regex, pattern, matcher, cap, signal);
|
|
1210
1296
|
return finalize_output(matches, max_results);
|
|
1211
1297
|
}
|
|
1212
1298
|
var grep_files_tool = {
|
|
@@ -1214,7 +1300,7 @@ var grep_files_tool = {
|
|
|
1214
1300
|
description: "Search files line-by-line with a regex, skipping node_modules/.git/dist and binary files.",
|
|
1215
1301
|
parameters: parameters7,
|
|
1216
1302
|
execute: async (args, context) => capture_errors(async () => {
|
|
1217
|
-
const output = await run_grep(args, context.work_dir);
|
|
1303
|
+
const output = await run_grep(args, context.work_dir, context.signal);
|
|
1218
1304
|
return { ok: true, output };
|
|
1219
1305
|
})
|
|
1220
1306
|
};
|
|
@@ -1224,6 +1310,7 @@ var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
|
1224
1310
|
var MAX_TIMEOUT_MS2 = 12e4;
|
|
1225
1311
|
var DEFAULT_MAX_CHARS2 = 2e4;
|
|
1226
1312
|
var MAX_MAX_CHARS2 = 1e5;
|
|
1313
|
+
var MAX_BODY_BYTES2 = MAX_MAX_CHARS2 * 4;
|
|
1227
1314
|
var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
1228
1315
|
var REPORTED_HEADERS = ["content-length", "ratelimit-remaining", "retry-after"];
|
|
1229
1316
|
var parameters8 = {
|
|
@@ -1288,13 +1375,14 @@ async function run_http(args, external) {
|
|
|
1288
1375
|
}
|
|
1289
1376
|
const response = await safe_fetch(url, init);
|
|
1290
1377
|
const content_type = response.headers.get("content-type") ?? "unknown";
|
|
1291
|
-
const
|
|
1378
|
+
const byte_budget = Math.min(MAX_BODY_BYTES2, max_chars * 4);
|
|
1379
|
+
const clamped = await read_clamped_text(response, byte_budget);
|
|
1292
1380
|
const sections = [
|
|
1293
1381
|
`# status ${response.status}`,
|
|
1294
1382
|
`# content-type ${content_type}`,
|
|
1295
1383
|
...header_lines(response),
|
|
1296
1384
|
"",
|
|
1297
|
-
clamp_output(text, max_chars)
|
|
1385
|
+
clamp_output(clamped.text, max_chars)
|
|
1298
1386
|
];
|
|
1299
1387
|
return { ok: true, output: sections.join("\n") };
|
|
1300
1388
|
}
|
|
@@ -1400,7 +1488,7 @@ var list_dir_tool = {
|
|
|
1400
1488
|
// src/tools/builtin/process_list.ts
|
|
1401
1489
|
import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
1402
1490
|
var DEFAULT_MAX_RESULTS3 = 50;
|
|
1403
|
-
var
|
|
1491
|
+
var MAX_MAX_RESULTS2 = 500;
|
|
1404
1492
|
var CMDLINE_MAX_CHARS = 200;
|
|
1405
1493
|
var PROC_DIR = "/proc";
|
|
1406
1494
|
var PID_PATTERN = /^[0-9]+$/;
|
|
@@ -1455,7 +1543,7 @@ function collect_lines2(filter, max_results) {
|
|
|
1455
1543
|
}
|
|
1456
1544
|
function run_process_list(args) {
|
|
1457
1545
|
const filter = optional_string_arg(args, "filter", "").toLowerCase();
|
|
1458
|
-
const max_results = clamp_int_arg(args, "max_results", DEFAULT_MAX_RESULTS3,
|
|
1546
|
+
const max_results = clamp_int_arg(args, "max_results", DEFAULT_MAX_RESULTS3, MAX_MAX_RESULTS2);
|
|
1459
1547
|
let listing;
|
|
1460
1548
|
try {
|
|
1461
1549
|
listing = collect_lines2(filter, max_results);
|
|
@@ -1525,6 +1613,46 @@ var read_file_tool = {
|
|
|
1525
1613
|
// src/tools/builtin/run_tests.ts
|
|
1526
1614
|
import { spawn as spawn2 } from "child_process";
|
|
1527
1615
|
|
|
1616
|
+
// src/tools/process_group.ts
|
|
1617
|
+
var live_children = /* @__PURE__ */ new Set();
|
|
1618
|
+
var exit_handlers_installed = false;
|
|
1619
|
+
function kill_all_live_children() {
|
|
1620
|
+
for (const child of live_children) {
|
|
1621
|
+
kill_process_group(child, "SIGKILL");
|
|
1622
|
+
}
|
|
1623
|
+
live_children.clear();
|
|
1624
|
+
}
|
|
1625
|
+
function install_exit_handlers() {
|
|
1626
|
+
if (exit_handlers_installed === true) {
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
exit_handlers_installed = true;
|
|
1630
|
+
process.on("exit", kill_all_live_children);
|
|
1631
|
+
}
|
|
1632
|
+
function track_detached_child(child) {
|
|
1633
|
+
install_exit_handlers();
|
|
1634
|
+
live_children.add(child);
|
|
1635
|
+
const drop = () => {
|
|
1636
|
+
live_children.delete(child);
|
|
1637
|
+
};
|
|
1638
|
+
child.on("exit", drop);
|
|
1639
|
+
child.on("error", drop);
|
|
1640
|
+
}
|
|
1641
|
+
function kill_process_group(child, signal = "SIGKILL") {
|
|
1642
|
+
const pid = child.pid;
|
|
1643
|
+
if (pid === void 0) {
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
try {
|
|
1647
|
+
process.kill(-pid, signal);
|
|
1648
|
+
} catch {
|
|
1649
|
+
try {
|
|
1650
|
+
child.kill(signal);
|
|
1651
|
+
} catch {
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1528
1656
|
// src/tools/builtin/terminal.ts
|
|
1529
1657
|
import { spawn } from "child_process";
|
|
1530
1658
|
|
|
@@ -1600,12 +1728,13 @@ function scrub_spawn_env(process_env, context_env) {
|
|
|
1600
1728
|
return scrubbed;
|
|
1601
1729
|
}
|
|
1602
1730
|
function wire_kill(child, timeout_signal, external) {
|
|
1603
|
-
|
|
1604
|
-
|
|
1731
|
+
const kill = () => kill_process_group(child, "SIGKILL");
|
|
1732
|
+
timeout_signal.addEventListener("abort", kill, { once: true });
|
|
1733
|
+
external?.addEventListener("abort", kill, { once: true });
|
|
1605
1734
|
}
|
|
1606
|
-
function
|
|
1735
|
+
function wait_exit(child) {
|
|
1607
1736
|
return new Promise((resolve) => {
|
|
1608
|
-
child.on("
|
|
1737
|
+
child.on("exit", (code) => resolve(code ?? -1));
|
|
1609
1738
|
child.on("error", () => resolve(-1));
|
|
1610
1739
|
});
|
|
1611
1740
|
}
|
|
@@ -1614,22 +1743,26 @@ async function run_command(command, work_dir, env, timeout_ms, external) {
|
|
|
1614
1743
|
const stderr = { text: "" };
|
|
1615
1744
|
const child = spawn("bash", ["-lc", command], {
|
|
1616
1745
|
cwd: work_dir,
|
|
1617
|
-
env: scrub_spawn_env(process.env, env)
|
|
1746
|
+
env: scrub_spawn_env(process.env, env),
|
|
1747
|
+
detached: true,
|
|
1748
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1618
1749
|
});
|
|
1619
|
-
child
|
|
1620
|
-
child.
|
|
1621
|
-
|
|
1750
|
+
track_detached_child(child);
|
|
1751
|
+
child.stdout?.on("data", (chunk) => stream_chunk(stdout, chunk));
|
|
1752
|
+
child.stderr?.on("data", (chunk) => stream_chunk(stderr, chunk));
|
|
1753
|
+
const exit_promise = wait_exit(child);
|
|
1622
1754
|
let timed_out = false;
|
|
1623
1755
|
let exit_code;
|
|
1624
1756
|
try {
|
|
1625
1757
|
exit_code = await with_timeout((timeout_signal) => {
|
|
1626
1758
|
wire_kill(child, timeout_signal, external);
|
|
1627
|
-
return
|
|
1759
|
+
return exit_promise;
|
|
1628
1760
|
}, timeout_ms, "terminal");
|
|
1629
1761
|
} catch (err) {
|
|
1630
1762
|
if (err instanceof ToolTimeoutError === true) {
|
|
1631
1763
|
timed_out = true;
|
|
1632
|
-
|
|
1764
|
+
kill_process_group(child, "SIGKILL");
|
|
1765
|
+
exit_code = await exit_promise;
|
|
1633
1766
|
} else {
|
|
1634
1767
|
throw err;
|
|
1635
1768
|
}
|
|
@@ -1640,7 +1773,7 @@ async function run_command(command, work_dir, env, timeout_ms, external) {
|
|
|
1640
1773
|
}
|
|
1641
1774
|
function terminal_result(outcome) {
|
|
1642
1775
|
const result = {
|
|
1643
|
-
ok: outcome.exit_code === 0 && outcome.cancelled === false,
|
|
1776
|
+
ok: outcome.exit_code === 0 && outcome.cancelled === false && outcome.timed_out === false,
|
|
1644
1777
|
output: clamp_output(outcome.output)
|
|
1645
1778
|
};
|
|
1646
1779
|
if (outcome.cancelled === true) {
|
|
@@ -1666,28 +1799,43 @@ var terminal_tool = {
|
|
|
1666
1799
|
// src/tools/builtin/run_tests.ts
|
|
1667
1800
|
var MAX_OUTPUT_CHARS = 2e3;
|
|
1668
1801
|
var DEFAULT_TEST_COMMAND = "node node_modules/vitest/vitest.mjs run";
|
|
1802
|
+
var DEFAULT_TIMEOUT_MS4 = 6e5;
|
|
1803
|
+
var MAX_TIMEOUT_MS4 = 6e5;
|
|
1669
1804
|
var parameters13 = {
|
|
1670
1805
|
type: "object",
|
|
1671
1806
|
properties: {
|
|
1672
1807
|
filter: {
|
|
1673
1808
|
type: "string",
|
|
1674
1809
|
description: "Optional test-file filter appended to the test command (e.g. a vitest file filter)"
|
|
1810
|
+
},
|
|
1811
|
+
timeout_ms: {
|
|
1812
|
+
type: "number",
|
|
1813
|
+
description: "Kill the suite after this many ms (default 600000, max 600000)"
|
|
1675
1814
|
}
|
|
1676
1815
|
},
|
|
1677
1816
|
additionalProperties: false
|
|
1678
1817
|
};
|
|
1679
1818
|
var busy = false;
|
|
1680
1819
|
var run_test_command = default_runner;
|
|
1681
|
-
function
|
|
1820
|
+
function wire_kill2(child, signal) {
|
|
1821
|
+
if (signal === void 0) {
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
signal.addEventListener("abort", () => kill_process_group(child, "SIGKILL"), { once: true });
|
|
1825
|
+
}
|
|
1826
|
+
function default_runner(command, cwd, on_chunk, signal) {
|
|
1682
1827
|
const child = spawn2("bash", ["-lc", command], {
|
|
1683
1828
|
cwd,
|
|
1684
1829
|
env: scrub_spawn_env(process.env, {}),
|
|
1830
|
+
detached: true,
|
|
1685
1831
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1686
1832
|
});
|
|
1833
|
+
track_detached_child(child);
|
|
1687
1834
|
child.stdout?.on("data", (chunk) => on_chunk("stdout", chunk));
|
|
1688
1835
|
child.stderr?.on("data", (chunk) => on_chunk("stderr", chunk));
|
|
1836
|
+
wire_kill2(child, signal);
|
|
1689
1837
|
return new Promise((resolve) => {
|
|
1690
|
-
child.on("
|
|
1838
|
+
child.on("exit", (code) => resolve({ exit_code: code ?? -1 }));
|
|
1691
1839
|
child.on("error", () => resolve({ exit_code: -1 }));
|
|
1692
1840
|
});
|
|
1693
1841
|
}
|
|
@@ -1698,11 +1846,21 @@ function build_command(filter, env) {
|
|
|
1698
1846
|
const base = optional_string_arg(env, "LICH_TEST_COMMAND", DEFAULT_TEST_COMMAND);
|
|
1699
1847
|
return filter === void 0 ? base : `${base} ${shell_quote(filter)}`;
|
|
1700
1848
|
}
|
|
1849
|
+
function clamp_timeout2(raw) {
|
|
1850
|
+
return Math.min(MAX_TIMEOUT_MS4, Math.max(1, Math.floor(raw)));
|
|
1851
|
+
}
|
|
1852
|
+
function append_clamped(current, chunk, budget) {
|
|
1853
|
+
if (current.length >= budget) {
|
|
1854
|
+
return current;
|
|
1855
|
+
}
|
|
1856
|
+
const next = current + chunk.toString("utf8");
|
|
1857
|
+
return next.length > budget ? next.slice(0, budget) : next;
|
|
1858
|
+
}
|
|
1701
1859
|
var run_tests_tool = {
|
|
1702
1860
|
name: "run_tests",
|
|
1703
1861
|
description: "Run the project's test suite via LICH_TEST_COMMAND (default: vitest) in work_dir and report a structured pass/fail result with clamped output.",
|
|
1704
1862
|
parameters: parameters13,
|
|
1705
|
-
timeout_ms:
|
|
1863
|
+
timeout_ms: MAX_TIMEOUT_MS4,
|
|
1706
1864
|
execute: async (args, context) => capture_errors(async () => {
|
|
1707
1865
|
if (busy === true) {
|
|
1708
1866
|
return { ok: false, output: "", error: "run_tests_busy" };
|
|
@@ -1713,17 +1871,44 @@ var run_tests_tool = {
|
|
|
1713
1871
|
if (filter.startsWith("-") === true) {
|
|
1714
1872
|
return { ok: false, output: "", error: "invalid_filter: must not start with -" };
|
|
1715
1873
|
}
|
|
1874
|
+
const timeout_ms = clamp_timeout2(optional_number_arg(args, "timeout_ms", DEFAULT_TIMEOUT_MS4));
|
|
1716
1875
|
const command = build_command(filter === "" ? void 0 : filter, context.env);
|
|
1717
1876
|
const streams = { stdout: "", stderr: "" };
|
|
1718
1877
|
const on_chunk = (stream, chunk) => {
|
|
1719
|
-
streams[stream] = streams[stream]
|
|
1878
|
+
streams[stream] = append_clamped(streams[stream], chunk, MAX_OUTPUT_CHARS);
|
|
1720
1879
|
};
|
|
1721
|
-
|
|
1722
|
-
|
|
1880
|
+
let timed_out = false;
|
|
1881
|
+
let exit_code;
|
|
1882
|
+
try {
|
|
1883
|
+
const outcome = await with_timeout((timeout_signal) => {
|
|
1884
|
+
const signals = [timeout_signal];
|
|
1885
|
+
if (context.signal !== void 0) {
|
|
1886
|
+
signals.push(context.signal);
|
|
1887
|
+
}
|
|
1888
|
+
return run_test_command(command, context.work_dir, on_chunk, AbortSignal.any(signals));
|
|
1889
|
+
}, timeout_ms, "run_tests");
|
|
1890
|
+
exit_code = outcome.exit_code;
|
|
1891
|
+
} catch (err) {
|
|
1892
|
+
if (err instanceof ToolTimeoutError === true) {
|
|
1893
|
+
timed_out = true;
|
|
1894
|
+
exit_code = -1;
|
|
1895
|
+
} else {
|
|
1896
|
+
throw err;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
if (timed_out === true || context.signal?.aborted === true) {
|
|
1900
|
+
return {
|
|
1901
|
+
ok: false,
|
|
1902
|
+
output: clamp_output(`${streams.stdout}${streams.stderr}
|
|
1903
|
+
[exit ${exit_code}]`, MAX_OUTPUT_CHARS),
|
|
1904
|
+
error: timed_out === true ? "timeout" : "cancelled"
|
|
1905
|
+
};
|
|
1906
|
+
}
|
|
1907
|
+
const ok = exit_code === 0;
|
|
1723
1908
|
return {
|
|
1724
1909
|
ok,
|
|
1725
1910
|
output: clamp_output(`${streams.stdout}${streams.stderr}
|
|
1726
|
-
[exit ${
|
|
1911
|
+
[exit ${exit_code}]`, MAX_OUTPUT_CHARS),
|
|
1727
1912
|
...ok ? {} : { error: "tests_failed" }
|
|
1728
1913
|
};
|
|
1729
1914
|
} finally {
|
|
@@ -1735,8 +1920,9 @@ var run_tests_tool = {
|
|
|
1735
1920
|
// src/tools/builtin/web_search.ts
|
|
1736
1921
|
var DEFAULT_MAX_RESULTS4 = 8;
|
|
1737
1922
|
var MAX_RESULTS = 20;
|
|
1738
|
-
var
|
|
1739
|
-
var
|
|
1923
|
+
var DEFAULT_TIMEOUT_MS5 = 2e4;
|
|
1924
|
+
var MAX_TIMEOUT_MS5 = 6e4;
|
|
1925
|
+
var MAX_SEARCH_BODY_BYTES = 512e3;
|
|
1740
1926
|
var SEARCH_ENDPOINT = "https://html.duckduckgo.com/html/?q=";
|
|
1741
1927
|
var REDIRECT_PREFIXES = ["//duckduckgo.com/l/?", "/l/?"];
|
|
1742
1928
|
var RESULT_PATTERN = /<a[^>]+class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
|
|
@@ -1815,7 +2001,7 @@ function format_results(hits) {
|
|
|
1815
2001
|
async function run_search(args, external) {
|
|
1816
2002
|
const query = require_string_arg(args, "query");
|
|
1817
2003
|
const max_results = clamp_int_arg(args, "max_results", DEFAULT_MAX_RESULTS4, MAX_RESULTS);
|
|
1818
|
-
const timeout_ms = clamp_int_arg(args, "timeout_ms",
|
|
2004
|
+
const timeout_ms = clamp_int_arg(args, "timeout_ms", DEFAULT_TIMEOUT_MS5, MAX_TIMEOUT_MS5);
|
|
1819
2005
|
try {
|
|
1820
2006
|
const response = await fetch(`${SEARCH_ENDPOINT}${encodeURIComponent(query)}`, {
|
|
1821
2007
|
headers: { "user-agent": USER_AGENT, "accept-language": "en" },
|
|
@@ -1824,7 +2010,8 @@ async function run_search(args, external) {
|
|
|
1824
2010
|
if (response.ok === false) {
|
|
1825
2011
|
return { ok: false, output: "", error: `search_failed: http_${response.status}` };
|
|
1826
2012
|
}
|
|
1827
|
-
const
|
|
2013
|
+
const clamped = await read_clamped_text(response, MAX_SEARCH_BODY_BYTES);
|
|
2014
|
+
const hits = parse_results(clamped.text, max_results);
|
|
1828
2015
|
return { ok: true, output: hits.length === 0 ? "no results" : format_results(hits) };
|
|
1829
2016
|
} catch (err) {
|
|
1830
2017
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -2011,9 +2198,23 @@ var ToolExecutor = class {
|
|
|
2011
2198
|
};
|
|
2012
2199
|
|
|
2013
2200
|
// src/plugins/hooks.ts
|
|
2201
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2014
2202
|
var SUMMARY_MAX_CHARS = 300;
|
|
2203
|
+
var run_als = new AsyncLocalStorage();
|
|
2015
2204
|
var plugin_state = /* @__PURE__ */ new WeakMap();
|
|
2205
|
+
function bags_for_run() {
|
|
2206
|
+
return run_als.getStore();
|
|
2207
|
+
}
|
|
2016
2208
|
function hook_state_for(plugin) {
|
|
2209
|
+
const bags = bags_for_run();
|
|
2210
|
+
if (bags !== void 0) {
|
|
2211
|
+
let state2 = bags.get(plugin);
|
|
2212
|
+
if (state2 === void 0) {
|
|
2213
|
+
state2 = /* @__PURE__ */ new Map();
|
|
2214
|
+
bags.set(plugin, state2);
|
|
2215
|
+
}
|
|
2216
|
+
return state2;
|
|
2217
|
+
}
|
|
2017
2218
|
let state = plugin_state.get(plugin);
|
|
2018
2219
|
if (state === void 0) {
|
|
2019
2220
|
state = /* @__PURE__ */ new Map();
|
|
@@ -2021,6 +2222,16 @@ function hook_state_for(plugin) {
|
|
|
2021
2222
|
}
|
|
2022
2223
|
return state;
|
|
2023
2224
|
}
|
|
2225
|
+
function reset_plugin_bag(plugin) {
|
|
2226
|
+
const fresh = /* @__PURE__ */ new Map();
|
|
2227
|
+
const bags = bags_for_run();
|
|
2228
|
+
if (bags !== void 0) {
|
|
2229
|
+
bags.set(plugin, fresh);
|
|
2230
|
+
} else {
|
|
2231
|
+
plugin_state.set(plugin, fresh);
|
|
2232
|
+
}
|
|
2233
|
+
return fresh;
|
|
2234
|
+
}
|
|
2024
2235
|
function with_hook_state(base, plugin) {
|
|
2025
2236
|
return { ...base, state: hook_state_for(plugin) };
|
|
2026
2237
|
}
|
|
@@ -2034,6 +2245,17 @@ var HookedToolRunner = class {
|
|
|
2034
2245
|
this.wrapped = wrapped;
|
|
2035
2246
|
this.hooked_plugins = plugins.filter((plugin) => plugin.hooks !== void 0);
|
|
2036
2247
|
}
|
|
2248
|
+
/**
|
|
2249
|
+
* Isolate plugin state for one Agent.run so concurrent runs on the same
|
|
2250
|
+
* Agent cannot reset each other's bags (M-6).
|
|
2251
|
+
*/
|
|
2252
|
+
run_scope(fn) {
|
|
2253
|
+
const bags = /* @__PURE__ */ new Map();
|
|
2254
|
+
for (const plugin of this.hooked_plugins) {
|
|
2255
|
+
bags.set(plugin, /* @__PURE__ */ new Map());
|
|
2256
|
+
}
|
|
2257
|
+
return run_als.run(bags, fn);
|
|
2258
|
+
}
|
|
2037
2259
|
/** Run before hooks in order; the first {block: true} verdict wins. */
|
|
2038
2260
|
async run_before_hooks(info, base) {
|
|
2039
2261
|
for (const plugin of this.hooked_plugins) {
|
|
@@ -2088,7 +2310,7 @@ var HookedToolRunner = class {
|
|
|
2088
2310
|
/** Best-effort on_run_start fan-out used by Agent.run; never throws. Swaps in a fresh state sub-map per hooked plugin first. */
|
|
2089
2311
|
async call_run_start(info, base) {
|
|
2090
2312
|
for (const plugin of this.hooked_plugins) {
|
|
2091
|
-
|
|
2313
|
+
reset_plugin_bag(plugin);
|
|
2092
2314
|
}
|
|
2093
2315
|
for (const plugin of this.hooked_plugins) {
|
|
2094
2316
|
const hook = plugin.hooks?.on_run_start;
|
|
@@ -2219,142 +2441,585 @@ var ProviderError = class extends Error {
|
|
|
2219
2441
|
}
|
|
2220
2442
|
};
|
|
2221
2443
|
|
|
2222
|
-
// src/
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
var PUBLIC_PLATFORMS = /* @__PURE__ */ new Set(["telegram", "discord", "twitch"]);
|
|
2227
|
-
var DEFAULT_GATEWAY_TOOLS_ENABLED = [
|
|
2228
|
-
"read_file",
|
|
2229
|
-
"list_dir",
|
|
2230
|
-
"grep_files",
|
|
2231
|
-
"fetch_url",
|
|
2232
|
-
"web_search",
|
|
2233
|
-
"docs_read",
|
|
2234
|
-
"docs_search"
|
|
2235
|
-
];
|
|
2236
|
-
function gateway_tools_enabled(config) {
|
|
2237
|
-
return config.gateway?.tools_enabled ?? DEFAULT_GATEWAY_TOOLS_ENABLED;
|
|
2444
|
+
// src/context/tokens.ts
|
|
2445
|
+
var TOOL_MESSAGE_OVERHEAD_TOKENS = 8;
|
|
2446
|
+
function estimate_text_tokens(text) {
|
|
2447
|
+
return Math.ceil(text.length / 4);
|
|
2238
2448
|
}
|
|
2239
|
-
function
|
|
2240
|
-
|
|
2241
|
-
|
|
2449
|
+
function estimate_message_tokens(message) {
|
|
2450
|
+
const content_tokens = estimate_text_tokens(message.content);
|
|
2451
|
+
if (message.role === "assistant" && message.tool_calls !== void 0) {
|
|
2452
|
+
return content_tokens + estimate_text_tokens(safe_stringify(message.tool_calls));
|
|
2242
2453
|
}
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
if (users.length === 0 && chats.length === 0) {
|
|
2246
|
-
return false;
|
|
2454
|
+
if (message.role === "tool") {
|
|
2455
|
+
return content_tokens + TOOL_MESSAGE_OVERHEAD_TOKENS;
|
|
2247
2456
|
}
|
|
2248
|
-
|
|
2249
|
-
const chat_ok = chats.length === 0 || chats.includes(chat_id);
|
|
2250
|
-
return user_ok && chat_ok;
|
|
2457
|
+
return content_tokens;
|
|
2251
2458
|
}
|
|
2252
|
-
function
|
|
2253
|
-
|
|
2254
|
-
return true;
|
|
2255
|
-
}
|
|
2256
|
-
logger.warn(`gateway denied ${platform} chat=${chat_id} user=${user_id}`);
|
|
2257
|
-
return false;
|
|
2459
|
+
function estimate_messages_tokens(messages) {
|
|
2460
|
+
return messages.reduce((total, message) => total + estimate_message_tokens(message), 0);
|
|
2258
2461
|
}
|
|
2259
2462
|
|
|
2260
|
-
// src/
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
function catalog_dir() {
|
|
2269
|
-
let dir = path9.dirname(fileURLToPath2(import.meta.url));
|
|
2270
|
-
for (let hop = 0; hop < 6; hop += 1) {
|
|
2271
|
-
const candidate = path9.join(dir, "optional-mcps");
|
|
2272
|
-
if (existsSync2(path9.join(candidate, "redot", "manifest.json")) === true) {
|
|
2273
|
-
return candidate;
|
|
2274
|
-
}
|
|
2275
|
-
dir = path9.dirname(dir);
|
|
2276
|
-
}
|
|
2277
|
-
throw new Error("mcp catalog not found");
|
|
2463
|
+
// src/context/compressor.ts
|
|
2464
|
+
var COMPRESSION_SYSTEM_PROMPT = "You compress agent conversation history into terse factual summaries. Preserve: goals, decisions, file paths, commands run, errors, open questions. Output plain text only.";
|
|
2465
|
+
var MIN_TRANSCRIPT_CHARS = 8e3;
|
|
2466
|
+
var MAX_TRANSCRIPT_CHARS = 96e3;
|
|
2467
|
+
var CHARS_PER_BUDGET_TOKEN = 0.24;
|
|
2468
|
+
var MIN_PER_MESSAGE_CHARS = 256;
|
|
2469
|
+
function should_compress(messages, budget_tokens, threshold) {
|
|
2470
|
+
return estimate_messages_tokens(messages) >= budget_tokens * threshold;
|
|
2278
2471
|
}
|
|
2279
|
-
function
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
throw new Error("mcp catalog manifest rejected");
|
|
2283
|
-
}
|
|
2284
|
-
return parsed;
|
|
2472
|
+
function transcript_char_budget(budget_tokens) {
|
|
2473
|
+
const scaled = Math.floor(Math.max(0, budget_tokens) * CHARS_PER_BUDGET_TOKEN);
|
|
2474
|
+
return Math.min(MAX_TRANSCRIPT_CHARS, Math.max(MIN_TRANSCRIPT_CHARS, scaled));
|
|
2285
2475
|
}
|
|
2286
|
-
function
|
|
2287
|
-
if (
|
|
2288
|
-
return
|
|
2476
|
+
function truncate_head_tail(text, max_chars) {
|
|
2477
|
+
if (max_chars <= 0) {
|
|
2478
|
+
return "";
|
|
2289
2479
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
const file = path9.join(catalog_dir(), name, "manifest.json");
|
|
2293
|
-
if (existsSync2(file) === true) {
|
|
2294
|
-
manifests.push(read_manifest(file));
|
|
2295
|
-
}
|
|
2480
|
+
if (text.length <= max_chars) {
|
|
2481
|
+
return text;
|
|
2296
2482
|
}
|
|
2297
|
-
|
|
2298
|
-
|
|
2483
|
+
if (max_chars < 40) {
|
|
2484
|
+
return text.slice(0, max_chars);
|
|
2485
|
+
}
|
|
2486
|
+
const marker_budget = 28;
|
|
2487
|
+
const half = Math.floor((max_chars - marker_budget) / 2);
|
|
2488
|
+
const omitted = text.length - half * 2;
|
|
2489
|
+
return `${text.slice(0, half)}
|
|
2490
|
+
[...${omitted} chars...]
|
|
2491
|
+
${text.slice(-half)}`;
|
|
2299
2492
|
}
|
|
2300
|
-
function
|
|
2301
|
-
|
|
2493
|
+
function format_history_line(message) {
|
|
2494
|
+
const rendered_calls = message.role === "assistant" && message.tool_calls !== void 0 ? ` tool_calls=${safe_stringify(message.tool_calls)}` : "";
|
|
2495
|
+
return `[${message.role}] ${message.content}${rendered_calls}`;
|
|
2302
2496
|
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
var SHELL = /[;&|`$<>]/;
|
|
2307
|
-
var DOWNLOADERS = /* @__PURE__ */ new Set(["npx", "npm", "bunx", "uvx", "curl", "wget"]);
|
|
2308
|
-
function refuse_stdio_command(command) {
|
|
2309
|
-
if (command.includes("://") === true) {
|
|
2310
|
-
return "refused url; only a local binary is allowed";
|
|
2497
|
+
function format_capped_transcript(older, max_chars) {
|
|
2498
|
+
if (older.length === 0) {
|
|
2499
|
+
return "";
|
|
2311
2500
|
}
|
|
2312
|
-
|
|
2313
|
-
|
|
2501
|
+
const per_message = Math.max(MIN_PER_MESSAGE_CHARS, Math.floor(max_chars / older.length));
|
|
2502
|
+
const lines = older.map((message) => truncate_head_tail(format_history_line(message), per_message));
|
|
2503
|
+
const joined = lines.join("\n");
|
|
2504
|
+
if (joined.length <= max_chars) {
|
|
2505
|
+
return joined;
|
|
2314
2506
|
}
|
|
2315
|
-
|
|
2316
|
-
|
|
2507
|
+
return truncate_head_tail(joined, max_chars);
|
|
2508
|
+
}
|
|
2509
|
+
function build_summary_request(older, model_hint, budget_tokens) {
|
|
2510
|
+
const max_chars = transcript_char_budget(budget_tokens);
|
|
2511
|
+
const transcript = format_capped_transcript(older, max_chars);
|
|
2512
|
+
const hint = model_hint === void 0 ? "" : `
|
|
2513
|
+
(Continuing agent run as model: ${model_hint})`;
|
|
2514
|
+
return {
|
|
2515
|
+
role: "user",
|
|
2516
|
+
content: "Summarize the following earlier conversation so the agent can continue the task from the summary alone.\n\n" + transcript + hint
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
function log_compression_failure(error) {
|
|
2520
|
+
if (error instanceof ProviderError) {
|
|
2521
|
+
logger.warn(`context compression failed kind=${error.kind} provider=${error.provider_name}`, error);
|
|
2522
|
+
return;
|
|
2317
2523
|
}
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2524
|
+
logger.warn("context compression failed", error);
|
|
2525
|
+
}
|
|
2526
|
+
function split_keep_recent(non_system, keep_recent) {
|
|
2527
|
+
let cut = Math.max(0, non_system.length - keep_recent);
|
|
2528
|
+
while (cut > 0 && non_system[cut]?.role === "tool") {
|
|
2529
|
+
cut -= 1;
|
|
2321
2530
|
}
|
|
2322
|
-
return
|
|
2531
|
+
return { recent: non_system.slice(cut), older: non_system.slice(0, cut) };
|
|
2323
2532
|
}
|
|
2324
|
-
function
|
|
2325
|
-
|
|
2326
|
-
|
|
2533
|
+
async function compress_messages(deps, messages, params) {
|
|
2534
|
+
const system_messages = messages.filter((message) => message.role === "system");
|
|
2535
|
+
const non_system = messages.filter((message) => message.role !== "system");
|
|
2536
|
+
const keep_recent = Math.max(0, params.keep_recent);
|
|
2537
|
+
const { recent, older } = split_keep_recent(non_system, keep_recent);
|
|
2538
|
+
if (older.length === 0) {
|
|
2539
|
+
return { messages: [...messages], summary_chars: 0 };
|
|
2327
2540
|
}
|
|
2328
|
-
|
|
2329
|
-
|
|
2541
|
+
try {
|
|
2542
|
+
const result = await deps.chat(
|
|
2543
|
+
[
|
|
2544
|
+
{ role: "system", content: COMPRESSION_SYSTEM_PROMPT },
|
|
2545
|
+
build_summary_request(older, params.model_hint, params.budget_tokens)
|
|
2546
|
+
],
|
|
2547
|
+
[],
|
|
2548
|
+
{ signal: params.signal }
|
|
2549
|
+
);
|
|
2550
|
+
const summary = result.message.content;
|
|
2551
|
+
const summary_message = {
|
|
2552
|
+
role: "user",
|
|
2553
|
+
content: `[context summary of earlier turns]
|
|
2554
|
+
${summary}
|
|
2555
|
+
[end summary]`
|
|
2556
|
+
};
|
|
2557
|
+
return { messages: [...system_messages, summary_message, ...recent], summary_chars: summary.length };
|
|
2558
|
+
} catch (error) {
|
|
2559
|
+
log_compression_failure(error);
|
|
2560
|
+
return { messages: [...messages], summary_chars: 0 };
|
|
2330
2561
|
}
|
|
2331
|
-
return void 0;
|
|
2332
2562
|
}
|
|
2333
2563
|
|
|
2334
|
-
// src/
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
}
|
|
2340
|
-
|
|
2564
|
+
// src/agent/loop.ts
|
|
2565
|
+
var DEFAULT_COMPRESS_THRESHOLD = 0.8;
|
|
2566
|
+
var KEEP_RECENT_TURNS = 8;
|
|
2567
|
+
var COMPRESS_BACKOFF_TURNS = 3;
|
|
2568
|
+
function turn_range(max_turns) {
|
|
2569
|
+
return Array.from({ length: Math.max(0, max_turns) }, (_unused, index) => index + 1);
|
|
2570
|
+
}
|
|
2571
|
+
function seed_system_prompt(messages, system_prompt) {
|
|
2572
|
+
const history = [...messages];
|
|
2573
|
+
if (system_prompt === void 0) {
|
|
2574
|
+
return history;
|
|
2341
2575
|
}
|
|
2342
|
-
|
|
2343
|
-
|
|
2576
|
+
const system_index = history.findIndex((message) => message.role === "system");
|
|
2577
|
+
if (system_index === -1) {
|
|
2578
|
+
const seeded = { role: "system", content: system_prompt };
|
|
2579
|
+
return [seeded, ...history];
|
|
2344
2580
|
}
|
|
2345
|
-
|
|
2346
|
-
|
|
2581
|
+
const existing = history[system_index];
|
|
2582
|
+
if (existing !== void 0 && existing.content === system_prompt) {
|
|
2583
|
+
return history;
|
|
2347
2584
|
}
|
|
2348
|
-
const
|
|
2349
|
-
|
|
2350
|
-
|
|
2585
|
+
const replaced = { role: "system", content: system_prompt };
|
|
2586
|
+
return [...history.slice(0, system_index), replaced, ...history.slice(system_index + 1)];
|
|
2587
|
+
}
|
|
2588
|
+
function format_tool_result_content(result) {
|
|
2589
|
+
if (result.error !== void 0) {
|
|
2590
|
+
return JSON.stringify({ ok: false, output: result.output, error: result.error });
|
|
2351
2591
|
}
|
|
2352
|
-
return
|
|
2592
|
+
return result.output;
|
|
2593
|
+
}
|
|
2594
|
+
function tool_message_from_result(call, result) {
|
|
2595
|
+
const tool_message = {
|
|
2596
|
+
role: "tool",
|
|
2597
|
+
tool_call_id: call.id,
|
|
2598
|
+
name: call.name,
|
|
2599
|
+
content: format_tool_result_content(result)
|
|
2600
|
+
};
|
|
2601
|
+
if (result.ok !== true) {
|
|
2602
|
+
tool_message.is_error = true;
|
|
2603
|
+
}
|
|
2604
|
+
return tool_message;
|
|
2605
|
+
}
|
|
2606
|
+
async function run_tool_calls(deps, history, turn, calls, emitter, signal) {
|
|
2607
|
+
for (const call of calls) {
|
|
2608
|
+
if (signal_aborted(signal) === true) {
|
|
2609
|
+
const cancelled = { ok: false, output: "", error: "cancelled" };
|
|
2610
|
+
history.push(tool_message_from_result(call, cancelled));
|
|
2611
|
+
emitter?.emit({ type: "tool_call_end", turn, call, result: cancelled, cancelled: true });
|
|
2612
|
+
continue;
|
|
2613
|
+
}
|
|
2614
|
+
emitter?.emit({ type: "tool_call_start", turn, call });
|
|
2615
|
+
const result = await deps.tools.execute(call.name, call.args, deps.tool_context);
|
|
2616
|
+
history.push(tool_message_from_result(call, result));
|
|
2617
|
+
emitter?.emit({ type: "tool_call_end", turn, call, result });
|
|
2618
|
+
}
|
|
2619
|
+
return signal_aborted(signal) === true ? "aborted" : "continued";
|
|
2620
|
+
}
|
|
2621
|
+
async function call_chat(deps, history, params, emitter) {
|
|
2622
|
+
try {
|
|
2623
|
+
return await deps.chat(history, deps.definitions(), {
|
|
2624
|
+
temperature: params.temperature,
|
|
2625
|
+
max_tokens: params.max_tokens,
|
|
2626
|
+
signal: params.signal
|
|
2627
|
+
});
|
|
2628
|
+
} catch (error) {
|
|
2629
|
+
if (signal_aborted(params.signal) === true) {
|
|
2630
|
+
throw error;
|
|
2631
|
+
}
|
|
2632
|
+
if (error instanceof ProviderError) {
|
|
2633
|
+
logger.error(`provider error kind=${error.kind} provider=${error.provider_name}`, error);
|
|
2634
|
+
} else {
|
|
2635
|
+
logger.error("agent chat call failed", error);
|
|
2636
|
+
}
|
|
2637
|
+
emitter?.emit({ type: "error", error });
|
|
2638
|
+
throw error;
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
function replace_history(history, next) {
|
|
2642
|
+
history.length = 0;
|
|
2643
|
+
for (const message of next) {
|
|
2644
|
+
history.push(message);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
function kept_tail_over_budget(history, budget_tokens, threshold) {
|
|
2648
|
+
const system_messages = history.filter((message) => message.role === "system");
|
|
2649
|
+
const non_system = history.filter((message) => message.role !== "system");
|
|
2650
|
+
const { recent } = split_keep_recent(non_system, KEEP_RECENT_TURNS);
|
|
2651
|
+
return should_compress([...system_messages, ...recent], budget_tokens, threshold);
|
|
2652
|
+
}
|
|
2653
|
+
function schedule_compress_backoff(backoff, turn) {
|
|
2654
|
+
backoff.skip_until_turn = turn + COMPRESS_BACKOFF_TURNS + 1;
|
|
2655
|
+
}
|
|
2656
|
+
async function compress_if_needed(deps, history, params, emitter, turn, backoff) {
|
|
2657
|
+
const budget_tokens = params.context_budget_tokens;
|
|
2658
|
+
if (budget_tokens === void 0) {
|
|
2659
|
+
return;
|
|
2660
|
+
}
|
|
2661
|
+
if (turn < backoff.skip_until_turn) {
|
|
2662
|
+
return;
|
|
2663
|
+
}
|
|
2664
|
+
const threshold = params.compress_threshold ?? DEFAULT_COMPRESS_THRESHOLD;
|
|
2665
|
+
if (!should_compress(history, budget_tokens, threshold)) {
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
const non_system_count = history.filter((message) => message.role !== "system").length;
|
|
2669
|
+
if (non_system_count <= KEEP_RECENT_TURNS) {
|
|
2670
|
+
schedule_compress_backoff(backoff, turn);
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
emitter?.emit({ type: "compress_start", estimated_tokens: estimate_messages_tokens(history) });
|
|
2674
|
+
let summarizer_usage;
|
|
2675
|
+
const counting_chat = async (messages, tools, options) => {
|
|
2676
|
+
const result = await deps.chat(messages, tools, options);
|
|
2677
|
+
summarizer_usage = result.usage;
|
|
2678
|
+
return result;
|
|
2679
|
+
};
|
|
2680
|
+
const outcome = await compress_messages(
|
|
2681
|
+
{ chat: counting_chat },
|
|
2682
|
+
history,
|
|
2683
|
+
{ budget_tokens, keep_recent: KEEP_RECENT_TURNS, signal: params.signal }
|
|
2684
|
+
);
|
|
2685
|
+
replace_history(history, outcome.messages);
|
|
2686
|
+
emitter?.emit({
|
|
2687
|
+
type: "compress_end",
|
|
2688
|
+
summary_chars: outcome.summary_chars,
|
|
2689
|
+
usage: summarizer_usage
|
|
2690
|
+
});
|
|
2691
|
+
if (outcome.summary_chars === 0) {
|
|
2692
|
+
schedule_compress_backoff(backoff, turn);
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
if (should_compress(history, budget_tokens, threshold) !== true) {
|
|
2696
|
+
return;
|
|
2697
|
+
}
|
|
2698
|
+
if (kept_tail_over_budget(history, budget_tokens, threshold) === true) {
|
|
2699
|
+
schedule_compress_backoff(backoff, turn);
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
schedule_compress_backoff(backoff, turn);
|
|
2703
|
+
}
|
|
2704
|
+
function find_last_assistant(messages) {
|
|
2705
|
+
return [...messages].reverse().find((message) => message.role === "assistant");
|
|
2706
|
+
}
|
|
2707
|
+
function signal_aborted(signal) {
|
|
2708
|
+
return signal?.aborted === true;
|
|
2709
|
+
}
|
|
2710
|
+
function aborted_outcome(history, turns_used, emitter) {
|
|
2711
|
+
emitter?.emit({ type: "error", error: new DOMException("agent loop aborted", "AbortError") });
|
|
2712
|
+
return {
|
|
2713
|
+
messages: history,
|
|
2714
|
+
final: find_last_assistant(history),
|
|
2715
|
+
result: void 0,
|
|
2716
|
+
turns_used,
|
|
2717
|
+
stopped_reason: "aborted"
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
async function run_conversation(deps, messages, params) {
|
|
2721
|
+
const history = seed_system_prompt(messages, params.system_prompt);
|
|
2722
|
+
const emitter = deps.emitter;
|
|
2723
|
+
const compress_backoff = { skip_until_turn: 0 };
|
|
2724
|
+
for (const turn of turn_range(params.max_turns)) {
|
|
2725
|
+
if (signal_aborted(params.signal) === true) {
|
|
2726
|
+
return aborted_outcome(history, turn - 1, emitter);
|
|
2727
|
+
}
|
|
2728
|
+
emitter?.emit({ type: "turn_start", turn });
|
|
2729
|
+
await compress_if_needed(deps, history, params, emitter, turn, compress_backoff);
|
|
2730
|
+
emitter?.emit({ type: "llm_start", turn });
|
|
2731
|
+
let result;
|
|
2732
|
+
try {
|
|
2733
|
+
result = await call_chat(deps, history, params, emitter);
|
|
2734
|
+
} catch (error) {
|
|
2735
|
+
if (signal_aborted(params.signal) === true) {
|
|
2736
|
+
return aborted_outcome(history, turn - 1, emitter);
|
|
2737
|
+
}
|
|
2738
|
+
throw error;
|
|
2739
|
+
}
|
|
2740
|
+
emitter?.emit({ type: "llm_end", turn, result });
|
|
2741
|
+
history.push(result.message);
|
|
2742
|
+
const calls = result.message.tool_calls ?? [];
|
|
2743
|
+
if (calls.length === 0) {
|
|
2744
|
+
emitter?.emit({ type: "final", message: result.message, result });
|
|
2745
|
+
emitter?.emit({ type: "turn_end", turn });
|
|
2746
|
+
return { messages: history, final: result.message, result, turns_used: turn, stopped_reason: "final" };
|
|
2747
|
+
}
|
|
2748
|
+
const tool_status = await run_tool_calls(deps, history, turn, calls, emitter, params.signal);
|
|
2749
|
+
if (tool_status === "aborted") {
|
|
2750
|
+
return aborted_outcome(history, turn, emitter);
|
|
2751
|
+
}
|
|
2752
|
+
if (turn < params.max_turns) {
|
|
2753
|
+
emitter?.emit({ type: "turn_end", turn });
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
emitter?.emit({ type: "budget_exhausted", turns_used: params.max_turns });
|
|
2757
|
+
emitter?.emit({ type: "turn_end", turn: params.max_turns });
|
|
2758
|
+
return {
|
|
2759
|
+
messages: history,
|
|
2760
|
+
final: find_last_assistant(history),
|
|
2761
|
+
result: void 0,
|
|
2762
|
+
turns_used: params.max_turns,
|
|
2763
|
+
stopped_reason: "budget"
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
// src/agent/config.ts
|
|
2768
|
+
import { z } from "zod";
|
|
2769
|
+
|
|
2770
|
+
// src/gateway/access.ts
|
|
2771
|
+
var PUBLIC_PLATFORMS = /* @__PURE__ */ new Set(["telegram", "discord", "twitch"]);
|
|
2772
|
+
var DEFAULT_GATEWAY_TOOLS_ENABLED = [
|
|
2773
|
+
"read_file",
|
|
2774
|
+
"list_dir",
|
|
2775
|
+
"grep_files",
|
|
2776
|
+
"fetch_url",
|
|
2777
|
+
"web_search",
|
|
2778
|
+
"docs_read",
|
|
2779
|
+
"docs_search"
|
|
2780
|
+
];
|
|
2781
|
+
function gateway_tools_enabled(config) {
|
|
2782
|
+
return config.gateway?.tools_enabled ?? DEFAULT_GATEWAY_TOOLS_ENABLED;
|
|
2783
|
+
}
|
|
2784
|
+
function is_gateway_sender_allowed(config, platform, chat_id, user_id) {
|
|
2785
|
+
if (PUBLIC_PLATFORMS.has(platform) === false) {
|
|
2786
|
+
return true;
|
|
2787
|
+
}
|
|
2788
|
+
const users = config.gateway?.allowed_users?.[platform] ?? [];
|
|
2789
|
+
const chats = config.gateway?.allowed_chats?.[platform] ?? [];
|
|
2790
|
+
if (users.length === 0 && chats.length === 0) {
|
|
2791
|
+
return false;
|
|
2792
|
+
}
|
|
2793
|
+
const user_ok = users.length === 0 || users.includes(user_id);
|
|
2794
|
+
const chat_ok = chats.length === 0 || chats.includes(chat_id);
|
|
2795
|
+
return user_ok && chat_ok;
|
|
2796
|
+
}
|
|
2797
|
+
function check_gateway_sender(config, platform, chat_id, user_id) {
|
|
2798
|
+
if (is_gateway_sender_allowed(config, platform, chat_id, user_id) === true) {
|
|
2799
|
+
return true;
|
|
2800
|
+
}
|
|
2801
|
+
logger.warn(`gateway denied ${platform} chat=${chat_id} user=${user_id}`);
|
|
2802
|
+
return false;
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// src/mcp/mcp_pin.ts
|
|
2806
|
+
import path11 from "path";
|
|
2807
|
+
|
|
2808
|
+
// src/mcp/mcp_catalog.ts
|
|
2809
|
+
import { existsSync as existsSync2, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
|
|
2810
|
+
import path9 from "path";
|
|
2811
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2812
|
+
var cached;
|
|
2813
|
+
function catalog_dir() {
|
|
2814
|
+
let dir = path9.dirname(fileURLToPath2(import.meta.url));
|
|
2815
|
+
for (let hop = 0; hop < 6; hop += 1) {
|
|
2816
|
+
const candidate = path9.join(dir, "optional-mcps");
|
|
2817
|
+
if (existsSync2(path9.join(candidate, "redot", "manifest.json")) === true) {
|
|
2818
|
+
return candidate;
|
|
2819
|
+
}
|
|
2820
|
+
dir = path9.dirname(dir);
|
|
2821
|
+
}
|
|
2822
|
+
throw new Error("mcp catalog not found");
|
|
2823
|
+
}
|
|
2824
|
+
function read_manifest(file) {
|
|
2825
|
+
const parsed = safe_json_parse(readFileSync4(file, "utf8"));
|
|
2826
|
+
if (parsed === void 0 || typeof parsed.name !== "string") {
|
|
2827
|
+
throw new Error("mcp catalog manifest rejected");
|
|
2828
|
+
}
|
|
2829
|
+
return parsed;
|
|
2830
|
+
}
|
|
2831
|
+
function load_catalog() {
|
|
2832
|
+
if (cached !== void 0) {
|
|
2833
|
+
return cached;
|
|
2834
|
+
}
|
|
2835
|
+
const manifests = [];
|
|
2836
|
+
for (const name of readdirSync3(catalog_dir())) {
|
|
2837
|
+
const file = path9.join(catalog_dir(), name, "manifest.json");
|
|
2838
|
+
if (existsSync2(file) === true) {
|
|
2839
|
+
manifests.push(read_manifest(file));
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
cached = manifests;
|
|
2843
|
+
return cached;
|
|
2844
|
+
}
|
|
2845
|
+
function catalog_by_name(name) {
|
|
2846
|
+
return load_catalog().find((entry) => entry.name === name);
|
|
2847
|
+
}
|
|
2848
|
+
function catalog_by_basename(command_basename) {
|
|
2849
|
+
const base = command_basename.toLowerCase();
|
|
2850
|
+
return load_catalog().find((entry) => entry.command_basename?.toLowerCase() === base);
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// src/mcp/mcp_refuse.ts
|
|
2854
|
+
import path10 from "path";
|
|
2855
|
+
var SHELL_META = /[;&|`$<>]/;
|
|
2856
|
+
var DOWNLOADERS = /* @__PURE__ */ new Set([
|
|
2857
|
+
"npx",
|
|
2858
|
+
"npm",
|
|
2859
|
+
"bunx",
|
|
2860
|
+
"uvx",
|
|
2861
|
+
"curl",
|
|
2862
|
+
"wget",
|
|
2863
|
+
"pnpm",
|
|
2864
|
+
"yarn",
|
|
2865
|
+
"deno"
|
|
2866
|
+
]);
|
|
2867
|
+
var SHELLS = /* @__PURE__ */ new Set([
|
|
2868
|
+
"sh",
|
|
2869
|
+
"bash",
|
|
2870
|
+
"zsh",
|
|
2871
|
+
"fish",
|
|
2872
|
+
"dash",
|
|
2873
|
+
"csh",
|
|
2874
|
+
"tcsh",
|
|
2875
|
+
"ksh",
|
|
2876
|
+
"cmd",
|
|
2877
|
+
"cmd.exe",
|
|
2878
|
+
"powershell",
|
|
2879
|
+
"pwsh"
|
|
2880
|
+
]);
|
|
2881
|
+
var INTERPRETERS = /* @__PURE__ */ new Set(["node", "nodejs", "python", "python3", "python2", "ruby", "perl", "php"]);
|
|
2882
|
+
var EVAL_FLAGS = /* @__PURE__ */ new Set(["-c", "-e", "--eval", "-Command", "-EncodedCommand"]);
|
|
2883
|
+
function base_of(command) {
|
|
2884
|
+
return path10.basename(command).toLowerCase();
|
|
2885
|
+
}
|
|
2886
|
+
function flag_name(arg) {
|
|
2887
|
+
return arg.split("=")[0] ?? arg;
|
|
2888
|
+
}
|
|
2889
|
+
function refuse_eval_args(base, args) {
|
|
2890
|
+
if (INTERPRETERS.has(base) === false && base !== "bun") {
|
|
2891
|
+
return void 0;
|
|
2892
|
+
}
|
|
2893
|
+
for (const arg of args) {
|
|
2894
|
+
const name = flag_name(arg);
|
|
2895
|
+
if (EVAL_FLAGS.has(name) === true) {
|
|
2896
|
+
return `refused ${base} eval flag '${arg}'`;
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
return void 0;
|
|
2900
|
+
}
|
|
2901
|
+
function refuse_bun_x(base, args) {
|
|
2902
|
+
if (base !== "bun") {
|
|
2903
|
+
return void 0;
|
|
2904
|
+
}
|
|
2905
|
+
const first = args[0];
|
|
2906
|
+
if (first === "x" || first === "exec") {
|
|
2907
|
+
return "refused bun x / bun exec";
|
|
2908
|
+
}
|
|
2909
|
+
return void 0;
|
|
2910
|
+
}
|
|
2911
|
+
function refuse_pkg_dlx(base, args) {
|
|
2912
|
+
if (base !== "pnpm" && base !== "yarn" && base !== "npm") {
|
|
2913
|
+
return void 0;
|
|
2914
|
+
}
|
|
2915
|
+
const first = args[0];
|
|
2916
|
+
if (first === "dlx" || first === "exec" || first === "create") {
|
|
2917
|
+
return `refused ${base} ${first}`;
|
|
2918
|
+
}
|
|
2919
|
+
return void 0;
|
|
2920
|
+
}
|
|
2921
|
+
function refuse_env_chain(args) {
|
|
2922
|
+
for (const arg of args) {
|
|
2923
|
+
if (arg.startsWith("-") === true) {
|
|
2924
|
+
if (arg === "-S" || arg.startsWith("-S") === true || arg === "--split-string" || arg.startsWith("--split-string=") === true) {
|
|
2925
|
+
return "refused env -S / --split-string";
|
|
2926
|
+
}
|
|
2927
|
+
continue;
|
|
2928
|
+
}
|
|
2929
|
+
if (arg.includes("=") === true) {
|
|
2930
|
+
continue;
|
|
2931
|
+
}
|
|
2932
|
+
const base = base_of(arg);
|
|
2933
|
+
if (DOWNLOADERS.has(base) === true || SHELLS.has(base) === true) {
|
|
2934
|
+
return `refused env \u2192 '${base}'`;
|
|
2935
|
+
}
|
|
2936
|
+
return void 0;
|
|
2937
|
+
}
|
|
2938
|
+
return void 0;
|
|
2939
|
+
}
|
|
2940
|
+
function refuse_arg_downloaders(args) {
|
|
2941
|
+
const limit = Math.min(args.length, 3);
|
|
2942
|
+
for (let index = 0; index < limit; index += 1) {
|
|
2943
|
+
const arg = args[index];
|
|
2944
|
+
if (arg === void 0 || arg.startsWith("-") === true || arg.includes("=") === true) {
|
|
2945
|
+
continue;
|
|
2946
|
+
}
|
|
2947
|
+
const base = base_of(arg);
|
|
2948
|
+
if (DOWNLOADERS.has(base) === true) {
|
|
2949
|
+
return `refused download command '${base}' in args`;
|
|
2950
|
+
}
|
|
2951
|
+
if (SHELLS.has(base) === true) {
|
|
2952
|
+
return `refused shell '${base}' in args`;
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
return void 0;
|
|
2956
|
+
}
|
|
2957
|
+
function refuse_stdio_command(command, args = []) {
|
|
2958
|
+
if (command.includes("://") === true) {
|
|
2959
|
+
return "refused url; only a local binary is allowed";
|
|
2960
|
+
}
|
|
2961
|
+
if (SHELL_META.test(command) === true) {
|
|
2962
|
+
return "refused shell metacharacters in mcp command";
|
|
2963
|
+
}
|
|
2964
|
+
if (command.length === 0 || command.trim() !== command || /\s/.test(command) === true) {
|
|
2965
|
+
return "refused mcp command";
|
|
2966
|
+
}
|
|
2967
|
+
const base = base_of(command);
|
|
2968
|
+
if (DOWNLOADERS.has(base) === true) {
|
|
2969
|
+
return `refused download command '${base}'`;
|
|
2970
|
+
}
|
|
2971
|
+
if (SHELLS.has(base) === true) {
|
|
2972
|
+
return `refused shell '${base}'`;
|
|
2973
|
+
}
|
|
2974
|
+
if (base === "env") {
|
|
2975
|
+
return refuse_env_chain(args) ?? refuse_arg_downloaders(args);
|
|
2976
|
+
}
|
|
2977
|
+
return refuse_bun_x(base, args) ?? refuse_pkg_dlx(base, args) ?? refuse_eval_args(base, args) ?? refuse_arg_downloaders(args);
|
|
2978
|
+
}
|
|
2979
|
+
function refuse_stdio_arg(arg) {
|
|
2980
|
+
if (arg.includes("://") === true) {
|
|
2981
|
+
return "refused url in mcp args";
|
|
2982
|
+
}
|
|
2983
|
+
if (SHELL_META.test(arg) === true) {
|
|
2984
|
+
return "refused shell metacharacters in mcp args";
|
|
2985
|
+
}
|
|
2986
|
+
return void 0;
|
|
2987
|
+
}
|
|
2988
|
+
|
|
2989
|
+
// src/mcp/mcp_url.ts
|
|
2990
|
+
function refuse_http_url(url) {
|
|
2991
|
+
let parsed;
|
|
2992
|
+
try {
|
|
2993
|
+
parsed = new URL(url);
|
|
2994
|
+
} catch {
|
|
2995
|
+
return "refused mcp url";
|
|
2996
|
+
}
|
|
2997
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
2998
|
+
return "refused mcp url";
|
|
2999
|
+
}
|
|
3000
|
+
if (parsed.username.length > 0 || parsed.password.length > 0) {
|
|
3001
|
+
return "refused mcp url credentials";
|
|
3002
|
+
}
|
|
3003
|
+
const host = parsed.hostname.toLowerCase();
|
|
3004
|
+
if (host !== "127.0.0.1" && host !== "localhost") {
|
|
3005
|
+
return "refused mcp url; loopback only";
|
|
3006
|
+
}
|
|
3007
|
+
return void 0;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// src/mcp/mcp_pin.ts
|
|
3011
|
+
function pin_for(name, command) {
|
|
3012
|
+
const by_name = catalog_by_name(name);
|
|
3013
|
+
if (by_name?.command_basename !== void 0) {
|
|
3014
|
+
return by_name;
|
|
3015
|
+
}
|
|
3016
|
+
if (command === void 0) {
|
|
3017
|
+
return by_name;
|
|
3018
|
+
}
|
|
3019
|
+
return catalog_by_basename(path11.basename(command)) ?? by_name;
|
|
2353
3020
|
}
|
|
2354
|
-
|
|
2355
|
-
// src/mcp/mcp_pin.ts
|
|
2356
3021
|
function refuse_catalog_stdio(name, command, args) {
|
|
2357
|
-
const pin =
|
|
3022
|
+
const pin = pin_for(name, command);
|
|
2358
3023
|
if (pin?.command_basename === void 0) {
|
|
2359
3024
|
return void 0;
|
|
2360
3025
|
}
|
|
@@ -2363,11 +3028,11 @@ function refuse_catalog_stdio(name, command, args) {
|
|
|
2363
3028
|
}
|
|
2364
3029
|
const prefix = pin.args_prefix ?? [];
|
|
2365
3030
|
if (args.length !== prefix.length + 1) {
|
|
2366
|
-
return `refused ${name} args; expected ${prefix.join(" ")} <project>`;
|
|
3031
|
+
return `refused ${pin.name} args; expected ${prefix.join(" ")} <project>`;
|
|
2367
3032
|
}
|
|
2368
3033
|
for (let index = 0; index < prefix.length; index += 1) {
|
|
2369
3034
|
if (args[index] !== prefix[index]) {
|
|
2370
|
-
return `refused ${name} args; expected ${prefix.join(" ")} <project>`;
|
|
3035
|
+
return `refused ${pin.name} args; expected ${prefix.join(" ")} <project>`;
|
|
2371
3036
|
}
|
|
2372
3037
|
}
|
|
2373
3038
|
const project = args[prefix.length];
|
|
@@ -2393,7 +3058,7 @@ function refuse_mcp_entry(name, entry) {
|
|
|
2393
3058
|
return "refused mcp entry";
|
|
2394
3059
|
}
|
|
2395
3060
|
const args = entry.args ?? [];
|
|
2396
|
-
return refuse_stdio_command(entry.command) ?? refuse_catalog_stdio(name, entry.command, args) ?? refuse_arg_list(args);
|
|
3061
|
+
return refuse_stdio_command(entry.command, args) ?? refuse_catalog_stdio(name, entry.command, args) ?? refuse_arg_list(args);
|
|
2397
3062
|
}
|
|
2398
3063
|
|
|
2399
3064
|
// src/agent/config.ts
|
|
@@ -2447,12 +3112,26 @@ var provider_schema = z.object({
|
|
|
2447
3112
|
/** Injectable fetch, mainly for tests; passes through untouched. */
|
|
2448
3113
|
fetch_fn: z.custom(() => true).optional()
|
|
2449
3114
|
}).passthrough();
|
|
3115
|
+
var providers_schema = z.array(provider_schema).min(1).superRefine((providers, ctx) => {
|
|
3116
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3117
|
+
for (const [index, provider] of providers.entries()) {
|
|
3118
|
+
if (seen.has(provider.name) === true) {
|
|
3119
|
+
ctx.addIssue({
|
|
3120
|
+
code: z.ZodIssueCode.custom,
|
|
3121
|
+
path: [index, "name"],
|
|
3122
|
+
message: `duplicate provider name "${provider.name}"`
|
|
3123
|
+
});
|
|
3124
|
+
continue;
|
|
3125
|
+
}
|
|
3126
|
+
seen.add(provider.name);
|
|
3127
|
+
}
|
|
3128
|
+
});
|
|
2450
3129
|
var agent_config_schema = z.object({
|
|
2451
3130
|
/** Wizard label. The TUI banner uses the active theme welcome string. */
|
|
2452
3131
|
agent_name: z.string().min(1).default("lich"),
|
|
2453
3132
|
system_prompt: z.string().optional(),
|
|
2454
3133
|
max_turns: z.number().int().min(1).default(25),
|
|
2455
|
-
providers:
|
|
3134
|
+
providers: providers_schema,
|
|
2456
3135
|
work_dir: z.string().optional(),
|
|
2457
3136
|
tools_enabled: z.union([z.literal("all"), z.array(z.string())]).default("all"),
|
|
2458
3137
|
temperature: z.number().min(0).max(2).optional(),
|
|
@@ -2483,6 +3162,13 @@ function freeze_config(config) {
|
|
|
2483
3162
|
for (const provider of config.providers) {
|
|
2484
3163
|
Object.freeze(provider);
|
|
2485
3164
|
}
|
|
3165
|
+
Object.freeze(config.plugins);
|
|
3166
|
+
for (const plugin of config.plugins) {
|
|
3167
|
+
Object.freeze(plugin);
|
|
3168
|
+
}
|
|
3169
|
+
if (Array.isArray(config.tools_enabled) === true) {
|
|
3170
|
+
Object.freeze(config.tools_enabled);
|
|
3171
|
+
}
|
|
2486
3172
|
if (config.gateway !== void 0) {
|
|
2487
3173
|
Object.freeze(config.gateway.platforms);
|
|
2488
3174
|
Object.freeze(config.gateway.token_envs);
|
|
@@ -2538,12 +3224,19 @@ var AgentEmitter = class {
|
|
|
2538
3224
|
};
|
|
2539
3225
|
|
|
2540
3226
|
// src/mcp/mcp_http.ts
|
|
2541
|
-
|
|
3227
|
+
function aborted(signal) {
|
|
3228
|
+
return signal?.aborted === true;
|
|
3229
|
+
}
|
|
3230
|
+
async function post_rpc(url, fetch_fn, body, signal) {
|
|
3231
|
+
if (aborted(signal) === true) {
|
|
3232
|
+
throw new Error("cancelled");
|
|
3233
|
+
}
|
|
2542
3234
|
const response = await fetch_fn(url, {
|
|
2543
3235
|
method: "POST",
|
|
2544
3236
|
redirect: "error",
|
|
2545
3237
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
2546
|
-
body: JSON.stringify(body)
|
|
3238
|
+
body: JSON.stringify(body),
|
|
3239
|
+
signal
|
|
2547
3240
|
});
|
|
2548
3241
|
const parsed = await response.json();
|
|
2549
3242
|
if (parsed.error !== void 0) {
|
|
@@ -2555,10 +3248,10 @@ async function post_rpc(url, fetch_fn, body) {
|
|
|
2555
3248
|
function http_pipe(url, fetch_fn) {
|
|
2556
3249
|
let next_id = 1;
|
|
2557
3250
|
return {
|
|
2558
|
-
request(method, params) {
|
|
3251
|
+
request(method, params, signal) {
|
|
2559
3252
|
const id = next_id;
|
|
2560
3253
|
next_id += 1;
|
|
2561
|
-
return post_rpc(url, fetch_fn, { jsonrpc: "2.0", id, method, params });
|
|
3254
|
+
return post_rpc(url, fetch_fn, { jsonrpc: "2.0", id, method, params }, signal);
|
|
2562
3255
|
},
|
|
2563
3256
|
notify(method) {
|
|
2564
3257
|
void fetch_fn(url, {
|
|
@@ -2611,7 +3304,6 @@ function plan_stdio(name, command, args, env_path) {
|
|
|
2611
3304
|
}
|
|
2612
3305
|
|
|
2613
3306
|
// src/mcp/mcp_pipe.ts
|
|
2614
|
-
var SKIP_LIMIT = 32;
|
|
2615
3307
|
function parse_rpc_line(line) {
|
|
2616
3308
|
const parsed = safe_json_parse(line);
|
|
2617
3309
|
if (typeof parsed !== "object" || parsed === null) {
|
|
@@ -2619,41 +3311,112 @@ function parse_rpc_line(line) {
|
|
|
2619
3311
|
}
|
|
2620
3312
|
return parsed;
|
|
2621
3313
|
}
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
3314
|
+
function error_message(error) {
|
|
3315
|
+
const detail = error?.message;
|
|
3316
|
+
return typeof detail === "string" && detail.length > 0 ? detail : "mcp error";
|
|
3317
|
+
}
|
|
3318
|
+
function reject_all(pending, message) {
|
|
3319
|
+
for (const waiter of pending.values()) {
|
|
3320
|
+
waiter.reject(new Error(message));
|
|
3321
|
+
}
|
|
3322
|
+
pending.clear();
|
|
3323
|
+
}
|
|
3324
|
+
function settle_response(pending, parsed) {
|
|
3325
|
+
if (typeof parsed.id !== "number") {
|
|
3326
|
+
return;
|
|
3327
|
+
}
|
|
3328
|
+
const waiter = pending.get(parsed.id);
|
|
3329
|
+
if (waiter === void 0) {
|
|
3330
|
+
return;
|
|
3331
|
+
}
|
|
3332
|
+
pending.delete(parsed.id);
|
|
3333
|
+
if (parsed.error !== void 0) {
|
|
3334
|
+
waiter.reject(new Error(error_message(parsed.error)));
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
waiter.resolve(parsed.result);
|
|
3338
|
+
}
|
|
3339
|
+
function start_pump(child, pending, mark_dead) {
|
|
3340
|
+
return (async () => {
|
|
3341
|
+
for (; ; ) {
|
|
3342
|
+
const line = await child.read_line();
|
|
3343
|
+
const failure = child.failed();
|
|
3344
|
+
if (failure !== void 0) {
|
|
3345
|
+
mark_dead(failure);
|
|
3346
|
+
reject_all(pending, failure);
|
|
3347
|
+
return;
|
|
3348
|
+
}
|
|
3349
|
+
if (line === void 0) {
|
|
3350
|
+
mark_dead("mcp closed the pipe");
|
|
3351
|
+
reject_all(pending, "mcp closed the pipe");
|
|
3352
|
+
return;
|
|
3353
|
+
}
|
|
3354
|
+
const parsed = parse_rpc_line(line);
|
|
3355
|
+
if (parsed === void 0 || typeof parsed.method === "string") {
|
|
3356
|
+
continue;
|
|
3357
|
+
}
|
|
3358
|
+
settle_response(pending, parsed);
|
|
2639
3359
|
}
|
|
2640
|
-
|
|
3360
|
+
})();
|
|
3361
|
+
}
|
|
3362
|
+
function aborted2(signal) {
|
|
3363
|
+
return signal?.aborted === true;
|
|
3364
|
+
}
|
|
3365
|
+
function with_signal(promise, signal) {
|
|
3366
|
+
if (signal === void 0) {
|
|
3367
|
+
return promise;
|
|
2641
3368
|
}
|
|
2642
|
-
|
|
3369
|
+
if (aborted2(signal) === true) {
|
|
3370
|
+
return Promise.reject(new Error("cancelled"));
|
|
3371
|
+
}
|
|
3372
|
+
return new Promise((resolve, reject) => {
|
|
3373
|
+
const on_abort = () => {
|
|
3374
|
+
reject(new Error("cancelled"));
|
|
3375
|
+
};
|
|
3376
|
+
signal.addEventListener("abort", on_abort, { once: true });
|
|
3377
|
+
promise.then(
|
|
3378
|
+
(value) => {
|
|
3379
|
+
signal.removeEventListener("abort", on_abort);
|
|
3380
|
+
resolve(value);
|
|
3381
|
+
},
|
|
3382
|
+
(error) => {
|
|
3383
|
+
signal.removeEventListener("abort", on_abort);
|
|
3384
|
+
reject(error);
|
|
3385
|
+
}
|
|
3386
|
+
);
|
|
3387
|
+
});
|
|
2643
3388
|
}
|
|
2644
3389
|
function stdio_pipe(child) {
|
|
2645
3390
|
let next_id = 1;
|
|
3391
|
+
const pending = /* @__PURE__ */ new Map();
|
|
3392
|
+
let pump;
|
|
3393
|
+
let dead;
|
|
3394
|
+
const mark_dead = (message) => {
|
|
3395
|
+
dead = message;
|
|
3396
|
+
};
|
|
2646
3397
|
return {
|
|
2647
|
-
request(method, params) {
|
|
3398
|
+
request(method, params, signal) {
|
|
3399
|
+
if (aborted2(signal) === true) {
|
|
3400
|
+
return Promise.reject(new Error("cancelled"));
|
|
3401
|
+
}
|
|
3402
|
+
if (dead !== void 0) {
|
|
3403
|
+
return Promise.reject(new Error(dead));
|
|
3404
|
+
}
|
|
2648
3405
|
const id = next_id;
|
|
2649
3406
|
next_id += 1;
|
|
3407
|
+
const wait = new Promise((resolve, reject) => {
|
|
3408
|
+
pending.set(id, { resolve, reject });
|
|
3409
|
+
});
|
|
2650
3410
|
child.write_line(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
|
|
2651
|
-
|
|
3411
|
+
pump ??= start_pump(child, pending, mark_dead);
|
|
3412
|
+
return with_signal(wait, signal);
|
|
2652
3413
|
},
|
|
2653
3414
|
notify(method) {
|
|
2654
3415
|
child.write_line(JSON.stringify({ jsonrpc: "2.0", method }));
|
|
2655
3416
|
},
|
|
2656
3417
|
close() {
|
|
3418
|
+
mark_dead("mcp closed the pipe");
|
|
3419
|
+
reject_all(pending, "mcp closed the pipe");
|
|
2657
3420
|
child.stop();
|
|
2658
3421
|
}
|
|
2659
3422
|
};
|
|
@@ -2679,7 +3442,7 @@ function run_call(session, wire_name, args, context) {
|
|
|
2679
3442
|
if (context.signal?.aborted === true) {
|
|
2680
3443
|
throw new Error("cancelled");
|
|
2681
3444
|
}
|
|
2682
|
-
return { ok: true, output: await session.call_tool(wire_name, args) };
|
|
3445
|
+
return { ok: true, output: await session.call_tool(wire_name, args, context.signal) };
|
|
2683
3446
|
});
|
|
2684
3447
|
}
|
|
2685
3448
|
function tool_for(registered, wire_name, spec, session) {
|
|
@@ -2691,8 +3454,8 @@ function tool_for(registered, wire_name, spec, session) {
|
|
|
2691
3454
|
execute: (args, context) => run_call(session, wire_name, args, context)
|
|
2692
3455
|
};
|
|
2693
3456
|
}
|
|
2694
|
-
function register_listed(registry, server, listed, enabled, session) {
|
|
2695
|
-
const excluded = new Set(
|
|
3457
|
+
function register_listed(registry, server, listed, enabled, session, command) {
|
|
3458
|
+
const excluded = new Set(pin_for(server, command)?.exclude_tools ?? []);
|
|
2696
3459
|
for (const spec of listed) {
|
|
2697
3460
|
if (excluded.has(spec.name) === true) {
|
|
2698
3461
|
continue;
|
|
@@ -2728,6 +3491,7 @@ function assert_handshake(result) {
|
|
|
2728
3491
|
}
|
|
2729
3492
|
|
|
2730
3493
|
// src/mcp/mcp_content.ts
|
|
3494
|
+
var MCP_ERROR_MAX = 4e3;
|
|
2731
3495
|
function content_text(result) {
|
|
2732
3496
|
if (typeof result !== "object" || result === null) {
|
|
2733
3497
|
return "";
|
|
@@ -2748,16 +3512,30 @@ function content_text(result) {
|
|
|
2748
3512
|
}
|
|
2749
3513
|
}
|
|
2750
3514
|
}
|
|
2751
|
-
const text = parts.join("\n");
|
|
3515
|
+
const text = clamp_output(parts.join("\n"));
|
|
2752
3516
|
if (body.isError === true) {
|
|
2753
|
-
|
|
3517
|
+
const detail = text.length > 0 ? text : "mcp tool failed";
|
|
3518
|
+
throw new Error(detail.length > MCP_ERROR_MAX ? detail.slice(0, MCP_ERROR_MAX) : detail);
|
|
2754
3519
|
}
|
|
2755
3520
|
return text;
|
|
2756
3521
|
}
|
|
2757
3522
|
|
|
2758
3523
|
// src/mcp/mcp_result.ts
|
|
3524
|
+
var MCP_NAME_MAX = 64;
|
|
3525
|
+
var MCP_DESC_MAX = 2e3;
|
|
3526
|
+
var MCP_SCHEMA_JSON_MAX = 16e3;
|
|
3527
|
+
function clamp_text(value, max) {
|
|
3528
|
+
return value.length > max ? value.slice(0, max) : value;
|
|
3529
|
+
}
|
|
3530
|
+
function schema_within_budget(raw) {
|
|
3531
|
+
try {
|
|
3532
|
+
return JSON.stringify(raw).length <= MCP_SCHEMA_JSON_MAX;
|
|
3533
|
+
} catch {
|
|
3534
|
+
return false;
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
2759
3537
|
function tool_schema(raw) {
|
|
2760
|
-
if (typeof raw !== "object" || raw === null) {
|
|
3538
|
+
if (typeof raw !== "object" || raw === null || schema_within_budget(raw) === false) {
|
|
2761
3539
|
return { type: "object" };
|
|
2762
3540
|
}
|
|
2763
3541
|
const body = raw;
|
|
@@ -2783,12 +3561,13 @@ function parse_tools(result) {
|
|
|
2783
3561
|
continue;
|
|
2784
3562
|
}
|
|
2785
3563
|
const tool = item;
|
|
2786
|
-
if (typeof tool.name !== "string" || tool.name.length === 0) {
|
|
3564
|
+
if (typeof tool.name !== "string" || tool.name.length === 0 || tool.name.length > MCP_NAME_MAX) {
|
|
2787
3565
|
continue;
|
|
2788
3566
|
}
|
|
3567
|
+
const description = typeof tool.description === "string" ? clamp_text(tool.description, MCP_DESC_MAX) : tool.name;
|
|
2789
3568
|
tools.push({
|
|
2790
3569
|
name: tool.name,
|
|
2791
|
-
description
|
|
3570
|
+
description,
|
|
2792
3571
|
parameters: tool_schema(tool.inputSchema)
|
|
2793
3572
|
});
|
|
2794
3573
|
}
|
|
@@ -2810,20 +3589,20 @@ var McpSession = class {
|
|
|
2810
3589
|
this.closed = true;
|
|
2811
3590
|
this.pipe.close();
|
|
2812
3591
|
}
|
|
2813
|
-
async list_tools() {
|
|
2814
|
-
await this.ensure_ready();
|
|
2815
|
-
return parse_tools(await this.pipe.request("tools/list", {}));
|
|
3592
|
+
async list_tools(signal) {
|
|
3593
|
+
await this.ensure_ready(signal);
|
|
3594
|
+
return parse_tools(await this.pipe.request("tools/list", {}, signal));
|
|
2816
3595
|
}
|
|
2817
|
-
async call_tool(name, args) {
|
|
2818
|
-
await this.ensure_ready();
|
|
2819
|
-
return content_text(await this.pipe.request("tools/call", { name, arguments: args }));
|
|
3596
|
+
async call_tool(name, args, signal) {
|
|
3597
|
+
await this.ensure_ready(signal);
|
|
3598
|
+
return content_text(await this.pipe.request("tools/call", { name, arguments: args }, signal));
|
|
2820
3599
|
}
|
|
2821
|
-
async ensure_ready() {
|
|
3600
|
+
async ensure_ready(signal) {
|
|
2822
3601
|
if (this.ready_done === true) {
|
|
2823
3602
|
return;
|
|
2824
3603
|
}
|
|
2825
3604
|
try {
|
|
2826
|
-
assert_handshake(await this.pipe.request("initialize", init_params()));
|
|
3605
|
+
assert_handshake(await this.pipe.request("initialize", init_params(), signal));
|
|
2827
3606
|
this.pipe.notify("notifications/initialized");
|
|
2828
3607
|
this.ready_done = true;
|
|
2829
3608
|
} catch (error) {
|
|
@@ -2920,11 +3699,27 @@ async function drain_stderr(stream) {
|
|
|
2920
3699
|
continue;
|
|
2921
3700
|
}
|
|
2922
3701
|
}
|
|
3702
|
+
function bun_child_env(extra) {
|
|
3703
|
+
if (extra === void 0) {
|
|
3704
|
+
return void 0;
|
|
3705
|
+
}
|
|
3706
|
+
const merged = {};
|
|
3707
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
3708
|
+
if (typeof value === "string") {
|
|
3709
|
+
merged[key] = value;
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
3713
|
+
merged[key] = value;
|
|
3714
|
+
}
|
|
3715
|
+
return merged;
|
|
3716
|
+
}
|
|
2923
3717
|
function bun_line_child(command, args, env) {
|
|
2924
3718
|
const queue = create_line_queue();
|
|
2925
3719
|
try {
|
|
2926
3720
|
const options = { stdin: "pipe", stdout: "pipe", stderr: "pipe" };
|
|
2927
|
-
const
|
|
3721
|
+
const merged = bun_child_env(env);
|
|
3722
|
+
const child = merged === void 0 ? Bun.spawn([command, ...args], options) : Bun.spawn([command, ...args], { ...options, env: merged });
|
|
2928
3723
|
void pump_stdout(child.stdout, queue);
|
|
2929
3724
|
void drain_stderr(child.stderr);
|
|
2930
3725
|
return {
|
|
@@ -2980,6 +3775,10 @@ function node_line_child(command, args, env) {
|
|
|
2980
3775
|
queue.close();
|
|
2981
3776
|
});
|
|
2982
3777
|
child.stderr?.resume();
|
|
3778
|
+
child.stdin?.on("error", () => {
|
|
3779
|
+
failure ??= "mcp closed the pipe";
|
|
3780
|
+
queue.close();
|
|
3781
|
+
});
|
|
2983
3782
|
child.on("error", (error) => {
|
|
2984
3783
|
failure = spawn_failure(error.code);
|
|
2985
3784
|
queue.close();
|
|
@@ -3013,9 +3812,28 @@ function default_line_spawner(command, args, env) {
|
|
|
3013
3812
|
}
|
|
3014
3813
|
|
|
3015
3814
|
// src/mcp/mcp_attach.ts
|
|
3016
|
-
|
|
3815
|
+
var ATTACH_TIMEOUT_MS = 15e3;
|
|
3816
|
+
function with_timeout2(promise, label) {
|
|
3817
|
+
return new Promise((resolve, reject) => {
|
|
3818
|
+
const timer = setTimeout(() => {
|
|
3819
|
+
reject(new Error(`${label} timed out after ${ATTACH_TIMEOUT_MS}ms`));
|
|
3820
|
+
}, ATTACH_TIMEOUT_MS);
|
|
3821
|
+
promise.then(
|
|
3822
|
+
(value) => {
|
|
3823
|
+
clearTimeout(timer);
|
|
3824
|
+
resolve(value);
|
|
3825
|
+
},
|
|
3826
|
+
(error) => {
|
|
3827
|
+
clearTimeout(timer);
|
|
3828
|
+
reject(error);
|
|
3829
|
+
}
|
|
3830
|
+
);
|
|
3831
|
+
});
|
|
3832
|
+
}
|
|
3833
|
+
async function open_and_register(registry, name, enabled, session, command) {
|
|
3017
3834
|
try {
|
|
3018
|
-
|
|
3835
|
+
const listed = await with_timeout2(session.list_tools(), `mcp ${name} attach`);
|
|
3836
|
+
register_listed(registry, name, listed, enabled, session, command);
|
|
3019
3837
|
return session;
|
|
3020
3838
|
} catch (error) {
|
|
3021
3839
|
session.close();
|
|
@@ -3032,7 +3850,7 @@ async function attach_stdio(registry, name, entry, config, runtime) {
|
|
|
3032
3850
|
}
|
|
3033
3851
|
const spawn5 = runtime?.spawn ?? default_line_spawner;
|
|
3034
3852
|
const session = new McpSession(stdio_pipe(spawn5(planned.command, planned.args, entry.env)));
|
|
3035
|
-
return open_and_register(registry, name, config.tools_enabled, session);
|
|
3853
|
+
return open_and_register(registry, name, config.tools_enabled, session, planned.command);
|
|
3036
3854
|
}
|
|
3037
3855
|
async function attach_http(registry, name, url, config, runtime) {
|
|
3038
3856
|
const session = new McpSession(http_pipe(url, runtime?.fetch_fn ?? fetch));
|
|
@@ -3228,11 +4046,30 @@ function git_commit_tool() {
|
|
|
3228
4046
|
}
|
|
3229
4047
|
};
|
|
3230
4048
|
}
|
|
4049
|
+
var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
|
|
4050
|
+
"read_file",
|
|
4051
|
+
"list_dir",
|
|
4052
|
+
"grep_files",
|
|
4053
|
+
"docs_read",
|
|
4054
|
+
"docs_search",
|
|
4055
|
+
"env_get",
|
|
4056
|
+
"fetch_url",
|
|
4057
|
+
"http_request",
|
|
4058
|
+
"web_search",
|
|
4059
|
+
"process_list",
|
|
4060
|
+
"disk_usage"
|
|
4061
|
+
]);
|
|
3231
4062
|
function seed_state(ctx) {
|
|
3232
4063
|
ctx.state?.set("tests_ok", false);
|
|
3233
4064
|
ctx.state?.set("dirty", true);
|
|
3234
4065
|
ctx.state?.set("commits", 0);
|
|
3235
4066
|
}
|
|
4067
|
+
function mark_dirty_if_writer(tool_name, ctx) {
|
|
4068
|
+
if (READ_ONLY_TOOLS.has(tool_name) === true || tool_name === "run_tests" || tool_name === "git_commit") {
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
ctx.state?.set("dirty", true);
|
|
4072
|
+
}
|
|
3236
4073
|
function gatekeeper_hooks(allow_self_commit) {
|
|
3237
4074
|
return {
|
|
3238
4075
|
on_run_start: (_info, ctx) => {
|
|
@@ -3267,19 +4104,20 @@ function gatekeeper_hooks(allow_self_commit) {
|
|
|
3267
4104
|
return {};
|
|
3268
4105
|
},
|
|
3269
4106
|
after_tool_call: (info, ctx) => {
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
} else if (info.tool_name === "run_tests") {
|
|
4107
|
+
mark_dirty_if_writer(info.tool_name, ctx);
|
|
4108
|
+
if (info.tool_name === "run_tests") {
|
|
4109
|
+
if (info.ok !== true) {
|
|
4110
|
+
return;
|
|
4111
|
+
}
|
|
3276
4112
|
const filter = info.args["filter"];
|
|
3277
4113
|
const filtered = typeof filter === "string" && filter.length > 0;
|
|
3278
4114
|
if (filtered === false) {
|
|
3279
4115
|
ctx.state?.set("tests_ok", true);
|
|
3280
4116
|
ctx.state?.set("dirty", false);
|
|
3281
4117
|
}
|
|
3282
|
-
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4120
|
+
if (info.tool_name === "git_commit" && info.ok === true) {
|
|
3283
4121
|
ctx.state?.set("commits", state_count(ctx, "commits") + 1);
|
|
3284
4122
|
}
|
|
3285
4123
|
}
|
|
@@ -3315,6 +4153,7 @@ function sleep(ms, signal) {
|
|
|
3315
4153
|
// src/providers/failover.ts
|
|
3316
4154
|
var DEFAULT_BACKOFF_BASE_MS = 500;
|
|
3317
4155
|
var DEFAULT_BACKOFF_MAX_MS = 8e3;
|
|
4156
|
+
var MAX_RETRY_AFTER_MS = 3e4;
|
|
3318
4157
|
function classify_error(error) {
|
|
3319
4158
|
if (error instanceof ProviderError) {
|
|
3320
4159
|
return error.kind;
|
|
@@ -3347,7 +4186,7 @@ async function execute_retry_loop(fn, params) {
|
|
|
3347
4186
|
return { ok: false, error: make_abort_error(outcome.error) };
|
|
3348
4187
|
}
|
|
3349
4188
|
const kind = classify_error(outcome.error);
|
|
3350
|
-
if (is_retryable_kind(kind) === false || attempt >= params.max_attempts) {
|
|
4189
|
+
if (is_retryable_kind(kind) === false || attempt >= params.max_attempts || retry_after_too_long(outcome.error) === true) {
|
|
3351
4190
|
return outcome;
|
|
3352
4191
|
}
|
|
3353
4192
|
const delay_ms = delay_for_error(outcome.error, attempt);
|
|
@@ -3375,11 +4214,15 @@ function caller_aborted(signal) {
|
|
|
3375
4214
|
}
|
|
3376
4215
|
function delay_for_error(error, attempt) {
|
|
3377
4216
|
const backoff = compute_backoff_ms(attempt);
|
|
3378
|
-
if (error instanceof ProviderError && error.retry_after_ms !== void 0
|
|
3379
|
-
|
|
4217
|
+
if (error instanceof ProviderError && error.retry_after_ms !== void 0) {
|
|
4218
|
+
const capped = Math.min(error.retry_after_ms, MAX_RETRY_AFTER_MS);
|
|
4219
|
+
return capped > backoff ? capped : backoff;
|
|
3380
4220
|
}
|
|
3381
4221
|
return backoff;
|
|
3382
4222
|
}
|
|
4223
|
+
function retry_after_too_long(error) {
|
|
4224
|
+
return error instanceof ProviderError && error.retry_after_ms !== void 0 && error.retry_after_ms > MAX_RETRY_AFTER_MS;
|
|
4225
|
+
}
|
|
3383
4226
|
async function wait_out_delay(delay_ms, signal) {
|
|
3384
4227
|
if (delay_ms <= 0) {
|
|
3385
4228
|
return;
|
|
@@ -3417,11 +4260,13 @@ var DEFAULT_BASE_URL = "https://api.anthropic.com";
|
|
|
3417
4260
|
var ANTHROPIC_VERSION = "2023-06-01";
|
|
3418
4261
|
var DEFAULT_KEY_ENV = "ANTHROPIC_API_KEY";
|
|
3419
4262
|
var MAX_ERROR_BODY_CHARS = 500;
|
|
3420
|
-
var OVERFLOW_BODY_PATTERN = /context|token|
|
|
4263
|
+
var OVERFLOW_BODY_PATTERN = /context.?length|maximum context|prompt(?: is)? too (?:long|large)|token.?limit|context window|too many tokens|exceed.{0,30}context limit/i;
|
|
3421
4264
|
var OVERLOADED_STATUS = 529;
|
|
3422
4265
|
var UNPARSEABLE_ARGS_NOTE = "[unparseable tool arguments]";
|
|
4266
|
+
var TRUNCATED_TOOL_CALLS_NOTE = "[truncated tool call omitted]";
|
|
3423
4267
|
var EMPTY_TEXT_PLACEHOLDER = "(empty)";
|
|
3424
|
-
var DEFAULT_MAX_TOKENS =
|
|
4268
|
+
var DEFAULT_MAX_TOKENS = 16384;
|
|
4269
|
+
var ANTHROPIC_TEMP_MAX = 1;
|
|
3425
4270
|
var AnthropicProvider = class {
|
|
3426
4271
|
name;
|
|
3427
4272
|
model;
|
|
@@ -3445,7 +4290,7 @@ var AnthropicProvider = class {
|
|
|
3445
4290
|
build_endpoint(this.config),
|
|
3446
4291
|
build_request_init(
|
|
3447
4292
|
api_key,
|
|
3448
|
-
safe_stringify(build_request_body(this.config.model, messages, tools, options)),
|
|
4293
|
+
safe_stringify(build_request_body(this.config.model, messages, tools, options, this.config)),
|
|
3449
4294
|
build_abort_signal(options, this.config.timeout_ms)
|
|
3450
4295
|
),
|
|
3451
4296
|
this.config.name
|
|
@@ -3493,7 +4338,7 @@ function build_abort_signal(options, timeout_ms) {
|
|
|
3493
4338
|
}
|
|
3494
4339
|
return AbortSignal.any(signals);
|
|
3495
4340
|
}
|
|
3496
|
-
function build_request_body(model, messages, tools, options) {
|
|
4341
|
+
function build_request_body(model, messages, tools, options, config) {
|
|
3497
4342
|
const body = {
|
|
3498
4343
|
model,
|
|
3499
4344
|
max_tokens: options?.max_tokens ?? DEFAULT_MAX_TOKENS,
|
|
@@ -3506,8 +4351,8 @@ function build_request_body(model, messages, tools, options) {
|
|
|
3506
4351
|
if (tools.length > 0) {
|
|
3507
4352
|
body.tools = to_anthropic_tools(tools);
|
|
3508
4353
|
}
|
|
3509
|
-
if (options?.temperature !== void 0) {
|
|
3510
|
-
body.temperature = options.temperature;
|
|
4354
|
+
if (config.send_temperature === true && options?.temperature !== void 0) {
|
|
4355
|
+
body.temperature = Math.min(options.temperature, ANTHROPIC_TEMP_MAX);
|
|
3511
4356
|
}
|
|
3512
4357
|
return body;
|
|
3513
4358
|
}
|
|
@@ -3563,6 +4408,9 @@ function tool_message_to_block(message) {
|
|
|
3563
4408
|
return block;
|
|
3564
4409
|
}
|
|
3565
4410
|
function assistant_to_blocks(message) {
|
|
4411
|
+
if (message.provider_content !== void 0 && message.provider_content.length > 0) {
|
|
4412
|
+
return message.provider_content;
|
|
4413
|
+
}
|
|
3566
4414
|
const blocks = [];
|
|
3567
4415
|
if (message.content.length > 0) {
|
|
3568
4416
|
blocks.push({ type: "text", text: message.content });
|
|
@@ -3645,7 +4493,7 @@ function status_to_error_kind(status, body_text) {
|
|
|
3645
4493
|
if (status === 429 || status === OVERLOADED_STATUS || status >= 500) {
|
|
3646
4494
|
return "rate_limit";
|
|
3647
4495
|
}
|
|
3648
|
-
if (status === 400 && OVERFLOW_BODY_PATTERN.test(body_text) === true) {
|
|
4496
|
+
if (status === 413 || status === 400 && OVERFLOW_BODY_PATTERN.test(body_text) === true) {
|
|
3649
4497
|
return "overflow";
|
|
3650
4498
|
}
|
|
3651
4499
|
return "bad_request";
|
|
@@ -3662,28 +4510,57 @@ async function to_http_error(response, provider_name) {
|
|
|
3662
4510
|
});
|
|
3663
4511
|
}
|
|
3664
4512
|
function parse_chat_response(dto, config) {
|
|
4513
|
+
const finish_reason = map_stop_reason(dto.stop_reason);
|
|
3665
4514
|
return {
|
|
3666
|
-
message: parse_assistant_message(dto.content ?? []),
|
|
4515
|
+
message: parse_assistant_message(dto.content ?? [], finish_reason),
|
|
3667
4516
|
usage: parse_usage(dto.usage),
|
|
3668
|
-
finish_reason
|
|
4517
|
+
finish_reason,
|
|
3669
4518
|
model: dto.model ?? config.model,
|
|
3670
4519
|
provider_name: config.name
|
|
3671
4520
|
};
|
|
3672
4521
|
}
|
|
3673
|
-
function parse_assistant_message(blocks) {
|
|
4522
|
+
function parse_assistant_message(blocks, finish_reason) {
|
|
3674
4523
|
const text_parts = [];
|
|
3675
4524
|
const tool_calls = [];
|
|
4525
|
+
const provider_content = [];
|
|
4526
|
+
let has_thinking = false;
|
|
3676
4527
|
for (const block of blocks) {
|
|
4528
|
+
if (block.type === "thinking" || block.type === "redacted_thinking") {
|
|
4529
|
+
has_thinking = true;
|
|
4530
|
+
provider_content.push({ ...block });
|
|
4531
|
+
continue;
|
|
4532
|
+
}
|
|
3677
4533
|
if (block.type === "text") {
|
|
3678
4534
|
text_parts.push(block.text ?? "");
|
|
3679
|
-
|
|
3680
|
-
|
|
4535
|
+
const trimmed_text = (block.text ?? "").trim();
|
|
4536
|
+
if (trimmed_text.length > 0) {
|
|
4537
|
+
provider_content.push({ type: "text", text: block.text ?? "" });
|
|
4538
|
+
}
|
|
4539
|
+
continue;
|
|
3681
4540
|
}
|
|
4541
|
+
if (block.type === "tool_use") {
|
|
4542
|
+
const call = tool_use_to_call(block, text_parts);
|
|
4543
|
+
if (call !== void 0) {
|
|
4544
|
+
tool_calls.push(call);
|
|
4545
|
+
provider_content.push({
|
|
4546
|
+
type: "tool_use",
|
|
4547
|
+
id: call.id,
|
|
4548
|
+
name: call.name,
|
|
4549
|
+
input: call.args
|
|
4550
|
+
});
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
}
|
|
4554
|
+
const safe_calls = finish_reason === "length" ? [] : tool_calls;
|
|
4555
|
+
if (finish_reason === "length" && tool_calls.length > 0) {
|
|
4556
|
+
text_parts.push(TRUNCATED_TOOL_CALLS_NOTE);
|
|
3682
4557
|
}
|
|
4558
|
+
const safe_provider = finish_reason === "length" ? provider_content.filter((block) => block["type"] !== "tool_use") : provider_content;
|
|
3683
4559
|
return {
|
|
3684
4560
|
role: "assistant",
|
|
3685
4561
|
content: text_parts.filter((part) => part.length > 0).join("\n"),
|
|
3686
|
-
...
|
|
4562
|
+
...safe_calls.length > 0 ? { tool_calls: safe_calls } : {},
|
|
4563
|
+
...has_thinking === true ? { provider_content: safe_provider } : {}
|
|
3687
4564
|
};
|
|
3688
4565
|
}
|
|
3689
4566
|
function tool_use_to_call(block, text_parts) {
|
|
@@ -3692,7 +4569,7 @@ function tool_use_to_call(block, text_parts) {
|
|
|
3692
4569
|
return { id: block.id ?? "", name: block.name ?? "", args };
|
|
3693
4570
|
}
|
|
3694
4571
|
text_parts.push(UNPARSEABLE_ARGS_NOTE);
|
|
3695
|
-
return
|
|
4572
|
+
return void 0;
|
|
3696
4573
|
}
|
|
3697
4574
|
function parse_usage(dto) {
|
|
3698
4575
|
const prompt_tokens = dto?.input_tokens ?? 0;
|
|
@@ -3736,8 +4613,9 @@ function is_abort_like2(error) {
|
|
|
3736
4613
|
// src/providers/ollama.ts
|
|
3737
4614
|
var DEFAULT_BASE_URL2 = "http://localhost:11434";
|
|
3738
4615
|
var MAX_ERROR_BODY_CHARS2 = 500;
|
|
3739
|
-
var OVERFLOW_BODY_PATTERN2 = /context|token|
|
|
4616
|
+
var OVERFLOW_BODY_PATTERN2 = /context.?length|maximum context|prompt(?: is)? too (?:long|large)|token.?limit|context window|too many tokens|too long|exceed.{0,30}context limit/i;
|
|
3740
4617
|
var UNPARSEABLE_ARGS_NOTE2 = "[unparseable tool arguments]";
|
|
4618
|
+
var TRUNCATED_TOOL_CALLS_NOTE2 = "[truncated tool call omitted]";
|
|
3741
4619
|
var tool_call_counter = 0;
|
|
3742
4620
|
var OllamaProvider = class {
|
|
3743
4621
|
name;
|
|
@@ -3845,7 +4723,7 @@ function build_request_body2(config, messages, tools, options) {
|
|
|
3845
4723
|
if (wire_tools.length > 0) {
|
|
3846
4724
|
body.tools = wire_tools;
|
|
3847
4725
|
}
|
|
3848
|
-
const wire_options = build_wire_options(options);
|
|
4726
|
+
const wire_options = build_wire_options(config, options);
|
|
3849
4727
|
if (wire_options !== void 0) {
|
|
3850
4728
|
body.options = wire_options;
|
|
3851
4729
|
}
|
|
@@ -3857,7 +4735,7 @@ function build_request_body2(config, messages, tools, options) {
|
|
|
3857
4735
|
}
|
|
3858
4736
|
return body;
|
|
3859
4737
|
}
|
|
3860
|
-
function build_wire_options(options) {
|
|
4738
|
+
function build_wire_options(config, options) {
|
|
3861
4739
|
const wire_options = {};
|
|
3862
4740
|
if (options?.temperature !== void 0) {
|
|
3863
4741
|
wire_options.temperature = options.temperature;
|
|
@@ -3865,7 +4743,10 @@ function build_wire_options(options) {
|
|
|
3865
4743
|
if (options?.max_tokens !== void 0) {
|
|
3866
4744
|
wire_options.num_predict = options.max_tokens;
|
|
3867
4745
|
}
|
|
3868
|
-
|
|
4746
|
+
if (config.num_ctx !== void 0) {
|
|
4747
|
+
wire_options.num_ctx = config.num_ctx;
|
|
4748
|
+
}
|
|
4749
|
+
const has_any = wire_options.temperature !== void 0 || wire_options.num_predict !== void 0 || wire_options.num_ctx !== void 0;
|
|
3869
4750
|
return has_any === true ? wire_options : void 0;
|
|
3870
4751
|
}
|
|
3871
4752
|
function build_request_init2(api_key, body, signal) {
|
|
@@ -3931,7 +4812,7 @@ function status_to_error_kind2(status, body_text) {
|
|
|
3931
4812
|
if (status === 429 || status >= 500) {
|
|
3932
4813
|
return "rate_limit";
|
|
3933
4814
|
}
|
|
3934
|
-
if (status === 400 && OVERFLOW_BODY_PATTERN2.test(body_text) === true) {
|
|
4815
|
+
if (status === 413 || status === 400 && OVERFLOW_BODY_PATTERN2.test(body_text) === true) {
|
|
3935
4816
|
return "overflow";
|
|
3936
4817
|
}
|
|
3937
4818
|
return "bad_request";
|
|
@@ -3967,7 +4848,11 @@ function to_chat_response(dto, config) {
|
|
|
3967
4848
|
if (message_content.length > 0) {
|
|
3968
4849
|
content_parts.push(message_content);
|
|
3969
4850
|
}
|
|
3970
|
-
const
|
|
4851
|
+
const finish_reason = map_done_reason(dto.done_reason, (dto.message.tool_calls ?? []).length > 0);
|
|
4852
|
+
const parsed_calls = finish_reason === "length" ? [] : parse_tool_calls(dto.message.tool_calls ?? [], content_parts);
|
|
4853
|
+
if (finish_reason === "length" && (dto.message.tool_calls ?? []).length > 0) {
|
|
4854
|
+
content_parts.push(TRUNCATED_TOOL_CALLS_NOTE2);
|
|
4855
|
+
}
|
|
3971
4856
|
const has_tool_calls = parsed_calls.length > 0;
|
|
3972
4857
|
return {
|
|
3973
4858
|
message: {
|
|
@@ -3976,7 +4861,7 @@ function to_chat_response(dto, config) {
|
|
|
3976
4861
|
...has_tool_calls === true ? { tool_calls: parsed_calls } : {}
|
|
3977
4862
|
},
|
|
3978
4863
|
usage: parse_usage2(dto),
|
|
3979
|
-
finish_reason:
|
|
4864
|
+
finish_reason: has_tool_calls === true ? "tool_calls" : finish_reason === "tool_calls" ? "stop" : finish_reason,
|
|
3980
4865
|
model: dto.model ?? config.model,
|
|
3981
4866
|
provider_name: config.name
|
|
3982
4867
|
};
|
|
@@ -3985,30 +4870,34 @@ function next_tool_call_id() {
|
|
|
3985
4870
|
tool_call_counter += 1;
|
|
3986
4871
|
return `ollama_${Date.now().toString(36)}_${tool_call_counter}`;
|
|
3987
4872
|
}
|
|
3988
|
-
function normalize_tool_arguments(raw_arguments, content_parts) {
|
|
3989
|
-
if (is_record2(raw_arguments) === true) {
|
|
3990
|
-
return raw_arguments;
|
|
3991
|
-
}
|
|
3992
|
-
if (typeof raw_arguments === "string") {
|
|
3993
|
-
const parsed = safe_json_parse(raw_arguments);
|
|
3994
|
-
if (is_record2(parsed) === true) {
|
|
3995
|
-
return parsed;
|
|
3996
|
-
}
|
|
3997
|
-
}
|
|
3998
|
-
content_parts.push(UNPARSEABLE_ARGS_NOTE2);
|
|
3999
|
-
return {};
|
|
4000
|
-
}
|
|
4001
4873
|
function parse_tool_calls(raw_calls, content_parts) {
|
|
4002
4874
|
const tool_calls = [];
|
|
4003
4875
|
for (const raw_call of raw_calls) {
|
|
4004
4876
|
const name = raw_call.function?.name ?? "";
|
|
4877
|
+
const args = try_normalize_tool_arguments(raw_call.function?.arguments, content_parts);
|
|
4878
|
+
if (args === void 0) {
|
|
4879
|
+
continue;
|
|
4880
|
+
}
|
|
4005
4881
|
tool_calls.push({
|
|
4006
4882
|
id: next_tool_call_id(),
|
|
4007
4883
|
name,
|
|
4008
|
-
args
|
|
4884
|
+
args
|
|
4009
4885
|
});
|
|
4010
4886
|
}
|
|
4011
|
-
return tool_calls;
|
|
4887
|
+
return tool_calls;
|
|
4888
|
+
}
|
|
4889
|
+
function try_normalize_tool_arguments(raw_arguments, content_parts) {
|
|
4890
|
+
if (is_record2(raw_arguments) === true) {
|
|
4891
|
+
return raw_arguments;
|
|
4892
|
+
}
|
|
4893
|
+
if (typeof raw_arguments === "string") {
|
|
4894
|
+
const parsed = safe_json_parse(raw_arguments);
|
|
4895
|
+
if (is_record2(parsed) === true) {
|
|
4896
|
+
return parsed;
|
|
4897
|
+
}
|
|
4898
|
+
}
|
|
4899
|
+
content_parts.push(UNPARSEABLE_ARGS_NOTE2);
|
|
4900
|
+
return void 0;
|
|
4012
4901
|
}
|
|
4013
4902
|
function parse_usage2(dto) {
|
|
4014
4903
|
const prompt_tokens = dto.prompt_eval_count ?? 0;
|
|
@@ -4050,8 +4939,10 @@ var DEFAULT_BASE_URL3 = "https://api.openai.com/v1";
|
|
|
4050
4939
|
var WELL_KNOWN_HOST = "api.openai.com";
|
|
4051
4940
|
var WELL_KNOWN_KEY_ENV = "OPENAI_API_KEY";
|
|
4052
4941
|
var MAX_ERROR_BODY_CHARS3 = 500;
|
|
4053
|
-
var OVERFLOW_BODY_PATTERN3 = /context|token|
|
|
4942
|
+
var OVERFLOW_BODY_PATTERN3 = /context.?length|maximum context|prompt(?: is)? too (?:long|large)|token.?limit|context window|too many tokens|exceed.{0,30}context limit/i;
|
|
4054
4943
|
var UNPARSEABLE_ARGS_NOTE3 = "[unparseable tool arguments]";
|
|
4944
|
+
var TRUNCATED_TOOL_CALLS_NOTE3 = "[truncated tool call omitted]";
|
|
4945
|
+
var REASONING_MODEL_PATTERN = /^(o[1-9]|o[1-9]-|gpt-5)/i;
|
|
4055
4946
|
var OpenAICompatProvider = class {
|
|
4056
4947
|
name;
|
|
4057
4948
|
model;
|
|
@@ -4178,14 +5069,21 @@ function build_request_body3(model, messages, tools, options) {
|
|
|
4178
5069
|
if (wire_tools.length > 0) {
|
|
4179
5070
|
body.tools = wire_tools;
|
|
4180
5071
|
}
|
|
4181
|
-
if (options?.temperature !== void 0) {
|
|
5072
|
+
if (options?.temperature !== void 0 && is_reasoning_model(model) === false) {
|
|
4182
5073
|
body.temperature = options.temperature;
|
|
4183
5074
|
}
|
|
4184
5075
|
if (options?.max_tokens !== void 0) {
|
|
4185
|
-
|
|
5076
|
+
if (is_reasoning_model(model) === true) {
|
|
5077
|
+
body.max_completion_tokens = options.max_tokens;
|
|
5078
|
+
} else {
|
|
5079
|
+
body.max_tokens = options.max_tokens;
|
|
5080
|
+
}
|
|
4186
5081
|
}
|
|
4187
5082
|
return body;
|
|
4188
5083
|
}
|
|
5084
|
+
function is_reasoning_model(model) {
|
|
5085
|
+
return REASONING_MODEL_PATTERN.test(model) === true;
|
|
5086
|
+
}
|
|
4189
5087
|
function build_request_init3(api_key, body, signal) {
|
|
4190
5088
|
return {
|
|
4191
5089
|
method: "POST",
|
|
@@ -4249,7 +5147,7 @@ function status_to_error_kind3(status, body_text) {
|
|
|
4249
5147
|
if (status === 429 || status >= 500) {
|
|
4250
5148
|
return "rate_limit";
|
|
4251
5149
|
}
|
|
4252
|
-
if (status === 400 && OVERFLOW_BODY_PATTERN3.test(body_text) === true) {
|
|
5150
|
+
if (status === 413 || status === 400 && OVERFLOW_BODY_PATTERN3.test(body_text) === true) {
|
|
4253
5151
|
return "overflow";
|
|
4254
5152
|
}
|
|
4255
5153
|
return "bad_request";
|
|
@@ -4274,27 +5172,39 @@ function parse_chat_response2(dto, config) {
|
|
|
4274
5172
|
message: "provider returned a success response without choices"
|
|
4275
5173
|
});
|
|
4276
5174
|
}
|
|
5175
|
+
const finish_reason = map_finish_reason(choice.finish_reason);
|
|
4277
5176
|
return {
|
|
4278
|
-
message: parse_assistant_message2(choice.message),
|
|
5177
|
+
message: parse_assistant_message2(choice.message, finish_reason),
|
|
4279
5178
|
usage: parse_usage3(dto.usage),
|
|
4280
|
-
finish_reason
|
|
5179
|
+
finish_reason,
|
|
4281
5180
|
model: dto.model ?? config.model,
|
|
4282
5181
|
provider_name: config.name
|
|
4283
5182
|
};
|
|
4284
5183
|
}
|
|
4285
|
-
function parse_assistant_message2(dto) {
|
|
5184
|
+
function parse_assistant_message2(dto, finish_reason) {
|
|
4286
5185
|
const content_parts = [];
|
|
4287
5186
|
if (dto.content !== void 0 && dto.content !== null && dto.content.length > 0) {
|
|
4288
5187
|
content_parts.push(dto.content);
|
|
4289
5188
|
}
|
|
5189
|
+
const raw_calls = dto.tool_calls ?? [];
|
|
5190
|
+
if (finish_reason === "length" && raw_calls.length > 0) {
|
|
5191
|
+
content_parts.push(TRUNCATED_TOOL_CALLS_NOTE3);
|
|
5192
|
+
return {
|
|
5193
|
+
role: "assistant",
|
|
5194
|
+
content: content_parts.filter((part) => part.length > 0).join("\n")
|
|
5195
|
+
};
|
|
5196
|
+
}
|
|
4290
5197
|
const tool_calls = [];
|
|
4291
|
-
for (const raw_call of
|
|
5198
|
+
for (const raw_call of raw_calls) {
|
|
4292
5199
|
const raw_arguments = raw_call.function?.arguments ?? "";
|
|
4293
5200
|
const parsed_arguments = raw_arguments.length === 0 ? {} : safe_json_parse(raw_arguments);
|
|
4294
5201
|
if (is_record3(parsed_arguments) === true) {
|
|
4295
|
-
tool_calls.push({
|
|
5202
|
+
tool_calls.push({
|
|
5203
|
+
id: raw_call.id ?? "",
|
|
5204
|
+
name: raw_call.function?.name ?? "",
|
|
5205
|
+
args: parsed_arguments
|
|
5206
|
+
});
|
|
4296
5207
|
} else {
|
|
4297
|
-
tool_calls.push({ id: raw_call.id ?? "", name: raw_call.function?.name ?? "", args: {} });
|
|
4298
5208
|
content_parts.push(UNPARSEABLE_ARGS_NOTE3);
|
|
4299
5209
|
}
|
|
4300
5210
|
}
|
|
@@ -4407,9 +5317,10 @@ function build_provider(config) {
|
|
|
4407
5317
|
}
|
|
4408
5318
|
async function chat_with_failover(router, messages, tools, options) {
|
|
4409
5319
|
let last_error;
|
|
5320
|
+
let first_hard_error;
|
|
4410
5321
|
for (const provider of router.list()) {
|
|
4411
5322
|
if (options?.signal?.aborted === true) {
|
|
4412
|
-
throw last_error ?? make_router_abort_error();
|
|
5323
|
+
throw first_hard_error ?? last_error ?? make_router_abort_error();
|
|
4413
5324
|
}
|
|
4414
5325
|
const result = await attempt_provider(provider, messages, tools, options);
|
|
4415
5326
|
if (result.ok === true) {
|
|
@@ -4419,15 +5330,24 @@ async function chat_with_failover(router, messages, tools, options) {
|
|
|
4419
5330
|
throw result.error;
|
|
4420
5331
|
}
|
|
4421
5332
|
last_error = result.error;
|
|
5333
|
+
if (first_hard_error === void 0 && is_hard_error(result.error) === true) {
|
|
5334
|
+
first_hard_error = result.error;
|
|
5335
|
+
}
|
|
4422
5336
|
log_fail_over(result.error);
|
|
4423
5337
|
}
|
|
4424
|
-
throw last_error ?? new Error("no providers configured for failover");
|
|
5338
|
+
throw first_hard_error ?? last_error ?? new Error("no providers configured for failover");
|
|
5339
|
+
}
|
|
5340
|
+
function is_hard_error(error) {
|
|
5341
|
+
return error.kind === "auth" || error.kind === "overflow" || error.kind === "bad_request";
|
|
4425
5342
|
}
|
|
4426
5343
|
function log_fail_over(error) {
|
|
4427
5344
|
if (error.kind === "rate_limit" || error.kind === "network") {
|
|
4428
5345
|
return;
|
|
4429
5346
|
}
|
|
4430
|
-
|
|
5347
|
+
const status_part = error.status !== void 0 ? ` status=${error.status}` : "";
|
|
5348
|
+
logger.warn(
|
|
5349
|
+
`provider "${error.provider_name}" failed with ${error.kind}${status_part}: ${error.message}; failing over to next provider`
|
|
5350
|
+
);
|
|
4431
5351
|
}
|
|
4432
5352
|
function make_router_abort_error() {
|
|
4433
5353
|
return new ProviderError({
|
|
@@ -4475,246 +5395,6 @@ function to_provider_error(error, fallback_name) {
|
|
|
4475
5395
|
});
|
|
4476
5396
|
}
|
|
4477
5397
|
|
|
4478
|
-
// src/context/tokens.ts
|
|
4479
|
-
var TOOL_MESSAGE_OVERHEAD_TOKENS = 8;
|
|
4480
|
-
function estimate_text_tokens(text) {
|
|
4481
|
-
return Math.ceil(text.length / 4);
|
|
4482
|
-
}
|
|
4483
|
-
function estimate_message_tokens(message) {
|
|
4484
|
-
const content_tokens = estimate_text_tokens(message.content);
|
|
4485
|
-
if (message.role === "assistant" && message.tool_calls !== void 0) {
|
|
4486
|
-
return content_tokens + estimate_text_tokens(safe_stringify(message.tool_calls));
|
|
4487
|
-
}
|
|
4488
|
-
if (message.role === "tool") {
|
|
4489
|
-
return content_tokens + TOOL_MESSAGE_OVERHEAD_TOKENS;
|
|
4490
|
-
}
|
|
4491
|
-
return content_tokens;
|
|
4492
|
-
}
|
|
4493
|
-
function estimate_messages_tokens(messages) {
|
|
4494
|
-
return messages.reduce((total, message) => total + estimate_message_tokens(message), 0);
|
|
4495
|
-
}
|
|
4496
|
-
|
|
4497
|
-
// src/context/compressor.ts
|
|
4498
|
-
var COMPRESSION_SYSTEM_PROMPT = "You compress agent conversation history into terse factual summaries. Preserve: goals, decisions, file paths, commands run, errors, open questions. Output plain text only.";
|
|
4499
|
-
var MAX_TRANSCRIPT_CHARS = 24e3;
|
|
4500
|
-
function should_compress(messages, budget_tokens, threshold) {
|
|
4501
|
-
return estimate_messages_tokens(messages) >= budget_tokens * threshold;
|
|
4502
|
-
}
|
|
4503
|
-
function format_history_line(message) {
|
|
4504
|
-
const rendered_calls = message.role === "assistant" && message.tool_calls !== void 0 ? ` tool_calls=${safe_stringify(message.tool_calls)}` : "";
|
|
4505
|
-
return `[${message.role}] ${message.content}${rendered_calls}`;
|
|
4506
|
-
}
|
|
4507
|
-
function build_summary_request(older, model_hint) {
|
|
4508
|
-
const transcript = older.map((message) => format_history_line(message)).join("\n");
|
|
4509
|
-
const hint = model_hint === void 0 ? "" : `
|
|
4510
|
-
(Continuing agent run as model: ${model_hint})`;
|
|
4511
|
-
return {
|
|
4512
|
-
role: "user",
|
|
4513
|
-
content: "Summarize the following earlier conversation so the agent can continue the task from the summary alone.\n\n" + truncate_text(transcript, MAX_TRANSCRIPT_CHARS)
|
|
4514
|
-
};
|
|
4515
|
-
}
|
|
4516
|
-
function log_compression_failure(error) {
|
|
4517
|
-
if (error instanceof ProviderError) {
|
|
4518
|
-
logger.warn(`context compression failed kind=${error.kind} provider=${error.provider_name}`, error);
|
|
4519
|
-
return;
|
|
4520
|
-
}
|
|
4521
|
-
logger.warn("context compression failed", error);
|
|
4522
|
-
}
|
|
4523
|
-
function split_keep_recent(non_system, keep_recent) {
|
|
4524
|
-
let cut = Math.max(0, non_system.length - keep_recent);
|
|
4525
|
-
while (cut > 0 && non_system[cut]?.role === "tool") {
|
|
4526
|
-
cut -= 1;
|
|
4527
|
-
}
|
|
4528
|
-
return { recent: non_system.slice(cut), older: non_system.slice(0, cut) };
|
|
4529
|
-
}
|
|
4530
|
-
async function compress_messages(deps, messages, params) {
|
|
4531
|
-
const system_messages = messages.filter((message) => message.role === "system");
|
|
4532
|
-
const non_system = messages.filter((message) => message.role !== "system");
|
|
4533
|
-
const keep_recent = Math.max(0, params.keep_recent);
|
|
4534
|
-
const { recent, older } = split_keep_recent(non_system, keep_recent);
|
|
4535
|
-
if (older.length === 0) {
|
|
4536
|
-
return { messages: [...messages], summary_chars: 0 };
|
|
4537
|
-
}
|
|
4538
|
-
try {
|
|
4539
|
-
const result = await deps.chat(
|
|
4540
|
-
[{ role: "system", content: COMPRESSION_SYSTEM_PROMPT }, build_summary_request(older, params.model_hint)],
|
|
4541
|
-
[],
|
|
4542
|
-
{ signal: params.signal }
|
|
4543
|
-
);
|
|
4544
|
-
const summary = result.message.content;
|
|
4545
|
-
const summary_message = {
|
|
4546
|
-
role: "user",
|
|
4547
|
-
content: `[context summary of earlier turns]
|
|
4548
|
-
${summary}
|
|
4549
|
-
[end summary]`
|
|
4550
|
-
};
|
|
4551
|
-
return { messages: [...system_messages, summary_message, ...recent], summary_chars: summary.length };
|
|
4552
|
-
} catch (error) {
|
|
4553
|
-
log_compression_failure(error);
|
|
4554
|
-
return { messages: [...messages], summary_chars: 0 };
|
|
4555
|
-
}
|
|
4556
|
-
}
|
|
4557
|
-
|
|
4558
|
-
// src/agent/loop.ts
|
|
4559
|
-
var DEFAULT_COMPRESS_THRESHOLD = 0.8;
|
|
4560
|
-
var KEEP_RECENT_TURNS = 8;
|
|
4561
|
-
function turn_range(max_turns) {
|
|
4562
|
-
return Array.from({ length: Math.max(0, max_turns) }, (_unused, index) => index + 1);
|
|
4563
|
-
}
|
|
4564
|
-
function seed_system_prompt(messages, system_prompt) {
|
|
4565
|
-
const history = [...messages];
|
|
4566
|
-
if (system_prompt === void 0) {
|
|
4567
|
-
return history;
|
|
4568
|
-
}
|
|
4569
|
-
const system_index = history.findIndex((message) => message.role === "system");
|
|
4570
|
-
if (system_index === -1) {
|
|
4571
|
-
const seeded = { role: "system", content: system_prompt };
|
|
4572
|
-
return [seeded, ...history];
|
|
4573
|
-
}
|
|
4574
|
-
const existing = history[system_index];
|
|
4575
|
-
if (existing !== void 0 && existing.content === system_prompt) {
|
|
4576
|
-
return history;
|
|
4577
|
-
}
|
|
4578
|
-
const replaced = { role: "system", content: system_prompt };
|
|
4579
|
-
return [...history.slice(0, system_index), replaced, ...history.slice(system_index + 1)];
|
|
4580
|
-
}
|
|
4581
|
-
function format_tool_result_content(result) {
|
|
4582
|
-
if (result.error !== void 0) {
|
|
4583
|
-
return JSON.stringify({ ok: false, output: result.output, error: result.error });
|
|
4584
|
-
}
|
|
4585
|
-
return result.output;
|
|
4586
|
-
}
|
|
4587
|
-
function tool_message_from_result(call, result) {
|
|
4588
|
-
const tool_message = {
|
|
4589
|
-
role: "tool",
|
|
4590
|
-
tool_call_id: call.id,
|
|
4591
|
-
name: call.name,
|
|
4592
|
-
content: format_tool_result_content(result)
|
|
4593
|
-
};
|
|
4594
|
-
if (result.ok !== true) {
|
|
4595
|
-
tool_message.is_error = true;
|
|
4596
|
-
}
|
|
4597
|
-
return tool_message;
|
|
4598
|
-
}
|
|
4599
|
-
async function run_tool_calls(deps, history, turn, calls, emitter, signal) {
|
|
4600
|
-
for (const call of calls) {
|
|
4601
|
-
if (signal_aborted(signal) === true) {
|
|
4602
|
-
const cancelled = { ok: false, output: "", error: "cancelled" };
|
|
4603
|
-
history.push(tool_message_from_result(call, cancelled));
|
|
4604
|
-
emitter?.emit({ type: "tool_call_end", turn, call, result: cancelled, cancelled: true });
|
|
4605
|
-
continue;
|
|
4606
|
-
}
|
|
4607
|
-
emitter?.emit({ type: "tool_call_start", turn, call });
|
|
4608
|
-
const result = await deps.tools.execute(call.name, call.args, deps.tool_context);
|
|
4609
|
-
history.push(tool_message_from_result(call, result));
|
|
4610
|
-
emitter?.emit({ type: "tool_call_end", turn, call, result });
|
|
4611
|
-
}
|
|
4612
|
-
return signal_aborted(signal) === true ? "aborted" : "continued";
|
|
4613
|
-
}
|
|
4614
|
-
async function call_chat(deps, history, params, emitter) {
|
|
4615
|
-
try {
|
|
4616
|
-
return await deps.chat(history, deps.definitions(), {
|
|
4617
|
-
temperature: params.temperature,
|
|
4618
|
-
max_tokens: params.max_tokens,
|
|
4619
|
-
signal: params.signal
|
|
4620
|
-
});
|
|
4621
|
-
} catch (error) {
|
|
4622
|
-
if (signal_aborted(params.signal) === true) {
|
|
4623
|
-
throw error;
|
|
4624
|
-
}
|
|
4625
|
-
if (error instanceof ProviderError) {
|
|
4626
|
-
logger.error(`provider error kind=${error.kind} provider=${error.provider_name}`, error);
|
|
4627
|
-
} else {
|
|
4628
|
-
logger.error("agent chat call failed", error);
|
|
4629
|
-
}
|
|
4630
|
-
emitter?.emit({ type: "error", error });
|
|
4631
|
-
throw error;
|
|
4632
|
-
}
|
|
4633
|
-
}
|
|
4634
|
-
async function compress_if_needed(deps, history, params, emitter) {
|
|
4635
|
-
const budget_tokens = params.context_budget_tokens;
|
|
4636
|
-
if (budget_tokens === void 0) {
|
|
4637
|
-
return;
|
|
4638
|
-
}
|
|
4639
|
-
const threshold = params.compress_threshold ?? DEFAULT_COMPRESS_THRESHOLD;
|
|
4640
|
-
if (!should_compress(history, budget_tokens, threshold)) {
|
|
4641
|
-
return;
|
|
4642
|
-
}
|
|
4643
|
-
const non_system_count = history.filter((message) => message.role !== "system").length;
|
|
4644
|
-
if (non_system_count <= KEEP_RECENT_TURNS) {
|
|
4645
|
-
return;
|
|
4646
|
-
}
|
|
4647
|
-
emitter?.emit({ type: "compress_start", estimated_tokens: estimate_messages_tokens(history) });
|
|
4648
|
-
const outcome = await compress_messages(
|
|
4649
|
-
{ chat: deps.chat },
|
|
4650
|
-
history,
|
|
4651
|
-
{ budget_tokens, keep_recent: KEEP_RECENT_TURNS, signal: params.signal }
|
|
4652
|
-
);
|
|
4653
|
-
history.length = 0;
|
|
4654
|
-
for (const message of outcome.messages) {
|
|
4655
|
-
history.push(message);
|
|
4656
|
-
}
|
|
4657
|
-
emitter?.emit({ type: "compress_end", summary_chars: outcome.summary_chars });
|
|
4658
|
-
}
|
|
4659
|
-
function find_last_assistant(messages) {
|
|
4660
|
-
return [...messages].reverse().find((message) => message.role === "assistant");
|
|
4661
|
-
}
|
|
4662
|
-
function signal_aborted(signal) {
|
|
4663
|
-
return signal?.aborted === true;
|
|
4664
|
-
}
|
|
4665
|
-
function aborted_outcome(history, turns_used, emitter) {
|
|
4666
|
-
emitter?.emit({ type: "error", error: new DOMException("agent loop aborted", "AbortError") });
|
|
4667
|
-
return {
|
|
4668
|
-
messages: history,
|
|
4669
|
-
final: find_last_assistant(history),
|
|
4670
|
-
result: void 0,
|
|
4671
|
-
turns_used,
|
|
4672
|
-
stopped_reason: "aborted"
|
|
4673
|
-
};
|
|
4674
|
-
}
|
|
4675
|
-
async function run_conversation(deps, messages, params) {
|
|
4676
|
-
const history = seed_system_prompt(messages, params.system_prompt);
|
|
4677
|
-
const emitter = deps.emitter;
|
|
4678
|
-
for (const turn of turn_range(params.max_turns)) {
|
|
4679
|
-
if (signal_aborted(params.signal) === true) {
|
|
4680
|
-
return aborted_outcome(history, turn - 1, emitter);
|
|
4681
|
-
}
|
|
4682
|
-
emitter?.emit({ type: "turn_start", turn });
|
|
4683
|
-
await compress_if_needed(deps, history, params, emitter);
|
|
4684
|
-
emitter?.emit({ type: "llm_start", turn });
|
|
4685
|
-
let result;
|
|
4686
|
-
try {
|
|
4687
|
-
result = await call_chat(deps, history, params, emitter);
|
|
4688
|
-
} catch (error) {
|
|
4689
|
-
if (signal_aborted(params.signal) === true) {
|
|
4690
|
-
return aborted_outcome(history, turn - 1, emitter);
|
|
4691
|
-
}
|
|
4692
|
-
throw error;
|
|
4693
|
-
}
|
|
4694
|
-
emitter?.emit({ type: "llm_end", turn, result });
|
|
4695
|
-
history.push(result.message);
|
|
4696
|
-
const calls = result.message.tool_calls ?? [];
|
|
4697
|
-
if (calls.length === 0) {
|
|
4698
|
-
emitter?.emit({ type: "final", message: result.message, result });
|
|
4699
|
-
emitter?.emit({ type: "turn_end", turn });
|
|
4700
|
-
return { messages: history, final: result.message, result, turns_used: turn, stopped_reason: "final" };
|
|
4701
|
-
}
|
|
4702
|
-
const tool_status = await run_tool_calls(deps, history, turn, calls, emitter, params.signal);
|
|
4703
|
-
if (tool_status === "aborted") {
|
|
4704
|
-
return aborted_outcome(history, turn, emitter);
|
|
4705
|
-
}
|
|
4706
|
-
}
|
|
4707
|
-
emitter?.emit({ type: "budget_exhausted", turns_used: params.max_turns });
|
|
4708
|
-
emitter?.emit({ type: "turn_end", turn: params.max_turns });
|
|
4709
|
-
return {
|
|
4710
|
-
messages: history,
|
|
4711
|
-
final: find_last_assistant(history),
|
|
4712
|
-
result: void 0,
|
|
4713
|
-
turns_used: params.max_turns,
|
|
4714
|
-
stopped_reason: "budget"
|
|
4715
|
-
};
|
|
4716
|
-
}
|
|
4717
|
-
|
|
4718
5398
|
// src/session/recorder.ts
|
|
4719
5399
|
var seeded_handles = /* @__PURE__ */ new WeakSet();
|
|
4720
5400
|
function record_ts() {
|
|
@@ -4799,12 +5479,19 @@ function filter_registry(base, enabled) {
|
|
|
4799
5479
|
}
|
|
4800
5480
|
return filtered;
|
|
4801
5481
|
}
|
|
5482
|
+
function add_usage(total, usage) {
|
|
5483
|
+
total.prompt_tokens += usage.prompt_tokens;
|
|
5484
|
+
total.completion_tokens += usage.completion_tokens;
|
|
5485
|
+
total.total_tokens += usage.total_tokens;
|
|
5486
|
+
}
|
|
4802
5487
|
function collect_usage(total) {
|
|
4803
5488
|
return (event) => {
|
|
4804
5489
|
if (event.type === "llm_end") {
|
|
4805
|
-
total
|
|
4806
|
-
|
|
4807
|
-
|
|
5490
|
+
add_usage(total, event.result.usage);
|
|
5491
|
+
return;
|
|
5492
|
+
}
|
|
5493
|
+
if (event.type === "compress_end" && event.usage !== void 0) {
|
|
5494
|
+
add_usage(total, event.usage);
|
|
4808
5495
|
}
|
|
4809
5496
|
};
|
|
4810
5497
|
}
|
|
@@ -4866,6 +5553,13 @@ var Agent = class {
|
|
|
4866
5553
|
}
|
|
4867
5554
|
async run(options) {
|
|
4868
5555
|
await this.attach_mcp_once();
|
|
5556
|
+
const body = () => this.run_body(options);
|
|
5557
|
+
if (this.hook_runner !== void 0) {
|
|
5558
|
+
return this.hook_runner.run_scope(body);
|
|
5559
|
+
}
|
|
5560
|
+
return body();
|
|
5561
|
+
}
|
|
5562
|
+
async run_body(options) {
|
|
4869
5563
|
const usage_total = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
4870
5564
|
const run_events = new AgentEmitter();
|
|
4871
5565
|
const stop_forwarding = run_events.on((event) => this.events.emit(event));
|
|
@@ -5009,6 +5703,7 @@ export {
|
|
|
5009
5703
|
plugin_errors_summary,
|
|
5010
5704
|
sleep,
|
|
5011
5705
|
ProviderError,
|
|
5706
|
+
run_conversation,
|
|
5012
5707
|
gateway_tools_enabled,
|
|
5013
5708
|
check_gateway_sender,
|
|
5014
5709
|
parse_agent_config,
|
|
@@ -5018,4 +5713,4 @@ export {
|
|
|
5018
5713
|
create_agent_with_plugins,
|
|
5019
5714
|
run_agent
|
|
5020
5715
|
};
|
|
5021
|
-
//# sourceMappingURL=chunk-
|
|
5716
|
+
//# sourceMappingURL=chunk-KOEZQUIX.js.map
|