@livx.cc/agentx 0.99.96 → 0.99.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{Agent-RHsiEAiP.d.ts → Agent--Kxl9DXO.d.ts} +2 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +140 -23
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +23 -2
- package/dist/index.js +84 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -279,7 +279,8 @@ interface ChatFragment {
|
|
|
279
279
|
* Map a normalized effort to a partial `ChatOptions` fragment for `model`'s provider.
|
|
280
280
|
* Returns `{}` (safe no-op) for `'off'`, undefined, or an unsupported provider.
|
|
281
281
|
*
|
|
282
|
-
* Supported: `anthropic/*` (extended thinking), `openai/*` (reasoning_effort)
|
|
282
|
+
* Supported: `anthropic/*` (extended thinking), `openai/*` (reasoning_effort), `claude-code/*` (its `effort`:
|
|
283
|
+
* low thinks least — a spoken turn's first word comes seconds sooner).
|
|
283
284
|
* `google/*` is intentionally NOT mapped here — its adapter `Object.assign`s
|
|
284
285
|
* providerOptions over the built `generationConfig`, clobbering it; pass an
|
|
285
286
|
* explicit `providerOptions` if you need Gemini thinking.
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { m as RouterState, H as Hooks, n as RunResult, R as ReasoningEffort, A as Agent } from './Agent
|
|
2
|
+
import { m as RouterState, H as Hooks, n as RunResult, R as ReasoningEffort, A as Agent } from './Agent--Kxl9DXO.js';
|
|
3
3
|
import { IFilesystem } from '@livx.cc/wcli/core';
|
|
4
4
|
import { M as Message, U as UserQuestion, H as HostBridge, c as ContentPart, o as MessageContent } from './tools-lC3jn60o.js';
|
|
5
5
|
|
package/dist/cli.js
CHANGED
|
@@ -4645,6 +4645,8 @@ function reasoningToChatFragment(model, effort) {
|
|
|
4645
4645
|
}
|
|
4646
4646
|
case "openai":
|
|
4647
4647
|
return { providerOptions: { reasoning_effort: toLabel(effort) } };
|
|
4648
|
+
case "claude-code":
|
|
4649
|
+
return { providerOptions: { effort: toLabel(effort) } };
|
|
4648
4650
|
default:
|
|
4649
4651
|
return {};
|
|
4650
4652
|
}
|
|
@@ -7694,12 +7696,12 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7694
7696
|
if (!m && bare.length < 80) return "";
|
|
7695
7697
|
this.ackProbe = null;
|
|
7696
7698
|
this.adaptiveAckFired = false;
|
|
7697
|
-
const
|
|
7698
|
-
const norm3 =
|
|
7699
|
+
const lead2 = m ? m[1] : bare;
|
|
7700
|
+
const norm3 = lead2.replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
|
|
7699
7701
|
const same = this.ackPickedFired ? bare.match(_VoiceEngine.leadingPhrase(this.lastAck)) : null;
|
|
7700
7702
|
if (_VoiceEngine.ACK_LEADING.test(norm3)) {
|
|
7701
7703
|
const rest = m ? m[2] : "";
|
|
7702
|
-
this.diag("reflex_ack_suppressed", { lead:
|
|
7704
|
+
this.diag("reflex_ack_suppressed", { lead: lead2.trim() });
|
|
7703
7705
|
if (!rest.trim()) return "";
|
|
7704
7706
|
text = rest;
|
|
7705
7707
|
} else if (same) {
|
|
@@ -9728,6 +9730,17 @@ function refusedCall(e) {
|
|
|
9728
9730
|
const code = Number(e?.statusCode ?? e?.status);
|
|
9729
9731
|
return code === 401 || code === 403 || /API key|unauthori[sz]ed|authentication|invalid x-api-key|permission denied/i.test(String(e?.message ?? e ?? ""));
|
|
9730
9732
|
}
|
|
9733
|
+
function rateLimited(e) {
|
|
9734
|
+
return Number(e?.statusCode ?? e?.status) === 429 || /rate.?limit|RATE_LIMIT|tokens per minute|too many requests/i.test(String(e?.message ?? e?.code ?? e ?? ""));
|
|
9735
|
+
}
|
|
9736
|
+
var LEAD_TAGS = String.raw`^(?:\s*\[[^\]]*\]\s*)*\s*`;
|
|
9737
|
+
var RESULT_CLAIM_RE = new RegExp(LEAD_TAGS + String.raw`(?:(?:all\s+)?done|finished|(?:i\s+)?found|i(?:['’]ve|\s+have)?\s+(?:found|checked|looked|scanned|searched)|there\s+(?:are|is|were|was)\s+no|no\s+(?:files?|results?|matches|sessions?))\b`, "i");
|
|
9738
|
+
var DONE_CLAIM_RE = new RegExp(LEAD_TAGS + String.raw`(?:(?:all\s+)?done|finished|it['’]?s\s+(?:done|finished)|completed?)\b`, "i");
|
|
9739
|
+
function wordOverlap(sentence, prior) {
|
|
9740
|
+
const words3 = (x) => x.replace(/\[[^\]]*\]/g, " ").toLowerCase().replace(/['’]/g, "").match(new RegExp("\\p{L}{4,}", "gu")) ?? [];
|
|
9741
|
+
const have = new Set(words3(prior)), ws = words3(sentence);
|
|
9742
|
+
return ws.length ? ws.filter((w) => have.has(w)).length / ws.length : 0;
|
|
9743
|
+
}
|
|
9731
9744
|
var DuplexAgentOptions = class {
|
|
9732
9745
|
/** Any ai.libx.js AIClient — shared by all tiers (routed by model). */
|
|
9733
9746
|
ai;
|
|
@@ -9737,6 +9750,10 @@ var DuplexAgentOptions = class {
|
|
|
9737
9750
|
// mislabels gpt-oss harmony channels under load, leaking raw analysis into the spoken `final` channel
|
|
9738
9751
|
// (and misfiring Hold). 120b is the same price tier (~$0.15/$0.60) — the quality/cost trade is free.
|
|
9739
9752
|
reflexModel = "groq/openai/gpt-oss-120b";
|
|
9753
|
+
/** A reflex turn REJECTED for load (429 / TPM limit — live 20261013-152733: the org's 120b TPM ran out and the user heard
|
|
9754
|
+
* "could you say that again?") is re-run once on this model — on groq a sibling model has its own rate limit. Only used
|
|
9755
|
+
* for a reflex on the same provider (its key is known to work). Off: ''. */
|
|
9756
|
+
reflexFallbackModel = "groq/openai/gpt-oss-20b";
|
|
9740
9757
|
actModel = latestClaude("sonnet");
|
|
9741
9758
|
/** Premium reasoning model. Set to `false` to disable the Think tier entirely. */
|
|
9742
9759
|
thinkModel = latestClaude("opus");
|
|
@@ -9805,6 +9822,9 @@ var DuplexAgentOptions = class {
|
|
|
9805
9822
|
/** Extra roots QuickLook ls/file try for a relative path not found under the cwd (the CLI passes ~ and ~/Documents:
|
|
9806
9823
|
* a spoken "Projects/X" means ~/Documents/Projects/X far more often than <cwd>/Projects/X). */
|
|
9807
9824
|
lookRoots = [];
|
|
9825
|
+
/** The words the speech-to-text is primed with (names, companies, projects — e.g. memoryTerms → Soniox context.terms), read
|
|
9826
|
+
* live: QuickLook `capabilities` lists them, so "do you have pre-recognized words?" is answered from fact (live 20261013-152733). */
|
|
9827
|
+
sttTerms;
|
|
9808
9828
|
/** Memory directory/directories on the WORKER fs. If set, the voice agent gets Remember + Recall
|
|
9809
9829
|
* tools directly (no delegation needed) and implicit capture guidance. */
|
|
9810
9830
|
memoryDir;
|
|
@@ -9888,7 +9908,7 @@ function isOfferAcceptance(text) {
|
|
|
9888
9908
|
}
|
|
9889
9909
|
var isQuestion = (s) => /\?["'”’)\]]*$/.test(s.trim());
|
|
9890
9910
|
var OFFER_OVERRUN = 150;
|
|
9891
|
-
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 it ENDS mid-sentence or on a hesitation ("and then we", "but the problem is", "so I was, um") \u2014 call `Hold` instead of answering. A hesitation INSIDE a finished message ("can you, um, open it?") does not make it incomplete, and an unclear or odd but finished message is not a fragment: answer it, or ask one short question \u2014 never Hold on it. 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.';
|
|
9911
|
+
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. When the user says you looked in the wrong place, or doubts a "not found", do not defend the search you did: dispatch `Act` with their hint (the place, the id, the tool they name). Only say you checked or searched something you actually checked with a tool this conversation \u2014 name exactly what you looked at.\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 it ENDS mid-sentence or on a hesitation ("and then we", "but the problem is", "so I was, um") \u2014 call `Hold` instead of answering. A hesitation INSIDE a finished message ("can you, um, open it?") does not make it incomplete, and an unclear or odd but finished message is not a fragment: answer it, or ask one short question \u2014 never Hold on it. 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.';
|
|
9892
9912
|
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.";
|
|
9893
9913
|
var THINK_DISABLED_GUIDANCE = "(Think tier is not available \u2014 use Act for all escalations.)";
|
|
9894
9914
|
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).`;
|
|
@@ -9967,6 +9987,10 @@ var DuplexAgent = class _DuplexAgent {
|
|
|
9967
9987
|
// this turn (or its repair) had a leak cut — the dead-air fallback must not blame the user
|
|
9968
9988
|
stepMarks = [];
|
|
9969
9989
|
// reflexBuf offsets where a step ended (its text is complete there)
|
|
9990
|
+
dispatchAt = -1;
|
|
9991
|
+
// reflexBuf offset of this turn's first Act/Think dispatch (-1 = none): text after it is the ack only
|
|
9992
|
+
cutKind;
|
|
9993
|
+
// why claimCut cut the current sentence (diag/log wording)
|
|
9970
9994
|
leakVerdicts = /* @__PURE__ */ new Map();
|
|
9971
9995
|
// judged suspect sentences this turn (true = leak)
|
|
9972
9996
|
judging;
|
|
@@ -10026,7 +10050,7 @@ var DuplexAgent = class _DuplexAgent {
|
|
|
10026
10050
|
];
|
|
10027
10051
|
const workerMcp = mcpNames.length ? `, and it can use these MCP servers: ${[...new Set(mcpNames)].join(", ")}` + (mcpNames.some((n) => /browser/i.test(n)) ? ' \u2014 including driving a REAL browser (open tabs, navigate, click, screenshot), so answer "yes" if asked whether you can control/drive a browser and route an actual browse to Act' : "") : "";
|
|
10028
10052
|
const prompt = VOICE_SYSTEM_PROMPT.replace("{{MEMORY_SLOT}}", memSlot).replace("{{THINK_SLOT}}", thinkSlot).replace("{{WORKER_WEB}}", workerWeb + workerMcp) + (o.quickLook?.sessions ? `
|
|
10029
|
-
Past conversations with this user are saved. When they refer to an EARLIER conversation or session ("last time", "the previous session", "you already did it before", "earlier today"), call QuickLook what:"sessions" (then what:"sessions" with path:"<id>" to read one) BEFORE answering \u2014 never say it does not exist without checking. Report only what the recap says: if it does not say why something happened, say you don't know \u2014 never guess a motive.` : "") + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : "") + (o.emotionTags ? "\n" + EMOTION_TAGS_GUIDANCE : "") + `
|
|
10053
|
+
Past conversations with this user are saved. When they refer to an EARLIER conversation or session ("last time", "the previous session", "you already did it before", "earlier today"), call QuickLook what:"sessions" (then what:"sessions" with path:"<id>" to read one) BEFORE answering \u2014 never say it does not exist without checking. Those are only YOUR (agentx) conversations: sessions of other tools (e.g. a Claude Code session) are not there \u2014 dispatch Act to find those. Report only what the recap says: if it does not say why something happened, say you don't know \u2014 never guess a motive.` : "") + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : "") + (o.emotionTags ? "\n" + EMOTION_TAGS_GUIDANCE : "") + `
|
|
10030
10054
|
${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today", "now", "current", "recent", "latest", "this year" \u2014 to THIS moment. When the user asks about something recent (news, sports, events), they mean near this date, not a well-known past instance; brief your worker with the current year so it searches for what is happening NOW, not a famous older event.`;
|
|
10031
10055
|
const tools = [
|
|
10032
10056
|
...o.reflexOptions?.tools ?? [],
|
|
@@ -10151,7 +10175,8 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10151
10175
|
repeat = e;
|
|
10152
10176
|
break;
|
|
10153
10177
|
}
|
|
10154
|
-
|
|
10178
|
+
this.cutKind = void 0;
|
|
10179
|
+
const v = this.claimCut(sent, from + ok, this.turnSpoken + region.slice(0, ok)) || this.leakVerdict(sent);
|
|
10155
10180
|
if (v === void 0) break;
|
|
10156
10181
|
if (v) {
|
|
10157
10182
|
leak = region.slice(ok, e);
|
|
@@ -10173,8 +10198,9 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10173
10198
|
const at = from + ok;
|
|
10174
10199
|
this.leakHit = true;
|
|
10175
10200
|
this.leakCuts.push(leak.trim().slice(0, 40));
|
|
10176
|
-
|
|
10177
|
-
|
|
10201
|
+
const why = this.cutKind === "claim" ? "stated a result no task has given yet" : this.cutKind === "restart" ? "started a second, restated reply" : "spoke its own analysis";
|
|
10202
|
+
log21.warn(`reflex ${why} ("${leak.trim().slice(0, 40)}\u2026") \u2014 cut before TTS`);
|
|
10203
|
+
this.notify("diag", this.cutKind === "claim" ? "result_claim" : this.cutKind === "restart" ? "reply_restart" : "analysis_leak", { text: leak.trim().slice(0, 80), at: this.turnSpoken.trim() ? "mid_reply" : "before_speech" });
|
|
10178
10204
|
const next = this.stepMarks.find((k) => k > at);
|
|
10179
10205
|
if (next === void 0) {
|
|
10180
10206
|
this.reflexForwarded = buf.length;
|
|
@@ -10225,6 +10251,28 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10225
10251
|
return void 0;
|
|
10226
10252
|
}
|
|
10227
10253
|
static LEAK_JUDGE_TIMEOUT_MS = 1500;
|
|
10254
|
+
/** Deterministic cuts of a complete reflex sentence at buffer offset `at` (`prior` = this turn's reply before it):
|
|
10255
|
+
* - after this turn's dispatch only ONE ack line may follow, and it may not state an outcome (RESULT_CLAIM_RE) — the
|
|
10256
|
+
* task has only just started;
|
|
10257
|
+
* - while any task runs, a "Done."-style completion claim is false (the worker speaks its own result);
|
|
10258
|
+
* - a second reply glued onto a finished one ("…saved.[neutral] I've scanned…", two harmony finals in one step,
|
|
10259
|
+
* live 20261013-152733 msg 62) restating it is dropped with the rest of its step.
|
|
10260
|
+
* A cut goes through the leak path: muted before TTS, scrubbed from the transcript. */
|
|
10261
|
+
claimCut(sentence, at, prior) {
|
|
10262
|
+
const s = sentence.trim();
|
|
10263
|
+
if (!s) return false;
|
|
10264
|
+
const post = this.dispatchAt >= 0 && at >= this.dispatchAt;
|
|
10265
|
+
const running = [...this.tasks.values()].some((t) => t.status === "running");
|
|
10266
|
+
if (post && (/[\p{L}\p{N}]/u.test(this.reflexBuf.slice(this.dispatchAt, at).replace(/\[[^\]]*\]/g, "")) || RESULT_CLAIM_RE.test(s)) || running && DONE_CLAIM_RE.test(s)) {
|
|
10267
|
+
this.cutKind = "claim";
|
|
10268
|
+
return true;
|
|
10269
|
+
}
|
|
10270
|
+
if (at > 0 && /[.!?…]$/.test(this.reflexBuf.slice(0, at)) && /^\[[^\]]+\]/.test(sentence) && wordOverlap(s, prior) >= 0.3) {
|
|
10271
|
+
this.cutKind = "restart";
|
|
10272
|
+
return true;
|
|
10273
|
+
}
|
|
10274
|
+
return false;
|
|
10275
|
+
}
|
|
10228
10276
|
/** The stream is done: settle every pending verdict and release everything the gates still hold. */
|
|
10229
10277
|
async drainReflex() {
|
|
10230
10278
|
for (; ; ) {
|
|
@@ -10306,10 +10354,28 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10306
10354
|
this.armHoldResume();
|
|
10307
10355
|
return;
|
|
10308
10356
|
}
|
|
10309
|
-
if (this.silentTurn && res?.finishReason === "error" && refusedCall(res.error)) this.emitHost({ kind: "text_delta", message: fallback ?? _DuplexAgent.FALLBACK_ERROR });
|
|
10357
|
+
if (this.silentTurn && res?.finishReason === "error" && (refusedCall(res.error) || rateLimited(res.error))) this.emitHost({ kind: "text_delta", message: fallback ?? _DuplexAgent.FALLBACK_ERROR });
|
|
10310
10358
|
else if (this.silentTurn) await this.ackIfSilent(fallback);
|
|
10311
10359
|
else if (this.hollowPromise) await this.followThrough();
|
|
10312
10360
|
}
|
|
10361
|
+
/** One reflex model call for a turn; a turn rejected for load (rateLimited) that has not spoken or dispatched yet is rolled
|
|
10362
|
+
* back and re-run once on `reflexFallbackModel` — a re-prompt of the same model (the dead-air nudge) only hits the
|
|
10363
|
+
* same limit and ends in "could you say that again?". */
|
|
10364
|
+
async reflexSend(content) {
|
|
10365
|
+
const base = this.voice.transcript.length;
|
|
10366
|
+
const res = await this.voice.send(content);
|
|
10367
|
+
const fb = this.options.reflexFallbackModel, model = this.voice.options.model;
|
|
10368
|
+
if (res.finishReason !== "error" || !rateLimited(res.error) || !fb || fb === model || fb.includes("/") && model.includes("/") && fb.split("/")[0] !== model.split("/")[0] || this.turnDispatched || this.turnSpoken.trim()) return res;
|
|
10369
|
+
log21.warn(`reflex ${model} rate-limited \u2014 re-running the turn on ${fb}`);
|
|
10370
|
+
this.notify("diag", "reflex_fallback", { from: model, to: fb });
|
|
10371
|
+
this.voice.transcript.length = base;
|
|
10372
|
+
this.voice.options.model = fb;
|
|
10373
|
+
try {
|
|
10374
|
+
return await this.voice.send(content);
|
|
10375
|
+
} finally {
|
|
10376
|
+
this.voice.options.model = model;
|
|
10377
|
+
}
|
|
10378
|
+
}
|
|
10313
10379
|
/** The turn SAID it is on it ("got it — checking…") but called no tool and nothing is running: a promise nothing
|
|
10314
10380
|
* keeps — the user waits on silence (live Para #798: a follow-up while the reflex believed a finished task still ran). */
|
|
10315
10381
|
get hollowPromise() {
|
|
@@ -10318,7 +10384,8 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10318
10384
|
if ([...this.tasks.values()].some((t) => t.status === "running")) return false;
|
|
10319
10385
|
return _DuplexAgent.PROMISE_RE.test(this.turnSpoken);
|
|
10320
10386
|
}
|
|
10321
|
-
|
|
10387
|
+
// "looking for…"/"searching…" too: live 20261013-152733 msg 88 "Got it, looking for any Claude Code session files" — no tool, no task.
|
|
10388
|
+
static PROMISE_RE = /\b(?:checking|searching|digging|looking (?:into|for|through|up|it up)|pulling (?:up|that|it)|let me (?:check|look|see|find|pull|search|dig|grab)|i(?:'ll| will|['’]m going to) (?:check|look|find|pull|search|dig|grab)|on it|one (?:moment|sec(?:ond)?))\b/i;
|
|
10322
10389
|
static FOLLOW_THROUGH = "[reminder] You told the user you are checking, but no task is running and you called no tool. Keep that promise now: if a finished task result above already answers it, give that answer; otherwise call Act with a clear brief. Do not acknowledge again.";
|
|
10323
10390
|
/** Re-prompt the reflex ONCE, tools allowed, to keep its hollow promise; the reminder never stays in the transcript. */
|
|
10324
10391
|
async followThrough() {
|
|
@@ -10379,6 +10446,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10379
10446
|
this.leakVerdicts.clear();
|
|
10380
10447
|
this.turnSpoken = "";
|
|
10381
10448
|
this.offerOpen = false;
|
|
10449
|
+
this.dispatchAt = -1;
|
|
10382
10450
|
this.dropJudge();
|
|
10383
10451
|
this.turnBase = this.voice.transcript.length;
|
|
10384
10452
|
this.turnFollowUp = false;
|
|
@@ -10461,6 +10529,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10461
10529
|
this.reflexForwarded = 0;
|
|
10462
10530
|
this.stepMarks = [];
|
|
10463
10531
|
this.dropJudge();
|
|
10532
|
+
if (this.dispatchAt >= 0) this.dispatchAt = 0;
|
|
10464
10533
|
this.nudging = true;
|
|
10465
10534
|
try {
|
|
10466
10535
|
await this.voice.send(reminder);
|
|
@@ -10665,7 +10734,7 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10665
10734
|
}
|
|
10666
10735
|
let res;
|
|
10667
10736
|
try {
|
|
10668
|
-
res = await this.
|
|
10737
|
+
res = await this.reflexSend(content);
|
|
10669
10738
|
} finally {
|
|
10670
10739
|
if (signal) {
|
|
10671
10740
|
this.voice.options.signal = ext;
|
|
@@ -10961,7 +11030,7 @@ Update: ${brief}`;
|
|
|
10961
11030
|
const failed = events.find((e) => /^\[task\b[^\]\n]*\bfailed\b/i.test(e));
|
|
10962
11031
|
this.resetTurn();
|
|
10963
11032
|
this.turnFollowUp = nonCleanTurn;
|
|
10964
|
-
const res = await this.
|
|
11033
|
+
const res = await this.reflexSend(events.join("\n"));
|
|
10965
11034
|
await this.endTurn(res, failed ? "Sorry, that didn't work \u2014 the task failed." : void 0);
|
|
10966
11035
|
this.notify("revoice_done", "");
|
|
10967
11036
|
});
|
|
@@ -10977,7 +11046,7 @@ Update: ${brief}`;
|
|
|
10977
11046
|
buildBrief(brief, tier = "act", deliver = true) {
|
|
10978
11047
|
const recent = this.recentExcerpt();
|
|
10979
11048
|
const verify = tier === "act" ? "\n\nBefore reporting done: re-read what you changed and check it against EVERY requirement above \u2014 fix any gap first. Your report is trusted without review." : "";
|
|
10980
|
-
const deliverContract = deliver ?
|
|
11049
|
+
const deliverContract = deliver ? '\n\n## DELIVER (spoken delivery)\nYou are reporting back to a user who is LISTENING. Stream your work normally \u2014 your prose is the written work record and detail, and is NOT spoken. Wrap anything the user should HEAR in <spoken>\u2026</spoken> tags. LEAD WITH the actual content they asked for: if they asked for a specific piece of content \u2014 a value, a name, the actual lines, the writing itself \u2014 that content goes INSIDE the <spoken> tags, not a remark about it. Your FIRST <spoken> segment is substantive \u2014 never a greeting or an acknowledgement (the front-end has already acked; do not double-ack). Keep spoken text concise and natural for the ear: short sentences, no markdown. Never read a file path, URL, command or code aloud, nor spell one out ("slash \u2026 slash \u2026") \u2014 say it the way a person would, by the name of the folder, project, file or site; the exact text stays in your written prose. ' + (this.options.spokenBudgetChars ? `Speak a SHORT summary only: at most three short sentences with the one thing that matters most, then ONE short offer that names what else you have, as a question they can answer in a word \u2014 under ${this.options.spokenBudgetChars} characters in all, the offer included. Everything else stays in your written prose (it is kept and they can ask for it); spoken text past that budget is cut and held until they ask. ` : "") + `NEVER enumerate in speech \u2014 no numbered or bulleted lists ("One. \u2026 Two. \u2026" is robotic). Deliver multiple items as flowing conversation with brief connective phrasing ("here's one\u2026", "and another\u2026", "oh, and\u2026"), pausing between items with sentence breaks, not numbers.` + (this.options.emotionTags ? " Inside <spoken>, you may prefix a sentence with an inline [emotion] tag (e.g. [excited], [curious]) to color how it is voiced \u2014 only when it genuinely fits, and vary it; [laughter] gives a natural laugh." : "") : "";
|
|
10981
11050
|
return `${nowLine()}.
|
|
10982
11051
|
|
|
10983
11052
|
` + (recent ? `${brief}
|
|
@@ -11328,6 +11397,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11328
11397
|
run: async ({ brief, label, refines }) => {
|
|
11329
11398
|
this.spokeBeforeDispatch = this.spokeThisTurn;
|
|
11330
11399
|
this.turnDispatched = true;
|
|
11400
|
+
if (this.dispatchAt < 0) this.dispatchAt = this.reflexBuf.length;
|
|
11331
11401
|
this.turnBriefs.add(String(brief ?? ""));
|
|
11332
11402
|
this.voice.options.toolChoice = "none";
|
|
11333
11403
|
const amended = refines ? this.amendTask(String(refines), String(brief ?? "")) : void 0;
|
|
@@ -11352,6 +11422,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11352
11422
|
run: async ({ brief, label }) => {
|
|
11353
11423
|
this.spokeBeforeDispatch = this.spokeThisTurn;
|
|
11354
11424
|
this.turnDispatched = true;
|
|
11425
|
+
if (this.dispatchAt < 0) this.dispatchAt = this.reflexBuf.length;
|
|
11355
11426
|
this.turnBriefs.add(String(brief ?? ""));
|
|
11356
11427
|
this.voice.options.toolChoice = "none";
|
|
11357
11428
|
const id = await this.dispatch(String(brief ?? ""), "think", label ? String(label) : void 0, this.turnFollowUp);
|
|
@@ -11385,7 +11456,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11385
11456
|
const kinds = [.../* @__PURE__ */ new Set(["time", "branch", "ls", "file", "capabilities", ...Object.keys(this.options.quickLook ?? {})])];
|
|
11386
11457
|
return {
|
|
11387
11458
|
name: "QuickLook",
|
|
11388
|
-
description: `Instant read-only lookup \u2014 one of: ${kinds.join(", ")}. ` + (kinds.includes("sessions") ? '`sessions` = EARLIER conversations with this user: what:"sessions" lists them; what:"sessions" with path:"<session id>" reads one (not what:"file"). ' : "") + "For trivial facts only; anything needing search, commands, or reasoning goes through Act.",
|
|
11459
|
+
description: `Instant read-only lookup \u2014 one of: ${kinds.join(", ")}. ` + (kinds.includes("sessions") ? '`sessions` = EARLIER agentx conversations with this user (only agentx\'s own \u2014 other tools\' sessions need Act): what:"sessions" lists them; what:"sessions" with path:"<session id>" reads one (not what:"file"). ' : "") + "For trivial facts only; anything needing search, commands, or reasoning goes through Act.",
|
|
11389
11460
|
parameters: {
|
|
11390
11461
|
type: "object",
|
|
11391
11462
|
required: ["what"],
|
|
@@ -11406,7 +11477,8 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11406
11477
|
const actTools = this.options.actOptions?.tools ?? [];
|
|
11407
11478
|
const names = actTools.map((t) => t.name);
|
|
11408
11479
|
const mcpServers = Object.keys(this.options.actOptions?.providerOptions?.mcpServers ?? {});
|
|
11409
|
-
const
|
|
11480
|
+
const terms = this.options.sttTerms?.() ?? [];
|
|
11481
|
+
const mcpNote = (mcpServers.length ? ` Plus MCP servers your worker can use: ${mcpServers.join(", ")} (e.g. browser-bridge \u2192 drive a real browser: open tabs, navigate, click, screenshot).` : "") + (terms.length ? ` Speech-to-text is primed with these known words (from memory names + the project; refreshed when voice starts): ${terms.join(", ")}.` : "");
|
|
11410
11482
|
if (!names.length)
|
|
11411
11483
|
return "Your worker uses Act's default local toolset (reading/editing files, running shell commands). No extra tools (e.g. web/internet) are configured; if a request is not a basic file or shell operation, assume you can't do it and say so." + mcpNote;
|
|
11412
11484
|
const hasFetch = names.some((n) => /WebFetch/i.test(n));
|
|
@@ -13708,6 +13780,24 @@ function isPhatic(text, bank) {
|
|
|
13708
13780
|
return !!q2 && (t === q2 || t.startsWith(q2 + " "));
|
|
13709
13781
|
});
|
|
13710
13782
|
}
|
|
13783
|
+
var SKIP = new Set("um uh uhm erm hmm mm oh ok okay so no yes yeah yep well hey hi and but right alright actually like now then wait hang on me speaker \u05D0\u05D5\u05E7\u05D9\u05D9 \u05D8\u05D5\u05D1 \u05D0\u05D6 \u05DC\u05D0 \u05DB\u05DF \u05E8\u05D2\u05E2 \u05D0\u05D4 \u05D0\u05DE \u05D9\u05D0\u05DC\u05DC\u05D4 \u05D5".split(" "));
|
|
13784
|
+
var ASK = "what whats how why where when who which whose is are was were do does did can could would will should shall have has may isnt arent dont doesnt didnt cant wont check open show tell find search look send run go lets please take give make write explain select focus order try compare list use set activate highlight outline capture start stop read play call book create add remove delete put type click close switch help get summarize translate mark keep remind schedule \u05DE\u05D4 \u05D0\u05D9\u05DA \u05DC\u05DE\u05D4 \u05D0\u05D9\u05E4\u05D4 \u05DE\u05EA\u05D9 \u05DE\u05D9 \u05D4\u05D0\u05DD \u05DB\u05DE\u05D4 \u05EA\u05DF \u05EA\u05E0\u05D9 \u05EA\u05D2\u05D9\u05D3 \u05EA\u05D2\u05D9\u05D3\u05D9 \u05D1\u05D5\u05D0 \u05D1\u05D5\u05D0\u05D9".split(" ");
|
|
13785
|
+
var WANT = /^(i (want|need|would like|d like|wanna)|אני (רוצה|צריך|צריכה|מבקש|מבקשת))(\s|$)/u;
|
|
13786
|
+
var lead = (text) => {
|
|
13787
|
+
const w = text.toLowerCase().replace(/[^\p{L}\p{N}\s]+/gu, "").split(/\s+/).filter(Boolean);
|
|
13788
|
+
let i = 0;
|
|
13789
|
+
while (i < w.length && (SKIP.has(w[i]) || /^\d+$/.test(w[i]))) i++;
|
|
13790
|
+
return w.slice(i);
|
|
13791
|
+
};
|
|
13792
|
+
function requestOpener(text) {
|
|
13793
|
+
const w = lead(text);
|
|
13794
|
+
if (!w.length) return false;
|
|
13795
|
+
return ASK.includes(w[0]) || WANT.test(w.slice(0, 4).join(" ")) || new RegExp("^\u05EA\\p{Script=Hebrew}{2,}$", "u").test(w[0]);
|
|
13796
|
+
}
|
|
13797
|
+
function pendingQuestion(text, lastWords = 6) {
|
|
13798
|
+
const t = text.toLowerCase().replace(/[^\p{L}\p{N}'\s]+/gu, " ").split(/\s+/).filter(Boolean).slice(-lastWords).join(" ");
|
|
13799
|
+
return /\b(can|could|would|will|do|does|did|is|are|should)\s+(you|we|i|it|there|they|he|she)\b/.test(t) || /(^|\s)(האם|אתה יכול|את יכולה)(\s|$)/u.test(t);
|
|
13800
|
+
}
|
|
13711
13801
|
var SCRIPTS = [
|
|
13712
13802
|
[new RegExp("\\p{Script=Hebrew}", "gu"), "he"],
|
|
13713
13803
|
[new RegExp("\\p{Script=Arabic}", "gu"), "ar"],
|
|
@@ -13778,13 +13868,14 @@ function cleanLine(s, lang, maxWords = 6) {
|
|
|
13778
13868
|
}
|
|
13779
13869
|
return l.split(/\s+/).length > maxWords || l.length > 60 ? "" : l;
|
|
13780
13870
|
}
|
|
13871
|
+
var SPEAKER_FILLER = /^(?:so|well|actually|basically|anyway|anyways|like|you know|i mean|kind of|sort of|literally|honestly|and|but|because|then|the thing is)\b/i;
|
|
13781
13872
|
function validateBank(raw, lang, minPerKind = 3) {
|
|
13782
13873
|
if (!raw || typeof raw !== "object") return null;
|
|
13783
13874
|
const out = {};
|
|
13784
13875
|
for (const k of BANK_KEYS) {
|
|
13785
13876
|
const v = raw[k];
|
|
13786
13877
|
const seen = /* @__PURE__ */ new Set();
|
|
13787
|
-
out[k] = (Array.isArray(v) ? v : []).filter((x) => typeof x === "string").map((x) => cleanLine(x, lang)).filter((x) => x && !seen.has(norm2(x)) && seen.add(norm2(x)));
|
|
13878
|
+
out[k] = (Array.isArray(v) ? v : []).filter((x) => typeof x === "string").map((x) => cleanLine(x, lang)).filter((x) => x && !(k === "filler" && SPEAKER_FILLER.test(norm2(x))) && !seen.has(norm2(x)) && seen.add(norm2(x)));
|
|
13788
13879
|
if (out[k].length < minPerKind) {
|
|
13789
13880
|
log29.warn(`bank ${lang}: only ${out[k].length} valid ${k} lines \u2014 rejected`);
|
|
13790
13881
|
return null;
|
|
@@ -13973,7 +14064,8 @@ Reply JSON only: {"turn":"...","density":"...","cues":[{"when":["phrase",...],"l
|
|
|
13973
14064
|
// src/voice/secondChannel.ts
|
|
13974
14065
|
var log31 = forComponent("SecondChannel");
|
|
13975
14066
|
var SecondChannelOptions = class {
|
|
13976
|
-
/** Refiner (Jev/fast LLM): the choice over CHANNEL_ACTIONS, or null on failure. Absent → the rule alone decides.
|
|
14067
|
+
/** Refiner (Jev/fast LLM): the choice over CHANNEL_ACTIONS, or null on failure. Absent → the rule alone decides.
|
|
14068
|
+
* `state.rule` is the kind the rule proposed (fastBackchannelDecider answers only 'backchannel' moments). */
|
|
13977
14069
|
decide;
|
|
13978
14070
|
/** Fast-model line writer (see fastLineGenerator). Absent or late → the canned bank. */
|
|
13979
14071
|
generate;
|
|
@@ -14010,7 +14102,11 @@ var SecondChannelOptions = class {
|
|
|
14010
14102
|
avoidLines = 4;
|
|
14011
14103
|
/** Canned picks exclude the last N picks of the same kind. */
|
|
14012
14104
|
bankExclude = 2;
|
|
14013
|
-
|
|
14105
|
+
/** A backchannel needs a substantial chunk first: a turn of ≤8 words is a request/answer, not a story (replay: most
|
|
14106
|
+
* misplaced "mm-hm"s fired at words 6-8 of a short command). */
|
|
14107
|
+
backchannelMinWords = 10;
|
|
14108
|
+
/** A turn that opens as a question/command (requestOpener) holds backchannels until it has grown into an explanation. */
|
|
14109
|
+
requestHoldWords = 18;
|
|
14014
14110
|
/** No new interim for this long mid-utterance (a breath / clause gap, shorter than a pause) → a backchannel moment. */
|
|
14015
14111
|
backchannelPauseMs = 350;
|
|
14016
14112
|
/** A bridge waits this long after the user's turn is dispatched, and plays only if channel-1 has said nothing yet
|
|
@@ -14295,7 +14391,8 @@ var SecondChannel = class {
|
|
|
14295
14391
|
* sentence end only deep into a story — a short request's end is the end of their turn (replay: "mm-hm" right after
|
|
14296
14392
|
* "Check hacker news for recent news.", just before the final). */
|
|
14297
14393
|
backchannelMoment(n, breath) {
|
|
14298
|
-
if (n < this.o.backchannelMinWords || /\?$/.test(this.heard)) return false;
|
|
14394
|
+
if (n < this.o.backchannelMinWords || /\?$/.test(this.heard) || pendingQuestion(this.heard)) return false;
|
|
14395
|
+
if (n < this.o.requestHoldWords && requestOpener(this.heard)) return false;
|
|
14299
14396
|
if (/[.!]$/.test(this.heard)) return n >= 2 * this.o.backchannelMinWords;
|
|
14300
14397
|
return breath || /[,;:…]$/.test(this.heard);
|
|
14301
14398
|
}
|
|
@@ -14314,7 +14411,7 @@ var SecondChannel = class {
|
|
|
14314
14411
|
let kind = rule;
|
|
14315
14412
|
let choice;
|
|
14316
14413
|
if (this.o.decide && !cue) {
|
|
14317
|
-
const d = await withBudget(this.o.decide(this.tail(), this.state(pauseMs)), this.o.decisionBudgetMs, "channel decision");
|
|
14414
|
+
const d = await withBudget(this.o.decide(this.tail(), { ...this.state(pauseMs), rule }), this.o.decisionBudgetMs, "channel decision");
|
|
14318
14415
|
choice = d?.choice;
|
|
14319
14416
|
if (choice && choice in CHANNEL_ACTIONS && (choice === "silence" || this.allowed(choice))) kind = choice;
|
|
14320
14417
|
else if (choice && choice in CHANNEL_ACTIONS) log31.debug(`decider chose ${choice} for a ${rule} moment \u2014 the rule stands`);
|
|
@@ -14394,6 +14491,23 @@ var SecondChannel = class {
|
|
|
14394
14491
|
return true;
|
|
14395
14492
|
}
|
|
14396
14493
|
};
|
|
14494
|
+
function fastBackchannelDecider(ai, model = "groq/qwen/qwen3.8-27b") {
|
|
14495
|
+
return async (heard, state) => {
|
|
14496
|
+
if (state.rule !== "backchannel") return null;
|
|
14497
|
+
const prompt = `Live voice call. The user is still talking and just took a breath. So far: "${heard}"
|
|
14498
|
+
Would an attentive human listener say a tiny "mm-hm" now? Yes ONLY if they are telling a story/explaining across several clauses. No if it is a request, command, question, or they are mid-request. Answer yes or no.`;
|
|
14499
|
+
const reasoning = /qwen/.test(model) ? { providerOptions: { reasoning_effort: "none" } } : /gpt-oss/.test(model) ? { providerOptions: { reasoning_effort: "low" } } : {};
|
|
14500
|
+
const r = await ai.chat({ model, messages: [{ role: "user", content: prompt }], stream: false, maxTokens: /gpt-oss/.test(model) ? 200 : 3, ...reasoning });
|
|
14501
|
+
const a = (r.content ?? "").trim().toLowerCase();
|
|
14502
|
+
return /^yes/.test(a) ? { choice: "backchannel" } : /^no/.test(a) ? { choice: "silence" } : null;
|
|
14503
|
+
};
|
|
14504
|
+
}
|
|
14505
|
+
function channelDecide(ai, instinct, env = process.env) {
|
|
14506
|
+
const model = env.AGENTX_CHANNEL_DECIDER_MODEL || void 0;
|
|
14507
|
+
const bc = model || env.GROQ_API_KEY ? fastBackchannelDecider(ai, model) : void 0;
|
|
14508
|
+
if (!bc && !instinct) return void 0;
|
|
14509
|
+
return async (heard, state) => bc && state.rule === "backchannel" ? bc(heard, state) : instinct ? instinct.channel(heard, state) : null;
|
|
14510
|
+
}
|
|
14397
14511
|
var KIND_HINT2 = {
|
|
14398
14512
|
backchannel: 'a tiny listener cue while they keep talking ("mm-hm", "right", "yeah") \u2014 shows you follow, never takes the turn',
|
|
14399
14513
|
filler: 'a soft listener sound while THEY search for words ("mm\u2026", "mhm", "yeah\u2026") \u2014 never a word that continues their sentence or starts yours',
|
|
@@ -14878,7 +14992,7 @@ function sessionRecap(store, currentId, which, cap = 2500, maxParseBytes = 4e6)
|
|
|
14878
14992
|
const asks = (data2?.messages ?? []).filter((m) => m.role === "user").map(recapLine).filter(Boolean).slice(0, 3).map((l) => `"${l.slice(6, 86)}"`);
|
|
14879
14993
|
return `- ${f.id} (${when(created(f.id))}${data2 ? `, ${data2.meta.turns} turns` : ""})${asks.length ? `: ${asks.join("; ")}` : ""}`;
|
|
14880
14994
|
});
|
|
14881
|
-
return `Earlier sessions (newest first) \u2014 to read one, call QuickLook with what:"sessions" and path:"<id>":
|
|
14995
|
+
return `Earlier agentx sessions in this project (newest first; only agentx's own \u2014 other tools' sessions, e.g. Claude Code's, are not here: dispatch Act for those) \u2014 to read one, call QuickLook with what:"sessions" and path:"<id>":
|
|
14882
14996
|
${rows.join("\n")}`;
|
|
14883
14997
|
}
|
|
14884
14998
|
const recent = others.slice(0, 5).map((f) => f.id).join(", ");
|
|
@@ -14894,7 +15008,7 @@ ${rows.join("\n")}`;
|
|
|
14894
15008
|
hit = pre[0];
|
|
14895
15009
|
}
|
|
14896
15010
|
}
|
|
14897
|
-
if (!hit) return `No session '${w}'. Recent: ${recent}
|
|
15011
|
+
if (!hit) return `No session '${w}'. Recent: ${recent}` + (/^\d{8}-\d{6}/.test(w) ? "" : ` (That id is not an agentx session id \u2014 this lookup covers only agentx's own sessions. A session of another tool, e.g. Claude Code, is not here: dispatch Act to find it.)`);
|
|
14898
15012
|
const data = load(hit);
|
|
14899
15013
|
if (!data) return `Session ${hit.id} is too large (or unreadable) to recap here \u2014 ask a worker (Act) to read it.`;
|
|
14900
15014
|
let body = data.messages.map(recapLine).filter(Boolean).map((l) => l.length > 600 ? `${l.slice(0, 600)}\u2026` : l).join("\n");
|
|
@@ -18868,6 +18982,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
18868
18982
|
let dx;
|
|
18869
18983
|
let voiceIO;
|
|
18870
18984
|
let voiceDiag;
|
|
18985
|
+
let liveSttTerms = [];
|
|
18871
18986
|
const decider = duplex ? leakDecider() : void 0;
|
|
18872
18987
|
const instinct = decider && process.env.VOICE_INSTINCT === "1" ? new Instinct({
|
|
18873
18988
|
decider,
|
|
@@ -19119,6 +19234,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
19119
19234
|
// Cross-session recall: earlier conversations in this project (path = a session id / "previous").
|
|
19120
19235
|
sessions: (which) => sessionRecap(store, session.meta.id, which)
|
|
19121
19236
|
},
|
|
19237
|
+
sttTerms: () => liveSttTerms,
|
|
19122
19238
|
// The voice runs on the REAL fs (it has no fs tools — harmless) so @mentions, !cmd and #note
|
|
19123
19239
|
// resolve against the project; + CC-parity chrome for its own tool calls (⚙ Act …).
|
|
19124
19240
|
reflexOptions: { fs: agent.options.fs, hooks: displayHooks(agent.options.fs), tools: [rewindFilesTool, exitSessionTool(() => {
|
|
@@ -20729,6 +20845,7 @@ ${out}
|
|
|
20729
20845
|
`));
|
|
20730
20846
|
return [];
|
|
20731
20847
|
}) : [basename4(agent.options.fs?.getCwd() ?? process.cwd())];
|
|
20848
|
+
liveSttTerms = sttTerms;
|
|
20732
20849
|
voiceIO = new VoiceIO({
|
|
20733
20850
|
...fakeVoice ?? {},
|
|
20734
20851
|
sttTerms,
|
|
@@ -21161,7 +21278,7 @@ function makeSecondChannel(ai, reflexModel, opts) {
|
|
|
21161
21278
|
return new SecondChannel({
|
|
21162
21279
|
// Banks in the user's language: generated once per language, cached + topped up across sessions (~/.agent/cache).
|
|
21163
21280
|
banks: new ChannelBanks({ generate: fastBankGenerator(ai, model), cacheDir: join16(homedir10(), ".agent", "cache", "channel-banks") }),
|
|
21164
|
-
decide:
|
|
21281
|
+
decide: channelDecide(ai, instinct),
|
|
21165
21282
|
generate: fastLineGenerator(ai, model),
|
|
21166
21283
|
// Anticipated backchannels (off until the offline replay proves them — tests/voice-bc-anticipate-eval.ts)
|
|
21167
21284
|
...process.env.AGENTX_CHANNEL_ANTICIPATE === "1" ? { anticipate: true, plan: (c, s) => planner.plan(c, s) } : {},
|