@sendbird/ai-agent-messenger-react 1.40.0 → 1.41.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.
Files changed (4) hide show
  1. package/dist/index.cjs +164 -164
  2. package/dist/index.d.ts +902 -26
  3. package/dist/index.js +5326 -4707
  4. package/package.json +11 -7
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';
@@ -306,6 +307,11 @@ declare abstract class AIAgentBaseStats {
306
307
  protected abstract buildPayload(): AIAgentStatPayload | null;
307
308
  protected abstract getMetricKey(): string;
308
309
 
310
+ /** The stat type this metric is collected under; assertion metrics override it. */
311
+ protected getStatType(): string {
312
+ return AI_AGENT_STAT_TYPE;
313
+ }
314
+
309
315
  setCommitCallback(callback: StatsAppendCallback): this {
310
316
  this.commitCallback = callback;
311
317
  return this;
@@ -402,7 +408,7 @@ declare abstract class AIAgentBaseStats {
402
408
  if (!payload) return false;
403
409
 
404
410
  this.committed = true;
405
- return this.commitCallback(AI_AGENT_STAT_TYPE, payload);
411
+ return this.commitCallback(this.getStatType(), payload);
406
412
  }
407
413
  }
408
414
 
@@ -636,7 +642,15 @@ declare interface AIAgentConversationContextValue {
636
642
  input: InputState;
637
643
  };
638
644
 
645
+ // TODO: fold into `_stats.enabled`.
639
646
  _statsEnabled: boolean;
647
+ /** @internal What the platform packages report to the stats trackers. */
648
+ _stats: {
649
+ /** A message row's reveal animation starting. */
650
+ onMessageLifecycleAnimationStart: (messageId: number) => void;
651
+ /** A message row's reveal animation stopping, and whether it revealed everything. */
652
+ onMessageLifecycleAnimationComplete: (messageId: number, completed: boolean) => void;
653
+ };
640
654
  }
641
655
 
642
656
  declare interface AIAgentConversationListContextValue {
@@ -1142,6 +1156,9 @@ declare type AIAgentStatsTrackerCollection = {
1142
1156
  streamStalled: StreamStalledStatsTracker;
1143
1157
  typingIndicatorAbsentTimeout: TypingIndicatorAbsentTimeoutStatsTracker;
1144
1158
  renderedContentUnusable: RenderedContentUnusableStatsTracker;
1159
+ streamingMessageValidation: StreamingMessageValidationStatsTracker;
1160
+ duplicateMessageResponse: DuplicateMessageResponseStatsTracker;
1161
+ messageLifecycleEvent: MessageLifecycleEventStatsTracker;
1145
1162
  };
1146
1163
 
1147
1164
  declare type AIAgentStatsTrackers = AIAgentStatsTrackerCollection & {
@@ -1492,6 +1509,8 @@ declare type ChatSDKBaseChannelCollectionEventHandler = BaseChannelCollectionEve
1492
1509
 
1493
1510
  declare type ChatSDKBaseMessage = BaseMessage;
1494
1511
 
1512
+ declare type ChatSDKCollectionEventSource = CollectionEventSource;
1513
+
1495
1514
  declare type ChatSDKConversation = Conversation_2;
1496
1515
 
1497
1516
  declare type ChatSDKConversationStatus = ConversationStatus;
@@ -2975,6 +2994,86 @@ declare interface Dispatcher {
2975
2994
  ): void;
2976
2995
  }
2977
2996
 
2997
+ /**
2998
+ * Spec: duplicate_message_response
2999
+ *
3000
+ * Detects the AI giving the same answer text twice: either as two bubbles in a row, or once
3001
+ * per turn with the user asking again in between. The point is to size how often it happens,
3002
+ * not to suppress it.
3003
+ *
3004
+ * Timeline:
3005
+ * - Whenever the conversation changes, the latest settled reply is compared with the reply
3006
+ * before it. Anyone other than the agent is stepped over, so a turn in between does not
3007
+ * separate the two. A reply that cannot be compared — still streaming, or with no text —
3008
+ * does end the run, because it sits between them as an answer that was given.
3009
+ *
3010
+ * Payload:
3011
+ * - value: empty; the pair is the whole observation.
3012
+ * - extra.prev_message_id / extra.next_message_id: the two replies that matched.
3013
+ *
3014
+ * Policy:
3015
+ * - A reply that already reported a duplicate never reports one again, which is what keeps the
3016
+ * tail from being counted again every time it is re-read. That memory lives only for the
3017
+ * session: a duplicate is rare enough, and rarely still the last two replies on a later
3018
+ * visit, for a repeat count to matter.
3019
+ * - Runs of three or more are not modelled; each settled reply looks back exactly one reply.
3020
+ * - Only succeeded messages are read, because the list pins unsent ones past everything else.
3021
+ */
3022
+ declare class DuplicateMessageResponseStatsTracker extends StatsTrackerBase {
3023
+ private readonly emittedMessageChannels = new Map<number, string>();
3024
+
3025
+ onMessagesChanged(params: ObservedConversation): void {
3026
+ const replies = this.findLastTwoReplies(params.messages, params.aiAgentUserId);
3027
+ if (!replies) return;
3028
+ if (replies.previous.message !== replies.latest.message) return;
3029
+ if (this.emittedMessageChannels.has(replies.latest.messageId)) return;
3030
+
3031
+ this.emit(replies.latest, replies.previous, params);
3032
+ }
3033
+
3034
+ clear(): void {
3035
+ this.emittedMessageChannels.clear();
3036
+ }
3037
+
3038
+ private findLastTwoReplies(
3039
+ messages: ChatSDKBaseMessage[],
3040
+ aiAgentUserId: string,
3041
+ ): { latest: ChatSDKBaseMessage; previous: ChatSDKBaseMessage } | null {
3042
+ let latest: ChatSDKBaseMessage | null = null;
3043
+ let previous: ChatSDKBaseMessage | null = null;
3044
+
3045
+ forEachMessageNewestFirst(messages, (message) => {
3046
+ // Both list comparators pin an unsent message past every succeeded one, so its position
3047
+ // says nothing about when it was sent: it can neither end a run nor be compared.
3048
+ if (isSendableMessage(message) && message.sendingStatus !== SendingStatus.SUCCEEDED) return;
3049
+ // Anyone other than the agent is stepped over: answering two separate turns with the same
3050
+ // text is the same defect as answering one turn twice.
3051
+ if (!isSentBy(message, aiAgentUserId)) return;
3052
+ // A reply that cannot be compared sits between the two, so they are not adjacent either.
3053
+ if (!isComparableReply(message, aiAgentUserId)) return 'stop';
3054
+
3055
+ if (!latest) latest = message;
3056
+ else {
3057
+ previous = message;
3058
+ return 'stop';
3059
+ }
3060
+ });
3061
+
3062
+ return latest && previous ? { latest, previous } : null;
3063
+ }
3064
+
3065
+ private emit(latest: ChatSDKBaseMessage, previous: ChatSDKBaseMessage, params: ObservedConversation): void {
3066
+ evictOldestIfAtLimit(this.emittedMessageChannels, DUPLICATE_MESSAGE_RESPONSE_EMITTED_LIMIT);
3067
+ this.emittedMessageChannels.set(latest.messageId, params.channelUrl);
3068
+
3069
+ new DuplicateMessageResponseStats()
3070
+ .setDuplicatePair({ previousMessageId: previous.messageId, nextMessageId: latest.messageId })
3071
+ .setConversationId(params.conversationId)
3072
+ .setCommitCallback(this.commitCallback)
3073
+ .commit();
3074
+ }
3075
+ }
3076
+
2978
3077
  declare enum DurationKey {
2979
3078
  ACK_TO_FIRST_CHUNK = 'ack_to_first_chunk_ms',
2980
3079
  AFTER_TPEN_ABSENT = 'after_tpen_absent_ms',
@@ -3184,6 +3283,21 @@ export declare interface GroundednessInfo {
3184
3283
  preview_title: string;
3185
3284
  }
3186
3285
 
3286
+ declare type GroupChannelMessagesCollectionEvent =
3287
+ | {
3288
+ kind: 'initialized';
3289
+ source: 'cache' | 'api';
3290
+ channelUrl: string;
3291
+ messages: ChatSDKBaseMessage[];
3292
+ error?: Error | null;
3293
+ }
3294
+ | {
3295
+ kind: 'messagesAdded' | 'messagesUpdated' | 'messagesDeleted';
3296
+ source: ChatSDKCollectionEventSource;
3297
+ channelUrl: string;
3298
+ messages: ChatSDKBaseMessage[];
3299
+ };
3300
+
3187
3301
  declare interface HeaderTitleProps {
3188
3302
  /** Overrides the default title, which is derived from the channel (AI agent nickname or channel name). */
3189
3303
  title?: string;
@@ -3241,7 +3355,8 @@ BaseMessageProps<{
3241
3355
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
3242
3356
  onStreamAnimationStart?: () => void;
3243
3357
  onStreamAnimationProgress?: () => void;
3244
- onStreamAnimationComplete?: () => void;
3358
+ /** `completed` is false when the row went away before the text was fully revealed. */
3359
+ onStreamAnimationComplete?: (params: { completed: boolean }) => void;
3245
3360
  /**
3246
3361
  * Fired (layout effect) when this row starts/stops holding its text — the stream animation is
3247
3362
  * running but nothing is visible yet. The row renders nothing while holding, so the list draws its
@@ -3341,7 +3456,9 @@ export declare const IncomingMessageLayout: {
3341
3456
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
3342
3457
  onStreamAnimationStart?: () => void;
3343
3458
  onStreamAnimationProgress?: () => void;
3344
- onStreamAnimationComplete?: () => void;
3459
+ onStreamAnimationComplete?: (params: {
3460
+ completed: boolean;
3461
+ }) => void;
3345
3462
  onStreamAnimationHoldChange?: (holding: boolean) => void;
3346
3463
  messageTemplateErrorFallback?: ReactNode;
3347
3464
  messageTemplateLoadingFallback?: ReactNode;
@@ -3424,7 +3541,9 @@ export declare const IncomingMessageLayout: {
3424
3541
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
3425
3542
  onStreamAnimationStart?: () => void;
3426
3543
  onStreamAnimationProgress?: () => void;
3427
- onStreamAnimationComplete?: () => void;
3544
+ onStreamAnimationComplete?: (params: {
3545
+ completed: boolean;
3546
+ }) => void;
3428
3547
  onStreamAnimationHoldChange?: (holding: boolean) => void;
3429
3548
  messageTemplateErrorFallback?: ReactNode;
3430
3549
  messageTemplateLoadingFallback?: ReactNode;
@@ -3507,7 +3626,9 @@ export declare const IncomingMessageLayout: {
3507
3626
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
3508
3627
  onStreamAnimationStart?: () => void;
3509
3628
  onStreamAnimationProgress?: () => void;
3510
- onStreamAnimationComplete?: () => void;
3629
+ onStreamAnimationComplete?: (params: {
3630
+ completed: boolean;
3631
+ }) => void;
3511
3632
  onStreamAnimationHoldChange?: (holding: boolean) => void;
3512
3633
  messageTemplateErrorFallback?: ReactNode;
3513
3634
  messageTemplateLoadingFallback?: ReactNode;
@@ -3586,7 +3707,9 @@ export declare const IncomingMessageLayout: {
3586
3707
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
3587
3708
  onStreamAnimationStart?: () => void;
3588
3709
  onStreamAnimationProgress?: () => void;
3589
- onStreamAnimationComplete?: () => void;
3710
+ onStreamAnimationComplete?: (params: {
3711
+ completed: boolean;
3712
+ }) => void;
3590
3713
  onStreamAnimationHoldChange?: (holding: boolean) => void;
3591
3714
  messageTemplateErrorFallback?: ReactNode;
3592
3715
  messageTemplateLoadingFallback?: ReactNode;
@@ -3669,7 +3792,9 @@ export declare const IncomingMessageLayout: {
3669
3792
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
3670
3793
  onStreamAnimationStart?: () => void;
3671
3794
  onStreamAnimationProgress?: () => void;
3672
- onStreamAnimationComplete?: () => void;
3795
+ onStreamAnimationComplete?: (params: {
3796
+ completed: boolean;
3797
+ }) => void;
3673
3798
  onStreamAnimationHoldChange?: (holding: boolean) => void;
3674
3799
  messageTemplateErrorFallback?: ReactNode;
3675
3800
  messageTemplateLoadingFallback?: ReactNode;
@@ -3752,7 +3877,9 @@ export declare const IncomingMessageLayout: {
3752
3877
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
3753
3878
  onStreamAnimationStart?: () => void;
3754
3879
  onStreamAnimationProgress?: () => void;
3755
- onStreamAnimationComplete?: () => void;
3880
+ onStreamAnimationComplete?: (params: {
3881
+ completed: boolean;
3882
+ }) => void;
3756
3883
  onStreamAnimationHoldChange?: (holding: boolean) => void;
3757
3884
  messageTemplateErrorFallback?: ReactNode;
3758
3885
  messageTemplateLoadingFallback?: ReactNode;
@@ -3864,7 +3991,9 @@ export declare const IncomingMessageLayout: {
3864
3991
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
3865
3992
  onStreamAnimationStart?: () => void;
3866
3993
  onStreamAnimationProgress?: () => void;
3867
- onStreamAnimationComplete?: () => void;
3994
+ onStreamAnimationComplete?: (params: {
3995
+ completed: boolean;
3996
+ }) => void;
3868
3997
  onStreamAnimationHoldChange?: (holding: boolean) => void;
3869
3998
  messageTemplateErrorFallback?: ReactNode;
3870
3999
  messageTemplateLoadingFallback?: ReactNode;
@@ -3947,7 +4076,9 @@ export declare const IncomingMessageLayout: {
3947
4076
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
3948
4077
  onStreamAnimationStart?: () => void;
3949
4078
  onStreamAnimationProgress?: () => void;
3950
- onStreamAnimationComplete?: () => void;
4079
+ onStreamAnimationComplete?: (params: {
4080
+ completed: boolean;
4081
+ }) => void;
3951
4082
  onStreamAnimationHoldChange?: (holding: boolean) => void;
3952
4083
  messageTemplateErrorFallback?: ReactNode;
3953
4084
  messageTemplateLoadingFallback?: ReactNode;
@@ -4030,7 +4161,9 @@ export declare const IncomingMessageLayout: {
4030
4161
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
4031
4162
  onStreamAnimationStart?: () => void;
4032
4163
  onStreamAnimationProgress?: () => void;
4033
- onStreamAnimationComplete?: () => void;
4164
+ onStreamAnimationComplete?: (params: {
4165
+ completed: boolean;
4166
+ }) => void;
4034
4167
  onStreamAnimationHoldChange?: (holding: boolean) => void;
4035
4168
  messageTemplateErrorFallback?: ReactNode;
4036
4169
  messageTemplateLoadingFallback?: ReactNode;
@@ -4109,7 +4242,9 @@ export declare const IncomingMessageLayout: {
4109
4242
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
4110
4243
  onStreamAnimationStart?: () => void;
4111
4244
  onStreamAnimationProgress?: () => void;
4112
- onStreamAnimationComplete?: () => void;
4245
+ onStreamAnimationComplete?: (params: {
4246
+ completed: boolean;
4247
+ }) => void;
4113
4248
  onStreamAnimationHoldChange?: (holding: boolean) => void;
4114
4249
  messageTemplateErrorFallback?: ReactNode;
4115
4250
  messageTemplateLoadingFallback?: ReactNode;
@@ -4192,7 +4327,9 @@ export declare const IncomingMessageLayout: {
4192
4327
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
4193
4328
  onStreamAnimationStart?: () => void;
4194
4329
  onStreamAnimationProgress?: () => void;
4195
- onStreamAnimationComplete?: () => void;
4330
+ onStreamAnimationComplete?: (params: {
4331
+ completed: boolean;
4332
+ }) => void;
4196
4333
  onStreamAnimationHoldChange?: (holding: boolean) => void;
4197
4334
  messageTemplateErrorFallback?: ReactNode;
4198
4335
  messageTemplateLoadingFallback?: ReactNode;
@@ -4275,7 +4412,9 @@ export declare const IncomingMessageLayout: {
4275
4412
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
4276
4413
  onStreamAnimationStart?: () => void;
4277
4414
  onStreamAnimationProgress?: () => void;
4278
- onStreamAnimationComplete?: () => void;
4415
+ onStreamAnimationComplete?: (params: {
4416
+ completed: boolean;
4417
+ }) => void;
4279
4418
  onStreamAnimationHoldChange?: (holding: boolean) => void;
4280
4419
  messageTemplateErrorFallback?: ReactNode;
4281
4420
  messageTemplateLoadingFallback?: ReactNode;
@@ -4382,7 +4521,9 @@ export declare const IncomingMessageLayout: {
4382
4521
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
4383
4522
  onStreamAnimationStart?: () => void;
4384
4523
  onStreamAnimationProgress?: () => void;
4385
- onStreamAnimationComplete?: () => void;
4524
+ onStreamAnimationComplete?: (params: {
4525
+ completed: boolean;
4526
+ }) => void;
4386
4527
  onStreamAnimationHoldChange?: (holding: boolean) => void;
4387
4528
  messageTemplateErrorFallback?: ReactNode;
4388
4529
  messageTemplateLoadingFallback?: ReactNode;
@@ -4465,7 +4606,9 @@ export declare const IncomingMessageLayout: {
4465
4606
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
4466
4607
  onStreamAnimationStart?: () => void;
4467
4608
  onStreamAnimationProgress?: () => void;
4468
- onStreamAnimationComplete?: () => void;
4609
+ onStreamAnimationComplete?: (params: {
4610
+ completed: boolean;
4611
+ }) => void;
4469
4612
  onStreamAnimationHoldChange?: (holding: boolean) => void;
4470
4613
  messageTemplateErrorFallback?: ReactNode;
4471
4614
  messageTemplateLoadingFallback?: ReactNode;
@@ -4548,7 +4691,9 @@ export declare const IncomingMessageLayout: {
4548
4691
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
4549
4692
  onStreamAnimationStart?: () => void;
4550
4693
  onStreamAnimationProgress?: () => void;
4551
- onStreamAnimationComplete?: () => void;
4694
+ onStreamAnimationComplete?: (params: {
4695
+ completed: boolean;
4696
+ }) => void;
4552
4697
  onStreamAnimationHoldChange?: (holding: boolean) => void;
4553
4698
  messageTemplateErrorFallback?: ReactNode;
4554
4699
  messageTemplateLoadingFallback?: ReactNode;
@@ -4627,7 +4772,9 @@ export declare const IncomingMessageLayout: {
4627
4772
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
4628
4773
  onStreamAnimationStart?: () => void;
4629
4774
  onStreamAnimationProgress?: () => void;
4630
- onStreamAnimationComplete?: () => void;
4775
+ onStreamAnimationComplete?: (params: {
4776
+ completed: boolean;
4777
+ }) => void;
4631
4778
  onStreamAnimationHoldChange?: (holding: boolean) => void;
4632
4779
  messageTemplateErrorFallback?: ReactNode;
4633
4780
  messageTemplateLoadingFallback?: ReactNode;
@@ -4710,7 +4857,9 @@ export declare const IncomingMessageLayout: {
4710
4857
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
4711
4858
  onStreamAnimationStart?: () => void;
4712
4859
  onStreamAnimationProgress?: () => void;
4713
- onStreamAnimationComplete?: () => void;
4860
+ onStreamAnimationComplete?: (params: {
4861
+ completed: boolean;
4862
+ }) => void;
4714
4863
  onStreamAnimationHoldChange?: (holding: boolean) => void;
4715
4864
  messageTemplateErrorFallback?: ReactNode;
4716
4865
  messageTemplateLoadingFallback?: ReactNode;
@@ -4793,7 +4942,9 @@ export declare const IncomingMessageLayout: {
4793
4942
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
4794
4943
  onStreamAnimationStart?: () => void;
4795
4944
  onStreamAnimationProgress?: () => void;
4796
- onStreamAnimationComplete?: () => void;
4945
+ onStreamAnimationComplete?: (params: {
4946
+ completed: boolean;
4947
+ }) => void;
4797
4948
  onStreamAnimationHoldChange?: (holding: boolean) => void;
4798
4949
  messageTemplateErrorFallback?: ReactNode;
4799
4950
  messageTemplateLoadingFallback?: ReactNode;
@@ -4923,7 +5074,9 @@ export declare const IncomingMessageLayout: {
4923
5074
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
4924
5075
  onStreamAnimationStart?: () => void;
4925
5076
  onStreamAnimationProgress?: () => void;
4926
- onStreamAnimationComplete?: () => void;
5077
+ onStreamAnimationComplete?: (params: {
5078
+ completed: boolean;
5079
+ }) => void;
4927
5080
  onStreamAnimationHoldChange?: (holding: boolean) => void;
4928
5081
  messageTemplateErrorFallback?: ReactNode;
4929
5082
  messageTemplateLoadingFallback?: ReactNode;
@@ -5006,7 +5159,9 @@ export declare const IncomingMessageLayout: {
5006
5159
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
5007
5160
  onStreamAnimationStart?: () => void;
5008
5161
  onStreamAnimationProgress?: () => void;
5009
- onStreamAnimationComplete?: () => void;
5162
+ onStreamAnimationComplete?: (params: {
5163
+ completed: boolean;
5164
+ }) => void;
5010
5165
  onStreamAnimationHoldChange?: (holding: boolean) => void;
5011
5166
  messageTemplateErrorFallback?: ReactNode;
5012
5167
  messageTemplateLoadingFallback?: ReactNode;
@@ -5089,7 +5244,9 @@ export declare const IncomingMessageLayout: {
5089
5244
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
5090
5245
  onStreamAnimationStart?: () => void;
5091
5246
  onStreamAnimationProgress?: () => void;
5092
- onStreamAnimationComplete?: () => void;
5247
+ onStreamAnimationComplete?: (params: {
5248
+ completed: boolean;
5249
+ }) => void;
5093
5250
  onStreamAnimationHoldChange?: (holding: boolean) => void;
5094
5251
  messageTemplateErrorFallback?: ReactNode;
5095
5252
  messageTemplateLoadingFallback?: ReactNode;
@@ -5170,7 +5327,9 @@ export declare const IncomingMessageLayout: {
5170
5327
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
5171
5328
  onStreamAnimationStart?: () => void;
5172
5329
  onStreamAnimationProgress?: () => void;
5173
- onStreamAnimationComplete?: () => void;
5330
+ onStreamAnimationComplete?: (params: {
5331
+ completed: boolean;
5332
+ }) => void;
5174
5333
  onStreamAnimationHoldChange?: (holding: boolean) => void;
5175
5334
  messageTemplateErrorFallback?: ReactNode;
5176
5335
  messageTemplateLoadingFallback?: ReactNode;
@@ -5253,7 +5412,9 @@ export declare const IncomingMessageLayout: {
5253
5412
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
5254
5413
  onStreamAnimationStart?: () => void;
5255
5414
  onStreamAnimationProgress?: () => void;
5256
- onStreamAnimationComplete?: () => void;
5415
+ onStreamAnimationComplete?: (params: {
5416
+ completed: boolean;
5417
+ }) => void;
5257
5418
  onStreamAnimationHoldChange?: (holding: boolean) => void;
5258
5419
  messageTemplateErrorFallback?: ReactNode;
5259
5420
  messageTemplateLoadingFallback?: ReactNode;
@@ -5336,7 +5497,9 @@ export declare const IncomingMessageLayout: {
5336
5497
  onSendChallengeAction?: <T = object>(params: ChallengeActionParams) => Promise<T>;
5337
5498
  onStreamAnimationStart?: () => void;
5338
5499
  onStreamAnimationProgress?: () => void;
5339
- onStreamAnimationComplete?: () => void;
5500
+ onStreamAnimationComplete?: (params: {
5501
+ completed: boolean;
5502
+ }) => void;
5340
5503
  onStreamAnimationHoldChange?: (holding: boolean) => void;
5341
5504
  messageTemplateErrorFallback?: ReactNode;
5342
5505
  messageTemplateLoadingFallback?: ReactNode;
@@ -5664,6 +5827,477 @@ declare interface MemoryInfo {
5664
5827
  * */
5665
5828
  declare type MessageGroupType = 'single' | 'top' | 'bottom' | 'middle';
5666
5829
 
5830
+ /**
5831
+ * One turn: the acknowledged user message and every bubble that answers it.
5832
+ *
5833
+ * `first` is the first bubble's span, opened at the send so it can time out and carry the
5834
+ * typing; it joins `spans` once a bubble announces its stream. The block stops accepting
5835
+ * bubbles once the one flagged last has closed its stream, and is discarded whole by the next
5836
+ * send.
5837
+ */
5838
+ declare type MessageLifecycleBlock = {
5839
+ channelUrl: string;
5840
+ userMessageId: number;
5841
+ conversationId?: number;
5842
+ first: MessageLifecycleEventStatsSpan;
5843
+ firstAnnounced: boolean;
5844
+ spans: Map<number, MessageLifecycleEventStatsSpan>;
5845
+ // The bubble flagged `is_last_in_turn` on any of its frames; the block closes once its
5846
+ // stream does, whether or not the closing frame repeats the flag.
5847
+ lastMessageId?: number;
5848
+ closed: boolean;
5849
+ };
5850
+
5851
+ declare class MessageLifecycleEventStats extends AIAgentBaseStats {
5852
+ private timestamps?: MessageLifecycleTimestamps;
5853
+
5854
+ /**
5855
+ * The order these moments happen in is fixed, but the order they arrive in is not, so every
5856
+ * one is stored as it came and the durations between them are left to be derived later.
5857
+ */
5858
+ setTimestamps(timestamps: MessageLifecycleTimestamps): this {
5859
+ this.timestamps = timestamps;
5860
+ return this;
5861
+ }
5862
+
5863
+ /** A domain string, never a caught error, so no payload value can leak into the description. */
5864
+ setFailure(failure: MessageLifecycleFailure): this {
5865
+ this.setError(failure);
5866
+ return this;
5867
+ }
5868
+
5869
+ protected getStatType(): string {
5870
+ return AI_AGENT_ASSERTION_STAT_TYPE;
5871
+ }
5872
+
5873
+ protected getMetricKey(): string {
5874
+ return METRIC_KEY_MESSAGE_LIFECYCLE_EVENT;
5875
+ }
5876
+
5877
+ protected buildPayload(): AIAgentStatPayload | null {
5878
+ const timestamps = this.timestamps;
5879
+ if (!timestamps) return null;
5880
+
5881
+ // The reveal is measured from the send for the first bubble and from its own announcement
5882
+ // for the bubbles after it.
5883
+ const revealFrom = timestamps.messageSentAt ?? timestamps.streamStartAt;
5884
+ const animationEndAt = timestamps.animationEndAt;
5885
+ const value =
5886
+ animationEndAt === undefined || revealFrom === undefined ? UNMEASURABLE_DURATION : animationEndAt - revealFrom;
5887
+
5888
+ return this.createPayload(value, {
5889
+ extra: {
5890
+ message_id: timestamps.messageId === undefined ? undefined : String(timestamps.messageId),
5891
+ message_sent_at: timestamps.messageSentAt,
5892
+ typing_start_at: timestamps.typingStartAt,
5893
+ typing_end_at: timestamps.typingEndAt,
5894
+ stream_start_at: timestamps.streamStartAt,
5895
+ stream_failed_at: timestamps.streamFailedAt,
5896
+ last_stream_at: timestamps.lastStreamAt,
5897
+ stream_end_at: timestamps.streamEndAt,
5898
+ animation_start_at: timestamps.animationStartAt,
5899
+ animation_end_at: timestamps.animationEndAt,
5900
+ },
5901
+ });
5902
+ }
5903
+ }
5904
+
5905
+ /**
5906
+ * One bubble being measured, from its announcement to the reveal that shows it. The first
5907
+ * bubble's span opens earlier, at the acknowledged user message, so it also owns the wait for
5908
+ * the reply to start.
5909
+ *
5910
+ * The span owns its own timers, so a discarded attempt cannot report on a later one: cancelling
5911
+ * severs the metric reference, and a timer that fires afterwards finds nothing to record.
5912
+ */
5913
+ declare class MessageLifecycleEventStatsSpan extends StatsSpanBase<MessageLifecycleEventStats, MessageLifecycleMeasure> {
5914
+ protected measure: MessageLifecycleMeasure;
5915
+
5916
+ constructor(
5917
+ stats: MessageLifecycleEventStats | null,
5918
+ params: { messageSentAt?: number; stopBackgroundWatch: (() => boolean) | null },
5919
+ ) {
5920
+ // Nothing calls setVisible on this tracker, so a span is never hidden.
5921
+ super(stats, true);
5922
+ this.measure = {
5923
+ messageSentAt: params.messageSentAt,
5924
+ startTimeoutTimer: null,
5925
+ stallTimer: null,
5926
+ stopBackgroundWatch: params.stopBackgroundWatch,
5927
+ };
5928
+ }
5929
+
5930
+ /** Starts the window the input stays blocked for; a reply that never streams runs it out. */
5931
+ armStartTimeout(): void {
5932
+ this.measure.startTimeoutTimer = setTimeout(() => this.failWith('sent_timeout'), STREAM_START_TIMEOUT_MS);
5933
+ }
5934
+
5935
+ setConversationId(conversationId?: number): void {
5936
+ this.stats?.setConversationId(conversationId);
5937
+ }
5938
+
5939
+ onTypingChanged(isAgentTyping: boolean): void {
5940
+ if (!this.stats) return;
5941
+
5942
+ // Only the first pair of edges describes the wait; later ones belong to a later reply.
5943
+ if (isAgentTyping) {
5944
+ if (this.measure.typingStartAt === undefined) this.measure.typingStartAt = Date.now();
5945
+ return;
5946
+ }
5947
+ if (this.measure.typingStartAt !== undefined && this.measure.typingEndAt === undefined)
5948
+ this.measure.typingEndAt = Date.now();
5949
+ }
5950
+
5951
+ /** Latches onto the bubble announcing its stream; nothing else this span sees can be a chunk. */
5952
+ onStreamStart(message: ChatSDKBaseMessage): void {
5953
+ if (!this.stats) return;
5954
+
5955
+ this.measure.streamMessageId = message.messageId;
5956
+ this.measure.streamStartAt = Date.now();
5957
+ this.clearStartTimeout();
5958
+ this.armStall();
5959
+ }
5960
+
5961
+ onStreamFrame(message: ChatSDKBaseMessage, advancesStream: boolean): void {
5962
+ if (!this.stats) return;
5963
+
5964
+ const now = Date.now();
5965
+ // Only a MEDI carries a chunk. The stall timer is armed by either, because a redelivered
5966
+ // announcement still says the server is there.
5967
+ if (advancesStream) this.measure.lastStreamAt = now;
5968
+
5969
+ if (messageExtension.isStreaming(message)) return this.armStall();
5970
+
5971
+ this.measure.streamEndAt = now;
5972
+ this.clearStall();
5973
+ if (this.measure.animationStartAt === undefined) this.failWith('animation_not_started');
5974
+ }
5975
+
5976
+ onAnimationStart(): void {
5977
+ if (!this.stats) return;
5978
+ if (this.measure.animationStartAt === undefined) this.measure.animationStartAt = Date.now();
5979
+ }
5980
+
5981
+ onAnimationComplete(completed: boolean): void {
5982
+ if (!this.stats) return;
5983
+
5984
+ // A reveal cut short says nothing about how long the answer took to appear.
5985
+ if (!completed) return this.cancel();
5986
+
5987
+ this.settle({ animationEndAt: Date.now() });
5988
+ }
5989
+
5990
+ override cancel(): void {
5991
+ this.clearTimers();
5992
+ this.measure.stopBackgroundWatch?.();
5993
+ super.cancel();
5994
+ }
5995
+
5996
+ private failWith(failure: MessageLifecycleFailure): void {
5997
+ if (!this.stats) return;
5998
+
5999
+ this.stats.setFailure(failure);
6000
+ if (failure !== 'stream_failed') return this.settle({});
6001
+
6002
+ // The last chunk time only describes a stall, and a stall that never produced a chunk
6003
+ // drops the stream keys entirely: their absence is what marks it in the payload.
6004
+ this.settle(
6005
+ this.measure.lastStreamAt === undefined
6006
+ ? { streamFailedAt: Date.now(), streamStartAt: undefined }
6007
+ : { streamFailedAt: Date.now(), lastStreamAt: this.measure.lastStreamAt },
6008
+ );
6009
+ }
6010
+
6011
+ private settle(ending: {
6012
+ animationEndAt?: number;
6013
+ streamFailedAt?: number;
6014
+ lastStreamAt?: number;
6015
+ streamStartAt?: number;
6016
+ }): void {
6017
+ if (!this.stats) return;
6018
+
6019
+ this.clearTimers();
6020
+
6021
+ // A backgrounded app stops rendering, so the moments after it went away are not the
6022
+ // product's timing any more.
6023
+ if (this.measure.stopBackgroundWatch?.()) return super.cancel();
6024
+
6025
+ this.stats.setTimestamps({
6026
+ messageId: this.measure.streamMessageId,
6027
+ messageSentAt: this.measure.messageSentAt,
6028
+ typingStartAt: this.measure.typingStartAt,
6029
+ typingEndAt: this.measure.typingEndAt,
6030
+ streamStartAt: this.measure.streamStartAt,
6031
+ streamEndAt: this.measure.streamEndAt,
6032
+ animationStartAt: this.measure.animationStartAt,
6033
+ ...ending,
6034
+ });
6035
+ this.commitOrDefer();
6036
+ }
6037
+
6038
+ private armStall(): void {
6039
+ this.clearStall();
6040
+ this.measure.stallTimer = setTimeout(() => this.failWith('stream_failed'), STREAM_STALL_TIMEOUT_MS);
6041
+ }
6042
+
6043
+ private clearTimers(): void {
6044
+ this.clearStartTimeout();
6045
+ this.clearStall();
6046
+ }
6047
+
6048
+ private clearStartTimeout(): void {
6049
+ if (this.measure.startTimeoutTimer !== null) clearTimeout(this.measure.startTimeoutTimer);
6050
+ this.measure.startTimeoutTimer = null;
6051
+ }
6052
+
6053
+ private clearStall(): void {
6054
+ if (this.measure.stallTimer !== null) clearTimeout(this.measure.stallTimer);
6055
+ this.measure.stallTimer = null;
6056
+ }
6057
+ }
6058
+
6059
+ /**
6060
+ * Spec: message_lifecycle_event
6061
+ *
6062
+ * Records when each moment of one reply happened, from the user sending a message to the
6063
+ * reply finishing its reveal. The moments are emitted as wall-clock times rather than
6064
+ * durations, because their order of arrival is not guaranteed even though their order of
6065
+ * occurrence is: deriving durations here would bake a wrong value into the sample.
6066
+ *
6067
+ * Timeline:
6068
+ * - The user's message is acknowledged, which opens the block.
6069
+ * - The agent starts and stops typing.
6070
+ * - Each bubble announces a stream, streams its chunks, and closes it. A stream is announced
6071
+ * by a MESG and advanced by MEDIs, which the delivering event says and the collection's own
6072
+ * add/update does not — so the same announcement arriving twice is not a chunk.
6073
+ * - Each bubble's reveal animation starts and finishes.
6074
+ *
6075
+ * Payload, one sample per bubble:
6076
+ * - value: animation end minus message sent for the first bubble, minus stream start for the
6077
+ * bubbles after it, or -1 when the reveal never happened.
6078
+ * - extra.message_id: the bubble.
6079
+ * - extra.*_at: each moment that happened; the rest are left out. Only the first bubble
6080
+ * carries the send and typing moments, since only it was waited for.
6081
+ * - error_description: sent_timeout, stream_failed or animation_not_started.
6082
+ *
6083
+ * Method call order:
6084
+ * - `onUserMessageSent` opens a block and discards whatever was open.
6085
+ * - Every other method addresses the open block; there is no handle for callers to hold,
6086
+ * because they are spread across the provider and both platform packages.
6087
+ *
6088
+ * Policy:
6089
+ * - One block at a time. The next message the user sends discards whatever is still open,
6090
+ * spans included, and a discarded span is non-recording, so its own timers can no longer
6091
+ * report anything.
6092
+ * - A reply split into bubbles is measured bubble by bubble: each announcement opens its own
6093
+ * span, and every span settles on its own. The block closes when the bubble flagged
6094
+ * `is_last_in_turn` closes its stream — the flag is only reliable then — and nothing waits
6095
+ * for a last bubble that never comes: the block is simply discarded by the next send.
6096
+ * - Only bubbles that reach the screen are measured. A bubble is dropped when the reveal is
6097
+ * cut short, when it arrives already settled and so never animates, when it is deleted,
6098
+ * when the conversation goes away, or when the app was backgrounded, because none of those
6099
+ * say anything about how fast the product answered.
6100
+ * - Every collection source is observed, because a reconnect delivers the rest of a stream
6101
+ * through a sync rather than a live event. Correlation is what scopes an event to this turn.
6102
+ * - A reply that never starts streaming within the input block window is reported as a
6103
+ * timeout, which covers both silence and typing that leads nowhere.
6104
+ * - A stream that closes before its reveal ever starts is a rendering fault, so it is reported
6105
+ * right away instead of waiting for an animation that is not coming.
6106
+ */
6107
+ declare class MessageLifecycleEventStatsTracker extends StatsTrackerBase {
6108
+ private block: MessageLifecycleBlock | null = null;
6109
+ private watchBackgrounded?: BackgroundedWatcher;
6110
+
6111
+ setBackgroundedWatcher(watchBackgrounded?: BackgroundedWatcher): void {
6112
+ this.watchBackgrounded = watchBackgrounded;
6113
+ }
6114
+
6115
+ /** Opens a block for the message the user just got acknowledged. */
6116
+ onUserMessageSent(params: { channelUrl: string; messageId: number; conversationId?: number }): void {
6117
+ this.discardBlock();
6118
+
6119
+ const first = this.createSpan(params.conversationId, { messageSentAt: Date.now() });
6120
+ first.armStartTimeout();
6121
+ this.block = {
6122
+ channelUrl: params.channelUrl,
6123
+ userMessageId: params.messageId,
6124
+ conversationId: params.conversationId,
6125
+ first,
6126
+ firstAnnounced: false,
6127
+ spans: new Map(),
6128
+ closed: false,
6129
+ };
6130
+ }
6131
+
6132
+ /**
6133
+ * The conversation is initialized after the channel is exposed, so a block can open before
6134
+ * its id exists — including for a turn that only ever commits on a timeout, which no later
6135
+ * event would reach. The setter keeps the first real id it is given.
6136
+ */
6137
+ onConversationIdResolved(params: { channelUrl: string; conversationId?: number }): void {
6138
+ const block = this.blockFor(params.channelUrl);
6139
+ if (!block) return;
6140
+
6141
+ block.conversationId ??= params.conversationId;
6142
+ block.first.setConversationId(params.conversationId);
6143
+ block.spans.forEach((span) => span.setConversationId(params.conversationId));
6144
+ }
6145
+
6146
+ onTypingChanged(params: { channelUrl: string; isAgentTyping: boolean }): void {
6147
+ // Typing describes the wait for the first bubble; the span that carries it keeps ignoring
6148
+ // later edges on its own.
6149
+ this.blockFor(params.channelUrl)?.first.onTypingChanged(params.isAgentTyping);
6150
+ }
6151
+
6152
+ onCollectionEvent(params: { event: GroupChannelMessagesCollectionEvent; channelUrl: string; aiAgentUserId: string }) {
6153
+ const block = this.blockFor(params.channelUrl);
6154
+ const event = params.event;
6155
+ if (!block) return;
6156
+ if (event.channelUrl !== params.channelUrl) return;
6157
+
6158
+ // A deleted bubble can no longer reach the screen, and the stall timer would report it as a
6159
+ // stream that died rather than one that was removed.
6160
+ if (event.kind === 'messagesDeleted') {
6161
+ event.messages.forEach((message) => block.spans.get(message.messageId)?.cancel());
6162
+ return;
6163
+ }
6164
+
6165
+ if (event.kind === 'initialized') return;
6166
+
6167
+ // The source is not filtered. A reconnect delivers the rest of a stream through the
6168
+ // collection's changelog sync rather than a live event (browser-e2e/tests/streaming.e2e.test.ts
6169
+ // TC-CPE-08), and rejecting that recovery leaves the turn to run out its timers and report a
6170
+ // failure the screen contradicts. Correlation is what scopes an event to this turn, and
6171
+ // only a MEDI counts as a chunk, so a recovery closes the bubble without inventing one.
6172
+ const advancesStream = event.source === CollectionEventSource.EVENT_MESSAGE_UPDATED;
6173
+
6174
+ event.messages.forEach((message) => {
6175
+ if (!isSentBy(message, params.aiAgentUserId)) return;
6176
+ // The reply names the message it answers, which is what ties it to this block.
6177
+ if (parseAIResponseMessageData(message.data).userMessageId !== block.userMessageId) return;
6178
+
6179
+ this.observeReply(block, message, advancesStream);
6180
+ });
6181
+ }
6182
+
6183
+ onAnimationStart(params: { messageId: number }): void {
6184
+ this.block?.spans.get(params.messageId)?.onAnimationStart();
6185
+ }
6186
+
6187
+ onAnimationComplete(params: { messageId: number; completed: boolean }): void {
6188
+ this.block?.spans.get(params.messageId)?.onAnimationComplete(params.completed);
6189
+ }
6190
+
6191
+ /** The conversation going away leaves the turn unfinished, so nothing is reported. */
6192
+ clearChannel(channelUrl?: string): void {
6193
+ if (!channelUrl || !this.blockFor(channelUrl)) return;
6194
+ this.discardBlock();
6195
+ }
6196
+
6197
+ clear(): void {
6198
+ this.discardBlock();
6199
+ }
6200
+
6201
+ private observeReply(block: MessageLifecycleBlock, message: ChatSDKBaseMessage, advancesStream: boolean): void {
6202
+ const streaming = messageExtension.isStreaming(message);
6203
+
6204
+ // Judged on the frames alone, so a bubble whose span is already settled or discarded can
6205
+ // still end the turn.
6206
+ if (isLastInTurn(message)) block.lastMessageId = message.messageId;
6207
+ const closesTurn = block.lastMessageId === message.messageId && !streaming;
6208
+
6209
+ const known = block.spans.get(message.messageId);
6210
+ if (known) {
6211
+ known.onStreamFrame(message, advancesStream);
6212
+ if (closesTurn) block.closed = true;
6213
+ return;
6214
+ }
6215
+
6216
+ if (block.closed) return;
6217
+
6218
+ if (!streaming) {
6219
+ // A bubble that arrives already settled never animates — `useStreamingText` only reveals
6220
+ // a row whose stream was on at mount — so it has no reveal to measure. Arriving as the
6221
+ // first bubble it also ends the wait.
6222
+ if (!block.firstAnnounced) block.first.cancel();
6223
+ block.firstAnnounced = true;
6224
+ if (closesTurn) block.closed = true;
6225
+ return;
6226
+ }
6227
+
6228
+ // The first bubble takes over the span that waited for it; later bubbles get their own.
6229
+ const span = block.firstAnnounced ? this.createSpan(block.conversationId, {}) : block.first;
6230
+ block.firstAnnounced = true;
6231
+ span.onStreamStart(message);
6232
+ block.spans.set(message.messageId, span);
6233
+ }
6234
+
6235
+ private createSpan(
6236
+ conversationId: number | undefined,
6237
+ params: { messageSentAt?: number },
6238
+ ): MessageLifecycleEventStatsSpan {
6239
+ const stats = new MessageLifecycleEventStats()
6240
+ .setConversationId(conversationId)
6241
+ .setCommitCallback(this.commitCallback);
6242
+
6243
+ return new MessageLifecycleEventStatsSpan(stats, {
6244
+ messageSentAt: params.messageSentAt,
6245
+ stopBackgroundWatch: this.watchBackgrounded?.() ?? null,
6246
+ });
6247
+ }
6248
+
6249
+ private discardBlock(): void {
6250
+ const block = this.block;
6251
+ if (!block) return;
6252
+
6253
+ block.first.cancel();
6254
+ block.spans.forEach((span) => span.cancel());
6255
+ this.block = null;
6256
+ }
6257
+
6258
+ /** The open block, only when it belongs to the channel in hand. */
6259
+ private blockFor(channelUrl: string): MessageLifecycleBlock | undefined {
6260
+ const block = this.block;
6261
+ if (!block || block.channelUrl !== channelUrl) return undefined;
6262
+ return block;
6263
+ }
6264
+ }
6265
+
6266
+ /** Why the turn never reached the screen. */
6267
+ declare type MessageLifecycleFailure = 'sent_timeout' | 'stream_failed' | 'animation_not_started';
6268
+
6269
+ declare type MessageLifecycleMeasure = {
6270
+ // The first bubble's span starts at the user's send and carries the wait; later bubbles start
6271
+ // at their own announcement and carry none of these.
6272
+ messageSentAt?: number;
6273
+ typingStartAt?: number;
6274
+ typingEndAt?: number;
6275
+ streamMessageId?: number;
6276
+ streamStartAt?: number;
6277
+ lastStreamAt?: number;
6278
+ streamEndAt?: number;
6279
+ animationStartAt?: number;
6280
+ startTimeoutTimer: TimeoutId | null;
6281
+ stallTimer: TimeoutId | null;
6282
+ stopBackgroundWatch: (() => boolean) | null;
6283
+ };
6284
+
6285
+ /** Every moment of the bubble, as wall-clock times rather than durations. */
6286
+ declare type MessageLifecycleTimestamps = {
6287
+ /** The bubble; absent when the reply never announced one. */
6288
+ messageId?: number;
6289
+ /** Only the first bubble of a turn was waited for, so only it carries the send and typing. */
6290
+ messageSentAt?: number;
6291
+ typingStartAt?: number;
6292
+ typingEndAt?: number;
6293
+ streamStartAt?: number;
6294
+ streamFailedAt?: number;
6295
+ lastStreamAt?: number;
6296
+ streamEndAt?: number;
6297
+ animationStartAt?: number;
6298
+ animationEndAt?: number;
6299
+ };
6300
+
5667
6301
  export declare const MessageListUILayout: {
5668
6302
  (props: PropsWithChildren): ReactNode;
5669
6303
  defaults: {
@@ -6085,6 +6719,20 @@ declare type NumberField = {
6085
6719
  };
6086
6720
  };
6087
6721
 
6722
+ declare type ObservedCollectionEvent = {
6723
+ event: GroupChannelMessagesCollectionEvent;
6724
+ channelUrl: string;
6725
+ aiAgentUserId: string;
6726
+ conversationId?: number;
6727
+ };
6728
+
6729
+ declare type ObservedConversation = {
6730
+ channelUrl: string;
6731
+ aiAgentUserId: string;
6732
+ messages: ChatSDKBaseMessage[];
6733
+ conversationId?: number;
6734
+ };
6735
+
6088
6736
  declare type OnJoinedProactiveConversation = (params: {
6089
6737
  channel: ChatSDKGroupChannel;
6090
6738
  }) => void;
@@ -6363,6 +7011,14 @@ export declare interface PrecheckRecord {
6363
7011
  key: string;
6364
7012
  }
6365
7013
 
7014
+ /**
7015
+ * Consent prechecks status bar: a row of passed checks shown above a consent-gated
7016
+ * bot bubble. Rendered only in the dashboard tester (via the DashboardTester layout
7017
+ * override), so customers never see it.
7018
+ * @see @contexts/_UNSAFE_INTERNAL_DashboardTesterContext.tsx
7019
+ */
7020
+ export declare const Prechecks: ({ prechecks, maxBodyWidth }: Props_3) => ReactNode;
7021
+
6366
7022
  declare type PresentMethod = 'launcher_toggle' | 'direct_present' | 'embedded';
6367
7023
 
6368
7024
  declare type Props = {
@@ -6418,6 +7074,11 @@ declare type Props_2 = {
6418
7074
  }) => ReactNode;
6419
7075
  };
6420
7076
 
7077
+ declare type Props_3 = {
7078
+ prechecks: PrecheckRecord[];
7079
+ maxBodyWidth?: number;
7080
+ };
7081
+
6421
7082
  declare type ReactOnlyIconName = 'expand' | 'collapse' | 'chevron-right' | 'attach' | 'close-filled' | 'actionbook' | 'function' | 'confluence' | 'zendesk' | 'salesforce' | 'sprinklr' | 'website' | 'snippet' | 'template' | 'show' | 'mute' | 'activity';
6422
7083
 
6423
7084
  declare type RecoveryMeasure = {
@@ -6862,6 +7523,211 @@ declare type StreamAnimation = {
6862
7523
  holdStop: (id: string | number) => void;
6863
7524
  };
6864
7525
 
7526
+ declare class StreamingMessageValidationStats extends AIAgentBaseStats {
7527
+ private durationMs?: number;
7528
+ private messageId?: number;
7529
+ private chunkIntervalsMs: number[] = [];
7530
+ private regenerated = false;
7531
+
7532
+ setMessageId(messageId: number): this {
7533
+ this.messageId = messageId;
7534
+ return this;
7535
+ }
7536
+
7537
+ setStream(params: { durationMs: number; chunkIntervalsMs: number[]; regenerated: boolean }): this {
7538
+ this.durationMs = params.durationMs;
7539
+ this.chunkIntervalsMs = params.chunkIntervalsMs;
7540
+ this.regenerated = params.regenerated;
7541
+ return this;
7542
+ }
7543
+
7544
+ protected getStatType(): string {
7545
+ return AI_AGENT_ASSERTION_STAT_TYPE;
7546
+ }
7547
+
7548
+ protected getMetricKey(): string {
7549
+ return METRIC_KEY_STREAMING_MESSAGE_VALIDATION;
7550
+ }
7551
+
7552
+ protected buildPayload(): AIAgentStatPayload | null {
7553
+ if (this.durationMs === undefined || this.messageId === undefined) return null;
7554
+
7555
+ return this.createPayload(this.durationMs, {
7556
+ extra: {
7557
+ message_id: String(this.messageId),
7558
+ chunk_intervals_ms: this.chunkIntervalsMs,
7559
+ regenerated: this.regenerated,
7560
+ },
7561
+ });
7562
+ }
7563
+ }
7564
+
7565
+ /**
7566
+ * Spec: streaming_message_validation
7567
+ *
7568
+ * Measures how a single AI answer streamed in, and whether it was regenerated
7569
+ * mid-flight. Tracking is keyed by message_id because one stream is one message.
7570
+ *
7571
+ * Timeline:
7572
+ * - Start on the received message that announces a stream.
7573
+ * - Each later update that changes the text is one chunk; its interval is measured from
7574
+ * the previous text change. An update that repeats the current text is not a chunk, so
7575
+ * a redelivered snapshot folds into the next real interval instead of inventing one.
7576
+ * - Regeneration is a text that does not extend the previous text. It records a -1
7577
+ * marker followed by the interval from the last good chunk to the regenerated one.
7578
+ * - End on the update that clears the stream flag.
7579
+ *
7580
+ * Payload:
7581
+ * - value: MESG(stream=true) to MEDI(stream=false) duration.
7582
+ * - extra.message_id: the streamed message id.
7583
+ * - extra.chunk_intervals_ms: per-chunk intervals, with -1 markers at regenerations.
7584
+ * - extra.regenerated: whether the answer was regenerated at least once.
7585
+ *
7586
+ * Policy:
7587
+ * - Only realtime collection sources are observed; history redelivery is ignored.
7588
+ * - Only a received message opens a record; an update for an untracked message belongs to a
7589
+ * stream that was already dropped, and reopening it would measure a fragment of it.
7590
+ * - Only the AI agent's own streamed messages are tracked.
7591
+ * - Only streams that end explicitly are emitted; the sample is otherwise dropped.
7592
+ * - A new stream in the channel drops any stream still in flight, which is how a
7593
+ * stream that never terminates is discarded. A deleted message is dropped as it goes.
7594
+ * - A stream that spans a backgrounding is dropped, because a throttled tab makes the
7595
+ * intervals measure the browser rather than the server.
7596
+ */
7597
+ declare class StreamingMessageValidationStatsTracker extends MultiStatsTrackerBase<
7598
+ StreamingMessageValidationStats,
7599
+ StreamValidationMeasure
7600
+ > {
7601
+ private watchBackgrounded?: BackgroundedWatcher;
7602
+
7603
+ /** Platform capability used to drop samples that spanned a backgrounding. */
7604
+ setBackgroundedWatcher(watchBackgrounded?: BackgroundedWatcher): void {
7605
+ this.watchBackgrounded = watchBackgrounded;
7606
+ }
7607
+
7608
+ onCollectionEvent(params: ObservedCollectionEvent): void {
7609
+ const event = params.event;
7610
+ if (event.channelUrl !== params.channelUrl) return;
7611
+
7612
+ // A deleted message can no longer deliver the update that ends its stream, so the sample
7613
+ // is gone with it — regardless of which source reported the deletion.
7614
+ if (event.kind === 'messagesDeleted') {
7615
+ event.messages.forEach((message) => {
7616
+ const record = this.records.get(message.messageId);
7617
+ if (record) this.drop(message.messageId, record);
7618
+ });
7619
+ return;
7620
+ }
7621
+
7622
+ if (event.kind === 'initialized') return;
7623
+ if (!isRealtimeSource(event.source)) return;
7624
+
7625
+ // Whether the collection added or updated the row says how the list absorbed the
7626
+ // message, not whether the stream is starting or advancing; only the delivering event
7627
+ // knows that, because a stream is announced by a MESG and advanced by MEDIs.
7628
+ const announcesStream = event.source === CollectionEventSource.EVENT_MESSAGE_RECEIVED;
7629
+
7630
+ event.messages.forEach((message) => {
7631
+ const record = this.records.get(message.messageId);
7632
+
7633
+ if (!record) {
7634
+ // Stream metadata is an AI convention rather than a guarantee, so the sender is
7635
+ // checked too: a handed-off human agent's message must never open a stream record.
7636
+ if (announcesStream && isSentBy(message, params.aiAgentUserId) && messageExtension.isStreaming(message)) {
7637
+ this.start(message, params.channelUrl, params.conversationId);
7638
+ }
7639
+ return;
7640
+ }
7641
+ if (record.measure.channelUrl !== params.channelUrl) return;
7642
+
7643
+ // The conversation is initialized after the channel is exposed, so a stream can open
7644
+ // before its id exists; the setter keeps the first real one it is given.
7645
+ record.stats.setConversationId(params.conversationId);
7646
+ this.advance(record, message);
7647
+ if (!messageExtension.isStreaming(message)) this.finish(message.messageId, record);
7648
+ });
7649
+ }
7650
+
7651
+ clearChannel(channelUrl?: string): void {
7652
+ if (!channelUrl) return;
7653
+ this.records.forEach((record, messageId) => {
7654
+ if (record.measure.channelUrl === channelUrl) this.drop(messageId, record);
7655
+ });
7656
+ }
7657
+
7658
+ clear(): void {
7659
+ this.records.forEach((record, messageId) => this.drop(messageId, record));
7660
+ }
7661
+
7662
+ private start(message: ChatSDKBaseMessage, channelUrl: string, conversationId?: number): void {
7663
+ // A stream that never ended can no longer end once the next one begins.
7664
+ this.clearChannel(channelUrl);
7665
+
7666
+ const startedAt = performance.now();
7667
+ this.records.set(message.messageId, {
7668
+ stats: new StreamingMessageValidationStats()
7669
+ .setMessageId(message.messageId)
7670
+ .setConversationId(conversationId)
7671
+ .setCommitCallback(this.commitCallback),
7672
+ measure: {
7673
+ channelUrl,
7674
+ startedAt,
7675
+ lastAt: startedAt,
7676
+ lastText: message.message,
7677
+ chunkIntervalsMs: [],
7678
+ regenerated: false,
7679
+ stopBackgroundWatch: this.watchBackgrounded?.() ?? null,
7680
+ },
7681
+ });
7682
+ }
7683
+
7684
+ private advance(
7685
+ record: StatsRecord<StreamingMessageValidationStats, StreamValidationMeasure>,
7686
+ message: ChatSDKBaseMessage,
7687
+ ): void {
7688
+ const measure = record.measure;
7689
+ const text = message.message;
7690
+
7691
+ if (text === measure.lastText) return;
7692
+
7693
+ if (!text.startsWith(measure.lastText)) {
7694
+ measure.regenerated = true;
7695
+ measure.chunkIntervalsMs.push(REGENERATED_MARKER);
7696
+ }
7697
+
7698
+ const now = performance.now();
7699
+ measure.chunkIntervalsMs.push(Math.round(now - measure.lastAt));
7700
+ measure.lastText = text;
7701
+ measure.lastAt = now;
7702
+ }
7703
+
7704
+ private finish(
7705
+ messageId: number,
7706
+ record: StatsRecord<StreamingMessageValidationStats, StreamValidationMeasure>,
7707
+ ): void {
7708
+ const measure = record.measure;
7709
+ this.records.delete(messageId);
7710
+
7711
+ if (measure.stopBackgroundWatch?.()) return;
7712
+
7713
+ record.stats
7714
+ .setStream({
7715
+ durationMs: Math.round(performance.now() - measure.startedAt),
7716
+ chunkIntervalsMs: measure.chunkIntervalsMs,
7717
+ regenerated: measure.regenerated,
7718
+ })
7719
+ .commit();
7720
+ }
7721
+
7722
+ private drop(
7723
+ messageId: string | number,
7724
+ record: StatsRecord<StreamingMessageValidationStats, StreamValidationMeasure>,
7725
+ ): void {
7726
+ record.measure.stopBackgroundWatch?.();
7727
+ this.records.delete(messageId);
7728
+ }
7729
+ }
7730
+
6865
7731
  declare type StreamMeasure = {
6866
7732
  channelUrl?: string;
6867
7733
  deliveryTimer: TimeoutId | null;
@@ -7190,6 +8056,16 @@ declare class StreamStalledStatsTracker extends MultiStatsTrackerBase<StreamStal
7190
8056
 
7191
8057
  declare type StreamStallKind = 'delivery' | 'render';
7192
8058
 
8059
+ declare type StreamValidationMeasure = {
8060
+ channelUrl: string;
8061
+ startedAt: number;
8062
+ lastAt: number;
8063
+ lastText: string;
8064
+ chunkIntervalsMs: number[];
8065
+ regenerated: boolean;
8066
+ stopBackgroundWatch: (() => boolean) | null;
8067
+ };
8068
+
7193
8069
  /**
7194
8070
  * StringSet type for React components
7195
8071
  * Uses SNAKE_CASE keys for backward compatibility