@livx.cc/agentx 0.97.9 → 0.98.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +347 -106
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +224 -30
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1755,7 +1755,7 @@ function makeRealShellTool(options) {
|
|
|
1755
1755
|
proc.stderr?.on("data", collect);
|
|
1756
1756
|
proc.on("error", (err2) => {
|
|
1757
1757
|
if (err2?.name === "AbortError" || ctl.signal.aborted) return finish(reasonFor(timedOut, timeoutMs, clean(out)));
|
|
1758
|
-
|
|
1758
|
+
log14.debug("shell spawn error", err2);
|
|
1759
1759
|
finish(`[exit 1] ${err2?.message ?? err2}${out ? "\n" + clean(out) : ""}`);
|
|
1760
1760
|
});
|
|
1761
1761
|
proc.on("close", (code) => {
|
|
@@ -1817,7 +1817,7 @@ ${clean(out) || "(no output yet)"}`;
|
|
|
1817
1817
|
}
|
|
1818
1818
|
];
|
|
1819
1819
|
}
|
|
1820
|
-
var
|
|
1820
|
+
var log14, clean, DETACHED, SECRET_ENV_RE, _spawn, ShellJobRegistry, NO_JOB2;
|
|
1821
1821
|
var init_tools_shell = __esm({
|
|
1822
1822
|
"src/tools.shell.ts"() {
|
|
1823
1823
|
"use strict";
|
|
@@ -1825,7 +1825,7 @@ var init_tools_shell = __esm({
|
|
|
1825
1825
|
init_redact();
|
|
1826
1826
|
init_logging();
|
|
1827
1827
|
init_shell_sandbox();
|
|
1828
|
-
|
|
1828
|
+
log14 = forComponent("shell");
|
|
1829
1829
|
clean = (s) => truncateOutput(redactSecrets(s.replace(/\n+$/, "")));
|
|
1830
1830
|
DETACHED = { stdio: ["ignore", "pipe", "pipe"], detached: true };
|
|
1831
1831
|
SECRET_ENV_RE = /(_API_KEY|_TOKEN|_SECRET|_PASSWORD|_PRIVATE_KEY|^AWS_|^GITHUB_TOKEN$|^OPENAI_|^ANTHROPIC_|^GOOGLE_|^GEMINI_|^GROQ_|^NPM_TOKEN$)/i;
|
|
@@ -4582,6 +4582,188 @@ function digestRun(messages, maxChars) {
|
|
|
4582
4582
|
import { MemFilesystem as MemFilesystem2 } from "@livx.cc/wcli/core";
|
|
4583
4583
|
init_logging();
|
|
4584
4584
|
|
|
4585
|
+
// src/voice/emotion.ts
|
|
4586
|
+
init_logging();
|
|
4587
|
+
var log8 = forComponent("Emotion");
|
|
4588
|
+
var EMOTIONS = [
|
|
4589
|
+
// primary (best results)
|
|
4590
|
+
"neutral",
|
|
4591
|
+
"angry",
|
|
4592
|
+
"excited",
|
|
4593
|
+
"content",
|
|
4594
|
+
"sad",
|
|
4595
|
+
"scared",
|
|
4596
|
+
// extended
|
|
4597
|
+
"happy",
|
|
4598
|
+
"enthusiastic",
|
|
4599
|
+
"elated",
|
|
4600
|
+
"triumphant",
|
|
4601
|
+
"amazed",
|
|
4602
|
+
"surprised",
|
|
4603
|
+
"flirtatious",
|
|
4604
|
+
"curious",
|
|
4605
|
+
"calm",
|
|
4606
|
+
"grateful",
|
|
4607
|
+
"affectionate",
|
|
4608
|
+
"sympathetic",
|
|
4609
|
+
"mysterious",
|
|
4610
|
+
"frustrated",
|
|
4611
|
+
"disgusted",
|
|
4612
|
+
"sarcastic",
|
|
4613
|
+
"disappointed",
|
|
4614
|
+
"hurt",
|
|
4615
|
+
"guilty",
|
|
4616
|
+
"bored",
|
|
4617
|
+
"tired",
|
|
4618
|
+
"nostalgic",
|
|
4619
|
+
"apologetic",
|
|
4620
|
+
"hesitant",
|
|
4621
|
+
"confused",
|
|
4622
|
+
"anxious",
|
|
4623
|
+
"panicked",
|
|
4624
|
+
"proud",
|
|
4625
|
+
"confident",
|
|
4626
|
+
"skeptical",
|
|
4627
|
+
"contemplative",
|
|
4628
|
+
"determined"
|
|
4629
|
+
];
|
|
4630
|
+
var VALID = new Set(EMOTIONS);
|
|
4631
|
+
var ALIASES = {
|
|
4632
|
+
cheerful: "happy",
|
|
4633
|
+
joyful: "happy",
|
|
4634
|
+
joy: "happy",
|
|
4635
|
+
glad: "happy",
|
|
4636
|
+
pleased: "happy",
|
|
4637
|
+
warm: "affectionate",
|
|
4638
|
+
thrilled: "excited",
|
|
4639
|
+
eager: "enthusiastic",
|
|
4640
|
+
ecstatic: "elated",
|
|
4641
|
+
euphoric: "elated",
|
|
4642
|
+
mad: "angry",
|
|
4643
|
+
furious: "angry",
|
|
4644
|
+
annoyed: "frustrated",
|
|
4645
|
+
irritated: "frustrated",
|
|
4646
|
+
agitated: "frustrated",
|
|
4647
|
+
shocked: "surprised",
|
|
4648
|
+
astonished: "amazed",
|
|
4649
|
+
wonder: "amazed",
|
|
4650
|
+
worried: "anxious",
|
|
4651
|
+
nervous: "anxious",
|
|
4652
|
+
afraid: "scared",
|
|
4653
|
+
alarmed: "panicked",
|
|
4654
|
+
unsure: "hesitant",
|
|
4655
|
+
uncertain: "hesitant",
|
|
4656
|
+
doubtful: "skeptical",
|
|
4657
|
+
suspicious: "skeptical",
|
|
4658
|
+
thoughtful: "contemplative",
|
|
4659
|
+
focused: "determined",
|
|
4660
|
+
serious: "determined",
|
|
4661
|
+
playful: "flirtatious",
|
|
4662
|
+
teasing: "flirtatious",
|
|
4663
|
+
ironic: "sarcastic",
|
|
4664
|
+
cheeky: "sarcastic",
|
|
4665
|
+
thankful: "grateful",
|
|
4666
|
+
sorry: "apologetic",
|
|
4667
|
+
down: "sad",
|
|
4668
|
+
melancholic: "sad",
|
|
4669
|
+
gloomy: "sad",
|
|
4670
|
+
peaceful: "calm",
|
|
4671
|
+
serene: "calm",
|
|
4672
|
+
relaxed: "calm",
|
|
4673
|
+
sleepy: "tired"
|
|
4674
|
+
};
|
|
4675
|
+
var NONVERBAL = { laughter: "laughter", laughs: "laughter", laugh: "laughter", laughing: "laughter" };
|
|
4676
|
+
function normalizeEmotion(raw) {
|
|
4677
|
+
const k = raw.trim().toLowerCase();
|
|
4678
|
+
if (VALID.has(k)) return k;
|
|
4679
|
+
return ALIASES[k] ?? null;
|
|
4680
|
+
}
|
|
4681
|
+
function resolveTag(raw) {
|
|
4682
|
+
const k = raw.trim().toLowerCase();
|
|
4683
|
+
if (NONVERBAL[k]) return { kind: "nonverbal", value: NONVERBAL[k] };
|
|
4684
|
+
const e = normalizeEmotion(k);
|
|
4685
|
+
return e ? { kind: "emotion", value: e } : null;
|
|
4686
|
+
}
|
|
4687
|
+
var TAG_RE = /\[([a-zA-Z][a-zA-Z ]{0,24})\]/g;
|
|
4688
|
+
var PARTIAL_RE = /\[[a-zA-Z ]*$/;
|
|
4689
|
+
var cartesiaTag = (t) => t.kind === "nonverbal" ? `[${t.value}]` : `<emotion value="${t.value}"/>`;
|
|
4690
|
+
function renderEmotions(text, opts = { show: true }) {
|
|
4691
|
+
let speech = "", display = "", prose = "", last = 0;
|
|
4692
|
+
TAG_RE.lastIndex = 0;
|
|
4693
|
+
for (let m = TAG_RE.exec(text); m; m = TAG_RE.exec(text)) {
|
|
4694
|
+
const before = text.slice(last, m.index);
|
|
4695
|
+
speech += before;
|
|
4696
|
+
display += before;
|
|
4697
|
+
prose += before;
|
|
4698
|
+
const tag = resolveTag(m[1]);
|
|
4699
|
+
if (tag) {
|
|
4700
|
+
speech += cartesiaTag(tag);
|
|
4701
|
+
if (opts.show) display += m[0];
|
|
4702
|
+
} else {
|
|
4703
|
+
log8.debug(`dropping unknown emotion tag ${m[0]}`);
|
|
4704
|
+
}
|
|
4705
|
+
last = m.index + m[0].length;
|
|
4706
|
+
}
|
|
4707
|
+
const tail = text.slice(last);
|
|
4708
|
+
return { speech: speech + tail, display: display + tail, prose: prose + tail };
|
|
4709
|
+
}
|
|
4710
|
+
var EmotionStream = class {
|
|
4711
|
+
constructor(show = true) {
|
|
4712
|
+
this.show = show;
|
|
4713
|
+
}
|
|
4714
|
+
show;
|
|
4715
|
+
buf = "";
|
|
4716
|
+
pending = null;
|
|
4717
|
+
feed(delta) {
|
|
4718
|
+
this.buf += delta;
|
|
4719
|
+
return this.drain(false);
|
|
4720
|
+
}
|
|
4721
|
+
flush() {
|
|
4722
|
+
return this.drain(true);
|
|
4723
|
+
}
|
|
4724
|
+
drain(final) {
|
|
4725
|
+
let body = this.buf;
|
|
4726
|
+
if (!final) {
|
|
4727
|
+
const p = body.match(PARTIAL_RE);
|
|
4728
|
+
if (p) {
|
|
4729
|
+
this.buf = p[0];
|
|
4730
|
+
body = body.slice(0, body.length - p[0].length);
|
|
4731
|
+
} else this.buf = "";
|
|
4732
|
+
} else this.buf = "";
|
|
4733
|
+
let speech = "", display = "", prose = "", last = 0;
|
|
4734
|
+
TAG_RE.lastIndex = 0;
|
|
4735
|
+
for (let m = TAG_RE.exec(body); m; m = TAG_RE.exec(body)) {
|
|
4736
|
+
this.emit(body.slice(last, m.index), (s, d, p) => {
|
|
4737
|
+
speech += s;
|
|
4738
|
+
display += d;
|
|
4739
|
+
prose += p;
|
|
4740
|
+
});
|
|
4741
|
+
const tag = resolveTag(m[1]);
|
|
4742
|
+
if (tag) {
|
|
4743
|
+
this.pending = tag;
|
|
4744
|
+
if (this.show) display += m[0];
|
|
4745
|
+
} else log8.debug(`dropping unknown emotion tag ${m[0]}`);
|
|
4746
|
+
last = m.index + m[0].length;
|
|
4747
|
+
}
|
|
4748
|
+
this.emit(body.slice(last), (s, d, p) => {
|
|
4749
|
+
speech += s;
|
|
4750
|
+
display += d;
|
|
4751
|
+
prose += p;
|
|
4752
|
+
});
|
|
4753
|
+
return { speech, display, prose };
|
|
4754
|
+
}
|
|
4755
|
+
/** Emit a prose span, flushing any pending tag onto its FRONT (only once real words appear). */
|
|
4756
|
+
emit(text, sink) {
|
|
4757
|
+
if (!text) return;
|
|
4758
|
+
let speech = text;
|
|
4759
|
+
if (this.pending && /[\p{L}\p{N}]/u.test(text)) {
|
|
4760
|
+
speech = cartesiaTag(this.pending) + text;
|
|
4761
|
+
this.pending = null;
|
|
4762
|
+
}
|
|
4763
|
+
sink(speech, text, text);
|
|
4764
|
+
}
|
|
4765
|
+
};
|
|
4766
|
+
|
|
4585
4767
|
// src/voice/spokenSplitter.ts
|
|
4586
4768
|
var OPEN = "<spoken>";
|
|
4587
4769
|
var CLOSE = "</spoken>";
|
|
@@ -4655,7 +4837,7 @@ var SpokenSplitter = class {
|
|
|
4655
4837
|
};
|
|
4656
4838
|
|
|
4657
4839
|
// src/duplex.ts
|
|
4658
|
-
var
|
|
4840
|
+
var log9 = forComponent("DuplexAgent");
|
|
4659
4841
|
function describeCall(call) {
|
|
4660
4842
|
const v = call.args && Object.values(call.args).find((x) => typeof x === "string" && x.trim());
|
|
4661
4843
|
const hint = v ? ` (${String(v).replace(/\s+/g, " ").trim().slice(0, 48)})` : "";
|
|
@@ -4694,6 +4876,9 @@ var DuplexAgentOptions = class {
|
|
|
4694
4876
|
/** Voice register: 'neutral' = clean spoken style; 'conversational' = human-like — fillers,
|
|
4695
4877
|
* backchannels, impulsive first reactions before content (mimics real duplex conversation). */
|
|
4696
4878
|
voiceStyle = "neutral";
|
|
4879
|
+
/** Teach the model to emit inline `[emotion]` tags for Cartesia emotion control. Only set when the
|
|
4880
|
+
* TTS actually speaks them — text-duplex (no TTS) would otherwise print literal tags. */
|
|
4881
|
+
emotionTags = false;
|
|
4697
4882
|
/** Awaited BEFORE a worker spawns — open a per-task checkpoint frame, audit, etc.
|
|
4698
4883
|
* (post-spawn would race the worker's first edits). */
|
|
4699
4884
|
onTaskStart;
|
|
@@ -4726,6 +4911,7 @@ var VOICE_SYSTEM_PROMPT = 'You are a spoken voice assistant \u2014 the user HEAR
|
|
|
4726
4911
|
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.";
|
|
4727
4912
|
var THINK_DISABLED_GUIDANCE = "(Think tier is not available \u2014 use Act for all escalations.)";
|
|
4728
4913
|
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).`;
|
|
4914
|
+
var EMOTION_TAGS_GUIDANCE = `EMOTION: your voice is synthesized with emotion control. Prefix a sentence with an inline [emotion] tag, placed directly before the sentence it colors, to shape how it is spoken. Use it ONLY when the emotion genuinely fits the words (it amplifies real feeling, it cannot fake it) \u2014 do not tag every sentence; reserve it for moments that carry feeling, and vary which one you use. You may also drop [laughter] for a natural laugh. Available emotions: ${EMOTIONS.join(", ")}.`;
|
|
4729
4915
|
var DuplexAgent = class _DuplexAgent {
|
|
4730
4916
|
options;
|
|
4731
4917
|
voice;
|
|
@@ -4751,6 +4937,8 @@ var DuplexAgent = class _DuplexAgent {
|
|
|
4751
4937
|
// briefs dispatched this turn (detect identical re-dispatch)
|
|
4752
4938
|
spokeThisTurn = false;
|
|
4753
4939
|
// any non-empty text_delta streamed this turn
|
|
4940
|
+
heldThisTurn = false;
|
|
4941
|
+
// Hold called this turn → turn is INTENTIONALLY silent (suppress reflex text + no dead-air ack)
|
|
4754
4942
|
nudging = false;
|
|
4755
4943
|
// re-ack pass in flight: block ALL tools, prevent recursion
|
|
4756
4944
|
reflexBuf = "";
|
|
@@ -4795,7 +4983,7 @@ var DuplexAgent = class _DuplexAgent {
|
|
|
4795
4983
|
...new Set(workerToolNames.filter((n) => n.startsWith("mcp__")).map((n) => n.slice(5).split("__")[0]))
|
|
4796
4984
|
];
|
|
4797
4985
|
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' : "") : "";
|
|
4798
|
-
const prompt = VOICE_SYSTEM_PROMPT.replace("{{MEMORY_SLOT}}", memSlot).replace("{{THINK_SLOT}}", thinkSlot).replace("{{WORKER_WEB}}", workerWeb + workerMcp) + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : "") + `
|
|
4986
|
+
const prompt = VOICE_SYSTEM_PROMPT.replace("{{MEMORY_SLOT}}", memSlot).replace("{{THINK_SLOT}}", thinkSlot).replace("{{WORKER_WEB}}", workerWeb + workerMcp) + (o.voiceStyle === "conversational" ? "\n" + VOICE_STYLE_CONVERSATIONAL : "") + (o.emotionTags ? "\n" + EMOTION_TAGS_GUIDANCE : "") + `
|
|
4799
4987
|
Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
4800
4988
|
const tools = [
|
|
4801
4989
|
...o.reflexOptions?.tools ?? [],
|
|
@@ -4813,13 +5001,14 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
4813
5001
|
confirm: host.confirm ? (p, m) => host.confirm(p, m) : void 0,
|
|
4814
5002
|
notify: (ev) => {
|
|
4815
5003
|
if (ev?.kind === "text_delta" && typeof ev.message === "string") {
|
|
5004
|
+
if (this.heldThisTurn) return;
|
|
4816
5005
|
if (this.fabricationCut) return;
|
|
4817
5006
|
const msg = ev.message;
|
|
4818
5007
|
this.reflexBuf += msg;
|
|
4819
5008
|
const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
|
|
4820
5009
|
if (m) {
|
|
4821
5010
|
this.fabricationCut = true;
|
|
4822
|
-
|
|
5011
|
+
log9.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
|
|
4823
5012
|
const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
|
|
4824
5013
|
if (!safe) return;
|
|
4825
5014
|
if (safe.trim()) this.spokeThisTurn = true;
|
|
@@ -4882,6 +5071,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
4882
5071
|
this.turnDispatched = false;
|
|
4883
5072
|
this.turnBriefs.clear();
|
|
4884
5073
|
this.spokeThisTurn = false;
|
|
5074
|
+
this.heldThisTurn = false;
|
|
4885
5075
|
this.reflexBuf = "";
|
|
4886
5076
|
this.reflexForwarded = 0;
|
|
4887
5077
|
this.fabricationCut = false;
|
|
@@ -4910,7 +5100,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
4910
5100
|
* voice) and emits an empty `final`, so no text_delta ever streams. Both ship silence; both repair.
|
|
4911
5101
|
* Requires a host: without one there's no stream to detect speech on (and no one to speak to). */
|
|
4912
5102
|
get silentTurn() {
|
|
4913
|
-
return !!this.options.host && !this.spokeThisTurn;
|
|
5103
|
+
return !!this.options.host && !this.spokeThisTurn && !this.heldThisTurn;
|
|
4914
5104
|
}
|
|
4915
5105
|
/** A turn that voiced nothing is dead air. Re-prompt the reflex ONCE so the LLM itself voices a short
|
|
4916
5106
|
* line (no template). If it STILL says nothing, fall back to a minimal line so silence never ships.
|
|
@@ -4921,7 +5111,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
4921
5111
|
try {
|
|
4922
5112
|
await this.voice.send(fallback ? "[reminder] You said nothing to the user this turn. Tell them, in ONE short spoken sentence, what just happened \u2014 no tools." : dispatched ? "[reminder] You dispatched a task but said nothing to the user. Say ONE short spoken acknowledgement now \u2014 no tools." : "[reminder] You said nothing to the user this turn. Give your ONE short spoken reply now \u2014 no tools.");
|
|
4923
5113
|
} catch (e) {
|
|
4924
|
-
|
|
5114
|
+
log9.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
4925
5115
|
} finally {
|
|
4926
5116
|
this.nudging = false;
|
|
4927
5117
|
}
|
|
@@ -5013,7 +5203,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
5013
5203
|
buildBrief(brief, tier = "act", deliver = true) {
|
|
5014
5204
|
const recent = this.voice.transcript.filter((m) => (m.role === "user" || m.role === "assistant") && contentText(m.content).trim()).slice(-this.options.excerptTurns).map((m) => `${m.role}: ${contentText(m.content)}`).join("\n");
|
|
5015
5205
|
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." : "";
|
|
5016
|
-
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." : "";
|
|
5206
|
+
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." + (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." : "") : "";
|
|
5017
5207
|
return (recent ? `${brief}
|
|
5018
5208
|
|
|
5019
5209
|
## Recent conversation (for context)
|
|
@@ -5129,7 +5319,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
5129
5319
|
this.notify("task_verify", `task ${id}: verifying`, { id });
|
|
5130
5320
|
const cres = await new Agent(checkerOpts).run(checkBrief);
|
|
5131
5321
|
if (cres.finishReason !== "stop") {
|
|
5132
|
-
|
|
5322
|
+
log9.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
|
|
5133
5323
|
this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
|
|
5134
5324
|
}
|
|
5135
5325
|
const sum = (a = 0, b = 0) => a + b;
|
|
@@ -5265,7 +5455,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
5265
5455
|
rec.status = "done";
|
|
5266
5456
|
rec.result = res.text;
|
|
5267
5457
|
const incomplete = res.finishReason !== "stop";
|
|
5268
|
-
|
|
5458
|
+
log9.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
|
|
5269
5459
|
this.notify("task_done", `task ${id} (${rec.label}) completed`, {
|
|
5270
5460
|
id,
|
|
5271
5461
|
text: res.text,
|
|
@@ -5291,7 +5481,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
5291
5481
|
this.dropAsk(rec.id);
|
|
5292
5482
|
rec.status = "error";
|
|
5293
5483
|
rec.result = msg;
|
|
5294
|
-
|
|
5484
|
+
log9.warn(`task ${rec.id} failed: ${msg}`);
|
|
5295
5485
|
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
|
|
5296
5486
|
this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
|
|
5297
5487
|
}
|
|
@@ -5471,6 +5661,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
5471
5661
|
}
|
|
5472
5662
|
},
|
|
5473
5663
|
run: async ({ filler }) => {
|
|
5664
|
+
this.heldThisTurn = true;
|
|
5474
5665
|
if (filler) this.notify("hold_filler", String(filler));
|
|
5475
5666
|
return "Holding \u2014 listening for the rest of the user's thought. Do not respond further this turn.";
|
|
5476
5667
|
}
|
|
@@ -5596,7 +5787,7 @@ init_logging();
|
|
|
5596
5787
|
|
|
5597
5788
|
// src/voice/engine.ts
|
|
5598
5789
|
init_logging();
|
|
5599
|
-
var
|
|
5790
|
+
var log10 = forComponent("VoiceEngine");
|
|
5600
5791
|
var now = () => performance.now();
|
|
5601
5792
|
var forSpeech = (t) => t.replace(/[*_`#]+/g, "").replace(/^[ \t]*[-•]\s+/gm, "").replace(/\s*[\u2013\u2014]\s*/g, ", ").replace(/[\u2010\u2011]/g, "-").replace(/\s*\|\s*/g, ", ").replace(/(\d)\s+%/g, "$1%").replace(/\.{3,}/g, ".");
|
|
5602
5793
|
var VoiceEngineOptions = class {
|
|
@@ -5664,6 +5855,11 @@ var VoiceEngineOptions = class {
|
|
|
5664
5855
|
* speech at all, an audible hiccup. Default OFF: the genuine-gated STT partial is the
|
|
5665
5856
|
* mechanism-correct pause trigger; enable only if barge-in onset feels sluggish in a clean-AEC room. */
|
|
5666
5857
|
overlapEnergyHold = false;
|
|
5858
|
+
/** Map inline `[emotion]` tags (emitted by the model, prompt-taught) into Cartesia inline emotion
|
|
5859
|
+
* tags in the spoken transcript (sonic-3 stitches the prosody). false = strip them silently. */
|
|
5860
|
+
emotions = true;
|
|
5861
|
+
/** Show the `[emotion]` tags in the on-screen echo (debug). false = hide (spoken-only). */
|
|
5862
|
+
showEmotions = false;
|
|
5667
5863
|
};
|
|
5668
5864
|
var VoiceEngine = class _VoiceEngine {
|
|
5669
5865
|
options;
|
|
@@ -5709,6 +5905,9 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5709
5905
|
// Central speech queue (above the TTS context): complete worker utterances serialize into ONE
|
|
5710
5906
|
// playback stream, one-at-a-time, never splicing into the live reflex's open utterance.
|
|
5711
5907
|
uttQueue = [];
|
|
5908
|
+
// Per-turn emotion-tag parser (reset on beginSpeech) — converts `[emotion]` → Cartesia inline tags
|
|
5909
|
+
// for TTS, tracks tag-free prose for echo discrimination, and surfaces display text for the screen.
|
|
5910
|
+
emo = null;
|
|
5712
5911
|
constructor(options) {
|
|
5713
5912
|
this.options = { ...new VoiceEngineOptions(), ...options };
|
|
5714
5913
|
const o = this.options;
|
|
@@ -5726,7 +5925,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5726
5925
|
this.stt.onLevel = (rms) => this.handleLevel(rms);
|
|
5727
5926
|
await Promise.all([this.tts.connect(), this.stt.start()]);
|
|
5728
5927
|
this.setState("listening");
|
|
5729
|
-
|
|
5928
|
+
log10.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
|
|
5730
5929
|
}
|
|
5731
5930
|
get usingAec() {
|
|
5732
5931
|
return this.stt.usingAec;
|
|
@@ -5735,6 +5934,10 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5735
5934
|
setBargeIn(on) {
|
|
5736
5935
|
this.options.bargeIn = on;
|
|
5737
5936
|
}
|
|
5937
|
+
/** Show/hide the `[emotion]` debug tags in the echo (next turn's stream picks it up). */
|
|
5938
|
+
setShowEmotions(on) {
|
|
5939
|
+
this.options.showEmotions = on;
|
|
5940
|
+
}
|
|
5738
5941
|
idleWaiters = [];
|
|
5739
5942
|
setState(s) {
|
|
5740
5943
|
if (this.state === s) return;
|
|
@@ -5766,6 +5969,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5766
5969
|
this.ctxOpen = true;
|
|
5767
5970
|
this.spokeDeltas = false;
|
|
5768
5971
|
this.reply = "";
|
|
5972
|
+
this.emo = this.options.emotions ? new EmotionStream(this.options.showEmotions) : null;
|
|
5769
5973
|
this.echoWords = new Set(this.words(this.prevReply));
|
|
5770
5974
|
this.tts.newContext();
|
|
5771
5975
|
if (ack && this.options.ackPhrase) {
|
|
@@ -5776,21 +5980,31 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5776
5980
|
if (!this.turnStartAt) this.turnStartAt = now();
|
|
5777
5981
|
this.setState("thinking");
|
|
5778
5982
|
}
|
|
5983
|
+
/** Feed a spoken delta. Returns the on-screen echo text (emotion tags shown/hidden per config) so the
|
|
5984
|
+
* host renders the SAME stream that was parsed for TTS — no second, state-doubling parse. */
|
|
5779
5985
|
speakDelta(text) {
|
|
5780
|
-
if (this.interrupted) return;
|
|
5986
|
+
if (this.interrupted) return "";
|
|
5781
5987
|
if (!this.speaking || !this.ctxOpen) this.beginSpeech();
|
|
5782
|
-
this.
|
|
5988
|
+
const { speech, display, prose } = this.emo ? this.emo.feed(text) : { speech: text, display: text, prose: text };
|
|
5989
|
+
this.reply += prose;
|
|
5783
5990
|
for (const w of this.words(this.reply)) this.echoWords.add(w);
|
|
5784
|
-
this.tts.speak(forSpeech(
|
|
5785
|
-
if (!this.spokeDeltas && this.turnStartAt)
|
|
5991
|
+
this.tts.speak(forSpeech(speech), true);
|
|
5992
|
+
if (!this.spokeDeltas && this.turnStartAt) log10.debug(`ttft: ${Math.round(now() - this.turnStartAt)}ms`);
|
|
5786
5993
|
this.spokeDeltas = true;
|
|
5787
5994
|
this.setState("speaking");
|
|
5995
|
+
return display;
|
|
5788
5996
|
}
|
|
5789
5997
|
/** close the spoken turn (idempotent); stays audible until ALL audio arrived AND playback drains */
|
|
5790
5998
|
endSpeech() {
|
|
5791
5999
|
this.interrupted = false;
|
|
5792
6000
|
if (!this.speaking) return;
|
|
5793
6001
|
this.ctxOpen = false;
|
|
6002
|
+
if (this.emo) {
|
|
6003
|
+
const t = this.emo.flush();
|
|
6004
|
+
this.emo = null;
|
|
6005
|
+
if (t.prose) this.reply += t.prose;
|
|
6006
|
+
if (t.speech) this.tts.speak(forSpeech(t.speech), true);
|
|
6007
|
+
}
|
|
5794
6008
|
if (this.reply) this.prevReply = this.reply;
|
|
5795
6009
|
const settle = () => {
|
|
5796
6010
|
if (this.ctxOpen) {
|
|
@@ -5803,7 +6017,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5803
6017
|
}
|
|
5804
6018
|
this.drainTimer = null;
|
|
5805
6019
|
this.speaking = false;
|
|
5806
|
-
if (this.turnStartAt)
|
|
6020
|
+
if (this.turnStartAt) log10.debug(`turn: ${Math.round(now() - this.turnStartAt)}ms (incl. playback)`);
|
|
5807
6021
|
this.echoUntil = now() + 2500;
|
|
5808
6022
|
if (!this.usingAec) this.stt.reset();
|
|
5809
6023
|
this.setState("listening");
|
|
@@ -5995,7 +6209,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5995
6209
|
this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
|
|
5996
6210
|
if (this.pendingTimer) clearTimeout(this.pendingTimer);
|
|
5997
6211
|
if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
|
|
5998
|
-
|
|
6212
|
+
log10.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
|
|
5999
6213
|
this.options.onHold();
|
|
6000
6214
|
if (this.options.holdFiller && !this.speaking) {
|
|
6001
6215
|
this.beginSpeech();
|
|
@@ -6094,7 +6308,7 @@ async function resolveAuth(auth) {
|
|
|
6094
6308
|
}
|
|
6095
6309
|
|
|
6096
6310
|
// src/voice/soniox.ts
|
|
6097
|
-
var
|
|
6311
|
+
var log11 = forComponent("SonioxSTT");
|
|
6098
6312
|
var now2 = () => performance.now();
|
|
6099
6313
|
var SonioxSTTOptions = class {
|
|
6100
6314
|
auth = "";
|
|
@@ -6163,9 +6377,9 @@ var SonioxSTT = class {
|
|
|
6163
6377
|
this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
|
|
6164
6378
|
this.ws.onclose = (ev) => {
|
|
6165
6379
|
if (this.stopped) return;
|
|
6166
|
-
|
|
6380
|
+
log11.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
|
|
6167
6381
|
this.reset();
|
|
6168
|
-
this.connectWs().catch((e) =>
|
|
6382
|
+
this.connectWs().catch((e) => log11.error(`soniox reconnect failed: ${e.message}`));
|
|
6169
6383
|
};
|
|
6170
6384
|
}
|
|
6171
6385
|
async start() {
|
|
@@ -6175,7 +6389,7 @@ var SonioxSTT = class {
|
|
|
6175
6389
|
this.endpointTimer = setInterval(() => {
|
|
6176
6390
|
const combined = (this.finalText + this.partialText).trim();
|
|
6177
6391
|
if (!combined || now2() - this.lastChangeAt < this.options.silenceEndpointMs) return;
|
|
6178
|
-
if (this.firstTokenAt)
|
|
6392
|
+
if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
|
|
6179
6393
|
this.reset();
|
|
6180
6394
|
this.onUtterance(combined, now2());
|
|
6181
6395
|
}, 120);
|
|
@@ -6187,7 +6401,7 @@ var SonioxSTT = class {
|
|
|
6187
6401
|
if (this.stopped) return;
|
|
6188
6402
|
const ref = this.lastChunkAt || this.startedChunksAt;
|
|
6189
6403
|
if (now2() - ref > noAudioMs) {
|
|
6190
|
-
|
|
6404
|
+
log11.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
|
|
6191
6405
|
this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
|
|
6192
6406
|
this.stop();
|
|
6193
6407
|
}
|
|
@@ -6207,7 +6421,7 @@ var SonioxSTT = class {
|
|
|
6207
6421
|
});
|
|
6208
6422
|
}
|
|
6209
6423
|
handle(m) {
|
|
6210
|
-
if (m.error_message) return
|
|
6424
|
+
if (m.error_message) return log11.error(`soniox: ${m.error_message}`);
|
|
6211
6425
|
let endpoint = false;
|
|
6212
6426
|
for (const t of m.tokens ?? []) {
|
|
6213
6427
|
if (t.text === "<end>") endpoint = true;
|
|
@@ -6223,7 +6437,7 @@ var SonioxSTT = class {
|
|
|
6223
6437
|
this.onPartial(combined);
|
|
6224
6438
|
if (endpoint && this.finalText.trim()) {
|
|
6225
6439
|
const utterance = this.finalText.trim();
|
|
6226
|
-
if (this.firstTokenAt)
|
|
6440
|
+
if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
|
|
6227
6441
|
this.reset();
|
|
6228
6442
|
this.onUtterance(utterance, now2());
|
|
6229
6443
|
}
|
|
@@ -6246,7 +6460,7 @@ var SonioxSTT = class {
|
|
|
6246
6460
|
|
|
6247
6461
|
// src/voice/cartesia.ts
|
|
6248
6462
|
init_logging();
|
|
6249
|
-
var
|
|
6463
|
+
var log12 = forComponent("CartesiaTTS");
|
|
6250
6464
|
var now3 = () => performance.now();
|
|
6251
6465
|
var CartesiaTTSOptions = class {
|
|
6252
6466
|
auth = "";
|
|
@@ -6296,9 +6510,9 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
6296
6510
|
this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
|
|
6297
6511
|
});
|
|
6298
6512
|
this.ws.onclose = (ev) => {
|
|
6299
|
-
|
|
6513
|
+
log12.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
|
|
6300
6514
|
if (!this.closed) {
|
|
6301
|
-
this.connecting = this.doConnect().catch((e) =>
|
|
6515
|
+
this.connecting = this.doConnect().catch((e) => log12.error(`cartesia reconnect failed: ${e.message}`));
|
|
6302
6516
|
}
|
|
6303
6517
|
};
|
|
6304
6518
|
this.ws.onmessage = (ev) => {
|
|
@@ -6320,11 +6534,11 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
6320
6534
|
this.down = true;
|
|
6321
6535
|
this.downAt = now3();
|
|
6322
6536
|
this.consecutiveOk = 0;
|
|
6323
|
-
|
|
6537
|
+
log12.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
|
|
6324
6538
|
this.onDone();
|
|
6325
6539
|
this.startProbe();
|
|
6326
6540
|
} else if (!this.down) {
|
|
6327
|
-
|
|
6541
|
+
log12.warn(`cartesia: ${JSON.stringify(m)}`);
|
|
6328
6542
|
}
|
|
6329
6543
|
}
|
|
6330
6544
|
};
|
|
@@ -6338,7 +6552,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
6338
6552
|
this.consecutiveOk = 0;
|
|
6339
6553
|
this.stopProbe();
|
|
6340
6554
|
const downMs = this.downAt ? now3() - this.downAt : 0;
|
|
6341
|
-
(downMs < 2e3 ?
|
|
6555
|
+
(downMs < 2e3 ? log12.debug : log12.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
|
|
6342
6556
|
}
|
|
6343
6557
|
/** Ensure the WS is open before sending — reconnects if idle-closed. */
|
|
6344
6558
|
async ensureConnected() {
|
|
@@ -6418,7 +6632,7 @@ import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor a
|
|
|
6418
6632
|
init_logging();
|
|
6419
6633
|
import { spawn } from "child_process";
|
|
6420
6634
|
import { createHash } from "crypto";
|
|
6421
|
-
var
|
|
6635
|
+
var log13 = forComponent("mcp");
|
|
6422
6636
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
6423
6637
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
6424
6638
|
var StdioTransport = class {
|
|
@@ -6437,7 +6651,7 @@ var StdioTransport = class {
|
|
|
6437
6651
|
proc.stdout.setEncoding("utf8");
|
|
6438
6652
|
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
6439
6653
|
proc.stderr.setEncoding("utf8");
|
|
6440
|
-
proc.stderr.on("data", (chunk) =>
|
|
6654
|
+
proc.stderr.on("data", (chunk) => log13.debug(`[${command}] stderr:`, chunk.trimEnd()));
|
|
6441
6655
|
proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
|
|
6442
6656
|
proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
|
|
6443
6657
|
}
|
|
@@ -6451,7 +6665,7 @@ var StdioTransport = class {
|
|
|
6451
6665
|
try {
|
|
6452
6666
|
this.dispatch(JSON.parse(line));
|
|
6453
6667
|
} catch (e) {
|
|
6454
|
-
|
|
6668
|
+
log13.debug("dropping non-JSON line from MCP server:", line, e);
|
|
6455
6669
|
}
|
|
6456
6670
|
}
|
|
6457
6671
|
}
|
|
@@ -6500,7 +6714,7 @@ var StdioTransport = class {
|
|
|
6500
6714
|
try {
|
|
6501
6715
|
this.proc?.stdin?.end();
|
|
6502
6716
|
} catch (e) {
|
|
6503
|
-
|
|
6717
|
+
log13.debug("stdin end failed", e);
|
|
6504
6718
|
}
|
|
6505
6719
|
this.proc?.kill();
|
|
6506
6720
|
}
|
|
@@ -6569,7 +6783,7 @@ function parseSseResponse(body) {
|
|
|
6569
6783
|
const obj = JSON.parse(trimmed.slice(5).trim());
|
|
6570
6784
|
if (obj && (obj.result !== void 0 || obj.error !== void 0)) return obj;
|
|
6571
6785
|
} catch (e) {
|
|
6572
|
-
|
|
6786
|
+
log13.debug("skipping unparseable SSE data line", e);
|
|
6573
6787
|
}
|
|
6574
6788
|
}
|
|
6575
6789
|
return {};
|
|
@@ -6637,7 +6851,7 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
|
|
|
6637
6851
|
return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
|
|
6638
6852
|
})(), mountTimeoutMs, name);
|
|
6639
6853
|
} catch (e) {
|
|
6640
|
-
await client.close().catch((err2) =>
|
|
6854
|
+
await client.close().catch((err2) => log13.debug(`close after failed mount of "${name}": ${err2}`));
|
|
6641
6855
|
throw e;
|
|
6642
6856
|
}
|
|
6643
6857
|
}
|
|
@@ -6648,15 +6862,15 @@ function validEntries(servers) {
|
|
|
6648
6862
|
return Object.entries(servers).filter(([name, cfg]) => {
|
|
6649
6863
|
if (!cfg || cfg.disabled) return false;
|
|
6650
6864
|
if (!cfg.command && !cfg.url) {
|
|
6651
|
-
|
|
6865
|
+
log13.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
|
|
6652
6866
|
return false;
|
|
6653
6867
|
}
|
|
6654
6868
|
return true;
|
|
6655
6869
|
});
|
|
6656
6870
|
}
|
|
6657
6871
|
function logMountFailure(name, e) {
|
|
6658
|
-
if (e instanceof McpAuthError)
|
|
6659
|
-
else
|
|
6872
|
+
if (e instanceof McpAuthError) log13.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
|
|
6873
|
+
else log13.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
|
|
6660
6874
|
}
|
|
6661
6875
|
async function mountMcpServers(servers = {}, opts = {}) {
|
|
6662
6876
|
const entries = validEntries(servers);
|
|
@@ -6666,7 +6880,7 @@ async function mountMcpServers(servers = {}, opts = {}) {
|
|
|
6666
6880
|
const name = entries[i][0];
|
|
6667
6881
|
if (r.status === "fulfilled") {
|
|
6668
6882
|
out.push(r.value);
|
|
6669
|
-
|
|
6883
|
+
log13.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
|
|
6670
6884
|
} else logMountFailure(name, r.reason);
|
|
6671
6885
|
});
|
|
6672
6886
|
return out;
|
|
@@ -6706,7 +6920,7 @@ var McpPool = class {
|
|
|
6706
6920
|
const prev = this.warm.get(key);
|
|
6707
6921
|
if (prev) {
|
|
6708
6922
|
clearTimeout(prev.timer);
|
|
6709
|
-
if (prev.client !== client) void prev.client.close().catch((err2) =>
|
|
6923
|
+
if (prev.client !== client) void prev.client.close().catch((err2) => log13.debug(`warm-pool replace close failed: ${err2}`));
|
|
6710
6924
|
}
|
|
6711
6925
|
const e = { client, timer: void 0 };
|
|
6712
6926
|
this.warm.set(key, e);
|
|
@@ -6723,7 +6937,7 @@ var McpPool = class {
|
|
|
6723
6937
|
const e = this.warm.get(key);
|
|
6724
6938
|
if (!e) return;
|
|
6725
6939
|
this.warm.delete(key);
|
|
6726
|
-
await e.client.close().catch((err2) =>
|
|
6940
|
+
await e.client.close().catch((err2) => log13.debug(`warm-pool evict close failed: ${err2}`));
|
|
6727
6941
|
}
|
|
6728
6942
|
async closeAll() {
|
|
6729
6943
|
for (const e of this.warm.values()) {
|
|
@@ -6941,7 +7155,7 @@ init_tools_shell();
|
|
|
6941
7155
|
// src/tools.notify.ts
|
|
6942
7156
|
init_logging();
|
|
6943
7157
|
import { execFile } from "child_process";
|
|
6944
|
-
var
|
|
7158
|
+
var log15 = forComponent("notify");
|
|
6945
7159
|
function makeNotifyTool(opts = {}) {
|
|
6946
7160
|
const platform2 = opts.platform ?? process.platform;
|
|
6947
7161
|
const run = opts.exec ?? execFile;
|
|
@@ -6965,7 +7179,7 @@ function makeNotifyTool(opts = {}) {
|
|
|
6965
7179
|
return new Promise((resolve4) => {
|
|
6966
7180
|
run(argv[0], argv[1], { timeout: 5e3 }, (e) => {
|
|
6967
7181
|
if (e) {
|
|
6968
|
-
|
|
7182
|
+
log15.debug("notification failed", e);
|
|
6969
7183
|
resolve4(`Notification failed: ${e.message}`);
|
|
6970
7184
|
} else resolve4("Notification shown.");
|
|
6971
7185
|
});
|
|
@@ -6981,7 +7195,7 @@ import { BodDB as BodDB2 } from "@bod.ee/db";
|
|
|
6981
7195
|
init_logging();
|
|
6982
7196
|
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
6983
7197
|
import { homedir } from "os";
|
|
6984
|
-
var
|
|
7198
|
+
var log16 = forComponent("cli-util");
|
|
6985
7199
|
function dotDirs(base, sub, opts = {}) {
|
|
6986
7200
|
const home = opts.home ?? homedir();
|
|
6987
7201
|
const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
|
|
@@ -6998,7 +7212,7 @@ function parseJson(text, fallback, what = "json") {
|
|
|
6998
7212
|
try {
|
|
6999
7213
|
return JSON.parse(text);
|
|
7000
7214
|
} catch (e) {
|
|
7001
|
-
|
|
7215
|
+
log16.debug(`parseJson(${what}) failed: ${e.message}`);
|
|
7002
7216
|
return fallback;
|
|
7003
7217
|
}
|
|
7004
7218
|
}
|
|
@@ -7008,7 +7222,7 @@ function readJsonFile(path, fallback) {
|
|
|
7008
7222
|
try {
|
|
7009
7223
|
text = readFileSync3(path, "utf8");
|
|
7010
7224
|
} catch (e) {
|
|
7011
|
-
|
|
7225
|
+
log16.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
|
|
7012
7226
|
return fallback;
|
|
7013
7227
|
}
|
|
7014
7228
|
return parseJson(text, fallback, path);
|
|
@@ -7261,7 +7475,7 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as rea
|
|
|
7261
7475
|
import { homedir as homedir3 } from "os";
|
|
7262
7476
|
import { dirname as dirname3, join as join6 } from "path";
|
|
7263
7477
|
import { fileURLToPath } from "url";
|
|
7264
|
-
var
|
|
7478
|
+
var log17 = forComponent("VoiceIO");
|
|
7265
7479
|
var now4 = () => performance.now();
|
|
7266
7480
|
var Player = class {
|
|
7267
7481
|
proc = null;
|
|
@@ -7275,7 +7489,7 @@ var Player = class {
|
|
|
7275
7489
|
["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
|
|
7276
7490
|
{ stdio: ["pipe", "ignore", "ignore"] }
|
|
7277
7491
|
);
|
|
7278
|
-
this.proc.on("error", (e) =>
|
|
7492
|
+
this.proc.on("error", (e) => log17.warn(`ffplay error: ${e.message}`));
|
|
7279
7493
|
this.proc.stdin.on("error", () => {
|
|
7280
7494
|
});
|
|
7281
7495
|
this.bytesWritten = 0;
|
|
@@ -7313,7 +7527,7 @@ function detectFfmpegMic() {
|
|
|
7313
7527
|
const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
|
|
7314
7528
|
const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
|
|
7315
7529
|
if (!mic) throw new Error("no audio input device found");
|
|
7316
|
-
|
|
7530
|
+
log17.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
|
|
7317
7531
|
return `:${mic.idx}`;
|
|
7318
7532
|
}
|
|
7319
7533
|
function detectedInputDevice() {
|
|
@@ -7349,15 +7563,15 @@ function resolveAecBinary() {
|
|
|
7349
7563
|
if (existsSync4(bin) && statSync3(bin).mtimeMs >= statSync3(src).mtimeMs) return bin;
|
|
7350
7564
|
if (spawnSync2("which", ["swiftc"]).status !== 0) return null;
|
|
7351
7565
|
mkdirSync5(cacheDir, { recursive: true });
|
|
7352
|
-
|
|
7566
|
+
log17.info("compiling AEC mic helper (first run)\u2026");
|
|
7353
7567
|
const build = spawnSync2("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
|
|
7354
7568
|
if (build.status !== 0) {
|
|
7355
|
-
|
|
7569
|
+
log17.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
|
|
7356
7570
|
return null;
|
|
7357
7571
|
}
|
|
7358
7572
|
const sign = spawnSync2("codesign", ["-fs", "-", bin], { encoding: "utf8" });
|
|
7359
7573
|
if (sign.status !== 0) {
|
|
7360
|
-
|
|
7574
|
+
log17.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
|
|
7361
7575
|
return null;
|
|
7362
7576
|
}
|
|
7363
7577
|
return bin;
|
|
@@ -7399,16 +7613,16 @@ var NodeMicSource = class {
|
|
|
7399
7613
|
this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
|
|
7400
7614
|
} else {
|
|
7401
7615
|
if (spawnSync2("which", ["ffmpeg"]).status !== 0) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
|
|
7402
|
-
|
|
7616
|
+
log17.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
|
|
7403
7617
|
this.proc = spawn2(
|
|
7404
7618
|
"ffmpeg",
|
|
7405
7619
|
["-loglevel", "error", "-f", "avfoundation", "-i", detectFfmpegMic(), "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
|
|
7406
7620
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
7407
7621
|
);
|
|
7408
|
-
this.proc.stderr.on("data", (d) =>
|
|
7622
|
+
this.proc.stderr.on("data", (d) => log17.warn(`ffmpeg: ${String(d).trim()}`));
|
|
7409
7623
|
}
|
|
7410
7624
|
this.proc.on("exit", (c) => {
|
|
7411
|
-
if (c && !this.stopped)
|
|
7625
|
+
if (c && !this.stopped) log17.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
|
|
7412
7626
|
});
|
|
7413
7627
|
this.proc.stdout.on("data", (chunk) => onChunk(chunk));
|
|
7414
7628
|
}
|
|
@@ -7474,7 +7688,7 @@ var AecDuplexAudio = class {
|
|
|
7474
7688
|
this.proc.stdin.on("error", () => {
|
|
7475
7689
|
});
|
|
7476
7690
|
this.proc.on("exit", (c) => {
|
|
7477
|
-
if (c && !this.stopped)
|
|
7691
|
+
if (c && !this.stopped) log17.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
|
|
7478
7692
|
});
|
|
7479
7693
|
this.proc.stdout.on("data", (chunk) => {
|
|
7480
7694
|
this.gotChunk = true;
|
|
@@ -7490,7 +7704,7 @@ var AecDuplexAudio = class {
|
|
|
7490
7704
|
openMicSettings();
|
|
7491
7705
|
this.onFatal?.("microphone permission denied \u2014 enable it in System Settings \u2192 Privacy & Security \u2192 Microphone for your terminal, then restart it");
|
|
7492
7706
|
}
|
|
7493
|
-
} else
|
|
7707
|
+
} else log17.debug(`mic-aec: ${s}`);
|
|
7494
7708
|
}
|
|
7495
7709
|
});
|
|
7496
7710
|
if (!this.noVpio && !this.triedFallback) {
|
|
@@ -7499,7 +7713,7 @@ var AecDuplexAudio = class {
|
|
|
7499
7713
|
this.triedFallback = true;
|
|
7500
7714
|
this.noVpio = true;
|
|
7501
7715
|
this._aec = false;
|
|
7502
|
-
|
|
7716
|
+
log17.warn("mic-aec: VPIO delivered no audio in 2.5s \u2014 falling back to non-VPIO capture (no AEC \u2192 half-duplex, no barge-in)");
|
|
7503
7717
|
this.onDegrade?.();
|
|
7504
7718
|
this.killProc();
|
|
7505
7719
|
this.spawnHelper();
|
|
@@ -7607,6 +7821,10 @@ var VoiceIOOptions = class extends VoiceEngineOptions {
|
|
|
7607
7821
|
sonioxApiKey = process.env.SONIOX_API_KEY ?? "";
|
|
7608
7822
|
cartesiaApiKey = process.env.CARTESIA_API_KEY ?? "";
|
|
7609
7823
|
cartesiaVoiceId = process.env.CARTESIA_VOICE_ID ?? "";
|
|
7824
|
+
emotions = process.env.VOICE_EMOTIONS !== "0";
|
|
7825
|
+
// Cartesia inline emotion tags (sonic-3)
|
|
7826
|
+
showEmotions = process.env.VOICE_SHOW_EMOTIONS === "1";
|
|
7827
|
+
// surface the tags in the on-screen echo (debug; opt-in)
|
|
7610
7828
|
};
|
|
7611
7829
|
var VoiceIO = class extends VoiceEngine {
|
|
7612
7830
|
duplexSource;
|
|
@@ -7750,7 +7968,7 @@ async function loadConfig(cwd) {
|
|
|
7750
7968
|
|
|
7751
7969
|
// cli/hooks-config.ts
|
|
7752
7970
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
7753
|
-
var
|
|
7971
|
+
var log18 = forComponent("hooks");
|
|
7754
7972
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
7755
7973
|
function ruleMatches(rule, toolName) {
|
|
7756
7974
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -7767,7 +7985,7 @@ function runCmd(rule, env) {
|
|
|
7767
7985
|
});
|
|
7768
7986
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
7769
7987
|
} catch (e) {
|
|
7770
|
-
|
|
7988
|
+
log18.debug(`hook command failed: ${rule.command}`, e);
|
|
7771
7989
|
return { code: 1, out: String(e?.message ?? e) };
|
|
7772
7990
|
}
|
|
7773
7991
|
}
|
|
@@ -7874,7 +8092,7 @@ function formatDiff(ops, opts = {}) {
|
|
|
7874
8092
|
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4, readdirSync, renameSync, symlinkSync as symlinkSync2, unlinkSync, readlinkSync } from "fs";
|
|
7875
8093
|
import { homedir as homedir5 } from "os";
|
|
7876
8094
|
import { join as join8 } from "path";
|
|
7877
|
-
var
|
|
8095
|
+
var log19 = forComponent("session");
|
|
7878
8096
|
var globalDir = () => join8(homedir5(), ".agent", "sessions");
|
|
7879
8097
|
var SessionStore = class {
|
|
7880
8098
|
dir;
|
|
@@ -7919,7 +8137,7 @@ var SessionStore = class {
|
|
|
7919
8137
|
}
|
|
7920
8138
|
load(id) {
|
|
7921
8139
|
if (!this.safeId(id)) {
|
|
7922
|
-
|
|
8140
|
+
log19.debug(`rejecting unsafe session id: ${id}`);
|
|
7923
8141
|
return void 0;
|
|
7924
8142
|
}
|
|
7925
8143
|
const path = join8(this.dir, `${id}.json`);
|
|
@@ -7927,7 +8145,7 @@ var SessionStore = class {
|
|
|
7927
8145
|
try {
|
|
7928
8146
|
return JSON.parse(readFileSync6(path, "utf8"));
|
|
7929
8147
|
} catch (e) {
|
|
7930
|
-
|
|
8148
|
+
log19.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
7931
8149
|
return void 0;
|
|
7932
8150
|
}
|
|
7933
8151
|
}
|
|
@@ -7940,7 +8158,7 @@ var SessionStore = class {
|
|
|
7940
8158
|
try {
|
|
7941
8159
|
metas.push(JSON.parse(readFileSync6(join8(this.dir, f), "utf8")).meta);
|
|
7942
8160
|
} catch (e) {
|
|
7943
|
-
|
|
8161
|
+
log19.debug(`skipping unreadable session file ${f}`, e);
|
|
7944
8162
|
}
|
|
7945
8163
|
}
|
|
7946
8164
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -8108,7 +8326,7 @@ import { execFile as execFile3 } from "child_process";
|
|
|
8108
8326
|
import { promisify } from "util";
|
|
8109
8327
|
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync7 } from "fs";
|
|
8110
8328
|
import { join as join9, resolve as resolve2, sep as sep2 } from "path";
|
|
8111
|
-
var
|
|
8329
|
+
var log20 = forComponent("checkpoints");
|
|
8112
8330
|
var exec = promisify(execFile3);
|
|
8113
8331
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
8114
8332
|
var ShadowRepo = class {
|
|
@@ -8146,7 +8364,7 @@ var ShadowRepo = class {
|
|
|
8146
8364
|
writeFileSync5(join9(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
8147
8365
|
this.ready = true;
|
|
8148
8366
|
} catch (e) {
|
|
8149
|
-
|
|
8367
|
+
log20.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
8150
8368
|
this.ready = false;
|
|
8151
8369
|
}
|
|
8152
8370
|
return this.ready;
|
|
@@ -8157,7 +8375,7 @@ var ShadowRepo = class {
|
|
|
8157
8375
|
}
|
|
8158
8376
|
async commit(label, forced = []) {
|
|
8159
8377
|
await this.run("add", "-A");
|
|
8160
|
-
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) =>
|
|
8378
|
+
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log20.debug(`force-add failed: ${p}`, e));
|
|
8161
8379
|
await this.run("commit", "--allow-empty", "-q", "-m", label);
|
|
8162
8380
|
}
|
|
8163
8381
|
/** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
|
|
@@ -8165,8 +8383,8 @@ var ShadowRepo = class {
|
|
|
8165
8383
|
* turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
|
|
8166
8384
|
* point per turn, so the REPL's turn↔frame mapping stays intact. */
|
|
8167
8385
|
async amendForced(paths) {
|
|
8168
|
-
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) =>
|
|
8169
|
-
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) =>
|
|
8386
|
+
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log20.debug(`force-capture failed: ${p}`, e));
|
|
8387
|
+
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log20.debug("amend failed", e));
|
|
8170
8388
|
}
|
|
8171
8389
|
/** Commits on `ref`, oldest-first (canonical index space). */
|
|
8172
8390
|
async log(ref) {
|
|
@@ -8226,7 +8444,7 @@ var ShadowRepo = class {
|
|
|
8226
8444
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
8227
8445
|
});
|
|
8228
8446
|
} catch (e) {
|
|
8229
|
-
|
|
8447
|
+
log20.debug("checkpoint prune failed", e);
|
|
8230
8448
|
}
|
|
8231
8449
|
}
|
|
8232
8450
|
};
|
|
@@ -8285,7 +8503,7 @@ var GitCheckpoints = class {
|
|
|
8285
8503
|
use(sessionId) {
|
|
8286
8504
|
if (sessionId === this.session) return;
|
|
8287
8505
|
this.session = sessionId;
|
|
8288
|
-
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) =>
|
|
8506
|
+
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log20.debug("re-point failed", e));
|
|
8289
8507
|
}
|
|
8290
8508
|
async begin(label) {
|
|
8291
8509
|
if (!await this.start()) return;
|
|
@@ -8297,7 +8515,7 @@ var GitCheckpoints = class {
|
|
|
8297
8515
|
try {
|
|
8298
8516
|
await this.repos[i].commit(msg, forced);
|
|
8299
8517
|
} catch (e) {
|
|
8300
|
-
|
|
8518
|
+
log20.debug("checkpoint commit failed", e);
|
|
8301
8519
|
}
|
|
8302
8520
|
}
|
|
8303
8521
|
if (slow) clearTimeout(slow);
|
|
@@ -8327,7 +8545,7 @@ var GitCheckpoints = class {
|
|
|
8327
8545
|
if (this.forced.has(abs)) continue;
|
|
8328
8546
|
this.forced.add(abs);
|
|
8329
8547
|
const i = this.repoIndexFor(abs);
|
|
8330
|
-
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) =>
|
|
8548
|
+
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log20.debug("amendForced failed", e));
|
|
8331
8549
|
}
|
|
8332
8550
|
}
|
|
8333
8551
|
};
|
|
@@ -10033,7 +10251,7 @@ import { spawnSync as spawnSync5 } from "child_process";
|
|
|
10033
10251
|
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync9, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
|
|
10034
10252
|
import { homedir as homedir7 } from "os";
|
|
10035
10253
|
import { join as join12 } from "path";
|
|
10036
|
-
var
|
|
10254
|
+
var log21 = forComponent("os-sched");
|
|
10037
10255
|
var OsScheduler = class {
|
|
10038
10256
|
options;
|
|
10039
10257
|
constructor(options) {
|
|
@@ -10095,7 +10313,7 @@ var OsScheduler = class {
|
|
|
10095
10313
|
}
|
|
10096
10314
|
}
|
|
10097
10315
|
} catch (e) {
|
|
10098
|
-
|
|
10316
|
+
log21.debug(`cancel ${id}`, e);
|
|
10099
10317
|
}
|
|
10100
10318
|
for (const f of [`${id}.json`, `${id}.sh`]) {
|
|
10101
10319
|
try {
|
|
@@ -10212,7 +10430,7 @@ import { spawn as spawn3 } from "child_process";
|
|
|
10212
10430
|
import { existsSync as existsSync8, mkdirSync as mkdirSync10, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "fs";
|
|
10213
10431
|
import { homedir as homedir8 } from "os";
|
|
10214
10432
|
import { join as join13 } from "path";
|
|
10215
|
-
var
|
|
10433
|
+
var log22 = forComponent("remote-trigger");
|
|
10216
10434
|
var TRIGGER_DIR = () => join13(homedir8(), ".agent", "triggers");
|
|
10217
10435
|
var sockPath = (sessionId, dir = TRIGGER_DIR()) => join13(dir, `${sessionId}.sock`);
|
|
10218
10436
|
var TriggerServer = class {
|
|
@@ -10250,13 +10468,13 @@ var TriggerServer = class {
|
|
|
10250
10468
|
conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
10251
10469
|
}
|
|
10252
10470
|
});
|
|
10253
|
-
conn.on("error", (e) =>
|
|
10471
|
+
conn.on("error", (e) => log22.debug("trigger conn error", e));
|
|
10254
10472
|
});
|
|
10255
|
-
this.server.on("error", (e) =>
|
|
10473
|
+
this.server.on("error", (e) => log22.debug("trigger server error", e));
|
|
10256
10474
|
this.server.listen(p);
|
|
10257
10475
|
this.path = p;
|
|
10258
10476
|
} catch (e) {
|
|
10259
|
-
|
|
10477
|
+
log22.debug("trigger server unavailable", e);
|
|
10260
10478
|
}
|
|
10261
10479
|
}
|
|
10262
10480
|
/** Re-bind on /resume (the session id changed). */
|
|
@@ -10377,7 +10595,7 @@ var italic = C("3");
|
|
|
10377
10595
|
var strike = C("9");
|
|
10378
10596
|
var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
|
|
10379
10597
|
var err = (s) => process.stderr.write(s);
|
|
10380
|
-
var
|
|
10598
|
+
var log23 = forComponent("cli");
|
|
10381
10599
|
var VERSION = (() => {
|
|
10382
10600
|
try {
|
|
10383
10601
|
return JSON.parse(readFileSync8(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -11016,7 +11234,7 @@ function costOf(pricing, promptTokens = 0, completionTokens = 0, cacheCreationTo
|
|
|
11016
11234
|
function turnCost(model, usage) {
|
|
11017
11235
|
return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, model);
|
|
11018
11236
|
}
|
|
11019
|
-
async function evaluateGoal(ai, condition, transcript,
|
|
11237
|
+
async function evaluateGoal(ai, condition, transcript, log24) {
|
|
11020
11238
|
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
11021
11239
|
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
11022
11240
|
return text.slice(0, 600);
|
|
@@ -11036,7 +11254,7 @@ ${recent}` }
|
|
|
11036
11254
|
const match = r.content.match(/\{[\s\S]*\}/);
|
|
11037
11255
|
if (match) return JSON.parse(match[0]);
|
|
11038
11256
|
} catch (e) {
|
|
11039
|
-
|
|
11257
|
+
log24(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
11040
11258
|
`));
|
|
11041
11259
|
}
|
|
11042
11260
|
return { met: false, reason: "evaluation unclear" };
|
|
@@ -11248,7 +11466,7 @@ function mcpAgentTools(mounted, opts) {
|
|
|
11248
11466
|
return tools;
|
|
11249
11467
|
}
|
|
11250
11468
|
async function closeMcp(mounted) {
|
|
11251
|
-
await Promise.all(mounted.map((m) => m.client.close().catch((e) =>
|
|
11469
|
+
await Promise.all(mounted.map((m) => m.client.close().catch((e) => log23.debug("mcp close failed", e))));
|
|
11252
11470
|
}
|
|
11253
11471
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
11254
11472
|
function mentionRefs(line) {
|
|
@@ -11298,7 +11516,7 @@ async function expandMentions(fs, line) {
|
|
|
11298
11516
|
if (loaded.includes(ref) || missing.includes(ref)) continue;
|
|
11299
11517
|
if (ref.includes(":") && mcpMentionResolver) {
|
|
11300
11518
|
const body = await mcpMentionResolver(ref).catch((e) => {
|
|
11301
|
-
|
|
11519
|
+
log23.debug("mcp mention resolve failed", e);
|
|
11302
11520
|
return null;
|
|
11303
11521
|
});
|
|
11304
11522
|
if (body != null) {
|
|
@@ -11381,7 +11599,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
11381
11599
|
try {
|
|
11382
11600
|
store.save(session);
|
|
11383
11601
|
} catch (ex) {
|
|
11384
|
-
|
|
11602
|
+
log23.debug("mid-turn session flush failed", ex);
|
|
11385
11603
|
}
|
|
11386
11604
|
}
|
|
11387
11605
|
return origNotify(e);
|
|
@@ -11528,14 +11746,14 @@ var isCancelTeardown = (e) => {
|
|
|
11528
11746
|
function installCancelGuards(mounted) {
|
|
11529
11747
|
process.on("unhandledRejection", (e) => {
|
|
11530
11748
|
if (isCancelTeardown(e)) {
|
|
11531
|
-
|
|
11749
|
+
log23.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
11532
11750
|
return;
|
|
11533
11751
|
}
|
|
11534
|
-
|
|
11752
|
+
log23.error("unhandledRejection", e);
|
|
11535
11753
|
});
|
|
11536
11754
|
process.on("uncaughtException", (e) => {
|
|
11537
11755
|
if (isCancelTeardown(e)) {
|
|
11538
|
-
|
|
11756
|
+
log23.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
11539
11757
|
return;
|
|
11540
11758
|
}
|
|
11541
11759
|
console.error(e);
|
|
@@ -11595,6 +11813,8 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
11595
11813
|
let dx;
|
|
11596
11814
|
let voiceIO;
|
|
11597
11815
|
let voiceLineOpen = false;
|
|
11816
|
+
const emotionsOn = process.env.VOICE_EMOTIONS !== "0";
|
|
11817
|
+
let showEmotions = process.env.VOICE_SHOW_EMOTIONS === "1";
|
|
11598
11818
|
const voiceEcho = (text) => {
|
|
11599
11819
|
const s = forSpeech(text);
|
|
11600
11820
|
if (!s) return;
|
|
@@ -11669,7 +11889,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
11669
11889
|
spinner.stop();
|
|
11670
11890
|
voiceIO.enqueueUtterance(e.message);
|
|
11671
11891
|
editorRef?.suspend();
|
|
11672
|
-
voiceEcho(e.message);
|
|
11892
|
+
voiceEcho(emotionsOn ? renderEmotions(e.message, { show: showEmotions }).display : e.message);
|
|
11673
11893
|
voiceEchoEnd();
|
|
11674
11894
|
editorRef?.resume();
|
|
11675
11895
|
editorRef?.redrawNow();
|
|
@@ -11683,9 +11903,9 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
11683
11903
|
}
|
|
11684
11904
|
if (e.kind === "text_delta" && voiceIO) {
|
|
11685
11905
|
spinner.stop();
|
|
11686
|
-
voiceIO.speakDelta(e.message);
|
|
11906
|
+
const echo = voiceIO.speakDelta(e.message);
|
|
11687
11907
|
editorRef?.suspend();
|
|
11688
|
-
voiceEcho(
|
|
11908
|
+
voiceEcho(echo);
|
|
11689
11909
|
return;
|
|
11690
11910
|
} else if (e.kind === "text_delta" && stashText()) {
|
|
11691
11911
|
process.stdout.write("\r\x1B[K");
|
|
@@ -11759,8 +11979,8 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
11759
11979
|
providerOptionsFor: (m) => cursorProviderOptions(m, cwd, cfg.mcpServers),
|
|
11760
11980
|
...(args.thinkModel ?? cfg.thinkModel) !== void 0 ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {},
|
|
11761
11981
|
host,
|
|
11762
|
-
...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true } : {},
|
|
11763
|
-
// voice: progress asides + worker questions relayed through the conversation
|
|
11982
|
+
...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true, emotionTags: emotionsOn } : {},
|
|
11983
|
+
// voice: progress asides + worker questions relayed through the conversation; emotion tags taught only with a TTS that speaks them
|
|
11764
11984
|
// Per-TASK checkpoint frames (the natural undo unit in duplex = one delegation): opened BEFORE
|
|
11765
11985
|
// the worker spawns (post-spawn would race its first edits). `checkpoints` is bound below.
|
|
11766
11986
|
onTaskStart: async (_id, label) => {
|
|
@@ -12012,7 +12232,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
12012
12232
|
mkdirSync11(join14(cwd, ".agent"), { recursive: true });
|
|
12013
12233
|
appendFileSync(histPath, line + "\n");
|
|
12014
12234
|
} catch (e) {
|
|
12015
|
-
|
|
12235
|
+
log23.debug("history write failed", e);
|
|
12016
12236
|
}
|
|
12017
12237
|
};
|
|
12018
12238
|
const ago = (t) => {
|
|
@@ -12083,7 +12303,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
12083
12303
|
try {
|
|
12084
12304
|
store.save(session);
|
|
12085
12305
|
} catch (e) {
|
|
12086
|
-
|
|
12306
|
+
log23.debug("session save after rewind failed", e);
|
|
12087
12307
|
}
|
|
12088
12308
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
12089
12309
|
`));
|
|
@@ -12117,7 +12337,7 @@ ${task}`;
|
|
|
12117
12337
|
bangContext.length = 0;
|
|
12118
12338
|
}
|
|
12119
12339
|
const delta = await refreshCatalogs().catch((e) => {
|
|
12120
|
-
|
|
12340
|
+
log23.debug("catalog refresh failed", e);
|
|
12121
12341
|
return "";
|
|
12122
12342
|
});
|
|
12123
12343
|
if (delta) {
|
|
@@ -12380,7 +12600,7 @@ ${task}`;
|
|
|
12380
12600
|
desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
|
|
12381
12601
|
run: async () => {
|
|
12382
12602
|
await refreshCatalogs().catch((e) => {
|
|
12383
|
-
|
|
12603
|
+
log23.debug("catalog refresh failed", e);
|
|
12384
12604
|
});
|
|
12385
12605
|
face.reprepare();
|
|
12386
12606
|
err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
|
|
@@ -12450,6 +12670,24 @@ ${task}`;
|
|
|
12450
12670
|
}
|
|
12451
12671
|
await toggleVoice();
|
|
12452
12672
|
}
|
|
12673
|
+
}, "voice-emotions": {
|
|
12674
|
+
desc: "show/hide the [emotion] debug tags in the echo \u2014 /voice-emotions <on|off> (TTS emotion control stays on regardless)",
|
|
12675
|
+
run: async (a) => {
|
|
12676
|
+
if (!emotionsOn) {
|
|
12677
|
+
err(dim(" (emotion control is off \u2014 VOICE_EMOTIONS=0)\n"));
|
|
12678
|
+
return;
|
|
12679
|
+
}
|
|
12680
|
+
const v = a[0]?.toLowerCase();
|
|
12681
|
+
if (v === "on" || v === "off") {
|
|
12682
|
+
showEmotions = v === "on";
|
|
12683
|
+
voiceIO?.setShowEmotions(showEmotions);
|
|
12684
|
+
err(green(` \u2713 emotion tags ${showEmotions ? "shown" : "hidden"} in echo
|
|
12685
|
+
`));
|
|
12686
|
+
return;
|
|
12687
|
+
}
|
|
12688
|
+
err(dim(` emotion tags in echo: ${showEmotions ? "shown" : "hidden"} (use /voice-emotions on|off)
|
|
12689
|
+
`));
|
|
12690
|
+
}
|
|
12453
12691
|
}, "voice-model": {
|
|
12454
12692
|
desc: "switch the reflex (voice) model \u2014 /voice-model <id>, or alone for a picker",
|
|
12455
12693
|
run: async (a) => {
|
|
@@ -12842,7 +13080,7 @@ ${task}`;
|
|
|
12842
13080
|
try {
|
|
12843
13081
|
for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
|
|
12844
13082
|
} catch (e) {
|
|
12845
|
-
|
|
13083
|
+
log23.debug(`loadAgents(${d}) failed`, e);
|
|
12846
13084
|
}
|
|
12847
13085
|
}
|
|
12848
13086
|
if (!seen.size) {
|
|
@@ -12930,7 +13168,7 @@ ${task}`;
|
|
|
12930
13168
|
}
|
|
12931
13169
|
if (idx >= 0) {
|
|
12932
13170
|
const old = mounted.splice(idx, 1)[0];
|
|
12933
|
-
await old.client.close().catch((e) =>
|
|
13171
|
+
await old.client.close().catch((e) => log23.debug("mcp close failed", e));
|
|
12934
13172
|
}
|
|
12935
13173
|
try {
|
|
12936
13174
|
const m = await mountMcpServer(name, conf);
|
|
@@ -12958,7 +13196,7 @@ ${task}`;
|
|
|
12958
13196
|
}
|
|
12959
13197
|
const m = mounted.splice(idx, 1)[0];
|
|
12960
13198
|
remountMcpTools();
|
|
12961
|
-
await m.client.close().catch((e) =>
|
|
13199
|
+
await m.client.close().catch((e) => log23.debug("mcp close failed", e));
|
|
12962
13200
|
err(dim(` removed "${name}"
|
|
12963
13201
|
`));
|
|
12964
13202
|
return;
|
|
@@ -13178,7 +13416,7 @@ ${task}`;
|
|
|
13178
13416
|
try {
|
|
13179
13417
|
return readdirSync4(join14(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
13180
13418
|
} catch (e) {
|
|
13181
|
-
|
|
13419
|
+
log23.debug("completion readdir failed", absDir, e);
|
|
13182
13420
|
return null;
|
|
13183
13421
|
}
|
|
13184
13422
|
};
|
|
@@ -13305,7 +13543,7 @@ ${out}
|
|
|
13305
13543
|
return;
|
|
13306
13544
|
}
|
|
13307
13545
|
await refreshCatalogs().catch((e) => {
|
|
13308
|
-
|
|
13546
|
+
log23.debug("catalog refresh failed", e);
|
|
13309
13547
|
});
|
|
13310
13548
|
const sk = skills.find((x) => x.name === name);
|
|
13311
13549
|
if (sk) {
|
|
@@ -13348,6 +13586,9 @@ ${out}
|
|
|
13348
13586
|
const fakeVoice = process.env.AGENTX_VOICE_FAKE ? fakeVoiceParts(process.env.AGENTX_VOICE_FAKE) : null;
|
|
13349
13587
|
voiceIO = new VoiceIO({
|
|
13350
13588
|
...fakeVoice ?? {},
|
|
13589
|
+
emotions: emotionsOn,
|
|
13590
|
+
showEmotions,
|
|
13591
|
+
// local is authoritative (a /voice-emotions before mic-on still applies)
|
|
13351
13592
|
// No ack phrase by default: a fixed "Mm-hm," every turn reads robotic, Haiku's TTFT doesn't
|
|
13352
13593
|
// need masking (~0.7-1.2s full turns), and the conversational register already opens with a
|
|
13353
13594
|
// natural reaction. The mechanism (+ echo-leak guard) stays for slower voice models.
|