@kaltura-sdk/avatars 1.16.0 → 1.16.2

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/index.esm.js CHANGED
@@ -6386,7 +6386,7 @@ const textCommunicationState = {
6386
6386
  }) => ({
6387
6387
  message: event.text
6388
6388
  })
6389
- }, 'appendUserTextEntry']
6389
+ }, 'trackInternalAskEcho']
6390
6390
  }
6391
6391
  },
6392
6392
  states: {
@@ -20623,32 +20623,9 @@ const appendAvatarChatEntry = assign$1(({
20623
20623
  context,
20624
20624
  event
20625
20625
  }) => {
20626
- var _a, _b;
20627
20626
  assertEvent(event, AvatarConnectionMachineEventTypes.AvatarChatEntryReceived);
20628
- // The avatar's FINAL segment responding means the human phase for this
20629
- // turn is done - a pendingTextInput still anchored to this turnId (see
20630
- // appendUserChatEntry) has nothing left to merge into; clear it so the
20631
- // next askViaText starts a fresh entry instead of merging into a turn
20632
- // that's already been answered. A non-final segment (avatar-filler,
20633
- // spoken ahead of the main answer during a tool call) must NOT clear it
20634
- // - the real turn is still open server-side and more sends can still
20635
- // legitimately merge into it; closing early here caused a real bug: a
20636
- // later send got a fresh local entry, but the server's own confirmation
20637
- // for it still carried the still-open turn's turnId, landing two chat
20638
- // entries on the same id - every later merge (by id) then grew both.
20639
- //
20640
- // Compares against `turnId`, NOT `id` - a real capture showed the
20641
- // server can reuse the same turnId across multiple, already-answered
20642
- // exchanges (e.g. repeated ambiguous input like "="), so `id` alone is
20643
- // not a safe proxy for "this is the turn that's still open" once it's
20644
- // also being used as a collision-safe chatLog entry id (see
20645
- // on-chat-log.ts / appendUserChatEntry).
20646
- const isFinalSegment = (_a = event.isFinalSegment) !== null && _a !== void 0 ? _a : true;
20647
- const pendingTurnId = (_b = context.pendingTextInput) === null || _b === void 0 ? void 0 : _b.turnId;
20648
- const pendingTextInput = isFinalSegment && pendingTurnId != null && pendingTurnId === event.entry.turnId ? null : context.pendingTextInput;
20649
20627
  return {
20650
- chatLog: [...context.chatLog, event.entry],
20651
- pendingTextInput
20628
+ chatLog: [...context.chatLog, event.entry]
20652
20629
  };
20653
20630
  });
20654
20631
 
@@ -20671,133 +20648,44 @@ const removeStaleChatEntriesForRetry = assign$1(({
20671
20648
  };
20672
20649
  });
20673
20650
 
20651
+ // Upserts by id - a resend superseding a still-open turn keeps that turn's
20652
+ // original entry id (see on-chat-log.ts openTurnEntryIds), so this replaces
20653
+ // the entry in place instead of duplicating it.
20674
20654
  const appendUserChatEntry = assign$1(({
20675
20655
  context,
20676
20656
  event
20677
20657
  }) => {
20678
- var _a;
20679
20658
  assertEvent(event, AvatarConnectionMachineEventTypes.UserVoiceTurnReceived);
20680
- const pending = context.pendingTextInput;
20681
- if (!pending) {
20659
+ if (context.awaitingInternalAskEcho) {
20660
+ // Invisible kickoff prompt's own echo - consume without surfacing it.
20661
+ return {
20662
+ awaitingInternalAskEcho: false
20663
+ };
20664
+ }
20665
+ const existingIndex = context.chatLog.findIndex(entry => entry.id === event.entry.id);
20666
+ if (existingIndex === -1) {
20682
20667
  return {
20683
20668
  chatLog: [...context.chatLog, event.entry]
20684
20669
  };
20685
20670
  }
20686
- // Only one send can be in flight at a time - appendUserTextEntry merges
20687
- // any further sends into it instead of starting a second one - so this
20688
- // turn is unambiguously that pending entry's confirmation. The server is
20689
- // authoritative for what actually gets saved as message history, so its
20690
- // entry always wins here, whether or not our locally-accumulated text
20691
- // happens to match it exactly.
20692
- const chatLog = pending.id ? context.chatLog.map(entry => entry.id === pending.id ? event.entry : entry) : [...context.chatLog, event.entry];
20693
- // Stay anchored to this turn via its now-confirmed id instead of
20694
- // clearing to null - the server re-sends this same "human" echo once per
20695
- // rapid send under one still-open turnId, each carrying the full
20696
- // cumulative text (see on-chat-log.ts), so a further send before the
20697
- // avatar responds must keep merging into this same entry. What actually
20698
- // closes this out is the avatar responding - see appendAvatarChatEntry.
20699
- // `turnId` is tracked alongside `id` (not derived from it) - a real
20700
- // capture showed the server can reuse the same turnId across multiple,
20701
- // already-answered exchanges, so `event.entry.id` must stay collision-safe
20702
- // (see on-chat-log.ts) while `turnId` is what appendAvatarChatEntry
20703
- // actually compares against to decide its turn just closed.
20671
+ const chatLog = [...context.chatLog];
20672
+ chatLog[existingIndex] = event.entry;
20704
20673
  return {
20705
- chatLog,
20706
- pendingTextInput: {
20707
- id: event.entry.id,
20708
- turnId: (_a = event.entry.turnId) !== null && _a !== void 0 ? _a : null,
20709
- text: event.entry.text
20710
- }
20674
+ chatLog
20711
20675
  };
20712
20676
  });
20713
20677
 
20714
- const appendUserTextEntry = assign$1(({
20678
+ // No optimistic entry for askViaText - chat log only reflects the server's
20679
+ // echoed `human` text (see on-chat-log.ts). The internal kickoff prompt
20680
+ // goes through that same echo, so its next one must be suppressed.
20681
+ const trackInternalAskEcho = assign$1(({
20715
20682
  context,
20716
20683
  event
20717
20684
  }) => {
20718
20685
  assertEvent(event, AvatarConnectionMachineEventTypes.AskViaText);
20719
- const {
20720
- text,
20721
- internal
20722
- } = event;
20723
- if (context.pendingTextInput) {
20724
- // A send while the previous turn is still open mirrors the server's
20725
- // own behavior: it merges rapid back-to-back input into one turn, so
20726
- // append into the same pending message instead of starting a new one.
20727
- // The server re-confirms this same turn once per send (see
20728
- // appendUserChatEntry) rather than clearing pendingTextInput, so
20729
- // further sends keep landing here until the avatar actually answers
20730
- // (see appendAvatarChatEntry, which is what closes it out) or the turn
20731
- // is interrupted.
20732
- const {
20733
- id,
20734
- turnId,
20735
- text: pendingText
20736
- } = context.pendingTextInput;
20737
- const mergedText = `${pendingText} ${text}`;
20738
- if (id) {
20739
- return {
20740
- pendingTextInput: {
20741
- id,
20742
- turnId,
20743
- text: mergedText
20744
- },
20745
- chatLog: context.chatLog.map(entry => entry.id === id ? Object.assign(Object.assign({}, entry), {
20746
- text: mergedText
20747
- }) : entry)
20748
- };
20749
- }
20750
- if (internal) {
20751
- // Still an internal-only accumulation - stays out of the chat log.
20752
- return {
20753
- pendingTextInput: {
20754
- id: null,
20755
- turnId: null,
20756
- text: mergedText
20757
- }
20758
- };
20759
- }
20760
- // The pending message so far was internal (invisible) but this
20761
- // follow-up isn't - it becomes visible now, under a fresh entry.
20762
- const _entry = {
20763
- id: crypto.randomUUID(),
20764
- role: 'user',
20765
- text: mergedText
20766
- };
20767
- return {
20768
- chatLog: [...context.chatLog, _entry],
20769
- pendingTextInput: {
20770
- id: _entry.id,
20771
- turnId: null,
20772
- text: mergedText
20773
- }
20774
- };
20775
- }
20776
- // raiseAskViaTextKickoff marks its auto-sent ask `internal` — it's a bootstrap prompt,
20777
- // not something the user said, so keep it out of the visible chat log. Still tracked
20778
- // in pendingTextInput so the server's turn echo is deduped the same way. No chatLog
20779
- // entry backs it, so `id: null`.
20780
- if (internal) {
20781
- return {
20782
- pendingTextInput: {
20783
- id: null,
20784
- turnId: null,
20785
- text
20786
- }
20787
- };
20788
- }
20789
- const entry = {
20790
- id: crypto.randomUUID(),
20791
- role: 'user',
20792
- text
20793
- };
20686
+ if (!event.internal) return {};
20794
20687
  return {
20795
- chatLog: [...context.chatLog, entry],
20796
- pendingTextInput: {
20797
- id: entry.id,
20798
- turnId: null,
20799
- text
20800
- }
20688
+ awaitingInternalAskEcho: true
20801
20689
  };
20802
20690
  });
20803
20691
 
@@ -20860,35 +20748,32 @@ const source$a = createTinyPubsubEventsSource(websocketMessage, {
20860
20748
  // Builds alternating user/avatar chat log from agent_raw_text, keyed by turnId.
20861
20749
  //
20862
20750
  // delta.type:
20863
- // - 'think' with `human` — user's turn text, one-shot on the bootstrap
20864
- // "preparing to answer..." segment (precedes the turn's avatar segments).
20865
- // No `human` field = real thinking tokens, ignored (like 'tool'/'share').
20866
- // Deduped per turnId:speechId. Entry id is that composite, not bare
20867
- // turnId — server can reuse one turnId across several already-answered
20868
- // exchanges (e.g. repeated "="), so speechId keeps entries from colliding.
20751
+ // - 'think' with `human` — user's turn text. A resend while the turnId's
20752
+ // exchange is still unanswered cancels it server-side with no signal to
20753
+ // the client (conversation-manager's AgentAdapter.invalidateTask), gets
20754
+ // its own real messageId, and supersedes it in place client-side via
20755
+ // openTurnEntryIds below. A LATER, already-answered exchange reusing
20756
+ // the same turnId is unrelated and gets a fresh entry instead.
20869
20757
  // - 'avatar' / 'avatar-filler' — chat text, chunked by speechId, closed by
20870
- // segmentEnd=true. Filler is spoken content too (e.g. tool-call "let me
20871
- // check..." lines).
20872
- // - 'user-interruption' — turn aborted server-side (no literal 'abort'
20873
- // type exists). Flushes whatever partial avatar text accumulated and
20874
- // evicts that turnId:speechId buffer only.
20758
+ // segmentEnd=true. Filler is spoken content too (tool-call ack lines).
20759
+ // - 'user-interruption' — turn aborted server-side. Flushes accumulated
20760
+ // avatar text and evicts that turnId:speechId buffer only.
20875
20761
  //
20876
- // A retry reuses the same turnId under a new speechId; each speechId's
20877
- // segmentEnd/user-interruption is tracked/flushed independently.
20762
+ // A retry reuses the turnId under a new speechId, tracked independently.
20878
20763
  //
20879
- // Every entry carries `messageId` (parsed.messageId) — constant for every
20880
- // segment of one exchange, unique across exchanges even when turnId repeats
20881
- // (confirmed against kaltura_agent_executer.py). remove-stale-chat-entries-
20882
- // for-retry.ts matches on it to drop exactly one exchange on retry.
20764
+ // `messageId` is constant per exchange, unique across exchanges even when
20765
+ // turnId repeats (confirmed against kaltura_agent_executer.py) —
20766
+ // remove-stale-chat-entries-for-retry.ts matches on it for retry cleanup.
20767
+ //
20768
+ // `speechId` is set on every entry too — unlike messageId, it stays constant
20769
+ // across a filler segment and its later tool-call answer, so group-chat-log.ts keys on it.
20883
20770
  //
20884
20771
  // Opening phrase (resumed thread, avatar speaks first): never sent as a
20885
- // normal turnId-keyed segment. Server sends it via `init_response` (too
20886
- // early — fires before this actor's setHandler(), so it's dropped by the
20887
- // pubsub queue reset) and a mirrored `generatingSpeech` event, which we use
20888
- // as the text source instead. `agentTurnToTalk` (userTranscription === '')
20889
- // is the flush trigger — nothing else marks "greeting done, no real user
20890
- // turn happened first". BLANK_TEXT_MARKER is the server's "no opening
20891
- // phrase configured" sentinel, skipped.
20772
+ // normal turnId-keyed segment — `init_response` fires too early (before
20773
+ // this actor's setHandler(), dropped by the pubsub queue reset), so
20774
+ // `generatingSpeech` is the text source instead, flushed by
20775
+ // `agentTurnToTalk` (userTranscription === ''). BLANK_TEXT_MARKER is the
20776
+ // server's "no opening phrase configured" sentinel, skipped.
20892
20777
  const onChatLog = fromCallback(({
20893
20778
  input,
20894
20779
  sendBack
@@ -20896,8 +20781,10 @@ const onChatLog = fromCallback(({
20896
20781
  // Accumulate avatar text by `${turnId}:${speechId}` — each speechId
20897
20782
  // closes independently (e.g. a filler segment vs the main answer).
20898
20783
  const pending = new Map();
20899
- const seenHumanTurnKeys = new Set();
20900
20784
  let avatarEntrySequence = 0;
20785
+ // turnId -> entry id of its current unanswered exchange - reused across
20786
+ // superseding resends, cleared on a real answer (see top-of-file comment).
20787
+ const openTurnEntryIds = new Map();
20901
20788
  const createAvatarEntryId = (messageId, speechId) => {
20902
20789
  avatarEntrySequence += 1;
20903
20790
  return `${messageId}:${speechId}:${avatarEntrySequence}`;
@@ -20911,7 +20798,7 @@ const onChatLog = fromCallback(({
20911
20798
  logger: input.logger,
20912
20799
  handlers: {
20913
20800
  agent_raw_text: payload => {
20914
- var _a, _b;
20801
+ var _a, _b, _c;
20915
20802
  try {
20916
20803
  const delta = JSON.parse(payload.delta);
20917
20804
  const result = agentRawTextDeltaSchema.safeParse(delta);
@@ -20919,22 +20806,25 @@ const onChatLog = fromCallback(({
20919
20806
  const parsed = result.data;
20920
20807
  const key = `${payload.turnId}:${payload.speechId}`;
20921
20808
  if (parsed.type === 'think') {
20922
- if (parsed.human && !seenHumanTurnKeys.has(key)) {
20923
- seenHumanTurnKeys.add(key);
20809
+ if (parsed.human) {
20810
+ const id = (_a = openTurnEntryIds.get(payload.turnId)) !== null && _a !== void 0 ? _a : parsed.messageId;
20811
+ openTurnEntryIds.set(payload.turnId, id);
20924
20812
  sendBack({
20925
20813
  type: AvatarConnectionMachineEventTypes.UserVoiceTurnReceived,
20926
20814
  entry: {
20927
- id: key,
20815
+ id,
20928
20816
  role: 'user',
20929
20817
  text: parsed.human,
20930
20818
  turnId: payload.turnId,
20931
- messageId: parsed.messageId
20819
+ messageId: parsed.messageId,
20820
+ speechId: payload.speechId
20932
20821
  }
20933
20822
  });
20934
20823
  }
20935
20824
  return;
20936
20825
  }
20937
20826
  if (parsed.type === 'user-interruption') {
20827
+ openTurnEntryIds.delete(payload.turnId);
20938
20828
  const text = pending.get(key);
20939
20829
  pending.delete(key);
20940
20830
  if (text) {
@@ -20945,7 +20835,8 @@ const onChatLog = fromCallback(({
20945
20835
  role: 'avatar',
20946
20836
  text,
20947
20837
  turnId: payload.turnId,
20948
- messageId: parsed.messageId
20838
+ messageId: parsed.messageId,
20839
+ speechId: payload.speechId
20949
20840
  }
20950
20841
  });
20951
20842
  }
@@ -20953,12 +20844,17 @@ const onChatLog = fromCallback(({
20953
20844
  }
20954
20845
  if (parsed.type !== 'avatar' && parsed.type !== 'avatar-filler') return;
20955
20846
  if (typeof parsed.content === 'string' && parsed.content) {
20956
- pending.set(key, ((_a = pending.get(key)) !== null && _a !== void 0 ? _a : '') + parsed.content);
20847
+ pending.set(key, ((_b = pending.get(key)) !== null && _b !== void 0 ? _b : '') + parsed.content);
20957
20848
  }
20958
20849
  if (parsed.segmentEnd) {
20959
- const text = (_b = pending.get(key)) !== null && _b !== void 0 ? _b : '';
20850
+ const text = (_c = pending.get(key)) !== null && _c !== void 0 ? _c : '';
20960
20851
  pending.delete(key);
20961
20852
  if (!text) return;
20853
+ // Real answer closes the exchange - a later reuse of this
20854
+ // turnId (e.g. repeated "=") must get a fresh entry, not this one.
20855
+ if (parsed.type === 'avatar') {
20856
+ openTurnEntryIds.delete(payload.turnId);
20857
+ }
20962
20858
  sendBack({
20963
20859
  type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
20964
20860
  entry: {
@@ -20966,13 +20862,12 @@ const onChatLog = fromCallback(({
20966
20862
  role: 'avatar',
20967
20863
  text,
20968
20864
  turnId: payload.turnId,
20969
- messageId: parsed.messageId
20970
- },
20971
- // Filler doesn't close an open pendingTextInput (see append-avatar-chat-entry.ts).
20972
- isFinalSegment: parsed.type !== 'avatar-filler'
20865
+ messageId: parsed.messageId,
20866
+ speechId: payload.speechId
20867
+ }
20973
20868
  });
20974
20869
  }
20975
- } catch (_c) {
20870
+ } catch (_d) {
20976
20871
  // invalid JSON delta — ignore
20977
20872
  }
20978
20873
  },
@@ -20997,7 +20892,8 @@ const onChatLog = fromCallback(({
20997
20892
  id: openingPhraseSpeechId !== null && openingPhraseSpeechId !== void 0 ? openingPhraseSpeechId : 'init_response',
20998
20893
  role: 'avatar',
20999
20894
  text: openingPhraseText,
21000
- turnId: 'init_response'
20895
+ turnId: 'init_response',
20896
+ speechId: openingPhraseSpeechId !== null && openingPhraseSpeechId !== void 0 ? openingPhraseSpeechId : undefined
21001
20897
  }
21002
20898
  });
21003
20899
  }
@@ -23323,7 +23219,7 @@ const avatarConnectionMachine = setup({
23323
23219
  appendAvatarChatEntry,
23324
23220
  removeStaleChatEntriesForRetry,
23325
23221
  appendUserChatEntry,
23326
- appendUserTextEntry,
23222
+ trackInternalAskEcho,
23327
23223
  setScreenShareActive,
23328
23224
  setScreenShareStream,
23329
23225
  showScreenShareErrorNotification,
@@ -23495,7 +23391,7 @@ const avatarConnectionMachine = setup({
23495
23391
  content: null,
23496
23392
  chatLog: [],
23497
23393
  clientToolCallLog: [],
23498
- pendingTextInput: null,
23394
+ awaitingInternalAskEcho: false,
23499
23395
  userAgent: '',
23500
23396
  userAgentHints: null,
23501
23397
  kalturaAnalyticsService: input.kalturaAnalyticsService,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaltura-sdk/avatars",
3
- "version": "1.16.0",
3
+ "version": "1.16.2",
4
4
  "author": "kaltura",
5
5
  "license": "AGPL-3.0",
6
6
  "repository": {
@@ -9,11 +9,11 @@
9
9
  },
10
10
  "types": "./src/index.d.ts",
11
11
  "dependencies": {
12
- "@kaltura-sdk/rtc-avatar": "^1.32.34",
13
- "@kaltura-sdk/rtc-core": "^1.25.21",
12
+ "@kaltura-sdk/rtc-avatar": "^1.33.1",
13
+ "@kaltura-sdk/rtc-core": "^1.26.1",
14
14
  "@local/genie-shared-utils": "^1.0.0",
15
15
  "@unisphere/core": "^1.99.0",
16
- "@unisphere/genie-types": "^1.39.3",
16
+ "@unisphere/genie-types": "^1.39.6",
17
17
  "@unisphere/notifications-core": "^1.25.0",
18
18
  "@unisphere/runtime": "^1.98.0",
19
19
  "@unisphere/runtime-js": "^1.96.0",
@@ -0,0 +1,2 @@
1
+ import { AvatarConnectionMachineAction } from '../../types';
2
+ export declare const trackInternalAskEcho: AvatarConnectionMachineAction;
@@ -398,7 +398,7 @@ export declare const avatarConnectionMachine: import("xstate").StateMachine<Avat
398
398
  type: "appendUserChatEntry";
399
399
  params: unknown;
400
400
  } | {
401
- type: "appendUserTextEntry";
401
+ type: "trackInternalAskEcho";
402
402
  params: unknown;
403
403
  } | {
404
404
  type: "setScreenShareActive";
@@ -96,7 +96,7 @@ export declare const connectedState: {
96
96
  initial: string;
97
97
  on: {
98
98
  AskViaText: {
99
- actions: ("appendUserTextEntry" | {
99
+ actions: ("trackInternalAskEcho" | {
100
100
  type: "sendAskViaText";
101
101
  params: ({ event }: {
102
102
  event: import("../types").AskViaTextEvent;
@@ -4,7 +4,7 @@ export declare const textCommunicationState: {
4
4
  initial: string;
5
5
  on: {
6
6
  AskViaText: {
7
- actions: ("appendUserTextEntry" | {
7
+ actions: ("trackInternalAskEcho" | {
8
8
  type: "sendAskViaText";
9
9
  params: ({ event }: {
10
10
  event: AskViaTextEvent;
@@ -66,11 +66,7 @@ export interface AvatarConnectionMachineContext {
66
66
  content: UnisphereChatBlock | null;
67
67
  chatLog: ChatEntry[];
68
68
  clientToolCallLog: ClientToolCallLogEntry[];
69
- pendingTextInput: {
70
- id: string | null;
71
- turnId: string | null;
72
- text: string;
73
- } | null;
69
+ awaitingInternalAskEcho: boolean;
74
70
  isAgentTalking: boolean;
75
71
  userAgent: string;
76
72
  userAgentHints: Record<string, string> | null;
@@ -164,7 +164,6 @@ export type AgentStartSpeechReceivedEvent = {
164
164
  export type AvatarChatEntryReceivedEvent = {
165
165
  type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived;
166
166
  entry: ChatEntry;
167
- isFinalSegment?: boolean;
168
167
  };
169
168
  export type UserVoiceTurnReceivedEvent = {
170
169
  type: AvatarConnectionMachineEventTypes.UserVoiceTurnReceived;
@@ -1,2 +0,0 @@
1
- import { AvatarConnectionMachineAction } from '../../types';
2
- export declare const appendUserTextEntry: AvatarConnectionMachineAction;