@moikapy/lich 0.7.1 → 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 +63 -0
- package/README.md +38 -19
- package/dist/{chunk-CVX7LZWC.js → chunk-4N3S6ACI.js} +2 -2
- package/dist/{chunk-WNFBIX4E.js → chunk-KOEZQUIX.js} +1312 -600
- package/dist/chunk-KOEZQUIX.js.map +1 -0
- package/dist/{chunk-PZNYVGD4.js → chunk-P2YEHNM4.js} +2 -2
- package/dist/chunk-P2YEHNM4.js.map +1 -0
- package/dist/chunk-PL6MKRKE.js +75 -0
- package/dist/chunk-PL6MKRKE.js.map +1 -0
- package/dist/chunk-TYRKVBWQ.js +104 -0
- package/dist/chunk-TYRKVBWQ.js.map +1 -0
- package/dist/cli.d.ts +19 -1
- package/dist/cli.js +125 -47
- package/dist/cli.js.map +1 -1
- package/dist/{gateway-44QTIJTJ.js → gateway-OSB7254F.js} +209 -46
- package/dist/gateway-OSB7254F.js.map +1 -0
- package/dist/index.d.ts +122 -62
- package/dist/index.js +12 -4
- package/dist/resolve-H22TOVB5.js +7 -0
- package/dist/resolve-H22TOVB5.js.map +1 -0
- package/dist/store-YYLEB7JG.js +9 -0
- package/dist/store-YYLEB7JG.js.map +1 -0
- package/dist/{tui-MEJGEILU.js → tui-XXIU4W7K.js} +216 -25
- package/dist/tui-XXIU4W7K.js.map +1 -0
- package/docs/architecture/agent-loop.md +28 -9
- package/docs/architecture/overview.md +10 -9
- 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 +17 -8
- package/docs/user-guide/games.md +5 -4
- package/docs/user-guide/library.md +6 -3
- package/docs/user-guide/redot.md +2 -2
- package/docs/user-guide/tui.md +3 -2
- package/package.json +1 -1
- package/dist/chunk-PZNYVGD4.js.map +0 -1
- package/dist/chunk-WNFBIX4E.js.map +0 -1
- package/dist/gateway-44QTIJTJ.js.map +0 -1
- package/dist/tui-MEJGEILU.js.map +0 -1
- /package/dist/{chunk-CVX7LZWC.js.map → chunk-4N3S6ACI.js.map} +0 -0
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
import {
|
|
2
|
+
open_session,
|
|
3
|
+
safe_json_parse,
|
|
4
|
+
safe_stringify,
|
|
5
|
+
truncate_text
|
|
6
|
+
} from "./chunk-TYRKVBWQ.js";
|
|
7
|
+
|
|
8
|
+
// src/util/log.ts
|
|
9
|
+
var level_order = {
|
|
10
|
+
debug: 10,
|
|
11
|
+
info: 20,
|
|
12
|
+
warn: 30,
|
|
13
|
+
error: 40
|
|
14
|
+
};
|
|
15
|
+
var current_level = "info";
|
|
16
|
+
function set_log_level(level) {
|
|
17
|
+
current_level = level;
|
|
18
|
+
}
|
|
19
|
+
function log(level, message, data) {
|
|
20
|
+
if (level_order[level] < level_order[current_level]) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const line = `[lich:${level}] ${message}`;
|
|
24
|
+
if (data === void 0) {
|
|
25
|
+
console.error(line);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
console.error(line, data);
|
|
29
|
+
}
|
|
30
|
+
var logger = {
|
|
31
|
+
debug: (message, data) => log("debug", message, data),
|
|
32
|
+
info: (message, data) => log("info", message, data),
|
|
33
|
+
warn: (message, data) => log("warn", message, data),
|
|
34
|
+
error: (message, data) => log("error", message, data)
|
|
35
|
+
};
|
|
36
|
+
|
|
1
37
|
// src/tools/builtin/disk_usage.ts
|
|
2
38
|
import { execFile } from "child_process";
|
|
3
39
|
import { readdir } from "fs/promises";
|
|
@@ -6,32 +42,6 @@ import path2 from "path";
|
|
|
6
42
|
// src/tools/guard.ts
|
|
7
43
|
import fs from "fs";
|
|
8
44
|
import path from "path";
|
|
9
|
-
|
|
10
|
-
// src/util/json.ts
|
|
11
|
-
function safe_json_parse(raw) {
|
|
12
|
-
try {
|
|
13
|
-
return JSON.parse(raw);
|
|
14
|
-
} catch {
|
|
15
|
-
return void 0;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
function safe_stringify(value, space) {
|
|
19
|
-
try {
|
|
20
|
-
return JSON.stringify(value, null, space) ?? String(value);
|
|
21
|
-
} catch {
|
|
22
|
-
return String(value);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function truncate_text(text, max_chars) {
|
|
26
|
-
if (text.length <= max_chars) {
|
|
27
|
-
return text;
|
|
28
|
-
}
|
|
29
|
-
const omitted = text.length - max_chars;
|
|
30
|
-
return `${text.slice(0, max_chars)}
|
|
31
|
-
[... truncated, ${omitted} chars omitted ...]`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// src/tools/guard.ts
|
|
35
45
|
var DEFAULT_MAX_OUTPUT_CHARS = 2e4;
|
|
36
46
|
var DEFAULT_TOOL_TIMEOUT_MS = 3e4;
|
|
37
47
|
function is_inside(base, candidate) {
|
|
@@ -176,6 +186,38 @@ async function capture_errors(run) {
|
|
|
176
186
|
}
|
|
177
187
|
}
|
|
178
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
|
+
|
|
179
221
|
// src/tools/url_guard.ts
|
|
180
222
|
import dns from "dns/promises";
|
|
181
223
|
import http from "http";
|
|
@@ -408,6 +450,7 @@ var DEFAULT_MAX_CHARS = 2e4;
|
|
|
408
450
|
var MAX_MAX_CHARS = 1e5;
|
|
409
451
|
var DEFAULT_TIMEOUT_MS = 2e4;
|
|
410
452
|
var MAX_TIMEOUT_MS = 6e4;
|
|
453
|
+
var MAX_BODY_BYTES = MAX_MAX_CHARS * 4;
|
|
411
454
|
var USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36";
|
|
412
455
|
var parameters = {
|
|
413
456
|
type: "object",
|
|
@@ -445,16 +488,17 @@ async function run_fetch_url(args, external) {
|
|
|
445
488
|
if (content_type.startsWith("image/") === true || content_type.startsWith("application/octet-stream") === true) {
|
|
446
489
|
return { ok: false, output: "", error: `unsupported_content_type: ${content_type}` };
|
|
447
490
|
}
|
|
448
|
-
const
|
|
491
|
+
const byte_budget = Math.min(MAX_BODY_BYTES, max_chars * 4);
|
|
492
|
+
const clamped = await read_clamped_text(response, byte_budget);
|
|
449
493
|
const marker = content_type.toLowerCase().includes("text/html") === true ? "[html content]\n" : "";
|
|
450
|
-
const body = clamp_output(`${marker}${text}`, max_chars);
|
|
451
|
-
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)}
|
|
452
496
|
${body}` };
|
|
453
497
|
}
|
|
454
|
-
function header_line(response,
|
|
498
|
+
function header_line(response, bytes_read) {
|
|
455
499
|
const content_type = response.headers.get("content-type") ?? "unknown";
|
|
456
500
|
const declared = Number.parseInt(response.headers.get("content-length") ?? "", 10);
|
|
457
|
-
const bytes = Number.isFinite(declared) === true ? declared :
|
|
501
|
+
const bytes = Number.isFinite(declared) === true ? declared : bytes_read;
|
|
458
502
|
return `# ${response.status} ${content_type} (${bytes} bytes)`;
|
|
459
503
|
}
|
|
460
504
|
var fetch_url_tool = {
|
|
@@ -1013,17 +1057,21 @@ var env_get_tool = {
|
|
|
1013
1057
|
// src/tools/builtin/grep_files.ts
|
|
1014
1058
|
import { readFile as readFile2, readdir as readdir2, stat } from "fs/promises";
|
|
1015
1059
|
import path5 from "path";
|
|
1060
|
+
import vm from "vm";
|
|
1016
1061
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", ".lich", ".cursor"]);
|
|
1017
1062
|
var MAX_FILE_BYTES = 1e6;
|
|
1018
1063
|
var SNIFF_BYTES = 1e3;
|
|
1019
1064
|
var DEFAULT_MAX_RESULTS2 = 200;
|
|
1065
|
+
var MAX_MAX_RESULTS = 2e3;
|
|
1066
|
+
var MAX_LINE_CHARS = 4e3;
|
|
1067
|
+
var REGEX_TIMEOUT_MS = 50;
|
|
1020
1068
|
var parameters7 = {
|
|
1021
1069
|
type: "object",
|
|
1022
1070
|
properties: {
|
|
1023
1071
|
pattern: { type: "string", description: "Regular expression source to match against each line" },
|
|
1024
1072
|
path: { type: "string", description: "Directory or file to search, relative to the working directory (default .)" },
|
|
1025
1073
|
glob: { type: "string", description: "Simple filename filter like *.ts (suffix match only)" },
|
|
1026
|
-
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)" }
|
|
1027
1075
|
},
|
|
1028
1076
|
required: ["pattern"],
|
|
1029
1077
|
additionalProperties: false
|
|
@@ -1060,12 +1108,46 @@ async function read_if_text(file_path, size) {
|
|
|
1060
1108
|
return void 0;
|
|
1061
1109
|
}
|
|
1062
1110
|
}
|
|
1063
|
-
|
|
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) {
|
|
1064
1142
|
const hits = [];
|
|
1065
1143
|
for (let index = 0; index < lines.length; index += 1) {
|
|
1066
1144
|
const line = lines[index];
|
|
1067
|
-
if (line
|
|
1068
|
-
|
|
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() });
|
|
1069
1151
|
}
|
|
1070
1152
|
}
|
|
1071
1153
|
return hits;
|
|
@@ -1073,7 +1155,6 @@ function match_lines(lines, regex) {
|
|
|
1073
1155
|
async function safe_readdir(dir) {
|
|
1074
1156
|
try {
|
|
1075
1157
|
return await readdir2(dir, { withFileTypes: true });
|
|
1076
|
-
abort_marker: ;
|
|
1077
1158
|
} catch {
|
|
1078
1159
|
return void 0;
|
|
1079
1160
|
}
|
|
@@ -1112,7 +1193,13 @@ function guard_grep_target(work_dir, absolute) {
|
|
|
1112
1193
|
assert_file_tool_access(work_dir, safe, "read");
|
|
1113
1194
|
return safe;
|
|
1114
1195
|
}
|
|
1115
|
-
|
|
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);
|
|
1116
1203
|
let safe;
|
|
1117
1204
|
try {
|
|
1118
1205
|
safe = guard_grep_target(work_dir, frame.dir);
|
|
@@ -1125,7 +1212,7 @@ async function search_file(frame, work_dir, relative_root, regex, collected, max
|
|
|
1125
1212
|
return false;
|
|
1126
1213
|
}
|
|
1127
1214
|
const relative = path5.relative(relative_root, safe);
|
|
1128
|
-
for (const hit of match_lines(lines, regex)) {
|
|
1215
|
+
for (const hit of match_lines(lines, regex, pattern)) {
|
|
1129
1216
|
collected.push(`${relative}:${hit.line_no}: ${hit.text}`);
|
|
1130
1217
|
if (collected.length >= max_results) {
|
|
1131
1218
|
return true;
|
|
@@ -1133,17 +1220,18 @@ async function search_file(frame, work_dir, relative_root, regex, collected, max
|
|
|
1133
1220
|
}
|
|
1134
1221
|
return false;
|
|
1135
1222
|
}
|
|
1136
|
-
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) {
|
|
1137
1224
|
const collected = [];
|
|
1138
1225
|
const stack = [{ dir: root, name: root }];
|
|
1139
1226
|
while (stack.length > 0 && collected.length < max_results) {
|
|
1227
|
+
throw_if_aborted(signal);
|
|
1140
1228
|
const frame = stack.pop();
|
|
1141
1229
|
if (frame === void 0) {
|
|
1142
1230
|
break;
|
|
1143
1231
|
}
|
|
1144
1232
|
const found = await scan_dir(frame, matcher);
|
|
1145
1233
|
for (const file of found.files) {
|
|
1146
|
-
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);
|
|
1147
1235
|
if (hit_cap === true) {
|
|
1148
1236
|
break;
|
|
1149
1237
|
}
|
|
@@ -1163,27 +1251,32 @@ function finalize_output(matches, max_results) {
|
|
|
1163
1251
|
}
|
|
1164
1252
|
return matches.join("\n");
|
|
1165
1253
|
}
|
|
1166
|
-
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) {
|
|
1167
1255
|
return search_file(
|
|
1168
1256
|
{ dir: file_path, name: path5.basename(file_path) },
|
|
1169
1257
|
work_dir,
|
|
1170
1258
|
path5.dirname(file_path),
|
|
1171
1259
|
regex,
|
|
1260
|
+
pattern,
|
|
1172
1261
|
collected,
|
|
1173
|
-
max_results
|
|
1262
|
+
max_results,
|
|
1263
|
+
signal
|
|
1174
1264
|
);
|
|
1175
1265
|
}
|
|
1176
|
-
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) {
|
|
1177
1267
|
const collected = [];
|
|
1178
1268
|
if (matcher(path5.basename(root)) === true) {
|
|
1179
|
-
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);
|
|
1180
1270
|
}
|
|
1181
1271
|
return collected;
|
|
1182
1272
|
}
|
|
1183
|
-
|
|
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) {
|
|
1184
1277
|
const pattern = require_string_arg(args, "pattern");
|
|
1185
1278
|
const target = optional_string_arg(args, "path", ".");
|
|
1186
|
-
const max_results =
|
|
1279
|
+
const max_results = clamp_max_results(optional_number_arg(args, "max_results", DEFAULT_MAX_RESULTS2));
|
|
1187
1280
|
const glob = optional_string_arg(args, "glob", "");
|
|
1188
1281
|
let regex;
|
|
1189
1282
|
try {
|
|
@@ -1191,12 +1284,15 @@ async function run_grep(args, work_dir) {
|
|
|
1191
1284
|
} catch {
|
|
1192
1285
|
throw new Error(`invalid_regex: ${pattern}`);
|
|
1193
1286
|
}
|
|
1287
|
+
if (has_nested_quantifiers(pattern) === true) {
|
|
1288
|
+
throw new Error(`unsafe_regex: nested quantifiers are not supported (${pattern})`);
|
|
1289
|
+
}
|
|
1194
1290
|
const matcher = glob.length > 0 ? glob_matcher(glob) : () => true;
|
|
1195
1291
|
const root = resolve_safe_path(work_dir, target);
|
|
1196
1292
|
assert_file_tool_access(work_dir, root, "read");
|
|
1197
1293
|
const root_stat = await stat(root);
|
|
1198
1294
|
const cap = max_results + 1;
|
|
1199
|
-
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);
|
|
1200
1296
|
return finalize_output(matches, max_results);
|
|
1201
1297
|
}
|
|
1202
1298
|
var grep_files_tool = {
|
|
@@ -1204,7 +1300,7 @@ var grep_files_tool = {
|
|
|
1204
1300
|
description: "Search files line-by-line with a regex, skipping node_modules/.git/dist and binary files.",
|
|
1205
1301
|
parameters: parameters7,
|
|
1206
1302
|
execute: async (args, context) => capture_errors(async () => {
|
|
1207
|
-
const output = await run_grep(args, context.work_dir);
|
|
1303
|
+
const output = await run_grep(args, context.work_dir, context.signal);
|
|
1208
1304
|
return { ok: true, output };
|
|
1209
1305
|
})
|
|
1210
1306
|
};
|
|
@@ -1214,6 +1310,7 @@ var DEFAULT_TIMEOUT_MS2 = 3e4;
|
|
|
1214
1310
|
var MAX_TIMEOUT_MS2 = 12e4;
|
|
1215
1311
|
var DEFAULT_MAX_CHARS2 = 2e4;
|
|
1216
1312
|
var MAX_MAX_CHARS2 = 1e5;
|
|
1313
|
+
var MAX_BODY_BYTES2 = MAX_MAX_CHARS2 * 4;
|
|
1217
1314
|
var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
1218
1315
|
var REPORTED_HEADERS = ["content-length", "ratelimit-remaining", "retry-after"];
|
|
1219
1316
|
var parameters8 = {
|
|
@@ -1278,13 +1375,14 @@ async function run_http(args, external) {
|
|
|
1278
1375
|
}
|
|
1279
1376
|
const response = await safe_fetch(url, init);
|
|
1280
1377
|
const content_type = response.headers.get("content-type") ?? "unknown";
|
|
1281
|
-
const
|
|
1378
|
+
const byte_budget = Math.min(MAX_BODY_BYTES2, max_chars * 4);
|
|
1379
|
+
const clamped = await read_clamped_text(response, byte_budget);
|
|
1282
1380
|
const sections = [
|
|
1283
1381
|
`# status ${response.status}`,
|
|
1284
1382
|
`# content-type ${content_type}`,
|
|
1285
1383
|
...header_lines(response),
|
|
1286
1384
|
"",
|
|
1287
|
-
clamp_output(text, max_chars)
|
|
1385
|
+
clamp_output(clamped.text, max_chars)
|
|
1288
1386
|
];
|
|
1289
1387
|
return { ok: true, output: sections.join("\n") };
|
|
1290
1388
|
}
|
|
@@ -1390,7 +1488,7 @@ var list_dir_tool = {
|
|
|
1390
1488
|
// src/tools/builtin/process_list.ts
|
|
1391
1489
|
import { readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
1392
1490
|
var DEFAULT_MAX_RESULTS3 = 50;
|
|
1393
|
-
var
|
|
1491
|
+
var MAX_MAX_RESULTS2 = 500;
|
|
1394
1492
|
var CMDLINE_MAX_CHARS = 200;
|
|
1395
1493
|
var PROC_DIR = "/proc";
|
|
1396
1494
|
var PID_PATTERN = /^[0-9]+$/;
|
|
@@ -1445,7 +1543,7 @@ function collect_lines2(filter, max_results) {
|
|
|
1445
1543
|
}
|
|
1446
1544
|
function run_process_list(args) {
|
|
1447
1545
|
const filter = optional_string_arg(args, "filter", "").toLowerCase();
|
|
1448
|
-
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);
|
|
1449
1547
|
let listing;
|
|
1450
1548
|
try {
|
|
1451
1549
|
listing = collect_lines2(filter, max_results);
|
|
@@ -1515,6 +1613,46 @@ var read_file_tool = {
|
|
|
1515
1613
|
// src/tools/builtin/run_tests.ts
|
|
1516
1614
|
import { spawn as spawn2 } from "child_process";
|
|
1517
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
|
+
|
|
1518
1656
|
// src/tools/builtin/terminal.ts
|
|
1519
1657
|
import { spawn } from "child_process";
|
|
1520
1658
|
|
|
@@ -1590,12 +1728,13 @@ function scrub_spawn_env(process_env, context_env) {
|
|
|
1590
1728
|
return scrubbed;
|
|
1591
1729
|
}
|
|
1592
1730
|
function wire_kill(child, timeout_signal, external) {
|
|
1593
|
-
|
|
1594
|
-
|
|
1731
|
+
const kill = () => kill_process_group(child, "SIGKILL");
|
|
1732
|
+
timeout_signal.addEventListener("abort", kill, { once: true });
|
|
1733
|
+
external?.addEventListener("abort", kill, { once: true });
|
|
1595
1734
|
}
|
|
1596
|
-
function
|
|
1735
|
+
function wait_exit(child) {
|
|
1597
1736
|
return new Promise((resolve) => {
|
|
1598
|
-
child.on("
|
|
1737
|
+
child.on("exit", (code) => resolve(code ?? -1));
|
|
1599
1738
|
child.on("error", () => resolve(-1));
|
|
1600
1739
|
});
|
|
1601
1740
|
}
|
|
@@ -1604,22 +1743,26 @@ async function run_command(command, work_dir, env, timeout_ms, external) {
|
|
|
1604
1743
|
const stderr = { text: "" };
|
|
1605
1744
|
const child = spawn("bash", ["-lc", command], {
|
|
1606
1745
|
cwd: work_dir,
|
|
1607
|
-
env: scrub_spawn_env(process.env, env)
|
|
1746
|
+
env: scrub_spawn_env(process.env, env),
|
|
1747
|
+
detached: true,
|
|
1748
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
1608
1749
|
});
|
|
1609
|
-
child
|
|
1610
|
-
child.
|
|
1611
|
-
|
|
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);
|
|
1612
1754
|
let timed_out = false;
|
|
1613
1755
|
let exit_code;
|
|
1614
1756
|
try {
|
|
1615
1757
|
exit_code = await with_timeout((timeout_signal) => {
|
|
1616
1758
|
wire_kill(child, timeout_signal, external);
|
|
1617
|
-
return
|
|
1759
|
+
return exit_promise;
|
|
1618
1760
|
}, timeout_ms, "terminal");
|
|
1619
1761
|
} catch (err) {
|
|
1620
1762
|
if (err instanceof ToolTimeoutError === true) {
|
|
1621
1763
|
timed_out = true;
|
|
1622
|
-
|
|
1764
|
+
kill_process_group(child, "SIGKILL");
|
|
1765
|
+
exit_code = await exit_promise;
|
|
1623
1766
|
} else {
|
|
1624
1767
|
throw err;
|
|
1625
1768
|
}
|
|
@@ -1630,7 +1773,7 @@ async function run_command(command, work_dir, env, timeout_ms, external) {
|
|
|
1630
1773
|
}
|
|
1631
1774
|
function terminal_result(outcome) {
|
|
1632
1775
|
const result = {
|
|
1633
|
-
ok: outcome.exit_code === 0 && outcome.cancelled === false,
|
|
1776
|
+
ok: outcome.exit_code === 0 && outcome.cancelled === false && outcome.timed_out === false,
|
|
1634
1777
|
output: clamp_output(outcome.output)
|
|
1635
1778
|
};
|
|
1636
1779
|
if (outcome.cancelled === true) {
|
|
@@ -1656,28 +1799,43 @@ var terminal_tool = {
|
|
|
1656
1799
|
// src/tools/builtin/run_tests.ts
|
|
1657
1800
|
var MAX_OUTPUT_CHARS = 2e3;
|
|
1658
1801
|
var DEFAULT_TEST_COMMAND = "node node_modules/vitest/vitest.mjs run";
|
|
1802
|
+
var DEFAULT_TIMEOUT_MS4 = 6e5;
|
|
1803
|
+
var MAX_TIMEOUT_MS4 = 6e5;
|
|
1659
1804
|
var parameters13 = {
|
|
1660
1805
|
type: "object",
|
|
1661
1806
|
properties: {
|
|
1662
1807
|
filter: {
|
|
1663
1808
|
type: "string",
|
|
1664
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)"
|
|
1665
1814
|
}
|
|
1666
1815
|
},
|
|
1667
1816
|
additionalProperties: false
|
|
1668
1817
|
};
|
|
1669
1818
|
var busy = false;
|
|
1670
1819
|
var run_test_command = default_runner;
|
|
1671
|
-
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) {
|
|
1672
1827
|
const child = spawn2("bash", ["-lc", command], {
|
|
1673
1828
|
cwd,
|
|
1674
1829
|
env: scrub_spawn_env(process.env, {}),
|
|
1830
|
+
detached: true,
|
|
1675
1831
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1676
1832
|
});
|
|
1833
|
+
track_detached_child(child);
|
|
1677
1834
|
child.stdout?.on("data", (chunk) => on_chunk("stdout", chunk));
|
|
1678
1835
|
child.stderr?.on("data", (chunk) => on_chunk("stderr", chunk));
|
|
1836
|
+
wire_kill2(child, signal);
|
|
1679
1837
|
return new Promise((resolve) => {
|
|
1680
|
-
child.on("
|
|
1838
|
+
child.on("exit", (code) => resolve({ exit_code: code ?? -1 }));
|
|
1681
1839
|
child.on("error", () => resolve({ exit_code: -1 }));
|
|
1682
1840
|
});
|
|
1683
1841
|
}
|
|
@@ -1688,11 +1846,21 @@ function build_command(filter, env) {
|
|
|
1688
1846
|
const base = optional_string_arg(env, "LICH_TEST_COMMAND", DEFAULT_TEST_COMMAND);
|
|
1689
1847
|
return filter === void 0 ? base : `${base} ${shell_quote(filter)}`;
|
|
1690
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
|
+
}
|
|
1691
1859
|
var run_tests_tool = {
|
|
1692
1860
|
name: "run_tests",
|
|
1693
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.",
|
|
1694
1862
|
parameters: parameters13,
|
|
1695
|
-
timeout_ms:
|
|
1863
|
+
timeout_ms: MAX_TIMEOUT_MS4,
|
|
1696
1864
|
execute: async (args, context) => capture_errors(async () => {
|
|
1697
1865
|
if (busy === true) {
|
|
1698
1866
|
return { ok: false, output: "", error: "run_tests_busy" };
|
|
@@ -1703,17 +1871,44 @@ var run_tests_tool = {
|
|
|
1703
1871
|
if (filter.startsWith("-") === true) {
|
|
1704
1872
|
return { ok: false, output: "", error: "invalid_filter: must not start with -" };
|
|
1705
1873
|
}
|
|
1874
|
+
const timeout_ms = clamp_timeout2(optional_number_arg(args, "timeout_ms", DEFAULT_TIMEOUT_MS4));
|
|
1706
1875
|
const command = build_command(filter === "" ? void 0 : filter, context.env);
|
|
1707
1876
|
const streams = { stdout: "", stderr: "" };
|
|
1708
1877
|
const on_chunk = (stream, chunk) => {
|
|
1709
|
-
streams[stream] = streams[stream]
|
|
1878
|
+
streams[stream] = append_clamped(streams[stream], chunk, MAX_OUTPUT_CHARS);
|
|
1710
1879
|
};
|
|
1711
|
-
|
|
1712
|
-
|
|
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;
|
|
1713
1908
|
return {
|
|
1714
1909
|
ok,
|
|
1715
1910
|
output: clamp_output(`${streams.stdout}${streams.stderr}
|
|
1716
|
-
[exit ${
|
|
1911
|
+
[exit ${exit_code}]`, MAX_OUTPUT_CHARS),
|
|
1717
1912
|
...ok ? {} : { error: "tests_failed" }
|
|
1718
1913
|
};
|
|
1719
1914
|
} finally {
|
|
@@ -1725,8 +1920,9 @@ var run_tests_tool = {
|
|
|
1725
1920
|
// src/tools/builtin/web_search.ts
|
|
1726
1921
|
var DEFAULT_MAX_RESULTS4 = 8;
|
|
1727
1922
|
var MAX_RESULTS = 20;
|
|
1728
|
-
var
|
|
1729
|
-
var
|
|
1923
|
+
var DEFAULT_TIMEOUT_MS5 = 2e4;
|
|
1924
|
+
var MAX_TIMEOUT_MS5 = 6e4;
|
|
1925
|
+
var MAX_SEARCH_BODY_BYTES = 512e3;
|
|
1730
1926
|
var SEARCH_ENDPOINT = "https://html.duckduckgo.com/html/?q=";
|
|
1731
1927
|
var REDIRECT_PREFIXES = ["//duckduckgo.com/l/?", "/l/?"];
|
|
1732
1928
|
var RESULT_PATTERN = /<a[^>]+class="result__a"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/g;
|
|
@@ -1805,7 +2001,7 @@ function format_results(hits) {
|
|
|
1805
2001
|
async function run_search(args, external) {
|
|
1806
2002
|
const query = require_string_arg(args, "query");
|
|
1807
2003
|
const max_results = clamp_int_arg(args, "max_results", DEFAULT_MAX_RESULTS4, MAX_RESULTS);
|
|
1808
|
-
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);
|
|
1809
2005
|
try {
|
|
1810
2006
|
const response = await fetch(`${SEARCH_ENDPOINT}${encodeURIComponent(query)}`, {
|
|
1811
2007
|
headers: { "user-agent": USER_AGENT, "accept-language": "en" },
|
|
@@ -1814,7 +2010,8 @@ async function run_search(args, external) {
|
|
|
1814
2010
|
if (response.ok === false) {
|
|
1815
2011
|
return { ok: false, output: "", error: `search_failed: http_${response.status}` };
|
|
1816
2012
|
}
|
|
1817
|
-
const
|
|
2013
|
+
const clamped = await read_clamped_text(response, MAX_SEARCH_BODY_BYTES);
|
|
2014
|
+
const hits = parse_results(clamped.text, max_results);
|
|
1818
2015
|
return { ok: true, output: hits.length === 0 ? "no results" : format_results(hits) };
|
|
1819
2016
|
} catch (err) {
|
|
1820
2017
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -1866,35 +2063,6 @@ var write_file_tool = {
|
|
|
1866
2063
|
})
|
|
1867
2064
|
};
|
|
1868
2065
|
|
|
1869
|
-
// src/util/log.ts
|
|
1870
|
-
var level_order = {
|
|
1871
|
-
debug: 10,
|
|
1872
|
-
info: 20,
|
|
1873
|
-
warn: 30,
|
|
1874
|
-
error: 40
|
|
1875
|
-
};
|
|
1876
|
-
var current_level = "info";
|
|
1877
|
-
function set_log_level(level) {
|
|
1878
|
-
current_level = level;
|
|
1879
|
-
}
|
|
1880
|
-
function log(level, message, data) {
|
|
1881
|
-
if (level_order[level] < level_order[current_level]) {
|
|
1882
|
-
return;
|
|
1883
|
-
}
|
|
1884
|
-
const line = `[lich:${level}] ${message}`;
|
|
1885
|
-
if (data === void 0) {
|
|
1886
|
-
console.error(line);
|
|
1887
|
-
return;
|
|
1888
|
-
}
|
|
1889
|
-
console.error(line, data);
|
|
1890
|
-
}
|
|
1891
|
-
var logger = {
|
|
1892
|
-
debug: (message, data) => log("debug", message, data),
|
|
1893
|
-
info: (message, data) => log("info", message, data),
|
|
1894
|
-
warn: (message, data) => log("warn", message, data),
|
|
1895
|
-
error: (message, data) => log("error", message, data)
|
|
1896
|
-
};
|
|
1897
|
-
|
|
1898
2066
|
// src/tools/builtin/index.ts
|
|
1899
2067
|
var core_tools = [
|
|
1900
2068
|
read_file_tool,
|
|
@@ -2030,9 +2198,23 @@ var ToolExecutor = class {
|
|
|
2030
2198
|
};
|
|
2031
2199
|
|
|
2032
2200
|
// src/plugins/hooks.ts
|
|
2201
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2033
2202
|
var SUMMARY_MAX_CHARS = 300;
|
|
2203
|
+
var run_als = new AsyncLocalStorage();
|
|
2034
2204
|
var plugin_state = /* @__PURE__ */ new WeakMap();
|
|
2205
|
+
function bags_for_run() {
|
|
2206
|
+
return run_als.getStore();
|
|
2207
|
+
}
|
|
2035
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
|
+
}
|
|
2036
2218
|
let state = plugin_state.get(plugin);
|
|
2037
2219
|
if (state === void 0) {
|
|
2038
2220
|
state = /* @__PURE__ */ new Map();
|
|
@@ -2040,6 +2222,16 @@ function hook_state_for(plugin) {
|
|
|
2040
2222
|
}
|
|
2041
2223
|
return state;
|
|
2042
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
|
+
}
|
|
2043
2235
|
function with_hook_state(base, plugin) {
|
|
2044
2236
|
return { ...base, state: hook_state_for(plugin) };
|
|
2045
2237
|
}
|
|
@@ -2053,6 +2245,17 @@ var HookedToolRunner = class {
|
|
|
2053
2245
|
this.wrapped = wrapped;
|
|
2054
2246
|
this.hooked_plugins = plugins.filter((plugin) => plugin.hooks !== void 0);
|
|
2055
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
|
+
}
|
|
2056
2259
|
/** Run before hooks in order; the first {block: true} verdict wins. */
|
|
2057
2260
|
async run_before_hooks(info, base) {
|
|
2058
2261
|
for (const plugin of this.hooked_plugins) {
|
|
@@ -2107,7 +2310,7 @@ var HookedToolRunner = class {
|
|
|
2107
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. */
|
|
2108
2311
|
async call_run_start(info, base) {
|
|
2109
2312
|
for (const plugin of this.hooked_plugins) {
|
|
2110
|
-
|
|
2313
|
+
reset_plugin_bag(plugin);
|
|
2111
2314
|
}
|
|
2112
2315
|
for (const plugin of this.hooked_plugins) {
|
|
2113
2316
|
const hook = plugin.hooks?.on_run_start;
|
|
@@ -2238,116 +2441,549 @@ var ProviderError = class extends Error {
|
|
|
2238
2441
|
}
|
|
2239
2442
|
};
|
|
2240
2443
|
|
|
2241
|
-
// src/
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
var PUBLIC_PLATFORMS = /* @__PURE__ */ new Set(["telegram", "discord", "twitch"]);
|
|
2246
|
-
var DEFAULT_GATEWAY_TOOLS_ENABLED = [
|
|
2247
|
-
"read_file",
|
|
2248
|
-
"list_dir",
|
|
2249
|
-
"grep_files",
|
|
2250
|
-
"fetch_url",
|
|
2251
|
-
"web_search",
|
|
2252
|
-
"docs_read",
|
|
2253
|
-
"docs_search"
|
|
2254
|
-
];
|
|
2255
|
-
function gateway_tools_enabled(config) {
|
|
2256
|
-
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);
|
|
2257
2448
|
}
|
|
2258
|
-
function
|
|
2259
|
-
|
|
2260
|
-
|
|
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));
|
|
2261
2453
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
if (users.length === 0 && chats.length === 0) {
|
|
2265
|
-
return false;
|
|
2454
|
+
if (message.role === "tool") {
|
|
2455
|
+
return content_tokens + TOOL_MESSAGE_OVERHEAD_TOKENS;
|
|
2266
2456
|
}
|
|
2267
|
-
|
|
2268
|
-
const chat_ok = chats.length === 0 || chats.includes(chat_id);
|
|
2269
|
-
return user_ok && chat_ok;
|
|
2457
|
+
return content_tokens;
|
|
2270
2458
|
}
|
|
2271
|
-
function
|
|
2272
|
-
|
|
2273
|
-
return true;
|
|
2274
|
-
}
|
|
2275
|
-
logger.warn(`gateway denied ${platform} chat=${chat_id} user=${user_id}`);
|
|
2276
|
-
return false;
|
|
2459
|
+
function estimate_messages_tokens(messages) {
|
|
2460
|
+
return messages.reduce((total, message) => total + estimate_message_tokens(message), 0);
|
|
2277
2461
|
}
|
|
2278
2462
|
|
|
2279
|
-
// src/
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
function catalog_dir() {
|
|
2288
|
-
let dir = path9.dirname(fileURLToPath2(import.meta.url));
|
|
2289
|
-
for (let hop = 0; hop < 6; hop += 1) {
|
|
2290
|
-
const candidate = path9.join(dir, "optional-mcps");
|
|
2291
|
-
if (existsSync2(path9.join(candidate, "redot", "manifest.json")) === true) {
|
|
2292
|
-
return candidate;
|
|
2293
|
-
}
|
|
2294
|
-
dir = path9.dirname(dir);
|
|
2295
|
-
}
|
|
2296
|
-
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;
|
|
2297
2471
|
}
|
|
2298
|
-
function
|
|
2299
|
-
const
|
|
2300
|
-
|
|
2301
|
-
throw new Error("mcp catalog manifest rejected");
|
|
2302
|
-
}
|
|
2303
|
-
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));
|
|
2304
2475
|
}
|
|
2305
|
-
function
|
|
2306
|
-
if (
|
|
2307
|
-
return
|
|
2476
|
+
function truncate_head_tail(text, max_chars) {
|
|
2477
|
+
if (max_chars <= 0) {
|
|
2478
|
+
return "";
|
|
2308
2479
|
}
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
const file = path9.join(catalog_dir(), name, "manifest.json");
|
|
2312
|
-
if (existsSync2(file) === true) {
|
|
2313
|
-
manifests.push(read_manifest(file));
|
|
2314
|
-
}
|
|
2480
|
+
if (text.length <= max_chars) {
|
|
2481
|
+
return text;
|
|
2315
2482
|
}
|
|
2316
|
-
|
|
2317
|
-
|
|
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)}`;
|
|
2318
2492
|
}
|
|
2319
|
-
function
|
|
2320
|
-
|
|
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}`;
|
|
2321
2496
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
var SHELL = /[;&|`$<>]/;
|
|
2326
|
-
var DOWNLOADERS = /* @__PURE__ */ new Set(["npx", "npm", "bunx", "uvx", "curl", "wget"]);
|
|
2327
|
-
function refuse_stdio_command(command) {
|
|
2328
|
-
if (command.includes("://") === true) {
|
|
2329
|
-
return "refused url; only a local binary is allowed";
|
|
2330
|
-
}
|
|
2331
|
-
if (SHELL.test(command) === true) {
|
|
2332
|
-
return "refused shell metacharacters in mcp command";
|
|
2333
|
-
}
|
|
2334
|
-
if (command.length === 0 || command.trim() !== command || /\s/.test(command) === true) {
|
|
2335
|
-
return "refused mcp command";
|
|
2497
|
+
function format_capped_transcript(older, max_chars) {
|
|
2498
|
+
if (older.length === 0) {
|
|
2499
|
+
return "";
|
|
2336
2500
|
}
|
|
2337
|
-
const
|
|
2338
|
-
|
|
2339
|
-
|
|
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;
|
|
2340
2506
|
}
|
|
2341
|
-
return
|
|
2507
|
+
return truncate_head_tail(joined, max_chars);
|
|
2342
2508
|
}
|
|
2343
|
-
function
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
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;
|
|
2523
|
+
}
|
|
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;
|
|
2530
|
+
}
|
|
2531
|
+
return { recent: non_system.slice(cut), older: non_system.slice(0, cut) };
|
|
2532
|
+
}
|
|
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 };
|
|
2540
|
+
}
|
|
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 };
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
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;
|
|
2575
|
+
}
|
|
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];
|
|
2580
|
+
}
|
|
2581
|
+
const existing = history[system_index];
|
|
2582
|
+
if (existing !== void 0 && existing.content === system_prompt) {
|
|
2583
|
+
return history;
|
|
2584
|
+
}
|
|
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 });
|
|
2591
|
+
}
|
|
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;
|
|
2351
2987
|
}
|
|
2352
2988
|
|
|
2353
2989
|
// src/mcp/mcp_url.ts
|
|
@@ -2372,8 +3008,18 @@ function refuse_http_url(url) {
|
|
|
2372
3008
|
}
|
|
2373
3009
|
|
|
2374
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;
|
|
3020
|
+
}
|
|
2375
3021
|
function refuse_catalog_stdio(name, command, args) {
|
|
2376
|
-
const pin =
|
|
3022
|
+
const pin = pin_for(name, command);
|
|
2377
3023
|
if (pin?.command_basename === void 0) {
|
|
2378
3024
|
return void 0;
|
|
2379
3025
|
}
|
|
@@ -2382,11 +3028,11 @@ function refuse_catalog_stdio(name, command, args) {
|
|
|
2382
3028
|
}
|
|
2383
3029
|
const prefix = pin.args_prefix ?? [];
|
|
2384
3030
|
if (args.length !== prefix.length + 1) {
|
|
2385
|
-
return `refused ${name} args; expected ${prefix.join(" ")} <project>`;
|
|
3031
|
+
return `refused ${pin.name} args; expected ${prefix.join(" ")} <project>`;
|
|
2386
3032
|
}
|
|
2387
3033
|
for (let index = 0; index < prefix.length; index += 1) {
|
|
2388
3034
|
if (args[index] !== prefix[index]) {
|
|
2389
|
-
return `refused ${name} args; expected ${prefix.join(" ")} <project>`;
|
|
3035
|
+
return `refused ${pin.name} args; expected ${prefix.join(" ")} <project>`;
|
|
2390
3036
|
}
|
|
2391
3037
|
}
|
|
2392
3038
|
const project = args[prefix.length];
|
|
@@ -2412,7 +3058,7 @@ function refuse_mcp_entry(name, entry) {
|
|
|
2412
3058
|
return "refused mcp entry";
|
|
2413
3059
|
}
|
|
2414
3060
|
const args = entry.args ?? [];
|
|
2415
|
-
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);
|
|
2416
3062
|
}
|
|
2417
3063
|
|
|
2418
3064
|
// src/agent/config.ts
|
|
@@ -2466,12 +3112,26 @@ var provider_schema = z.object({
|
|
|
2466
3112
|
/** Injectable fetch, mainly for tests; passes through untouched. */
|
|
2467
3113
|
fetch_fn: z.custom(() => true).optional()
|
|
2468
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
|
+
});
|
|
2469
3129
|
var agent_config_schema = z.object({
|
|
2470
3130
|
/** Wizard label. The TUI banner uses the active theme welcome string. */
|
|
2471
3131
|
agent_name: z.string().min(1).default("lich"),
|
|
2472
3132
|
system_prompt: z.string().optional(),
|
|
2473
3133
|
max_turns: z.number().int().min(1).default(25),
|
|
2474
|
-
providers:
|
|
3134
|
+
providers: providers_schema,
|
|
2475
3135
|
work_dir: z.string().optional(),
|
|
2476
3136
|
tools_enabled: z.union([z.literal("all"), z.array(z.string())]).default("all"),
|
|
2477
3137
|
temperature: z.number().min(0).max(2).optional(),
|
|
@@ -2502,6 +3162,13 @@ function freeze_config(config) {
|
|
|
2502
3162
|
for (const provider of config.providers) {
|
|
2503
3163
|
Object.freeze(provider);
|
|
2504
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
|
+
}
|
|
2505
3172
|
if (config.gateway !== void 0) {
|
|
2506
3173
|
Object.freeze(config.gateway.platforms);
|
|
2507
3174
|
Object.freeze(config.gateway.token_envs);
|
|
@@ -2557,12 +3224,19 @@ var AgentEmitter = class {
|
|
|
2557
3224
|
};
|
|
2558
3225
|
|
|
2559
3226
|
// src/mcp/mcp_http.ts
|
|
2560
|
-
|
|
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
|
+
}
|
|
2561
3234
|
const response = await fetch_fn(url, {
|
|
2562
3235
|
method: "POST",
|
|
2563
3236
|
redirect: "error",
|
|
2564
3237
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
2565
|
-
body: JSON.stringify(body)
|
|
3238
|
+
body: JSON.stringify(body),
|
|
3239
|
+
signal
|
|
2566
3240
|
});
|
|
2567
3241
|
const parsed = await response.json();
|
|
2568
3242
|
if (parsed.error !== void 0) {
|
|
@@ -2574,10 +3248,10 @@ async function post_rpc(url, fetch_fn, body) {
|
|
|
2574
3248
|
function http_pipe(url, fetch_fn) {
|
|
2575
3249
|
let next_id = 1;
|
|
2576
3250
|
return {
|
|
2577
|
-
request(method, params) {
|
|
3251
|
+
request(method, params, signal) {
|
|
2578
3252
|
const id = next_id;
|
|
2579
3253
|
next_id += 1;
|
|
2580
|
-
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);
|
|
2581
3255
|
},
|
|
2582
3256
|
notify(method) {
|
|
2583
3257
|
void fetch_fn(url, {
|
|
@@ -2630,7 +3304,6 @@ function plan_stdio(name, command, args, env_path) {
|
|
|
2630
3304
|
}
|
|
2631
3305
|
|
|
2632
3306
|
// src/mcp/mcp_pipe.ts
|
|
2633
|
-
var SKIP_LIMIT = 32;
|
|
2634
3307
|
function parse_rpc_line(line) {
|
|
2635
3308
|
const parsed = safe_json_parse(line);
|
|
2636
3309
|
if (typeof parsed !== "object" || parsed === null) {
|
|
@@ -2638,41 +3311,112 @@ function parse_rpc_line(line) {
|
|
|
2638
3311
|
}
|
|
2639
3312
|
return parsed;
|
|
2640
3313
|
}
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
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);
|
|
2658
3359
|
}
|
|
2659
|
-
|
|
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;
|
|
3368
|
+
}
|
|
3369
|
+
if (aborted2(signal) === true) {
|
|
3370
|
+
return Promise.reject(new Error("cancelled"));
|
|
2660
3371
|
}
|
|
2661
|
-
|
|
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
|
+
});
|
|
2662
3388
|
}
|
|
2663
3389
|
function stdio_pipe(child) {
|
|
2664
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
|
+
};
|
|
2665
3397
|
return {
|
|
2666
|
-
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
|
+
}
|
|
2667
3405
|
const id = next_id;
|
|
2668
3406
|
next_id += 1;
|
|
3407
|
+
const wait = new Promise((resolve, reject) => {
|
|
3408
|
+
pending.set(id, { resolve, reject });
|
|
3409
|
+
});
|
|
2669
3410
|
child.write_line(JSON.stringify({ jsonrpc: "2.0", id, method, params }));
|
|
2670
|
-
|
|
3411
|
+
pump ??= start_pump(child, pending, mark_dead);
|
|
3412
|
+
return with_signal(wait, signal);
|
|
2671
3413
|
},
|
|
2672
3414
|
notify(method) {
|
|
2673
3415
|
child.write_line(JSON.stringify({ jsonrpc: "2.0", method }));
|
|
2674
3416
|
},
|
|
2675
3417
|
close() {
|
|
3418
|
+
mark_dead("mcp closed the pipe");
|
|
3419
|
+
reject_all(pending, "mcp closed the pipe");
|
|
2676
3420
|
child.stop();
|
|
2677
3421
|
}
|
|
2678
3422
|
};
|
|
@@ -2698,7 +3442,7 @@ function run_call(session, wire_name, args, context) {
|
|
|
2698
3442
|
if (context.signal?.aborted === true) {
|
|
2699
3443
|
throw new Error("cancelled");
|
|
2700
3444
|
}
|
|
2701
|
-
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) };
|
|
2702
3446
|
});
|
|
2703
3447
|
}
|
|
2704
3448
|
function tool_for(registered, wire_name, spec, session) {
|
|
@@ -2710,8 +3454,8 @@ function tool_for(registered, wire_name, spec, session) {
|
|
|
2710
3454
|
execute: (args, context) => run_call(session, wire_name, args, context)
|
|
2711
3455
|
};
|
|
2712
3456
|
}
|
|
2713
|
-
function register_listed(registry, server, listed, enabled, session) {
|
|
2714
|
-
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 ?? []);
|
|
2715
3459
|
for (const spec of listed) {
|
|
2716
3460
|
if (excluded.has(spec.name) === true) {
|
|
2717
3461
|
continue;
|
|
@@ -2747,6 +3491,7 @@ function assert_handshake(result) {
|
|
|
2747
3491
|
}
|
|
2748
3492
|
|
|
2749
3493
|
// src/mcp/mcp_content.ts
|
|
3494
|
+
var MCP_ERROR_MAX = 4e3;
|
|
2750
3495
|
function content_text(result) {
|
|
2751
3496
|
if (typeof result !== "object" || result === null) {
|
|
2752
3497
|
return "";
|
|
@@ -2767,16 +3512,30 @@ function content_text(result) {
|
|
|
2767
3512
|
}
|
|
2768
3513
|
}
|
|
2769
3514
|
}
|
|
2770
|
-
const text = parts.join("\n");
|
|
3515
|
+
const text = clamp_output(parts.join("\n"));
|
|
2771
3516
|
if (body.isError === true) {
|
|
2772
|
-
|
|
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);
|
|
2773
3519
|
}
|
|
2774
3520
|
return text;
|
|
2775
3521
|
}
|
|
2776
3522
|
|
|
2777
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
|
+
}
|
|
2778
3537
|
function tool_schema(raw) {
|
|
2779
|
-
if (typeof raw !== "object" || raw === null) {
|
|
3538
|
+
if (typeof raw !== "object" || raw === null || schema_within_budget(raw) === false) {
|
|
2780
3539
|
return { type: "object" };
|
|
2781
3540
|
}
|
|
2782
3541
|
const body = raw;
|
|
@@ -2802,12 +3561,13 @@ function parse_tools(result) {
|
|
|
2802
3561
|
continue;
|
|
2803
3562
|
}
|
|
2804
3563
|
const tool = item;
|
|
2805
|
-
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) {
|
|
2806
3565
|
continue;
|
|
2807
3566
|
}
|
|
3567
|
+
const description = typeof tool.description === "string" ? clamp_text(tool.description, MCP_DESC_MAX) : tool.name;
|
|
2808
3568
|
tools.push({
|
|
2809
3569
|
name: tool.name,
|
|
2810
|
-
description
|
|
3570
|
+
description,
|
|
2811
3571
|
parameters: tool_schema(tool.inputSchema)
|
|
2812
3572
|
});
|
|
2813
3573
|
}
|
|
@@ -2829,20 +3589,20 @@ var McpSession = class {
|
|
|
2829
3589
|
this.closed = true;
|
|
2830
3590
|
this.pipe.close();
|
|
2831
3591
|
}
|
|
2832
|
-
async list_tools() {
|
|
2833
|
-
await this.ensure_ready();
|
|
2834
|
-
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));
|
|
2835
3595
|
}
|
|
2836
|
-
async call_tool(name, args) {
|
|
2837
|
-
await this.ensure_ready();
|
|
2838
|
-
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));
|
|
2839
3599
|
}
|
|
2840
|
-
async ensure_ready() {
|
|
3600
|
+
async ensure_ready(signal) {
|
|
2841
3601
|
if (this.ready_done === true) {
|
|
2842
3602
|
return;
|
|
2843
3603
|
}
|
|
2844
3604
|
try {
|
|
2845
|
-
assert_handshake(await this.pipe.request("initialize", init_params()));
|
|
3605
|
+
assert_handshake(await this.pipe.request("initialize", init_params(), signal));
|
|
2846
3606
|
this.pipe.notify("notifications/initialized");
|
|
2847
3607
|
this.ready_done = true;
|
|
2848
3608
|
} catch (error) {
|
|
@@ -2939,11 +3699,27 @@ async function drain_stderr(stream) {
|
|
|
2939
3699
|
continue;
|
|
2940
3700
|
}
|
|
2941
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
|
+
}
|
|
2942
3717
|
function bun_line_child(command, args, env) {
|
|
2943
3718
|
const queue = create_line_queue();
|
|
2944
3719
|
try {
|
|
2945
3720
|
const options = { stdin: "pipe", stdout: "pipe", stderr: "pipe" };
|
|
2946
|
-
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 });
|
|
2947
3723
|
void pump_stdout(child.stdout, queue);
|
|
2948
3724
|
void drain_stderr(child.stderr);
|
|
2949
3725
|
return {
|
|
@@ -2999,6 +3775,10 @@ function node_line_child(command, args, env) {
|
|
|
2999
3775
|
queue.close();
|
|
3000
3776
|
});
|
|
3001
3777
|
child.stderr?.resume();
|
|
3778
|
+
child.stdin?.on("error", () => {
|
|
3779
|
+
failure ??= "mcp closed the pipe";
|
|
3780
|
+
queue.close();
|
|
3781
|
+
});
|
|
3002
3782
|
child.on("error", (error) => {
|
|
3003
3783
|
failure = spawn_failure(error.code);
|
|
3004
3784
|
queue.close();
|
|
@@ -3032,9 +3812,28 @@ function default_line_spawner(command, args, env) {
|
|
|
3032
3812
|
}
|
|
3033
3813
|
|
|
3034
3814
|
// src/mcp/mcp_attach.ts
|
|
3035
|
-
|
|
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) {
|
|
3036
3834
|
try {
|
|
3037
|
-
|
|
3835
|
+
const listed = await with_timeout2(session.list_tools(), `mcp ${name} attach`);
|
|
3836
|
+
register_listed(registry, name, listed, enabled, session, command);
|
|
3038
3837
|
return session;
|
|
3039
3838
|
} catch (error) {
|
|
3040
3839
|
session.close();
|
|
@@ -3051,7 +3850,7 @@ async function attach_stdio(registry, name, entry, config, runtime) {
|
|
|
3051
3850
|
}
|
|
3052
3851
|
const spawn5 = runtime?.spawn ?? default_line_spawner;
|
|
3053
3852
|
const session = new McpSession(stdio_pipe(spawn5(planned.command, planned.args, entry.env)));
|
|
3054
|
-
return open_and_register(registry, name, config.tools_enabled, session);
|
|
3853
|
+
return open_and_register(registry, name, config.tools_enabled, session, planned.command);
|
|
3055
3854
|
}
|
|
3056
3855
|
async function attach_http(registry, name, url, config, runtime) {
|
|
3057
3856
|
const session = new McpSession(http_pipe(url, runtime?.fetch_fn ?? fetch));
|
|
@@ -3247,11 +4046,30 @@ function git_commit_tool() {
|
|
|
3247
4046
|
}
|
|
3248
4047
|
};
|
|
3249
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
|
+
]);
|
|
3250
4062
|
function seed_state(ctx) {
|
|
3251
4063
|
ctx.state?.set("tests_ok", false);
|
|
3252
4064
|
ctx.state?.set("dirty", true);
|
|
3253
4065
|
ctx.state?.set("commits", 0);
|
|
3254
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
|
+
}
|
|
3255
4073
|
function gatekeeper_hooks(allow_self_commit) {
|
|
3256
4074
|
return {
|
|
3257
4075
|
on_run_start: (_info, ctx) => {
|
|
@@ -3286,19 +4104,20 @@ function gatekeeper_hooks(allow_self_commit) {
|
|
|
3286
4104
|
return {};
|
|
3287
4105
|
},
|
|
3288
4106
|
after_tool_call: (info, ctx) => {
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
} 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
|
+
}
|
|
3295
4112
|
const filter = info.args["filter"];
|
|
3296
4113
|
const filtered = typeof filter === "string" && filter.length > 0;
|
|
3297
4114
|
if (filtered === false) {
|
|
3298
4115
|
ctx.state?.set("tests_ok", true);
|
|
3299
4116
|
ctx.state?.set("dirty", false);
|
|
3300
4117
|
}
|
|
3301
|
-
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4120
|
+
if (info.tool_name === "git_commit" && info.ok === true) {
|
|
3302
4121
|
ctx.state?.set("commits", state_count(ctx, "commits") + 1);
|
|
3303
4122
|
}
|
|
3304
4123
|
}
|
|
@@ -3334,6 +4153,7 @@ function sleep(ms, signal) {
|
|
|
3334
4153
|
// src/providers/failover.ts
|
|
3335
4154
|
var DEFAULT_BACKOFF_BASE_MS = 500;
|
|
3336
4155
|
var DEFAULT_BACKOFF_MAX_MS = 8e3;
|
|
4156
|
+
var MAX_RETRY_AFTER_MS = 3e4;
|
|
3337
4157
|
function classify_error(error) {
|
|
3338
4158
|
if (error instanceof ProviderError) {
|
|
3339
4159
|
return error.kind;
|
|
@@ -3366,7 +4186,7 @@ async function execute_retry_loop(fn, params) {
|
|
|
3366
4186
|
return { ok: false, error: make_abort_error(outcome.error) };
|
|
3367
4187
|
}
|
|
3368
4188
|
const kind = classify_error(outcome.error);
|
|
3369
|
-
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) {
|
|
3370
4190
|
return outcome;
|
|
3371
4191
|
}
|
|
3372
4192
|
const delay_ms = delay_for_error(outcome.error, attempt);
|
|
@@ -3394,11 +4214,15 @@ function caller_aborted(signal) {
|
|
|
3394
4214
|
}
|
|
3395
4215
|
function delay_for_error(error, attempt) {
|
|
3396
4216
|
const backoff = compute_backoff_ms(attempt);
|
|
3397
|
-
if (error instanceof ProviderError && error.retry_after_ms !== void 0
|
|
3398
|
-
|
|
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;
|
|
3399
4220
|
}
|
|
3400
4221
|
return backoff;
|
|
3401
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
|
+
}
|
|
3402
4226
|
async function wait_out_delay(delay_ms, signal) {
|
|
3403
4227
|
if (delay_ms <= 0) {
|
|
3404
4228
|
return;
|
|
@@ -3436,11 +4260,13 @@ var DEFAULT_BASE_URL = "https://api.anthropic.com";
|
|
|
3436
4260
|
var ANTHROPIC_VERSION = "2023-06-01";
|
|
3437
4261
|
var DEFAULT_KEY_ENV = "ANTHROPIC_API_KEY";
|
|
3438
4262
|
var MAX_ERROR_BODY_CHARS = 500;
|
|
3439
|
-
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;
|
|
3440
4264
|
var OVERLOADED_STATUS = 529;
|
|
3441
4265
|
var UNPARSEABLE_ARGS_NOTE = "[unparseable tool arguments]";
|
|
4266
|
+
var TRUNCATED_TOOL_CALLS_NOTE = "[truncated tool call omitted]";
|
|
3442
4267
|
var EMPTY_TEXT_PLACEHOLDER = "(empty)";
|
|
3443
|
-
var DEFAULT_MAX_TOKENS =
|
|
4268
|
+
var DEFAULT_MAX_TOKENS = 16384;
|
|
4269
|
+
var ANTHROPIC_TEMP_MAX = 1;
|
|
3444
4270
|
var AnthropicProvider = class {
|
|
3445
4271
|
name;
|
|
3446
4272
|
model;
|
|
@@ -3464,7 +4290,7 @@ var AnthropicProvider = class {
|
|
|
3464
4290
|
build_endpoint(this.config),
|
|
3465
4291
|
build_request_init(
|
|
3466
4292
|
api_key,
|
|
3467
|
-
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)),
|
|
3468
4294
|
build_abort_signal(options, this.config.timeout_ms)
|
|
3469
4295
|
),
|
|
3470
4296
|
this.config.name
|
|
@@ -3512,7 +4338,7 @@ function build_abort_signal(options, timeout_ms) {
|
|
|
3512
4338
|
}
|
|
3513
4339
|
return AbortSignal.any(signals);
|
|
3514
4340
|
}
|
|
3515
|
-
function build_request_body(model, messages, tools, options) {
|
|
4341
|
+
function build_request_body(model, messages, tools, options, config) {
|
|
3516
4342
|
const body = {
|
|
3517
4343
|
model,
|
|
3518
4344
|
max_tokens: options?.max_tokens ?? DEFAULT_MAX_TOKENS,
|
|
@@ -3525,8 +4351,8 @@ function build_request_body(model, messages, tools, options) {
|
|
|
3525
4351
|
if (tools.length > 0) {
|
|
3526
4352
|
body.tools = to_anthropic_tools(tools);
|
|
3527
4353
|
}
|
|
3528
|
-
if (options?.temperature !== void 0) {
|
|
3529
|
-
body.temperature = options.temperature;
|
|
4354
|
+
if (config.send_temperature === true && options?.temperature !== void 0) {
|
|
4355
|
+
body.temperature = Math.min(options.temperature, ANTHROPIC_TEMP_MAX);
|
|
3530
4356
|
}
|
|
3531
4357
|
return body;
|
|
3532
4358
|
}
|
|
@@ -3582,6 +4408,9 @@ function tool_message_to_block(message) {
|
|
|
3582
4408
|
return block;
|
|
3583
4409
|
}
|
|
3584
4410
|
function assistant_to_blocks(message) {
|
|
4411
|
+
if (message.provider_content !== void 0 && message.provider_content.length > 0) {
|
|
4412
|
+
return message.provider_content;
|
|
4413
|
+
}
|
|
3585
4414
|
const blocks = [];
|
|
3586
4415
|
if (message.content.length > 0) {
|
|
3587
4416
|
blocks.push({ type: "text", text: message.content });
|
|
@@ -3664,7 +4493,7 @@ function status_to_error_kind(status, body_text) {
|
|
|
3664
4493
|
if (status === 429 || status === OVERLOADED_STATUS || status >= 500) {
|
|
3665
4494
|
return "rate_limit";
|
|
3666
4495
|
}
|
|
3667
|
-
if (status === 400 && OVERFLOW_BODY_PATTERN.test(body_text) === true) {
|
|
4496
|
+
if (status === 413 || status === 400 && OVERFLOW_BODY_PATTERN.test(body_text) === true) {
|
|
3668
4497
|
return "overflow";
|
|
3669
4498
|
}
|
|
3670
4499
|
return "bad_request";
|
|
@@ -3681,28 +4510,57 @@ async function to_http_error(response, provider_name) {
|
|
|
3681
4510
|
});
|
|
3682
4511
|
}
|
|
3683
4512
|
function parse_chat_response(dto, config) {
|
|
4513
|
+
const finish_reason = map_stop_reason(dto.stop_reason);
|
|
3684
4514
|
return {
|
|
3685
|
-
message: parse_assistant_message(dto.content ?? []),
|
|
4515
|
+
message: parse_assistant_message(dto.content ?? [], finish_reason),
|
|
3686
4516
|
usage: parse_usage(dto.usage),
|
|
3687
|
-
finish_reason
|
|
4517
|
+
finish_reason,
|
|
3688
4518
|
model: dto.model ?? config.model,
|
|
3689
4519
|
provider_name: config.name
|
|
3690
4520
|
};
|
|
3691
4521
|
}
|
|
3692
|
-
function parse_assistant_message(blocks) {
|
|
4522
|
+
function parse_assistant_message(blocks, finish_reason) {
|
|
3693
4523
|
const text_parts = [];
|
|
3694
4524
|
const tool_calls = [];
|
|
4525
|
+
const provider_content = [];
|
|
4526
|
+
let has_thinking = false;
|
|
3695
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
|
+
}
|
|
3696
4533
|
if (block.type === "text") {
|
|
3697
4534
|
text_parts.push(block.text ?? "");
|
|
3698
|
-
|
|
3699
|
-
|
|
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;
|
|
3700
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);
|
|
3701
4557
|
}
|
|
4558
|
+
const safe_provider = finish_reason === "length" ? provider_content.filter((block) => block["type"] !== "tool_use") : provider_content;
|
|
3702
4559
|
return {
|
|
3703
4560
|
role: "assistant",
|
|
3704
4561
|
content: text_parts.filter((part) => part.length > 0).join("\n"),
|
|
3705
|
-
...
|
|
4562
|
+
...safe_calls.length > 0 ? { tool_calls: safe_calls } : {},
|
|
4563
|
+
...has_thinking === true ? { provider_content: safe_provider } : {}
|
|
3706
4564
|
};
|
|
3707
4565
|
}
|
|
3708
4566
|
function tool_use_to_call(block, text_parts) {
|
|
@@ -3711,7 +4569,7 @@ function tool_use_to_call(block, text_parts) {
|
|
|
3711
4569
|
return { id: block.id ?? "", name: block.name ?? "", args };
|
|
3712
4570
|
}
|
|
3713
4571
|
text_parts.push(UNPARSEABLE_ARGS_NOTE);
|
|
3714
|
-
return
|
|
4572
|
+
return void 0;
|
|
3715
4573
|
}
|
|
3716
4574
|
function parse_usage(dto) {
|
|
3717
4575
|
const prompt_tokens = dto?.input_tokens ?? 0;
|
|
@@ -3755,8 +4613,9 @@ function is_abort_like2(error) {
|
|
|
3755
4613
|
// src/providers/ollama.ts
|
|
3756
4614
|
var DEFAULT_BASE_URL2 = "http://localhost:11434";
|
|
3757
4615
|
var MAX_ERROR_BODY_CHARS2 = 500;
|
|
3758
|
-
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;
|
|
3759
4617
|
var UNPARSEABLE_ARGS_NOTE2 = "[unparseable tool arguments]";
|
|
4618
|
+
var TRUNCATED_TOOL_CALLS_NOTE2 = "[truncated tool call omitted]";
|
|
3760
4619
|
var tool_call_counter = 0;
|
|
3761
4620
|
var OllamaProvider = class {
|
|
3762
4621
|
name;
|
|
@@ -3864,7 +4723,7 @@ function build_request_body2(config, messages, tools, options) {
|
|
|
3864
4723
|
if (wire_tools.length > 0) {
|
|
3865
4724
|
body.tools = wire_tools;
|
|
3866
4725
|
}
|
|
3867
|
-
const wire_options = build_wire_options(options);
|
|
4726
|
+
const wire_options = build_wire_options(config, options);
|
|
3868
4727
|
if (wire_options !== void 0) {
|
|
3869
4728
|
body.options = wire_options;
|
|
3870
4729
|
}
|
|
@@ -3876,7 +4735,7 @@ function build_request_body2(config, messages, tools, options) {
|
|
|
3876
4735
|
}
|
|
3877
4736
|
return body;
|
|
3878
4737
|
}
|
|
3879
|
-
function build_wire_options(options) {
|
|
4738
|
+
function build_wire_options(config, options) {
|
|
3880
4739
|
const wire_options = {};
|
|
3881
4740
|
if (options?.temperature !== void 0) {
|
|
3882
4741
|
wire_options.temperature = options.temperature;
|
|
@@ -3884,7 +4743,10 @@ function build_wire_options(options) {
|
|
|
3884
4743
|
if (options?.max_tokens !== void 0) {
|
|
3885
4744
|
wire_options.num_predict = options.max_tokens;
|
|
3886
4745
|
}
|
|
3887
|
-
|
|
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;
|
|
3888
4750
|
return has_any === true ? wire_options : void 0;
|
|
3889
4751
|
}
|
|
3890
4752
|
function build_request_init2(api_key, body, signal) {
|
|
@@ -3950,7 +4812,7 @@ function status_to_error_kind2(status, body_text) {
|
|
|
3950
4812
|
if (status === 429 || status >= 500) {
|
|
3951
4813
|
return "rate_limit";
|
|
3952
4814
|
}
|
|
3953
|
-
if (status === 400 && OVERFLOW_BODY_PATTERN2.test(body_text) === true) {
|
|
4815
|
+
if (status === 413 || status === 400 && OVERFLOW_BODY_PATTERN2.test(body_text) === true) {
|
|
3954
4816
|
return "overflow";
|
|
3955
4817
|
}
|
|
3956
4818
|
return "bad_request";
|
|
@@ -3986,7 +4848,11 @@ function to_chat_response(dto, config) {
|
|
|
3986
4848
|
if (message_content.length > 0) {
|
|
3987
4849
|
content_parts.push(message_content);
|
|
3988
4850
|
}
|
|
3989
|
-
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
|
+
}
|
|
3990
4856
|
const has_tool_calls = parsed_calls.length > 0;
|
|
3991
4857
|
return {
|
|
3992
4858
|
message: {
|
|
@@ -3995,7 +4861,7 @@ function to_chat_response(dto, config) {
|
|
|
3995
4861
|
...has_tool_calls === true ? { tool_calls: parsed_calls } : {}
|
|
3996
4862
|
},
|
|
3997
4863
|
usage: parse_usage2(dto),
|
|
3998
|
-
finish_reason:
|
|
4864
|
+
finish_reason: has_tool_calls === true ? "tool_calls" : finish_reason === "tool_calls" ? "stop" : finish_reason,
|
|
3999
4865
|
model: dto.model ?? config.model,
|
|
4000
4866
|
provider_name: config.name
|
|
4001
4867
|
};
|
|
@@ -4004,30 +4870,34 @@ function next_tool_call_id() {
|
|
|
4004
4870
|
tool_call_counter += 1;
|
|
4005
4871
|
return `ollama_${Date.now().toString(36)}_${tool_call_counter}`;
|
|
4006
4872
|
}
|
|
4007
|
-
function normalize_tool_arguments(raw_arguments, content_parts) {
|
|
4008
|
-
if (is_record2(raw_arguments) === true) {
|
|
4009
|
-
return raw_arguments;
|
|
4010
|
-
}
|
|
4011
|
-
if (typeof raw_arguments === "string") {
|
|
4012
|
-
const parsed = safe_json_parse(raw_arguments);
|
|
4013
|
-
if (is_record2(parsed) === true) {
|
|
4014
|
-
return parsed;
|
|
4015
|
-
}
|
|
4016
|
-
}
|
|
4017
|
-
content_parts.push(UNPARSEABLE_ARGS_NOTE2);
|
|
4018
|
-
return {};
|
|
4019
|
-
}
|
|
4020
4873
|
function parse_tool_calls(raw_calls, content_parts) {
|
|
4021
4874
|
const tool_calls = [];
|
|
4022
4875
|
for (const raw_call of raw_calls) {
|
|
4023
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
|
+
}
|
|
4024
4881
|
tool_calls.push({
|
|
4025
4882
|
id: next_tool_call_id(),
|
|
4026
4883
|
name,
|
|
4027
|
-
args
|
|
4884
|
+
args
|
|
4028
4885
|
});
|
|
4029
4886
|
}
|
|
4030
|
-
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;
|
|
4031
4901
|
}
|
|
4032
4902
|
function parse_usage2(dto) {
|
|
4033
4903
|
const prompt_tokens = dto.prompt_eval_count ?? 0;
|
|
@@ -4069,8 +4939,10 @@ var DEFAULT_BASE_URL3 = "https://api.openai.com/v1";
|
|
|
4069
4939
|
var WELL_KNOWN_HOST = "api.openai.com";
|
|
4070
4940
|
var WELL_KNOWN_KEY_ENV = "OPENAI_API_KEY";
|
|
4071
4941
|
var MAX_ERROR_BODY_CHARS3 = 500;
|
|
4072
|
-
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;
|
|
4073
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;
|
|
4074
4946
|
var OpenAICompatProvider = class {
|
|
4075
4947
|
name;
|
|
4076
4948
|
model;
|
|
@@ -4197,14 +5069,21 @@ function build_request_body3(model, messages, tools, options) {
|
|
|
4197
5069
|
if (wire_tools.length > 0) {
|
|
4198
5070
|
body.tools = wire_tools;
|
|
4199
5071
|
}
|
|
4200
|
-
if (options?.temperature !== void 0) {
|
|
5072
|
+
if (options?.temperature !== void 0 && is_reasoning_model(model) === false) {
|
|
4201
5073
|
body.temperature = options.temperature;
|
|
4202
5074
|
}
|
|
4203
5075
|
if (options?.max_tokens !== void 0) {
|
|
4204
|
-
|
|
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
|
+
}
|
|
4205
5081
|
}
|
|
4206
5082
|
return body;
|
|
4207
5083
|
}
|
|
5084
|
+
function is_reasoning_model(model) {
|
|
5085
|
+
return REASONING_MODEL_PATTERN.test(model) === true;
|
|
5086
|
+
}
|
|
4208
5087
|
function build_request_init3(api_key, body, signal) {
|
|
4209
5088
|
return {
|
|
4210
5089
|
method: "POST",
|
|
@@ -4268,7 +5147,7 @@ function status_to_error_kind3(status, body_text) {
|
|
|
4268
5147
|
if (status === 429 || status >= 500) {
|
|
4269
5148
|
return "rate_limit";
|
|
4270
5149
|
}
|
|
4271
|
-
if (status === 400 && OVERFLOW_BODY_PATTERN3.test(body_text) === true) {
|
|
5150
|
+
if (status === 413 || status === 400 && OVERFLOW_BODY_PATTERN3.test(body_text) === true) {
|
|
4272
5151
|
return "overflow";
|
|
4273
5152
|
}
|
|
4274
5153
|
return "bad_request";
|
|
@@ -4293,27 +5172,39 @@ function parse_chat_response2(dto, config) {
|
|
|
4293
5172
|
message: "provider returned a success response without choices"
|
|
4294
5173
|
});
|
|
4295
5174
|
}
|
|
5175
|
+
const finish_reason = map_finish_reason(choice.finish_reason);
|
|
4296
5176
|
return {
|
|
4297
|
-
message: parse_assistant_message2(choice.message),
|
|
5177
|
+
message: parse_assistant_message2(choice.message, finish_reason),
|
|
4298
5178
|
usage: parse_usage3(dto.usage),
|
|
4299
|
-
finish_reason
|
|
5179
|
+
finish_reason,
|
|
4300
5180
|
model: dto.model ?? config.model,
|
|
4301
5181
|
provider_name: config.name
|
|
4302
5182
|
};
|
|
4303
5183
|
}
|
|
4304
|
-
function parse_assistant_message2(dto) {
|
|
5184
|
+
function parse_assistant_message2(dto, finish_reason) {
|
|
4305
5185
|
const content_parts = [];
|
|
4306
5186
|
if (dto.content !== void 0 && dto.content !== null && dto.content.length > 0) {
|
|
4307
5187
|
content_parts.push(dto.content);
|
|
4308
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
|
+
}
|
|
4309
5197
|
const tool_calls = [];
|
|
4310
|
-
for (const raw_call of
|
|
5198
|
+
for (const raw_call of raw_calls) {
|
|
4311
5199
|
const raw_arguments = raw_call.function?.arguments ?? "";
|
|
4312
5200
|
const parsed_arguments = raw_arguments.length === 0 ? {} : safe_json_parse(raw_arguments);
|
|
4313
5201
|
if (is_record3(parsed_arguments) === true) {
|
|
4314
|
-
tool_calls.push({
|
|
5202
|
+
tool_calls.push({
|
|
5203
|
+
id: raw_call.id ?? "",
|
|
5204
|
+
name: raw_call.function?.name ?? "",
|
|
5205
|
+
args: parsed_arguments
|
|
5206
|
+
});
|
|
4315
5207
|
} else {
|
|
4316
|
-
tool_calls.push({ id: raw_call.id ?? "", name: raw_call.function?.name ?? "", args: {} });
|
|
4317
5208
|
content_parts.push(UNPARSEABLE_ARGS_NOTE3);
|
|
4318
5209
|
}
|
|
4319
5210
|
}
|
|
@@ -4426,9 +5317,10 @@ function build_provider(config) {
|
|
|
4426
5317
|
}
|
|
4427
5318
|
async function chat_with_failover(router, messages, tools, options) {
|
|
4428
5319
|
let last_error;
|
|
5320
|
+
let first_hard_error;
|
|
4429
5321
|
for (const provider of router.list()) {
|
|
4430
5322
|
if (options?.signal?.aborted === true) {
|
|
4431
|
-
throw last_error ?? make_router_abort_error();
|
|
5323
|
+
throw first_hard_error ?? last_error ?? make_router_abort_error();
|
|
4432
5324
|
}
|
|
4433
5325
|
const result = await attempt_provider(provider, messages, tools, options);
|
|
4434
5326
|
if (result.ok === true) {
|
|
@@ -4438,15 +5330,24 @@ async function chat_with_failover(router, messages, tools, options) {
|
|
|
4438
5330
|
throw result.error;
|
|
4439
5331
|
}
|
|
4440
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
|
+
}
|
|
4441
5336
|
log_fail_over(result.error);
|
|
4442
5337
|
}
|
|
4443
|
-
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";
|
|
4444
5342
|
}
|
|
4445
5343
|
function log_fail_over(error) {
|
|
4446
5344
|
if (error.kind === "rate_limit" || error.kind === "network") {
|
|
4447
5345
|
return;
|
|
4448
5346
|
}
|
|
4449
|
-
|
|
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
|
+
);
|
|
4450
5351
|
}
|
|
4451
5352
|
function make_router_abort_error() {
|
|
4452
5353
|
return new ProviderError({
|
|
@@ -4494,272 +5395,73 @@ function to_provider_error(error, fallback_name) {
|
|
|
4494
5395
|
});
|
|
4495
5396
|
}
|
|
4496
5397
|
|
|
4497
|
-
// src/session/
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
}
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
const id = `${Date.now().toString(36)}-${counter_state.value}${label_part}`;
|
|
4510
|
-
const file_path = path14.join(dir, `${id}.jsonl`);
|
|
4511
|
-
return {
|
|
4512
|
-
id,
|
|
4513
|
-
path: file_path,
|
|
4514
|
-
append: async (record) => {
|
|
4515
|
-
await appendFile(file_path, `${safe_stringify(record)}
|
|
4516
|
-
`, "utf8");
|
|
4517
|
-
}
|
|
4518
|
-
};
|
|
4519
|
-
}
|
|
4520
|
-
|
|
4521
|
-
// src/context/tokens.ts
|
|
4522
|
-
var TOOL_MESSAGE_OVERHEAD_TOKENS = 8;
|
|
4523
|
-
function estimate_text_tokens(text) {
|
|
4524
|
-
return Math.ceil(text.length / 4);
|
|
4525
|
-
}
|
|
4526
|
-
function estimate_message_tokens(message) {
|
|
4527
|
-
const content_tokens = estimate_text_tokens(message.content);
|
|
4528
|
-
if (message.role === "assistant" && message.tool_calls !== void 0) {
|
|
4529
|
-
return content_tokens + estimate_text_tokens(safe_stringify(message.tool_calls));
|
|
4530
|
-
}
|
|
4531
|
-
if (message.role === "tool") {
|
|
4532
|
-
return content_tokens + TOOL_MESSAGE_OVERHEAD_TOKENS;
|
|
4533
|
-
}
|
|
4534
|
-
return content_tokens;
|
|
4535
|
-
}
|
|
4536
|
-
function estimate_messages_tokens(messages) {
|
|
4537
|
-
return messages.reduce((total, message) => total + estimate_message_tokens(message), 0);
|
|
4538
|
-
}
|
|
4539
|
-
|
|
4540
|
-
// src/context/compressor.ts
|
|
4541
|
-
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.";
|
|
4542
|
-
var MAX_TRANSCRIPT_CHARS = 24e3;
|
|
4543
|
-
function should_compress(messages, budget_tokens, threshold) {
|
|
4544
|
-
return estimate_messages_tokens(messages) >= budget_tokens * threshold;
|
|
4545
|
-
}
|
|
4546
|
-
function format_history_line(message) {
|
|
4547
|
-
const rendered_calls = message.role === "assistant" && message.tool_calls !== void 0 ? ` tool_calls=${safe_stringify(message.tool_calls)}` : "";
|
|
4548
|
-
return `[${message.role}] ${message.content}${rendered_calls}`;
|
|
4549
|
-
}
|
|
4550
|
-
function build_summary_request(older, model_hint) {
|
|
4551
|
-
const transcript = older.map((message) => format_history_line(message)).join("\n");
|
|
4552
|
-
const hint = model_hint === void 0 ? "" : `
|
|
4553
|
-
(Continuing agent run as model: ${model_hint})`;
|
|
4554
|
-
return {
|
|
4555
|
-
role: "user",
|
|
4556
|
-
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)
|
|
5398
|
+
// src/session/recorder.ts
|
|
5399
|
+
var seeded_handles = /* @__PURE__ */ new WeakSet();
|
|
5400
|
+
function record_ts() {
|
|
5401
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
5402
|
+
}
|
|
5403
|
+
function warn_append(error) {
|
|
5404
|
+
logger.warn("session persistence failed; continuing without transcript", error);
|
|
5405
|
+
}
|
|
5406
|
+
function create_session_recorder(handle) {
|
|
5407
|
+
let chain = Promise.resolve();
|
|
5408
|
+
const enqueue = (write) => {
|
|
5409
|
+
chain = chain.then(write).catch(warn_append);
|
|
4557
5410
|
};
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
}
|
|
4566
|
-
function split_keep_recent(non_system, keep_recent) {
|
|
4567
|
-
let cut = Math.max(0, non_system.length - keep_recent);
|
|
4568
|
-
while (cut > 0 && non_system[cut]?.role === "tool") {
|
|
4569
|
-
cut -= 1;
|
|
4570
|
-
}
|
|
4571
|
-
return { recent: non_system.slice(cut), older: non_system.slice(0, cut) };
|
|
4572
|
-
}
|
|
4573
|
-
async function compress_messages(deps, messages, params) {
|
|
4574
|
-
const system_messages = messages.filter((message) => message.role === "system");
|
|
4575
|
-
const non_system = messages.filter((message) => message.role !== "system");
|
|
4576
|
-
const keep_recent = Math.max(0, params.keep_recent);
|
|
4577
|
-
const { recent, older } = split_keep_recent(non_system, keep_recent);
|
|
4578
|
-
if (older.length === 0) {
|
|
4579
|
-
return { messages: [...messages], summary_chars: 0 };
|
|
4580
|
-
}
|
|
4581
|
-
try {
|
|
4582
|
-
const result = await deps.chat(
|
|
4583
|
-
[{ role: "system", content: COMPRESSION_SYSTEM_PROMPT }, build_summary_request(older, params.model_hint)],
|
|
4584
|
-
[],
|
|
4585
|
-
{ signal: params.signal }
|
|
4586
|
-
);
|
|
4587
|
-
const summary = result.message.content;
|
|
4588
|
-
const summary_message = {
|
|
4589
|
-
role: "user",
|
|
4590
|
-
content: `[context summary of earlier turns]
|
|
4591
|
-
${summary}
|
|
4592
|
-
[end summary]`
|
|
4593
|
-
};
|
|
4594
|
-
return { messages: [...system_messages, summary_message, ...recent], summary_chars: summary.length };
|
|
4595
|
-
} catch (error) {
|
|
4596
|
-
log_compression_failure(error);
|
|
4597
|
-
return { messages: [...messages], summary_chars: 0 };
|
|
4598
|
-
}
|
|
4599
|
-
}
|
|
4600
|
-
|
|
4601
|
-
// src/agent/loop.ts
|
|
4602
|
-
var DEFAULT_COMPRESS_THRESHOLD = 0.8;
|
|
4603
|
-
var KEEP_RECENT_TURNS = 8;
|
|
4604
|
-
function turn_range(max_turns) {
|
|
4605
|
-
return Array.from({ length: Math.max(0, max_turns) }, (_unused, index) => index + 1);
|
|
4606
|
-
}
|
|
4607
|
-
function seed_system_prompt(messages, system_prompt) {
|
|
4608
|
-
const history = [...messages];
|
|
4609
|
-
if (system_prompt === void 0) {
|
|
4610
|
-
return history;
|
|
4611
|
-
}
|
|
4612
|
-
const system_index = history.findIndex((message) => message.role === "system");
|
|
4613
|
-
if (system_index === -1) {
|
|
4614
|
-
const seeded = { role: "system", content: system_prompt };
|
|
4615
|
-
return [seeded, ...history];
|
|
4616
|
-
}
|
|
4617
|
-
const existing = history[system_index];
|
|
4618
|
-
if (existing !== void 0 && existing.content === system_prompt) {
|
|
4619
|
-
return history;
|
|
4620
|
-
}
|
|
4621
|
-
const replaced = { role: "system", content: system_prompt };
|
|
4622
|
-
return [...history.slice(0, system_index), replaced, ...history.slice(system_index + 1)];
|
|
4623
|
-
}
|
|
4624
|
-
function format_tool_result_content(result) {
|
|
4625
|
-
if (result.error !== void 0) {
|
|
4626
|
-
return JSON.stringify({ ok: false, output: result.output, error: result.error });
|
|
4627
|
-
}
|
|
4628
|
-
return result.output;
|
|
4629
|
-
}
|
|
4630
|
-
async function run_tool_calls(deps, history, turn, calls, emitter, signal) {
|
|
4631
|
-
for (const call of calls) {
|
|
4632
|
-
if (signal_aborted(signal) === true) {
|
|
4633
|
-
history.push(cancelled_tool_message(call));
|
|
4634
|
-
continue;
|
|
5411
|
+
const append = (record) => handle.append(record);
|
|
5412
|
+
const append_message = (message) => append({ ts: record_ts(), kind: "message", message });
|
|
5413
|
+
const append_meta = (meta) => append({ ts: record_ts(), kind: "meta", meta });
|
|
5414
|
+
const on_event = (event) => {
|
|
5415
|
+
if (event.type === "llm_end") {
|
|
5416
|
+
enqueue(() => append_message(event.result.message));
|
|
5417
|
+
return;
|
|
4635
5418
|
}
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
role: "tool",
|
|
4640
|
-
tool_call_id: call.id,
|
|
4641
|
-
name: call.name,
|
|
4642
|
-
content: format_tool_result_content(result)
|
|
4643
|
-
};
|
|
4644
|
-
if (result.ok !== true) {
|
|
4645
|
-
tool_message.is_error = true;
|
|
5419
|
+
if (event.type === "tool_call_end") {
|
|
5420
|
+
enqueue(() => append_message(tool_message_from_result(event.call, event.result)));
|
|
5421
|
+
return;
|
|
4646
5422
|
}
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
return signal_aborted(signal) === true ? "aborted" : "continued";
|
|
4651
|
-
}
|
|
4652
|
-
function cancelled_tool_message(call) {
|
|
4653
|
-
return {
|
|
4654
|
-
role: "tool",
|
|
4655
|
-
tool_call_id: call.id,
|
|
4656
|
-
name: call.name,
|
|
4657
|
-
content: format_tool_result_content({ ok: false, output: "", error: "cancelled" }),
|
|
4658
|
-
is_error: true
|
|
4659
|
-
};
|
|
4660
|
-
}
|
|
4661
|
-
async function call_chat(deps, history, params, emitter) {
|
|
4662
|
-
try {
|
|
4663
|
-
return await deps.chat(history, deps.definitions(), {
|
|
4664
|
-
temperature: params.temperature,
|
|
4665
|
-
max_tokens: params.max_tokens,
|
|
4666
|
-
signal: params.signal
|
|
4667
|
-
});
|
|
4668
|
-
} catch (error) {
|
|
4669
|
-
if (signal_aborted(params.signal) === true) {
|
|
4670
|
-
throw error;
|
|
5423
|
+
if (event.type === "budget_exhausted") {
|
|
5424
|
+
enqueue(() => append_meta({ event: "budget_exhausted" }));
|
|
5425
|
+
return;
|
|
4671
5426
|
}
|
|
4672
|
-
if (
|
|
4673
|
-
|
|
4674
|
-
} else {
|
|
4675
|
-
logger.error("agent chat call failed", error);
|
|
5427
|
+
if (event.type === "compress_end") {
|
|
5428
|
+
enqueue(() => append_meta({ event: "compress_end", summary_chars: event.summary_chars }));
|
|
4676
5429
|
}
|
|
4677
|
-
emitter?.emit({ type: "error", error });
|
|
4678
|
-
throw error;
|
|
4679
|
-
}
|
|
4680
|
-
}
|
|
4681
|
-
async function compress_if_needed(deps, history, params, emitter) {
|
|
4682
|
-
const budget_tokens = params.context_budget_tokens;
|
|
4683
|
-
if (budget_tokens === void 0) {
|
|
4684
|
-
return;
|
|
4685
|
-
}
|
|
4686
|
-
const threshold = params.compress_threshold ?? DEFAULT_COMPRESS_THRESHOLD;
|
|
4687
|
-
if (!should_compress(history, budget_tokens, threshold)) {
|
|
4688
|
-
return;
|
|
4689
|
-
}
|
|
4690
|
-
const non_system_count = history.filter((message) => message.role !== "system").length;
|
|
4691
|
-
if (non_system_count <= KEEP_RECENT_TURNS) {
|
|
4692
|
-
return;
|
|
4693
|
-
}
|
|
4694
|
-
emitter?.emit({ type: "compress_start", estimated_tokens: estimate_messages_tokens(history) });
|
|
4695
|
-
const outcome = await compress_messages(
|
|
4696
|
-
{ chat: deps.chat },
|
|
4697
|
-
history,
|
|
4698
|
-
{ budget_tokens, keep_recent: KEEP_RECENT_TURNS, signal: params.signal }
|
|
4699
|
-
);
|
|
4700
|
-
history.length = 0;
|
|
4701
|
-
for (const message of outcome.messages) {
|
|
4702
|
-
history.push(message);
|
|
4703
|
-
}
|
|
4704
|
-
emitter?.emit({ type: "compress_end", summary_chars: outcome.summary_chars });
|
|
4705
|
-
}
|
|
4706
|
-
function find_last_assistant(messages) {
|
|
4707
|
-
return [...messages].reverse().find((message) => message.role === "assistant");
|
|
4708
|
-
}
|
|
4709
|
-
function signal_aborted(signal) {
|
|
4710
|
-
return signal?.aborted === true;
|
|
4711
|
-
}
|
|
4712
|
-
function aborted_outcome(history, turns_used, emitter) {
|
|
4713
|
-
emitter?.emit({ type: "error", error: new DOMException("agent loop aborted", "AbortError") });
|
|
4714
|
-
return {
|
|
4715
|
-
messages: history,
|
|
4716
|
-
final: find_last_assistant(history),
|
|
4717
|
-
result: void 0,
|
|
4718
|
-
turns_used,
|
|
4719
|
-
stopped_reason: "aborted"
|
|
4720
5430
|
};
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
return aborted_outcome(history, turn - 1, emitter);
|
|
5431
|
+
const seed = async (seed_opts) => {
|
|
5432
|
+
const history_size = seed_opts.history.length;
|
|
5433
|
+
await append_meta({
|
|
5434
|
+
event: "run_start",
|
|
5435
|
+
input_chars: seed_opts.input.length,
|
|
5436
|
+
history_size
|
|
5437
|
+
}).catch(warn_append);
|
|
5438
|
+
const needs_history = seed_opts.owned || seeded_handles.has(handle) === false;
|
|
5439
|
+
if (needs_history) {
|
|
5440
|
+
seeded_handles.add(handle);
|
|
5441
|
+
const has_system = seed_opts.history.some((message) => message.role === "system");
|
|
5442
|
+
if (has_system === false && seed_opts.system_prompt !== void 0) {
|
|
5443
|
+
await append_message({ role: "system", content: seed_opts.system_prompt }).catch(warn_append);
|
|
5444
|
+
}
|
|
5445
|
+
for (const message of seed_opts.history) {
|
|
5446
|
+
await append_message(message).catch(warn_append);
|
|
4738
5447
|
}
|
|
4739
|
-
throw error;
|
|
4740
|
-
}
|
|
4741
|
-
emitter?.emit({ type: "llm_end", turn, result });
|
|
4742
|
-
history.push(result.message);
|
|
4743
|
-
const calls = result.message.tool_calls ?? [];
|
|
4744
|
-
if (calls.length === 0) {
|
|
4745
|
-
emitter?.emit({ type: "final", message: result.message, result });
|
|
4746
|
-
emitter?.emit({ type: "turn_end", turn });
|
|
4747
|
-
return { messages: history, final: result.message, result, turns_used: turn, stopped_reason: "final" };
|
|
4748
|
-
}
|
|
4749
|
-
const tool_status = await run_tool_calls(deps, history, turn, calls, emitter, params.signal);
|
|
4750
|
-
if (tool_status === "aborted") {
|
|
4751
|
-
return aborted_outcome(history, turn, emitter);
|
|
4752
5448
|
}
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
5449
|
+
await append_message({ role: "user", content: seed_opts.input }).catch(warn_append);
|
|
5450
|
+
};
|
|
5451
|
+
const flush = () => chain;
|
|
5452
|
+
const finish = async (stopped_reason, usage_total) => {
|
|
5453
|
+
await flush();
|
|
5454
|
+
await append_meta({
|
|
5455
|
+
event: "run_end",
|
|
5456
|
+
stopped_reason,
|
|
5457
|
+
usage: {
|
|
5458
|
+
prompt_tokens: usage_total.prompt_tokens,
|
|
5459
|
+
completion_tokens: usage_total.completion_tokens,
|
|
5460
|
+
total_tokens: usage_total.total_tokens
|
|
5461
|
+
}
|
|
5462
|
+
}).catch(warn_append);
|
|
4762
5463
|
};
|
|
5464
|
+
return { path: handle.path, on_event, seed, finish, flush };
|
|
4763
5465
|
}
|
|
4764
5466
|
|
|
4765
5467
|
// src/agent/agent.ts
|
|
@@ -4777,28 +5479,21 @@ function filter_registry(base, enabled) {
|
|
|
4777
5479
|
}
|
|
4778
5480
|
return filtered;
|
|
4779
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
|
+
}
|
|
4780
5487
|
function collect_usage(total) {
|
|
4781
5488
|
return (event) => {
|
|
4782
5489
|
if (event.type === "llm_end") {
|
|
4783
|
-
total
|
|
4784
|
-
|
|
4785
|
-
total.total_tokens += event.result.usage.total_tokens;
|
|
5490
|
+
add_usage(total, event.result.usage);
|
|
5491
|
+
return;
|
|
4786
5492
|
}
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
function append_meta(handle, meta) {
|
|
4790
|
-
return handle.append({ ts: (/* @__PURE__ */ new Date()).toISOString(), kind: "meta", meta });
|
|
4791
|
-
}
|
|
4792
|
-
function append_run_end(handle, stopped_reason, usage_total) {
|
|
4793
|
-
return append_meta(handle, {
|
|
4794
|
-
event: "run_end",
|
|
4795
|
-
stopped_reason,
|
|
4796
|
-
usage: {
|
|
4797
|
-
prompt_tokens: usage_total.prompt_tokens,
|
|
4798
|
-
completion_tokens: usage_total.completion_tokens,
|
|
4799
|
-
total_tokens: usage_total.total_tokens
|
|
5493
|
+
if (event.type === "compress_end" && event.usage !== void 0) {
|
|
5494
|
+
add_usage(total, event.usage);
|
|
4800
5495
|
}
|
|
4801
|
-
}
|
|
5496
|
+
};
|
|
4802
5497
|
}
|
|
4803
5498
|
function register_plugin_tools(registry, plugins) {
|
|
4804
5499
|
for (const loaded of plugins) {
|
|
@@ -4858,13 +5553,30 @@ var Agent = class {
|
|
|
4858
5553
|
}
|
|
4859
5554
|
async run(options) {
|
|
4860
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) {
|
|
4861
5563
|
const usage_total = { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 };
|
|
4862
5564
|
const run_events = new AgentEmitter();
|
|
4863
5565
|
const stop_forwarding = run_events.on((event) => this.events.emit(event));
|
|
4864
5566
|
const stop_collecting = run_events.on(collect_usage(usage_total));
|
|
5567
|
+
const recorder = await this.open_recorder(options);
|
|
5568
|
+
const stop_recording = recorder === void 0 ? void 0 : run_events.on((event) => recorder.on_event(event));
|
|
4865
5569
|
await this.call_plugin_run_start(options.input);
|
|
4866
5570
|
let outcome;
|
|
4867
5571
|
try {
|
|
5572
|
+
if (recorder !== void 0) {
|
|
5573
|
+
await recorder.seed({
|
|
5574
|
+
input: options.input,
|
|
5575
|
+
history: options.history ?? [],
|
|
5576
|
+
system_prompt: this.config.system_prompt ?? DEFAULT_AGENT_SYSTEM_PROMPT,
|
|
5577
|
+
owned: options.session === void 0
|
|
5578
|
+
});
|
|
5579
|
+
}
|
|
4868
5580
|
const seed_messages = [...options.history ?? []];
|
|
4869
5581
|
seed_messages.push({ role: "user", content: options.input });
|
|
4870
5582
|
const tool_context = {
|
|
@@ -4882,14 +5594,23 @@ var Agent = class {
|
|
|
4882
5594
|
signal: options.signal
|
|
4883
5595
|
});
|
|
4884
5596
|
} finally {
|
|
5597
|
+
stop_recording?.();
|
|
4885
5598
|
stop_collecting();
|
|
4886
5599
|
stop_forwarding();
|
|
5600
|
+
if (recorder !== void 0) {
|
|
5601
|
+
await recorder.flush();
|
|
5602
|
+
}
|
|
4887
5603
|
if (outcome !== void 0) {
|
|
4888
5604
|
await this.call_plugin_run_end(outcome);
|
|
4889
5605
|
}
|
|
4890
5606
|
}
|
|
4891
|
-
|
|
4892
|
-
|
|
5607
|
+
if (outcome === void 0) {
|
|
5608
|
+
throw new Error("agent run ended without outcome");
|
|
5609
|
+
}
|
|
5610
|
+
if (recorder !== void 0) {
|
|
5611
|
+
await recorder.finish(outcome.stopped_reason, usage_total);
|
|
5612
|
+
}
|
|
5613
|
+
return { outcome, messages: outcome.messages, usage_total, session_path: recorder?.path };
|
|
4893
5614
|
}
|
|
4894
5615
|
/** Close MCP sessions so stdio children do not keep the event loop alive. */
|
|
4895
5616
|
close() {
|
|
@@ -4935,19 +5656,11 @@ var Agent = class {
|
|
|
4935
5656
|
ctx
|
|
4936
5657
|
);
|
|
4937
5658
|
}
|
|
4938
|
-
/** Best-effort
|
|
4939
|
-
async
|
|
5659
|
+
/** Best-effort recorder: open failures warn and skip persistence for this run. */
|
|
5660
|
+
async open_recorder(options) {
|
|
4940
5661
|
try {
|
|
4941
|
-
const handle = await open_session(this.config.session_dir, options.label);
|
|
4942
|
-
|
|
4943
|
-
for (const message of outcome.messages) {
|
|
4944
|
-
await handle.append({ ts: (/* @__PURE__ */ new Date()).toISOString(), kind: "message", message });
|
|
4945
|
-
}
|
|
4946
|
-
if (outcome.stopped_reason === "budget") {
|
|
4947
|
-
await append_meta(handle, { event: "budget_exhausted" });
|
|
4948
|
-
}
|
|
4949
|
-
await append_run_end(handle, outcome.stopped_reason, usage_total);
|
|
4950
|
-
return handle.path;
|
|
5662
|
+
const handle = options.session ?? await open_session(this.config.session_dir, options.label);
|
|
5663
|
+
return create_session_recorder(handle);
|
|
4951
5664
|
} catch (error) {
|
|
4952
5665
|
logger.warn("session persistence failed; continuing without transcript", error);
|
|
4953
5666
|
return void 0;
|
|
@@ -4975,8 +5688,6 @@ async function run_agent(raw_config, input, options) {
|
|
|
4975
5688
|
}
|
|
4976
5689
|
|
|
4977
5690
|
export {
|
|
4978
|
-
safe_json_parse,
|
|
4979
|
-
truncate_text,
|
|
4980
5691
|
DEFAULT_GATEWAY_TOKEN_ENVS,
|
|
4981
5692
|
is_env_var_name,
|
|
4982
5693
|
platform_token_env,
|
|
@@ -4992,6 +5703,7 @@ export {
|
|
|
4992
5703
|
plugin_errors_summary,
|
|
4993
5704
|
sleep,
|
|
4994
5705
|
ProviderError,
|
|
5706
|
+
run_conversation,
|
|
4995
5707
|
gateway_tools_enabled,
|
|
4996
5708
|
check_gateway_sender,
|
|
4997
5709
|
parse_agent_config,
|
|
@@ -5001,4 +5713,4 @@ export {
|
|
|
5001
5713
|
create_agent_with_plugins,
|
|
5002
5714
|
run_agent
|
|
5003
5715
|
};
|
|
5004
|
-
//# sourceMappingURL=chunk-
|
|
5716
|
+
//# sourceMappingURL=chunk-KOEZQUIX.js.map
|