@kaltura-sdk/avatars 1.15.5 → 1.16.1

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
@@ -6226,6 +6226,9 @@ const agentRawTextBaseDeltaSchema = z$1.object({
6226
6226
  role: z$1.optional(z$1.string()),
6227
6227
  type: z$1.optional(z$1.string()),
6228
6228
  content: z$1.optional(z$1.union([z$1.string(), z$1.record(z$1.string(), z$1.unknown())])),
6229
+ // present on the bootstrap 'think' segment - the user's turn text, echoed
6230
+ // there instead of a dedicated 'human' segment type.
6231
+ human: z$1.optional(z$1.string()),
6229
6232
  segmentNumber: z$1.optional(z$1.number()),
6230
6233
  et: z$1.optional(z$1.number()),
6231
6234
  segmentStart: z$1.optional(z$1.boolean()),
@@ -6383,7 +6386,7 @@ const textCommunicationState = {
6383
6386
  }) => ({
6384
6387
  message: event.text
6385
6388
  })
6386
- }, 'appendUserTextEntry']
6389
+ }, 'trackInternalAskEcho']
6387
6390
  }
6388
6391
  },
6389
6392
  states: {
@@ -7025,13 +7028,13 @@ const agentRecoveryMonitorState = {
7025
7028
  actions: 'resetAgentRecoveryRetryCount'
7026
7029
  }, {
7027
7030
  guard: 'hasMoreRecoveryRetries',
7028
- actions: ['incrementAgentRecoveryRetryCount', 'sendReprocessTurn']
7031
+ actions: ['removeStaleChatEntriesForRetry', 'incrementAgentRecoveryRetryCount', 'sendReprocessTurn']
7029
7032
  }, {
7030
7033
  guard: 'justExceededRecoveryRetries',
7031
7034
  actions: ['incrementAgentRecoveryRetryCount', 'showAgentRecoveryExhaustedNotification']
7032
7035
  }],
7033
7036
  [AvatarConnectionMachineEventTypes.RequestAgentTurnRecovery]: {
7034
- actions: 'sendReprocessTurn'
7037
+ actions: ['removeStaleChatEntriesForRetry', 'sendReprocessTurn']
7035
7038
  }
7036
7039
  },
7037
7040
  invoke: {
@@ -20626,50 +20629,63 @@ const appendAvatarChatEntry = assign$1(({
20626
20629
  };
20627
20630
  });
20628
20631
 
20632
+ // A retry re-runs the same turn (same userInput -> same turnId) under a new
20633
+ // speechId/messageId, re-emitting both the user's `human` segment and the
20634
+ // avatar answer — both go stale, not just the avatar one.
20635
+ //
20636
+ // Matched on messageId, not turnId: the server can reuse the same turnId
20637
+ // across distinct, already-answered exchanges (e.g. repeated "="), so
20638
+ // turnId would wipe an unrelated earlier exchange too. messageId is
20639
+ // constant per exchange and never reused (confirmed against
20640
+ // kaltura_agent_executer.py), so it removes exactly the retried exchange.
20641
+ const removeStaleChatEntriesForRetry = assign$1(({
20642
+ context,
20643
+ event
20644
+ }) => {
20645
+ assertEvent(event, [AvatarConnectionMachineEventTypes.RequestAgentTurnRecovery, AvatarConnectionMachineEventTypes.AgentRecoveryTurnCompleted]);
20646
+ return {
20647
+ chatLog: context.chatLog.filter(entry => entry.messageId !== event.messageId)
20648
+ };
20649
+ });
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.
20629
20654
  const appendUserChatEntry = assign$1(({
20630
20655
  context,
20631
20656
  event
20632
20657
  }) => {
20633
20658
  assertEvent(event, AvatarConnectionMachineEventTypes.UserVoiceTurnReceived);
20634
- const idx = context.pendingTextInputs.indexOf(event.entry.text);
20635
- if (idx !== -1) {
20636
- // Already added optimistically via text input — consume the pending entry and skip
20637
- const pendingTextInputs = [...context.pendingTextInputs];
20638
- pendingTextInputs.splice(idx, 1);
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) {
20639
20667
  return {
20640
- pendingTextInputs
20668
+ chatLog: [...context.chatLog, event.entry]
20641
20669
  };
20642
20670
  }
20671
+ const chatLog = [...context.chatLog];
20672
+ chatLog[existingIndex] = event.entry;
20643
20673
  return {
20644
- chatLog: [...context.chatLog, event.entry]
20674
+ chatLog
20645
20675
  };
20646
20676
  });
20647
20677
 
20648
- 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(({
20649
20682
  context,
20650
20683
  event
20651
20684
  }) => {
20652
20685
  assertEvent(event, AvatarConnectionMachineEventTypes.AskViaText);
20653
- const {
20654
- text,
20655
- internal
20656
- } = event;
20657
- // raiseAskViaTextKickoff marks its auto-sent ask `internal` — it's a bootstrap prompt,
20658
- // not something the user said, so keep it out of the visible chat log. Still record it
20659
- // in pendingTextInputs so the ASR echo of it (agentTurnToTalk) is deduped the same way.
20660
- if (internal) {
20661
- return {
20662
- pendingTextInputs: [...context.pendingTextInputs, text]
20663
- };
20664
- }
20665
- const entry = {
20666
- id: crypto.randomUUID(),
20667
- role: 'user',
20668
- text
20669
- };
20686
+ if (!event.internal) return {};
20670
20687
  return {
20671
- chatLog: [...context.chatLog, entry],
20672
- pendingTextInputs: [...context.pendingTextInputs, text]
20688
+ awaitingInternalAskEcho: true
20673
20689
  };
20674
20690
  });
20675
20691
 
@@ -20720,255 +20736,159 @@ const source$a = createTinyPubsubEventsSource(websocketMessage, {
20720
20736
  agent_raw_text: {
20721
20737
  schema: agentRawTextSchema
20722
20738
  },
20723
- agentTurnToTalk: {
20724
- schema: agentTurnToTalkSchema
20725
- },
20726
20739
  generatingSpeech: {
20727
20740
  schema: generatingSpeechSchema
20741
+ },
20742
+ agentTurnToTalk: {
20743
+ schema: agentTurnToTalkSchema
20728
20744
  }
20729
20745
  }, {
20730
20746
  queueWhilePending: true
20731
20747
  });
20732
- // FLOW OVERVIEW
20733
- // =============
20734
- // Builds the alternating user/avatar chat log from two independent, unordered
20735
- // websocket event streams that share no common id:
20736
- // - agentTurnToTalk: fires once per USER turn, carries the ASR transcript
20737
- // (userTranscription). Empty userTranscription means a proactive/opening
20738
- // turn (avatar speaks first, no preceding user input).
20739
- // - agent_raw_text: streams the AVATAR's reply for a turn as chunked deltas
20740
- // keyed by speechId, terminated by a delta with segmentEnd=true.
20741
- // delta.type === 'avatar' or 'avatar-filler' chunks are chat text (filler
20742
- // is spoken content too, e.g. "Let me check that for you..." streamed
20743
- // ahead of the main answer while a tool call runs); 'think'/'tool'/'share'
20744
- // chunks are filtered out here.
20748
+ // Builds alternating user/avatar chat log from agent_raw_text, keyed by turnId.
20745
20749
  //
20746
- // The two streams race: for a given turn, agentTurnToTalk can arrive before,
20747
- // after, or interleaved with that turn's agent_raw_text segmentEnd. Neither
20748
- // event carries a shared id to correlate by (agentTurnToTalk has no speechId;
20749
- // see the pendingUserTurnCount comment below), so pairing is done by arrival
20750
- // order via a queue + counter:
20751
- // - awaitingUserTurnQueue: avatar answers that finished (segmentEnd) before
20752
- // their turn's agentTurnToTalk showed up. FIFO — first queued avatar
20753
- // answer is paired with the next agentTurnToTalk that needs one.
20754
- // - pendingUserTurnCount: agentTurnToTalk arrivals still waiting on their
20755
- // avatar answer to finish. Incremented when a talk event arrives with an
20756
- // empty queue; decremented when an avatar segmentEnd arrives and directly
20757
- // emits instead of queueing.
20758
- // - resolvedTurnIds: guards against a single turn producing more than one
20759
- // avatar segmentEnd — either multiple segments under one speechId (e.g. a
20760
- // tool call splits the reply into a chunk before and a chunk after the
20761
- // tool result), or multiple speechIds sharing one turnId (a reprocessTurn
20762
- // retry regenerates the whole answer under a new speechId but keeps the
20763
- // same turnId). Only the FIRST resolved segment/attempt for a turnId
20764
- // goes through the queue/counter above; later ones are known to already
20765
- // belong to a claimed turn, so they're emitted immediately without
20766
- // consuming another slot.
20750
+ // delta.type:
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.
20757
+ // - 'avatar' / 'avatar-filler' — chat text, chunked by speechId, closed by
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.
20767
20761
  //
20768
- // Per real turn, the emitted order ends up being: UserVoiceTurnReceived then
20769
- // AvatarChatEntryReceived (see appendUserChatEntry/appendAvatarChatEntry,
20770
- // which just push onto context.chatLog in the order events arrive here).
20762
+ // A retry reuses the turnId under a new speechId, tracked independently.
20771
20763
  //
20772
- // Separately, generatingSpeech + agentTurnToTalk-with-empty-transcription
20773
- // handle ONE special case: resumed-thread sessions where the avatar speaks an
20774
- // opening phrase with no prior user turn to pair against — that text is
20775
- // accumulated by speechId and flushed as its own avatar-only entry.
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.
20776
20767
  //
20777
- // Known unresolved risk (backend-side, not fixable from here — see
20778
- // pendingUserTurnCount's comment): the queue/counter can permanently strand
20779
- // or over-count if either stream's event never fires for a turn.
20768
+ // Opening phrase (resumed thread, avatar speaks first): never sent as a
20769
+ // normal turnId-keyed segment — `init_response` fires too early (before
20770
+ // this actor's setHandler(), dropped by the pubsub queue reset), so
20771
+ // `generatingSpeech` is the text source instead, flushed by
20772
+ // `agentTurnToTalk` (userTranscription === ''). BLANK_TEXT_MARKER is the
20773
+ // server's "no opening phrase configured" sentinel, skipped.
20780
20774
  const onChatLog = fromCallback(({
20781
20775
  input,
20782
20776
  sendBack
20783
20777
  }) => {
20784
- // Accumulate chunks by speechId while avatar is speaking
20778
+ // Accumulate avatar text by `${turnId}:${speechId}` — each speechId
20779
+ // closes independently (e.g. a filler segment vs the main answer).
20785
20780
  const pending = new Map();
20786
- // speechId of the turn currently accumulating chunks.
20787
- // When a new speechId is seen, the previous turn's incomplete text is abandoned (interrupted).
20788
- let currentSpeechId = null;
20789
- // Avatar answers whose segmentEnd fired before their matching agentTurnToTalk arrived.
20790
- // Completed avatar answers waiting for their paired agentTurnToTalk.
20791
- const awaitingUserTurnQueue = [];
20792
- // agentTurnToTalk events (with transcription) whose matching avatar answer hasn't
20793
- // completed yet. Talk-vs-avatar-stream ordering isn't fixed — the ASR transcript can
20794
- // beat or lag the avatar's own text stream on a per-turn basis (races harder when the
20795
- // answer carries tool content) — so pairing is done via this queue+counter rather than
20796
- // a single slot, which a later turn's speechId change would otherwise clobber.
20797
- //
20798
- // Known risk (backend, not fixable here — agentTurnToTalk carries no turnId/speechId
20799
- // to correlate by id, only arrival order): both sides can drift out of 1:1 and desync
20800
- // pairing for a later, unrelated turn instead of the intended one.
20801
- // - Queue can strand an entry forever if its own agentTurnToTalk never fires — e.g.
20802
- // conversation-manager's ttv.sayTurn throwing on the first chunk, or the
20803
- // first-sentence punctuation gate in SpeechBuffer.append swallowing a short
20804
- // punctuation-less reply. Raw text still gets forwarded; the talk pipeline doesn't run.
20805
- // - Counter can stay inflated forever if agentTurnToTalk fires but its avatar segment
20806
- // never reaches segmentEnd — e.g. onUserInterrupted not aborting AgentAdapter's loop,
20807
- // dropping buffered avatar text without ever calling finish()/completeStream().
20808
- // Real fix is backend: always run finish()/completeStream() on error/interrupt, and
20809
- // populate turnId on agentTurnToTalk (schema already has the field, unused) so pairing
20810
- // can be done by id instead of arrival order.
20811
- let pendingUserTurnCount = 0;
20812
- // turnIds whose first segment already claimed a pairing slot (queued or emitted).
20813
- // Keyed by turnId, NOT speechId, because a single logical turn can produce more than
20814
- // one speechId:
20815
- // - Genie can stream a reply as multiple avatar segments under one speechId (e.g. a
20816
- // tool call splits the answer into a segment before and a segment after the tool
20817
- // result) — those segments share both speechId and turnId.
20818
- // - A reprocessTurn retry (sendReprocessTurn, triggered by agentRecoveryMonitor or a
20819
- // manual "retry" action) makes the backend regenerate the FULL answer under a NEW
20820
- // speechId while keeping the SAME turnId — confirmed in logs: two distinct speechIds
20821
- // sharing turnId "c83e031d-..." after a reprocessTurn. Guarding on speechId alone
20822
- // would treat the retry's answer as an unrelated new turn.
20823
- // Either way, the extra segment/retry belongs to a turn that's already paired, so it
20824
- // must bypass the queue/counter entirely; otherwise it would consume the next,
20825
- // unrelated user turn's pairing slot and desync every pairing for the rest of the
20826
- // conversation. Intentionally never cleared: a later segment/retry for the same turnId
20827
- // can arrive arbitrarily long after the first (slow tool call, slow retry, other turns
20828
- // in between), so "already claimed" must be remembered for the rest of the session,
20829
- // same lifetime as awaitingUserTurnQueue/pendingUserTurnCount above. Actor-scoped, dies
20830
- // with it.
20831
- const resolvedTurnIds = new Set();
20832
- // Opening phrase accumulation: for resumed-thread sessions where init_response is not sent,
20833
- // generatingSpeech chunks arrive before the first user turn — accumulate them keyed by speechId
20834
- // and flush on the first proactive agentTurnToTalk (empty userTranscription).
20835
- let openingPhraseSpeechId = null;
20836
- const openingPhraseChunks = new Map();
20837
- let openingPhraseEmitted = false;
20838
20781
  let avatarEntrySequence = 0;
20782
+ // turnId -> entry id of its current unanswered exchange - reused across
20783
+ // superseding resends, cleared on a real answer (see top-of-file comment).
20784
+ const openTurnEntryIds = new Map();
20839
20785
  const createAvatarEntryId = (messageId, speechId) => {
20840
20786
  avatarEntrySequence += 1;
20841
20787
  return `${messageId}:${speechId}:${avatarEntrySequence}`;
20842
20788
  };
20843
- const emitQueuedAvatarEntry = () => {
20844
- const entry = awaitingUserTurnQueue.shift();
20845
- if (entry) {
20846
- sendBack({
20847
- type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
20848
- entry
20849
- });
20850
- }
20851
- };
20789
+ // Opening phrase accumulation — see top-of-file comment.
20790
+ let openingPhraseSpeechId = null;
20791
+ let openingPhraseText = '';
20792
+ let openingPhraseSettled = false;
20852
20793
  return createTinyPubsubEventsHandler({
20853
20794
  source: source$a,
20854
20795
  logger: input.logger,
20855
20796
  handlers: {
20856
20797
  agent_raw_text: payload => {
20857
- var _a, _b;
20798
+ var _a, _b, _c;
20858
20799
  try {
20859
20800
  const delta = JSON.parse(payload.delta);
20860
20801
  const result = agentRawTextDeltaSchema.safeParse(delta);
20861
20802
  if (!result.success) return;
20862
20803
  const parsed = result.data;
20863
- // 'avatar-filler' is spoken content too (e.g. "Let me check that for you..." while
20864
- // a tool call runs) — Genie streams it as its own segment ahead of the 'avatar'
20865
- // segment. Without it here, part of what the avatar actually says never reaches
20866
- // the chat log.
20867
- if (parsed.type !== 'avatar' && parsed.type !== 'avatar-filler') return;
20868
- const speechId = payload.speechId;
20869
- // New speech started — abandon prior speech's incomplete accumulation.
20870
- // Does NOT touch awaitingUserTurnQueue/pendingUserTurnCount: those are completed,
20871
- // already-paired-or-waiting turns and must survive across speechId changes.
20872
- if (speechId !== currentSpeechId) {
20873
- pending.delete(currentSpeechId !== null && currentSpeechId !== void 0 ? currentSpeechId : '');
20874
- currentSpeechId = speechId;
20875
- }
20876
- if (typeof parsed.content === 'string' && parsed.content) {
20877
- pending.set(speechId, ((_a = pending.get(speechId)) !== null && _a !== void 0 ? _a : '') + parsed.content);
20878
- }
20879
- if (parsed.segmentEnd) {
20880
- const text = (_b = pending.get(speechId)) !== null && _b !== void 0 ? _b : '';
20881
- pending.delete(speechId);
20882
- if (!text) return;
20883
- const entry = {
20884
- id: createAvatarEntryId(parsed.messageId, speechId),
20885
- role: 'avatar',
20886
- text,
20887
- turnId: speechId
20888
- };
20889
- if (resolvedTurnIds.has(payload.turnId)) {
20890
- // Not this turn's first segment/attempt — its pairing slot is already claimed
20891
- // (multi-segment reply, or a reprocessTurn retry answering the same turnId).
20804
+ const key = `${payload.turnId}:${payload.speechId}`;
20805
+ if (parsed.type === 'think') {
20806
+ if (parsed.human) {
20807
+ const id = (_a = openTurnEntryIds.get(payload.turnId)) !== null && _a !== void 0 ? _a : parsed.messageId;
20808
+ openTurnEntryIds.set(payload.turnId, id);
20892
20809
  sendBack({
20893
- type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
20894
- entry
20895
- });
20896
- return;
20897
- }
20898
- resolvedTurnIds.add(payload.turnId);
20899
- if (pendingUserTurnCount > 0) {
20900
- // agentTurnToTalk for this turn already fired — emit directly
20901
- pendingUserTurnCount -= 1;
20902
- sendBack({
20903
- type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
20904
- entry
20810
+ type: AvatarConnectionMachineEventTypes.UserVoiceTurnReceived,
20811
+ entry: {
20812
+ id,
20813
+ role: 'user',
20814
+ text: parsed.human,
20815
+ turnId: payload.turnId,
20816
+ messageId: parsed.messageId
20817
+ }
20905
20818
  });
20906
- } else {
20907
- // agentTurnToTalk hasn't arrived yet — queue until it does
20908
- awaitingUserTurnQueue.push(entry);
20909
20819
  }
20820
+ return;
20910
20821
  }
20911
- } catch (_c) {
20912
- // invalid JSON delta — ignore
20913
- }
20914
- },
20915
- generatingSpeech: payload => {
20916
- var _a;
20917
- if (openingPhraseEmitted) return;
20918
- // Server sends this literal sentinel instead of real speech when no opening phrase
20919
- // is configured — must not leak into the chat log as visible avatar text.
20920
- if (payload.text === BLANK_TEXT_MARKER) return;
20921
- if (openingPhraseSpeechId !== null && openingPhraseSpeechId !== payload.speechId) return;
20922
- openingPhraseSpeechId = payload.speechId;
20923
- openingPhraseChunks.set(payload.speechId, ((_a = openingPhraseChunks.get(payload.speechId)) !== null && _a !== void 0 ? _a : '') + payload.text);
20924
- },
20925
- agentTurnToTalk: payload => {
20926
- var _a;
20927
- const {
20928
- userTranscription,
20929
- turnId
20930
- } = payload;
20931
- if (!userTranscription) {
20932
- // Proactive turn — flush opening phrase built from generatingSpeech chunks if present
20933
- if (!openingPhraseEmitted && openingPhraseSpeechId) {
20934
- const text = (_a = openingPhraseChunks.get(openingPhraseSpeechId)) !== null && _a !== void 0 ? _a : '';
20935
- openingPhraseChunks.clear();
20936
- openingPhraseEmitted = true;
20822
+ if (parsed.type === 'user-interruption') {
20823
+ openTurnEntryIds.delete(payload.turnId);
20824
+ const text = pending.get(key);
20825
+ pending.delete(key);
20937
20826
  if (text) {
20938
20827
  sendBack({
20939
20828
  type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
20940
20829
  entry: {
20941
- id: openingPhraseSpeechId,
20830
+ id: createAvatarEntryId(parsed.messageId, payload.speechId),
20942
20831
  role: 'avatar',
20943
20832
  text,
20944
- turnId: 'init_response'
20833
+ turnId: payload.turnId,
20834
+ messageId: parsed.messageId
20945
20835
  }
20946
20836
  });
20947
20837
  }
20838
+ return;
20948
20839
  }
20949
- // Also emit any queued avatar entry left over from the agent_raw_text path
20950
- emitQueuedAvatarEntry();
20840
+ if (parsed.type !== 'avatar' && parsed.type !== 'avatar-filler') return;
20841
+ if (typeof parsed.content === 'string' && parsed.content) {
20842
+ pending.set(key, ((_b = pending.get(key)) !== null && _b !== void 0 ? _b : '') + parsed.content);
20843
+ }
20844
+ if (parsed.segmentEnd) {
20845
+ const text = (_c = pending.get(key)) !== null && _c !== void 0 ? _c : '';
20846
+ pending.delete(key);
20847
+ if (!text) return;
20848
+ // Real answer closes the exchange - a later reuse of this
20849
+ // turnId (e.g. repeated "=") must get a fresh entry, not this one.
20850
+ if (parsed.type === 'avatar') {
20851
+ openTurnEntryIds.delete(payload.turnId);
20852
+ }
20853
+ sendBack({
20854
+ type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
20855
+ entry: {
20856
+ id: createAvatarEntryId(parsed.messageId, payload.speechId),
20857
+ role: 'avatar',
20858
+ text,
20859
+ turnId: payload.turnId,
20860
+ messageId: parsed.messageId
20861
+ }
20862
+ });
20863
+ }
20864
+ } catch (_d) {
20865
+ // invalid JSON delta — ignore
20866
+ }
20867
+ },
20868
+ generatingSpeech: payload => {
20869
+ if (openingPhraseSettled) return;
20870
+ // No opening phrase configured — must not leak into the chat log.
20871
+ if (payload.text === BLANK_TEXT_MARKER) return;
20872
+ if (openingPhraseSpeechId !== null && openingPhraseSpeechId !== payload.speechId) {
20951
20873
  return;
20952
20874
  }
20953
- openingPhraseEmitted = true;
20954
- openingPhraseChunks.clear();
20955
- // Emit user first, then pair with its avatar answer — whichever side finished
20956
- // streaming first (queue has it) or hasn't yet (bump the counter for agent_raw_text
20957
- // to consume once its segmentEnd arrives)
20875
+ openingPhraseSpeechId = payload.speechId;
20876
+ openingPhraseText += payload.text;
20877
+ },
20878
+ agentTurnToTalk: payload => {
20879
+ // Only the first occurrence decides the opening — later turns also fire this.
20880
+ if (openingPhraseSettled) return;
20881
+ openingPhraseSettled = true;
20882
+ if (payload.userTranscription || !openingPhraseText) return;
20958
20883
  sendBack({
20959
- type: AvatarConnectionMachineEventTypes.UserVoiceTurnReceived,
20884
+ type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
20960
20885
  entry: {
20961
- id: turnId !== null && turnId !== void 0 ? turnId : crypto.randomUUID(),
20962
- role: 'user',
20963
- text: userTranscription,
20964
- turnId
20886
+ id: openingPhraseSpeechId !== null && openingPhraseSpeechId !== void 0 ? openingPhraseSpeechId : 'init_response',
20887
+ role: 'avatar',
20888
+ text: openingPhraseText,
20889
+ turnId: 'init_response'
20965
20890
  }
20966
20891
  });
20967
- if (awaitingUserTurnQueue.length > 0) {
20968
- emitQueuedAvatarEntry();
20969
- } else {
20970
- pendingUserTurnCount += 1;
20971
- }
20972
20892
  }
20973
20893
  }
20974
20894
  });
@@ -23290,8 +23210,9 @@ const avatarConnectionMachine = setup({
23290
23210
  logAgentClientToolCallResult,
23291
23211
  recordClientToolCallLogEntry,
23292
23212
  appendAvatarChatEntry,
23213
+ removeStaleChatEntriesForRetry,
23293
23214
  appendUserChatEntry,
23294
- appendUserTextEntry,
23215
+ trackInternalAskEcho,
23295
23216
  setScreenShareActive,
23296
23217
  setScreenShareStream,
23297
23218
  showScreenShareErrorNotification,
@@ -23463,7 +23384,7 @@ const avatarConnectionMachine = setup({
23463
23384
  content: null,
23464
23385
  chatLog: [],
23465
23386
  clientToolCallLog: [],
23466
- pendingTextInputs: [],
23387
+ awaitingInternalAskEcho: false,
23467
23388
  userAgent: '',
23468
23389
  userAgentHints: null,
23469
23390
  kalturaAnalyticsService: input.kalturaAnalyticsService,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaltura-sdk/avatars",
3
- "version": "1.15.5",
3
+ "version": "1.16.1",
4
4
  "author": "kaltura",
5
5
  "license": "AGPL-3.0",
6
6
  "repository": {
@@ -13,7 +13,7 @@
13
13
  "@kaltura-sdk/rtc-core": "^1.25.21",
14
14
  "@local/genie-shared-utils": "^1.0.0",
15
15
  "@unisphere/core": "^1.99.0",
16
- "@unisphere/genie-types": "^1.39.1",
16
+ "@unisphere/genie-types": "^1.39.3",
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 removeStaleChatEntriesForRetry: AvatarConnectionMachineAction;
@@ -0,0 +1,4 @@
1
+ import type { ActionFunction, MachineContext } from 'xstate';
2
+ export declare function asHarnessAction<TContext extends MachineContext, TEvent extends {
3
+ type: string;
4
+ }>(action: unknown): ActionFunction<TContext, TEvent, TEvent, undefined, never, never, never, never, never>;
@@ -0,0 +1,2 @@
1
+ import { AvatarConnectionMachineAction } from '../../types';
2
+ export declare const trackInternalAskEcho: AvatarConnectionMachineAction;
@@ -391,11 +391,14 @@ export declare const avatarConnectionMachine: import("xstate").StateMachine<Avat
391
391
  } | {
392
392
  type: "appendAvatarChatEntry";
393
393
  params: unknown;
394
+ } | {
395
+ type: "removeStaleChatEntriesForRetry";
396
+ params: unknown;
394
397
  } | {
395
398
  type: "appendUserChatEntry";
396
399
  params: unknown;
397
400
  } | {
398
- type: "appendUserTextEntry";
401
+ type: "trackInternalAskEcho";
399
402
  params: unknown;
400
403
  } | {
401
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;
@@ -728,13 +728,13 @@ export declare const connectedState: {
728
728
  actions: "resetAgentRecoveryRetryCount";
729
729
  } | {
730
730
  guard: "hasMoreRecoveryRetries";
731
- actions: readonly ["incrementAgentRecoveryRetryCount", "sendReprocessTurn"];
731
+ actions: readonly ["removeStaleChatEntriesForRetry", "incrementAgentRecoveryRetryCount", "sendReprocessTurn"];
732
732
  } | {
733
733
  guard: "justExceededRecoveryRetries";
734
734
  actions: readonly ["incrementAgentRecoveryRetryCount", "showAgentRecoveryExhaustedNotification"];
735
735
  })[];
736
736
  RequestAgentTurnRecovery: {
737
- actions: "sendReprocessTurn";
737
+ actions: readonly ["removeStaleChatEntriesForRetry", "sendReprocessTurn"];
738
738
  };
739
739
  };
740
740
  invoke: {
@@ -6,13 +6,13 @@ export declare const agentRecoveryMonitorState: {
6
6
  actions: "resetAgentRecoveryRetryCount";
7
7
  } | {
8
8
  guard: "hasMoreRecoveryRetries";
9
- actions: readonly ["incrementAgentRecoveryRetryCount", "sendReprocessTurn"];
9
+ actions: readonly ["removeStaleChatEntriesForRetry", "incrementAgentRecoveryRetryCount", "sendReprocessTurn"];
10
10
  } | {
11
11
  guard: "justExceededRecoveryRetries";
12
12
  actions: readonly ["incrementAgentRecoveryRetryCount", "showAgentRecoveryExhaustedNotification"];
13
13
  })[];
14
14
  RequestAgentTurnRecovery: {
15
- actions: "sendReprocessTurn";
15
+ actions: readonly ["removeStaleChatEntriesForRetry", "sendReprocessTurn"];
16
16
  };
17
17
  };
18
18
  invoke: {
@@ -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,7 +66,7 @@ export interface AvatarConnectionMachineContext {
66
66
  content: UnisphereChatBlock | null;
67
67
  chatLog: ChatEntry[];
68
68
  clientToolCallLog: ClientToolCallLogEntry[];
69
- pendingTextInputs: string[];
69
+ awaitingInternalAskEcho: boolean;
70
70
  isAgentTalking: boolean;
71
71
  userAgent: string;
72
72
  userAgentHints: Record<string, string> | null;
@@ -69,6 +69,7 @@ export declare const agentRawTextDeltaSchema: z.ZodMiniObject<{
69
69
  role: z.ZodMiniOptional<z.ZodMiniString<string>>;
70
70
  type: z.ZodMiniOptional<z.ZodMiniString<string>>;
71
71
  content: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniUnknown>]>>;
72
+ human: z.ZodMiniOptional<z.ZodMiniString<string>>;
72
73
  segmentNumber: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
73
74
  et: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
74
75
  segmentStart: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
@@ -86,6 +87,7 @@ export declare const agentRawTextClientToolDeltaSchema: z.ZodMiniObject<{
86
87
  threadId: z.ZodMiniOptional<z.ZodMiniString<string>>;
87
88
  role: z.ZodMiniOptional<z.ZodMiniString<string>>;
88
89
  content: z.ZodMiniOptional<z.ZodMiniUnion<readonly [z.ZodMiniString<string>, z.ZodMiniRecord<z.ZodMiniString<string>, z.ZodMiniUnknown>]>>;
90
+ human: z.ZodMiniOptional<z.ZodMiniString<string>>;
89
91
  segmentNumber: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
90
92
  et: z.ZodMiniOptional<z.ZodMiniNumber<number>>;
91
93
  segmentStart: z.ZodMiniOptional<z.ZodMiniBoolean<boolean>>;
@@ -1,2 +0,0 @@
1
- import { AvatarConnectionMachineAction } from '../../types';
2
- export declare const appendUserTextEntry: AvatarConnectionMachineAction;