@livx.cc/agentx 0.99.99 → 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
@@ -3432,7 +3432,7 @@ function copyTextToClipboard(text, platform2 = process.platform) {
3432
3432
 
3433
3433
  // cli/cli.ts
3434
3434
  import { join as join16, resolve as resolve5, basename as basename4, extname, dirname as dirname6 } from "path";
3435
- import { AIClient, DecideClient, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported, disposeCursorSessions, disposeClaudeCodeSessions } from "ai.libx.js";
3435
+ import { AIClient as AIClient2, listModels, listProviders, getProviderFromModel, getModelInfo, resolveModel, isModelSupported, disposeCursorSessions, disposeClaudeCodeSessions } from "ai.libx.js";
3436
3436
 
3437
3437
  // src/ccBridge.ts
3438
3438
  var CC_BRIDGED = /* @__PURE__ */ new Set(["Recall", "Remember", "MemorySearch"]);
@@ -9251,12 +9251,20 @@ configureLogging((name) => {
9251
9251
 
9252
9252
  // src/voice/instinct.ts
9253
9253
  init_logging();
9254
+ import { AIClient, DecideClient, JevAdapter, LlmDecideAdapter } from "ai.libx.js";
9254
9255
  var log19 = forComponent("Instinct");
9255
9256
  function voiceDeciderOptions(env = process.env) {
9256
9257
  if (env.TYPESAFE_AI_KEY) return { apiKey: env.TYPESAFE_AI_KEY };
9257
9258
  if (env.GROQ_API_KEY) return { provider: "llm", model: "groq/openai/gpt-oss-120b", apiKey: env.GROQ_API_KEY };
9258
9259
  return void 0;
9259
9260
  }
9261
+ function voiceDecider(env = process.env, timeoutMs = 1500) {
9262
+ const o = voiceDeciderOptions(env);
9263
+ if (!o) return void 0;
9264
+ const ai = new AIClient();
9265
+ const llm = new LlmDecideAdapter({ client: { chat: (c) => ai.chat({ reasoningEffort: "low", ...c }) } });
9266
+ return new DecideClient({ ...o, timeoutMs, adapters: { typesafe: new JevAdapter(), llm } });
9267
+ }
9260
9268
  var InstinctOptions = class {
9261
9269
  decider;
9262
9270
  /** Partial unchanged this long (a micro-pause) → consult. */
@@ -14063,9 +14071,36 @@ Reply JSON only: {"turn":"...","density":"...","cues":[{"when":["phrase",...],"l
14063
14071
 
14064
14072
  // src/voice/secondChannel.ts
14065
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
+ }
14066
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;
14067
14102
  /** Refiner (Jev/fast LLM): the choice over CHANNEL_ACTIONS, or null on failure. Absent → the rule alone decides.
14068
- * `state.rule` is the kind the rule proposed (fastBackchannelDecider answers only 'backchannel' moments). */
14103
+ * `state.rule` is the kind the rule proposed (fastChannelDecider answers yes/no on it). `signal` aborts a late call. */
14069
14104
  decide;
14070
14105
  /** Fast-model line writer (see fastLineGenerator). Absent or late → the canned bank. */
14071
14106
  generate;
@@ -14141,11 +14176,12 @@ var SecondChannelOptions = class {
14141
14176
  random = Math.random;
14142
14177
  };
14143
14178
  var words2 = (s) => s.trim() ? s.trim().split(/\s+/) : [];
14144
- function withBudget(p, ms, what) {
14179
+ function withBudget(p, ms, what, ac) {
14145
14180
  let t;
14146
14181
  const late = new Promise((r) => {
14147
14182
  t = setTimeout(() => {
14148
14183
  log31.debug(`${what}: over the ${ms}ms budget \u2014 dropped`);
14184
+ ac?.abort();
14149
14185
  r(null);
14150
14186
  }, ms);
14151
14187
  });
@@ -14157,7 +14193,10 @@ function withBudget(p, ms, what) {
14157
14193
  var SecondChannel = class {
14158
14194
  constructor(options) {
14159
14195
  this.options = options;
14160
- 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];
14161
14200
  this.startedAt = this.o.now();
14162
14201
  if (this.o.lang) this.setLanguage(this.o.lang, true);
14163
14202
  }
@@ -14204,6 +14243,10 @@ var SecondChannel = class {
14204
14243
  reply = "";
14205
14244
  // channel-1's text this turn (the plan's context)
14206
14245
  lastUser = "";
14246
+ lastReactAt = -Infinity;
14247
+ normalTiming = {};
14248
+ // chatty without aec: the preset's keys back at their defaults
14249
+ degradedLogged = false;
14207
14250
  /** The language lines are spoken in (detected from the user's words, or set by the host). */
14208
14251
  get language() {
14209
14252
  return this.lang;
@@ -14232,7 +14275,13 @@ var SecondChannel = class {
14232
14275
  return this.lang ? this.o.banks ? this.o.banks.get(this.lang) : this.lang === "en" ? EN_BANK : null : null;
14233
14276
  }
14234
14277
  get o() {
14235
- 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 };
14236
14285
  }
14237
14286
  /** The user's live partial for the current turn (the whole partial, not a delta). */
14238
14287
  onInterim(text) {
@@ -14368,6 +14417,7 @@ var SecondChannel = class {
14368
14417
  }
14369
14418
  /** Hard gates every line passes, whoever proposed it. */
14370
14419
  allowed(k) {
14420
+ if (!this.o.kinds.includes(k)) return false;
14371
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;
14372
14422
  if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle";
14373
14423
  if (k === "filler") return !this.userSpeaking && this.main === "idle";
@@ -14425,7 +14475,8 @@ var SecondChannel = class {
14425
14475
  let kind = rule;
14426
14476
  let choice;
14427
14477
  if (this.o.decide && !cue) {
14428
- const d = await withBudget(this.o.decide(this.tail(), { ...this.state(pauseMs), rule }), this.o.decisionBudgetMs, "channel decision");
14478
+ const ac = new AbortController();
14479
+ const d = await withBudget(this.o.decide(this.tail(), { ...this.state(pauseMs), rule }, ac.signal), this.o.decisionBudgetMs, "channel decision", ac);
14429
14480
  choice = d?.choice;
14430
14481
  if (choice && choice in CHANNEL_ACTIONS && (choice === "silence" || this.allowed(choice))) kind = choice;
14431
14482
  else if (choice && choice in CHANNEL_ACTIONS) log31.debug(`decider chose ${choice} for a ${rule} moment \u2014 the rule stands`);
@@ -14433,12 +14484,14 @@ var SecondChannel = class {
14433
14484
  if (kind === "silence") return ev({ choice, acted: "silence" });
14434
14485
  if (stale()) return ev({ choice, kind, acted: "stale" });
14435
14486
  if (!this.allowed(kind)) return ev({ choice, kind, acted: "gated" });
14436
- 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);
14437
14489
  if (!text) return ev({ choice, kind, acted: "no_bank" });
14438
14490
  if (stale()) return ev({ choice, kind, text, acted: "stale" });
14439
14491
  if (!this.allowed(kind)) return ev({ choice, kind, text, acted: "gated" });
14440
14492
  const said = this.say(kind, text);
14441
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();
14442
14495
  ev({ choice, kind, text, acted: said ? "spoken" : "skipped" });
14443
14496
  } finally {
14444
14497
  this.busy = false;
@@ -14455,10 +14508,10 @@ var SecondChannel = class {
14455
14508
  call_ms: Math.round(now4 - this.startedAt)
14456
14509
  };
14457
14510
  }
14458
- async line(kind) {
14459
- 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)) {
14460
14513
  const ac = new AbortController();
14461
- 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 } : {} };
14462
14515
  const raw = await withBudget(this.o.generate(ctx, ac.signal), this.o.generateBudgetMs, `${kind} generate`);
14463
14516
  if (raw === null) ac.abort();
14464
14517
  const text = cleanLine(raw ?? "", this.lang);
@@ -14505,32 +14558,40 @@ var SecondChannel = class {
14505
14558
  return true;
14506
14559
  }
14507
14560
  };
14508
- function fastBackchannelDecider(ai, model = "groq/qwen/qwen3.8-27b") {
14509
- return async (heard, state) => {
14510
- if (state.rule !== "backchannel") return null;
14511
- const prompt = `Live voice call. The user is still talking and just took a breath. So far: "${heard}"
14512
- Would an attentive human listener say a tiny "mm-hm" now? Yes ONLY if they are telling a story/explaining across several clauses. No if it is a request, command, question, or they are mid-request. Answer yes or no.`;
14561
+ var FAST_ASK = {
14562
+ backchannel: 'The user is still talking and just took a breath. Would an attentive human listener say a tiny "mm-hm" now? Yes ONLY if they are telling a story/explaining across several clauses. No if it is a request, command, question, or they are mid-request.',
14563
+ filler: 'The user went quiet mid-turn. Would a human listener make a soft "hmm\u2026" now? Yes ONLY if their sentence is unfinished and they are searching for words. No if they said a complete request, question, answer, thanks or remark (the reply comes next).',
14564
+ bridge: `The user finished and the assistant's answer is taking a moment. Would a human say a short "let me check\u2014" now? Yes if they asked for real work or a substantial/hard question. No for small talk, a greeting, thanks, an ok/yes/agreement.`
14565
+ };
14566
+ function fastChannelDecider(ai, model = "groq/qwen/qwen3.8-27b") {
14567
+ return async (heard, state, signal) => {
14568
+ const kind = state.rule;
14569
+ if (!(kind in FAST_ASK)) return null;
14570
+ const prompt = `Live voice call. ${FAST_ASK[kind]}
14571
+ So far the user said: "${heard}"
14572
+ Answer yes or no.`;
14513
14573
  const reasoning = /qwen/.test(model) ? { providerOptions: { reasoning_effort: "none" } } : /gpt-oss/.test(model) ? { providerOptions: { reasoning_effort: "low" } } : {};
14514
- const r = await ai.chat({ model, messages: [{ role: "user", content: prompt }], stream: false, maxTokens: /gpt-oss/.test(model) ? 200 : 3, ...reasoning });
14574
+ const r = await ai.chat({ model, messages: [{ role: "user", content: prompt }], stream: false, signal, temperature: 0, maxTokens: /gpt-oss/.test(model) ? 200 : 3, ...reasoning });
14515
14575
  const a = (r.content ?? "").trim().toLowerCase();
14516
- return /^yes/.test(a) ? { choice: "backchannel" } : /^no/.test(a) ? { choice: "silence" } : null;
14576
+ return /^yes/.test(a) ? { choice: kind } : /^no/.test(a) ? { choice: "silence" } : null;
14517
14577
  };
14518
14578
  }
14519
14579
  function channelDecide(ai, instinct, env = process.env) {
14520
14580
  const model = env.AGENTX_CHANNEL_DECIDER_MODEL || void 0;
14521
- const bc = model || env.GROQ_API_KEY ? fastBackchannelDecider(ai, model) : void 0;
14522
- if (!bc && !instinct) return void 0;
14523
- return async (heard, state) => bc && state.rule === "backchannel" ? bc(heard, state) : instinct ? instinct.channel(heard, state) : null;
14581
+ if (model || env.GROQ_API_KEY) return fastChannelDecider(ai, model);
14582
+ return instinct ? (heard, state) => instinct.channel(heard, state) : void 0;
14524
14583
  }
14525
14584
  var KIND_HINT2 = {
14526
14585
  backchannel: 'a tiny listener cue while they keep talking ("mm-hm", "right", "yeah") \u2014 shows you follow, never takes the turn',
14527
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',
14528
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")'
14529
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"';
14530
14590
  function fastLineGenerator(ai, model) {
14531
14591
  return async (c, signal) => {
14532
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]}.
14533
- The user just said: "${c.heard}"
14593
+ ` + (c.react ? `Here the line may be ${REACT_HINT}.
14594
+ ` : "") + `The user just said: "${c.heard}"
14534
14595
  The assistant is: ${c.state}.
14535
14596
  ` + (c.avoid.length ? `Do not reuse any of: ${c.avoid.map((a) => `"${a}"`).join(", ")}.
14536
14597
  ` : "") + `React to their actual words, casually, in ${languageName(c.lang)}. Output only the line.`;
@@ -17671,6 +17732,7 @@ function parseArgs(argv) {
17671
17732
  a.duplex = true;
17672
17733
  a.duplexV2 = true;
17673
17734
  } else if (x === "--channel-only") a.channelOnly = true;
17735
+ else if (x === "--channel-mode") a.channelMode = parseChannelMode(val(++i, x));
17674
17736
  else if (x === "--conversational" || x === "--convo" || x === "--voice") {
17675
17737
  a.voice = true;
17676
17738
  a.duplex = true;
@@ -17714,6 +17776,8 @@ function parseArgs(argv) {
17714
17776
  if (a.channelOnly && !a.inputFormat) throw new Error("--channel-only needs --input-format stream-json --output-format stream-json");
17715
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)");
17716
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);
17717
17781
  if (a.voiceModel && !a.duplex && !a.channelOnly) throw new Error("--voice-model only applies with --duplex or --channel-only");
17718
17782
  if (a.thinkModel !== void 0 && !a.duplex) throw new Error("--think-model/--no-think only apply with --duplex");
17719
17783
  if (!canPrompt && (a.ask || a.plan))
@@ -17760,6 +17824,9 @@ Flags:
17760
17824
  enter the agent's history (headless: stdin interim/pause/agent_state, stdout channel)
17761
17825
  --channel-only headless (--input-format stream-json): ONLY the second channel, for a host whose main
17762
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
17763
17830
  --conversational duplex with a conversation-native register \u2014 short fast turns, fillers,
17764
17831
  impulsive reactions, human pacing (implies --duplex; aliases: --convo, --voice)
17765
17832
  with SONIOX_API_KEY + CARTESIA_API_KEY(+VOICE_ID) set: real voice I/O \u2014 mic in,
@@ -21024,7 +21091,7 @@ ${out}
21024
21091
  if (dx) dx.options.holdResumeMs = HOLD_RESUME_MS;
21025
21092
  if (args.duplexV2 && dx) {
21026
21093
  const v = voiceIO;
21027
- bindSecondChannel(v, makeSecondChannel(ai, dx.options.reflexModel, { show: (t) => {
21094
+ bindSecondChannel(v, makeSecondChannel(ai, dx.options.reflexModel, args.channelMode, { show: (t) => {
21028
21095
  err("\r\x1B[0J" + dim(` \u21B3 ${t}
21029
21096
  `));
21030
21097
  editorRef?.redrawNow();
@@ -21280,11 +21347,8 @@ function duplexError(e, reflexModel) {
21280
21347
  return code && !msg.includes(String(code)) ? `${msg} (${code})` : msg;
21281
21348
  }
21282
21349
  var HOLD_RESUME_MS = 4e3;
21283
- var leakDecider = () => {
21284
- const o = voiceDeciderOptions();
21285
- return o ? new DecideClient(o) : void 0;
21286
- };
21287
- function makeSecondChannel(ai, reflexModel, opts) {
21350
+ var leakDecider = () => voiceDecider();
21351
+ function makeSecondChannel(ai, reflexModel, mode = "normal", opts) {
21288
21352
  const decider = leakDecider();
21289
21353
  const instinct = decider ? new Instinct({ decider }) : void 0;
21290
21354
  const model = process.env.AGENTX_CHANNEL_MODEL || reflexModel;
@@ -21294,8 +21358,10 @@ function makeSecondChannel(ai, reflexModel, opts) {
21294
21358
  banks: new ChannelBanks({ generate: fastBankGenerator(ai, model), cacheDir: join16(homedir10(), ".agent", "cache", "channel-banks") }),
21295
21359
  decide: channelDecide(ai, instinct),
21296
21360
  generate: fastLineGenerator(ai, model),
21297
- // Anticipated backchannels (off until the offline replay proves them — tests/voice-bc-anticipate-eval.ts)
21298
- ...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 } : {},
21299
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`),
21300
21366
  ...opts
21301
21367
  });
@@ -21349,7 +21415,7 @@ async function duplexServe(args, ai, cfg, cwd) {
21349
21415
  });
21350
21416
  if (args.voiceSystemPrompt) dx.voice.options.systemPrompt += "\n\n" + args.voiceSystemPrompt;
21351
21417
  let scTurn = 0;
21352
- 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;
21353
21419
  const store = new SessionStore(cwd);
21354
21420
  const session = startSession(args, store, dx.voice, cwd);
21355
21421
  persist = () => {
@@ -21414,8 +21480,8 @@ async function channelServe(args, ai, cfg) {
21414
21480
  const out = (o) => process.stdout.write(JSON.stringify(o) + "\n");
21415
21481
  const { reflexModel, notes } = duplexModels({ voiceModel: args.voiceModel, reflexModel: cfg.reflexModel, thinkModel: false, actModel: String(args.model ?? cfg.model ?? ""), apiKeys: { ...cfg.apiKeys, ...apiKeysFromEnv() } });
21416
21482
  const model = reflexModel ?? new DuplexAgentOptions().reflexModel;
21417
- const sc = makeSecondChannel(ai, model, { show: (text, kind) => out({ type: "channel", kind, text }) });
21418
- 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 } : {} });
21419
21485
  const rl = createInterface({ input: process.stdin });
21420
21486
  rl.on("line", (line) => {
21421
21487
  let m;
@@ -21550,7 +21616,7 @@ async function main() {
21550
21616
  console.error(red("No provider key found. Set ANTHROPIC_API_KEY (or OPENAI_API_KEY / GOOGLE_API_KEY / GROQ_API_KEY) in ~/.agent/.env, or set BODIFY_API_KEY + BODIFY_APP_ID to pull from Bodify."));
21551
21617
  process.exit(1);
21552
21618
  }
21553
- const ai = new AIClient({
21619
+ const ai = new AIClient2({
21554
21620
  apiKeys,
21555
21621
  ...cfg.baseUrls ? { baseUrls: cfg.baseUrls } : {},
21556
21622
  retry: {