@livx.cc/agentx 0.99.118 → 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 +1 -1
- package/dist/cli.js +148 -28
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +52 -8
- package/dist/index.js +144 -27
- package/dist/index.js.map +1 -1
- package/dist/{secondChannel-BCEr_QyM.d.ts → secondChannel-DepBV0Zv.d.ts} +6 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as AgentOptions, H as Hooks, p as RunResult, f as Decider, A as Agent, i as PermissionPolicy, G as Gender, L as LineKind, C as ChannelMode, q as SecondChannelOptions, S as SecondChannel } from './secondChannel-
|
|
2
|
-
export { b as ChatFragment, c as Classification, d as Classifier, e as CompactResult, D as DEFAULT_MUTATING, g as Decision, M as ModelRouter, h as ModelRouterOptions, P as PermissionOptions, j as PermissionRule, k as PreToolUseDecision, R as ReasoningEffort, l as RecordingHooks, m as RecordingLifecycle, n as RouteDecision, o as RouterState, T as Tier, r as ToolUse, s as ToolUseMeta, t as claudeCodeDisallowedTools, u as composeHooks, v as estimateTokens, w as jevAvailable, x as jevClassifier, y as pickTier, z as planMode, B as reasoningToChatFragment } from './secondChannel-
|
|
1
|
+
import { a as AgentOptions, H as Hooks, p as RunResult, f as Decider, A as Agent, i as PermissionPolicy, G as Gender, L as LineKind, C as ChannelMode, q as SecondChannelOptions, S as SecondChannel } from './secondChannel-DepBV0Zv.js';
|
|
2
|
+
export { b as ChatFragment, c as Classification, d as Classifier, e as CompactResult, D as DEFAULT_MUTATING, g as Decision, M as ModelRouter, h as ModelRouterOptions, P as PermissionOptions, j as PermissionRule, k as PreToolUseDecision, R as ReasoningEffort, l as RecordingHooks, m as RecordingLifecycle, n as RouteDecision, o as RouterState, T as Tier, r as ToolUse, s as ToolUseMeta, t as claudeCodeDisallowedTools, u as composeHooks, v as estimateTokens, w as jevAvailable, x as jevClassifier, y as pickTier, z as planMode, B as reasoningToChatFragment } from './secondChannel-DepBV0Zv.js';
|
|
3
3
|
export { MODEL_ALIASES, ModelSwitch, ResolveModelSwitchOpts, latestClaude, modelShortLabel, resolveModelAlias, resolveModelSwitch } from './models.js';
|
|
4
4
|
import { IFilesystem, FileMetadata } from '@livx.cc/wcli/core';
|
|
5
5
|
export { CommandExecutor, FileMetadata, IFilesystem, IndexedDbFilesystem, MemFilesystem, registerHeadlessCommands } from '@livx.cc/wcli/core';
|
|
@@ -1666,15 +1666,40 @@ interface MainEvent {
|
|
|
1666
1666
|
name?: string;
|
|
1667
1667
|
args?: unknown;
|
|
1668
1668
|
}
|
|
1669
|
+
/** One turn's timeline, ms from the user's last word (the STT's word clock when the host passes it, else the final's
|
|
1670
|
+
* arrival). A stage that did not happen is absent. */
|
|
1671
|
+
interface TurnTimeline {
|
|
1672
|
+
/** The STT's final reached the call (STT latency + silence endpoint + the engine's merge hold). */
|
|
1673
|
+
final?: number;
|
|
1674
|
+
/** TurnGate sent the turn to MAIN (its own hold for a mid-phrase line included). */
|
|
1675
|
+
sent?: number;
|
|
1676
|
+
/** The fill channel chose its first line for this turn (bridge/filler/progress) — handed to the host, not yet heard. */
|
|
1677
|
+
fill?: number;
|
|
1678
|
+
fillKind?: string;
|
|
1679
|
+
/** That line's audio started (hostAudio: the host's player; else = fill). */
|
|
1680
|
+
fillAudio?: number;
|
|
1681
|
+
/** MAIN's first event (text or tool). */
|
|
1682
|
+
mainFirst?: number;
|
|
1683
|
+
/** MAIN's first text token. */
|
|
1684
|
+
mainText?: number;
|
|
1685
|
+
/** MAIN's first audio started (the host's player). */
|
|
1686
|
+
mainAudio?: number;
|
|
1687
|
+
/** false: the host gave no last-word time — the anchor is the final's arrival. */
|
|
1688
|
+
lastWord: boolean;
|
|
1689
|
+
/** How the turn ended: answered = MAIN audio played · cut = barged · replaced = a fuller re-final took it · silent = no MAIN audio. */
|
|
1690
|
+
end?: 'answered' | 'cut' | 'replaced' | 'silent';
|
|
1691
|
+
}
|
|
1669
1692
|
interface CallMetric {
|
|
1670
|
-
name: 'first_sound' | 'first_word' | 'turn' | 'drop' | 'hold' | 'fill' | 'lang';
|
|
1693
|
+
name: 'first_sound' | 'first_word' | 'turn' | 'drop' | 'hold' | 'fill' | 'lang' | 'timeline';
|
|
1671
1694
|
id?: string;
|
|
1672
|
-
/** ms from the user's
|
|
1695
|
+
/** ms from the user's last word (see TurnTimeline) */
|
|
1673
1696
|
ms?: number;
|
|
1674
1697
|
by?: 'fill' | 'progress' | 'main';
|
|
1675
1698
|
kind?: string;
|
|
1676
1699
|
text?: string;
|
|
1677
1700
|
lang?: string | null;
|
|
1701
|
+
/** name 'timeline': the turn's stages. */
|
|
1702
|
+
timeline?: TurnTimeline;
|
|
1678
1703
|
}
|
|
1679
1704
|
declare class VoiceCallOptions {
|
|
1680
1705
|
/** Host runs MAIN with exactly `text` (a replace: roll the `replaces` turn back first). */
|
|
@@ -1707,6 +1732,9 @@ declare class VoiceCallOptions {
|
|
|
1707
1732
|
/** Extra SecondChannel options (tests, tuning). speak/show/cancel are always VoiceCall's own. */
|
|
1708
1733
|
channel: Partial<SecondChannelOptions>;
|
|
1709
1734
|
onMetric: (m: CallMetric) => void;
|
|
1735
|
+
/** The host reports when a fill line's audio REALLY starts (heard()). Off: a line handed to `speak` counts as heard —
|
|
1736
|
+
* right only for a host whose speak() plays at once (a relay host would count lines its player later dropped). */
|
|
1737
|
+
hostAudio: boolean;
|
|
1710
1738
|
now: () => number;
|
|
1711
1739
|
/** An aside (aside()) needs this long since the last one — a cue answered every time sounds scripted. */
|
|
1712
1740
|
asideGapMs: number;
|
|
@@ -1722,6 +1750,8 @@ declare class VoiceCall {
|
|
|
1722
1750
|
private lastAside;
|
|
1723
1751
|
private lastAudibleAt;
|
|
1724
1752
|
private firstFinalAt;
|
|
1753
|
+
private lastWordAt;
|
|
1754
|
+
private earlyHeard;
|
|
1725
1755
|
private progressTimer;
|
|
1726
1756
|
private ended;
|
|
1727
1757
|
constructor(options?: Partial<VoiceCallOptions> | undefined);
|
|
@@ -1738,14 +1768,25 @@ declare class VoiceCall {
|
|
|
1738
1768
|
setLanguage(tag: string): void;
|
|
1739
1769
|
/** STT partial (the whole partial). */
|
|
1740
1770
|
interim(text: string, _atMs?: number, aec?: boolean): void;
|
|
1741
|
-
/** STT final → TurnGate.
|
|
1742
|
-
|
|
1771
|
+
/** STT final → TurnGate. `lastWordAgoMs`: how long ago the user's last word of it ended (the STT's word clock,
|
|
1772
|
+
* bod-voice UtteranceMeta.lastWordAgoMs) — the turn's latency anchor; absent = the final's arrival. `lang`: the
|
|
1773
|
+
* language the STT identified for it (UtteranceMeta.language) — what the turn's words follow when they alone can't tell
|
|
1774
|
+
* (a short English line after a Hebrew one left the fill channel on Hebrew: its bridge was refused, the turn silent). */
|
|
1775
|
+
final(text: string, sttId?: string, o?: {
|
|
1776
|
+
lastWordAgoMs?: number;
|
|
1777
|
+
lang?: string;
|
|
1778
|
+
}): void;
|
|
1779
|
+
private finalAt;
|
|
1780
|
+
private sttLang;
|
|
1781
|
+
/** The host's player started a line's audio now (or in `inMs`): `fill`/`progress` = the fill channel's, `main` = MAIN's
|
|
1782
|
+
* reply (as main({type:'audio'})). With hostAudio this is what first_sound counts. */
|
|
1783
|
+
heard(by: 'fill' | 'progress' | 'main', inMs?: number): void;
|
|
1743
1784
|
/** A v2 host that decides turns itself: the user's line already went to MAIN (no TurnGate, no sendTurn) — the fill
|
|
1744
1785
|
* channel follows its language and covers the wait. */
|
|
1745
1786
|
hostTurn(text: string): void;
|
|
1746
1787
|
pause(ms: number): void;
|
|
1747
|
-
/** MAIN's stream. */
|
|
1748
|
-
main(ev: MainEvent): void;
|
|
1788
|
+
/** MAIN's stream. (`inMs`: an audio event's start is that far ahead — the host player's queue.) */
|
|
1789
|
+
main(ev: MainEvent, inMs?: number): void;
|
|
1749
1790
|
/** The user barged in (hard): cut fill + MAIN audio (the host cuts its own), abort a running MAIN turn. */
|
|
1750
1791
|
barge(): void;
|
|
1751
1792
|
/** Soft barge-in: keep (a listener cue — no turn) or cut (→ barge()). */
|
|
@@ -1768,6 +1809,9 @@ declare class VoiceCall {
|
|
|
1768
1809
|
/** Every fill/progress line passes here: only in the turn's known language, never after the call ended. */
|
|
1769
1810
|
private say;
|
|
1770
1811
|
private sounded;
|
|
1812
|
+
/** The turn is over (answered + audible, replaced, the next turn, the call's end): its timeline, once. A turn whose MAIN
|
|
1813
|
+
* never became audible still reports first_word — with no ms and why (cut / replaced / silent) — so every turn has one. */
|
|
1814
|
+
private close;
|
|
1771
1815
|
private onTool;
|
|
1772
1816
|
private clearProgress;
|
|
1773
1817
|
}
|
package/dist/index.js
CHANGED
|
@@ -13104,11 +13104,26 @@ var SecondChannel = class {
|
|
|
13104
13104
|
}
|
|
13105
13105
|
/** Hard gates every line passes, whoever proposed it. */
|
|
13106
13106
|
allowed(k) {
|
|
13107
|
-
|
|
13108
|
-
|
|
13109
|
-
|
|
13110
|
-
|
|
13111
|
-
|
|
13107
|
+
return !this.blocked(k);
|
|
13108
|
+
}
|
|
13109
|
+
/** Why a line of kind `k` can't play now ('' = it can) — the call's ledger names a missing bridge's cause. */
|
|
13110
|
+
blocked(k) {
|
|
13111
|
+
if (!this.o.kinds.includes(k)) return "kind off";
|
|
13112
|
+
if (this.audioLocked) return `main audio (${this.main})`;
|
|
13113
|
+
if (this.inflight) return "a line still playing";
|
|
13114
|
+
if (this.o.now() - this.lastAt < this.o.minGapMs) return `${Math.round(this.o.now() - this.lastAt)}ms since the last line`;
|
|
13115
|
+
if (k === this.lastKind && (k !== "backchannel" || this.o.now() - this.lastAt < this.o.backchannelGapMs)) return "same kind as the last line";
|
|
13116
|
+
if (k === "backchannel") return this.o.aec && this.userSpeaking && this.main === "idle" && !this.capped() ? "" : "not a backchannel moment";
|
|
13117
|
+
if (k === "filler") return !this.userSpeaking && this.main === "idle" ? "" : "not a filler moment";
|
|
13118
|
+
return this.bridged ? "bridged already" : this.main === "queued" || this.main === "thinking" ? "" : `main ${this.main}`;
|
|
13119
|
+
}
|
|
13120
|
+
/** Why this turn gets no bridge by rule ('' = it may) — language, a quick/phatic turn, or blocked(). */
|
|
13121
|
+
bridgeBlock() {
|
|
13122
|
+
const w = words2(this.heard);
|
|
13123
|
+
if (!this.lang) return "language unknown";
|
|
13124
|
+
if (w.length <= 3 || w.length <= 6 && isPhatic(this.heard, this.bank)) return w.length <= 3 ? "quick turn" : "phatic";
|
|
13125
|
+
if (w.length < this.o.bridgeMinWords) return `under ${this.o.bridgeMinWords} words`;
|
|
13126
|
+
return this.blocked("bridge");
|
|
13112
13127
|
}
|
|
13113
13128
|
bcTimes = [];
|
|
13114
13129
|
capped() {
|
|
@@ -13155,7 +13170,10 @@ var SecondChannel = class {
|
|
|
13155
13170
|
return breath || /[,;:…]$/.test(this.heard);
|
|
13156
13171
|
}
|
|
13157
13172
|
async consider(trigger, pauseMs, breath = false, cue = null) {
|
|
13158
|
-
if (this.busy)
|
|
13173
|
+
if (this.busy) {
|
|
13174
|
+
if (trigger === "working") this.o.onEvent({ trigger, rule: "silence", ms: 0, acted: "silence", reason: "deciding another line" });
|
|
13175
|
+
return;
|
|
13176
|
+
}
|
|
13159
13177
|
const n0 = words2(this.heard).length;
|
|
13160
13178
|
this.decidedAt = n0;
|
|
13161
13179
|
if (!(this.o.anticipate && this.plan && trigger === "interim")) cue = null;
|
|
@@ -13165,7 +13183,10 @@ var SecondChannel = class {
|
|
|
13165
13183
|
log28.info("chatty mode without echo cancellation \u2014 normal timing (our lines would re-trigger the mic)");
|
|
13166
13184
|
}
|
|
13167
13185
|
const rule = this.rule(trigger, pauseMs, breath, cue);
|
|
13168
|
-
if (rule === "silence")
|
|
13186
|
+
if (rule === "silence") {
|
|
13187
|
+
if (trigger === "working") this.o.onEvent({ trigger, rule, ms: 0, acted: "silence", reason: this.bridgeBlock() || "rule" });
|
|
13188
|
+
return;
|
|
13189
|
+
}
|
|
13169
13190
|
this.busy = true;
|
|
13170
13191
|
const gen = this.gen, t0 = this.o.now();
|
|
13171
13192
|
const ev = (e) => this.o.onEvent({ trigger, rule, ms: Math.round(this.o.now() - t0), ...cue ? { cue: cue.line } : {}, ...e });
|
|
@@ -13359,6 +13380,9 @@ var VoiceCallOptions = class {
|
|
|
13359
13380
|
channel = {};
|
|
13360
13381
|
onMetric = () => {
|
|
13361
13382
|
};
|
|
13383
|
+
/** The host reports when a fill line's audio REALLY starts (heard()). Off: a line handed to `speak` counts as heard —
|
|
13384
|
+
* right only for a host whose speak() plays at once (a relay host would count lines its player later dropped). */
|
|
13385
|
+
hostAudio = false;
|
|
13362
13386
|
now = () => performance.now();
|
|
13363
13387
|
/** An aside (aside()) needs this long since the last one — a cue answered every time sounds scripted. */
|
|
13364
13388
|
asideGapMs = 8e3;
|
|
@@ -13378,11 +13402,17 @@ var VoiceCall = class {
|
|
|
13378
13402
|
} : {},
|
|
13379
13403
|
mode: o.mode,
|
|
13380
13404
|
aec: o.aec,
|
|
13381
|
-
|
|
13405
|
+
// 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
|
|
13406
|
+
// goes 300ms after the turn if MAIN is still silent (it always is that early: warm MAIN's first token is ≥1s), no veto
|
|
13407
|
+
bridgeDelayMs: 300,
|
|
13382
13408
|
instantBridge: true,
|
|
13383
|
-
// v3 §3: first sound <1.2s p50 — a bridge at 700ms if MAIN is still silent, no veto, no wait
|
|
13384
13409
|
...this.lang ? { lang: this.lang } : {},
|
|
13385
13410
|
...o.channel,
|
|
13411
|
+
// the ledger: a turn's bridge moment that stayed silent, and why (its wait then has no first sound until MAIN speaks)
|
|
13412
|
+
onEvent: (e) => {
|
|
13413
|
+
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 } : {} });
|
|
13414
|
+
o.channel.onEvent?.(e);
|
|
13415
|
+
},
|
|
13386
13416
|
speak: (text, kind) => this.say(text, kind),
|
|
13387
13417
|
show: (text, kind) => o.show(text, kind),
|
|
13388
13418
|
cancel: () => o.cancel(),
|
|
@@ -13409,6 +13439,10 @@ var VoiceCall = class {
|
|
|
13409
13439
|
lastAudibleAt = -Infinity;
|
|
13410
13440
|
firstFinalAt = null;
|
|
13411
13441
|
// the first final of the turn being formed (hold/merge)
|
|
13442
|
+
lastWordAt = null;
|
|
13443
|
+
// the user's last word before the turn being formed (its latest final's)
|
|
13444
|
+
earlyHeard = null;
|
|
13445
|
+
// a fill line heard while that turn was still held
|
|
13412
13446
|
progressTimer = null;
|
|
13413
13447
|
ended = false;
|
|
13414
13448
|
get o() {
|
|
@@ -13437,12 +13471,37 @@ var VoiceCall = class {
|
|
|
13437
13471
|
this.follow(text);
|
|
13438
13472
|
this.fill.onInterim(text);
|
|
13439
13473
|
}
|
|
13440
|
-
/** STT final → TurnGate.
|
|
13441
|
-
|
|
13474
|
+
/** STT final → TurnGate. `lastWordAgoMs`: how long ago the user's last word of it ended (the STT's word clock,
|
|
13475
|
+
* bod-voice UtteranceMeta.lastWordAgoMs) — the turn's latency anchor; absent = the final's arrival. `lang`: the
|
|
13476
|
+
* language the STT identified for it (UtteranceMeta.language) — what the turn's words follow when they alone can't tell
|
|
13477
|
+
* (a short English line after a Hebrew one left the fill channel on Hebrew: its bridge was refused, the turn silent). */
|
|
13478
|
+
final(text, sttId, o = {}) {
|
|
13442
13479
|
if (this.ended) return;
|
|
13443
|
-
|
|
13480
|
+
const now3 = this.o.now(), { lastWordAgoMs } = o;
|
|
13481
|
+
this.firstFinalAt ??= now3;
|
|
13482
|
+
this.sttLang = o.lang ? primaryLang(o.lang) : null;
|
|
13483
|
+
this.lastWordAt = typeof lastWordAgoMs === "number" && lastWordAgoMs >= 0 ? now3 - lastWordAgoMs : null;
|
|
13484
|
+
this.finalAt = now3;
|
|
13444
13485
|
this.gate.final(text, sttId);
|
|
13445
13486
|
}
|
|
13487
|
+
finalAt = 0;
|
|
13488
|
+
sttLang = null;
|
|
13489
|
+
// the STT's language for the final being followed
|
|
13490
|
+
/** The host's player started a line's audio now (or in `inMs`): `fill`/`progress` = the fill channel's, `main` = MAIN's
|
|
13491
|
+
* reply (as main({type:'audio'})). With hostAudio this is what first_sound counts. */
|
|
13492
|
+
heard(by, inMs = 0) {
|
|
13493
|
+
if (this.ended) return;
|
|
13494
|
+
if (by === "main") return this.main({ type: "audio", ...this.cur ? { id: this.cur.id } : {} }, inMs);
|
|
13495
|
+
const at = this.o.now() + Math.max(0, inMs);
|
|
13496
|
+
if (this.gate.holding || !this.cur || this.cur.closed) {
|
|
13497
|
+
this.earlyHeard ??= { at, by };
|
|
13498
|
+
return;
|
|
13499
|
+
}
|
|
13500
|
+
this.lastAudibleAt = at;
|
|
13501
|
+
const c = this.cur;
|
|
13502
|
+
c.marks.fillAudio ??= at;
|
|
13503
|
+
this.sounded(c, by, at);
|
|
13504
|
+
}
|
|
13446
13505
|
/** A v2 host that decides turns itself: the user's line already went to MAIN (no TurnGate, no sendTurn) — the fill
|
|
13447
13506
|
* channel follows its language and covers the wait. */
|
|
13448
13507
|
hostTurn(text) {
|
|
@@ -13453,8 +13512,8 @@ var VoiceCall = class {
|
|
|
13453
13512
|
pause(ms) {
|
|
13454
13513
|
if (!this.ended) this.fill.onPause(ms);
|
|
13455
13514
|
}
|
|
13456
|
-
/** MAIN's stream. */
|
|
13457
|
-
main(ev) {
|
|
13515
|
+
/** MAIN's stream. (`inMs`: an audio event's start is that far ahead — the host player's queue.) */
|
|
13516
|
+
main(ev, inMs = 0) {
|
|
13458
13517
|
const c = this.cur;
|
|
13459
13518
|
if (!c || ev.id && ev.id !== c.id) return;
|
|
13460
13519
|
switch (ev.type) {
|
|
@@ -13463,6 +13522,8 @@ var VoiceCall = class {
|
|
|
13463
13522
|
return;
|
|
13464
13523
|
case "text":
|
|
13465
13524
|
if (!ev.text) return;
|
|
13525
|
+
c.marks.mainFirst ??= this.o.now();
|
|
13526
|
+
c.marks.mainText ??= this.o.now();
|
|
13466
13527
|
if (!c.streamed) {
|
|
13467
13528
|
c.streamed = true;
|
|
13468
13529
|
this.gate.replyStarted(c.id);
|
|
@@ -13472,25 +13533,32 @@ var VoiceCall = class {
|
|
|
13472
13533
|
this.fill.onMainTokens(ev.text);
|
|
13473
13534
|
return;
|
|
13474
13535
|
case "tool":
|
|
13536
|
+
c.marks.mainFirst ??= this.o.now();
|
|
13475
13537
|
this.gate.replyStarted(c.id);
|
|
13476
13538
|
void this.onTool(c, ev.name ?? "", ev.args);
|
|
13477
13539
|
return;
|
|
13478
13540
|
case "tool_done":
|
|
13479
13541
|
return;
|
|
13480
|
-
case "audio":
|
|
13481
|
-
|
|
13542
|
+
case "audio": {
|
|
13543
|
+
const at = this.o.now() + Math.max(0, inMs);
|
|
13544
|
+
this.lastAudibleAt = at;
|
|
13482
13545
|
this.fill.onAgentState("first_audio");
|
|
13483
|
-
this.sounded(c, "main");
|
|
13546
|
+
this.sounded(c, "main", at);
|
|
13484
13547
|
if (!c.worded) {
|
|
13485
13548
|
c.worded = true;
|
|
13486
|
-
|
|
13549
|
+
c.marks.mainAudio = at;
|
|
13550
|
+
this.o.onMetric({ name: "first_word", id: c.id, ms: Math.round(at - c.at), by: "main" });
|
|
13487
13551
|
}
|
|
13552
|
+
if (c.done) this.close(c, "answered");
|
|
13488
13553
|
return;
|
|
13554
|
+
}
|
|
13489
13555
|
case "done":
|
|
13490
13556
|
case "error":
|
|
13491
13557
|
c.running = false;
|
|
13558
|
+
c.done = true;
|
|
13492
13559
|
this.clearProgress();
|
|
13493
13560
|
this.fill.onAgentState("idle");
|
|
13561
|
+
if (c.worded) this.close(c, "answered");
|
|
13494
13562
|
return;
|
|
13495
13563
|
}
|
|
13496
13564
|
}
|
|
@@ -13507,6 +13575,7 @@ var VoiceCall = class {
|
|
|
13507
13575
|
this.cur.running = false;
|
|
13508
13576
|
this.o.abortTurn(this.cur.id);
|
|
13509
13577
|
}
|
|
13578
|
+
if (this.cur && !this.cur.worded) this.cur.end ??= "cut";
|
|
13510
13579
|
}
|
|
13511
13580
|
/** Soft barge-in: keep (a listener cue — no turn) or cut (→ barge()). */
|
|
13512
13581
|
async overlap(text) {
|
|
@@ -13536,6 +13605,7 @@ var VoiceCall = class {
|
|
|
13536
13605
|
return this.fill.drain();
|
|
13537
13606
|
}
|
|
13538
13607
|
end() {
|
|
13608
|
+
if (this.cur) this.close(this.cur);
|
|
13539
13609
|
this.ended = true;
|
|
13540
13610
|
this.gate.end();
|
|
13541
13611
|
this.clearProgress();
|
|
@@ -13544,24 +13614,39 @@ var VoiceCall = class {
|
|
|
13544
13614
|
/** Follow the user's language from their own words: a confident detection, else the current language if the words
|
|
13545
13615
|
* fit it, else unknown (silent). Pinned into the channel so its own detection never diverges. */
|
|
13546
13616
|
follow(text) {
|
|
13547
|
-
const
|
|
13617
|
+
const fits = (l2) => l2 && fitsLanguage(text, l2) ? l2 : null;
|
|
13618
|
+
const l = detectLanguage(text) ?? fits(this.sttLang) ?? fits(this.lang);
|
|
13548
13619
|
if (l !== this.lang) this.o.onMetric({ name: "lang", lang: l, text });
|
|
13549
13620
|
this.lang = l;
|
|
13550
13621
|
if (l) this.fill.setLanguage(l, true);
|
|
13551
13622
|
}
|
|
13552
13623
|
onTurn(t) {
|
|
13553
|
-
const
|
|
13624
|
+
const now3 = this.o.now(), lastWord = this.lastWordAt !== null;
|
|
13625
|
+
const at = this.lastWordAt ?? this.firstFinalAt ?? now3;
|
|
13626
|
+
const finalAt = this.finalAt || now3;
|
|
13554
13627
|
this.firstFinalAt = null;
|
|
13628
|
+
this.lastWordAt = null;
|
|
13629
|
+
if (this.cur) this.close(this.cur, t.replaces === this.cur.id ? "replaced" : void 0);
|
|
13555
13630
|
this.follow(t.text);
|
|
13556
13631
|
this.clearProgress();
|
|
13557
|
-
this.cur = { id: t.id, text: t.text, at, running: true, streamed: false, sounded: false, worded: false };
|
|
13632
|
+
this.cur = { id: t.id, text: t.text, at, finalAt, lastWord, running: true, streamed: false, sounded: false, worded: false, done: false, marks: { sent: now3 }, closed: false };
|
|
13633
|
+
const early = this.earlyHeard;
|
|
13634
|
+
this.earlyHeard = null;
|
|
13635
|
+
if (early && early.at >= at) {
|
|
13636
|
+
this.cur.marks.fillAudio = early.at;
|
|
13637
|
+
this.sounded(this.cur, early.by, early.at);
|
|
13638
|
+
}
|
|
13558
13639
|
this.progress.turn();
|
|
13559
13640
|
this.o.onMetric({ name: "turn", id: t.id, kind: t.kind, text: t.text });
|
|
13560
13641
|
this.o.sendTurn(t.text, { id: t.id, ...t.replaces ? { replaces: t.replaces } : {} });
|
|
13561
13642
|
if (t.kind === "new") this.fill.onUserTurn(t.text);
|
|
13562
13643
|
}
|
|
13563
13644
|
onDrop(text, why) {
|
|
13564
|
-
if (!this.gate.holding)
|
|
13645
|
+
if (!this.gate.holding) {
|
|
13646
|
+
this.firstFinalAt = null;
|
|
13647
|
+
this.lastWordAt = null;
|
|
13648
|
+
this.earlyHeard = null;
|
|
13649
|
+
}
|
|
13565
13650
|
this.o.onMetric({ name: "drop", kind: why, text });
|
|
13566
13651
|
}
|
|
13567
13652
|
/** Every fill/progress line passes here: only in the turn's known language, never after the call ended. */
|
|
@@ -13575,16 +13660,48 @@ var VoiceCall = class {
|
|
|
13575
13660
|
return false;
|
|
13576
13661
|
}
|
|
13577
13662
|
if (r === false) return false;
|
|
13578
|
-
this.lastAudibleAt = this.o.now();
|
|
13579
13663
|
this.o.onMetric({ name: "fill", kind, text, lang: this.lang });
|
|
13580
|
-
|
|
13581
|
-
|
|
13664
|
+
const c = this.cur && !this.cur.closed ? this.cur : void 0;
|
|
13665
|
+
const forTurn = !!c && (kind === "bridge" || kind === "progress" || kind === "filler" && c.running && !c.sounded);
|
|
13666
|
+
if (c && forTurn && c.marks.fill === void 0) {
|
|
13667
|
+
c.marks.fill = this.o.now();
|
|
13668
|
+
c.fillKind = kind;
|
|
13669
|
+
}
|
|
13670
|
+
if (this.o.hostAudio) return r;
|
|
13671
|
+
this.lastAudibleAt = this.o.now();
|
|
13672
|
+
if (c && forTurn) {
|
|
13673
|
+
c.marks.fillAudio ??= this.o.now();
|
|
13674
|
+
this.sounded(c, kind === "progress" ? "progress" : "fill");
|
|
13675
|
+
}
|
|
13582
13676
|
return r;
|
|
13583
13677
|
}
|
|
13584
|
-
sounded(c, by) {
|
|
13678
|
+
sounded(c, by, at = this.o.now()) {
|
|
13585
13679
|
if (c.sounded) return;
|
|
13586
13680
|
c.sounded = true;
|
|
13587
|
-
this.o.onMetric({ name: "first_sound", id: c.id, ms: Math.round(
|
|
13681
|
+
this.o.onMetric({ name: "first_sound", id: c.id, ms: Math.round(at - c.at), by });
|
|
13682
|
+
}
|
|
13683
|
+
/** The turn is over (answered + audible, replaced, the next turn, the call's end): its timeline, once. A turn whose MAIN
|
|
13684
|
+
* never became audible still reports first_word — with no ms and why (cut / replaced / silent) — so every turn has one. */
|
|
13685
|
+
close(c, end) {
|
|
13686
|
+
if (c.closed) return;
|
|
13687
|
+
c.closed = true;
|
|
13688
|
+
c.end = c.worded ? "answered" : end ?? c.end ?? "silent";
|
|
13689
|
+
if (!c.worded) this.o.onMetric({ name: "first_word", id: c.id, by: "main", kind: c.end });
|
|
13690
|
+
const rel = (t) => t === void 0 ? void 0 : Math.round(t - c.at);
|
|
13691
|
+
const m = c.marks, tl = {
|
|
13692
|
+
final: rel(c.finalAt),
|
|
13693
|
+
sent: rel(m.sent),
|
|
13694
|
+
fill: rel(m.fill),
|
|
13695
|
+
...c.fillKind ? { fillKind: c.fillKind } : {},
|
|
13696
|
+
fillAudio: rel(m.fillAudio),
|
|
13697
|
+
mainFirst: rel(m.mainFirst),
|
|
13698
|
+
mainText: rel(m.mainText),
|
|
13699
|
+
mainAudio: rel(m.mainAudio),
|
|
13700
|
+
lastWord: c.lastWord,
|
|
13701
|
+
end: c.end
|
|
13702
|
+
};
|
|
13703
|
+
for (const k of Object.keys(tl)) if (tl[k] === void 0) delete tl[k];
|
|
13704
|
+
this.o.onMetric({ name: "timeline", id: c.id, text: c.text, timeline: tl });
|
|
13588
13705
|
}
|
|
13589
13706
|
async onTool(c, name, args) {
|
|
13590
13707
|
const lang = this.lang;
|