@illuma-ai/agents 1.0.98 → 1.1.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.
Files changed (66) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +6 -2
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/constants.cjs +53 -0
  4. package/dist/cjs/common/constants.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +195 -31
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/main.cjs +14 -0
  8. package/dist/cjs/main.cjs.map +1 -1
  9. package/dist/cjs/messages/dedup.cjs +95 -0
  10. package/dist/cjs/messages/dedup.cjs.map +1 -0
  11. package/dist/cjs/tools/CodeExecutor.cjs +22 -3
  12. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  13. package/dist/cjs/types/graph.cjs.map +1 -1
  14. package/dist/cjs/utils/pruneCalibration.cjs +78 -0
  15. package/dist/cjs/utils/pruneCalibration.cjs.map +1 -0
  16. package/dist/cjs/utils/run.cjs.map +1 -1
  17. package/dist/cjs/utils/tokens.cjs.map +1 -1
  18. package/dist/cjs/utils/toolDiscoveryCache.cjs +127 -0
  19. package/dist/cjs/utils/toolDiscoveryCache.cjs.map +1 -0
  20. package/dist/esm/agents/AgentContext.mjs +6 -2
  21. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  22. package/dist/esm/common/constants.mjs +48 -1
  23. package/dist/esm/common/constants.mjs.map +1 -1
  24. package/dist/esm/graphs/Graph.mjs +196 -32
  25. package/dist/esm/graphs/Graph.mjs.map +1 -1
  26. package/dist/esm/main.mjs +4 -1
  27. package/dist/esm/main.mjs.map +1 -1
  28. package/dist/esm/messages/dedup.mjs +93 -0
  29. package/dist/esm/messages/dedup.mjs.map +1 -0
  30. package/dist/esm/tools/CodeExecutor.mjs +22 -3
  31. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  32. package/dist/esm/types/graph.mjs.map +1 -1
  33. package/dist/esm/utils/pruneCalibration.mjs +74 -0
  34. package/dist/esm/utils/pruneCalibration.mjs.map +1 -0
  35. package/dist/esm/utils/run.mjs.map +1 -1
  36. package/dist/esm/utils/tokens.mjs.map +1 -1
  37. package/dist/esm/utils/toolDiscoveryCache.mjs +125 -0
  38. package/dist/esm/utils/toolDiscoveryCache.mjs.map +1 -0
  39. package/dist/types/agents/AgentContext.d.ts +4 -1
  40. package/dist/types/common/constants.d.ts +35 -0
  41. package/dist/types/graphs/Graph.d.ts +34 -0
  42. package/dist/types/messages/dedup.d.ts +25 -0
  43. package/dist/types/messages/index.d.ts +1 -0
  44. package/dist/types/types/graph.d.ts +63 -0
  45. package/dist/types/utils/index.d.ts +2 -0
  46. package/dist/types/utils/pruneCalibration.d.ts +43 -0
  47. package/dist/types/utils/toolDiscoveryCache.d.ts +77 -0
  48. package/package.json +1 -1
  49. package/src/agents/AgentContext.ts +7 -0
  50. package/src/common/constants.ts +56 -0
  51. package/src/graphs/Graph.ts +250 -50
  52. package/src/graphs/gapFeatures.test.ts +520 -0
  53. package/src/graphs/nonBlockingSummarization.test.ts +307 -0
  54. package/src/messages/__tests__/dedup.test.ts +166 -0
  55. package/src/messages/dedup.ts +104 -0
  56. package/src/messages/index.ts +1 -0
  57. package/src/tools/CodeExecutor.ts +22 -3
  58. package/src/types/graph.ts +73 -0
  59. package/src/utils/__tests__/pruneCalibration.test.ts +148 -0
  60. package/src/utils/__tests__/toolDiscoveryCache.test.ts +214 -0
  61. package/src/utils/contextPressure.test.ts +24 -9
  62. package/src/utils/index.ts +2 -0
  63. package/src/utils/pruneCalibration.ts +92 -0
  64. package/src/utils/run.ts +108 -108
  65. package/src/utils/tokens.ts +118 -118
  66. package/src/utils/toolDiscoveryCache.ts +150 -0
@@ -10,9 +10,9 @@ import { createPruneMessages } from '../messages/prune.mjs';
10
10
  import { ensureThinkingBlockInMessages } from '../messages/format.mjs';
11
11
  import { addCacheControl, addBedrockCacheControl } from '../messages/cache.mjs';
12
12
  import { formatContentStrings } from '../messages/content.mjs';
13
- import { extractToolDiscoveries } from '../messages/tools.mjs';
14
13
  import { GraphNodeKeys, Providers, ContentTypes, GraphEvents, MessageTypes, StepTypes, Constants } from '../common/enum.mjs';
15
- import { TOOL_TURN_THINKING_BUDGET } from '../common/constants.mjs';
14
+ import { TOOL_TURN_THINKING_BUDGET, SUMMARIZATION_CONTEXT_THRESHOLD } from '../common/constants.mjs';
15
+ import { deduplicateSystemMessages } from '../messages/dedup.mjs';
16
16
  import { resetIfNotEmpty, joinKeys } from '../utils/graph.mjs';
17
17
  import { isOpenAILike, isGoogleLike } from '../utils/llm.mjs';
18
18
  import { ChatModelStreamHandler } from '../stream.mjs';
@@ -23,6 +23,8 @@ import '../utils/toonFormat.mjs';
23
23
  import { buildContextAnalytics } from '../utils/contextAnalytics.mjs';
24
24
  import 'zod-to-json-schema';
25
25
  import { hasTaskTool, buildPostPruneNote, detectDocuments, shouldInjectMultiDocHint, buildMultiDocHintContent } from '../utils/contextPressure.mjs';
26
+ import { ToolDiscoveryCache } from '../utils/toolDiscoveryCache.mjs';
27
+ import { createPruneCalibration, applyCalibration, updatePruneCalibration } from '../utils/pruneCalibration.mjs';
26
28
  import { getChatModelClass, manualToolStreamProviders } from '../llm/providers.mjs';
27
29
  import { ToolNode, toolsCondition } from '../tools/ToolNode.mjs';
28
30
  import { ChatOpenAI, AzureChatOpenAI } from '../llm/openai/index.mjs';
@@ -91,6 +93,22 @@ class StandardGraph extends Graph {
91
93
  runId;
92
94
  startIndex = 0;
93
95
  signal;
96
+ /** Cached summary from the first prune in this run.
97
+ * Reused for subsequent prunes to avoid blocking LLM calls on every tool iteration. */
98
+ _cachedRunSummary;
99
+ /** EMA-based pruning calibration state — smooths token budget adjustments across iterations */
100
+ _pruneCalibration;
101
+ /** Run-scoped tool discovery cache — avoids re-parsing conversation history on every iteration */
102
+ _toolDiscoveryCache;
103
+ /**
104
+ * SCALE: Tracks whether a summary call is already in-flight for this Graph instance.
105
+ * Prevents multiple concurrent summary LLM calls when rapid tool iterations each
106
+ * trigger pruning. At 2000 users with 3+ tool calls per turn, this prevents
107
+ * 6000+ summary calls/turn from becoming 2000.
108
+ */
109
+ _summaryInFlight = false;
110
+ /** Messages accumulated across tool iterations while a summary call is in-flight */
111
+ _pendingMessagesToRefine = [];
94
112
  /** Map of agent contexts by agent ID */
95
113
  agentContexts = new Map();
96
114
  /** Default agent ID to use */
@@ -111,6 +129,19 @@ class StandardGraph extends Graph {
111
129
  this.agentContexts.set(agentConfig.agentId, agentContext);
112
130
  }
113
131
  this.defaultAgentId = agents[0].agentId;
132
+ // Seed cached summary from persisted storage so the first prune in a
133
+ // resumed conversation can also skip the synchronous LLM summarization call
134
+ const primaryContext = this.agentContexts.get(this.defaultAgentId);
135
+ if (primaryContext?.persistedSummary) {
136
+ this._cachedRunSummary = primaryContext.persistedSummary;
137
+ }
138
+ // Initialize EMA pruning calibration
139
+ this._pruneCalibration = createPruneCalibration();
140
+ // Initialize tool discovery cache, seeded with any pre-existing discoveries
141
+ this._toolDiscoveryCache = new ToolDiscoveryCache();
142
+ if (primaryContext?.discoveredToolNames.size) {
143
+ this._toolDiscoveryCache.seed([...primaryContext.discoveredToolNames]);
144
+ }
114
145
  }
115
146
  /* Init */
116
147
  resetValues(keepContent) {
@@ -133,6 +164,11 @@ class StandardGraph extends Graph {
133
164
  this.messageStepHasToolCalls = resetIfNotEmpty(this.messageStepHasToolCalls, new Map());
134
165
  this.prelimMessageIdsByStepKey = resetIfNotEmpty(this.prelimMessageIdsByStepKey, new Map());
135
166
  this.invokedToolIds = resetIfNotEmpty(this.invokedToolIds, undefined);
167
+ // Reset EMA calibration, tool discovery cache, and summary debounce for fresh run
168
+ this._pruneCalibration = createPruneCalibration();
169
+ this._toolDiscoveryCache.reset();
170
+ this._summaryInFlight = false;
171
+ this._pendingMessagesToRefine = [];
136
172
  for (const context of this.agentContexts.values()) {
137
173
  context.reset();
138
174
  }
@@ -221,6 +257,62 @@ class StandardGraph extends Graph {
221
257
  }
222
258
  return clientOptions;
223
259
  }
260
+ /**
261
+ * Determines whether summarization should trigger based on SummarizationConfig.
262
+ *
263
+ * Supports three trigger strategies:
264
+ * - contextPercentage (default): Trigger when context utilization >= threshold%
265
+ * - messageCount: Trigger when pruned message count >= threshold
266
+ * - tokenThreshold: Trigger when total estimated tokens >= threshold
267
+ *
268
+ * When no config is provided, always triggers (preserves backward compatibility).
269
+ *
270
+ * @param prunedMessageCount - Number of messages that were pruned
271
+ * @param maxContextTokens - Maximum context token budget
272
+ * @param indexTokenCountMap - Token count map by message index
273
+ * @param instructionTokens - Token count for instructions/system message
274
+ * @param config - Optional SummarizationConfig
275
+ * @returns Whether summarization should be triggered
276
+ */
277
+ shouldTriggerSummarization(prunedMessageCount, maxContextTokens, indexTokenCountMap, instructionTokens, config) {
278
+ // No pruned messages means nothing to summarize
279
+ if (prunedMessageCount === 0) {
280
+ return false;
281
+ }
282
+ // No config = backward compatible (always summarize when messages are pruned)
283
+ if (!config || !config.triggerType) {
284
+ return true;
285
+ }
286
+ const threshold = config.triggerThreshold;
287
+ switch (config.triggerType) {
288
+ case 'contextPercentage': {
289
+ if (maxContextTokens <= 0)
290
+ return true;
291
+ const effectiveThreshold = threshold ?? SUMMARIZATION_CONTEXT_THRESHOLD;
292
+ let totalTokens = instructionTokens;
293
+ for (const key in indexTokenCountMap) {
294
+ totalTokens += indexTokenCountMap[key] ?? 0;
295
+ }
296
+ const utilization = (totalTokens / maxContextTokens) * 100;
297
+ return utilization >= effectiveThreshold;
298
+ }
299
+ case 'messageCount': {
300
+ const effectiveThreshold = threshold ?? 5;
301
+ return prunedMessageCount >= effectiveThreshold;
302
+ }
303
+ case 'tokenThreshold': {
304
+ if (threshold == null)
305
+ return true;
306
+ let totalTokens = instructionTokens;
307
+ for (const key in indexTokenCountMap) {
308
+ totalTokens += indexTokenCountMap[key] ?? 0;
309
+ }
310
+ return totalTokens >= threshold;
311
+ }
312
+ default:
313
+ return true;
314
+ }
315
+ }
224
316
  /**
225
317
  * Returns the normalized finish/stop reason from the last LLM invocation.
226
318
  * Used by callers to detect when the response was truncated due to max_tokens.
@@ -359,7 +451,6 @@ class StandardGraph extends Graph {
359
451
  /* Misc.*/
360
452
  getRunMessages() {
361
453
  const result = this.messages.slice(this.startIndex);
362
- console.debug(`[Graph] getRunMessages() | totalMessages=${this.messages.length} | startIndex=${this.startIndex} | runMessages=${result.length}`);
363
454
  return result;
364
455
  }
365
456
  getContentParts() {
@@ -915,10 +1006,12 @@ class StandardGraph extends Graph {
915
1006
  });
916
1007
  messages = [dynamicContextMessage, ackMessage, ...messages];
917
1008
  }
918
- // Extract tool discoveries from current turn only (similar to formatArtifactPayload pattern)
919
- const discoveredNames = extractToolDiscoveries(messages);
920
- if (discoveredNames.length > 0) {
921
- agentContext.markToolsAsDiscovered(discoveredNames);
1009
+ // Tool discovery caching: only scan new messages since last iteration
1010
+ // instead of re-parsing the full history via extractToolDiscoveries()
1011
+ const cachedDiscoveries = this._toolDiscoveryCache.getNewDiscoveries(messages);
1012
+ if (cachedDiscoveries.length > 0) {
1013
+ agentContext.markToolsAsDiscovered(cachedDiscoveries);
1014
+ console.debug(`[Graph:ToolDiscovery] Cached ${cachedDiscoveries.length} new tools (total: ${this._toolDiscoveryCache.size})`);
922
1015
  }
923
1016
  const toolsForBinding = agentContext.getToolsForBinding();
924
1017
  // PERF: Detect subsequent ReAct iterations (tool results present in messages)
@@ -968,56 +1061,136 @@ class StandardGraph extends Graph {
968
1061
  (agentContext.provider === Providers.OPENAI &&
969
1062
  agentContext.clientOptions.modelKwargs
970
1063
  ?.thinking?.type === 'enabled');
1064
+ // Apply EMA calibration to max token budget — smooths pruning across iterations
1065
+ const calibratedMaxTokens = applyCalibration(agentContext.maxContextTokens, this._pruneCalibration);
971
1066
  agentContext.pruneMessages = createPruneMessages({
972
1067
  startIndex: this.startIndex,
973
1068
  provider: agentContext.provider,
974
1069
  tokenCounter: agentContext.tokenCounter,
975
- maxTokens: agentContext.maxContextTokens,
1070
+ maxTokens: calibratedMaxTokens,
976
1071
  thinkingEnabled: isAnthropicWithThinking,
977
1072
  indexTokenCountMap: agentContext.indexTokenCountMap,
978
1073
  });
979
1074
  }
1075
+ // Update EMA calibration with actual token usage from API response
1076
+ if (agentContext.currentUsage?.input_tokens &&
1077
+ agentContext.maxContextTokens) {
1078
+ const estimatedTokens = Object.values(agentContext.indexTokenCountMap).reduce((sum, v) => (sum ?? 0) + (v ?? 0), 0);
1079
+ if (estimatedTokens > 0) {
1080
+ this._pruneCalibration = updatePruneCalibration(this._pruneCalibration, agentContext.currentUsage.input_tokens, estimatedTokens);
1081
+ }
1082
+ }
980
1083
  if (agentContext.pruneMessages) {
981
- console.debug(`[Graph:ContextMgmt] Pruning messages | inputCount=${messages.length} | maxTokens=${agentContext.maxContextTokens}`);
982
1084
  const { context, indexTokenCountMap, messagesToRefine } = agentContext.pruneMessages({
983
1085
  messages,
984
1086
  usageMetadata: agentContext.currentUsage,
985
- // startOnMessageType: 'human',
986
1087
  });
987
1088
  agentContext.indexTokenCountMap = indexTokenCountMap;
988
1089
  messagesToUse = context;
989
- console.debug(`[Graph:ContextMgmt] Pruned | kept=${context.length} | discarded=${messagesToRefine.length} | originalCount=${messages.length}`);
990
- // Summarize discarded messages if callback provided
1090
+ // ── Non-blocking summarization ──────────────────────────────────
1091
+ // NEVER block the LLM call waiting for summarization. Instead:
1092
+ // 1. If _cachedRunSummary exists → use it, fire async update
1093
+ // 2. If persistedSummary exists → use it as fallback, fire async update
1094
+ // 3. If NOTHING exists (first-ever prune) → skip summary, fire async generation
1095
+ // The summary catches up asynchronously and is available for subsequent
1096
+ // iterations (tool calls) and the next conversation turn.
1097
+ //
1098
+ // SummarizationConfig integration:
1099
+ // - triggerType/triggerThreshold control WHEN summarization fires
1100
+ // - reserveRatio is enforced via calibrated maxTokens (above)
1101
+ // - initialSummary provides cross-run seeding as fallback before persistedSummary
991
1102
  let hasSummary = false;
992
- if (messagesToRefine.length > 0 && agentContext.summarizeCallback) {
993
- console.debug(`[Graph:ContextMgmt] Summarizing ${messagesToRefine.length} discarded messages`);
1103
+ const sumConfig = agentContext.summarizationConfig;
1104
+ const shouldSummarize = this.shouldTriggerSummarization(messagesToRefine.length, agentContext.maxContextTokens ?? 0, agentContext.indexTokenCountMap, agentContext.instructionTokens, sumConfig);
1105
+ if (messagesToRefine.length > 0 &&
1106
+ agentContext.summarizeCallback &&
1107
+ shouldSummarize) {
994
1108
  try {
995
- const summary = await agentContext.summarizeCallback(messagesToRefine);
996
- console.debug(`[Graph:ContextMgmt] Summary received | len=${summary?.length ?? 0} | hasContent=${summary != null && summary !== ''}`);
1109
+ let summary;
1110
+ let summarySource;
1111
+ if (this._cachedRunSummary != null) {
1112
+ summary = this._cachedRunSummary;
1113
+ summarySource = 'cached';
1114
+ }
1115
+ else if (agentContext.persistedSummary != null &&
1116
+ agentContext.persistedSummary !== '') {
1117
+ summary = agentContext.persistedSummary;
1118
+ this._cachedRunSummary = summary;
1119
+ summarySource = 'persisted';
1120
+ }
1121
+ else if (sumConfig?.initialSummary != null &&
1122
+ sumConfig.initialSummary !== '') {
1123
+ // Cross-run seed: use initialSummary when no persisted summary exists
1124
+ summary = sumConfig.initialSummary;
1125
+ this._cachedRunSummary = summary;
1126
+ summarySource = 'initial-seed';
1127
+ }
1128
+ else {
1129
+ summarySource = 'none';
1130
+ }
1131
+ // Single consolidated log for the entire prune+summarize decision
1132
+ console.debug(`[Graph:ContextMgmt] Pruned ${messages.length}→${context.length} msgs (${messagesToRefine.length} discarded) | summary=${summarySource}${summary ? ` (len=${summary.length})` : ''} | calibration=${this._pruneCalibration.ratio.toFixed(3)}(${this._pruneCalibration.iterations})`);
1133
+ // SCALE: Debounce background summarization — if a summary call is already
1134
+ // in-flight (from a prior tool iteration), accumulate messages instead of
1135
+ // firing another concurrent LLM call. At 2000 users with 3+ tool calls
1136
+ // per turn, this prevents 3x summary call volume.
1137
+ if (this._summaryInFlight) {
1138
+ this._pendingMessagesToRefine.push(...messagesToRefine);
1139
+ console.debug(`[Graph:ContextMgmt] Summary in-flight, queued ${messagesToRefine.length} msgs (pending=${this._pendingMessagesToRefine.length})`);
1140
+ }
1141
+ else {
1142
+ this._summaryInFlight = true;
1143
+ const allMessages = this._pendingMessagesToRefine.length > 0
1144
+ ? [...this._pendingMessagesToRefine, ...messagesToRefine]
1145
+ : messagesToRefine;
1146
+ this._pendingMessagesToRefine = [];
1147
+ agentContext
1148
+ .summarizeCallback(allMessages)
1149
+ .then((updated) => {
1150
+ if (updated != null && updated !== '') {
1151
+ this._cachedRunSummary = updated;
1152
+ }
1153
+ })
1154
+ .catch((err) => {
1155
+ console.error('[Graph] Background summary failed (non-fatal):', err);
1156
+ })
1157
+ .finally(() => {
1158
+ this._summaryInFlight = false;
1159
+ });
1160
+ }
997
1161
  if (summary != null && summary !== '') {
998
1162
  hasSummary = true;
999
1163
  const summaryMsg = new SystemMessage(`[Conversation Summary]\n${summary}`);
1000
- // Insert after system message (if present), before conversation messages
1001
1164
  const systemIdx = messagesToUse[0]?.getType() === 'system' ? 1 : 0;
1002
1165
  messagesToUse = [
1003
1166
  ...messagesToUse.slice(0, systemIdx),
1004
1167
  summaryMsg,
1005
1168
  ...messagesToUse.slice(systemIdx),
1006
1169
  ];
1007
- console.debug(`[Graph:ContextMgmt] Summary injected at index ${systemIdx} | finalMsgCount=${messagesToUse.length}`);
1008
1170
  }
1009
1171
  }
1010
1172
  catch (err) {
1011
- console.error('[Graph] Summarization callback failed:', err);
1173
+ console.error('[Graph] Summarization failed:', err);
1012
1174
  }
1013
1175
  }
1014
- // Post-prune context note: inform the LLM that context was compressed
1015
- // without exposing token numbers (prevents voluntary bail-out)
1176
+ else if (messagesToRefine.length > 0) {
1177
+ // Log pruning even when no summarize callback (discard mode)
1178
+ console.debug(`[Graph:ContextMgmt] Pruned ${messages.length}→${context.length} msgs (${messagesToRefine.length} discarded, no summary callback) | calibration=${this._pruneCalibration.ratio.toFixed(3)}`);
1179
+ }
1180
+ // Deduplicate system messages that accumulate from repeated tool iterations
1181
+ const { messages: dedupedMessages, removedCount } = deduplicateSystemMessages(messagesToUse);
1182
+ if (removedCount > 0) {
1183
+ messagesToUse = dedupedMessages;
1184
+ console.debug(`[Graph:Dedup] Removed ${removedCount} duplicate system message(s)`);
1185
+ }
1186
+ // Post-prune context note for task-tool-enabled agents
1016
1187
  if (messagesToRefine.length > 0 && hasTaskTool(agentContext.tools)) {
1017
1188
  const postPruneNote = buildPostPruneNote(messagesToRefine.length, hasSummary);
1018
1189
  if (postPruneNote) {
1019
- messagesToUse = [...messagesToUse, new SystemMessage(postPruneNote)];
1020
- console.debug(`[Graph:ContextMgmt] Post-prune note injected | hasSummary=${hasSummary} | discarded=${messagesToRefine.length}`);
1190
+ messagesToUse = [
1191
+ ...messagesToUse,
1192
+ new SystemMessage(postPruneNote),
1193
+ ];
1021
1194
  }
1022
1195
  }
1023
1196
  }
@@ -1141,11 +1314,6 @@ class StandardGraph extends Graph {
1141
1314
  // ====================================================================
1142
1315
  if (hasTaskTool(agentContext.tools)) {
1143
1316
  const { count: documentCount, names: documentNames } = detectDocuments(finalMessages);
1144
- // Observability log (no token numbers exposed to LLM)
1145
- if (contextAnalytics.utilizationPercent != null) {
1146
- console.debug(`[Graph] Context utilization: ${contextAnalytics.utilizationPercent.toFixed(1)}% | ` +
1147
- `messages: ${finalMessages.length} | docs: ${documentCount}`);
1148
- }
1149
1317
  // Multi-document delegation: first iteration only (before AI has responded)
1150
1318
  const hasAiResponse = finalMessages.some((m) => m._getType() === 'ai' || m._getType() === 'tool');
1151
1319
  if (shouldInjectMultiDocHint(documentCount, hasAiResponse)) {
@@ -1549,10 +1717,6 @@ If I seem to be missing something we discussed earlier, just give me a quick rem
1549
1717
  reducer: (a, b) => {
1550
1718
  if (!a.length) {
1551
1719
  this.startIndex = a.length + b.length;
1552
- console.debug(`[Graph:Reducer] Initial messages | startIndex=${this.startIndex} | inputMsgCount=${b.length}`);
1553
- }
1554
- else {
1555
- console.debug(`[Graph:Reducer] Appending messages | existing=${a.length} | new=${b.length} | startIndex=${this.startIndex}`);
1556
1720
  }
1557
1721
  const result = messagesStateReducer(a, b);
1558
1722
  this.messages = result;