@livx.cc/agentx 0.99.26 → 0.99.29
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-qoEmC4ic.d.ts} +54 -10
- package/dist/cli.d.ts +10 -7
- package/dist/cli.js +787 -332
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -5
- package/dist/index.js +610 -139
- 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). */
|
|
@@ -3443,7 +3768,7 @@ var Agent = class _Agent {
|
|
|
3443
3768
|
const disk = new NodeDiskFilesystem2(process.cwd());
|
|
3444
3769
|
await disk.init();
|
|
3445
3770
|
this.options.fs = new JailedFilesystem2(disk);
|
|
3446
|
-
|
|
3771
|
+
log6.info(`no fs provided \u2014 defaulting to jailed real disk at ${process.cwd()}`);
|
|
3447
3772
|
}
|
|
3448
3773
|
this.buildCtx();
|
|
3449
3774
|
}
|
|
@@ -3561,19 +3886,42 @@ var Agent = class _Agent {
|
|
|
3561
3886
|
return this.runLoop();
|
|
3562
3887
|
}
|
|
3563
3888
|
/**
|
|
3564
|
-
* Fold the conversation in place (manual `/compact`)
|
|
3565
|
-
*
|
|
3566
|
-
*
|
|
3567
|
-
*
|
|
3568
|
-
*
|
|
3889
|
+
* Fold the conversation in place (manual `/compact`) until it fits a TOKEN budget — not a message
|
|
3890
|
+
* count. Message-count folding alone frees almost nothing when a few recent tool results are huge,
|
|
3891
|
+
* so we escalate: fold the middle into a synthetic recap → elide oversized tool-result bodies in the
|
|
3892
|
+
* retained window → halve the window → drop-oldest backstop. Deterministic, no LLM call.
|
|
3893
|
+
* `focus` (e.g. `/compact keep the API details`) preserves matching lines from the dropped span verbatim.
|
|
3894
|
+
* Returns before/after token estimates so the caller can report honestly (0 freed => say so).
|
|
3569
3895
|
*/
|
|
3570
3896
|
compactNow(maxMessages = 12, focus) {
|
|
3571
|
-
const
|
|
3572
|
-
|
|
3897
|
+
const cap = _Agent.contextCap(this.options);
|
|
3898
|
+
const target = Math.round(cap * _Agent.COMPACT_TARGET);
|
|
3899
|
+
const kr = this.options.keepRecentImages ?? 1;
|
|
3900
|
+
const tokensBefore = estimateTokens(this.transcript, kr);
|
|
3901
|
+
const msgsBefore = this.transcript.length;
|
|
3902
|
+
let max = Math.max(2, maxMessages);
|
|
3903
|
+
if (this.transcript.length <= max && tokensBefore <= target)
|
|
3904
|
+
return { tokensBefore, tokensAfter: tokensBefore, target, cap, removed: 0 };
|
|
3573
3905
|
void this.activeHooks?.onPreCompact?.(this.transcript);
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3906
|
+
let out = this.transcript;
|
|
3907
|
+
if (out.length > max) out = compact(out, max, focus);
|
|
3908
|
+
if (estimateTokens(out, kr) > target) out = elideOversizedImages(out, target, true, kr);
|
|
3909
|
+
if (estimateTokens(out, kr) > target) out = capOversizedBodies(out, target, kr);
|
|
3910
|
+
if (estimateTokens(out, kr) > target) out = elideOversizedImages(out, target, false, kr);
|
|
3911
|
+
while (estimateTokens(out, kr) > target && max > 2 && out.length > 2) {
|
|
3912
|
+
max = Math.max(2, Math.floor(max / 2));
|
|
3913
|
+
out = capOversizedBodies(compact(out, max, focus), target, kr);
|
|
3914
|
+
}
|
|
3915
|
+
if (estimateTokens(out, kr) > target) out = fitTokenBudget(out, target, kr);
|
|
3916
|
+
out = dropOrphanToolResults(out);
|
|
3917
|
+
this.transcript = out;
|
|
3918
|
+
this.releaseStaleImages();
|
|
3919
|
+
return { tokensBefore, tokensAfter: estimateTokens(this.transcript, kr), target, cap, removed: msgsBefore - this.transcript.length };
|
|
3920
|
+
}
|
|
3921
|
+
/** Drop inline base64 images from older messages so Bun/JSC can reclaim the heap (see contentBytes). */
|
|
3922
|
+
releaseStaleImages(keepRecent = 1) {
|
|
3923
|
+
const next = elideStaleImages(this.transcript, keepRecent);
|
|
3924
|
+
if (next !== this.transcript) this.transcript = next;
|
|
3577
3925
|
}
|
|
3578
3926
|
async runLoop() {
|
|
3579
3927
|
const o = this.options;
|
|
@@ -3589,15 +3937,16 @@ var Agent = class _Agent {
|
|
|
3589
3937
|
let lastFp = "";
|
|
3590
3938
|
let repeats = 0;
|
|
3591
3939
|
let closureNudged = false;
|
|
3940
|
+
let lastProgressAt = start;
|
|
3592
3941
|
const kill = (finishReason) => {
|
|
3593
|
-
|
|
3942
|
+
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
3943
|
this.ctx.jobs?.killAll();
|
|
3595
3944
|
return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated };
|
|
3596
3945
|
};
|
|
3597
3946
|
while (true) {
|
|
3598
3947
|
if (o.signal?.aborted) return kill("aborted");
|
|
3599
3948
|
if (steps >= o.maxSteps) return kill("max_steps");
|
|
3600
|
-
if (o.timeoutMs && Date.now() -
|
|
3949
|
+
if (o.timeoutMs && Date.now() - lastProgressAt - this.parkedMs >= o.timeoutMs) return kill("timeout");
|
|
3601
3950
|
const budgetTokens = usage.totalTokens - 0.9 * usage.cacheReadTokens;
|
|
3602
3951
|
if (o.maxTokens && budgetTokens >= o.maxTokens) return kill("budget");
|
|
3603
3952
|
steps++;
|
|
@@ -3637,27 +3986,38 @@ var Agent = class _Agent {
|
|
|
3637
3986
|
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
3987
|
res = r;
|
|
3639
3988
|
}
|
|
3989
|
+
if (steps === 1 && this.isEmptyStop(res) && !this.pendingInjections.length) {
|
|
3990
|
+
const e = new Error("model returned an empty turn (finishReason=stop, no text, no tools)");
|
|
3991
|
+
e.code = "empty";
|
|
3992
|
+
throw e;
|
|
3993
|
+
}
|
|
3640
3994
|
break;
|
|
3641
3995
|
} catch (err) {
|
|
3642
3996
|
const sc = err?.statusCode;
|
|
3643
3997
|
const serverSide = sc >= 500 || sc === 408 || /Service Unavailable|overloaded|Internal server error|Bad gateway|Gateway time/i.test(String(err?.message ?? ""));
|
|
3644
3998
|
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
3999
|
const stalled = err?.code === "stall";
|
|
3646
|
-
const
|
|
4000
|
+
const empty = err?.code === "empty" || err?.code === "cursor_silent";
|
|
4001
|
+
const maxAttempts = empty ? Math.max(o.emptyRetries, 0) : 2;
|
|
4002
|
+
const transient = !o.signal?.aborted && !isAbortError(err) && attempt < maxAttempts && (network || serverSide || stalled || empty);
|
|
3647
4003
|
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})` });
|
|
4004
|
+
const waitMs = (empty ? o.emptyRetryBaseMs : 1e3) * (attempt + 1);
|
|
4005
|
+
log6.warn(`${empty ? "empty model turn" : stalled ? "model stall" : "network drop"} mid-step (${err?.message ?? err}) \u2014 retrying in ${waitMs}ms`);
|
|
4006
|
+
o.host?.notify?.({ kind: "retry", message: `${empty ? "model returned nothing" : stalled ? "model stalled" : "connection dropped"} \u2014 retrying step (#${attempt + 1})` });
|
|
3651
4007
|
await new Promise((r) => setTimeout(r, waitMs));
|
|
3652
4008
|
}
|
|
3653
4009
|
}
|
|
3654
4010
|
} catch (err) {
|
|
3655
4011
|
if (err?.code === "budget") return kill("budget");
|
|
3656
4012
|
if (err?.code === "stall") {
|
|
3657
|
-
|
|
4013
|
+
log6.warn(`model stalled out after retries: ${err?.message ?? err}`);
|
|
3658
4014
|
return kill("timeout");
|
|
3659
4015
|
}
|
|
3660
4016
|
if (o.signal?.aborted || isAbortError(err)) return kill("aborted");
|
|
4017
|
+
if (err?.code === "empty" || err?.code === "cursor_silent") {
|
|
4018
|
+
log6.warn(`model returned empty/silent turns after retries \u2014 surfacing as failed`);
|
|
4019
|
+
return { text: "", steps, finishReason: "empty", messages: this.transcript, usage, usageEstimated, error: err };
|
|
4020
|
+
}
|
|
3661
4021
|
const body = err?.body ?? err?.response?.data ?? err?.error;
|
|
3662
4022
|
let bodyStr;
|
|
3663
4023
|
try {
|
|
@@ -3667,7 +4027,7 @@ var Agent = class _Agent {
|
|
|
3667
4027
|
}
|
|
3668
4028
|
if (bodyStr && err instanceof Error && !err.message.includes(bodyStr)) err.detail = bodyStr;
|
|
3669
4029
|
const errLog = err instanceof Error ? { message: err.message, ...err.detail ? { detail: err.detail } : {}, stack: err.stack } : err;
|
|
3670
|
-
|
|
4030
|
+
log6.error(`chat() failed: ${err?.message ?? err}${bodyStr ? ` \u2014 ${bodyStr}` : ""}`, errLog);
|
|
3671
4031
|
return { text: "", steps, finishReason: "error", messages: this.transcript, usage, usageEstimated, error: err };
|
|
3672
4032
|
}
|
|
3673
4033
|
if (o.signal?.aborted) return kill("aborted");
|
|
@@ -3711,6 +4071,7 @@ var Agent = class _Agent {
|
|
|
3711
4071
|
}
|
|
3712
4072
|
const toolCalls = res.toolCalls ?? [];
|
|
3713
4073
|
const emptyTurn = toolCalls.length === 0 && delegatedTools.length === 0 && contentText(res.content ?? "").trim() === "";
|
|
4074
|
+
if (!emptyTurn) lastProgressAt = Date.now();
|
|
3714
4075
|
if (!emptyTurn) {
|
|
3715
4076
|
this.transcript.push({
|
|
3716
4077
|
role: "assistant",
|
|
@@ -3722,12 +4083,13 @@ var Agent = class _Agent {
|
|
|
3722
4083
|
if (this.drainInjections()) continue;
|
|
3723
4084
|
if (o.closeDelegatedTurns && res.endedOnToolNoText && !closureNudged && steps < o.maxSteps) {
|
|
3724
4085
|
closureNudged = true;
|
|
3725
|
-
|
|
4086
|
+
log6.verbose("delegated turn went silent after a tool \u2014 nudging a closing summary");
|
|
3726
4087
|
this.transcript.push({ role: "user", content: "Summarize what you did this turn and what (if anything) still needs doing. Be concise." });
|
|
3727
4088
|
continue;
|
|
3728
4089
|
}
|
|
3729
|
-
|
|
4090
|
+
log6.verbose(`completed in ${steps} step(s)`);
|
|
3730
4091
|
await this.ctx.jobs?.drain();
|
|
4092
|
+
this.releaseStaleImages();
|
|
3731
4093
|
await this.activeHooks?.onStop?.(res.content ?? "");
|
|
3732
4094
|
return { text: res.content ?? "", steps, finishReason: "stop", messages: this.transcript, usage, usageEstimated };
|
|
3733
4095
|
}
|
|
@@ -3753,7 +4115,7 @@ var Agent = class _Agent {
|
|
|
3753
4115
|
if (this.ctx.needsInput) {
|
|
3754
4116
|
const question = this.ctx.needsInput;
|
|
3755
4117
|
this.ctx.needsInput = void 0;
|
|
3756
|
-
|
|
4118
|
+
log6.warn(`needs_input: ${question.question} (steps=${steps})`);
|
|
3757
4119
|
await this.ctx.jobs?.drain();
|
|
3758
4120
|
const text = [
|
|
3759
4121
|
`OPEN QUESTION (run paused \u2014 no human was reachable): ${question.question}`,
|
|
@@ -3810,6 +4172,12 @@ var Agent = class _Agent {
|
|
|
3810
4172
|
}
|
|
3811
4173
|
};
|
|
3812
4174
|
}
|
|
4175
|
+
/** A clean-stop turn that carries no assistant text AND no tool calls (native or delegated) — a
|
|
4176
|
+
* non-result the model produced by going silent. Callers retry it rather than report "done". */
|
|
4177
|
+
isEmptyStop(res) {
|
|
4178
|
+
const stop = res.finishReason === "stop" || res.finishReason == null;
|
|
4179
|
+
return stop && (res.toolCalls?.length ?? 0) === 0 && (res.delegatedTools?.length ?? 0) === 0 && contentText(res.content ?? "").trim() === "";
|
|
4180
|
+
}
|
|
3813
4181
|
async consumeStream(stream, poke) {
|
|
3814
4182
|
let content = "";
|
|
3815
4183
|
let finishReason;
|
|
@@ -3868,13 +4236,13 @@ var Agent = class _Agent {
|
|
|
3868
4236
|
const decision = await this.park(Promise.resolve(hooks?.preToolUse?.(call, meta)));
|
|
3869
4237
|
if (decision?.block) {
|
|
3870
4238
|
const blocked = `Blocked by hook: ${decision.reason ?? "no reason given"}`;
|
|
3871
|
-
|
|
4239
|
+
log6.debug(`${tc.function.name} -> ${blocked}`);
|
|
3872
4240
|
await hooks?.postToolUse?.(call, blocked, meta);
|
|
3873
4241
|
return blocked;
|
|
3874
4242
|
}
|
|
3875
4243
|
this.options.host?.notify?.({ kind: "tool_use", id: tc.id ?? "", name: tc.function.name, input: args });
|
|
3876
4244
|
if (earlyError) {
|
|
3877
|
-
|
|
4245
|
+
log6.debug(`${tc.function.name} -> ${earlyError}`);
|
|
3878
4246
|
await hooks?.postToolUse?.(call, earlyError, meta);
|
|
3879
4247
|
this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: earlyError, isError: true });
|
|
3880
4248
|
return earlyError;
|
|
@@ -3883,12 +4251,12 @@ var Agent = class _Agent {
|
|
|
3883
4251
|
let images;
|
|
3884
4252
|
let threw = false;
|
|
3885
4253
|
try {
|
|
3886
|
-
|
|
4254
|
+
log6.debug(`${tc.function.name}(${tc.function.arguments})`);
|
|
3887
4255
|
this.ctx.emit = hooks?.onToolOutput ? (chunk) => {
|
|
3888
4256
|
try {
|
|
3889
4257
|
hooks.onToolOutput(call, chunk, meta);
|
|
3890
4258
|
} catch (e) {
|
|
3891
|
-
|
|
4259
|
+
log6.debug(`onToolOutput hook error: ${e}`);
|
|
3892
4260
|
}
|
|
3893
4261
|
} : void 0;
|
|
3894
4262
|
const raw = await tool.run(args, this.ctx);
|
|
@@ -3900,7 +4268,7 @@ var Agent = class _Agent {
|
|
|
3900
4268
|
}
|
|
3901
4269
|
} catch (e) {
|
|
3902
4270
|
const msg = e instanceof Error ? e.message : String(e);
|
|
3903
|
-
|
|
4271
|
+
log6.debug(`${tc.function.name} -> error: ${msg}`);
|
|
3904
4272
|
result = `Error: ${msg}`;
|
|
3905
4273
|
threw = true;
|
|
3906
4274
|
} finally {
|
|
@@ -3926,6 +4294,16 @@ var Agent = class _Agent {
|
|
|
3926
4294
|
}
|
|
3927
4295
|
return images?.length ? { text: result, images } : result;
|
|
3928
4296
|
}
|
|
4297
|
+
/** `/compact` aims to land the transcript at this fraction of the context window. */
|
|
4298
|
+
static COMPACT_TARGET = 0.5;
|
|
4299
|
+
/** THE context-window cap resolution — `/compact`'s budget and the CLI's context footer/warnings both
|
|
4300
|
+
* route here, so the percentage the user reads and the one compaction targets can never drift. */
|
|
4301
|
+
static contextCap(o) {
|
|
4302
|
+
if (o.maxContextTokens) return o.maxContextTokens;
|
|
4303
|
+
const w = o.contextWindow || 0;
|
|
4304
|
+
if (w > ANTHROPIC_WINDOW_WITHOUT_1M_BETA && isAnthropicModel(o.model)) return ANTHROPIC_WINDOW_WITHOUT_1M_BETA;
|
|
4305
|
+
return w || 2e5;
|
|
4306
|
+
}
|
|
3929
4307
|
static WRITE_CLASS = ["Write", "Edit", "MultiEdit", "ApplyEdits"];
|
|
3930
4308
|
/** Append an autoTest failure section to a write-class tool result, if configured. */
|
|
3931
4309
|
async maybeAutoTest(toolName, result) {
|
|
@@ -3959,26 +4337,27 @@ ${out}`;
|
|
|
3959
4337
|
const browserKeep = this.browserActive ? o.browserTrim?.keepOutputs : void 0;
|
|
3960
4338
|
const keep = browserKeep != null ? o.keepToolOutputs ? Math.min(o.keepToolOutputs, browserKeep) : browserKeep : o.keepToolOutputs;
|
|
3961
4339
|
if (keep) out = stubOldToolResults(out ?? m, keep);
|
|
4340
|
+
out = expandImagesForSend(dropOrphanToolResults(out ?? m), o.keepRecentImages ?? 1);
|
|
3962
4341
|
if (o.maxContextTokens) {
|
|
3963
|
-
const pre =
|
|
3964
|
-
out = fitTokenBudget(out
|
|
4342
|
+
const pre = out.length;
|
|
4343
|
+
out = fitTokenBudget(out, o.maxContextTokens, o.keepRecentImages ?? 1);
|
|
3965
4344
|
const dropped = pre - out.length;
|
|
3966
4345
|
if (dropped > 0 && dropped !== this.lastTrimNotified) {
|
|
3967
4346
|
this.lastTrimNotified = dropped;
|
|
3968
4347
|
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
4348
|
}
|
|
3970
4349
|
}
|
|
3971
|
-
return dropOrphanToolResults(out
|
|
4350
|
+
return dropOrphanToolResults(out);
|
|
3972
4351
|
}
|
|
3973
4352
|
};
|
|
3974
4353
|
function dropOldest(m, max) {
|
|
3975
4354
|
const head = m[0]?.role === "system" ? [m[0]] : [];
|
|
3976
4355
|
return [...head, ...m.slice(-(max - head.length))];
|
|
3977
4356
|
}
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
return Math.ceil(
|
|
4357
|
+
var ANTHROPIC_WINDOW_WITHOUT_1M_BETA = 2e5;
|
|
4358
|
+
var isAnthropicModel = (m) => !!m && (m.startsWith("anthropic/") || /(^|\/)claude-/.test(m));
|
|
4359
|
+
function estimateTokens(m, keepRecentImages = 1) {
|
|
4360
|
+
return Math.ceil(sendBytes(m, keepRecentImages) / 4);
|
|
3982
4361
|
}
|
|
3983
4362
|
function cropResult(result, cap) {
|
|
3984
4363
|
const head = result.slice(0, cap);
|
|
@@ -4012,6 +4391,60 @@ function stubOldToolResults(messages, keep) {
|
|
|
4012
4391
|
return { ...x, content: `[${x.name ?? "tool"}${where ? ` ${where}` : ""} output elided \u2014 ${lines} lines; re-run the tool to view]` };
|
|
4013
4392
|
});
|
|
4014
4393
|
}
|
|
4394
|
+
function elideOversizedImages(messages, target, protectNewest = false, keepRecentImages = 1) {
|
|
4395
|
+
let out = messages;
|
|
4396
|
+
const withImages = out.map((_, i) => i).filter((i) => messageHasImage(out[i].content));
|
|
4397
|
+
const newest = protectNewest ? withImages[withImages.length - 1] : -1;
|
|
4398
|
+
const weight = sendBytesPer(out, keepRecentImages);
|
|
4399
|
+
const heaviestFirst = withImages.filter((i) => i !== newest).sort((a, b) => weight[b] - weight[a]);
|
|
4400
|
+
for (const i of heaviestFirst) {
|
|
4401
|
+
if (estimateTokens(out, keepRecentImages) <= target) break;
|
|
4402
|
+
const c = elideImages(out[i].content);
|
|
4403
|
+
if (c === out[i].content) continue;
|
|
4404
|
+
out = out.map((m, j) => j === i ? { ...m, content: c } : m);
|
|
4405
|
+
}
|
|
4406
|
+
return out;
|
|
4407
|
+
}
|
|
4408
|
+
function capOversizedBodies(messages, target, keepRecentImages = 1) {
|
|
4409
|
+
let out = messages;
|
|
4410
|
+
for (const cap of [8e3, 4e3, 2e3, 800, 300, 120]) {
|
|
4411
|
+
if (estimateTokens(out, keepRecentImages) <= target) break;
|
|
4412
|
+
out = out.map((x) => x.role === "system" ? x : elideBody(x, cap));
|
|
4413
|
+
}
|
|
4414
|
+
return out;
|
|
4415
|
+
}
|
|
4416
|
+
function elideBody(x, cap) {
|
|
4417
|
+
const note = (bytes, imgs2) => `
|
|
4418
|
+
|
|
4419
|
+
[${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` : ""}]`;
|
|
4420
|
+
if (!Array.isArray(x.content)) {
|
|
4421
|
+
const text = contentText(x.content);
|
|
4422
|
+
if (text.length <= cap) return x;
|
|
4423
|
+
return { ...x, content: `${text.slice(0, cap)}${note(text.length - cap, 0)}` };
|
|
4424
|
+
}
|
|
4425
|
+
const imgs = x.content.filter((p) => p.type !== "text").length;
|
|
4426
|
+
let left = cap;
|
|
4427
|
+
let elided = 0;
|
|
4428
|
+
const parts = [];
|
|
4429
|
+
for (const p of x.content) {
|
|
4430
|
+
if (p.type !== "text") {
|
|
4431
|
+
parts.push(p);
|
|
4432
|
+
continue;
|
|
4433
|
+
}
|
|
4434
|
+
const t = p.text ?? "";
|
|
4435
|
+
if (t.length <= left) {
|
|
4436
|
+
left -= t.length;
|
|
4437
|
+
if (t) parts.push(p);
|
|
4438
|
+
continue;
|
|
4439
|
+
}
|
|
4440
|
+
const keep = left;
|
|
4441
|
+
left = 0;
|
|
4442
|
+
elided += t.length - keep;
|
|
4443
|
+
if (keep > 0) parts.push({ ...p, text: t.slice(0, keep) });
|
|
4444
|
+
}
|
|
4445
|
+
if (!elided) return parts.length === x.content.length || !parts.length ? x : { ...x, content: parts };
|
|
4446
|
+
return { ...x, content: [...parts, { type: "text", text: note(elided, imgs).trimStart() }] };
|
|
4447
|
+
}
|
|
4015
4448
|
var callIdSet = (msgs) => {
|
|
4016
4449
|
const ids = /* @__PURE__ */ new Set();
|
|
4017
4450
|
for (const m of msgs) if (m.role === "assistant") for (const tc of m.tool_calls ?? []) ids.add(tc.id);
|
|
@@ -4022,8 +4455,8 @@ function dropOrphanToolResults(messages) {
|
|
|
4022
4455
|
const ok = (m) => m.role !== "tool" || ids.has(m.tool_call_id ?? "");
|
|
4023
4456
|
return messages.every(ok) ? messages : messages.filter(ok);
|
|
4024
4457
|
}
|
|
4025
|
-
function fitTokenBudget(messages, maxTokens) {
|
|
4026
|
-
const per = messages.map((
|
|
4458
|
+
function fitTokenBudget(messages, maxTokens, keepRecentImages = 1) {
|
|
4459
|
+
const per = sendBytesPer(messages, keepRecentImages).map((b) => Math.ceil(b / 4));
|
|
4027
4460
|
let total = per.reduce((a, b) => a + b, 0);
|
|
4028
4461
|
if (total <= maxTokens) return messages;
|
|
4029
4462
|
const head = messages[0]?.role === "system" ? [messages[0]] : [];
|
|
@@ -4032,7 +4465,7 @@ function fitTokenBudget(messages, maxTokens) {
|
|
|
4032
4465
|
const ids = callIdSet(messages.slice(from));
|
|
4033
4466
|
while (from < messages.length && messages[from].role === "tool" && !ids.has(messages[from].tool_call_id ?? "")) total -= per[from++];
|
|
4034
4467
|
if (total > maxTokens)
|
|
4035
|
-
|
|
4468
|
+
log6.warn(`context ~${total} tok still over maxContextTokens=${maxTokens} after trimming (system head can't be dropped)`);
|
|
4036
4469
|
return [...head, ...messages.slice(from)];
|
|
4037
4470
|
}
|
|
4038
4471
|
function compact(m, max, focus) {
|
|
@@ -4044,11 +4477,24 @@ function compact(m, max, focus) {
|
|
|
4044
4477
|
if (dropped.length === 0) return [...head, ...tail];
|
|
4045
4478
|
return [...head, { role: "system", content: summarize(dropped, focus) }, ...tail];
|
|
4046
4479
|
}
|
|
4480
|
+
var SUMMARY_HEAD = "[summary of ";
|
|
4481
|
+
var MAX_CARRIED_COUNT = 1e4;
|
|
4482
|
+
var isSyntheticSummary = (m) => m.role === "system" && typeof m.content === "string" && m.content.startsWith(SUMMARY_HEAD);
|
|
4047
4483
|
function summarize(messages, focus) {
|
|
4048
4484
|
const tools = /* @__PURE__ */ new Set();
|
|
4049
4485
|
const files = /* @__PURE__ */ new Set();
|
|
4486
|
+
const carried = [];
|
|
4487
|
+
let carriedCount = 0;
|
|
4488
|
+
let priors = 0;
|
|
4050
4489
|
let lastAssistant = "";
|
|
4051
4490
|
for (const msg of messages) {
|
|
4491
|
+
if (isSyntheticSummary(msg)) {
|
|
4492
|
+
priors++;
|
|
4493
|
+
const [head, ...rest] = String(msg.content).split("\n");
|
|
4494
|
+
carriedCount += Math.min(Math.max(Number(/of (\d+)/.exec(head)?.[1]) || 0, 0), MAX_CARRIED_COUNT);
|
|
4495
|
+
for (const l of rest) if (l.trim() && !carried.includes(l) && carried.length < 20) carried.push(l);
|
|
4496
|
+
continue;
|
|
4497
|
+
}
|
|
4052
4498
|
for (const tc of msg.tool_calls ?? []) {
|
|
4053
4499
|
tools.add(tc.function.name);
|
|
4054
4500
|
try {
|
|
@@ -4059,14 +4505,16 @@ function summarize(messages, focus) {
|
|
|
4059
4505
|
}
|
|
4060
4506
|
if (msg.role === "assistant" && msg.content) lastAssistant = contentText(msg.content);
|
|
4061
4507
|
}
|
|
4062
|
-
const lines = [
|
|
4508
|
+
const lines = [`${SUMMARY_HEAD}${messages.length - priors + carriedCount} earlier message(s)]`];
|
|
4063
4509
|
if (tools.size) lines.push(`tools used: ${[...tools].join(", ")}`);
|
|
4064
4510
|
if (files.size) lines.push(`files touched: ${[...files].join(", ")}`);
|
|
4065
4511
|
if (lastAssistant) lines.push(`last assistant: ${lastAssistant}`);
|
|
4512
|
+
if (carried.length) lines.push("earlier recap:", ...carried.map((l) => ` ${l}`));
|
|
4066
4513
|
if (focus?.trim()) {
|
|
4067
4514
|
const words = focus.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
4068
4515
|
const kept = [];
|
|
4069
|
-
|
|
4516
|
+
const ordered = [...messages.filter((m) => !isSyntheticSummary(m)), ...messages.filter(isSyntheticSummary)];
|
|
4517
|
+
outer: for (const msg of ordered) {
|
|
4070
4518
|
if (msg.role === "tool") continue;
|
|
4071
4519
|
for (const line of contentText(msg.content).split("\n")) {
|
|
4072
4520
|
const l = line.toLowerCase();
|
|
@@ -4732,7 +5180,7 @@ init_tools_web();
|
|
|
4732
5180
|
init_tools();
|
|
4733
5181
|
init_tools_structured();
|
|
4734
5182
|
init_logging();
|
|
4735
|
-
var
|
|
5183
|
+
var log8 = forComponent("scratch");
|
|
4736
5184
|
var SCRATCH_DIR = "/scratch";
|
|
4737
5185
|
function shortArgs(args) {
|
|
4738
5186
|
try {
|
|
@@ -4772,7 +5220,7 @@ var Scratch = class {
|
|
|
4772
5220
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
4773
5221
|
await this.fs.writeFile(path, header + raw);
|
|
4774
5222
|
} catch (e) {
|
|
4775
|
-
|
|
5223
|
+
log8.debug("scratch write failed; returning raw", e);
|
|
4776
5224
|
return raw;
|
|
4777
5225
|
}
|
|
4778
5226
|
const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
|
|
@@ -4802,7 +5250,7 @@ To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026",
|
|
|
4802
5250
|
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
4803
5251
|
await this.fs.writeFile(path, header + full);
|
|
4804
5252
|
} catch (e) {
|
|
4805
|
-
|
|
5253
|
+
log8.debug("scratch spill failed; cropping lossy", e);
|
|
4806
5254
|
return full.slice(0, pageBytes) + `
|
|
4807
5255
|
|
|
4808
5256
|
[output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
|
|
@@ -4848,7 +5296,7 @@ Question: ${q}`);
|
|
|
4848
5296
|
const answer = (res.text ?? "").trim();
|
|
4849
5297
|
return answer || "(no answer found in scratch)";
|
|
4850
5298
|
} catch (e) {
|
|
4851
|
-
|
|
5299
|
+
log8.debug("Ask peek failed", e);
|
|
4852
5300
|
return `Error querying scratch: ${e?.message ?? e}`;
|
|
4853
5301
|
}
|
|
4854
5302
|
}
|
|
@@ -4857,7 +5305,7 @@ Question: ${q}`);
|
|
|
4857
5305
|
|
|
4858
5306
|
// src/lessons.ts
|
|
4859
5307
|
init_logging();
|
|
4860
|
-
var
|
|
5308
|
+
var log9 = forComponent("Lessons");
|
|
4861
5309
|
var LessonOptionsDefaults = class {
|
|
4862
5310
|
minRepeats = 2;
|
|
4863
5311
|
};
|
|
@@ -4882,15 +5330,16 @@ function lessonCapture(options) {
|
|
|
4882
5330
|
counts.set(lesson.slug, n);
|
|
4883
5331
|
if (n < o.minRepeats) return;
|
|
4884
5332
|
written.add(lesson.slug);
|
|
4885
|
-
await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) =>
|
|
4886
|
-
|
|
5333
|
+
await writeFact(o.fs, o.dir, lesson.slug, lesson.body).catch((e) => log9.warn(`could not persist ${lesson.slug}: ${e?.message ?? e}`));
|
|
5334
|
+
log9.debug(`captured lesson ${lesson.slug} (recurred ${n}\xD7)`);
|
|
4887
5335
|
}
|
|
4888
5336
|
};
|
|
4889
5337
|
}
|
|
4890
5338
|
|
|
4891
5339
|
// src/reflect.ts
|
|
5340
|
+
init_llm();
|
|
4892
5341
|
init_logging();
|
|
4893
|
-
var
|
|
5342
|
+
var log10 = forComponent("Reflect");
|
|
4894
5343
|
async function reflectOnRun(o) {
|
|
4895
5344
|
const digest = digestRun(o.result.messages, o.maxDigestChars ?? 6e3);
|
|
4896
5345
|
if (!digest.trim()) return null;
|
|
@@ -4906,7 +5355,7 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
4906
5355
|
const r = await o.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false });
|
|
4907
5356
|
text = r?.content ?? "";
|
|
4908
5357
|
} catch (e) {
|
|
4909
|
-
|
|
5358
|
+
log10.warn(`reflection call failed: ${e?.message ?? e}`);
|
|
4910
5359
|
return null;
|
|
4911
5360
|
}
|
|
4912
5361
|
const m = text.match(/LESSON:\s*(.+)/i);
|
|
@@ -4916,10 +5365,10 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
4916
5365
|
try {
|
|
4917
5366
|
await writeFact(o.fs, o.dir, slug2, lesson);
|
|
4918
5367
|
} catch (e) {
|
|
4919
|
-
|
|
5368
|
+
log10.warn(`could not persist lesson: ${e?.message ?? e}`);
|
|
4920
5369
|
return null;
|
|
4921
5370
|
}
|
|
4922
|
-
|
|
5371
|
+
log10.debug(`reflection persisted ${slug2}`);
|
|
4923
5372
|
return slug2;
|
|
4924
5373
|
}
|
|
4925
5374
|
function digestRun(messages, maxChars) {
|
|
@@ -4935,6 +5384,7 @@ function digestRun(messages, maxChars) {
|
|
|
4935
5384
|
|
|
4936
5385
|
// src/duplex.ts
|
|
4937
5386
|
import { MemFilesystem as MemFilesystem2 } from "@livx.cc/wcli/core";
|
|
5387
|
+
init_llm();
|
|
4938
5388
|
init_logging();
|
|
4939
5389
|
|
|
4940
5390
|
// node_modules/@bod.ee/voice/src/core/logging.ts
|
|
@@ -4967,7 +5417,7 @@ function forComponent2(name) {
|
|
|
4967
5417
|
}
|
|
4968
5418
|
|
|
4969
5419
|
// node_modules/@bod.ee/voice/src/core/emotion.ts
|
|
4970
|
-
var
|
|
5420
|
+
var log11 = forComponent2("Emotion");
|
|
4971
5421
|
var EMOTIONS = [
|
|
4972
5422
|
// primary (best results)
|
|
4973
5423
|
"neutral",
|
|
@@ -5121,7 +5571,7 @@ var EmotionStream = class {
|
|
|
5121
5571
|
if (tag) {
|
|
5122
5572
|
this.pending = tag;
|
|
5123
5573
|
if (this.show) display += m[0];
|
|
5124
|
-
} else
|
|
5574
|
+
} else log11.debug(`dropping unknown emotion tag ${m[0]}`);
|
|
5125
5575
|
last = m.index + m[0].length;
|
|
5126
5576
|
}
|
|
5127
5577
|
this.emit(body.slice(last), (s, d, p) => {
|
|
@@ -5144,7 +5594,7 @@ var EmotionStream = class {
|
|
|
5144
5594
|
};
|
|
5145
5595
|
|
|
5146
5596
|
// node_modules/@bod.ee/voice/src/core/engine.ts
|
|
5147
|
-
var
|
|
5597
|
+
var log12 = forComponent2("VoiceEngine");
|
|
5148
5598
|
var realClock = {
|
|
5149
5599
|
now: () => performance.now(),
|
|
5150
5600
|
setTimeout: (fn, ms) => setTimeout(fn, ms),
|
|
@@ -5419,7 +5869,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5419
5869
|
await Promise.all([this.tts.connect(), this.stt.start()]);
|
|
5420
5870
|
this.tts.warmup?.();
|
|
5421
5871
|
this.setState("listening");
|
|
5422
|
-
|
|
5872
|
+
log12.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
|
|
5423
5873
|
}
|
|
5424
5874
|
get usingAec() {
|
|
5425
5875
|
return this.stt.usingAec;
|
|
@@ -5441,7 +5891,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5441
5891
|
this.options.onDiag({ t: this.clock.now(), kind, ...fields });
|
|
5442
5892
|
} catch (e) {
|
|
5443
5893
|
this.diagOn = false;
|
|
5444
|
-
|
|
5894
|
+
log12.debug(`onDiag threw \u2014 diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
5445
5895
|
}
|
|
5446
5896
|
}
|
|
5447
5897
|
idleWaiters = [];
|
|
@@ -5550,7 +6000,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5550
6000
|
if (prose.trim()) this.repliedSinceDispatch = true;
|
|
5551
6001
|
for (const w of this.words(this.reply)) this.echoWords.add(w);
|
|
5552
6002
|
this.tts.speak(forSpeech(speech), true);
|
|
5553
|
-
if (!this.spokeDeltas && this.turnStartAt)
|
|
6003
|
+
if (!this.spokeDeltas && this.turnStartAt) log12.debug(`ttft: ${Math.round(this.clock.now() - this.turnStartAt)}ms`);
|
|
5554
6004
|
this.spokeDeltas = true;
|
|
5555
6005
|
this.setState("speaking");
|
|
5556
6006
|
if (this.options.revealMode !== "off" && display) {
|
|
@@ -5627,7 +6077,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5627
6077
|
this.drainTimer = null;
|
|
5628
6078
|
this.speaking = false;
|
|
5629
6079
|
this.stopWordReveal(true);
|
|
5630
|
-
if (this.turnStartAt)
|
|
6080
|
+
if (this.turnStartAt) log12.debug(`turn: ${Math.round(this.clock.now() - this.turnStartAt)}ms (incl. playback)`);
|
|
5631
6081
|
this.echoUntil = this.clock.now() + 2500;
|
|
5632
6082
|
if (!this.usingAec) this.stt.reset();
|
|
5633
6083
|
this.setState("listening");
|
|
@@ -5715,7 +6165,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5715
6165
|
this.clearAckTimer("interrupt");
|
|
5716
6166
|
this.adaptiveAckFired = false;
|
|
5717
6167
|
this.ackProbe = null;
|
|
5718
|
-
if (this.uttQueue.length)
|
|
6168
|
+
if (this.uttQueue.length) log12.info(`barge-in dropped ${this.uttQueue.length} queued worker utterance(s)`);
|
|
5719
6169
|
const droppedQueued = this.uttQueue.length;
|
|
5720
6170
|
this.uttQueue = [];
|
|
5721
6171
|
if (!this.speaking && !this.drainTimer) return;
|
|
@@ -5861,7 +6311,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5861
6311
|
if (flat && this.lastDispatchFlat && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS && (this.lastDispatchFlat.startsWith(flat) || flat.startsWith(this.lastDispatchFlat))) return;
|
|
5862
6312
|
this.specSpent = true;
|
|
5863
6313
|
this.specText = this.specPartial;
|
|
5864
|
-
|
|
6314
|
+
log12.debug(`speculate: "${this.specText.slice(0, 60)}"`);
|
|
5865
6315
|
this.diag("speculate", { text: this.specText });
|
|
5866
6316
|
this.options.onSpeculate(this.specText);
|
|
5867
6317
|
}, this.options.speculativeMs);
|
|
@@ -5930,7 +6380,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5930
6380
|
};
|
|
5931
6381
|
this.tts.newContext();
|
|
5932
6382
|
this.tts.speak(phrase, false);
|
|
5933
|
-
|
|
6383
|
+
log12.debug(`backchannel: "${phrase}"`);
|
|
5934
6384
|
this.diag("backchannel", { phrase });
|
|
5935
6385
|
this.options.onBackchannel(phrase);
|
|
5936
6386
|
}
|
|
@@ -5963,7 +6413,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5963
6413
|
});
|
|
5964
6414
|
if (idx < 0) return text;
|
|
5965
6415
|
toks.splice(idx, 1);
|
|
5966
|
-
|
|
6416
|
+
log12.verbose(`stripped backchannel echo "${this.lastBcPhrase}" from dispatching final`);
|
|
5967
6417
|
return toks.join(" ");
|
|
5968
6418
|
}
|
|
5969
6419
|
/** Merge a resumed utterance into the pending one, deduping any word-overlap. Soniox re-finalizes
|
|
@@ -6054,7 +6504,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6054
6504
|
this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
|
|
6055
6505
|
if (this.pendingTimer) this.clock.clearTimeout(this.pendingTimer);
|
|
6056
6506
|
if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
|
|
6057
|
-
|
|
6507
|
+
log12.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
|
|
6058
6508
|
this.diag("hold", { reason: "incomplete", tail: this.pendingUtt.slice(-40) });
|
|
6059
6509
|
this.options.onHold();
|
|
6060
6510
|
if (this.options.holdFiller && !this.speaking) this.speakFiller(this.options.holdFiller);
|
|
@@ -6089,13 +6539,13 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
6089
6539
|
if (text) {
|
|
6090
6540
|
const flat = text.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
6091
6541
|
if (flat && flat === this.lastDispatchFlat && !this.repliedSinceDispatch && this.clock.now() - this.lastDispatchAt < _VoiceEngine.DUP_FINAL_MS) {
|
|
6092
|
-
|
|
6542
|
+
log12.verbose(`dropped duplicate final "${text.slice(0, 40)}" (identical to the just-dispatched utterance, no reply between)`);
|
|
6093
6543
|
this.diag("utterance_drop", { reason: "dup_final", text: text.slice(0, 60) });
|
|
6094
6544
|
return;
|
|
6095
6545
|
}
|
|
6096
6546
|
const refinal = this.refinalizes(text);
|
|
6097
6547
|
if (refinal && this.repliedSinceDispatch) {
|
|
6098
|
-
|
|
6548
|
+
log12.verbose(`dropped re-finalization "${text.slice(0, 60)}" (superset of the just-dispatched utterance, reply already flowing)`);
|
|
6099
6549
|
this.diag("utterance_drop", { reason: "refinalized", text: text.slice(0, 60) });
|
|
6100
6550
|
this.lastDispatchFlat = flat;
|
|
6101
6551
|
this.lastDispatchWords = text.trim().split(/\s+/);
|
|
@@ -6255,7 +6705,7 @@ var SpokenSplitter = class {
|
|
|
6255
6705
|
};
|
|
6256
6706
|
|
|
6257
6707
|
// node_modules/@bod.ee/voice/src/adapters/soniox.ts
|
|
6258
|
-
var
|
|
6708
|
+
var log13 = forComponent2("SonioxSTT");
|
|
6259
6709
|
var now = () => performance.now();
|
|
6260
6710
|
var SonioxSTTOptions = class {
|
|
6261
6711
|
auth = "";
|
|
@@ -6296,7 +6746,7 @@ var SonioxSTT = class {
|
|
|
6296
6746
|
this.onDiag({ t: now(), kind, ...fields });
|
|
6297
6747
|
} catch (e) {
|
|
6298
6748
|
this.diagOn = false;
|
|
6299
|
-
|
|
6749
|
+
log13.debug(`onDiag threw \u2014 STT diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
6300
6750
|
}
|
|
6301
6751
|
}
|
|
6302
6752
|
lastChunkAt = 0;
|
|
@@ -6338,11 +6788,11 @@ var SonioxSTT = class {
|
|
|
6338
6788
|
this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
|
|
6339
6789
|
this.ws.onclose = (ev) => {
|
|
6340
6790
|
if (this.stopped) return;
|
|
6341
|
-
|
|
6791
|
+
log13.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
|
|
6342
6792
|
this.diag("stt_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: true });
|
|
6343
6793
|
this.reset();
|
|
6344
6794
|
this.connectWs().catch((e) => {
|
|
6345
|
-
|
|
6795
|
+
log13.error(`soniox reconnect failed: ${e.message}`);
|
|
6346
6796
|
this.diag("stt_reconnect_failed", { message: e.message });
|
|
6347
6797
|
});
|
|
6348
6798
|
};
|
|
@@ -6354,7 +6804,7 @@ var SonioxSTT = class {
|
|
|
6354
6804
|
this.endpointTimer = setInterval(() => {
|
|
6355
6805
|
const combined = (this.finalText + this.partialText).trim();
|
|
6356
6806
|
if (!combined || now() - this.lastChangeAt < this.options.silenceEndpointMs) return;
|
|
6357
|
-
if (this.firstTokenAt)
|
|
6807
|
+
if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
|
|
6358
6808
|
this.reset();
|
|
6359
6809
|
this.onUtterance(combined, now());
|
|
6360
6810
|
}, 120);
|
|
@@ -6366,7 +6816,7 @@ var SonioxSTT = class {
|
|
|
6366
6816
|
if (this.stopped) return;
|
|
6367
6817
|
const ref = this.lastChunkAt || this.startedChunksAt;
|
|
6368
6818
|
if (now() - ref > noAudioMs) {
|
|
6369
|
-
|
|
6819
|
+
log13.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
|
|
6370
6820
|
this.diag("stt_watchdog_fatal", { noAudioMs });
|
|
6371
6821
|
this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
|
|
6372
6822
|
this.stop();
|
|
@@ -6389,7 +6839,7 @@ var SonioxSTT = class {
|
|
|
6389
6839
|
handle(m) {
|
|
6390
6840
|
if (m.error_message) {
|
|
6391
6841
|
this.diag("stt_error", { message: String(m.error_message), code: m.error_code });
|
|
6392
|
-
return
|
|
6842
|
+
return log13.error(`soniox: ${m.error_message}`);
|
|
6393
6843
|
}
|
|
6394
6844
|
let endpoint = false;
|
|
6395
6845
|
for (const t of m.tokens ?? []) {
|
|
@@ -6406,7 +6856,7 @@ var SonioxSTT = class {
|
|
|
6406
6856
|
this.onPartial(combined);
|
|
6407
6857
|
if (endpoint && this.finalText.trim()) {
|
|
6408
6858
|
const utterance = this.finalText.trim();
|
|
6409
|
-
if (this.firstTokenAt)
|
|
6859
|
+
if (this.firstTokenAt) log13.debug(`stt: ${Math.round(now() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
|
|
6410
6860
|
this.reset();
|
|
6411
6861
|
this.onUtterance(utterance, now());
|
|
6412
6862
|
}
|
|
@@ -6428,7 +6878,7 @@ var SonioxSTT = class {
|
|
|
6428
6878
|
};
|
|
6429
6879
|
|
|
6430
6880
|
// node_modules/@bod.ee/voice/src/adapters/cartesia.ts
|
|
6431
|
-
var
|
|
6881
|
+
var log14 = forComponent2("CartesiaTTS");
|
|
6432
6882
|
var now2 = () => performance.now();
|
|
6433
6883
|
var CartesiaTTSOptions = class {
|
|
6434
6884
|
auth = "";
|
|
@@ -6464,7 +6914,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
6464
6914
|
this.onDiag({ t: now2(), kind, ...fields });
|
|
6465
6915
|
} catch (e) {
|
|
6466
6916
|
this.diagOn = false;
|
|
6467
|
-
|
|
6917
|
+
log14.debug(`onDiag threw \u2014 TTS diagnostics disabled: ${e instanceof Error ? e.message : e}`);
|
|
6468
6918
|
}
|
|
6469
6919
|
}
|
|
6470
6920
|
firstAudioAt = 0;
|
|
@@ -6499,11 +6949,11 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
6499
6949
|
this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
|
|
6500
6950
|
});
|
|
6501
6951
|
this.ws.onclose = (ev) => {
|
|
6502
|
-
|
|
6952
|
+
log14.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
|
|
6503
6953
|
this.diag("tts_ws_closed", { code: ev.code, reason: String(ev.reason || ""), reconnecting: !this.closed });
|
|
6504
6954
|
if (!this.closed) {
|
|
6505
6955
|
this.connecting = this.doConnect().catch((e) => {
|
|
6506
|
-
|
|
6956
|
+
log14.error(`cartesia reconnect failed: ${e.message}`);
|
|
6507
6957
|
this.diag("tts_reconnect_failed", { message: e.message });
|
|
6508
6958
|
});
|
|
6509
6959
|
}
|
|
@@ -6531,12 +6981,12 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
6531
6981
|
this.down = true;
|
|
6532
6982
|
this.downAt = now2();
|
|
6533
6983
|
this.consecutiveOk = 0;
|
|
6534
|
-
|
|
6984
|
+
log14.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
|
|
6535
6985
|
this.diag("tts_breaker_open", { errors: this.consecutiveErrors });
|
|
6536
6986
|
this.onDone();
|
|
6537
6987
|
this.startProbe();
|
|
6538
6988
|
} else if (!this.down) {
|
|
6539
|
-
(/No valid transcripts/i.test(m.message || "") ?
|
|
6989
|
+
(/No valid transcripts/i.test(m.message || "") ? log14.debug : log14.warn)(`cartesia: ${JSON.stringify(m)}`);
|
|
6540
6990
|
}
|
|
6541
6991
|
}
|
|
6542
6992
|
};
|
|
@@ -6551,7 +7001,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
6551
7001
|
this.stopProbe();
|
|
6552
7002
|
const downMs = this.downAt ? now2() - this.downAt : 0;
|
|
6553
7003
|
this.diag("tts_breaker_close", { downMs: Math.round(downMs) });
|
|
6554
|
-
(downMs < 2e3 ?
|
|
7004
|
+
(downMs < 2e3 ? log14.debug : log14.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
|
|
6555
7005
|
}
|
|
6556
7006
|
/** Ensure the WS is open before sending — reconnects if idle-closed. */
|
|
6557
7007
|
async ensureConnected() {
|
|
@@ -6642,7 +7092,7 @@ configureLogging((name) => {
|
|
|
6642
7092
|
});
|
|
6643
7093
|
|
|
6644
7094
|
// src/duplex.ts
|
|
6645
|
-
var
|
|
7095
|
+
var log15 = forComponent("DuplexAgent");
|
|
6646
7096
|
function describeCall(call) {
|
|
6647
7097
|
const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
|
|
6648
7098
|
const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
|
|
@@ -6853,7 +7303,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
6853
7303
|
const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
|
|
6854
7304
|
if (m) {
|
|
6855
7305
|
this.fabricationCut = true;
|
|
6856
|
-
|
|
7306
|
+
log15.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
|
|
6857
7307
|
const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
|
|
6858
7308
|
if (!safe) return;
|
|
6859
7309
|
if (safe.trim()) this.spokeThisTurn = true;
|
|
@@ -6946,7 +7396,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
6946
7396
|
return {
|
|
6947
7397
|
preToolUse: (call) => {
|
|
6948
7398
|
if (this.spec?.state === "pending" && !_DuplexAgent.SPEC_SAFE_TOOLS.has(call.name)) {
|
|
6949
|
-
|
|
7399
|
+
log15.verbose(`speculation aborted: reflex called ${call.name} on unconfirmed input`);
|
|
6950
7400
|
this.abortSpeculation();
|
|
6951
7401
|
return { block: true, reason: "Speculative turn aborted." };
|
|
6952
7402
|
}
|
|
@@ -6984,7 +7434,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
6984
7434
|
try {
|
|
6985
7435
|
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
7436
|
} catch (e) {
|
|
6987
|
-
|
|
7437
|
+
log15.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
6988
7438
|
} finally {
|
|
6989
7439
|
this.nudging = false;
|
|
6990
7440
|
}
|
|
@@ -7077,7 +7527,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7077
7527
|
try {
|
|
7078
7528
|
res = await this.voice.send(spec.text);
|
|
7079
7529
|
} catch (e) {
|
|
7080
|
-
|
|
7530
|
+
log15.warn(`speculative turn failed: ${e instanceof Error ? e.message : e}`);
|
|
7081
7531
|
} finally {
|
|
7082
7532
|
this.voice.options.signal = prevSignal;
|
|
7083
7533
|
}
|
|
@@ -7091,7 +7541,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
7091
7541
|
if (d === "abort") {
|
|
7092
7542
|
if (this.voice.transcript.length > base) this.voice.transcript.length = base;
|
|
7093
7543
|
this.speculativeAbortedCalls++;
|
|
7094
|
-
|
|
7544
|
+
log15.verbose(`speculation aborted (${this.speculativeAbortedCalls} total): "${spec.text.slice(0, 50)}"`);
|
|
7095
7545
|
if (this.spec === spec) this.spec = void 0;
|
|
7096
7546
|
return res ?? empty;
|
|
7097
7547
|
}
|
|
@@ -7356,7 +7806,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
7356
7806
|
this.notify("task_verify", `task ${id}: verifying`, { id });
|
|
7357
7807
|
const cres = await new Agent(checkerOpts).run(checkBrief);
|
|
7358
7808
|
if (cres.finishReason !== "stop") {
|
|
7359
|
-
|
|
7809
|
+
log15.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
|
|
7360
7810
|
this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
|
|
7361
7811
|
}
|
|
7362
7812
|
const sum = (a = 0, b = 0) => a + b;
|
|
@@ -7422,14 +7872,14 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
7422
7872
|
* '[task <id> asks] …' and resolves with the user's answer via AnswerTask — or '' on timeout/
|
|
7423
7873
|
* task settle (callers map '' to deny / best-judgment). Workers never block forever. */
|
|
7424
7874
|
parkQuestion(askId, question) {
|
|
7425
|
-
return new Promise((
|
|
7875
|
+
return new Promise((resolve2) => {
|
|
7426
7876
|
let settled = false;
|
|
7427
7877
|
const finish = (answer) => {
|
|
7428
7878
|
if (settled) return;
|
|
7429
7879
|
settled = true;
|
|
7430
7880
|
clearTimeout(timer);
|
|
7431
7881
|
this.pendingAsks.delete(askId);
|
|
7432
|
-
|
|
7882
|
+
resolve2(answer);
|
|
7433
7883
|
};
|
|
7434
7884
|
const timer = setTimeout(() => {
|
|
7435
7885
|
this.notify("task_ask_timeout", `task ${askId}: question timed out \u2014 proceeding without an answer`);
|
|
@@ -7491,8 +7941,8 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
7491
7941
|
}
|
|
7492
7942
|
rec.status = "done";
|
|
7493
7943
|
rec.result = res.text;
|
|
7494
|
-
const incomplete = res.finishReason !== "stop";
|
|
7495
|
-
|
|
7944
|
+
const incomplete = res.finishReason !== "stop" && res.finishReason !== "empty";
|
|
7945
|
+
log15.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
|
|
7496
7946
|
this.notify("task_done", `task ${id} (${rec.label}) completed`, {
|
|
7497
7947
|
id,
|
|
7498
7948
|
text: res.text,
|
|
@@ -7534,14 +7984,14 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
7534
7984
|
foldIfSuperseded(rec, note) {
|
|
7535
7985
|
if (!(rec.deliveryParked && this.suppressParkedResume)) return false;
|
|
7536
7986
|
if (note.trim()) this.voice.transcript.push({ role: "assistant", content: note });
|
|
7537
|
-
|
|
7987
|
+
log15.verbose(`task ${rec.id} settled but superseded (${rec.status}) \u2014 folded into transcript, not voiced`);
|
|
7538
7988
|
return true;
|
|
7539
7989
|
}
|
|
7540
7990
|
failTask(rec, msg) {
|
|
7541
7991
|
this.dropAsk(rec.id);
|
|
7542
7992
|
rec.status = "error";
|
|
7543
7993
|
rec.result = msg;
|
|
7544
|
-
|
|
7994
|
+
log15.warn(`task ${rec.id} failed: ${msg}`);
|
|
7545
7995
|
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
|
|
7546
7996
|
if (this.foldIfSuperseded(rec, `The "${rec.label}" task failed: ${msg}`)) return;
|
|
7547
7997
|
this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
|
|
@@ -7846,11 +8296,11 @@ function buildMcpCatalog(servers) {
|
|
|
7846
8296
|
}
|
|
7847
8297
|
return { specs, routes };
|
|
7848
8298
|
}
|
|
7849
|
-
function searchOverCatalog(servers, specs, routes,
|
|
8299
|
+
function searchOverCatalog(servers, specs, routes, resolve2, options) {
|
|
7850
8300
|
const tools = specs.length ? makeMcpToolSearch(specs, (name, args) => {
|
|
7851
8301
|
const r = routes.get(name);
|
|
7852
8302
|
if (!r) throw new Error(`unknown MCP tool '${name}' \u2014 use ToolSearch to find valid names`);
|
|
7853
|
-
return
|
|
8303
|
+
return resolve2(r.server, r.rawName, args ?? {});
|
|
7854
8304
|
}, options) : [];
|
|
7855
8305
|
return { tools, serverNames: servers, toolCount: specs.length };
|
|
7856
8306
|
}
|
|
@@ -7859,9 +8309,9 @@ function makeMcpToolSearchFromMounted(mounted, options) {
|
|
|
7859
8309
|
const byName = new Map(mounted.map((m) => [m.name, m]));
|
|
7860
8310
|
return searchOverCatalog(mounted.map((m) => m.name), specs, routes, (server, rawName, args) => byName.get(server).client.callTool(rawName, args), options);
|
|
7861
8311
|
}
|
|
7862
|
-
function makeLazyMcpToolSearch(servers,
|
|
8312
|
+
function makeLazyMcpToolSearch(servers, resolve2, options) {
|
|
7863
8313
|
const { specs, routes } = buildMcpCatalog(servers);
|
|
7864
|
-
return searchOverCatalog(servers.map((s) => s.name), specs, routes,
|
|
8314
|
+
return searchOverCatalog(servers.map((s) => s.name), specs, routes, resolve2, options);
|
|
7865
8315
|
}
|
|
7866
8316
|
|
|
7867
8317
|
// src/hooks.ts
|
|
@@ -7920,6 +8370,7 @@ var RecordingLifecycle = class {
|
|
|
7920
8370
|
|
|
7921
8371
|
// src/index.ts
|
|
7922
8372
|
init_logging();
|
|
8373
|
+
init_llm();
|
|
7923
8374
|
import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor as CommandExecutor2, registerHeadlessCommands as registerHeadlessCommands2 } from "@livx.cc/wcli/core";
|
|
7924
8375
|
export {
|
|
7925
8376
|
Agent,
|
|
@@ -7934,6 +8385,8 @@ export {
|
|
|
7934
8385
|
DuplexAgent,
|
|
7935
8386
|
DuplexAgentOptions,
|
|
7936
8387
|
FakeAIClient,
|
|
8388
|
+
IMAGE_ELIDE_STUB,
|
|
8389
|
+
IMAGE_REF_SCHEME,
|
|
7937
8390
|
IndexedDbFilesystem,
|
|
7938
8391
|
JailOptions,
|
|
7939
8392
|
JailedFilesystem,
|
|
@@ -7968,19 +8421,26 @@ export {
|
|
|
7968
8421
|
checkpointTool,
|
|
7969
8422
|
checkpointTools,
|
|
7970
8423
|
cleanupWorktree,
|
|
8424
|
+
clearImageRefs,
|
|
7971
8425
|
compileSynthesizedTool,
|
|
7972
8426
|
composeHooks,
|
|
8427
|
+
contentBytes,
|
|
7973
8428
|
contentText,
|
|
7974
8429
|
cronMatches,
|
|
7975
8430
|
decodeDdgUrl,
|
|
7976
8431
|
defaultTools,
|
|
7977
8432
|
diskAgentOptions,
|
|
7978
8433
|
editTool,
|
|
8434
|
+
elideImages,
|
|
8435
|
+
elideInlineImages,
|
|
8436
|
+
elideStaleImages,
|
|
7979
8437
|
enterWorktree,
|
|
8438
|
+
estimateTokens,
|
|
7980
8439
|
exitSessionTool,
|
|
7981
8440
|
exitWorktree,
|
|
7982
8441
|
expandCommand,
|
|
7983
8442
|
expandEntry,
|
|
8443
|
+
expandImagesForSend,
|
|
7984
8444
|
expandTemplate,
|
|
7985
8445
|
findGitRoot,
|
|
7986
8446
|
forComponent,
|
|
@@ -7992,6 +8452,8 @@ export {
|
|
|
7992
8452
|
htmlToText,
|
|
7993
8453
|
idfWeights,
|
|
7994
8454
|
imagePart,
|
|
8455
|
+
imageRefPart,
|
|
8456
|
+
imageRefResult,
|
|
7995
8457
|
lessonCapture,
|
|
7996
8458
|
loadAgents,
|
|
7997
8459
|
loadCommands,
|
|
@@ -8013,12 +8475,17 @@ export {
|
|
|
8013
8475
|
makeWebSearchTool,
|
|
8014
8476
|
mcpToolToAgentTool,
|
|
8015
8477
|
mcpToolsToAgentTools,
|
|
8478
|
+
messageHasImage,
|
|
8479
|
+
messageHasImageRef,
|
|
8480
|
+
messageHasInlineImage,
|
|
8481
|
+
mintImageRef,
|
|
8016
8482
|
mkdirp,
|
|
8017
8483
|
modelShortLabel,
|
|
8018
8484
|
multiEditTool,
|
|
8019
8485
|
nextCronAfter,
|
|
8020
8486
|
parseCron,
|
|
8021
8487
|
parseDdgHtml,
|
|
8488
|
+
parseImageRef,
|
|
8022
8489
|
planMode,
|
|
8023
8490
|
raceAttempts,
|
|
8024
8491
|
readTool,
|
|
@@ -8026,6 +8493,7 @@ export {
|
|
|
8026
8493
|
reflectOnRun,
|
|
8027
8494
|
registerHeadlessCommands2 as registerHeadlessCommands,
|
|
8028
8495
|
relevanceScore,
|
|
8496
|
+
remintImageRefs,
|
|
8029
8497
|
repoIndex,
|
|
8030
8498
|
repoMapTool,
|
|
8031
8499
|
resolveAuth,
|
|
@@ -8035,11 +8503,14 @@ export {
|
|
|
8035
8503
|
sandboxAgentOptions,
|
|
8036
8504
|
scanCommands,
|
|
8037
8505
|
scanSkills,
|
|
8506
|
+
sendBytes,
|
|
8507
|
+
sendBytesPer,
|
|
8038
8508
|
slugify,
|
|
8039
8509
|
toWireTools,
|
|
8040
8510
|
todoWriteTool,
|
|
8041
8511
|
tokenize,
|
|
8042
8512
|
toolCall,
|
|
8513
|
+
toolImageRef,
|
|
8043
8514
|
toolRegistry,
|
|
8044
8515
|
toolsByName,
|
|
8045
8516
|
topByRelevance,
|