@livx.cc/agentx 0.99.95 → 0.99.96
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 +219 -63
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -13445,6 +13445,11 @@ function bindSecondChannel(v, sc, live) {
|
|
|
13445
13445
|
sc.onBargeIn();
|
|
13446
13446
|
onBargeIn(phase);
|
|
13447
13447
|
};
|
|
13448
|
+
const speakDelta = v.speakDelta.bind(v);
|
|
13449
|
+
v.speakDelta = ((t, ...rest) => {
|
|
13450
|
+
if (live()) sc.noteReply(t);
|
|
13451
|
+
return speakDelta(t, ...rest);
|
|
13452
|
+
});
|
|
13448
13453
|
Object.assign(sc.options, {
|
|
13449
13454
|
aec: v.usingAec,
|
|
13450
13455
|
speak: (text, kind) => {
|
|
@@ -13657,6 +13662,9 @@ var JsonlDiagSink = class {
|
|
|
13657
13662
|
// src/voice/secondChannel.ts
|
|
13658
13663
|
init_logging();
|
|
13659
13664
|
|
|
13665
|
+
// src/voice/backchannelPlan.ts
|
|
13666
|
+
init_logging();
|
|
13667
|
+
|
|
13660
13668
|
// src/voice/channelLang.ts
|
|
13661
13669
|
init_logging();
|
|
13662
13670
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
@@ -13891,8 +13899,79 @@ var ChannelBanks = class {
|
|
|
13891
13899
|
}
|
|
13892
13900
|
};
|
|
13893
13901
|
|
|
13902
|
+
// src/voice/backchannelPlan.ts
|
|
13903
|
+
var log30 = forComponent("BackchannelPlan");
|
|
13904
|
+
var TURN_TYPES = ["story", "list", "complaint", "explanation", "short_command", "question", "other"];
|
|
13905
|
+
var DENSITIES = ["none", "low", "normal", "high"];
|
|
13906
|
+
var silentTurn = (p) => p.density === "none" || p.turn === "short_command" || p.turn === "question";
|
|
13907
|
+
var cueWords = (s) => s.toLowerCase().replace(/[^\p{L}\p{N}'\s]+/gu, " ").split(/\s+/).filter(Boolean);
|
|
13908
|
+
function parsePlan(raw, lang, maxCues = 8) {
|
|
13909
|
+
const m = raw.match(/\{[\s\S]*\}/);
|
|
13910
|
+
if (!m) return null;
|
|
13911
|
+
let o;
|
|
13912
|
+
try {
|
|
13913
|
+
o = JSON.parse(m[0]);
|
|
13914
|
+
} catch {
|
|
13915
|
+
return null;
|
|
13916
|
+
}
|
|
13917
|
+
const turn = String(o?.turn ?? "").toLowerCase().replace(/[\s-]+/g, "_");
|
|
13918
|
+
const density = String(o?.density ?? "").toLowerCase();
|
|
13919
|
+
if (!TURN_TYPES.includes(turn) || !DENSITIES.includes(density)) return null;
|
|
13920
|
+
const cues = [];
|
|
13921
|
+
for (const c of Array.isArray(o.cues) ? o.cues : []) {
|
|
13922
|
+
const when = (Array.isArray(c?.when) ? c.when : [c?.when]).filter((w) => typeof w === "string").map((w) => cueWords(w).join(" ")).filter((w) => w && w.split(" ").length <= 4);
|
|
13923
|
+
const line = typeof c?.line === "string" ? cleanLine(c.line, lang, 4) : "";
|
|
13924
|
+
if (when.length && line) cues.push({ when: [...new Set(when)], line });
|
|
13925
|
+
if (cues.length >= maxCues) break;
|
|
13926
|
+
}
|
|
13927
|
+
return { turn, density, cues };
|
|
13928
|
+
}
|
|
13929
|
+
function matchCue(plan, heard, from = 0) {
|
|
13930
|
+
if (!plan || silentTurn(plan)) return null;
|
|
13931
|
+
const w = cueWords(heard);
|
|
13932
|
+
for (let i = Math.max(0, from); i < w.length; i++) {
|
|
13933
|
+
for (const c of plan.cues) {
|
|
13934
|
+
for (const p of c.when) {
|
|
13935
|
+
const pw = p.split(" ");
|
|
13936
|
+
if (i + pw.length <= w.length && pw.every((x, j) => w[i + j] === x)) return c;
|
|
13937
|
+
}
|
|
13938
|
+
}
|
|
13939
|
+
}
|
|
13940
|
+
return null;
|
|
13941
|
+
}
|
|
13942
|
+
var BackchannelPlannerOptions = class {
|
|
13943
|
+
/** Fast model (Groq gpt-oss / the reflex). */
|
|
13944
|
+
model = "groq/openai/gpt-oss-120b";
|
|
13945
|
+
maxTokens = 900;
|
|
13946
|
+
};
|
|
13947
|
+
var BackchannelPlanner = class {
|
|
13948
|
+
constructor(ai, options) {
|
|
13949
|
+
this.ai = ai;
|
|
13950
|
+
this.options = options;
|
|
13951
|
+
this.options = { ...new BackchannelPlannerOptions(), ...options };
|
|
13952
|
+
}
|
|
13953
|
+
ai;
|
|
13954
|
+
options;
|
|
13955
|
+
async plan(c, signal) {
|
|
13956
|
+
const o = this.options;
|
|
13957
|
+
const name = languageName(c.lang);
|
|
13958
|
+
const prompt = `You plan a live voice assistant's LISTENER sounds ("mm-hm", "right", "oh no") for the user's ${c.heard ? "CURRENT" : "NEXT"} spoken turn.
|
|
13959
|
+
Assistant just said: "${c.agent.slice(-400)}"
|
|
13960
|
+
` + (c.user ? `User's previous turn ended: "${c.user.slice(-200)}"
|
|
13961
|
+
` : "") + (c.heard ? `User so far in this turn: "${c.heard.slice(-300)}"
|
|
13962
|
+
` : "") + `Predict the turn: turn = story | list | complaint | explanation | short_command | question | other; density = none (short request or a question: never interrupt) | low | normal | high (a long story wants frequent cues).
|
|
13963
|
+
cues: 3-8 short words/phrases the user is likely to say at a clause boundary where a listener cue fits (list markers like "first", "and then", "also"; narrative markers; emotional words; names/topics from the context), each with ONE fitting listener line of 1-3 words in ${name} that reacts to it (sympathy for bad news, "okay" for a list step) \u2014 never a question, never an answer.
|
|
13964
|
+
Reply JSON only: {"turn":"...","density":"...","cues":[{"when":["phrase",...],"line":"..."}]}. Phrases in the user's language.`;
|
|
13965
|
+
const reasoning = /gpt-oss/.test(o.model) ? { providerOptions: { reasoning_effort: "low" } } : {};
|
|
13966
|
+
const r = await this.ai.chat({ model: o.model, messages: [{ role: "user", content: prompt }], stream: false, signal, maxTokens: o.maxTokens, ...reasoning });
|
|
13967
|
+
const p = parsePlan(r.content ?? "", c.lang);
|
|
13968
|
+
if (!p) log30.debug(`unusable plan: ${(r.content ?? "").slice(0, 120)}`);
|
|
13969
|
+
return p;
|
|
13970
|
+
}
|
|
13971
|
+
};
|
|
13972
|
+
|
|
13894
13973
|
// src/voice/secondChannel.ts
|
|
13895
|
-
var
|
|
13974
|
+
var log31 = forComponent("SecondChannel");
|
|
13896
13975
|
var SecondChannelOptions = class {
|
|
13897
13976
|
/** Refiner (Jev/fast LLM): the choice over CHANNEL_ACTIONS, or null on failure. Absent → the rule alone decides. */
|
|
13898
13977
|
decide;
|
|
@@ -13941,6 +14020,23 @@ var SecondChannelOptions = class {
|
|
|
13941
14020
|
fillerPauseMs = 700;
|
|
13942
14021
|
fillerMinWords = 4;
|
|
13943
14022
|
bridgeMinWords = 5;
|
|
14023
|
+
/** ANTICIPATE (backchannelPlan.ts): plan the user's next turn off the hot path (after the agent's reply / at turn start,
|
|
14024
|
+
* revised once at `reviseAtWords`); live, a backchannel fires only on a planned cue, at a `cuePauseMs` breath, with the
|
|
14025
|
+
* cue's line and no decider call. A silent plan (short command / question / density none) vetoes backchannels. No plan
|
|
14026
|
+
* yet / failed / late → the reactive path. Off by default (AGENTX_CHANNEL_ANTICIPATE=1). */
|
|
14027
|
+
anticipate = false;
|
|
14028
|
+
plan;
|
|
14029
|
+
planBudgetMs = 4e3;
|
|
14030
|
+
reviseAtWords = 7;
|
|
14031
|
+
/** A predicted moment needs only a short breath (the decision is already made). */
|
|
14032
|
+
cuePauseMs = 200;
|
|
14033
|
+
/** A cue counts only within the last N words (older matches were gated or passed). */
|
|
14034
|
+
cueWindow = 6;
|
|
14035
|
+
/** true: a plan fires on its cues only. false: a non-silent plan with density normal/high also lets the reactive breath
|
|
14036
|
+
* backchannel through (the plan then only vetoes short commands / questions / low density). */
|
|
14037
|
+
cueOnly = true;
|
|
14038
|
+
onPlan = () => {
|
|
14039
|
+
};
|
|
13944
14040
|
now = () => performance.now();
|
|
13945
14041
|
random = Math.random;
|
|
13946
14042
|
};
|
|
@@ -13949,12 +14045,12 @@ function withBudget(p, ms, what) {
|
|
|
13949
14045
|
let t;
|
|
13950
14046
|
const late = new Promise((r) => {
|
|
13951
14047
|
t = setTimeout(() => {
|
|
13952
|
-
|
|
14048
|
+
log31.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
|
|
13953
14049
|
r(null);
|
|
13954
14050
|
}, ms);
|
|
13955
14051
|
});
|
|
13956
14052
|
return Promise.race([p.catch((e) => {
|
|
13957
|
-
|
|
14053
|
+
log31.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
|
|
13958
14054
|
return null;
|
|
13959
14055
|
}), late]).finally(() => clearTimeout(t));
|
|
13960
14056
|
}
|
|
@@ -13991,6 +14087,18 @@ var SecondChannel = class {
|
|
|
13991
14087
|
startedAt;
|
|
13992
14088
|
lang = null;
|
|
13993
14089
|
pinned = false;
|
|
14090
|
+
// anticipate: the plan for the user's current/next turn (turnNo bumps per dispatched turn → a late plan is dropped)
|
|
14091
|
+
turnNo = 0;
|
|
14092
|
+
plan = null;
|
|
14093
|
+
planning = false;
|
|
14094
|
+
planTries = 0;
|
|
14095
|
+
// per turn: the reply-end plan + one turn-start retry, never a call per interim
|
|
14096
|
+
revised = false;
|
|
14097
|
+
cueFrom = 0;
|
|
14098
|
+
// word index after the last planned backchannel
|
|
14099
|
+
reply = "";
|
|
14100
|
+
// channel-1's text this turn (the plan's context)
|
|
14101
|
+
lastUser = "";
|
|
13994
14102
|
/** The language lines are spoken in (detected from the user's words, or set by the host). */
|
|
13995
14103
|
get language() {
|
|
13996
14104
|
return this.lang;
|
|
@@ -14002,7 +14110,7 @@ var SecondChannel = class {
|
|
|
14002
14110
|
if (host) this.pinned = true;
|
|
14003
14111
|
const lang = primaryLang(tag);
|
|
14004
14112
|
if (!lang || lang === this.lang) return;
|
|
14005
|
-
|
|
14113
|
+
log31.debug(`language ${this.lang} \u2192 ${lang}`);
|
|
14006
14114
|
this.lang = lang;
|
|
14007
14115
|
this.recent = [];
|
|
14008
14116
|
this.picks.clear();
|
|
@@ -14034,11 +14142,21 @@ var SecondChannel = class {
|
|
|
14034
14142
|
this.pauseTimer = null;
|
|
14035
14143
|
this.onPause(this.o.fillerPauseMs);
|
|
14036
14144
|
}, this.o.fillerPauseMs);
|
|
14145
|
+
let cue = null;
|
|
14146
|
+
if (this.o.anticipate) {
|
|
14147
|
+
const n = words2(t).length;
|
|
14148
|
+
if (!this.plan && !this.planning && this.planTries < 2) this.requestPlan();
|
|
14149
|
+
else if (this.plan && !this.revised && n >= this.o.reviseAtWords) {
|
|
14150
|
+
this.revised = true;
|
|
14151
|
+
this.requestPlan(t);
|
|
14152
|
+
}
|
|
14153
|
+
cue = matchCue(this.plan, t, Math.max(this.cueFrom, n - this.o.cueWindow));
|
|
14154
|
+
}
|
|
14037
14155
|
if (this.bcTimer) clearTimeout(this.bcTimer);
|
|
14038
14156
|
this.bcTimer = this.o.aec ? setTimeout(() => {
|
|
14039
14157
|
this.bcTimer = null;
|
|
14040
|
-
if (this.userSpeaking && this.heard === t) void this.consider("interim", 0, true);
|
|
14041
|
-
}, this.o.backchannelPauseMs) : null;
|
|
14158
|
+
if (this.userSpeaking && this.heard === t) void this.consider("interim", 0, true, cue);
|
|
14159
|
+
}, cue ? this.o.cuePauseMs : this.o.backchannelPauseMs) : null;
|
|
14042
14160
|
if (words2(t).length - this.decidedAt >= this.o.decideEveryWords) void this.consider("interim");
|
|
14043
14161
|
}
|
|
14044
14162
|
/** The user went quiet for `ms` (host VAD/STT pause, or the internal interim timer). */
|
|
@@ -14067,9 +14185,17 @@ var SecondChannel = class {
|
|
|
14067
14185
|
this.heard = "";
|
|
14068
14186
|
this.decidedAt = 0;
|
|
14069
14187
|
this.bridged = false;
|
|
14188
|
+
this.cueFrom = 0;
|
|
14189
|
+
if (this.o.anticipate && this.reply.trim() && !this.plan && !this.planning) this.requestPlan();
|
|
14070
14190
|
return;
|
|
14071
14191
|
}
|
|
14072
14192
|
if (s === "queued") {
|
|
14193
|
+
this.turnNo++;
|
|
14194
|
+
this.plan = null;
|
|
14195
|
+
this.planning = false;
|
|
14196
|
+
this.planTries = 0;
|
|
14197
|
+
this.revised = false;
|
|
14198
|
+
this.reply = "";
|
|
14073
14199
|
this.gen++;
|
|
14074
14200
|
this.userSpeaking = false;
|
|
14075
14201
|
this.bridged = false;
|
|
@@ -14088,14 +14214,20 @@ var SecondChannel = class {
|
|
|
14088
14214
|
/** The user's final line was dispatched to the main agent (its words are the bridge's micro-context). */
|
|
14089
14215
|
onUserTurn(text) {
|
|
14090
14216
|
this.heard = text.trim();
|
|
14217
|
+
this.lastUser = this.heard;
|
|
14091
14218
|
this.detect(this.heard);
|
|
14092
14219
|
this.main = "idle";
|
|
14093
14220
|
this.onAgentState("queued");
|
|
14094
14221
|
}
|
|
14095
14222
|
/** The main answer's streamed text (before its audio): channel-1 is responding — no bridge from here on. */
|
|
14096
14223
|
onMainTokens(text) {
|
|
14224
|
+
this.noteReply(text);
|
|
14097
14225
|
if (text.trim() && (this.main === "queued" || this.main === "thinking")) this.onAgentState("streaming_text");
|
|
14098
14226
|
}
|
|
14227
|
+
/** Channel-1's reply text (a delta) — only the plan's context; no state change (hosts without onMainTokens). */
|
|
14228
|
+
noteReply(text) {
|
|
14229
|
+
this.reply += text;
|
|
14230
|
+
}
|
|
14099
14231
|
/** The user barged in: cut the in-flight line now and drop every pending decision. */
|
|
14100
14232
|
onBargeIn() {
|
|
14101
14233
|
this.gen++;
|
|
@@ -14105,7 +14237,7 @@ var SecondChannel = class {
|
|
|
14105
14237
|
try {
|
|
14106
14238
|
this.o.cancel();
|
|
14107
14239
|
} catch (e) {
|
|
14108
|
-
|
|
14240
|
+
log31.warn(`cancel threw: ${e.message}`);
|
|
14109
14241
|
}
|
|
14110
14242
|
}
|
|
14111
14243
|
}
|
|
@@ -14134,13 +14266,31 @@ var SecondChannel = class {
|
|
|
14134
14266
|
return (this.main === "queued" || this.main === "thinking") && !this.bridged;
|
|
14135
14267
|
}
|
|
14136
14268
|
/** 0ms rule: the candidate line for this trigger, or silence. */
|
|
14137
|
-
rule(trigger, pauseMs = 0, breath = false) {
|
|
14269
|
+
rule(trigger, pauseMs = 0, breath = false, cue = null) {
|
|
14138
14270
|
const w = words2(this.heard);
|
|
14139
14271
|
if (!this.lang) return "silence";
|
|
14140
14272
|
if (w.length <= 3 || w.length <= 6 && isPhatic(this.heard, this.bank)) return "silence";
|
|
14141
|
-
const k = trigger === "interim" ? this.backchannelMoment(w.length, breath) ? "backchannel" : null : trigger === "pause" ? pauseMs >= this.o.fillerPauseMs && w.length >= this.o.fillerMinWords ? "filler" : null : w.length >= this.o.bridgeMinWords ? "bridge" : null;
|
|
14273
|
+
const k = trigger === "interim" ? this.backchannelMoment(w.length, breath) && (!this.o.anticipate || !this.plan || !silentTurn(this.plan) && (!!cue || !this.o.cueOnly && this.plan.density !== "low")) ? "backchannel" : null : trigger === "pause" ? pauseMs >= this.o.fillerPauseMs && w.length >= this.o.fillerMinWords ? "filler" : null : w.length >= this.o.bridgeMinWords ? "bridge" : null;
|
|
14142
14274
|
return k && this.allowed(k) ? k : "silence";
|
|
14143
14275
|
}
|
|
14276
|
+
/** Plan (or, with `heard`, revise) the backchannels of the user's turn — async, never on the hot path. */
|
|
14277
|
+
requestPlan(heard) {
|
|
14278
|
+
if (!this.o.plan || !this.lang) return;
|
|
14279
|
+
const turn = this.turnNo, t0 = this.o.now(), ac = new AbortController();
|
|
14280
|
+
if (!heard) {
|
|
14281
|
+
this.planning = true;
|
|
14282
|
+
this.planTries++;
|
|
14283
|
+
}
|
|
14284
|
+
const ctx = { agent: this.reply.trim(), user: words2(this.lastUser).slice(-30).join(" "), recent: [...this.recent], lang: this.lang, heard };
|
|
14285
|
+
void withBudget(this.o.plan(ctx, ac.signal), this.o.planBudgetMs, heard ? "plan revision" : "plan").then((p) => {
|
|
14286
|
+
if (p === null) ac.abort();
|
|
14287
|
+
if (turn !== this.turnNo) return;
|
|
14288
|
+
if (!heard) this.planning = false;
|
|
14289
|
+
if (!p) return;
|
|
14290
|
+
this.plan = p;
|
|
14291
|
+
this.o.onPlan(p, !!heard, Math.round(this.o.now() - t0));
|
|
14292
|
+
});
|
|
14293
|
+
}
|
|
14144
14294
|
/** A clause boundary mid-floor: a breath or a comma-like close. Not a question (it wants the answer, not "mm-hm"), and a
|
|
14145
14295
|
* sentence end only deep into a story — a short request's end is the end of their turn (replay: "mm-hm" right after
|
|
14146
14296
|
* "Check hacker news for recent news.", just before the final). */
|
|
@@ -14149,33 +14299,36 @@ var SecondChannel = class {
|
|
|
14149
14299
|
if (/[.!]$/.test(this.heard)) return n >= 2 * this.o.backchannelMinWords;
|
|
14150
14300
|
return breath || /[,;:…]$/.test(this.heard);
|
|
14151
14301
|
}
|
|
14152
|
-
async consider(trigger, pauseMs, breath = false) {
|
|
14302
|
+
async consider(trigger, pauseMs, breath = false, cue = null) {
|
|
14153
14303
|
if (this.busy) return;
|
|
14154
14304
|
const n0 = words2(this.heard).length;
|
|
14155
14305
|
this.decidedAt = n0;
|
|
14156
|
-
|
|
14306
|
+
if (!(this.o.anticipate && this.plan && trigger === "interim")) cue = null;
|
|
14307
|
+
const rule = this.rule(trigger, pauseMs, breath, cue);
|
|
14157
14308
|
if (rule === "silence") return;
|
|
14158
14309
|
this.busy = true;
|
|
14159
14310
|
const gen = this.gen, t0 = this.o.now();
|
|
14160
|
-
const ev = (e) => this.o.onEvent({ trigger, rule, ms: Math.round(this.o.now() - t0), ...e });
|
|
14311
|
+
const ev = (e) => this.o.onEvent({ trigger, rule, ms: Math.round(this.o.now() - t0), ...cue ? { cue: cue.line } : {}, ...e });
|
|
14161
14312
|
const stale = () => gen !== this.gen || words2(this.heard).length - n0 >= this.o.staleWords;
|
|
14162
14313
|
try {
|
|
14163
14314
|
let kind = rule;
|
|
14164
14315
|
let choice;
|
|
14165
|
-
if (this.o.decide) {
|
|
14316
|
+
if (this.o.decide && !cue) {
|
|
14166
14317
|
const d = await withBudget(this.o.decide(this.tail(), this.state(pauseMs)), this.o.decisionBudgetMs, "channel decision");
|
|
14167
14318
|
choice = d?.choice;
|
|
14168
14319
|
if (choice && choice in CHANNEL_ACTIONS && (choice === "silence" || this.allowed(choice))) kind = choice;
|
|
14169
|
-
else if (choice && choice in CHANNEL_ACTIONS)
|
|
14320
|
+
else if (choice && choice in CHANNEL_ACTIONS) log31.debug(`decider chose ${choice} for a ${rule} moment \u2014 the rule stands`);
|
|
14170
14321
|
}
|
|
14171
14322
|
if (kind === "silence") return ev({ choice, acted: "silence" });
|
|
14172
14323
|
if (stale()) return ev({ choice, kind, acted: "stale" });
|
|
14173
14324
|
if (!this.allowed(kind)) return ev({ choice, kind, acted: "gated" });
|
|
14174
|
-
const text = await this.line(kind);
|
|
14325
|
+
const text = cue && !this.recent.some((r) => norm2(r) === norm2(cue.line)) ? cue.line : await this.line(kind);
|
|
14175
14326
|
if (!text) return ev({ choice, kind, acted: "no_bank" });
|
|
14176
14327
|
if (stale()) return ev({ choice, kind, text, acted: "stale" });
|
|
14177
14328
|
if (!this.allowed(kind)) return ev({ choice, kind, text, acted: "gated" });
|
|
14178
|
-
|
|
14329
|
+
const said = this.say(kind, text);
|
|
14330
|
+
if (said && cue) this.cueFrom = words2(this.heard).length;
|
|
14331
|
+
ev({ choice, kind, text, acted: said ? "spoken" : "skipped" });
|
|
14179
14332
|
} finally {
|
|
14180
14333
|
this.busy = false;
|
|
14181
14334
|
}
|
|
@@ -14199,7 +14352,7 @@ var SecondChannel = class {
|
|
|
14199
14352
|
if (raw === null) ac.abort();
|
|
14200
14353
|
const text = cleanLine(raw ?? "", this.lang);
|
|
14201
14354
|
if (text && !this.recent.some((r) => norm2(r) === norm2(text))) return text;
|
|
14202
|
-
if (text)
|
|
14355
|
+
if (text) log31.debug(`generated "${text}" repeats a recent line \u2014 canned`);
|
|
14203
14356
|
}
|
|
14204
14357
|
return this.pick(kind);
|
|
14205
14358
|
}
|
|
@@ -14221,7 +14374,7 @@ var SecondChannel = class {
|
|
|
14221
14374
|
try {
|
|
14222
14375
|
r = this.o.speak(text, kind);
|
|
14223
14376
|
} catch (e) {
|
|
14224
|
-
|
|
14377
|
+
log31.warn(`speak threw \u2014 line dropped: ${e.message}`);
|
|
14225
14378
|
return false;
|
|
14226
14379
|
}
|
|
14227
14380
|
if (r === false) return false;
|
|
@@ -14233,7 +14386,7 @@ var SecondChannel = class {
|
|
|
14233
14386
|
this.o.show(text, kind);
|
|
14234
14387
|
const p = Promise.resolve(r).then(() => {
|
|
14235
14388
|
}, (e) => {
|
|
14236
|
-
|
|
14389
|
+
log31.warn(`channel line playback failed: ${e?.message ?? e}`);
|
|
14237
14390
|
}).finally(() => {
|
|
14238
14391
|
if (this.inflight === p) this.inflight = null;
|
|
14239
14392
|
});
|
|
@@ -14405,7 +14558,7 @@ function applyOverlay(cfg, file) {
|
|
|
14405
14558
|
// cli/hooks-config.ts
|
|
14406
14559
|
import { spawnSync as spawnSync4 } from "child_process";
|
|
14407
14560
|
init_platform();
|
|
14408
|
-
var
|
|
14561
|
+
var log32 = forComponent("hooks");
|
|
14409
14562
|
var escapeRegex = (s) => s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
14410
14563
|
function ruleMatches(rule, toolName) {
|
|
14411
14564
|
if (!rule.tool || rule.tool === "*") return true;
|
|
@@ -14424,7 +14577,7 @@ function runCmd(rule, env) {
|
|
|
14424
14577
|
});
|
|
14425
14578
|
return { code: r.status ?? 1, out: ((r.stdout ?? "") + (r.stderr ?? "")).trim() };
|
|
14426
14579
|
} catch (e) {
|
|
14427
|
-
|
|
14580
|
+
log32.debug(`hook command failed: ${rule.command}`, e);
|
|
14428
14581
|
return { code: 1, out: String(e?.message ?? e) };
|
|
14429
14582
|
}
|
|
14430
14583
|
}
|
|
@@ -14532,7 +14685,7 @@ import { existsSync as existsSync6, mkdirSync as mkdirSync8, readFileSync as rea
|
|
|
14532
14685
|
import { homedir as homedir6 } from "os";
|
|
14533
14686
|
import { join as join11 } from "path";
|
|
14534
14687
|
init_platform();
|
|
14535
|
-
var
|
|
14688
|
+
var log33 = forComponent("session");
|
|
14536
14689
|
var globalDir = () => join11(homedir6(), ".agent", "sessions");
|
|
14537
14690
|
function indexLink(target, link2) {
|
|
14538
14691
|
try {
|
|
@@ -14595,7 +14748,7 @@ var SessionStore = class {
|
|
|
14595
14748
|
}
|
|
14596
14749
|
load(id) {
|
|
14597
14750
|
if (!this.safeId(id)) {
|
|
14598
|
-
|
|
14751
|
+
log33.debug(`rejecting unsafe session id: ${id}`);
|
|
14599
14752
|
return void 0;
|
|
14600
14753
|
}
|
|
14601
14754
|
const path = join11(this.dir, `${id}.json`);
|
|
@@ -14603,7 +14756,7 @@ var SessionStore = class {
|
|
|
14603
14756
|
try {
|
|
14604
14757
|
return JSON.parse(readFileSync8(path, "utf8"));
|
|
14605
14758
|
} catch (e) {
|
|
14606
|
-
|
|
14759
|
+
log33.debug(`unreadable session ${id} \u2014 ignoring`, e);
|
|
14607
14760
|
return void 0;
|
|
14608
14761
|
}
|
|
14609
14762
|
}
|
|
@@ -14616,7 +14769,7 @@ var SessionStore = class {
|
|
|
14616
14769
|
try {
|
|
14617
14770
|
metas.push(JSON.parse(readFileSync8(join11(this.dir, f), "utf8")).meta);
|
|
14618
14771
|
} catch (e) {
|
|
14619
|
-
|
|
14772
|
+
log33.debug(`skipping unreadable session file ${f}`, e);
|
|
14620
14773
|
}
|
|
14621
14774
|
}
|
|
14622
14775
|
return metas.sort((a, b) => b.updated - a.updated);
|
|
@@ -14855,7 +15008,7 @@ import { promisify } from "util";
|
|
|
14855
15008
|
import { writeFileSync as writeFileSync6, mkdirSync as mkdirSync9 } from "fs";
|
|
14856
15009
|
import { homedir as homedir7 } from "os";
|
|
14857
15010
|
import { join as join12, resolve as resolve4, sep as sep3, parse as parsePath } from "path";
|
|
14858
|
-
var
|
|
15011
|
+
var log34 = forComponent("checkpoints");
|
|
14859
15012
|
var exec = promisify(execFile4);
|
|
14860
15013
|
var DEFAULT_EXCLUDE = [".agent/", ".git/", "node_modules/", "dist/", "build/", ".next/", "target/", ".venv/", "__pycache__/", "*.log"];
|
|
14861
15014
|
function isBroadRoot(p) {
|
|
@@ -14901,7 +15054,7 @@ var ShadowRepo = class {
|
|
|
14901
15054
|
writeFileSync6(join12(this.gitDir, "info", "exclude"), this.exclude.join("\n") + "\n");
|
|
14902
15055
|
this.ready = true;
|
|
14903
15056
|
} catch (e) {
|
|
14904
|
-
|
|
15057
|
+
log34.debug(`git checkpoints unavailable for ${this.workTree}`, e);
|
|
14905
15058
|
this.ready = false;
|
|
14906
15059
|
}
|
|
14907
15060
|
return this.ready;
|
|
@@ -14912,7 +15065,7 @@ var ShadowRepo = class {
|
|
|
14912
15065
|
}
|
|
14913
15066
|
async commit(label, forced = []) {
|
|
14914
15067
|
await this.run("add", "-A");
|
|
14915
|
-
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) =>
|
|
15068
|
+
for (const p of forced) await this.run("add", "-f", "--", p).catch((e) => log34.debug(`force-add failed: ${p}`, e));
|
|
14916
15069
|
await this.run("commit", "--allow-empty", "-q", "-m", label);
|
|
14917
15070
|
}
|
|
14918
15071
|
/** Inject the CURRENT (pre-edit) content of `paths` into the turn-open restore point by amending it.
|
|
@@ -14920,8 +15073,8 @@ var ShadowRepo = class {
|
|
|
14920
15073
|
* turn-boundary `add -A` would never have captured it. Amend (vs a new commit) keeps one restore
|
|
14921
15074
|
* point per turn, so the REPL's turn↔frame mapping stays intact. */
|
|
14922
15075
|
async amendForced(paths) {
|
|
14923
|
-
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) =>
|
|
14924
|
-
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) =>
|
|
15076
|
+
for (const p of paths) await this.run("add", "-f", "--", p).catch((e) => log34.debug(`force-capture failed: ${p}`, e));
|
|
15077
|
+
await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log34.debug("amend failed", e));
|
|
14925
15078
|
}
|
|
14926
15079
|
/** Commits on `ref`, oldest-first (canonical index space). */
|
|
14927
15080
|
async log(ref) {
|
|
@@ -14981,7 +15134,7 @@ var ShadowRepo = class {
|
|
|
14981
15134
|
await this.run("gc", "--auto", "-q").catch(() => {
|
|
14982
15135
|
});
|
|
14983
15136
|
} catch (e) {
|
|
14984
|
-
|
|
15137
|
+
log34.debug("checkpoint prune failed", e);
|
|
14985
15138
|
}
|
|
14986
15139
|
}
|
|
14987
15140
|
};
|
|
@@ -15049,7 +15202,7 @@ var GitCheckpoints = class {
|
|
|
15049
15202
|
use(sessionId) {
|
|
15050
15203
|
if (sessionId === this.session) return;
|
|
15051
15204
|
this.session = sessionId;
|
|
15052
|
-
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) =>
|
|
15205
|
+
if (this.started) for (const r of this.repos) void r.point(this.ref()).catch((e) => log34.debug("re-point failed", e));
|
|
15053
15206
|
}
|
|
15054
15207
|
async begin(label) {
|
|
15055
15208
|
if (!await this.start()) return;
|
|
@@ -15069,7 +15222,7 @@ var GitCheckpoints = class {
|
|
|
15069
15222
|
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
|
|
15070
15223
|
`);
|
|
15071
15224
|
} else {
|
|
15072
|
-
|
|
15225
|
+
log34.debug("checkpoint commit failed", e);
|
|
15073
15226
|
survivors.push(this.repos[i]);
|
|
15074
15227
|
survivorCaches.push(this.caches[i] ?? []);
|
|
15075
15228
|
}
|
|
@@ -15104,7 +15257,7 @@ var GitCheckpoints = class {
|
|
|
15104
15257
|
if (this.forced.has(abs)) continue;
|
|
15105
15258
|
this.forced.add(abs);
|
|
15106
15259
|
const i = this.repoIndexFor(abs);
|
|
15107
|
-
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) =>
|
|
15260
|
+
if (i >= 0) await this.repos[i].amendForced([abs]).catch((e) => log34.debug("amendForced failed", e));
|
|
15108
15261
|
}
|
|
15109
15262
|
}
|
|
15110
15263
|
};
|
|
@@ -16889,7 +17042,7 @@ import { spawnSync as spawnSync6 } from "child_process";
|
|
|
16889
17042
|
import { writeFileSync as writeFileSync8, mkdirSync as mkdirSync10, readdirSync as readdirSync2, unlinkSync as unlinkSync3, chmodSync, existsSync as existsSync7 } from "fs";
|
|
16890
17043
|
import { homedir as homedir8 } from "os";
|
|
16891
17044
|
import { join as join14 } from "path";
|
|
16892
|
-
var
|
|
17045
|
+
var log35 = forComponent("os-sched");
|
|
16893
17046
|
var OsScheduler = class {
|
|
16894
17047
|
options;
|
|
16895
17048
|
constructor(options) {
|
|
@@ -16951,7 +17104,7 @@ var OsScheduler = class {
|
|
|
16951
17104
|
}
|
|
16952
17105
|
}
|
|
16953
17106
|
} catch (e) {
|
|
16954
|
-
|
|
17107
|
+
log35.debug(`cancel ${id}`, e);
|
|
16955
17108
|
}
|
|
16956
17109
|
for (const f of [`${id}.json`, `${id}.sh`]) {
|
|
16957
17110
|
try {
|
|
@@ -17069,7 +17222,7 @@ import { existsSync as existsSync8, mkdirSync as mkdirSync11, unlinkSync as unli
|
|
|
17069
17222
|
import { homedir as homedir9 } from "os";
|
|
17070
17223
|
import { join as join15 } from "path";
|
|
17071
17224
|
init_platform();
|
|
17072
|
-
var
|
|
17225
|
+
var log36 = forComponent("remote-trigger");
|
|
17073
17226
|
var TRIGGER_DIR = () => join15(homedir9(), ".agent", "triggers");
|
|
17074
17227
|
var sockPath = (sessionId, dir = TRIGGER_DIR()) => join15(dir, `${sessionId}.sock`);
|
|
17075
17228
|
var TriggerServer = class {
|
|
@@ -17107,13 +17260,13 @@ var TriggerServer = class {
|
|
|
17107
17260
|
conn.end(JSON.stringify({ ok: false, error: String(e) }) + "\n");
|
|
17108
17261
|
}
|
|
17109
17262
|
});
|
|
17110
|
-
conn.on("error", (e) =>
|
|
17263
|
+
conn.on("error", (e) => log36.debug("trigger conn error", e));
|
|
17111
17264
|
});
|
|
17112
|
-
this.server.on("error", (e) =>
|
|
17265
|
+
this.server.on("error", (e) => log36.debug("trigger server error", e));
|
|
17113
17266
|
this.server.listen(p);
|
|
17114
17267
|
this.path = p;
|
|
17115
17268
|
} catch (e) {
|
|
17116
|
-
|
|
17269
|
+
log36.debug("trigger server unavailable", e);
|
|
17117
17270
|
}
|
|
17118
17271
|
}
|
|
17119
17272
|
/** Re-bind on /resume (the session id changed). */
|
|
@@ -17245,7 +17398,7 @@ var strike = C("9");
|
|
|
17245
17398
|
var link = (text, url) => useColor ? `\x1B]8;;${url}\x1B\\${cyan(text)}\x1B]8;;\x1B\\` : `${text} (${url})`;
|
|
17246
17399
|
var mdPalette = { bold, dim, cyan, italic, strike, green, link };
|
|
17247
17400
|
var err = (s) => process.stderr.write(s);
|
|
17248
|
-
var
|
|
17401
|
+
var log37 = forComponent("cli");
|
|
17249
17402
|
var VERSION = (() => {
|
|
17250
17403
|
try {
|
|
17251
17404
|
return JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8")).version ?? "?";
|
|
@@ -18088,7 +18241,7 @@ function turnCost(model, usage) {
|
|
|
18088
18241
|
const priced = model.startsWith("claude-code/") ? `anthropic/${resolveModelAlias(model.slice("claude-code/".length))}` : model;
|
|
18089
18242
|
return costOf(getModelInfo(priced)?.pricing, usage?.promptTokens ?? 0, usage?.completionTokens ?? 0, usage?.cacheCreationTokens ?? 0, usage?.cacheReadTokens ?? 0, priced);
|
|
18090
18243
|
}
|
|
18091
|
-
async function evaluateGoal(ai, condition, transcript,
|
|
18244
|
+
async function evaluateGoal(ai, condition, transcript, log38) {
|
|
18092
18245
|
const recent = transcript.filter((m) => m.role === "assistant").slice(-8).map((m) => {
|
|
18093
18246
|
const text = typeof m.content === "string" ? m.content : m.content.filter((p) => p.type === "text").map((p) => p.text).join(" ");
|
|
18094
18247
|
return text.slice(0, 600);
|
|
@@ -18108,7 +18261,7 @@ ${recent}` }
|
|
|
18108
18261
|
const match = r.content.match(/\{[\s\S]*\}/);
|
|
18109
18262
|
if (match) return JSON.parse(match[0]);
|
|
18110
18263
|
} catch (e) {
|
|
18111
|
-
|
|
18264
|
+
log38(dim(` (goal evaluator error: ${e?.message ?? e})
|
|
18112
18265
|
`));
|
|
18113
18266
|
}
|
|
18114
18267
|
return { met: false, reason: "evaluation unclear" };
|
|
@@ -18334,7 +18487,7 @@ function mcpAgentTools(mounted, opts) {
|
|
|
18334
18487
|
return tools;
|
|
18335
18488
|
}
|
|
18336
18489
|
async function closeMcp(mounted) {
|
|
18337
|
-
await Promise.all(mounted.map((m) => m.client.close().catch((e) =>
|
|
18490
|
+
await Promise.all(mounted.map((m) => m.client.close().catch((e) => log37.debug("mcp close failed", e))));
|
|
18338
18491
|
}
|
|
18339
18492
|
var IMG_EXT = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".webp": "image/webp" };
|
|
18340
18493
|
function mentionRefs(line) {
|
|
@@ -18393,7 +18546,7 @@ async function expandMentions(fs, line) {
|
|
|
18393
18546
|
if (loaded.includes(ref) || missing.includes(ref)) continue;
|
|
18394
18547
|
if (ref.includes(":") && mcpMentionResolver) {
|
|
18395
18548
|
const body = await mcpMentionResolver(ref).catch((e) => {
|
|
18396
|
-
|
|
18549
|
+
log37.debug("mcp mention resolve failed", e);
|
|
18397
18550
|
return null;
|
|
18398
18551
|
});
|
|
18399
18552
|
if (body != null) {
|
|
@@ -18484,7 +18637,7 @@ async function runTurn(agent, store, session, task, cp, cwd = process.cwd(), sen
|
|
|
18484
18637
|
try {
|
|
18485
18638
|
store.save(session);
|
|
18486
18639
|
} catch (ex) {
|
|
18487
|
-
|
|
18640
|
+
log37.debug("mid-turn session flush failed", ex);
|
|
18488
18641
|
}
|
|
18489
18642
|
}
|
|
18490
18643
|
return origNotify(e);
|
|
@@ -18648,14 +18801,14 @@ var isCancelTeardown = (e) => {
|
|
|
18648
18801
|
function installCancelGuards(mounted) {
|
|
18649
18802
|
process.on("unhandledRejection", (e) => {
|
|
18650
18803
|
if (isCancelTeardown(e)) {
|
|
18651
|
-
|
|
18804
|
+
log37.debug("suppressed unhandledRejection (cursor stream cancel)", e);
|
|
18652
18805
|
return;
|
|
18653
18806
|
}
|
|
18654
|
-
|
|
18807
|
+
log37.error("unhandledRejection", e);
|
|
18655
18808
|
});
|
|
18656
18809
|
process.on("uncaughtException", (e) => {
|
|
18657
18810
|
if (isCancelTeardown(e)) {
|
|
18658
|
-
|
|
18811
|
+
log37.debug("suppressed uncaughtException (cursor stream cancel)", e);
|
|
18659
18812
|
return;
|
|
18660
18813
|
}
|
|
18661
18814
|
console.error(e);
|
|
@@ -19082,7 +19235,7 @@ async function repl(args, ai, cfg, cwd) {
|
|
|
19082
19235
|
try {
|
|
19083
19236
|
mkdirSync12(dir, { recursive: true });
|
|
19084
19237
|
} catch (e) {
|
|
19085
|
-
|
|
19238
|
+
log37.debug("paste image dir create failed", e);
|
|
19086
19239
|
}
|
|
19087
19240
|
const img = grabClipboardImage(dir, String(Date.now()));
|
|
19088
19241
|
return img ? { display: "Image", ref: "@" + img.path, path: img.path } : null;
|
|
@@ -19195,7 +19348,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
19195
19348
|
mkdirSync12(join16(cwd, ".agent"), { recursive: true });
|
|
19196
19349
|
appendFileSync(histPath, line + "\n");
|
|
19197
19350
|
} catch (e) {
|
|
19198
|
-
|
|
19351
|
+
log37.debug("history write failed", e);
|
|
19199
19352
|
}
|
|
19200
19353
|
};
|
|
19201
19354
|
const ago = (t) => {
|
|
@@ -19267,7 +19420,7 @@ Added entries are loadable now via the Skill/SlashCommand tools; removed ones ar
|
|
|
19267
19420
|
try {
|
|
19268
19421
|
store.save(session);
|
|
19269
19422
|
} catch (e) {
|
|
19270
|
-
|
|
19423
|
+
log37.debug("session save after rewind failed", e);
|
|
19271
19424
|
}
|
|
19272
19425
|
err(green(" \u27F2 jumped back") + dim(` \u2014 ${face.transcript.length} message(s) kept; edit + resend
|
|
19273
19426
|
`));
|
|
@@ -19302,7 +19455,7 @@ ${task}`;
|
|
|
19302
19455
|
bangContext.length = 0;
|
|
19303
19456
|
}
|
|
19304
19457
|
const delta = await refreshCatalogs().catch((e) => {
|
|
19305
|
-
|
|
19458
|
+
log37.debug("catalog refresh failed", e);
|
|
19306
19459
|
return "";
|
|
19307
19460
|
});
|
|
19308
19461
|
if (delta) {
|
|
@@ -19565,7 +19718,7 @@ ${task}`;
|
|
|
19565
19718
|
desc: "rescan skills/commands dirs and rebuild the system prompt (one cache miss) \u2014 picks up entries created mid-session",
|
|
19566
19719
|
run: async () => {
|
|
19567
19720
|
await refreshCatalogs().catch((e) => {
|
|
19568
|
-
|
|
19721
|
+
log37.debug("catalog refresh failed", e);
|
|
19569
19722
|
});
|
|
19570
19723
|
face.reprepare();
|
|
19571
19724
|
err(green(` \u2713 reloaded \u2014 ${skills.length} skill(s), ${cmds.length} command(s); system prompt rebuilds on next message
|
|
@@ -20068,7 +20221,7 @@ ${task}`;
|
|
|
20068
20221
|
try {
|
|
20069
20222
|
for (const def of (await loadAgents(fs2, d)).agents) if (!seen.has(def.name)) seen.set(def.name, { def, from: d });
|
|
20070
20223
|
} catch (e) {
|
|
20071
|
-
|
|
20224
|
+
log37.debug(`loadAgents(${d}) failed`, e);
|
|
20072
20225
|
}
|
|
20073
20226
|
}
|
|
20074
20227
|
if (!seen.size) {
|
|
@@ -20156,7 +20309,7 @@ ${task}`;
|
|
|
20156
20309
|
}
|
|
20157
20310
|
if (idx >= 0) {
|
|
20158
20311
|
const old = mounted.splice(idx, 1)[0];
|
|
20159
|
-
await old.client.close().catch((e) =>
|
|
20312
|
+
await old.client.close().catch((e) => log37.debug("mcp close failed", e));
|
|
20160
20313
|
}
|
|
20161
20314
|
try {
|
|
20162
20315
|
const m = await mountMcpServer(name, conf);
|
|
@@ -20184,7 +20337,7 @@ ${task}`;
|
|
|
20184
20337
|
}
|
|
20185
20338
|
const m = mounted.splice(idx, 1)[0];
|
|
20186
20339
|
remountMcpTools();
|
|
20187
|
-
await m.client.close().catch((e) =>
|
|
20340
|
+
await m.client.close().catch((e) => log37.debug("mcp close failed", e));
|
|
20188
20341
|
err(dim(` removed "${name}"
|
|
20189
20342
|
`));
|
|
20190
20343
|
return;
|
|
@@ -20387,7 +20540,7 @@ ${task}`;
|
|
|
20387
20540
|
try {
|
|
20388
20541
|
return readdirSync4(join16(cwd, absDir.replace(/^\/+/, "")), { withFileTypes: true }).map((d) => ({ name: d.name, dir: d.isDirectory() }));
|
|
20389
20542
|
} catch (e) {
|
|
20390
|
-
|
|
20543
|
+
log37.debug("completion readdir failed", absDir, e);
|
|
20391
20544
|
return null;
|
|
20392
20545
|
}
|
|
20393
20546
|
};
|
|
@@ -20524,7 +20677,7 @@ ${out}
|
|
|
20524
20677
|
return;
|
|
20525
20678
|
}
|
|
20526
20679
|
await refreshCatalogs().catch((e) => {
|
|
20527
|
-
|
|
20680
|
+
log37.debug("catalog refresh failed", e);
|
|
20528
20681
|
});
|
|
20529
20682
|
const sk = skills.find((x) => x.name === name);
|
|
20530
20683
|
if (sk) {
|
|
@@ -20642,7 +20795,7 @@ ${out}
|
|
|
20642
20795
|
if (delivery || cut) dx?.parkCutDelivery(delivery, !!cut, heard);
|
|
20643
20796
|
const cutReply = cut && cut.full.length - cut.heard.length > 40 ? cut : null;
|
|
20644
20797
|
const noteP = replaces ? replaces.note : !cutReply ? Promise.resolve("") : (instinct ? instinct.bargeIntent(text).catch((e) => {
|
|
20645
|
-
|
|
20798
|
+
log37.warn(`barge intent failed \u2014 model decides: ${e.message}`);
|
|
20646
20799
|
return void 0;
|
|
20647
20800
|
}) : Promise.resolve(void 0)).then(async (intent) => interruptionNote(cutReply, intent) + ackedNote(await instinct?.ackFor(text)));
|
|
20648
20801
|
const cutIn = replaces ? replaces.cutIn : interjectNote;
|
|
@@ -21004,12 +21157,15 @@ function makeSecondChannel(ai, reflexModel, opts) {
|
|
|
21004
21157
|
const decider = leakDecider();
|
|
21005
21158
|
const instinct = decider ? new Instinct({ decider }) : void 0;
|
|
21006
21159
|
const model = process.env.AGENTX_CHANNEL_MODEL || reflexModel;
|
|
21160
|
+
const planner = new BackchannelPlanner(ai, { model });
|
|
21007
21161
|
return new SecondChannel({
|
|
21008
21162
|
// Banks in the user's language: generated once per language, cached + topped up across sessions (~/.agent/cache).
|
|
21009
21163
|
banks: new ChannelBanks({ generate: fastBankGenerator(ai, model), cacheDir: join16(homedir10(), ".agent", "cache", "channel-banks") }),
|
|
21010
21164
|
decide: instinct ? (heard, state) => instinct.channel(heard, state) : void 0,
|
|
21011
21165
|
generate: fastLineGenerator(ai, model),
|
|
21012
|
-
|
|
21166
|
+
// Anticipated backchannels (off until the offline replay proves them — tests/voice-bc-anticipate-eval.ts)
|
|
21167
|
+
...process.env.AGENTX_CHANNEL_ANTICIPATE === "1" ? { anticipate: true, plan: (c, s) => planner.plan(c, s) } : {},
|
|
21168
|
+
onEvent: (e) => log37.debug(`channel ${e.trigger}: rule=${e.rule} choice=${e.choice ?? "-"} \u2192 ${e.acted}${e.text ? ` "${e.text}"` : ""}${e.cue ? " (planned cue)" : ""} ${e.ms}ms`),
|
|
21013
21169
|
...opts
|
|
21014
21170
|
});
|
|
21015
21171
|
}
|
|
@@ -21071,7 +21227,7 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21071
21227
|
try {
|
|
21072
21228
|
store.save(session);
|
|
21073
21229
|
} catch (e) {
|
|
21074
|
-
|
|
21230
|
+
log37.warn("duplex session save failed", e);
|
|
21075
21231
|
}
|
|
21076
21232
|
};
|
|
21077
21233
|
out({ type: "ready", sessionId: session.meta.id, reflexModel: dx.options.reflexModel, actModel: dx.options.actModel, ...notes.length ? { notes } : {} });
|
|
@@ -21109,7 +21265,7 @@ async function duplexServe(args, ai, cfg, cwd) {
|
|
|
21109
21265
|
done2();
|
|
21110
21266
|
out({ type: "turn_done", id, finishReason: res?.finishReason ?? "error", ...why ? { error: why } : {} });
|
|
21111
21267
|
}).catch((e) => {
|
|
21112
|
-
|
|
21268
|
+
log37.error("duplex turn failed", e);
|
|
21113
21269
|
done2();
|
|
21114
21270
|
out({ type: "turn_done", id, finishReason: "error", error: String(e?.message ?? e) });
|
|
21115
21271
|
});
|