@tagit/ai-client-core 1.2.0-beta.27.1 → 1.2.0-beta.29.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 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` | `true` | Shows tool call activity in the UI layer. | `true` |
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 to simulate streaming when the server batched
94
- * the entire response in one chunk. Targets ~1.5 s total regardless of length.
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;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
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;AAYxB;;;GAGG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,cAAc,CAAuC;IAC7D,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,WAAW,CAAmC;IACtD,OAAO,CAAC,SAAS,CAAwB;IACzC,OAAO,CAAC,4BAA4B,CAA6C;IACjF,OAAO,CAAC,sBAAsB,CAA0B;IACxD,OAAO,CAAC,qBAAqB,CAAK;gBAGhC,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE;QACR,SAAS,CAAC,EAAE,qBAAqB,CAAC;QAClC,4BAA4B,CAAC,EAAE,4BAA4B,CAAC;KAC7D;IA2BH,OAAO,CAAC,+BAA+B;IASvC,OAAO,CAAC,UAAU;IAIlB,SAAS,IAAI,WAAW,EAAE;IAU1B,mBAAmB,IAAI,OAAO;IAI9B,cAAc,IAAI,WAAW;IAU7B,OAAO,CAAC,qBAAqB;IAc7B,OAAO,CAAC,aAAa;IA+BrB,OAAO,CAAC,uBAAuB;IAQ/B,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW;IAY3C,YAAY,CACV,MAAM,EAAE,WAAW,EAAE,EACrB,OAAO,GAAE,sBAA2B,GACnC,qBAAqB;IAoDxB,OAAO,CAAC,wCAAwC;IAQhD;;OAEG;IACH,QAAQ,IAAI,iBAAiB;IAI7B;;OAEG;IACH,EAAE,CAAC,QAAQ,EAAE,mBAAmB,GAAG,MAAM,IAAI;IAO7C;;OAEG;IACH,OAAO,CAAC,IAAI;IAUZ;;OAEG;IACH,OAAO,CAAC,eAAe;IAUvB;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAa3B,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,oBAAoB;IAwB5B,aAAa,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,CAAC,GAAG,MAAM;IAwBnE,qBAAqB,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,GAAG,MAAM;IASnF,OAAO,CAAC,6BAA6B;IAoBrC,OAAO,CAAC,uBAAuB;IAI/B,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,sBAAsB;IAc9B,OAAO,CAAC,uBAAuB;IAI/B,OAAO,CAAC,qBAAqB;IAI7B,OAAO,CAAC,sBAAsB;YAahB,kBAAkB;YA2DlB,+BAA+B;IA6B7C,OAAO,CAAC,mCAAmC;IAoB3C,2BAA2B,IAAI,OAAO;IAItC,mBAAmB,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,kBAAkB,EAAE,QAAQ,GAAG,gBAAgB,CAAC,GAAG;QACpF,MAAM,EAAE,MAAM,CAAC;QACf,cAAc,EAAE,MAAM,CAAC;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,aAAa,EAAE,MAAM,CAAC;KACvB;IAYD;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAmB5B;;OAEG;IACG,WAAW,CACf,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,kBAAkB,GAC3B,OAAO,CAAC,IAAI,CAAC;YAoEF,uBAAuB;IAwDrC;;OAEG;YACW,cAAc;IAqK5B;;OAEG;YACW,WAAW;YAsIX,gBAAgB;IA6B9B,OAAO,CAAC,sBAAsB;IAmB9B;;;;OAIG;YACW,mBAAmB;IAwBjC,OAAO,CAAC,uBAAuB;IAO/B,OAAO,CAAC,mBAAmB;IAY3B,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,gBAAgB;IASxB,OAAO,CAAC,oBAAoB;IAwB5B,OAAO,CAAC,oBAAoB;YA0Bd,0BAA0B;IA2CxC,OAAO,CAAC,oBAAoB;IA+C5B,OAAO,CAAC,oBAAoB;IAkC5B,OAAO,CAAC,8BAA8B;IAYtC,OAAO,CAAC,mBAAmB;IAM3B,OAAO,CAAC,cAAc;IAgCtB,OAAO,CAAC,iBAAiB;IAWzB,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,0BAA0B;IAUlC,OAAO,CAAC,mBAAmB;IA+C3B;;OAEG;YACW,kBAAkB;IAkEhC;;OAEG;IACH,MAAM,IAAI,IAAI;IAUd;;;OAGG;IACH,YAAY,IAAI,IAAI;IAuBpB;;;;OAIG;IACG,oBAAoB,CAAC,OAAO,CAAC,EAAE,gCAAgC,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;IA8BrG,iBAAiB,CAAC,OAAO,CAAC,EAAE,8BAA8B,GAAG,OAAO,CAAC,6BAA6B,CAAC;IAYnG,gBAAgB,CACpB,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,8BAA8B,GACvC,OAAO,CAAC,iCAAiC,GAAG;QAAE,YAAY,EAAE,mBAAmB,CAAA;KAAE,CAAC;IA4C/E,kBAAkB,CACtB,cAAc,EAAE,MAAM,EACtB,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE,IAAI,CAAC,gCAAgC,EAAE,QAAQ,GAAG,WAAW,CAAC,GACvE,OAAO,CAAC,mBAAmB,CAAC;IAazB,mBAAmB,CACvB,cAAc,EAAE,MAAM,EACtB,OAAO,CAAC,EAAE,IAAI,CAAC,gCAAgC,EAAE,QAAQ,GAAG,WAAW,CAAC,GACvE,OAAO,CAAC,mBAAmB,CAAC;IAS/B;;;;;OAKG;IACH,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;CAWpD;AAED,eAAe,QAAQ,CAAC"}
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 decoder = new TextDecoder();
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
- buffer += decoder.decode(value, { stream: true });
693
- const lines = buffer.split('\n');
694
- buffer = lines.pop() || '';
695
- for (const line of lines) {
696
- if (line.startsWith('data: ')) {
697
- const dataStr = line.slice(6).trim();
698
- if (!dataStr)
699
- continue;
700
- if (dataStr === '[DONE]') {
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
- // Process any remaining buffer
726
- if (buffer && buffer.startsWith('data: ')) {
727
- const dataStr = buffer.slice(6).trim();
728
- if (dataStr) {
729
- try {
730
- if (dataStr === '[DONE]') {
731
- streamCompleted = true;
732
- }
733
- else {
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 hasAssistantContent = this.hasAssistantContent();
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
- lastMessage.content += text;
809
- // Truncate if exceeds limit
810
- const limit = this.config.ui?.maxMessageCharacters?.assistant ?? 3000;
811
- if (lastMessage.content.length > limit) {
812
- lastMessage.content = this.truncateMessage('assistant', lastMessage.content);
799
+ if (this.shouldSmoothRevealDelta(text, lastMessage.content)) {
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,13 +879,60 @@ export class AIClient {
883
879
  });
884
880
  }
885
881
  default:
886
- console.warn('Unknown chunk type:', chunk.type);
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 to simulate streaming when the server batched
892
- * the entire response in one chunk. Targets ~1.5 s total regardless of length.
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
938
  // Split on word boundaries, keeping trailing whitespace with each token.
@@ -907,15 +950,35 @@ export class AIClient {
907
950
  while (i < tokens.length) {
908
951
  const chunk = tokens.slice(i, i + batchSize).join('');
909
952
  i += batchSize;
910
- lastMessage.content += chunk;
911
- this.emit({ type: 'message_updated', message: lastMessage });
912
- this.emit({ type: 'stream_delta', messageId, text: chunk });
953
+ this.appendAssistantText(lastMessage, chunk, messageId);
913
954
  if (i < tokens.length) {
914
955
  await new Promise((resolve) => setTimeout(resolve, INTERVAL_MS));
915
956
  }
916
957
  }
917
958
  }
959
+ shouldSmoothRevealDelta(text, currentContent) {
960
+ if ((this.config.ui?.streaming ?? true) === false)
961
+ return false;
962
+ if (!text.trim())
963
+ return false;
964
+ if (currentContent.trim())
965
+ return false;
966
+ return text.length >= 180 || (text.match(/\S+\s*/g)?.length ?? 0) >= 28;
967
+ }
968
+ appendAssistantText(message, text, messageId) {
969
+ message.content += text;
970
+ const limit = this.config.ui?.maxMessageCharacters?.assistant ?? 3000;
971
+ if (message.content.length > limit) {
972
+ message.content = this.truncateMessage('assistant', message.content);
973
+ }
974
+ this.emit({ type: 'message_updated', message });
975
+ this.emit({ type: 'stream_delta', messageId, text });
976
+ }
918
977
  shouldRetry(error, attempt, maxAttempts) {
978
+ if (error.code === 'STREAM_INCOMPLETE' &&
979
+ (error.details?.hasAssistantContent || error.details?.hasToolActivity)) {
980
+ return false;
981
+ }
919
982
  return error.retryable && error.code !== 'CANCELED' && attempt < maxAttempts;
920
983
  }
921
984
  tryParseJson(value) {
@@ -1093,10 +1156,7 @@ export class AIClient {
1093
1156
  if (error.code !== 'STREAM_INCOMPLETE') {
1094
1157
  return false;
1095
1158
  }
1096
- if (attempt < maxAttempts) {
1097
- return false;
1098
- }
1099
- return Boolean(error.details?.hasAssistantContent);
1159
+ return Boolean(error.details?.hasAssistantContent) && !this.shouldRetry(error, attempt, maxAttempts);
1100
1160
  }
1101
1161
  hasAssistantContent() {
1102
1162
  return this.conversationState.messages.some((message) => message.role === 'assistant' && message.content.trim().length > 0);
@@ -1279,6 +1339,10 @@ export class AIClient {
1279
1339
  }
1280
1340
  this.conversationState.error = undefined;
1281
1341
  this.conversationState.lastError = undefined;
1342
+ this.conversationState.streamStatus = 'idle';
1343
+ this.conversationState.toolActivities = [];
1344
+ this.conversationState.usageEvents = [];
1345
+ this.conversationState.streamDiagnostics = [];
1282
1346
  this.conversationState.isStreaming = false;
1283
1347
  this.emit({ type: 'state_synced' });
1284
1348
  }
@@ -1305,6 +1369,7 @@ export class AIClient {
1305
1369
  isLoading: false,
1306
1370
  isStreaming: false,
1307
1371
  useCaseId: this.resolveHistoryUseCaseId(options),
1372
+ streamStatus: 'idle',
1308
1373
  };
1309
1374
  this.setActiveConversationMetadata(conversationId, options?.title, {
1310
1375
  clearTitle: !options?.title?.trim(),
@@ -1357,6 +1422,7 @@ export class AIClient {
1357
1422
  useCaseId: identity.useCaseId,
1358
1423
  error: undefined,
1359
1424
  lastError: undefined,
1425
+ streamStatus: 'idle',
1360
1426
  };
1361
1427
  this.setActiveConversationMetadata(conversation.conversationId, conversation.title);
1362
1428
  this.createdConversationIds.add(conversation.conversationId);