agent.libx.js 0.92.8 → 0.93.1
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 +147 -18
- package/dist/{Agent-QwBA0wu6.d.ts → Agent-B_xvSHlG.d.ts} +7 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +414 -130
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +45 -22
- package/dist/index.js +207 -101
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1974,34 +1974,61 @@ async function loadCommands(fs, dir, opts = {}) {
|
|
|
1974
1974
|
// src/memory.ts
|
|
1975
1975
|
init_tools_structured();
|
|
1976
1976
|
var MAX_INDEX = 25;
|
|
1977
|
+
var MAX_INDEX_LINES = 200;
|
|
1978
|
+
var MAX_INDEX_BYTES = 25e3;
|
|
1979
|
+
var MEMORY_PROMPT = '## Memory guidelines\n**When to Remember:** user corrections ("no, do X instead"), preferences ("I prefer Y"), project constraints, recurring gotchas, role/context ("I\'m a data scientist"), workflow patterns confirmed by the user. Capture the WHY, not just the what.\n**When NOT to Remember:** task-specific scratch, transient state, things derivable from code/git, conversation filler, anything already in instruction files. If removing the memory wouldn\'t change future behavior, don\'t save it.\n**Types** (pass to Remember \u2014 this determines where the memory is stored):\n- `user` \u2014 role, preferences, knowledge level, collaboration style (GLOBAL \u2014 follows you across projects)\n- `feedback` \u2014 corrections, what to avoid/repeat, with WHY (GLOBAL)\n- `project` \u2014 ongoing work, goals, constraints, deadlines (LOCAL to this project; convert relative dates to absolute)\n- `reference` \u2014 pointers to external resources, URLs, vendor docs (LOCAL to this project)\n**Before acting on a recalled memory:** verify it\'s still true \u2014 a memory naming a file/function/flag is a claim from when it was written. Grep/read to confirm before recommending. Stale memory \u2192 update via Remember (same slug overwrites).\n**Dedup:** before writing, check if a similar memory exists (Recall or MemorySearch). Update the existing one rather than creating duplicates.\nCall `Recall` with a slug (or multiple slugs/a pattern) to load full bodies when relevant.\nCall `MemorySearch` with a query to find memories by content when you don\'t know the slug.\nCall `Remember` to persist a durable fact for future sessions.';
|
|
1980
|
+
var VOICE_MEMORY_PROMPT = `You have Remember and Recall tools \u2014 use them directly, no delegation needed.
|
|
1981
|
+
IMPLICIT CAPTURE: when the user shares their name, role, a preference, a correction ("no, do X instead"), or a project constraint \u2014 call Remember immediately without announcing it. Natural memory, not a ceremony.
|
|
1982
|
+
For explicit "remember X" requests, also call Remember directly and confirm briefly ("got it").
|
|
1983
|
+
Do NOT remember: transient task details, conversation filler, things you'd forget in a real conversation.
|
|
1984
|
+
Keep it invisible: never announce "saving to memory" or list what you remembered unless asked.
|
|
1985
|
+
For anything requiring files, shell, or web \u2014 still Delegate.`;
|
|
1977
1986
|
async function loadMemory(fs, dir, opts = {}) {
|
|
1978
|
-
const
|
|
1979
|
-
const
|
|
1980
|
-
const
|
|
1981
|
-
|
|
1982
|
-
const
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1987
|
+
const dirs = (Array.isArray(dir) ? dir : [dir]).filter(Boolean);
|
|
1988
|
+
const writeDir = dirs[0];
|
|
1989
|
+
const tools = [recallTool(fs, dirs), rememberTool(fs, writeDir, opts), memorySearchTool(fs, dirs)];
|
|
1990
|
+
const allPointers = [];
|
|
1991
|
+
const seenSlugs = /* @__PURE__ */ new Set();
|
|
1992
|
+
let header = "";
|
|
1993
|
+
let hasContent = false;
|
|
1994
|
+
for (const d of dirs) {
|
|
1995
|
+
const indexPath = `${d}/MEMORY.md`;
|
|
1996
|
+
const md = await fs.exists(indexPath) ? (await fs.readFile(indexPath)).trim() : "";
|
|
1997
|
+
if (!md) continue;
|
|
1998
|
+
hasContent = true;
|
|
1999
|
+
const lines = md.split("\n");
|
|
2000
|
+
if (!header) header = lines.filter((l) => !/^\s*-\s*\[.+\]\(.+\.md\)/.test(l)).join("\n").trim();
|
|
2001
|
+
for (const l of lines.filter((l2) => /^\s*-\s*\[.+\]\(.+\.md\)/.test(l2))) {
|
|
2002
|
+
const slug = l.match(/\]\(([^)]+)\.md\)/)?.[1];
|
|
2003
|
+
if (slug && !seenSlugs.has(slug)) {
|
|
2004
|
+
seenSlugs.add(slug);
|
|
2005
|
+
allPointers.push(l);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
if (!hasContent) return { index: MEMORY_PROMPT, tools };
|
|
2010
|
+
const { kept, rest } = topByRelevance(allPointers, opts.relevanceHint ?? "", (l) => l, opts.max ?? MAX_INDEX);
|
|
1986
2011
|
const restSlugs = rest.map((l) => l.match(/\]\(([^)]+)\.md\)/)?.[1] ?? l.match(/\[([^\]]+)\]/)?.[1] ?? "").filter(Boolean);
|
|
1987
|
-
const index = "## Memory (persistent context \u2014 recalled across sessions)\n" + (header ? header + "\n" : "") + kept.join("\n") + (restSlugs.length ? `
|
|
1988
|
-
- (${restSlugs.length} more learnings, slugs only \u2014 call \`Recall\` if relevant): ${restSlugs.join(", ")}` : "")
|
|
1989
|
-
|
|
1990
|
-
These are pointers only. Call \`Recall\` with a slug (or multiple slugs/a pattern) to load full bodies when relevant.
|
|
1991
|
-
Call \`MemorySearch\` with a query to find memories by content when you don't know the slug.
|
|
1992
|
-
Call \`Remember\` to persist a durable fact for future sessions.`;
|
|
2012
|
+
const index = MEMORY_PROMPT + "\n\n## Memory index (persistent context \u2014 recalled across sessions)\n" + (header ? header + "\n" : "") + kept.join("\n") + (restSlugs.length ? `
|
|
2013
|
+
- (${restSlugs.length} more learnings, slugs only \u2014 call \`Recall\` if relevant): ${restSlugs.join(", ")}` : "");
|
|
1993
2014
|
return { index, tools };
|
|
1994
2015
|
}
|
|
1995
2016
|
function slugify(s, fallback = "note") {
|
|
1996
2017
|
const base = String(s ?? "").trim().toLowerCase().replace(/\.md$/i, "").replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 48);
|
|
1997
2018
|
return base || fallback;
|
|
1998
2019
|
}
|
|
1999
|
-
async function writeFact(fs, dir, slug, body) {
|
|
2020
|
+
async function writeFact(fs, dir, slug, body, opts) {
|
|
2000
2021
|
await mkdirp(fs, dir);
|
|
2001
|
-
|
|
2022
|
+
const content = opts?.type ? `---
|
|
2023
|
+
type: ${opts.type}
|
|
2024
|
+
---
|
|
2025
|
+
|
|
2026
|
+
${body}` : body;
|
|
2027
|
+
await fs.writeFile(`${dir}/${slug}.md`, content.endsWith("\n") ? content : content + "\n");
|
|
2002
2028
|
const indexPath = `${dir}/MEMORY.md`;
|
|
2003
2029
|
const idx = await fs.exists(indexPath) ? await fs.readFile(indexPath) : "# Memory Index\n";
|
|
2004
|
-
const
|
|
2030
|
+
const summary = opts?.description || body.split("\n")[0].slice(0, 80);
|
|
2031
|
+
const line = `- [${slug}](${slug}.md) \u2014 ${summary}`;
|
|
2005
2032
|
const lines = idx.split("\n");
|
|
2006
2033
|
const at = lines.findIndex((l) => l.includes(`(${slug}.md)`));
|
|
2007
2034
|
if (at >= 0) {
|
|
@@ -2014,6 +2041,24 @@ async function writeFact(fs, dir, slug, body) {
|
|
|
2014
2041
|
${line}
|
|
2015
2042
|
`);
|
|
2016
2043
|
}
|
|
2044
|
+
await truncateIndex(fs, indexPath);
|
|
2045
|
+
}
|
|
2046
|
+
async function truncateIndex(fs, path) {
|
|
2047
|
+
const raw = await fs.readFile(path);
|
|
2048
|
+
if (raw.length <= MAX_INDEX_BYTES) {
|
|
2049
|
+
const count = raw.split("\n").filter((l) => /^\s*-\s*\[.+\]\(.+\.md\)/.test(l)).length;
|
|
2050
|
+
if (count <= MAX_INDEX_LINES) return;
|
|
2051
|
+
}
|
|
2052
|
+
const lines = raw.split("\n");
|
|
2053
|
+
const pointerIdxs = [];
|
|
2054
|
+
for (let i = 0; i < lines.length; i++) if (/^\s*-\s*\[.+\]\(.+\.md\)/.test(lines[i])) pointerIdxs.push(i);
|
|
2055
|
+
const drop = /* @__PURE__ */ new Set();
|
|
2056
|
+
for (const pi of pointerIdxs) {
|
|
2057
|
+
const candidate = lines.filter((_, i) => !drop.has(i)).join("\n");
|
|
2058
|
+
if (candidate.length <= MAX_INDEX_BYTES && pointerIdxs.length - drop.size <= MAX_INDEX_LINES) break;
|
|
2059
|
+
drop.add(pi);
|
|
2060
|
+
}
|
|
2061
|
+
if (drop.size) await fs.writeFile(path, lines.filter((_, i) => !drop.has(i)).join("\n"));
|
|
2017
2062
|
}
|
|
2018
2063
|
function cleanSlug(raw) {
|
|
2019
2064
|
let s = String(raw ?? "").trim().replace(/\.md$/i, "").replace(/\\/g, "/").replace(/\/+/g, "/").replace(/^\/|\/$/g, "");
|
|
@@ -2039,12 +2084,30 @@ async function listSlugs(fs, dir, prefix = "") {
|
|
|
2039
2084
|
async function loadFact(fs, dir, slug) {
|
|
2040
2085
|
const path = `${dir}/${slug}.md`;
|
|
2041
2086
|
try {
|
|
2042
|
-
|
|
2087
|
+
const raw = await fs.readFile(path);
|
|
2088
|
+
const { body } = splitFrontmatter(raw);
|
|
2089
|
+
return body || raw;
|
|
2043
2090
|
} catch {
|
|
2044
2091
|
return null;
|
|
2045
2092
|
}
|
|
2046
2093
|
}
|
|
2047
|
-
function
|
|
2094
|
+
async function loadFactMulti(fs, dirs, slug) {
|
|
2095
|
+
for (const d of dirs) {
|
|
2096
|
+
const r = await loadFact(fs, d, slug);
|
|
2097
|
+
if (r != null) return r;
|
|
2098
|
+
}
|
|
2099
|
+
return null;
|
|
2100
|
+
}
|
|
2101
|
+
async function listSlugsMulti(fs, dirs) {
|
|
2102
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2103
|
+
const out = [];
|
|
2104
|
+
for (const d of dirs) for (const s of await listSlugs(fs, d)) if (!seen.has(s)) {
|
|
2105
|
+
seen.add(s);
|
|
2106
|
+
out.push(s);
|
|
2107
|
+
}
|
|
2108
|
+
return out;
|
|
2109
|
+
}
|
|
2110
|
+
function recallTool(fs, dirs) {
|
|
2048
2111
|
return {
|
|
2049
2112
|
name: "Recall",
|
|
2050
2113
|
description: 'Load memory facts by slug. Pass `slug` for one, `slugs` for several, or `pattern` (glob like "auth*") to match. Returns full bodies, separated by `--- slug ---` headers.',
|
|
@@ -2063,12 +2126,12 @@ function recallTool(fs, dir) {
|
|
|
2063
2126
|
else if (pattern) {
|
|
2064
2127
|
const escaped = String(pattern).replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
2065
2128
|
const re = new RegExp("^" + escaped.replace(/\*/g, ".*").replace(/\?/g, ".") + "$", "i");
|
|
2066
|
-
targets = (await
|
|
2129
|
+
targets = (await listSlugsMulti(fs, dirs)).filter((s) => re.test(s));
|
|
2067
2130
|
}
|
|
2068
2131
|
if (!targets.length) return `Error: no slugs resolved. Pass slug, slugs, or pattern.`;
|
|
2069
2132
|
const parts = [];
|
|
2070
2133
|
for (const s of targets.slice(0, 20)) {
|
|
2071
|
-
const body = await
|
|
2134
|
+
const body = await loadFactMulti(fs, dirs, s);
|
|
2072
2135
|
if (body != null) parts.push(targets.length > 1 ? `--- ${s} ---
|
|
2073
2136
|
${body}` : body);
|
|
2074
2137
|
else parts.push(targets.length > 1 ? `--- ${s} ---
|
|
@@ -2079,7 +2142,7 @@ ${body}` : body);
|
|
|
2079
2142
|
}
|
|
2080
2143
|
};
|
|
2081
2144
|
}
|
|
2082
|
-
function memorySearchTool(fs,
|
|
2145
|
+
function memorySearchTool(fs, dirs) {
|
|
2083
2146
|
return {
|
|
2084
2147
|
name: "MemorySearch",
|
|
2085
2148
|
description: "Search memory facts by content \u2014 find relevant memories when you don't know the exact slug. Returns up to 10 matching slug + snippet pairs, ranked by relevance. Use `regex: true` for regex patterns.",
|
|
@@ -2094,7 +2157,7 @@ function memorySearchTool(fs, dir) {
|
|
|
2094
2157
|
async run({ query, regex }, ctx) {
|
|
2095
2158
|
const q2 = String(query ?? "").trim();
|
|
2096
2159
|
if (!q2) return "Error: empty query.";
|
|
2097
|
-
const slugs = await
|
|
2160
|
+
const slugs = await listSlugsMulti(fs, dirs);
|
|
2098
2161
|
if (!slugs.length) return "(no memory facts found)";
|
|
2099
2162
|
let matcher;
|
|
2100
2163
|
if (regex) {
|
|
@@ -2110,7 +2173,7 @@ function memorySearchTool(fs, dir) {
|
|
|
2110
2173
|
}
|
|
2111
2174
|
const loaded = [];
|
|
2112
2175
|
for (const slug of slugs) {
|
|
2113
|
-
const body = await
|
|
2176
|
+
const body = await loadFactMulti(fs, dirs, slug);
|
|
2114
2177
|
if (body) loaded.push({ slug, body });
|
|
2115
2178
|
}
|
|
2116
2179
|
const idf = idfWeights(loaded.map((l) => l.body));
|
|
@@ -2129,7 +2192,9 @@ function memorySearchTool(fs, dir) {
|
|
|
2129
2192
|
}
|
|
2130
2193
|
};
|
|
2131
2194
|
}
|
|
2132
|
-
function rememberTool(fs, dir) {
|
|
2195
|
+
function rememberTool(fs, dir, memOpts = {}) {
|
|
2196
|
+
const maxWrites = memOpts.maxWritesPerSession ?? 25;
|
|
2197
|
+
let writes = 0;
|
|
2133
2198
|
return {
|
|
2134
2199
|
name: "Remember",
|
|
2135
2200
|
description: "Persist a durable fact for future sessions (a fix you found, a gotcha, a project constraint). Adds a pointer to the Memory index and stores the body. Use sparingly \u2014 only genuinely reusable knowledge, not task-specific scratch.",
|
|
@@ -2138,14 +2203,20 @@ function rememberTool(fs, dir) {
|
|
|
2138
2203
|
required: ["fact"],
|
|
2139
2204
|
properties: {
|
|
2140
2205
|
fact: { type: "string", description: "the durable fact to remember (one or more lines)" },
|
|
2141
|
-
slug: { type: "string", description: "optional kebab-case id; derived from the fact if omitted" }
|
|
2206
|
+
slug: { type: "string", description: "optional kebab-case id; derived from the fact if omitted" },
|
|
2207
|
+
type: { type: "string", enum: ["user", "feedback", "project", "reference"], description: "memory category (user/feedback/project/reference)" },
|
|
2208
|
+
description: { type: "string", description: "one-line summary for the memory index (\u226480 chars)" }
|
|
2142
2209
|
}
|
|
2143
2210
|
},
|
|
2144
|
-
async run({ fact, slug }, ctx) {
|
|
2211
|
+
async run({ fact, slug, type, description }, ctx) {
|
|
2145
2212
|
const body = String(fact ?? "").trim();
|
|
2146
2213
|
if (!body) return `Error: nothing to remember (empty fact).`;
|
|
2214
|
+
if (++writes > maxWrites) return `Rate limit: too many memories this session (${maxWrites}). Only persist genuinely durable facts.`;
|
|
2147
2215
|
const name = slugify(slug || body.split("\n")[0]);
|
|
2148
|
-
|
|
2216
|
+
const isGlobal = (type === "user" || type === "feedback") && memOpts.userDir;
|
|
2217
|
+
const targetDir = isGlobal ? memOpts.userDir : dir;
|
|
2218
|
+
await writeFact(fs, targetDir, name, body, { type, description });
|
|
2219
|
+
memOpts.onMemorySaved?.(name, type);
|
|
2149
2220
|
return `Remembered '${name}' (recallable in future sessions).`;
|
|
2150
2221
|
}
|
|
2151
2222
|
};
|
|
@@ -2579,8 +2650,11 @@ var AgentOptions = class {
|
|
|
2579
2650
|
skillsDir;
|
|
2580
2651
|
/** 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). */
|
|
2581
2652
|
commandsDir;
|
|
2582
|
-
/** VFS dir of memory (`<dir>/MEMORY.md`). If set: inject the index at run start (persistence = backend).
|
|
2653
|
+
/** VFS dir(s) of memory (`<dir>/MEMORY.md`). If set: inject the index at run start (persistence = backend).
|
|
2654
|
+
* Multiple dirs are merged (reads search all; writes go to first). */
|
|
2583
2655
|
memoryDir;
|
|
2656
|
+
/** User-scope memory dir for global facts (type=user/feedback). Remember routes by type when set. */
|
|
2657
|
+
memoryUserDir;
|
|
2584
2658
|
/** Filenames to discover as project instructions (e.g. `AGENT.md`, `AGENTS.md`, `CLAUDE.md`).
|
|
2585
2659
|
* Walks the VFS tree and merges all found files (general → specific, like Claude Code).
|
|
2586
2660
|
* `true` (default) = auto-discover standard names. `string[]` = custom names. `false` = skip. */
|
|
@@ -2617,6 +2691,8 @@ var AgentOptions = class {
|
|
|
2617
2691
|
autoTest;
|
|
2618
2692
|
/** Provider-specific options forwarded to ai.chat() (e.g. cursor mcpServers, cwd). */
|
|
2619
2693
|
providerOptions;
|
|
2694
|
+
/** Tool selection mode: 'auto' = model decides (needed for Groq); undefined = provider default. */
|
|
2695
|
+
toolChoice;
|
|
2620
2696
|
/** Extended-thinking / reasoning effort, normalized across providers (anthropic, openai).
|
|
2621
2697
|
* `'off'`/undefined = none; `'low'|'medium'|'high'` or a raw token budget. Mapped to the
|
|
2622
2698
|
* provider-specific request shape via {@link reasoningToChatFragment}; explicit `providerOptions` wins. */
|
|
@@ -2703,7 +2779,7 @@ var Agent = class _Agent {
|
|
|
2703
2779
|
if (ins) systemPrompt += "\n\n" + ins;
|
|
2704
2780
|
}
|
|
2705
2781
|
if (o.memoryDir) {
|
|
2706
|
-
const { index, tools: memTools } = await loadMemory(fs, o.memoryDir, { relevanceHint: taskHint });
|
|
2782
|
+
const { index, tools: memTools } = await loadMemory(fs, o.memoryDir, { relevanceHint: taskHint, userDir: o.memoryUserDir });
|
|
2707
2783
|
if (index) systemPrompt += "\n\n" + index;
|
|
2708
2784
|
tools = [...tools, ...memTools];
|
|
2709
2785
|
}
|
|
@@ -2830,10 +2906,10 @@ var Agent = class _Agent {
|
|
|
2830
2906
|
};
|
|
2831
2907
|
try {
|
|
2832
2908
|
if (useStream) {
|
|
2833
|
-
const r = await o.ai.chat({ model: o.model, messages: sent, tools: wireTools, stream: true, signal: o.signal, ...reasonOpts });
|
|
2909
|
+
const r = await o.ai.chat({ model: o.model, messages: sent, tools: wireTools, stream: true, signal: o.signal, ...o.toolChoice ? { toolChoice: o.toolChoice } : {}, ...reasonOpts });
|
|
2834
2910
|
res = await this.consumeStream(r);
|
|
2835
2911
|
} else {
|
|
2836
|
-
const r = await o.ai.chat({ model: o.model, messages: sent, tools: wireTools, stream: false, signal: o.signal, ...reasonOpts });
|
|
2912
|
+
const r = await o.ai.chat({ model: o.model, messages: sent, tools: wireTools, stream: false, signal: o.signal, ...o.toolChoice ? { toolChoice: o.toolChoice } : {}, ...reasonOpts });
|
|
2837
2913
|
res = r;
|
|
2838
2914
|
}
|
|
2839
2915
|
} catch (err2) {
|
|
@@ -3454,8 +3530,13 @@ var DuplexAgentOptions = class {
|
|
|
3454
3530
|
/** Host overrides for QuickLook lookups (keyed by `what`). The engine's defaults go through the
|
|
3455
3531
|
* (possibly jailed) fs — e.g. `.git/**` is deny-listed, so the CLI supplies 'branch' itself. */
|
|
3456
3532
|
quickLook;
|
|
3533
|
+
/** Memory directory/directories on the WORKER fs. If set, the voice agent gets Remember + Recall
|
|
3534
|
+
* tools directly (no delegation needed) and implicit capture guidance. */
|
|
3535
|
+
memoryDir;
|
|
3536
|
+
/** User-scope memory dir for global facts (type=user/feedback). Forwarded to Remember's routing. */
|
|
3537
|
+
memoryUserDir;
|
|
3457
3538
|
};
|
|
3458
|
-
var VOICE_SYSTEM_PROMPT = 'You are a spoken voice assistant \u2014 the user HEARS everything you say. Use short sentences. One idea per sentence. No markdown, no bullet lists, no code blocks, no headings, no emoji.\nKeep turns SHORT \u2014 one to three sentences, then stop. Never lecture, enumerate cases, or add caveats unprompted. Conversation is a fast exchange: give the one thing asked, and let the user pull more if they want it.\nYou work in a pair: you talk, and a background worker with FULL access to the user\'s environment (files, shell, web) does the hands-on work. You can find out or do ANYTHING by calling `Delegate` with a clear, self-contained brief \u2014 so NEVER tell the user you can\'t see, access, or do something. Delegate and find out. When the user mentions their project, folder, files, or environment ("this project", "the current folder", "my code"), delegate IMMEDIATELY \u2014 do not ask for paths or details the worker can discover itself. Never pretend to have done the work or invent results \u2014 the worker\'s report is your only source.\nAfter calling Delegate, tell the user you are on it in one short sentence, then end your turn. Do not wait for the result.\nResults arrive later as events like "[task t1 completed] \u2026" or "[task t1 failed] \u2026". When one arrives, summarize it for the ear in one or two short sentences. "[task t1 progress] \u2026" events are interim status, NOT results \u2014 give at most a half-sentence aside ("still on it \u2014 running tests now") and end your turn. Never present progress as a finished result.\nNever read raw file paths, diffs, or code aloud verbatim.\n"[task t1 asks] \u2026" events are QUESTIONS from a background task \u2014 relay to the user in your own words, short, then end your turn. When the user answers, call `AnswerTask` with that id and their answer. NEVER answer on the user\'s behalf for permissions or risky operations; if their reply is ambiguous, confirm first.\nDo not fire a second Delegate for work already in flight \u2014 check `TaskStatus` first. Use `CancelTask` when the user asks to stop something.\nPRIORITY: when the user says goodbye or wants to end/finish/wrap up the session ("ok bye", "that\'s all", "let\'s finish", "let\'s end", "goodnight", "exit", "wrap up"), call `ExitSession` IMMEDIATELY \u2014 do not delegate, do not check status, just exit.\nFor TRIVIAL instant lookups only \u2014 current time, git branch, listing a folder, peeking at a small file \u2014 use `QuickLook` (instant, no task). Anything requiring searching, reasoning, running commands, or editing still goes through `Delegate`.\
|
|
3539
|
+
var VOICE_SYSTEM_PROMPT = 'You are a spoken voice assistant \u2014 the user HEARS everything you say. Use short sentences. One idea per sentence. No markdown, no bullet lists, no code blocks, no headings, no emoji.\nKeep turns SHORT \u2014 one to three sentences, then stop. Never lecture, enumerate cases, or add caveats unprompted. Conversation is a fast exchange: give the one thing asked, and let the user pull more if they want it.\nYou work in a pair: you talk, and a background worker with FULL access to the user\'s environment (files, shell, web) does the hands-on work. You can find out or do ANYTHING by calling `Delegate` with a clear, self-contained brief \u2014 so NEVER tell the user you can\'t see, access, or do something. Delegate and find out. When the user mentions their project, folder, files, or environment ("this project", "the current folder", "my code"), delegate IMMEDIATELY \u2014 do not ask for paths or details the worker can discover itself. Never pretend to have done the work or invent results \u2014 the worker\'s report is your only source.\nAfter calling Delegate, tell the user you are on it in one short sentence, then end your turn. Do not wait for the result.\nResults arrive later as events like "[task t1 completed] \u2026" or "[task t1 failed] \u2026". When one arrives, summarize it for the ear in one or two short sentences. "[task t1 progress] \u2026" events are interim status, NOT results \u2014 give at most a half-sentence aside ("still on it \u2014 running tests now") and end your turn. Never present progress as a finished result.\nNever read raw file paths, diffs, or code aloud verbatim.\n"[task t1 asks] \u2026" events are QUESTIONS from a background task \u2014 relay to the user in your own words, short, then end your turn. When the user answers, call `AnswerTask` with that id and their answer. NEVER answer on the user\'s behalf for permissions or risky operations; if their reply is ambiguous, confirm first.\nDo not fire a second Delegate for work already in flight \u2014 check `TaskStatus` first. Use `CancelTask` when the user asks to stop something.\nPRIORITY: when the user says goodbye or wants to end/finish/wrap up the session ("ok bye", "that\'s all", "let\'s finish", "let\'s end", "goodnight", "exit", "wrap up"), call `ExitSession` IMMEDIATELY \u2014 do not delegate, do not check status, just exit.\nFor TRIVIAL instant lookups only \u2014 current time, git branch, listing a folder, peeking at a small file \u2014 use `QuickLook` (instant, no task). Anything requiring searching, reasoning, running commands, or editing still goes through `Delegate`.\n{{MEMORY_SLOT}}\nUser messages may arrive via speech-to-text and can carry transcription artifacts \u2014 odd words, cut-offs, homophones ("for you" vs "folder"). Read for INTENT, not surface text. If a message seems garbled or surprising, briefly confirm what they meant ("did you mean\u2026?") instead of answering the literal words.';
|
|
3459
3540
|
var VOICE_STYLE_CONVERSATIONAL = `Speak like a person in a live conversation, not an assistant reading a script. React first, then deliver: a quick impulsive beat ("oh nice", "hmm, hold on", "ah, got it") before the substance. Use contractions always. Vary sentence length \u2014 some very short. Light fillers and backchannels are fine ("mm-hm", "right", "let's see") but at most one per reply \u2014 never stack them. When you delegate, say it like a human would ("hang on, let me actually dig into that \u2014 gimme a minute") instead of announcing a task. When a result comes back, react to it like you just found out ("okay so \u2014 turns out\u2026"). Match the user's energy: a quick question gets a quick answer \u2014 a few words is a perfectly good turn. Prefer a short answer plus an offer ("want the details?") over covering everything. Never narrate your own mechanics (no "I will now delegate", no task ids out loud).`;
|
|
3460
3541
|
var DuplexAgent = class {
|
|
3461
3542
|
options;
|
|
@@ -3467,34 +3548,45 @@ var DuplexAgent = class {
|
|
|
3467
3548
|
flushQueued = false;
|
|
3468
3549
|
/** Parked worker questions awaiting a (voice-relayed) user answer, keyed by ask id. */
|
|
3469
3550
|
pendingAsks = /* @__PURE__ */ new Map();
|
|
3551
|
+
/** Lazily resolved memory tools (async loadMemory runs in initMemory). */
|
|
3552
|
+
memoryReady;
|
|
3470
3553
|
constructor(options) {
|
|
3471
3554
|
this.options = { ...new DuplexAgentOptions(), ...options };
|
|
3472
3555
|
const o = this.options;
|
|
3556
|
+
if (o.memoryDir && o.fs) {
|
|
3557
|
+
this.memoryReady = loadMemory(o.fs, o.memoryDir, { maxWritesPerSession: 10, userDir: o.memoryUserDir });
|
|
3558
|
+
}
|
|
3559
|
+
const memSlot = o.memoryDir && o.fs ? VOICE_MEMORY_PROMPT : "NEVER claim to have stored, saved, or remembered something durably \u2014 you cannot. Anything the user wants persisted (their name, preferences, notes) must be Delegated so a worker writes it to memory.";
|
|
3560
|
+
const prompt = VOICE_SYSTEM_PROMPT.replace("{{MEMORY_SLOT}}", memSlot) + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : "") + `
|
|
3561
|
+
Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
3473
3562
|
this.voice = new Agent({
|
|
3474
3563
|
ai: o.ai,
|
|
3475
3564
|
fs: new MemFilesystem2(),
|
|
3476
|
-
// scratch — NOT Agent's jailed-disk default (voice has no fs tools; edge-safe)
|
|
3477
3565
|
model: o.voiceModel,
|
|
3478
3566
|
stream: true,
|
|
3479
3567
|
host: o.host,
|
|
3480
|
-
|
|
3481
|
-
// (its training cutoff) instead of delegating or admitting it doesn't know.
|
|
3482
|
-
systemPrompt: VOICE_SYSTEM_PROMPT + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : "") + `
|
|
3483
|
-
Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`,
|
|
3568
|
+
systemPrompt: prompt,
|
|
3484
3569
|
instructionFiles: false,
|
|
3485
3570
|
maxSteps: 8,
|
|
3486
|
-
// a voice turn should never loop
|
|
3487
3571
|
timeoutMs: 3e4,
|
|
3488
3572
|
...o.voiceOptions,
|
|
3489
|
-
// no defaultTools() — the voice can only Delegate, never touch files itself. Set AFTER the
|
|
3490
|
-
// voiceOptions spread (addTools() would be clobbered by the first prepare()); extra voice
|
|
3491
|
-
// tools come in via voiceOptions.tools and are merged here.
|
|
3492
3573
|
tools: [...o.voiceOptions?.tools ?? [], this.delegateTool(), this.taskStatusTool(), this.cancelTaskTool(), this.quickLookTool(), this.answerTaskTool()]
|
|
3493
3574
|
});
|
|
3494
3575
|
}
|
|
3576
|
+
/** Resolve memory tools + inject index into voice system prompt (once). */
|
|
3577
|
+
async initMemory() {
|
|
3578
|
+
if (!this.memoryReady) return;
|
|
3579
|
+
const mem = await this.memoryReady;
|
|
3580
|
+
this.memoryReady = void 0;
|
|
3581
|
+
this.voice.options.tools.push(...mem.tools);
|
|
3582
|
+
if (mem.index) this.voice.options.systemPrompt += "\n\n" + mem.index;
|
|
3583
|
+
}
|
|
3495
3584
|
/** One user turn: the voice agent streams the reply (and may Delegate). Serialized with re-voice turns. */
|
|
3496
3585
|
send(content) {
|
|
3497
|
-
return this.enqueue(() =>
|
|
3586
|
+
return this.enqueue(async () => {
|
|
3587
|
+
await this.initMemory();
|
|
3588
|
+
return this.voice.send(content);
|
|
3589
|
+
});
|
|
3498
3590
|
}
|
|
3499
3591
|
/** Resolve when all queued voice turns AND all in-flight worker tasks have settled (tests, graceful shutdown). */
|
|
3500
3592
|
async idle() {
|
|
@@ -3589,6 +3681,7 @@ ${recent}` : brief;
|
|
|
3589
3681
|
let steps = 0;
|
|
3590
3682
|
let inflight = null;
|
|
3591
3683
|
const due = () => {
|
|
3684
|
+
if (this.pendingAsks.size) return void 0;
|
|
3592
3685
|
const rec = this.tasks.get(id);
|
|
3593
3686
|
return rec && rec.status === "running" && Date.now() - lastAt >= this.options.progressIntervalMs ? rec : void 0;
|
|
3594
3687
|
};
|
|
@@ -3866,18 +3959,15 @@ var VoiceEngineOptions = class {
|
|
|
3866
3959
|
/** heuristic (non-AEC) energy barge-in tuning */
|
|
3867
3960
|
bargeRmsMult = 2;
|
|
3868
3961
|
bargeRmsFloor = 500;
|
|
3869
|
-
/** Overlap turn-taking (AEC tier, needs player.pause/resume) — human phone-call model
|
|
3870
|
-
*
|
|
3871
|
-
*
|
|
3872
|
-
*
|
|
3962
|
+
/** Overlap turn-taking (AEC tier, needs player.pause/resume) — human phone-call model, driven by
|
|
3963
|
+
* the STT ITSELF (a trained speech classifier) instead of energy thresholds (energy could not
|
|
3964
|
+
* separate residue bursts from speech in every room — hiccup whack-a-mole): partial text while
|
|
3965
|
+
* speaking → PAUSE (exact-sample hold); partial grows into dominant-novel ≥2 words → cede
|
|
3966
|
+
* (interrupt; the LLM re-enters); partial stalls/endpoints without ceding (backchannel by
|
|
3967
|
+
* DURATION, not vocabulary) → resume + drop. false disables. */
|
|
3873
3968
|
overlapPause = true;
|
|
3874
|
-
/**
|
|
3875
|
-
overlapSustainMs = 350;
|
|
3876
|
-
/** quiet for this long while paused → resume, drop the interjection */
|
|
3969
|
+
/** no new partial activity for this long while paused → resume, drop the interjection */
|
|
3877
3970
|
overlapResumeMs = 700;
|
|
3878
|
-
/** energy floor for "overlap candidate" — must sit ABOVE typical room ambient (~110 rms measured;
|
|
3879
|
-
* ungated ambient re-arming the resume timer forever was a live wedge). User speech ≫ 300. */
|
|
3880
|
-
overlapRms = 300;
|
|
3881
3971
|
};
|
|
3882
3972
|
var VoiceEngine = class {
|
|
3883
3973
|
options;
|
|
@@ -3910,10 +4000,8 @@ var VoiceEngine = class {
|
|
|
3910
4000
|
lastInterrupted = null;
|
|
3911
4001
|
// overlap (pause) tier state — AEC + pause-capable sinks only
|
|
3912
4002
|
pausedAt = 0;
|
|
3913
|
-
|
|
3914
|
-
//
|
|
3915
|
-
overlapLastLoudAt = 0;
|
|
3916
|
-
// continuity guard: a gap re-arms the onset (sparse noise ≠ sustained speech)
|
|
4003
|
+
lastOverlapPartial = "";
|
|
4004
|
+
// change-detection: only NEW partial text counts as activity
|
|
3917
4005
|
resumeTimer = null;
|
|
3918
4006
|
constructor(options) {
|
|
3919
4007
|
this.options = { ...new VoiceEngineOptions(), ...options };
|
|
@@ -4077,7 +4165,24 @@ var VoiceEngine = class {
|
|
|
4077
4165
|
}
|
|
4078
4166
|
handlePartial(text) {
|
|
4079
4167
|
if (this.speaking) {
|
|
4080
|
-
|
|
4168
|
+
if (this.overlapCapable) {
|
|
4169
|
+
const txt = text.trim();
|
|
4170
|
+
if (!txt || txt === this.lastOverlapPartial) return;
|
|
4171
|
+
this.lastOverlapPartial = txt;
|
|
4172
|
+
if (!this.pausedAt) {
|
|
4173
|
+
this.pausedAt = now();
|
|
4174
|
+
this.player.pause();
|
|
4175
|
+
}
|
|
4176
|
+
if (this.genuine(txt) && this.words(txt).length >= 2) {
|
|
4177
|
+
const phase = this.ctxOpen ? "speaking" : "drain";
|
|
4178
|
+
this.interrupt();
|
|
4179
|
+
this.options.onBargeIn(phase);
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
4182
|
+
this.armResume();
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
const barge = this.usingAec ? this.genuine(text) : this.novelWords(text).length >= (this.suspectUntil ? 1 : 2);
|
|
4081
4186
|
if (barge) {
|
|
4082
4187
|
const phase = this.ctxOpen ? "speaking" : "drain";
|
|
4083
4188
|
this.interrupt();
|
|
@@ -4086,15 +4191,13 @@ var VoiceEngine = class {
|
|
|
4086
4191
|
return;
|
|
4087
4192
|
}
|
|
4088
4193
|
if (this.pendingUtt && text.trim()) {
|
|
4089
|
-
if (this.pendingTimer)
|
|
4090
|
-
|
|
4091
|
-
this.pendingTimer = null;
|
|
4092
|
-
}
|
|
4194
|
+
if (this.pendingTimer) clearTimeout(this.pendingTimer);
|
|
4195
|
+
this.pendingTimer = setTimeout(() => this.flushUtterance(), Math.max(800, this.options.utteranceMergeMs));
|
|
4093
4196
|
}
|
|
4094
4197
|
if (!this.echoActive() || (this.usingAec ? this.genuine(text) : this.novelWords(text).length >= 1)) this.options.onPartial(text);
|
|
4095
4198
|
}
|
|
4096
4199
|
handleUtterance(text) {
|
|
4097
|
-
if (this.speaking && this.ctxOpen && this.overlapCapable) {
|
|
4200
|
+
if (this.speaking && (this.ctxOpen || this.pausedAt) && this.overlapCapable) {
|
|
4098
4201
|
this.stt.reset();
|
|
4099
4202
|
return;
|
|
4100
4203
|
}
|
|
@@ -4109,7 +4212,7 @@ var VoiceEngine = class {
|
|
|
4109
4212
|
}
|
|
4110
4213
|
this.pendingUtt = this.pendingUtt ? `${this.pendingUtt} ${text}` : text;
|
|
4111
4214
|
if (this.pendingTimer) clearTimeout(this.pendingTimer);
|
|
4112
|
-
if (!this.options.utteranceMergeMs) return this.flushUtterance();
|
|
4215
|
+
if (!this.options.utteranceMergeMs || this.words(this.pendingUtt).length >= 4) return this.flushUtterance();
|
|
4113
4216
|
this.pendingTimer = setTimeout(() => this.flushUtterance(), this.options.utteranceMergeMs);
|
|
4114
4217
|
}
|
|
4115
4218
|
flushUtterance() {
|
|
@@ -4124,45 +4227,12 @@ var VoiceEngine = class {
|
|
|
4124
4227
|
get overlapCapable() {
|
|
4125
4228
|
return this.usingAec && this.options.overlapPause && !!this.player.pause && !!this.player.resume;
|
|
4126
4229
|
}
|
|
4127
|
-
/** Overlap turn-taking (AEC tier): onset → pause (exact-sample hold); sustained → cede; died out
|
|
4128
|
-
* → resume. No vocabulary anywhere — duration and persistence decide (backchannels are short
|
|
4129
|
-
* and stop). Nothing is lost across a pause, so a false positive costs only a brief hold. */
|
|
4130
|
-
handleOverlap(rms) {
|
|
4131
|
-
const o = this.options;
|
|
4132
|
-
if (!this.speaking || !this.overlapCapable) return;
|
|
4133
|
-
if (rms < o.overlapRms) return;
|
|
4134
|
-
const t = now();
|
|
4135
|
-
if (!this.pausedAt) {
|
|
4136
|
-
this.overlapLoud = t - this.overlapLastLoudAt <= 60 ? this.overlapLoud + 1 : 1;
|
|
4137
|
-
this.overlapLastLoudAt = t;
|
|
4138
|
-
if (this.overlapLoud < 3) return;
|
|
4139
|
-
this.pausedAt = t;
|
|
4140
|
-
this.player.pause();
|
|
4141
|
-
this.armResume();
|
|
4142
|
-
return;
|
|
4143
|
-
}
|
|
4144
|
-
if (t - this.overlapLastLoudAt > 300) {
|
|
4145
|
-
this.pausedAt = t;
|
|
4146
|
-
this.overlapLoud = 1;
|
|
4147
|
-
this.overlapLastLoudAt = t;
|
|
4148
|
-
this.armResume();
|
|
4149
|
-
return;
|
|
4150
|
-
}
|
|
4151
|
-
this.overlapLastLoudAt = t;
|
|
4152
|
-
this.overlapLoud++;
|
|
4153
|
-
if (t - this.pausedAt >= o.overlapSustainMs && this.overlapLoud >= 4) {
|
|
4154
|
-
const phase = this.ctxOpen ? "speaking" : "drain";
|
|
4155
|
-
this.interrupt();
|
|
4156
|
-
this.options.onBargeIn(phase);
|
|
4157
|
-
return;
|
|
4158
|
-
}
|
|
4159
|
-
this.armResume();
|
|
4160
|
-
}
|
|
4161
4230
|
armResume() {
|
|
4162
4231
|
if (this.resumeTimer) clearTimeout(this.resumeTimer);
|
|
4163
4232
|
this.resumeTimer = setTimeout(() => {
|
|
4164
4233
|
this.resumeTimer = null;
|
|
4165
4234
|
if (!this.pausedAt) return;
|
|
4235
|
+
this.stt.reset();
|
|
4166
4236
|
this.resetOverlap(true);
|
|
4167
4237
|
}, this.options.overlapResumeMs);
|
|
4168
4238
|
}
|
|
@@ -4173,11 +4243,25 @@ var VoiceEngine = class {
|
|
|
4173
4243
|
}
|
|
4174
4244
|
if (this.pausedAt && resume) this.player.resume?.();
|
|
4175
4245
|
this.pausedAt = 0;
|
|
4176
|
-
this.
|
|
4246
|
+
this.lastOverlapPartial = "";
|
|
4247
|
+
this.gatePassTimes = [];
|
|
4177
4248
|
}
|
|
4178
4249
|
/** energy two-stage barge-in (heuristic tier only): spike over echo baseline → pause + confirm via STT */
|
|
4250
|
+
gatePassTimes = [];
|
|
4251
|
+
// recent gate-PASSING chunks (helper zeroes residue — nonzero = vetted)
|
|
4179
4252
|
handleLevel(rms) {
|
|
4180
|
-
if (this.usingAec)
|
|
4253
|
+
if (this.usingAec) {
|
|
4254
|
+
if (!this.speaking || !this.overlapCapable || this.pausedAt || rms < 50) return;
|
|
4255
|
+
const t = now();
|
|
4256
|
+
this.gatePassTimes = this.gatePassTimes.filter((x) => t - x < 350);
|
|
4257
|
+
this.gatePassTimes.push(t);
|
|
4258
|
+
if (this.gatePassTimes.length < 2) return;
|
|
4259
|
+
this.gatePassTimes = [];
|
|
4260
|
+
this.pausedAt = t;
|
|
4261
|
+
this.player.pause();
|
|
4262
|
+
this.armResume();
|
|
4263
|
+
return;
|
|
4264
|
+
}
|
|
4181
4265
|
if (!this.speaking) {
|
|
4182
4266
|
this.baseline = 0;
|
|
4183
4267
|
this.hot = 0;
|
|
@@ -4217,6 +4301,9 @@ var SonioxSTTOptions = class {
|
|
|
4217
4301
|
source;
|
|
4218
4302
|
model = "stt-rt-preview";
|
|
4219
4303
|
languageHints = ["en"];
|
|
4304
|
+
/** Client-side endpoint: finalized text + no new tokens for this long = utterance (don't wait for
|
|
4305
|
+
* Soniox's semantic <end>, which adds 0.5-1.5s — the difference between ping-pong and lag). */
|
|
4306
|
+
silenceEndpointMs = 500;
|
|
4220
4307
|
};
|
|
4221
4308
|
var SonioxSTT = class {
|
|
4222
4309
|
options;
|
|
@@ -4232,6 +4319,9 @@ var SonioxSTT = class {
|
|
|
4232
4319
|
};
|
|
4233
4320
|
finalText = "";
|
|
4234
4321
|
partialText = "";
|
|
4322
|
+
lastChangeAt = 0;
|
|
4323
|
+
lastCombined = "";
|
|
4324
|
+
endpointTimer = null;
|
|
4235
4325
|
constructor(options) {
|
|
4236
4326
|
this.options = { ...new SonioxSTTOptions(), ...options };
|
|
4237
4327
|
}
|
|
@@ -4268,6 +4358,13 @@ var SonioxSTT = class {
|
|
|
4268
4358
|
await this.connectWs();
|
|
4269
4359
|
if (this.sourceStarted) return;
|
|
4270
4360
|
this.sourceStarted = true;
|
|
4361
|
+
this.endpointTimer = setInterval(() => {
|
|
4362
|
+
const combined = (this.finalText + this.partialText).trim();
|
|
4363
|
+
if (!combined || now2() - this.lastChangeAt < this.options.silenceEndpointMs) return;
|
|
4364
|
+
this.reset();
|
|
4365
|
+
this.onUtterance(combined, now2());
|
|
4366
|
+
}, 120);
|
|
4367
|
+
this.endpointTimer.unref?.();
|
|
4271
4368
|
await this.options.source.start((chunk) => {
|
|
4272
4369
|
let sum = 0;
|
|
4273
4370
|
const view = new DataView(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
@@ -4287,7 +4384,12 @@ var SonioxSTT = class {
|
|
|
4287
4384
|
else if (t.is_final) this.finalText += t.text;
|
|
4288
4385
|
}
|
|
4289
4386
|
this.partialText = (m.tokens ?? []).filter((t) => !t.is_final && t.text !== "<end>").map((t) => t.text).join("");
|
|
4290
|
-
this.
|
|
4387
|
+
const combined = this.finalText + this.partialText;
|
|
4388
|
+
if (combined !== this.lastCombined) {
|
|
4389
|
+
this.lastCombined = combined;
|
|
4390
|
+
this.lastChangeAt = now2();
|
|
4391
|
+
}
|
|
4392
|
+
this.onPartial(combined);
|
|
4291
4393
|
if (endpoint && this.finalText.trim()) {
|
|
4292
4394
|
const utterance = this.finalText.trim();
|
|
4293
4395
|
this.reset();
|
|
@@ -4297,9 +4399,11 @@ var SonioxSTT = class {
|
|
|
4297
4399
|
reset() {
|
|
4298
4400
|
this.finalText = "";
|
|
4299
4401
|
this.partialText = "";
|
|
4402
|
+
this.lastCombined = "";
|
|
4300
4403
|
}
|
|
4301
4404
|
stop() {
|
|
4302
4405
|
this.stopped = true;
|
|
4406
|
+
if (this.endpointTimer) clearInterval(this.endpointTimer);
|
|
4303
4407
|
this.options.source?.stop();
|
|
4304
4408
|
if (this.ws) this.ws.onclose = null;
|
|
4305
4409
|
this.ws?.close();
|
|
@@ -4919,8 +5023,26 @@ Reference files in them by their mount path (the left side).`;
|
|
|
4919
5023
|
].filter(Boolean);
|
|
4920
5024
|
return dirs.length ? dirs : void 0;
|
|
4921
5025
|
};
|
|
4922
|
-
const memoryDir =
|
|
4923
|
-
|
|
5026
|
+
const memoryDir = (() => {
|
|
5027
|
+
const home = homedir();
|
|
5028
|
+
const projectDir = `${cwd}/.agent/memory`;
|
|
5029
|
+
const readDirs = [
|
|
5030
|
+
existsSync2(projectDir) ? projectDir : void 0,
|
|
5031
|
+
existsSync2(`${cwd}/.claude/memory`) ? `${cwd}/.claude/memory` : void 0,
|
|
5032
|
+
existsSync2(`${home}/.agent/memory`) ? `${home}/.agent/memory` : void 0,
|
|
5033
|
+
existsSync2(`${home}/.claude/memory`) ? `${home}/.claude/memory` : void 0
|
|
5034
|
+
].filter(Boolean);
|
|
5035
|
+
return readDirs[0] === projectDir ? readDirs : [projectDir, ...readDirs];
|
|
5036
|
+
})();
|
|
5037
|
+
const memoryUserDir = (() => {
|
|
5038
|
+
const home = homedir();
|
|
5039
|
+
return [
|
|
5040
|
+
existsSync2(`${home}/.agent/memory`) ? `${home}/.agent/memory` : void 0,
|
|
5041
|
+
existsSync2(`${home}/.claude/memory`) ? `${home}/.claude/memory` : void 0
|
|
5042
|
+
].find(Boolean) ?? `${home}/.agent/memory`;
|
|
5043
|
+
})();
|
|
5044
|
+
const memoryWriteDir = memoryDir[0];
|
|
5045
|
+
const hooks = o.learnFromMistakes && memoryWriteDir ? composeHooks(o.hooks, lessonCapture({ fs, dir: memoryWriteDir, minRepeats: 2 })) : o.hooks;
|
|
4924
5046
|
let realShell = [];
|
|
4925
5047
|
const useRealShell = o.realShell ?? !virtual;
|
|
4926
5048
|
if (useRealShell && !virtual) {
|
|
@@ -4977,6 +5099,7 @@ The filesystem root '/' is the real machine root \u2014 you have full filesystem
|
|
|
4977
5099
|
skillsDir: dots("skills"),
|
|
4978
5100
|
commandsDir: dots("commands"),
|
|
4979
5101
|
memoryDir,
|
|
5102
|
+
memoryUserDir,
|
|
4980
5103
|
agentsDir: dot("agents"),
|
|
4981
5104
|
instructionFiles: o.instructionFiles ?? ["AGENTS.md", "CLAUDE.md"],
|
|
4982
5105
|
// Only override the Agent's safety defaults when explicitly configured.
|
|
@@ -5139,13 +5262,16 @@ var AecDuplexAudio = class {
|
|
|
5139
5262
|
startedAt = 0;
|
|
5140
5263
|
// --- AudioSource ---
|
|
5141
5264
|
start(onChunk) {
|
|
5142
|
-
this.proc = spawn2(this.bin, [], { stdio: ["pipe", "pipe", "
|
|
5265
|
+
this.proc = spawn2(this.bin, [], { stdio: ["pipe", "pipe", "pipe"] });
|
|
5143
5266
|
this.proc.stdin.on("error", () => {
|
|
5144
5267
|
});
|
|
5145
5268
|
this.proc.on("exit", (c) => {
|
|
5146
5269
|
if (c && !this.stopped) log12.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
|
|
5147
5270
|
});
|
|
5148
5271
|
this.proc.stdout.on("data", (chunk) => onChunk(chunk));
|
|
5272
|
+
this.proc.stderr.on("data", (d) => {
|
|
5273
|
+
for (const ln of String(d).split("\n")) if (ln.trim()) log12.debug(`mic-aec: ${ln.trim()}`);
|
|
5274
|
+
});
|
|
5149
5275
|
}
|
|
5150
5276
|
stop() {
|
|
5151
5277
|
this.stopped = true;
|
|
@@ -5222,6 +5348,17 @@ var AecDuplexAudio = class {
|
|
|
5222
5348
|
pausedMs() {
|
|
5223
5349
|
return this.pausedAccum + (this.pausedSince ? now4() - this.pausedSince : 0);
|
|
5224
5350
|
}
|
|
5351
|
+
/** TEST HARNESS: queue simulated user speech (s16le 16k mono) — the helper mixes it into the real
|
|
5352
|
+
* mic stream pre-gate, so the full live pipeline runs while a script plays the human. */
|
|
5353
|
+
inject(pcm16k) {
|
|
5354
|
+
const stdin = this.proc?.stdin;
|
|
5355
|
+
if (!stdin || stdin.destroyed) return;
|
|
5356
|
+
const hdr = Buffer.alloc(8);
|
|
5357
|
+
hdr.writeUInt32LE(4294967293, 0);
|
|
5358
|
+
hdr.writeUInt32LE(pcm16k.length, 4);
|
|
5359
|
+
stdin.write(hdr);
|
|
5360
|
+
stdin.write(pcm16k);
|
|
5361
|
+
}
|
|
5225
5362
|
};
|
|
5226
5363
|
var VoiceIOOptions = class extends VoiceEngineOptions {
|
|
5227
5364
|
sonioxApiKey = process.env.SONIOX_API_KEY ?? "";
|
|
@@ -6682,14 +6819,15 @@ function createLineEditor(out) {
|
|
|
6682
6819
|
process.stdin.setRawMode(true);
|
|
6683
6820
|
process.stdin.resume();
|
|
6684
6821
|
out.write("\x1B[?2004h");
|
|
6685
|
-
|
|
6822
|
+
const promptOf = () => typeof opts.prompt === "function" ? opts.prompt() : opts.prompt;
|
|
6823
|
+
render(s, promptOf(), maxVisible, opts.status);
|
|
6686
6824
|
const onResize = () => {
|
|
6687
6825
|
curRow = 0;
|
|
6688
|
-
render(s,
|
|
6826
|
+
render(s, promptOf(), maxVisible, opts.status);
|
|
6689
6827
|
};
|
|
6690
6828
|
activeRedraw = () => {
|
|
6691
6829
|
curRow = 0;
|
|
6692
|
-
render(s,
|
|
6830
|
+
render(s, promptOf(), maxVisible, opts.status);
|
|
6693
6831
|
};
|
|
6694
6832
|
process.on("SIGWINCH", onResize);
|
|
6695
6833
|
return new Promise((resolve4) => {
|
|
@@ -6697,7 +6835,7 @@ function createLineEditor(out) {
|
|
|
6697
6835
|
finish();
|
|
6698
6836
|
resolve4(null);
|
|
6699
6837
|
};
|
|
6700
|
-
const redraw = () => render(s,
|
|
6838
|
+
const redraw = () => render(s, promptOf(), maxVisible, opts.status);
|
|
6701
6839
|
let lastStatus = opts.status?.() ?? "";
|
|
6702
6840
|
const ticker = opts.statusTickMs && opts.status ? setInterval(() => {
|
|
6703
6841
|
if (s.pasting) return;
|
|
@@ -6772,11 +6910,11 @@ function createLineEditor(out) {
|
|
|
6772
6910
|
}
|
|
6773
6911
|
s.reset();
|
|
6774
6912
|
s.refresh();
|
|
6775
|
-
render(s,
|
|
6913
|
+
render(s, promptOf(), maxVisible, opts.status);
|
|
6776
6914
|
return;
|
|
6777
6915
|
}
|
|
6778
6916
|
if (s.pasting) return;
|
|
6779
|
-
render(s,
|
|
6917
|
+
render(s, promptOf(), maxVisible, opts.status);
|
|
6780
6918
|
};
|
|
6781
6919
|
const finish = () => {
|
|
6782
6920
|
activeRedraw = void 0;
|
|
@@ -6787,7 +6925,7 @@ function createLineEditor(out) {
|
|
|
6787
6925
|
out.write("\x1B[?2004l");
|
|
6788
6926
|
s.closeMenu();
|
|
6789
6927
|
s.end();
|
|
6790
|
-
render(s,
|
|
6928
|
+
render(s, promptOf(), maxVisible);
|
|
6791
6929
|
out.write("\r\n");
|
|
6792
6930
|
};
|
|
6793
6931
|
process.stdin.on("keypress", onKey);
|
|
@@ -7277,7 +7415,7 @@ Project instructions: ./AGENTS.md or ./CLAUDE.md are auto-loaded (scaffold with
|
|
|
7277
7415
|
Auto-loaded from ./.agent/: commands/, skills/, memory/, agents/.
|
|
7278
7416
|
|
|
7279
7417
|
REPL shortcuts: !<cmd> runs a shell command inline \xB7 #<note> saves a memory \xB7 @path inlines a file
|
|
7280
|
-
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 /exit
|
|
7418
|
+
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
|
|
7281
7419
|
REPL completion: type / (commands+skills) or @ (files) for a LIVE menu \u2014 \u2191/\u2193 select, \u23CE/Tab accept, Esc dismiss.
|
|
7282
7420
|
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.
|
|
7283
7421
|
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.
|
|
@@ -7523,6 +7661,31 @@ function costOf(pricing, promptTokens = 0, completionTokens = 0) {
|
|
|
7523
7661
|
function turnCost(model, usage) {
|
|
7524
7662
|
return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0);
|
|
7525
7663
|
}
|
|
7664
|
+
async function evaluateGoal(ai, condition, transcript, log17) {
|
|
7665
|
+
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
7666
|
+
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
7667
|
+
return text.slice(0, 600);
|
|
7668
|
+
}).join("\n---\n");
|
|
7669
|
+
try {
|
|
7670
|
+
const r = await ai.chat({
|
|
7671
|
+
model: "anthropic/claude-haiku-4-5",
|
|
7672
|
+
stream: false,
|
|
7673
|
+
messages: [
|
|
7674
|
+
{ role: "system", content: 'You judge whether a goal condition has been met based on conversation transcript. Respond ONLY with JSON: {"met": boolean, "reason": "one sentence"}. Be strict \u2014 only met:true if there is clear evidence in the transcript.' },
|
|
7675
|
+
{ role: "user", content: `Goal condition: ${condition}
|
|
7676
|
+
|
|
7677
|
+
Recent assistant messages:
|
|
7678
|
+
${recent}` }
|
|
7679
|
+
]
|
|
7680
|
+
});
|
|
7681
|
+
const match = r.content.match(/\{[\s\S]*\}/);
|
|
7682
|
+
if (match) return JSON.parse(match[0]);
|
|
7683
|
+
} catch (e) {
|
|
7684
|
+
log17(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
7685
|
+
`));
|
|
7686
|
+
}
|
|
7687
|
+
return { met: false, reason: "evaluation unclear" };
|
|
7688
|
+
}
|
|
7526
7689
|
function errInfo(e) {
|
|
7527
7690
|
return { message: String(e?.message ?? e), statusCode: e?.statusCode, code: e?.code };
|
|
7528
7691
|
}
|
|
@@ -7548,6 +7711,9 @@ async function runShellLine(fs, cmd) {
|
|
|
7548
7711
|
if (r.exitCode !== 0) return `[exit ${r.exitCode}]${r.error ? " " + r.error.trim() : ""}${out ? "\n" + out : ""}`;
|
|
7549
7712
|
return out || "(no output)";
|
|
7550
7713
|
}
|
|
7714
|
+
function primaryMemDir(dir, fallback) {
|
|
7715
|
+
return (Array.isArray(dir) ? dir[0] : dir) || fallback;
|
|
7716
|
+
}
|
|
7551
7717
|
async function appendMemoryNote(fs, dir, text) {
|
|
7552
7718
|
await mkdirp(fs, dir);
|
|
7553
7719
|
const idx = `${dir}/MEMORY.md`;
|
|
@@ -7966,13 +8132,17 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
7966
8132
|
const duplexAsk = async (call) => {
|
|
7967
8133
|
if (args.voice && dx) {
|
|
7968
8134
|
const hint = summarizeCall(call.name, call.args).slice(0, 80);
|
|
8135
|
+
if (cfg.voiceAskUi === "menu") {
|
|
8136
|
+
editorRef?.suspend();
|
|
8137
|
+
const v = await selectMenu(process.stderr, { title: `? background worker asks to run ${call.name} ${hint}`, items: [{ label: "Allow", value: "y" }, { label: "Deny", value: "n" }], current: "n" });
|
|
8138
|
+
editorRef?.resume();
|
|
8139
|
+
editorRef?.redrawNow();
|
|
8140
|
+
return { decision: v === "y" ? "allow" : "deny" };
|
|
8141
|
+
}
|
|
7969
8142
|
const id = `perm-${++permSeq}`;
|
|
7970
|
-
|
|
7971
|
-
`));
|
|
7972
|
-
editorRef?.redrawNow();
|
|
7973
|
-
const a = await dx.parkQuestion(id, `Permission: may the background worker run ${call.name}${hint ? ` (${hint})` : ""}? Yes or no.`);
|
|
8143
|
+
const a = await dx.parkQuestion(id, `Permission: may the background worker run ${call.name}${hint ? ` (${hint})` : ""}? Answer yes or no (you can also type it).`);
|
|
7974
8144
|
const allow = /^\s*(y(es|ep|eah)?|sure|ok(ay)?|allow|go|approved?|do it)\b/i.test(a);
|
|
7975
|
-
err("\r\x1B[0J" +
|
|
8145
|
+
err("\r\x1B[0J" + (allow ? green(` \u2713 allowed ${call.name}`) : yellow(` \u2298 denied ${call.name}`)) + dim(` (${a.trim() || "no answer"})
|
|
7976
8146
|
`));
|
|
7977
8147
|
editorRef?.redrawNow();
|
|
7978
8148
|
return { decision: allow ? "allow" : "deny" };
|
|
@@ -8022,8 +8192,9 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
8022
8192
|
}
|
|
8023
8193
|
if (typeof e.kind === "string" && e.kind.startsWith("task_")) {
|
|
8024
8194
|
spinner.stop();
|
|
8025
|
-
err("\r\x1B[0J" +
|
|
8026
|
-
`)
|
|
8195
|
+
err("\r\x1B[0J" + (e.kind === "task_ask" ? yellow(` ? ${e.message} \u2014 answer by voice or type yes/no
|
|
8196
|
+
`) : dim(` \xB7 ${e.message}
|
|
8197
|
+
`)));
|
|
8027
8198
|
editorRef?.redrawNow();
|
|
8028
8199
|
return;
|
|
8029
8200
|
}
|
|
@@ -8046,6 +8217,8 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
8046
8217
|
dx = new DuplexAgent({
|
|
8047
8218
|
ai,
|
|
8048
8219
|
fs: agent.options.fs,
|
|
8220
|
+
memoryDir: agent.options.memoryDir,
|
|
8221
|
+
memoryUserDir: agent.options.memoryUserDir,
|
|
8049
8222
|
...args.voiceModel ?? cfg.voiceModel ? { voiceModel: resolveModelOrNewest(args.voiceModel ?? cfg.voiceModel) } : {},
|
|
8050
8223
|
workerModel: agent.options.model,
|
|
8051
8224
|
workerOptions,
|
|
@@ -8068,16 +8241,18 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
8068
8241
|
return "not a git repository";
|
|
8069
8242
|
}
|
|
8070
8243
|
},
|
|
8071
|
-
// Memory READS are QuickLook material (instant, capped); memory WRITES stay delegated —
|
|
8072
|
-
// a worker creates/updates the files under .agent/memory/.
|
|
8073
8244
|
memory: async () => {
|
|
8074
|
-
const
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8245
|
+
const _adot = (s) => `${agent.options.fs.getCwd() === "/" ? "" : agent.options.fs.getCwd()}/.agent/${s}`;
|
|
8246
|
+
const dirs = Array.isArray(agent.options.memoryDir) ? agent.options.memoryDir : [agent.options.memoryDir || _adot("memory")];
|
|
8247
|
+
const parts = [];
|
|
8248
|
+
for (const d of dirs) {
|
|
8249
|
+
try {
|
|
8250
|
+
const idx = await fs.readFile(`${d}/MEMORY.md`);
|
|
8251
|
+
if (idx.trim()) parts.push(idx.trim());
|
|
8252
|
+
} catch {
|
|
8253
|
+
}
|
|
8080
8254
|
}
|
|
8255
|
+
return parts.length ? parts.join("\n").slice(0, 2e3) : "(no memory yet)";
|
|
8081
8256
|
}
|
|
8082
8257
|
},
|
|
8083
8258
|
// The voice runs on the REAL fs (it has no fs tools — harmless) so @mentions, !cmd and #note
|
|
@@ -8209,7 +8384,14 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
8209
8384
|
face.transcript = data.messages;
|
|
8210
8385
|
session = data;
|
|
8211
8386
|
checkpoints.use?.(data.meta.id);
|
|
8387
|
+
const m = data.meta;
|
|
8388
|
+
goalCondition = m.goalCondition;
|
|
8389
|
+
goalTurns = m.goalTurns ?? 0;
|
|
8390
|
+
goalTokens = m.goalTokens ?? 0;
|
|
8391
|
+
goalLastReason = m.goalLastReason;
|
|
8212
8392
|
err(dim(` resumed ${data.meta.id} (${data.meta.turns} turns)${data.meta.title ? " \u2014 " + data.meta.title : ""}
|
|
8393
|
+
`));
|
|
8394
|
+
if (goalCondition) err(dim(` \u25CE goal active: ${goalCondition} (${goalTurns} turns)
|
|
8213
8395
|
`));
|
|
8214
8396
|
printHistory(data.messages);
|
|
8215
8397
|
};
|
|
@@ -8353,6 +8535,45 @@ ${extra}` : body);
|
|
|
8353
8535
|
const picked = await selectMenu(process.stderr, { title: `Select a model \xB7 current: ${current}`, items: providerItems, current, filterable: true });
|
|
8354
8536
|
return picked && !picked.startsWith("__") ? picked : null;
|
|
8355
8537
|
};
|
|
8538
|
+
let goalCondition = session.meta.goalCondition;
|
|
8539
|
+
let goalTurns = session.meta.goalTurns ?? 0;
|
|
8540
|
+
let goalTokens = session.meta.goalTokens ?? 0;
|
|
8541
|
+
let goalLastReason = session.meta.goalLastReason;
|
|
8542
|
+
const GOAL_MAX_TURNS = 50;
|
|
8543
|
+
const persistGoal = () => {
|
|
8544
|
+
const m = session.meta;
|
|
8545
|
+
m.goalCondition = goalCondition;
|
|
8546
|
+
m.goalTurns = goalTurns;
|
|
8547
|
+
m.goalTokens = goalTokens;
|
|
8548
|
+
m.goalLastReason = goalLastReason;
|
|
8549
|
+
};
|
|
8550
|
+
const goalLoop = async () => {
|
|
8551
|
+
while (goalCondition && !aborting && !exitRequested && goalTurns < GOAL_MAX_TURNS) {
|
|
8552
|
+
const result = await evaluateGoal(ai, goalCondition, face.transcript, err);
|
|
8553
|
+
goalLastReason = result.reason;
|
|
8554
|
+
if (result.met) {
|
|
8555
|
+
err(green(` \u2713 goal met: ${result.reason}
|
|
8556
|
+
`));
|
|
8557
|
+
goalCondition = void 0;
|
|
8558
|
+
goalLastReason = void 0;
|
|
8559
|
+
persistGoal();
|
|
8560
|
+
return;
|
|
8561
|
+
}
|
|
8562
|
+
err(dim(` \u25CE not yet (${result.reason}) \u2014 turn ${goalTurns + 1}
|
|
8563
|
+
`));
|
|
8564
|
+
aborting = false;
|
|
8565
|
+
const tokensBefore = session.meta.tokens ?? 0;
|
|
8566
|
+
await turn(`Continue working toward the goal: ${goalCondition}`);
|
|
8567
|
+
goalTokens += (session.meta.tokens ?? 0) - tokensBefore;
|
|
8568
|
+
goalTurns++;
|
|
8569
|
+
persistGoal();
|
|
8570
|
+
if (exitRequested) return;
|
|
8571
|
+
}
|
|
8572
|
+
if (goalTurns >= GOAL_MAX_TURNS) {
|
|
8573
|
+
err(yellow(` \u26A0 goal reached ${GOAL_MAX_TURNS} turns \u2014 pausing. /goal to check status, /goal clear to cancel
|
|
8574
|
+
`));
|
|
8575
|
+
}
|
|
8576
|
+
};
|
|
8356
8577
|
const builtins = {
|
|
8357
8578
|
help: { desc: "show this help", run: () => {
|
|
8358
8579
|
err(HELP + "\n");
|
|
@@ -8845,6 +9066,49 @@ ${extra}` : body);
|
|
|
8845
9066
|
}
|
|
8846
9067
|
}
|
|
8847
9068
|
},
|
|
9069
|
+
goal: {
|
|
9070
|
+
desc: "autonomous loop \u2014 /goal <condition> | /goal (status) | /goal clear",
|
|
9071
|
+
run: async (a) => {
|
|
9072
|
+
if (!a.length) {
|
|
9073
|
+
if (!goalCondition) {
|
|
9074
|
+
err(dim(" no active goal\n"));
|
|
9075
|
+
return;
|
|
9076
|
+
}
|
|
9077
|
+
const tokStr = goalTokens > 1e3 ? `${(goalTokens / 1e3).toFixed(1)}k` : String(goalTokens);
|
|
9078
|
+
err(` ${bold("\u25CE goal:")} ${goalCondition}
|
|
9079
|
+
` + dim(` ${goalTurns} turn${goalTurns === 1 ? "" : "s"} \xB7 ${tokStr} tokens${goalLastReason ? ` \xB7 last: ${goalLastReason}` : ""}
|
|
9080
|
+
`));
|
|
9081
|
+
return;
|
|
9082
|
+
}
|
|
9083
|
+
if (a[0] === "clear") {
|
|
9084
|
+
if (!goalCondition) {
|
|
9085
|
+
err(dim(" no active goal\n"));
|
|
9086
|
+
return;
|
|
9087
|
+
}
|
|
9088
|
+
goalCondition = void 0;
|
|
9089
|
+
goalTurns = 0;
|
|
9090
|
+
goalTokens = 0;
|
|
9091
|
+
goalLastReason = void 0;
|
|
9092
|
+
persistGoal();
|
|
9093
|
+
err(green(" \u2713 goal cleared\n"));
|
|
9094
|
+
return;
|
|
9095
|
+
}
|
|
9096
|
+
goalCondition = a.join(" ");
|
|
9097
|
+
goalTurns = 0;
|
|
9098
|
+
goalTokens = 0;
|
|
9099
|
+
goalLastReason = void 0;
|
|
9100
|
+
persistGoal();
|
|
9101
|
+
err(green(` \u25CE goal set: ${goalCondition}
|
|
9102
|
+
`) + dim(" working\u2026 (Esc to pause)\n"));
|
|
9103
|
+
const tokensBefore = session.meta.tokens ?? 0;
|
|
9104
|
+
await turn(goalCondition);
|
|
9105
|
+
goalTokens += (session.meta.tokens ?? 0) - tokensBefore;
|
|
9106
|
+
goalTurns++;
|
|
9107
|
+
persistGoal();
|
|
9108
|
+
if (!exitRequested) await goalLoop();
|
|
9109
|
+
if (exitRequested) return true;
|
|
9110
|
+
}
|
|
9111
|
+
},
|
|
8848
9112
|
exit: { desc: "quit", run: () => true },
|
|
8849
9113
|
quit: { desc: "quit", run: () => true }
|
|
8850
9114
|
};
|
|
@@ -8953,7 +9217,7 @@ ${extra}` : body);
|
|
|
8953
9217
|
if (line.startsWith("#")) {
|
|
8954
9218
|
const note = line.slice(1).trim();
|
|
8955
9219
|
if (note) {
|
|
8956
|
-
const where = await appendMemoryNote(agent.options.fs, agent.options.memoryDir
|
|
9220
|
+
const where = await appendMemoryNote(agent.options.fs, primaryMemDir(agent.options.memoryDir, adot("memory")), note);
|
|
8957
9221
|
err(green(` \u270E remembered \u2192 ${where}
|
|
8958
9222
|
`));
|
|
8959
9223
|
}
|
|
@@ -8989,6 +9253,11 @@ ${extra}` : body);
|
|
|
8989
9253
|
const task = pendingImages.length ? `${line} ${pendingImages.map((p) => "@" + p).join(" ")}` : line;
|
|
8990
9254
|
pendingImages.length = 0;
|
|
8991
9255
|
await turn(task);
|
|
9256
|
+
if (goalCondition && !aborting && !exitRequested) {
|
|
9257
|
+
goalTurns++;
|
|
9258
|
+
persistGoal();
|
|
9259
|
+
await goalLoop();
|
|
9260
|
+
}
|
|
8992
9261
|
if (exitRequested) return "quit";
|
|
8993
9262
|
};
|
|
8994
9263
|
let voicePartial = "";
|
|
@@ -9080,6 +9349,7 @@ ${extra}` : body);
|
|
|
9080
9349
|
const r = work.reasoning;
|
|
9081
9350
|
if (r && r !== "off") parts.push(`reasoning:${r}`);
|
|
9082
9351
|
if (verboseOutput) parts.push("verbose");
|
|
9352
|
+
if (goalCondition) parts.push(`\u25CE goal (${goalTurns} turns)`);
|
|
9083
9353
|
if (inputStash.length) parts.push(`${inputStash.length} stashed (\u2303S to pop)`);
|
|
9084
9354
|
const taskLines = [];
|
|
9085
9355
|
if (dx) {
|
|
@@ -9089,8 +9359,14 @@ ${extra}` : body);
|
|
|
9089
9359
|
}
|
|
9090
9360
|
return [...taskLines, parts.join(" \xB7 ")].filter(Boolean).join("\n");
|
|
9091
9361
|
};
|
|
9362
|
+
const livePrompt = () => {
|
|
9363
|
+
const ask = dx?.pendingAsks.size ? dx.pendingAsks.values().next().value : void 0;
|
|
9364
|
+
if (!ask) return promptStr;
|
|
9365
|
+
const q2 = ask.question.replace(/\s+/g, " ").slice(0, 64);
|
|
9366
|
+
return bold(yellow(`? ${q2}${ask.question.length > 64 ? "\u2026" : ""} \u2039yes/no\u203A `));
|
|
9367
|
+
};
|
|
9092
9368
|
const result = await readMultiline((cont) => editor.readLine({
|
|
9093
|
-
prompt: cont ? contPrompt :
|
|
9369
|
+
prompt: cont ? contPrompt : livePrompt,
|
|
9094
9370
|
suggest,
|
|
9095
9371
|
history,
|
|
9096
9372
|
classifyPaste,
|
|
@@ -9128,6 +9404,13 @@ ${extra}` : body);
|
|
|
9128
9404
|
}
|
|
9129
9405
|
const line = result.trim();
|
|
9130
9406
|
if (!line) continue;
|
|
9407
|
+
if (dx?.pendingAsks.size && /^(y(es|ep|eah)?|n(o|ope)?|sure|ok(ay)?|allow|deny|go( ahead)?)[.!]?$/i.test(line)) {
|
|
9408
|
+
const [id, ask] = dx.pendingAsks.entries().next().value;
|
|
9409
|
+
ask.resolve(line);
|
|
9410
|
+
err(dim(` \u21B3 answered ${id}: ${line}
|
|
9411
|
+
`));
|
|
9412
|
+
continue;
|
|
9413
|
+
}
|
|
9131
9414
|
let quit = await dispatchLine(line) === "quit";
|
|
9132
9415
|
while (!quit && inputStash.length) {
|
|
9133
9416
|
const next = inputStash.shift();
|
|
@@ -9231,7 +9514,8 @@ async function main() {
|
|
|
9231
9514
|
process.once("SIGINT", () => activeTurn?.abort());
|
|
9232
9515
|
const { ok, res } = await runTurn(agent, store, session, args.task, void 0, cwd);
|
|
9233
9516
|
if (cfg.reflectOnFailure && !ok && res && agent.options.memoryDir) {
|
|
9234
|
-
const
|
|
9517
|
+
const _fsBase = agent.options.fs.getCwd() === "/" ? "" : agent.options.fs.getCwd();
|
|
9518
|
+
const slug = await reflectOnRun({ ai, model: agent.options.model, fs: agent.options.fs, dir: primaryMemDir(agent.options.memoryDir, `${_fsBase}/.agent/memory`), result: res });
|
|
9235
9519
|
if (slug) err(dim(` \u270E learned a lesson \u2192 ${slug}
|
|
9236
9520
|
`));
|
|
9237
9521
|
}
|