@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/{Agent-ChSs1QSx.d.ts → Agent-DC7FEEjF.d.ts} +58 -12
- package/dist/cli.d.ts +10 -7
- package/dist/cli.js +775 -299
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +618 -142
- package/dist/index.js.map +1 -1
- package/dist/{mcp-DiAiMiG5.d.ts → mcp-Cnq0pS95.d.ts} +1 -1
- package/dist/mcp.client.d.ts +2 -2
- package/dist/{tools-CJAPSBhS.d.ts → tools-BZVppx-6.d.ts} +98 -1
- package/dist/tools.shell.d.ts +1 -1
- package/dist/tools.shell.js +20 -11
- package/dist/tools.shell.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
3183
|
-
|
|
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
|
|
3259
|
-
*
|
|
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
|
-
|
|
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`)
|
|
3565
|
-
*
|
|
3566
|
-
*
|
|
3567
|
-
*
|
|
3568
|
-
*
|
|
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
|
|
3572
|
-
|
|
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
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
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
|
-
|
|
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() -
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3726
|
-
this.transcript.push({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
3964
|
-
out = fitTokenBudget(out
|
|
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
|
|
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
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
return Math.ceil(
|
|
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((
|
|
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
|
-
|
|
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 = [
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) =>
|
|
4886
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
5373
|
+
log10.warn(`could not persist lesson: ${e?.message ?? e}`);
|
|
4920
5374
|
return null;
|
|
4921
5375
|
}
|
|
4922
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 || "") ?
|
|
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 ?
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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,
|
|
8317
|
+
function makeLazyMcpToolSearch(servers, resolve2, options) {
|
|
7863
8318
|
const { specs, routes } = buildMcpCatalog(servers);
|
|
7864
|
-
return searchOverCatalog(servers.map((s) => s.name), specs, routes,
|
|
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,
|