@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.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
- * @internal
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 when the user is inactive in launcher mode.
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(AI_AGENT_STAT_TYPE, payload);
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
- * @internal
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 when the user is inactive in launcher mode.
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
- onStreamAnimationComplete?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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?: () => void;
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, }: LauncherProps): JSX.Element;
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