@livx.cc/agentx 0.99.92 → 0.99.94
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.d.ts +8 -0
- package/dist/cli.js +166 -39
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.js +40 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ interface SessionMeta {
|
|
|
27
27
|
costEstimated?: boolean;
|
|
28
28
|
/** Per-turn forensic log: timing, outcome, usage, and provider error per turn — for investigating a reported issue. */
|
|
29
29
|
events?: TurnEvent[];
|
|
30
|
+
/** claude-code/*: Claude Code's own session for this transcript — resumed while the transcript is still the `n`
|
|
31
|
+
* messages it last saw, so the history is a cached prefix rather than a re-sent flattened prompt. */
|
|
32
|
+
cc?: {
|
|
33
|
+
id: string;
|
|
34
|
+
n: number;
|
|
35
|
+
model: string;
|
|
36
|
+
};
|
|
30
37
|
}
|
|
31
38
|
/** One turn's diagnostics, captured for offline forensics (mirrors what the footer prints, but persisted). */
|
|
32
39
|
interface TurnEvent {
|
|
@@ -134,6 +141,7 @@ interface Args {
|
|
|
134
141
|
version: boolean;
|
|
135
142
|
duplex: boolean;
|
|
136
143
|
duplexV2?: boolean;
|
|
144
|
+
channelOnly?: boolean;
|
|
137
145
|
voiceModel?: string;
|
|
138
146
|
thinkModel?: string | false;
|
|
139
147
|
voice: boolean;
|
package/dist/cli.js
CHANGED
|
@@ -5105,7 +5105,8 @@ var Agent = class _Agent {
|
|
|
5105
5105
|
// a child obeys the parent's allow/deny rules (else Task is a one-call bypass)
|
|
5106
5106
|
tools: [...o.tools, ...this.injectedTools],
|
|
5107
5107
|
systemPrompt: o.systemPrompt,
|
|
5108
|
-
|
|
5108
|
+
// A child never resumes the parent's own claude-code session (the host's `resume` link is the parent's).
|
|
5109
|
+
providerOptionsFor: o.providerOptionsFor ?? ((m) => modelProvider(m) === modelProvider(o.model) && o.providerOptions ? { ...o.providerOptions, resume: void 0, onClaudeCodeSession: void 0 } : void 0)
|
|
5109
5110
|
};
|
|
5110
5111
|
tools = [...tools, makeTaskTool(taskOpts), makeTaskBatchTool(taskOpts)];
|
|
5111
5112
|
}
|
|
@@ -9757,6 +9758,10 @@ var DuplexAgentOptions = class {
|
|
|
9757
9758
|
host;
|
|
9758
9759
|
/** How many recent transcript messages are rendered into a worker's brief. */
|
|
9759
9760
|
excerptTurns = 6;
|
|
9761
|
+
/** A Hold turn whose user says nothing more for this long is answered after all (a re-voice turn told they stopped):
|
|
9762
|
+
* without it a wrong Hold left the turn as a lone "mm-hm" forever. 0 = off. Only for a host that reports the user's
|
|
9763
|
+
* live speech via userActive() — else a slow talker's next words would be spoken over. */
|
|
9764
|
+
holdResumeMs = 0;
|
|
9760
9765
|
/** Voice register: 'neutral' = clean spoken style; 'conversational' = human-like — fillers,
|
|
9761
9766
|
* backchannels, impulsive first reactions before content (mimics real duplex conversation). */
|
|
9762
9767
|
voiceStyle = "neutral";
|
|
@@ -9883,7 +9888,7 @@ function isOfferAcceptance(text) {
|
|
|
9883
9888
|
}
|
|
9884
9889
|
var isQuestion = (s) => /\?["'”’)\]]*$/.test(s.trim());
|
|
9885
9890
|
var OFFER_OVERRUN = 150;
|
|
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.';
|
|
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.';
|
|
9887
9892
|
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.";
|
|
9888
9893
|
var THINK_DISABLED_GUIDANCE = "(Think tier is not available \u2014 use Act for all escalations.)";
|
|
9889
9894
|
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).`;
|
|
@@ -9938,6 +9943,10 @@ var DuplexAgent = class _DuplexAgent {
|
|
|
9938
9943
|
// host spoke on our behalf (adaptive micro-ack) — not reflex output
|
|
9939
9944
|
heldThisTurn = false;
|
|
9940
9945
|
// Hold called this turn → turn is INTENTIONALLY silent (suppress reflex text + no dead-air ack)
|
|
9946
|
+
holdTimer = null;
|
|
9947
|
+
// armed by a Hold turn's end (holdResumeMs)
|
|
9948
|
+
holdResumed = false;
|
|
9949
|
+
// the hold expired: the resume turn must answer (Hold refused) — until the user speaks
|
|
9941
9950
|
nudging = false;
|
|
9942
9951
|
// re-ack pass in flight: block ALL tools, prevent recursion
|
|
9943
9952
|
reflexBuf = "";
|
|
@@ -10293,6 +10302,10 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10293
10302
|
await this.drainReflex();
|
|
10294
10303
|
this.scrubLeakFromTranscript();
|
|
10295
10304
|
this.scrubHeldText();
|
|
10305
|
+
if (this.heldThisTurn) {
|
|
10306
|
+
this.armHoldResume();
|
|
10307
|
+
return;
|
|
10308
|
+
}
|
|
10296
10309
|
if (this.silentTurn && res?.finishReason === "error" && refusedCall(res.error)) this.emitHost({ kind: "text_delta", message: fallback ?? _DuplexAgent.FALLBACK_ERROR });
|
|
10297
10310
|
else if (this.silentTurn) await this.ackIfSilent(fallback);
|
|
10298
10311
|
}
|
|
@@ -10375,6 +10388,27 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10375
10388
|
noteExternalSpeech() {
|
|
10376
10389
|
this.externalSpeech = true;
|
|
10377
10390
|
}
|
|
10391
|
+
/** The user is speaking (a live partial, a barge): a pending Hold expiry waits for them again. */
|
|
10392
|
+
userActive() {
|
|
10393
|
+
if (this.holdTimer) this.armHoldResume();
|
|
10394
|
+
}
|
|
10395
|
+
clearHoldResume() {
|
|
10396
|
+
if (this.holdTimer) clearTimeout(this.holdTimer);
|
|
10397
|
+
this.holdTimer = null;
|
|
10398
|
+
}
|
|
10399
|
+
armHoldResume() {
|
|
10400
|
+
this.clearHoldResume();
|
|
10401
|
+
const ms = this.options.holdResumeMs;
|
|
10402
|
+
if (!ms) return;
|
|
10403
|
+
this.holdTimer = setTimeout(() => {
|
|
10404
|
+
this.holdTimer = null;
|
|
10405
|
+
this.holdResumed = true;
|
|
10406
|
+
this.notify("diag", "hold_expired", { ms });
|
|
10407
|
+
this.queueRevoice(_DuplexAgent.HOLD_EXPIRED);
|
|
10408
|
+
}, ms);
|
|
10409
|
+
this.holdTimer.unref?.();
|
|
10410
|
+
}
|
|
10411
|
+
static HOLD_EXPIRED = "[the user stopped there and did not go on \u2014 respond to what they said now; if it is unclear, ask one short question. Do not Hold.]";
|
|
10378
10412
|
/** True when the just-finished turn voiced NOTHING — dead air to repair. Two ways this happens:
|
|
10379
10413
|
* (a) a dispatch with no spoken ack, and (b) an INLINE turn whose `final` channel came back empty —
|
|
10380
10414
|
* gpt-oss harmony sometimes puts the whole reply in `analysis` (→ thinking_delta, suppressed in
|
|
@@ -10437,6 +10471,8 @@ ${nowLine()}. Anchor every relative or time-sensitive reference \u2014 "today",
|
|
|
10437
10471
|
* second Act). The host gets `turn_replace` {id, replaces, rolledBack, followed?}: only a rollback drops the replaced
|
|
10438
10472
|
* line; `followed` = the kept turn whose follow-up ran (absent: the merged text ran). */
|
|
10439
10473
|
send(content, opts) {
|
|
10474
|
+
this.clearHoldResume();
|
|
10475
|
+
this.holdResumed = false;
|
|
10440
10476
|
if (opts?.replaces) this.replaceTurn(opts.replaces);
|
|
10441
10477
|
const cur = this.voice.options.signal;
|
|
10442
10478
|
const ext = cur && cur === this.turnSignal?.own ? this.turnSignal.ext : cur;
|
|
@@ -11465,6 +11501,8 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
11465
11501
|
}
|
|
11466
11502
|
},
|
|
11467
11503
|
run: async ({ filler }) => {
|
|
11504
|
+
if (this.heldThisTurn) return "Already holding \u2014 stay silent.";
|
|
11505
|
+
if (this.holdResumed) return "Not now \u2014 the user already stopped after those words. Answer them now in one or two short sentences, or ask one short question.";
|
|
11468
11506
|
this.heldThisTurn = true;
|
|
11469
11507
|
this.notify("hold_filler", filler ? String(filler) : "");
|
|
11470
11508
|
return "Holding \u2014 listening for the rest of the user's thought. Do not respond further this turn.";
|
|
@@ -13657,7 +13695,7 @@ var scriptOf = (w) => SCRIPTS.find(([re]) => {
|
|
|
13657
13695
|
re.lastIndex = 0;
|
|
13658
13696
|
return re.test(w);
|
|
13659
13697
|
})?.[1] ?? null;
|
|
13660
|
-
function detectLanguage(text) {
|
|
13698
|
+
function detectLanguage(text, switching = false) {
|
|
13661
13699
|
const ws = text.toLowerCase().match(new RegExp("\\p{L}+", "gu")) ?? [];
|
|
13662
13700
|
const n = /* @__PURE__ */ new Map();
|
|
13663
13701
|
for (const w of ws) {
|
|
@@ -13665,9 +13703,10 @@ function detectLanguage(text) {
|
|
|
13665
13703
|
if (s) n.set(s, (n.get(s) ?? 0) + 1);
|
|
13666
13704
|
}
|
|
13667
13705
|
const [best, c] = [...n].sort((a2, b2) => b2[1] - a2[1])[0] ?? [null, 0];
|
|
13668
|
-
if (best !== "latin") return c >= 1 && (best === "zh" || best === "ja" || c >= 2 || ws.length === 1) ? best : null;
|
|
13706
|
+
if (best !== "latin") return c >= 1 && (best === "zh" || best === "ja" || c >= 2 || ws.length === 1 && !switching) ? best : null;
|
|
13669
13707
|
const score = Object.entries(STOP3).map(([l2, s]) => [l2, ws.filter((w) => s.has(w)).length]).sort((a2, b2) => b2[1] - a2[1]);
|
|
13670
13708
|
const [[l, a], [, b]] = score;
|
|
13709
|
+
if (switching) return a - b >= 2 && ws.length >= 4 ? l : null;
|
|
13671
13710
|
return a > b && (a >= 2 || a === 1 && ws.length <= 3) ? l : null;
|
|
13672
13711
|
}
|
|
13673
13712
|
var NAMES = new Intl.DisplayNames(["en"], { type: "language" });
|
|
@@ -13716,8 +13755,8 @@ function validateBank(raw, lang, minPerKind = 3) {
|
|
|
13716
13755
|
}
|
|
13717
13756
|
var KIND_HINT = {
|
|
13718
13757
|
backchannel: 'tiny listener cues while they keep talking (like "mm-hm", "right") \u2014 never takes the turn',
|
|
13719
|
-
filler: 'soft
|
|
13720
|
-
bridge:
|
|
13758
|
+
filler: 'soft LISTENER sounds while they pause mid-thought (like "mm\u2026", "mhm", "yeah\u2026") \u2014 never a word that continues or completes their sentence ("kind of", "basically", "you know") and never the start of yours',
|
|
13759
|
+
bridge: `neutral one-moment cues while the answer is prepared (like "let me see\u2026", "one sec\u2026") \u2014 never the start of a statement or opinion ("the thing is", "well actually", "here's the deal")`,
|
|
13721
13760
|
phatic: 'greetings, thanks and yes/no openers a person starts a turn with (like "hi", "thanks", "how are you")'
|
|
13722
13761
|
};
|
|
13723
13762
|
function fastBankGenerator(ai, model, perKind = 12) {
|
|
@@ -13750,6 +13789,7 @@ var ChannelBanksOptions = class {
|
|
|
13750
13789
|
fallback = { en: EN_BANK };
|
|
13751
13790
|
now = () => Date.now();
|
|
13752
13791
|
};
|
|
13792
|
+
var BANK_VERSION = 2;
|
|
13753
13793
|
var ChannelBanks = class {
|
|
13754
13794
|
constructor(options) {
|
|
13755
13795
|
this.options = options;
|
|
@@ -13780,6 +13820,10 @@ var ChannelBanks = class {
|
|
|
13780
13820
|
if (e.code !== "ENOENT") log29.warn(`bank cache ${f} unreadable \u2014 regenerating: ${e.message}`);
|
|
13781
13821
|
return null;
|
|
13782
13822
|
});
|
|
13823
|
+
if (cached && cached.v !== BANK_VERSION) {
|
|
13824
|
+
log29.debug(`bank ${lang}: cache v${cached.v ?? 1} predates v${BANK_VERSION} \u2014 regenerating`);
|
|
13825
|
+
cached = null;
|
|
13826
|
+
}
|
|
13783
13827
|
const valid = cached && validateBank(cached.bank, lang, 0);
|
|
13784
13828
|
const complete = !!valid && BANK_KEYS.every((k) => valid[k].length);
|
|
13785
13829
|
if (valid) {
|
|
@@ -13806,7 +13850,7 @@ var ChannelBanks = class {
|
|
|
13806
13850
|
this.banks.set(lang, bank);
|
|
13807
13851
|
log29.debug(`bank ${lang}: ${have ? "refreshed" : "generated"} (${BANK_KEYS.map((k) => bank[k].length).join("/")}) in ${Math.round(this.o.now() - t0)}ms`);
|
|
13808
13852
|
const f = this.file(lang);
|
|
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}`));
|
|
13853
|
+
if (f) await mkdir(this.o.cacheDir, { recursive: true }).then(() => writeFile(f, JSON.stringify({ at: this.o.now(), v: BANK_VERSION, bank }))).catch((e) => log29.warn(`bank cache write ${f} failed: ${e.message}`));
|
|
13810
13854
|
} catch (e) {
|
|
13811
13855
|
log29.warn(`bank ${lang}: generation failed \u2014 ${have ? "keeping the cached bank" : lang in this.o.fallback ? "fallback bank" : "silent"}: ${e.message}`);
|
|
13812
13856
|
} finally {
|
|
@@ -13829,7 +13873,8 @@ var SecondChannelOptions = class {
|
|
|
13829
13873
|
* tell it confidently; the channel is silent until then (never English by default). */
|
|
13830
13874
|
lang;
|
|
13831
13875
|
/** Kinds worth generating (content-conditioned); the rest are canned sounds. */
|
|
13832
|
-
generateFor = ["
|
|
13876
|
+
generateFor = ["bridge"];
|
|
13877
|
+
// a backchannel is timing, not content: canned at 0ms (a generated one arrived after the micro-pause)
|
|
13833
13878
|
/** Host TTS on the second channel; resolves when the line finished playing. `false` = the host could not play it. */
|
|
13834
13879
|
speak = () => {
|
|
13835
13880
|
};
|
|
@@ -13855,7 +13900,12 @@ var SecondChannelOptions = class {
|
|
|
13855
13900
|
avoidLines = 4;
|
|
13856
13901
|
/** Canned picks exclude the last N picks of the same kind. */
|
|
13857
13902
|
bankExclude = 2;
|
|
13858
|
-
backchannelMinWords =
|
|
13903
|
+
backchannelMinWords = 6;
|
|
13904
|
+
/** No new interim for this long mid-utterance (a breath / clause gap, shorter than a pause) → a backchannel moment. */
|
|
13905
|
+
backchannelPauseMs = 350;
|
|
13906
|
+
/** A bridge waits this long after the user's turn is dispatched, and plays only if channel-1 has said nothing yet
|
|
13907
|
+
* (a fast reply/ack needs no bridge — one before it reads as the agent starting a different sentence). 0 = at once. */
|
|
13908
|
+
bridgeDelayMs = 1200;
|
|
13859
13909
|
/** No new interim for this long → a pause (hosts may also call onPause). */
|
|
13860
13910
|
fillerPauseMs = 700;
|
|
13861
13911
|
fillerMinWords = 4;
|
|
@@ -13888,13 +13938,14 @@ var SecondChannel = class {
|
|
|
13888
13938
|
heard = "";
|
|
13889
13939
|
userSpeaking = false;
|
|
13890
13940
|
main = "idle";
|
|
13891
|
-
lead = "";
|
|
13892
13941
|
gen = 0;
|
|
13893
13942
|
// bumped by barge-in / a new user turn → in-flight decisions drop
|
|
13894
13943
|
decidedAt = 0;
|
|
13895
13944
|
// word count at the last decision
|
|
13896
13945
|
busy = false;
|
|
13897
13946
|
pauseTimer = null;
|
|
13947
|
+
bcTimer = null;
|
|
13948
|
+
bridgeTimer = null;
|
|
13898
13949
|
lastAt = -Infinity;
|
|
13899
13950
|
lastKind = null;
|
|
13900
13951
|
live = null;
|
|
@@ -13926,9 +13977,11 @@ var SecondChannel = class {
|
|
|
13926
13977
|
this.picks.clear();
|
|
13927
13978
|
void this.o.banks?.ensure(lang);
|
|
13928
13979
|
}
|
|
13980
|
+
/** Once a language is known, switching needs strong evidence (detectLanguage's `switching`): an English "Um," read as
|
|
13981
|
+
* Portuguese flipped a live session to pt (silence until a pt bank was generated — it then spoke Portuguese). */
|
|
13929
13982
|
detect(text) {
|
|
13930
13983
|
if (this.pinned) return;
|
|
13931
|
-
const l = detectLanguage(text);
|
|
13984
|
+
const l = detectLanguage(text, !!this.lang);
|
|
13932
13985
|
if (l) this.setLanguage(l);
|
|
13933
13986
|
}
|
|
13934
13987
|
get bank() {
|
|
@@ -13950,6 +14003,11 @@ var SecondChannel = class {
|
|
|
13950
14003
|
this.pauseTimer = null;
|
|
13951
14004
|
this.onPause(this.o.fillerPauseMs);
|
|
13952
14005
|
}, this.o.fillerPauseMs);
|
|
14006
|
+
if (this.bcTimer) clearTimeout(this.bcTimer);
|
|
14007
|
+
this.bcTimer = this.o.aec ? setTimeout(() => {
|
|
14008
|
+
this.bcTimer = null;
|
|
14009
|
+
if (this.userSpeaking && this.heard === t) void this.consider("interim", 0, true);
|
|
14010
|
+
}, this.o.backchannelPauseMs) : null;
|
|
13953
14011
|
if (words2(t).length - this.decidedAt >= this.o.decideEveryWords) void this.consider("interim");
|
|
13954
14012
|
}
|
|
13955
14013
|
/** The user went quiet for `ms` (host VAD/STT pause, or the internal interim timer). */
|
|
@@ -13958,6 +14016,10 @@ var SecondChannel = class {
|
|
|
13958
14016
|
clearTimeout(this.pauseTimer);
|
|
13959
14017
|
this.pauseTimer = null;
|
|
13960
14018
|
}
|
|
14019
|
+
if (this.bcTimer) {
|
|
14020
|
+
clearTimeout(this.bcTimer);
|
|
14021
|
+
this.bcTimer = null;
|
|
14022
|
+
}
|
|
13961
14023
|
this.userSpeaking = false;
|
|
13962
14024
|
void this.consider("pause", ms);
|
|
13963
14025
|
}
|
|
@@ -13966,22 +14028,30 @@ var SecondChannel = class {
|
|
|
13966
14028
|
if (s === this.main) return;
|
|
13967
14029
|
this.main = s;
|
|
13968
14030
|
if (s === "first_audio" || s === "speaking") this.live = null;
|
|
14031
|
+
if (s !== "queued" && s !== "thinking" && this.bridgeTimer) {
|
|
14032
|
+
clearTimeout(this.bridgeTimer);
|
|
14033
|
+
this.bridgeTimer = null;
|
|
14034
|
+
}
|
|
13969
14035
|
if (s === "idle") {
|
|
13970
14036
|
this.heard = "";
|
|
13971
14037
|
this.decidedAt = 0;
|
|
13972
|
-
this.lead = "";
|
|
13973
14038
|
this.bridged = false;
|
|
13974
14039
|
return;
|
|
13975
14040
|
}
|
|
13976
14041
|
if (s === "queued") {
|
|
13977
14042
|
this.gen++;
|
|
13978
14043
|
this.userSpeaking = false;
|
|
13979
|
-
this.lead = "";
|
|
13980
14044
|
this.bridged = false;
|
|
13981
|
-
|
|
13982
|
-
|
|
14045
|
+
for (const k of ["pauseTimer", "bcTimer", "bridgeTimer"]) {
|
|
14046
|
+
if (this[k]) clearTimeout(this[k]);
|
|
14047
|
+
this[k] = null;
|
|
14048
|
+
}
|
|
14049
|
+
if (this.o.bridgeDelayMs > 0) this.bridgeTimer = setTimeout(() => {
|
|
14050
|
+
this.bridgeTimer = null;
|
|
14051
|
+
void this.consider("working");
|
|
14052
|
+
}, this.o.bridgeDelayMs);
|
|
14053
|
+
else void this.consider("working");
|
|
13983
14054
|
}
|
|
13984
|
-
if (s === "queued" || s === "thinking" || s === "streaming_text") void this.consider("working");
|
|
13985
14055
|
}
|
|
13986
14056
|
/** The user's final line was dispatched to the main agent (its words are the bridge's micro-context). */
|
|
13987
14057
|
onUserTurn(text) {
|
|
@@ -13990,10 +14060,9 @@ var SecondChannel = class {
|
|
|
13990
14060
|
this.main = "idle";
|
|
13991
14061
|
this.onAgentState("queued");
|
|
13992
14062
|
}
|
|
13993
|
-
/** The main answer's streamed text (before its audio):
|
|
14063
|
+
/** The main answer's streamed text (before its audio): channel-1 is responding — no bridge from here on. */
|
|
13994
14064
|
onMainTokens(text) {
|
|
13995
|
-
if (this.
|
|
13996
|
-
if (this.main === "queued" || this.main === "thinking") this.onAgentState("streaming_text");
|
|
14065
|
+
if (text.trim() && (this.main === "queued" || this.main === "thinking")) this.onAgentState("streaming_text");
|
|
13997
14066
|
}
|
|
13998
14067
|
/** The user barged in: cut the in-flight line now and drop every pending decision. */
|
|
13999
14068
|
onBargeIn() {
|
|
@@ -14022,9 +14091,6 @@ var SecondChannel = class {
|
|
|
14022
14091
|
get audioLocked() {
|
|
14023
14092
|
return this.main === "first_audio" || this.main === "speaking";
|
|
14024
14093
|
}
|
|
14025
|
-
get working() {
|
|
14026
|
-
return this.main === "queued" || this.main === "thinking" || this.main === "streaming_text";
|
|
14027
|
-
}
|
|
14028
14094
|
tail() {
|
|
14029
14095
|
return words2(this.heard).slice(-this.o.microWords).join(" ");
|
|
14030
14096
|
}
|
|
@@ -14033,21 +14099,29 @@ var SecondChannel = class {
|
|
|
14033
14099
|
if (this.audioLocked || this.inflight || this.o.now() - this.lastAt < this.o.minGapMs || k === this.lastKind && (k !== "backchannel" || this.o.now() - this.lastAt < this.o.backchannelGapMs)) return false;
|
|
14034
14100
|
if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle";
|
|
14035
14101
|
if (k === "filler") return !this.userSpeaking && this.main === "idle";
|
|
14036
|
-
return this.
|
|
14102
|
+
return (this.main === "queued" || this.main === "thinking") && !this.bridged;
|
|
14037
14103
|
}
|
|
14038
14104
|
/** 0ms rule: the candidate line for this trigger, or silence. */
|
|
14039
|
-
rule(trigger, pauseMs = 0) {
|
|
14105
|
+
rule(trigger, pauseMs = 0, breath = false) {
|
|
14040
14106
|
const w = words2(this.heard);
|
|
14041
14107
|
if (!this.lang) return "silence";
|
|
14042
14108
|
if (w.length <= 3 || w.length <= 6 && isPhatic(this.heard, this.bank)) return "silence";
|
|
14043
|
-
const k = trigger === "interim" ? w.length
|
|
14109
|
+
const k = trigger === "interim" ? this.backchannelMoment(w.length, breath) ? "backchannel" : null : trigger === "pause" ? pauseMs >= this.o.fillerPauseMs && w.length >= this.o.fillerMinWords ? "filler" : null : w.length >= this.o.bridgeMinWords ? "bridge" : null;
|
|
14044
14110
|
return k && this.allowed(k) ? k : "silence";
|
|
14045
14111
|
}
|
|
14046
|
-
|
|
14112
|
+
/** A clause boundary mid-floor: a breath or a comma-like close. Not a question (it wants the answer, not "mm-hm"), and a
|
|
14113
|
+
* sentence end only deep into a story — a short request's end is the end of their turn (replay: "mm-hm" right after
|
|
14114
|
+
* "Check hacker news for recent news.", just before the final). */
|
|
14115
|
+
backchannelMoment(n, breath) {
|
|
14116
|
+
if (n < this.o.backchannelMinWords || /\?$/.test(this.heard)) return false;
|
|
14117
|
+
if (/[.!]$/.test(this.heard)) return n >= 2 * this.o.backchannelMinWords;
|
|
14118
|
+
return breath || /[,;:…]$/.test(this.heard);
|
|
14119
|
+
}
|
|
14120
|
+
async consider(trigger, pauseMs, breath = false) {
|
|
14047
14121
|
if (this.busy) return;
|
|
14048
14122
|
const n0 = words2(this.heard).length;
|
|
14049
14123
|
this.decidedAt = n0;
|
|
14050
|
-
const rule = this.rule(trigger, pauseMs);
|
|
14124
|
+
const rule = this.rule(trigger, pauseMs, breath);
|
|
14051
14125
|
if (rule === "silence") return;
|
|
14052
14126
|
this.busy = true;
|
|
14053
14127
|
const gen = this.gen, t0 = this.o.now();
|
|
@@ -14087,7 +14161,7 @@ var SecondChannel = class {
|
|
|
14087
14161
|
async line(kind) {
|
|
14088
14162
|
if (this.o.generate && this.o.generateFor.includes(kind)) {
|
|
14089
14163
|
const ac = new AbortController();
|
|
14090
|
-
const ctx = { kind, heard: this.tail(), state: this.main, avoid: [...this.recent], lang: this.lang
|
|
14164
|
+
const ctx = { kind, heard: this.tail(), state: this.main, avoid: [...this.recent], lang: this.lang };
|
|
14091
14165
|
const raw = await withBudget(this.o.generate(ctx, ac.signal), this.o.generateBudgetMs, `${kind} generate`);
|
|
14092
14166
|
if (raw === null) ac.abort();
|
|
14093
14167
|
const text = cleanLine(raw ?? "", this.lang);
|
|
@@ -14135,17 +14209,16 @@ var SecondChannel = class {
|
|
|
14135
14209
|
}
|
|
14136
14210
|
};
|
|
14137
14211
|
var KIND_HINT2 = {
|
|
14138
|
-
backchannel:
|
|
14139
|
-
filler:
|
|
14140
|
-
bridge:
|
|
14212
|
+
backchannel: 'a tiny listener cue while they keep talking ("mm-hm", "right", "yeah") \u2014 shows you follow, never takes the turn',
|
|
14213
|
+
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',
|
|
14214
|
+
bridge: 'a neutral one-moment cue while the answer is prepared ("let me see\u2026", "one sec\u2026") \u2014 never the start of a statement or opinion ("the thing is", "well actually")'
|
|
14141
14215
|
};
|
|
14142
14216
|
function fastLineGenerator(ai, model) {
|
|
14143
14217
|
return async (c, signal) => {
|
|
14144
14218
|
const prompt = `You voice the tiny side-sounds of a live spoken conversation. Write ONE line of 1-5 words: ${KIND_HINT2[c.kind]}.
|
|
14145
14219
|
The user just said: "${c.heard}"
|
|
14146
14220
|
The assistant is: ${c.state}.
|
|
14147
|
-
` + (c.
|
|
14148
|
-
` : "") + (c.avoid.length ? `Do not reuse any of: ${c.avoid.map((a) => `"${a}"`).join(", ")}.
|
|
14221
|
+
` + (c.avoid.length ? `Do not reuse any of: ${c.avoid.map((a) => `"${a}"`).join(", ")}.
|
|
14149
14222
|
` : "") + `React to their actual words, casually, in ${languageName(c.lang)}. Output only the line.`;
|
|
14150
14223
|
const reasoning = /gpt-oss/.test(model) ? { providerOptions: { reasoning_effort: "low" } } : {};
|
|
14151
14224
|
const r = await ai.chat({ model, messages: [{ role: "user", content: prompt }], stream: false, signal, maxTokens: 150, ...reasoning });
|
|
@@ -17283,7 +17356,8 @@ function parseArgs(argv) {
|
|
|
17283
17356
|
else if (x === "--duplex-v2") {
|
|
17284
17357
|
a.duplex = true;
|
|
17285
17358
|
a.duplexV2 = true;
|
|
17286
|
-
} else if (x === "--
|
|
17359
|
+
} else if (x === "--channel-only") a.channelOnly = true;
|
|
17360
|
+
else if (x === "--conversational" || x === "--convo" || x === "--voice") {
|
|
17287
17361
|
a.voice = true;
|
|
17288
17362
|
a.duplex = true;
|
|
17289
17363
|
} else if (x === "--voice-model") a.voiceModel = val(++i, x);
|
|
@@ -17322,10 +17396,11 @@ function parseArgs(argv) {
|
|
|
17322
17396
|
if (a.seed && !a.boddb) throw new Error("--seed only applies with --boddb (it seeds the database from cwd on first run)");
|
|
17323
17397
|
if (a.worktree && (a.vfs || a.boddb)) throw new Error("--worktree is a real-disk concept \u2014 incompatible with --vfs/--boddb");
|
|
17324
17398
|
if (a.duplex && (a.task || a.print)) throw new Error("--duplex is interactive-only (a conversational mode) \u2014 drop the task/-p");
|
|
17325
|
-
if (a.inputFormat && (!a.duplex && !a.print || a.outputFormat !== "stream-json")) throw new Error("--input-format stream-json needs --output-format stream-json and --duplex (or --voice) or -p");
|
|
17399
|
+
if (a.inputFormat && (!a.duplex && !a.print && !a.channelOnly || a.outputFormat !== "stream-json")) throw new Error("--input-format stream-json needs --output-format stream-json and --duplex (or --voice), --channel-only or -p");
|
|
17400
|
+
if (a.channelOnly && !a.inputFormat) throw new Error("--channel-only needs --input-format stream-json --output-format stream-json");
|
|
17326
17401
|
if (a.duplex && a.plan) throw new Error("--plan is not supported in --duplex (workers are non-interactive; a plan could never be approved)");
|
|
17327
17402
|
if (a.voiceSystemPrompt && !a.duplex) throw new Error("--voice-system-prompt only applies with --duplex");
|
|
17328
|
-
if (a.voiceModel && !a.duplex) throw new Error("--voice-model only applies with --duplex");
|
|
17403
|
+
if (a.voiceModel && !a.duplex && !a.channelOnly) throw new Error("--voice-model only applies with --duplex or --channel-only");
|
|
17329
17404
|
if (a.thinkModel !== void 0 && !a.duplex) throw new Error("--think-model/--no-think only apply with --duplex");
|
|
17330
17405
|
if (!canPrompt && (a.ask || a.plan))
|
|
17331
17406
|
throw new Error(`${a.ask ? "--ask" : "--plan"} needs an interactive terminal to approve on \u2014 none is attached (stdin/stderr are not a TTY). For an unattended run use --yes, or pre-authorize with rules in .agent/permissions.json.`);
|
|
@@ -17369,6 +17444,8 @@ Flags:
|
|
|
17369
17444
|
to a background worker agent (-m model); results are re-voiced when ready
|
|
17370
17445
|
--duplex-v2 duplex + a second voice channel: backchannels, fillers and bridges that never
|
|
17371
17446
|
enter the agent's history (headless: stdin interim/pause/agent_state, stdout channel)
|
|
17447
|
+
--channel-only headless (--input-format stream-json): ONLY the second channel, for a host whose main
|
|
17448
|
+
answer comes from elsewhere (stdin user/text/barge/interim/pause/agent_state, stdout channel)
|
|
17372
17449
|
--conversational duplex with a conversation-native register \u2014 short fast turns, fillers,
|
|
17373
17450
|
impulsive reactions, human pacing (implies --duplex; aliases: --convo, --voice)
|
|
17374
17451
|
with SONIOX_API_KEY + CARTESIA_API_KEY(+VOICE_ID) set: real voice I/O \u2014 mic in,
|
|
@@ -18379,6 +18456,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
18379
18456
|
}
|
|
18380
18457
|
return origNotify(e);
|
|
18381
18458
|
};
|
|
18459
|
+
const ccRecord = bindClaudeCodeSession(agent, session);
|
|
18382
18460
|
try {
|
|
18383
18461
|
res = await (sendFn ? sendFn(content) : agent.send(content));
|
|
18384
18462
|
} catch (e) {
|
|
@@ -18430,6 +18508,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
18430
18508
|
const ev = { ts: t0, durationMs: Date.now() - t0, model: agent.options.model, finishReason: res.finishReason, steps: res.steps, tools, tokens: res.usage?.totalTokens, costUsd: cost, estimated: res.usageEstimated };
|
|
18431
18509
|
if (res.finishReason === "error" && res.error) ev.error = errInfo(res.error);
|
|
18432
18510
|
(session.meta.events ??= []).push(ev);
|
|
18511
|
+
ccRecord?.(ok);
|
|
18433
18512
|
if (!session.meta.title) {
|
|
18434
18513
|
session.meta.title = titleOf(agent.transcript);
|
|
18435
18514
|
if (session.meta.title) setTermTitle(`agentx \xB7 ${session.meta.title}`);
|
|
@@ -18442,6 +18521,19 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
18442
18521
|
}
|
|
18443
18522
|
return { ok, res };
|
|
18444
18523
|
}
|
|
18524
|
+
function bindClaudeCodeSession(agent, session) {
|
|
18525
|
+
const po = agent.options.providerOptions;
|
|
18526
|
+
if (!po?.claudeCodeSession) return void 0;
|
|
18527
|
+
const cc = session.meta.cc, model = agent.options.model;
|
|
18528
|
+
let id;
|
|
18529
|
+
const resume = cc && cc.model === model && cc.n === agent.transcript.length ? cc.id : void 0;
|
|
18530
|
+
agent.options.providerOptions = { ...po, resume, onClaudeCodeSession: (s) => {
|
|
18531
|
+
id = s;
|
|
18532
|
+
} };
|
|
18533
|
+
return (ok) => {
|
|
18534
|
+
session.meta.cc = ok && id ? { id, n: agent.transcript.length, model } : void 0;
|
|
18535
|
+
};
|
|
18536
|
+
}
|
|
18445
18537
|
var remintSessionImages = (data, store, cwd) => remintImageRefs(data.messages, [join16(store.dir, `${data.meta.id}.images`), cwd]);
|
|
18446
18538
|
function startSession(args, store, agent, cwd) {
|
|
18447
18539
|
if (args.resume || args.cont) {
|
|
@@ -20479,6 +20571,7 @@ ${out}
|
|
|
20479
20571
|
onPartial: (text) => {
|
|
20480
20572
|
if (text === voicePartial) return;
|
|
20481
20573
|
voicePartial = text;
|
|
20574
|
+
if (text.trim()) dx?.userActive();
|
|
20482
20575
|
if (!partialRedraw) partialRedraw = setTimeout(() => {
|
|
20483
20576
|
partialRedraw = null;
|
|
20484
20577
|
editorRef?.redrawNow();
|
|
@@ -20611,6 +20704,7 @@ ${out}
|
|
|
20611
20704
|
};
|
|
20612
20705
|
try {
|
|
20613
20706
|
await voiceIO.start();
|
|
20707
|
+
if (dx) dx.options.holdResumeMs = HOLD_RESUME_MS;
|
|
20614
20708
|
if (args.duplexV2 && dx) {
|
|
20615
20709
|
const v = voiceIO;
|
|
20616
20710
|
bindSecondChannel(v, makeSecondChannel(ai, dx.options.reflexModel, { show: (t) => {
|
|
@@ -20868,6 +20962,7 @@ function duplexError(e, reflexModel) {
|
|
|
20868
20962
|
const code = e?.statusCode;
|
|
20869
20963
|
return code && !msg.includes(String(code)) ? `${msg} (${code})` : msg;
|
|
20870
20964
|
}
|
|
20965
|
+
var HOLD_RESUME_MS = 4e3;
|
|
20871
20966
|
var leakDecider = () => {
|
|
20872
20967
|
const o = voiceDeciderOptions();
|
|
20873
20968
|
return o ? new DecideClient(o) : void 0;
|
|
@@ -20899,8 +20994,10 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
20899
20994
|
sc?.onMainTokens(String(e.message ?? ""));
|
|
20900
20995
|
} else if (k === "thinking_delta") out({ type: "thinking", text: e.message });
|
|
20901
20996
|
else if (k === "speak_utterance") out({ type: "speak", text: e.message });
|
|
20902
|
-
else if (k === "hold_filler")
|
|
20903
|
-
|
|
20997
|
+
else if (k === "hold_filler") {
|
|
20998
|
+
out({ type: "filler", text: e.message });
|
|
20999
|
+
sc?.onAgentState("idle");
|
|
21000
|
+
} else if (k === "turn_replace") out({ type: "replaced", ...e.data });
|
|
20904
21001
|
else if (k === "revoice_done") {
|
|
20905
21002
|
out({ type: "revoice_done" });
|
|
20906
21003
|
persist();
|
|
@@ -20926,7 +21023,9 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
20926
21023
|
host,
|
|
20927
21024
|
// --voice: the CLI voice register (progress asides, spoken budget). No emotion tags: the host's TTS may not speak them.
|
|
20928
21025
|
...args.voice ? { voiceStyle: "conversational", progressUpdates: true, spokenBudgetChars: 350 } : {},
|
|
20929
|
-
reflexOptions: { fs: agent.options.fs, tools: [exitSessionTool(() => out({ type: "exit" }))] }
|
|
21026
|
+
reflexOptions: { fs: agent.options.fs, tools: [exitSessionTool(() => out({ type: "exit" }))] },
|
|
21027
|
+
...args.duplexV2 ? { holdResumeMs: HOLD_RESUME_MS } : {}
|
|
21028
|
+
// v2 hosts stream the user's partials (userActive) — v1 hosts don't
|
|
20930
21029
|
});
|
|
20931
21030
|
if (args.voiceSystemPrompt) dx.voice.options.systemPrompt += "\n\n" + args.voiceSystemPrompt;
|
|
20932
21031
|
let scTurn = 0;
|
|
@@ -20954,9 +21053,11 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
20954
21053
|
}
|
|
20955
21054
|
if (m.type === "barge") {
|
|
20956
21055
|
sc?.onBargeIn();
|
|
21056
|
+
dx.userActive();
|
|
20957
21057
|
activeTurn?.abort();
|
|
20958
21058
|
dx.parkInFlightDeliveries();
|
|
20959
21059
|
} else if (sc && channelInput(sc, m)) {
|
|
21060
|
+
if (m.type === "interim" && String(m.text ?? "").trim()) dx.userActive();
|
|
20960
21061
|
} else if (m.type === "cancel") out({ type: "task", kind: "cancel_result", id: m.task, message: dx.cancelTask(String(m.task)) });
|
|
20961
21062
|
else if (m.type === "exit") rl.close();
|
|
20962
21063
|
else if (m.type === "user" && typeof m.text === "string" && m.text.trim()) {
|
|
@@ -20989,6 +21090,28 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
20989
21090
|
persist();
|
|
20990
21091
|
await closeMcp(mounted);
|
|
20991
21092
|
}
|
|
21093
|
+
async function channelServe(args, ai, cfg) {
|
|
21094
|
+
const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
|
|
21095
|
+
const { reflexModel, notes } = duplexModels({ voiceModel: args.voiceModel, reflexModel: cfg.reflexModel, thinkModel: false, actModel: String(args.model ?? cfg.model ?? ""), apiKeys: { ...cfg.apiKeys, ...apiKeysFromEnv() } });
|
|
21096
|
+
const model = reflexModel ?? new DuplexAgentOptions().reflexModel;
|
|
21097
|
+
const sc = makeSecondChannel(ai, model, { show: (text, kind) => out({ type: "channel", kind, text }) });
|
|
21098
|
+
out({ type: "ready", model, ...notes.length ? { notes } : {} });
|
|
21099
|
+
const rl = createInterface({ input: process.stdin });
|
|
21100
|
+
rl.on("line", (line) => {
|
|
21101
|
+
let m;
|
|
21102
|
+
try {
|
|
21103
|
+
m = JSON.parse(line);
|
|
21104
|
+
} catch {
|
|
21105
|
+
return;
|
|
21106
|
+
}
|
|
21107
|
+
if (channelInput(sc, m)) return;
|
|
21108
|
+
if (m.type === "user" && typeof m.text === "string") sc.onUserTurn(m.text);
|
|
21109
|
+
else if (m.type === "text" && typeof m.text === "string") sc.onMainTokens(m.text);
|
|
21110
|
+
else if (m.type === "barge") sc.onBargeIn();
|
|
21111
|
+
else if (m.type === "exit") rl.close();
|
|
21112
|
+
});
|
|
21113
|
+
await new Promise((r) => rl.on("close", () => r()));
|
|
21114
|
+
}
|
|
20992
21115
|
function readStdinSteers() {
|
|
20993
21116
|
const early = [];
|
|
20994
21117
|
let sink, got;
|
|
@@ -21158,6 +21281,10 @@ async function main() {
|
|
|
21158
21281
|
worktreeCleanup?.();
|
|
21159
21282
|
process.exit(ok ? 0 : res?.finishReason === "needs_input" ? 2 : 1);
|
|
21160
21283
|
}
|
|
21284
|
+
if (args.inputFormat && args.channelOnly) {
|
|
21285
|
+
await channelServe(args, ai, cfg);
|
|
21286
|
+
process.exit(0);
|
|
21287
|
+
}
|
|
21161
21288
|
if (args.inputFormat) {
|
|
21162
21289
|
await duplexServe(args, ai, cfg, cwd);
|
|
21163
21290
|
worktreeCleanup?.();
|