@livx.cc/agentx 0.97.7 → 0.98.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +360 -108
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +23 -1
- package/dist/index.js +234 -32
- 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;
|
|
@@ -4795,7 +4981,7 @@ var DuplexAgent = class _DuplexAgent {
|
|
|
4795
4981
|
...new Set(workerToolNames.filter((n) => n.startsWith("mcp__")).map((n) => n.slice(5).split("__")[0]))
|
|
4796
4982
|
];
|
|
4797
4983
|
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 : "") + `
|
|
4984
|
+
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
4985
|
Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
4800
4986
|
const tools = [
|
|
4801
4987
|
...o.reflexOptions?.tools ?? [],
|
|
@@ -4819,7 +5005,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
4819
5005
|
const m = this.reflexBuf.match(RESERVED_EVENT_MARKER) ?? this.reflexBuf.match(RESERVED_EVENT_OPENER);
|
|
4820
5006
|
if (m) {
|
|
4821
5007
|
this.fabricationCut = true;
|
|
4822
|
-
|
|
5008
|
+
log9.warn(`reflex fabricated a [task \u2026] event in its spoken stream \u2014 cutting it (kept ${m.index} chars)`);
|
|
4823
5009
|
const safe = this.reflexBuf.slice(this.reflexForwarded, m.index);
|
|
4824
5010
|
if (!safe) return;
|
|
4825
5011
|
if (safe.trim()) this.spokeThisTurn = true;
|
|
@@ -4921,7 +5107,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
4921
5107
|
try {
|
|
4922
5108
|
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
5109
|
} catch (e) {
|
|
4924
|
-
|
|
5110
|
+
log9.warn(`ack nudge failed: ${e instanceof Error ? e.message : e}`);
|
|
4925
5111
|
} finally {
|
|
4926
5112
|
this.nudging = false;
|
|
4927
5113
|
}
|
|
@@ -4948,6 +5134,19 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
4948
5134
|
rec.controller.abort();
|
|
4949
5135
|
return `Task ${rec.id} (${rec.label}) cancelled.`;
|
|
4950
5136
|
}
|
|
5137
|
+
/** Barge-in: the user took the floor while task(s) were running. Suppress those tasks' remaining SPOKEN
|
|
5138
|
+
* delivery so a superseded topic never talks over the new one (the debt-after-jokes regression). The
|
|
5139
|
+
* tasks keep running and still fold their result into the transcript — recoverable, just not spoken.
|
|
5140
|
+
* Returns the parked ids (for logging). Does NOT cancel: that's a deliberate reflex/user action. */
|
|
5141
|
+
parkInFlightDeliveries() {
|
|
5142
|
+
const parked = [];
|
|
5143
|
+
for (const rec of this.tasks.values())
|
|
5144
|
+
if (rec.status === "running" && !rec.deliveryParked) {
|
|
5145
|
+
rec.deliveryParked = true;
|
|
5146
|
+
parked.push(rec.id);
|
|
5147
|
+
}
|
|
5148
|
+
return parked;
|
|
5149
|
+
}
|
|
4951
5150
|
/** Resolve when all queued voice turns AND all in-flight worker tasks have settled (tests, graceful shutdown). */
|
|
4952
5151
|
async idle() {
|
|
4953
5152
|
while (true) {
|
|
@@ -5000,7 +5199,7 @@ Today's date: ${(/* @__PURE__ */ new Date()).toDateString()}.`;
|
|
|
5000
5199
|
buildBrief(brief, tier = "act", deliver = true) {
|
|
5001
5200
|
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");
|
|
5002
5201
|
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." : "";
|
|
5003
|
-
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." : "";
|
|
5202
|
+
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." : "") : "";
|
|
5004
5203
|
return (recent ? `${brief}
|
|
5005
5204
|
|
|
5006
5205
|
## Recent conversation (for context)
|
|
@@ -5045,7 +5244,7 @@ ${recent}` : brief) + verify + deliverContract;
|
|
|
5045
5244
|
};
|
|
5046
5245
|
const splitter = new SpokenSplitter();
|
|
5047
5246
|
const speak = (seg) => {
|
|
5048
|
-
if (seg) o.host?.notify?.({ kind: "speak_utterance", message: seg });
|
|
5247
|
+
if (seg && !this.tasks.get(id)?.deliveryParked) o.host?.notify?.({ kind: "speak_utterance", message: seg });
|
|
5049
5248
|
};
|
|
5050
5249
|
const coalescer = new SentenceCoalescer();
|
|
5051
5250
|
const feedSpoken = (s) => {
|
|
@@ -5116,7 +5315,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
5116
5315
|
this.notify("task_verify", `task ${id}: verifying`, { id });
|
|
5117
5316
|
const cres = await new Agent(checkerOpts).run(checkBrief);
|
|
5118
5317
|
if (cres.finishReason !== "stop") {
|
|
5119
|
-
|
|
5318
|
+
log9.warn(`task ${id}: verify inconclusive (${cres.finishReason})`);
|
|
5120
5319
|
this.notify("task_verify", `task ${id}: verify inconclusive (${cres.finishReason})`, { id, finishReason: cres.finishReason });
|
|
5121
5320
|
}
|
|
5122
5321
|
const sum = (a = 0, b = 0) => a + b;
|
|
@@ -5252,7 +5451,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
5252
5451
|
rec.status = "done";
|
|
5253
5452
|
rec.result = res.text;
|
|
5254
5453
|
const incomplete = res.finishReason !== "stop";
|
|
5255
|
-
|
|
5454
|
+
log9.verbose(`task ${id} done (${res.steps} steps${incomplete ? `, INCOMPLETE: ${res.finishReason}` : ""})`);
|
|
5256
5455
|
this.notify("task_done", `task ${id} (${rec.label}) completed`, {
|
|
5257
5456
|
id,
|
|
5258
5457
|
text: res.text,
|
|
@@ -5266,9 +5465,9 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
5266
5465
|
return this.queueRevoice(this.integrationPrompt(rec, "incomplete", res.text, res.finishReason), true);
|
|
5267
5466
|
}
|
|
5268
5467
|
const tail = rec.splitter?.flush();
|
|
5269
|
-
if (tail?.spoken) this.options.host?.notify?.({ kind: "speak_utterance", message: tail.spoken });
|
|
5468
|
+
if (tail?.spoken && !rec.deliveryParked) this.options.host?.notify?.({ kind: "speak_utterance", message: tail.spoken });
|
|
5270
5469
|
if (res.text.trim()) this.voice.transcript.push({ role: "assistant", content: res.text });
|
|
5271
|
-
if (!rec.splitter?.spokeAny && res.text.trim())
|
|
5470
|
+
if (!rec.splitter?.spokeAny && res.text.trim() && !rec.deliveryParked)
|
|
5272
5471
|
this.options.host?.notify?.({ kind: "speak_utterance", message: res.text });
|
|
5273
5472
|
}
|
|
5274
5473
|
onWorkerFailed(id, err2) {
|
|
@@ -5278,7 +5477,7 @@ Another agent just implemented the above. Independently check the CURRENT state
|
|
|
5278
5477
|
this.dropAsk(rec.id);
|
|
5279
5478
|
rec.status = "error";
|
|
5280
5479
|
rec.result = msg;
|
|
5281
|
-
|
|
5480
|
+
log9.warn(`task ${rec.id} failed: ${msg}`);
|
|
5282
5481
|
this.notify("task_error", `task ${rec.id} (${rec.label}) failed: ${msg}`);
|
|
5283
5482
|
this.queueRevoice(this.integrationPrompt(rec, "error", msg, "error"), true);
|
|
5284
5483
|
}
|
|
@@ -5583,7 +5782,7 @@ init_logging();
|
|
|
5583
5782
|
|
|
5584
5783
|
// src/voice/engine.ts
|
|
5585
5784
|
init_logging();
|
|
5586
|
-
var
|
|
5785
|
+
var log10 = forComponent("VoiceEngine");
|
|
5587
5786
|
var now = () => performance.now();
|
|
5588
5787
|
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, ".");
|
|
5589
5788
|
var VoiceEngineOptions = class {
|
|
@@ -5651,6 +5850,11 @@ var VoiceEngineOptions = class {
|
|
|
5651
5850
|
* speech at all, an audible hiccup. Default OFF: the genuine-gated STT partial is the
|
|
5652
5851
|
* mechanism-correct pause trigger; enable only if barge-in onset feels sluggish in a clean-AEC room. */
|
|
5653
5852
|
overlapEnergyHold = false;
|
|
5853
|
+
/** Map inline `[emotion]` tags (emitted by the model, prompt-taught) into Cartesia inline emotion
|
|
5854
|
+
* tags in the spoken transcript (sonic-3 stitches the prosody). false = strip them silently. */
|
|
5855
|
+
emotions = true;
|
|
5856
|
+
/** Show the `[emotion]` tags in the on-screen echo (debug). false = hide (spoken-only). */
|
|
5857
|
+
showEmotions = false;
|
|
5654
5858
|
};
|
|
5655
5859
|
var VoiceEngine = class _VoiceEngine {
|
|
5656
5860
|
options;
|
|
@@ -5696,6 +5900,9 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5696
5900
|
// Central speech queue (above the TTS context): complete worker utterances serialize into ONE
|
|
5697
5901
|
// playback stream, one-at-a-time, never splicing into the live reflex's open utterance.
|
|
5698
5902
|
uttQueue = [];
|
|
5903
|
+
// Per-turn emotion-tag parser (reset on beginSpeech) — converts `[emotion]` → Cartesia inline tags
|
|
5904
|
+
// for TTS, tracks tag-free prose for echo discrimination, and surfaces display text for the screen.
|
|
5905
|
+
emo = null;
|
|
5699
5906
|
constructor(options) {
|
|
5700
5907
|
this.options = { ...new VoiceEngineOptions(), ...options };
|
|
5701
5908
|
const o = this.options;
|
|
@@ -5713,7 +5920,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5713
5920
|
this.stt.onLevel = (rms) => this.handleLevel(rms);
|
|
5714
5921
|
await Promise.all([this.tts.connect(), this.stt.start()]);
|
|
5715
5922
|
this.setState("listening");
|
|
5716
|
-
|
|
5923
|
+
log10.debug(`voice I/O up (${this.stt.usingAec ? "AEC" : "heuristic echo"} capture)`);
|
|
5717
5924
|
}
|
|
5718
5925
|
get usingAec() {
|
|
5719
5926
|
return this.stt.usingAec;
|
|
@@ -5722,6 +5929,10 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5722
5929
|
setBargeIn(on) {
|
|
5723
5930
|
this.options.bargeIn = on;
|
|
5724
5931
|
}
|
|
5932
|
+
/** Show/hide the `[emotion]` debug tags in the echo (next turn's stream picks it up). */
|
|
5933
|
+
setShowEmotions(on) {
|
|
5934
|
+
this.options.showEmotions = on;
|
|
5935
|
+
}
|
|
5725
5936
|
idleWaiters = [];
|
|
5726
5937
|
setState(s) {
|
|
5727
5938
|
if (this.state === s) return;
|
|
@@ -5753,6 +5964,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5753
5964
|
this.ctxOpen = true;
|
|
5754
5965
|
this.spokeDeltas = false;
|
|
5755
5966
|
this.reply = "";
|
|
5967
|
+
this.emo = this.options.emotions ? new EmotionStream(this.options.showEmotions) : null;
|
|
5756
5968
|
this.echoWords = new Set(this.words(this.prevReply));
|
|
5757
5969
|
this.tts.newContext();
|
|
5758
5970
|
if (ack && this.options.ackPhrase) {
|
|
@@ -5763,21 +5975,31 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5763
5975
|
if (!this.turnStartAt) this.turnStartAt = now();
|
|
5764
5976
|
this.setState("thinking");
|
|
5765
5977
|
}
|
|
5978
|
+
/** Feed a spoken delta. Returns the on-screen echo text (emotion tags shown/hidden per config) so the
|
|
5979
|
+
* host renders the SAME stream that was parsed for TTS — no second, state-doubling parse. */
|
|
5766
5980
|
speakDelta(text) {
|
|
5767
|
-
if (this.interrupted) return;
|
|
5981
|
+
if (this.interrupted) return "";
|
|
5768
5982
|
if (!this.speaking || !this.ctxOpen) this.beginSpeech();
|
|
5769
|
-
this.
|
|
5983
|
+
const { speech, display, prose } = this.emo ? this.emo.feed(text) : { speech: text, display: text, prose: text };
|
|
5984
|
+
this.reply += prose;
|
|
5770
5985
|
for (const w of this.words(this.reply)) this.echoWords.add(w);
|
|
5771
|
-
this.tts.speak(forSpeech(
|
|
5772
|
-
if (!this.spokeDeltas && this.turnStartAt)
|
|
5986
|
+
this.tts.speak(forSpeech(speech), true);
|
|
5987
|
+
if (!this.spokeDeltas && this.turnStartAt) log10.debug(`ttft: ${Math.round(now() - this.turnStartAt)}ms`);
|
|
5773
5988
|
this.spokeDeltas = true;
|
|
5774
5989
|
this.setState("speaking");
|
|
5990
|
+
return display;
|
|
5775
5991
|
}
|
|
5776
5992
|
/** close the spoken turn (idempotent); stays audible until ALL audio arrived AND playback drains */
|
|
5777
5993
|
endSpeech() {
|
|
5778
5994
|
this.interrupted = false;
|
|
5779
5995
|
if (!this.speaking) return;
|
|
5780
5996
|
this.ctxOpen = false;
|
|
5997
|
+
if (this.emo) {
|
|
5998
|
+
const t = this.emo.flush();
|
|
5999
|
+
this.emo = null;
|
|
6000
|
+
if (t.prose) this.reply += t.prose;
|
|
6001
|
+
if (t.speech) this.tts.speak(forSpeech(t.speech), true);
|
|
6002
|
+
}
|
|
5781
6003
|
if (this.reply) this.prevReply = this.reply;
|
|
5782
6004
|
const settle = () => {
|
|
5783
6005
|
if (this.ctxOpen) {
|
|
@@ -5790,7 +6012,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5790
6012
|
}
|
|
5791
6013
|
this.drainTimer = null;
|
|
5792
6014
|
this.speaking = false;
|
|
5793
|
-
if (this.turnStartAt)
|
|
6015
|
+
if (this.turnStartAt) log10.debug(`turn: ${Math.round(now() - this.turnStartAt)}ms (incl. playback)`);
|
|
5794
6016
|
this.echoUntil = now() + 2500;
|
|
5795
6017
|
if (!this.usingAec) this.stt.reset();
|
|
5796
6018
|
this.setState("listening");
|
|
@@ -5982,7 +6204,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
5982
6204
|
this.pendingUtt = this.mergeUtterance(this.pendingUtt, text);
|
|
5983
6205
|
if (this.pendingTimer) clearTimeout(this.pendingTimer);
|
|
5984
6206
|
if (this.options.incompleteMergeMs && this.looksIncomplete(this.pendingUtt)) {
|
|
5985
|
-
|
|
6207
|
+
log10.verbose(`hold: incomplete utterance "${this.pendingUtt.slice(-40)}"`);
|
|
5986
6208
|
this.options.onHold();
|
|
5987
6209
|
if (this.options.holdFiller && !this.speaking) {
|
|
5988
6210
|
this.beginSpeech();
|
|
@@ -6081,7 +6303,7 @@ async function resolveAuth(auth) {
|
|
|
6081
6303
|
}
|
|
6082
6304
|
|
|
6083
6305
|
// src/voice/soniox.ts
|
|
6084
|
-
var
|
|
6306
|
+
var log11 = forComponent("SonioxSTT");
|
|
6085
6307
|
var now2 = () => performance.now();
|
|
6086
6308
|
var SonioxSTTOptions = class {
|
|
6087
6309
|
auth = "";
|
|
@@ -6150,9 +6372,9 @@ var SonioxSTT = class {
|
|
|
6150
6372
|
this.ws.onmessage = (ev) => this.handle(JSON.parse(String(ev.data)));
|
|
6151
6373
|
this.ws.onclose = (ev) => {
|
|
6152
6374
|
if (this.stopped) return;
|
|
6153
|
-
|
|
6375
|
+
log11.warn(`soniox ws closed (${ev.code} ${ev.reason || ""}) \u2014 reconnecting`);
|
|
6154
6376
|
this.reset();
|
|
6155
|
-
this.connectWs().catch((e) =>
|
|
6377
|
+
this.connectWs().catch((e) => log11.error(`soniox reconnect failed: ${e.message}`));
|
|
6156
6378
|
};
|
|
6157
6379
|
}
|
|
6158
6380
|
async start() {
|
|
@@ -6162,7 +6384,7 @@ var SonioxSTT = class {
|
|
|
6162
6384
|
this.endpointTimer = setInterval(() => {
|
|
6163
6385
|
const combined = (this.finalText + this.partialText).trim();
|
|
6164
6386
|
if (!combined || now2() - this.lastChangeAt < this.options.silenceEndpointMs) return;
|
|
6165
|
-
if (this.firstTokenAt)
|
|
6387
|
+
if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192silence-endpoint, "${combined.slice(0, 60)}"`);
|
|
6166
6388
|
this.reset();
|
|
6167
6389
|
this.onUtterance(combined, now2());
|
|
6168
6390
|
}, 120);
|
|
@@ -6174,7 +6396,7 @@ var SonioxSTT = class {
|
|
|
6174
6396
|
if (this.stopped) return;
|
|
6175
6397
|
const ref = this.lastChunkAt || this.startedChunksAt;
|
|
6176
6398
|
if (now2() - ref > noAudioMs) {
|
|
6177
|
-
|
|
6399
|
+
log11.error(`stt: no mic audio for >${Math.round(noAudioMs / 1e3)}s \u2014 capture device stopped delivering`);
|
|
6178
6400
|
this.onFatal("microphone stopped delivering audio (try a different input device, e.g. AirPods, or check System Settings \u2192 Sound \u2192 Input)");
|
|
6179
6401
|
this.stop();
|
|
6180
6402
|
}
|
|
@@ -6194,7 +6416,7 @@ var SonioxSTT = class {
|
|
|
6194
6416
|
});
|
|
6195
6417
|
}
|
|
6196
6418
|
handle(m) {
|
|
6197
|
-
if (m.error_message) return
|
|
6419
|
+
if (m.error_message) return log11.error(`soniox: ${m.error_message}`);
|
|
6198
6420
|
let endpoint = false;
|
|
6199
6421
|
for (const t of m.tokens ?? []) {
|
|
6200
6422
|
if (t.text === "<end>") endpoint = true;
|
|
@@ -6210,7 +6432,7 @@ var SonioxSTT = class {
|
|
|
6210
6432
|
this.onPartial(combined);
|
|
6211
6433
|
if (endpoint && this.finalText.trim()) {
|
|
6212
6434
|
const utterance = this.finalText.trim();
|
|
6213
|
-
if (this.firstTokenAt)
|
|
6435
|
+
if (this.firstTokenAt) log11.debug(`stt: ${Math.round(now2() - this.firstTokenAt)}ms first-token\u2192endpoint, "${utterance.slice(0, 60)}"`);
|
|
6214
6436
|
this.reset();
|
|
6215
6437
|
this.onUtterance(utterance, now2());
|
|
6216
6438
|
}
|
|
@@ -6233,7 +6455,7 @@ var SonioxSTT = class {
|
|
|
6233
6455
|
|
|
6234
6456
|
// src/voice/cartesia.ts
|
|
6235
6457
|
init_logging();
|
|
6236
|
-
var
|
|
6458
|
+
var log12 = forComponent("CartesiaTTS");
|
|
6237
6459
|
var now3 = () => performance.now();
|
|
6238
6460
|
var CartesiaTTSOptions = class {
|
|
6239
6461
|
auth = "";
|
|
@@ -6283,9 +6505,9 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
6283
6505
|
this.ws.onerror = (e) => rej(new Error(`cartesia ws: ${e.message || "connect failed"}`));
|
|
6284
6506
|
});
|
|
6285
6507
|
this.ws.onclose = (ev) => {
|
|
6286
|
-
|
|
6508
|
+
log12.warn(`cartesia ws closed (${ev.code} ${ev.reason || ""})`);
|
|
6287
6509
|
if (!this.closed) {
|
|
6288
|
-
this.connecting = this.doConnect().catch((e) =>
|
|
6510
|
+
this.connecting = this.doConnect().catch((e) => log12.error(`cartesia reconnect failed: ${e.message}`));
|
|
6289
6511
|
}
|
|
6290
6512
|
};
|
|
6291
6513
|
this.ws.onmessage = (ev) => {
|
|
@@ -6307,11 +6529,11 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
6307
6529
|
this.down = true;
|
|
6308
6530
|
this.downAt = now3();
|
|
6309
6531
|
this.consecutiveOk = 0;
|
|
6310
|
-
|
|
6532
|
+
log12.warn(`TTS circuit breaker open \u2014 ${this.consecutiveErrors} consecutive errors, switching to text-only`);
|
|
6311
6533
|
this.onDone();
|
|
6312
6534
|
this.startProbe();
|
|
6313
6535
|
} else if (!this.down) {
|
|
6314
|
-
|
|
6536
|
+
log12.warn(`cartesia: ${JSON.stringify(m)}`);
|
|
6315
6537
|
}
|
|
6316
6538
|
}
|
|
6317
6539
|
};
|
|
@@ -6325,7 +6547,7 @@ var CartesiaTTS = class _CartesiaTTS {
|
|
|
6325
6547
|
this.consecutiveOk = 0;
|
|
6326
6548
|
this.stopProbe();
|
|
6327
6549
|
const downMs = this.downAt ? now3() - this.downAt : 0;
|
|
6328
|
-
(downMs < 2e3 ?
|
|
6550
|
+
(downMs < 2e3 ? log12.debug : log12.info)(`TTS recovered${downMs ? ` (down ${downMs}ms)` : ""}`);
|
|
6329
6551
|
}
|
|
6330
6552
|
/** Ensure the WS is open before sending — reconnects if idle-closed. */
|
|
6331
6553
|
async ensureConnected() {
|
|
@@ -6405,7 +6627,7 @@ import { MemFilesystem as MemFilesystem3, IndexedDbFilesystem, CommandExecutor a
|
|
|
6405
6627
|
init_logging();
|
|
6406
6628
|
import { spawn } from "child_process";
|
|
6407
6629
|
import { createHash } from "crypto";
|
|
6408
|
-
var
|
|
6630
|
+
var log13 = forComponent("mcp");
|
|
6409
6631
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
6410
6632
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
6411
6633
|
var StdioTransport = class {
|
|
@@ -6424,7 +6646,7 @@ var StdioTransport = class {
|
|
|
6424
6646
|
proc.stdout.setEncoding("utf8");
|
|
6425
6647
|
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
6426
6648
|
proc.stderr.setEncoding("utf8");
|
|
6427
|
-
proc.stderr.on("data", (chunk) =>
|
|
6649
|
+
proc.stderr.on("data", (chunk) => log13.debug(`[${command}] stderr:`, chunk.trimEnd()));
|
|
6428
6650
|
proc.on("exit", (code) => this.failAll(new Error(`MCP server "${command}" exited (code ${code})`)));
|
|
6429
6651
|
proc.on("error", (e) => this.failAll(e instanceof Error ? e : new Error(String(e))));
|
|
6430
6652
|
}
|
|
@@ -6438,7 +6660,7 @@ var StdioTransport = class {
|
|
|
6438
6660
|
try {
|
|
6439
6661
|
this.dispatch(JSON.parse(line));
|
|
6440
6662
|
} catch (e) {
|
|
6441
|
-
|
|
6663
|
+
log13.debug("dropping non-JSON line from MCP server:", line, e);
|
|
6442
6664
|
}
|
|
6443
6665
|
}
|
|
6444
6666
|
}
|
|
@@ -6487,7 +6709,7 @@ var StdioTransport = class {
|
|
|
6487
6709
|
try {
|
|
6488
6710
|
this.proc?.stdin?.end();
|
|
6489
6711
|
} catch (e) {
|
|
6490
|
-
|
|
6712
|
+
log13.debug("stdin end failed", e);
|
|
6491
6713
|
}
|
|
6492
6714
|
this.proc?.kill();
|
|
6493
6715
|
}
|
|
@@ -6556,7 +6778,7 @@ function parseSseResponse(body) {
|
|
|
6556
6778
|
const obj = JSON.parse(trimmed.slice(5).trim());
|
|
6557
6779
|
if (obj && (obj.result !== void 0 || obj.error !== void 0)) return obj;
|
|
6558
6780
|
} catch (e) {
|
|
6559
|
-
|
|
6781
|
+
log13.debug("skipping unparseable SSE data line", e);
|
|
6560
6782
|
}
|
|
6561
6783
|
}
|
|
6562
6784
|
return {};
|
|
@@ -6624,7 +6846,7 @@ async function mountWithDeadline(name, cfg, mountTimeoutMs) {
|
|
|
6624
6846
|
return { name, client, tools, specs, serverInfo: init?.serverInfo, config: cfg };
|
|
6625
6847
|
})(), mountTimeoutMs, name);
|
|
6626
6848
|
} catch (e) {
|
|
6627
|
-
await client.close().catch((err2) =>
|
|
6849
|
+
await client.close().catch((err2) => log13.debug(`close after failed mount of "${name}": ${err2}`));
|
|
6628
6850
|
throw e;
|
|
6629
6851
|
}
|
|
6630
6852
|
}
|
|
@@ -6635,15 +6857,15 @@ function validEntries(servers) {
|
|
|
6635
6857
|
return Object.entries(servers).filter(([name, cfg]) => {
|
|
6636
6858
|
if (!cfg || cfg.disabled) return false;
|
|
6637
6859
|
if (!cfg.command && !cfg.url) {
|
|
6638
|
-
|
|
6860
|
+
log13.warn(`MCP server "${name}" needs a command (stdio) or url (http) \u2014 skipping`);
|
|
6639
6861
|
return false;
|
|
6640
6862
|
}
|
|
6641
6863
|
return true;
|
|
6642
6864
|
});
|
|
6643
6865
|
}
|
|
6644
6866
|
function logMountFailure(name, e) {
|
|
6645
|
-
if (e instanceof McpAuthError)
|
|
6646
|
-
else
|
|
6867
|
+
if (e instanceof McpAuthError) log13.warn(`MCP "${name}" needs-auth: HTTP ${e.status} \u2014 set bearerToken or headers in its config; skipping`);
|
|
6868
|
+
else log13.error(`MCP server "${name}" failed to mount: ${e?.message ?? e}`);
|
|
6647
6869
|
}
|
|
6648
6870
|
async function mountMcpServers(servers = {}, opts = {}) {
|
|
6649
6871
|
const entries = validEntries(servers);
|
|
@@ -6653,7 +6875,7 @@ async function mountMcpServers(servers = {}, opts = {}) {
|
|
|
6653
6875
|
const name = entries[i][0];
|
|
6654
6876
|
if (r.status === "fulfilled") {
|
|
6655
6877
|
out.push(r.value);
|
|
6656
|
-
|
|
6878
|
+
log13.debug(`MCP "${name}" mounted \u2014 ${r.value.tools.length} tool(s)${r.value.serverInfo?.name ? ` from ${r.value.serverInfo.name}` : ""}`);
|
|
6657
6879
|
} else logMountFailure(name, r.reason);
|
|
6658
6880
|
});
|
|
6659
6881
|
return out;
|
|
@@ -6693,7 +6915,7 @@ var McpPool = class {
|
|
|
6693
6915
|
const prev = this.warm.get(key);
|
|
6694
6916
|
if (prev) {
|
|
6695
6917
|
clearTimeout(prev.timer);
|
|
6696
|
-
if (prev.client !== client) void prev.client.close().catch((err2) =>
|
|
6918
|
+
if (prev.client !== client) void prev.client.close().catch((err2) => log13.debug(`warm-pool replace close failed: ${err2}`));
|
|
6697
6919
|
}
|
|
6698
6920
|
const e = { client, timer: void 0 };
|
|
6699
6921
|
this.warm.set(key, e);
|
|
@@ -6710,7 +6932,7 @@ var McpPool = class {
|
|
|
6710
6932
|
const e = this.warm.get(key);
|
|
6711
6933
|
if (!e) return;
|
|
6712
6934
|
this.warm.delete(key);
|
|
6713
|
-
await e.client.close().catch((err2) =>
|
|
6935
|
+
await e.client.close().catch((err2) => log13.debug(`warm-pool evict close failed: ${err2}`));
|
|
6714
6936
|
}
|
|
6715
6937
|
async closeAll() {
|
|
6716
6938
|
for (const e of this.warm.values()) {
|
|
@@ -6928,7 +7150,7 @@ init_tools_shell();
|
|
|
6928
7150
|
// src/tools.notify.ts
|
|
6929
7151
|
init_logging();
|
|
6930
7152
|
import { execFile } from "child_process";
|
|
6931
|
-
var
|
|
7153
|
+
var log15 = forComponent("notify");
|
|
6932
7154
|
function makeNotifyTool(opts = {}) {
|
|
6933
7155
|
const platform2 = opts.platform ?? process.platform;
|
|
6934
7156
|
const run = opts.exec ?? execFile;
|
|
@@ -6952,7 +7174,7 @@ function makeNotifyTool(opts = {}) {
|
|
|
6952
7174
|
return new Promise((resolve4) => {
|
|
6953
7175
|
run(argv[0], argv[1], { timeout: 5e3 }, (e) => {
|
|
6954
7176
|
if (e) {
|
|
6955
|
-
|
|
7177
|
+
log15.debug("notification failed", e);
|
|
6956
7178
|
resolve4(`Notification failed: ${e.message}`);
|
|
6957
7179
|
} else resolve4("Notification shown.");
|
|
6958
7180
|
});
|
|
@@ -6968,7 +7190,7 @@ import { BodDB as BodDB2 } from "@bod.ee/db";
|
|
|
6968
7190
|
init_logging();
|
|
6969
7191
|
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
|
|
6970
7192
|
import { homedir } from "os";
|
|
6971
|
-
var
|
|
7193
|
+
var log16 = forComponent("cli-util");
|
|
6972
7194
|
function dotDirs(base, sub, opts = {}) {
|
|
6973
7195
|
const home = opts.home ?? homedir();
|
|
6974
7196
|
const dirs = [`${base}/.agent/${sub}`, `${base}/.claude/${sub}`, `${home}/.agent/${sub}`, `${home}/.claude/${sub}`];
|
|
@@ -6985,7 +7207,7 @@ function parseJson(text, fallback, what = "json") {
|
|
|
6985
7207
|
try {
|
|
6986
7208
|
return JSON.parse(text);
|
|
6987
7209
|
} catch (e) {
|
|
6988
|
-
|
|
7210
|
+
log16.debug(`parseJson(${what}) failed: ${e.message}`);
|
|
6989
7211
|
return fallback;
|
|
6990
7212
|
}
|
|
6991
7213
|
}
|
|
@@ -6995,7 +7217,7 @@ function readJsonFile(path, fallback) {
|
|
|
6995
7217
|
try {
|
|
6996
7218
|
text = readFileSync3(path, "utf8");
|
|
6997
7219
|
} catch (e) {
|
|
6998
|
-
|
|
7220
|
+
log16.debug(`readJsonFile(${path}) unreadable: ${e.message}`);
|
|
6999
7221
|
return fallback;
|
|
7000
7222
|
}
|
|
7001
7223
|
return parseJson(text, fallback, path);
|
|
@@ -7248,7 +7470,7 @@ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as rea
|
|
|
7248
7470
|
import { homedir as homedir3 } from "os";
|
|
7249
7471
|
import { dirname as dirname3, join as join6 } from "path";
|
|
7250
7472
|
import { fileURLToPath } from "url";
|
|
7251
|
-
var
|
|
7473
|
+
var log17 = forComponent("VoiceIO");
|
|
7252
7474
|
var now4 = () => performance.now();
|
|
7253
7475
|
var Player = class {
|
|
7254
7476
|
proc = null;
|
|
@@ -7262,7 +7484,7 @@ var Player = class {
|
|
|
7262
7484
|
["-loglevel", "quiet", "-nodisp", "-fflags", "nobuffer", "-flags", "low_delay", "-probesize", "32", "-f", "s16le", "-ar", String(TTS_SAMPLE_RATE), "-ch_layout", "mono", "-i", "-"],
|
|
7263
7485
|
{ stdio: ["pipe", "ignore", "ignore"] }
|
|
7264
7486
|
);
|
|
7265
|
-
this.proc.on("error", (e) =>
|
|
7487
|
+
this.proc.on("error", (e) => log17.warn(`ffplay error: ${e.message}`));
|
|
7266
7488
|
this.proc.stdin.on("error", () => {
|
|
7267
7489
|
});
|
|
7268
7490
|
this.bytesWritten = 0;
|
|
@@ -7300,7 +7522,7 @@ function detectFfmpegMic() {
|
|
|
7300
7522
|
const devices = [...audio.matchAll(/\[(\d+)\] (.+)/g)].map(([, idx, name]) => ({ idx, name: name.trim() }));
|
|
7301
7523
|
const mic = devices.find((d) => /microphone|built-in/i.test(d.name) && !/teams|blackhole|loopback/i.test(d.name)) ?? devices[0];
|
|
7302
7524
|
if (!mic) throw new Error("no audio input device found");
|
|
7303
|
-
|
|
7525
|
+
log17.debug(`ffmpeg mic: [${mic.idx}] ${mic.name}`);
|
|
7304
7526
|
return `:${mic.idx}`;
|
|
7305
7527
|
}
|
|
7306
7528
|
function detectedInputDevice() {
|
|
@@ -7336,15 +7558,15 @@ function resolveAecBinary() {
|
|
|
7336
7558
|
if (existsSync4(bin) && statSync3(bin).mtimeMs >= statSync3(src).mtimeMs) return bin;
|
|
7337
7559
|
if (spawnSync2("which", ["swiftc"]).status !== 0) return null;
|
|
7338
7560
|
mkdirSync5(cacheDir, { recursive: true });
|
|
7339
|
-
|
|
7561
|
+
log17.info("compiling AEC mic helper (first run)\u2026");
|
|
7340
7562
|
const build = spawnSync2("swiftc", ["-O", "-o", bin, src, "-Xlinker", "-sectcreate", "-Xlinker", "__TEXT", "-Xlinker", "__info_plist", "-Xlinker", plist], { encoding: "utf8" });
|
|
7341
7563
|
if (build.status !== 0) {
|
|
7342
|
-
|
|
7564
|
+
log17.warn(`AEC build failed: ${build.stderr?.slice(0, 400)}`);
|
|
7343
7565
|
return null;
|
|
7344
7566
|
}
|
|
7345
7567
|
const sign = spawnSync2("codesign", ["-fs", "-", bin], { encoding: "utf8" });
|
|
7346
7568
|
if (sign.status !== 0) {
|
|
7347
|
-
|
|
7569
|
+
log17.warn(`codesign failed: ${sign.stderr?.slice(0, 200)}`);
|
|
7348
7570
|
return null;
|
|
7349
7571
|
}
|
|
7350
7572
|
return bin;
|
|
@@ -7386,16 +7608,16 @@ var NodeMicSource = class {
|
|
|
7386
7608
|
this.proc = spawn2(this.bin, [], { stdio: ["ignore", "pipe", "ignore"] });
|
|
7387
7609
|
} else {
|
|
7388
7610
|
if (spawnSync2("which", ["ffmpeg"]).status !== 0) throw new Error("voice I/O unavailable: no AEC helper and no ffmpeg on PATH");
|
|
7389
|
-
|
|
7611
|
+
log17.info("mic: raw capture (no AEC) \u2014 echo handled heuristically; headphones recommended");
|
|
7390
7612
|
this.proc = spawn2(
|
|
7391
7613
|
"ffmpeg",
|
|
7392
7614
|
["-loglevel", "error", "-f", "avfoundation", "-i", detectFfmpegMic(), "-ar", String(STT_SAMPLE_RATE), "-ac", "1", "-f", "s16le", "-"],
|
|
7393
7615
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
7394
7616
|
);
|
|
7395
|
-
this.proc.stderr.on("data", (d) =>
|
|
7617
|
+
this.proc.stderr.on("data", (d) => log17.warn(`ffmpeg: ${String(d).trim()}`));
|
|
7396
7618
|
}
|
|
7397
7619
|
this.proc.on("exit", (c) => {
|
|
7398
|
-
if (c && !this.stopped)
|
|
7620
|
+
if (c && !this.stopped) log17.error(`mic capture exited (${c}) \u2014 check mic permission / MIC_DEVICE / MIC_AEC=0`);
|
|
7399
7621
|
});
|
|
7400
7622
|
this.proc.stdout.on("data", (chunk) => onChunk(chunk));
|
|
7401
7623
|
}
|
|
@@ -7461,7 +7683,7 @@ var AecDuplexAudio = class {
|
|
|
7461
7683
|
this.proc.stdin.on("error", () => {
|
|
7462
7684
|
});
|
|
7463
7685
|
this.proc.on("exit", (c) => {
|
|
7464
|
-
if (c && !this.stopped)
|
|
7686
|
+
if (c && !this.stopped) log17.error(`aec duplex audio exited (${c}) \u2014 check mic permission / MIC_AEC=0`);
|
|
7465
7687
|
});
|
|
7466
7688
|
this.proc.stdout.on("data", (chunk) => {
|
|
7467
7689
|
this.gotChunk = true;
|
|
@@ -7477,7 +7699,7 @@ var AecDuplexAudio = class {
|
|
|
7477
7699
|
openMicSettings();
|
|
7478
7700
|
this.onFatal?.("microphone permission denied \u2014 enable it in System Settings \u2192 Privacy & Security \u2192 Microphone for your terminal, then restart it");
|
|
7479
7701
|
}
|
|
7480
|
-
} else
|
|
7702
|
+
} else log17.debug(`mic-aec: ${s}`);
|
|
7481
7703
|
}
|
|
7482
7704
|
});
|
|
7483
7705
|
if (!this.noVpio && !this.triedFallback) {
|
|
@@ -7486,7 +7708,7 @@ var AecDuplexAudio = class {
|
|
|
7486
7708
|
this.triedFallback = true;
|
|
7487
7709
|
this.noVpio = true;
|
|
7488
7710
|
this._aec = false;
|
|
7489
|
-
|
|
7711
|
+
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)");
|
|
7490
7712
|
this.onDegrade?.();
|
|
7491
7713
|
this.killProc();
|
|
7492
7714
|
this.spawnHelper();
|
|
@@ -7594,6 +7816,10 @@ var VoiceIOOptions = class extends VoiceEngineOptions {
|
|
|
7594
7816
|
sonioxApiKey = process.env.SONIOX_API_KEY ?? "";
|
|
7595
7817
|
cartesiaApiKey = process.env.CARTESIA_API_KEY ?? "";
|
|
7596
7818
|
cartesiaVoiceId = process.env.CARTESIA_VOICE_ID ?? "";
|
|
7819
|
+
emotions = process.env.VOICE_EMOTIONS !== "0";
|
|
7820
|
+
// Cartesia inline emotion tags (sonic-3)
|
|
7821
|
+
showEmotions = process.env.VOICE_SHOW_EMOTIONS === "1";
|
|
7822
|
+
// surface the tags in the on-screen echo (debug; opt-in)
|
|
7597
7823
|
};
|
|
7598
7824
|
var VoiceIO = class extends VoiceEngine {
|
|
7599
7825
|
duplexSource;
|
|
@@ -7737,7 +7963,7 @@ async function loadConfig(cwd) {
|
|
|
7737
7963
|
|
|
7738
7964
|
// cli/hooks-config.ts
|
|
7739
7965
|
import { spawnSync as spawnSync3 } from "child_process";
|
|
7740
|
-
var
|
|
7966
|
+
var log18 = forComponent("hooks");
|
|
7741
7967
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
7742
7968
|
function ruleMatches(rule, toolName) {
|
|
7743
7969
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -7754,7 +7980,7 @@ function runCmd(rule, env) {
|
|
|
7754
7980
|
});
|
|
7755
7981
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
7756
7982
|
} catch (e) {
|
|
7757
|
-
|
|
7983
|
+
log18.debug(`hook command failed: ${rule.command}`, e);
|
|
7758
7984
|
return { code: 1, out: String(e?.message ?? e) };
|
|
7759
7985
|
}
|
|
7760
7986
|
}
|
|
@@ -7861,7 +8087,7 @@ function formatDiff(ops, opts = {}) {
|
|
|
7861
8087
|
import { existsSync as existsSync6, mkdirSync as mkdirSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync4, readdirSync, renameSync, symlinkSync as symlinkSync2, unlinkSync, readlinkSync } from "fs";
|
|
7862
8088
|
import { homedir as homedir5 } from "os";
|
|
7863
8089
|
import { join as join8 } from "path";
|
|
7864
|
-
var
|
|
8090
|
+
var log19 = forComponent("session");
|
|
7865
8091
|
var globalDir = () => join8(homedir5(), ".agent", "sessions");
|
|
7866
8092
|
var SessionStore = class {
|
|
7867
8093
|
dir;
|
|
@@ -7906,7 +8132,7 @@ var SessionStore = class {
|
|
|
7906
8132
|
}
|
|
7907
8133
|
load(id) {
|
|
7908
8134
|
if (!this.safeId(id)) {
|
|
7909
|
-
|
|
8135
|
+
log19.debug(`rejecting unsafe session id: ${id}`);
|
|
7910
8136
|
return void 0;
|
|
7911
8137
|
}
|
|
7912
8138
|
const path = join8(this.dir, `${id}.json`);
|
|
@@ -7914,7 +8140,7 @@ var SessionStore = class {
|
|
|
7914
8140
|
try {
|
|
7915
8141
|
return JSON.parse(readFileSync6(path, "utf8"));
|
|
7916
8142
|
} catch (e) {
|
|
7917
|
-
|
|
8143
|
+
log19.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
7918
8144
|
return void 0;
|
|
7919
8145
|
}
|
|
7920
8146
|
}
|
|
@@ -7927,7 +8153,7 @@ var SessionStore = class {
|
|
|
7927
8153
|
try {
|
|
7928
8154
|
metas.push(JSON.parse(readFileSync6(join8(this.dir, f), "utf8")).meta);
|
|
7929
8155
|
} catch (e) {
|
|
7930
|
-
|
|
8156
|
+
log19.debug(`skipping unreadable session file ${f}`, e);
|
|
7931
8157
|
}
|
|
7932
8158
|
}
|
|
7933
8159
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -8095,7 +8321,7 @@ import { execFile as execFile3 } from "child_process";
|
|
|
8095
8321
|
import { promisify } from "util";
|
|
8096
8322
|
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync7 } from "fs";
|
|
8097
8323
|
import { join as join9, resolve as resolve2, sep as sep2 } from "path";
|
|
8098
|
-
var
|
|
8324
|
+
var log20 = forComponent("checkpoints");
|
|
8099
8325
|
var exec = promisify(execFile3);
|
|
8100
8326
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
8101
8327
|
var ShadowRepo = class {
|
|
@@ -8133,7 +8359,7 @@ var ShadowRepo = class {
|
|
|
8133
8359
|
writeFileSync5(join9(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
8134
8360
|
this.ready = true;
|
|
8135
8361
|
} catch (e) {
|
|
8136
|
-
|
|
8362
|
+
log20.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
8137
8363
|
this.ready = false;
|
|
8138
8364
|
}
|
|
8139
8365
|
return this.ready;
|
|
@@ -8144,7 +8370,7 @@ var ShadowRepo = class {
|
|
|
8144
8370
|
}
|
|
8145
8371
|
async commit(label, forced = []) {
|
|
8146
8372
|
await this.run("add", "-A");
|
|
8147
|
-
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) =>
|
|
8373
|
+
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log20.debug(`force-add failed: ${p}`, e));
|
|
8148
8374
|
await this.run("commit", "--allow-empty", "-q", "-m", label);
|
|
8149
8375
|
}
|
|
8150
8376
|
/** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
|
|
@@ -8152,8 +8378,8 @@ var ShadowRepo = class {
|
|
|
8152
8378
|
* turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
|
|
8153
8379
|
* point per turn, so the REPL's turn↔frame mapping stays intact. */
|
|
8154
8380
|
async amendForced(paths) {
|
|
8155
|
-
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) =>
|
|
8156
|
-
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) =>
|
|
8381
|
+
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log20.debug(`force-capture failed: ${p}`, e));
|
|
8382
|
+
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log20.debug("amend failed", e));
|
|
8157
8383
|
}
|
|
8158
8384
|
/** Commits on `ref`, oldest-first (canonical index space). */
|
|
8159
8385
|
async log(ref) {
|
|
@@ -8213,7 +8439,7 @@ var ShadowRepo = class {
|
|
|
8213
8439
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
8214
8440
|
});
|
|
8215
8441
|
} catch (e) {
|
|
8216
|
-
|
|
8442
|
+
log20.debug("checkpoint prune failed", e);
|
|
8217
8443
|
}
|
|
8218
8444
|
}
|
|
8219
8445
|
};
|
|
@@ -8272,7 +8498,7 @@ var GitCheckpoints = class {
|
|
|
8272
8498
|
use(sessionId) {
|
|
8273
8499
|
if (sessionId === this.session) return;
|
|
8274
8500
|
this.session = sessionId;
|
|
8275
|
-
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) =>
|
|
8501
|
+
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log20.debug("re-point failed", e));
|
|
8276
8502
|
}
|
|
8277
8503
|
async begin(label) {
|
|
8278
8504
|
if (!await this.start()) return;
|
|
@@ -8284,7 +8510,7 @@ var GitCheckpoints = class {
|
|
|
8284
8510
|
try {
|
|
8285
8511
|
await this.repos[i].commit(msg, forced);
|
|
8286
8512
|
} catch (e) {
|
|
8287
|
-
|
|
8513
|
+
log20.debug("checkpoint commit failed", e);
|
|
8288
8514
|
}
|
|
8289
8515
|
}
|
|
8290
8516
|
if (slow) clearTimeout(slow);
|
|
@@ -8314,7 +8540,7 @@ var GitCheckpoints = class {
|
|
|
8314
8540
|
if (this.forced.has(abs)) continue;
|
|
8315
8541
|
this.forced.add(abs);
|
|
8316
8542
|
const i = this.repoIndexFor(abs);
|
|
8317
|
-
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) =>
|
|
8543
|
+
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log20.debug("amendForced failed", e));
|
|
8318
8544
|
}
|
|
8319
8545
|
}
|
|
8320
8546
|
};
|
|
@@ -10020,7 +10246,7 @@ import { spawnSync as spawnSync5 } from "child_process";
|
|
|
10020
10246
|
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync9, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
|
|
10021
10247
|
import { homedir as homedir7 } from "os";
|
|
10022
10248
|
import { join as join12 } from "path";
|
|
10023
|
-
var
|
|
10249
|
+
var log21 = forComponent("os-sched");
|
|
10024
10250
|
var OsScheduler = class {
|
|
10025
10251
|
options;
|
|
10026
10252
|
constructor(options) {
|
|
@@ -10082,7 +10308,7 @@ var OsScheduler = class {
|
|
|
10082
10308
|
}
|
|
10083
10309
|
}
|
|
10084
10310
|
} catch (e) {
|
|
10085
|
-
|
|
10311
|
+
log21.debug(`cancel ${id}`, e);
|
|
10086
10312
|
}
|
|
10087
10313
|
for (const f of [`${id}.json`, `${id}.sh`]) {
|
|
10088
10314
|
try {
|
|
@@ -10199,7 +10425,7 @@ import { spawn as spawn3 } from "child_process";
|
|
|
10199
10425
|
import { existsSync as existsSync8, mkdirSync as mkdirSync10, unlinkSync as unlinkSync4, readdirSync as readdirSync3 } from "fs";
|
|
10200
10426
|
import { homedir as homedir8 } from "os";
|
|
10201
10427
|
import { join as join13 } from "path";
|
|
10202
|
-
var
|
|
10428
|
+
var log22 = forComponent("remote-trigger");
|
|
10203
10429
|
var TRIGGER_DIR = () => join13(homedir8(), ".agent", "triggers");
|
|
10204
10430
|
var sockPath = (sessionId, dir = TRIGGER_DIR()) => join13(dir, `${sessionId}.sock`);
|
|
10205
10431
|
var TriggerServer = class {
|
|
@@ -10237,13 +10463,13 @@ var TriggerServer = class {
|
|
|
10237
10463
|
conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
10238
10464
|
}
|
|
10239
10465
|
});
|
|
10240
|
-
conn.on("error", (e) =>
|
|
10466
|
+
conn.on("error", (e) => log22.debug("trigger conn error", e));
|
|
10241
10467
|
});
|
|
10242
|
-
this.server.on("error", (e) =>
|
|
10468
|
+
this.server.on("error", (e) => log22.debug("trigger server error", e));
|
|
10243
10469
|
this.server.listen(p);
|
|
10244
10470
|
this.path = p;
|
|
10245
10471
|
} catch (e) {
|
|
10246
|
-
|
|
10472
|
+
log22.debug("trigger server unavailable", e);
|
|
10247
10473
|
}
|
|
10248
10474
|
}
|
|
10249
10475
|
/** Re-bind on /resume (the session id changed). */
|
|
@@ -10364,7 +10590,7 @@ var italic = C("3");
|
|
|
10364
10590
|
var strike = C("9");
|
|
10365
10591
|
var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
|
|
10366
10592
|
var err = (s) => process.stderr.write(s);
|
|
10367
|
-
var
|
|
10593
|
+
var log23 = forComponent("cli");
|
|
10368
10594
|
var VERSION = (() => {
|
|
10369
10595
|
try {
|
|
10370
10596
|
return JSON.parse(readFileSync8(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -11003,7 +11229,7 @@ function costOf(pricing, promptTokens = 0, completionTokens = 0, cacheCreationTo
|
|
|
11003
11229
|
function turnCost(model, usage) {
|
|
11004
11230
|
return costOf(getModelInfo(model)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, model);
|
|
11005
11231
|
}
|
|
11006
|
-
async function evaluateGoal(ai, condition, transcript,
|
|
11232
|
+
async function evaluateGoal(ai, condition, transcript, log24) {
|
|
11007
11233
|
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
11008
11234
|
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
11009
11235
|
return text.slice(0, 600);
|
|
@@ -11023,7 +11249,7 @@ ${recent}` }
|
|
|
11023
11249
|
const match = r.content.match(/\{[\s\S]*\}/);
|
|
11024
11250
|
if (match) return JSON.parse(match[0]);
|
|
11025
11251
|
} catch (e) {
|
|
11026
|
-
|
|
11252
|
+
log24(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
11027
11253
|
`));
|
|
11028
11254
|
}
|
|
11029
11255
|
return { met: false, reason: "evaluation unclear" };
|
|
@@ -11235,7 +11461,7 @@ function mcpAgentTools(mounted, opts) {
|
|
|
11235
11461
|
return tools;
|
|
11236
11462
|
}
|
|
11237
11463
|
async function closeMcp(mounted) {
|
|
11238
|
-
await Promise.all(mounted.map((m) => m.client.close().catch((e) =>
|
|
11464
|
+
await Promise.all(mounted.map((m) => m.client.close().catch((e) => log23.debug("mcp close failed", e))));
|
|
11239
11465
|
}
|
|
11240
11466
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
11241
11467
|
function mentionRefs(line) {
|
|
@@ -11285,7 +11511,7 @@ async function expandMentions(fs, line) {
|
|
|
11285
11511
|
if (loaded.includes(ref) || missing.includes(ref)) continue;
|
|
11286
11512
|
if (ref.includes(":") && mcpMentionResolver) {
|
|
11287
11513
|
const body = await mcpMentionResolver(ref).catch((e) => {
|
|
11288
|
-
|
|
11514
|
+
log23.debug("mcp mention resolve failed", e);
|
|
11289
11515
|
return null;
|
|
11290
11516
|
});
|
|
11291
11517
|
if (body != null) {
|
|
@@ -11368,7 +11594,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
11368
11594
|
try {
|
|
11369
11595
|
store.save(session);
|
|
11370
11596
|
} catch (ex) {
|
|
11371
|
-
|
|
11597
|
+
log23.debug("mid-turn session flush failed", ex);
|
|
11372
11598
|
}
|
|
11373
11599
|
}
|
|
11374
11600
|
return origNotify(e);
|
|
@@ -11515,14 +11741,14 @@ var isCancelTeardown = (e) => {
|
|
|
11515
11741
|
function installCancelGuards(mounted) {
|
|
11516
11742
|
process.on("unhandledRejection", (e) => {
|
|
11517
11743
|
if (isCancelTeardown(e)) {
|
|
11518
|
-
|
|
11744
|
+
log23.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
11519
11745
|
return;
|
|
11520
11746
|
}
|
|
11521
|
-
|
|
11747
|
+
log23.error("unhandledRejection", e);
|
|
11522
11748
|
});
|
|
11523
11749
|
process.on("uncaughtException", (e) => {
|
|
11524
11750
|
if (isCancelTeardown(e)) {
|
|
11525
|
-
|
|
11751
|
+
log23.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
11526
11752
|
return;
|
|
11527
11753
|
}
|
|
11528
11754
|
console.error(e);
|
|
@@ -11582,6 +11808,8 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
11582
11808
|
let dx;
|
|
11583
11809
|
let voiceIO;
|
|
11584
11810
|
let voiceLineOpen = false;
|
|
11811
|
+
const emotionsOn = process.env.VOICE_EMOTIONS !== "0";
|
|
11812
|
+
let showEmotions = process.env.VOICE_SHOW_EMOTIONS === "1";
|
|
11585
11813
|
const voiceEcho = (text) => {
|
|
11586
11814
|
const s = forSpeech(text);
|
|
11587
11815
|
if (!s) return;
|
|
@@ -11656,7 +11884,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
11656
11884
|
spinner.stop();
|
|
11657
11885
|
voiceIO.enqueueUtterance(e.message);
|
|
11658
11886
|
editorRef?.suspend();
|
|
11659
|
-
voiceEcho(e.message);
|
|
11887
|
+
voiceEcho(emotionsOn ? renderEmotions(e.message, { show: showEmotions }).display : e.message);
|
|
11660
11888
|
voiceEchoEnd();
|
|
11661
11889
|
editorRef?.resume();
|
|
11662
11890
|
editorRef?.redrawNow();
|
|
@@ -11670,9 +11898,9 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
11670
11898
|
}
|
|
11671
11899
|
if (e.kind === "text_delta" && voiceIO) {
|
|
11672
11900
|
spinner.stop();
|
|
11673
|
-
voiceIO.speakDelta(e.message);
|
|
11901
|
+
const echo = voiceIO.speakDelta(e.message);
|
|
11674
11902
|
editorRef?.suspend();
|
|
11675
|
-
voiceEcho(
|
|
11903
|
+
voiceEcho(echo);
|
|
11676
11904
|
return;
|
|
11677
11905
|
} else if (e.kind === "text_delta" && stashText()) {
|
|
11678
11906
|
process.stdout.write("\r\x1B[K");
|
|
@@ -11746,8 +11974,8 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
11746
11974
|
providerOptionsFor: (m) => cursorProviderOptions(m, cwd, cfg.mcpServers),
|
|
11747
11975
|
...(args.thinkModel ?? cfg.thinkModel) !== void 0 ? { thinkModel: (args.thinkModel ?? cfg.thinkModel) === false ? false : resolveModelOrNewest(String(args.thinkModel ?? cfg.thinkModel)) } : {},
|
|
11748
11976
|
host,
|
|
11749
|
-
...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true } : {},
|
|
11750
|
-
// voice: progress asides + worker questions relayed through the conversation
|
|
11977
|
+
...args.voice ? { voiceStyle: "conversational", progressUpdates: true, askRelay: true, emotionTags: emotionsOn } : {},
|
|
11978
|
+
// voice: progress asides + worker questions relayed through the conversation; emotion tags taught only with a TTS that speaks them
|
|
11751
11979
|
// Per-TASK checkpoint frames (the natural undo unit in duplex = one delegation): opened BEFORE
|
|
11752
11980
|
// the worker spawns (post-spawn would race its first edits). `checkpoints` is bound below.
|
|
11753
11981
|
onTaskStart: async (_id, label) => {
|
|
@@ -11999,7 +12227,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
11999
12227
|
mkdirSync11(join14(cwd, ".agent"), { recursive: true });
|
|
12000
12228
|
appendFileSync(histPath, line + "\n");
|
|
12001
12229
|
} catch (e) {
|
|
12002
|
-
|
|
12230
|
+
log23.debug("history write failed", e);
|
|
12003
12231
|
}
|
|
12004
12232
|
};
|
|
12005
12233
|
const ago = (t) => {
|
|
@@ -12070,7 +12298,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
12070
12298
|
try {
|
|
12071
12299
|
store.save(session);
|
|
12072
12300
|
} catch (e) {
|
|
12073
|
-
|
|
12301
|
+
log23.debug("session save after rewind failed", e);
|
|
12074
12302
|
}
|
|
12075
12303
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
12076
12304
|
`));
|
|
@@ -12104,7 +12332,7 @@ ${task}`;
|
|
|
12104
12332
|
bangContext.length = 0;
|
|
12105
12333
|
}
|
|
12106
12334
|
const delta = await refreshCatalogs().catch((e) => {
|
|
12107
|
-
|
|
12335
|
+
log23.debug("catalog refresh failed", e);
|
|
12108
12336
|
return "";
|
|
12109
12337
|
});
|
|
12110
12338
|
if (delta) {
|
|
@@ -12367,7 +12595,7 @@ ${task}`;
|
|
|
12367
12595
|
desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
|
|
12368
12596
|
run: async () => {
|
|
12369
12597
|
await refreshCatalogs().catch((e) => {
|
|
12370
|
-
|
|
12598
|
+
log23.debug("catalog refresh failed", e);
|
|
12371
12599
|
});
|
|
12372
12600
|
face.reprepare();
|
|
12373
12601
|
err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
|
|
@@ -12437,6 +12665,24 @@ ${task}`;
|
|
|
12437
12665
|
}
|
|
12438
12666
|
await toggleVoice();
|
|
12439
12667
|
}
|
|
12668
|
+
}, "voice-emotions": {
|
|
12669
|
+
desc: "show/hide the [emotion] debug tags in the echo \u2014 /voice-emotions <on|off> (TTS emotion control stays on regardless)",
|
|
12670
|
+
run: async (a) => {
|
|
12671
|
+
if (!emotionsOn) {
|
|
12672
|
+
err(dim(" (emotion control is off \u2014 VOICE_EMOTIONS=0)\n"));
|
|
12673
|
+
return;
|
|
12674
|
+
}
|
|
12675
|
+
const v = a[0]?.toLowerCase();
|
|
12676
|
+
if (v === "on" || v === "off") {
|
|
12677
|
+
showEmotions = v === "on";
|
|
12678
|
+
voiceIO?.setShowEmotions(showEmotions);
|
|
12679
|
+
err(green(` \u2713 emotion tags ${showEmotions ? "shown" : "hidden"} in echo
|
|
12680
|
+
`));
|
|
12681
|
+
return;
|
|
12682
|
+
}
|
|
12683
|
+
err(dim(` emotion tags in echo: ${showEmotions ? "shown" : "hidden"} (use /voice-emotions on|off)
|
|
12684
|
+
`));
|
|
12685
|
+
}
|
|
12440
12686
|
}, "voice-model": {
|
|
12441
12687
|
desc: "switch the reflex (voice) model \u2014 /voice-model <id>, or alone for a picker",
|
|
12442
12688
|
run: async (a) => {
|
|
@@ -12829,7 +13075,7 @@ ${task}`;
|
|
|
12829
13075
|
try {
|
|
12830
13076
|
for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
|
|
12831
13077
|
} catch (e) {
|
|
12832
|
-
|
|
13078
|
+
log23.debug(`loadAgents(${d}) failed`, e);
|
|
12833
13079
|
}
|
|
12834
13080
|
}
|
|
12835
13081
|
if (!seen.size) {
|
|
@@ -12917,7 +13163,7 @@ ${task}`;
|
|
|
12917
13163
|
}
|
|
12918
13164
|
if (idx >= 0) {
|
|
12919
13165
|
const old = mounted.splice(idx, 1)[0];
|
|
12920
|
-
await old.client.close().catch((e) =>
|
|
13166
|
+
await old.client.close().catch((e) => log23.debug("mcp close failed", e));
|
|
12921
13167
|
}
|
|
12922
13168
|
try {
|
|
12923
13169
|
const m = await mountMcpServer(name, conf);
|
|
@@ -12945,7 +13191,7 @@ ${task}`;
|
|
|
12945
13191
|
}
|
|
12946
13192
|
const m = mounted.splice(idx, 1)[0];
|
|
12947
13193
|
remountMcpTools();
|
|
12948
|
-
await m.client.close().catch((e) =>
|
|
13194
|
+
await m.client.close().catch((e) => log23.debug("mcp close failed", e));
|
|
12949
13195
|
err(dim(` removed "${name}"
|
|
12950
13196
|
`));
|
|
12951
13197
|
return;
|
|
@@ -13165,7 +13411,7 @@ ${task}`;
|
|
|
13165
13411
|
try {
|
|
13166
13412
|
return readdirSync4(join14(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
13167
13413
|
} catch (e) {
|
|
13168
|
-
|
|
13414
|
+
log23.debug("completion readdir failed", absDir, e);
|
|
13169
13415
|
return null;
|
|
13170
13416
|
}
|
|
13171
13417
|
};
|
|
@@ -13292,7 +13538,7 @@ ${out}
|
|
|
13292
13538
|
return;
|
|
13293
13539
|
}
|
|
13294
13540
|
await refreshCatalogs().catch((e) => {
|
|
13295
|
-
|
|
13541
|
+
log23.debug("catalog refresh failed", e);
|
|
13296
13542
|
});
|
|
13297
13543
|
const sk = skills.find((x) => x.name === name);
|
|
13298
13544
|
if (sk) {
|
|
@@ -13335,6 +13581,9 @@ ${out}
|
|
|
13335
13581
|
const fakeVoice = process.env.AGENTX_VOICE_FAKE ? fakeVoiceParts(process.env.AGENTX_VOICE_FAKE) : null;
|
|
13336
13582
|
voiceIO = new VoiceIO({
|
|
13337
13583
|
...fakeVoice ?? {},
|
|
13584
|
+
emotions: emotionsOn,
|
|
13585
|
+
showEmotions,
|
|
13586
|
+
// local is authoritative (a /voice-emotions before mic-on still applies)
|
|
13338
13587
|
// No ack phrase by default: a fixed "Mm-hm," every turn reads robotic, Haiku's TTFT doesn't
|
|
13339
13588
|
// need masking (~0.7-1.2s full turns), and the conversational register already opens with a
|
|
13340
13589
|
// natural reaction. The mechanism (+ echo-leak guard) stays for slower voice models.
|
|
@@ -13352,8 +13601,11 @@ ${out}
|
|
|
13352
13601
|
// voiceEchoEnd closes the open echo line; '\r\x1b[0J' wipes the stale prompt/footer before the
|
|
13353
13602
|
// notice — every other async-chrome writer does this, and without it "✋ interrupted" overprints
|
|
13354
13603
|
// the footer's leading chars (the "interrupted% ctx" glue).
|
|
13604
|
+
// Barge-in also parks any in-flight task's spoken delivery so a superseded topic can't keep talking
|
|
13605
|
+
// over the user's new one (debt-after-jokes regression) — the result still lands in the transcript.
|
|
13355
13606
|
onBargeIn: (phase) => {
|
|
13356
13607
|
activeTurn?.abort();
|
|
13608
|
+
dx?.parkInFlightDeliveries();
|
|
13357
13609
|
voiceEchoEnd();
|
|
13358
13610
|
if (phase === "speaking") err("\r\x1B[0J" + yellow(" \u270B interrupted\n"));
|
|
13359
13611
|
},
|