@sendbird/ai-agent-messenger-react 1.40.1 → 1.42.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/dist/index.cjs +91 -91
- package/dist/index.d.ts +951 -33
- package/dist/index.js +5214 -4558
- package/package.json +12 -12
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { AIAgentModule } from '@sendbird/chat/aiAgent';
|
|
|
4
4
|
import { AriaRole } from 'react';
|
|
5
5
|
import type { BaseChannelCollectionEventHandler } from '@sendbird/chat/groupChannel';
|
|
6
6
|
import type { BaseMessage } from '@sendbird/chat/message';
|
|
7
|
+
import type { CollectionEventSource } from '@sendbird/chat';
|
|
7
8
|
import { ComponentType } from 'react';
|
|
8
9
|
import { Context } from 'react';
|
|
9
10
|
import type { Conversation as Conversation_2 } from '@sendbird/chat/aiAgent';
|
|
@@ -64,8 +65,11 @@ declare interface ActiveChannelUpdatedParams {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
export declare interface AgentClientHandlers extends AIAgentClientHandlers {
|
|
68
|
+
/** @internal */
|
|
67
69
|
onMessageSend?: (message: string) => void;
|
|
70
|
+
/** @internal */
|
|
68
71
|
onFunctionCallDetailClick?: (func: FunctionCallsInfo) => void;
|
|
72
|
+
/** @internal */
|
|
69
73
|
onInitializeFailed?: (error: Error) => void;
|
|
70
74
|
}
|
|
71
75
|
|
|
@@ -250,8 +254,7 @@ customApiHost?: string;
|
|
|
250
254
|
* */
|
|
251
255
|
agentPreviewConfigs?: AgentPreviewConfigs;
|
|
252
256
|
/**
|
|
253
|
-
* @
|
|
254
|
-
* @description Event handlers for the ai agent client.
|
|
257
|
+
* @public
|
|
255
258
|
*/
|
|
256
259
|
handlers?: AgentClientHandlers;
|
|
257
260
|
/**
|
|
@@ -266,7 +269,8 @@ enableCloseConversationButton?: boolean;
|
|
|
266
269
|
enableExpandButton?: boolean;
|
|
267
270
|
/**
|
|
268
271
|
* @internal
|
|
269
|
-
* @description Whether to automatically disconnect the WebSocket
|
|
272
|
+
* @description Whether to automatically disconnect the WebSocket after the messenger stays closed for 60 seconds.
|
|
273
|
+
* The legacy name is kept for compatibility; this applies regardless of launcher rendering or customization.
|
|
270
274
|
* Set to false to keep the connection alive even during inactivity. (default: true)
|
|
271
275
|
*/
|
|
272
276
|
enableAutoDisconnectInLauncher?: boolean;
|
|
@@ -306,6 +310,11 @@ declare abstract class AIAgentBaseStats {
|
|
|
306
310
|
protected abstract buildPayload(): AIAgentStatPayload | null;
|
|
307
311
|
protected abstract getMetricKey(): string;
|
|
308
312
|
|
|
313
|
+
/** The stat type this metric is collected under; assertion metrics override it. */
|
|
314
|
+
protected getStatType(): string {
|
|
315
|
+
return AI_AGENT_STAT_TYPE;
|
|
316
|
+
}
|
|
317
|
+
|
|
309
318
|
setCommitCallback(callback: StatsAppendCallback): this {
|
|
310
319
|
this.commitCallback = callback;
|
|
311
320
|
return this;
|
|
@@ -402,7 +411,7 @@ declare abstract class AIAgentBaseStats {
|
|
|
402
411
|
if (!payload) return false;
|
|
403
412
|
|
|
404
413
|
this.committed = true;
|
|
405
|
-
return this.commitCallback(
|
|
414
|
+
return this.commitCallback(this.getStatType(), payload);
|
|
406
415
|
}
|
|
407
416
|
}
|
|
408
417
|
|
|
@@ -433,6 +442,10 @@ declare interface AIAgentClientHandlers {
|
|
|
433
442
|
* should be efficient and non-blocking, as this may be called frequently.
|
|
434
443
|
*/
|
|
435
444
|
onLiveMetric?: (metric: LiveMetric) => void;
|
|
445
|
+
/**
|
|
446
|
+
* Called when a displayable nudge message is received after `requestNudgeMessage`.
|
|
447
|
+
*/
|
|
448
|
+
onNudgeMessageReceived?: (response: NudgeMessageResponse) => void;
|
|
436
449
|
}
|
|
437
450
|
|
|
438
451
|
declare type AIAgentCommonIconName =
|
|
@@ -636,7 +649,15 @@ declare interface AIAgentConversationContextValue {
|
|
|
636
649
|
input: InputState;
|
|
637
650
|
};
|
|
638
651
|
|
|
652
|
+
// TODO: fold into `_stats.enabled`.
|
|
639
653
|
_statsEnabled: boolean;
|
|
654
|
+
/** @internal What the platform packages report to the stats trackers. */
|
|
655
|
+
_stats: {
|
|
656
|
+
/** A message row's reveal animation starting. */
|
|
657
|
+
onMessageLifecycleAnimationStart: (messageId: number) => void;
|
|
658
|
+
/** A message row's reveal animation stopping, and whether it revealed everything. */
|
|
659
|
+
onMessageLifecycleAnimationComplete: (messageId: number, completed: boolean) => void;
|
|
660
|
+
};
|
|
640
661
|
}
|
|
641
662
|
|
|
642
663
|
declare interface AIAgentConversationListContextValue {
|
|
@@ -1002,8 +1023,7 @@ export declare type AIAgentProps = PropsWithChildren<{
|
|
|
1002
1023
|
* */
|
|
1003
1024
|
agentPreviewConfigs?: AgentPreviewConfigs;
|
|
1004
1025
|
/**
|
|
1005
|
-
* @
|
|
1006
|
-
* @description Event handlers for the ai agent client.
|
|
1026
|
+
* @public
|
|
1007
1027
|
*/
|
|
1008
1028
|
handlers?: AgentClientHandlers;
|
|
1009
1029
|
/**
|
|
@@ -1018,7 +1038,8 @@ export declare type AIAgentProps = PropsWithChildren<{
|
|
|
1018
1038
|
enableExpandButton?: boolean;
|
|
1019
1039
|
/**
|
|
1020
1040
|
* @internal
|
|
1021
|
-
* @description Whether to automatically disconnect the WebSocket
|
|
1041
|
+
* @description Whether to automatically disconnect the WebSocket after the messenger stays closed for 60 seconds.
|
|
1042
|
+
* The legacy name is kept for compatibility; this applies regardless of launcher rendering or customization.
|
|
1022
1043
|
* Set to false to keep the connection alive even during inactivity. (default: true)
|
|
1023
1044
|
*/
|
|
1024
1045
|
enableAutoDisconnectInLauncher?: boolean;
|
|
@@ -1142,6 +1163,9 @@ declare type AIAgentStatsTrackerCollection = {
|
|
|
1142
1163
|
streamStalled: StreamStalledStatsTracker;
|
|
1143
1164
|
typingIndicatorAbsentTimeout: TypingIndicatorAbsentTimeoutStatsTracker;
|
|
1144
1165
|
renderedContentUnusable: RenderedContentUnusableStatsTracker;
|
|
1166
|
+
streamingMessageValidation: StreamingMessageValidationStatsTracker;
|
|
1167
|
+
duplicateMessageResponse: DuplicateMessageResponseStatsTracker;
|
|
1168
|
+
messageLifecycleEvent: MessageLifecycleEventStatsTracker;
|
|
1145
1169
|
};
|
|
1146
1170
|
|
|
1147
1171
|
declare type AIAgentStatsTrackers = AIAgentStatsTrackerCollection & {
|
|
@@ -1492,6 +1516,8 @@ declare type ChatSDKBaseChannelCollectionEventHandler = BaseChannelCollectionEve
|
|
|
1492
1516
|
|
|
1493
1517
|
declare type ChatSDKBaseMessage = BaseMessage;
|
|
1494
1518
|
|
|
1519
|
+
declare type ChatSDKCollectionEventSource = CollectionEventSource;
|
|
1520
|
+
|
|
1495
1521
|
declare type ChatSDKConversation = Conversation_2;
|
|
1496
1522
|
|
|
1497
1523
|
declare type ChatSDKConversationStatus = ConversationStatus;
|
|
@@ -2975,6 +3001,86 @@ declare interface Dispatcher {
|
|
|
2975
3001
|
): void;
|
|
2976
3002
|
}
|
|
2977
3003
|
|
|
3004
|
+
/**
|
|
3005
|
+
* Spec: duplicate_message_response
|
|
3006
|
+
*
|
|
3007
|
+
* Detects the AI giving the same answer text twice: either as two bubbles in a row, or once
|
|
3008
|
+
* per turn with the user asking again in between. The point is to size how often it happens,
|
|
3009
|
+
* not to suppress it.
|
|
3010
|
+
*
|
|
3011
|
+
* Timeline:
|
|
3012
|
+
* - Whenever the conversation changes, the latest settled reply is compared with the reply
|
|
3013
|
+
* before it. Anyone other than the agent is stepped over, so a turn in between does not
|
|
3014
|
+
* separate the two. A reply that cannot be compared — still streaming, or with no text —
|
|
3015
|
+
* does end the run, because it sits between them as an answer that was given.
|
|
3016
|
+
*
|
|
3017
|
+
* Payload:
|
|
3018
|
+
* - value: empty; the pair is the whole observation.
|
|
3019
|
+
* - extra.prev_message_id / extra.next_message_id: the two replies that matched.
|
|
3020
|
+
*
|
|
3021
|
+
* Policy:
|
|
3022
|
+
* - A reply that already reported a duplicate never reports one again, which is what keeps the
|
|
3023
|
+
* tail from being counted again every time it is re-read. That memory lives only for the
|
|
3024
|
+
* session: a duplicate is rare enough, and rarely still the last two replies on a later
|
|
3025
|
+
* visit, for a repeat count to matter.
|
|
3026
|
+
* - Runs of three or more are not modelled; each settled reply looks back exactly one reply.
|
|
3027
|
+
* - Only succeeded messages are read, because the list pins unsent ones past everything else.
|
|
3028
|
+
*/
|
|
3029
|
+
declare class DuplicateMessageResponseStatsTracker extends StatsTrackerBase {
|
|
3030
|
+
private readonly emittedMessageChannels = new Map<number, string>();
|
|
3031
|
+
|
|
3032
|
+
onMessagesChanged(params: ObservedConversation): void {
|
|
3033
|
+
const replies = this.findLastTwoReplies(params.messages, params.aiAgentUserId);
|
|
3034
|
+
if (!replies) return;
|
|
3035
|
+
if (replies.previous.message !== replies.latest.message) return;
|
|
3036
|
+
if (this.emittedMessageChannels.has(replies.latest.messageId)) return;
|
|
3037
|
+
|
|
3038
|
+
this.emit(replies.latest, replies.previous, params);
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
clear(): void {
|
|
3042
|
+
this.emittedMessageChannels.clear();
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
private findLastTwoReplies(
|
|
3046
|
+
messages: ChatSDKBaseMessage[],
|
|
3047
|
+
aiAgentUserId: string,
|
|
3048
|
+
): { latest: ChatSDKBaseMessage; previous: ChatSDKBaseMessage } | null {
|
|
3049
|
+
let latest: ChatSDKBaseMessage | null = null;
|
|
3050
|
+
let previous: ChatSDKBaseMessage | null = null;
|
|
3051
|
+
|
|
3052
|
+
forEachMessageNewestFirst(messages, (message) => {
|
|
3053
|
+
// Both list comparators pin an unsent message past every succeeded one, so its position
|
|
3054
|
+
// says nothing about when it was sent: it can neither end a run nor be compared.
|
|
3055
|
+
if (isSendableMessage(message) && message.sendingStatus !== SendingStatus.SUCCEEDED) return;
|
|
3056
|
+
// Anyone other than the agent is stepped over: answering two separate turns with the same
|
|
3057
|
+
// text is the same defect as answering one turn twice.
|
|
3058
|
+
if (!isSentBy(message, aiAgentUserId)) return;
|
|
3059
|
+
// A reply that cannot be compared sits between the two, so they are not adjacent either.
|
|
3060
|
+
if (!isComparableReply(message, aiAgentUserId)) return 'stop';
|
|
3061
|
+
|
|
3062
|
+
if (!latest) latest = message;
|
|
3063
|
+
else {
|
|
3064
|
+
previous = message;
|
|
3065
|
+
return 'stop';
|
|
3066
|
+
}
|
|
3067
|
+
});
|
|
3068
|
+
|
|
3069
|
+
return latest && previous ? { latest, previous } : null;
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
private emit(latest: ChatSDKBaseMessage, previous: ChatSDKBaseMessage, params: ObservedConversation): void {
|
|
3073
|
+
evictOldestIfAtLimit(this.emittedMessageChannels, DUPLICATE_MESSAGE_RESPONSE_EMITTED_LIMIT);
|
|
3074
|
+
this.emittedMessageChannels.set(latest.messageId, params.channelUrl);
|
|
3075
|
+
|
|
3076
|
+
new DuplicateMessageResponseStats()
|
|
3077
|
+
.setDuplicatePair({ previousMessageId: previous.messageId, nextMessageId: latest.messageId })
|
|
3078
|
+
.setConversationId(params.conversationId)
|
|
3079
|
+
.setCommitCallback(this.commitCallback)
|
|
3080
|
+
.commit();
|
|
3081
|
+
}
|
|
3082
|
+
}
|
|
3083
|
+
|
|
2978
3084
|
declare enum DurationKey {
|
|
2979
3085
|
ACK_TO_FIRST_CHUNK = 'ack_to_first_chunk_ms',
|
|
2980
3086
|
AFTER_TPEN_ABSENT = 'after_tpen_absent_ms',
|
|
@@ -3093,8 +3199,17 @@ export declare interface FeedbackInfo {
|
|
|
3093
3199
|
comment?: string;
|
|
3094
3200
|
}
|
|
3095
3201
|
|
|
3202
|
+
export declare interface FixedLauncherProps extends Pick<LauncherProps, 'opened' | 'setOpened' | 'entryPoint' | 'activeRoute'> {
|
|
3203
|
+
/** Configured launcher size, available for sizing or positioning your custom UI. */
|
|
3204
|
+
size?: number;
|
|
3205
|
+
/** Placement styles for the custom launcher. Apply them or use your own positioning. */
|
|
3206
|
+
style?: CSSProperties;
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3096
3209
|
export declare const FixedMessenger: ForwardRefExoticComponent<MessengerProps & RefAttributes<MessengerSessionRef>> & {
|
|
3097
3210
|
Style: (props: FixedMessengerStyleProps) => null;
|
|
3211
|
+
Launcher: ({ component }: FixedMessengerLauncherProps) => null;
|
|
3212
|
+
NudgeMessage: ({ component }: FixedMessengerNudgeMessageProps) => null;
|
|
3098
3213
|
Conversation: ({ component }: FixedMessengerConversationProps) => null;
|
|
3099
3214
|
ConversationChildren: ({ children }: PropsWithChildren) => null;
|
|
3100
3215
|
ConversationList: ({ component }: FixedMessengerConversationListProps) => null;
|
|
@@ -3109,6 +3224,10 @@ declare interface FixedMessengerConversationProps {
|
|
|
3109
3224
|
component: ComponentType<ConversationProps>;
|
|
3110
3225
|
}
|
|
3111
3226
|
|
|
3227
|
+
declare interface FixedMessengerLauncherProps {
|
|
3228
|
+
component: ComponentType<FixedLauncherProps>;
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3112
3231
|
export declare type FixedMessengerMargin = {
|
|
3113
3232
|
top: number;
|
|
3114
3233
|
bottom: number;
|
|
@@ -3116,6 +3235,10 @@ export declare type FixedMessengerMargin = {
|
|
|
3116
3235
|
end: number;
|
|
3117
3236
|
};
|
|
3118
3237
|
|
|
3238
|
+
declare interface FixedMessengerNudgeMessageProps {
|
|
3239
|
+
component: ComponentType<FixedNudgeMessageProps>;
|
|
3240
|
+
}
|
|
3241
|
+
|
|
3119
3242
|
export declare type FixedMessengerPosition = `${PositionHorizontal}-${PositionVertical}`;
|
|
3120
3243
|
|
|
3121
3244
|
declare interface FixedMessengerStyleProps {
|
|
@@ -3138,6 +3261,11 @@ declare interface FixedMessengerStyleProps {
|
|
|
3138
3261
|
launcherSize?: number;
|
|
3139
3262
|
}
|
|
3140
3263
|
|
|
3264
|
+
export declare interface FixedNudgeMessageProps extends Omit<NudgeMessageViewProps, 'nudgeMessageKey'> {
|
|
3265
|
+
/** Placement styles for the custom nudge message. Apply them or use your own positioning. */
|
|
3266
|
+
style?: CSSProperties;
|
|
3267
|
+
}
|
|
3268
|
+
|
|
3141
3269
|
declare type ForegroundTaskScheduler = (start: () => void) => void | (() => void);
|
|
3142
3270
|
|
|
3143
3271
|
declare interface Form {
|
|
@@ -3184,6 +3312,21 @@ export declare interface GroundednessInfo {
|
|
|
3184
3312
|
preview_title: string;
|
|
3185
3313
|
}
|
|
3186
3314
|
|
|
3315
|
+
declare type GroupChannelMessagesCollectionEvent =
|
|
3316
|
+
| {
|
|
3317
|
+
kind: 'initialized';
|
|
3318
|
+
source: 'cache' | 'api';
|
|
3319
|
+
channelUrl: string;
|
|
3320
|
+
messages: ChatSDKBaseMessage[];
|
|
3321
|
+
error?: Error | null;
|
|
3322
|
+
}
|
|
3323
|
+
| {
|
|
3324
|
+
kind: 'messagesAdded' | 'messagesUpdated' | 'messagesDeleted';
|
|
3325
|
+
source: ChatSDKCollectionEventSource;
|
|
3326
|
+
channelUrl: string;
|
|
3327
|
+
messages: ChatSDKBaseMessage[];
|
|
3328
|
+
};
|
|
3329
|
+
|
|
3187
3330
|
declare interface HeaderTitleProps {
|
|
3188
3331
|
/** Overrides the default title, which is derived from the channel (AI agent nickname or channel name). */
|
|
3189
3332
|
title?: string;
|
|
@@ -3241,7 +3384,8 @@ BaseMessageProps<{
|
|
|
3241
3384
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
3242
3385
|
onStreamAnimationStart?: () => void;
|
|
3243
3386
|
onStreamAnimationProgress?: () => void;
|
|
3244
|
-
|
|
3387
|
+
/** `completed` is false when the row went away before the text was fully revealed. */
|
|
3388
|
+
onStreamAnimationComplete?: (params: { completed: boolean }) => void;
|
|
3245
3389
|
/**
|
|
3246
3390
|
* Fired (layout effect) when this row starts/stops holding its text — the stream animation is
|
|
3247
3391
|
* running but nothing is visible yet. The row renders nothing while holding, so the list draws its
|
|
@@ -3341,7 +3485,9 @@ export declare const IncomingMessageLayout: {
|
|
|
3341
3485
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
3342
3486
|
onStreamAnimationStart?: () => void;
|
|
3343
3487
|
onStreamAnimationProgress?: () => void;
|
|
3344
|
-
onStreamAnimationComplete?: (
|
|
3488
|
+
onStreamAnimationComplete?: (params: {
|
|
3489
|
+
completed: boolean;
|
|
3490
|
+
}) => void;
|
|
3345
3491
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
3346
3492
|
messageTemplateErrorFallback?: ReactNode;
|
|
3347
3493
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -3424,7 +3570,9 @@ export declare const IncomingMessageLayout: {
|
|
|
3424
3570
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
3425
3571
|
onStreamAnimationStart?: () => void;
|
|
3426
3572
|
onStreamAnimationProgress?: () => void;
|
|
3427
|
-
onStreamAnimationComplete?: (
|
|
3573
|
+
onStreamAnimationComplete?: (params: {
|
|
3574
|
+
completed: boolean;
|
|
3575
|
+
}) => void;
|
|
3428
3576
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
3429
3577
|
messageTemplateErrorFallback?: ReactNode;
|
|
3430
3578
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -3507,7 +3655,9 @@ export declare const IncomingMessageLayout: {
|
|
|
3507
3655
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
3508
3656
|
onStreamAnimationStart?: () => void;
|
|
3509
3657
|
onStreamAnimationProgress?: () => void;
|
|
3510
|
-
onStreamAnimationComplete?: (
|
|
3658
|
+
onStreamAnimationComplete?: (params: {
|
|
3659
|
+
completed: boolean;
|
|
3660
|
+
}) => void;
|
|
3511
3661
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
3512
3662
|
messageTemplateErrorFallback?: ReactNode;
|
|
3513
3663
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -3586,7 +3736,9 @@ export declare const IncomingMessageLayout: {
|
|
|
3586
3736
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
3587
3737
|
onStreamAnimationStart?: () => void;
|
|
3588
3738
|
onStreamAnimationProgress?: () => void;
|
|
3589
|
-
onStreamAnimationComplete?: (
|
|
3739
|
+
onStreamAnimationComplete?: (params: {
|
|
3740
|
+
completed: boolean;
|
|
3741
|
+
}) => void;
|
|
3590
3742
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
3591
3743
|
messageTemplateErrorFallback?: ReactNode;
|
|
3592
3744
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -3669,7 +3821,9 @@ export declare const IncomingMessageLayout: {
|
|
|
3669
3821
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
3670
3822
|
onStreamAnimationStart?: () => void;
|
|
3671
3823
|
onStreamAnimationProgress?: () => void;
|
|
3672
|
-
onStreamAnimationComplete?: (
|
|
3824
|
+
onStreamAnimationComplete?: (params: {
|
|
3825
|
+
completed: boolean;
|
|
3826
|
+
}) => void;
|
|
3673
3827
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
3674
3828
|
messageTemplateErrorFallback?: ReactNode;
|
|
3675
3829
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -3752,7 +3906,9 @@ export declare const IncomingMessageLayout: {
|
|
|
3752
3906
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
3753
3907
|
onStreamAnimationStart?: () => void;
|
|
3754
3908
|
onStreamAnimationProgress?: () => void;
|
|
3755
|
-
onStreamAnimationComplete?: (
|
|
3909
|
+
onStreamAnimationComplete?: (params: {
|
|
3910
|
+
completed: boolean;
|
|
3911
|
+
}) => void;
|
|
3756
3912
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
3757
3913
|
messageTemplateErrorFallback?: ReactNode;
|
|
3758
3914
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -3864,7 +4020,9 @@ export declare const IncomingMessageLayout: {
|
|
|
3864
4020
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
3865
4021
|
onStreamAnimationStart?: () => void;
|
|
3866
4022
|
onStreamAnimationProgress?: () => void;
|
|
3867
|
-
onStreamAnimationComplete?: (
|
|
4023
|
+
onStreamAnimationComplete?: (params: {
|
|
4024
|
+
completed: boolean;
|
|
4025
|
+
}) => void;
|
|
3868
4026
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
3869
4027
|
messageTemplateErrorFallback?: ReactNode;
|
|
3870
4028
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -3947,7 +4105,9 @@ export declare const IncomingMessageLayout: {
|
|
|
3947
4105
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
3948
4106
|
onStreamAnimationStart?: () => void;
|
|
3949
4107
|
onStreamAnimationProgress?: () => void;
|
|
3950
|
-
onStreamAnimationComplete?: (
|
|
4108
|
+
onStreamAnimationComplete?: (params: {
|
|
4109
|
+
completed: boolean;
|
|
4110
|
+
}) => void;
|
|
3951
4111
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
3952
4112
|
messageTemplateErrorFallback?: ReactNode;
|
|
3953
4113
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -4030,7 +4190,9 @@ export declare const IncomingMessageLayout: {
|
|
|
4030
4190
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
4031
4191
|
onStreamAnimationStart?: () => void;
|
|
4032
4192
|
onStreamAnimationProgress?: () => void;
|
|
4033
|
-
onStreamAnimationComplete?: (
|
|
4193
|
+
onStreamAnimationComplete?: (params: {
|
|
4194
|
+
completed: boolean;
|
|
4195
|
+
}) => void;
|
|
4034
4196
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
4035
4197
|
messageTemplateErrorFallback?: ReactNode;
|
|
4036
4198
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -4109,7 +4271,9 @@ export declare const IncomingMessageLayout: {
|
|
|
4109
4271
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
4110
4272
|
onStreamAnimationStart?: () => void;
|
|
4111
4273
|
onStreamAnimationProgress?: () => void;
|
|
4112
|
-
onStreamAnimationComplete?: (
|
|
4274
|
+
onStreamAnimationComplete?: (params: {
|
|
4275
|
+
completed: boolean;
|
|
4276
|
+
}) => void;
|
|
4113
4277
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
4114
4278
|
messageTemplateErrorFallback?: ReactNode;
|
|
4115
4279
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -4192,7 +4356,9 @@ export declare const IncomingMessageLayout: {
|
|
|
4192
4356
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
4193
4357
|
onStreamAnimationStart?: () => void;
|
|
4194
4358
|
onStreamAnimationProgress?: () => void;
|
|
4195
|
-
onStreamAnimationComplete?: (
|
|
4359
|
+
onStreamAnimationComplete?: (params: {
|
|
4360
|
+
completed: boolean;
|
|
4361
|
+
}) => void;
|
|
4196
4362
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
4197
4363
|
messageTemplateErrorFallback?: ReactNode;
|
|
4198
4364
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -4275,7 +4441,9 @@ export declare const IncomingMessageLayout: {
|
|
|
4275
4441
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
4276
4442
|
onStreamAnimationStart?: () => void;
|
|
4277
4443
|
onStreamAnimationProgress?: () => void;
|
|
4278
|
-
onStreamAnimationComplete?: (
|
|
4444
|
+
onStreamAnimationComplete?: (params: {
|
|
4445
|
+
completed: boolean;
|
|
4446
|
+
}) => void;
|
|
4279
4447
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
4280
4448
|
messageTemplateErrorFallback?: ReactNode;
|
|
4281
4449
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -4382,7 +4550,9 @@ export declare const IncomingMessageLayout: {
|
|
|
4382
4550
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
4383
4551
|
onStreamAnimationStart?: () => void;
|
|
4384
4552
|
onStreamAnimationProgress?: () => void;
|
|
4385
|
-
onStreamAnimationComplete?: (
|
|
4553
|
+
onStreamAnimationComplete?: (params: {
|
|
4554
|
+
completed: boolean;
|
|
4555
|
+
}) => void;
|
|
4386
4556
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
4387
4557
|
messageTemplateErrorFallback?: ReactNode;
|
|
4388
4558
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -4465,7 +4635,9 @@ export declare const IncomingMessageLayout: {
|
|
|
4465
4635
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
4466
4636
|
onStreamAnimationStart?: () => void;
|
|
4467
4637
|
onStreamAnimationProgress?: () => void;
|
|
4468
|
-
onStreamAnimationComplete?: (
|
|
4638
|
+
onStreamAnimationComplete?: (params: {
|
|
4639
|
+
completed: boolean;
|
|
4640
|
+
}) => void;
|
|
4469
4641
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
4470
4642
|
messageTemplateErrorFallback?: ReactNode;
|
|
4471
4643
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -4548,7 +4720,9 @@ export declare const IncomingMessageLayout: {
|
|
|
4548
4720
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
4549
4721
|
onStreamAnimationStart?: () => void;
|
|
4550
4722
|
onStreamAnimationProgress?: () => void;
|
|
4551
|
-
onStreamAnimationComplete?: (
|
|
4723
|
+
onStreamAnimationComplete?: (params: {
|
|
4724
|
+
completed: boolean;
|
|
4725
|
+
}) => void;
|
|
4552
4726
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
4553
4727
|
messageTemplateErrorFallback?: ReactNode;
|
|
4554
4728
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -4627,7 +4801,9 @@ export declare const IncomingMessageLayout: {
|
|
|
4627
4801
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
4628
4802
|
onStreamAnimationStart?: () => void;
|
|
4629
4803
|
onStreamAnimationProgress?: () => void;
|
|
4630
|
-
onStreamAnimationComplete?: (
|
|
4804
|
+
onStreamAnimationComplete?: (params: {
|
|
4805
|
+
completed: boolean;
|
|
4806
|
+
}) => void;
|
|
4631
4807
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
4632
4808
|
messageTemplateErrorFallback?: ReactNode;
|
|
4633
4809
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -4710,7 +4886,9 @@ export declare const IncomingMessageLayout: {
|
|
|
4710
4886
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
4711
4887
|
onStreamAnimationStart?: () => void;
|
|
4712
4888
|
onStreamAnimationProgress?: () => void;
|
|
4713
|
-
onStreamAnimationComplete?: (
|
|
4889
|
+
onStreamAnimationComplete?: (params: {
|
|
4890
|
+
completed: boolean;
|
|
4891
|
+
}) => void;
|
|
4714
4892
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
4715
4893
|
messageTemplateErrorFallback?: ReactNode;
|
|
4716
4894
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -4793,7 +4971,9 @@ export declare const IncomingMessageLayout: {
|
|
|
4793
4971
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
4794
4972
|
onStreamAnimationStart?: () => void;
|
|
4795
4973
|
onStreamAnimationProgress?: () => void;
|
|
4796
|
-
onStreamAnimationComplete?: (
|
|
4974
|
+
onStreamAnimationComplete?: (params: {
|
|
4975
|
+
completed: boolean;
|
|
4976
|
+
}) => void;
|
|
4797
4977
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
4798
4978
|
messageTemplateErrorFallback?: ReactNode;
|
|
4799
4979
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -4923,7 +5103,9 @@ export declare const IncomingMessageLayout: {
|
|
|
4923
5103
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
4924
5104
|
onStreamAnimationStart?: () => void;
|
|
4925
5105
|
onStreamAnimationProgress?: () => void;
|
|
4926
|
-
onStreamAnimationComplete?: (
|
|
5106
|
+
onStreamAnimationComplete?: (params: {
|
|
5107
|
+
completed: boolean;
|
|
5108
|
+
}) => void;
|
|
4927
5109
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
4928
5110
|
messageTemplateErrorFallback?: ReactNode;
|
|
4929
5111
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -5006,7 +5188,9 @@ export declare const IncomingMessageLayout: {
|
|
|
5006
5188
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
5007
5189
|
onStreamAnimationStart?: () => void;
|
|
5008
5190
|
onStreamAnimationProgress?: () => void;
|
|
5009
|
-
onStreamAnimationComplete?: (
|
|
5191
|
+
onStreamAnimationComplete?: (params: {
|
|
5192
|
+
completed: boolean;
|
|
5193
|
+
}) => void;
|
|
5010
5194
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
5011
5195
|
messageTemplateErrorFallback?: ReactNode;
|
|
5012
5196
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -5089,7 +5273,9 @@ export declare const IncomingMessageLayout: {
|
|
|
5089
5273
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
5090
5274
|
onStreamAnimationStart?: () => void;
|
|
5091
5275
|
onStreamAnimationProgress?: () => void;
|
|
5092
|
-
onStreamAnimationComplete?: (
|
|
5276
|
+
onStreamAnimationComplete?: (params: {
|
|
5277
|
+
completed: boolean;
|
|
5278
|
+
}) => void;
|
|
5093
5279
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
5094
5280
|
messageTemplateErrorFallback?: ReactNode;
|
|
5095
5281
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -5170,7 +5356,9 @@ export declare const IncomingMessageLayout: {
|
|
|
5170
5356
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
5171
5357
|
onStreamAnimationStart?: () => void;
|
|
5172
5358
|
onStreamAnimationProgress?: () => void;
|
|
5173
|
-
onStreamAnimationComplete?: (
|
|
5359
|
+
onStreamAnimationComplete?: (params: {
|
|
5360
|
+
completed: boolean;
|
|
5361
|
+
}) => void;
|
|
5174
5362
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
5175
5363
|
messageTemplateErrorFallback?: ReactNode;
|
|
5176
5364
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -5253,7 +5441,9 @@ export declare const IncomingMessageLayout: {
|
|
|
5253
5441
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
5254
5442
|
onStreamAnimationStart?: () => void;
|
|
5255
5443
|
onStreamAnimationProgress?: () => void;
|
|
5256
|
-
onStreamAnimationComplete?: (
|
|
5444
|
+
onStreamAnimationComplete?: (params: {
|
|
5445
|
+
completed: boolean;
|
|
5446
|
+
}) => void;
|
|
5257
5447
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
5258
5448
|
messageTemplateErrorFallback?: ReactNode;
|
|
5259
5449
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -5336,7 +5526,9 @@ export declare const IncomingMessageLayout: {
|
|
|
5336
5526
|
onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
|
|
5337
5527
|
onStreamAnimationStart?: () => void;
|
|
5338
5528
|
onStreamAnimationProgress?: () => void;
|
|
5339
|
-
onStreamAnimationComplete?: (
|
|
5529
|
+
onStreamAnimationComplete?: (params: {
|
|
5530
|
+
completed: boolean;
|
|
5531
|
+
}) => void;
|
|
5340
5532
|
onStreamAnimationHoldChange?: (holding: boolean) => void;
|
|
5341
5533
|
messageTemplateErrorFallback?: ReactNode;
|
|
5342
5534
|
messageTemplateLoadingFallback?: ReactNode;
|
|
@@ -5461,7 +5653,7 @@ declare interface LanguageSet extends Omit<AIAgentLanguageSet, 'strings'> {
|
|
|
5461
5653
|
stringSet: StringSet;
|
|
5462
5654
|
}
|
|
5463
5655
|
|
|
5464
|
-
export declare function LauncherBase({ className, children, isOpened, onClick, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, type, imageUrl, dir, animated, backgroundColor, size, hasUnread, }:
|
|
5656
|
+
export declare function LauncherBase({ className, children, isOpened, onClick, 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedBy, type, imageUrl, dir, animated, backgroundColor, size, hasUnread, }: LauncherProps_2): JSX.Element;
|
|
5465
5657
|
|
|
5466
5658
|
declare type LauncherImageType = 'default_icon' | 'custom_image';
|
|
5467
5659
|
|
|
@@ -5471,6 +5663,15 @@ declare interface LauncherInfo {
|
|
|
5471
5663
|
}
|
|
5472
5664
|
|
|
5473
5665
|
declare type LauncherProps = {
|
|
5666
|
+
opened: boolean;
|
|
5667
|
+
setOpened: (opened: boolean) => void;
|
|
5668
|
+
size?: number;
|
|
5669
|
+
className?: string;
|
|
5670
|
+
entryPoint?: LauncherRoute;
|
|
5671
|
+
activeRoute?: LauncherRoute;
|
|
5672
|
+
};
|
|
5673
|
+
|
|
5674
|
+
declare type LauncherProps_2 = {
|
|
5474
5675
|
className?: string;
|
|
5475
5676
|
children?: ReactNode;
|
|
5476
5677
|
isOpened: boolean;
|
|
@@ -5486,6 +5687,8 @@ declare type LauncherProps = {
|
|
|
5486
5687
|
hasUnread?: boolean;
|
|
5487
5688
|
};
|
|
5488
5689
|
|
|
5690
|
+
export declare type LauncherRoute = 'Conversation' | 'ConversationList';
|
|
5691
|
+
|
|
5489
5692
|
declare interface LayoutContextValue<Props, Components extends Record<string, ComponentType<any>>> {
|
|
5490
5693
|
Template: ComponentType<Props>;
|
|
5491
5694
|
updateTemplate: (template: ComponentType<Props>) => void;
|
|
@@ -5664,6 +5867,477 @@ declare interface MemoryInfo {
|
|
|
5664
5867
|
* */
|
|
5665
5868
|
declare type MessageGroupType = 'single' | 'top' | 'bottom' | 'middle';
|
|
5666
5869
|
|
|
5870
|
+
/**
|
|
5871
|
+
* One turn: the acknowledged user message and every bubble that answers it.
|
|
5872
|
+
*
|
|
5873
|
+
* `first` is the first bubble's span, opened at the send so it can time out and carry the
|
|
5874
|
+
* typing; it joins `spans` once a bubble announces its stream. The block stops accepting
|
|
5875
|
+
* bubbles once the one flagged last has closed its stream, and is discarded whole by the next
|
|
5876
|
+
* send.
|
|
5877
|
+
*/
|
|
5878
|
+
declare type MessageLifecycleBlock = {
|
|
5879
|
+
channelUrl: string;
|
|
5880
|
+
userMessageId: number;
|
|
5881
|
+
conversationId?: number;
|
|
5882
|
+
first: MessageLifecycleEventStatsSpan;
|
|
5883
|
+
firstAnnounced: boolean;
|
|
5884
|
+
spans: Map<number, MessageLifecycleEventStatsSpan>;
|
|
5885
|
+
// The bubble flagged `is_last_in_turn` on any of its frames; the block closes once its
|
|
5886
|
+
// stream does, whether or not the closing frame repeats the flag.
|
|
5887
|
+
lastMessageId?: number;
|
|
5888
|
+
closed: boolean;
|
|
5889
|
+
};
|
|
5890
|
+
|
|
5891
|
+
declare class MessageLifecycleEventStats extends AIAgentBaseStats {
|
|
5892
|
+
private timestamps?: MessageLifecycleTimestamps;
|
|
5893
|
+
|
|
5894
|
+
/**
|
|
5895
|
+
* The order these moments happen in is fixed, but the order they arrive in is not, so every
|
|
5896
|
+
* one is stored as it came and the durations between them are left to be derived later.
|
|
5897
|
+
*/
|
|
5898
|
+
setTimestamps(timestamps: MessageLifecycleTimestamps): this {
|
|
5899
|
+
this.timestamps = timestamps;
|
|
5900
|
+
return this;
|
|
5901
|
+
}
|
|
5902
|
+
|
|
5903
|
+
/** A domain string, never a caught error, so no payload value can leak into the description. */
|
|
5904
|
+
setFailure(failure: MessageLifecycleFailure): this {
|
|
5905
|
+
this.setError(failure);
|
|
5906
|
+
return this;
|
|
5907
|
+
}
|
|
5908
|
+
|
|
5909
|
+
protected getStatType(): string {
|
|
5910
|
+
return AI_AGENT_ASSERTION_STAT_TYPE;
|
|
5911
|
+
}
|
|
5912
|
+
|
|
5913
|
+
protected getMetricKey(): string {
|
|
5914
|
+
return METRIC_KEY_MESSAGE_LIFECYCLE_EVENT;
|
|
5915
|
+
}
|
|
5916
|
+
|
|
5917
|
+
protected buildPayload(): AIAgentStatPayload | null {
|
|
5918
|
+
const timestamps = this.timestamps;
|
|
5919
|
+
if (!timestamps) return null;
|
|
5920
|
+
|
|
5921
|
+
// The reveal is measured from the send for the first bubble and from its own announcement
|
|
5922
|
+
// for the bubbles after it.
|
|
5923
|
+
const revealFrom = timestamps.messageSentAt ?? timestamps.streamStartAt;
|
|
5924
|
+
const animationEndAt = timestamps.animationEndAt;
|
|
5925
|
+
const value =
|
|
5926
|
+
animationEndAt === undefined || revealFrom === undefined ? UNMEASURABLE_DURATION : animationEndAt - revealFrom;
|
|
5927
|
+
|
|
5928
|
+
return this.createPayload(value, {
|
|
5929
|
+
extra: {
|
|
5930
|
+
message_id: timestamps.messageId === undefined ? undefined : String(timestamps.messageId),
|
|
5931
|
+
message_sent_at: timestamps.messageSentAt,
|
|
5932
|
+
typing_start_at: timestamps.typingStartAt,
|
|
5933
|
+
typing_end_at: timestamps.typingEndAt,
|
|
5934
|
+
stream_start_at: timestamps.streamStartAt,
|
|
5935
|
+
stream_failed_at: timestamps.streamFailedAt,
|
|
5936
|
+
last_stream_at: timestamps.lastStreamAt,
|
|
5937
|
+
stream_end_at: timestamps.streamEndAt,
|
|
5938
|
+
animation_start_at: timestamps.animationStartAt,
|
|
5939
|
+
animation_end_at: timestamps.animationEndAt,
|
|
5940
|
+
},
|
|
5941
|
+
});
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
|
|
5945
|
+
/**
|
|
5946
|
+
* One bubble being measured, from its announcement to the reveal that shows it. The first
|
|
5947
|
+
* bubble's span opens earlier, at the acknowledged user message, so it also owns the wait for
|
|
5948
|
+
* the reply to start.
|
|
5949
|
+
*
|
|
5950
|
+
* The span owns its own timers, so a discarded attempt cannot report on a later one: cancelling
|
|
5951
|
+
* severs the metric reference, and a timer that fires afterwards finds nothing to record.
|
|
5952
|
+
*/
|
|
5953
|
+
declare class MessageLifecycleEventStatsSpan extends StatsSpanBase<MessageLifecycleEventStats, MessageLifecycleMeasure> {
|
|
5954
|
+
protected measure: MessageLifecycleMeasure;
|
|
5955
|
+
|
|
5956
|
+
constructor(
|
|
5957
|
+
stats: MessageLifecycleEventStats | null,
|
|
5958
|
+
params: { messageSentAt?: number; stopBackgroundWatch: (() => boolean) | null },
|
|
5959
|
+
) {
|
|
5960
|
+
// Nothing calls setVisible on this tracker, so a span is never hidden.
|
|
5961
|
+
super(stats, true);
|
|
5962
|
+
this.measure = {
|
|
5963
|
+
messageSentAt: params.messageSentAt,
|
|
5964
|
+
startTimeoutTimer: null,
|
|
5965
|
+
stallTimer: null,
|
|
5966
|
+
stopBackgroundWatch: params.stopBackgroundWatch,
|
|
5967
|
+
};
|
|
5968
|
+
}
|
|
5969
|
+
|
|
5970
|
+
/** Starts the window the input stays blocked for; a reply that never streams runs it out. */
|
|
5971
|
+
armStartTimeout(): void {
|
|
5972
|
+
this.measure.startTimeoutTimer = setTimeout(() => this.failWith('sent_timeout'), STREAM_START_TIMEOUT_MS);
|
|
5973
|
+
}
|
|
5974
|
+
|
|
5975
|
+
setConversationId(conversationId?: number): void {
|
|
5976
|
+
this.stats?.setConversationId(conversationId);
|
|
5977
|
+
}
|
|
5978
|
+
|
|
5979
|
+
onTypingChanged(isAgentTyping: boolean): void {
|
|
5980
|
+
if (!this.stats) return;
|
|
5981
|
+
|
|
5982
|
+
// Only the first pair of edges describes the wait; later ones belong to a later reply.
|
|
5983
|
+
if (isAgentTyping) {
|
|
5984
|
+
if (this.measure.typingStartAt === undefined) this.measure.typingStartAt = Date.now();
|
|
5985
|
+
return;
|
|
5986
|
+
}
|
|
5987
|
+
if (this.measure.typingStartAt !== undefined && this.measure.typingEndAt === undefined)
|
|
5988
|
+
this.measure.typingEndAt = Date.now();
|
|
5989
|
+
}
|
|
5990
|
+
|
|
5991
|
+
/** Latches onto the bubble announcing its stream; nothing else this span sees can be a chunk. */
|
|
5992
|
+
onStreamStart(message: ChatSDKBaseMessage): void {
|
|
5993
|
+
if (!this.stats) return;
|
|
5994
|
+
|
|
5995
|
+
this.measure.streamMessageId = message.messageId;
|
|
5996
|
+
this.measure.streamStartAt = Date.now();
|
|
5997
|
+
this.clearStartTimeout();
|
|
5998
|
+
this.armStall();
|
|
5999
|
+
}
|
|
6000
|
+
|
|
6001
|
+
onStreamFrame(message: ChatSDKBaseMessage, advancesStream: boolean): void {
|
|
6002
|
+
if (!this.stats) return;
|
|
6003
|
+
|
|
6004
|
+
const now = Date.now();
|
|
6005
|
+
// Only a MEDI carries a chunk. The stall timer is armed by either, because a redelivered
|
|
6006
|
+
// announcement still says the server is there.
|
|
6007
|
+
if (advancesStream) this.measure.lastStreamAt = now;
|
|
6008
|
+
|
|
6009
|
+
if (messageExtension.isStreaming(message)) return this.armStall();
|
|
6010
|
+
|
|
6011
|
+
this.measure.streamEndAt = now;
|
|
6012
|
+
this.clearStall();
|
|
6013
|
+
if (this.measure.animationStartAt === undefined) this.failWith('animation_not_started');
|
|
6014
|
+
}
|
|
6015
|
+
|
|
6016
|
+
onAnimationStart(): void {
|
|
6017
|
+
if (!this.stats) return;
|
|
6018
|
+
if (this.measure.animationStartAt === undefined) this.measure.animationStartAt = Date.now();
|
|
6019
|
+
}
|
|
6020
|
+
|
|
6021
|
+
onAnimationComplete(completed: boolean): void {
|
|
6022
|
+
if (!this.stats) return;
|
|
6023
|
+
|
|
6024
|
+
// A reveal cut short says nothing about how long the answer took to appear.
|
|
6025
|
+
if (!completed) return this.cancel();
|
|
6026
|
+
|
|
6027
|
+
this.settle({ animationEndAt: Date.now() });
|
|
6028
|
+
}
|
|
6029
|
+
|
|
6030
|
+
override cancel(): void {
|
|
6031
|
+
this.clearTimers();
|
|
6032
|
+
this.measure.stopBackgroundWatch?.();
|
|
6033
|
+
super.cancel();
|
|
6034
|
+
}
|
|
6035
|
+
|
|
6036
|
+
private failWith(failure: MessageLifecycleFailure): void {
|
|
6037
|
+
if (!this.stats) return;
|
|
6038
|
+
|
|
6039
|
+
this.stats.setFailure(failure);
|
|
6040
|
+
if (failure !== 'stream_failed') return this.settle({});
|
|
6041
|
+
|
|
6042
|
+
// The last chunk time only describes a stall, and a stall that never produced a chunk
|
|
6043
|
+
// drops the stream keys entirely: their absence is what marks it in the payload.
|
|
6044
|
+
this.settle(
|
|
6045
|
+
this.measure.lastStreamAt === undefined
|
|
6046
|
+
? { streamFailedAt: Date.now(), streamStartAt: undefined }
|
|
6047
|
+
: { streamFailedAt: Date.now(), lastStreamAt: this.measure.lastStreamAt },
|
|
6048
|
+
);
|
|
6049
|
+
}
|
|
6050
|
+
|
|
6051
|
+
private settle(ending: {
|
|
6052
|
+
animationEndAt?: number;
|
|
6053
|
+
streamFailedAt?: number;
|
|
6054
|
+
lastStreamAt?: number;
|
|
6055
|
+
streamStartAt?: number;
|
|
6056
|
+
}): void {
|
|
6057
|
+
if (!this.stats) return;
|
|
6058
|
+
|
|
6059
|
+
this.clearTimers();
|
|
6060
|
+
|
|
6061
|
+
// A backgrounded app stops rendering, so the moments after it went away are not the
|
|
6062
|
+
// product's timing any more.
|
|
6063
|
+
if (this.measure.stopBackgroundWatch?.()) return super.cancel();
|
|
6064
|
+
|
|
6065
|
+
this.stats.setTimestamps({
|
|
6066
|
+
messageId: this.measure.streamMessageId,
|
|
6067
|
+
messageSentAt: this.measure.messageSentAt,
|
|
6068
|
+
typingStartAt: this.measure.typingStartAt,
|
|
6069
|
+
typingEndAt: this.measure.typingEndAt,
|
|
6070
|
+
streamStartAt: this.measure.streamStartAt,
|
|
6071
|
+
streamEndAt: this.measure.streamEndAt,
|
|
6072
|
+
animationStartAt: this.measure.animationStartAt,
|
|
6073
|
+
...ending,
|
|
6074
|
+
});
|
|
6075
|
+
this.commitOrDefer();
|
|
6076
|
+
}
|
|
6077
|
+
|
|
6078
|
+
private armStall(): void {
|
|
6079
|
+
this.clearStall();
|
|
6080
|
+
this.measure.stallTimer = setTimeout(() => this.failWith('stream_failed'), STREAM_STALL_TIMEOUT_MS);
|
|
6081
|
+
}
|
|
6082
|
+
|
|
6083
|
+
private clearTimers(): void {
|
|
6084
|
+
this.clearStartTimeout();
|
|
6085
|
+
this.clearStall();
|
|
6086
|
+
}
|
|
6087
|
+
|
|
6088
|
+
private clearStartTimeout(): void {
|
|
6089
|
+
if (this.measure.startTimeoutTimer !== null) clearTimeout(this.measure.startTimeoutTimer);
|
|
6090
|
+
this.measure.startTimeoutTimer = null;
|
|
6091
|
+
}
|
|
6092
|
+
|
|
6093
|
+
private clearStall(): void {
|
|
6094
|
+
if (this.measure.stallTimer !== null) clearTimeout(this.measure.stallTimer);
|
|
6095
|
+
this.measure.stallTimer = null;
|
|
6096
|
+
}
|
|
6097
|
+
}
|
|
6098
|
+
|
|
6099
|
+
/**
|
|
6100
|
+
* Spec: message_lifecycle_event
|
|
6101
|
+
*
|
|
6102
|
+
* Records when each moment of one reply happened, from the user sending a message to the
|
|
6103
|
+
* reply finishing its reveal. The moments are emitted as wall-clock times rather than
|
|
6104
|
+
* durations, because their order of arrival is not guaranteed even though their order of
|
|
6105
|
+
* occurrence is: deriving durations here would bake a wrong value into the sample.
|
|
6106
|
+
*
|
|
6107
|
+
* Timeline:
|
|
6108
|
+
* - The user's message is acknowledged, which opens the block.
|
|
6109
|
+
* - The agent starts and stops typing.
|
|
6110
|
+
* - Each bubble announces a stream, streams its chunks, and closes it. A stream is announced
|
|
6111
|
+
* by a MESG and advanced by MEDIs, which the delivering event says and the collection's own
|
|
6112
|
+
* add/update does not — so the same announcement arriving twice is not a chunk.
|
|
6113
|
+
* - Each bubble's reveal animation starts and finishes.
|
|
6114
|
+
*
|
|
6115
|
+
* Payload, one sample per bubble:
|
|
6116
|
+
* - value: animation end minus message sent for the first bubble, minus stream start for the
|
|
6117
|
+
* bubbles after it, or -1 when the reveal never happened.
|
|
6118
|
+
* - extra.message_id: the bubble.
|
|
6119
|
+
* - extra.*_at: each moment that happened; the rest are left out. Only the first bubble
|
|
6120
|
+
* carries the send and typing moments, since only it was waited for.
|
|
6121
|
+
* - error_description: sent_timeout, stream_failed or animation_not_started.
|
|
6122
|
+
*
|
|
6123
|
+
* Method call order:
|
|
6124
|
+
* - `onUserMessageSent` opens a block and discards whatever was open.
|
|
6125
|
+
* - Every other method addresses the open block; there is no handle for callers to hold,
|
|
6126
|
+
* because they are spread across the provider and both platform packages.
|
|
6127
|
+
*
|
|
6128
|
+
* Policy:
|
|
6129
|
+
* - One block at a time. The next message the user sends discards whatever is still open,
|
|
6130
|
+
* spans included, and a discarded span is non-recording, so its own timers can no longer
|
|
6131
|
+
* report anything.
|
|
6132
|
+
* - A reply split into bubbles is measured bubble by bubble: each announcement opens its own
|
|
6133
|
+
* span, and every span settles on its own. The block closes when the bubble flagged
|
|
6134
|
+
* `is_last_in_turn` closes its stream — the flag is only reliable then — and nothing waits
|
|
6135
|
+
* for a last bubble that never comes: the block is simply discarded by the next send.
|
|
6136
|
+
* - Only bubbles that reach the screen are measured. A bubble is dropped when the reveal is
|
|
6137
|
+
* cut short, when it arrives already settled and so never animates, when it is deleted,
|
|
6138
|
+
* when the conversation goes away, or when the app was backgrounded, because none of those
|
|
6139
|
+
* say anything about how fast the product answered.
|
|
6140
|
+
* - Every collection source is observed, because a reconnect delivers the rest of a stream
|
|
6141
|
+
* through a sync rather than a live event. Correlation is what scopes an event to this turn.
|
|
6142
|
+
* - A reply that never starts streaming within the input block window is reported as a
|
|
6143
|
+
* timeout, which covers both silence and typing that leads nowhere.
|
|
6144
|
+
* - A stream that closes before its reveal ever starts is a rendering fault, so it is reported
|
|
6145
|
+
* right away instead of waiting for an animation that is not coming.
|
|
6146
|
+
*/
|
|
6147
|
+
declare class MessageLifecycleEventStatsTracker extends StatsTrackerBase {
|
|
6148
|
+
private block: MessageLifecycleBlock | null = null;
|
|
6149
|
+
private watchBackgrounded?: BackgroundedWatcher;
|
|
6150
|
+
|
|
6151
|
+
setBackgroundedWatcher(watchBackgrounded?: BackgroundedWatcher): void {
|
|
6152
|
+
this.watchBackgrounded = watchBackgrounded;
|
|
6153
|
+
}
|
|
6154
|
+
|
|
6155
|
+
/** Opens a block for the message the user just got acknowledged. */
|
|
6156
|
+
onUserMessageSent(params: { channelUrl: string; messageId: number; conversationId?: number }): void {
|
|
6157
|
+
this.discardBlock();
|
|
6158
|
+
|
|
6159
|
+
const first = this.createSpan(params.conversationId, { messageSentAt: Date.now() });
|
|
6160
|
+
first.armStartTimeout();
|
|
6161
|
+
this.block = {
|
|
6162
|
+
channelUrl: params.channelUrl,
|
|
6163
|
+
userMessageId: params.messageId,
|
|
6164
|
+
conversationId: params.conversationId,
|
|
6165
|
+
first,
|
|
6166
|
+
firstAnnounced: false,
|
|
6167
|
+
spans: new Map(),
|
|
6168
|
+
closed: false,
|
|
6169
|
+
};
|
|
6170
|
+
}
|
|
6171
|
+
|
|
6172
|
+
/**
|
|
6173
|
+
* The conversation is initialized after the channel is exposed, so a block can open before
|
|
6174
|
+
* its id exists — including for a turn that only ever commits on a timeout, which no later
|
|
6175
|
+
* event would reach. The setter keeps the first real id it is given.
|
|
6176
|
+
*/
|
|
6177
|
+
onConversationIdResolved(params: { channelUrl: string; conversationId?: number }): void {
|
|
6178
|
+
const block = this.blockFor(params.channelUrl);
|
|
6179
|
+
if (!block) return;
|
|
6180
|
+
|
|
6181
|
+
block.conversationId ??= params.conversationId;
|
|
6182
|
+
block.first.setConversationId(params.conversationId);
|
|
6183
|
+
block.spans.forEach((span) => span.setConversationId(params.conversationId));
|
|
6184
|
+
}
|
|
6185
|
+
|
|
6186
|
+
onTypingChanged(params: { channelUrl: string; isAgentTyping: boolean }): void {
|
|
6187
|
+
// Typing describes the wait for the first bubble; the span that carries it keeps ignoring
|
|
6188
|
+
// later edges on its own.
|
|
6189
|
+
this.blockFor(params.channelUrl)?.first.onTypingChanged(params.isAgentTyping);
|
|
6190
|
+
}
|
|
6191
|
+
|
|
6192
|
+
onCollectionEvent(params: { event: GroupChannelMessagesCollectionEvent; channelUrl: string; aiAgentUserId: string }) {
|
|
6193
|
+
const block = this.blockFor(params.channelUrl);
|
|
6194
|
+
const event = params.event;
|
|
6195
|
+
if (!block) return;
|
|
6196
|
+
if (event.channelUrl !== params.channelUrl) return;
|
|
6197
|
+
|
|
6198
|
+
// A deleted bubble can no longer reach the screen, and the stall timer would report it as a
|
|
6199
|
+
// stream that died rather than one that was removed.
|
|
6200
|
+
if (event.kind === 'messagesDeleted') {
|
|
6201
|
+
event.messages.forEach((message) => block.spans.get(message.messageId)?.cancel());
|
|
6202
|
+
return;
|
|
6203
|
+
}
|
|
6204
|
+
|
|
6205
|
+
if (event.kind === 'initialized') return;
|
|
6206
|
+
|
|
6207
|
+
// The source is not filtered. A reconnect delivers the rest of a stream through the
|
|
6208
|
+
// collection's changelog sync rather than a live event (browser-e2e/tests/streaming.e2e.test.ts
|
|
6209
|
+
// TC-CPE-08), and rejecting that recovery leaves the turn to run out its timers and report a
|
|
6210
|
+
// failure the screen contradicts. Correlation is what scopes an event to this turn, and
|
|
6211
|
+
// only a MEDI counts as a chunk, so a recovery closes the bubble without inventing one.
|
|
6212
|
+
const advancesStream = event.source === CollectionEventSource.EVENT_MESSAGE_UPDATED;
|
|
6213
|
+
|
|
6214
|
+
event.messages.forEach((message) => {
|
|
6215
|
+
if (!isSentBy(message, params.aiAgentUserId)) return;
|
|
6216
|
+
// The reply names the message it answers, which is what ties it to this block.
|
|
6217
|
+
if (parseAIResponseMessageData(message.data).userMessageId !== block.userMessageId) return;
|
|
6218
|
+
|
|
6219
|
+
this.observeReply(block, message, advancesStream);
|
|
6220
|
+
});
|
|
6221
|
+
}
|
|
6222
|
+
|
|
6223
|
+
onAnimationStart(params: { messageId: number }): void {
|
|
6224
|
+
this.block?.spans.get(params.messageId)?.onAnimationStart();
|
|
6225
|
+
}
|
|
6226
|
+
|
|
6227
|
+
onAnimationComplete(params: { messageId: number; completed: boolean }): void {
|
|
6228
|
+
this.block?.spans.get(params.messageId)?.onAnimationComplete(params.completed);
|
|
6229
|
+
}
|
|
6230
|
+
|
|
6231
|
+
/** The conversation going away leaves the turn unfinished, so nothing is reported. */
|
|
6232
|
+
clearChannel(channelUrl?: string): void {
|
|
6233
|
+
if (!channelUrl || !this.blockFor(channelUrl)) return;
|
|
6234
|
+
this.discardBlock();
|
|
6235
|
+
}
|
|
6236
|
+
|
|
6237
|
+
clear(): void {
|
|
6238
|
+
this.discardBlock();
|
|
6239
|
+
}
|
|
6240
|
+
|
|
6241
|
+
private observeReply(block: MessageLifecycleBlock, message: ChatSDKBaseMessage, advancesStream: boolean): void {
|
|
6242
|
+
const streaming = messageExtension.isStreaming(message);
|
|
6243
|
+
|
|
6244
|
+
// Judged on the frames alone, so a bubble whose span is already settled or discarded can
|
|
6245
|
+
// still end the turn.
|
|
6246
|
+
if (isLastInTurn(message)) block.lastMessageId = message.messageId;
|
|
6247
|
+
const closesTurn = block.lastMessageId === message.messageId && !streaming;
|
|
6248
|
+
|
|
6249
|
+
const known = block.spans.get(message.messageId);
|
|
6250
|
+
if (known) {
|
|
6251
|
+
known.onStreamFrame(message, advancesStream);
|
|
6252
|
+
if (closesTurn) block.closed = true;
|
|
6253
|
+
return;
|
|
6254
|
+
}
|
|
6255
|
+
|
|
6256
|
+
if (block.closed) return;
|
|
6257
|
+
|
|
6258
|
+
if (!streaming) {
|
|
6259
|
+
// A bubble that arrives already settled never animates — `useStreamingText` only reveals
|
|
6260
|
+
// a row whose stream was on at mount — so it has no reveal to measure. Arriving as the
|
|
6261
|
+
// first bubble it also ends the wait.
|
|
6262
|
+
if (!block.firstAnnounced) block.first.cancel();
|
|
6263
|
+
block.firstAnnounced = true;
|
|
6264
|
+
if (closesTurn) block.closed = true;
|
|
6265
|
+
return;
|
|
6266
|
+
}
|
|
6267
|
+
|
|
6268
|
+
// The first bubble takes over the span that waited for it; later bubbles get their own.
|
|
6269
|
+
const span = block.firstAnnounced ? this.createSpan(block.conversationId, {}) : block.first;
|
|
6270
|
+
block.firstAnnounced = true;
|
|
6271
|
+
span.onStreamStart(message);
|
|
6272
|
+
block.spans.set(message.messageId, span);
|
|
6273
|
+
}
|
|
6274
|
+
|
|
6275
|
+
private createSpan(
|
|
6276
|
+
conversationId: number | undefined,
|
|
6277
|
+
params: { messageSentAt?: number },
|
|
6278
|
+
): MessageLifecycleEventStatsSpan {
|
|
6279
|
+
const stats = new MessageLifecycleEventStats()
|
|
6280
|
+
.setConversationId(conversationId)
|
|
6281
|
+
.setCommitCallback(this.commitCallback);
|
|
6282
|
+
|
|
6283
|
+
return new MessageLifecycleEventStatsSpan(stats, {
|
|
6284
|
+
messageSentAt: params.messageSentAt,
|
|
6285
|
+
stopBackgroundWatch: this.watchBackgrounded?.() ?? null,
|
|
6286
|
+
});
|
|
6287
|
+
}
|
|
6288
|
+
|
|
6289
|
+
private discardBlock(): void {
|
|
6290
|
+
const block = this.block;
|
|
6291
|
+
if (!block) return;
|
|
6292
|
+
|
|
6293
|
+
block.first.cancel();
|
|
6294
|
+
block.spans.forEach((span) => span.cancel());
|
|
6295
|
+
this.block = null;
|
|
6296
|
+
}
|
|
6297
|
+
|
|
6298
|
+
/** The open block, only when it belongs to the channel in hand. */
|
|
6299
|
+
private blockFor(channelUrl: string): MessageLifecycleBlock | undefined {
|
|
6300
|
+
const block = this.block;
|
|
6301
|
+
if (!block || block.channelUrl !== channelUrl) return undefined;
|
|
6302
|
+
return block;
|
|
6303
|
+
}
|
|
6304
|
+
}
|
|
6305
|
+
|
|
6306
|
+
/** Why the turn never reached the screen. */
|
|
6307
|
+
declare type MessageLifecycleFailure = 'sent_timeout' | 'stream_failed' | 'animation_not_started';
|
|
6308
|
+
|
|
6309
|
+
declare type MessageLifecycleMeasure = {
|
|
6310
|
+
// The first bubble's span starts at the user's send and carries the wait; later bubbles start
|
|
6311
|
+
// at their own announcement and carry none of these.
|
|
6312
|
+
messageSentAt?: number;
|
|
6313
|
+
typingStartAt?: number;
|
|
6314
|
+
typingEndAt?: number;
|
|
6315
|
+
streamMessageId?: number;
|
|
6316
|
+
streamStartAt?: number;
|
|
6317
|
+
lastStreamAt?: number;
|
|
6318
|
+
streamEndAt?: number;
|
|
6319
|
+
animationStartAt?: number;
|
|
6320
|
+
startTimeoutTimer: TimeoutId | null;
|
|
6321
|
+
stallTimer: TimeoutId | null;
|
|
6322
|
+
stopBackgroundWatch: (() => boolean) | null;
|
|
6323
|
+
};
|
|
6324
|
+
|
|
6325
|
+
/** Every moment of the bubble, as wall-clock times rather than durations. */
|
|
6326
|
+
declare type MessageLifecycleTimestamps = {
|
|
6327
|
+
/** The bubble; absent when the reply never announced one. */
|
|
6328
|
+
messageId?: number;
|
|
6329
|
+
/** Only the first bubble of a turn was waited for, so only it carries the send and typing. */
|
|
6330
|
+
messageSentAt?: number;
|
|
6331
|
+
typingStartAt?: number;
|
|
6332
|
+
typingEndAt?: number;
|
|
6333
|
+
streamStartAt?: number;
|
|
6334
|
+
streamFailedAt?: number;
|
|
6335
|
+
lastStreamAt?: number;
|
|
6336
|
+
streamEndAt?: number;
|
|
6337
|
+
animationStartAt?: number;
|
|
6338
|
+
animationEndAt?: number;
|
|
6339
|
+
};
|
|
6340
|
+
|
|
5667
6341
|
export declare const MessageListUILayout: {
|
|
5668
6342
|
(props: PropsWithChildren): ReactNode;
|
|
5669
6343
|
defaults: {
|
|
@@ -6070,6 +6744,21 @@ export declare interface NudgeMessageResponse {
|
|
|
6070
6744
|
callbackUserMessage?: string;
|
|
6071
6745
|
}
|
|
6072
6746
|
|
|
6747
|
+
declare interface NudgeMessageState {
|
|
6748
|
+
/** The nudge to display; null while the messenger is opened or no nudge is active. */
|
|
6749
|
+
nudgeMessage: NudgeMessageResponse | null;
|
|
6750
|
+
/**
|
|
6751
|
+
* Counts nudges received and never falls back when one is cleared; each platform's renderer keys
|
|
6752
|
+
* the bubble on it to replay the entrance animation.
|
|
6753
|
+
*/
|
|
6754
|
+
nudgeMessageKey: number;
|
|
6755
|
+
dismissNudgeMessage: () => void;
|
|
6756
|
+
/** Starts a new conversation from the nudge: creates a channel, opens the messenger, and sends the callback user message. */
|
|
6757
|
+
startNudgeConversation: () => Promise<void>;
|
|
6758
|
+
}
|
|
6759
|
+
|
|
6760
|
+
declare type NudgeMessageViewProps = NudgeMessageState;
|
|
6761
|
+
|
|
6073
6762
|
declare type NumberField = {
|
|
6074
6763
|
key: string;
|
|
6075
6764
|
type: 'number';
|
|
@@ -6085,6 +6774,20 @@ declare type NumberField = {
|
|
|
6085
6774
|
};
|
|
6086
6775
|
};
|
|
6087
6776
|
|
|
6777
|
+
declare type ObservedCollectionEvent = {
|
|
6778
|
+
event: GroupChannelMessagesCollectionEvent;
|
|
6779
|
+
channelUrl: string;
|
|
6780
|
+
aiAgentUserId: string;
|
|
6781
|
+
conversationId?: number;
|
|
6782
|
+
};
|
|
6783
|
+
|
|
6784
|
+
declare type ObservedConversation = {
|
|
6785
|
+
channelUrl: string;
|
|
6786
|
+
aiAgentUserId: string;
|
|
6787
|
+
messages: ChatSDKBaseMessage[];
|
|
6788
|
+
conversationId?: number;
|
|
6789
|
+
};
|
|
6790
|
+
|
|
6088
6791
|
declare type OnJoinedProactiveConversation = (params: {
|
|
6089
6792
|
channel: ChatSDKGroupChannel;
|
|
6090
6793
|
}) => void;
|
|
@@ -6875,6 +7578,211 @@ declare type StreamAnimation = {
|
|
|
6875
7578
|
holdStop: (id: string | number) => void;
|
|
6876
7579
|
};
|
|
6877
7580
|
|
|
7581
|
+
declare class StreamingMessageValidationStats extends AIAgentBaseStats {
|
|
7582
|
+
private durationMs?: number;
|
|
7583
|
+
private messageId?: number;
|
|
7584
|
+
private chunkIntervalsMs: number[] = [];
|
|
7585
|
+
private regenerated = false;
|
|
7586
|
+
|
|
7587
|
+
setMessageId(messageId: number): this {
|
|
7588
|
+
this.messageId = messageId;
|
|
7589
|
+
return this;
|
|
7590
|
+
}
|
|
7591
|
+
|
|
7592
|
+
setStream(params: { durationMs: number; chunkIntervalsMs: number[]; regenerated: boolean }): this {
|
|
7593
|
+
this.durationMs = params.durationMs;
|
|
7594
|
+
this.chunkIntervalsMs = params.chunkIntervalsMs;
|
|
7595
|
+
this.regenerated = params.regenerated;
|
|
7596
|
+
return this;
|
|
7597
|
+
}
|
|
7598
|
+
|
|
7599
|
+
protected getStatType(): string {
|
|
7600
|
+
return AI_AGENT_ASSERTION_STAT_TYPE;
|
|
7601
|
+
}
|
|
7602
|
+
|
|
7603
|
+
protected getMetricKey(): string {
|
|
7604
|
+
return METRIC_KEY_STREAMING_MESSAGE_VALIDATION;
|
|
7605
|
+
}
|
|
7606
|
+
|
|
7607
|
+
protected buildPayload(): AIAgentStatPayload | null {
|
|
7608
|
+
if (this.durationMs === undefined || this.messageId === undefined) return null;
|
|
7609
|
+
|
|
7610
|
+
return this.createPayload(this.durationMs, {
|
|
7611
|
+
extra: {
|
|
7612
|
+
message_id: String(this.messageId),
|
|
7613
|
+
chunk_intervals_ms: this.chunkIntervalsMs,
|
|
7614
|
+
regenerated: this.regenerated,
|
|
7615
|
+
},
|
|
7616
|
+
});
|
|
7617
|
+
}
|
|
7618
|
+
}
|
|
7619
|
+
|
|
7620
|
+
/**
|
|
7621
|
+
* Spec: streaming_message_validation
|
|
7622
|
+
*
|
|
7623
|
+
* Measures how a single AI answer streamed in, and whether it was regenerated
|
|
7624
|
+
* mid-flight. Tracking is keyed by message_id because one stream is one message.
|
|
7625
|
+
*
|
|
7626
|
+
* Timeline:
|
|
7627
|
+
* - Start on the received message that announces a stream.
|
|
7628
|
+
* - Each later update that changes the text is one chunk; its interval is measured from
|
|
7629
|
+
* the previous text change. An update that repeats the current text is not a chunk, so
|
|
7630
|
+
* a redelivered snapshot folds into the next real interval instead of inventing one.
|
|
7631
|
+
* - Regeneration is a text that does not extend the previous text. It records a -1
|
|
7632
|
+
* marker followed by the interval from the last good chunk to the regenerated one.
|
|
7633
|
+
* - End on the update that clears the stream flag.
|
|
7634
|
+
*
|
|
7635
|
+
* Payload:
|
|
7636
|
+
* - value: MESG(stream=true) to MEDI(stream=false) duration.
|
|
7637
|
+
* - extra.message_id: the streamed message id.
|
|
7638
|
+
* - extra.chunk_intervals_ms: per-chunk intervals, with -1 markers at regenerations.
|
|
7639
|
+
* - extra.regenerated: whether the answer was regenerated at least once.
|
|
7640
|
+
*
|
|
7641
|
+
* Policy:
|
|
7642
|
+
* - Only realtime collection sources are observed; history redelivery is ignored.
|
|
7643
|
+
* - Only a received message opens a record; an update for an untracked message belongs to a
|
|
7644
|
+
* stream that was already dropped, and reopening it would measure a fragment of it.
|
|
7645
|
+
* - Only the AI agent's own streamed messages are tracked.
|
|
7646
|
+
* - Only streams that end explicitly are emitted; the sample is otherwise dropped.
|
|
7647
|
+
* - A new stream in the channel drops any stream still in flight, which is how a
|
|
7648
|
+
* stream that never terminates is discarded. A deleted message is dropped as it goes.
|
|
7649
|
+
* - A stream that spans a backgrounding is dropped, because a throttled tab makes the
|
|
7650
|
+
* intervals measure the browser rather than the server.
|
|
7651
|
+
*/
|
|
7652
|
+
declare class StreamingMessageValidationStatsTracker extends MultiStatsTrackerBase<
|
|
7653
|
+
StreamingMessageValidationStats,
|
|
7654
|
+
StreamValidationMeasure
|
|
7655
|
+
> {
|
|
7656
|
+
private watchBackgrounded?: BackgroundedWatcher;
|
|
7657
|
+
|
|
7658
|
+
/** Platform capability used to drop samples that spanned a backgrounding. */
|
|
7659
|
+
setBackgroundedWatcher(watchBackgrounded?: BackgroundedWatcher): void {
|
|
7660
|
+
this.watchBackgrounded = watchBackgrounded;
|
|
7661
|
+
}
|
|
7662
|
+
|
|
7663
|
+
onCollectionEvent(params: ObservedCollectionEvent): void {
|
|
7664
|
+
const event = params.event;
|
|
7665
|
+
if (event.channelUrl !== params.channelUrl) return;
|
|
7666
|
+
|
|
7667
|
+
// A deleted message can no longer deliver the update that ends its stream, so the sample
|
|
7668
|
+
// is gone with it — regardless of which source reported the deletion.
|
|
7669
|
+
if (event.kind === 'messagesDeleted') {
|
|
7670
|
+
event.messages.forEach((message) => {
|
|
7671
|
+
const record = this.records.get(message.messageId);
|
|
7672
|
+
if (record) this.drop(message.messageId, record);
|
|
7673
|
+
});
|
|
7674
|
+
return;
|
|
7675
|
+
}
|
|
7676
|
+
|
|
7677
|
+
if (event.kind === 'initialized') return;
|
|
7678
|
+
if (!isRealtimeSource(event.source)) return;
|
|
7679
|
+
|
|
7680
|
+
// Whether the collection added or updated the row says how the list absorbed the
|
|
7681
|
+
// message, not whether the stream is starting or advancing; only the delivering event
|
|
7682
|
+
// knows that, because a stream is announced by a MESG and advanced by MEDIs.
|
|
7683
|
+
const announcesStream = event.source === CollectionEventSource.EVENT_MESSAGE_RECEIVED;
|
|
7684
|
+
|
|
7685
|
+
event.messages.forEach((message) => {
|
|
7686
|
+
const record = this.records.get(message.messageId);
|
|
7687
|
+
|
|
7688
|
+
if (!record) {
|
|
7689
|
+
// Stream metadata is an AI convention rather than a guarantee, so the sender is
|
|
7690
|
+
// checked too: a handed-off human agent's message must never open a stream record.
|
|
7691
|
+
if (announcesStream && isSentBy(message, params.aiAgentUserId) && messageExtension.isStreaming(message)) {
|
|
7692
|
+
this.start(message, params.channelUrl, params.conversationId);
|
|
7693
|
+
}
|
|
7694
|
+
return;
|
|
7695
|
+
}
|
|
7696
|
+
if (record.measure.channelUrl !== params.channelUrl) return;
|
|
7697
|
+
|
|
7698
|
+
// The conversation is initialized after the channel is exposed, so a stream can open
|
|
7699
|
+
// before its id exists; the setter keeps the first real one it is given.
|
|
7700
|
+
record.stats.setConversationId(params.conversationId);
|
|
7701
|
+
this.advance(record, message);
|
|
7702
|
+
if (!messageExtension.isStreaming(message)) this.finish(message.messageId, record);
|
|
7703
|
+
});
|
|
7704
|
+
}
|
|
7705
|
+
|
|
7706
|
+
clearChannel(channelUrl?: string): void {
|
|
7707
|
+
if (!channelUrl) return;
|
|
7708
|
+
this.records.forEach((record, messageId) => {
|
|
7709
|
+
if (record.measure.channelUrl === channelUrl) this.drop(messageId, record);
|
|
7710
|
+
});
|
|
7711
|
+
}
|
|
7712
|
+
|
|
7713
|
+
clear(): void {
|
|
7714
|
+
this.records.forEach((record, messageId) => this.drop(messageId, record));
|
|
7715
|
+
}
|
|
7716
|
+
|
|
7717
|
+
private start(message: ChatSDKBaseMessage, channelUrl: string, conversationId?: number): void {
|
|
7718
|
+
// A stream that never ended can no longer end once the next one begins.
|
|
7719
|
+
this.clearChannel(channelUrl);
|
|
7720
|
+
|
|
7721
|
+
const startedAt = performance.now();
|
|
7722
|
+
this.records.set(message.messageId, {
|
|
7723
|
+
stats: new StreamingMessageValidationStats()
|
|
7724
|
+
.setMessageId(message.messageId)
|
|
7725
|
+
.setConversationId(conversationId)
|
|
7726
|
+
.setCommitCallback(this.commitCallback),
|
|
7727
|
+
measure: {
|
|
7728
|
+
channelUrl,
|
|
7729
|
+
startedAt,
|
|
7730
|
+
lastAt: startedAt,
|
|
7731
|
+
lastText: message.message,
|
|
7732
|
+
chunkIntervalsMs: [],
|
|
7733
|
+
regenerated: false,
|
|
7734
|
+
stopBackgroundWatch: this.watchBackgrounded?.() ?? null,
|
|
7735
|
+
},
|
|
7736
|
+
});
|
|
7737
|
+
}
|
|
7738
|
+
|
|
7739
|
+
private advance(
|
|
7740
|
+
record: StatsRecord<StreamingMessageValidationStats, StreamValidationMeasure>,
|
|
7741
|
+
message: ChatSDKBaseMessage,
|
|
7742
|
+
): void {
|
|
7743
|
+
const measure = record.measure;
|
|
7744
|
+
const text = message.message;
|
|
7745
|
+
|
|
7746
|
+
if (text === measure.lastText) return;
|
|
7747
|
+
|
|
7748
|
+
if (!text.startsWith(measure.lastText)) {
|
|
7749
|
+
measure.regenerated = true;
|
|
7750
|
+
measure.chunkIntervalsMs.push(REGENERATED_MARKER);
|
|
7751
|
+
}
|
|
7752
|
+
|
|
7753
|
+
const now = performance.now();
|
|
7754
|
+
measure.chunkIntervalsMs.push(Math.round(now - measure.lastAt));
|
|
7755
|
+
measure.lastText = text;
|
|
7756
|
+
measure.lastAt = now;
|
|
7757
|
+
}
|
|
7758
|
+
|
|
7759
|
+
private finish(
|
|
7760
|
+
messageId: number,
|
|
7761
|
+
record: StatsRecord<StreamingMessageValidationStats, StreamValidationMeasure>,
|
|
7762
|
+
): void {
|
|
7763
|
+
const measure = record.measure;
|
|
7764
|
+
this.records.delete(messageId);
|
|
7765
|
+
|
|
7766
|
+
if (measure.stopBackgroundWatch?.()) return;
|
|
7767
|
+
|
|
7768
|
+
record.stats
|
|
7769
|
+
.setStream({
|
|
7770
|
+
durationMs: Math.round(performance.now() - measure.startedAt),
|
|
7771
|
+
chunkIntervalsMs: measure.chunkIntervalsMs,
|
|
7772
|
+
regenerated: measure.regenerated,
|
|
7773
|
+
})
|
|
7774
|
+
.commit();
|
|
7775
|
+
}
|
|
7776
|
+
|
|
7777
|
+
private drop(
|
|
7778
|
+
messageId: string | number,
|
|
7779
|
+
record: StatsRecord<StreamingMessageValidationStats, StreamValidationMeasure>,
|
|
7780
|
+
): void {
|
|
7781
|
+
record.measure.stopBackgroundWatch?.();
|
|
7782
|
+
this.records.delete(messageId);
|
|
7783
|
+
}
|
|
7784
|
+
}
|
|
7785
|
+
|
|
6878
7786
|
declare type StreamMeasure = {
|
|
6879
7787
|
channelUrl?: string;
|
|
6880
7788
|
deliveryTimer: TimeoutId | null;
|
|
@@ -7203,6 +8111,16 @@ declare class StreamStalledStatsTracker extends MultiStatsTrackerBase<StreamStal
|
|
|
7203
8111
|
|
|
7204
8112
|
declare type StreamStallKind = 'delivery' | 'render';
|
|
7205
8113
|
|
|
8114
|
+
declare type StreamValidationMeasure = {
|
|
8115
|
+
channelUrl: string;
|
|
8116
|
+
startedAt: number;
|
|
8117
|
+
lastAt: number;
|
|
8118
|
+
lastText: string;
|
|
8119
|
+
chunkIntervalsMs: number[];
|
|
8120
|
+
regenerated: boolean;
|
|
8121
|
+
stopBackgroundWatch: (() => boolean) | null;
|
|
8122
|
+
};
|
|
8123
|
+
|
|
7206
8124
|
/**
|
|
7207
8125
|
* StringSet type for React components
|
|
7208
8126
|
* Uses SNAKE_CASE keys for backward compatibility
|