@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 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 log30 = forComponent("SecondChannel");
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
- log30.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
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
- log30.warn(`${what} failed \u2014 falling back: ${e?.message ?? e}`);
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
- log30.debug(`language ${this.lang} \u2192 ${lang}`);
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
- log30.warn(`cancel threw: ${e.message}`);
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
- const rule = this.rule(trigger, pauseMs, breath);
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) log30.debug(`decider chose ${choice} for a ${rule} moment \u2014 the rule stands`);
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
- ev({ choice, kind, text, acted: this.say(kind, text) ? "spoken" : "skipped" });
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) log30.debug(`generated "${text}" repeats a recent line \u2014 canned`);
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
- log30.warn(`speak threw \u2014 line dropped: ${e.message}`);
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
- log30.warn(`channel line playback failed: ${e?.message ?? e}`);
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 log31 = forComponent("hooks");
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
- log31.debug(`hook command failed: ${rule.command}`, e);
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 log32 = forComponent("session");
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
- log32.debug(`rejecting unsafe session id: ${id}`);
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
- log32.debug(`unreadable session ${id} \u2014 ignoring`, e);
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
- log32.debug(`skipping unreadable session file ${f}`, e);
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 log33 = forComponent("checkpoints");
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
- log33.debug(`git checkpoints unavailable for ${this.workTree}`, e);
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) => log33.debug(`force-add failed: ${p}`, 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) => log33.debug(`force-capture failed: ${p}`, e));
14924
- await this.run("commit", "--amend", "--no-edit", "-q", "--allow-empty").catch((e) => log33.debug("amend failed", 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
- log33.debug("checkpoint prune failed", e);
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) => log33.debug("re-point failed", 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
- log33.debug("checkpoint commit failed", e);
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) => log33.debug("amendForced failed", 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 log34 = forComponent("os-sched");
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
- log34.debug(`cancel ${id}`, e);
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 log35 = forComponent("remote-trigger");
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) => log35.debug("trigger conn error", e));
17263
+ conn.on("error", (e) => log36.debug("trigger conn error", e));
17111
17264
  });
17112
- this.server.on("error", (e) => log35.debug("trigger server 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
- log35.debug("trigger server unavailable", e);
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 log36 = forComponent("cli");
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, log37) {
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
- log37(dim(` (goal evaluator error: ${e?.message ?? e})
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) => log36.debug("mcp close failed", 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
- log36.debug("mcp mention resolve failed", e);
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
- log36.debug("mid-turn session flush failed", ex);
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
- log36.debug("suppressed unhandledRejection (cursor stream cancel)", e);
18804
+ log37.debug("suppressed unhandledRejection (cursor stream cancel)", e);
18652
18805
  return;
18653
18806
  }
18654
- log36.error("unhandledRejection", e);
18807
+ log37.error("unhandledRejection", e);
18655
18808
  });
18656
18809
  process.on("uncaughtException", (e) => {
18657
18810
  if (isCancelTeardown(e)) {
18658
- log36.debug("suppressed uncaughtException (cursor stream cancel)", e);
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
- log36.debug("paste image dir create failed", e);
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
- log36.debug("history write failed", e);
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
- log36.debug("session save after rewind failed", e);
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
- log36.debug("catalog refresh failed", e);
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
- log36.debug("catalog refresh failed", e);
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
- log36.debug(`loadAgents(${d}) failed`, e);
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) => log36.debug("mcp close failed", 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) => log36.debug("mcp close failed", 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
- log36.debug("completion readdir failed", absDir, e);
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
- log36.debug("catalog refresh failed", e);
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
- log36.warn(`barge intent failed \u2014 model decides: ${e.message}`);
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
- onEvent: (e) => log36.debug(`channel ${e.trigger}: rule=${e.rule} choice=${e.choice ?? "-"} \u2192 ${e.acted}${e.text ? ` "${e.text}"` : ""} ${e.ms}ms`),
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
- log36.warn("duplex session save failed", e);
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
- log36.error("duplex turn failed", e);
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
  });