@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +38 -19
  3. package/dist/{chunk-CVX7LZWC.js → chunk-4N3S6ACI.js} +2 -2
  4. package/dist/{chunk-WNFBIX4E.js → chunk-KOEZQUIX.js} +1312 -600
  5. package/dist/chunk-KOEZQUIX.js.map +1 -0
  6. package/dist/{chunk-PZNYVGD4.js → chunk-P2YEHNM4.js} +2 -2
  7. package/dist/chunk-P2YEHNM4.js.map +1 -0
  8. package/dist/chunk-PL6MKRKE.js +75 -0
  9. package/dist/chunk-PL6MKRKE.js.map +1 -0
  10. package/dist/chunk-TYRKVBWQ.js +104 -0
  11. package/dist/chunk-TYRKVBWQ.js.map +1 -0
  12. package/dist/cli.d.ts +19 -1
  13. package/dist/cli.js +125 -47
  14. package/dist/cli.js.map +1 -1
  15. package/dist/{gateway-44QTIJTJ.js → gateway-OSB7254F.js} +209 -46
  16. package/dist/gateway-OSB7254F.js.map +1 -0
  17. package/dist/index.d.ts +122 -62
  18. package/dist/index.js +12 -4
  19. package/dist/resolve-H22TOVB5.js +7 -0
  20. package/dist/resolve-H22TOVB5.js.map +1 -0
  21. package/dist/store-YYLEB7JG.js +9 -0
  22. package/dist/store-YYLEB7JG.js.map +1 -0
  23. package/dist/{tui-MEJGEILU.js → tui-XXIU4W7K.js} +216 -25
  24. package/dist/tui-XXIU4W7K.js.map +1 -0
  25. package/docs/architecture/agent-loop.md +28 -9
  26. package/docs/architecture/overview.md +10 -9
  27. package/docs/architecture/tools.md +3 -1
  28. package/docs/getting-started.md +6 -6
  29. package/docs/index.md +1 -1
  30. package/docs/user-guide/cli.md +17 -8
  31. package/docs/user-guide/games.md +5 -4
  32. package/docs/user-guide/library.md +6 -3
  33. package/docs/user-guide/redot.md +2 -2
  34. package/docs/user-guide/tui.md +3 -2
  35. package/package.json +1 -1
  36. package/dist/chunk-PZNYVGD4.js.map +0 -1
  37. package/dist/chunk-WNFBIX4E.js.map +0 -1
  38. package/dist/gateway-44QTIJTJ.js.map +0 -1
  39. package/dist/tui-MEJGEILU.js.map +0 -1
  40. /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 text = await response.text();
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, text)}
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, text) {
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 : Buffer.byteLength(text, "utf8");
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
- function match_lines(lines, regex) {
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 !== void 0 && regex.test(line) === true) {
1068
- hits.push({ line_no: index + 1, text: line.trim() });
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
- async function search_file(frame, work_dir, relative_root, regex, collected, max_results) {
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
- async function run_grep(args, work_dir) {
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 = Math.max(1, Math.floor(optional_number_arg(args, "max_results", DEFAULT_MAX_RESULTS2)));
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 text = await response.text();
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 MAX_MAX_RESULTS = 500;
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, MAX_MAX_RESULTS);
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
- timeout_signal.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
1594
- external?.addEventListener("abort", () => child.kill("SIGKILL"), { once: true });
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 wait_close(child) {
1735
+ function wait_exit(child) {
1597
1736
  return new Promise((resolve) => {
1598
- child.on("close", (code) => resolve(code ?? -1));
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.stdout.on("data", (chunk) => stream_chunk(stdout, chunk));
1610
- child.stderr.on("data", (chunk) => stream_chunk(stderr, chunk));
1611
- const close_promise = wait_close(child);
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 close_promise;
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
- exit_code = await close_promise;
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 default_runner(command, cwd, on_chunk) {
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("close", (code) => resolve({ exit_code: code ?? -1 }));
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: 6e5,
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] + chunk.toString("utf8");
1878
+ streams[stream] = append_clamped(streams[stream], chunk, MAX_OUTPUT_CHARS);
1710
1879
  };
1711
- const outcome = await run_test_command(command, context.work_dir, on_chunk);
1712
- const ok = outcome.exit_code === 0;
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 ${outcome.exit_code}]`, MAX_OUTPUT_CHARS),
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 DEFAULT_TIMEOUT_MS4 = 2e4;
1729
- var MAX_TIMEOUT_MS4 = 6e4;
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", DEFAULT_TIMEOUT_MS4, MAX_TIMEOUT_MS4);
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 hits = parse_results(await response.text(), max_results);
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
- plugin_state.set(plugin, /* @__PURE__ */ new Map());
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/agent/config.ts
2242
- import { z } from "zod";
2243
-
2244
- // src/gateway/access.ts
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 is_gateway_sender_allowed(config, platform, chat_id, user_id) {
2259
- if (PUBLIC_PLATFORMS.has(platform) === false) {
2260
- return true;
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
- const users = config.gateway?.allowed_users?.[platform] ?? [];
2263
- const chats = config.gateway?.allowed_chats?.[platform] ?? [];
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
- const user_ok = users.length === 0 || users.includes(user_id);
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 check_gateway_sender(config, platform, chat_id, user_id) {
2272
- if (is_gateway_sender_allowed(config, platform, chat_id, user_id) === true) {
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/mcp/mcp_pin.ts
2280
- import path11 from "path";
2281
-
2282
- // src/mcp/mcp_catalog.ts
2283
- import { existsSync as existsSync2, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "fs";
2284
- import path9 from "path";
2285
- import { fileURLToPath as fileURLToPath2 } from "url";
2286
- var cached;
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 read_manifest(file) {
2299
- const parsed = safe_json_parse(readFileSync4(file, "utf8"));
2300
- if (parsed === void 0 || typeof parsed.name !== "string") {
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 load_catalog() {
2306
- if (cached !== void 0) {
2307
- return cached;
2476
+ function truncate_head_tail(text, max_chars) {
2477
+ if (max_chars <= 0) {
2478
+ return "";
2308
2479
  }
2309
- const manifests = [];
2310
- for (const name of readdirSync3(catalog_dir())) {
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
- cached = manifests;
2317
- return cached;
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 catalog_by_name(name) {
2320
- return load_catalog().find((entry) => entry.name === name);
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
- // src/mcp/mcp_refuse.ts
2324
- import path10 from "path";
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 base = path10.basename(command);
2338
- if (DOWNLOADERS.has(base) === true) {
2339
- return `refused download command '${base}'`;
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 void 0;
2507
+ return truncate_head_tail(joined, max_chars);
2342
2508
  }
2343
- function refuse_stdio_arg(arg) {
2344
- if (arg.includes("://") === true) {
2345
- return "refused url in mcp args";
2346
- }
2347
- if (SHELL.test(arg) === true) {
2348
- return "refused shell metacharacters in mcp args";
2349
- }
2350
- return void 0;
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 = catalog_by_name(name);
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: z.array(provider_schema).min(1),
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
- async function post_rpc(url, fetch_fn, body) {
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
- async function read_id(child, id) {
2642
- for (let skipped = 0; skipped < SKIP_LIMIT; skipped += 1) {
2643
- const line = await child.read_line();
2644
- const failure = child.failed();
2645
- if (failure !== void 0) {
2646
- throw new Error(failure);
2647
- }
2648
- if (line === void 0) {
2649
- throw new Error("mcp closed the pipe");
2650
- }
2651
- const parsed = parse_rpc_line(line);
2652
- if (parsed === void 0 || parsed.id !== id) {
2653
- continue;
2654
- }
2655
- if (parsed.error !== void 0) {
2656
- const detail = parsed.error.message;
2657
- throw new Error(typeof detail === "string" && detail.length > 0 ? detail : "mcp error");
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
- return parsed.result;
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
- throw new Error("mcp sent no matching response");
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
- return read_id(child, id);
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(catalog_by_name(server)?.exclude_tools ?? []);
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
- throw new Error(text.length > 0 ? text : "mcp tool failed");
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: typeof tool.description === "string" ? tool.description : tool.name,
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 child = env === void 0 ? Bun.spawn([command, ...args], options) : Bun.spawn([command, ...args], { ...options, env });
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
- async function open_and_register(registry, name, enabled, session) {
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
- register_listed(registry, name, await session.list_tools(), enabled, session);
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
- if (info.ok !== true) {
3290
- return;
3291
- }
3292
- if (info.tool_name === "write_file" || info.tool_name === "edit_file") {
3293
- ctx.state?.set("dirty", true);
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
- } else if (info.tool_name === "git_commit") {
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 && error.retry_after_ms > backoff) {
3398
- return error.retry_after_ms;
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|maximum/i;
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 = 4096;
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: map_stop_reason(dto.stop_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
- } else if (block.type === "tool_use") {
3699
- tool_calls.push(tool_use_to_call(block, text_parts));
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
- ...tool_calls.length > 0 ? { tool_calls } : {}
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 { id: block.id ?? "", name: block.name ?? "", args: {} };
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|maximum|too long/i;
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
- const has_any = wire_options.temperature !== void 0 || wire_options.num_predict !== void 0;
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 parsed_calls = parse_tool_calls(dto.message.tool_calls ?? [], content_parts);
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: map_done_reason(dto.done_reason, has_tool_calls),
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: normalize_tool_arguments(raw_call.function?.arguments, content_parts)
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|length/i;
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
- body.max_tokens = options.max_tokens;
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: map_finish_reason(choice.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 dto.tool_calls ?? []) {
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({ id: raw_call.id ?? "", name: raw_call.function?.name ?? "", args: parsed_arguments });
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
- logger.warn(`provider "${error.provider_name}" failed with ${error.kind}; failing over to next provider`);
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/store.ts
4498
- import { appendFile, mkdir as mkdir2, readFile as readFile4 } from "fs/promises";
4499
- import path14 from "path";
4500
- var counter_state = { value: 0 };
4501
- function slugify_label(label) {
4502
- const slug = label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
4503
- return slug.length > 0 ? `-${slug}` : "";
4504
- }
4505
- async function open_session(dir, label) {
4506
- await mkdir2(dir, { recursive: true });
4507
- counter_state.value += 1;
4508
- const label_part = label === void 0 ? "" : slugify_label(label);
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
- function log_compression_failure(error) {
4560
- if (error instanceof ProviderError) {
4561
- logger.warn(`context compression failed kind=${error.kind} provider=${error.provider_name}`, error);
4562
- return;
4563
- }
4564
- logger.warn("context compression failed", error);
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
- emitter?.emit({ type: "tool_call_start", turn, call });
4637
- const result = await deps.tools.execute(call.name, call.args, deps.tool_context);
4638
- const tool_message = {
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
- history.push(tool_message);
4648
- emitter?.emit({ type: "tool_call_end", turn, call, result });
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 (error instanceof ProviderError) {
4673
- logger.error(`provider error kind=${error.kind} provider=${error.provider_name}`, error);
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
- async function run_conversation(deps, messages, params) {
4723
- const history = seed_system_prompt(messages, params.system_prompt);
4724
- const emitter = deps.emitter;
4725
- for (const turn of turn_range(params.max_turns)) {
4726
- if (signal_aborted(params.signal) === true) {
4727
- return aborted_outcome(history, turn - 1, emitter);
4728
- }
4729
- emitter?.emit({ type: "turn_start", turn });
4730
- await compress_if_needed(deps, history, params, emitter);
4731
- emitter?.emit({ type: "llm_start", turn });
4732
- let result;
4733
- try {
4734
- result = await call_chat(deps, history, params, emitter);
4735
- } catch (error) {
4736
- if (signal_aborted(params.signal) === true) {
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
- emitter?.emit({ type: "budget_exhausted", turns_used: params.max_turns });
4755
- emitter?.emit({ type: "turn_end", turn: params.max_turns });
4756
- return {
4757
- messages: history,
4758
- final: find_last_assistant(history),
4759
- result: void 0,
4760
- turns_used: params.max_turns,
4761
- stopped_reason: "budget"
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.prompt_tokens += event.result.usage.prompt_tokens;
4784
- total.completion_tokens += event.result.usage.completion_tokens;
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
- const session_path = await this.persist_session(outcome, options, usage_total);
4892
- return { outcome, messages: outcome.messages, usage_total, session_path };
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 JSONL transcript: never fails the run, returns undefined path on error. */
4939
- async persist_session(outcome, options, usage_total) {
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
- await append_meta(handle, { event: "run_start", input_chars: options.input.length, history_size: outcome.messages.length });
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-WNFBIX4E.js.map
5716
+ //# sourceMappingURL=chunk-KOEZQUIX.js.map