agent.libx.js 0.93.29 → 0.93.31
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/cli/cli.ts +100 -33
- package/dist/{Agent-kWrJvtZM.d.ts → Agent-uWtu_WFY.d.ts} +11 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +373 -108
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +18 -3
- package/dist/index.js +86 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -26,7 +26,10 @@ var init_redact = __esm({
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
// src/tools.structured.ts
|
|
29
|
-
|
|
29
|
+
function ckAbort(signal) {
|
|
30
|
+
if (signal?.aborted) throw new Error("aborted");
|
|
31
|
+
}
|
|
32
|
+
async function walkFiles(fs, dir, signal, out = []) {
|
|
30
33
|
let entries;
|
|
31
34
|
try {
|
|
32
35
|
entries = await fs.readDir(dir);
|
|
@@ -34,8 +37,9 @@ async function walkFiles(fs, dir, out = []) {
|
|
|
34
37
|
return out;
|
|
35
38
|
}
|
|
36
39
|
for (const name of entries.sort()) {
|
|
40
|
+
ckAbort(signal);
|
|
37
41
|
const p = dir === "/" ? `/${name}` : `${dir}/${name}`;
|
|
38
|
-
if (await fs.isDirectory(p)) await walkFiles(fs, p, out);
|
|
42
|
+
if (await fs.isDirectory(p)) await walkFiles(fs, p, signal, out);
|
|
39
43
|
else out.push(p);
|
|
40
44
|
}
|
|
41
45
|
return out;
|
|
@@ -92,13 +96,14 @@ function signaturesOf(content, cap = 40) {
|
|
|
92
96
|
}
|
|
93
97
|
return out;
|
|
94
98
|
}
|
|
95
|
-
async function repoIndex(fs, glob, mode = "code") {
|
|
99
|
+
async function repoIndex(fs, glob, mode = "code", signal) {
|
|
96
100
|
const scope = glob ? anchoredGlob(fs, String(glob)) : null;
|
|
97
101
|
const filter = mode === "code" ? isCode : mode === "docs" ? isDoc : (p) => isCode(p) || isDoc(p);
|
|
98
|
-
const files = (await walkFiles(fs, fsCwd(fs))).filter((p) => scope ? scope.test(p) : filter(p));
|
|
102
|
+
const files = (await walkFiles(fs, fsCwd(fs), signal)).filter((p) => scope ? scope.test(p) : filter(p));
|
|
99
103
|
const blocks = [];
|
|
100
104
|
let shown = 0;
|
|
101
105
|
for (const path of files) {
|
|
106
|
+
ckAbort(signal);
|
|
102
107
|
let content;
|
|
103
108
|
try {
|
|
104
109
|
content = await fs.readFile(path);
|
|
@@ -218,7 +223,7 @@ var init_tools_structured = __esm({
|
|
|
218
223
|
const include = pats.filter((p) => !p.startsWith("!")).map((p) => anchoredGlob(ctx.fs, p));
|
|
219
224
|
const exclude = pats.filter((p) => p.startsWith("!")).map((p) => anchoredGlob(ctx.fs, p.slice(1)));
|
|
220
225
|
const includes = include.length ? include : [anchoredGlob(ctx.fs, "**")];
|
|
221
|
-
const hits = (await walkFiles(ctx.fs, fsCwd(ctx.fs))).filter(
|
|
226
|
+
const hits = (await walkFiles(ctx.fs, fsCwd(ctx.fs), ctx.signal)).filter(
|
|
222
227
|
(p) => includes.some((re) => re.test(p)) && !exclude.some((re) => re.test(p))
|
|
223
228
|
);
|
|
224
229
|
return hits.length ? hits.join("\n") : "(no matches)";
|
|
@@ -245,11 +250,12 @@ var init_tools_structured = __esm({
|
|
|
245
250
|
throw new Error(`invalid regex: ${String(e)}`);
|
|
246
251
|
}
|
|
247
252
|
const scope = glob ? anchoredGlob(ctx.fs, String(glob)) : null;
|
|
248
|
-
const files = (await walkFiles(ctx.fs, fsCwd(ctx.fs))).filter((p) => !scope || scope.test(p));
|
|
253
|
+
const files = (await walkFiles(ctx.fs, fsCwd(ctx.fs), ctx.signal)).filter((p) => !scope || scope.test(p));
|
|
249
254
|
const ctxN = Math.max(0, Number(context ?? 0));
|
|
250
255
|
const out = [];
|
|
251
256
|
const matched = [];
|
|
252
257
|
for (const path of files) {
|
|
258
|
+
ckAbort(ctx.signal);
|
|
253
259
|
let content;
|
|
254
260
|
try {
|
|
255
261
|
content = await ctx.fs.readFile(path);
|
|
@@ -285,7 +291,7 @@ var init_tools_structured = __esm({
|
|
|
285
291
|
scope: { type: "string", enum: ["code", "docs", "all"], description: 'what to map: "code" (default), "docs", or "all"' }
|
|
286
292
|
}
|
|
287
293
|
},
|
|
288
|
-
run: ({ glob, scope }, ctx) => repoIndex(ctx.fs, glob, scope || "code")
|
|
294
|
+
run: ({ glob, scope }, ctx) => repoIndex(ctx.fs, glob, scope || "code", ctx.signal)
|
|
289
295
|
};
|
|
290
296
|
writeTool = {
|
|
291
297
|
name: "Write",
|
|
@@ -1671,7 +1677,7 @@ close access f`;
|
|
|
1671
1677
|
|
|
1672
1678
|
// cli/cli.ts
|
|
1673
1679
|
import { join as join9, resolve as resolve3, basename as basename2, extname, dirname as dirname4 } from "path";
|
|
1674
|
-
import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported } from "ai.libx.js";
|
|
1680
|
+
import { AIClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported, disposeCursorSessions } from "ai.libx.js";
|
|
1675
1681
|
|
|
1676
1682
|
// src/llm.ts
|
|
1677
1683
|
function contentText(content) {
|
|
@@ -2002,9 +2008,9 @@ async function loadCommands(fs, dir, opts = {}) {
|
|
|
2002
2008
|
properties: { name: { type: "string" }, args: { type: "string", description: "arguments to fill the template" } }
|
|
2003
2009
|
},
|
|
2004
2010
|
async run({ name, args }, ctx) {
|
|
2005
|
-
const
|
|
2006
|
-
const c = commands.find((x) => x.name ===
|
|
2007
|
-
if (!c) return `Error: no command named '${
|
|
2011
|
+
const slug2 = String(name ?? "").replace(/^\//, "");
|
|
2012
|
+
const c = commands.find((x) => x.name === slug2);
|
|
2013
|
+
if (!c) return `Error: no command named '${slug2}'. Available: ${commands.map((x) => x.name).join(", ")}`;
|
|
2008
2014
|
return expandCommand(ctx.fs, c, String(args ?? ""));
|
|
2009
2015
|
}
|
|
2010
2016
|
};
|
|
@@ -2039,9 +2045,9 @@ async function loadMemory(fs, dir, opts = {}) {
|
|
|
2039
2045
|
const lines = md.split("\n");
|
|
2040
2046
|
if (!header) header = lines.filter((l) => !/^\s*-\s*\[.+\]\(.+\.md\)/.test(l)).join("\n").trim();
|
|
2041
2047
|
for (const l of lines.filter((l2) => /^\s*-\s*\[.+\]\(.+\.md\)/.test(l2))) {
|
|
2042
|
-
const
|
|
2043
|
-
if (
|
|
2044
|
-
seenSlugs.add(
|
|
2048
|
+
const slug2 = l.match(/\]\(([^)]+)\.md\)/)?.[1];
|
|
2049
|
+
if (slug2 && !seenSlugs.has(slug2)) {
|
|
2050
|
+
seenSlugs.add(slug2);
|
|
2045
2051
|
allPointers.push(l);
|
|
2046
2052
|
}
|
|
2047
2053
|
}
|
|
@@ -2057,20 +2063,20 @@ function slugify(s, fallback = "note") {
|
|
|
2057
2063
|
const base = String(s ?? "").trim().toLowerCase().replace(/\.md$/i, "").replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 48);
|
|
2058
2064
|
return base || fallback;
|
|
2059
2065
|
}
|
|
2060
|
-
async function writeFact(fs, dir,
|
|
2066
|
+
async function writeFact(fs, dir, slug2, body, opts) {
|
|
2061
2067
|
await mkdirp(fs, dir);
|
|
2062
2068
|
const content = opts?.type ? `---
|
|
2063
2069
|
type: ${opts.type}
|
|
2064
2070
|
---
|
|
2065
2071
|
|
|
2066
2072
|
${body}` : body;
|
|
2067
|
-
await fs.writeFile(`${dir}/${
|
|
2073
|
+
await fs.writeFile(`${dir}/${slug2}.md`, content.endsWith("\n") ? content : content + "\n");
|
|
2068
2074
|
const indexPath = `${dir}/MEMORY.md`;
|
|
2069
2075
|
const idx = await fs.exists(indexPath) ? await fs.readFile(indexPath) : "# Memory Index\n";
|
|
2070
2076
|
const summary = opts?.description || body.split("\n")[0].slice(0, 80);
|
|
2071
|
-
const line = `- [${
|
|
2077
|
+
const line = `- [${slug2}](${slug2}.md) \u2014 ${summary}`;
|
|
2072
2078
|
const lines = idx.split("\n");
|
|
2073
|
-
const at = lines.findIndex((l) => l.includes(`(${
|
|
2079
|
+
const at = lines.findIndex((l) => l.includes(`(${slug2}.md)`));
|
|
2074
2080
|
if (at >= 0) {
|
|
2075
2081
|
if (lines[at] !== line) {
|
|
2076
2082
|
lines[at] = line;
|
|
@@ -2121,8 +2127,8 @@ async function listSlugs(fs, dir, prefix = "") {
|
|
|
2121
2127
|
}
|
|
2122
2128
|
return out;
|
|
2123
2129
|
}
|
|
2124
|
-
async function loadFact(fs, dir,
|
|
2125
|
-
const path = `${dir}/${
|
|
2130
|
+
async function loadFact(fs, dir, slug2) {
|
|
2131
|
+
const path = `${dir}/${slug2}.md`;
|
|
2126
2132
|
try {
|
|
2127
2133
|
const raw = await fs.readFile(path);
|
|
2128
2134
|
const { body } = splitFrontmatter(raw);
|
|
@@ -2131,9 +2137,9 @@ async function loadFact(fs, dir, slug) {
|
|
|
2131
2137
|
return null;
|
|
2132
2138
|
}
|
|
2133
2139
|
}
|
|
2134
|
-
async function loadFactMulti(fs, dirs,
|
|
2140
|
+
async function loadFactMulti(fs, dirs, slug2) {
|
|
2135
2141
|
for (const d of dirs) {
|
|
2136
|
-
const r = await loadFact(fs, d,
|
|
2142
|
+
const r = await loadFact(fs, d, slug2);
|
|
2137
2143
|
if (r != null) return r;
|
|
2138
2144
|
}
|
|
2139
2145
|
return null;
|
|
@@ -2159,9 +2165,9 @@ function recallTool(fs, dirs) {
|
|
|
2159
2165
|
pattern: { type: "string", description: 'glob pattern to match against slugs (e.g. "auth*", "*database*")' }
|
|
2160
2166
|
}
|
|
2161
2167
|
},
|
|
2162
|
-
async run({ slug, slugs, pattern }, ctx) {
|
|
2168
|
+
async run({ slug: slug2, slugs, pattern }, ctx) {
|
|
2163
2169
|
let targets = [];
|
|
2164
|
-
if (
|
|
2170
|
+
if (slug2) targets = [cleanSlug(slug2)];
|
|
2165
2171
|
else if (Array.isArray(slugs)) targets = slugs.map(cleanSlug).filter(Boolean);
|
|
2166
2172
|
else if (pattern) {
|
|
2167
2173
|
const escaped = String(pattern).replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
@@ -2212,19 +2218,19 @@ function memorySearchTool(fs, dirs) {
|
|
|
2212
2218
|
matcher = (l) => l.toLowerCase().includes(lower);
|
|
2213
2219
|
}
|
|
2214
2220
|
const loaded = [];
|
|
2215
|
-
for (const
|
|
2216
|
-
const body = await loadFactMulti(fs, dirs,
|
|
2217
|
-
if (body) loaded.push({ slug, body });
|
|
2221
|
+
for (const slug2 of slugs) {
|
|
2222
|
+
const body = await loadFactMulti(fs, dirs, slug2);
|
|
2223
|
+
if (body) loaded.push({ slug: slug2, body });
|
|
2218
2224
|
}
|
|
2219
2225
|
const idf = idfWeights(loaded.map((l) => l.body));
|
|
2220
2226
|
const qTokens = tokenize(q2);
|
|
2221
2227
|
const hits = [];
|
|
2222
|
-
for (const { slug, body } of loaded) {
|
|
2228
|
+
for (const { slug: slug2, body } of loaded) {
|
|
2223
2229
|
const lines = body.split("\n");
|
|
2224
2230
|
const matchLine = lines.find(matcher);
|
|
2225
2231
|
if (!matchLine && !relevanceScore(body, qTokens, idf)) continue;
|
|
2226
2232
|
const snippet = matchLine?.trim().slice(0, 120) || lines.find((l) => l.trim())?.trim().slice(0, 120) || "";
|
|
2227
|
-
hits.push({ slug, snippet, score: relevanceScore(body, qTokens, idf) });
|
|
2233
|
+
hits.push({ slug: slug2, snippet, score: relevanceScore(body, qTokens, idf) });
|
|
2228
2234
|
}
|
|
2229
2235
|
if (!hits.length) return "(no matches)";
|
|
2230
2236
|
hits.sort((a, b) => b.score - a.score);
|
|
@@ -2248,11 +2254,11 @@ function rememberTool(fs, dir, memOpts = {}) {
|
|
|
2248
2254
|
description: { type: "string", description: "one-line summary for the memory index (\u226480 chars)" }
|
|
2249
2255
|
}
|
|
2250
2256
|
},
|
|
2251
|
-
async run({ fact, slug, type, description }, ctx) {
|
|
2257
|
+
async run({ fact, slug: slug2, type, description }, ctx) {
|
|
2252
2258
|
const body = String(fact ?? "").trim();
|
|
2253
2259
|
if (!body) return `Error: nothing to remember (empty fact).`;
|
|
2254
2260
|
if (++writes > maxWrites) return `Rate limit: too many memories this session (${maxWrites}). Only persist genuinely durable facts.`;
|
|
2255
|
-
const name = slugify(
|
|
2261
|
+
const name = slugify(slug2 || body.split("\n")[0]);
|
|
2256
2262
|
const isGlobal = (type === "user" || type === "feedback") && memOpts.userDir;
|
|
2257
2263
|
const targetDir = isGlobal ? memOpts.userDir : dir;
|
|
2258
2264
|
await writeFact(fs, targetDir, name, body, { type, description });
|
|
@@ -2686,6 +2692,14 @@ var AgentOptions = class {
|
|
|
2686
2692
|
/** Token-aware backstop (~4 chars/token estimate). After note-taking, drop oldest messages from the
|
|
2687
2693
|
* sent context until the estimate is under this ceiling (pairing-safe). 0 = off. */
|
|
2688
2694
|
maxContextTokens = 0;
|
|
2695
|
+
/** Pagination ceiling for a SINGLE tool result (bytes). A result over this is cropped to page 1 with
|
|
2696
|
+
* a marker telling the model it was cropped (refine the query, or page further). Guards against one
|
|
2697
|
+
* Grep/Read/MCP call blowing the whole context window. 0 = off. Default 60k (~15k tokens). */
|
|
2698
|
+
maxToolResultBytes = 6e4;
|
|
2699
|
+
/** Hook to handle an oversized tool result instead of the default lossy crop: receives the FULL output
|
|
2700
|
+
* and returns the (cropped) string to put in context — e.g. spill to scratch and return a recoverable,
|
|
2701
|
+
* paginated stub. Called only when a result exceeds `maxToolResultBytes`. */
|
|
2702
|
+
capToolResult;
|
|
2689
2703
|
/** VFS dir(s) of skills (`<dir>/<id>/SKILL.md`). If set: inject a catalog + add the `Skill` tool. Multiple dirs are merged (first wins on name collisions). */
|
|
2690
2704
|
skillsDir;
|
|
2691
2705
|
/** VFS dir(s) of slash-command templates (`<dir>/<name>.md`). If set: inject a catalog + add the `SlashCommand` tool. Multiple dirs are merged (first wins). */
|
|
@@ -3008,6 +3022,7 @@ var Agent = class _Agent {
|
|
|
3008
3022
|
toolCallsTotal += toolCalls.length;
|
|
3009
3023
|
if (o.maxToolCalls && toolCallsTotal > o.maxToolCalls) return kill("max_tool_calls");
|
|
3010
3024
|
for (const tc of toolCalls) {
|
|
3025
|
+
if (o.signal?.aborted) return kill("aborted");
|
|
3011
3026
|
const raw = await this.dispatch(tc);
|
|
3012
3027
|
let content;
|
|
3013
3028
|
if (typeof raw === "string") {
|
|
@@ -3100,6 +3115,11 @@ var Agent = class _Agent {
|
|
|
3100
3115
|
this.ctx.emit = void 0;
|
|
3101
3116
|
}
|
|
3102
3117
|
if (!threw) result = await this.maybeAutoTest(tc.function.name, result);
|
|
3118
|
+
const cap = this.options.maxToolResultBytes ?? 0;
|
|
3119
|
+
if (!threw && cap > 0 && result.length > cap) {
|
|
3120
|
+
const info = { tool: tc.function.name, args };
|
|
3121
|
+
result = this.options.capToolResult ? await this.options.capToolResult(result, info) : cropResult(result, cap);
|
|
3122
|
+
}
|
|
3103
3123
|
await hooks?.postToolUse?.(call, result, meta);
|
|
3104
3124
|
this.options.host?.notify?.({ kind: "tool_result", id: tc.id ?? "", output: result, isError: threw });
|
|
3105
3125
|
if (images?.length) {
|
|
@@ -3153,6 +3173,15 @@ function estimateTokens(m) {
|
|
|
3153
3173
|
for (const x of m) chars += contentText(x.content).length + (x.tool_calls ? JSON.stringify(x.tool_calls).length : 0);
|
|
3154
3174
|
return Math.ceil(chars / 4);
|
|
3155
3175
|
}
|
|
3176
|
+
function cropResult(result, cap) {
|
|
3177
|
+
const head = result.slice(0, cap);
|
|
3178
|
+
const nl = head.lastIndexOf("\n");
|
|
3179
|
+
const page = nl > cap * 0.5 ? head.slice(0, nl) : head;
|
|
3180
|
+
const omitted = result.length - page.length;
|
|
3181
|
+
return `${page}
|
|
3182
|
+
|
|
3183
|
+
[output cropped \u2014 showing ${page.length} of ${result.length} bytes; ${omitted} omitted. This is page 1. Refine your query/command to narrow it, or call the tool again with a tighter scope to see more.]`;
|
|
3184
|
+
}
|
|
3156
3185
|
function stubOldToolResults(messages, keep) {
|
|
3157
3186
|
const meta = /* @__PURE__ */ new Map();
|
|
3158
3187
|
for (const msg of messages)
|
|
@@ -3473,6 +3502,127 @@ init_tools();
|
|
|
3473
3502
|
init_tools_structured();
|
|
3474
3503
|
init_logging();
|
|
3475
3504
|
var log4 = forComponent("scratch");
|
|
3505
|
+
var SCRATCH_DIR = "/scratch";
|
|
3506
|
+
function shortArgs(args) {
|
|
3507
|
+
try {
|
|
3508
|
+
const s = JSON.stringify(args ?? {});
|
|
3509
|
+
return s.length > 50 ? s.slice(0, 47) + "\u2026" : s;
|
|
3510
|
+
} catch {
|
|
3511
|
+
return "";
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
var slug = (s) => s.replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").toLowerCase().slice(0, 32) || "out";
|
|
3515
|
+
var Scratch = class {
|
|
3516
|
+
constructor(fs, options = {}) {
|
|
3517
|
+
this.fs = fs;
|
|
3518
|
+
this.options = { threshold: 1500, dir: SCRATCH_DIR, previewChars: 320, ...options };
|
|
3519
|
+
}
|
|
3520
|
+
fs;
|
|
3521
|
+
options;
|
|
3522
|
+
seq = 0;
|
|
3523
|
+
dirReady;
|
|
3524
|
+
/** Number of captures so far. */
|
|
3525
|
+
get count() {
|
|
3526
|
+
return this.seq;
|
|
3527
|
+
}
|
|
3528
|
+
/** Wrap a tool: oversized STRING results spill to a scratch file; everything else passes through. */
|
|
3529
|
+
capture(tool) {
|
|
3530
|
+
const { threshold, dir, previewChars } = this.options;
|
|
3531
|
+
return {
|
|
3532
|
+
...tool,
|
|
3533
|
+
run: async (args, ctx) => {
|
|
3534
|
+
const raw = await tool.run(args, ctx);
|
|
3535
|
+
if (typeof raw !== "string" || raw.length <= threshold) return raw;
|
|
3536
|
+
const id = "a" + ++this.seq;
|
|
3537
|
+
const path = `${dir}/${id}-${slug(tool.name)}.txt`;
|
|
3538
|
+
const header = `# ${tool.name}(${shortArgs(args)}) \u2014 ${raw.length} bytes
|
|
3539
|
+
`;
|
|
3540
|
+
try {
|
|
3541
|
+
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
3542
|
+
await this.fs.writeFile(path, header + raw);
|
|
3543
|
+
} catch (e) {
|
|
3544
|
+
log4.debug("scratch write failed; returning raw", e);
|
|
3545
|
+
return raw;
|
|
3546
|
+
}
|
|
3547
|
+
const preview = raw.slice(0, previewChars).replace(/\s+/g, " ").trim();
|
|
3548
|
+
return `[scratch ${path} \xB7 ${tool.name} \xB7 ${raw.length} bytes \u2014 full output saved out of context to keep it clean]
|
|
3549
|
+
preview: ${preview}\u2026
|
|
3550
|
+
To pull a specific detail, Grep/Read ${path}, or call Ask({ question: "\u2026", over: "${path}" }). Do NOT guess at what the preview cuts off.`;
|
|
3551
|
+
}
|
|
3552
|
+
};
|
|
3553
|
+
}
|
|
3554
|
+
/** Wrap many tools at once. */
|
|
3555
|
+
captureAll(tools) {
|
|
3556
|
+
return tools.map((t) => this.capture(t));
|
|
3557
|
+
}
|
|
3558
|
+
/**
|
|
3559
|
+
* Spill an oversized tool result to a scratch file and return PAGE 1 + a recoverable, paginated stub.
|
|
3560
|
+
* Drop-in for `Agent.capToolResult`: the agent sees usable content immediately and knows how to get
|
|
3561
|
+
* the rest (refine the query, Read the file in pages with offset/limit, or Ask to extract specifics).
|
|
3562
|
+
* Lossless — unlike a plain crop, the full output stays available on the scratch FS.
|
|
3563
|
+
*/
|
|
3564
|
+
async spill(full, info, pageBytes = 8e3) {
|
|
3565
|
+
const { dir } = this.options;
|
|
3566
|
+
const id = "a" + ++this.seq;
|
|
3567
|
+
const path = `${dir}/${id}-${slug(info.tool)}.txt`;
|
|
3568
|
+
const header = `# ${info.tool}(${shortArgs(info.args)}) \u2014 ${full.length} bytes
|
|
3569
|
+
`;
|
|
3570
|
+
try {
|
|
3571
|
+
await (this.dirReady ??= mkdirp(this.fs, dir));
|
|
3572
|
+
await this.fs.writeFile(path, header + full);
|
|
3573
|
+
} catch (e) {
|
|
3574
|
+
log4.debug("scratch spill failed; cropping lossy", e);
|
|
3575
|
+
return full.slice(0, pageBytes) + `
|
|
3576
|
+
|
|
3577
|
+
[output cropped to ${pageBytes} of ${full.length} bytes; full output unavailable (scratch write failed) \u2014 refine your query]`;
|
|
3578
|
+
}
|
|
3579
|
+
const head = full.slice(0, pageBytes);
|
|
3580
|
+
const nl = head.lastIndexOf("\n");
|
|
3581
|
+
const page = nl > pageBytes * 0.5 ? head.slice(0, nl) : head;
|
|
3582
|
+
return `${page}
|
|
3583
|
+
|
|
3584
|
+
[output cropped \u2014 page 1 (${page.length} of ${full.length} bytes). Full output saved to ${path}. To see more: refine your query/command to narrow it, or Read ${path} with offset/limit to page through it, or Ask({ question: "\u2026", over: "${path}" }) to extract specifics.]`;
|
|
3585
|
+
}
|
|
3586
|
+
};
|
|
3587
|
+
var ASK_PROMPT = "You are a retrieval-extraction step with Read, Grep and Glob over a scratch filesystem holding raw outputs from earlier tools. Find the information that answers the question and return it concisely, quoting values/facts verbatim. Do NOT add analysis or anything not grounded in the files. If the answer is not present, say so plainly.";
|
|
3588
|
+
function makeAskTool(o) {
|
|
3589
|
+
const dir = o.dir ?? SCRATCH_DIR;
|
|
3590
|
+
return {
|
|
3591
|
+
name: "Ask",
|
|
3592
|
+
description: "Answer a question by peeking into the scratch files \u2014 large earlier outputs (web search, big reads, subagent reports) kept out of your context. Pass `over` with a scratch path for a targeted lookup, or omit it to search the scratch dir. Returns only the extracted answer; the full data never enters your context.",
|
|
3593
|
+
parameters: {
|
|
3594
|
+
type: "object",
|
|
3595
|
+
required: ["question"],
|
|
3596
|
+
properties: {
|
|
3597
|
+
question: { type: "string", description: "what you need from the scratch files" },
|
|
3598
|
+
over: { type: "string", description: 'scratch path to read, e.g. "/scratch/a3-websearch.txt"; omit to search' }
|
|
3599
|
+
}
|
|
3600
|
+
},
|
|
3601
|
+
async run({ question, over }) {
|
|
3602
|
+
const q2 = String(question ?? "").trim();
|
|
3603
|
+
if (!q2) return "Error: empty question";
|
|
3604
|
+
const child = new Agent({
|
|
3605
|
+
ai: o.ai,
|
|
3606
|
+
model: o.model,
|
|
3607
|
+
fs: o.fs,
|
|
3608
|
+
tools: toolsByName(["Read", "Grep", "Glob"]),
|
|
3609
|
+
maxSteps: o.maxSteps ?? 6,
|
|
3610
|
+
systemPrompt: ASK_PROMPT
|
|
3611
|
+
});
|
|
3612
|
+
const hint = over ? `Start by reading: ${over}.` : `Grep/Glob ${dir} to find the relevant file(s) first.`;
|
|
3613
|
+
try {
|
|
3614
|
+
const res = await child.run(`${hint}
|
|
3615
|
+
|
|
3616
|
+
Question: ${q2}`);
|
|
3617
|
+
const answer = (res.text ?? "").trim();
|
|
3618
|
+
return answer || "(no answer found in scratch)";
|
|
3619
|
+
} catch (e) {
|
|
3620
|
+
log4.debug("Ask peek failed", e);
|
|
3621
|
+
return `Error querying scratch: ${e?.message ?? e}`;
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
};
|
|
3625
|
+
}
|
|
3476
3626
|
|
|
3477
3627
|
// src/lessons.ts
|
|
3478
3628
|
init_logging();
|
|
@@ -3531,15 +3681,15 @@ If the run was fine or the issue was purely task-specific (not generalizable), r
|
|
|
3531
3681
|
const m = text.match(/LESSON:\s*(.+)/i);
|
|
3532
3682
|
const lesson = m?.[1]?.trim().slice(0, 200) ?? "";
|
|
3533
3683
|
if (!lesson || /^none\b/i.test(lesson)) return null;
|
|
3534
|
-
const
|
|
3684
|
+
const slug2 = ("lesson-" + slugify(lesson)).slice(0, 56);
|
|
3535
3685
|
try {
|
|
3536
|
-
await writeFact(o.fs, o.dir,
|
|
3686
|
+
await writeFact(o.fs, o.dir, slug2, lesson);
|
|
3537
3687
|
} catch (e) {
|
|
3538
3688
|
log6.warn(`could not persist lesson: ${e?.message ?? e}`);
|
|
3539
3689
|
return null;
|
|
3540
3690
|
}
|
|
3541
|
-
log6.debug(`reflection persisted ${
|
|
3542
|
-
return
|
|
3691
|
+
log6.debug(`reflection persisted ${slug2}`);
|
|
3692
|
+
return slug2;
|
|
3543
3693
|
}
|
|
3544
3694
|
function digestRun(messages, maxChars) {
|
|
3545
3695
|
const lines = [];
|
|
@@ -3566,10 +3716,18 @@ var DuplexAgentOptions = class {
|
|
|
3566
3716
|
ai;
|
|
3567
3717
|
/** The WORKER's filesystem (act + think). If omitted the worker keeps Agent's jailed-disk-at-cwd default. */
|
|
3568
3718
|
fs;
|
|
3569
|
-
|
|
3719
|
+
// The reflex IS the voice. 120b (not 20b) for channel discipline + instruction-following: the 20b
|
|
3720
|
+
// mislabels gpt-oss harmony channels under load, leaking raw analysis into the spoken `final` channel
|
|
3721
|
+
// (and misfiring Hold). 120b is the same price tier (~$0.15/$0.60) — the quality/cost trade is free.
|
|
3722
|
+
reflexModel = "groq/openai/gpt-oss-120b";
|
|
3570
3723
|
actModel = "anthropic/claude-sonnet-4-6";
|
|
3571
3724
|
/** Premium reasoning model. Set to `false` to disable the Think tier entirely. */
|
|
3572
3725
|
thinkModel = "anthropic/claude-opus-4-8";
|
|
3726
|
+
/** Per-worker providerOptions, derived from the worker's actual model at spawn time (IoC — keeps duplex
|
|
3727
|
+
* provider-agnostic). Workers override the reflex/main model, so provider-specific options (e.g. cursor's
|
|
3728
|
+
* cwd/cursorSession) must be recomputed for the worker's model, never inherited from the main template —
|
|
3729
|
+
* leaking cursor options to an anthropic worker is a hard 400. Returns undefined → no providerOptions. */
|
|
3730
|
+
providerOptionsFor;
|
|
3573
3731
|
/** Escape hatches merged over the derived per-agent options. */
|
|
3574
3732
|
reflexOptions;
|
|
3575
3733
|
actOptions;
|
|
@@ -3648,7 +3806,12 @@ var DuplexAgent = class {
|
|
|
3648
3806
|
const canSearch = workerToolNames.some((n) => /WebSearch/i.test(n));
|
|
3649
3807
|
const canFetch = workerToolNames.some((n) => /WebFetch/i.test(n));
|
|
3650
3808
|
const workerWeb = canSearch ? `, and it CAN search the web and read web pages \u2014 so when the user gives you something specific to look up ("search for X", "find me\u2026", "what's the latest on\u2026"), route it to Act. But a bare capability QUESTION like "can you search the web?" just gets a short spoken "yes, I can" \u2014 do NOT dispatch and NEVER invent a query the user did not give you` : canFetch ? ", and it can fetch a specific web page URL (but cannot search the web)" : "";
|
|
3651
|
-
const
|
|
3809
|
+
const mcpNames = [
|
|
3810
|
+
...Object.keys(o.actOptions?.providerOptions?.mcpServers ?? {}),
|
|
3811
|
+
...new Set(workerToolNames.filter((n) => n.startsWith("mcp__")).map((n) => n.slice(5).split("__")[0]))
|
|
3812
|
+
];
|
|
3813
|
+
const workerMcp = mcpNames.length ? `, and it can use these MCP servers: ${[...new Set(mcpNames)].join(", ")}` + (mcpNames.some((n) => /browser/i.test(n)) ? ' \u2014 including driving a REAL browser (open tabs, navigate, click, screenshot), so answer "yes" if asked whether you can control/drive a browser and route an actual browse to Act' : "") : "";
|
|
3814
|
+
const prompt = VOICE_SYSTEM_PROMPT.replace("{{MEMORY_SLOT}}", memSlot).replace("{{THINK_SLOT}}", thinkSlot).replace("{{WORKER_WEB}}", workerWeb + workerMcp) + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : "") + `
|
|
3652
3815
|
Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
3653
3816
|
const tools = [
|
|
3654
3817
|
...o.reflexOptions?.tools ?? [],
|
|
@@ -3832,6 +3995,9 @@ ${recent}` : brief) + verify;
|
|
|
3832
3995
|
model: tierModel,
|
|
3833
3996
|
...tier === "think" ? { reasoning: tierOpts?.reasoning ?? "high" } : {},
|
|
3834
3997
|
...tierOpts,
|
|
3998
|
+
// Recompute providerOptions for THIS worker's model (after tierOpts so it wins over any inherited
|
|
3999
|
+
// main-template value) — prevents cursor-only cwd/cursorSession leaking onto an anthropic worker.
|
|
4000
|
+
providerOptions: o.providerOptionsFor?.(tierModel),
|
|
3835
4001
|
...workerHost ? { host: workerHost } : {},
|
|
3836
4002
|
...hooks ? { hooks } : {},
|
|
3837
4003
|
signal: controller.signal
|
|
@@ -4080,8 +4246,10 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
4080
4246
|
case "capabilities": {
|
|
4081
4247
|
const actTools = this.options.actOptions?.tools ?? [];
|
|
4082
4248
|
const names = actTools.map((t) => t.name);
|
|
4249
|
+
const mcpServers = Object.keys(this.options.actOptions?.providerOptions?.mcpServers ?? {});
|
|
4250
|
+
const mcpNote = mcpServers.length ? ` Plus MCP servers your worker can use: ${mcpServers.join(", ")} (e.g. browser-bridge \u2192 drive a real browser: open tabs, navigate, click, screenshot).` : "";
|
|
4083
4251
|
if (!names.length)
|
|
4084
|
-
return "Your worker uses Act's default local toolset (reading/editing files, running shell commands). No extra tools (e.g. web/internet) are configured; if a request is not a basic file or shell operation, assume you can't do it and say so.";
|
|
4252
|
+
return "Your worker uses Act's default local toolset (reading/editing files, running shell commands). No extra tools (e.g. web/internet) are configured; if a request is not a basic file or shell operation, assume you can't do it and say so." + mcpNote;
|
|
4085
4253
|
const hasFetch = names.some((n) => /WebFetch/i.test(n));
|
|
4086
4254
|
const hasBrowser = names.some((n) => /browser.*(navigate|click|page|type)/i.test(n));
|
|
4087
4255
|
const hasSearch = names.some((n) => /(^|_)WebSearch$|search/i.test(n) && !/WebFetch|browser/i.test(n));
|
|
@@ -4090,7 +4258,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
4090
4258
|
if (hasBrowser) notes.push("The browser tools drive a real browser: you CAN open a site and, if needed, navigate to a search engine and search there \u2014 but it is manual and takes a moment, not an instant lookup.");
|
|
4091
4259
|
else if (!hasSearch && hasFetch) notes.push('You have no general web-search tool, so for an instant "search the web" you can only fetch a URL they provide.');
|
|
4092
4260
|
const webNote = notes.length ? " NOTE: " + notes.join(" ") : "";
|
|
4093
|
-
return `Tools your background worker (Act) can actually use: ${names.join(", ")}. Read each name literally and match the request to a SPECIFIC tool; if none fits, you do NOT have that ability \u2014 say so honestly.` + webNote;
|
|
4261
|
+
return `Tools your background worker (Act) can actually use: ${names.join(", ")}. Read each name literally and match the request to a SPECIFIC tool; if none fits, you do NOT have that ability \u2014 say so honestly.` + webNote + mcpNote;
|
|
4094
4262
|
}
|
|
4095
4263
|
case "time":
|
|
4096
4264
|
return (/* @__PURE__ */ new Date()).toString();
|
|
@@ -5475,6 +5643,10 @@ function toCursorMcp(servers) {
|
|
|
5475
5643
|
}
|
|
5476
5644
|
return Object.keys(out).length ? { mcpServers: out } : void 0;
|
|
5477
5645
|
}
|
|
5646
|
+
function cursorProviderOptions(model, cwd, mcpServers) {
|
|
5647
|
+
if (!(model ?? "").startsWith("cursor/")) return void 0;
|
|
5648
|
+
return { cwd, ...toCursorMcp(mcpServers) ?? {}, ...process.env.CURSOR_WARM === "0" ? {} : { cursorSession: randomUUID() } };
|
|
5649
|
+
}
|
|
5478
5650
|
async function buildAgent(o) {
|
|
5479
5651
|
if (typeof o.sandbox !== "boolean")
|
|
5480
5652
|
throw new Error(
|
|
@@ -5566,6 +5738,8 @@ Reference files in them by their mount path (the left side).`;
|
|
|
5566
5738
|
const jobs = new ShellJobRegistry({ cwd, killOnExit: true });
|
|
5567
5739
|
realShell = [makeRealShellTool({ cwd, registry: jobs }), ...makeShellJobTools(jobs)];
|
|
5568
5740
|
}
|
|
5741
|
+
const scratchDir = o.scratch ? o.scratchDir ?? `${cwd}/.agent/scratch` : void 0;
|
|
5742
|
+
const scratch = scratchDir ? new Scratch(fs, { dir: scratchDir }) : void 0;
|
|
5569
5743
|
return new Agent({
|
|
5570
5744
|
ai: o.ai,
|
|
5571
5745
|
fs,
|
|
@@ -5576,7 +5750,10 @@ Reference files in them by their mount path (the left side).`;
|
|
|
5576
5750
|
// would corrupt those calls.
|
|
5577
5751
|
// Warm cursor session (default on; CURSOR_WARM=0 to disable): one long-lived agent reused across
|
|
5578
5752
|
// turns, amortising the ~2s Agent.create + h2 handshake. Keyed per agentx launch.
|
|
5579
|
-
...
|
|
5753
|
+
...(() => {
|
|
5754
|
+
const po = cursorProviderOptions(o.model, cwd, o.mcpServers);
|
|
5755
|
+
return po ? { providerOptions: po } : {};
|
|
5756
|
+
})(),
|
|
5580
5757
|
...(() => {
|
|
5581
5758
|
const now5 = /* @__PURE__ */ new Date();
|
|
5582
5759
|
const platformNames = { darwin: "macOS", linux: "Linux", win32: "Windows" };
|
|
@@ -5590,7 +5767,13 @@ SANDBOX: you operate on an in-memory COPY of this directory \u2014 the real disk
|
|
|
5590
5767
|
The filesystem root '/' is the real machine root \u2014 you have full filesystem access, like a normal shell. Use paths relative to the working directory, or absolute paths. Secret files (.env, .ssh, keys, .git) are hidden for safety.`;
|
|
5591
5768
|
const shellNote = useRealShell && !virtual ? "The `Shell` tool runs /bin/sh \u2014 you can run any installed binary (git, bun, node, curl, etc). Use `Shell` for all shell commands." : void 0;
|
|
5592
5769
|
const toneNote = "Be concise. Do not use emojis unless the user asks. Do not explain what you are about to do \u2014 just do it.";
|
|
5593
|
-
const
|
|
5770
|
+
const mcpNote = (() => {
|
|
5771
|
+
const names = new Set(Object.keys(o.mcpServers ?? {}));
|
|
5772
|
+
for (const t of o.extraTools ?? []) if (t.name.startsWith("mcp__")) names.add(t.name.slice(5).split("__")[0]);
|
|
5773
|
+
const list = [...names];
|
|
5774
|
+
return `You are agentx (the agent.libx.js runtime) \u2014 NOT Claude Code or Claude Desktop, which are separate agents with their own config. Your MCP servers are configured in .agent/settings.json or .agent/config.* (this project's .agent/ or ~/.agent/), and their tools are already in your tool list as \`mcp__<server>__<tool>\`.${list.length ? ` Configured MCP servers: ${list.join(", ")}.` : " No MCP servers are currently configured."} When asked which MCPs/tools you have, answer from your own tool list \u2014 never read Claude Code/Desktop config (e.g. ~/.claude.json, claude_desktop_config.json) to answer; those belong to a different agent.`;
|
|
5775
|
+
})();
|
|
5776
|
+
const extra = [o.appendSystemPrompt, envNote, cwdNote, shellNote, toneNote, mountNote, mcpNote].filter(Boolean).join("\n\n");
|
|
5594
5777
|
const basePrompt = (() => {
|
|
5595
5778
|
let p = new AgentOptions().systemPrompt;
|
|
5596
5779
|
if (!virtual) p = p.replace("operating on a virtual filesystem", "operating on the host filesystem");
|
|
@@ -5601,9 +5784,11 @@ The filesystem root '/' is the real machine root \u2014 you have full filesystem
|
|
|
5601
5784
|
})(),
|
|
5602
5785
|
tools: (() => {
|
|
5603
5786
|
const base = toolsByName([...o.tools ?? DEFAULT_TOOLS, ...autoWebTools()]);
|
|
5604
|
-
|
|
5787
|
+
const tail = [...o.extraTools ?? []];
|
|
5788
|
+
if (scratch) tail.push(makeAskTool({ fs, ai: o.ai, model: o.scratchAskModel ?? o.model ?? "anthropic/claude-sonnet-4-6", dir: scratchDir }));
|
|
5789
|
+
if (!realShell.length) return [...base, ...tail];
|
|
5605
5790
|
const filtered = base.filter((t) => t.name !== "bash");
|
|
5606
|
-
return [...filtered, ...realShell, ...
|
|
5791
|
+
return [...filtered, ...realShell, ...tail];
|
|
5607
5792
|
})(),
|
|
5608
5793
|
maxSteps: o.maxSteps ?? 30,
|
|
5609
5794
|
...o.reasoning != null ? { reasoning: o.reasoning } : {},
|
|
@@ -5613,6 +5798,9 @@ The filesystem root '/' is the real machine root \u2014 you have full filesystem
|
|
|
5613
5798
|
planMode: o.planMode ?? false,
|
|
5614
5799
|
permissions: o.permissions,
|
|
5615
5800
|
subagents: o.subagents ?? false,
|
|
5801
|
+
// When scratch is on, an oversized tool result spills to a scratch file + recoverable paginated stub
|
|
5802
|
+
// (lossless). Without scratch, the Agent's default crop (lossy) still guards the context window.
|
|
5803
|
+
...scratch ? { capToolResult: (full, info) => scratch.spill(full, info) } : {},
|
|
5616
5804
|
backgroundJobs: o.backgroundJobs ?? virtual,
|
|
5617
5805
|
// default ON in virtual modes (no real shell there); disk uses ShellJobRegistry
|
|
5618
5806
|
skillsDir: dots("skills"),
|
|
@@ -7837,6 +8025,7 @@ function parseArgs(argv) {
|
|
|
7837
8025
|
else if (x === "--ask") a.ask = true;
|
|
7838
8026
|
else if (x === "--yes" || x === "-y") a.yes = true;
|
|
7839
8027
|
else if (x === "--vfs" || x === "--sandbox") a.vfs = true;
|
|
8028
|
+
else if (x === "--scratch") a.scratch = true;
|
|
7840
8029
|
else if (x === "--boddb") a.boddb = val(++i, x);
|
|
7841
8030
|
else if (x === "--seed") a.seed = true;
|
|
7842
8031
|
else if (x === "--shell") a.shell = true;
|
|
@@ -7890,6 +8079,7 @@ Flags:
|
|
|
7890
8079
|
--no-stream disable token streaming
|
|
7891
8080
|
(default: disk mode \u2014 full real filesystem access, like Claude Code)
|
|
7892
8081
|
--vfs, --sandbox sandbox mode: work over an in-memory copy of cwd \u2014 real disk is NEVER modified
|
|
8082
|
+
--scratch spill big web outputs to scratch files (kept out of context; peek via Grep/Ask)
|
|
7893
8083
|
--boddb <dir> database-backed workspace: files live in a persistent bod-db store at <dir>,
|
|
7894
8084
|
surviving across runs \u2014 real disk is NEVER modified (DB-native; add --seed below)
|
|
7895
8085
|
--seed with --boddb: hydrate the store from cwd on the first run (empty DB) only
|
|
@@ -7907,7 +8097,7 @@ Flags:
|
|
|
7907
8097
|
impulsive reactions, human pacing (implies --duplex; aliases: --convo, --voice)
|
|
7908
8098
|
with SONIOX_API_KEY + CARTESIA_API_KEY(+VOICE_ID) set: real voice I/O \u2014 mic in,
|
|
7909
8099
|
spoken replies out (echo-cancelled; speak over it to interrupt)
|
|
7910
|
-
--voice-model <id> with --duplex: the fast voice model (default groq/openai/gpt-oss-
|
|
8100
|
+
--voice-model <id> with --duplex: the fast voice model (default groq/openai/gpt-oss-120b)
|
|
7911
8101
|
--think-model <id> with --duplex: the premium deep-reasoning model (default anthropic/claude-opus-4-6)
|
|
7912
8102
|
--no-think with --duplex: disable the Think tier (Act handles everything)
|
|
7913
8103
|
--add-dir <path> mount another directory into the workspace (repeatable; disk mode only)
|
|
@@ -7939,7 +8129,7 @@ Project instructions: ./AGENTS.md or ./CLAUDE.md are auto-loaded (scaffold with
|
|
|
7939
8129
|
Auto-loaded from ./.agent/: commands/, skills/, memory/, agents/.
|
|
7940
8130
|
|
|
7941
8131
|
REPL shortcuts: !<cmd> runs a shell command inline \xB7 #<note> saves a memory \xB7 @path inlines a file
|
|
7942
|
-
REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /reasoning /config /rename /compact /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /goal /exit (duplex: /act /think /voice-model /think-model)
|
|
8132
|
+
REPL slash commands: /help /version /tools /permissions /status /cost /context /cwd /model /reasoning /config /rename /compact /rewind /undo /clear /sessions /resume /commands /skills /mcp /init /export /paste /goal /exit (duplex: /act /think /voice /voice-model /think-model)
|
|
7943
8133
|
REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
|
|
7944
8134
|
REPL multi-line: Option/Alt+Enter inserts a newline, or end a line with \\ to continue. Esc cancels a running turn / clears the input line; double-Esc jumps back to edit a previous message.
|
|
7945
8135
|
REPL shortcuts: Shift+Tab cycles permission posture (ask \u2192 accept-edits \u2192 plan) \xB7 Alt+T toggles reasoning \xB7 Alt+P switches model \xB7 Ctrl+O toggles verbose tool output \xB7 \u2192 or Tab accepts the dim history ghost-suggestion \xB7 Alt+S/Ctrl+S stash/unstash.
|
|
@@ -8356,6 +8546,7 @@ function optsFor(args, ai, cfg = {}, extraTools = []) {
|
|
|
8356
8546
|
seed: args.seed,
|
|
8357
8547
|
realShell: args.shell,
|
|
8358
8548
|
// undefined → core.ts defaults (on for disk, off for sandbox/boddb)
|
|
8549
|
+
scratch: args.scratch,
|
|
8359
8550
|
appendSystemPrompt: args.appendSystemPrompt,
|
|
8360
8551
|
addDirs: args.addDirs,
|
|
8361
8552
|
stream: args.stream,
|
|
@@ -8676,6 +8867,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
8676
8867
|
const duplex = args.duplex;
|
|
8677
8868
|
let dx;
|
|
8678
8869
|
let voiceIO;
|
|
8870
|
+
let toggleVoice;
|
|
8679
8871
|
let editorRef;
|
|
8680
8872
|
let repaintStash = () => {
|
|
8681
8873
|
};
|
|
@@ -8719,7 +8911,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
8719
8911
|
return { decision: "deny" };
|
|
8720
8912
|
};
|
|
8721
8913
|
if (duplex) {
|
|
8722
|
-
const { host: _host, stream: _stream, signal: _signal, ...wo } = agent.options;
|
|
8914
|
+
const { host: _host, stream: _stream, signal: _signal, providerOptions: _po, ...wo } = agent.options;
|
|
8723
8915
|
workerOptions = wo;
|
|
8724
8916
|
if (workerOptions.permissions)
|
|
8725
8917
|
workerOptions.permissions = new PermissionPolicy({ ...workerOptions.permissions.options, host: void 0, ask: duplexAsk });
|
|
@@ -8799,6 +8991,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
8799
8991
|
...args.voiceModel ?? cfg.reflexModel ? { reflexModel: resolveModelOrNewest(args.voiceModel ?? cfg.reflexModel) } : {},
|
|
8800
8992
|
actModel: agent.options.model,
|
|
8801
8993
|
actOptions: workerOptions,
|
|
8994
|
+
providerOptionsFor: (m) => cursorProviderOptions(m, cwd, cfg.mcpServers),
|
|
8802
8995
|
...(args.thinkModel ?? cfg.thinkModel) !== void 0 ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {},
|
|
8803
8996
|
host,
|
|
8804
8997
|
...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true } : {},
|
|
@@ -8901,15 +9094,30 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
8901
9094
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
8902
9095
|
return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
|
|
8903
9096
|
};
|
|
9097
|
+
const forceQuit = (code = 130) => {
|
|
9098
|
+
try {
|
|
9099
|
+
voiceIO?.stop();
|
|
9100
|
+
} catch {
|
|
9101
|
+
}
|
|
9102
|
+
try {
|
|
9103
|
+
disposeCursorSessions();
|
|
9104
|
+
} catch {
|
|
9105
|
+
}
|
|
9106
|
+
void closeMcp(mounted);
|
|
9107
|
+
process.exit(code);
|
|
9108
|
+
};
|
|
8904
9109
|
process.on("SIGINT", () => {
|
|
8905
9110
|
if (activeTurn) {
|
|
9111
|
+
if (aborting) {
|
|
9112
|
+
err(red("\n \u23FB force-quit\n"));
|
|
9113
|
+
forceQuit();
|
|
9114
|
+
return;
|
|
9115
|
+
}
|
|
8906
9116
|
activeTurn.abort();
|
|
8907
9117
|
voiceIO?.interrupt();
|
|
8908
9118
|
return;
|
|
8909
9119
|
}
|
|
8910
|
-
|
|
8911
|
-
void closeMcp(mounted);
|
|
8912
|
-
process.exit(130);
|
|
9120
|
+
forceQuit();
|
|
8913
9121
|
});
|
|
8914
9122
|
installCancelGuards(mounted);
|
|
8915
9123
|
const store = new SessionStore(cwd);
|
|
@@ -9267,6 +9475,15 @@ ${extra}` : body);
|
|
|
9267
9475
|
err(dim(` worker chrome: ${workerChrome} (use /workers full|minimal)
|
|
9268
9476
|
`));
|
|
9269
9477
|
}
|
|
9478
|
+
}, voice: {
|
|
9479
|
+
desc: "toggle live voice I/O on/off mid-session (needs SONIOX/CARTESIA keys + a TTY)",
|
|
9480
|
+
run: async () => {
|
|
9481
|
+
if (!toggleVoice) {
|
|
9482
|
+
err(dim(" (voice needs --duplex on a TTY)\n"));
|
|
9483
|
+
return;
|
|
9484
|
+
}
|
|
9485
|
+
await toggleVoice();
|
|
9486
|
+
}
|
|
9270
9487
|
}, "voice-model": {
|
|
9271
9488
|
desc: "switch the reflex (voice) model \u2014 /voice-model <id>, or alone for a picker",
|
|
9272
9489
|
run: async (a) => {
|
|
@@ -9492,7 +9709,7 @@ ${extra}` : body);
|
|
|
9492
9709
|
commands: { desc: "pick a custom slash command to run (./.agent/commands)", run: () => pickAndRun("command") },
|
|
9493
9710
|
skills: { desc: "pick a skill to run (./.agent/skills)", run: () => pickAndRun("skill") },
|
|
9494
9711
|
mcp: {
|
|
9495
|
-
desc: "manage MCP servers \u2014 /mcp [add <name> <cmd|url>] [login <name>] [remove <name>] [resources [name]]",
|
|
9712
|
+
desc: "manage MCP servers \u2014 /mcp [add <name> <cmd|url>] [login <name>] [remove <name>] [tools [name]] [resources [name]]",
|
|
9496
9713
|
run: async (a) => {
|
|
9497
9714
|
const sub = a[0]?.toLowerCase();
|
|
9498
9715
|
if (sub === "login") {
|
|
@@ -9568,6 +9785,22 @@ ${extra}` : body);
|
|
|
9568
9785
|
`));
|
|
9569
9786
|
return;
|
|
9570
9787
|
}
|
|
9788
|
+
if (sub === "tools") {
|
|
9789
|
+
const filter = a[1];
|
|
9790
|
+
const targets = filter ? mounted.filter((m) => m.name === filter) : mounted;
|
|
9791
|
+
if (!targets.length) {
|
|
9792
|
+
err(dim(` (no ${filter ? `MCP server "${filter}"` : "MCP servers"} found)
|
|
9793
|
+
`));
|
|
9794
|
+
return;
|
|
9795
|
+
}
|
|
9796
|
+
for (const m of targets) {
|
|
9797
|
+
err(` ${cyan(m.name)} ${dim(`(${m.tools.length} tools)`)}
|
|
9798
|
+
`);
|
|
9799
|
+
for (const t of m.tools) err(dim(` - ${t.name.replace(`mcp__${m.name}__`, "")}${t.description ? " \u2014 " + t.description.split("\n")[0] : ""}
|
|
9800
|
+
`));
|
|
9801
|
+
}
|
|
9802
|
+
return;
|
|
9803
|
+
}
|
|
9571
9804
|
if (sub === "resources") {
|
|
9572
9805
|
const filter = a[1];
|
|
9573
9806
|
const targets = filter ? mounted.filter((m) => m.name === filter) : mounted;
|
|
@@ -9602,16 +9835,16 @@ ${extra}` : body);
|
|
|
9602
9835
|
}
|
|
9603
9836
|
for (const m of mounted) {
|
|
9604
9837
|
const ver = m.serverInfo?.name ? dim(` \xB7 ${m.serverInfo.name}${m.serverInfo.version ? " v" + m.serverInfo.version : ""}`) : "";
|
|
9605
|
-
err(` ${cyan(m.name)}${ver} ${dim(`(${m.tools.length} tools)`)}
|
|
9838
|
+
err(` ${green("\u2713")} ${cyan(m.name)}${ver} ${dim(`(${m.tools.length} tools)`)}
|
|
9606
9839
|
`);
|
|
9607
|
-
for (const t of m.tools) err(dim(` - ${t.name.replace(`mcp__${m.name}__`, "")}${t.description ? " \u2014 " + t.description.split("\n")[0] : ""}
|
|
9608
|
-
`));
|
|
9609
9840
|
}
|
|
9841
|
+
err(dim(" /mcp tools [name] to list tools \xB7 /mcp resources [name] for resources\n"));
|
|
9610
9842
|
};
|
|
9611
9843
|
const items = [
|
|
9612
9844
|
{ label: "list", value: "list", desc: `show mounted servers (${mounted.length})` },
|
|
9613
9845
|
{ label: "add", value: "add", desc: "mount a new MCP server" },
|
|
9614
9846
|
...mounted.length ? [
|
|
9847
|
+
{ label: "tools", value: "tools", desc: "list a server's tools" },
|
|
9615
9848
|
{ label: "remove", value: "remove", desc: "unmount an MCP server" },
|
|
9616
9849
|
{ label: "resources", value: "resources", desc: "list server resources" }
|
|
9617
9850
|
] : []
|
|
@@ -9640,13 +9873,13 @@ ${extra}` : body);
|
|
|
9640
9873
|
const rv = await selectMenu(process.stderr, { title: "remove server", items: mounted.map((m) => ({ label: m.name, value: m.name })) });
|
|
9641
9874
|
if (!rv) return;
|
|
9642
9875
|
a = ["remove", rv];
|
|
9643
|
-
} else if (picked === "resources") {
|
|
9876
|
+
} else if (picked === "tools" || picked === "resources") {
|
|
9644
9877
|
if (mounted.length === 1) {
|
|
9645
|
-
a = [
|
|
9878
|
+
a = [picked, mounted[0].name];
|
|
9646
9879
|
} else {
|
|
9647
|
-
const rv = await selectMenu(process.stderr, { title:
|
|
9880
|
+
const rv = await selectMenu(process.stderr, { title: `server ${picked}`, items: [{ label: "(all)", value: "" }, ...mounted.map((m) => ({ label: m.name, value: m.name }))] });
|
|
9648
9881
|
if (rv === null) return;
|
|
9649
|
-
a = rv ? [
|
|
9882
|
+
a = rv ? [picked, rv] : [picked];
|
|
9650
9883
|
}
|
|
9651
9884
|
}
|
|
9652
9885
|
return builtins.mcp.run(a);
|
|
@@ -9786,7 +10019,13 @@ ${extra}` : body);
|
|
|
9786
10019
|
aborting = true;
|
|
9787
10020
|
activeTurn.abort();
|
|
9788
10021
|
voiceIO?.interrupt();
|
|
9789
|
-
err(yellow("\n \u238B cancelling\u2026\n"));
|
|
10022
|
+
err(yellow("\n \u238B cancelling\u2026") + dim(" (Ctrl-C again to force-quit)\n"));
|
|
10023
|
+
setTimeout(() => {
|
|
10024
|
+
if (activeTurn) err(red(" \u26A0 still cancelling \u2014 press Ctrl-C to force-quit\n"));
|
|
10025
|
+
}, 4e3).unref?.();
|
|
10026
|
+
} else if (key?.ctrl && k === "c") {
|
|
10027
|
+
err(red("\n \u23FB force-quit\n"));
|
|
10028
|
+
forceQuit();
|
|
9790
10029
|
} else if (k === "escape" && !pendingRewind) {
|
|
9791
10030
|
pendingRewind = true;
|
|
9792
10031
|
err(dim(" \u238B\u238B jumping back to edit\u2026\n"));
|
|
@@ -9889,67 +10128,91 @@ ${extra}` : body);
|
|
|
9889
10128
|
};
|
|
9890
10129
|
let voicePartial = "";
|
|
9891
10130
|
let partialRedraw = null;
|
|
9892
|
-
|
|
10131
|
+
const startVoice = async (greet) => {
|
|
10132
|
+
if (voiceIO) return true;
|
|
10133
|
+
if (!duplex || !process.stdin.isTTY) {
|
|
10134
|
+
err(dim(" (voice needs --duplex on a TTY)\n"));
|
|
10135
|
+
return false;
|
|
10136
|
+
}
|
|
9893
10137
|
if (!VoiceIO.available()) {
|
|
9894
10138
|
err(dim(" (voice I/O off \u2014 set SONIOX_API_KEY, CARTESIA_API_KEY, CARTESIA_VOICE_ID to talk)\n"));
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
},
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
10139
|
+
return false;
|
|
10140
|
+
}
|
|
10141
|
+
voiceIO = new VoiceIO({
|
|
10142
|
+
// No ack phrase by default: a fixed "Mm-hm," every turn reads robotic, Haiku's TTFT doesn't
|
|
10143
|
+
// need masking (~0.7-1.2s full turns), and the conversational register already opens with a
|
|
10144
|
+
// natural reaction. The mechanism (+ echo-leak guard) stays for slower voice models.
|
|
10145
|
+
onState: () => editorRef?.redrawNow(),
|
|
10146
|
+
// Throttled: each redraw clears the screen below the prompt — a partial-per-token storm
|
|
10147
|
+
// (fast speech, or echo bleed if AEC degrades) would continuously erase streamed text.
|
|
10148
|
+
onPartial: (text) => {
|
|
10149
|
+
if (text === voicePartial) return;
|
|
10150
|
+
voicePartial = text;
|
|
10151
|
+
if (!partialRedraw) partialRedraw = setTimeout(() => {
|
|
10152
|
+
partialRedraw = null;
|
|
10153
|
+
editorRef?.redrawNow();
|
|
10154
|
+
}, 250);
|
|
10155
|
+
},
|
|
10156
|
+
onBargeIn: (phase) => {
|
|
10157
|
+
activeTurn?.abort();
|
|
10158
|
+
if (phase === "speaking") err(yellow("\n \u270B interrupted\n"));
|
|
10159
|
+
},
|
|
10160
|
+
onUtterance: (text) => {
|
|
10161
|
+
voicePartial = "";
|
|
10162
|
+
if (!text.trim()) return;
|
|
10163
|
+
const cut = voiceIO.takeInterruptedReply();
|
|
10164
|
+
const note = cut && cut.full.length - cut.heard.length > 40 ? `
|
|
9920
10165
|
[the user interrupted you mid-speech \u2014 they only heard up to: "\u2026${cut.heard.slice(-80)}". Work any unheard essentials into your reply naturally, only if still relevant.]` : "";
|
|
9921
|
-
|
|
9922
|
-
|
|
10166
|
+
if (!/^[!#/]/.test(text.trim())) voiceIO.beginSpeech(true);
|
|
10167
|
+
err(`\r\x1B[K ${bold(cyan("\u{1F3A4} \u203A"))} ${text}
|
|
9923
10168
|
`);
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
9931
|
-
|
|
9932
|
-
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
for (const sig of ["SIGHUP", "SIGTERM"]) process.on(sig, () => {
|
|
9936
|
-
voiceIO?.stop();
|
|
9937
|
-
process.exit(0);
|
|
9938
|
-
});
|
|
9939
|
-
err(dim(` \u{1F3A4} voice on (${voiceIO.usingAec ? "echo-cancelled" : "heuristic echo \u2014 headphones recommended"}) \u2014 just talk; speak over it to interrupt
|
|
10169
|
+
void dispatchLine(text + note).then(async (r) => {
|
|
10170
|
+
if (r === "quit") {
|
|
10171
|
+
await voiceIO?.awaitIdle();
|
|
10172
|
+
editorRef?.abort();
|
|
10173
|
+
}
|
|
10174
|
+
}).finally(() => editorRef?.redrawNow());
|
|
10175
|
+
}
|
|
10176
|
+
});
|
|
10177
|
+
try {
|
|
10178
|
+
await voiceIO.start();
|
|
10179
|
+
err(dim(` \u{1F3A4} voice on (${voiceIO.usingAec ? "echo-cancelled" : "heuristic echo \u2014 headphones recommended"}) \u2014 just talk; speak over it to interrupt
|
|
9940
10180
|
`));
|
|
10181
|
+
if (greet) {
|
|
9941
10182
|
const where = cwd.split("/").pop();
|
|
9942
10183
|
const resumed = session.messages.length > 0;
|
|
9943
10184
|
void turn(
|
|
9944
10185
|
`[session started] First call QuickLook with what:"memory" \u2014 if it knows the user's name or preferences, use them. Then greet the user warmly in one or two short sentences, as the opener of a live voice conversation. Context: working directory "${where}"${resumed ? "; this resumes an earlier conversation \u2014 glance at it and pick up naturally" : ""}. Personalize from whatever you learned (memory, prior conversation). Then ask what they'd like to do.`
|
|
9945
10186
|
).finally(() => editorRef?.redrawNow());
|
|
9946
|
-
} catch (e) {
|
|
9947
|
-
err(yellow(` \u26A0 voice I/O failed to start: ${e?.message ?? e} \u2014 continuing text-only
|
|
9948
|
-
`));
|
|
9949
|
-
voiceIO = void 0;
|
|
9950
10187
|
}
|
|
10188
|
+
return true;
|
|
10189
|
+
} catch (e) {
|
|
10190
|
+
err(yellow(` \u26A0 voice I/O failed to start: ${e?.message ?? e} \u2014 continuing text-only
|
|
10191
|
+
`));
|
|
10192
|
+
voiceIO = void 0;
|
|
10193
|
+
return false;
|
|
9951
10194
|
}
|
|
10195
|
+
};
|
|
10196
|
+
if (duplex && process.stdin.isTTY) {
|
|
10197
|
+
process.on("exit", () => voiceIO?.stop());
|
|
10198
|
+
for (const sig of ["SIGHUP", "SIGTERM"]) process.on(sig, () => {
|
|
10199
|
+
voiceIO?.stop();
|
|
10200
|
+
process.exit(0);
|
|
10201
|
+
});
|
|
9952
10202
|
}
|
|
10203
|
+
if (duplex && process.stdin.isTTY) toggleVoice = async () => {
|
|
10204
|
+
if (voiceIO) {
|
|
10205
|
+
voiceIO.stop();
|
|
10206
|
+
voiceIO = void 0;
|
|
10207
|
+
voicePartial = "";
|
|
10208
|
+
err(dim(" \u{1F507} voice off\n"));
|
|
10209
|
+
editorRef?.redrawNow();
|
|
10210
|
+
return;
|
|
10211
|
+
}
|
|
10212
|
+
await startVoice(false);
|
|
10213
|
+
editorRef?.redrawNow();
|
|
10214
|
+
};
|
|
10215
|
+
if (args.voice && duplex && process.stdin.isTTY) await startVoice(true);
|
|
9953
10216
|
while (true) {
|
|
9954
10217
|
if (pendingRewind) {
|
|
9955
10218
|
pendingRewind = false;
|
|
@@ -10084,6 +10347,7 @@ ${extra}` : body);
|
|
|
10084
10347
|
if (forced) {
|
|
10085
10348
|
voiceIO?.stop();
|
|
10086
10349
|
releaseStdin();
|
|
10350
|
+
disposeCursorSessions();
|
|
10087
10351
|
await closeMcp(mounted);
|
|
10088
10352
|
process.exit(130);
|
|
10089
10353
|
}
|
|
@@ -10092,6 +10356,7 @@ ${extra}` : body);
|
|
|
10092
10356
|
}
|
|
10093
10357
|
}
|
|
10094
10358
|
releaseStdin();
|
|
10359
|
+
disposeCursorSessions();
|
|
10095
10360
|
await closeMcp(mounted);
|
|
10096
10361
|
}
|
|
10097
10362
|
function readAllStdin() {
|
|
@@ -10168,8 +10433,8 @@ async function main() {
|
|
|
10168
10433
|
const { ok, res } = await runTurn(agent, store, session, args.task, void 0, cwd);
|
|
10169
10434
|
if (cfg.reflectOnFailure && !ok && res && agent.options.memoryDir) {
|
|
10170
10435
|
const _fsBase = agent.options.fs.getCwd() === "/" ? "" : agent.options.fs.getCwd();
|
|
10171
|
-
const
|
|
10172
|
-
if (
|
|
10436
|
+
const slug2 = await reflectOnRun({ ai, model: agent.options.model, fs: agent.options.fs, dir: primaryMemDir(agent.options.memoryDir, `${_fsBase}/.agent/memory`), result: res });
|
|
10437
|
+
if (slug2) err(dim(` \u270E learned a lesson \u2192 ${slug2}
|
|
10173
10438
|
`));
|
|
10174
10439
|
}
|
|
10175
10440
|
await closeMcp(mounted);
|