@tagit/ai-client-core 1.2.0 → 1.3.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/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":"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;AASxB;;;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;IA0BH,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;IA8BrB,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;YA2DF,uBAAuB;IAyCrC;;OAEG;YACW,cAAc;IAiN5B;;OAEG;YACW,WAAW;IAqHzB;;;OAGG;YACW,mBAAmB;IA0BjC,OAAO,CAAC,WAAW;IAInB,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;IAgBtC,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;IAmBpB;;;;OAIG;IACG,oBAAoB,CAAC,OAAO,CAAC,EAAE,gCAAgC,GAAG,OAAO,CAAC,mBAAmB,GAAG,IAAI,CAAC;IA6BrG,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;IA2C/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"}
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 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)) {
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
- 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
- // Split on word boundaries, keeping trailing whitespace with each token.
896
- const tokens = text.match(/\S+\s*/g) ?? [];
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.content += chunk;
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
- if (attempt < maxAttempts) {
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);