@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 +4 -0
- package/dist/cli.js +67 -14
- package/dist/cli.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
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
21311
|
-
|
|
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;
|