@livx.cc/agentx 0.99.89 → 0.99.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +299 -45
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +22 -4
- package/dist/index.js +244 -37
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -3778,6 +3778,38 @@ async function loadMemory(fs, dir, opts = {}) {
|
|
|
3778
3778
|
- (${restSlugs.length} more learnings, slugs only \u2014 call \`Recall\` if relevant): ${restSlugs.join(", ")}` : "");
|
|
3779
3779
|
return { index, tools };
|
|
3780
3780
|
}
|
|
3781
|
+
var TERM_STOPWORDS = new Set(`the and for but not you your yours our ours his her hers its their they them this that these those
|
|
3782
|
+
there here what when where which who whom whose why how all any each every some many much more most few other such only own same
|
|
3783
|
+
also just very can will would should could may might must shall have has had does did done was were are been being use used using
|
|
3784
|
+
always never often sometimes usually once then than now today tomorrow yesterday before after later soon first last next new old
|
|
3785
|
+
read write see make take give get set keep run add note notes user users company name names project projects team work works
|
|
3786
|
+
working home inbox label labels file files code prefer prefers please don't doesn't via per with without from into about over
|
|
3787
|
+
under yes okay important example one two three monday tuesday wednesday thursday friday saturday sunday`.split(/\s+/));
|
|
3788
|
+
var SECRET_RE = /\b(code\s*word|pass(word|phrase|code)?|token|secret|api[\s_-]*key|key|pin|credentials?|otp|2fa|private)\b/i;
|
|
3789
|
+
async function memoryTerms(fs, dir, extra = [], max = 30) {
|
|
3790
|
+
const out = /* @__PURE__ */ new Map();
|
|
3791
|
+
const add = (t) => {
|
|
3792
|
+
const k = t.toLowerCase();
|
|
3793
|
+
if (t.length > 2 && !TERM_STOPWORDS.has(k) && !out.has(k)) out.set(k, t);
|
|
3794
|
+
};
|
|
3795
|
+
extra.filter(Boolean).forEach(add);
|
|
3796
|
+
const bodies = { user: [], other: [] };
|
|
3797
|
+
for (const d of (Array.isArray(dir) ? dir : [dir]).filter(Boolean)) {
|
|
3798
|
+
if (!await fs.exists(`${d}/MEMORY.md`)) continue;
|
|
3799
|
+
const slugs = [...(await fs.readFile(`${d}/MEMORY.md`)).matchAll(/\]\(([^)/]+)\.md\)/g)].map((m) => m[1]);
|
|
3800
|
+
for (const slug2 of slugs) {
|
|
3801
|
+
const path = `${d}/${slug2}.md`;
|
|
3802
|
+
if (!await fs.exists(path)) continue;
|
|
3803
|
+
const { block, body } = splitFrontmatter(await fs.readFile(path));
|
|
3804
|
+
const type = grabField(block, "type");
|
|
3805
|
+
if (!["user", "project", "reference"].includes(type ?? "") || SECRET_RE.test(body)) continue;
|
|
3806
|
+
bodies[type === "user" ? "user" : "other"].push(body);
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
for (const body of [...bodies.user, ...bodies.other])
|
|
3810
|
+
for (const m of body.matchAll(/\b(?:[A-Z][a-z]+[A-Za-z]*|[A-Z]{3,})\b/g)) add(m[0]);
|
|
3811
|
+
return [...out.values()].slice(0, max);
|
|
3812
|
+
}
|
|
3781
3813
|
function slugify(s, fallback = "note") {
|
|
3782
3814
|
const base = String(s ?? "").trim().toLowerCase().replace(/\.md$/i, "").replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").slice(0, 48);
|
|
3783
3815
|
return base || fallback;
|
|
@@ -9217,6 +9249,11 @@ configureLogging((name) => {
|
|
|
9217
9249
|
// src/voice/instinct.ts
|
|
9218
9250
|
init_logging();
|
|
9219
9251
|
var log19 = forComponent("Instinct");
|
|
9252
|
+
function voiceDeciderOptions(env = process.env) {
|
|
9253
|
+
if (env.TYPESAFE_AI_KEY) return { apiKey: env.TYPESAFE_AI_KEY };
|
|
9254
|
+
if (env.GROQ_API_KEY) return { provider: "llm", model: "groq/openai/gpt-oss-120b", apiKey: env.GROQ_API_KEY };
|
|
9255
|
+
return void 0;
|
|
9256
|
+
}
|
|
9220
9257
|
var InstinctOptions = class {
|
|
9221
9258
|
decider;
|
|
9222
9259
|
/** Partial unchanged this long (a micro-pause) → consult. */
|
|
@@ -9609,7 +9646,7 @@ var SUSPECT = [
|
|
|
9609
9646
|
/\bwe(?: have|'ve)\b|\bwe (?:already )?(?:responded|replied|dispatched|used|called|did|gave|said|asked)\b/i,
|
|
9610
9647
|
/\bno (?:further|more|other)\b/i,
|
|
9611
9648
|
/\[\s*task\b|\bt\d+\b|\bwait(?:ing)? for (?:the )?(?:events?|results?|task)\b/i,
|
|
9612
|
-
|
|
9649
|
+
/[\p{L}\p{N}.!?…]\s*\{\s*"|\bwhat:\s*"/u,
|
|
9613
9650
|
/\b(?:last|previous) (?:response|reply|message|turn)\b|\bthe (?:conversation|transcript|context)\b/i
|
|
9614
9651
|
];
|
|
9615
9652
|
var STRICT = [
|
|
@@ -9626,6 +9663,12 @@ var STRICT = [
|
|
|
9626
9663
|
// its own protocol: using its tools, tool-call syntax, task events
|
|
9627
9664
|
new RegExp(String.raw`(?:\b[Ww]e(?: need to| should| must| have to| will|'ll)|\b[Ll]et'?s|^[Ss]hould|^[Nn]ow we should) (?:call|use|do|dispatch(?: to)?|invoke) (?:the )?${TOOL}\b`),
|
|
9628
9665
|
new RegExp(String.raw`\b(?:[Ww]e (?:called|dispatched)|responded with) ${TOOL}\b|\b${TOOL}\s*\{|\bwhat:\s*"|\b${TOOL} (?:tool )?(?:brief|call|args?|arguments)\b`),
|
|
9666
|
+
// telegraphic self-instruction naming its own tool ("Use Act with brief to search npm.", "Then dispatch Act.") and
|
|
9667
|
+
// tool-call arguments glued onto the text ("One sentence ack.{"brief":…}" — harmony lost the call's recipient;
|
|
9668
|
+
// agentx sessions 20260713-124250, 20261012-225902). A JSON block printed for the listener follows whitespace.
|
|
9669
|
+
// "Use <Tool>" only in the planning shape (with/to/punctuation next) — "Use Act two of the play…" is speech.
|
|
9670
|
+
new RegExp(String.raw`^(?:Use|Call|Invoke) ${TOOL}(?= with\b| to\b|\s*[.,;:]|\s*$)|^(?:(?:Then|Now|So) )?[Dd]ispatch(?: to)? (?:the )?${TOOL}\b`),
|
|
9671
|
+
/[\p{L}\p{N}.!?…]\{\s*"\w+"\s*:/u,
|
|
9629
9672
|
/\bwait(?:ing)? for (?:the )?(?:events?|t\d+)\b|\bt\d+ (?:completed|failed)\b|\[\s*task\b/i
|
|
9630
9673
|
];
|
|
9631
9674
|
var SENTENCE_END = new RegExp(`[.!?\u2026]+["')\\]]*(?:\\s|(?=\\p{Lu}|\\[))`, "gu");
|
|
@@ -9659,6 +9702,19 @@ async function judgeLeak(decider, sentence, userSaid) {
|
|
|
9659
9702
|
if (!Number.isFinite(p)) throw new Error("leak judge: no noul in the answer");
|
|
9660
9703
|
return p;
|
|
9661
9704
|
}
|
|
9705
|
+
function scrubSpokenJunk(text, final = false) {
|
|
9706
|
+
let s = text.replace(/[-]/g, "").replace(/\[\s*emotion\b([^[\]]*)\]/gi, (_m, inner) => {
|
|
9707
|
+
const e = normalizeEmotion(inner.trim());
|
|
9708
|
+
return e ? `[${e}]` : "";
|
|
9709
|
+
}).replace(/\[[^\p{L}\p{N}[\]]*\]/gu, "").replace(/(?:[\s ]*(?:…|\.{2,}|[—–]|-{2,})){3,}/g, (m) => /^[\s ]/.test(m) ? " \u2026" : "\u2026");
|
|
9710
|
+
if (final) s = s.replace(/\[\s*emotion\b[^\]]*$/i, "");
|
|
9711
|
+
return s;
|
|
9712
|
+
}
|
|
9713
|
+
function isRepeatOf(sentence, spoken) {
|
|
9714
|
+
const norm3 = (x) => x.replace(/\[[a-z_ ]+\]/gi, "").replace(/[^\p{L}\p{N}]+/gu, " ").trim().toLowerCase();
|
|
9715
|
+
const s = norm3(sentence);
|
|
9716
|
+
return s.replace(/ /g, "").length >= 16 && spoken.split(SENTENCE_SPLIT).some((x) => norm3(x) === s);
|
|
9717
|
+
}
|
|
9662
9718
|
|
|
9663
9719
|
// src/duplex.ts
|
|
9664
9720
|
var log21 = forComponent("DuplexAgent");
|
|
@@ -9741,6 +9797,9 @@ var DuplexAgentOptions = class {
|
|
|
9741
9797
|
/** Host overrides for QuickLook lookups (keyed by `what`). The engine's defaults go through the
|
|
9742
9798
|
* (possibly jailed) fs — e.g. `.git/**` is deny-listed, so the CLI supplies 'branch' itself. */
|
|
9743
9799
|
quickLook;
|
|
9800
|
+
/** Extra roots QuickLook ls/file try for a relative path not found under the cwd (the CLI passes ~ and ~/Documents:
|
|
9801
|
+
* a spoken "Projects/X" means ~/Documents/Projects/X far more often than <cwd>/Projects/X). */
|
|
9802
|
+
lookRoots = [];
|
|
9744
9803
|
/** Memory directory/directories on the WORKER fs. If set, the voice agent gets Remember + Recall
|
|
9745
9804
|
* tools directly (no delegation needed) and implicit capture guidance. */
|
|
9746
9805
|
memoryDir;
|
|
@@ -9748,6 +9807,60 @@ var DuplexAgentOptions = class {
|
|
|
9748
9807
|
memoryUserDir;
|
|
9749
9808
|
};
|
|
9750
9809
|
var SESSION_ID_RE = /^\d{8}-\d{6}(?:-[\w-]+)?$/;
|
|
9810
|
+
var MID_PHRASE_WORDS = /* @__PURE__ */ new Set([
|
|
9811
|
+
"slash",
|
|
9812
|
+
"backslash",
|
|
9813
|
+
"dash",
|
|
9814
|
+
"hyphen",
|
|
9815
|
+
"dot",
|
|
9816
|
+
"underscore",
|
|
9817
|
+
"colon",
|
|
9818
|
+
"comma",
|
|
9819
|
+
"and",
|
|
9820
|
+
"or",
|
|
9821
|
+
"but",
|
|
9822
|
+
"so",
|
|
9823
|
+
"because",
|
|
9824
|
+
"if",
|
|
9825
|
+
"then",
|
|
9826
|
+
"called",
|
|
9827
|
+
"named",
|
|
9828
|
+
"the",
|
|
9829
|
+
"a",
|
|
9830
|
+
"an",
|
|
9831
|
+
"my",
|
|
9832
|
+
"your",
|
|
9833
|
+
"our",
|
|
9834
|
+
"their",
|
|
9835
|
+
"is",
|
|
9836
|
+
"are",
|
|
9837
|
+
"um",
|
|
9838
|
+
"uh"
|
|
9839
|
+
]);
|
|
9840
|
+
var TRAILING_PREPS = /* @__PURE__ */ new Set(["to", "for", "with", "about", "of", "at", "in", "on", "from", "by", "into", "under", "inside"]);
|
|
9841
|
+
var QUESTION_START = /^(?:what|who|whom|whose|where|when|why|how|which|is|are|was|were|do|does|did|can|could|will|would|should|have|has)\b/i;
|
|
9842
|
+
var PHRASAL_HOST = /* @__PURE__ */ new Set(["it", "this", "that", "them", "him", "her", "me", "us", "you", "up", "out", "off", "down", "back", "over", "away"]);
|
|
9843
|
+
function endsMidPhrase(text) {
|
|
9844
|
+
const t = text.split("\n[")[0].trim();
|
|
9845
|
+
if (!t || /[.?!…]["')\]]*$/.test(t)) return false;
|
|
9846
|
+
if (/\b[a-z][\w+.-]*:\/\/\S+$/i.test(t)) return false;
|
|
9847
|
+
if (/[,/\\:\-–—]$/.test(t)) return true;
|
|
9848
|
+
const [last, prev] = (t.toLowerCase().match(/[\p{L}']+/gu) ?? []).reverse();
|
|
9849
|
+
if (!last) return false;
|
|
9850
|
+
if (TRAILING_PREPS.has(last)) return !QUESTION_START.test(t) && !PHRASAL_HOST.has(prev);
|
|
9851
|
+
return MID_PHRASE_WORDS.has(last);
|
|
9852
|
+
}
|
|
9853
|
+
function nameScore(want, have) {
|
|
9854
|
+
const a = want.toLowerCase().replace(/[^\p{L}\p{N}]/gu, ""), b = have.toLowerCase().replace(/[^\p{L}\p{N}]/gu, "");
|
|
9855
|
+
if (!a || !b) return 0;
|
|
9856
|
+
let pre = 0;
|
|
9857
|
+
while (pre < a.length && a[pre] === b[pre]) pre++;
|
|
9858
|
+
const grams = (x) => new Set(Array.from({ length: Math.max(0, x.length - 1) }, (_, i) => x.slice(i, i + 2)));
|
|
9859
|
+
const ga = grams(a), gb = grams(b);
|
|
9860
|
+
let common = 0;
|
|
9861
|
+
for (const g of ga) if (gb.has(g)) common++;
|
|
9862
|
+
return pre * 2 + 2 * common / (ga.size + gb.size || 1);
|
|
9863
|
+
}
|
|
9751
9864
|
var RESERVED_EVENT_MARKER = /\[task\b[^\]\n]*\b(?:completed|failed|progress|asks)\b/i;
|
|
9752
9865
|
var RESERVED_EVENT_OPENER = /\[\s*task\b/i;
|
|
9753
9866
|
var STAGE_DIRECTION_RE = /^\(\s*(?:(?:waiting|checking|searching|thinking|processing|loading|working|fetching|looking)\b[^)]*|[^)]*(?:\.\.\.|…)\s*)\)$/i;
|
|
@@ -9770,11 +9883,11 @@ function isOfferAcceptance(text) {
|
|
|
9770
9883
|
}
|
|
9771
9884
|
var isQuestion = (s) => /\?["'”’)\]]*$/.test(s.trim());
|
|
9772
9885
|
var OFFER_OVERRUN = 150;
|
|
9773
|
-
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. Never emit stage directions or parenthetical asides about your own process \u2014 nothing like "(waiting for the result...)" or "(checking)"; while work runs, either say it as plain speech or end your turn.\nThis holds even when asked to "print", "list", "show", or "make a table" \u2014 there is no screen for the spoken channel. Speak it as flowing prose ("Tuesday is half a meter, Wednesday a bit less\u2026"), or if they truly need it on screen, route it to Act to render. Never emit dashes or pipes into speech.\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 have three cognitive tiers \u2014 like a human brain:\n\u2022 YOU (reflex) \u2014 instant, lightweight. Handle greetings, simple questions, status checks, QuickLook.\n\u2022 `Act` \u2014 your hands. A background worker with its own configured tools and access to the user\'s environment (files and shell{{WORKER_WEB}}). Use for reading, editing, searching, running tasks, building \u2014 any real work.\n{{THINK_SLOT}}\nWhen you are unsure whether you can do or access something, do NOT assume and do NOT claim a capability you have not confirmed. To check what you can do, QuickLook `capabilities` (instant \u2014 it lists your worker\'s real tools) and answer from that. Never promise an ability that is not in your capabilities; if it is not there, tell the user plainly you can\'t. To actually DO real work, call `Act`. When the user mentions their project, folder, files, or environment ("this project", "the current folder", "my code"), call `Act` 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.\nYou cannot mute the microphone or stop voice capture yourself \u2014 no tool does it. If the user asks you to stop listening or turn the voice off, never claim you did: tell them to say exactly "voice off" (handled by the app directly), or type /voice.\nYou are NOT a knowledge base. For any question whose answer needs SPECIFIC verifiable facts you do not already have in hand \u2014 how to build/configure/implement something, exact API, library, entitlement, command or option names, current events, or particular numbers, dates, or names \u2014 do NOT answer from your own memory: you will confidently make things up (a fake API, a wrong entitlement, an event that did not happen). Route it to `Act`, which can search and verify, and speak only what its report says. DELEGATION RULE \u2014 decide for yourself, the user never has to push: if you cannot answer confidently from the conversation plus trivial well-known knowledge, do NOT refuse and do NOT guess \u2014 dispatch `Act` immediately with a clear brief and say you are checking. Anything needing CURRENT data (weather, news, prices, dates, sky/astronomy, "right now"), real computation, or verification is an automatic dispatch \u2014 never a refusal. The user should never need to say "search the web" or "think harder" to make you act; needing fresh or verified information IS the trigger. Refuse only what your worker genuinely cannot do (check `capabilities`), and say why. Answer inline ONLY for general conversation, chit-chat, and trivia you are sure of, or facts you can see via QuickLook. When elaborating on a completed task ("tell me more", "the gist"), stay strictly within what that result actually said \u2014 if the user asks for something the result did not cover, that is NEW information: dispatch `Act`, do not improvise.\nALWAYS react before you work: the FIRST thing in your turn is a brief spoken acknowledgement of what you heard and what you are about to do ("got it \u2014 opening that now", "sure, let me pull it up", "okay, checking"). NEVER call a tool (Act, Think, QuickLook) silently \u2014 the user must hear you react before you go quiet to work. After dispatching Act or Think, that same one short sentence IS your turn \u2014 end it and do not wait for the result. Exactly ONE short line: never stack a second acknowledgement, and never narrate your own presence or status while waiting ("I\'m here", "let me see", "still checking" right after you already acked). One clean ack then silence reads as competent; repeated check-ins read as nervous.\nA completed task speaks its OWN result to the user (the worker voices what matters as it finishes) \u2014 you do NOT re-voice clean task results. A FAILED or INCOMPLETE task still arrives as a "[task t1 failed] \u2026" event for you to handle. The completed result stays in YOUR context \u2014 it is yours to draw on. When the user follows up ("tell me more", "what else", "and?"), answer FROM that result first: you already have the detail, so elaborate on what you have. Do NOT spawn a fresh worker to re-search or re-gather what you were just handed. Re-dispatch ONLY when genuinely new information is needed \u2014 e.g. the user wants the full contents of a SPECIFIC source, which is one WebFetch of that URL, not a brand-new search. "[task t1 progress] \u2026" events are interim status, NOT results \u2014 on a genuinely LONG wait you MAY give one brief half-sentence aside, but only occasionally; silence while working is normal and fine (the user knows you are on it). Do NOT narrate every step or re-announce yourself. Never present progress as a finished result.\nCRITICAL: while a task is still running you have NO answer yet \u2014 never state a specific result of any kind (a number, size, count, name, path, or value). The real answer exists ONLY once the task finishes (the worker speaks it, and `TaskStatus` then returns it); inventing one meanwhile (a made-up disk size, commit count, etc.) is a serious error. Until then, only acknowledge and wait.\nNever read raw file paths, diffs, or code aloud verbatim.\nDo NOT end every turn with the same canned offer ("want a rundown?", "want the steps?"). Offer once at most; if the user pushes back, repeats themselves, or sounds unsatisfied ("you know what I mean?", "think deeper", "are you sure?"), do NOT re-offer the same thing \u2014 change approach: dispatch `Act`/`Think` to actually dig in, or ask one concrete clarifying question. Repeating a non-answer is worse than silence.\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.\nIf the user\'s message sounds INCOMPLETE \u2014 trailing off mid-sentence, a fragment that needs more context ("and then we", "but the problem is"), hesitation fillers ("uh", "um") \u2014 call `Hold` instead of answering. This keeps listening for the rest of their thought. Only respond with substance when you have a complete question or request.\nDispatch discipline: send ONE self-contained task per request \u2014 a single worker with the full brief beats several workers with fragments (each worker starts fresh and re-discovers context). NEVER dispatch a worker just to read files or gather information \u2014 workers explore and discover context themselves; pass on what you already know and let one worker do the whole job. Split into parallel tasks only when the user asks for genuinely independent things. When a task completes, report its result and stop \u2014 do NOT dispatch follow-up work (verification, polish, extras) the user did not ask for, unless the report itself signals failure or doubt.\nDo not fire a second Act/Think for work already in flight, and NEVER spawn a second task to re-count, cross-check, or verify a result a worker already gave you \u2014 trust its answer; a single question gets ONE task. Call `TaskStatus` at most ONCE per turn; if a task is still running, give one brief status aside and end the turn \u2014 never poll it again and again in a loop. "Still on it" is ONLY for a repeat check on a task you already acknowledged earlier \u2014 NEVER as the ack when you first dispatch work ("still" is wrong when nothing was running yet; use a fresh "on it"/"got it, checking" instead). 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 act, do not check status, just exit.\nFor TRIVIAL instant lookups only \u2014 current time, git branch, listing a folder, peeking at a small file, or checking your own `capabilities`/tools \u2014 use `QuickLook` (instant, no task). Whenever the user asks what you can do or whether you have some ability, QuickLook `capabilities` and answer from that \u2014 never guess. Anything requiring searching, reasoning, running commands, or editing goes through `Act`.\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, surprising, or only half-parses, do NOT guess an action or improvise content from it \u2014 briefly confirm what they meant ("did you mean\u2026?") and wait. A one-line confirm beats a confident wrong answer or an invented response to a request you did not actually understand.';
|
|
9886
|
+
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. Never emit stage directions or parenthetical asides about your own process \u2014 nothing like "(waiting for the result...)" or "(checking)"; while work runs, either say it as plain speech or end your turn.\nThis holds even when asked to "print", "list", "show", or "make a table" \u2014 there is no screen for the spoken channel. Speak it as flowing prose ("Tuesday is half a meter, Wednesday a bit less\u2026"), or if they truly need it on screen, route it to Act to render. Never emit dashes or pipes into speech.\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 have three cognitive tiers \u2014 like a human brain:\n\u2022 YOU (reflex) \u2014 instant, lightweight. Handle greetings, simple questions, status checks, QuickLook.\n\u2022 `Act` \u2014 your hands. A background worker with its own configured tools and access to the user\'s environment (files and shell{{WORKER_WEB}}). Use for reading, editing, searching, running tasks, building \u2014 any real work.\n{{THINK_SLOT}}\nWhen you are unsure whether you can do or access something, do NOT assume and do NOT claim a capability you have not confirmed. To check what you can do, QuickLook `capabilities` (instant \u2014 it lists your worker\'s real tools) and answer from that. Never promise an ability that is not in your capabilities; if it is not there, tell the user plainly you can\'t. To actually DO real work, call `Act`. When the user mentions their project, folder, files, or environment ("this project", "the current folder", "my code"), call `Act` 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.\nYou cannot mute the microphone or stop voice capture yourself \u2014 no tool does it. If the user asks you to stop listening or turn the voice off, never claim you did: tell them to say exactly "voice off" (handled by the app directly), or type /voice.\nYou are NOT a knowledge base. For any question whose answer needs SPECIFIC verifiable facts you do not already have in hand \u2014 how to build/configure/implement something, exact API, library, entitlement, command or option names, current events, or particular numbers, dates, or names \u2014 do NOT answer from your own memory: you will confidently make things up (a fake API, a wrong entitlement, an event that did not happen). Route it to `Act`, which can search and verify, and speak only what its report says. DELEGATION RULE \u2014 decide for yourself, the user never has to push: if you cannot answer confidently from the conversation plus trivial well-known knowledge, do NOT refuse and do NOT guess \u2014 dispatch `Act` immediately with a clear brief and say you are checking. Anything needing CURRENT data (weather, news, prices, dates, sky/astronomy, "right now"), real computation, or verification is an automatic dispatch \u2014 never a refusal. The user should never need to say "search the web" or "think harder" to make you act; needing fresh or verified information IS the trigger. Refuse only what your worker genuinely cannot do (check `capabilities`), and say why. Answer inline ONLY for general conversation, chit-chat, and trivia you are sure of, or facts you can see via QuickLook. When elaborating on a completed task ("tell me more", "the gist"), stay strictly within what that result actually said \u2014 if the user asks for something the result did not cover, that is NEW information: dispatch `Act`, do not improvise.\nALWAYS react before you work: the FIRST thing in your turn is a brief spoken acknowledgement of what you heard and what you are about to do ("got it \u2014 opening that now", "sure, let me pull it up", "okay, checking"). NEVER call a tool (Act, Think, QuickLook) silently \u2014 the user must hear you react before you go quiet to work. After dispatching Act or Think, that same one short sentence IS your turn \u2014 end it and do not wait for the result. Exactly ONE short line: never stack a second acknowledgement, and never narrate your own presence or status while waiting ("I\'m here", "let me see", "still checking" right after you already acked). One clean ack then silence reads as competent; repeated check-ins read as nervous.\nA completed task speaks its OWN result to the user (the worker voices what matters as it finishes) \u2014 you do NOT re-voice clean task results. A FAILED or INCOMPLETE task still arrives as a "[task t1 failed] \u2026" event for you to handle. The completed result stays in YOUR context \u2014 it is yours to draw on. When the user follows up ("tell me more", "what else", "and?"), answer FROM that result first: you already have the detail, so elaborate on what you have. Do NOT spawn a fresh worker to re-search or re-gather what you were just handed. Re-dispatch ONLY when genuinely new information is needed \u2014 e.g. the user wants the full contents of a SPECIFIC source, which is one WebFetch of that URL, not a brand-new search. "[task t1 progress] \u2026" events are interim status, NOT results \u2014 on a genuinely LONG wait you MAY give one brief half-sentence aside, but only occasionally; silence while working is normal and fine (the user knows you are on it). Do NOT narrate every step or re-announce yourself. Never present progress as a finished result.\nCRITICAL: while a task is still running you have NO answer yet \u2014 never state a specific result of any kind (a number, size, count, name, path, or value). The real answer exists ONLY once the task finishes (the worker speaks it, and `TaskStatus` then returns it); inventing one meanwhile (a made-up disk size, commit count, etc.) is a serious error. Until then, only acknowledge and wait.\nNever read raw file paths, diffs, or code aloud verbatim.\nDo NOT end every turn with the same canned offer ("want a rundown?", "want the steps?"). Offer once at most; if the user pushes back, repeats themselves, or sounds unsatisfied ("you know what I mean?", "think deeper", "are you sure?"), do NOT re-offer the same thing \u2014 change approach: dispatch `Act`/`Think` to actually dig in, or ask one concrete clarifying question. Repeating a non-answer is worse than silence.\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.\nIf the user\'s message sounds INCOMPLETE \u2014 trailing off mid-sentence, a fragment that needs more context ("and then we", "but the problem is"), hesitation fillers ("uh", "um") \u2014 call `Hold` instead of answering. This keeps listening for the rest of their thought. Only respond with substance when you have a complete question or request.\nDispatch discipline: send ONE self-contained task per request \u2014 a single worker with the full brief beats several workers with fragments (each worker starts fresh and re-discovers context). NEVER dispatch a worker just to read files or gather information \u2014 workers explore and discover context themselves; pass on what you already know and let one worker do the whole job. Split into parallel tasks only when the user asks for genuinely independent things. When a task completes, report its result and stop \u2014 do NOT dispatch follow-up work (verification, polish, extras) the user did not ask for, unless the report itself signals failure or doubt.\nDo not fire a second Act/Think for work already in flight, and NEVER spawn a second task to re-count, cross-check, or verify a result a worker already gave you \u2014 trust its answer; a single question gets ONE task. Call `TaskStatus` at most ONCE per turn; if a task is still running, give one brief status aside and end the turn \u2014 never poll it again and again in a loop. "Still on it" is ONLY for a repeat check on a task you already acknowledged earlier \u2014 NEVER as the ack when you first dispatch work ("still" is wrong when nothing was running yet; use a fresh "on it"/"got it, checking" instead). Use `CancelTask` when the user asks to stop something \u2014 to stop everything ("stop all", "cancel everything") pass id "all". When the user only ADDS to or CORRECTS a request whose task is still running (a detail, a path, a name, a spelling), call `Act` with `refines` set to that task id \u2014 it updates the running worker in place; never cancel and re-dispatch for a refinement.\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 act, do not check status, just exit.\nFor TRIVIAL instant lookups only \u2014 current time, git branch, listing a folder, peeking at a small file, or checking your own `capabilities`/tools \u2014 use `QuickLook` (instant, no task). Whenever the user asks what you can do or whether you have some ability, QuickLook `capabilities` and answer from that \u2014 never guess. Anything requiring searching, reasoning, running commands, or editing goes through `Act`.\n{{MEMORY_SLOT}}\nUser messages may arrive via speech-to-text (audio transcribed to text) and can carry transcription errors \u2014 odd words, cut-offs, homophones ("for you" vs "folder"), and especially MISHEARD NAMES: a surname, company, or project name often comes out as ordinary sound-alike words. Read for INTENT, not surface text, and do not silently rewrite what they said. If a message seems garbled, surprising, or only half-parses, or a word sounds like a name you already know (from memory or this conversation) but is spelled differently, treat it with suspicion \u2014 do NOT act on the odd word blindly or improvise content from it; briefly confirm what they meant ("did you mean\u2026?") and wait. A one-line confirm beats a confident wrong answer. NEVER save to memory a name, spelling, or term you only HEARD (it may be a transcription error) \u2014 when the user spells something aloud, read the spelling back and get a yes before storing it.';
|
|
9774
9887
|
var THINK_GUIDANCE = "\u2022 `Think` \u2014 your brain. A premium reasoning model, FAR more expensive than Act. Reserve it for open-ended architecture/design questions, or a problem Act already FAILED at. ALL implementation work \u2014 coding, refactoring, debugging, edge cases, tests \u2014 goes to Act; Act is highly capable. Never send the same work to both.";
|
|
9775
9888
|
var THINK_DISABLED_GUIDANCE = "(Think tier is not available \u2014 use Act for all escalations.)";
|
|
9776
9889
|
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 escalate to Act or Think, 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 act", no task ids out loud).`;
|
|
9777
|
-
var EMOTION_TAGS_GUIDANCE = `EMOTION: your voice is synthesized with emotion control. Prefix a sentence with an inline
|
|
9890
|
+
var EMOTION_TAGS_GUIDANCE = `EMOTION: your voice is synthesized with emotion control. Prefix a sentence with an inline emotion tag \u2014 ONE emotion word from the list below in square brackets, the word alone (never the word "emotion" itself, never a guess or a question mark), placed directly before the sentence it colors, to shape how it is spoken. Use it ONLY when the emotion genuinely fits the words (it amplifies real feeling, it cannot fake it) \u2014 do not tag every sentence; reserve it for moments that carry feeling, and vary which one you use. You may also drop [laughter] for a natural laugh. Available emotions: ${EMOTIONS.join(", ")}.`;
|
|
9778
9891
|
var DuplexAgent = class _DuplexAgent {
|
|
9779
9892
|
options;
|
|
9780
9893
|
voice;
|
|
@@ -9835,6 +9948,10 @@ var DuplexAgent = class _DuplexAgent {
|
|
|
9835
9948
|
// reflex emitted a reserved [task …] marker → suppress its tail
|
|
9836
9949
|
leakCuts = [];
|
|
9837
9950
|
// heads of analysis-register leaks cut this turn — scrubbed from the transcript at turn end
|
|
9951
|
+
midPhraseBounced = false;
|
|
9952
|
+
// this turn's dispatch on a mid-phrase fragment was already bounced to Hold
|
|
9953
|
+
repeatCuts = [];
|
|
9954
|
+
// same-turn repeated sentences skipped this turn — their LATER copy is scrubbed from the transcript
|
|
9838
9955
|
leakStep = false;
|
|
9839
9956
|
// the CURRENT step's tail is muted by a leak cut (the next step speaks again, unlike a fabricated marker)
|
|
9840
9957
|
leakHit = false;
|
|
@@ -9998,7 +10115,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
9998
10115
|
pumpReflex(final) {
|
|
9999
10116
|
for (; ; ) {
|
|
10000
10117
|
if (this.leakStep) return;
|
|
10001
|
-
this.
|
|
10118
|
+
this.scrubUnforwarded(final);
|
|
10002
10119
|
const from = this.reflexForwarded;
|
|
10003
10120
|
const buf = this.reflexBuf;
|
|
10004
10121
|
const lastMark = Math.max(from, ...this.stepMarks);
|
|
@@ -10008,6 +10125,8 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10008
10125
|
if (tail) upto -= tail[0].length;
|
|
10009
10126
|
const paren = buf.lastIndexOf("(");
|
|
10010
10127
|
if (paren >= lastMark && !buf.includes(")", paren) && buf.length - paren <= 80) upto = Math.min(upto, paren);
|
|
10128
|
+
const brk = buf.lastIndexOf("[");
|
|
10129
|
+
if (brk >= lastMark && !buf.includes("]", brk) && buf.length - brk <= 60) upto = Math.min(upto, brk);
|
|
10011
10130
|
}
|
|
10012
10131
|
if (upto <= from) return;
|
|
10013
10132
|
const region = buf.slice(from, upto);
|
|
@@ -10015,9 +10134,14 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10015
10134
|
for (const k of this.stepMarks) if (k > from && k <= upto) ends.push(k - from);
|
|
10016
10135
|
if (final || !ends.length && region.length >= 160) ends.push(region.length);
|
|
10017
10136
|
ends.sort((a, b) => a - b);
|
|
10018
|
-
let ok = 0, leak;
|
|
10137
|
+
let ok = 0, leak, repeat;
|
|
10019
10138
|
for (const e of ends) {
|
|
10020
10139
|
if (e <= ok) continue;
|
|
10140
|
+
const sent = region.slice(ok, e);
|
|
10141
|
+
if (sent.trim() && !/[\p{L}\p{N}]/u.test(sent) || isRepeatOf(sent, this.turnSpoken + region.slice(0, ok))) {
|
|
10142
|
+
repeat = e;
|
|
10143
|
+
break;
|
|
10144
|
+
}
|
|
10021
10145
|
const v = this.leakVerdict(region.slice(ok, e));
|
|
10022
10146
|
if (v === void 0) break;
|
|
10023
10147
|
if (v) {
|
|
@@ -10027,6 +10151,15 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10027
10151
|
ok = e;
|
|
10028
10152
|
}
|
|
10029
10153
|
this.forwardReflex(from + ok);
|
|
10154
|
+
if (repeat !== void 0) {
|
|
10155
|
+
const cut = region.slice(ok, repeat).trim();
|
|
10156
|
+
if (/[\p{L}\p{N}]/u.test(cut)) {
|
|
10157
|
+
this.repeatCuts.push(cut);
|
|
10158
|
+
this.notify("diag", "reflex_repeat", { text: cut.slice(0, 80) });
|
|
10159
|
+
}
|
|
10160
|
+
this.reflexForwarded = from + repeat;
|
|
10161
|
+
continue;
|
|
10162
|
+
}
|
|
10030
10163
|
if (leak === void 0) return;
|
|
10031
10164
|
const at = from + ok;
|
|
10032
10165
|
this.leakHit = true;
|
|
@@ -10101,6 +10234,22 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10101
10234
|
scrubLeakFromTranscript() {
|
|
10102
10235
|
const keys = this.leakCuts.splice(0);
|
|
10103
10236
|
const t = this.voice.transcript;
|
|
10237
|
+
for (let i = this.turnBase; i < t.length; i++) {
|
|
10238
|
+
const m = t[i];
|
|
10239
|
+
if (m.role === "assistant" && typeof m.content === "string") m.content = scrubSpokenJunk(m.content, true).trim();
|
|
10240
|
+
}
|
|
10241
|
+
for (const cut of this.repeatCuts.splice(0)) {
|
|
10242
|
+
const rep = cut.replace(/^(?:\[[^\]]*\]\s*)+/, "");
|
|
10243
|
+
if (!rep) continue;
|
|
10244
|
+
for (let i = t.length - 1; i >= this.turnBase; i--) {
|
|
10245
|
+
const m = t[i];
|
|
10246
|
+
if (m.role !== "assistant" || typeof m.content !== "string") continue;
|
|
10247
|
+
const at = m.content.lastIndexOf(rep);
|
|
10248
|
+
if (at <= 0 && !(at === 0 && t.slice(this.turnBase, i).some((x) => x.role === "assistant" && contentText(x.content).includes(rep)))) continue;
|
|
10249
|
+
m.content = (m.content.slice(0, at).replace(/\[[^\]]*\]\s*$/, "") + m.content.slice(at + rep.length)).trim();
|
|
10250
|
+
break;
|
|
10251
|
+
}
|
|
10252
|
+
}
|
|
10104
10253
|
for (const key of keys) {
|
|
10105
10254
|
let i = t.length - 1;
|
|
10106
10255
|
while (i >= this.turnBase && !(t[i].role === "assistant" && typeof t[i].content === "string" && t[i].content.includes(key))) i--;
|
|
@@ -10147,14 +10296,25 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10147
10296
|
if (this.silentTurn && res?.finishReason === "error" && refusedCall(res.error)) this.emitHost({ kind: "text_delta", message: fallback ?? _DuplexAgent.FALLBACK_ERROR });
|
|
10148
10297
|
else if (this.silentTurn) await this.ackIfSilent(fallback);
|
|
10149
10298
|
}
|
|
10150
|
-
/**
|
|
10151
|
-
* Only the unforwarded region is touched — already-spoken audio can't be unsent, and
|
|
10152
|
-
* reflexForwarded would corrupt the forward offset.
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
const
|
|
10156
|
-
|
|
10157
|
-
|
|
10299
|
+
/** Rewrite the UNFORWARDED reflex text: drop complete stage-direction parentheticals (STAGE_DIRECTION_RE) and spoken
|
|
10300
|
+
* junk (scrubSpokenJunk). Only the unforwarded region is touched — already-spoken audio can't be unsent, and
|
|
10301
|
+
* splicing before reflexForwarded would corrupt the forward offset. Rewritten per step segment so the stepMarks
|
|
10302
|
+
* after it move with the text (a mark is a reflexBuf offset); a segment that ends at a step mark is complete. */
|
|
10303
|
+
scrubUnforwarded(final) {
|
|
10304
|
+
const from = this.reflexForwarded, buf = this.reflexBuf;
|
|
10305
|
+
if (from >= buf.length) return;
|
|
10306
|
+
const scrub = (seg, complete) => scrubSpokenJunk(seg.replace(/\([^()]*\)/g, (x) => STAGE_DIRECTION_RE.test(x) ? "" : x), complete);
|
|
10307
|
+
const marks = new Set(this.stepMarks.filter((k) => k > from && k <= buf.length));
|
|
10308
|
+
let out = buf.slice(0, from), at = from;
|
|
10309
|
+
const moved = /* @__PURE__ */ new Map();
|
|
10310
|
+
for (const k of [.../* @__PURE__ */ new Set([...marks, buf.length])].sort((x, y) => x - y)) {
|
|
10311
|
+
out += scrub(buf.slice(at, k), final || marks.has(k));
|
|
10312
|
+
moved.set(k, out.length);
|
|
10313
|
+
at = k;
|
|
10314
|
+
}
|
|
10315
|
+
if (out === buf) return;
|
|
10316
|
+
this.reflexBuf = out;
|
|
10317
|
+
this.stepMarks = this.stepMarks.map((k) => moved.get(k) ?? k);
|
|
10158
10318
|
}
|
|
10159
10319
|
/** Clear the per-turn guards. Called at the head of every voice turn (user send + re-voice flush). */
|
|
10160
10320
|
resetTurn() {
|
|
@@ -10168,6 +10328,8 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10168
10328
|
this.reflexForwarded = 0;
|
|
10169
10329
|
this.fabricationCut = false;
|
|
10170
10330
|
this.leakCuts = [];
|
|
10331
|
+
this.repeatCuts = [];
|
|
10332
|
+
this.midPhraseBounced = false;
|
|
10171
10333
|
this.leakStep = this.leakHit = false;
|
|
10172
10334
|
this.stepMarks = [];
|
|
10173
10335
|
this.leakVerdicts.clear();
|
|
@@ -10191,6 +10353,13 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10191
10353
|
return { block: true, reason: "Speculative turn aborted." };
|
|
10192
10354
|
}
|
|
10193
10355
|
if (this.nudging) return { block: true, reason: "Just say one short spoken acknowledgement \u2014 no tools this turn." };
|
|
10356
|
+
if ((call.name === "Act" || call.name === "Think") && !this.turnDispatched && !this.midPhraseBounced) {
|
|
10357
|
+
const said = [...this.voice.transcript].reverse().find((m) => m.role === "user");
|
|
10358
|
+
if (said && endsMidPhrase(contentText(said.content))) {
|
|
10359
|
+
this.midPhraseBounced = true;
|
|
10360
|
+
return { block: true, reason: "The user stopped mid-phrase \u2014 the rest of their request is still coming. Call Hold now (no filler) and wait for it; do not dispatch on a fragment." };
|
|
10361
|
+
}
|
|
10362
|
+
}
|
|
10194
10363
|
if (!this.turnDispatched) return;
|
|
10195
10364
|
if (call.name === "TaskStatus")
|
|
10196
10365
|
return { block: true, reason: "You just dispatched a task this turn \u2014 do NOT poll. Give one short spoken acknowledgement and end your turn; the result arrives later as a [task \u2026] event." };
|
|
@@ -10524,6 +10693,20 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10524
10693
|
spec.decide("abort");
|
|
10525
10694
|
this.notify("diag", "speculation_aborted", { text: spec.text.slice(0, 80) });
|
|
10526
10695
|
}
|
|
10696
|
+
/** Fold a refinement into a RUNNING task's worker (Agent.inject — picked up at its next step) instead of cancel +
|
|
10697
|
+
* respawn: live 20261013-085238 one request spoken over three fragments became t5 → cancel → t6 → cancel → t7, each
|
|
10698
|
+
* worker restarting from zero. Returns the id when amended, undefined when the task is not running (→ dispatch new). */
|
|
10699
|
+
amendTask(id, brief) {
|
|
10700
|
+
const rec = this.tasks.get(id);
|
|
10701
|
+
if (!rec || rec.status !== "running" || !rec.worker || !brief.trim()) return void 0;
|
|
10702
|
+
rec.worker.inject(`[update from the user while you work \u2014 fold it into the SAME task, do not start over]
|
|
10703
|
+
${brief}`);
|
|
10704
|
+
rec.brief += `
|
|
10705
|
+
|
|
10706
|
+
Update: ${brief}`;
|
|
10707
|
+
this.notify("diag", "task_amended", { id, brief: brief.slice(0, 80) });
|
|
10708
|
+
return rec.id;
|
|
10709
|
+
}
|
|
10527
10710
|
/** Cancel a running background task — shared by the CancelTask tool and the CLI /tasks picker. */
|
|
10528
10711
|
cancelTask(id) {
|
|
10529
10712
|
const rec = this.tasks.get(id);
|
|
@@ -10810,14 +10993,15 @@ ${recent}` : brief) + verify + deliverContract;
|
|
|
10810
10993
|
signal: controller.signal
|
|
10811
10994
|
// shared with the checker so a cancel tears down both
|
|
10812
10995
|
};
|
|
10813
|
-
const
|
|
10996
|
+
const worker = new Agent(agentOpts);
|
|
10997
|
+
const promise = worker.run(briefText).then((res) => {
|
|
10814
10998
|
const { spoken, detail } = splitter.flush();
|
|
10815
10999
|
feedSpoken(spoken);
|
|
10816
11000
|
if (detail.trim()) pushTail(detail.trim());
|
|
10817
11001
|
flushSpoken();
|
|
10818
11002
|
return res;
|
|
10819
11003
|
}).then((res) => this.maybeVerify(id, brief, res, tier, agentOpts, askBridge)).then((res) => this.onWorkerSettled(id, res)).catch((err2) => this.onWorkerFailed(id, err2));
|
|
10820
|
-
this.tasks.set(id, { id, label, status: "running", controller, promise, tail, brief, followUp, splitter, createdTurn: this.turn });
|
|
11004
|
+
this.tasks.set(id, { id, label, status: "running", controller, promise, tail, brief, followUp, splitter, createdTurn: this.turn, worker });
|
|
10821
11005
|
if (this.tasks.size > this.options.maxTaskRecords)
|
|
10822
11006
|
for (const [tid, rec] of this.tasks) {
|
|
10823
11007
|
if (this.tasks.size <= this.options.maxTaskRecords) break;
|
|
@@ -11070,14 +11254,17 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11070
11254
|
required: ["brief"],
|
|
11071
11255
|
properties: {
|
|
11072
11256
|
brief: { type: "string", description: "full, self-contained instructions for the worker" },
|
|
11073
|
-
label: { type: "string", description: "a short (2-4 word) label for the task" }
|
|
11257
|
+
label: { type: "string", description: "a short (2-4 word) label for the task" },
|
|
11258
|
+
refines: { type: "string", description: "id of a STILL-RUNNING task this message only adds detail to or corrects (a path, a name, a spelling) \u2014 the running worker is updated in place instead of starting a new task" }
|
|
11074
11259
|
}
|
|
11075
11260
|
},
|
|
11076
|
-
run: async ({ brief, label }) => {
|
|
11261
|
+
run: async ({ brief, label, refines }) => {
|
|
11077
11262
|
this.spokeBeforeDispatch = this.spokeThisTurn;
|
|
11078
11263
|
this.turnDispatched = true;
|
|
11079
11264
|
this.turnBriefs.add(String(brief ?? ""));
|
|
11080
11265
|
this.voice.options.toolChoice = "none";
|
|
11266
|
+
const amended = refines ? this.amendTask(String(refines), String(brief ?? "")) : void 0;
|
|
11267
|
+
if (amended) return this.dispatchResult(amended, "Updated the running");
|
|
11081
11268
|
const id = await this.dispatch(String(brief ?? ""), "act", label ? String(label) : void 0, this.turnFollowUp);
|
|
11082
11269
|
return this.dispatchResult(id, "Acting on");
|
|
11083
11270
|
}
|
|
@@ -11175,29 +11362,36 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11175
11362
|
case "ls": {
|
|
11176
11363
|
if (!fs) return "unavailable (no filesystem)";
|
|
11177
11364
|
const p = String(path ?? ".");
|
|
11178
|
-
|
|
11179
|
-
|
|
11180
|
-
return names.slice(0, 50).join("\n") + (names.length > 50 ? `
|
|
11181
|
-
\u2026 (+${names.length - 50} more)` : "");
|
|
11182
|
-
} catch {
|
|
11183
|
-
const names = await fs.readDir(".").catch(() => []);
|
|
11184
|
-
return `'${p}' not found here \u2014 you are likely already inside it. Current directory listing:
|
|
11185
|
-
` + names.slice(0, 50).join("\n") + (names.length > 50 ? `
|
|
11365
|
+
const list = (at, names) => (at === p ? "" : `(${at})
|
|
11366
|
+
`) + names.slice(0, 50).join("\n") + (names.length > 50 ? `
|
|
11186
11367
|
\u2026 (+${names.length - 50} more)` : "");
|
|
11368
|
+
for (const at of this.lookCandidates(p)) {
|
|
11369
|
+
try {
|
|
11370
|
+
return list(at, await fs.readDir(at));
|
|
11371
|
+
} catch {
|
|
11372
|
+
}
|
|
11187
11373
|
}
|
|
11374
|
+
const cwd = (fs.getCwd?.() ?? "").replace(/\/+$/, "");
|
|
11375
|
+
if (cwd && p.replace(/^\.\/|\/+$/g, "") === cwd.split("/").pop()) return `'${p}' is the current directory:
|
|
11376
|
+
` + list(".", await fs.readDir(".").catch(() => []));
|
|
11377
|
+
return await this.lookMiss(p);
|
|
11188
11378
|
}
|
|
11189
11379
|
case "file": {
|
|
11190
11380
|
if (!fs) return "unavailable (no filesystem)";
|
|
11191
11381
|
if (!path) return "file lookup needs a path";
|
|
11192
|
-
|
|
11193
|
-
|
|
11194
|
-
|
|
11195
|
-
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11382
|
+
for (const at of this.lookCandidates(String(path))) {
|
|
11383
|
+
let text;
|
|
11384
|
+
try {
|
|
11385
|
+
text = await fs.readFile(at);
|
|
11386
|
+
} catch {
|
|
11387
|
+
continue;
|
|
11388
|
+
}
|
|
11389
|
+
const head = at === String(path) ? "" : `(${at})
|
|
11390
|
+
`;
|
|
11391
|
+
return head + (text.length > CAP2 ? text.slice(0, CAP2) + `
|
|
11392
|
+
\u2026 (truncated \u2014 ${text.length} chars total; Act for the full file)` : text);
|
|
11200
11393
|
}
|
|
11394
|
+
return await this.lookMiss(String(path));
|
|
11201
11395
|
}
|
|
11202
11396
|
default:
|
|
11203
11397
|
return `unknown lookup '${what}'`;
|
|
@@ -11208,6 +11402,41 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11208
11402
|
}
|
|
11209
11403
|
};
|
|
11210
11404
|
}
|
|
11405
|
+
/** Where QuickLook tries a path: as given (cwd-relative or absolute), then — relative only — under each lookRoot. */
|
|
11406
|
+
lookCandidates(p) {
|
|
11407
|
+
const rel = p.replace(/^~\/?/, "");
|
|
11408
|
+
const roots = this.options.lookRoots;
|
|
11409
|
+
if (p.startsWith("~")) return roots.length ? [`${roots[0].replace(/\/+$/, "")}/${rel}`.replace(/\/$/, "")] : [p];
|
|
11410
|
+
if (p.startsWith("/")) return [p];
|
|
11411
|
+
return [p, ...roots.map((r) => `${r.replace(/\/+$/, "")}/${rel}`)];
|
|
11412
|
+
}
|
|
11413
|
+
/** A QuickLook path not found anywhere: say where it looked and, at the deepest folder that DID exist, the few closest
|
|
11414
|
+
* names (a mis-heard segment — "Lead Sheets" for "Livshitz" — is usually one of them). Never the whole cwd listing:
|
|
11415
|
+
* live 20261013-085238 dumped it three times for three spellings of the same path. */
|
|
11416
|
+
async lookMiss(p) {
|
|
11417
|
+
const fs = this.options.fs;
|
|
11418
|
+
const segs = p.replace(/^~\/?/, "").split("/").filter((x) => x && x !== ".");
|
|
11419
|
+
const bases = p.startsWith("/") ? ["/"] : p.startsWith("~") ? this.options.lookRoots.slice(0, 1) : [".", ...this.options.lookRoots];
|
|
11420
|
+
let best;
|
|
11421
|
+
for (const base of bases) {
|
|
11422
|
+
let dir = base;
|
|
11423
|
+
for (let i = 0; i < segs.length; i++) {
|
|
11424
|
+
const names = await fs.readDir(dir).catch(() => void 0);
|
|
11425
|
+
if (!names) break;
|
|
11426
|
+
const hit = names.find((n) => n.toLowerCase() === segs[i].toLowerCase());
|
|
11427
|
+
if (!hit) {
|
|
11428
|
+
if (i > (best?.depth ?? -1)) best = { dir, depth: i, names };
|
|
11429
|
+
break;
|
|
11430
|
+
}
|
|
11431
|
+
dir = dir === "." ? hit : `${dir.replace(/\/+$/, "")}/${hit}`;
|
|
11432
|
+
}
|
|
11433
|
+
}
|
|
11434
|
+
const where = ["the current folder", ...this.options.lookRoots].join(", ");
|
|
11435
|
+
if (!best || !best.names.length) return `'${p}' not found (looked under ${where}). Ask the user for the exact path, or Act to search for it.`;
|
|
11436
|
+
const missing = segs[best.depth];
|
|
11437
|
+
const close = [...best.names].sort((x, y) => nameScore(missing, y) - nameScore(missing, x)).slice(0, 6);
|
|
11438
|
+
return `'${p}' not found (looked under ${where}). '${missing}' is not in ${best.dir} \u2014 closest names there: ${close.join(", ")}. If one of these is what they meant (speech-to-text often mishears names), use it; otherwise ask the user or Act to search.`;
|
|
11439
|
+
}
|
|
11211
11440
|
answerTaskTool() {
|
|
11212
11441
|
return {
|
|
11213
11442
|
name: "AnswerTask",
|
|
@@ -11245,13 +11474,21 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11245
11474
|
cancelTaskTool() {
|
|
11246
11475
|
return {
|
|
11247
11476
|
name: "CancelTask",
|
|
11248
|
-
description:
|
|
11249
|
-
parameters: { type: "object", required: ["id"], properties: { id: { type: "string" } } },
|
|
11477
|
+
description: 'Cancel a running background task by id \u2014 or id "all" to cancel EVERY running task (the user said "stop everything", "stop all the searches").',
|
|
11478
|
+
parameters: { type: "object", required: ["id"], properties: { id: { type: "string", description: 'a task id, or "all"' } } },
|
|
11250
11479
|
// Cancel-and-redirect ("stop — do Y instead") is ONE user line: live, the reflex cancelled, said "fetching Y now" and
|
|
11251
11480
|
// never dispatched (2/5 Bare voice runs). The tool result is where it looks next — tell it the redirect needs Act.
|
|
11481
|
+
// "Stop all the searches" cancelled one task (and hit a done one) and left t4 running (live 20261013-085238): the
|
|
11482
|
+
// result always names what is STILL running, and "all" stops every one in a single call.
|
|
11252
11483
|
run: async ({ id }) => {
|
|
11253
|
-
const
|
|
11254
|
-
|
|
11484
|
+
const want = String(id ?? "").trim();
|
|
11485
|
+
const running = () => [...this.tasks.values()].filter((t) => t.status === "running");
|
|
11486
|
+
const ids = /^(?:all|\*|everything)$/i.test(want) ? running().map((t) => t.id) : [want];
|
|
11487
|
+
const lines = ids.length ? ids.map((x) => this.cancelTask(x)) : ["No running tasks."];
|
|
11488
|
+
const left = running();
|
|
11489
|
+
const tail = left.length ? ` Still running: ${left.map((t) => `${t.id} (${t.label})`).join(", ")} \u2014 if the user asked to stop ALL, cancel these too (or call CancelTask with id "all").` : " No tasks are running now.";
|
|
11490
|
+
const cancelled = ids.some((x) => this.tasks.get(x)?.status === "cancelled");
|
|
11491
|
+
return lines.join(" ") + tail + (cancelled ? ` If the user's message ALSO asked for different work (a redirect \u2014 "instead\u2026", "actually\u2026", "do Y"), call Act NOW with that new brief: saying you are on it without calling Act does nothing. If they only asked to stop, just acknowledge in a few words.` : "");
|
|
11255
11492
|
}
|
|
11256
11493
|
};
|
|
11257
11494
|
}
|
|
@@ -13181,6 +13418,8 @@ var VoiceIOOptions = class extends VoiceEngineOptions {
|
|
|
13181
13418
|
sonioxApiKey = process.env.SONIOX_API_KEY ?? "";
|
|
13182
13419
|
cartesiaApiKey = process.env.CARTESIA_API_KEY ?? "";
|
|
13183
13420
|
cartesiaVoiceId = process.env.CARTESIA_VOICE_ID ?? "";
|
|
13421
|
+
/** STT custom vocabulary (names/companies/projects) — biases Soniox toward them (see memoryTerms). */
|
|
13422
|
+
sttTerms = [];
|
|
13184
13423
|
emotions = process.env.VOICE_EMOTIONS !== "0";
|
|
13185
13424
|
// Cartesia inline emotion tags (sonic-3)
|
|
13186
13425
|
/** CLI voice default: mask a slow reflex TTFT with a one-shot varied micro-ack (a FIXED per-turn
|
|
@@ -13211,7 +13450,7 @@ var VoiceIO = class extends VoiceEngine {
|
|
|
13211
13450
|
const duplex = bin ? new AecDuplexAudio(bin) : null;
|
|
13212
13451
|
super({
|
|
13213
13452
|
...o,
|
|
13214
|
-
stt: o.stt ?? new SonioxSTT({ auth: o.sonioxApiKey, source: duplex ?? new NodeMicSource() }),
|
|
13453
|
+
stt: o.stt ?? new SonioxSTT({ auth: o.sonioxApiKey, source: duplex ?? new NodeMicSource(), terms: o.sttTerms }),
|
|
13215
13454
|
tts: o.tts ?? new CartesiaTTS({ auth: o.cartesiaApiKey, voiceId: o.cartesiaVoiceId }),
|
|
13216
13455
|
player: o.player ?? duplex ?? new Player(),
|
|
13217
13456
|
bargeRmsMult: Number(process.env.BARGE_RMS_MULT || o.bargeRmsMult),
|
|
@@ -13405,7 +13644,8 @@ var SCRIPTS = [
|
|
|
13405
13644
|
[new RegExp("\\p{Script=Latin}", "gu"), "latin"]
|
|
13406
13645
|
];
|
|
13407
13646
|
var STOP3 = Object.fromEntries(Object.entries({
|
|
13408
|
-
en: "hi hello hey yes thanks let see check one sec just give right well look the and is are you i it to of that what this was have with for not but my me",
|
|
13647
|
+
en: "hi hello hey yes no a thanks let see check one sec just give right well look the and is are you i it to of that what this was have with for not but my me",
|
|
13648
|
+
// 'a'/'no' are English too: without them 'thanks a lot', 'long time no see' failed fitsLanguage('en')
|
|
13409
13649
|
es: "hola s\xED gracias bueno a me te lo se le del al el la los las que es y de en un una por para con no pero mi yo qu\xE9 est\xE1",
|
|
13410
13650
|
fr: "bonjour salut oui merci le la les est et de des un une que je tu il pas pour avec mais mon qui",
|
|
13411
13651
|
de: "hallo ja danke der die das und ist ich du nicht ein eine mit zu was aber mein wie auch",
|
|
@@ -13540,12 +13780,14 @@ var ChannelBanks = class {
|
|
|
13540
13780
|
if (e.code !== "ENOENT") log29.warn(`bank cache ${f} unreadable \u2014 regenerating: ${e.message}`);
|
|
13541
13781
|
return null;
|
|
13542
13782
|
});
|
|
13543
|
-
const valid = cached && validateBank(cached.bank, lang,
|
|
13783
|
+
const valid = cached && validateBank(cached.bank, lang, 0);
|
|
13784
|
+
const complete = !!valid && BANK_KEYS.every((k) => valid[k].length);
|
|
13544
13785
|
if (valid) {
|
|
13786
|
+
for (const k of BANK_KEYS) if (!valid[k].length) valid[k] = [...this.o.fallback[lang]?.[k] ?? []];
|
|
13545
13787
|
this.banks.set(lang, valid);
|
|
13546
|
-
log29.debug(`bank ${lang}: cache hit (${BANK_KEYS.map((k) => valid[k].length).join("/")})`);
|
|
13788
|
+
log29.debug(`bank ${lang}: cache hit (${BANK_KEYS.map((k) => valid[k].length).join("/")})${complete ? "" : " \u2014 missing kinds, refreshing"}`);
|
|
13547
13789
|
}
|
|
13548
|
-
if (
|
|
13790
|
+
if (complete && this.o.now() - cached.at < this.o.refreshMs) return;
|
|
13549
13791
|
await this.generate(lang, valid);
|
|
13550
13792
|
}
|
|
13551
13793
|
async generate(lang, have) {
|
|
@@ -13562,7 +13804,7 @@ var ChannelBanks = class {
|
|
|
13562
13804
|
bank[k] = [...fresh[k], ...have?.[k] ?? []].filter((x) => !seen.has(norm2(x)) && seen.add(norm2(x))).slice(0, this.o.maxPerKind);
|
|
13563
13805
|
}
|
|
13564
13806
|
this.banks.set(lang, bank);
|
|
13565
|
-
log29.
|
|
13807
|
+
log29.debug(`bank ${lang}: ${have ? "refreshed" : "generated"} (${BANK_KEYS.map((k) => bank[k].length).join("/")}) in ${Math.round(this.o.now() - t0)}ms`);
|
|
13566
13808
|
const f = this.file(lang);
|
|
13567
13809
|
if (f) await mkdir(this.o.cacheDir, { recursive: true }).then(() => writeFile(f, JSON.stringify({ at: this.o.now(), bank }))).catch((e) => log29.warn(`bank cache write ${f} failed: ${e.message}`));
|
|
13568
13810
|
} catch (e) {
|
|
@@ -13678,7 +13920,7 @@ var SecondChannel = class {
|
|
|
13678
13920
|
if (host) this.pinned = true;
|
|
13679
13921
|
const lang = primaryLang(tag);
|
|
13680
13922
|
if (!lang || lang === this.lang) return;
|
|
13681
|
-
log30.
|
|
13923
|
+
log30.debug(`language ${this.lang} \u2192 ${lang}`);
|
|
13682
13924
|
this.lang = lang;
|
|
13683
13925
|
this.recent = [];
|
|
13684
13926
|
this.picks.clear();
|
|
@@ -16907,6 +17149,7 @@ var VERSION = (() => {
|
|
|
16907
17149
|
})();
|
|
16908
17150
|
async function checkForUpdate(current) {
|
|
16909
17151
|
if (current === "?") return null;
|
|
17152
|
+
if (existsSync9(new URL("../.git", import.meta.url))) return null;
|
|
16910
17153
|
const res = await fetch("https://registry.npmjs.org/@livx.cc/agentx/latest", {
|
|
16911
17154
|
signal: AbortSignal.timeout(3e3)
|
|
16912
17155
|
});
|
|
@@ -18569,6 +18812,8 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
18569
18812
|
onTaskStart: async (_id, label) => {
|
|
18570
18813
|
await checkpoints.begin(label);
|
|
18571
18814
|
},
|
|
18815
|
+
lookRoots: [homedir10(), join16(homedir10(), "Documents")],
|
|
18816
|
+
// QuickLook: a spoken "Projects/X" usually means ~/Documents/Projects/X
|
|
18572
18817
|
// The jail deny-lists .git/** (VCS internals can carry credentials), so the engine's fs-based
|
|
18573
18818
|
// 'branch' lookup can't see it — supply it host-side (one safe read-only file).
|
|
18574
18819
|
quickLook: {
|
|
@@ -20201,8 +20446,14 @@ ${out}
|
|
|
20201
20446
|
}
|
|
20202
20447
|
const fakeVoice = process.env.AGENTX_VOICE_FAKE ? fakeVoiceParts(process.env.AGENTX_VOICE_FAKE) : null;
|
|
20203
20448
|
voiceDiag = new JsonlDiagSink(join16(store.dir, `${session.meta.id}.voice.jsonl`));
|
|
20449
|
+
const sttTerms = agent.options.fs && agent.options.memoryDir ? await memoryTerms(agent.options.fs, agent.options.memoryDir, [basename4(agent.options.fs?.getCwd() ?? process.cwd())]).catch((e) => {
|
|
20450
|
+
err(dim(` (stt terms: ${e?.message ?? e})
|
|
20451
|
+
`));
|
|
20452
|
+
return [];
|
|
20453
|
+
}) : [basename4(agent.options.fs?.getCwd() ?? process.cwd())];
|
|
20204
20454
|
voiceIO = new VoiceIO({
|
|
20205
20455
|
...fakeVoice ?? {},
|
|
20456
|
+
sttTerms,
|
|
20206
20457
|
onDiag: (ev) => voiceDiag?.push(ev),
|
|
20207
20458
|
emotions: emotionsOn,
|
|
20208
20459
|
showEmotions,
|
|
@@ -20617,7 +20868,10 @@ function duplexError(e, reflexModel) {
|
|
|
20617
20868
|
const code = e?.statusCode;
|
|
20618
20869
|
return code && !msg.includes(String(code)) ? `${msg} (${code})` : msg;
|
|
20619
20870
|
}
|
|
20620
|
-
var leakDecider = () =>
|
|
20871
|
+
var leakDecider = () => {
|
|
20872
|
+
const o = voiceDeciderOptions();
|
|
20873
|
+
return o ? new DecideClient(o) : void 0;
|
|
20874
|
+
};
|
|
20621
20875
|
function makeSecondChannel(ai, reflexModel, opts) {
|
|
20622
20876
|
const decider = leakDecider();
|
|
20623
20877
|
const instinct = decider ? new Instinct({ decider }) : void 0;
|