@livx.cc/agentx 0.99.117 → 0.99.119

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
@@ -5300,11 +5300,12 @@ var Agent = class _Agent {
5300
5300
  let emptyNudged = false;
5301
5301
  let closureNudged = false;
5302
5302
  let lastProgressAt = start;
5303
+ const turnUser = this.transcript[this.transcript.length - 1];
5303
5304
  const kill = (finishReason) => {
5304
5305
  log8.warn(`kill-switch: ${finishReason} (steps=${steps}, tokens=${usage.totalTokens}, budgetTokens=${Math.round(spent)}, ms=${Date.now() - start - this.parkedMs}${finishReason === "timeout" ? `, idle=${Date.now() - lastProgressAt - this.parkedMs}ms` : ""}${this.parkedMs ? ` +${this.parkedMs} parked` : ""})`);
5305
5306
  this.jobs.killAll(`run ended: ${finishReason}`, true);
5306
5307
  const claims = this.survivingClaims();
5307
- return { text: lastAssistantText(this.transcript), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...finishReason === "budget" ? { budget: { spent: Math.round(spent), limit: o.maxTokens, stepPrompt } } : {}, ...claims.length ? { fabricatedToolClaims: claims } : {} };
5308
+ return { text: lastAssistantText(this.transcript, turnUser), steps, finishReason, messages: this.transcript, usage, usageEstimated, ...finishReason === "budget" ? { budget: { spent: Math.round(spent), limit: o.maxTokens, stepPrompt } } : {}, ...claims.length ? { fabricatedToolClaims: claims } : {} };
5308
5309
  };
5309
5310
  const overBudget = () => !!o.maxTokens && spent >= o.maxTokens;
5310
5311
  const canNudge = () => steps < o.maxSteps && !overBudget();
@@ -6164,8 +6165,9 @@ function summarize(messages, focus) {
6164
6165
  }
6165
6166
  return lines.join("\n");
6166
6167
  }
6167
- function lastAssistantText(messages) {
6168
+ function lastAssistantText(messages, since) {
6168
6169
  for (let i = messages.length - 1; i >= 0; i--) {
6170
+ if (since && messages[i] === since) return "";
6169
6171
  if (messages[i].role === "assistant") return contentText(messages[i].content);
6170
6172
  }
6171
6173
  return "";
@@ -13434,11 +13436,26 @@ var SecondChannel = class {
13434
13436
  }
13435
13437
  /** Hard gates every line passes, whoever proposed it. */
13436
13438
  allowed(k) {
13437
- if (!this.o.kinds.includes(k)) return false;
13438
- 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;
13439
- if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle" && !this.capped();
13440
- if (k === "filler") return !this.userSpeaking && this.main === "idle";
13441
- 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");
13442
13459
  }
13443
13460
  bcTimes = [];
13444
13461
  capped() {
@@ -13485,7 +13502,10 @@ var SecondChannel = class {
13485
13502
  return breath || /[,;:…]$/.test(this.heard);
13486
13503
  }
13487
13504
  async consider(trigger, pauseMs, breath = false, cue = null) {
13488
- 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
+ }
13489
13509
  const n0 = words3(this.heard).length;
13490
13510
  this.decidedAt = n0;
13491
13511
  if (!(this.o.anticipate && this.plan && trigger === "interim")) cue = null;
@@ -13495,7 +13515,10 @@ var SecondChannel = class {
13495
13515
  log27.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
13496
13516
  }
13497
13517
  const rule = this.rule(trigger, pauseMs, breath, cue);
13498
- 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
+ }
13499
13522
  this.busy = true;
13500
13523
  const gen = this.gen, t0 = this.o.now();
13501
13524
  const ev = (e) => this.o.onEvent({ trigger, rule, ms: Math.round(this.o.now() - t0), ...cue ? { cue: cue.line } : {}, ...e });
@@ -13689,6 +13712,9 @@ var VoiceCallOptions = class {
13689
13712
  channel = {};
13690
13713
  onMetric = () => {
13691
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;
13692
13718
  now = () => performance.now();
13693
13719
  /** An aside (aside()) needs this long since the last one — a cue answered every time sounds scripted. */
13694
13720
  asideGapMs = 8e3;
@@ -13708,11 +13734,17 @@ var VoiceCall = class {
13708
13734
  } : {},
13709
13735
  mode: o.mode,
13710
13736
  aec: o.aec,
13711
- 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,
13712
13740
  instantBridge: true,
13713
- // v3 §3: first sound <1.2s p50 — a bridge at 700ms if MAIN is still silent, no veto, no wait
13714
13741
  ...this.lang ? { lang: this.lang } : {},
13715
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
+ },
13716
13748
  speak: (text, kind) => this.say(text, kind),
13717
13749
  show: (text, kind) => o.show(text, kind),
13718
13750
  cancel: () => o.cancel(),
@@ -13739,6 +13771,10 @@ var VoiceCall = class {
13739
13771
  lastAudibleAt = -Infinity;
13740
13772
  firstFinalAt = null;
13741
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
13742
13778
  progressTimer = null;
13743
13779
  ended = false;
13744
13780
  get o() {
@@ -13767,12 +13803,37 @@ var VoiceCall = class {
13767
13803
  this.follow(text);
13768
13804
  this.fill.onInterim(text);
13769
13805
  }
13770
- /** STT final → TurnGate. */
13771
- 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 = {}) {
13772
13811
  if (this.ended) return;
13773
- 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;
13774
13817
  this.gate.final(text, sttId);
13775
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
+ }
13776
13837
  /** A v2 host that decides turns itself: the user's line already went to MAIN (no TurnGate, no sendTurn) — the fill
13777
13838
  * channel follows its language and covers the wait. */
13778
13839
  hostTurn(text) {
@@ -13783,8 +13844,8 @@ var VoiceCall = class {
13783
13844
  pause(ms) {
13784
13845
  if (!this.ended) this.fill.onPause(ms);
13785
13846
  }
13786
- /** MAIN's stream. */
13787
- 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) {
13788
13849
  const c = this.cur;
13789
13850
  if (!c || ev.id && ev.id !== c.id) return;
13790
13851
  switch (ev.type) {
@@ -13793,6 +13854,8 @@ var VoiceCall = class {
13793
13854
  return;
13794
13855
  case "text":
13795
13856
  if (!ev.text) return;
13857
+ c.marks.mainFirst ??= this.o.now();
13858
+ c.marks.mainText ??= this.o.now();
13796
13859
  if (!c.streamed) {
13797
13860
  c.streamed = true;
13798
13861
  this.gate.replyStarted(c.id);
@@ -13802,25 +13865,32 @@ var VoiceCall = class {
13802
13865
  this.fill.onMainTokens(ev.text);
13803
13866
  return;
13804
13867
  case "tool":
13868
+ c.marks.mainFirst ??= this.o.now();
13805
13869
  this.gate.replyStarted(c.id);
13806
13870
  void this.onTool(c, ev.name ?? "", ev.args);
13807
13871
  return;
13808
13872
  case "tool_done":
13809
13873
  return;
13810
- case "audio":
13811
- this.lastAudibleAt = this.o.now();
13874
+ case "audio": {
13875
+ const at = this.o.now() + Math.max(0, inMs);
13876
+ this.lastAudibleAt = at;
13812
13877
  this.fill.onAgentState("first_audio");
13813
- this.sounded(c, "main");
13878
+ this.sounded(c, "main", at);
13814
13879
  if (!c.worded) {
13815
13880
  c.worded = true;
13816
- 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" });
13817
13883
  }
13884
+ if (c.done) this.close(c, "answered");
13818
13885
  return;
13886
+ }
13819
13887
  case "done":
13820
13888
  case "error":
13821
13889
  c.running = false;
13890
+ c.done = true;
13822
13891
  this.clearProgress();
13823
13892
  this.fill.onAgentState("idle");
13893
+ if (c.worded) this.close(c, "answered");
13824
13894
  return;
13825
13895
  }
13826
13896
  }
@@ -13837,6 +13907,7 @@ var VoiceCall = class {
13837
13907
  this.cur.running = false;
13838
13908
  this.o.abortTurn(this.cur.id);
13839
13909
  }
13910
+ if (this.cur && !this.cur.worded) this.cur.end ??= "cut";
13840
13911
  }
13841
13912
  /** Soft barge-in: keep (a listener cue — no turn) or cut (→ barge()). */
13842
13913
  async overlap(text) {
@@ -13866,6 +13937,7 @@ var VoiceCall = class {
13866
13937
  return this.fill.drain();
13867
13938
  }
13868
13939
  end() {
13940
+ if (this.cur) this.close(this.cur);
13869
13941
  this.ended = true;
13870
13942
  this.gate.end();
13871
13943
  this.clearProgress();
@@ -13874,24 +13946,39 @@ var VoiceCall = class {
13874
13946
  /** Follow the user's language from their own words: a confident detection, else the current language if the words
13875
13947
  * fit it, else unknown (silent). Pinned into the channel so its own detection never diverges. */
13876
13948
  follow(text) {
13877
- 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);
13878
13951
  if (l !== this.lang) this.o.onMetric({ name: "lang", lang: l, text });
13879
13952
  this.lang = l;
13880
13953
  if (l) this.fill.setLanguage(l, true);
13881
13954
  }
13882
13955
  onTurn(t) {
13883
- 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;
13884
13959
  this.firstFinalAt = null;
13960
+ this.lastWordAt = null;
13961
+ if (this.cur) this.close(this.cur, t.replaces === this.cur.id ? "replaced" : void 0);
13885
13962
  this.follow(t.text);
13886
13963
  this.clearProgress();
13887
- 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
+ }
13888
13971
  this.progress.turn();
13889
13972
  this.o.onMetric({ name: "turn", id: t.id, kind: t.kind, text: t.text });
13890
13973
  this.o.sendTurn(t.text, { id: t.id, ...t.replaces ? { replaces: t.replaces } : {} });
13891
13974
  if (t.kind === "new") this.fill.onUserTurn(t.text);
13892
13975
  }
13893
13976
  onDrop(text, why) {
13894
- 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
+ }
13895
13982
  this.o.onMetric({ name: "drop", kind: why, text });
13896
13983
  }
13897
13984
  /** Every fill/progress line passes here: only in the turn's known language, never after the call ended. */
@@ -13905,16 +13992,48 @@ var VoiceCall = class {
13905
13992
  return false;
13906
13993
  }
13907
13994
  if (r === false) return false;
13908
- this.lastAudibleAt = this.o.now();
13909
13995
  this.o.onMetric({ name: "fill", kind, text, lang: this.lang });
13910
- if (this.cur && (kind === "bridge" || kind === "progress")) this.sounded(this.cur, kind === "progress" ? "progress" : "fill");
13911
- 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
+ }
13912
14008
  return r;
13913
14009
  }
13914
- sounded(c, by) {
14010
+ sounded(c, by, at = this.o.now()) {
13915
14011
  if (c.sounded) return;
13916
14012
  c.sounded = true;
13917
- 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 });
13918
14037
  }
13919
14038
  async onTool(c, name, args) {
13920
14039
  const lang = this.lang;
@@ -22705,6 +22824,8 @@ async function channelServe(args, ai, cfg) {
22705
22824
  channel: channelOptions(ai, process.env.AGENTX_CHANNEL_MODEL || model, args.voiceGender),
22706
22825
  speak: () => true,
22707
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
22708
22829
  show: (text, kind) => out({ type: "channel", kind, text }),
22709
22830
  sendTurn: (text, o) => out({ type: "turn", id: o.id, text, ...o.replaces ? { replaces: o.replaces } : {} }),
22710
22831
  abortTurn: (id) => out({ type: "abort", id }),
@@ -22729,7 +22850,8 @@ async function channelServe(args, ai, cfg) {
22729
22850
  if (m.type === "interim") {
22730
22851
  if (typeof m.lang === "string") call.setLanguage(m.lang);
22731
22852
  if (typeof m.text === "string") call.interim(m.text, Date.now(), typeof m.aec === "boolean" ? m.aec : void 0);
22732
- } 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);
22733
22855
  else if (m.type === "main" && m.ev && typeof m.ev.type === "string") call.main(m.ev);
22734
22856
  else if (m.type === "barge") call.barge();
22735
22857
  else if (channelInput(call.fill, m)) return;