@livx.cc/agentx 0.99.100 → 0.99.101

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 CHANGED
@@ -3,6 +3,9 @@ import { m as RouterState, H as Hooks, n as RunResult, R as ReasoningEffort, A a
3
3
  import { IFilesystem } from '@livx.cc/wcli/core';
4
4
  import { M as Message, U as UserQuestion, H as HostBridge, c as ContentPart, o as MessageContent } from './tools-lC3jn60o.js';
5
5
 
6
+ /** How talkative the second channel is (`--channel-mode`). */
7
+ type ChannelMode = 'turn' | 'normal' | 'chatty';
8
+
6
9
  /**
7
10
  * On-disk session store for the CLI: each conversation is one JSON file at
8
11
  * `<cwd>/.agent/sessions/<id>.json`. Writes are atomic (temp + rename) so a crash
@@ -142,6 +145,7 @@ interface Args {
142
145
  duplex: boolean;
143
146
  duplexV2?: boolean;
144
147
  channelOnly?: boolean;
148
+ channelMode?: ChannelMode;
145
149
  voiceModel?: string;
146
150
  thinkModel?: string | false;
147
151
  voice: boolean;
package/dist/cli.js CHANGED
@@ -14071,7 +14071,34 @@ Reply JSON only: {"turn":"...","density":"...","cues":[{"when":["phrase",...],"l
14071
14071
 
14072
14072
  // src/voice/secondChannel.ts
14073
14073
  var log31 = forComponent("SecondChannel");
14074
+ var CHANNEL_MODES = {
14075
+ turn: { kinds: ["bridge"], bridgeDelayMs: 1500 },
14076
+ normal: {},
14077
+ chatty: {
14078
+ anticipate: true,
14079
+ cueOnly: false,
14080
+ generateFor: ["backchannel", "bridge"],
14081
+ reactionGapMs: 6e3,
14082
+ generateBudgetMs: 400,
14083
+ backchannelMinWords: 5,
14084
+ backchannelGapMs: 1200,
14085
+ minGapMs: 1200,
14086
+ backchannelPauseMs: 250
14087
+ }
14088
+ };
14089
+ var CHANNEL_MODE_NAMES = Object.keys(CHANNEL_MODES);
14090
+ function parseChannelMode(v) {
14091
+ if (!v) return "normal";
14092
+ if (!(v in CHANNEL_MODES)) throw new Error(`unknown channel mode "${v}" (${CHANNEL_MODE_NAMES.join("|")})`);
14093
+ return v;
14094
+ }
14074
14095
  var SecondChannelOptions = class {
14096
+ /** Preset (CHANNEL_MODES) under the explicit options. */
14097
+ mode = "normal";
14098
+ /** Line kinds this channel may speak. */
14099
+ kinds = ["backchannel", "filler", "bridge"];
14100
+ /** A generated (content) backchannel needs this long since the last one; in between, canned light cues. 0 = always. */
14101
+ reactionGapMs = 0;
14075
14102
  /** Refiner (Jev/fast LLM): the choice over CHANNEL_ACTIONS, or null on failure. Absent → the rule alone decides.
14076
14103
  * `state.rule` is the kind the rule proposed (fastChannelDecider answers yes/no on it). `signal` aborts a late call. */
14077
14104
  decide;
@@ -14166,7 +14193,10 @@ function withBudget(p, ms, what, ac) {
14166
14193
  var SecondChannel = class {
14167
14194
  constructor(options) {
14168
14195
  this.options = options;
14169
- this.options = { ...new SecondChannelOptions(), ...options };
14196
+ const preset = CHANNEL_MODES[options?.mode ?? "normal"] ?? {};
14197
+ const base = new SecondChannelOptions();
14198
+ this.options = { ...base, ...preset, ...options };
14199
+ for (const k of Object.keys(preset)) if (!options || !(k in options)) this.normalTiming[k] = base[k];
14170
14200
  this.startedAt = this.o.now();
14171
14201
  if (this.o.lang) this.setLanguage(this.o.lang, true);
14172
14202
  }
@@ -14213,6 +14243,10 @@ var SecondChannel = class {
14213
14243
  reply = "";
14214
14244
  // channel-1's text this turn (the plan's context)
14215
14245
  lastUser = "";
14246
+ lastReactAt = -Infinity;
14247
+ normalTiming = {};
14248
+ // chatty without aec: the preset's keys back at their defaults
14249
+ degradedLogged = false;
14216
14250
  /** The language lines are spoken in (detected from the user's words, or set by the host). */
14217
14251
  get language() {
14218
14252
  return this.lang;
@@ -14241,7 +14275,13 @@ var SecondChannel = class {
14241
14275
  return this.lang ? this.o.banks ? this.o.banks.get(this.lang) : this.lang === "en" ? EN_BANK : null : null;
14242
14276
  }
14243
14277
  get o() {
14244
- return this.options;
14278
+ const o = this.options;
14279
+ if (o.mode !== "chatty" || o.aec) return o;
14280
+ if (!this.degradedLogged) {
14281
+ this.degradedLogged = true;
14282
+ log31.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
14283
+ }
14284
+ return { ...o, ...this.normalTiming };
14245
14285
  }
14246
14286
  /** The user's live partial for the current turn (the whole partial, not a delta). */
14247
14287
  onInterim(text) {
@@ -14377,6 +14417,7 @@ var SecondChannel = class {
14377
14417
  }
14378
14418
  /** Hard gates every line passes, whoever proposed it. */
14379
14419
  allowed(k) {
14420
+ if (!this.o.kinds.includes(k)) return false;
14380
14421
  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;
14381
14422
  if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle";
14382
14423
  if (k === "filler") return !this.userSpeaking && this.main === "idle";
@@ -14443,12 +14484,14 @@ var SecondChannel = class {
14443
14484
  if (kind === "silence") return ev({ choice, acted: "silence" });
14444
14485
  if (stale()) return ev({ choice, kind, acted: "stale" });
14445
14486
  if (!this.allowed(kind)) return ev({ choice, kind, acted: "gated" });
14446
- const text = cue && !this.recent.some((r) => norm2(r) === norm2(cue.line)) ? cue.line : await this.line(kind);
14487
+ const react = kind === "backchannel" && this.o.now() - this.lastReactAt >= this.o.reactionGapMs;
14488
+ const text = cue && react && !this.recent.some((r) => norm2(r) === norm2(cue.line)) ? cue.line : await this.line(kind, react);
14447
14489
  if (!text) return ev({ choice, kind, acted: "no_bank" });
14448
14490
  if (stale()) return ev({ choice, kind, text, acted: "stale" });
14449
14491
  if (!this.allowed(kind)) return ev({ choice, kind, text, acted: "gated" });
14450
14492
  const said = this.say(kind, text);
14451
14493
  if (said && cue) this.cueFrom = words2(this.heard).length;
14494
+ if (said && kind === "backchannel" && this.o.reactionGapMs && !this.bank?.backchannel.some((b) => norm2(b) === norm2(text))) this.lastReactAt = this.o.now();
14452
14495
  ev({ choice, kind, text, acted: said ? "spoken" : "skipped" });
14453
14496
  } finally {
14454
14497
  this.busy = false;
@@ -14465,10 +14508,10 @@ var SecondChannel = class {
14465
14508
  call_ms: Math.round(now4 - this.startedAt)
14466
14509
  };
14467
14510
  }
14468
- async line(kind) {
14469
- if (this.o.generate && this.o.generateFor.includes(kind)) {
14511
+ async line(kind, react = true) {
14512
+ if (this.o.generate && this.o.generateFor.includes(kind) && (kind !== "backchannel" || react)) {
14470
14513
  const ac = new AbortController();
14471
- const ctx = { kind, heard: this.tail(), state: this.main, avoid: [...this.recent], lang: this.lang };
14514
+ const ctx = { kind, heard: this.tail(), state: this.main, avoid: [...this.recent], lang: this.lang, ...kind === "backchannel" && this.o.reactionGapMs ? { react: true } : {} };
14472
14515
  const raw = await withBudget(this.o.generate(ctx, ac.signal), this.o.generateBudgetMs, `${kind} generate`);
14473
14516
  if (raw === null) ac.abort();
14474
14517
  const text = cleanLine(raw ?? "", this.lang);
@@ -14543,10 +14586,12 @@ var KIND_HINT2 = {
14543
14586
  filler: 'a soft listener sound while THEY search for words ("mm\u2026", "mhm", "yeah\u2026") \u2014 never a word that continues their sentence or starts yours',
14544
14587
  bridge: 'a neutral one-moment cue while the answer is prepared ("let me see\u2026", "one sec\u2026") \u2014 never the start of a statement or opinion ("the thing is", "well actually")'
14545
14588
  };
14589
+ var REACT_HINT = 'a short reaction to their LAST words instead: feeling ("oh no", "ha, nice", "ugh"), surprise ("wait, really?", "huh"), a light laugh if they joke or laugh ("ha!"), or an echo of their key word ("the deploy?") \u2014 still never asks for an answer or takes the turn; if nothing there deserves a reaction, a plain "mm-hm"';
14546
14590
  function fastLineGenerator(ai, model) {
14547
14591
  return async (c, signal) => {
14548
14592
  const prompt = `You voice the tiny side-sounds of a live spoken conversation. Write ONE line of 1-5 words: ${KIND_HINT2[c.kind]}.
14549
- The user just said: "${c.heard}"
14593
+ ` + (c.react ? `Here the line may be ${REACT_HINT}.
14594
+ ` : "") + `The user just said: "${c.heard}"
14550
14595
  The assistant is: ${c.state}.
14551
14596
  ` + (c.avoid.length ? `Do not reuse any of: ${c.avoid.map((a) => `"${a}"`).join(", ")}.
14552
14597
  ` : "") + `React to their actual words, casually, in ${languageName(c.lang)}. Output only the line.`;
@@ -17687,6 +17732,7 @@ function parseArgs(argv) {
17687
17732
  a.duplex = true;
17688
17733
  a.duplexV2 = true;
17689
17734
  } else if (x === "--channel-only") a.channelOnly = true;
17735
+ else if (x === "--channel-mode") a.channelMode = parseChannelMode(val(++i, x));
17690
17736
  else if (x === "--conversational" || x === "--convo" || x === "--voice") {
17691
17737
  a.voice = true;
17692
17738
  a.duplex = true;
@@ -17730,6 +17776,8 @@ function parseArgs(argv) {
17730
17776
  if (a.channelOnly && !a.inputFormat) throw new Error("--channel-only needs --input-format stream-json --output-format stream-json");
17731
17777
  if (a.duplex && a.plan) throw new Error("--plan is not supported in --duplex (workers are non-interactive; a plan could never be approved)");
17732
17778
  if (a.voiceSystemPrompt && !a.duplex) throw new Error("--voice-system-prompt only applies with --duplex");
17779
+ if (a.channelMode && !a.duplexV2 && !a.channelOnly) throw new Error("--channel-mode only applies with --duplex-v2 or --channel-only");
17780
+ a.channelMode ??= parseChannelMode(process.env.AGENTX_CHANNEL_MODE);
17733
17781
  if (a.voiceModel && !a.duplex && !a.channelOnly) throw new Error("--voice-model only applies with --duplex or --channel-only");
17734
17782
  if (a.thinkModel !== void 0 && !a.duplex) throw new Error("--think-model/--no-think only apply with --duplex");
17735
17783
  if (!canPrompt && (a.ask || a.plan))
@@ -17776,6 +17824,9 @@ Flags:
17776
17824
  enter the agent's history (headless: stdin interim/pause/agent_state, stdout channel)
17777
17825
  --channel-only headless (--input-format stream-json): ONLY the second channel, for a host whose main
17778
17826
  answer comes from elsewhere (stdin user/text/barge/interim/pause/agent_state, stdout channel)
17827
+ --channel-mode <m> second-channel talkativeness: turn (only a bridge when the answer is slow) | normal
17828
+ (default) | chatty (frequent cues + content reactions; needs echo cancellation)
17829
+ env fallback: AGENTX_CHANNEL_MODE
17779
17830
  --conversational duplex with a conversation-native register \u2014 short fast turns, fillers,
17780
17831
  impulsive reactions, human pacing (implies --duplex; aliases: --convo, --voice)
17781
17832
  with SONIOX_API_KEY + CARTESIA_API_KEY(+VOICE_ID) set: real voice I/O \u2014 mic in,
@@ -21040,7 +21091,7 @@ ${out}
21040
21091
  if (dx) dx.options.holdResumeMs = HOLD_RESUME_MS;
21041
21092
  if (args.duplexV2 && dx) {
21042
21093
  const v = voiceIO;
21043
- bindSecondChannel(v, makeSecondChannel(ai, dx.options.reflexModel, { show: (t) => {
21094
+ bindSecondChannel(v, makeSecondChannel(ai, dx.options.reflexModel, args.channelMode, { show: (t) => {
21044
21095
  err("\r\x1B[0J" + dim(` \u21B3 ${t}
21045
21096
  `));
21046
21097
  editorRef?.redrawNow();
@@ -21297,7 +21348,7 @@ function duplexError(e, reflexModel) {
21297
21348
  }
21298
21349
  var HOLD_RESUME_MS = 4e3;
21299
21350
  var leakDecider = () => voiceDecider();
21300
- function makeSecondChannel(ai, reflexModel, opts) {
21351
+ function makeSecondChannel(ai, reflexModel, mode = "normal", opts) {
21301
21352
  const decider = leakDecider();
21302
21353
  const instinct = decider ? new Instinct({ decider }) : void 0;
21303
21354
  const model = process.env.AGENTX_CHANNEL_MODEL || reflexModel;
@@ -21307,8 +21358,10 @@ function makeSecondChannel(ai, reflexModel, opts) {
21307
21358
  banks: new ChannelBanks({ generate: fastBankGenerator(ai, model), cacheDir: join16(homedir10(), ".agent", "cache", "channel-banks") }),
21308
21359
  decide: channelDecide(ai, instinct),
21309
21360
  generate: fastLineGenerator(ai, model),
21310
- // Anticipated backchannels (off until the offline replay proves them — tests/voice-bc-anticipate-eval.ts)
21311
- ...process.env.AGENTX_CHANNEL_ANTICIPATE === "1" ? { anticipate: true, plan: (c, s) => planner.plan(c, s) } : {},
21361
+ mode,
21362
+ // Anticipated backchannels: on in chatty mode (its preset), else off until the offline replay proves them (tests/voice-bc-anticipate-eval.ts)
21363
+ plan: (c, s) => planner.plan(c, s),
21364
+ ...process.env.AGENTX_CHANNEL_ANTICIPATE === "1" ? { anticipate: true } : {},
21312
21365
  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`),
21313
21366
  ...opts
21314
21367
  });
@@ -21362,7 +21415,7 @@ async function duplexServe(args, ai, cfg, cwd) {
21362
21415
  });
21363
21416
  if (args.voiceSystemPrompt) dx.voice.options.systemPrompt += "\n\n" + args.voiceSystemPrompt;
21364
21417
  let scTurn = 0;
21365
- const sc = args.duplexV2 ? makeSecondChannel(ai, dx.options.reflexModel, { show: (text, kind) => out({ type: "channel", kind, text }) }) : void 0;
21418
+ const sc = args.duplexV2 ? makeSecondChannel(ai, dx.options.reflexModel, args.channelMode, { show: (text, kind) => out({ type: "channel", kind, text }) }) : void 0;
21366
21419
  const store = new SessionStore(cwd);
21367
21420
  const session = startSession(args, store, dx.voice, cwd);
21368
21421
  persist = () => {
@@ -21427,8 +21480,8 @@ async function channelServe(args, ai, cfg) {
21427
21480
  const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
21428
21481
  const { reflexModel, notes } = duplexModels({ voiceModel: args.voiceModel, reflexModel: cfg.reflexModel, thinkModel: false, actModel: String(args.model ?? cfg.model ?? ""), apiKeys: { ...cfg.apiKeys, ...apiKeysFromEnv() } });
21429
21482
  const model = reflexModel ?? new DuplexAgentOptions().reflexModel;
21430
- const sc = makeSecondChannel(ai, model, { show: (text, kind) => out({ type: "channel", kind, text }) });
21431
- out({ type: "ready", model, ...notes.length ? { notes } : {} });
21483
+ const sc = makeSecondChannel(ai, model, args.channelMode, { show: (text, kind) => out({ type: "channel", kind, text }) });
21484
+ out({ type: "ready", model, channelMode: args.channelMode, ...notes.length ? { notes } : {} });
21432
21485
  const rl = createInterface({ input: process.stdin });
21433
21486
  rl.on("line", (line) => {
21434
21487
  let m;