@moikapy/lich 0.8.0 → 0.9.0

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