@kaltura-sdk/avatars 1.15.5 → 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 +244 -212
- package/package.json +2 -2
- package/src/lib/avatar-client-manager/machines/avatar-connection/actions/context/remove-stale-chat-entries-for-retry.d.ts +2 -0
- package/src/lib/avatar-client-manager/machines/avatar-connection/actions/context/test-utils/as-harness-action.d.ts +4 -0
- package/src/lib/avatar-client-manager/machines/avatar-connection/avatar-connection-machine.d.ts +3 -0
- package/src/lib/avatar-client-manager/machines/avatar-connection/states/connected-state.d.ts +2 -2
- package/src/lib/avatar-client-manager/machines/avatar-connection/states/connected-states/agent-recovery-monitor-state.d.ts +2 -2
- package/src/lib/avatar-client-manager/machines/avatar-connection/types/context.d.ts +5 -1
- package/src/lib/avatar-client-manager/machines/avatar-connection/types/events.d.ts +1 -0
- package/src/lib/avatar-client-manager/machines/avatar-connection/types/schemas.d.ts +2 -0
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()),
|
|
@@ -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: {
|
|
@@ -20620,9 +20623,51 @@ const appendAvatarChatEntry = assign$1(({
|
|
|
20620
20623
|
context,
|
|
20621
20624
|
event
|
|
20622
20625
|
}) => {
|
|
20626
|
+
var _a, _b;
|
|
20623
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]);
|
|
20624
20669
|
return {
|
|
20625
|
-
chatLog:
|
|
20670
|
+
chatLog: context.chatLog.filter(entry => entry.messageId !== event.messageId)
|
|
20626
20671
|
};
|
|
20627
20672
|
});
|
|
20628
20673
|
|
|
@@ -20630,18 +20675,39 @@ const appendUserChatEntry = assign$1(({
|
|
|
20630
20675
|
context,
|
|
20631
20676
|
event
|
|
20632
20677
|
}) => {
|
|
20678
|
+
var _a;
|
|
20633
20679
|
assertEvent(event, AvatarConnectionMachineEventTypes.UserVoiceTurnReceived);
|
|
20634
|
-
const
|
|
20635
|
-
if (
|
|
20636
|
-
// Already added optimistically via text input — consume the pending entry and skip
|
|
20637
|
-
const pendingTextInputs = [...context.pendingTextInputs];
|
|
20638
|
-
pendingTextInputs.splice(idx, 1);
|
|
20680
|
+
const pending = context.pendingTextInput;
|
|
20681
|
+
if (!pending) {
|
|
20639
20682
|
return {
|
|
20640
|
-
|
|
20683
|
+
chatLog: [...context.chatLog, event.entry]
|
|
20641
20684
|
};
|
|
20642
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.
|
|
20643
20704
|
return {
|
|
20644
|
-
chatLog
|
|
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
|
+
}
|
|
20645
20711
|
};
|
|
20646
20712
|
});
|
|
20647
20713
|
|
|
@@ -20654,12 +20720,70 @@ const appendUserTextEntry = assign$1(({
|
|
|
20654
20720
|
text,
|
|
20655
20721
|
internal
|
|
20656
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
|
+
}
|
|
20657
20776
|
// 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
|
|
20659
|
-
// in
|
|
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`.
|
|
20660
20780
|
if (internal) {
|
|
20661
20781
|
return {
|
|
20662
|
-
|
|
20782
|
+
pendingTextInput: {
|
|
20783
|
+
id: null,
|
|
20784
|
+
turnId: null,
|
|
20785
|
+
text
|
|
20786
|
+
}
|
|
20663
20787
|
};
|
|
20664
20788
|
}
|
|
20665
20789
|
const entry = {
|
|
@@ -20669,7 +20793,11 @@ const appendUserTextEntry = assign$1(({
|
|
|
20669
20793
|
};
|
|
20670
20794
|
return {
|
|
20671
20795
|
chatLog: [...context.chatLog, entry],
|
|
20672
|
-
|
|
20796
|
+
pendingTextInput: {
|
|
20797
|
+
id: entry.id,
|
|
20798
|
+
turnId: null,
|
|
20799
|
+
text
|
|
20800
|
+
}
|
|
20673
20801
|
};
|
|
20674
20802
|
});
|
|
20675
20803
|
|
|
@@ -20720,135 +20848,64 @@ const source$a = createTinyPubsubEventsSource(websocketMessage, {
|
|
|
20720
20848
|
agent_raw_text: {
|
|
20721
20849
|
schema: agentRawTextSchema
|
|
20722
20850
|
},
|
|
20723
|
-
agentTurnToTalk: {
|
|
20724
|
-
schema: agentTurnToTalkSchema
|
|
20725
|
-
},
|
|
20726
20851
|
generatingSpeech: {
|
|
20727
20852
|
schema: generatingSpeechSchema
|
|
20853
|
+
},
|
|
20854
|
+
agentTurnToTalk: {
|
|
20855
|
+
schema: agentTurnToTalkSchema
|
|
20728
20856
|
}
|
|
20729
20857
|
}, {
|
|
20730
20858
|
queueWhilePending: true
|
|
20731
20859
|
});
|
|
20732
|
-
//
|
|
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.
|
|
20860
|
+
// Builds alternating user/avatar chat log from agent_raw_text, keyed by turnId.
|
|
20745
20861
|
//
|
|
20746
|
-
//
|
|
20747
|
-
//
|
|
20748
|
-
//
|
|
20749
|
-
//
|
|
20750
|
-
//
|
|
20751
|
-
//
|
|
20752
|
-
//
|
|
20753
|
-
//
|
|
20754
|
-
//
|
|
20755
|
-
//
|
|
20756
|
-
//
|
|
20757
|
-
//
|
|
20758
|
-
//
|
|
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.
|
|
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.
|
|
20767
20875
|
//
|
|
20768
|
-
//
|
|
20769
|
-
//
|
|
20770
|
-
// 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.
|
|
20771
20878
|
//
|
|
20772
|
-
//
|
|
20773
|
-
//
|
|
20774
|
-
//
|
|
20775
|
-
//
|
|
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.
|
|
20776
20883
|
//
|
|
20777
|
-
//
|
|
20778
|
-
//
|
|
20779
|
-
//
|
|
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.
|
|
20780
20892
|
const onChatLog = fromCallback(({
|
|
20781
20893
|
input,
|
|
20782
20894
|
sendBack
|
|
20783
20895
|
}) => {
|
|
20784
|
-
// Accumulate
|
|
20896
|
+
// Accumulate avatar text by `${turnId}:${speechId}` — each speechId
|
|
20897
|
+
// closes independently (e.g. a filler segment vs the main answer).
|
|
20785
20898
|
const pending = new Map();
|
|
20786
|
-
|
|
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;
|
|
20899
|
+
const seenHumanTurnKeys = new Set();
|
|
20838
20900
|
let avatarEntrySequence = 0;
|
|
20839
20901
|
const createAvatarEntryId = (messageId, speechId) => {
|
|
20840
20902
|
avatarEntrySequence += 1;
|
|
20841
20903
|
return `${messageId}:${speechId}:${avatarEntrySequence}`;
|
|
20842
20904
|
};
|
|
20843
|
-
|
|
20844
|
-
|
|
20845
|
-
|
|
20846
|
-
|
|
20847
|
-
type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
|
|
20848
|
-
entry
|
|
20849
|
-
});
|
|
20850
|
-
}
|
|
20851
|
-
};
|
|
20905
|
+
// Opening phrase accumulation — see top-of-file comment.
|
|
20906
|
+
let openingPhraseSpeechId = null;
|
|
20907
|
+
let openingPhraseText = '';
|
|
20908
|
+
let openingPhraseSettled = false;
|
|
20852
20909
|
return createTinyPubsubEventsHandler({
|
|
20853
20910
|
source: source$a,
|
|
20854
20911
|
logger: input.logger,
|
|
@@ -20860,115 +20917,89 @@ const onChatLog = fromCallback(({
|
|
|
20860
20917
|
const result = agentRawTextDeltaSchema.safeParse(delta);
|
|
20861
20918
|
if (!result.success) return;
|
|
20862
20919
|
const parsed = result.data;
|
|
20863
|
-
|
|
20864
|
-
|
|
20865
|
-
|
|
20866
|
-
|
|
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).
|
|
20920
|
+
const key = `${payload.turnId}:${payload.speechId}`;
|
|
20921
|
+
if (parsed.type === 'think') {
|
|
20922
|
+
if (parsed.human && !seenHumanTurnKeys.has(key)) {
|
|
20923
|
+
seenHumanTurnKeys.add(key);
|
|
20892
20924
|
sendBack({
|
|
20893
|
-
type: AvatarConnectionMachineEventTypes.
|
|
20894
|
-
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
|
+
}
|
|
20895
20933
|
});
|
|
20896
|
-
return;
|
|
20897
20934
|
}
|
|
20898
|
-
|
|
20899
|
-
|
|
20900
|
-
|
|
20901
|
-
|
|
20935
|
+
return;
|
|
20936
|
+
}
|
|
20937
|
+
if (parsed.type === 'user-interruption') {
|
|
20938
|
+
const text = pending.get(key);
|
|
20939
|
+
pending.delete(key);
|
|
20940
|
+
if (text) {
|
|
20902
20941
|
sendBack({
|
|
20903
20942
|
type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
|
|
20904
|
-
entry
|
|
20943
|
+
entry: {
|
|
20944
|
+
id: createAvatarEntryId(parsed.messageId, payload.speechId),
|
|
20945
|
+
role: 'avatar',
|
|
20946
|
+
text,
|
|
20947
|
+
turnId: payload.turnId,
|
|
20948
|
+
messageId: parsed.messageId
|
|
20949
|
+
}
|
|
20905
20950
|
});
|
|
20906
|
-
} else {
|
|
20907
|
-
// agentTurnToTalk hasn't arrived yet — queue until it does
|
|
20908
|
-
awaitingUserTurnQueue.push(entry);
|
|
20909
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
|
+
});
|
|
20910
20974
|
}
|
|
20911
20975
|
} catch (_c) {
|
|
20912
20976
|
// invalid JSON delta — ignore
|
|
20913
20977
|
}
|
|
20914
20978
|
},
|
|
20915
20979
|
generatingSpeech: payload => {
|
|
20916
|
-
|
|
20917
|
-
|
|
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.
|
|
20980
|
+
if (openingPhraseSettled) return;
|
|
20981
|
+
// No opening phrase configured — must not leak into the chat log.
|
|
20920
20982
|
if (payload.text === BLANK_TEXT_MARKER) return;
|
|
20921
|
-
if (openingPhraseSpeechId !== null && openingPhraseSpeechId !== payload.speechId)
|
|
20983
|
+
if (openingPhraseSpeechId !== null && openingPhraseSpeechId !== payload.speechId) {
|
|
20984
|
+
return;
|
|
20985
|
+
}
|
|
20922
20986
|
openingPhraseSpeechId = payload.speechId;
|
|
20923
|
-
|
|
20987
|
+
openingPhraseText += payload.text;
|
|
20924
20988
|
},
|
|
20925
20989
|
agentTurnToTalk: payload => {
|
|
20926
|
-
|
|
20927
|
-
|
|
20928
|
-
|
|
20929
|
-
|
|
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;
|
|
20937
|
-
if (text) {
|
|
20938
|
-
sendBack({
|
|
20939
|
-
type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
|
|
20940
|
-
entry: {
|
|
20941
|
-
id: openingPhraseSpeechId,
|
|
20942
|
-
role: 'avatar',
|
|
20943
|
-
text,
|
|
20944
|
-
turnId: 'init_response'
|
|
20945
|
-
}
|
|
20946
|
-
});
|
|
20947
|
-
}
|
|
20948
|
-
}
|
|
20949
|
-
// Also emit any queued avatar entry left over from the agent_raw_text path
|
|
20950
|
-
emitQueuedAvatarEntry();
|
|
20951
|
-
return;
|
|
20952
|
-
}
|
|
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)
|
|
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;
|
|
20958
20994
|
sendBack({
|
|
20959
|
-
type: AvatarConnectionMachineEventTypes.
|
|
20995
|
+
type: AvatarConnectionMachineEventTypes.AvatarChatEntryReceived,
|
|
20960
20996
|
entry: {
|
|
20961
|
-
id:
|
|
20962
|
-
role: '
|
|
20963
|
-
text:
|
|
20964
|
-
turnId
|
|
20997
|
+
id: openingPhraseSpeechId !== null && openingPhraseSpeechId !== void 0 ? openingPhraseSpeechId : 'init_response',
|
|
20998
|
+
role: 'avatar',
|
|
20999
|
+
text: openingPhraseText,
|
|
21000
|
+
turnId: 'init_response'
|
|
20965
21001
|
}
|
|
20966
21002
|
});
|
|
20967
|
-
if (awaitingUserTurnQueue.length > 0) {
|
|
20968
|
-
emitQueuedAvatarEntry();
|
|
20969
|
-
} else {
|
|
20970
|
-
pendingUserTurnCount += 1;
|
|
20971
|
-
}
|
|
20972
21003
|
}
|
|
20973
21004
|
}
|
|
20974
21005
|
});
|
|
@@ -23290,6 +23321,7 @@ const avatarConnectionMachine = setup({
|
|
|
23290
23321
|
logAgentClientToolCallResult,
|
|
23291
23322
|
recordClientToolCallLogEntry,
|
|
23292
23323
|
appendAvatarChatEntry,
|
|
23324
|
+
removeStaleChatEntriesForRetry,
|
|
23293
23325
|
appendUserChatEntry,
|
|
23294
23326
|
appendUserTextEntry,
|
|
23295
23327
|
setScreenShareActive,
|
|
@@ -23463,7 +23495,7 @@ const avatarConnectionMachine = setup({
|
|
|
23463
23495
|
content: null,
|
|
23464
23496
|
chatLog: [],
|
|
23465
23497
|
clientToolCallLog: [],
|
|
23466
|
-
|
|
23498
|
+
pendingTextInput: null,
|
|
23467
23499
|
userAgent: '',
|
|
23468
23500
|
userAgentHints: null,
|
|
23469
23501
|
kalturaAnalyticsService: input.kalturaAnalyticsService,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaltura-sdk/avatars",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
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.
|
|
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,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>;
|
package/src/lib/avatar-client-manager/machines/avatar-connection/avatar-connection-machine.d.ts
CHANGED
|
@@ -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;
|
package/src/lib/avatar-client-manager/machines/avatar-connection/states/connected-state.d.ts
CHANGED
|
@@ -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: {
|
|
@@ -66,7 +66,11 @@ export interface AvatarConnectionMachineContext {
|
|
|
66
66
|
content: UnisphereChatBlock | null;
|
|
67
67
|
chatLog: ChatEntry[];
|
|
68
68
|
clientToolCallLog: ClientToolCallLogEntry[];
|
|
69
|
-
|
|
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;
|
|
@@ -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>>;
|