@livx.cc/agentx 0.99.27 → 0.99.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -8,6 +8,274 @@ var __export = (target, all) => {
8
8
  __defProp(target, name, { get: all[name], enumerable: true });
9
9
  };
10
10
 
11
+ // src/logging.ts
12
+ import { log } from "libx.js/src/modules/log";
13
+ var forComponent;
14
+ var init_logging = __esm({
15
+ "src/logging.ts"() {
16
+ "use strict";
17
+ forComponent = (name) => log.forComponent(name);
18
+ }
19
+ });
20
+
21
+ // src/llm.ts
22
+ import { readFileSync, statSync, realpathSync } from "fs";
23
+ import { resolve, sep } from "path";
24
+ import { randomBytes } from "crypto";
25
+ function contentText(content) {
26
+ if (content == null) return "";
27
+ if (typeof content === "string") return content;
28
+ return content.map((p) => p.type === "text" ? p.text ?? "" : "[image]").join(p_sep(content));
29
+ }
30
+ function imagePart(url) {
31
+ return { type: "image_url", image_url: { url } };
32
+ }
33
+ function messageHasInlineImage(content) {
34
+ if (content == null || typeof content === "string") return false;
35
+ return content.some((p) => p.type === "image_url" && (p.image_url?.url ?? "").startsWith("data:"));
36
+ }
37
+ function contentBytes(content) {
38
+ if (content == null) return 0;
39
+ if (typeof content === "string") return content.length;
40
+ let n = 0;
41
+ for (const p of content) {
42
+ if (p.type === "text") {
43
+ n += (p.text ?? "").length;
44
+ continue;
45
+ }
46
+ const url = p.image_url?.url ?? "";
47
+ const ref = parseImageRef(url);
48
+ n += (ref ? ref.bytes : url.length) || 7;
49
+ }
50
+ return n;
51
+ }
52
+ function elideInlineImages(content, stub = IMAGE_ELIDE_STUB) {
53
+ return elideImageParts(content, (url) => url.startsWith("data:"), stub);
54
+ }
55
+ function elideImages(content, stub = IMAGE_ELIDE_STUB) {
56
+ return elideImageParts(content, (url) => url.startsWith("data:") || isImageRefUrl(url), stub);
57
+ }
58
+ function elideImageParts(content, hit, stub) {
59
+ if (typeof content === "string") return content;
60
+ let changed = false;
61
+ const out = [];
62
+ for (const p of content) {
63
+ if (p.type !== "image_url" || !hit(p.image_url?.url ?? "")) {
64
+ out.push(p);
65
+ continue;
66
+ }
67
+ changed = true;
68
+ out.push({ type: "text", text: stub });
69
+ }
70
+ return changed ? out : content;
71
+ }
72
+ function mintImageRef(path, mime) {
73
+ const token = randomBytes(16).toString("hex");
74
+ let bytes = 0;
75
+ try {
76
+ bytes = Math.ceil(statSync(path).size * 4 / 3);
77
+ } catch (e) {
78
+ log2.warn(`image ref stat failed, weighing as 0: ${path}`, e);
79
+ }
80
+ refRegistry.set(token, { mime, path, bytes });
81
+ while (refRegistry.size > REF_REGISTRY_MAX) refRegistry.delete(refRegistry.keys().next().value);
82
+ return `${IMAGE_REF_SCHEME}${mime};${token};${path}`;
83
+ }
84
+ function clearImageRefs() {
85
+ refRegistry.clear();
86
+ }
87
+ function imageRefPart(path, mime) {
88
+ return { type: "image_url", image_url: { url: mintImageRef(path, mime) } };
89
+ }
90
+ function parseImageRef(url) {
91
+ const parts = refUrlParts(url);
92
+ const ref = parts && refRegistry.get(parts.token);
93
+ if (!ref) {
94
+ log2.debug("image ref not in registry \u2014 treating as non-image text");
95
+ return null;
96
+ }
97
+ return ref;
98
+ }
99
+ function refUrlParts(url) {
100
+ if (!isImageRefUrl(url)) return null;
101
+ const rest = url.slice(IMAGE_REF_SCHEME.length);
102
+ const a = rest.indexOf(";");
103
+ if (a < 0) return null;
104
+ const b = rest.indexOf(";", a + 1);
105
+ return { mime: rest.slice(0, a), token: b < 0 ? rest.slice(a + 1) : rest.slice(a + 1, b), path: b < 0 ? "" : rest.slice(b + 1) };
106
+ }
107
+ function remintImageRefs(messages, roots) {
108
+ const allowed = [];
109
+ for (const r of roots) {
110
+ try {
111
+ allowed.push(realpathSync(resolve(r)));
112
+ } catch (e) {
113
+ log2.debug(`image-ref root unavailable: ${r}`, e);
114
+ }
115
+ }
116
+ if (allowed.length === 0) return messages;
117
+ const inside = (p) => allowed.some((root) => p === root || p.startsWith(root + sep));
118
+ let changed = false;
119
+ const out = messages.map((m) => {
120
+ if (m.role !== "user" && m.role !== "assistant" || typeof m.content === "string" || m.content == null) return m;
121
+ let touched = false;
122
+ const parts = m.content.map((p) => {
123
+ if (p.type !== "image_url") return p;
124
+ const f = refUrlParts(p.image_url?.url);
125
+ if (!f || refRegistry.has(f.token)) return p;
126
+ let real;
127
+ try {
128
+ real = realpathSync(f.path);
129
+ } catch (e) {
130
+ log2.warn(`resumed image ref unreadable, will stub: ${f.path}`, e);
131
+ return p;
132
+ }
133
+ if (!inside(real)) {
134
+ log2.warn(`resumed image ref outside the allowed roots, will stub: ${f.path} -> ${real}`);
135
+ return p;
136
+ }
137
+ touched = true;
138
+ return { type: "image_url", image_url: { url: mintImageRef(real, f.mime) } };
139
+ });
140
+ if (!touched) return m;
141
+ changed = true;
142
+ return { ...m, content: parts };
143
+ });
144
+ return changed ? out : messages;
145
+ }
146
+ function messageHasImageRef(content) {
147
+ if (content == null || typeof content === "string") return false;
148
+ return content.some((p) => p.type === "image_url" && isImageRefUrl(p.image_url?.url));
149
+ }
150
+ function messageHasImage(content) {
151
+ return messageHasInlineImage(content) || messageHasImageRef(content);
152
+ }
153
+ function imageRefResult(path, mime) {
154
+ return JSON.stringify({ imageRef: mintImageRef(path, mime), image: path });
155
+ }
156
+ function toolImageRef(content) {
157
+ if (typeof content !== "string" || !content.startsWith('{"imageRef":')) return null;
158
+ try {
159
+ return parseImageRef(JSON.parse(content).imageRef);
160
+ } catch (e) {
161
+ log2.debug("bad imageRef tool result", e);
162
+ return null;
163
+ }
164
+ }
165
+ function elideStaleImages(messages, keepRecent = 1) {
166
+ const idx = messages.map((m, i) => messageHasImage(m.content) ? i : -1).filter((i) => i >= 0);
167
+ const drop = new Set(idx.slice(0, Math.max(0, idx.length - keepRecent)));
168
+ if (drop.size === 0) return messages;
169
+ let changed = false;
170
+ const out = messages.map((m, i) => {
171
+ if (!drop.has(i)) return m;
172
+ const c = elideInlineImages(m.content);
173
+ if (c === m.content) return m;
174
+ changed = true;
175
+ return { ...m, content: c };
176
+ });
177
+ return changed ? out : messages;
178
+ }
179
+ function imageWindows(messages, keepRecent) {
180
+ const attach = [];
181
+ const tool = [];
182
+ messages.forEach((m, i) => {
183
+ if (toolImageRef(m.content)) tool.push(i);
184
+ else if (messageHasImage(m.content)) attach.push(i);
185
+ });
186
+ const k = Math.max(0, keepRecent);
187
+ return /* @__PURE__ */ new Set([...attach.slice(Math.max(0, attach.length - k)), ...tool.slice(Math.max(0, tool.length - k))]);
188
+ }
189
+ function expandImagesForSend(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
190
+ if (!messages.some(bearsImage)) return messages;
191
+ const keep = imageWindows(messages, keepRecent);
192
+ let changed = false;
193
+ const out = messages.map((m, i) => {
194
+ const content = keep.has(i) ? expandContent(m.content, stub) : stripContent(m.content, stub);
195
+ if (content === m.content) return m;
196
+ changed = true;
197
+ return { ...m, content };
198
+ });
199
+ return changed ? out : messages;
200
+ }
201
+ function sendBytes(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
202
+ let n = 0;
203
+ for (const b of sendBytesPer(messages, keepRecent, stub)) n += b;
204
+ return n;
205
+ }
206
+ function sendBytesPer(messages, keepRecent = 1, stub = IMAGE_ELIDE_STUB) {
207
+ const keep = messages.some(bearsImage) ? imageWindows(messages, keepRecent) : null;
208
+ return messages.map((m, i) => wireBytes(m.content, keep == null || keep.has(i), stub) + (m.tool_calls ? JSON.stringify(m.tool_calls).length : 0));
209
+ }
210
+ function wireBytes(content, kept, stub) {
211
+ const tool = toolImageRef(content);
212
+ if (tool) return kept ? tool.bytes : stub.length;
213
+ if (content == null || typeof content === "string") return contentBytes(content);
214
+ if (kept) return contentBytes(content);
215
+ let n = 0;
216
+ for (const p of content) {
217
+ if (p.type === "text") {
218
+ n += (p.text ?? "").length;
219
+ continue;
220
+ }
221
+ const url = p.image_url?.url ?? "";
222
+ n += url.startsWith("data:") || isImageRefUrl(url) ? stub.length : url.length || 7;
223
+ }
224
+ return n;
225
+ }
226
+ function readRefDataUrl(ref) {
227
+ try {
228
+ return `data:${ref.mime};base64,${readFileSync(ref.path).toString("base64")}`;
229
+ } catch (e) {
230
+ log2.warn(`image ref unreadable, sending stub instead: ${ref.path}`, e);
231
+ return null;
232
+ }
233
+ }
234
+ function expandContent(content, stub) {
235
+ const tool = toolImageRef(content);
236
+ if (tool) {
237
+ const url = readRefDataUrl(tool);
238
+ return url ? JSON.stringify({ dataUrl: url, image: tool.path }) : stub;
239
+ }
240
+ if (typeof content === "string") return content;
241
+ let changed = false;
242
+ const out = content.map((p) => {
243
+ if (p.type !== "image_url" || !isImageRefUrl(p.image_url?.url)) return p;
244
+ changed = true;
245
+ const ref = parseImageRef(p.image_url?.url);
246
+ const url = ref && readRefDataUrl(ref);
247
+ return url ? imagePart(url) : { type: "text", text: stub };
248
+ });
249
+ return changed ? out : content;
250
+ }
251
+ function stripContent(content, stub) {
252
+ if (toolImageRef(content)) return stub;
253
+ if (typeof content === "string") return content;
254
+ let changed = false;
255
+ const out = content.map((p) => {
256
+ const url = p.type === "image_url" ? p.image_url?.url ?? "" : "";
257
+ if (!url.startsWith("data:") && !isImageRefUrl(url)) return p;
258
+ changed = true;
259
+ return { type: "text", text: stub };
260
+ });
261
+ return changed ? out : content;
262
+ }
263
+ var log2, p_sep, IMAGE_ELIDE_STUB, IMAGE_REF_SCHEME, REF_REGISTRY_MAX, refRegistry, isImageRefUrl, bearsImage;
264
+ var init_llm = __esm({
265
+ "src/llm.ts"() {
266
+ "use strict";
267
+ init_logging();
268
+ log2 = forComponent("llm");
269
+ p_sep = (parts) => parts.length > 1 ? "\n" : "";
270
+ IMAGE_ELIDE_STUB = "[image elided \u2014 re-attach @path or /paste to view again]";
271
+ IMAGE_REF_SCHEME = "agentx-image:";
272
+ REF_REGISTRY_MAX = 256;
273
+ refRegistry = /* @__PURE__ */ new Map();
274
+ isImageRefUrl = (url) => !!url && url.startsWith(IMAGE_REF_SCHEME);
275
+ bearsImage = (m) => messageHasImage(m.content) || toolImageRef(m.content) != null;
276
+ }
277
+ });
278
+
11
279
  // src/redact.ts
12
280
  function redactSecrets(text) {
13
281
  if (!text) return text;
@@ -474,16 +742,6 @@ var init_todo = __esm({
474
742
  }
475
743
  });
476
744
 
477
- // src/logging.ts
478
- import { log } from "libx.js/src/modules/log";
479
- var forComponent;
480
- var init_logging = __esm({
481
- "src/logging.ts"() {
482
- "use strict";
483
- forComponent = (name) => log.forComponent(name);
484
- }
485
- });
486
-
487
745
  // src/tools.web.ts
488
746
  function htmlToText(html) {
489
747
  let s = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<title[\s\S]*?<\/title>/gi, " ").replace(/<noscript[\s\S]*?<\/noscript>/gi, " ").replace(/<textarea[\s\S]*?<\/textarea>/gi, " ").replace(/<!--[\s\S]*?-->/g, " ").replace(/<\/(p|div|li|h[1-6]|tr|section|article|header|footer|nav)>/gi, "\n").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, " ");
@@ -605,7 +863,7 @@ function makeWebFetchTool(options = {}) {
605
863
 
606
864
  ${capped}`;
607
865
  } catch (e) {
608
- log2.debug(`WebFetch ${u} failed`, e);
866
+ log3.debug(`WebFetch ${u} failed`, e);
609
867
  return `Error fetching ${u}: ${e?.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e?.message ?? e}`;
610
868
  } finally {
611
869
  clearTimeout(timer);
@@ -729,7 +987,7 @@ function makeWebSearchTool(options = {}) {
729
987
  if (!res.ok) return `Error: search returned ${res.status} ${res.statusText}`;
730
988
  return formatHits(parseDdgHtml(await res.text(), maxResults));
731
989
  } catch (e) {
732
- log2.debug("WebSearch failed", e);
990
+ log3.debug("WebSearch failed", e);
733
991
  return `Error searching: ${e?.name === "AbortError" ? `timed out after ${timeoutMs}ms` : e?.message ?? e}`;
734
992
  } finally {
735
993
  clearTimeout(timer);
@@ -740,12 +998,12 @@ function makeWebSearchTool(options = {}) {
740
998
  function makeWebSearchAnthropicTool(opts = {}) {
741
999
  return makeWebSearchTool({ provider: "anthropic", name: "WebSearchAnthropic", description: ANTHROPIC_SEARCH_DESC, anthropicApiKey: opts.anthropicApiKey, model: opts.model });
742
1000
  }
743
- var log2, _dnsLookup, webFetchTool, webSearchTool, ANTHROPIC_SEARCH_DESC, webSearchAnthropicTool;
1001
+ var log3, _dnsLookup, webFetchTool, webSearchTool, ANTHROPIC_SEARCH_DESC, webSearchAnthropicTool;
744
1002
  var init_tools_web = __esm({
745
1003
  "src/tools.web.ts"() {
746
1004
  "use strict";
747
1005
  init_logging();
748
- log2 = forComponent("web");
1006
+ log3 = forComponent("web");
749
1007
  webFetchTool = makeWebFetchTool();
750
1008
  webSearchTool = makeWebSearchTool();
751
1009
  ANTHROPIC_SEARCH_DESC = "High-quality web search via Anthropic's native search index. Returns concise, sourced findings \u2014 one claim per line, each with its source URL. Prefer this over WebSearch when accuracy and citations matter; it is slower (~3\u20138s) and bills the Anthropic API account per call (one cheap-model turn + search fee).";
@@ -960,6 +1218,36 @@ ${out}`.trim() : out || "(command succeeded, no output)";
960
1218
  );
961
1219
  return `Started background job ${id} \u2014 poll with JobOutput({id:"${id}"}) / JobStatus, stop with JobKill.`;
962
1220
  }
1221
+ function realPathOf(fs, path) {
1222
+ if (typeof fs.realPath !== "function") return null;
1223
+ try {
1224
+ return fs.realPath(path);
1225
+ } catch (e) {
1226
+ log4.debug(`realPath unavailable for ${path} \u2014 inlining image bytes instead`, e);
1227
+ return null;
1228
+ }
1229
+ }
1230
+ function isBinary(raw) {
1231
+ if (raw === "") return false;
1232
+ const head = raw.length > 8192 ? raw.slice(0, 8192) : raw;
1233
+ let repl = 0;
1234
+ for (let i = 0; i < head.length; i++) {
1235
+ const c = head.charCodeAt(i);
1236
+ if (c === 0) return true;
1237
+ if (c === 65533) repl++;
1238
+ }
1239
+ return repl / head.length > 0.1;
1240
+ }
1241
+ async function byteSize(fs, path, raw) {
1242
+ const bfs = fs;
1243
+ if (typeof bfs.readFileBytes === "function") {
1244
+ try {
1245
+ return (await bfs.readFileBytes(path)).length;
1246
+ } catch {
1247
+ }
1248
+ }
1249
+ return Buffer.byteLength(raw, "utf8");
1250
+ }
963
1251
  function exitSessionTool(onExit) {
964
1252
  return {
965
1253
  name: "ExitSession",
@@ -986,15 +1274,18 @@ function toolsByName(names) {
986
1274
  return t;
987
1275
  });
988
1276
  }
989
- var numberLines, bashTool, IMG_MIME, readTool, editTool;
1277
+ var log4, numberLines, bashTool, IMG_MIME, readTool, editTool;
990
1278
  var init_tools = __esm({
991
1279
  "src/tools.ts"() {
992
1280
  "use strict";
1281
+ init_llm();
1282
+ init_logging();
993
1283
  init_tools_structured();
994
1284
  init_todo();
995
1285
  init_tools_web();
996
1286
  init_OverlayFilesystem();
997
1287
  init_redact();
1288
+ log4 = forComponent("tools");
998
1289
  numberLines = (content, offset = 0, limit) => {
999
1290
  const lines = content.split("\n");
1000
1291
  const start = Math.max(0, offset);
@@ -1049,12 +1340,22 @@ var init_tools = __esm({
1049
1340
  if (typeof fs.readFileBytes !== "function") {
1050
1341
  return `[${path} is an image, but this filesystem can't read binary \u2014 attach it as @${path} instead, or run on disk.]`;
1051
1342
  }
1343
+ const real = realPathOf(fs, path);
1344
+ if (real) {
1345
+ if (!await ctx.fs.exists(path)) return `Error: File not found: ${path}`;
1346
+ return imageRefResult(real, IMG_MIME[ext]);
1347
+ }
1052
1348
  const bytes = await fs.readFileBytes(path);
1053
1349
  const b64 = Buffer.from(bytes).toString("base64");
1054
1350
  return JSON.stringify({ dataUrl: `data:${IMG_MIME[ext]};base64,${b64}`, image: path });
1055
1351
  }
1056
1352
  const raw = await ctx.fs.readFile(path);
1353
+ if (isBinary(raw)) {
1354
+ const bytes = await byteSize(ctx.fs, path, raw);
1355
+ return `[binary file: ${path} \u2014 ${bytes} bytes, not shown as text. Read renders png/jpg/jpeg/gif/webp images directly; otherwise attach it as @${path}.]`;
1356
+ }
1057
1357
  ctx.readState.set(ctx.fs.resolvePath(path), raw);
1358
+ if (raw === "") return `[empty file: ${path} \u2014 0 bytes]`;
1058
1359
  const content = CONFIG_FILE_RE.test(ctx.fs.resolvePath(path)) ? redactSecrets(raw) : raw;
1059
1360
  const total = content === "" ? 0 : content.split("\n").length;
1060
1361
  const start = Math.max(0, offset ?? 0);
@@ -1188,6 +1489,11 @@ var init_NodeDiskFilesystem = __esm({
1188
1489
  throw new Error(`File not found: ${path}`);
1189
1490
  }
1190
1491
  }
1492
+ /** The REAL on-disk path behind a VFS path — an optional capability (not on IFilesystem) the Read
1493
+ * tool duck-types so it can persist an image REFERENCE instead of megabytes of base64. */
1494
+ realPath(path) {
1495
+ return this.real(this.resolvePath(path));
1496
+ }
1191
1497
  /** Read raw bytes (for binary files like images). Not on the base IFilesystem (which is utf8-only) — an
1192
1498
  * optional capability the Read tool duck-types to return image blocks. Same symlink/jail guards as readFile. */
1193
1499
  async readFileBytes(path) {
@@ -1370,6 +1676,12 @@ var init_JailedFilesystem = __esm({
1370
1676
  if (typeof inner.readFileBytes !== "function") throw new Error("binary read not supported by this filesystem");
1371
1677
  return inner.readFileBytes(this.guardRead("readFileBytes", path));
1372
1678
  }
1679
+ /** Forward the optional real-path capability (image REFS), jail-guarded like readFile. */
1680
+ realPath(path) {
1681
+ const inner = this.inner;
1682
+ if (typeof inner.realPath !== "function") throw new Error("realPath not supported by this filesystem");
1683
+ return inner.realPath(this.guardRead("realPath", path));
1684
+ }
1373
1685
  async writeFile(path, content) {
1374
1686
  return this.inner.writeFile(this.guardWrite("writeFile", path), content);
1375
1687
  }
@@ -1610,13 +1922,13 @@ function makeRealShellTool(options) {
1610
1922
  }
1611
1923
  };
1612
1924
  try {
1613
- return await new Promise((resolve) => {
1925
+ return await new Promise((resolve2) => {
1614
1926
  let out = "";
1615
1927
  let settled = false;
1616
1928
  const finish = (s) => {
1617
1929
  if (settled) return;
1618
1930
  settled = true;
1619
- resolve(s);
1931
+ resolve2(s);
1620
1932
  };
1621
1933
  let proc;
1622
1934
  try {
@@ -1639,7 +1951,7 @@ function makeRealShellTool(options) {
1639
1951
  proc.stderr?.on("data", collect);
1640
1952
  proc.on("error", (err) => {
1641
1953
  if (err?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
1642
- log5.debug("shell spawn error", err);
1954
+ log7.debug("shell spawn error", err);
1643
1955
  finish(`[exit 1] ${err?.message ?? err}${out ? "\n" + clean(out) : ""}`);
1644
1956
  });
1645
1957
  proc.on("close", (code) => {
@@ -1701,7 +2013,7 @@ ${clean(out) || "(no output yet)"}`;
1701
2013
  }
1702
2014
  ];
1703
2015
  }
1704
- var log5, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
2016
+ var log7, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
1705
2017
  var init_tools_shell = __esm({
1706
2018
  "src/tools.shell.ts"() {
1707
2019
  "use strict";
@@ -1709,7 +2021,7 @@ var init_tools_shell = __esm({
1709
2021
  init_redact();
1710
2022
  init_logging();
1711
2023
  init_shell_sandbox();
1712
- log5 = forComponent("shell");
2024
+ log7 = forComponent("shell");
1713
2025
  clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
1714
2026
  DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
1715
2027
  SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
@@ -1789,18 +2101,8 @@ var init_tools_shell = __esm({
1789
2101
  }
1790
2102
  });
1791
2103
 
1792
- // src/llm.ts
1793
- function contentText(content) {
1794
- if (content == null) return "";
1795
- if (typeof content === "string") return content;
1796
- return content.map((p) => p.type === "text" ? p.text ?? "" : "[image]").join(p_sep(content));
1797
- }
1798
- var p_sep = (parts) => parts.length > 1 ? "\n" : "";
1799
- function imagePart(url) {
1800
- return { type: "image_url", image_url: { url } };
1801
- }
1802
-
1803
2104
  // src/Agent.ts
2105
+ init_llm();
1804
2106
  init_tools();
1805
2107
 
1806
2108
  // src/tools.jobs.ts
@@ -2492,7 +2794,7 @@ init_OverlayFilesystem();
2492
2794
 
2493
2795
  // src/worktree.ts
2494
2796
  import { spawnSync } from "child_process";
2495
- import { symlinkSync, mkdirSync, statSync, readFileSync, rmSync } from "fs";
2797
+ import { symlinkSync, mkdirSync, statSync as statSync2, readFileSync as readFileSync2, rmSync } from "fs";
2496
2798
  import { join } from "path";
2497
2799
  var VALID_SEGMENT = /^[a-zA-Z0-9._-]+$/;
2498
2800
  var MAX_SLUG_LENGTH = 64;
@@ -2531,16 +2833,16 @@ function findGitRoot(from) {
2531
2833
  }
2532
2834
  function readWorktreeHead(worktreePath) {
2533
2835
  try {
2534
- const gitFile = readFileSync(join(worktreePath, ".git"), "utf-8").trim();
2836
+ const gitFile = readFileSync2(join(worktreePath, ".git"), "utf-8").trim();
2535
2837
  const m = gitFile.match(/^gitdir:\s*(.+)$/);
2536
2838
  if (!m) return null;
2537
2839
  const headPath = join(m[1], "HEAD");
2538
- const head = readFileSync(headPath, "utf-8").trim();
2840
+ const head = readFileSync2(headPath, "utf-8").trim();
2539
2841
  const refMatch = head.match(/^ref:\s*(.+)$/);
2540
2842
  if (!refMatch) return head.length === 40 ? head : null;
2541
2843
  const refPath = join(m[1], "..", refMatch[1]);
2542
2844
  try {
2543
- return readFileSync(refPath, "utf-8").trim();
2845
+ return readFileSync2(refPath, "utf-8").trim();
2544
2846
  } catch {
2545
2847
  }
2546
2848
  const r = git(["rev-parse", "HEAD"], worktreePath);
@@ -2576,7 +2878,7 @@ function symlinkDirs(repoRoot, worktreePath, dirs) {
2576
2878
  const src = join(repoRoot, dir);
2577
2879
  const dst = join(worktreePath, dir);
2578
2880
  try {
2579
- statSync(src);
2881
+ statSync2(src);
2580
2882
  } catch {
2581
2883
  continue;
2582
2884
  }
@@ -2592,7 +2894,7 @@ function cleanupWorktree(repoRoot, slug2, opts) {
2592
2894
  const worktreePath = worktreePathFor(repoRoot, slug2);
2593
2895
  const branch = worktreeBranchName(slug2);
2594
2896
  try {
2595
- statSync(worktreePath);
2897
+ statSync2(worktreePath);
2596
2898
  } catch {
2597
2899
  return { removed: false, reason: "not found" };
2598
2900
  }
@@ -2642,7 +2944,7 @@ function exitWorktree() {
2642
2944
  // src/subagent.ts
2643
2945
  init_NodeDiskFilesystem();
2644
2946
  init_logging();
2645
- var log3 = forComponent("subagent");
2947
+ var log5 = forComponent("subagent");
2646
2948
  async function boundedPool(items, limit, fn) {
2647
2949
  const out = new Array(items.length);
2648
2950
  let next = 0;
@@ -2684,7 +2986,7 @@ function traceFileFor(dir, label, depth) {
2684
2986
  const id = `${Date.now().toString(36)}-${(traceSeq++).toString(36)}`;
2685
2987
  return join3(dir, `${id}-d${depth}-${sanitizeSlug(label) || "sub"}.json`);
2686
2988
  } catch (e) {
2687
- log3.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
2989
+ log5.warn(`subagent trace dir unavailable (${dir}) \u2014 running without a trace: ${e instanceof Error ? e.message : String(e)}`);
2688
2990
  return void 0;
2689
2991
  }
2690
2992
  }
@@ -2692,7 +2994,7 @@ function flushTrace(file, meta, messages, status) {
2692
2994
  try {
2693
2995
  writeFileSync(file, JSON.stringify({ ...meta, status, updated: Date.now(), messages }, null, 2));
2694
2996
  } catch (e) {
2695
- log3.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
2997
+ log5.warn(`subagent trace flush failed (${file}): ${e instanceof Error ? e.message : String(e)}`);
2696
2998
  }
2697
2999
  }
2698
3000
  function installTrace(child, file, meta) {
@@ -3153,7 +3455,11 @@ function reasoningToChatFragment(model, effort) {
3153
3455
  }
3154
3456
 
3155
3457
  // src/Agent.ts
3156
- var log4 = forComponent("Agent");
3458
+ var log6 = forComponent("Agent");
3459
+ function envInt(key, fallback) {
3460
+ const n = typeof process !== "undefined" ? Number(process.env?.[key]) : NaN;
3461
+ return Number.isFinite(n) && n >= 0 ? n : fallback;
3462
+ }
3157
3463
  function isBrowserTool(name) {
3158
3464
  return name.startsWith("mcp__") && /browser/i.test(name.slice(5).split("__")[0]);
3159
3465
  }
@@ -3179,8 +3485,9 @@ var AgentOptions = class {
3179
3485
  /** Hard ceiling on accumulated tokens (prompt+completion) across the run; cache READS count at 0.1×
3180
3486
  * (their real price) so a fat cached context doesn't trip the guard on healthy turns. 0 = unbounded. */
3181
3487
  maxTokens = 2e5;
3182
- /** Wall-clock ceiling in ms for the whole run. 0 = unbounded. */
3183
- timeoutMs = 12e4;
3488
+ /** Idle ceiling in ms: max time since the last productive step before the run is killed (a long/active
3489
+ * step resets it — see runLoop's `lastProgressAt`). Not a whole-run wall clock. 0 = unbounded. */
3490
+ timeoutMs = 3e5;
3184
3491
  /** Idle-stall watchdog for a SINGLE streamed model request: abort it if no chunk (text OR reasoning)
3185
3492
  * arrives for this long. The between-steps `timeoutMs` can't preempt an in-flight `chat()`, so a
3186
3493
  * provider that goes silent mid-stream would otherwise park the loop forever. 0 = off. */
@@ -3191,15 +3498,33 @@ var AgentOptions = class {
3191
3498
  maxToolCalls = 100;
3192
3499
  /** External cancellation — abort the loop between steps (e.g. a UI "Cancel" button). */
3193
3500
  signal;
3501
+ /** Retry budget for an EMPTY/silent-turn (a delegated-runtime wedge: cursor's warm helper spins then
3502
+ * returns a content-less end, surfaced as `code:'empty'`/`code:'cursor_silent'`). Tuned WIDER than the
3503
+ * network-drop budget (fixed 2) because clearing a wedge needs several reap+respawn cycles — each retry
3504
+ * hits a FRESH cursor session (the adapter evicts the wedged warm helper on the silent-end). Still bounded:
3505
+ * after exhaustion the turn surfaces `finishReason:'empty'` (non-zero exit), never infinite, never silent.
3506
+ * Env: AGENTX_EMPTY_RETRIES. */
3507
+ emptyRetries = envInt("AGENTX_EMPTY_RETRIES", 4);
3508
+ /** Base backoff (ms) between empty-turn retries; grows linearly per attempt (longer than the network
3509
+ * class's fixed 1s so a wedged helper has time to be reaped/respawned). Env: AGENTX_EMPTY_RETRY_BASE_MS. */
3510
+ emptyRetryBaseMs = envInt("AGENTX_EMPTY_RETRY_BASE_MS", 1500);
3194
3511
  /** 0 = never trim. Otherwise cap the messages sent per turn (system + most recent). */
3195
3512
  maxContextMessages = 0;
3196
3513
  /** Note-taking: keep the most-recent N tool-result outputs verbatim; collapse OLDER ones to a one-line
3197
3514
  * stub in the sent context (the model already consumed them — it can re-Read/re-run). The stored
3198
3515
  * transcript is never mutated. 0 = keep all verbatim. */
3199
3516
  keepToolOutputs = 0;
3517
+ /** How many of the most-recent image-bearing messages get their image REF expanded to real base64 in
3518
+ * the outgoing request. Older ones (and legacy inline base64) go on the wire as the elision stub. */
3519
+ keepRecentImages = 1;
3200
3520
  /** Token-aware backstop (~4 chars/token estimate). After note-taking, drop oldest messages from the
3201
3521
  * sent context until the estimate is under this ceiling (pairing-safe). 0 = off. */
3202
3522
  maxContextTokens = 0;
3523
+ /** The MODEL's context window (tokens) — what the context % is measured against and what `/compact`
3524
+ * targets 50% of. Filled by the CLI from model metadata; 0 = fall back to 200k. Deliberately NOT
3525
+ * `maxTokens`: that is the cumulative SPEND kill-switch, and a spend budget must never be able to
3526
+ * drive destructive compaction (`--max-tokens 50000` would otherwise shred the transcript). */
3527
+ contextWindow = 0;
3203
3528
  /** Pagination ceiling for a SINGLE tool result (bytes). A result over this is cropped to page 1 with
3204
3529
  * a marker telling the model it was cropped (refine the query, or page further). Guards against one
3205
3530
  * Grep/Read/MCP call blowing the whole context window. 0 = off. Default 60k (~15k tokens). */
@@ -3255,8 +3580,10 @@ var AgentOptions = class {
3255
3580
  /** Stream tokens from the model. Takes effect only with a `host.notify`; off => current (non-stream) behavior. */
3256
3581
  stream = false;
3257
3582
  /** Closure reflex for delegated runtimes (cursor/claude-code): when such a turn ends on a tool call with
3258
- * no assistant text after it (the runtime went silent post-work), nudge ONE more step for a brief summary
3259
- * of what changed + what's pending — so the turn delivers a wrap-up instead of trailing off mid-narration.
3583
+ * no assistant text after it (the runtime went silent post-work), nudge ONE more step to ANSWER the user
3584
+ * from the tool results already in the transcript — so the turn delivers a reply instead of trailing off
3585
+ * mid-narration. Deliberately not a "summarize what you did" nudge: that produced process reports and
3586
+ * permission-asking instead of answers (see the nudge text in runLoop).
3260
3587
  * Fires at most once per run and only for delegated runtimes (native turns never emit the trigger). */
3261
3588
  closeDelegatedTurns = true;
3262
3589
  /** Fold the dropped middle of an over-long transcript into a synthetic summary (edge-safe, no LLM). Off => drop-oldest. */
@@ -3443,7 +3770,7 @@ var Agent = class _Agent {
3443
3770
  const disk = new NodeDiskFilesystem2(process.cwd());
3444
3771
  await disk.init();
3445
3772
  this.options.fs = new JailedFilesystem2(disk);
3446
- log4.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
3773
+ log6.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
3447
3774
  }
3448
3775
  this.buildCtx();
3449
3776
  }
@@ -3561,19 +3888,42 @@ var Agent = class _Agent {
3561
3888
  return this.runLoop();
3562
3889
  }
3563
3890
  /**
3564
- * Fold the conversation in place (manual `/compact`): keep the system message + the
3565
- * most-recent window, summarizing the dropped middle (deterministic, no LLM call).
3566
- * No-op when the transcript already fits. Returns the number of messages removed.
3567
- * `focus` (e.g. from `/compact keep the API details`) preserves matching lines from the
3568
- * dropped span verbatim in the summary, instead of losing them to the generic recap.
3891
+ * Fold the conversation in place (manual `/compact`) until it fits a TOKEN budget — not a message
3892
+ * count. Message-count folding alone frees almost nothing when a few recent tool results are huge,
3893
+ * so we escalate: fold the middle into a synthetic recap → elide oversized tool-result bodies in the
3894
+ * retained window → halve the window → drop-oldest backstop. Deterministic, no LLM call.
3895
+ * `focus` (e.g. `/compact keep the API details`) preserves matching lines from the dropped span verbatim.
3896
+ * Returns before/after token estimates so the caller can report honestly (0 freed => say so).
3569
3897
  */
3570
3898
  compactNow(maxMessages = 12, focus) {
3571
- const max = Math.max(2, maxMessages);
3572
- if (this.transcript.length <= max) return 0;
3899
+ const cap = _Agent.contextCap(this.options);
3900
+ const target = Math.round(cap * _Agent.COMPACT_TARGET);
3901
+ const kr = this.options.keepRecentImages ?? 1;
3902
+ const tokensBefore = estimateTokens(this.transcript, kr);
3903
+ const msgsBefore = this.transcript.length;
3904
+ let max = Math.max(2, maxMessages);
3905
+ if (this.transcript.length <= max && tokensBefore <= target)
3906
+ return { tokensBefore, tokensAfter: tokensBefore, target, cap, removed: 0 };
3573
3907
  void this.activeHooks?.onPreCompact?.(this.transcript);
3574
- const before = this.transcript.length;
3575
- this.transcript = compact(this.transcript, max, focus);
3576
- return before - this.transcript.length;
3908
+ let out = this.transcript;
3909
+ if (out.length > max) out = compact(out, max, focus);
3910
+ if (estimateTokens(out, kr) > target) out = elideOversizedImages(out, target, true, kr);
3911
+ if (estimateTokens(out, kr) > target) out = capOversizedBodies(out, target, kr);
3912
+ if (estimateTokens(out, kr) > target) out = elideOversizedImages(out, target, false, kr);
3913
+ while (estimateTokens(out, kr) > target && max > 2 && out.length > 2) {
3914
+ max = Math.max(2, Math.floor(max / 2));
3915
+ out = capOversizedBodies(compact(out, max, focus), target, kr);
3916
+ }
3917
+ if (estimateTokens(out, kr) > target) out = fitTokenBudget(out, target, kr);
3918
+ out = dropOrphanToolResults(out);
3919
+ this.transcript = out;
3920
+ this.releaseStaleImages();
3921
+ return { tokensBefore, tokensAfter: estimateTokens(this.transcript, kr), target, cap, removed: msgsBefore - this.transcript.length };
3922
+ }
3923
+ /** Drop inline base64 images from older messages so Bun/JSC can reclaim the heap (see contentBytes). */
3924
+ releaseStaleImages(keepRecent = 1) {
3925
+ const next = elideStaleImages(this.transcript, keepRecent);
3926
+ if (next !== this.transcript) this.transcript = next;
3577
3927
  }
3578
3928
  async runLoop() {
3579
3929
  const o = this.options;
@@ -3589,15 +3939,16 @@ var Agent = class _Agent {
3589
3939
  let lastFp = "";
3590
3940
  let repeats = 0;
3591
3941
  let closureNudged = false;
3942
+ let lastProgressAt = start;
3592
3943
  const kill = (finishReason) => {
3593
- log4.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(usage.totalTokens - 0.9 * usage.cacheReadTokens)}, ms=${Date.now() - start - this.parkedMs}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
3944
+ log6.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(usage.totalTokens - 0.9 * usage.cacheReadTokens)}, ms=${Date.now() - start - this.parkedMs}${finishReason === "timeout" ? `, idle=${Date.now() - lastProgressAt - this.parkedMs}ms` : ""}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
3594
3945
  this.ctx.jobs?.killAll();
3595
3946
  return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated };
3596
3947
  };
3597
3948
  while (true) {
3598
3949
  if (o.signal?.aborted) return kill("aborted");
3599
3950
  if (steps >= o.maxSteps) return kill("max_steps");
3600
- if (o.timeoutMs && Date.now() - start - this.parkedMs >= o.timeoutMs) return kill("timeout");
3951
+ if (o.timeoutMs && Date.now() - lastProgressAt - this.parkedMs >= o.timeoutMs) return kill("timeout");
3601
3952
  const budgetTokens = usage.totalTokens - 0.9 * usage.cacheReadTokens;
3602
3953
  if (o.maxTokens && budgetTokens >= o.maxTokens) return kill("budget");
3603
3954
  steps++;
@@ -3637,27 +3988,38 @@ var Agent = class _Agent {
3637
3988
  const r = await o.ai.chat({ model: o.model, messages: sent, tools: sentTools, stream: false, signal: o.signal, ...o.toolChoice ? { toolChoice: o.toolChoice } : {}, ...reasonOpts });
3638
3989
  res = r;
3639
3990
  }
3991
+ if (steps === 1 && this.isEmptyStop(res) && !this.pendingInjections.length) {
3992
+ const e = new Error("model returned an empty turn (finishReason=stop, no text, no tools)");
3993
+ e.code = "empty";
3994
+ throw e;
3995
+ }
3640
3996
  break;
3641
3997
  } catch (err) {
3642
3998
  const sc = err?.statusCode;
3643
3999
  const serverSide = sc >= 500 || sc === 408 || /Service Unavailable|overloaded|Internal server error|Bad gateway|Gateway time/i.test(String(err?.message ?? ""));
3644
4000
  const network = !sc && /ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|EPIPE|socket hang up|fetch failed|network|terminated|UND_ERR/i.test(String(err?.message ?? err?.code ?? err));
3645
4001
  const stalled = err?.code === "stall";
3646
- const transient = !o.signal?.aborted && !isAbortError(err) && attempt < 2 && (network || serverSide || stalled);
4002
+ const empty = err?.code === "empty" || err?.code === "cursor_silent";
4003
+ const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
4004
+ const transient = !o.signal?.aborted && !isAbortError(err) && attempt < maxAttempts && (network || serverSide || stalled || empty);
3647
4005
  if (!transient) throw err;
3648
- const waitMs = 1e3 * (attempt + 1);
3649
- log4.warn(`${stalled ? "model stall" : "network drop"} mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
3650
- o.host?.notify?.({ kind: "retry", message: `${stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
4006
+ const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
4007
+ log6.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
4008
+ o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
3651
4009
  await new Promise((r) => setTimeout(r, waitMs));
3652
4010
  }
3653
4011
  }
3654
4012
  } catch (err) {
3655
4013
  if (err?.code === "budget") return kill("budget");
3656
4014
  if (err?.code === "stall") {
3657
- log4.warn(`model stalled out after retries: ${err?.message ?? err}`);
4015
+ log6.warn(`model stalled out after retries: ${err?.message ?? err}`);
3658
4016
  return kill("timeout");
3659
4017
  }
3660
4018
  if (o.signal?.aborted || isAbortError(err)) return kill("aborted");
4019
+ if (err?.code === "empty" || err?.code === "cursor_silent") {
4020
+ log6.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
4021
+ return { text: "", steps, finishReason: "empty", messages: this.transcript, usage, usageEstimated, error: err };
4022
+ }
3661
4023
  const body = err?.body ?? err?.response?.data ?? err?.error;
3662
4024
  let bodyStr;
3663
4025
  try {
@@ -3667,7 +4029,7 @@ var Agent = class _Agent {
3667
4029
  }
3668
4030
  if (bodyStr && err instanceof Error && !err.message.includes(bodyStr)) err.detail = bodyStr;
3669
4031
  const errLog = err instanceof Error ? { message: err.message, ...err.detail ? { detail: err.detail } : {}, stack: err.stack } : err;
3670
- log4.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
4032
+ log6.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
3671
4033
  return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err };
3672
4034
  }
3673
4035
  if (o.signal?.aborted) return kill("aborted");
@@ -3711,6 +4073,7 @@ var Agent = class _Agent {
3711
4073
  }
3712
4074
  const toolCalls = res.toolCalls ?? [];
3713
4075
  const emptyTurn = toolCalls.length === 0 && delegatedTools.length === 0 && contentText(res.content ?? "").trim() === "";
4076
+ if (!emptyTurn) lastProgressAt = Date.now();
3714
4077
  if (!emptyTurn) {
3715
4078
  this.transcript.push({
3716
4079
  role: "assistant",
@@ -3722,12 +4085,16 @@ var Agent = class _Agent {
3722
4085
  if (this.drainInjections()) continue;
3723
4086
  if (o.closeDelegatedTurns && res.endedOnToolNoText && !closureNudged && steps < o.maxSteps) {
3724
4087
  closureNudged = true;
3725
- log4.verbose("delegated turn went silent after a tool \u2014 nudging a closing summary");
3726
- this.transcript.push({ role: "user", content: "Summarize what you did this turn and what (if anything) still needs doing. Be concise." });
4088
+ log6.verbose("delegated turn went silent after a tool \u2014 nudging a closing answer");
4089
+ this.transcript.push({
4090
+ role: "user",
4091
+ content: "You ran tools but did not reply. Answer the user now using the tool results above; if a tool failed, say so plainly. Do NOT re-run those tools, do NOT narrate what you did, and do NOT ask for permission to continue."
4092
+ });
3727
4093
  continue;
3728
4094
  }
3729
- log4.verbose(`completed in ${steps} step(s)`);
4095
+ log6.verbose(`completed in ${steps} step(s)`);
3730
4096
  await this.ctx.jobs?.drain();
4097
+ this.releaseStaleImages();
3731
4098
  await this.activeHooks?.onStop?.(res.content ?? "");
3732
4099
  return { text: res.content ?? "", steps, finishReason: "stop", messages: this.transcript, usage, usageEstimated };
3733
4100
  }
@@ -3753,7 +4120,7 @@ var Agent = class _Agent {
3753
4120
  if (this.ctx.needsInput) {
3754
4121
  const question = this.ctx.needsInput;
3755
4122
  this.ctx.needsInput = void 0;
3756
- log4.warn(`needs_input: ${question.question} (steps=${steps})`);
4123
+ log6.warn(`needs_input: ${question.question} (steps=${steps})`);
3757
4124
  await this.ctx.jobs?.drain();
3758
4125
  const text = [
3759
4126
  `OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
@@ -3810,6 +4177,12 @@ var Agent = class _Agent {
3810
4177
  }
3811
4178
  };
3812
4179
  }
4180
+ /** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
4181
+ * non-result the model produced by going silent. Callers retry it rather than report "done". */
4182
+ isEmptyStop(res) {
4183
+ const stop = res.finishReason === "stop" || res.finishReason == null;
4184
+ return stop && (res.toolCalls?.length ?? 0) === 0 && (res.delegatedTools?.length ?? 0) === 0 && contentText(res.content ?? "").trim() === "";
4185
+ }
3813
4186
  async consumeStream(stream, poke) {
3814
4187
  let content = "";
3815
4188
  let finishReason;
@@ -3868,13 +4241,13 @@ var Agent = class _Agent {
3868
4241
  const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
3869
4242
  if (decision?.block) {
3870
4243
  const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
3871
- log4.debug(`${tc.function.name} -> ${blocked}`);
4244
+ log6.debug(`${tc.function.name} -> ${blocked}`);
3872
4245
  await hooks?.postToolUse?.(call, blocked, meta);
3873
4246
  return blocked;
3874
4247
  }
3875
4248
  this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
3876
4249
  if (earlyError) {
3877
- log4.debug(`${tc.function.name} -> ${earlyError}`);
4250
+ log6.debug(`${tc.function.name} -> ${earlyError}`);
3878
4251
  await hooks?.postToolUse?.(call, earlyError, meta);
3879
4252
  this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
3880
4253
  return earlyError;
@@ -3883,12 +4256,12 @@ var Agent = class _Agent {
3883
4256
  let images;
3884
4257
  let threw = false;
3885
4258
  try {
3886
- log4.debug(`${tc.function.name}(${tc.function.arguments})`);
4259
+ log6.debug(`${tc.function.name}(${tc.function.arguments})`);
3887
4260
  this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
3888
4261
  try {
3889
4262
  hooks.onToolOutput(call, chunk, meta);
3890
4263
  } catch (e) {
3891
- log4.debug(`onToolOutput hook error: ${e}`);
4264
+ log6.debug(`onToolOutput hook error: ${e}`);
3892
4265
  }
3893
4266
  } : void 0;
3894
4267
  const raw = await tool.run(args, this.ctx);
@@ -3900,7 +4273,7 @@ var Agent = class _Agent {
3900
4273
  }
3901
4274
  } catch (e) {
3902
4275
  const msg = e instanceof Error ? e.message : String(e);
3903
- log4.debug(`${tc.function.name} -> error: ${msg}`);
4276
+ log6.debug(`${tc.function.name} -> error: ${msg}`);
3904
4277
  result = `Error: ${msg}`;
3905
4278
  threw = true;
3906
4279
  } finally {
@@ -3926,6 +4299,16 @@ var Agent = class _Agent {
3926
4299
  }
3927
4300
  return images?.length ? { text: result, images } : result;
3928
4301
  }
4302
+ /** `/compact` aims to land the transcript at this fraction of the context window. */
4303
+ static COMPACT_TARGET = 0.5;
4304
+ /** THE context-window cap resolution — `/compact`'s budget and the CLI's context footer/warnings both
4305
+ * route here, so the percentage the user reads and the one compaction targets can never drift. */
4306
+ static contextCap(o) {
4307
+ if (o.maxContextTokens) return o.maxContextTokens;
4308
+ const w = o.contextWindow || 0;
4309
+ if (w > ANTHROPIC_WINDOW_WITHOUT_1M_BETA && isAnthropicModel(o.model)) return ANTHROPIC_WINDOW_WITHOUT_1M_BETA;
4310
+ return w || 2e5;
4311
+ }
3929
4312
  static WRITE_CLASS = ["Write", "Edit", "MultiEdit", "ApplyEdits"];
3930
4313
  /** Append an autoTest failure section to a write-class tool result, if configured. */
3931
4314
  async maybeAutoTest(toolName, result) {
@@ -3959,26 +4342,27 @@ ${out}`;
3959
4342
  const browserKeep = this.browserActive ? o.browserTrim?.keepOutputs : void 0;
3960
4343
  const keep = browserKeep != null ? o.keepToolOutputs ? Math.min(o.keepToolOutputs, browserKeep) : browserKeep : o.keepToolOutputs;
3961
4344
  if (keep) out = stubOldToolResults(out ?? m, keep);
4345
+ out = expandImagesForSend(dropOrphanToolResults(out ?? m), o.keepRecentImages ?? 1);
3962
4346
  if (o.maxContextTokens) {
3963
- const pre = (out ?? m).length;
3964
- out = fitTokenBudget(out ?? m, o.maxContextTokens);
4347
+ const pre = out.length;
4348
+ out = fitTokenBudget(out, o.maxContextTokens, o.keepRecentImages ?? 1);
3965
4349
  const dropped = pre - out.length;
3966
4350
  if (dropped > 0 && dropped !== this.lastTrimNotified) {
3967
4351
  this.lastTrimNotified = dropped;
3968
4352
  o.host?.notify?.({ kind: "compaction", message: `context full \u2014 auto-trimmed ${dropped} oldest message(s) to fit ~${Math.round(o.maxContextTokens / 1e3)}k tokens` });
3969
4353
  }
3970
4354
  }
3971
- return dropOrphanToolResults(out ?? m);
4355
+ return dropOrphanToolResults(out);
3972
4356
  }
3973
4357
  };
3974
4358
  function dropOldest(m, max) {
3975
4359
  const head = m[0]?.role === "system" ? [m[0]] : [];
3976
4360
  return [...head, ...m.slice(-(max - head.length))];
3977
4361
  }
3978
- function estimateTokens(m) {
3979
- let chars = 0;
3980
- for (const x of m) chars += contentText(x.content).length + (x.tool_calls ? JSON.stringify(x.tool_calls).length : 0);
3981
- return Math.ceil(chars / 4);
4362
+ var ANTHROPIC_WINDOW_WITHOUT_1M_BETA = 2e5;
4363
+ var isAnthropicModel = (m) => !!m && (m.startsWith("anthropic/") || /(^|\/)claude-/.test(m));
4364
+ function estimateTokens(m, keepRecentImages = 1) {
4365
+ return Math.ceil(sendBytes(m, keepRecentImages) / 4);
3982
4366
  }
3983
4367
  function cropResult(result, cap) {
3984
4368
  const head = result.slice(0, cap);
@@ -4012,6 +4396,60 @@ function stubOldToolResults(messages, keep) {
4012
4396
  return { ...x, content: `[${x.name ?? "tool"}${where ? ` ${where}` : ""} output elided \u2014 ${lines} lines; re-run the tool to view]` };
4013
4397
  });
4014
4398
  }
4399
+ function elideOversizedImages(messages, target, protectNewest = false, keepRecentImages = 1) {
4400
+ let out = messages;
4401
+ const withImages = out.map((_, i) => i).filter((i) => messageHasImage(out[i].content));
4402
+ const newest = protectNewest ? withImages[withImages.length - 1] : -1;
4403
+ const weight = sendBytesPer(out, keepRecentImages);
4404
+ const heaviestFirst = withImages.filter((i) => i !== newest).sort((a, b) => weight[b] - weight[a]);
4405
+ for (const i of heaviestFirst) {
4406
+ if (estimateTokens(out, keepRecentImages) <= target) break;
4407
+ const c = elideImages(out[i].content);
4408
+ if (c === out[i].content) continue;
4409
+ out = out.map((m, j) => j === i ? { ...m, content: c } : m);
4410
+ }
4411
+ return out;
4412
+ }
4413
+ function capOversizedBodies(messages, target, keepRecentImages = 1) {
4414
+ let out = messages;
4415
+ for (const cap of [8e3, 4e3, 2e3, 800, 300, 120]) {
4416
+ if (estimateTokens(out, keepRecentImages) <= target) break;
4417
+ out = out.map((x) => x.role === "system" ? x : elideBody(x, cap));
4418
+ }
4419
+ return out;
4420
+ }
4421
+ function elideBody(x, cap) {
4422
+ const note = (bytes, imgs2) => `
4423
+
4424
+ [${bytes} bytes elided by /compact${x.role === "tool" ? " \u2014 re-run the tool to view" : ""}${imgs2 ? `; ${imgs2} image part(s) KEPT in full \u2014 they still cost context` : ""}]`;
4425
+ if (!Array.isArray(x.content)) {
4426
+ const text = contentText(x.content);
4427
+ if (text.length <= cap) return x;
4428
+ return { ...x, content: `${text.slice(0, cap)}${note(text.length - cap, 0)}` };
4429
+ }
4430
+ const imgs = x.content.filter((p) => p.type !== "text").length;
4431
+ let left = cap;
4432
+ let elided = 0;
4433
+ const parts = [];
4434
+ for (const p of x.content) {
4435
+ if (p.type !== "text") {
4436
+ parts.push(p);
4437
+ continue;
4438
+ }
4439
+ const t = p.text ?? "";
4440
+ if (t.length <= left) {
4441
+ left -= t.length;
4442
+ if (t) parts.push(p);
4443
+ continue;
4444
+ }
4445
+ const keep = left;
4446
+ left = 0;
4447
+ elided += t.length - keep;
4448
+ if (keep > 0) parts.push({ ...p, text: t.slice(0, keep) });
4449
+ }
4450
+ if (!elided) return parts.length === x.content.length || !parts.length ? x : { ...x, content: parts };
4451
+ return { ...x, content: [...parts, { type: "text", text: note(elided, imgs).trimStart() }] };
4452
+ }
4015
4453
  var callIdSet = (msgs) => {
4016
4454
  const ids = /* @__PURE__ */ new Set();
4017
4455
  for (const m of msgs) if (m.role === "assistant") for (const tc of m.tool_calls ?? []) ids.add(tc.id);
@@ -4022,8 +4460,8 @@ function dropOrphanToolResults(messages) {
4022
4460
  const ok = (m) => m.role !== "tool" || ids.has(m.tool_call_id ?? "");
4023
4461
  return messages.every(ok) ? messages : messages.filter(ok);
4024
4462
  }
4025
- function fitTokenBudget(messages, maxTokens) {
4026
- const per = messages.map((x) => estimateTokens([x]));
4463
+ function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
4464
+ const per = sendBytesPer(messages, keepRecentImages).map((b) => Math.ceil(b / 4));
4027
4465
  let total = per.reduce((a, b) => a + b, 0);
4028
4466
  if (total <= maxTokens) return messages;
4029
4467
  const head = messages[0]?.role === "system" ? [messages[0]] : [];
@@ -4032,7 +4470,7 @@ function fitTokenBudget(messages, maxTokens) {
4032
4470
  const ids = callIdSet(messages.slice(from));
4033
4471
  while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
4034
4472
  if (total > maxTokens)
4035
- log4.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
4473
+ log6.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
4036
4474
  return [...head, ...messages.slice(from)];
4037
4475
  }
4038
4476
  function compact(m, max, focus) {
@@ -4044,11 +4482,24 @@ function compact(m, max, focus) {
4044
4482
  if (dropped.length === 0) return [...head, ...tail];
4045
4483
  return [...head, { role: "system", content: summarize(dropped, focus) }, ...tail];
4046
4484
  }
4485
+ var SUMMARY_HEAD = "[summary of ";
4486
+ var MAX_CARRIED_COUNT = 1e4;
4487
+ var isSyntheticSummary = (m) => m.role === "system" && typeof m.content === "string" && m.content.startsWith(SUMMARY_HEAD);
4047
4488
  function summarize(messages, focus) {
4048
4489
  const tools = /* @__PURE__ */ new Set();
4049
4490
  const files = /* @__PURE__ */ new Set();
4491
+ const carried = [];
4492
+ let carriedCount = 0;
4493
+ let priors = 0;
4050
4494
  let lastAssistant = "";
4051
4495
  for (const msg of messages) {
4496
+ if (isSyntheticSummary(msg)) {
4497
+ priors++;
4498
+ const [head, ...rest] = String(msg.content).split("\n");
4499
+ carriedCount += Math.min(Math.max(Number(/of (\d+)/.exec(head)?.[1]) || 0, 0), MAX_CARRIED_COUNT);
4500
+ for (const l of rest) if (l.trim() && !carried.includes(l) && carried.length < 20) carried.push(l);
4501
+ continue;
4502
+ }
4052
4503
  for (const tc of msg.tool_calls ?? []) {
4053
4504
  tools.add(tc.function.name);
4054
4505
  try {
@@ -4059,14 +4510,16 @@ function summarize(messages, focus) {
4059
4510
  }
4060
4511
  if (msg.role === "assistant" && msg.content) lastAssistant = contentText(msg.content);
4061
4512
  }
4062
- const lines = [`[summary of ${messages.length} earlier message(s)]`];
4513
+ const lines = [`${SUMMARY_HEAD}${messages.length - priors + carriedCount} earlier message(s)]`];
4063
4514
  if (tools.size) lines.push(`tools used: ${[...tools].join(", ")}`);
4064
4515
  if (files.size) lines.push(`files touched: ${[...files].join(", ")}`);
4065
4516
  if (lastAssistant) lines.push(`last assistant: ${lastAssistant}`);
4517
+ if (carried.length) lines.push("earlier recap:", ...carried.map((l) => ` ${l}`));
4066
4518
  if (focus?.trim()) {
4067
4519
  const words = focus.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
4068
4520
  const kept = [];
4069
- outer: for (const msg of messages) {
4521
+ const ordered = [...messages.filter((m) => !isSyntheticSummary(m)), ...messages.filter(isSyntheticSummary)];
4522
+ outer: for (const msg of ordered) {
4070
4523
  if (msg.role === "tool") continue;
4071
4524
  for (const line of contentText(msg.content).split("\n")) {
4072
4525
  const l = line.toLowerCase();
@@ -4732,7 +5185,7 @@ init_tools_web();
4732
5185
  init_tools();
4733
5186
  init_tools_structured();
4734
5187
  init_logging();
4735
- var log6 = forComponent("scratch");
5188
+ var log8 = forComponent("scratch");
4736
5189
  var SCRATCH_DIR = "/scratch";
4737
5190
  function shortArgs(args) {
4738
5191
  try {
@@ -4772,7 +5225,7 @@ var Scratch = class {
4772
5225
  await (this.dirReady ??= mkdirp(this.fs, dir));
4773
5226
  await this.fs.writeFile(path, header + raw);
4774
5227
  } catch (e) {
4775
- log6.debug("scratch write failed; returning raw", e);
5228
+ log8.debug("scratch write failed; returning raw", e);
4776
5229
  return raw;
4777
5230
  }
4778
5231
  const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
@@ -4802,7 +5255,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
4802
5255
  await (this.dirReady ??= mkdirp(this.fs, dir));
4803
5256
  await this.fs.writeFile(path, header + full);
4804
5257
  } catch (e) {
4805
- log6.debug("scratch spill failed; cropping lossy", e);
5258
+ log8.debug("scratch spill failed; cropping lossy", e);
4806
5259
  return full.slice(0, pageBytes) + `
4807
5260
 
4808
5261
  [output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
@@ -4848,7 +5301,7 @@ Question: ${q}`);
4848
5301
  const answer = (res.text ?? "").trim();
4849
5302
  return answer || "(no answer found in scratch)";
4850
5303
  } catch (e) {
4851
- log6.debug("Ask peek failed", e);
5304
+ log8.debug("Ask peek failed", e);
4852
5305
  return `Error querying scratch: ${e?.message ?? e}`;
4853
5306
  }
4854
5307
  }
@@ -4857,7 +5310,7 @@ Question: ${q}`);
4857
5310
 
4858
5311
  // src/lessons.ts
4859
5312
  init_logging();
4860
- var log7 = forComponent("Lessons");
5313
+ var log9 = forComponent("Lessons");
4861
5314
  var LessonOptionsDefaults = class {
4862
5315
  minRepeats = 2;
4863
5316
  };
@@ -4882,15 +5335,16 @@ function lessonCapture(options) {
4882
5335
  counts.set(lesson.slug, n);
4883
5336
  if (n < o.minRepeats) return;
4884
5337
  written.add(lesson.slug);
4885
- await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log7.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
4886
- log7.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
5338
+ await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log9.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
5339
+ log9.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
4887
5340
  }
4888
5341
  };
4889
5342
  }
4890
5343
 
4891
5344
  // src/reflect.ts
5345
+ init_llm();
4892
5346
  init_logging();
4893
- var log8 = forComponent("Reflect");
5347
+ var log10 = forComponent("Reflect");
4894
5348
  async function reflectOnRun(o) {
4895
5349
  const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
4896
5350
  if (!digest.trim()) return null;
@@ -4906,7 +5360,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
4906
5360
  const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
4907
5361
  text = r?.content ?? "";
4908
5362
  } catch (e) {
4909
- log8.warn(`reflection call failed: ${e?.message ?? e}`);
5363
+ log10.warn(`reflection call failed: ${e?.message ?? e}`);
4910
5364
  return null;
4911
5365
  }
4912
5366
  const m = text.match(/LESSON:\s*(.+)/i);
@@ -4916,10 +5370,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
4916
5370
  try {
4917
5371
  await writeFact(o.fs, o.dir, slug2, lesson);
4918
5372
  } catch (e) {
4919
- log8.warn(`could not persist lesson: ${e?.message ?? e}`);
5373
+ log10.warn(`could not persist lesson: ${e?.message ?? e}`);
4920
5374
  return null;
4921
5375
  }
4922
- log8.debug(`reflection persisted ${slug2}`);
5376
+ log10.debug(`reflection persisted ${slug2}`);
4923
5377
  return slug2;
4924
5378
  }
4925
5379
  function digestRun(messages, maxChars) {
@@ -4935,6 +5389,7 @@ function digestRun(messages, maxChars) {
4935
5389
 
4936
5390
  // src/duplex.ts
4937
5391
  import { MemFilesystem as MemFilesystem2 } from "@livx.cc/wcli/core";
5392
+ init_llm();
4938
5393
  init_logging();
4939
5394
 
4940
5395
  // node_modules/@bod.ee/voice/src/core/logging.ts
@@ -4967,7 +5422,7 @@ function forComponent2(name) {
4967
5422
  }
4968
5423
 
4969
5424
  // node_modules/@bod.ee/voice/src/core/emotion.ts
4970
- var log9 = forComponent2("Emotion");
5425
+ var log11 = forComponent2("Emotion");
4971
5426
  var EMOTIONS = [
4972
5427
  // primary (best results)
4973
5428
  "neutral",
@@ -5121,7 +5576,7 @@ var EmotionStream = class {
5121
5576
  if (tag) {
5122
5577
  this.pending = tag;
5123
5578
  if (this.show) display += m[0];
5124
- } else log9.debug(`dropping unknown emotion tag ${m[0]}`);
5579
+ } else log11.debug(`dropping unknown emotion tag ${m[0]}`);
5125
5580
  last = m.index + m[0].length;
5126
5581
  }
5127
5582
  this.emit(body.slice(last), (s, d, p) => {
@@ -5144,7 +5599,7 @@ var EmotionStream = class {
5144
5599
  };
5145
5600
 
5146
5601
  // node_modules/@bod.ee/voice/src/core/engine.ts
5147
- var log10 = forComponent2("VoiceEngine");
5602
+ var log12 = forComponent2("VoiceEngine");
5148
5603
  var realClock = {
5149
5604
  now: () => performance.now(),
5150
5605
  setTimeout: (fn, ms) => setTimeout(fn, ms),
@@ -5419,7 +5874,7 @@ var VoiceEngine = class _VoiceEngine {
5419
5874
  await Promise.all([this.tts.connect(), this.stt.start()]);
5420
5875
  this.tts.warmup?.();
5421
5876
  this.setState("listening");
5422
- log10.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
5877
+ log12.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
5423
5878
  }
5424
5879
  get usingAec() {
5425
5880
  return this.stt.usingAec;
@@ -5441,7 +5896,7 @@ var VoiceEngine = class _VoiceEngine {
5441
5896
  this.options.onDiag({ t: this.clock.now(), kind, ...fields });
5442
5897
  } catch (e) {
5443
5898
  this.diagOn = false;
5444
- log10.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
5899
+ log12.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
5445
5900
  }
5446
5901
  }
5447
5902
  idleWaiters = [];
@@ -5550,7 +6005,7 @@ var VoiceEngine = class _VoiceEngine {
5550
6005
  if (prose.trim()) this.repliedSinceDispatch = true;
5551
6006
  for (const w of this.words(this.reply)) this.echoWords.add(w);
5552
6007
  this.tts.speak(forSpeech(speech), true);
5553
- if (!this.spokeDeltas && this.turnStartAt) log10.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
6008
+ if (!this.spokeDeltas && this.turnStartAt) log12.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
5554
6009
  this.spokeDeltas = true;
5555
6010
  this.setState("speaking");
5556
6011
  if (this.options.revealMode !== "off" && display) {
@@ -5627,7 +6082,7 @@ var VoiceEngine = class _VoiceEngine {
5627
6082
  this.drainTimer = null;
5628
6083
  this.speaking = false;
5629
6084
  this.stopWordReveal(true);
5630
- if (this.turnStartAt) log10.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
6085
+ if (this.turnStartAt) log12.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
5631
6086
  this.echoUntil = this.clock.now() + 2500;
5632
6087
  if (!this.usingAec) this.stt.reset();
5633
6088
  this.setState("listening");
@@ -5715,7 +6170,7 @@ var VoiceEngine = class _VoiceEngine {
5715
6170
  this.clearAckTimer("interrupt");
5716
6171
  this.adaptiveAckFired = false;
5717
6172
  this.ackProbe = null;
5718
- if (this.uttQueue.length) log10.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
6173
+ if (this.uttQueue.length) log12.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
5719
6174
  const droppedQueued = this.uttQueue.length;
5720
6175
  this.uttQueue = [];
5721
6176
  if (!this.speaking && !this.drainTimer) return;
@@ -5861,7 +6316,7 @@ var VoiceEngine = class _VoiceEngine {
5861
6316
  if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
5862
6317
  this.specSpent = true;
5863
6318
  this.specText = this.specPartial;
5864
- log10.debug(`speculate: "${this.specText.slice(0, 60)}"`);
6319
+ log12.debug(`speculate: "${this.specText.slice(0, 60)}"`);
5865
6320
  this.diag("speculate", { text: this.specText });
5866
6321
  this.options.onSpeculate(this.specText);
5867
6322
  }, this.options.speculativeMs);
@@ -5930,7 +6385,7 @@ var VoiceEngine = class _VoiceEngine {
5930
6385
  };
5931
6386
  this.tts.newContext();
5932
6387
  this.tts.speak(phrase, false);
5933
- log10.debug(`backchannel: "${phrase}"`);
6388
+ log12.debug(`backchannel: "${phrase}"`);
5934
6389
  this.diag("backchannel", { phrase });
5935
6390
  this.options.onBackchannel(phrase);
5936
6391
  }
@@ -5963,7 +6418,7 @@ var VoiceEngine = class _VoiceEngine {
5963
6418
  });
5964
6419
  if (idx < 0) return text;
5965
6420
  toks.splice(idx, 1);
5966
- log10.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
6421
+ log12.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
5967
6422
  return toks.join(" ");
5968
6423
  }
5969
6424
  /** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
@@ -6054,7 +6509,7 @@ var VoiceEngine = class _VoiceEngine {
6054
6509
  this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
6055
6510
  if (this.pendingTimer) this.clock.clearTimeout(this.pendingTimer);
6056
6511
  if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
6057
- log10.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
6512
+ log12.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
6058
6513
  this.diag("hold", { reason: "incomplete", tail: this.pendingUtt.slice(-40) });
6059
6514
  this.options.onHold();
6060
6515
  if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
@@ -6089,13 +6544,13 @@ var VoiceEngine = class _VoiceEngine {
6089
6544
  if (text) {
6090
6545
  const flat = text.toLowerCase().replace(/[^a-z0-9]/g, "");
6091
6546
  if (flat && flat === this.lastDispatchFlat && !this.repliedSinceDispatch && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
6092
- log10.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
6547
+ log12.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
6093
6548
  this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
6094
6549
  return;
6095
6550
  }
6096
6551
  const refinal = this.refinalizes(text);
6097
6552
  if (refinal && this.repliedSinceDispatch) {
6098
- log10.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
6553
+ log12.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
6099
6554
  this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
6100
6555
  this.lastDispatchFlat = flat;
6101
6556
  this.lastDispatchWords = text.trim().split(/\s+/);
@@ -6255,7 +6710,7 @@ var SpokenSplitter = class {
6255
6710
  };
6256
6711
 
6257
6712
  // node_modules/@bod.ee/voice/src/adapters/soniox.ts
6258
- var log11 = forComponent2("SonioxSTT");
6713
+ var log13 = forComponent2("SonioxSTT");
6259
6714
  var now = () => performance.now();
6260
6715
  var SonioxSTTOptions = class {
6261
6716
  auth = "";
@@ -6296,7 +6751,7 @@ var SonioxSTT = class {
6296
6751
  this.onDiag({ t: now(), kind, ...fields });
6297
6752
  } catch (e) {
6298
6753
  this.diagOn = false;
6299
- log11.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6754
+ log13.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6300
6755
  }
6301
6756
  }
6302
6757
  lastChunkAt = 0;
@@ -6338,11 +6793,11 @@ var SonioxSTT = class {
6338
6793
  this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
6339
6794
  this.ws.onclose = (ev) => {
6340
6795
  if (this.stopped) return;
6341
- log11.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
6796
+ log13.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
6342
6797
  this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
6343
6798
  this.reset();
6344
6799
  this.connectWs().catch((e) => {
6345
- log11.error(`soniox reconnect failed: ${e.message}`);
6800
+ log13.error(`soniox reconnect failed: ${e.message}`);
6346
6801
  this.diag("stt_reconnect_failed", { message: e.message });
6347
6802
  });
6348
6803
  };
@@ -6354,7 +6809,7 @@ var SonioxSTT = class {
6354
6809
  this.endpointTimer = setInterval(() => {
6355
6810
  const combined = (this.finalText + this.partialText).trim();
6356
6811
  if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
6357
- if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
6812
+ if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
6358
6813
  this.reset();
6359
6814
  this.onUtterance(combined, now());
6360
6815
  }, 120);
@@ -6366,7 +6821,7 @@ var SonioxSTT = class {
6366
6821
  if (this.stopped) return;
6367
6822
  const ref = this.lastChunkAt || this.startedChunksAt;
6368
6823
  if (now() - ref > noAudioMs) {
6369
- log11.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
6824
+ log13.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
6370
6825
  this.diag("stt_watchdog_fatal", { noAudioMs });
6371
6826
  this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
6372
6827
  this.stop();
@@ -6389,7 +6844,7 @@ var SonioxSTT = class {
6389
6844
  handle(m) {
6390
6845
  if (m.error_message) {
6391
6846
  this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
6392
- return log11.error(`soniox: ${m.error_message}`);
6847
+ return log13.error(`soniox: ${m.error_message}`);
6393
6848
  }
6394
6849
  let endpoint = false;
6395
6850
  for (const t of m.tokens ?? []) {
@@ -6406,7 +6861,7 @@ var SonioxSTT = class {
6406
6861
  this.onPartial(combined);
6407
6862
  if (endpoint && this.finalText.trim()) {
6408
6863
  const utterance = this.finalText.trim();
6409
- if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
6864
+ if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
6410
6865
  this.reset();
6411
6866
  this.onUtterance(utterance, now());
6412
6867
  }
@@ -6428,7 +6883,7 @@ var SonioxSTT = class {
6428
6883
  };
6429
6884
 
6430
6885
  // node_modules/@bod.ee/voice/src/adapters/cartesia.ts
6431
- var log12 = forComponent2("CartesiaTTS");
6886
+ var log14 = forComponent2("CartesiaTTS");
6432
6887
  var now2 = () => performance.now();
6433
6888
  var CartesiaTTSOptions = class {
6434
6889
  auth = "";
@@ -6464,7 +6919,7 @@ var CartesiaTTS = class _CartesiaTTS {
6464
6919
  this.onDiag({ t: now2(), kind, ...fields });
6465
6920
  } catch (e) {
6466
6921
  this.diagOn = false;
6467
- log12.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6922
+ log14.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
6468
6923
  }
6469
6924
  }
6470
6925
  firstAudioAt = 0;
@@ -6499,11 +6954,11 @@ var CartesiaTTS = class _CartesiaTTS {
6499
6954
  this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
6500
6955
  });
6501
6956
  this.ws.onclose = (ev) => {
6502
- log12.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
6957
+ log14.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
6503
6958
  this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
6504
6959
  if (!this.closed) {
6505
6960
  this.connecting = this.doConnect().catch((e) => {
6506
- log12.error(`cartesia reconnect failed: ${e.message}`);
6961
+ log14.error(`cartesia reconnect failed: ${e.message}`);
6507
6962
  this.diag("tts_reconnect_failed", { message: e.message });
6508
6963
  });
6509
6964
  }
@@ -6531,12 +6986,12 @@ var CartesiaTTS = class _CartesiaTTS {
6531
6986
  this.down = true;
6532
6987
  this.downAt = now2();
6533
6988
  this.consecutiveOk = 0;
6534
- log12.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
6989
+ log14.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
6535
6990
  this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
6536
6991
  this.onDone();
6537
6992
  this.startProbe();
6538
6993
  } else if (!this.down) {
6539
- (/No valid transcripts/i.test(m.message || "") ? log12.debug : log12.warn)(`cartesia: ${JSON.stringify(m)}`);
6994
+ (/No valid transcripts/i.test(m.message || "") ? log14.debug : log14.warn)(`cartesia: ${JSON.stringify(m)}`);
6540
6995
  }
6541
6996
  }
6542
6997
  };
@@ -6551,7 +7006,7 @@ var CartesiaTTS = class _CartesiaTTS {
6551
7006
  this.stopProbe();
6552
7007
  const downMs = this.downAt ? now2() - this.downAt : 0;
6553
7008
  this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
6554
- (downMs < 2e3 ? log12.debug : log12.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
7009
+ (downMs < 2e3 ? log14.debug : log14.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
6555
7010
  }
6556
7011
  /** Ensure the WS is open before sending — reconnects if idle-closed. */
6557
7012
  async ensureConnected() {
@@ -6642,7 +7097,7 @@ configureLogging((name) => {
6642
7097
  });
6643
7098
 
6644
7099
  // src/duplex.ts
6645
- var log13 = forComponent("DuplexAgent");
7100
+ var log15 = forComponent("DuplexAgent");
6646
7101
  function describeCall(call) {
6647
7102
  const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
6648
7103
  const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
@@ -6853,7 +7308,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
6853
7308
  const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
6854
7309
  if (m) {
6855
7310
  this.fabricationCut = true;
6856
- log13.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
7311
+ log15.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
6857
7312
  const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
6858
7313
  if (!safe) return;
6859
7314
  if (safe.trim()) this.spokeThisTurn = true;
@@ -6946,7 +7401,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
6946
7401
  return {
6947
7402
  preToolUse: (call) => {
6948
7403
  if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
6949
- log13.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
7404
+ log15.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
6950
7405
  this.abortSpeculation();
6951
7406
  return { block: true, reason: "Speculative turn aborted." };
6952
7407
  }
@@ -6984,7 +7439,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
6984
7439
  try {
6985
7440
  await this.voice.send(fallback ? "[reminder] You said nothing to the user this turn. Tell them, in ONE short spoken sentence, what just happened \u2014 no tools." : dispatched ? "[reminder] You dispatched a task but said nothing to the user. Say ONE short spoken acknowledgement now \u2014 no tools." : "[reminder] You said nothing to the user this turn. Give your ONE short spoken reply now \u2014 no tools.");
6986
7441
  } catch (e) {
6987
- log13.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
7442
+ log15.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
6988
7443
  } finally {
6989
7444
  this.nudging = false;
6990
7445
  }
@@ -7077,7 +7532,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7077
7532
  try {
7078
7533
  res = await this.voice.send(spec.text);
7079
7534
  } catch (e) {
7080
- log13.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
7535
+ log15.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
7081
7536
  } finally {
7082
7537
  this.voice.options.signal = prevSignal;
7083
7538
  }
@@ -7091,7 +7546,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
7091
7546
  if (d === "abort") {
7092
7547
  if (this.voice.transcript.length > base) this.voice.transcript.length = base;
7093
7548
  this.speculativeAbortedCalls++;
7094
- log13.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
7549
+ log15.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
7095
7550
  if (this.spec === spec) this.spec = void 0;
7096
7551
  return res ?? empty;
7097
7552
  }
@@ -7356,7 +7811,7 @@ Another agent just implemented the above. Independently check the CURRENT state
7356
7811
  this.notify("task_verify", `task ${id}: verifying`, { id });
7357
7812
  const cres = await new Agent(checkerOpts).run(checkBrief);
7358
7813
  if (cres.finishReason !== "stop") {
7359
- log13.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
7814
+ log15.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
7360
7815
  this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
7361
7816
  }
7362
7817
  const sum = (a = 0, b = 0) => a + b;
@@ -7422,14 +7877,14 @@ Another agent just implemented the above. Independently check the CURRENT state
7422
7877
  * '[task <id> asks] …' and resolves with the user's answer via AnswerTask — or '' on timeout/
7423
7878
  * task settle (callers map '' to deny / best-judgment). Workers never block forever. */
7424
7879
  parkQuestion(askId, question) {
7425
- return new Promise((resolve) => {
7880
+ return new Promise((resolve2) => {
7426
7881
  let settled = false;
7427
7882
  const finish = (answer) => {
7428
7883
  if (settled) return;
7429
7884
  settled = true;
7430
7885
  clearTimeout(timer);
7431
7886
  this.pendingAsks.delete(askId);
7432
- resolve(answer);
7887
+ resolve2(answer);
7433
7888
  };
7434
7889
  const timer = setTimeout(() => {
7435
7890
  this.notify("task_ask_timeout", `task ${askId}: question timed out \u2014 proceeding without an answer`);
@@ -7491,8 +7946,8 @@ Another agent just implemented the above. Independently check the CURRENT state
7491
7946
  }
7492
7947
  rec.status = "done";
7493
7948
  rec.result = res.text;
7494
- const incomplete = res.finishReason !== "stop";
7495
- log13.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
7949
+ const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
7950
+ log15.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
7496
7951
  this.notify("task_done", `task ${id} (${rec.label}) completed`, {
7497
7952
  id,
7498
7953
  text: res.text,
@@ -7534,14 +7989,14 @@ Another agent just implemented the above. Independently check the CURRENT state
7534
7989
  foldIfSuperseded(rec, note) {
7535
7990
  if (!(rec.deliveryParked && this.suppressParkedResume)) return false;
7536
7991
  if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
7537
- log13.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
7992
+ log15.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
7538
7993
  return true;
7539
7994
  }
7540
7995
  failTask(rec, msg) {
7541
7996
  this.dropAsk(rec.id);
7542
7997
  rec.status = "error";
7543
7998
  rec.result = msg;
7544
- log13.warn(`task ${rec.id} failed: ${msg}`);
7999
+ log15.warn(`task ${rec.id} failed: ${msg}`);
7545
8000
  this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
7546
8001
  if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
7547
8002
  this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
@@ -7846,11 +8301,11 @@ function buildMcpCatalog(servers) {
7846
8301
  }
7847
8302
  return { specs, routes };
7848
8303
  }
7849
- function searchOverCatalog(servers, specs, routes, resolve, options) {
8304
+ function searchOverCatalog(servers, specs, routes, resolve2, options) {
7850
8305
  const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
7851
8306
  const r = routes.get(name);
7852
8307
  if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
7853
- return resolve(r.server, r.rawName, args ?? {});
8308
+ return resolve2(r.server, r.rawName, args ?? {});
7854
8309
  }, options) : [];
7855
8310
  return { tools, serverNames: servers, toolCount: specs.length };
7856
8311
  }
@@ -7859,9 +8314,9 @@ function makeMcpToolSearchFromMounted(mounted, options) {
7859
8314
  const byName = new Map(mounted.map((m) => [m.name, m]));
7860
8315
  return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args) => byName.get(server).client.callTool(rawName, args), options);
7861
8316
  }
7862
- function makeLazyMcpToolSearch(servers, resolve, options) {
8317
+ function makeLazyMcpToolSearch(servers, resolve2, options) {
7863
8318
  const { specs, routes } = buildMcpCatalog(servers);
7864
- return searchOverCatalog(servers.map((s) => s.name), specs, routes, resolve, options);
8319
+ return searchOverCatalog(servers.map((s) => s.name), specs, routes, resolve2, options);
7865
8320
  }
7866
8321
 
7867
8322
  // src/hooks.ts
@@ -7920,6 +8375,7 @@ var RecordingLifecycle = class {
7920
8375
 
7921
8376
  // src/index.ts
7922
8377
  init_logging();
8378
+ init_llm();
7923
8379
  import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor as CommandExecutor2, registerHeadlessCommands as registerHeadlessCommands2 } from "@livx.cc/wcli/core";
7924
8380
  export {
7925
8381
  Agent,
@@ -7934,6 +8390,8 @@ export {
7934
8390
  DuplexAgent,
7935
8391
  DuplexAgentOptions,
7936
8392
  FakeAIClient,
8393
+ IMAGE_ELIDE_STUB,
8394
+ IMAGE_REF_SCHEME,
7937
8395
  IndexedDbFilesystem,
7938
8396
  JailOptions,
7939
8397
  JailedFilesystem,
@@ -7968,19 +8426,26 @@ export {
7968
8426
  checkpointTool,
7969
8427
  checkpointTools,
7970
8428
  cleanupWorktree,
8429
+ clearImageRefs,
7971
8430
  compileSynthesizedTool,
7972
8431
  composeHooks,
8432
+ contentBytes,
7973
8433
  contentText,
7974
8434
  cronMatches,
7975
8435
  decodeDdgUrl,
7976
8436
  defaultTools,
7977
8437
  diskAgentOptions,
7978
8438
  editTool,
8439
+ elideImages,
8440
+ elideInlineImages,
8441
+ elideStaleImages,
7979
8442
  enterWorktree,
8443
+ estimateTokens,
7980
8444
  exitSessionTool,
7981
8445
  exitWorktree,
7982
8446
  expandCommand,
7983
8447
  expandEntry,
8448
+ expandImagesForSend,
7984
8449
  expandTemplate,
7985
8450
  findGitRoot,
7986
8451
  forComponent,
@@ -7992,6 +8457,8 @@ export {
7992
8457
  htmlToText,
7993
8458
  idfWeights,
7994
8459
  imagePart,
8460
+ imageRefPart,
8461
+ imageRefResult,
7995
8462
  lessonCapture,
7996
8463
  loadAgents,
7997
8464
  loadCommands,
@@ -8013,12 +8480,17 @@ export {
8013
8480
  makeWebSearchTool,
8014
8481
  mcpToolToAgentTool,
8015
8482
  mcpToolsToAgentTools,
8483
+ messageHasImage,
8484
+ messageHasImageRef,
8485
+ messageHasInlineImage,
8486
+ mintImageRef,
8016
8487
  mkdirp,
8017
8488
  modelShortLabel,
8018
8489
  multiEditTool,
8019
8490
  nextCronAfter,
8020
8491
  parseCron,
8021
8492
  parseDdgHtml,
8493
+ parseImageRef,
8022
8494
  planMode,
8023
8495
  raceAttempts,
8024
8496
  readTool,
@@ -8026,6 +8498,7 @@ export {
8026
8498
  reflectOnRun,
8027
8499
  registerHeadlessCommands2 as registerHeadlessCommands,
8028
8500
  relevanceScore,
8501
+ remintImageRefs,
8029
8502
  repoIndex,
8030
8503
  repoMapTool,
8031
8504
  resolveAuth,
@@ -8035,11 +8508,14 @@ export {
8035
8508
  sandboxAgentOptions,
8036
8509
  scanCommands,
8037
8510
  scanSkills,
8511
+ sendBytes,
8512
+ sendBytesPer,
8038
8513
  slugify,
8039
8514
  toWireTools,
8040
8515
  todoWriteTool,
8041
8516
  tokenize,
8042
8517
  toolCall,
8518
+ toolImageRef,
8043
8519
  toolRegistry,
8044
8520
  toolsByName,
8045
8521
  topByRelevance,