@tagit/ai-client-core 1.2.0 → 1.3.0-beta.34.1
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/README.md +1 -1
- package/dist/client.d.ts +7 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +160 -92
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +3 -3
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/sse.d.ts +32 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +159 -0
- package/dist/sse.js.map +1 -0
- package/dist/stream-session.d.ts +48 -0
- package/dist/stream-session.d.ts.map +1 -0
- package/dist/stream-session.js +258 -0
- package/dist/stream-session.js.map +1 -0
- package/dist/streaming-fixtures.d.ts +29 -0
- package/dist/streaming-fixtures.d.ts.map +1 -0
- package/dist/streaming-fixtures.js +196 -0
- package/dist/streaming-fixtures.js.map +1 -0
- package/dist/testing.d.ts +3 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +2 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +37 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -285,7 +285,7 @@ Legacy collector identity fields are still accepted for migration compatibility,
|
|
|
285
285
|
|---|---|---:|---|---|
|
|
286
286
|
| `ui.theme` | `'light' \| 'dark'` | `undefined` | Forces the UI theme used by the React package. | `dark` |
|
|
287
287
|
| `ui.streaming` | `boolean` | `true` | Enables streaming response handling. | `true` |
|
|
288
|
-
| `ui.showToolCalls` | `boolean` | `
|
|
288
|
+
| `ui.showToolCalls` | `boolean` | `false` | Reserved for host-owned debug/tool-call UI. The built-in chat keeps tool execution out of the end-user message surface by default. | `false` |
|
|
289
289
|
| `ui.maxMessageCharacters.user` | positive integer | `2000` | Maximum user message length before truncation. | `2000` |
|
|
290
290
|
| `ui.maxMessageCharacters.assistant` | positive integer | `3000` | Maximum assistant message length before truncation. | `3000` |
|
|
291
291
|
| `ui.maxHistoryMessages` | positive integer | `50` | Maximum messages preserved before history trimming warnings. | `50` |
|
package/dist/client.d.ts
CHANGED
|
@@ -89,11 +89,16 @@ export declare class AIClient {
|
|
|
89
89
|
* Handle a single chunk from the orchestrator
|
|
90
90
|
*/
|
|
91
91
|
private handleChunk;
|
|
92
|
+
private handleSseMessage;
|
|
93
|
+
private syncStreamSessionState;
|
|
92
94
|
/**
|
|
93
|
-
* Drip-feed text word-by-word
|
|
94
|
-
*
|
|
95
|
+
* Drip-feed text word-by-word when a provider or proxy batches a large answer
|
|
96
|
+
* into one chunk. This preserves a readable progressive reveal without
|
|
97
|
+
* changing the durable final text.
|
|
95
98
|
*/
|
|
96
99
|
private simulateDeltaStream;
|
|
100
|
+
private shouldSmoothRevealDelta;
|
|
101
|
+
private appendAssistantText;
|
|
97
102
|
private shouldRetry;
|
|
98
103
|
private tryParseJson;
|
|
99
104
|
private isCanonicalChunk;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,qBAAqB,EACrB,WAAW,EAGX,YAAY,EAEZ,mBAAmB,EACnB,gCAAgC,EAChC,8BAA8B,EAC9B,6BAA6B,EAC7B,8BAA8B,EAC9B,iCAAiC,EACjC,4BAA4B,EAC5B,gCAAgC,EAChC,mBAAmB,EACnB,iBAAiB,EAMjB,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAIL,KAAK,qBAAqB,EAC3B,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { validateConfig } from './config.js';
|
|
2
2
|
import { FetchConversationHistoryTransport, FetchOrchestratorTransport, } from './transport.js';
|
|
3
|
+
import { SseDecoder } from './sse.js';
|
|
4
|
+
import { StreamSession } from './stream-session.js';
|
|
3
5
|
/**
|
|
4
6
|
* Generate a unique ID
|
|
5
7
|
*/
|
|
@@ -85,6 +87,7 @@ export class AIClient {
|
|
|
85
87
|
isLoading: false,
|
|
86
88
|
isStreaming: false,
|
|
87
89
|
useCaseId: initialUseCaseId,
|
|
90
|
+
streamStatus: 'idle',
|
|
88
91
|
};
|
|
89
92
|
this.notifyActiveAgentChange(this.getActiveAgent());
|
|
90
93
|
}
|
|
@@ -150,6 +153,7 @@ export class AIClient {
|
|
|
150
153
|
isLoading: false,
|
|
151
154
|
isStreaming: false,
|
|
152
155
|
useCaseId: agent.useCaseId,
|
|
156
|
+
streamStatus: 'idle',
|
|
153
157
|
};
|
|
154
158
|
this.emit({
|
|
155
159
|
type: 'active_agent_changed',
|
|
@@ -543,6 +547,15 @@ export class AIClient {
|
|
|
543
547
|
const normalized = this.normalizeError(error);
|
|
544
548
|
this.conversationState.error = normalized.safeMessage;
|
|
545
549
|
this.conversationState.lastError = normalized;
|
|
550
|
+
this.conversationState.streamStatus = normalized.code === 'CANCELED' ? 'canceled' : 'failed';
|
|
551
|
+
this.conversationState.streamDiagnostics = [
|
|
552
|
+
...(this.conversationState.streamDiagnostics ?? []),
|
|
553
|
+
{
|
|
554
|
+
code: normalized.code,
|
|
555
|
+
message: normalized.safeMessage,
|
|
556
|
+
retryable: normalized.retryable,
|
|
557
|
+
},
|
|
558
|
+
];
|
|
546
559
|
if (normalized.code === 'CANCELED') {
|
|
547
560
|
this.emit({ type: 'request_canceled' });
|
|
548
561
|
}
|
|
@@ -571,6 +584,15 @@ export class AIClient {
|
|
|
571
584
|
catch (error) {
|
|
572
585
|
const normalized = this.normalizeError(error);
|
|
573
586
|
if (this.shouldAcceptDegradedCompletion(normalized, attempt, maxAttempts)) {
|
|
587
|
+
this.conversationState.streamStatus = 'degraded';
|
|
588
|
+
this.conversationState.streamDiagnostics = [
|
|
589
|
+
...(this.conversationState.streamDiagnostics ?? []),
|
|
590
|
+
{
|
|
591
|
+
code: normalized.code,
|
|
592
|
+
message: normalized.safeMessage,
|
|
593
|
+
retryable: normalized.retryable,
|
|
594
|
+
},
|
|
595
|
+
];
|
|
574
596
|
this.emit({ type: 'streaming_completed' });
|
|
575
597
|
this.emit({
|
|
576
598
|
type: 'streaming_completed_degraded',
|
|
@@ -578,9 +600,13 @@ export class AIClient {
|
|
|
578
600
|
});
|
|
579
601
|
return;
|
|
580
602
|
}
|
|
581
|
-
// Remove incomplete assistant-side artifacts before next attempt.
|
|
582
|
-
this.conversationState.messages = this.conversationState.messages.slice(0, baselineLength);
|
|
583
603
|
if (this.shouldRetry(normalized, attempt, maxAttempts)) {
|
|
604
|
+
// Remove incomplete assistant-side artifacts before next attempt.
|
|
605
|
+
this.conversationState.messages = this.conversationState.messages.slice(0, baselineLength);
|
|
606
|
+
this.conversationState.streamStatus = 'idle';
|
|
607
|
+
this.conversationState.toolActivities = [];
|
|
608
|
+
this.conversationState.usageEvents = [];
|
|
609
|
+
this.conversationState.streamDiagnostics = [];
|
|
584
610
|
this.emit({
|
|
585
611
|
type: 'request_retried',
|
|
586
612
|
attempt: attempt + 1,
|
|
@@ -588,6 +614,9 @@ export class AIClient {
|
|
|
588
614
|
});
|
|
589
615
|
continue;
|
|
590
616
|
}
|
|
617
|
+
if (!normalized.details?.hasAssistantContent && !normalized.details?.hasToolActivity) {
|
|
618
|
+
this.conversationState.messages = this.conversationState.messages.slice(0, baselineLength);
|
|
619
|
+
}
|
|
591
620
|
throw normalized;
|
|
592
621
|
}
|
|
593
622
|
}
|
|
@@ -596,6 +625,8 @@ export class AIClient {
|
|
|
596
625
|
* Stream response from orchestrator
|
|
597
626
|
*/
|
|
598
627
|
async streamResponse(options) {
|
|
628
|
+
const streamSession = new StreamSession();
|
|
629
|
+
this.syncStreamSessionState(streamSession.snapshot(), false);
|
|
599
630
|
const requestMessages = this.buildRequestMessages(options?.useCaseMessage);
|
|
600
631
|
const endpointPath = this.getEndpointPath();
|
|
601
632
|
const request = {
|
|
@@ -680,8 +711,7 @@ export class AIClient {
|
|
|
680
711
|
retryable: true,
|
|
681
712
|
});
|
|
682
713
|
}
|
|
683
|
-
const
|
|
684
|
-
let buffer = '';
|
|
714
|
+
const sseDecoder = new SseDecoder();
|
|
685
715
|
let streamCompleted = false;
|
|
686
716
|
let emittedFirstToken = false;
|
|
687
717
|
try {
|
|
@@ -689,76 +719,32 @@ export class AIClient {
|
|
|
689
719
|
const { done, value } = await reader.read();
|
|
690
720
|
if (done)
|
|
691
721
|
break;
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
streamCompleted = true;
|
|
702
|
-
continue;
|
|
703
|
-
}
|
|
704
|
-
const parsed = this.tryParseJson(dataStr);
|
|
705
|
-
if (!parsed || !this.isCanonicalChunk(parsed)) {
|
|
706
|
-
throw this.createError({
|
|
707
|
-
code: 'STREAM_FAILED',
|
|
708
|
-
message: 'Received non-canonical stream chunk payload',
|
|
709
|
-
safeMessage: 'The orchestrator returned an unsupported stream chunk format.',
|
|
710
|
-
retryable: true,
|
|
711
|
-
details: { chunk: dataStr },
|
|
712
|
-
});
|
|
713
|
-
}
|
|
714
|
-
const result = await this.handleChunk(parsed);
|
|
715
|
-
if (result.type === 'delta' && result.text && !emittedFirstToken) {
|
|
716
|
-
emittedFirstToken = true;
|
|
717
|
-
this.emit({ type: 'first_token', messageId: result.messageId });
|
|
718
|
-
}
|
|
719
|
-
if (result.type === 'complete') {
|
|
720
|
-
streamCompleted = true;
|
|
721
|
-
}
|
|
722
|
+
const messages = sseDecoder.push(value);
|
|
723
|
+
for (const message of messages) {
|
|
724
|
+
const result = await this.handleSseMessage(message.data, streamSession);
|
|
725
|
+
if (result.type === 'delta' && result.text && !emittedFirstToken) {
|
|
726
|
+
emittedFirstToken = true;
|
|
727
|
+
this.emit({ type: 'first_token', messageId: result.messageId });
|
|
728
|
+
}
|
|
729
|
+
if (result.type === 'complete') {
|
|
730
|
+
streamCompleted = true;
|
|
722
731
|
}
|
|
723
732
|
}
|
|
724
733
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
const
|
|
728
|
-
if (
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
const parsed = this.tryParseJson(dataStr);
|
|
735
|
-
if (!parsed || !this.isCanonicalChunk(parsed)) {
|
|
736
|
-
throw this.createError({
|
|
737
|
-
code: 'STREAM_FAILED',
|
|
738
|
-
message: 'Received non-canonical final stream chunk payload',
|
|
739
|
-
safeMessage: 'The orchestrator returned an unsupported stream chunk format.',
|
|
740
|
-
retryable: true,
|
|
741
|
-
details: { chunk: dataStr },
|
|
742
|
-
});
|
|
743
|
-
}
|
|
744
|
-
const result = await this.handleChunk(parsed);
|
|
745
|
-
if (result.type === 'delta' && result.text && !emittedFirstToken) {
|
|
746
|
-
emittedFirstToken = true;
|
|
747
|
-
this.emit({ type: 'first_token', messageId: result.messageId });
|
|
748
|
-
}
|
|
749
|
-
if (result.type === 'complete') {
|
|
750
|
-
streamCompleted = true;
|
|
751
|
-
}
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
catch (error) {
|
|
755
|
-
console.warn('Failed to parse final chunk:', error);
|
|
756
|
-
throw error;
|
|
757
|
-
}
|
|
734
|
+
const finalMessages = sseDecoder.flush();
|
|
735
|
+
for (const message of finalMessages) {
|
|
736
|
+
const result = await this.handleSseMessage(message.data, streamSession, true);
|
|
737
|
+
if (result.type === 'delta' && result.text && !emittedFirstToken) {
|
|
738
|
+
emittedFirstToken = true;
|
|
739
|
+
this.emit({ type: 'first_token', messageId: result.messageId });
|
|
740
|
+
}
|
|
741
|
+
if (result.type === 'complete') {
|
|
742
|
+
streamCompleted = true;
|
|
758
743
|
}
|
|
759
744
|
}
|
|
760
745
|
if (!streamCompleted) {
|
|
761
|
-
const
|
|
746
|
+
const sessionSnapshot = streamSession.snapshot();
|
|
747
|
+
const hasAssistantContent = this.hasAssistantContent() || sessionSnapshot.hasVisibleOutput;
|
|
762
748
|
throw this.createError({
|
|
763
749
|
code: 'STREAM_INCOMPLETE',
|
|
764
750
|
message: 'Stream ended before receiving a complete event',
|
|
@@ -766,6 +752,7 @@ export class AIClient {
|
|
|
766
752
|
retryable: true,
|
|
767
753
|
details: {
|
|
768
754
|
hasAssistantContent,
|
|
755
|
+
hasToolActivity: sessionSnapshot.hasToolActivity,
|
|
769
756
|
},
|
|
770
757
|
});
|
|
771
758
|
}
|
|
@@ -786,7 +773,11 @@ export class AIClient {
|
|
|
786
773
|
/**
|
|
787
774
|
* Handle a single chunk from the orchestrator
|
|
788
775
|
*/
|
|
789
|
-
async handleChunk(chunk) {
|
|
776
|
+
async handleChunk(chunk, streamSession) {
|
|
777
|
+
const sessionResult = streamSession?.apply(chunk);
|
|
778
|
+
if (streamSession) {
|
|
779
|
+
this.syncStreamSessionState(streamSession.snapshot());
|
|
780
|
+
}
|
|
790
781
|
switch (chunk.type) {
|
|
791
782
|
case 'start':
|
|
792
783
|
{
|
|
@@ -805,14 +796,12 @@ export class AIClient {
|
|
|
805
796
|
const lastMessage = this.conversationState.messages[this.conversationState.messages.length - 1];
|
|
806
797
|
if (lastMessage && lastMessage.role === 'assistant') {
|
|
807
798
|
const text = String(chunk.data.text || '');
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
799
|
+
if (this.shouldSmoothRevealDelta(text)) {
|
|
800
|
+
await this.simulateDeltaStream(text, lastMessage.id);
|
|
801
|
+
}
|
|
802
|
+
else {
|
|
803
|
+
this.appendAssistantText(lastMessage, text, lastMessage.id);
|
|
813
804
|
}
|
|
814
|
-
this.emit({ type: 'message_updated', message: lastMessage });
|
|
815
|
-
this.emit({ type: 'stream_delta', messageId: lastMessage.id, text });
|
|
816
805
|
return { type: 'delta', text, messageId: lastMessage.id };
|
|
817
806
|
}
|
|
818
807
|
}
|
|
@@ -853,6 +842,10 @@ export class AIClient {
|
|
|
853
842
|
}
|
|
854
843
|
}
|
|
855
844
|
return { type: 'none' };
|
|
845
|
+
case 'tool_progress':
|
|
846
|
+
case 'provider_usage':
|
|
847
|
+
case 'stream_error':
|
|
848
|
+
return { type: 'none' };
|
|
856
849
|
case 'complete': {
|
|
857
850
|
const text = this.extractAssistantText(chunk.data) ?? '';
|
|
858
851
|
const lastMsg = this.conversationState.messages[this.conversationState.messages.length - 1];
|
|
@@ -869,6 +862,9 @@ export class AIClient {
|
|
|
869
862
|
}
|
|
870
863
|
case 'error':
|
|
871
864
|
{
|
|
865
|
+
if (streamSession) {
|
|
866
|
+
this.syncStreamSessionState(streamSession.snapshot());
|
|
867
|
+
}
|
|
872
868
|
const payloadMessage = typeof chunk.data.message === 'string'
|
|
873
869
|
? chunk.data.message
|
|
874
870
|
: typeof chunk.data.error === 'string'
|
|
@@ -883,39 +879,108 @@ export class AIClient {
|
|
|
883
879
|
});
|
|
884
880
|
}
|
|
885
881
|
default:
|
|
886
|
-
|
|
882
|
+
if (sessionResult?.type === 'error') {
|
|
883
|
+
throw this.createError({
|
|
884
|
+
code: 'STREAM_FAILED',
|
|
885
|
+
message: sessionResult.diagnostic.message,
|
|
886
|
+
safeMessage: 'Orchestrator reported an error while streaming.',
|
|
887
|
+
retryable: true,
|
|
888
|
+
details: { diagnostic: sessionResult.diagnostic },
|
|
889
|
+
});
|
|
890
|
+
}
|
|
887
891
|
return { type: 'none' };
|
|
888
892
|
}
|
|
889
893
|
}
|
|
894
|
+
async handleSseMessage(dataStr, streamSession, isFinalChunk = false) {
|
|
895
|
+
const data = dataStr.trim();
|
|
896
|
+
if (!data)
|
|
897
|
+
return { type: 'none' };
|
|
898
|
+
if (data === '[DONE]')
|
|
899
|
+
return { type: 'complete' };
|
|
900
|
+
const parsed = this.tryParseJson(data);
|
|
901
|
+
if (!parsed || !this.isCanonicalChunk(parsed)) {
|
|
902
|
+
throw this.createError({
|
|
903
|
+
code: 'STREAM_FAILED',
|
|
904
|
+
message: isFinalChunk
|
|
905
|
+
? 'Received non-canonical final stream chunk payload'
|
|
906
|
+
: 'Received non-canonical stream chunk payload',
|
|
907
|
+
safeMessage: 'The orchestrator returned an unsupported stream chunk format.',
|
|
908
|
+
retryable: true,
|
|
909
|
+
details: { chunk: data },
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
return this.handleChunk(parsed, streamSession);
|
|
913
|
+
}
|
|
914
|
+
syncStreamSessionState(snapshot, emitEvent = true) {
|
|
915
|
+
this.conversationState.streamStatus = snapshot.status;
|
|
916
|
+
this.conversationState.toolActivities = snapshot.toolActivities;
|
|
917
|
+
this.conversationState.usageEvents = snapshot.usageEvents;
|
|
918
|
+
this.conversationState.streamDiagnostics = snapshot.diagnostics;
|
|
919
|
+
if (snapshot.workflowId) {
|
|
920
|
+
this.conversationState.workflowId = snapshot.workflowId;
|
|
921
|
+
}
|
|
922
|
+
if (snapshot.workflowStatus) {
|
|
923
|
+
this.conversationState.workflowStatus = snapshot.workflowStatus;
|
|
924
|
+
}
|
|
925
|
+
if (snapshot.checkpointPrompt !== undefined) {
|
|
926
|
+
this.conversationState.checkpointPrompt = snapshot.checkpointPrompt;
|
|
927
|
+
}
|
|
928
|
+
if (emitEvent) {
|
|
929
|
+
this.emit({ type: 'stream_state_updated' });
|
|
930
|
+
}
|
|
931
|
+
}
|
|
890
932
|
/**
|
|
891
|
-
* Drip-feed text word-by-word
|
|
892
|
-
*
|
|
933
|
+
* Drip-feed text word-by-word when a provider or proxy batches a large answer
|
|
934
|
+
* into one chunk. This preserves a readable progressive reveal without
|
|
935
|
+
* changing the durable final text.
|
|
893
936
|
*/
|
|
894
937
|
async simulateDeltaStream(text, messageId) {
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
if (!tokens.length)
|
|
938
|
+
const lastMessage = this.conversationState.messages[this.conversationState.messages.length - 1];
|
|
939
|
+
if (!lastMessage || lastMessage.role !== 'assistant')
|
|
898
940
|
return;
|
|
941
|
+
// Split on word boundaries while preserving leading/trailing whitespace
|
|
942
|
+
// from transport chunks. Dropping leading spaces can glue words together
|
|
943
|
+
// across real streamed delta boundaries.
|
|
944
|
+
const tokens = text.match(/\s*\S+\s*/g) ?? [];
|
|
945
|
+
if (!tokens.length) {
|
|
946
|
+
this.appendAssistantText(lastMessage, text, messageId);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
899
949
|
const TARGET_MS = 1500;
|
|
900
950
|
const INTERVAL_MS = 10;
|
|
901
951
|
const maxChunks = TARGET_MS / INTERVAL_MS; // 150 ticks
|
|
902
952
|
const batchSize = Math.max(1, Math.ceil(tokens.length / maxChunks));
|
|
903
|
-
const lastMessage = this.conversationState.messages[this.conversationState.messages.length - 1];
|
|
904
|
-
if (!lastMessage || lastMessage.role !== 'assistant')
|
|
905
|
-
return;
|
|
906
953
|
let i = 0;
|
|
907
954
|
while (i < tokens.length) {
|
|
908
955
|
const chunk = tokens.slice(i, i + batchSize).join('');
|
|
909
956
|
i += batchSize;
|
|
910
|
-
lastMessage
|
|
911
|
-
this.emit({ type: 'message_updated', message: lastMessage });
|
|
912
|
-
this.emit({ type: 'stream_delta', messageId, text: chunk });
|
|
957
|
+
this.appendAssistantText(lastMessage, chunk, messageId);
|
|
913
958
|
if (i < tokens.length) {
|
|
914
959
|
await new Promise((resolve) => setTimeout(resolve, INTERVAL_MS));
|
|
915
960
|
}
|
|
916
961
|
}
|
|
917
962
|
}
|
|
963
|
+
shouldSmoothRevealDelta(text) {
|
|
964
|
+
if ((this.config.ui?.streaming ?? true) === false)
|
|
965
|
+
return false;
|
|
966
|
+
if (!text.trim())
|
|
967
|
+
return false;
|
|
968
|
+
return text.length >= 48 || (text.match(/\S+\s*/g)?.length ?? 0) >= 8;
|
|
969
|
+
}
|
|
970
|
+
appendAssistantText(message, text, messageId) {
|
|
971
|
+
message.content += text;
|
|
972
|
+
const limit = this.config.ui?.maxMessageCharacters?.assistant ?? 3000;
|
|
973
|
+
if (message.content.length > limit) {
|
|
974
|
+
message.content = this.truncateMessage('assistant', message.content);
|
|
975
|
+
}
|
|
976
|
+
this.emit({ type: 'message_updated', message });
|
|
977
|
+
this.emit({ type: 'stream_delta', messageId, text });
|
|
978
|
+
}
|
|
918
979
|
shouldRetry(error, attempt, maxAttempts) {
|
|
980
|
+
if (error.code === 'STREAM_INCOMPLETE' &&
|
|
981
|
+
(error.details?.hasAssistantContent || error.details?.hasToolActivity)) {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
919
984
|
return error.retryable && error.code !== 'CANCELED' && attempt < maxAttempts;
|
|
920
985
|
}
|
|
921
986
|
tryParseJson(value) {
|
|
@@ -1093,10 +1158,7 @@ export class AIClient {
|
|
|
1093
1158
|
if (error.code !== 'STREAM_INCOMPLETE') {
|
|
1094
1159
|
return false;
|
|
1095
1160
|
}
|
|
1096
|
-
|
|
1097
|
-
return false;
|
|
1098
|
-
}
|
|
1099
|
-
return Boolean(error.details?.hasAssistantContent);
|
|
1161
|
+
return Boolean(error.details?.hasAssistantContent) && !this.shouldRetry(error, attempt, maxAttempts);
|
|
1100
1162
|
}
|
|
1101
1163
|
hasAssistantContent() {
|
|
1102
1164
|
return this.conversationState.messages.some((message) => message.role === 'assistant' && message.content.trim().length > 0);
|
|
@@ -1279,6 +1341,10 @@ export class AIClient {
|
|
|
1279
1341
|
}
|
|
1280
1342
|
this.conversationState.error = undefined;
|
|
1281
1343
|
this.conversationState.lastError = undefined;
|
|
1344
|
+
this.conversationState.streamStatus = 'idle';
|
|
1345
|
+
this.conversationState.toolActivities = [];
|
|
1346
|
+
this.conversationState.usageEvents = [];
|
|
1347
|
+
this.conversationState.streamDiagnostics = [];
|
|
1282
1348
|
this.conversationState.isStreaming = false;
|
|
1283
1349
|
this.emit({ type: 'state_synced' });
|
|
1284
1350
|
}
|
|
@@ -1305,6 +1371,7 @@ export class AIClient {
|
|
|
1305
1371
|
isLoading: false,
|
|
1306
1372
|
isStreaming: false,
|
|
1307
1373
|
useCaseId: this.resolveHistoryUseCaseId(options),
|
|
1374
|
+
streamStatus: 'idle',
|
|
1308
1375
|
};
|
|
1309
1376
|
this.setActiveConversationMetadata(conversationId, options?.title, {
|
|
1310
1377
|
clearTitle: !options?.title?.trim(),
|
|
@@ -1357,6 +1424,7 @@ export class AIClient {
|
|
|
1357
1424
|
useCaseId: identity.useCaseId,
|
|
1358
1425
|
error: undefined,
|
|
1359
1426
|
lastError: undefined,
|
|
1427
|
+
streamStatus: 'idle',
|
|
1360
1428
|
};
|
|
1361
1429
|
this.setActiveConversationMetadata(conversation.conversationId, conversation.title);
|
|
1362
1430
|
this.createdConversationIds.add(conversation.conversationId);
|