@sendbird/ai-agent-messenger-react 1.40.1 → 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.
- package/dist/index.cjs +164 -164
- package/dist/index.d.ts +889 -26
- package/dist/index.js +5326 -4708
- package/package.json +12 -8
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(
|
|
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
|
-
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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?: (
|
|
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;
|
|
@@ -6875,6 +7523,211 @@ declare type StreamAnimation = {
|
|
|
6875
7523
|
holdStop: (id: string | number) => void;
|
|
6876
7524
|
};
|
|
6877
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
|
+
|
|
6878
7731
|
declare type StreamMeasure = {
|
|
6879
7732
|
channelUrl?: string;
|
|
6880
7733
|
deliveryTimer: TimeoutId | null;
|
|
@@ -7203,6 +8056,16 @@ declare class StreamStalledStatsTracker extends MultiStatsTrackerBase<StreamStal
|
|
|
7203
8056
|
|
|
7204
8057
|
declare type StreamStallKind = 'delivery' | 'render';
|
|
7205
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
|
+
|
|
7206
8069
|
/**
|
|
7207
8070
|
* StringSet type for React components
|
|
7208
8071
|
* Uses SNAKE_CASE keys for backward compatibility
|