@librechat/agents 3.1.57 → 3.1.61

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 (214) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +326 -62
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/enum.cjs +13 -0
  4. package/dist/cjs/common/enum.cjs.map +1 -1
  5. package/dist/cjs/events.cjs +7 -27
  6. package/dist/cjs/events.cjs.map +1 -1
  7. package/dist/cjs/graphs/Graph.cjs +303 -222
  8. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +4 -4
  10. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +6 -2
  12. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/llm/init.cjs +60 -0
  14. package/dist/cjs/llm/init.cjs.map +1 -0
  15. package/dist/cjs/llm/invoke.cjs +90 -0
  16. package/dist/cjs/llm/invoke.cjs.map +1 -0
  17. package/dist/cjs/llm/openai/index.cjs +2 -0
  18. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  19. package/dist/cjs/llm/request.cjs +41 -0
  20. package/dist/cjs/llm/request.cjs.map +1 -0
  21. package/dist/cjs/main.cjs +40 -0
  22. package/dist/cjs/main.cjs.map +1 -1
  23. package/dist/cjs/messages/cache.cjs +76 -89
  24. package/dist/cjs/messages/cache.cjs.map +1 -1
  25. package/dist/cjs/messages/contextPruning.cjs +156 -0
  26. package/dist/cjs/messages/contextPruning.cjs.map +1 -0
  27. package/dist/cjs/messages/contextPruningSettings.cjs +53 -0
  28. package/dist/cjs/messages/contextPruningSettings.cjs.map +1 -0
  29. package/dist/cjs/messages/core.cjs +23 -37
  30. package/dist/cjs/messages/core.cjs.map +1 -1
  31. package/dist/cjs/messages/format.cjs +156 -11
  32. package/dist/cjs/messages/format.cjs.map +1 -1
  33. package/dist/cjs/messages/prune.cjs +1161 -49
  34. package/dist/cjs/messages/prune.cjs.map +1 -1
  35. package/dist/cjs/messages/reducer.cjs +87 -0
  36. package/dist/cjs/messages/reducer.cjs.map +1 -0
  37. package/dist/cjs/run.cjs +81 -42
  38. package/dist/cjs/run.cjs.map +1 -1
  39. package/dist/cjs/stream.cjs +54 -7
  40. package/dist/cjs/stream.cjs.map +1 -1
  41. package/dist/cjs/summarization/index.cjs +75 -0
  42. package/dist/cjs/summarization/index.cjs.map +1 -0
  43. package/dist/cjs/summarization/node.cjs +663 -0
  44. package/dist/cjs/summarization/node.cjs.map +1 -0
  45. package/dist/cjs/tools/ToolNode.cjs +16 -8
  46. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  47. package/dist/cjs/tools/handlers.cjs +2 -0
  48. package/dist/cjs/tools/handlers.cjs.map +1 -1
  49. package/dist/cjs/utils/errors.cjs +115 -0
  50. package/dist/cjs/utils/errors.cjs.map +1 -0
  51. package/dist/cjs/utils/events.cjs +17 -0
  52. package/dist/cjs/utils/events.cjs.map +1 -1
  53. package/dist/cjs/utils/handlers.cjs +16 -0
  54. package/dist/cjs/utils/handlers.cjs.map +1 -1
  55. package/dist/cjs/utils/llm.cjs +10 -0
  56. package/dist/cjs/utils/llm.cjs.map +1 -1
  57. package/dist/cjs/utils/tokens.cjs +247 -14
  58. package/dist/cjs/utils/tokens.cjs.map +1 -1
  59. package/dist/cjs/utils/truncation.cjs +107 -0
  60. package/dist/cjs/utils/truncation.cjs.map +1 -0
  61. package/dist/esm/agents/AgentContext.mjs +325 -61
  62. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  63. package/dist/esm/common/enum.mjs +13 -0
  64. package/dist/esm/common/enum.mjs.map +1 -1
  65. package/dist/esm/events.mjs +8 -28
  66. package/dist/esm/events.mjs.map +1 -1
  67. package/dist/esm/graphs/Graph.mjs +307 -226
  68. package/dist/esm/graphs/Graph.mjs.map +1 -1
  69. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +4 -4
  70. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  71. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +6 -2
  72. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  73. package/dist/esm/llm/init.mjs +58 -0
  74. package/dist/esm/llm/init.mjs.map +1 -0
  75. package/dist/esm/llm/invoke.mjs +87 -0
  76. package/dist/esm/llm/invoke.mjs.map +1 -0
  77. package/dist/esm/llm/openai/index.mjs +2 -0
  78. package/dist/esm/llm/openai/index.mjs.map +1 -1
  79. package/dist/esm/llm/request.mjs +38 -0
  80. package/dist/esm/llm/request.mjs.map +1 -0
  81. package/dist/esm/main.mjs +13 -3
  82. package/dist/esm/main.mjs.map +1 -1
  83. package/dist/esm/messages/cache.mjs +76 -89
  84. package/dist/esm/messages/cache.mjs.map +1 -1
  85. package/dist/esm/messages/contextPruning.mjs +154 -0
  86. package/dist/esm/messages/contextPruning.mjs.map +1 -0
  87. package/dist/esm/messages/contextPruningSettings.mjs +50 -0
  88. package/dist/esm/messages/contextPruningSettings.mjs.map +1 -0
  89. package/dist/esm/messages/core.mjs +23 -37
  90. package/dist/esm/messages/core.mjs.map +1 -1
  91. package/dist/esm/messages/format.mjs +156 -11
  92. package/dist/esm/messages/format.mjs.map +1 -1
  93. package/dist/esm/messages/prune.mjs +1158 -52
  94. package/dist/esm/messages/prune.mjs.map +1 -1
  95. package/dist/esm/messages/reducer.mjs +83 -0
  96. package/dist/esm/messages/reducer.mjs.map +1 -0
  97. package/dist/esm/run.mjs +82 -43
  98. package/dist/esm/run.mjs.map +1 -1
  99. package/dist/esm/stream.mjs +54 -7
  100. package/dist/esm/stream.mjs.map +1 -1
  101. package/dist/esm/summarization/index.mjs +73 -0
  102. package/dist/esm/summarization/index.mjs.map +1 -0
  103. package/dist/esm/summarization/node.mjs +659 -0
  104. package/dist/esm/summarization/node.mjs.map +1 -0
  105. package/dist/esm/tools/ToolNode.mjs +16 -8
  106. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  107. package/dist/esm/tools/handlers.mjs +2 -0
  108. package/dist/esm/tools/handlers.mjs.map +1 -1
  109. package/dist/esm/utils/errors.mjs +111 -0
  110. package/dist/esm/utils/errors.mjs.map +1 -0
  111. package/dist/esm/utils/events.mjs +17 -1
  112. package/dist/esm/utils/events.mjs.map +1 -1
  113. package/dist/esm/utils/handlers.mjs +16 -0
  114. package/dist/esm/utils/handlers.mjs.map +1 -1
  115. package/dist/esm/utils/llm.mjs +10 -1
  116. package/dist/esm/utils/llm.mjs.map +1 -1
  117. package/dist/esm/utils/tokens.mjs +245 -15
  118. package/dist/esm/utils/tokens.mjs.map +1 -1
  119. package/dist/esm/utils/truncation.mjs +102 -0
  120. package/dist/esm/utils/truncation.mjs.map +1 -0
  121. package/dist/types/agents/AgentContext.d.ts +124 -6
  122. package/dist/types/common/enum.d.ts +14 -1
  123. package/dist/types/graphs/Graph.d.ts +22 -27
  124. package/dist/types/index.d.ts +5 -0
  125. package/dist/types/llm/init.d.ts +18 -0
  126. package/dist/types/llm/invoke.d.ts +48 -0
  127. package/dist/types/llm/request.d.ts +14 -0
  128. package/dist/types/messages/contextPruning.d.ts +42 -0
  129. package/dist/types/messages/contextPruningSettings.d.ts +44 -0
  130. package/dist/types/messages/core.d.ts +1 -1
  131. package/dist/types/messages/format.d.ts +17 -1
  132. package/dist/types/messages/index.d.ts +3 -0
  133. package/dist/types/messages/prune.d.ts +162 -1
  134. package/dist/types/messages/reducer.d.ts +18 -0
  135. package/dist/types/run.d.ts +12 -1
  136. package/dist/types/summarization/index.d.ts +20 -0
  137. package/dist/types/summarization/node.d.ts +29 -0
  138. package/dist/types/tools/ToolNode.d.ts +3 -1
  139. package/dist/types/types/graph.d.ts +44 -6
  140. package/dist/types/types/index.d.ts +1 -0
  141. package/dist/types/types/run.d.ts +30 -0
  142. package/dist/types/types/stream.d.ts +31 -4
  143. package/dist/types/types/summarize.d.ts +47 -0
  144. package/dist/types/types/tools.d.ts +7 -0
  145. package/dist/types/utils/errors.d.ts +28 -0
  146. package/dist/types/utils/events.d.ts +13 -0
  147. package/dist/types/utils/index.d.ts +2 -0
  148. package/dist/types/utils/llm.d.ts +4 -0
  149. package/dist/types/utils/tokens.d.ts +14 -1
  150. package/dist/types/utils/truncation.d.ts +49 -0
  151. package/package.json +3 -3
  152. package/src/agents/AgentContext.ts +388 -58
  153. package/src/agents/__tests__/AgentContext.test.ts +265 -5
  154. package/src/common/enum.ts +13 -0
  155. package/src/events.ts +9 -39
  156. package/src/graphs/Graph.ts +468 -331
  157. package/src/index.ts +7 -0
  158. package/src/llm/anthropic/llm.spec.ts +3 -3
  159. package/src/llm/anthropic/utils/message_inputs.ts +6 -4
  160. package/src/llm/bedrock/llm.spec.ts +1 -1
  161. package/src/llm/bedrock/utils/message_inputs.ts +6 -2
  162. package/src/llm/init.ts +63 -0
  163. package/src/llm/invoke.ts +144 -0
  164. package/src/llm/request.ts +55 -0
  165. package/src/messages/__tests__/observationMasking.test.ts +221 -0
  166. package/src/messages/cache.ts +77 -102
  167. package/src/messages/contextPruning.ts +191 -0
  168. package/src/messages/contextPruningSettings.ts +90 -0
  169. package/src/messages/core.ts +32 -53
  170. package/src/messages/ensureThinkingBlock.test.ts +39 -39
  171. package/src/messages/format.ts +227 -15
  172. package/src/messages/formatAgentMessages.test.ts +511 -1
  173. package/src/messages/index.ts +3 -0
  174. package/src/messages/prune.ts +1548 -62
  175. package/src/messages/reducer.ts +22 -0
  176. package/src/run.ts +104 -51
  177. package/src/scripts/bedrock-merge-test.ts +1 -1
  178. package/src/scripts/test-thinking-handoff-bedrock.ts +1 -1
  179. package/src/scripts/test-thinking-handoff.ts +1 -1
  180. package/src/scripts/thinking-bedrock.ts +1 -1
  181. package/src/scripts/thinking.ts +1 -1
  182. package/src/specs/anthropic.simple.test.ts +1 -1
  183. package/src/specs/multi-agent-summarization.test.ts +396 -0
  184. package/src/specs/prune.test.ts +1196 -23
  185. package/src/specs/summarization-unit.test.ts +868 -0
  186. package/src/specs/summarization.test.ts +3827 -0
  187. package/src/specs/summarize-prune.test.ts +376 -0
  188. package/src/specs/thinking-handoff.test.ts +10 -10
  189. package/src/specs/thinking-prune.test.ts +7 -4
  190. package/src/specs/token-accounting-e2e.test.ts +1034 -0
  191. package/src/specs/token-accounting-pipeline.test.ts +882 -0
  192. package/src/specs/token-distribution-edge-case.test.ts +25 -26
  193. package/src/splitStream.test.ts +42 -33
  194. package/src/stream.ts +64 -11
  195. package/src/summarization/__tests__/aggregator.test.ts +153 -0
  196. package/src/summarization/__tests__/node.test.ts +708 -0
  197. package/src/summarization/__tests__/trigger.test.ts +50 -0
  198. package/src/summarization/index.ts +102 -0
  199. package/src/summarization/node.ts +982 -0
  200. package/src/tools/ToolNode.ts +25 -3
  201. package/src/types/graph.ts +62 -7
  202. package/src/types/index.ts +1 -0
  203. package/src/types/run.ts +32 -0
  204. package/src/types/stream.ts +45 -5
  205. package/src/types/summarize.ts +58 -0
  206. package/src/types/tools.ts +7 -0
  207. package/src/utils/errors.ts +117 -0
  208. package/src/utils/events.ts +31 -0
  209. package/src/utils/handlers.ts +18 -0
  210. package/src/utils/index.ts +2 -0
  211. package/src/utils/llm.ts +12 -0
  212. package/src/utils/tokens.ts +336 -18
  213. package/src/utils/truncation.ts +124 -0
  214. package/src/scripts/image.ts +0 -180
@@ -1,35 +1,36 @@
1
1
  import { nanoid } from 'nanoid';
2
- import { concat } from '@langchain/core/utils/stream';
3
- import { ChatVertexAI } from '@langchain/google-vertexai';
4
- import { Annotation, messagesStateReducer, StateGraph, START, END } from '@langchain/langgraph';
5
- import { RunnableLambda } from '@langchain/core/runnables';
6
- import { SystemMessage, AIMessageChunk, ToolMessage } from '@langchain/core/messages';
7
- import { convertMessagesToContent, modifyDeltaProperties, formatAnthropicArtifactContent, formatArtifactPayload } from '../messages/core.mjs';
2
+ import { AIMessageChunk, ToolMessage } from '@langchain/core/messages';
3
+ import { Annotation, StateGraph, START, END } from '@langchain/langgraph';
4
+ import { convertMessagesToContent, formatAnthropicArtifactContent, formatArtifactPayload } from '../messages/core.mjs';
8
5
  import { getMessageId } from '../messages/ids.mjs';
9
- import { createPruneMessages } from '../messages/prune.mjs';
6
+ import { createPruneMessages, sanitizeOrphanToolBlocks } from '../messages/prune.mjs';
10
7
  import { ensureThinkingBlockInMessages } from '../messages/format.mjs';
11
8
  import { addCacheControl, addBedrockCacheControl } from '../messages/cache.mjs';
12
9
  import { formatContentStrings } from '../messages/content.mjs';
13
10
  import { extractToolDiscoveries } from '../messages/tools.mjs';
14
- import { GraphNodeKeys, ContentTypes, Providers, GraphEvents, StepTypes } from '../common/enum.mjs';
11
+ import { messagesStateReducer } from '../messages/reducer.mjs';
12
+ import { GraphNodeKeys, ContentTypes, Providers, StepTypes, GraphEvents } from '../common/enum.mjs';
15
13
  import { resetIfNotEmpty, joinKeys } from '../utils/graph.mjs';
16
- import { isOpenAILike, isGoogleLike } from '../utils/llm.mjs';
17
- import { ChatModelStreamHandler } from '../stream.mjs';
14
+ import { isAnthropicLike, isOpenAILike, isGoogleLike } from '../utils/llm.mjs';
18
15
  import { handleToolCalls } from '../tools/handlers.mjs';
19
16
  import { sleep } from '../utils/run.mjs';
20
17
  import 'ai-tokenizer';
21
18
  import 'zod-to-json-schema';
22
- import { getChatModelClass, manualToolStreamProviders } from '../llm/providers.mjs';
23
19
  import { ToolNode, toolsCondition } from '../tools/ToolNode.mjs';
24
- import { ChatOpenAI, AzureChatOpenAI } from '../llm/openai/index.mjs';
25
- import { safeDispatchCustomEvent } from '../utils/events.mjs';
20
+ import { emitAgentLog, safeDispatchCustomEvent } from '../utils/events.mjs';
21
+ import { attemptInvoke, tryFallbackProviders } from '../llm/invoke.mjs';
22
+ import { shouldTriggerSummarization } from '../summarization/index.mjs';
23
+ import { createSummarizeNode } from '../summarization/node.mjs';
26
24
  import { createSchemaOnlyTools } from '../tools/schema.mjs';
27
25
  import { AgentContext } from '../agents/AgentContext.mjs';
28
26
  import { createFakeStreamingLLM } from '../llm/fake.mjs';
27
+ import { isThinkingEnabled } from '../llm/request.mjs';
28
+ import { initializeModel } from '../llm/init.mjs';
29
29
 
30
30
  /* eslint-disable no-console */
31
- // src/graphs/Graph.ts
32
- const { AGENT, TOOLS } = GraphNodeKeys;
31
+ const { AGENT, TOOLS, SUMMARIZE } = GraphNodeKeys;
32
+ /** Minimum relative variance before calibrated toolSchemaTokens overrides current value. */
33
+ const CALIBRATION_VARIANCE_THRESHOLD = 0.15;
33
34
  class Graph {
34
35
  messageStepHasToolCalls = new Map();
35
36
  messageIdsByStepKey = new Map();
@@ -39,6 +40,12 @@ class Graph {
39
40
  stepKeyIds = new Map();
40
41
  contentIndexMap = new Map();
41
42
  toolCallStepIds = new Map();
43
+ /**
44
+ * Step IDs that have been dispatched via handler registry directly
45
+ * (in dispatchRunStep). Used by the custom event callback to skip
46
+ * duplicate dispatch through the LangGraph callback chain.
47
+ */
48
+ handlerDispatchedStepIds = new Set();
42
49
  signal;
43
50
  /** Set of invoked tool call IDs from non-message run steps completed mid-run, if any */
44
51
  invokedToolIds;
@@ -74,16 +81,23 @@ class StandardGraph extends Graph {
74
81
  /** Optional compile options passed into workflow.compile() */
75
82
  compileOptions;
76
83
  messages = [];
84
+ /** Cached run messages preserved before clearHeavyState() so getRunMessages() works after cleanup. */
85
+ cachedRunMessages;
77
86
  runId;
87
+ /**
88
+ * Boundary between historical messages (loaded from conversation state)
89
+ * and messages produced during the current run. Set once in the state
90
+ * reducer when messages first arrive. Used by `getRunMessages()` and
91
+ * multi-agent message filtering — NOT for pruner token counting (the
92
+ * pruner maintains its own `lastTurnStartIndex` in its closure).
93
+ */
78
94
  startIndex = 0;
79
95
  signal;
80
96
  /** Map of agent contexts by agent ID */
81
97
  agentContexts = new Map();
82
98
  /** Default agent ID to use */
83
99
  defaultAgentId;
84
- constructor({
85
- // parent-level graph inputs
86
- runId, signal, agents, tokenCounter, indexTokenCountMap, }) {
100
+ constructor({ runId, signal, agents, tokenCounter, indexTokenCountMap, calibrationRatio, }) {
87
101
  super();
88
102
  this.runId = runId;
89
103
  this.signal = signal;
@@ -92,6 +106,9 @@ class StandardGraph extends Graph {
92
106
  }
93
107
  for (const agentConfig of agents) {
94
108
  const agentContext = AgentContext.fromConfig(agentConfig, tokenCounter, indexTokenCountMap);
109
+ if (calibrationRatio != null && calibrationRatio > 0) {
110
+ agentContext.calibrationRatio = calibrationRatio;
111
+ }
95
112
  this.agentContexts.set(agentConfig.agentId, agentContext);
96
113
  }
97
114
  this.defaultAgentId = agents[0].agentId;
@@ -99,6 +116,7 @@ class StandardGraph extends Graph {
99
116
  /* Init */
100
117
  resetValues(keepContent) {
101
118
  this.messages = [];
119
+ this.cachedRunMessages = undefined;
102
120
  this.config = resetIfNotEmpty(this.config, undefined);
103
121
  if (keepContent !== true) {
104
122
  this.contentData = resetIfNotEmpty(this.contentData, []);
@@ -112,6 +130,7 @@ class StandardGraph extends Graph {
112
130
  * a stale reference on 2nd+ processStream calls.
113
131
  */
114
132
  this.toolCallStepIds.clear();
133
+ this.handlerDispatchedStepIds = resetIfNotEmpty(this.handlerDispatchedStepIds, new Set());
115
134
  this.messageIdsByStepKey = resetIfNotEmpty(this.messageIdsByStepKey, new Map());
116
135
  this.messageStepHasToolCalls = resetIfNotEmpty(this.messageStepHasToolCalls, new Map());
117
136
  this.prelimMessageIdsByStepKey = resetIfNotEmpty(this.prelimMessageIdsByStepKey, new Map());
@@ -121,6 +140,7 @@ class StandardGraph extends Graph {
121
140
  }
122
141
  }
123
142
  clearHeavyState() {
143
+ this.cachedRunMessages = this.messages.slice(this.startIndex);
124
144
  super.clearHeavyState();
125
145
  this.messages = [];
126
146
  this.overrideModel = undefined;
@@ -151,6 +171,9 @@ class StandardGraph extends Graph {
151
171
  else if (currentNode.startsWith(TOOLS)) {
152
172
  agentId = currentNode.substring(TOOLS.length);
153
173
  }
174
+ else if (currentNode.startsWith(SUMMARIZE)) {
175
+ agentId = currentNode.substring(SUMMARIZE.length);
176
+ }
154
177
  const agentContext = this.agentContexts.get(agentId ?? '');
155
178
  if (!agentContext) {
156
179
  throw new Error(`No agent context found for agent ID ${agentId}`);
@@ -220,11 +243,26 @@ class StandardGraph extends Graph {
220
243
  }
221
244
  /* Misc.*/
222
245
  getRunMessages() {
246
+ if (this.messages.length === 0 && this.cachedRunMessages != null) {
247
+ return this.cachedRunMessages;
248
+ }
223
249
  return this.messages.slice(this.startIndex);
224
250
  }
225
251
  getContentParts() {
226
252
  return convertMessagesToContent(this.messages.slice(this.startIndex));
227
253
  }
254
+ getCalibrationRatio() {
255
+ const context = this.agentContexts.get(this.defaultAgentId);
256
+ return context?.calibrationRatio ?? 1;
257
+ }
258
+ getResolvedInstructionOverhead() {
259
+ const context = this.agentContexts.get(this.defaultAgentId);
260
+ return context?.resolvedInstructionOverhead;
261
+ }
262
+ getToolCount() {
263
+ const context = this.agentContexts.get(this.defaultAgentId);
264
+ return ((context?.tools?.length ?? 0) + (context?.toolDefinitions?.length ?? 0));
265
+ }
228
266
  /**
229
267
  * Get all run steps, optionally filtered by agent ID
230
268
  */
@@ -276,35 +314,6 @@ class StandardGraph extends Graph {
276
314
  return contentPartAgentMap;
277
315
  }
278
316
  /* Graph */
279
- createSystemRunnable({ provider, clientOptions, instructions, additional_instructions, }) {
280
- let finalInstructions = instructions;
281
- if (additional_instructions != null && additional_instructions !== '') {
282
- finalInstructions =
283
- finalInstructions != null && finalInstructions
284
- ? `${finalInstructions}\n\n${additional_instructions}`
285
- : additional_instructions;
286
- }
287
- if (finalInstructions != null &&
288
- finalInstructions &&
289
- provider === Providers.ANTHROPIC &&
290
- clientOptions.promptCache === true) {
291
- finalInstructions = {
292
- content: [
293
- {
294
- type: 'text',
295
- text: instructions,
296
- cache_control: { type: 'ephemeral' },
297
- },
298
- ],
299
- };
300
- }
301
- if (finalInstructions != null && finalInstructions !== '') {
302
- const systemMessage = new SystemMessage(finalInstructions);
303
- return RunnableLambda.from((messages) => {
304
- return [systemMessage, ...messages];
305
- }).withConfig({ runName: 'prompt' });
306
- }
307
- }
308
317
  initializeTools({ currentTools, currentToolMap, agentContext, }) {
309
318
  const toolDefinitions = agentContext?.toolDefinitions;
310
319
  const eventDrivenMode = toolDefinitions != null && toolDefinitions.length > 0;
@@ -334,6 +343,8 @@ class StandardGraph extends Graph {
334
343
  toolCallStepIds: this.toolCallStepIds,
335
344
  toolRegistry: agentContext?.toolRegistry,
336
345
  directToolNames: directToolNames.size > 0 ? directToolNames : undefined,
346
+ maxContextTokens: agentContext?.maxContextTokens,
347
+ maxToolResultChars: agentContext?.maxToolResultChars,
337
348
  errorHandler: (data, metadata) => StandardGraph.handleToolCallErrorStatic(this, data, metadata),
338
349
  });
339
350
  }
@@ -357,42 +368,10 @@ class StandardGraph extends Graph {
357
368
  errorHandler: (data, metadata) => StandardGraph.handleToolCallErrorStatic(this, data, metadata),
358
369
  toolRegistry: agentContext?.toolRegistry,
359
370
  sessions: this.sessions,
371
+ maxContextTokens: agentContext?.maxContextTokens,
372
+ maxToolResultChars: agentContext?.maxToolResultChars,
360
373
  });
361
374
  }
362
- initializeModel({ provider, tools, clientOptions, }) {
363
- const ChatModelClass = getChatModelClass(provider);
364
- const model = new ChatModelClass(clientOptions ?? {});
365
- if (isOpenAILike(provider) &&
366
- (model instanceof ChatOpenAI || model instanceof AzureChatOpenAI)) {
367
- model.temperature = clientOptions
368
- .temperature;
369
- model.topP = clientOptions.topP;
370
- model.frequencyPenalty = clientOptions
371
- .frequencyPenalty;
372
- model.presencePenalty = clientOptions
373
- .presencePenalty;
374
- model.n = clientOptions.n;
375
- }
376
- else if (provider === Providers.VERTEXAI &&
377
- model instanceof ChatVertexAI) {
378
- model.temperature = clientOptions
379
- .temperature;
380
- model.topP = clientOptions.topP;
381
- model.topK = clientOptions.topK;
382
- model.topLogprobs = clientOptions
383
- .topLogprobs;
384
- model.frequencyPenalty = clientOptions
385
- .frequencyPenalty;
386
- model.presencePenalty = clientOptions
387
- .presencePenalty;
388
- model.maxOutputTokens = clientOptions
389
- .maxOutputTokens;
390
- }
391
- if (!tools || tools.length === 0) {
392
- return model;
393
- }
394
- return model.bindTools(tools);
395
- }
396
375
  overrideTestModel(responses, sleep, toolCalls) {
397
376
  this.overrideModel = createFakeStreamingLLM({
398
377
  responses,
@@ -400,10 +379,6 @@ class StandardGraph extends Graph {
400
379
  toolCalls,
401
380
  });
402
381
  }
403
- getNewModel({ provider, clientOptions, }) {
404
- const ChatModelClass = getChatModelClass(provider);
405
- return new ChatModelClass(clientOptions ?? {});
406
- }
407
382
  getUsageMetadata(finalMessage) {
408
383
  if (finalMessage &&
409
384
  'usage_metadata' in finalMessage &&
@@ -411,58 +386,6 @@ class StandardGraph extends Graph {
411
386
  return finalMessage.usage_metadata;
412
387
  }
413
388
  }
414
- /** Execute model invocation with streaming support */
415
- async attemptInvoke({ currentModel, finalMessages, provider, tools: _tools, }, config) {
416
- const model = this.overrideModel ?? currentModel;
417
- if (!model) {
418
- throw new Error('No model found');
419
- }
420
- if (model.stream) {
421
- /**
422
- * Process all model output through a local ChatModelStreamHandler in the
423
- * graph execution context. Each chunk is awaited before the next one is
424
- * consumed, so by the time the stream is exhausted every run step
425
- * (MESSAGE_CREATION, TOOL_CALLS) has been created and toolCallStepIds is
426
- * fully populated — the graph will not transition to ToolNode until this
427
- * is done.
428
- *
429
- * This replaces the previous pattern where ChatModelStreamHandler lived
430
- * in the for-await stream consumer (handler registry). That consumer
431
- * runs concurrently with graph execution, so the graph could advance to
432
- * ToolNode before the consumer had processed all events. By handling
433
- * chunks here, inside the agent node, the race is eliminated.
434
- *
435
- * The for-await consumer no longer needs a ChatModelStreamHandler; its
436
- * on_chat_model_stream events are simply ignored (no handler registered).
437
- * The dispatched custom events (ON_RUN_STEP, ON_MESSAGE_DELTA, etc.)
438
- * still reach the content aggregator and SSE handlers through the custom
439
- * event callback in Run.createCustomEventCallback.
440
- */
441
- const metadata = config?.metadata;
442
- const streamHandler = new ChatModelStreamHandler();
443
- const stream = await model.stream(finalMessages, config);
444
- let finalChunk;
445
- for await (const chunk of stream) {
446
- await streamHandler.handle(GraphEvents.CHAT_MODEL_STREAM, { chunk }, metadata, this);
447
- finalChunk = finalChunk ? concat(finalChunk, chunk) : chunk;
448
- }
449
- if (manualToolStreamProviders.has(provider)) {
450
- finalChunk = modifyDeltaProperties(provider, finalChunk);
451
- }
452
- if ((finalChunk?.tool_calls?.length ?? 0) > 0) {
453
- finalChunk.tool_calls = finalChunk.tool_calls?.filter((tool_call) => !!tool_call.name);
454
- }
455
- return { messages: [finalChunk] };
456
- }
457
- else {
458
- /** Fallback for models without stream support. */
459
- const finalMessage = await model.invoke(finalMessages, config);
460
- if ((finalMessage.tool_calls?.length ?? 0) > 0) {
461
- finalMessage.tool_calls = finalMessage.tool_calls?.filter((tool_call) => !!tool_call.name);
462
- }
463
- return { messages: [finalMessage] };
464
- }
465
- }
466
389
  cleanupSignalListener(currentModel) {
467
390
  if (!this.signal) {
468
391
  return;
@@ -480,9 +403,6 @@ class StandardGraph extends Graph {
480
403
  }
481
404
  createCallModel(agentId = 'default') {
482
405
  return async (state, config) => {
483
- /**
484
- * Get agent context - it must exist by this point
485
- */
486
406
  const agentContext = this.agentContexts.get(agentId);
487
407
  if (!agentContext) {
488
408
  throw new Error(`Agent context not found for agentId: ${agentId}`);
@@ -491,14 +411,13 @@ class StandardGraph extends Graph {
491
411
  throw new Error('No config provided');
492
412
  }
493
413
  const { messages } = state;
494
- // Extract tool discoveries from current turn only (similar to formatArtifactPayload pattern)
495
414
  const discoveredNames = extractToolDiscoveries(messages);
496
415
  if (discoveredNames.length > 0) {
497
416
  agentContext.markToolsAsDiscovered(discoveredNames);
498
417
  }
499
418
  const toolsForBinding = agentContext.getToolsForBinding();
500
419
  let model = this.overrideModel ??
501
- this.initializeModel({
420
+ initializeModel({
502
421
  tools: toolsForBinding,
503
422
  provider: agentContext.provider,
504
423
  clientOptions: agentContext.clientOptions,
@@ -516,34 +435,97 @@ class StandardGraph extends Graph {
516
435
  let messagesToUse = messages;
517
436
  if (!agentContext.pruneMessages &&
518
437
  agentContext.tokenCounter &&
519
- agentContext.maxContextTokens != null &&
520
- agentContext.indexTokenCountMap[0] != null) {
521
- const isAnthropicWithThinking = (agentContext.provider === Providers.ANTHROPIC &&
522
- agentContext.clientOptions.thinking !=
523
- null) ||
524
- (agentContext.provider === Providers.BEDROCK &&
525
- agentContext.clientOptions
526
- .additionalModelRequestFields?.['thinking'] != null) ||
527
- (agentContext.provider === Providers.OPENAI &&
528
- agentContext.clientOptions.modelKwargs
529
- ?.thinking?.type === 'enabled');
438
+ agentContext.maxContextTokens != null) {
530
439
  agentContext.pruneMessages = createPruneMessages({
531
- startIndex: this.startIndex,
440
+ startIndex: agentContext.indexTokenCountMap[0] != null ? this.startIndex : 0,
532
441
  provider: agentContext.provider,
533
442
  tokenCounter: agentContext.tokenCounter,
534
443
  maxTokens: agentContext.maxContextTokens,
535
- thinkingEnabled: isAnthropicWithThinking,
444
+ thinkingEnabled: isThinkingEnabled(agentContext.provider, agentContext.clientOptions),
536
445
  indexTokenCountMap: agentContext.indexTokenCountMap,
446
+ contextPruningConfig: agentContext.contextPruningConfig,
447
+ summarizationEnabled: agentContext.summarizationEnabled,
448
+ reserveRatio: agentContext.summarizationConfig?.reserveRatio,
449
+ calibrationRatio: agentContext.calibrationRatio,
450
+ getInstructionTokens: () => agentContext.instructionTokens,
451
+ log: (level, message, data) => {
452
+ emitAgentLog(config, level, 'prune', message, data, {
453
+ runId: this.runId,
454
+ agentId,
455
+ });
456
+ },
537
457
  });
538
458
  }
539
459
  if (agentContext.pruneMessages) {
540
- const { context, indexTokenCountMap } = agentContext.pruneMessages({
460
+ const { context, indexTokenCountMap, messagesToRefine, prePruneContextTokens, remainingContextTokens, originalToolContent, calibrationRatio, resolvedInstructionOverhead, } = agentContext.pruneMessages({
541
461
  messages,
542
462
  usageMetadata: agentContext.currentUsage,
543
- // startOnMessageType: 'human',
463
+ lastCallUsage: agentContext.lastCallUsage,
464
+ totalTokensFresh: agentContext.totalTokensFresh,
544
465
  });
545
466
  agentContext.indexTokenCountMap = indexTokenCountMap;
467
+ if (calibrationRatio != null && calibrationRatio > 0) {
468
+ agentContext.calibrationRatio = calibrationRatio;
469
+ }
470
+ if (resolvedInstructionOverhead != null) {
471
+ agentContext.resolvedInstructionOverhead =
472
+ resolvedInstructionOverhead;
473
+ const nonToolOverhead = agentContext.instructionTokens - agentContext.toolSchemaTokens;
474
+ const calibratedToolTokens = Math.max(0, resolvedInstructionOverhead - nonToolOverhead);
475
+ const currentToolTokens = agentContext.toolSchemaTokens;
476
+ const variance = currentToolTokens > 0
477
+ ? Math.abs(calibratedToolTokens - currentToolTokens) /
478
+ currentToolTokens
479
+ : 1;
480
+ if (variance > CALIBRATION_VARIANCE_THRESHOLD) {
481
+ agentContext.toolSchemaTokens = calibratedToolTokens;
482
+ }
483
+ }
546
484
  messagesToUse = context;
485
+ const hasPrunedMessages = agentContext.summarizationEnabled === true &&
486
+ Array.isArray(messagesToRefine) &&
487
+ messagesToRefine.length > 0;
488
+ if (hasPrunedMessages) {
489
+ const shouldSkip = agentContext.shouldSkipSummarization(messages.length);
490
+ const triggerResult = !shouldSkip &&
491
+ shouldTriggerSummarization({
492
+ trigger: agentContext.summarizationConfig?.trigger,
493
+ maxContextTokens: agentContext.maxContextTokens,
494
+ prePruneContextTokens: prePruneContextTokens != null
495
+ ? prePruneContextTokens + agentContext.instructionTokens
496
+ : undefined,
497
+ remainingContextTokens,
498
+ messagesToRefineCount: messagesToRefine.length,
499
+ });
500
+ if (triggerResult) {
501
+ if (originalToolContent != null && originalToolContent.size > 0) {
502
+ agentContext.pendingOriginalToolContent = originalToolContent;
503
+ }
504
+ emitAgentLog(config, 'info', 'graph', 'Summarization triggered', undefined, { runId: this.runId, agentId });
505
+ emitAgentLog(config, 'debug', 'graph', 'Summarization trigger details', {
506
+ totalMessages: messages.length,
507
+ remainingContextTokens: remainingContextTokens ?? 0,
508
+ summaryVersion: agentContext.summaryVersion + 1,
509
+ toolSchemaTokens: agentContext.toolSchemaTokens,
510
+ instructionTokens: agentContext.instructionTokens,
511
+ systemMessageTokens: agentContext.systemMessageTokens,
512
+ }, { runId: this.runId, agentId });
513
+ agentContext.markSummarizationTriggered(messages.length);
514
+ return {
515
+ summarizationRequest: {
516
+ remainingContextTokens: remainingContextTokens ?? 0,
517
+ agentId: agentId || agentContext.agentId,
518
+ },
519
+ };
520
+ }
521
+ if (shouldSkip) {
522
+ emitAgentLog(config, 'debug', 'graph', 'Summarization skipped — no new messages or per-run cap reached', {
523
+ messageCount: messages.length,
524
+ messagesToRefineCount: messagesToRefine.length,
525
+ contextLength: context.length,
526
+ }, { runId: this.runId, agentId });
527
+ }
528
+ }
547
529
  }
548
530
  let finalMessages = messagesToUse;
549
531
  if (agentContext.useLegacyContent) {
@@ -555,26 +537,29 @@ class StandardGraph extends Graph {
555
537
  const lastMessageY = finalMessages.length >= 1
556
538
  ? finalMessages[finalMessages.length - 1]
557
539
  : null;
540
+ const anthropicLike = isAnthropicLike(agentContext.provider, agentContext.clientOptions);
558
541
  if (agentContext.provider === Providers.BEDROCK &&
559
542
  lastMessageX instanceof AIMessageChunk &&
560
543
  lastMessageY instanceof ToolMessage &&
561
544
  typeof lastMessageX.content === 'string') {
562
- finalMessages[finalMessages.length - 2].content = '';
563
- }
564
- const isLatestToolMessage = lastMessageY instanceof ToolMessage;
565
- if (isLatestToolMessage &&
566
- agentContext.provider === Providers.ANTHROPIC) {
567
- formatAnthropicArtifactContent(finalMessages);
545
+ const trimmed = lastMessageX.content.trim();
546
+ finalMessages[finalMessages.length - 2].content =
547
+ trimmed.length > 0 ? [{ type: 'text', text: trimmed }] : '';
568
548
  }
569
- else if (isLatestToolMessage &&
570
- ((isOpenAILike(agentContext.provider) &&
549
+ if (lastMessageY instanceof ToolMessage) {
550
+ if (anthropicLike) {
551
+ formatAnthropicArtifactContent(finalMessages);
552
+ }
553
+ else if ((isOpenAILike(agentContext.provider) &&
571
554
  agentContext.provider !== Providers.DEEPSEEK) ||
572
- isGoogleLike(agentContext.provider))) {
573
- formatArtifactPayload(finalMessages);
555
+ isGoogleLike(agentContext.provider)) {
556
+ formatArtifactPayload(finalMessages);
557
+ }
574
558
  }
575
559
  if (agentContext.provider === Providers.ANTHROPIC) {
576
560
  const anthropicOptions = agentContext.clientOptions;
577
- if (anthropicOptions?.promptCache === true) {
561
+ if (anthropicOptions?.promptCache === true &&
562
+ !agentContext.systemRunnable) {
578
563
  finalMessages = addCacheControl(finalMessages);
579
564
  }
580
565
  }
@@ -584,19 +569,25 @@ class StandardGraph extends Graph {
584
569
  finalMessages = addBedrockCacheControl(finalMessages);
585
570
  }
586
571
  }
587
- /**
588
- * Handle edge case: when switching from a non-thinking agent to a thinking-enabled agent,
589
- * convert AI messages with tool calls to HumanMessages to avoid thinking block requirements.
590
- * This is required by Anthropic/Bedrock when thinking is enabled.
591
- */
592
- const isAnthropicWithThinking = (agentContext.provider === Providers.ANTHROPIC &&
593
- agentContext.clientOptions.thinking !=
594
- null) ||
595
- (agentContext.provider === Providers.BEDROCK &&
596
- agentContext.clientOptions
597
- .additionalModelRequestFields?.['thinking'] != null);
598
- if (isAnthropicWithThinking) {
599
- finalMessages = ensureThinkingBlockInMessages(finalMessages, agentContext.provider);
572
+ if (isThinkingEnabled(agentContext.provider, agentContext.clientOptions)) {
573
+ finalMessages = ensureThinkingBlockInMessages(finalMessages, agentContext.provider, config);
574
+ }
575
+ // Intentionally broad: runs when the pruner wasn't used OR any post-pruning
576
+ // transform (addCacheControl, ensureThinkingBlock, etc.) reassigned finalMessages.
577
+ // sanitizeOrphanToolBlocks fast-paths to a Set diff check when no orphans exist,
578
+ // so the cost is negligible and this acts as a safety net for Anthropic/Bedrock.
579
+ const needsOrphanSanitize = anthropicLike &&
580
+ (!agentContext.pruneMessages || finalMessages !== messagesToUse);
581
+ if (needsOrphanSanitize) {
582
+ const beforeSanitize = finalMessages.length;
583
+ finalMessages = sanitizeOrphanToolBlocks(finalMessages);
584
+ if (finalMessages.length !== beforeSanitize) {
585
+ emitAgentLog(config, 'warn', 'sanitize', 'Orphan tool blocks removed', {
586
+ before: beforeSanitize,
587
+ after: finalMessages.length,
588
+ dropped: beforeSanitize - finalMessages.length,
589
+ }, { runId: this.runId, agentId });
590
+ }
600
591
  }
601
592
  if (agentContext.lastStreamCall != null &&
602
593
  agentContext.streamBuffer != null) {
@@ -608,52 +599,68 @@ class StandardGraph extends Graph {
608
599
  }
609
600
  }
610
601
  agentContext.lastStreamCall = Date.now();
602
+ agentContext.markTokensStale();
611
603
  let result;
612
604
  const fallbacks = agentContext.clientOptions?.fallbacks ??
613
605
  [];
614
- if (finalMessages.length === 0) {
606
+ if (finalMessages.length === 0 &&
607
+ !agentContext.hasPendingCompactionSummary()) {
608
+ const budgetBreakdown = agentContext.getTokenBudgetBreakdown(messages);
609
+ const breakdown = agentContext.formatTokenBudgetBreakdown(messages);
610
+ const instructionsExceedBudget = budgetBreakdown.instructionTokens > budgetBreakdown.maxContextTokens;
611
+ let guidance;
612
+ if (instructionsExceedBudget) {
613
+ const toolPct = budgetBreakdown.toolSchemaTokens > 0
614
+ ? Math.round((budgetBreakdown.toolSchemaTokens /
615
+ budgetBreakdown.instructionTokens) *
616
+ 100)
617
+ : 0;
618
+ guidance =
619
+ toolPct > 50
620
+ ? `Tool definitions consume ${budgetBreakdown.toolSchemaTokens} tokens (${toolPct}% of instructions) across ${budgetBreakdown.toolCount} tools, exceeding maxContextTokens (${budgetBreakdown.maxContextTokens}). Reduce the number of tools or increase maxContextTokens.`
621
+ : `Instructions (${budgetBreakdown.instructionTokens} tokens) exceed maxContextTokens (${budgetBreakdown.maxContextTokens}). Increase maxContextTokens or shorten the system prompt.`;
622
+ if (agentContext.summarizationEnabled === true) {
623
+ guidance +=
624
+ ' Summarization was skipped because the summary would further increase the instruction overhead.';
625
+ }
626
+ }
627
+ else {
628
+ guidance =
629
+ 'Please increase the context window size or make your message shorter.';
630
+ }
631
+ emitAgentLog(config, 'error', 'graph', 'Empty messages after pruning', {
632
+ messageCount: messages.length,
633
+ instructionsExceedBudget,
634
+ breakdown,
635
+ }, { runId: this.runId, agentId });
615
636
  throw new Error(JSON.stringify({
616
637
  type: 'empty_messages',
617
- info: 'Message pruning removed all messages as none fit in the context window. Please increase the context window size or make your message shorter.',
638
+ info: `Message pruning removed all messages as none fit in the context window. ${guidance}\n${breakdown}`,
618
639
  }));
619
640
  }
641
+ const invokeStart = Date.now();
642
+ const invokeMeta = { runId: this.runId, agentId };
643
+ emitAgentLog(config, 'debug', 'graph', 'Invoking LLM', {
644
+ messageCount: finalMessages.length,
645
+ provider: agentContext.provider,
646
+ }, invokeMeta, { force: true });
620
647
  try {
621
- result = await this.attemptInvoke({
622
- currentModel: model,
623
- finalMessages,
648
+ result = await attemptInvoke({
649
+ model: (this.overrideModel ?? model),
650
+ messages: finalMessages,
624
651
  provider: agentContext.provider,
625
- tools: agentContext.tools,
652
+ context: this,
626
653
  }, config);
627
654
  }
628
655
  catch (primaryError) {
629
- let lastError = primaryError;
630
- for (const fb of fallbacks) {
631
- try {
632
- let model = this.getNewModel({
633
- provider: fb.provider,
634
- clientOptions: fb.clientOptions,
635
- });
636
- const bindableTools = agentContext.tools;
637
- model = (!bindableTools || bindableTools.length === 0
638
- ? model
639
- : model.bindTools(bindableTools));
640
- result = await this.attemptInvoke({
641
- currentModel: model,
642
- finalMessages,
643
- provider: fb.provider,
644
- tools: agentContext.tools,
645
- }, config);
646
- lastError = undefined;
647
- break;
648
- }
649
- catch (e) {
650
- lastError = e;
651
- continue;
652
- }
653
- }
654
- if (lastError !== undefined) {
655
- throw lastError;
656
- }
656
+ result = await tryFallbackProviders({
657
+ fallbacks,
658
+ tools: agentContext.tools,
659
+ messages: finalMessages,
660
+ config,
661
+ primaryError,
662
+ context: this,
663
+ });
657
664
  }
658
665
  if (!result) {
659
666
  throw new Error('No result after model invocation');
@@ -753,20 +760,42 @@ class StandardGraph extends Graph {
753
760
  }
754
761
  }
755
762
  }
763
+ const invokeElapsed = ((Date.now() - invokeStart) / 1000).toFixed(2);
756
764
  agentContext.currentUsage = this.getUsageMetadata(result.messages?.[0]);
765
+ if (agentContext.currentUsage) {
766
+ agentContext.updateLastCallUsage(agentContext.currentUsage);
767
+ emitAgentLog(config, 'debug', 'graph', `LLM call complete (${invokeElapsed}s)`, {
768
+ ...agentContext.currentUsage,
769
+ elapsedSeconds: Number(invokeElapsed),
770
+ instructionTokens: agentContext.instructionTokens,
771
+ toolSchemaTokens: agentContext.toolSchemaTokens,
772
+ messageCount: finalMessages.length,
773
+ }, invokeMeta, { force: true });
774
+ }
775
+ else {
776
+ emitAgentLog(config, 'debug', 'graph', `LLM call complete (${invokeElapsed}s)`, {
777
+ elapsedSeconds: Number(invokeElapsed),
778
+ messageCount: finalMessages.length,
779
+ }, invokeMeta, { force: true });
780
+ }
757
781
  this.cleanupSignalListener();
758
782
  return result;
759
783
  };
760
784
  }
761
785
  createAgentNode(agentId) {
786
+ const getConfig = () => this.config;
762
787
  const agentContext = this.agentContexts.get(agentId);
763
788
  if (!agentContext) {
764
789
  throw new Error(`Agent context not found for agentId: ${agentId}`);
765
790
  }
766
791
  const agentNode = `${AGENT}${agentId}`;
767
792
  const toolNode = `${TOOLS}${agentId}`;
793
+ const summarizeNode = `${SUMMARIZE}${agentId}`;
768
794
  const routeMessage = (state, config) => {
769
795
  this.config = config;
796
+ if (state.summarizationRequest != null) {
797
+ return summarizeNode;
798
+ }
770
799
  return toolsCondition(state, toolNode, this.invokedToolIds);
771
800
  };
772
801
  const StateAnnotation = Annotation.Root({
@@ -774,6 +803,10 @@ class StandardGraph extends Graph {
774
803
  reducer: messagesStateReducer,
775
804
  default: () => [],
776
805
  }),
806
+ summarizationRequest: Annotation({
807
+ reducer: (_, b) => b,
808
+ default: () => undefined,
809
+ }),
777
810
  });
778
811
  const workflow = new StateGraph(StateAnnotation)
779
812
  .addNode(agentNode, this.createCallModel(agentId))
@@ -781,15 +814,54 @@ class StandardGraph extends Graph {
781
814
  currentTools: agentContext.tools,
782
815
  currentToolMap: agentContext.toolMap,
783
816
  agentContext,
817
+ }))
818
+ .addNode(summarizeNode, createSummarizeNode({
819
+ agentContext,
820
+ graph: {
821
+ contentData: this.contentData,
822
+ contentIndexMap: this.contentIndexMap,
823
+ get config() {
824
+ return getConfig();
825
+ },
826
+ runId: this.runId,
827
+ isMultiAgent: this.isMultiAgentGraph(),
828
+ dispatchRunStep: async (runStep, nodeConfig) => {
829
+ this.contentData.push(runStep);
830
+ this.contentIndexMap.set(runStep.id, runStep.index);
831
+ const resolvedConfig = nodeConfig ?? this.config;
832
+ const handler = this.handlerRegistry?.getHandler(GraphEvents.ON_RUN_STEP);
833
+ if (handler) {
834
+ await handler.handle(GraphEvents.ON_RUN_STEP, runStep, resolvedConfig?.configurable, this);
835
+ this.handlerDispatchedStepIds.add(runStep.id);
836
+ }
837
+ if (resolvedConfig) {
838
+ await safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP, runStep, resolvedConfig);
839
+ }
840
+ },
841
+ dispatchRunStepCompleted: async (stepId, result, nodeConfig) => {
842
+ const resolvedConfig = nodeConfig ?? this.config;
843
+ const runStep = this.contentData.find((s) => s.id === stepId);
844
+ const handler = this.handlerRegistry?.getHandler(GraphEvents.ON_RUN_STEP_COMPLETED);
845
+ if (handler) {
846
+ await handler.handle(GraphEvents.ON_RUN_STEP_COMPLETED, {
847
+ result: {
848
+ ...result,
849
+ id: stepId,
850
+ index: runStep?.index ?? 0,
851
+ },
852
+ }, resolvedConfig?.configurable, this);
853
+ }
854
+ },
855
+ },
856
+ generateStepId: (stepKey) => this.generateStepId(stepKey),
784
857
  }))
785
858
  .addEdge(START, agentNode)
786
859
  .addConditionalEdges(agentNode, routeMessage)
860
+ .addEdge(summarizeNode, agentNode)
787
861
  .addEdge(toolNode, agentContext.toolEnd ? END : agentNode);
788
- // Cast to unknown to avoid tight coupling to external types; options are opt-in
789
- return workflow.compile(this.compileOptions);
862
+ return workflow.compile();
790
863
  }
791
864
  createWorkflow() {
792
- /** Use the default (first) agent for now */
793
865
  const agentNode = this.createAgentNode(this.defaultAgentId);
794
866
  const StateAnnotation = Annotation.Root({
795
867
  messages: Annotation({
@@ -807,7 +879,8 @@ class StandardGraph extends Graph {
807
879
  const workflow = new StateGraph(StateAnnotation)
808
880
  .addNode(this.defaultAgentId, agentNode, { ends: [END] })
809
881
  .addEdge(START, this.defaultAgentId)
810
- .compile();
882
+ // LangGraph compile() types are overly strict for opt-in options
883
+ .compile(this.compileOptions);
811
884
  return workflow;
812
885
  }
813
886
  /**
@@ -858,18 +931,11 @@ class StandardGraph extends Graph {
858
931
  if (runId) {
859
932
  runStep.runId = runId;
860
933
  }
861
- /**
862
- * Extract agentId and parallelGroupId from metadata
863
- * Only set agentId for MultiAgentGraph (so frontend knows when to show agent labels)
864
- */
865
934
  if (metadata) {
866
935
  try {
867
936
  const agentContext = this.getAgentContext(metadata);
868
937
  if (this.isMultiAgentGraph() && agentContext.agentId) {
869
- // Only include agentId for MultiAgentGraph - enables frontend to show agent labels
870
938
  runStep.agentId = agentContext.agentId;
871
- // Set group ID if this agent is part of a parallel group
872
- // Group IDs are incrementing numbers (1, 2, 3...) reflecting execution order
873
939
  const groupId = this.getParallelGroupIdForAgent(agentContext.agentId);
874
940
  if (groupId != null) {
875
941
  runStep.groupId = groupId;
@@ -882,6 +948,21 @@ class StandardGraph extends Graph {
882
948
  }
883
949
  this.contentData.push(runStep);
884
950
  this.contentIndexMap.set(stepId, runStep.index);
951
+ // Primary dispatch: handler registry (reliable, always works).
952
+ // This mirrors how handleToolCallCompleted dispatches ON_RUN_STEP_COMPLETED
953
+ // via the handler registry, ensuring the event always reaches the handler
954
+ // even when LangGraph's callback system drops the custom event.
955
+ const handler = this.handlerRegistry?.getHandler(GraphEvents.ON_RUN_STEP);
956
+ if (handler) {
957
+ await handler.handle(GraphEvents.ON_RUN_STEP, runStep, metadata, this);
958
+ this.handlerDispatchedStepIds.add(stepId);
959
+ }
960
+ // Secondary dispatch: custom event for LangGraph callback chain
961
+ // (tracing, Langfuse, external consumers). May be silently dropped
962
+ // in some scenarios (stale run ID, subgraph callback propagation issues),
963
+ // but the primary dispatch above guarantees the event reaches the handler.
964
+ // The customEventCallback in run.ts skips events already dispatched above
965
+ // to prevent double handling.
885
966
  await safeDispatchCustomEvent(GraphEvents.ON_RUN_STEP, runStep, this.config);
886
967
  return stepId;
887
968
  }