@livx.cc/agentx 0.99.85 → 0.99.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +513 -73
- package/dist/cli.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -709,16 +709,16 @@ ${entries.map((s) => " " + s).join("\n")}`);
|
|
|
709
709
|
return blocks.length ? blocks.join("\n") : `(no ${label} found)`;
|
|
710
710
|
}
|
|
711
711
|
function fuzzyLineReplace(content, oldStr, newStr) {
|
|
712
|
-
const
|
|
712
|
+
const norm3 = (s) => s.trim();
|
|
713
713
|
const cl = content.split("\n");
|
|
714
|
-
const ol = oldStr.split("\n").map(
|
|
714
|
+
const ol = oldStr.split("\n").map(norm3);
|
|
715
715
|
while (ol.length && ol[ol.length - 1] === "") ol.pop();
|
|
716
716
|
while (ol.length && ol[0] === "") ol.shift();
|
|
717
717
|
if (!ol.length) return null;
|
|
718
718
|
const matches = [];
|
|
719
719
|
for (let i2 = 0; i2 + ol.length <= cl.length; i2++) {
|
|
720
720
|
let ok = true;
|
|
721
|
-
for (let j = 0; j < ol.length; j++) if (
|
|
721
|
+
for (let j = 0; j < ol.length; j++) if (norm3(cl[i2 + j]) !== ol[j]) {
|
|
722
722
|
ok = false;
|
|
723
723
|
break;
|
|
724
724
|
}
|
|
@@ -6046,14 +6046,14 @@ function summarize(messages, focus) {
|
|
|
6046
6046
|
if (lastAssistant) lines.push(`last assistant: ${lastAssistant}`);
|
|
6047
6047
|
if (carried.length) lines.push("earlier recap:", ...carried.map((l) => ` ${l}`));
|
|
6048
6048
|
if (focus?.trim()) {
|
|
6049
|
-
const
|
|
6049
|
+
const words3 = focus.toLowerCase().split(/\s+/).filter((w) => w.length > 2);
|
|
6050
6050
|
const kept = [];
|
|
6051
6051
|
const ordered = [...messages.filter((m) => !isSyntheticSummary(m)), ...messages.filter(isSyntheticSummary)];
|
|
6052
6052
|
outer: for (const msg of ordered) {
|
|
6053
6053
|
if (msg.role === "tool") continue;
|
|
6054
6054
|
for (const line of contentText(msg.content).split("\n")) {
|
|
6055
6055
|
const l = line.toLowerCase();
|
|
6056
|
-
if (line.trim() &&
|
|
6056
|
+
if (line.trim() && words3.some((w) => l.includes(w))) {
|
|
6057
6057
|
kept.push(line.trim().slice(0, 200));
|
|
6058
6058
|
if (kept.length >= 12) break outer;
|
|
6059
6059
|
}
|
|
@@ -7101,13 +7101,13 @@ function isTranscriptComplete(text) {
|
|
|
7101
7101
|
const trimmed = text.trim();
|
|
7102
7102
|
if (!trimmed) return false;
|
|
7103
7103
|
if (/[.?!]$/.test(trimmed)) return true;
|
|
7104
|
-
const
|
|
7105
|
-
const lastWord =
|
|
7104
|
+
const words3 = trimmed.toLowerCase().split(/\s+/);
|
|
7105
|
+
const lastWord = words3[words3.length - 1];
|
|
7106
7106
|
if (COMPLETE_ENDINGS.has(lastWord)) return true;
|
|
7107
7107
|
if (INCOMPLETE_ENDINGS.has(lastWord)) return false;
|
|
7108
7108
|
const lower = trimmed.toLowerCase();
|
|
7109
7109
|
for (const marker of CONTINUATION_MARKERS) if (lower.endsWith(marker)) return false;
|
|
7110
|
-
if (
|
|
7110
|
+
if (words3.length <= 2) return false;
|
|
7111
7111
|
return true;
|
|
7112
7112
|
}
|
|
7113
7113
|
var MAX_HOLD_MS = 2e3;
|
|
@@ -7508,13 +7508,13 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7508
7508
|
};
|
|
7509
7509
|
if (this.options.revealMode === "word" || this.options.heardTimestamps) {
|
|
7510
7510
|
this.tts.wantTimestamps = true;
|
|
7511
|
-
this.tts.onTimestamps = (
|
|
7511
|
+
this.tts.onTimestamps = (words3, start) => {
|
|
7512
7512
|
if (this.interrupted) return;
|
|
7513
7513
|
start.forEach((s, i) => {
|
|
7514
|
-
if (this.wordAnchorPending) this.wordPending.push([
|
|
7514
|
+
if (this.wordAnchorPending) this.wordPending.push([words3[i] ?? "", s]);
|
|
7515
7515
|
else {
|
|
7516
7516
|
this.wordStarts.push(this.wordOrigin + s);
|
|
7517
|
-
this.wordTexts.push(
|
|
7517
|
+
this.wordTexts.push(words3[i] ?? "");
|
|
7518
7518
|
}
|
|
7519
7519
|
});
|
|
7520
7520
|
};
|
|
@@ -7662,9 +7662,9 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7662
7662
|
this.ackProbe = null;
|
|
7663
7663
|
this.adaptiveAckFired = false;
|
|
7664
7664
|
const lead = m ? m[1] : bare;
|
|
7665
|
-
const
|
|
7665
|
+
const norm3 = lead.replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
|
|
7666
7666
|
const same = this.ackPickedFired ? bare.match(_VoiceEngine.leadingPhrase(this.lastAck)) : null;
|
|
7667
|
-
if (_VoiceEngine.ACK_LEADING.test(
|
|
7667
|
+
if (_VoiceEngine.ACK_LEADING.test(norm3)) {
|
|
7668
7668
|
const rest = m ? m[2] : "";
|
|
7669
7669
|
this.diag("reflex_ack_suppressed", { lead: lead.trim() });
|
|
7670
7670
|
if (!rest.trim()) return "";
|
|
@@ -7750,8 +7750,8 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7750
7750
|
while (n < this.wordStarts.length && this.wordStarts[n] <= playedSec) n++;
|
|
7751
7751
|
if (n !== this.revealedN) {
|
|
7752
7752
|
this.revealedN = n;
|
|
7753
|
-
const
|
|
7754
|
-
this.options.onReveal(
|
|
7753
|
+
const words3 = this.revealText.trim().split(/\s+/).filter(Boolean);
|
|
7754
|
+
this.options.onReveal(words3.slice(0, n).join(" "));
|
|
7755
7755
|
}
|
|
7756
7756
|
this.revealPoll = this.clock.setTimeout(tick, 60);
|
|
7757
7757
|
};
|
|
@@ -7778,8 +7778,8 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
7778
7778
|
this.adaptiveAckFired = false;
|
|
7779
7779
|
this.ackProbe = null;
|
|
7780
7780
|
if (heldProbe) {
|
|
7781
|
-
const
|
|
7782
|
-
if (_VoiceEngine.ACK_LEADING.test(
|
|
7781
|
+
const norm3 = heldProbe.replace(/\[emotion[^\]]*\]/gi, "").replace(/[^a-z ]/gi, " ").replace(/\s+/g, " ").trim().toLowerCase();
|
|
7782
|
+
if (_VoiceEngine.ACK_LEADING.test(norm3)) this.diag("reflex_ack_suppressed", { lead: heldProbe.trim() });
|
|
7783
7783
|
else this.speakDelta(heldProbe);
|
|
7784
7784
|
}
|
|
7785
7785
|
if (!this.speaking) return;
|
|
@@ -8317,14 +8317,14 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
8317
8317
|
if (!prev) return next;
|
|
8318
8318
|
if (!next) return prev;
|
|
8319
8319
|
const pw = prev.split(/\s+/), nw = next.split(/\s+/);
|
|
8320
|
-
const
|
|
8320
|
+
const norm3 = _VoiceEngine.normWord;
|
|
8321
8321
|
if (pw.length >= 2 && pw.length <= nw.length) {
|
|
8322
8322
|
let pfx = true;
|
|
8323
|
-
for (let i = 0; i < pw.length - 1; i++) if (
|
|
8323
|
+
for (let i = 0; i < pw.length - 1; i++) if (norm3(pw[i]) !== norm3(nw[i])) {
|
|
8324
8324
|
pfx = false;
|
|
8325
8325
|
break;
|
|
8326
8326
|
}
|
|
8327
|
-
if (pfx &&
|
|
8327
|
+
if (pfx && norm3(nw[pw.length - 1]).startsWith(norm3(pw[pw.length - 1]))) return next;
|
|
8328
8328
|
}
|
|
8329
8329
|
if (_VoiceEngine.restates(prev, next)) {
|
|
8330
8330
|
this.diag("merge_restated", { dropped: prev.slice(0, 60) });
|
|
@@ -8333,7 +8333,7 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
8333
8333
|
const max = Math.min(pw.length, nw.length);
|
|
8334
8334
|
for (let k = max; k > 0; k--) {
|
|
8335
8335
|
let match = true;
|
|
8336
|
-
for (let i = 0; i < k; i++) if (
|
|
8336
|
+
for (let i = 0; i < k; i++) if (norm3(pw[pw.length - k + i]) !== norm3(nw[i])) {
|
|
8337
8337
|
match = false;
|
|
8338
8338
|
break;
|
|
8339
8339
|
}
|
|
@@ -8385,11 +8385,11 @@ var VoiceEngine = class _VoiceEngine {
|
|
|
8385
8385
|
* race), never worse. Fillers ("mhm") deliberately don't count as replies — see speakFiller. */
|
|
8386
8386
|
refinalizes(next) {
|
|
8387
8387
|
const pw = this.lastDispatchWords, nw = next.trim().split(/\s+/);
|
|
8388
|
-
const
|
|
8388
|
+
const norm3 = _VoiceEngine.normWord;
|
|
8389
8389
|
if (pw.length < 3 || nw.length < pw.length) return false;
|
|
8390
8390
|
if (this.clock.now() - this.lastDispatchAt >= _VoiceEngine.DUP_FINAL_MS) return false;
|
|
8391
|
-
for (let i = 0; i < pw.length - 1; i++) if (
|
|
8392
|
-
const pLast =
|
|
8391
|
+
for (let i = 0; i < pw.length - 1; i++) if (norm3(pw[i]) !== norm3(nw[i])) return false;
|
|
8392
|
+
const pLast = norm3(pw[pw.length - 1]), nSame = norm3(nw[pw.length - 1]);
|
|
8393
8393
|
if (!nSame.startsWith(pLast)) return false;
|
|
8394
8394
|
return nw.length > pw.length || nSame.length > pLast.length;
|
|
8395
8395
|
}
|
|
@@ -9285,6 +9285,17 @@ var ACK_BANK = {
|
|
|
9285
9285
|
steer: ["Ah, okay, switching\u2014", "Ah, got it, so\u2014", "Oh, gotcha, sure\u2014"],
|
|
9286
9286
|
resume: ["Right, so\u2014", "Anyway, so\u2014", "So, as I was saying\u2014"]
|
|
9287
9287
|
};
|
|
9288
|
+
var CHANNEL_ACTIONS = {
|
|
9289
|
+
silence: 'the default \u2014 nothing to add. ALWAYS for a greeting, thanks, a yes/no, or any short complete turn the real reply will answer instantly ("hey", "how are you?", "thanks"); also mid-word or mid-clause',
|
|
9290
|
+
backchannel: 'they are mid-way through a longer story or explanation (several clauses) and just closed a clause: a tiny "mm-hm"/"right" shows you follow, without taking the turn',
|
|
9291
|
+
filler: 'they paused mid-thought, searching for words, and a soft "hmm" keeps the line warm',
|
|
9292
|
+
bridge: 'they finished a substantial request or hard question and the real answer takes a moment: a short "let me check\u2014" lead-in'
|
|
9293
|
+
};
|
|
9294
|
+
var CHANNEL_BANK = {
|
|
9295
|
+
backchannel: ["Mm-hm.", "Right.", "Mm, yeah.", "Uh-huh.", "I see.", "Mm."],
|
|
9296
|
+
filler: ["Hmm\u2026", "Mm\u2026", "Hm, mm\u2026", "Ahh\u2026"],
|
|
9297
|
+
bridge: ["Let me check\u2014", "One sec, looking\u2014", "Hm, let me see\u2014", "Okay, give me a moment\u2014", "Let me think on that\u2014"]
|
|
9298
|
+
};
|
|
9288
9299
|
var INTENT_ACK_OVER = {
|
|
9289
9300
|
stop: Object.keys(ACK_SITUATIONS).filter((s) => s !== "silent"),
|
|
9290
9301
|
steer: ["command"],
|
|
@@ -9502,6 +9513,17 @@ var Instinct = class {
|
|
|
9502
9513
|
if (pick?.text !== text.trim()) return null;
|
|
9503
9514
|
return Promise.race([pick.p.then((p) => p ? pick.spoken : null), pick.spoken]);
|
|
9504
9515
|
}
|
|
9516
|
+
/** Duplex v2 second channel: which (if any) listener line fits THIS instant — one Choice over CHANNEL_ACTIONS.
|
|
9517
|
+
* `heard` is the micro-context tail only (never history). Never rejects: a failed call → null (the rule decides). */
|
|
9518
|
+
channel(heard, state) {
|
|
9519
|
+
return this.ask(
|
|
9520
|
+
"channel",
|
|
9521
|
+
heard,
|
|
9522
|
+
{ heard_so_far: heard, ...state },
|
|
9523
|
+
FRAME + "You are the quick side-voice of the conversation, NOT the one who answers. Pick the tiny sound a person would make right now, or none. Silence is the default: most moments call for nothing.",
|
|
9524
|
+
CHANNEL_ACTIONS
|
|
9525
|
+
);
|
|
9526
|
+
}
|
|
9505
9527
|
/** A random phrase from `pool`, never the one said last (no back-to-back repeat across keys). */
|
|
9506
9528
|
rotate(pool) {
|
|
9507
9529
|
const fresh = pool.filter((x) => x !== this.lastAckSaid);
|
|
@@ -13022,8 +13044,8 @@ var CutInAbsorber = class {
|
|
|
13022
13044
|
const cut = this.cut || (meta?.supersedes !== void 0 && meta.supersedes === this.absorbed?.text ? this.absorbed.cut : "");
|
|
13023
13045
|
this.cut = "";
|
|
13024
13046
|
this.absorbed = void 0;
|
|
13025
|
-
const
|
|
13026
|
-
if (!cut ||
|
|
13047
|
+
const words3 = (s) => s.trim().split(/\s+/).filter(Boolean).length;
|
|
13048
|
+
if (!cut || words3(text) - words3(cut) >= 3) return false;
|
|
13027
13049
|
this.absorbed = { text, cut };
|
|
13028
13050
|
return true;
|
|
13029
13051
|
}
|
|
@@ -13082,6 +13104,77 @@ function bindAckInstinct(v, instinct) {
|
|
|
13082
13104
|
onDiag(ev);
|
|
13083
13105
|
};
|
|
13084
13106
|
}
|
|
13107
|
+
function bindSecondChannel(v, sc, live) {
|
|
13108
|
+
const o = v.options;
|
|
13109
|
+
const { onPartial, onUtterance, onState, onBargeIn, onBackchannel } = o;
|
|
13110
|
+
const { show } = sc.options;
|
|
13111
|
+
let own = false;
|
|
13112
|
+
let blip = "";
|
|
13113
|
+
o.onBackchannel = (p) => {
|
|
13114
|
+
blip = p;
|
|
13115
|
+
onBackchannel(p);
|
|
13116
|
+
};
|
|
13117
|
+
const take = v.takeInterruptedReply.bind(v);
|
|
13118
|
+
v.takeInterruptedReply = () => {
|
|
13119
|
+
const r = take();
|
|
13120
|
+
if (!r || !sc.spoke(r.full)) return r;
|
|
13121
|
+
return r.queued.length ? { ...r, full: "", heard: "" } : null;
|
|
13122
|
+
};
|
|
13123
|
+
o.onPartial = (t) => {
|
|
13124
|
+
if (live() && !v.audible) sc.onInterim(t);
|
|
13125
|
+
onPartial(t);
|
|
13126
|
+
};
|
|
13127
|
+
o.onUtterance = ((t, meta) => {
|
|
13128
|
+
if (live() && t.trim()) sc.onUserTurn(t);
|
|
13129
|
+
onUtterance(t, meta);
|
|
13130
|
+
});
|
|
13131
|
+
o.onState = (s) => {
|
|
13132
|
+
if (live() && !own) {
|
|
13133
|
+
if (s === "speaking") sc.onAgentState("first_audio");
|
|
13134
|
+
else if (s === "listening") sc.onAgentState("idle");
|
|
13135
|
+
}
|
|
13136
|
+
onState(s);
|
|
13137
|
+
};
|
|
13138
|
+
o.onBargeIn = (phase) => {
|
|
13139
|
+
sc.onBargeIn();
|
|
13140
|
+
onBargeIn(phase);
|
|
13141
|
+
};
|
|
13142
|
+
Object.assign(sc.options, {
|
|
13143
|
+
aec: v.usingAec,
|
|
13144
|
+
speak: (text, kind) => {
|
|
13145
|
+
if (!live() || v.audible) return false;
|
|
13146
|
+
if (kind === "backchannel") {
|
|
13147
|
+
blip = "";
|
|
13148
|
+
v.fireBackchannel();
|
|
13149
|
+
return;
|
|
13150
|
+
}
|
|
13151
|
+
own = true;
|
|
13152
|
+
v.speakFiller(text);
|
|
13153
|
+
return v.awaitIdle().finally(() => {
|
|
13154
|
+
own = false;
|
|
13155
|
+
});
|
|
13156
|
+
},
|
|
13157
|
+
show: (text, kind) => show(kind === "backchannel" && blip ? blip : text, kind)
|
|
13158
|
+
});
|
|
13159
|
+
}
|
|
13160
|
+
function channelInput(sc, m) {
|
|
13161
|
+
if (m.type === "interim") {
|
|
13162
|
+
if (typeof m.aec === "boolean") sc.options.aec = m.aec;
|
|
13163
|
+
if (typeof m.text === "string") sc.onInterim(m.text);
|
|
13164
|
+
return true;
|
|
13165
|
+
}
|
|
13166
|
+
if (m.type === "pause") {
|
|
13167
|
+
sc.onPause(Number(m.ms) || 0);
|
|
13168
|
+
return true;
|
|
13169
|
+
}
|
|
13170
|
+
if (m.type === "agent_state") {
|
|
13171
|
+
if (typeof m.state === "string" && MAIN_STATES.has(m.state)) sc.onAgentState(m.state);
|
|
13172
|
+
else log27.warn(`agent_state: unknown state ${JSON.stringify(m.state)} \u2014 ignored`);
|
|
13173
|
+
return true;
|
|
13174
|
+
}
|
|
13175
|
+
return false;
|
|
13176
|
+
}
|
|
13177
|
+
var MAIN_STATES = /* @__PURE__ */ new Set(["idle", "queued", "thinking", "streaming_text", "first_audio", "speaking"]);
|
|
13085
13178
|
var VoiceIOOptions = class extends VoiceEngineOptions {
|
|
13086
13179
|
sonioxApiKey = process.env.SONIOX_API_KEY ?? "";
|
|
13087
13180
|
cartesiaApiKey = process.env.CARTESIA_API_KEY ?? "";
|
|
@@ -13251,6 +13344,316 @@ var JsonlDiagSink = class {
|
|
|
13251
13344
|
}
|
|
13252
13345
|
};
|
|
13253
13346
|
|
|
13347
|
+
// src/voice/secondChannel.ts
|
|
13348
|
+
init_logging();
|
|
13349
|
+
var log29 = forComponent("SecondChannel");
|
|
13350
|
+
var SecondChannelOptions = class {
|
|
13351
|
+
/** Refiner (Jev/fast LLM): the choice over CHANNEL_ACTIONS, or null on failure. Absent → the rule alone decides. */
|
|
13352
|
+
decide;
|
|
13353
|
+
/** Fast-model line writer (see fastLineGenerator). Absent or late → the canned bank. */
|
|
13354
|
+
generate;
|
|
13355
|
+
/** Kinds worth generating (content-conditioned); the rest are canned sounds. */
|
|
13356
|
+
generateFor = ["backchannel", "bridge"];
|
|
13357
|
+
/** Host TTS on the second channel; resolves when the line finished playing. `false` = the host could not play it. */
|
|
13358
|
+
speak = () => {
|
|
13359
|
+
};
|
|
13360
|
+
/** Host UI (caption / muted line) — never history. */
|
|
13361
|
+
show = () => {
|
|
13362
|
+
};
|
|
13363
|
+
/** Stop the in-flight line's audio now (barge-in only). */
|
|
13364
|
+
cancel = () => {
|
|
13365
|
+
};
|
|
13366
|
+
onEvent = () => {
|
|
13367
|
+
};
|
|
13368
|
+
/** Echo cancellation on: backchannels may play while the user speaks. Off (half-duplex) → fillers/bridges in silences only. */
|
|
13369
|
+
aec = false;
|
|
13370
|
+
decideEveryWords = 3;
|
|
13371
|
+
decisionBudgetMs = 300;
|
|
13372
|
+
generateBudgetMs = 500;
|
|
13373
|
+
minGapMs = 2500;
|
|
13374
|
+
/** Backchannel after backchannel needs this gap (v1 Instinct's cadence, ≤12/min). */
|
|
13375
|
+
backchannelGapMs = 5e3;
|
|
13376
|
+
/** A decision whose partial grew by this many words while in flight is stale → dropped. */
|
|
13377
|
+
staleWords = 3;
|
|
13378
|
+
microWords = 30;
|
|
13379
|
+
avoidLines = 4;
|
|
13380
|
+
/** Canned picks exclude the last N picks of the same kind. */
|
|
13381
|
+
bankExclude = 2;
|
|
13382
|
+
backchannelMinWords = 8;
|
|
13383
|
+
/** No new interim for this long → a pause (hosts may also call onPause). */
|
|
13384
|
+
fillerPauseMs = 700;
|
|
13385
|
+
fillerMinWords = 4;
|
|
13386
|
+
bridgeMinWords = 5;
|
|
13387
|
+
now = () => performance.now();
|
|
13388
|
+
random = Math.random;
|
|
13389
|
+
};
|
|
13390
|
+
var words2 = (s) => s.trim() ? s.trim().split(/\s+/) : [];
|
|
13391
|
+
var QUICK = /^(hey|hi|hello|yo|hiya|morning|good (morning|evening|afternoon)|thanks|thank you|ok(ay)?|yes|yeah|yep|no|nope|sure|bye|goodbye|how are you|what'?s up|can you hear me)\b/i;
|
|
13392
|
+
function withBudget(p, ms, what) {
|
|
13393
|
+
let t;
|
|
13394
|
+
const late = new Promise((r) => {
|
|
13395
|
+
t = setTimeout(() => {
|
|
13396
|
+
log29.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
|
|
13397
|
+
r(null);
|
|
13398
|
+
}, ms);
|
|
13399
|
+
});
|
|
13400
|
+
return Promise.race([p.catch((e) => {
|
|
13401
|
+
log29.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
|
|
13402
|
+
return null;
|
|
13403
|
+
}), late]).finally(() => clearTimeout(t));
|
|
13404
|
+
}
|
|
13405
|
+
var SecondChannel = class {
|
|
13406
|
+
constructor(options) {
|
|
13407
|
+
this.options = options;
|
|
13408
|
+
this.options = { ...new SecondChannelOptions(), ...options };
|
|
13409
|
+
this.startedAt = this.o.now();
|
|
13410
|
+
}
|
|
13411
|
+
options;
|
|
13412
|
+
heard = "";
|
|
13413
|
+
userSpeaking = false;
|
|
13414
|
+
main = "idle";
|
|
13415
|
+
lead = "";
|
|
13416
|
+
gen = 0;
|
|
13417
|
+
// bumped by barge-in / a new user turn → in-flight decisions drop
|
|
13418
|
+
decidedAt = 0;
|
|
13419
|
+
// word count at the last decision
|
|
13420
|
+
busy = false;
|
|
13421
|
+
pauseTimer = null;
|
|
13422
|
+
lastAt = -Infinity;
|
|
13423
|
+
lastKind = null;
|
|
13424
|
+
live = null;
|
|
13425
|
+
// our last line, until channel-1 audio takes the floor
|
|
13426
|
+
cutLine = null;
|
|
13427
|
+
// the line the last barge actually cut (spoke())
|
|
13428
|
+
bridged = false;
|
|
13429
|
+
// one bridge per main turn
|
|
13430
|
+
recent = [];
|
|
13431
|
+
picks = /* @__PURE__ */ new Map();
|
|
13432
|
+
inflight = null;
|
|
13433
|
+
startedAt;
|
|
13434
|
+
get o() {
|
|
13435
|
+
return this.options;
|
|
13436
|
+
}
|
|
13437
|
+
/** The user's live partial for the current turn (the whole partial, not a delta). */
|
|
13438
|
+
onInterim(text) {
|
|
13439
|
+
const t = text.trim();
|
|
13440
|
+
if (!t || t === this.heard) return;
|
|
13441
|
+
if (this.main !== "idle") this.onAgentState("idle");
|
|
13442
|
+
this.heard = t;
|
|
13443
|
+
this.userSpeaking = true;
|
|
13444
|
+
if (this.pauseTimer) clearTimeout(this.pauseTimer);
|
|
13445
|
+
this.pauseTimer = setTimeout(() => {
|
|
13446
|
+
this.pauseTimer = null;
|
|
13447
|
+
this.onPause(this.o.fillerPauseMs);
|
|
13448
|
+
}, this.o.fillerPauseMs);
|
|
13449
|
+
if (words2(t).length - this.decidedAt >= this.o.decideEveryWords) void this.consider("interim");
|
|
13450
|
+
}
|
|
13451
|
+
/** The user went quiet for `ms` (host VAD/STT pause, or the internal interim timer). */
|
|
13452
|
+
onPause(ms) {
|
|
13453
|
+
if (this.pauseTimer) {
|
|
13454
|
+
clearTimeout(this.pauseTimer);
|
|
13455
|
+
this.pauseTimer = null;
|
|
13456
|
+
}
|
|
13457
|
+
this.userSpeaking = false;
|
|
13458
|
+
void this.consider("pause", ms);
|
|
13459
|
+
}
|
|
13460
|
+
/** Channel-1 lifecycle. `queued` = the user's turn was dispatched to the main agent. */
|
|
13461
|
+
onAgentState(s) {
|
|
13462
|
+
if (s === this.main) return;
|
|
13463
|
+
this.main = s;
|
|
13464
|
+
if (s === "first_audio" || s === "speaking") this.live = null;
|
|
13465
|
+
if (s === "idle") {
|
|
13466
|
+
this.heard = "";
|
|
13467
|
+
this.decidedAt = 0;
|
|
13468
|
+
this.lead = "";
|
|
13469
|
+
this.bridged = false;
|
|
13470
|
+
return;
|
|
13471
|
+
}
|
|
13472
|
+
if (s === "queued") {
|
|
13473
|
+
this.gen++;
|
|
13474
|
+
this.userSpeaking = false;
|
|
13475
|
+
this.lead = "";
|
|
13476
|
+
this.bridged = false;
|
|
13477
|
+
if (this.pauseTimer) clearTimeout(this.pauseTimer);
|
|
13478
|
+
this.pauseTimer = null;
|
|
13479
|
+
}
|
|
13480
|
+
if (s === "queued" || s === "thinking" || s === "streaming_text") void this.consider("working");
|
|
13481
|
+
}
|
|
13482
|
+
/** The user's final line was dispatched to the main agent (its words are the bridge's micro-context). */
|
|
13483
|
+
onUserTurn(text) {
|
|
13484
|
+
this.heard = text.trim();
|
|
13485
|
+
this.main = "idle";
|
|
13486
|
+
this.onAgentState("queued");
|
|
13487
|
+
}
|
|
13488
|
+
/** The main answer's streamed text (before its audio): a bridge can lead into it. */
|
|
13489
|
+
onMainTokens(text) {
|
|
13490
|
+
if (this.lead.length < 120) this.lead += text;
|
|
13491
|
+
if (this.main === "queued" || this.main === "thinking") this.onAgentState("streaming_text");
|
|
13492
|
+
}
|
|
13493
|
+
/** The user barged in: cut the in-flight line now and drop every pending decision. */
|
|
13494
|
+
onBargeIn() {
|
|
13495
|
+
this.gen++;
|
|
13496
|
+
this.cutLine = this.inflight || this.live ? this.live : null;
|
|
13497
|
+
if (this.inflight) {
|
|
13498
|
+
this.inflight = null;
|
|
13499
|
+
try {
|
|
13500
|
+
this.o.cancel();
|
|
13501
|
+
} catch (e) {
|
|
13502
|
+
log29.warn(`cancel threw: ${e.message}`);
|
|
13503
|
+
}
|
|
13504
|
+
}
|
|
13505
|
+
}
|
|
13506
|
+
/** Is `text` the line the last barge-in cut (ours, still holding the floor)? Hosts strip it from the cut reply — it is
|
|
13507
|
+
* not the agent's speech. By identity, not by text: a real reply that merely repeats a recent line is kept. Consumed once. */
|
|
13508
|
+
spoke(text) {
|
|
13509
|
+
const c = this.cutLine;
|
|
13510
|
+
this.cutLine = null;
|
|
13511
|
+
return !!c && c === text.trim();
|
|
13512
|
+
}
|
|
13513
|
+
/** Resolves when the in-flight line finished playing (immediately when none) — hosts enqueue channel-1 audio behind it. */
|
|
13514
|
+
drain() {
|
|
13515
|
+
return this.inflight ?? Promise.resolve();
|
|
13516
|
+
}
|
|
13517
|
+
get audioLocked() {
|
|
13518
|
+
return this.main === "first_audio" || this.main === "speaking";
|
|
13519
|
+
}
|
|
13520
|
+
get working() {
|
|
13521
|
+
return this.main === "queued" || this.main === "thinking" || this.main === "streaming_text";
|
|
13522
|
+
}
|
|
13523
|
+
tail() {
|
|
13524
|
+
return words2(this.heard).slice(-this.o.microWords).join(" ");
|
|
13525
|
+
}
|
|
13526
|
+
/** Hard gates every line passes, whoever proposed it. */
|
|
13527
|
+
allowed(k) {
|
|
13528
|
+
if (this.audioLocked || this.inflight || this.o.now() - this.lastAt < this.o.minGapMs || k === this.lastKind && (k !== "backchannel" || this.o.now() - this.lastAt < this.o.backchannelGapMs)) return false;
|
|
13529
|
+
if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle";
|
|
13530
|
+
if (k === "filler") return !this.userSpeaking && this.main === "idle";
|
|
13531
|
+
return this.working && !this.bridged;
|
|
13532
|
+
}
|
|
13533
|
+
/** 0ms rule: the candidate line for this trigger, or silence. */
|
|
13534
|
+
rule(trigger, pauseMs = 0) {
|
|
13535
|
+
const w = words2(this.heard);
|
|
13536
|
+
if (w.length <= 3 || QUICK.test(this.heard) && w.length <= 6) return "silence";
|
|
13537
|
+
const k = trigger === "interim" ? w.length >= this.o.backchannelMinWords && /[,.;:!?…]$/.test(this.heard) ? "backchannel" : null : trigger === "pause" ? pauseMs >= this.o.fillerPauseMs && w.length >= this.o.fillerMinWords ? "filler" : null : w.length >= this.o.bridgeMinWords ? "bridge" : null;
|
|
13538
|
+
return k && this.allowed(k) ? k : "silence";
|
|
13539
|
+
}
|
|
13540
|
+
async consider(trigger, pauseMs) {
|
|
13541
|
+
if (this.busy) return;
|
|
13542
|
+
const n0 = words2(this.heard).length;
|
|
13543
|
+
this.decidedAt = n0;
|
|
13544
|
+
const rule = this.rule(trigger, pauseMs);
|
|
13545
|
+
if (rule === "silence") return;
|
|
13546
|
+
this.busy = true;
|
|
13547
|
+
const gen = this.gen, t0 = this.o.now();
|
|
13548
|
+
const ev = (e) => this.o.onEvent({ trigger, rule, ms: Math.round(this.o.now() - t0), ...e });
|
|
13549
|
+
const stale = () => gen !== this.gen || words2(this.heard).length - n0 >= this.o.staleWords;
|
|
13550
|
+
try {
|
|
13551
|
+
let kind = rule;
|
|
13552
|
+
let choice;
|
|
13553
|
+
if (this.o.decide) {
|
|
13554
|
+
const d = await withBudget(this.o.decide(this.tail(), this.state(pauseMs)), this.o.decisionBudgetMs, "channel decision");
|
|
13555
|
+
choice = d?.choice;
|
|
13556
|
+
if (choice && choice in CHANNEL_ACTIONS) kind = choice;
|
|
13557
|
+
}
|
|
13558
|
+
if (kind === "silence") return ev({ choice, acted: "silence" });
|
|
13559
|
+
if (stale()) return ev({ choice, kind, acted: "stale" });
|
|
13560
|
+
if (!this.allowed(kind)) return ev({ choice, kind, acted: "gated" });
|
|
13561
|
+
const text = await this.line(kind);
|
|
13562
|
+
if (stale()) return ev({ choice, kind, text, acted: "stale" });
|
|
13563
|
+
if (!this.allowed(kind)) return ev({ choice, kind, text, acted: "gated" });
|
|
13564
|
+
ev({ choice, kind, text, acted: this.say(kind, text) ? "spoken" : "skipped" });
|
|
13565
|
+
} finally {
|
|
13566
|
+
this.busy = false;
|
|
13567
|
+
}
|
|
13568
|
+
}
|
|
13569
|
+
state(pauseMs) {
|
|
13570
|
+
const now4 = this.o.now();
|
|
13571
|
+
return {
|
|
13572
|
+
user_speaking: this.userSpeaking,
|
|
13573
|
+
pause_ms: pauseMs ?? null,
|
|
13574
|
+
agent_state: this.main,
|
|
13575
|
+
last_line: this.recent.at(-1) ?? null,
|
|
13576
|
+
ms_since_last_line: Number.isFinite(this.lastAt) ? Math.round(now4 - this.lastAt) : null,
|
|
13577
|
+
call_ms: Math.round(now4 - this.startedAt)
|
|
13578
|
+
};
|
|
13579
|
+
}
|
|
13580
|
+
async line(kind) {
|
|
13581
|
+
if (this.o.generate && this.o.generateFor.includes(kind)) {
|
|
13582
|
+
const ac = new AbortController();
|
|
13583
|
+
const ctx = { kind, heard: this.tail(), state: this.main, avoid: [...this.recent], ...this.lead ? { lead: this.lead } : {} };
|
|
13584
|
+
const raw = await withBudget(this.o.generate(ctx, ac.signal), this.o.generateBudgetMs, `${kind} generate`);
|
|
13585
|
+
if (raw === null) ac.abort();
|
|
13586
|
+
const text = cleanLine(raw ?? "");
|
|
13587
|
+
if (text && !this.recent.some((r) => norm2(r) === norm2(text))) return text;
|
|
13588
|
+
if (text) log29.debug(`generated "${text}" repeats a recent line \u2014 canned`);
|
|
13589
|
+
}
|
|
13590
|
+
return this.pick(kind);
|
|
13591
|
+
}
|
|
13592
|
+
/** Canned rotation: never one of the last `bankExclude` picks of this kind, nor a recent line. */
|
|
13593
|
+
pick(kind) {
|
|
13594
|
+
const pool = CHANNEL_BANK[kind];
|
|
13595
|
+
const hist = this.picks.get(kind) ?? [];
|
|
13596
|
+
const recent = new Set([...hist.slice(-this.o.bankExclude), ...this.recent].map(norm2));
|
|
13597
|
+
const fresh = pool.filter((x) => !recent.has(norm2(x)));
|
|
13598
|
+
const from = fresh.length ? fresh : pool;
|
|
13599
|
+
const p = from[Math.floor(this.o.random() * from.length)];
|
|
13600
|
+
this.picks.set(kind, [...hist, p].slice(-this.o.bankExclude));
|
|
13601
|
+
return p;
|
|
13602
|
+
}
|
|
13603
|
+
say(kind, text) {
|
|
13604
|
+
let r;
|
|
13605
|
+
try {
|
|
13606
|
+
r = this.o.speak(text, kind);
|
|
13607
|
+
} catch (e) {
|
|
13608
|
+
log29.warn(`speak threw \u2014 line dropped: ${e.message}`);
|
|
13609
|
+
return false;
|
|
13610
|
+
}
|
|
13611
|
+
if (r === false) return false;
|
|
13612
|
+
this.lastAt = this.o.now();
|
|
13613
|
+
this.lastKind = kind;
|
|
13614
|
+
if (kind === "bridge") this.bridged = true;
|
|
13615
|
+
this.recent = [...this.recent, text].slice(-this.o.avoidLines);
|
|
13616
|
+
this.live = text.trim();
|
|
13617
|
+
this.o.show(text, kind);
|
|
13618
|
+
const p = Promise.resolve(r).then(() => {
|
|
13619
|
+
}, (e) => {
|
|
13620
|
+
log29.warn(`channel line playback failed: ${e?.message ?? e}`);
|
|
13621
|
+
}).finally(() => {
|
|
13622
|
+
if (this.inflight === p) this.inflight = null;
|
|
13623
|
+
});
|
|
13624
|
+
this.inflight = p;
|
|
13625
|
+
return true;
|
|
13626
|
+
}
|
|
13627
|
+
};
|
|
13628
|
+
var norm2 = (s) => s.toLowerCase().replace(/[^a-z' ]/g, "").trim();
|
|
13629
|
+
function cleanLine(s) {
|
|
13630
|
+
const raw = s.trim().split("\n")[0];
|
|
13631
|
+
const l = raw.replace(/^["'“”]+|["'“”]+$/g, "").replace(/^\w+:\s*/, "").trim();
|
|
13632
|
+
if (isLeakSuspect(raw) || isLeakSuspect(l)) {
|
|
13633
|
+
if (l) log29.debug(`generated "${raw}" reads as meta \u2014 canned`);
|
|
13634
|
+
return "";
|
|
13635
|
+
}
|
|
13636
|
+
return words2(l).length > 6 ? "" : l;
|
|
13637
|
+
}
|
|
13638
|
+
var KIND_HINT = {
|
|
13639
|
+
backchannel: "a tiny listener cue while they keep talking \u2014 shows you follow THEIR point, does not take the turn",
|
|
13640
|
+
filler: "a soft thinking sound at their pause",
|
|
13641
|
+
bridge: "a quick lead-in before the real answer, while it is being prepared"
|
|
13642
|
+
};
|
|
13643
|
+
function fastLineGenerator(ai, model) {
|
|
13644
|
+
return async (c, signal) => {
|
|
13645
|
+
const prompt = `You voice the tiny side-sounds of a live spoken conversation. Write ONE line of 1-5 words: ${KIND_HINT[c.kind]}.
|
|
13646
|
+
The user just said: "${c.heard}"
|
|
13647
|
+
The assistant is: ${c.state}.
|
|
13648
|
+
` + (c.lead ? `The real answer begins: "${c.lead.trim()}" \u2014 lead naturally into it; do not repeat it.
|
|
13649
|
+
` : "") + (c.avoid.length ? `Do not reuse any of: ${c.avoid.map((a) => `"${a}"`).join(", ")}.
|
|
13650
|
+
` : "") + "React to their actual words, casually. Output only the line.";
|
|
13651
|
+
const reasoning = /gpt-oss/.test(model) ? { providerOptions: { reasoning_effort: "low" } } : {};
|
|
13652
|
+
const r = await ai.chat({ model, messages: [{ role: "user", content: prompt }], stream: false, signal, maxTokens: 150, ...reasoning });
|
|
13653
|
+
return r.content ?? "";
|
|
13654
|
+
};
|
|
13655
|
+
}
|
|
13656
|
+
|
|
13254
13657
|
// cli/config.ts
|
|
13255
13658
|
import { homedir as homedir5 } from "os";
|
|
13256
13659
|
import { existsSync as existsSync5, readFileSync as readFileSync7 } from "fs";
|
|
@@ -13397,7 +13800,7 @@ function applyOverlay(cfg, file) {
|
|
|
13397
13800
|
// cli/hooks-config.ts
|
|
13398
13801
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
13399
13802
|
init_platform();
|
|
13400
|
-
var
|
|
13803
|
+
var log30 = forComponent("hooks");
|
|
13401
13804
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
13402
13805
|
function ruleMatches(rule, toolName) {
|
|
13403
13806
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -13416,7 +13819,7 @@ function runCmd(rule, env) {
|
|
|
13416
13819
|
});
|
|
13417
13820
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
13418
13821
|
} catch (e) {
|
|
13419
|
-
|
|
13822
|
+
log30.debug(`hook command failed: ${rule.command}`, e);
|
|
13420
13823
|
return { code: 1, out: String(e?.message ?? e) };
|
|
13421
13824
|
}
|
|
13422
13825
|
}
|
|
@@ -13524,7 +13927,7 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as rea
|
|
|
13524
13927
|
import { homedir as homedir6 } from "os";
|
|
13525
13928
|
import { join as join10 } from "path";
|
|
13526
13929
|
init_platform();
|
|
13527
|
-
var
|
|
13930
|
+
var log31 = forComponent("session");
|
|
13528
13931
|
var globalDir = () => join10(homedir6(), ".agent", "sessions");
|
|
13529
13932
|
function indexLink(target, link2) {
|
|
13530
13933
|
try {
|
|
@@ -13587,7 +13990,7 @@ var SessionStore = class {
|
|
|
13587
13990
|
}
|
|
13588
13991
|
load(id) {
|
|
13589
13992
|
if (!this.safeId(id)) {
|
|
13590
|
-
|
|
13993
|
+
log31.debug(`rejecting unsafe session id: ${id}`);
|
|
13591
13994
|
return void 0;
|
|
13592
13995
|
}
|
|
13593
13996
|
const path = join10(this.dir, `${id}.json`);
|
|
@@ -13595,7 +13998,7 @@ var SessionStore = class {
|
|
|
13595
13998
|
try {
|
|
13596
13999
|
return JSON.parse(readFileSync8(path, "utf8"));
|
|
13597
14000
|
} catch (e) {
|
|
13598
|
-
|
|
14001
|
+
log31.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
13599
14002
|
return void 0;
|
|
13600
14003
|
}
|
|
13601
14004
|
}
|
|
@@ -13608,7 +14011,7 @@ var SessionStore = class {
|
|
|
13608
14011
|
try {
|
|
13609
14012
|
metas.push(JSON.parse(readFileSync8(join10(this.dir, f), "utf8")).meta);
|
|
13610
14013
|
} catch (e) {
|
|
13611
|
-
|
|
14014
|
+
log31.debug(`skipping unreadable session file ${f}`, e);
|
|
13612
14015
|
}
|
|
13613
14016
|
}
|
|
13614
14017
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -13847,7 +14250,7 @@ import { promisify } from "util";
|
|
|
13847
14250
|
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync9 } from "fs";
|
|
13848
14251
|
import { homedir as homedir7 } from "os";
|
|
13849
14252
|
import { join as join11, resolve as resolve4, sep as sep3, parse as parsePath } from "path";
|
|
13850
|
-
var
|
|
14253
|
+
var log32 = forComponent("checkpoints");
|
|
13851
14254
|
var exec = promisify(execFile4);
|
|
13852
14255
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
13853
14256
|
function isBroadRoot(p) {
|
|
@@ -13893,7 +14296,7 @@ var ShadowRepo = class {
|
|
|
13893
14296
|
writeFileSync6(join11(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
13894
14297
|
this.ready = true;
|
|
13895
14298
|
} catch (e) {
|
|
13896
|
-
|
|
14299
|
+
log32.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
13897
14300
|
this.ready = false;
|
|
13898
14301
|
}
|
|
13899
14302
|
return this.ready;
|
|
@@ -13904,7 +14307,7 @@ var ShadowRepo = class {
|
|
|
13904
14307
|
}
|
|
13905
14308
|
async commit(label, forced = []) {
|
|
13906
14309
|
await this.run("add", "-A");
|
|
13907
|
-
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) =>
|
|
14310
|
+
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log32.debug(`force-add failed: ${p}`, e));
|
|
13908
14311
|
await this.run("commit", "--allow-empty", "-q", "-m", label);
|
|
13909
14312
|
}
|
|
13910
14313
|
/** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
|
|
@@ -13912,8 +14315,8 @@ var ShadowRepo = class {
|
|
|
13912
14315
|
* turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
|
|
13913
14316
|
* point per turn, so the REPL's turn↔frame mapping stays intact. */
|
|
13914
14317
|
async amendForced(paths) {
|
|
13915
|
-
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) =>
|
|
13916
|
-
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) =>
|
|
14318
|
+
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log32.debug(`force-capture failed: ${p}`, e));
|
|
14319
|
+
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log32.debug("amend failed", e));
|
|
13917
14320
|
}
|
|
13918
14321
|
/** Commits on `ref`, oldest-first (canonical index space). */
|
|
13919
14322
|
async log(ref) {
|
|
@@ -13973,7 +14376,7 @@ var ShadowRepo = class {
|
|
|
13973
14376
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
13974
14377
|
});
|
|
13975
14378
|
} catch (e) {
|
|
13976
|
-
|
|
14379
|
+
log32.debug("checkpoint prune failed", e);
|
|
13977
14380
|
}
|
|
13978
14381
|
}
|
|
13979
14382
|
};
|
|
@@ -14041,7 +14444,7 @@ var GitCheckpoints = class {
|
|
|
14041
14444
|
use(sessionId) {
|
|
14042
14445
|
if (sessionId === this.session) return;
|
|
14043
14446
|
this.session = sessionId;
|
|
14044
|
-
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) =>
|
|
14447
|
+
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log32.debug("re-point failed", e));
|
|
14045
14448
|
}
|
|
14046
14449
|
async begin(label) {
|
|
14047
14450
|
if (!await this.start()) return;
|
|
@@ -14061,7 +14464,7 @@ var GitCheckpoints = class {
|
|
|
14061
14464
|
process.stderr.write(`\x1B[2m checkpoints: '${this.repos[i].root}' too large to snapshot in ${Math.round(this.options.opTimeoutMs / 1e3)}s \u2014 file rewind disabled here\x1B[0m
|
|
14062
14465
|
`);
|
|
14063
14466
|
} else {
|
|
14064
|
-
|
|
14467
|
+
log32.debug("checkpoint commit failed", e);
|
|
14065
14468
|
survivors.push(this.repos[i]);
|
|
14066
14469
|
survivorCaches.push(this.caches[i] ?? []);
|
|
14067
14470
|
}
|
|
@@ -14096,7 +14499,7 @@ var GitCheckpoints = class {
|
|
|
14096
14499
|
if (this.forced.has(abs)) continue;
|
|
14097
14500
|
this.forced.add(abs);
|
|
14098
14501
|
const i = this.repoIndexFor(abs);
|
|
14099
|
-
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) =>
|
|
14502
|
+
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log32.debug("amendForced failed", e));
|
|
14100
14503
|
}
|
|
14101
14504
|
}
|
|
14102
14505
|
};
|
|
@@ -15881,7 +16284,7 @@ import { spawnSync as spawnSync6 } from "child_process";
|
|
|
15881
16284
|
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
|
|
15882
16285
|
import { homedir as homedir8 } from "os";
|
|
15883
16286
|
import { join as join13 } from "path";
|
|
15884
|
-
var
|
|
16287
|
+
var log33 = forComponent("os-sched");
|
|
15885
16288
|
var OsScheduler = class {
|
|
15886
16289
|
options;
|
|
15887
16290
|
constructor(options) {
|
|
@@ -15943,7 +16346,7 @@ var OsScheduler = class {
|
|
|
15943
16346
|
}
|
|
15944
16347
|
}
|
|
15945
16348
|
} catch (e) {
|
|
15946
|
-
|
|
16349
|
+
log33.debug(`cancel ${id}`, e);
|
|
15947
16350
|
}
|
|
15948
16351
|
for (const f of [`${id}.json`, `${id}.sh`]) {
|
|
15949
16352
|
try {
|
|
@@ -16061,7 +16464,7 @@ import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unli
|
|
|
16061
16464
|
import { homedir as homedir9 } from "os";
|
|
16062
16465
|
import { join as join14 } from "path";
|
|
16063
16466
|
init_platform();
|
|
16064
|
-
var
|
|
16467
|
+
var log34 = forComponent("remote-trigger");
|
|
16065
16468
|
var TRIGGER_DIR = () => join14(homedir9(), ".agent", "triggers");
|
|
16066
16469
|
var sockPath = (sessionId, dir = TRIGGER_DIR()) => join14(dir, `${sessionId}.sock`);
|
|
16067
16470
|
var TriggerServer = class {
|
|
@@ -16099,13 +16502,13 @@ var TriggerServer = class {
|
|
|
16099
16502
|
conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
16100
16503
|
}
|
|
16101
16504
|
});
|
|
16102
|
-
conn.on("error", (e) =>
|
|
16505
|
+
conn.on("error", (e) => log34.debug("trigger conn error", e));
|
|
16103
16506
|
});
|
|
16104
|
-
this.server.on("error", (e) =>
|
|
16507
|
+
this.server.on("error", (e) => log34.debug("trigger server error", e));
|
|
16105
16508
|
this.server.listen(p);
|
|
16106
16509
|
this.path = p;
|
|
16107
16510
|
} catch (e) {
|
|
16108
|
-
|
|
16511
|
+
log34.debug("trigger server unavailable", e);
|
|
16109
16512
|
}
|
|
16110
16513
|
}
|
|
16111
16514
|
/** Re-bind on /resume (the session id changed). */
|
|
@@ -16237,7 +16640,7 @@ var strike = C("9");
|
|
|
16237
16640
|
var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
|
|
16238
16641
|
var mdPalette = { bold, dim, cyan, italic, strike, green, link };
|
|
16239
16642
|
var err = (s) => process.stderr.write(s);
|
|
16240
|
-
var
|
|
16643
|
+
var log35 = forComponent("cli");
|
|
16241
16644
|
var VERSION = (() => {
|
|
16242
16645
|
try {
|
|
16243
16646
|
return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -16377,7 +16780,10 @@ function parseArgs(argv) {
|
|
|
16377
16780
|
a.hardenNet = true;
|
|
16378
16781
|
} else if (x === "--subagents") a.subagents = true;
|
|
16379
16782
|
else if (x === "--duplex") a.duplex = true;
|
|
16380
|
-
else if (x === "--
|
|
16783
|
+
else if (x === "--duplex-v2") {
|
|
16784
|
+
a.duplex = true;
|
|
16785
|
+
a.duplexV2 = true;
|
|
16786
|
+
} else if (x === "--conversational" || x === "--convo" || x === "--voice") {
|
|
16381
16787
|
a.voice = true;
|
|
16382
16788
|
a.duplex = true;
|
|
16383
16789
|
} else if (x === "--voice-model") a.voiceModel = val(++i, x);
|
|
@@ -16461,6 +16867,8 @@ Flags:
|
|
|
16461
16867
|
--append-system-prompt <t> extra instructions appended to the system prompt for this run
|
|
16462
16868
|
--duplex duplex mode: a fast voice model replies instantly and delegates real work
|
|
16463
16869
|
to a background worker agent (-m model); results are re-voiced when ready
|
|
16870
|
+
--duplex-v2 duplex + a second voice channel: backchannels, fillers and bridges that never
|
|
16871
|
+
enter the agent's history (headless: stdin interim/pause/agent_state, stdout channel)
|
|
16464
16872
|
--conversational duplex with a conversation-native register \u2014 short fast turns, fillers,
|
|
16465
16873
|
impulsive reactions, human pacing (implies --duplex; aliases: --convo, --voice)
|
|
16466
16874
|
with SONIOX_API_KEY + CARTESIA_API_KEY(+VOICE_ID) set: real voice I/O \u2014 mic in,
|
|
@@ -17063,7 +17471,7 @@ function turnCost(model, usage) {
|
|
|
17063
17471
|
const priced = model.startsWith("claude-code/") ? `anthropic/${resolveModelAlias(model.slice("claude-code/".length))}` : model;
|
|
17064
17472
|
return costOf(getModelInfo(priced)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, priced);
|
|
17065
17473
|
}
|
|
17066
|
-
async function evaluateGoal(ai, condition, transcript,
|
|
17474
|
+
async function evaluateGoal(ai, condition, transcript, log36) {
|
|
17067
17475
|
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
17068
17476
|
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
17069
17477
|
return text.slice(0, 600);
|
|
@@ -17083,7 +17491,7 @@ ${recent}` }
|
|
|
17083
17491
|
const match = r.content.match(/\{[\s\S]*\}/);
|
|
17084
17492
|
if (match) return JSON.parse(match[0]);
|
|
17085
17493
|
} catch (e) {
|
|
17086
|
-
|
|
17494
|
+
log36(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
17087
17495
|
`));
|
|
17088
17496
|
}
|
|
17089
17497
|
return { met: false, reason: "evaluation unclear" };
|
|
@@ -17309,7 +17717,7 @@ function mcpAgentTools(mounted, opts) {
|
|
|
17309
17717
|
return tools;
|
|
17310
17718
|
}
|
|
17311
17719
|
async function closeMcp(mounted) {
|
|
17312
|
-
await Promise.all(mounted.map((m) => m.client.close().catch((e) =>
|
|
17720
|
+
await Promise.all(mounted.map((m) => m.client.close().catch((e) => log35.debug("mcp close failed", e))));
|
|
17313
17721
|
}
|
|
17314
17722
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
17315
17723
|
function mentionRefs(line) {
|
|
@@ -17368,7 +17776,7 @@ async function expandMentions(fs, line) {
|
|
|
17368
17776
|
if (loaded.includes(ref) || missing.includes(ref)) continue;
|
|
17369
17777
|
if (ref.includes(":") && mcpMentionResolver) {
|
|
17370
17778
|
const body = await mcpMentionResolver(ref).catch((e) => {
|
|
17371
|
-
|
|
17779
|
+
log35.debug("mcp mention resolve failed", e);
|
|
17372
17780
|
return null;
|
|
17373
17781
|
});
|
|
17374
17782
|
if (body != null) {
|
|
@@ -17459,7 +17867,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
17459
17867
|
try {
|
|
17460
17868
|
store.save(session);
|
|
17461
17869
|
} catch (ex) {
|
|
17462
|
-
|
|
17870
|
+
log35.debug("mid-turn session flush failed", ex);
|
|
17463
17871
|
}
|
|
17464
17872
|
}
|
|
17465
17873
|
return origNotify(e);
|
|
@@ -17608,14 +18016,14 @@ var isCancelTeardown = (e) => {
|
|
|
17608
18016
|
function installCancelGuards(mounted) {
|
|
17609
18017
|
process.on("unhandledRejection", (e) => {
|
|
17610
18018
|
if (isCancelTeardown(e)) {
|
|
17611
|
-
|
|
18019
|
+
log35.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
17612
18020
|
return;
|
|
17613
18021
|
}
|
|
17614
|
-
|
|
18022
|
+
log35.error("unhandledRejection", e);
|
|
17615
18023
|
});
|
|
17616
18024
|
process.on("uncaughtException", (e) => {
|
|
17617
18025
|
if (isCancelTeardown(e)) {
|
|
17618
|
-
|
|
18026
|
+
log35.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
17619
18027
|
return;
|
|
17620
18028
|
}
|
|
17621
18029
|
console.error(e);
|
|
@@ -18040,7 +18448,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
18040
18448
|
try {
|
|
18041
18449
|
mkdirSync12(dir, { recursive: true });
|
|
18042
18450
|
} catch (e) {
|
|
18043
|
-
|
|
18451
|
+
log35.debug("paste image dir create failed", e);
|
|
18044
18452
|
}
|
|
18045
18453
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
18046
18454
|
return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
|
|
@@ -18153,7 +18561,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
18153
18561
|
mkdirSync12(join15(cwd, ".agent"), { recursive: true });
|
|
18154
18562
|
appendFileSync(histPath, line + "\n");
|
|
18155
18563
|
} catch (e) {
|
|
18156
|
-
|
|
18564
|
+
log35.debug("history write failed", e);
|
|
18157
18565
|
}
|
|
18158
18566
|
};
|
|
18159
18567
|
const ago = (t) => {
|
|
@@ -18225,7 +18633,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
18225
18633
|
try {
|
|
18226
18634
|
store.save(session);
|
|
18227
18635
|
} catch (e) {
|
|
18228
|
-
|
|
18636
|
+
log35.debug("session save after rewind failed", e);
|
|
18229
18637
|
}
|
|
18230
18638
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
18231
18639
|
`));
|
|
@@ -18260,7 +18668,7 @@ ${task}`;
|
|
|
18260
18668
|
bangContext.length = 0;
|
|
18261
18669
|
}
|
|
18262
18670
|
const delta = await refreshCatalogs().catch((e) => {
|
|
18263
|
-
|
|
18671
|
+
log35.debug("catalog refresh failed", e);
|
|
18264
18672
|
return "";
|
|
18265
18673
|
});
|
|
18266
18674
|
if (delta) {
|
|
@@ -18523,7 +18931,7 @@ ${task}`;
|
|
|
18523
18931
|
desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
|
|
18524
18932
|
run: async () => {
|
|
18525
18933
|
await refreshCatalogs().catch((e) => {
|
|
18526
|
-
|
|
18934
|
+
log35.debug("catalog refresh failed", e);
|
|
18527
18935
|
});
|
|
18528
18936
|
face.reprepare();
|
|
18529
18937
|
err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
|
|
@@ -19026,7 +19434,7 @@ ${task}`;
|
|
|
19026
19434
|
try {
|
|
19027
19435
|
for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
|
|
19028
19436
|
} catch (e) {
|
|
19029
|
-
|
|
19437
|
+
log35.debug(`loadAgents(${d}) failed`, e);
|
|
19030
19438
|
}
|
|
19031
19439
|
}
|
|
19032
19440
|
if (!seen.size) {
|
|
@@ -19114,7 +19522,7 @@ ${task}`;
|
|
|
19114
19522
|
}
|
|
19115
19523
|
if (idx >= 0) {
|
|
19116
19524
|
const old = mounted.splice(idx, 1)[0];
|
|
19117
|
-
await old.client.close().catch((e) =>
|
|
19525
|
+
await old.client.close().catch((e) => log35.debug("mcp close failed", e));
|
|
19118
19526
|
}
|
|
19119
19527
|
try {
|
|
19120
19528
|
const m = await mountMcpServer(name, conf);
|
|
@@ -19142,7 +19550,7 @@ ${task}`;
|
|
|
19142
19550
|
}
|
|
19143
19551
|
const m = mounted.splice(idx, 1)[0];
|
|
19144
19552
|
remountMcpTools();
|
|
19145
|
-
await m.client.close().catch((e) =>
|
|
19553
|
+
await m.client.close().catch((e) => log35.debug("mcp close failed", e));
|
|
19146
19554
|
err(dim(` removed "${name}"
|
|
19147
19555
|
`));
|
|
19148
19556
|
return;
|
|
@@ -19345,7 +19753,7 @@ ${task}`;
|
|
|
19345
19753
|
try {
|
|
19346
19754
|
return readdirSync4(join15(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
19347
19755
|
} catch (e) {
|
|
19348
|
-
|
|
19756
|
+
log35.debug("completion readdir failed", absDir, e);
|
|
19349
19757
|
return null;
|
|
19350
19758
|
}
|
|
19351
19759
|
};
|
|
@@ -19482,7 +19890,7 @@ ${out}
|
|
|
19482
19890
|
return;
|
|
19483
19891
|
}
|
|
19484
19892
|
await refreshCatalogs().catch((e) => {
|
|
19485
|
-
|
|
19893
|
+
log35.debug("catalog refresh failed", e);
|
|
19486
19894
|
});
|
|
19487
19895
|
const sk = skills.find((x) => x.name === name);
|
|
19488
19896
|
if (sk) {
|
|
@@ -19593,7 +20001,7 @@ ${out}
|
|
|
19593
20001
|
if (delivery || cut) dx?.parkCutDelivery(delivery, !!cut, heard);
|
|
19594
20002
|
const cutReply = cut && cut.full.length - cut.heard.length > 40 ? cut : null;
|
|
19595
20003
|
const noteP = replaces ? replaces.note : !cutReply ? Promise.resolve("") : (instinct ? instinct.bargeIntent(text).catch((e) => {
|
|
19596
|
-
|
|
20004
|
+
log35.warn(`barge intent failed \u2014 model decides: ${e.message}`);
|
|
19597
20005
|
return void 0;
|
|
19598
20006
|
}) : Promise.resolve(void 0)).then(async (intent) => interruptionNote(cutReply, intent) + ackedNote(await instinct?.ackFor(text)));
|
|
19599
20007
|
const cutIn = replaces ? replaces.cutIn : interjectNote;
|
|
@@ -19688,6 +20096,14 @@ ${out}
|
|
|
19688
20096
|
};
|
|
19689
20097
|
try {
|
|
19690
20098
|
await voiceIO.start();
|
|
20099
|
+
if (args.duplexV2 && dx) {
|
|
20100
|
+
const v = voiceIO;
|
|
20101
|
+
bindSecondChannel(v, makeSecondChannel(ai, dx.options.reflexModel, { show: (t) => {
|
|
20102
|
+
err("\r\x1B[0J" + dim(` \u21B3 ${t}
|
|
20103
|
+
`));
|
|
20104
|
+
editorRef?.redrawNow();
|
|
20105
|
+
} }), () => voiceIO === v);
|
|
20106
|
+
}
|
|
19691
20107
|
const inDev = voiceIO.usingAec ? detectedInputDevice() : null;
|
|
19692
20108
|
const inNote = inDev ? `, in: ${inDev.name}` : "";
|
|
19693
20109
|
err(dim(` \u{1F3A4} voice on (${voiceIO.usingAec ? "echo-cancelled" : "heuristic echo \u2014 headphones recommended"}${inNote}) \u2014 just talk; speak over it to interrupt; say "voice off" to stop
|
|
@@ -19938,6 +20354,16 @@ function duplexError(e, reflexModel) {
|
|
|
19938
20354
|
return code && !msg.includes(String(code)) ? `${msg} (${code})` : msg;
|
|
19939
20355
|
}
|
|
19940
20356
|
var leakDecider = () => process.env.TYPESAFE_AI_KEY ? new DecideClient({ apiKey: process.env.TYPESAFE_AI_KEY }) : void 0;
|
|
20357
|
+
function makeSecondChannel(ai, reflexModel, opts) {
|
|
20358
|
+
const decider = leakDecider();
|
|
20359
|
+
const instinct = decider ? new Instinct({ decider }) : void 0;
|
|
20360
|
+
return new SecondChannel({
|
|
20361
|
+
decide: instinct ? (heard, state) => instinct.channel(heard, state) : void 0,
|
|
20362
|
+
generate: fastLineGenerator(ai, process.env.AGENTX_CHANNEL_MODEL || reflexModel),
|
|
20363
|
+
onEvent: (e) => log35.debug(`channel ${e.trigger}: rule=${e.rule} choice=${e.choice ?? "-"} \u2192 ${e.acted}${e.text ? ` "${e.text}"` : ""} ${e.ms}ms`),
|
|
20364
|
+
...opts
|
|
20365
|
+
});
|
|
20366
|
+
}
|
|
19941
20367
|
async function duplexServe(args, ai, cfg, cwd) {
|
|
19942
20368
|
const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
|
|
19943
20369
|
const mounted = await mountMcp(cfg, new McpOAuth({ storePath: join15(cwd, ".agent", "mcp-auth.json") }));
|
|
@@ -19947,8 +20373,10 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
19947
20373
|
const host = {
|
|
19948
20374
|
notify(e) {
|
|
19949
20375
|
const k = String(e.kind);
|
|
19950
|
-
if (k === "text_delta")
|
|
19951
|
-
|
|
20376
|
+
if (k === "text_delta") {
|
|
20377
|
+
out({ type: "text", text: e.message });
|
|
20378
|
+
sc?.onMainTokens(String(e.message ?? ""));
|
|
20379
|
+
} else if (k === "thinking_delta") out({ type: "thinking", text: e.message });
|
|
19952
20380
|
else if (k === "speak_utterance") out({ type: "speak", text: e.message });
|
|
19953
20381
|
else if (k === "hold_filler") out({ type: "filler", text: e.message });
|
|
19954
20382
|
else if (k === "turn_replace") out({ type: "replaced", ...e.data });
|
|
@@ -19980,6 +20408,8 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
19980
20408
|
reflexOptions: { fs: agent.options.fs, tools: [exitSessionTool(() => out({ type: "exit" }))] }
|
|
19981
20409
|
});
|
|
19982
20410
|
if (args.voiceSystemPrompt) dx.voice.options.systemPrompt += "\n\n" + args.voiceSystemPrompt;
|
|
20411
|
+
let scTurn = 0;
|
|
20412
|
+
const sc = args.duplexV2 ? makeSecondChannel(ai, dx.options.reflexModel, { show: (text, kind) => out({ type: "channel", kind, text }) }) : void 0;
|
|
19983
20413
|
const store = new SessionStore(cwd);
|
|
19984
20414
|
const session = startSession(args, store, dx.voice, cwd);
|
|
19985
20415
|
persist = () => {
|
|
@@ -19988,7 +20418,7 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
19988
20418
|
try {
|
|
19989
20419
|
store.save(session);
|
|
19990
20420
|
} catch (e) {
|
|
19991
|
-
|
|
20421
|
+
log35.warn("duplex session save failed", e);
|
|
19992
20422
|
}
|
|
19993
20423
|
};
|
|
19994
20424
|
out({ type: "ready", sessionId: session.meta.id, reflexModel: dx.options.reflexModel, actModel: dx.options.actModel, ...notes.length ? { notes } : {} });
|
|
@@ -20002,20 +20432,30 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
20002
20432
|
return;
|
|
20003
20433
|
}
|
|
20004
20434
|
if (m.type === "barge") {
|
|
20435
|
+
sc?.onBargeIn();
|
|
20005
20436
|
activeTurn?.abort();
|
|
20006
20437
|
dx.parkInFlightDeliveries();
|
|
20438
|
+
} else if (sc && channelInput(sc, m)) {
|
|
20007
20439
|
} else if (m.type === "cancel") out({ type: "task", kind: "cancel_result", id: m.task, message: dx.cancelTask(String(m.task)) });
|
|
20008
20440
|
else if (m.type === "exit") rl.close();
|
|
20009
20441
|
else if (m.type === "user" && typeof m.text === "string" && m.text.trim()) {
|
|
20010
|
-
const {
|
|
20442
|
+
const cut = m.cut && sc?.spoke(String(m.cut.full ?? "")) ? { ...m.cut, full: "", heard: "" } : m.cut;
|
|
20443
|
+
const { reply, delivery, heard } = splitCut(cut ?? null);
|
|
20011
20444
|
if (delivery || reply) dx.parkCutDelivery(delivery, !!reply, heard);
|
|
20012
20445
|
const note = reply && reply.full.length - reply.heard.length > 40 ? interruptionNote(reply) : "";
|
|
20013
20446
|
const id = m.id ? String(m.id) : void 0;
|
|
20447
|
+
sc?.onUserTurn(m.text);
|
|
20448
|
+
const seq = ++scTurn;
|
|
20449
|
+
const done2 = () => {
|
|
20450
|
+
if (seq === scTurn) sc?.onAgentState("idle");
|
|
20451
|
+
};
|
|
20014
20452
|
void runTurn(dx.voice, store, session, m.text + note, void 0, cwd, (c) => dx.send(c, { id, replaces: m.replaces ? String(m.replaces) : void 0, followUp: typeof m.followUp === "string" || m.followUp && typeof m.followUp === "object" ? m.followUp : void 0 })).then(({ res, error }) => {
|
|
20015
20453
|
const why = (res?.finishReason ?? "error") === "error" ? duplexError(res?.error ?? error, dx.options.reflexModel) : "";
|
|
20454
|
+
done2();
|
|
20016
20455
|
out({ type: "turn_done", id, finishReason: res?.finishReason ?? "error", ...why ? { error: why } : {} });
|
|
20017
20456
|
}).catch((e) => {
|
|
20018
|
-
|
|
20457
|
+
log35.error("duplex turn failed", e);
|
|
20458
|
+
done2();
|
|
20019
20459
|
out({ type: "turn_done", id, finishReason: "error", error: String(e?.message ?? e) });
|
|
20020
20460
|
});
|
|
20021
20461
|
}
|