@livx.cc/agentx 0.99.118 → 0.99.120

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
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bun
2
- import { o as RouterState, H as Hooks, p as RunResult, R as ReasoningEffort, C as ChannelMode, A as Agent } from './secondChannel-BCEr_QyM.js';
2
+ import { o as RouterState, H as Hooks, p as RunResult, R as ReasoningEffort, C as ChannelMode, A as Agent } from './secondChannel-DepBV0Zv.js';
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-DZHZIyxh.js';
5
5
 
package/dist/cli.js CHANGED
@@ -13436,11 +13436,26 @@ var SecondChannel = class {
13436
13436
  }
13437
13437
  /** Hard gates every line passes, whoever proposed it. */
13438
13438
  allowed(k) {
13439
- if (!this.o.kinds.includes(k)) return false;
13440
- 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;
13441
- if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle" && !this.capped();
13442
- if (k === "filler") return !this.userSpeaking && this.main === "idle";
13443
- return (this.main === "queued" || this.main === "thinking") && !this.bridged;
13439
+ return !this.blocked(k);
13440
+ }
13441
+ /** Why a line of kind `k` can't play now ('' = it can) — the call's ledger names a missing bridge's cause. */
13442
+ blocked(k) {
13443
+ if (!this.o.kinds.includes(k)) return "kind off";
13444
+ if (this.audioLocked) return `main audio (${this.main})`;
13445
+ if (this.inflight) return "a line still playing";
13446
+ if (this.o.now() - this.lastAt < this.o.minGapMs) return `${Math.round(this.o.now() - this.lastAt)}ms since the last line`;
13447
+ if (k === this.lastKind && (k !== "backchannel" || this.o.now() - this.lastAt < this.o.backchannelGapMs)) return "same kind as the last line";
13448
+ if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle" && !this.capped() ? "" : "not a backchannel moment";
13449
+ if (k === "filler") return !this.userSpeaking && this.main === "idle" ? "" : "not a filler moment";
13450
+ return this.bridged ? "bridged already" : this.main === "queued" || this.main === "thinking" ? "" : `main ${this.main}`;
13451
+ }
13452
+ /** Why this turn gets no bridge by rule ('' = it may) — language, a quick/phatic turn, or blocked(). */
13453
+ bridgeBlock() {
13454
+ const w = words3(this.heard);
13455
+ if (!this.lang) return "language unknown";
13456
+ if (w.length <= 3 || w.length <= 6 && isPhatic(this.heard, this.bank)) return w.length <= 3 ? "quick turn" : "phatic";
13457
+ if (w.length < this.o.bridgeMinWords) return `under ${this.o.bridgeMinWords} words`;
13458
+ return this.blocked("bridge");
13444
13459
  }
13445
13460
  bcTimes = [];
13446
13461
  capped() {
@@ -13487,7 +13502,10 @@ var SecondChannel = class {
13487
13502
  return breath || /[,;:…]$/.test(this.heard);
13488
13503
  }
13489
13504
  async consider(trigger, pauseMs, breath = false, cue = null) {
13490
- if (this.busy) return;
13505
+ if (this.busy) {
13506
+ if (trigger === "working") this.o.onEvent({ trigger, rule: "silence", ms: 0, acted: "silence", reason: "deciding another line" });
13507
+ return;
13508
+ }
13491
13509
  const n0 = words3(this.heard).length;
13492
13510
  this.decidedAt = n0;
13493
13511
  if (!(this.o.anticipate && this.plan && trigger === "interim")) cue = null;
@@ -13497,7 +13515,10 @@ var SecondChannel = class {
13497
13515
  log27.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
13498
13516
  }
13499
13517
  const rule = this.rule(trigger, pauseMs, breath, cue);
13500
- if (rule === "silence") return;
13518
+ if (rule === "silence") {
13519
+ if (trigger === "working") this.o.onEvent({ trigger, rule, ms: 0, acted: "silence", reason: this.bridgeBlock() || "rule" });
13520
+ return;
13521
+ }
13501
13522
  this.busy = true;
13502
13523
  const gen = this.gen, t0 = this.o.now();
13503
13524
  const ev = (e) => this.o.onEvent({ trigger, rule, ms: Math.round(this.o.now() - t0), ...cue ? { cue: cue.line } : {}, ...e });
@@ -13691,6 +13712,9 @@ var VoiceCallOptions = class {
13691
13712
  channel = {};
13692
13713
  onMetric = () => {
13693
13714
  };
13715
+ /** The host reports when a fill line's audio REALLY starts (heard()). Off: a line handed to `speak` counts as heard —
13716
+ * right only for a host whose speak() plays at once (a relay host would count lines its player later dropped). */
13717
+ hostAudio = false;
13694
13718
  now = () => performance.now();
13695
13719
  /** An aside (aside()) needs this long since the last one — a cue answered every time sounds scripted. */
13696
13720
  asideGapMs = 8e3;
@@ -13710,11 +13734,17 @@ var VoiceCall = class {
13710
13734
  } : {},
13711
13735
  mode: o.mode,
13712
13736
  aec: o.aec,
13713
- bridgeDelayMs: 700,
13737
+ // v3 §3: first sound <1.2s p50 from the last word — the endpoint takes ~0.6s and the bridge's TTS ~0.2s, so the bridge
13738
+ // goes 300ms after the turn if MAIN is still silent (it always is that early: warm MAIN's first token is ≥1s), no veto
13739
+ bridgeDelayMs: 300,
13714
13740
  instantBridge: true,
13715
- // v3 §3: first sound <1.2s p50 — a bridge at 700ms if MAIN is still silent, no veto, no wait
13716
13741
  ...this.lang ? { lang: this.lang } : {},
13717
13742
  ...o.channel,
13743
+ // the ledger: a turn's bridge moment that stayed silent, and why (its wait then has no first sound until MAIN speaks)
13744
+ onEvent: (e) => {
13745
+ if (e.trigger === "working" && e.acted !== "spoken") o.onMetric({ name: "drop", ...this.cur ? { id: this.cur.id } : {}, kind: `no bridge: ${e.reason ?? e.acted}`, ...e.text ? { text: e.text } : {} });
13746
+ o.channel.onEvent?.(e);
13747
+ },
13718
13748
  speak: (text, kind) => this.say(text, kind),
13719
13749
  show: (text, kind) => o.show(text, kind),
13720
13750
  cancel: () => o.cancel(),
@@ -13741,6 +13771,10 @@ var VoiceCall = class {
13741
13771
  lastAudibleAt = -Infinity;
13742
13772
  firstFinalAt = null;
13743
13773
  // the first final of the turn being formed (hold/merge)
13774
+ lastWordAt = null;
13775
+ // the user's last word before the turn being formed (its latest final's)
13776
+ earlyHeard = null;
13777
+ // a fill line heard while that turn was still held
13744
13778
  progressTimer = null;
13745
13779
  ended = false;
13746
13780
  get o() {
@@ -13769,12 +13803,37 @@ var VoiceCall = class {
13769
13803
  this.follow(text);
13770
13804
  this.fill.onInterim(text);
13771
13805
  }
13772
- /** STT final → TurnGate. */
13773
- final(text, sttId) {
13806
+ /** STT final → TurnGate. `lastWordAgoMs`: how long ago the user's last word of it ended (the STT's word clock,
13807
+ * bod-voice UtteranceMeta.lastWordAgoMs) — the turn's latency anchor; absent = the final's arrival. `lang`: the
13808
+ * language the STT identified for it (UtteranceMeta.language) — what the turn's words follow when they alone can't tell
13809
+ * (a short English line after a Hebrew one left the fill channel on Hebrew: its bridge was refused, the turn silent). */
13810
+ final(text, sttId, o = {}) {
13774
13811
  if (this.ended) return;
13775
- this.firstFinalAt ??= this.o.now();
13812
+ const now4 = this.o.now(), { lastWordAgoMs } = o;
13813
+ this.firstFinalAt ??= now4;
13814
+ this.sttLang = o.lang ? primaryLang(o.lang) : null;
13815
+ this.lastWordAt = typeof lastWordAgoMs === "number" && lastWordAgoMs >= 0 ? now4 - lastWordAgoMs : null;
13816
+ this.finalAt = now4;
13776
13817
  this.gate.final(text, sttId);
13777
13818
  }
13819
+ finalAt = 0;
13820
+ sttLang = null;
13821
+ // the STT's language for the final being followed
13822
+ /** The host's player started a line's audio now (or in `inMs`): `fill`/`progress` = the fill channel's, `main` = MAIN's
13823
+ * reply (as main({type:'audio'})). With hostAudio this is what first_sound counts. */
13824
+ heard(by, inMs = 0) {
13825
+ if (this.ended) return;
13826
+ if (by === "main") return this.main({ type: "audio", ...this.cur ? { id: this.cur.id } : {} }, inMs);
13827
+ const at = this.o.now() + Math.max(0, inMs);
13828
+ if (this.gate.holding || !this.cur || this.cur.closed) {
13829
+ this.earlyHeard ??= { at, by };
13830
+ return;
13831
+ }
13832
+ this.lastAudibleAt = at;
13833
+ const c = this.cur;
13834
+ c.marks.fillAudio ??= at;
13835
+ this.sounded(c, by, at);
13836
+ }
13778
13837
  /** A v2 host that decides turns itself: the user's line already went to MAIN (no TurnGate, no sendTurn) — the fill
13779
13838
  * channel follows its language and covers the wait. */
13780
13839
  hostTurn(text) {
@@ -13785,8 +13844,8 @@ var VoiceCall = class {
13785
13844
  pause(ms) {
13786
13845
  if (!this.ended) this.fill.onPause(ms);
13787
13846
  }
13788
- /** MAIN's stream. */
13789
- main(ev) {
13847
+ /** MAIN's stream. (`inMs`: an audio event's start is that far ahead — the host player's queue.) */
13848
+ main(ev, inMs = 0) {
13790
13849
  const c = this.cur;
13791
13850
  if (!c || ev.id && ev.id !== c.id) return;
13792
13851
  switch (ev.type) {
@@ -13795,6 +13854,8 @@ var VoiceCall = class {
13795
13854
  return;
13796
13855
  case "text":
13797
13856
  if (!ev.text) return;
13857
+ c.marks.mainFirst ??= this.o.now();
13858
+ c.marks.mainText ??= this.o.now();
13798
13859
  if (!c.streamed) {
13799
13860
  c.streamed = true;
13800
13861
  this.gate.replyStarted(c.id);
@@ -13804,25 +13865,32 @@ var VoiceCall = class {
13804
13865
  this.fill.onMainTokens(ev.text);
13805
13866
  return;
13806
13867
  case "tool":
13868
+ c.marks.mainFirst ??= this.o.now();
13807
13869
  this.gate.replyStarted(c.id);
13808
13870
  void this.onTool(c, ev.name ?? "", ev.args);
13809
13871
  return;
13810
13872
  case "tool_done":
13811
13873
  return;
13812
- case "audio":
13813
- this.lastAudibleAt = this.o.now();
13874
+ case "audio": {
13875
+ const at = this.o.now() + Math.max(0, inMs);
13876
+ this.lastAudibleAt = at;
13814
13877
  this.fill.onAgentState("first_audio");
13815
- this.sounded(c, "main");
13878
+ this.sounded(c, "main", at);
13816
13879
  if (!c.worded) {
13817
13880
  c.worded = true;
13818
- this.o.onMetric({ name: "first_word", id: c.id, ms: Math.round(this.o.now() - c.at), by: "main" });
13881
+ c.marks.mainAudio = at;
13882
+ this.o.onMetric({ name: "first_word", id: c.id, ms: Math.round(at - c.at), by: "main" });
13819
13883
  }
13884
+ if (c.done) this.close(c, "answered");
13820
13885
  return;
13886
+ }
13821
13887
  case "done":
13822
13888
  case "error":
13823
13889
  c.running = false;
13890
+ c.done = true;
13824
13891
  this.clearProgress();
13825
13892
  this.fill.onAgentState("idle");
13893
+ if (c.worded) this.close(c, "answered");
13826
13894
  return;
13827
13895
  }
13828
13896
  }
@@ -13839,6 +13907,7 @@ var VoiceCall = class {
13839
13907
  this.cur.running = false;
13840
13908
  this.o.abortTurn(this.cur.id);
13841
13909
  }
13910
+ if (this.cur && !this.cur.worded) this.cur.end ??= "cut";
13842
13911
  }
13843
13912
  /** Soft barge-in: keep (a listener cue — no turn) or cut (→ barge()). */
13844
13913
  async overlap(text) {
@@ -13868,6 +13937,7 @@ var VoiceCall = class {
13868
13937
  return this.fill.drain();
13869
13938
  }
13870
13939
  end() {
13940
+ if (this.cur) this.close(this.cur);
13871
13941
  this.ended = true;
13872
13942
  this.gate.end();
13873
13943
  this.clearProgress();
@@ -13876,24 +13946,39 @@ var VoiceCall = class {
13876
13946
  /** Follow the user's language from their own words: a confident detection, else the current language if the words
13877
13947
  * fit it, else unknown (silent). Pinned into the channel so its own detection never diverges. */
13878
13948
  follow(text) {
13879
- const l = detectLanguage(text) ?? (this.lang && fitsLanguage(text, this.lang) ? this.lang : null);
13949
+ const fits = (l2) => l2 && fitsLanguage(text, l2) ? l2 : null;
13950
+ const l = detectLanguage(text) ?? fits(this.sttLang) ?? fits(this.lang);
13880
13951
  if (l !== this.lang) this.o.onMetric({ name: "lang", lang: l, text });
13881
13952
  this.lang = l;
13882
13953
  if (l) this.fill.setLanguage(l, true);
13883
13954
  }
13884
13955
  onTurn(t) {
13885
- const at = this.firstFinalAt ?? this.o.now();
13956
+ const now4 = this.o.now(), lastWord = this.lastWordAt !== null;
13957
+ const at = this.lastWordAt ?? this.firstFinalAt ?? now4;
13958
+ const finalAt = this.finalAt || now4;
13886
13959
  this.firstFinalAt = null;
13960
+ this.lastWordAt = null;
13961
+ if (this.cur) this.close(this.cur, t.replaces === this.cur.id ? "replaced" : void 0);
13887
13962
  this.follow(t.text);
13888
13963
  this.clearProgress();
13889
- this.cur = { id: t.id, text: t.text, at, running: true, streamed: false, sounded: false, worded: false };
13964
+ this.cur = { id: t.id, text: t.text, at, finalAt, lastWord, running: true, streamed: false, sounded: false, worded: false, done: false, marks: { sent: now4 }, closed: false };
13965
+ const early = this.earlyHeard;
13966
+ this.earlyHeard = null;
13967
+ if (early && early.at >= at) {
13968
+ this.cur.marks.fillAudio = early.at;
13969
+ this.sounded(this.cur, early.by, early.at);
13970
+ }
13890
13971
  this.progress.turn();
13891
13972
  this.o.onMetric({ name: "turn", id: t.id, kind: t.kind, text: t.text });
13892
13973
  this.o.sendTurn(t.text, { id: t.id, ...t.replaces ? { replaces: t.replaces } : {} });
13893
13974
  if (t.kind === "new") this.fill.onUserTurn(t.text);
13894
13975
  }
13895
13976
  onDrop(text, why) {
13896
- if (!this.gate.holding) this.firstFinalAt = null;
13977
+ if (!this.gate.holding) {
13978
+ this.firstFinalAt = null;
13979
+ this.lastWordAt = null;
13980
+ this.earlyHeard = null;
13981
+ }
13897
13982
  this.o.onMetric({ name: "drop", kind: why, text });
13898
13983
  }
13899
13984
  /** Every fill/progress line passes here: only in the turn's known language, never after the call ended. */
@@ -13907,16 +13992,48 @@ var VoiceCall = class {
13907
13992
  return false;
13908
13993
  }
13909
13994
  if (r === false) return false;
13910
- this.lastAudibleAt = this.o.now();
13911
13995
  this.o.onMetric({ name: "fill", kind, text, lang: this.lang });
13912
- if (this.cur && (kind === "bridge" || kind === "progress")) this.sounded(this.cur, kind === "progress" ? "progress" : "fill");
13913
- else if (this.cur && this.cur.running && !this.cur.sounded && kind === "filler") this.sounded(this.cur, "fill");
13996
+ const c = this.cur && !this.cur.closed ? this.cur : void 0;
13997
+ const forTurn = !!c && (kind === "bridge" || kind === "progress" || kind === "filler" && c.running && !c.sounded);
13998
+ if (c && forTurn && c.marks.fill === void 0) {
13999
+ c.marks.fill = this.o.now();
14000
+ c.fillKind = kind;
14001
+ }
14002
+ if (this.o.hostAudio) return r;
14003
+ this.lastAudibleAt = this.o.now();
14004
+ if (c && forTurn) {
14005
+ c.marks.fillAudio ??= this.o.now();
14006
+ this.sounded(c, kind === "progress" ? "progress" : "fill");
14007
+ }
13914
14008
  return r;
13915
14009
  }
13916
- sounded(c, by) {
14010
+ sounded(c, by, at = this.o.now()) {
13917
14011
  if (c.sounded) return;
13918
14012
  c.sounded = true;
13919
- this.o.onMetric({ name: "first_sound", id: c.id, ms: Math.round(this.o.now() - c.at), by });
14013
+ this.o.onMetric({ name: "first_sound", id: c.id, ms: Math.round(at - c.at), by });
14014
+ }
14015
+ /** The turn is over (answered + audible, replaced, the next turn, the call's end): its timeline, once. A turn whose MAIN
14016
+ * never became audible still reports first_word — with no ms and why (cut / replaced / silent) — so every turn has one. */
14017
+ close(c, end) {
14018
+ if (c.closed) return;
14019
+ c.closed = true;
14020
+ c.end = c.worded ? "answered" : end ?? c.end ?? "silent";
14021
+ if (!c.worded) this.o.onMetric({ name: "first_word", id: c.id, by: "main", kind: c.end });
14022
+ const rel = (t) => t === void 0 ? void 0 : Math.round(t - c.at);
14023
+ const m = c.marks, tl = {
14024
+ final: rel(c.finalAt),
14025
+ sent: rel(m.sent),
14026
+ fill: rel(m.fill),
14027
+ ...c.fillKind ? { fillKind: c.fillKind } : {},
14028
+ fillAudio: rel(m.fillAudio),
14029
+ mainFirst: rel(m.mainFirst),
14030
+ mainText: rel(m.mainText),
14031
+ mainAudio: rel(m.mainAudio),
14032
+ lastWord: c.lastWord,
14033
+ end: c.end
14034
+ };
14035
+ for (const k of Object.keys(tl)) if (tl[k] === void 0) delete tl[k];
14036
+ this.o.onMetric({ name: "timeline", id: c.id, text: c.text, timeline: tl });
13920
14037
  }
13921
14038
  async onTool(c, name, args) {
13922
14039
  const lang = this.lang;
@@ -22707,6 +22824,8 @@ async function channelServe(args, ai, cfg) {
22707
22824
  channel: channelOptions(ai, process.env.AGENTX_CHANNEL_MODEL || model, args.voiceGender),
22708
22825
  speak: () => true,
22709
22826
  // the host plays {type:'channel'} lines (shown via show)
22827
+ hostAudio: true,
22828
+ // …and says when one really plays ({type:'heard'}) — a line its player drops never counts as sound
22710
22829
  show: (text, kind) => out({ type: "channel", kind, text }),
22711
22830
  sendTurn: (text, o) => out({ type: "turn", id: o.id, text, ...o.replaces ? { replaces: o.replaces } : {} }),
22712
22831
  abortTurn: (id) => out({ type: "abort", id }),
@@ -22731,7 +22850,8 @@ async function channelServe(args, ai, cfg) {
22731
22850
  if (m.type === "interim") {
22732
22851
  if (typeof m.lang === "string") call.setLanguage(m.lang);
22733
22852
  if (typeof m.text === "string") call.interim(m.text, Date.now(), typeof m.aec === "boolean" ? m.aec : void 0);
22734
- } else if (m.type === "final" && typeof m.text === "string") call.final(m.text, m.sttId === void 0 ? void 0 : String(m.sttId));
22853
+ } else if (m.type === "final" && typeof m.text === "string") call.final(m.text, m.sttId === void 0 ? void 0 : String(m.sttId), { ...typeof m.lastWordAgoMs === "number" ? { lastWordAgoMs: m.lastWordAgoMs } : {}, ...typeof m.lang === "string" ? { lang: m.lang } : {} });
22854
+ else if (m.type === "heard" && ["fill", "progress", "main"].includes(m.by)) call.heard(m.by, typeof m.inMs === "number" ? m.inMs : 0);
22735
22855
  else if (m.type === "main" && m.ev && typeof m.ev.type === "string") call.main(m.ev);
22736
22856
  else if (m.type === "barge") call.barge();
22737
22857
  else if (channelInput(call.fill, m)) return;