@kaltura-sdk/avatars 1.15.4 → 1.16.0

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
@@ -2,10 +2,10 @@ import { UnisphereLoggerFactory, TinyDataStore, TinyPubSub, performanceMonitor }
2
2
  import { getPartnerIdFromKs } from '@unisphere/runtime-js';
3
3
  import { DisconnectReasons, getAvatarDefaultCapabilities, resolveActiveContextType, AvatarConnectionStatus } from '@unisphere/genie-types';
4
4
  import { z as z$1 } from 'zod/mini';
5
+ import { AlertIconEnum } from '@unisphere/notifications-core';
5
6
  import { initializeLogsCollector, EselfSession, MediaTrackState, sendLogs } from '@kaltura-sdk/rtc-avatar';
6
7
  import { produce } from 'immer';
7
8
  import * as yaml from 'js-yaml';
8
- import { AlertIconEnum, NotificationTypes } from '@unisphere/notifications-core';
9
9
  import { ApplicationPageTypes, ApplicationEventTypes, AnalyticsBucket } from '@unisphere/runtime';
10
10
  import { executeClientToolCall, sendClientToolResponse } from '@local/genie-shared-utils';
11
11
 
@@ -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()),
@@ -6260,6 +6263,48 @@ const stvSpeechChunkSchema = z$1.object({
6260
6263
  durationMs: z$1.number(),
6261
6264
  speechId: z$1.string()
6262
6265
  });
6266
+ const unsupportedClientSchema = z$1.object({
6267
+ code: z$1.optional(z$1.string())
6268
+ });
6269
+
6270
+ // Codes for notifications that are not tied to a connection disconnect reason.
6271
+ // Keep these string-stable: consumers of `externalActions.showNotification`
6272
+ // match on `code` to look up their own localized copy.
6273
+ var AvatarNotificationCode;
6274
+ (function (AvatarNotificationCode) {
6275
+ AvatarNotificationCode["ScreenShareError"] = "ScreenShareError";
6276
+ AvatarNotificationCode["AgentRecoveryExhausted"] = "AgentRecoveryExhausted";
6277
+ AvatarNotificationCode["ConversationTimeWarning"] = "ConversationTimeWarning";
6278
+ })(AvatarNotificationCode || (AvatarNotificationCode = {}));
6279
+ const disconnectReasonVariantMap = {
6280
+ [DisconnectReasons.ConnectionError]: AlertIconEnum.Error,
6281
+ [DisconnectReasons.ExceededTierLimits]: AlertIconEnum.Warning,
6282
+ [DisconnectReasons.NoAgentsAvailable]: AlertIconEnum.Warning,
6283
+ [DisconnectReasons.UserMediaInitializationFailed]: AlertIconEnum.Error,
6284
+ [DisconnectReasons.UserRequested]: AlertIconEnum.Success,
6285
+ [DisconnectReasons.WebsocketConnectionFailed]: AlertIconEnum.Error,
6286
+ [DisconnectReasons.WebsocketDisconnected]: AlertIconEnum.Warning,
6287
+ [DisconnectReasons.PeerRemoved]: AlertIconEnum.Warning,
6288
+ [DisconnectReasons.BadRequest]: AlertIconEnum.Error,
6289
+ [DisconnectReasons.ASRConnectionFailed]: AlertIconEnum.Warning,
6290
+ [DisconnectReasons.DestroyFailed]: AlertIconEnum.Error,
6291
+ [DisconnectReasons.ConversationEnded]: AlertIconEnum.Warning,
6292
+ [DisconnectReasons.UnknownReason]: AlertIconEnum.Error,
6293
+ [DisconnectReasons.DevicesPermissionDenied]: AlertIconEnum.Warning,
6294
+ [DisconnectReasons.AgentResponseTimeout]: AlertIconEnum.Warning,
6295
+ [DisconnectReasons.UnsupportedClient]: AlertIconEnum.Error,
6296
+ [DisconnectReasons.JoinRoomTimeout]: AlertIconEnum.Warning,
6297
+ [DisconnectReasons.ConnectionTimeout]: AlertIconEnum.Warning,
6298
+ [DisconnectReasons.PlayerConnectionFailed]: AlertIconEnum.Error
6299
+ };
6300
+ const defaultDisconnectVariant = AlertIconEnum.Error;
6301
+ function isMappedAvatarDisconnectReason(reason) {
6302
+ return !!disconnectReasonVariantMap[reason];
6303
+ }
6304
+ function getAvatarDisconnectNotificationVariant(reason) {
6305
+ var _a;
6306
+ return (_a = disconnectReasonVariantMap[reason]) !== null && _a !== void 0 ? _a : defaultDisconnectVariant;
6307
+ }
6263
6308
 
6264
6309
  const AvatarConnectionMachineId = 'AvatarClient';
6265
6310
 
@@ -6983,13 +7028,13 @@ const agentRecoveryMonitorState = {
6983
7028
  actions: 'resetAgentRecoveryRetryCount'
6984
7029
  }, {
6985
7030
  guard: 'hasMoreRecoveryRetries',
6986
- actions: ['incrementAgentRecoveryRetryCount', 'sendReprocessTurn']
7031
+ actions: ['removeStaleChatEntriesForRetry', 'incrementAgentRecoveryRetryCount', 'sendReprocessTurn']
6987
7032
  }, {
6988
7033
  guard: 'justExceededRecoveryRetries',
6989
7034
  actions: ['incrementAgentRecoveryRetryCount', 'showAgentRecoveryExhaustedNotification']
6990
7035
  }],
6991
7036
  [AvatarConnectionMachineEventTypes.RequestAgentTurnRecovery]: {
6992
- actions: 'sendReprocessTurn'
7037
+ actions: ['removeStaleChatEntriesForRetry', 'sendReprocessTurn']
6993
7038
  }
6994
7039
  },
6995
7040
  invoke: {
@@ -19259,97 +19304,6 @@ const setDisconnectData = assign$1(({
19259
19304
  });
19260
19305
  });
19261
19306
 
19262
- const avatarConnectionNotificationsMap = {
19263
- [DisconnectReasons.ConnectionError]: {
19264
- text: 'There was a problem connecting to the server. Please try again later.',
19265
- variant: AlertIconEnum.Error
19266
- },
19267
- [DisconnectReasons.ExceededTierLimits]: {
19268
- text: 'You have exceeded your usage limits. Please upgrade your plan or try again later.',
19269
- variant: AlertIconEnum.Warning
19270
- },
19271
- [DisconnectReasons.NoAgentsAvailable]: {
19272
- text: 'No agents are currently available. Please try again later.',
19273
- variant: AlertIconEnum.Warning
19274
- },
19275
- [DisconnectReasons.UserMediaInitializationFailed]: {
19276
- text: 'Unable to access your microphone or camera. Please check your device settings.',
19277
- variant: AlertIconEnum.Error
19278
- },
19279
- [DisconnectReasons.UserRequested]: {
19280
- text: 'You have ended the session.',
19281
- variant: AlertIconEnum.Success
19282
- },
19283
- [DisconnectReasons.WebsocketConnectionFailed]: {
19284
- text: 'WebSocket connection failed. Please check your internet connection and try again.',
19285
- variant: AlertIconEnum.Error
19286
- },
19287
- [DisconnectReasons.WebsocketDisconnected]: {
19288
- text: 'The connection was lost. Please try reconnecting.',
19289
- variant: AlertIconEnum.Warning
19290
- },
19291
- [DisconnectReasons.PeerRemoved]: {
19292
- text: 'The session has ended as the peer has disconnected.',
19293
- variant: AlertIconEnum.Warning
19294
- },
19295
- [DisconnectReasons.BadRequest]: {
19296
- text: 'There was a bad request error. Please verify your settings and try again.',
19297
- variant: AlertIconEnum.Error
19298
- },
19299
- [DisconnectReasons.ASRConnectionFailed]: {
19300
- text: 'Voice Activity Detection connection failed. Some features may not work properly.',
19301
- variant: AlertIconEnum.Warning
19302
- },
19303
- [DisconnectReasons.DestroyFailed]: {
19304
- text: 'An error occurred while ending the session. Please try again later.',
19305
- variant: AlertIconEnum.Error
19306
- },
19307
- [DisconnectReasons.ConversationEnded]: {
19308
- text: 'The conversation has ended.',
19309
- variant: AlertIconEnum.Warning
19310
- },
19311
- [DisconnectReasons.UnknownReason]: {
19312
- text: 'An unknown error occurred. Please try again later or contact support.',
19313
- variant: AlertIconEnum.Error
19314
- },
19315
- [DisconnectReasons.DevicesPermissionDenied]: {
19316
- text: 'Microphone access was denied. Please allow microphone permissions and try again.',
19317
- variant: AlertIconEnum.Warning
19318
- },
19319
- [DisconnectReasons.AgentResponseTimeout]: {
19320
- text: 'The agent took too long to respond. Please try again later.',
19321
- variant: AlertIconEnum.Warning
19322
- },
19323
- [DisconnectReasons.UnsupportedClient]: {
19324
- text: 'The client you are using is not supported. Please contact support.',
19325
- variant: AlertIconEnum.Error
19326
- },
19327
- [DisconnectReasons.JoinRoomTimeout]: {
19328
- text: 'Joining the room timed out. Please check your connection and try again.',
19329
- variant: AlertIconEnum.Warning
19330
- },
19331
- [DisconnectReasons.ConnectionTimeout]: {
19332
- text: 'The connection timed out. Please check your internet connection and try again.',
19333
- variant: AlertIconEnum.Warning
19334
- },
19335
- [DisconnectReasons.PlayerConnectionFailed]: {
19336
- text: 'A playback error occurred while connecting to the avatar. Please try again.',
19337
- variant: AlertIconEnum.Error
19338
- }
19339
- };
19340
- const defaultDisconnectNotification = {
19341
- text: 'An unknown error occurred. Please try again later or contact support.',
19342
- variant: AlertIconEnum.Error
19343
- };
19344
- function isMappedAvatarDisconnectReason(reason) {
19345
- return !!avatarConnectionNotificationsMap[reason];
19346
- }
19347
- function getAvatarConnectionNotification(reason) {
19348
- return Object.assign({
19349
- type: NotificationTypes.Alert
19350
- }, avatarConnectionNotificationsMap[reason] || defaultDisconnectNotification);
19351
- }
19352
-
19353
19307
  const showDisconnectNotification = ({
19354
19308
  context
19355
19309
  }) => {
@@ -19377,7 +19331,10 @@ const showDisconnectNotification = ({
19377
19331
  }
19378
19332
  });
19379
19333
  }
19380
- const notification = getAvatarConnectionNotification(context.disconnectData.reason);
19334
+ const notification = {
19335
+ code: context.disconnectData.reason,
19336
+ variant: getAvatarDisconnectNotificationVariant(context.disconnectData.reason)
19337
+ };
19381
19338
  try {
19382
19339
  context.externalActions.showNotification(notification);
19383
19340
  } catch (error) {
@@ -19428,6 +19385,7 @@ const onConversationEnded = fromCallback(({
19428
19385
  });
19429
19386
  });
19430
19387
 
19388
+ const USAGE_LIMIT_EXCEEDED_CODE = 'USAGE_LIMIT_EXCEEDED';
19431
19389
  const source$g = createTinyPubsubEventsSource(websocketMessage, {
19432
19390
  disconnect: {
19433
19391
  schema: null
@@ -19451,7 +19409,7 @@ const source$g = createTinyPubsubEventsSource(websocketMessage, {
19451
19409
  schema: null
19452
19410
  },
19453
19411
  unsupportedClient: {
19454
- schema: null
19412
+ schema: unsupportedClientSchema
19455
19413
  }
19456
19414
  }, {
19457
19415
  queueWhilePending: true
@@ -19480,7 +19438,10 @@ const onError = fromCallback(({
19480
19438
  throwToNoAgent: createHandler(DisconnectReasons.NoAgentsAvailable),
19481
19439
  throwToExceededTier: createHandler(DisconnectReasons.ExceededTierLimits),
19482
19440
  throwToBadRequest: createHandler(DisconnectReasons.BadRequest),
19483
- unsupportedClient: createHandler(DisconnectReasons.UnsupportedClient)
19441
+ unsupportedClient: event => {
19442
+ const reason = (event === null || event === void 0 ? void 0 : event.code) === USAGE_LIMIT_EXCEEDED_CODE ? DisconnectReasons.ExceededTierLimits : DisconnectReasons.UnsupportedClient;
19443
+ createHandler(reason)(event);
19444
+ }
19484
19445
  }
19485
19446
  });
19486
19447
  });
@@ -20407,9 +20368,11 @@ const showTimeWarningNotification = ({
20407
20368
  }) => {
20408
20369
  assertEvent(event, AvatarConnectionMachineEventTypes.ConversationTimeWarningReceived);
20409
20370
  const notification = {
20410
- type: NotificationTypes.Alert,
20411
- text: `Your conversation will end in ${event.payload.remainingTime} seconds.`,
20371
+ code: AvatarNotificationCode.ConversationTimeWarning,
20412
20372
  variant: AlertIconEnum.Warning,
20373
+ params: {
20374
+ remainingTime: event.payload.remainingTime
20375
+ },
20413
20376
  options: {
20414
20377
  duration: event.payload.remainingTime,
20415
20378
  showClose: true
@@ -20660,9 +20623,51 @@ const appendAvatarChatEntry = assign$1(({
20660
20623
  context,
20661
20624
  event
20662
20625
  }) => {
20626
+ var _a, _b;
20663
20627
  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
+ return {
20650
+ chatLog: [...context.chatLog, event.entry],
20651
+ pendingTextInput
20652
+ };
20653
+ });
20654
+
20655
+ // A retry re-runs the same turn (same userInput -> same turnId) under a new
20656
+ // speechId/messageId, re-emitting both the user's `human` segment and the
20657
+ // avatar answer — both go stale, not just the avatar one.
20658
+ //
20659
+ // Matched on messageId, not turnId: the server can reuse the same turnId
20660
+ // across distinct, already-answered exchanges (e.g. repeated "="), so
20661
+ // turnId would wipe an unrelated earlier exchange too. messageId is
20662
+ // constant per exchange and never reused (confirmed against
20663
+ // kaltura_agent_executer.py), so it removes exactly the retried exchange.
20664
+ const removeStaleChatEntriesForRetry = assign$1(({
20665
+ context,
20666
+ event
20667
+ }) => {
20668
+ assertEvent(event, [AvatarConnectionMachineEventTypes.RequestAgentTurnRecovery, AvatarConnectionMachineEventTypes.AgentRecoveryTurnCompleted]);
20664
20669
  return {
20665
- chatLog: [...context.chatLog, event.entry]
20670
+ chatLog: context.chatLog.filter(entry => entry.messageId !== event.messageId)
20666
20671
  };
20667
20672
  });
20668
20673
 
@@ -20670,18 +20675,39 @@ const appendUserChatEntry = assign$1(({
20670
20675
  context,
20671
20676
  event
20672
20677
  }) => {
20678
+ var _a;
20673
20679
  assertEvent(event, AvatarConnectionMachineEventTypes.UserVoiceTurnReceived);
20674
- const idx = context.pendingTextInputs.indexOf(event.entry.text);
20675
- if (idx !== -1) {
20676
- // Already added optimistically via text input — consume the pending entry and skip
20677
- const pendingTextInputs = [...context.pendingTextInputs];
20678
- pendingTextInputs.splice(idx, 1);
20680
+ const pending = context.pendingTextInput;
20681
+ if (!pending) {
20679
20682
  return {
20680
- pendingTextInputs
20683
+ chatLog: [...context.chatLog, event.entry]
20681
20684
  };
20682
20685
  }
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.
20683
20704
  return {
20684
- chatLog: [...context.chatLog, event.entry]
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
+ }
20685
20711
  };
20686
20712
  });
20687
20713
 
@@ -20694,12 +20720,70 @@ const appendUserTextEntry = assign$1(({
20694
20720
  text,
20695
20721
  internal
20696
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
+ }
20697
20776
  // raiseAskViaTextKickoff marks its auto-sent ask `internal` — it's a bootstrap prompt,
20698
- // not something the user said, so keep it out of the visible chat log. Still record it
20699
- // in pendingTextInputs so the ASR echo of it (agentTurnToTalk) is deduped the same way.
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`.
20700
20780
  if (internal) {
20701
20781
  return {
20702
- pendingTextInputs: [...context.pendingTextInputs, text]
20782
+ pendingTextInput: {
20783
+ id: null,
20784
+ turnId: null,
20785
+ text
20786
+ }
20703
20787
  };
20704
20788
  }
20705
20789
  const entry = {
@@ -20709,7 +20793,11 @@ const appendUserTextEntry = assign$1(({
20709
20793
  };
20710
20794
  return {
20711
20795
  chatLog: [...context.chatLog, entry],
20712
- pendingTextInputs: [...context.pendingTextInputs, text]
20796
+ pendingTextInput: {
20797
+ id: entry.id,
20798
+ turnId: null,
20799
+ text
20800
+ }
20713
20801
  };
20714
20802
  });
20715
20803
 
@@ -20760,135 +20848,64 @@ const source$a = createTinyPubsubEventsSource(websocketMessage, {
20760
20848
  agent_raw_text: {
20761
20849
  schema: agentRawTextSchema
20762
20850
  },
20763
- agentTurnToTalk: {
20764
- schema: agentTurnToTalkSchema
20765
- },
20766
20851
  generatingSpeech: {
20767
20852
  schema: generatingSpeechSchema
20853
+ },
20854
+ agentTurnToTalk: {
20855
+ schema: agentTurnToTalkSchema
20768
20856
  }
20769
20857
  }, {
20770
20858
  queueWhilePending: true
20771
20859
  });
20772
- // FLOW OVERVIEW
20773
- // =============
20774
- // Builds the alternating user/avatar chat log from two independent, unordered
20775
- // websocket event streams that share no common id:
20776
- // - agentTurnToTalk: fires once per USER turn, carries the ASR transcript
20777
- // (userTranscription). Empty userTranscription means a proactive/opening
20778
- // turn (avatar speaks first, no preceding user input).
20779
- // - agent_raw_text: streams the AVATAR's reply for a turn as chunked deltas
20780
- // keyed by speechId, terminated by a delta with segmentEnd=true.
20781
- // delta.type === 'avatar' or 'avatar-filler' chunks are chat text (filler
20782
- // is spoken content too, e.g. "Let me check that for you..." streamed
20783
- // ahead of the main answer while a tool call runs); 'think'/'tool'/'share'
20784
- // chunks are filtered out here.
20860
+ // Builds alternating user/avatar chat log from agent_raw_text, keyed by turnId.
20785
20861
  //
20786
- // The two streams race: for a given turn, agentTurnToTalk can arrive before,
20787
- // after, or interleaved with that turn's agent_raw_text segmentEnd. Neither
20788
- // event carries a shared id to correlate by (agentTurnToTalk has no speechId;
20789
- // see the pendingUserTurnCount comment below), so pairing is done by arrival
20790
- // order via a queue + counter:
20791
- // - awaitingUserTurnQueue: avatar answers that finished (segmentEnd) before
20792
- // their turn's agentTurnToTalk showed up. FIFO — first queued avatar
20793
- // answer is paired with the next agentTurnToTalk that needs one.
20794
- // - pendingUserTurnCount: agentTurnToTalk arrivals still waiting on their
20795
- // avatar answer to finish. Incremented when a talk event arrives with an
20796
- // empty queue; decremented when an avatar segmentEnd arrives and directly
20797
- // emits instead of queueing.
20798
- // - resolvedTurnIds: guards against a single turn producing more than one
20799
- // avatar segmentEnd — either multiple segments under one speechId (e.g. a
20800
- // tool call splits the reply into a chunk before and a chunk after the
20801
- // tool result), or multiple speechIds sharing one turnId (a reprocessTurn
20802
- // retry regenerates the whole answer under a new speechId but keeps the
20803
- // same turnId). Only the FIRST resolved segment/attempt for a turnId
20804
- // goes through the queue/counter above; later ones are known to already
20805
- // belong to a claimed turn, so they're emitted immediately without
20806
- // consuming another slot.
20862
+ // 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.
20869
+ // - '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.
20807
20875
  //
20808
- // Per real turn, the emitted order ends up being: UserVoiceTurnReceived then
20809
- // AvatarChatEntryReceived (see appendUserChatEntry/appendAvatarChatEntry,
20810
- // which just push onto context.chatLog in the order events arrive here).
20876
+ // A retry reuses the same turnId under a new speechId; each speechId's
20877
+ // segmentEnd/user-interruption is tracked/flushed independently.
20811
20878
  //
20812
- // Separately, generatingSpeech + agentTurnToTalk-with-empty-transcription
20813
- // handle ONE special case: resumed-thread sessions where the avatar speaks an
20814
- // opening phrase with no prior user turn to pair against — that text is
20815
- // accumulated by speechId and flushed as its own avatar-only entry.
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.
20816
20883
  //
20817
- // Known unresolved risk (backend-side, not fixable from here — see
20818
- // pendingUserTurnCount's comment): the queue/counter can permanently strand
20819
- // or over-count if either stream's event never fires for a turn.
20884
+ // 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.
20820
20892
  const onChatLog = fromCallback(({
20821
20893
  input,
20822
20894
  sendBack
20823
20895
  }) => {
20824
- // Accumulate chunks by speechId while avatar is speaking
20896
+ // Accumulate avatar text by `${turnId}:${speechId}` — each speechId
20897
+ // closes independently (e.g. a filler segment vs the main answer).
20825
20898
  const pending = new Map();
20826
- // speechId of the turn currently accumulating chunks.
20827
- // When a new speechId is seen, the previous turn's incomplete text is abandoned (interrupted).
20828
- let currentSpeechId = null;
20829
- // Avatar answers whose segmentEnd fired before their matching agentTurnToTalk arrived.
20830
- // Completed avatar answers waiting for their paired agentTurnToTalk.
20831
- const awaitingUserTurnQueue = [];
20832
- // agentTurnToTalk events (with transcription) whose matching avatar answer hasn't
20833
- // completed yet. Talk-vs-avatar-stream ordering isn't fixed — the ASR transcript can
20834
- // beat or lag the avatar's own text stream on a per-turn basis (races harder when the
20835
- // answer carries tool content) — so pairing is done via this queue+counter rather than
20836
- // a single slot, which a later turn's speechId change would otherwise clobber.
20837
- //
20838
- // Known risk (backend, not fixable here — agentTurnToTalk carries no turnId/speechId
20839
- // to correlate by id, only arrival order): both sides can drift out of 1:1 and desync
20840
- // pairing for a later, unrelated turn instead of the intended one.
20841
- // - Queue can strand an entry forever if its own agentTurnToTalk never fires — e.g.
20842
- // conversation-manager's ttv.sayTurn throwing on the first chunk, or the
20843
- // first-sentence punctuation gate in SpeechBuffer.append swallowing a short
20844
- // punctuation-less reply. Raw text still gets forwarded; the talk pipeline doesn't run.
20845
- // - Counter can stay inflated forever if agentTurnToTalk fires but its avatar segment
20846
- // never reaches segmentEnd — e.g. onUserInterrupted not aborting AgentAdapter's loop,
20847
- // dropping buffered avatar text without ever calling finish()/completeStream().
20848
- // Real fix is backend: always run finish()/completeStream() on error/interrupt, and
20849
- // populate turnId on agentTurnToTalk (schema already has the field, unused) so pairing
20850
- // can be done by id instead of arrival order.
20851
- let pendingUserTurnCount = 0;
20852
- // turnIds whose first segment already claimed a pairing slot (queued or emitted).
20853
- // Keyed by turnId, NOT speechId, because a single logical turn can produce more than
20854
- // one speechId:
20855
- // - Genie can stream a reply as multiple avatar segments under one speechId (e.g. a
20856
- // tool call splits the answer into a segment before and a segment after the tool
20857
- // result) — those segments share both speechId and turnId.
20858
- // - A reprocessTurn retry (sendReprocessTurn, triggered by agentRecoveryMonitor or a
20859
- // manual "retry" action) makes the backend regenerate the FULL answer under a NEW
20860
- // speechId while keeping the SAME turnId — confirmed in logs: two distinct speechIds
20861
- // sharing turnId "c83e031d-..." after a reprocessTurn. Guarding on speechId alone
20862
- // would treat the retry's answer as an unrelated new turn.
20863
- // Either way, the extra segment/retry belongs to a turn that's already paired, so it
20864
- // must bypass the queue/counter entirely; otherwise it would consume the next,
20865
- // unrelated user turn's pairing slot and desync every pairing for the rest of the
20866
- // conversation. Intentionally never cleared: a later segment/retry for the same turnId
20867
- // can arrive arbitrarily long after the first (slow tool call, slow retry, other turns
20868
- // in between), so "already claimed" must be remembered for the rest of the session,
20869
- // same lifetime as awaitingUserTurnQueue/pendingUserTurnCount above. Actor-scoped, dies
20870
- // with it.
20871
- const resolvedTurnIds = new Set();
20872
- // Opening phrase accumulation: for resumed-thread sessions where init_response is not sent,
20873
- // generatingSpeech chunks arrive before the first user turn — accumulate them keyed by speechId
20874
- // and flush on the first proactive agentTurnToTalk (empty userTranscription).
20875
- let openingPhraseSpeechId = null;
20876
- const openingPhraseChunks = new Map();
20877
- let openingPhraseEmitted = false;
20899
+ const seenHumanTurnKeys = new Set();
20878
20900
  let avatarEntrySequence = 0;
20879
20901
  const createAvatarEntryId = (messageId, speechId) => {
20880
20902
  avatarEntrySequence += 1;
20881
20903
  return `${messageId}:${speechId}:${avatarEntrySequence}`;
20882
20904
  };
20883
- const emitQueuedAvatarEntry = () => {
20884
- const entry = awaitingUserTurnQueue.shift();
20885
- if (entry) {
20886
- sendBack({
20887
- type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
20888
- entry
20889
- });
20890
- }
20891
- };
20905
+ // Opening phrase accumulation — see top-of-file comment.
20906
+ let openingPhraseSpeechId = null;
20907
+ let openingPhraseText = '';
20908
+ let openingPhraseSettled = false;
20892
20909
  return createTinyPubsubEventsHandler({
20893
20910
  source: source$a,
20894
20911
  logger: input.logger,
@@ -20900,115 +20917,89 @@ const onChatLog = fromCallback(({
20900
20917
  const result = agentRawTextDeltaSchema.safeParse(delta);
20901
20918
  if (!result.success) return;
20902
20919
  const parsed = result.data;
20903
- // 'avatar-filler' is spoken content too (e.g. "Let me check that for you..." while
20904
- // a tool call runs) — Genie streams it as its own segment ahead of the 'avatar'
20905
- // segment. Without it here, part of what the avatar actually says never reaches
20906
- // the chat log.
20907
- if (parsed.type !== 'avatar' && parsed.type !== 'avatar-filler') return;
20908
- const speechId = payload.speechId;
20909
- // New speech started — abandon prior speech's incomplete accumulation.
20910
- // Does NOT touch awaitingUserTurnQueue/pendingUserTurnCount: those are completed,
20911
- // already-paired-or-waiting turns and must survive across speechId changes.
20912
- if (speechId !== currentSpeechId) {
20913
- pending.delete(currentSpeechId !== null && currentSpeechId !== void 0 ? currentSpeechId : '');
20914
- currentSpeechId = speechId;
20915
- }
20916
- if (typeof parsed.content === 'string' && parsed.content) {
20917
- pending.set(speechId, ((_a = pending.get(speechId)) !== null && _a !== void 0 ? _a : '') + parsed.content);
20918
- }
20919
- if (parsed.segmentEnd) {
20920
- const text = (_b = pending.get(speechId)) !== null && _b !== void 0 ? _b : '';
20921
- pending.delete(speechId);
20922
- if (!text) return;
20923
- const entry = {
20924
- id: createAvatarEntryId(parsed.messageId, speechId),
20925
- role: 'avatar',
20926
- text,
20927
- turnId: speechId
20928
- };
20929
- if (resolvedTurnIds.has(payload.turnId)) {
20930
- // Not this turn's first segment/attempt — its pairing slot is already claimed
20931
- // (multi-segment reply, or a reprocessTurn retry answering the same turnId).
20920
+ const key = `${payload.turnId}:${payload.speechId}`;
20921
+ if (parsed.type === 'think') {
20922
+ if (parsed.human && !seenHumanTurnKeys.has(key)) {
20923
+ seenHumanTurnKeys.add(key);
20932
20924
  sendBack({
20933
- type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
20934
- entry
20925
+ type: AvatarConnectionMachineEventTypes.UserVoiceTurnReceived,
20926
+ entry: {
20927
+ id: key,
20928
+ role: 'user',
20929
+ text: parsed.human,
20930
+ turnId: payload.turnId,
20931
+ messageId: parsed.messageId
20932
+ }
20935
20933
  });
20936
- return;
20937
20934
  }
20938
- resolvedTurnIds.add(payload.turnId);
20939
- if (pendingUserTurnCount > 0) {
20940
- // agentTurnToTalk for this turn already fired — emit directly
20941
- pendingUserTurnCount -= 1;
20935
+ return;
20936
+ }
20937
+ if (parsed.type === 'user-interruption') {
20938
+ const text = pending.get(key);
20939
+ pending.delete(key);
20940
+ if (text) {
20942
20941
  sendBack({
20943
20942
  type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
20944
- entry
20943
+ entry: {
20944
+ id: createAvatarEntryId(parsed.messageId, payload.speechId),
20945
+ role: 'avatar',
20946
+ text,
20947
+ turnId: payload.turnId,
20948
+ messageId: parsed.messageId
20949
+ }
20945
20950
  });
20946
- } else {
20947
- // agentTurnToTalk hasn't arrived yet — queue until it does
20948
- awaitingUserTurnQueue.push(entry);
20949
20951
  }
20952
+ return;
20953
+ }
20954
+ if (parsed.type !== 'avatar' && parsed.type !== 'avatar-filler') return;
20955
+ if (typeof parsed.content === 'string' && parsed.content) {
20956
+ pending.set(key, ((_a = pending.get(key)) !== null && _a !== void 0 ? _a : '') + parsed.content);
20957
+ }
20958
+ if (parsed.segmentEnd) {
20959
+ const text = (_b = pending.get(key)) !== null && _b !== void 0 ? _b : '';
20960
+ pending.delete(key);
20961
+ if (!text) return;
20962
+ sendBack({
20963
+ type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
20964
+ entry: {
20965
+ id: createAvatarEntryId(parsed.messageId, payload.speechId),
20966
+ role: 'avatar',
20967
+ text,
20968
+ 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'
20973
+ });
20950
20974
  }
20951
20975
  } catch (_c) {
20952
20976
  // invalid JSON delta — ignore
20953
20977
  }
20954
20978
  },
20955
20979
  generatingSpeech: payload => {
20956
- var _a;
20957
- if (openingPhraseEmitted) return;
20958
- // Server sends this literal sentinel instead of real speech when no opening phrase
20959
- // is configured — must not leak into the chat log as visible avatar text.
20980
+ if (openingPhraseSettled) return;
20981
+ // No opening phrase configured — must not leak into the chat log.
20960
20982
  if (payload.text === BLANK_TEXT_MARKER) return;
20961
- if (openingPhraseSpeechId !== null && openingPhraseSpeechId !== payload.speechId) return;
20983
+ if (openingPhraseSpeechId !== null && openingPhraseSpeechId !== payload.speechId) {
20984
+ return;
20985
+ }
20962
20986
  openingPhraseSpeechId = payload.speechId;
20963
- openingPhraseChunks.set(payload.speechId, ((_a = openingPhraseChunks.get(payload.speechId)) !== null && _a !== void 0 ? _a : '') + payload.text);
20987
+ openingPhraseText += payload.text;
20964
20988
  },
20965
20989
  agentTurnToTalk: payload => {
20966
- var _a;
20967
- const {
20968
- userTranscription,
20969
- turnId
20970
- } = payload;
20971
- if (!userTranscription) {
20972
- // Proactive turn — flush opening phrase built from generatingSpeech chunks if present
20973
- if (!openingPhraseEmitted && openingPhraseSpeechId) {
20974
- const text = (_a = openingPhraseChunks.get(openingPhraseSpeechId)) !== null && _a !== void 0 ? _a : '';
20975
- openingPhraseChunks.clear();
20976
- openingPhraseEmitted = true;
20977
- if (text) {
20978
- sendBack({
20979
- type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
20980
- entry: {
20981
- id: openingPhraseSpeechId,
20982
- role: 'avatar',
20983
- text,
20984
- turnId: 'init_response'
20985
- }
20986
- });
20987
- }
20988
- }
20989
- // Also emit any queued avatar entry left over from the agent_raw_text path
20990
- emitQueuedAvatarEntry();
20991
- return;
20992
- }
20993
- openingPhraseEmitted = true;
20994
- openingPhraseChunks.clear();
20995
- // Emit user first, then pair with its avatar answer — whichever side finished
20996
- // streaming first (queue has it) or hasn't yet (bump the counter for agent_raw_text
20997
- // to consume once its segmentEnd arrives)
20990
+ // Only the first occurrence decides the opening — later turns also fire this.
20991
+ if (openingPhraseSettled) return;
20992
+ openingPhraseSettled = true;
20993
+ if (payload.userTranscription || !openingPhraseText) return;
20998
20994
  sendBack({
20999
- type: AvatarConnectionMachineEventTypes.UserVoiceTurnReceived,
20995
+ type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
21000
20996
  entry: {
21001
- id: turnId !== null && turnId !== void 0 ? turnId : crypto.randomUUID(),
21002
- role: 'user',
21003
- text: userTranscription,
21004
- turnId
20997
+ id: openingPhraseSpeechId !== null && openingPhraseSpeechId !== void 0 ? openingPhraseSpeechId : 'init_response',
20998
+ role: 'avatar',
20999
+ text: openingPhraseText,
21000
+ turnId: 'init_response'
21005
21001
  }
21006
21002
  });
21007
- if (awaitingUserTurnQueue.length > 0) {
21008
- emitQueuedAvatarEntry();
21009
- } else {
21010
- pendingUserTurnCount += 1;
21011
- }
21012
21003
  }
21013
21004
  }
21014
21005
  });
@@ -21423,8 +21414,7 @@ const showScreenShareErrorNotification = ({
21423
21414
  }) => {
21424
21415
  try {
21425
21416
  context.externalActions.showNotification({
21426
- type: NotificationTypes.Alert,
21427
- text: 'Screen sharing encountered an error. Please try again.',
21417
+ code: AvatarNotificationCode.ScreenShareError,
21428
21418
  variant: AlertIconEnum.Error
21429
21419
  });
21430
21420
  } catch (error) {
@@ -23287,8 +23277,7 @@ const showAgentRecoveryExhaustedNotification = ({
23287
23277
  context
23288
23278
  }) => {
23289
23279
  const notification = {
23290
- type: NotificationTypes.Alert,
23291
- text: "We're having trouble getting a response. Please try asking again.",
23280
+ code: AvatarNotificationCode.AgentRecoveryExhausted,
23292
23281
  variant: AlertIconEnum.Warning,
23293
23282
  options: {
23294
23283
  showClose: true
@@ -23332,6 +23321,7 @@ const avatarConnectionMachine = setup({
23332
23321
  logAgentClientToolCallResult,
23333
23322
  recordClientToolCallLogEntry,
23334
23323
  appendAvatarChatEntry,
23324
+ removeStaleChatEntriesForRetry,
23335
23325
  appendUserChatEntry,
23336
23326
  appendUserTextEntry,
23337
23327
  setScreenShareActive,
@@ -23505,7 +23495,7 @@ const avatarConnectionMachine = setup({
23505
23495
  content: null,
23506
23496
  chatLog: [],
23507
23497
  clientToolCallLog: [],
23508
- pendingTextInputs: [],
23498
+ pendingTextInput: null,
23509
23499
  userAgent: '',
23510
23500
  userAgentHints: null,
23511
23501
  kalturaAnalyticsService: input.kalturaAnalyticsService,
@@ -24390,4 +24380,4 @@ class AvatarClientManager extends ActorManager {
24390
24380
  }
24391
24381
  }
24392
24382
 
24393
- export { ApplicationFeatures, AvatarClientManager, AvatarConnectionMachineEventTypes, AvatarConnectionMachineId, ButtonNames, PageNames, agentRawTextClientToolDeltaSchema, agentRawTextDeltaSchema, agentRawTextSchema, agentTurnToTalkSchema, askPermissionsSchema, avatarClientLoggerFactory, avatarConnectionMachine, avatarRTCClientManager, clientConfigurationSchema, conversationEndedSchema, conversationTimeWarningSchema, createTinyPubsubEventsHandler, createTinyPubsubEventsSource, generatingSpeechSchema, getQueryParam, initializeLogsCollectionIfNeeded, isMobileDevice, joinCompleteSchema, noAgentSchema, onServerConnectedSchema, pauseExpiryWarningSchema, resetWebsocketClientTeardownState, showAgentSchema, showStartSpeechSchema, stvFinishedTalkingSchema, stvNewSessionSchema, stvSpeechChunkSchema, stvStartedTalkingSchema, timingTracker, websocketClientManager, websocketDebugInfo, websocketMessage, websocketNonRecoveredReconnect, websocketRecoveryState, websocketSystemMessage };
24383
+ export { ApplicationFeatures, AvatarClientManager, AvatarConnectionMachineEventTypes, AvatarConnectionMachineId, AvatarNotificationCode, ButtonNames, PageNames, agentRawTextClientToolDeltaSchema, agentRawTextDeltaSchema, agentRawTextSchema, agentTurnToTalkSchema, askPermissionsSchema, avatarClientLoggerFactory, avatarConnectionMachine, avatarRTCClientManager, clientConfigurationSchema, conversationEndedSchema, conversationTimeWarningSchema, createTinyPubsubEventsHandler, createTinyPubsubEventsSource, generatingSpeechSchema, getAvatarDisconnectNotificationVariant, getQueryParam, initializeLogsCollectionIfNeeded, isMappedAvatarDisconnectReason, isMobileDevice, joinCompleteSchema, noAgentSchema, onServerConnectedSchema, pauseExpiryWarningSchema, resetWebsocketClientTeardownState, showAgentSchema, showStartSpeechSchema, stvFinishedTalkingSchema, stvNewSessionSchema, stvSpeechChunkSchema, stvStartedTalkingSchema, timingTracker, unsupportedClientSchema, websocketClientManager, websocketDebugInfo, websocketMessage, websocketNonRecoveredReconnect, websocketRecoveryState, websocketSystemMessage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaltura-sdk/avatars",
3
- "version": "1.15.4",
3
+ "version": "1.16.0",
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.28",
13
- "@kaltura-sdk/rtc-core": "^1.25.17",
12
+ "@kaltura-sdk/rtc-avatar": "^1.32.34",
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>;
@@ -391,6 +391,9 @@ 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;
@@ -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: {
@@ -1,5 +1,5 @@
1
1
  import { UnisphereLoggerType } from '@unisphere/core';
2
- import { Notifications } from '@unisphere/notifications-core';
2
+ import { AvatarNotificationRequest } from './notifications';
3
3
  import { clientConfigurationSchema } from './schemas';
4
4
  import { z } from 'zod/mini';
5
5
  import { KalturaAnalyticsServiceType, PubSubServiceType } from '@unisphere/runtime';
@@ -53,7 +53,7 @@ export interface AvatarConnectionMachineContext {
53
53
  /** @deprecated TODO sk consider to avoid saving entire service object inside the machine's context */
54
54
  pubSubService: PubSubServiceType;
55
55
  externalActions: {
56
- showNotification: (notification: Notifications) => void;
56
+ showNotification: (notification: AvatarNotificationRequest) => void;
57
57
  };
58
58
  connectionId: string;
59
59
  clientConfiguration: z.infer<typeof clientConfigurationSchema>['clientConfiguration'] | null;
@@ -66,7 +66,11 @@ export interface AvatarConnectionMachineContext {
66
66
  content: UnisphereChatBlock | null;
67
67
  chatLog: ChatEntry[];
68
68
  clientToolCallLog: ClientToolCallLogEntry[];
69
- pendingTextInputs: string[];
69
+ pendingTextInput: {
70
+ id: string | null;
71
+ turnId: string | null;
72
+ text: string;
73
+ } | null;
70
74
  isAgentTalking: boolean;
71
75
  userAgent: string;
72
76
  userAgentHints: Record<string, string> | null;
@@ -164,6 +164,7 @@ export type AgentStartSpeechReceivedEvent = {
164
164
  export type AvatarChatEntryReceivedEvent = {
165
165
  type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived;
166
166
  entry: ChatEntry;
167
+ isFinalSegment?: boolean;
167
168
  };
168
169
  export type UserVoiceTurnReceivedEvent = {
169
170
  type: AvatarConnectionMachineEventTypes.UserVoiceTurnReceived;
@@ -6,6 +6,7 @@ export * from './events';
6
6
  export * from './context';
7
7
  export * from './input';
8
8
  export * from './schemas';
9
+ export * from './notifications';
9
10
  export declare const AvatarConnectionMachineId = "AvatarClient";
10
11
  export type AvatarConnectionActor = ActorRefFrom<typeof avatarConnectionMachine>;
11
12
  export type AvatarConnectionMachineState = SnapshotFrom<typeof avatarConnectionMachine>;
@@ -1,5 +1,5 @@
1
1
  import { UnisphereLoggerType } from '@unisphere/core';
2
- import { Notifications } from '@unisphere/notifications-core';
2
+ import { AvatarNotificationRequest } from './notifications';
3
3
  import { KalturaAnalyticsServiceType, PubSubServiceType } from '@unisphere/runtime';
4
4
  import { AvatarRuntimeSettings } from '@unisphere/genie-types';
5
5
  import type { ClientToolHandler, ClientToolsInput, ClientToolsRegistry, GetClientToolHandler } from '@local/genie-shared-utils';
@@ -14,7 +14,7 @@ export interface AvatarConnectionMachineInput {
14
14
  kalturaAnalyticsService: KalturaAnalyticsServiceType;
15
15
  pubSubService: PubSubServiceType;
16
16
  externalActions: {
17
- showNotification: (notification: Notifications) => void;
17
+ showNotification: (notification: AvatarNotificationRequest) => void;
18
18
  };
19
19
  runtimeSettings: AvatarRuntimeSettings;
20
20
  stickyId?: string;
@@ -1,4 +1,21 @@
1
- import { Notifications } from '@unisphere/notifications-core';
1
+ import { AlertIconEnum } from '@unisphere/notifications-core';
2
2
  import { DisconnectReasons } from '@unisphere/genie-types';
3
+ export declare enum AvatarNotificationCode {
4
+ ScreenShareError = "ScreenShareError",
5
+ AgentRecoveryExhausted = "AgentRecoveryExhausted",
6
+ ConversationTimeWarning = "ConversationTimeWarning"
7
+ }
8
+ export type AvatarNotificationReasonCode = DisconnectReasons | AvatarNotificationCode;
9
+ export interface AvatarNotificationRequest {
10
+ code: AvatarNotificationReasonCode;
11
+ variant: AlertIconEnum;
12
+ /** Interpolation values the host's translation for `code` may reference. */
13
+ params?: Record<string, string | number>;
14
+ options?: {
15
+ duration?: number;
16
+ isPermanent?: boolean;
17
+ showClose?: boolean;
18
+ };
19
+ }
3
20
  export declare function isMappedAvatarDisconnectReason(reason: DisconnectReasons): boolean;
4
- export declare function getAvatarConnectionNotification(reason: DisconnectReasons): Notifications;
21
+ export declare function getAvatarDisconnectNotificationVariant(reason: DisconnectReasons): AlertIconEnum;
@@ -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>>;
@@ -103,3 +105,6 @@ export declare const stvSpeechChunkSchema: z.ZodMiniObject<{
103
105
  durationMs: z.ZodMiniNumber<number>;
104
106
  speechId: z.ZodMiniString<string>;
105
107
  }, z.core.$strip>;
108
+ export declare const unsupportedClientSchema: z.ZodMiniObject<{
109
+ code: z.ZodMiniOptional<z.ZodMiniString<string>>;
110
+ }, z.core.$strip>;
@@ -1,4 +1,4 @@
1
- import { Notifications } from '@unisphere/notifications-core';
1
+ import { AvatarNotificationRequest } from './machines/avatar-connection/types/notifications';
2
2
  export type AvatarClientManagerActions = {
3
- showNotification: (notification: Notifications) => void;
3
+ showNotification: (notification: AvatarNotificationRequest) => void;
4
4
  };