@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 +4 -0
- package/dist/cli.js +99 -33
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
14509
|
-
|
|
14510
|
-
|
|
14511
|
-
|
|
14512
|
-
|
|
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:
|
|
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
|
-
|
|
14522
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21298
|
-
|
|
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
|
|
21619
|
+
const ai = new AIClient2({
|
|
21554
21620
|
apiKeys,
|
|
21555
21621
|
...cfg.baseUrls ? { baseUrls: cfg.baseUrls } : {},
|
|
21556
21622
|
retry: {
|