@librechat/agents 3.8.0 → 3.8.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 (209) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +13 -3
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +174 -53
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +15 -5
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/langfuse.cjs +42 -0
  8. package/dist/cjs/langfuse.cjs.map +1 -1
  9. package/dist/cjs/langfuseToolOutputTracing.cjs +42 -0
  10. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  11. package/dist/cjs/langfuseTraceShaping.cjs +7 -1
  12. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  13. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +5 -9
  14. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  15. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +2 -7
  16. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  17. package/dist/cjs/llm/contextPressureMeter.cjs +51 -10
  18. package/dist/cjs/llm/contextPressureMeter.cjs.map +1 -1
  19. package/dist/cjs/llm/init.cjs +1 -1
  20. package/dist/cjs/llm/invoke.cjs +2 -2
  21. package/dist/cjs/llm/openai/index.cjs +4 -3
  22. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  23. package/dist/cjs/llm/openai/utils/index.cjs +5 -4
  24. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  25. package/dist/cjs/llm/preempt.cjs +3 -2
  26. package/dist/cjs/llm/preempt.cjs.map +1 -1
  27. package/dist/cjs/llm/prepareProviderRequest.cjs +9 -7
  28. package/dist/cjs/llm/prepareProviderRequest.cjs.map +1 -1
  29. package/dist/cjs/llm/providers.cjs +1 -1
  30. package/dist/cjs/main.cjs +11 -5
  31. package/dist/cjs/messages/alternation.cjs +1 -5
  32. package/dist/cjs/messages/alternation.cjs.map +1 -1
  33. package/dist/cjs/messages/budget.cjs +206 -7
  34. package/dist/cjs/messages/budget.cjs.map +1 -1
  35. package/dist/cjs/messages/cache.cjs +3 -9
  36. package/dist/cjs/messages/cache.cjs.map +1 -1
  37. package/dist/cjs/messages/core.cjs +25 -37
  38. package/dist/cjs/messages/core.cjs.map +1 -1
  39. package/dist/cjs/messages/format.cjs +131 -42
  40. package/dist/cjs/messages/format.cjs.map +1 -1
  41. package/dist/cjs/messages/index.cjs +2 -0
  42. package/dist/cjs/messages/prune.cjs +12 -7
  43. package/dist/cjs/messages/prune.cjs.map +1 -1
  44. package/dist/cjs/messages/reasoningTypes.cjs +21 -0
  45. package/dist/cjs/messages/reasoningTypes.cjs.map +1 -0
  46. package/dist/cjs/messages/recency.cjs +15 -94
  47. package/dist/cjs/messages/recency.cjs.map +1 -1
  48. package/dist/cjs/messages/toolHistoryProjection.cjs +243 -0
  49. package/dist/cjs/messages/toolHistoryProjection.cjs.map +1 -0
  50. package/dist/cjs/messages/toolResultTypes.cjs +145 -4
  51. package/dist/cjs/messages/toolResultTypes.cjs.map +1 -1
  52. package/dist/cjs/run.cjs +5 -4
  53. package/dist/cjs/run.cjs.map +1 -1
  54. package/dist/cjs/session/AgentSession.cjs +6 -4
  55. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  56. package/dist/cjs/session/JsonlSessionStore.cjs +3 -0
  57. package/dist/cjs/session/JsonlSessionStore.cjs.map +1 -1
  58. package/dist/cjs/session/index.cjs +1 -1
  59. package/dist/cjs/session/sessionProjection.cjs +75 -0
  60. package/dist/cjs/session/sessionProjection.cjs.map +1 -0
  61. package/dist/cjs/stream.cjs +18 -17
  62. package/dist/cjs/stream.cjs.map +1 -1
  63. package/dist/cjs/summarization/index.cjs.map +1 -1
  64. package/dist/cjs/summarization/node.cjs +18 -8
  65. package/dist/cjs/summarization/node.cjs.map +1 -1
  66. package/dist/cjs/summarization/shared.cjs +9 -0
  67. package/dist/cjs/summarization/shared.cjs.map +1 -1
  68. package/dist/cjs/tools/local/CompileCheckTool.cjs +1 -1
  69. package/dist/cjs/tools/local/LocalCodingTools.cjs +1 -1
  70. package/dist/cjs/utils/events.cjs +13 -0
  71. package/dist/cjs/utils/events.cjs.map +1 -1
  72. package/dist/cjs/utils/tokens.cjs +105 -0
  73. package/dist/cjs/utils/tokens.cjs.map +1 -1
  74. package/dist/esm/agents/AgentContext.mjs +13 -3
  75. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  76. package/dist/esm/graphs/Graph.mjs +176 -55
  77. package/dist/esm/graphs/Graph.mjs.map +1 -1
  78. package/dist/esm/graphs/MultiAgentGraph.mjs +15 -5
  79. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  80. package/dist/esm/langfuse.mjs +43 -2
  81. package/dist/esm/langfuse.mjs.map +1 -1
  82. package/dist/esm/langfuseToolOutputTracing.mjs +41 -1
  83. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  84. package/dist/esm/langfuseTraceShaping.mjs +7 -1
  85. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  86. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +5 -9
  87. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  88. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +2 -7
  89. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  90. package/dist/esm/llm/contextPressureMeter.mjs +52 -11
  91. package/dist/esm/llm/contextPressureMeter.mjs.map +1 -1
  92. package/dist/esm/llm/init.mjs +1 -1
  93. package/dist/esm/llm/invoke.mjs +2 -2
  94. package/dist/esm/llm/openai/index.mjs +2 -1
  95. package/dist/esm/llm/openai/index.mjs.map +1 -1
  96. package/dist/esm/llm/openai/utils/index.mjs +5 -4
  97. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  98. package/dist/esm/llm/preempt.mjs +2 -1
  99. package/dist/esm/llm/preempt.mjs.map +1 -1
  100. package/dist/esm/llm/prepareProviderRequest.mjs +9 -7
  101. package/dist/esm/llm/prepareProviderRequest.mjs.map +1 -1
  102. package/dist/esm/llm/providers.mjs +1 -1
  103. package/dist/esm/main.mjs +10 -7
  104. package/dist/esm/messages/alternation.mjs +2 -6
  105. package/dist/esm/messages/alternation.mjs.map +1 -1
  106. package/dist/esm/messages/budget.mjs +206 -8
  107. package/dist/esm/messages/budget.mjs.map +1 -1
  108. package/dist/esm/messages/cache.mjs +3 -9
  109. package/dist/esm/messages/cache.mjs.map +1 -1
  110. package/dist/esm/messages/core.mjs +23 -33
  111. package/dist/esm/messages/core.mjs.map +1 -1
  112. package/dist/esm/messages/format.mjs +132 -43
  113. package/dist/esm/messages/format.mjs.map +1 -1
  114. package/dist/esm/messages/index.mjs +2 -0
  115. package/dist/esm/messages/prune.mjs +12 -7
  116. package/dist/esm/messages/prune.mjs.map +1 -1
  117. package/dist/esm/messages/reasoningTypes.mjs +20 -0
  118. package/dist/esm/messages/reasoningTypes.mjs.map +1 -0
  119. package/dist/esm/messages/recency.mjs +16 -95
  120. package/dist/esm/messages/recency.mjs.map +1 -1
  121. package/dist/esm/messages/toolHistoryProjection.mjs +237 -0
  122. package/dist/esm/messages/toolHistoryProjection.mjs.map +1 -0
  123. package/dist/esm/messages/toolResultTypes.mjs +141 -4
  124. package/dist/esm/messages/toolResultTypes.mjs.map +1 -1
  125. package/dist/esm/run.mjs +5 -4
  126. package/dist/esm/run.mjs.map +1 -1
  127. package/dist/esm/session/AgentSession.mjs +6 -4
  128. package/dist/esm/session/AgentSession.mjs.map +1 -1
  129. package/dist/esm/session/JsonlSessionStore.mjs +3 -0
  130. package/dist/esm/session/JsonlSessionStore.mjs.map +1 -1
  131. package/dist/esm/session/index.mjs +1 -1
  132. package/dist/esm/session/sessionProjection.mjs +72 -0
  133. package/dist/esm/session/sessionProjection.mjs.map +1 -0
  134. package/dist/esm/stream.mjs +15 -14
  135. package/dist/esm/stream.mjs.map +1 -1
  136. package/dist/esm/summarization/index.mjs.map +1 -1
  137. package/dist/esm/summarization/node.mjs +19 -9
  138. package/dist/esm/summarization/node.mjs.map +1 -1
  139. package/dist/esm/summarization/shared.mjs +9 -1
  140. package/dist/esm/summarization/shared.mjs.map +1 -1
  141. package/dist/esm/tools/local/CompileCheckTool.mjs +1 -1
  142. package/dist/esm/tools/local/LocalCodingTools.mjs +1 -1
  143. package/dist/esm/utils/events.mjs +13 -0
  144. package/dist/esm/utils/events.mjs.map +1 -1
  145. package/dist/esm/utils/tokens.mjs +105 -1
  146. package/dist/esm/utils/tokens.mjs.map +1 -1
  147. package/dist/types/agents/AgentContext.d.ts +13 -1
  148. package/dist/types/graphs/Graph.d.ts +27 -0
  149. package/dist/types/hooks/types.d.ts +4 -2
  150. package/dist/types/index.d.ts +1 -0
  151. package/dist/types/langfuse.d.ts +5 -0
  152. package/dist/types/langfuseToolOutputTracing.d.ts +2 -0
  153. package/dist/types/llm/contextPressureMeter.d.ts +2 -1
  154. package/dist/types/llm/prepareProviderRequest.d.ts +7 -1
  155. package/dist/types/messages/budget.d.ts +15 -10
  156. package/dist/types/messages/core.d.ts +8 -17
  157. package/dist/types/messages/format.d.ts +3 -2
  158. package/dist/types/messages/index.d.ts +1 -1
  159. package/dist/types/messages/prune.d.ts +1 -1
  160. package/dist/types/messages/reasoningTypes.d.ts +7 -0
  161. package/dist/types/messages/recency.d.ts +3 -0
  162. package/dist/types/messages/toolHistoryProjection.d.ts +65 -0
  163. package/dist/types/messages/toolResultTypes.d.ts +31 -1
  164. package/dist/types/session/sessionProjection.d.ts +10 -0
  165. package/dist/types/summarization/index.d.ts +2 -1
  166. package/dist/types/summarization/node.d.ts +1 -0
  167. package/dist/types/summarization/shared.d.ts +12 -0
  168. package/dist/types/types/graph.d.ts +20 -0
  169. package/dist/types/types/run.d.ts +4 -0
  170. package/dist/types/types/summarize.d.ts +4 -1
  171. package/dist/types/utils/tokens.d.ts +2 -1
  172. package/package.json +1 -1
  173. package/src/agents/AgentContext.ts +34 -3
  174. package/src/graphs/Graph.ts +465 -147
  175. package/src/graphs/MultiAgentGraph.ts +28 -6
  176. package/src/hooks/types.ts +4 -1
  177. package/src/index.ts +1 -0
  178. package/src/langfuse.ts +67 -9
  179. package/src/langfuseToolOutputTracing.ts +91 -0
  180. package/src/langfuseTraceShaping.ts +17 -4
  181. package/src/llm/anthropic/utils/message_inputs.ts +5 -10
  182. package/src/llm/bedrock/utils/message_inputs.ts +2 -8
  183. package/src/llm/contextPressureMeter.ts +80 -12
  184. package/src/llm/openai/utils/index.ts +5 -4
  185. package/src/llm/prepareProviderRequest.ts +21 -16
  186. package/src/messages/alternation.ts +2 -15
  187. package/src/messages/budget.ts +439 -23
  188. package/src/messages/cache.ts +8 -9
  189. package/src/messages/core.ts +65 -95
  190. package/src/messages/format.ts +262 -78
  191. package/src/messages/index.ts +1 -1
  192. package/src/messages/prune.ts +31 -12
  193. package/src/messages/reasoningTypes.ts +23 -0
  194. package/src/messages/recency.ts +35 -179
  195. package/src/messages/toolHistoryProjection.ts +460 -0
  196. package/src/messages/toolResultTypes.ts +331 -100
  197. package/src/run.ts +10 -2
  198. package/src/session/AgentSession.ts +33 -16
  199. package/src/session/JsonlSessionStore.ts +6 -0
  200. package/src/session/sessionProjection.ts +126 -0
  201. package/src/stream.ts +14 -19
  202. package/src/summarization/index.ts +2 -0
  203. package/src/summarization/node.ts +62 -13
  204. package/src/summarization/shared.ts +23 -0
  205. package/src/types/graph.ts +20 -0
  206. package/src/types/run.ts +4 -0
  207. package/src/types/summarize.ts +4 -1
  208. package/src/utils/events.ts +19 -0
  209. package/src/utils/tokens.ts +183 -0
@@ -21,9 +21,12 @@ import type {
21
21
  SessionEntry,
22
22
  SessionForkOptions,
23
23
  } from './types';
24
- import { isFadingTier } from '@/messages/fading';
25
24
  import type { HookRegistry } from '@/hooks';
26
25
  import type * as t from '@/types';
26
+ import {
27
+ deriveSessionMessages,
28
+ releaseSessionProjection,
29
+ } from './sessionProjection';
27
30
  import { createSummarizeNode } from '@/summarization/node';
28
31
  import { resolveStreamLimits } from '@/llm/streamLimits';
29
32
  import { JsonlSessionStore } from './JsonlSessionStore';
@@ -31,6 +34,7 @@ import { AgentContext } from '@/agents/AgentContext';
31
34
  import { ContentTypes, GraphEvents } from '@/common';
32
35
  import { createRunId, createSessionId } from './ids';
33
36
  import { deriveMessages } from './deriveMessages';
37
+ import { isFadingTier } from '@/messages/fading';
34
38
  import { createRunHandlers } from './handlers';
35
39
  import { Run } from '@/run';
36
40
 
@@ -377,7 +381,9 @@ function fadingGenerationKey(scopeKey: string, agentKey: string): string {
377
381
 
378
382
  function fadingGenerationScope(generationKey: string): string {
379
383
  const parsed: unknown = JSON.parse(generationKey);
380
- return Array.isArray(parsed) && typeof parsed[0] === 'string' ? parsed[0] : '';
384
+ return Array.isArray(parsed) && typeof parsed[0] === 'string'
385
+ ? parsed[0]
386
+ : '';
381
387
  }
382
388
 
383
389
  /** What a run saw when it started, so its capture can tell stale from current. */
@@ -404,10 +410,7 @@ function mergeFadingTier(
404
410
  if (incoming == null) {
405
411
  return { ...current };
406
412
  }
407
- const budgetTokens = Math.min(
408
- current.budgetTokens,
409
- incoming.budgetTokens
410
- );
413
+ const budgetTokens = Math.min(current.budgetTokens, incoming.budgetTokens);
411
414
  const masked = current.masked || incoming.masked;
412
415
  const latched =
413
416
  current.latched === true ||
@@ -1010,6 +1013,9 @@ export class AgentSession {
1010
1013
  }
1011
1014
 
1012
1015
  getSessionStore(): JsonlSessionStore | undefined {
1016
+ if (this.store) {
1017
+ releaseSessionProjection(this.store);
1018
+ }
1013
1019
  return this.store;
1014
1020
  }
1015
1021
 
@@ -1017,7 +1023,10 @@ export class AgentSession {
1017
1023
  return this.checkpointing.checkpointer;
1018
1024
  }
1019
1025
 
1020
- private getFadingState(threadId: string, checkpointNs = ''): {
1026
+ private getFadingState(
1027
+ threadId: string,
1028
+ checkpointNs = ''
1029
+ ): {
1021
1030
  fadingTier?: t.FadingTier;
1022
1031
  fadingTiers?: t.FadingTiers;
1023
1032
  } {
@@ -1058,8 +1067,7 @@ export class AgentSession {
1058
1067
 
1059
1068
  private trimAlternateFadingStates(): void {
1060
1069
  while (
1061
- this.alternateThreadFadingState.size >
1062
- MAX_ALTERNATE_THREAD_FADING_STATES
1070
+ this.alternateThreadFadingState.size > MAX_ALTERNATE_THREAD_FADING_STATES
1063
1071
  ) {
1064
1072
  const oldestThreadId = this.alternateThreadFadingState.keys().next();
1065
1073
  if (oldestThreadId.done === true) {
@@ -1172,7 +1180,10 @@ export class AgentSession {
1172
1180
  };
1173
1181
  const advanceGeneration = (agentKey: string): void => {
1174
1182
  const key = fadingGenerationKey(scopeKey, agentKey);
1175
- this.fadingGenerations.set(key, (this.fadingGenerations.get(key) ?? 0) + 1);
1183
+ this.fadingGenerations.set(
1184
+ key,
1185
+ (this.fadingGenerations.get(key) ?? 0) + 1
1186
+ );
1176
1187
  };
1177
1188
  const mergeCapturedTier = (
1178
1189
  existing: t.FadingTier | undefined,
@@ -1218,7 +1229,11 @@ export class AgentSession {
1218
1229
  if (didResetTier) {
1219
1230
  advanceGeneration('');
1220
1231
  }
1221
- nextTier = mergeCapturedTier(current.fadingTier, incomingTier, didResetTier);
1232
+ nextTier = mergeCapturedTier(
1233
+ current.fadingTier,
1234
+ incomingTier,
1235
+ didResetTier
1236
+ );
1222
1237
  }
1223
1238
  if (threadId === this.threadId && checkpointNs === '') {
1224
1239
  this.fadingTier = nextTier;
@@ -1408,8 +1423,8 @@ export class AgentSession {
1408
1423
  handlerResult.events.push(streamEvent);
1409
1424
  onEvent?.(streamEvent);
1410
1425
  };
1411
- const sessionState = deriveMessages(
1412
- isSessionThread ? (this.store?.getPath() ?? []) : []
1426
+ const sessionState = deriveSessionMessages(
1427
+ isSessionThread ? this.store : undefined
1413
1428
  );
1414
1429
  const fadingState = this.getFadingState(threadId, checkpointNs);
1415
1430
  let run: Run<t.IState> | undefined;
@@ -1690,7 +1705,9 @@ export class AgentSession {
1690
1705
  throw new Error('Cannot compact an ephemeral session');
1691
1706
  }
1692
1707
  const activePath = path ?? store.getPath();
1693
- const sessionState = deriveMessages(activePath);
1708
+ const sessionState = path
1709
+ ? deriveMessages(path)
1710
+ : deriveSessionMessages(store);
1694
1711
  const messageEntries = activePath.filter(isMessageEntry);
1695
1712
  if (sessionState.messages.length === 0) {
1696
1713
  return undefined;
@@ -1819,8 +1836,8 @@ export class AgentSession {
1819
1836
  threadId,
1820
1837
  userHandlers: this.runConfig.customHandlers,
1821
1838
  });
1822
- const sessionState = deriveMessages(
1823
- isSessionThread ? (this.store?.getPath() ?? []) : []
1839
+ const sessionState = deriveSessionMessages(
1840
+ isSessionThread ? this.store : undefined
1824
1841
  );
1825
1842
  const fadingState = this.getFadingState(threadId, checkpointNs);
1826
1843
  let run: Run<t.IState> | undefined;
@@ -35,6 +35,10 @@ import {
35
35
  serializeMessage,
36
36
  toJsonValue,
37
37
  } from './messageSerialization';
38
+ import {
39
+ initializeSessionProjection,
40
+ shareSessionProjectionOwnership,
41
+ } from './sessionProjection';
38
42
  import { createEntryId, createSessionId, createTimestamp } from './ids';
39
43
 
40
44
  const SESSION_VERSION = 1;
@@ -126,6 +130,7 @@ export class JsonlSessionStore {
126
130
  this.path = params.path;
127
131
  this.header = params.header;
128
132
  this.entries = sortEntries(params.entries);
133
+ initializeSessionProjection(this, this.entries);
129
134
  }
130
135
 
131
136
  static getDefaultRoot(): string {
@@ -573,6 +578,7 @@ export class JsonlSessionStore {
573
578
  name: options.name ?? this.header.name,
574
579
  parentSession: this.path,
575
580
  });
581
+ shareSessionProjectionOwnership(this, newStore);
576
582
  const pathEntries = target ? this.getPath(target.id) : [];
577
583
  for (const entry of pathEntries) {
578
584
  await newStore.appendExistingEntry(entry);
@@ -0,0 +1,126 @@
1
+ import type { DerivedSessionMessages } from './deriveMessages';
2
+ import type { SessionEntry, SessionStateEntry } from './types';
3
+ import type { JsonlSessionStore } from './JsonlSessionStore';
4
+ import { deriveMessages } from './deriveMessages';
5
+
6
+ interface SessionProjection {
7
+ entries: readonly SessionEntry[];
8
+ ownership: { exposed: boolean };
9
+ indexedCount: number;
10
+ byId: Map<string, SessionEntry>;
11
+ lastState?: SessionStateEntry;
12
+ lastMessage?: SessionEntry;
13
+ leaf?: SessionEntry;
14
+ projected: SessionEntry[];
15
+ }
16
+
17
+ const projections = new WeakMap<JsonlSessionStore, SessionProjection>();
18
+
19
+ /** Internal: only AgentSession's unexposed stores may reuse log topology. */
20
+ export function initializeSessionProjection(
21
+ store: JsonlSessionStore,
22
+ entries: readonly SessionEntry[]
23
+ ): void {
24
+ projections.set(store, {
25
+ entries,
26
+ ownership: { exposed: false },
27
+ indexedCount: 0,
28
+ byId: new Map(),
29
+ projected: [],
30
+ });
31
+ }
32
+
33
+ /** Forks share entry references, so exposing either disables the whole family. */
34
+ export function shareSessionProjectionOwnership(
35
+ source: JsonlSessionStore,
36
+ target: JsonlSessionStore
37
+ ): void {
38
+ const sourceProjection = projections.get(source);
39
+ const targetProjection = projections.get(target);
40
+ if (sourceProjection && targetProjection) {
41
+ targetProjection.ownership = sourceProjection.ownership;
42
+ }
43
+ }
44
+
45
+ export function releaseSessionProjection(store: JsonlSessionStore): void {
46
+ const projection = projections.get(store);
47
+ if (projection) {
48
+ projection.ownership.exposed = true;
49
+ projection.byId.clear();
50
+ projection.projected = [];
51
+ projection.leaf = undefined;
52
+ }
53
+ }
54
+
55
+ function indexAppends(projection: SessionProjection): boolean {
56
+ for (
57
+ ;
58
+ projection.indexedCount < projection.entries.length;
59
+ projection.indexedCount++
60
+ ) {
61
+ const entry = projection.entries[projection.indexedCount];
62
+ if (projection.byId.has(entry.id)) {
63
+ return false;
64
+ }
65
+ projection.byId.set(entry.id, entry);
66
+ if (entry.type === 'session_state') {
67
+ projection.lastState = entry;
68
+ } else if (entry.type === 'message' || entry.type === 'summary') {
69
+ projection.lastMessage = entry;
70
+ }
71
+ }
72
+ return true;
73
+ }
74
+
75
+ function projectLeaf(projection: SessionProjection): void {
76
+ const leafId = projection.lastState
77
+ ? projection.lastState.data.leafId
78
+ : projection.lastMessage?.id;
79
+ const leaf =
80
+ leafId != null && leafId !== '' ? projection.byId.get(leafId) : undefined;
81
+ if (leaf === projection.leaf) {
82
+ return;
83
+ }
84
+ const delta: SessionEntry[] = [];
85
+ let current = leaf;
86
+ while (current && current !== projection.leaf) {
87
+ if (current.type === 'message' || current.type === 'summary') {
88
+ delta.push(current);
89
+ }
90
+ if (current.type === 'summary') {
91
+ current = undefined;
92
+ break;
93
+ }
94
+ current =
95
+ current.parentId == null
96
+ ? undefined
97
+ : projection.byId.get(current.parentId);
98
+ }
99
+ if (!current) {
100
+ projection.projected = [];
101
+ }
102
+ for (let i = delta.length - 1; i >= 0; i--) {
103
+ projection.projected.push(delta[i]);
104
+ }
105
+ projection.leaf = leaf;
106
+ }
107
+
108
+ /** Reuses log topology, but materializes fresh mutable messages for every run. */
109
+ export function deriveSessionMessages(
110
+ store: JsonlSessionStore | undefined
111
+ ): DerivedSessionMessages {
112
+ if (!store) {
113
+ return { messages: [] };
114
+ }
115
+ const projection = projections.get(store);
116
+ if (!projection || projection.ownership.exposed) {
117
+ releaseSessionProjection(store);
118
+ return deriveMessages(store.getPath());
119
+ }
120
+ if (!indexAppends(projection)) {
121
+ releaseSessionProjection(store);
122
+ return deriveMessages(store.getPath());
123
+ }
124
+ projectLeaf(projection);
125
+ return deriveMessages(projection.projected);
126
+ }
package/src/stream.ts CHANGED
@@ -1014,18 +1014,6 @@ function pruneEagerToolCallChunkStates(args: {
1014
1014
  }
1015
1015
  }
1016
1016
 
1017
- function isEagerToolChunkStateComplete(
1018
- state: t.EagerEventToolCallChunkState
1019
- ): boolean {
1020
- return (
1021
- state.id != null &&
1022
- state.id !== '' &&
1023
- state.name != null &&
1024
- state.name !== '' &&
1025
- coerceRecordArgs(state.argsText) != null
1026
- );
1027
- }
1028
-
1029
1017
  function mergeToolCallArgsText(existing: string, incoming: string): string {
1030
1018
  if (incoming === '') {
1031
1019
  return existing;
@@ -1180,6 +1168,7 @@ function getStreamedReadyToolCalls(args: {
1180
1168
  key: string;
1181
1169
  state: t.EagerEventToolCallChunkState;
1182
1170
  sealedByAdapter: boolean;
1171
+ parsedArgs: ToolCall['args'];
1183
1172
  }> = [];
1184
1173
 
1185
1174
  for (const [key, state] of graph.eagerEventToolCallChunks) {
@@ -1190,7 +1179,12 @@ function getStreamedReadyToolCalls(args: {
1190
1179
  graph.eagerEventToolCallChunks.delete(key);
1191
1180
  continue;
1192
1181
  }
1193
- if (!isEagerToolChunkStateComplete(state)) {
1182
+ if (
1183
+ state.id == null ||
1184
+ state.id === '' ||
1185
+ state.name == null ||
1186
+ state.name === ''
1187
+ ) {
1194
1188
  continue;
1195
1189
  }
1196
1190
  const isSealedByLaterChunk =
@@ -1209,10 +1203,15 @@ function getStreamedReadyToolCalls(args: {
1209
1203
  isSealedByLaterChunk ||
1210
1204
  isSealedExplicitly
1211
1205
  ) {
1206
+ const parsedArgs = coerceRecordArgs(state.argsText);
1207
+ if (parsedArgs == null) {
1208
+ continue;
1209
+ }
1212
1210
  readyEntries.push({
1213
1211
  key,
1214
1212
  state,
1215
1213
  sealedByAdapter: isSealedExplicitly || seal?.kind === 'all',
1214
+ parsedArgs,
1216
1215
  });
1217
1216
  }
1218
1217
  }
@@ -1232,11 +1231,7 @@ function getStreamedReadyToolCalls(args: {
1232
1231
 
1233
1232
  return readyEntries
1234
1233
  .sort((left, right) => (left.state.index ?? 0) - (right.state.index ?? 0))
1235
- .flatMap(({ state, sealedByAdapter }) => {
1236
- const args = coerceRecordArgs(state.argsText);
1237
- if (args == null) {
1238
- return [];
1239
- }
1234
+ .flatMap(({ state, sealedByAdapter, parsedArgs }) => {
1240
1235
  // The final request's args come from LangChain's canonical verbatim
1241
1236
  // concatenation of fragments, while `argsText` reconciles provider
1242
1237
  // quirks with lossy heuristics that can also swallow legitimately
@@ -1267,7 +1262,7 @@ function getStreamedReadyToolCalls(args: {
1267
1262
  {
1268
1263
  id: state.id,
1269
1264
  name: state.name ?? '',
1270
- args,
1265
+ args: parsedArgs,
1271
1266
  },
1272
1267
  ];
1273
1268
  });
@@ -9,7 +9,9 @@ export {
9
9
  buildSummarizationInstruction,
10
10
  buildSummaryCarrierText,
11
11
  separateSummarizationParameters,
12
+ ManualSummarizationSkippedError,
12
13
  } from './shared';
14
+ export type { ManualSummarizationSkipReason } from './shared';
13
15
 
14
16
  const VALID_TRIGGER_TYPES = [
15
17
  'token_ratio',
@@ -14,6 +14,7 @@ import {
14
14
  buildSummaryCarrierText,
15
15
  separateSummarizationParameters,
16
16
  buildSummarizationInstruction,
17
+ ManualSummarizationSkippedError,
17
18
  } from './shared';
18
19
  import {
19
20
  addTailCacheControl,
@@ -1026,6 +1027,14 @@ function findStreamLimitAbortReason(
1026
1027
  return undefined;
1027
1028
  }
1028
1029
 
1030
+ /** Wording for the history-preserved log and completion, by request reason. */
1031
+ const PRESERVATION_REASONS: Partial<
1032
+ Record<NonNullable<t.SummarizationNodeInput['reason']>, string>
1033
+ > = {
1034
+ overflow: 'overflow recovery',
1035
+ manual: 'manual compaction',
1036
+ };
1037
+
1029
1038
  export function createSummarizeNode({
1030
1039
  agentContext,
1031
1040
  graph: adapterGraph,
@@ -1037,7 +1046,11 @@ export function createSummarizeNode({
1037
1046
  summarizationRequest?: t.SummarizationNodeInput;
1038
1047
  },
1039
1048
  config?: RunnableConfig
1040
- ): Promise<{ summarizationRequest: undefined; messages?: BaseMessage[] }> => {
1049
+ ): Promise<{
1050
+ summarizationRequest: undefined;
1051
+ messages?: BaseMessage[];
1052
+ manualSummary?: string;
1053
+ }> => {
1041
1054
  /** The breaker signal is captured at node ENTRY, before dispatchRunStep,
1042
1055
  * ON_SUMMARIZE_START, or PreCompact awaits: a sibling's trip plus a
1043
1056
  * prompt next run can reset the controller during one of those awaits,
@@ -1114,6 +1127,12 @@ export function createSummarizeNode({
1114
1127
  * its recursion budget dispatching empty summary steps.
1115
1128
  */
1116
1129
  if (agentContext.summarizationExhausted) {
1130
+ if (request.reason === 'manual') {
1131
+ throw new ManualSummarizationSkippedError(
1132
+ 'exhausted',
1133
+ 'Compaction skipped: consecutive summarization attempts produced no usable summary'
1134
+ );
1135
+ }
1117
1136
  emitAgentLog(
1118
1137
  config,
1119
1138
  'warn',
@@ -1131,6 +1150,12 @@ export function createSummarizeNode({
1131
1150
 
1132
1151
  const maxCtx = agentContext.maxContextTokens ?? 0;
1133
1152
  if (maxCtx > 0 && agentContext.instructionTokens >= maxCtx) {
1153
+ if (request.reason === 'manual') {
1154
+ throw new ManualSummarizationSkippedError(
1155
+ 'instructions_exceed_budget',
1156
+ 'Compaction skipped: instructions and tool definitions exceed the context budget'
1157
+ );
1158
+ }
1134
1159
  emitAgentLog(
1135
1160
  config,
1136
1161
  'warn',
@@ -1167,6 +1192,16 @@ export function createSummarizeNode({
1167
1192
  const runnableConfig = config ?? graph.config;
1168
1193
 
1169
1194
  const retainRecent = agentContext.summarizationConfig?.retainRecent;
1195
+ /**
1196
+ * A manual summary is the user asking for the whole history to become a
1197
+ * checkpoint, so the default recency window does not apply to it. A
1198
+ * `retainRecent` the host configured explicitly still keeps its tail,
1199
+ * with the ordinary turn default when it only sets a token cap.
1200
+ */
1201
+ const retainTurns =
1202
+ request.reason === 'manual' && retainRecent == null
1203
+ ? 0
1204
+ : (retainRecent?.turns ?? DEFAULT_RETAIN_RECENT_TURNS);
1170
1205
  const recencyTokenCounter =
1171
1206
  agentContext.contextPressureTokenCounts?.count ??
1172
1207
  agentContext.tokenCounter;
@@ -1175,7 +1210,8 @@ export function createSummarizeNode({
1175
1210
  tailStartIndex,
1176
1211
  usedIntraTurnFallback,
1177
1212
  } = splitAtRecencyBoundary(restoredMessages, {
1178
- turns: retainRecent?.turns ?? DEFAULT_RETAIN_RECENT_TURNS,
1213
+ provider: agentContext.provider,
1214
+ turns: retainTurns,
1179
1215
  tokens: retainRecent?.tokens,
1180
1216
  tokenCounter: recencyTokenCounter,
1181
1217
  intraTurnTokens: resolveIntraTurnRetainTokens({
@@ -1198,6 +1234,12 @@ export function createSummarizeNode({
1198
1234
  const messagesToRetain = state.messages.slice(tailStartIndex);
1199
1235
 
1200
1236
  if (messagesToRefine.length === 0) {
1237
+ if (request.reason === 'manual') {
1238
+ throw new ManualSummarizationSkippedError(
1239
+ 'nothing_to_summarize',
1240
+ 'Nothing to compact: the retained recent turns cover the whole conversation'
1241
+ );
1242
+ }
1201
1243
  /**
1202
1244
  * Recency window covers the entire conversation — there is no
1203
1245
  * older content to summarize. Skipping prevents the model from
@@ -1213,7 +1255,7 @@ export function createSummarizeNode({
1213
1255
  'Summarization skipped — recency window retains all messages',
1214
1256
  {
1215
1257
  messagesRetained: messagesToRetain.length,
1216
- retainTurns: retainRecent?.turns ?? DEFAULT_RETAIN_RECENT_TURNS,
1258
+ retainTurns,
1217
1259
  },
1218
1260
  { runId: graph.runId, agentId: request.agentId }
1219
1261
  );
@@ -1290,7 +1332,10 @@ export function createSummarizeNode({
1290
1332
  threadId,
1291
1333
  agentId: request.agentId,
1292
1334
  messagesBeforeCount: messagesToRefine.length,
1293
- trigger: agentContext.summarizationConfig?.trigger?.type ?? 'default',
1335
+ trigger:
1336
+ request.reason === 'manual'
1337
+ ? 'manual'
1338
+ : (agentContext.summarizationConfig?.trigger?.type ?? 'default'),
1294
1339
  },
1295
1340
  sessionId,
1296
1341
  }).catch(() => {
@@ -1340,7 +1385,7 @@ export function createSummarizeNode({
1340
1385
  compaction_semantic_index_entries: semanticIndex.entryCount,
1341
1386
  compaction_semantic_index_chars: semanticIndex.charCount,
1342
1387
  compaction_semantic_index_omitted_entries:
1343
- semanticIndex.omittedEntryCount,
1388
+ semanticIndex.omittedEntryCount,
1344
1389
  /**
1345
1390
  * Per-call model attribution for usage consumers (the subagent
1346
1391
  * usage-capture handler): the summarizer's model can differ from
@@ -1398,19 +1443,21 @@ export function createSummarizeNode({
1398
1443
  /**
1399
1444
  * The metadata stub describes the history rather than summarizing it, so
1400
1445
  * committing it means removing the head and keeping nothing of what it
1401
- * said. That trade is never worth making to paper over an overflow: the
1402
- * recovery would "succeed" only by destroying the conversation it was
1403
- * supposed to preserve. Leave state untouched and let the provider error
1404
- * surface instead.
1446
+ * said. That trade is never worth making to paper over an overflow, and
1447
+ * never for a manual compaction, which would replace the whole history
1448
+ * with a message count: the recovery would "succeed" only by destroying
1449
+ * the conversation it was supposed to preserve. Leave state untouched
1450
+ * and let the provider error surface instead.
1405
1451
  */
1406
1452
  if (
1407
1453
  usedMetadataStub === true &&
1408
- (request.reason === 'overflow' || usedIntraTurnFallback)
1454
+ (request.reason === 'overflow' ||
1455
+ request.reason === 'manual' ||
1456
+ usedIntraTurnFallback)
1409
1457
  ) {
1410
1458
  const preservationReason =
1411
- request.reason === 'overflow'
1412
- ? 'overflow recovery'
1413
- : 'intra-turn compaction';
1459
+ PRESERVATION_REASONS[request.reason ?? 'trigger'] ??
1460
+ 'intra-turn compaction';
1414
1461
  log(
1415
1462
  'warn',
1416
1463
  `Summarization failed during ${preservationReason}; keeping history rather than replacing it with a metadata stub`
@@ -1570,6 +1617,8 @@ export function createSummarizeNode({
1570
1617
  messagesToRetain.length > 0
1571
1618
  ? [createRemoveAllMessage(), ...messagesToRetain]
1572
1619
  : [createRemoveAllMessage()],
1620
+ /** The run's result, for trace roots that otherwise see no reply. */
1621
+ ...(request.reason === 'manual' ? { manualSummary: summaryText } : {}),
1573
1622
  };
1574
1623
  };
1575
1624
  }
@@ -128,3 +128,26 @@ export function buildSummarizationInstruction(
128
128
  }
129
129
  return parts.join('');
130
130
  }
131
+
132
+ /** Why a summarize-only run could not attempt its summary. */
133
+ export type ManualSummarizationSkipReason =
134
+ | 'disabled'
135
+ | 'exhausted'
136
+ | 'instructions_exceed_budget'
137
+ | 'nothing_to_summarize';
138
+
139
+ /**
140
+ * A summarize-only run asked for a summary the graph could not attempt.
141
+ * Thrown rather than ending the run quietly: the summary is the whole result
142
+ * of such a run, so a host left with neither a summary nor an error would
143
+ * have nothing to show the user.
144
+ */
145
+ export class ManualSummarizationSkippedError extends Error {
146
+ readonly reason: ManualSummarizationSkipReason;
147
+
148
+ constructor(reason: ManualSummarizationSkipReason, message: string) {
149
+ super(message);
150
+ this.name = 'ManualSummarizationSkippedError';
151
+ this.reason = reason;
152
+ }
153
+ }
@@ -81,6 +81,13 @@ export type SystemCallbacks = {
81
81
  export type BaseGraphState = {
82
82
  messages: BaseMessage[];
83
83
  runStepState?: RunStepResumeState;
84
+ /**
85
+ * The summary a summarize-only run produced. Kept in state because such a
86
+ * run has no assistant reply: trace roots report it as the run's output
87
+ * instead of the retained tail or the raw state. Empty once any later run
88
+ * on the same checkpointed thread starts, so it never outlives its run.
89
+ */
90
+ manualSummary?: string;
84
91
  };
85
92
 
86
93
  export type AgentSubgraphState = BaseGraphState & {
@@ -906,6 +913,19 @@ interface AgentInputFields {
906
913
  */
907
914
  discoveredTools?: string[];
908
915
  summarizationEnabled?: boolean;
916
+ /**
917
+ * Runs this agent as a summarize-only pass: the first model step requests
918
+ * summarization outright instead of consulting the trigger, and the step
919
+ * after the summary emits its context snapshot and ends the run without a
920
+ * model call. Hosts use it for user-initiated compaction, where the summary
921
+ * is the whole response. Requires `summarizationEnabled`: a run that cannot
922
+ * attempt the summary rejects with `ManualSummarizationSkippedError`
923
+ * rather than ending with nothing, and one whose provider calls all fail
924
+ * keeps the history and reports the failure on the summary step. A
925
+ * multi-agent workflow compiles to the agent that opted in alone; no other
926
+ * agent runs.
927
+ */
928
+ summarizeOnly?: boolean;
909
929
  summarizationConfig?: SummarizationConfig;
910
930
  /**
911
931
  * Optional host-supplied, user-visible guidance for compaction. The SDK
package/src/types/run.ts CHANGED
@@ -475,6 +475,10 @@ export type TokenBudgetBreakdown = {
475
475
  toolTokenCounts?: Record<string, number>;
476
476
  /** Names of counted tools that are deferred (`defer_loading`) and discovered. */
477
477
  deferredToolNames?: string[];
478
+ /** Calibrated retained tool traffic: tool results plus assistant turns made only of tool calls, inline provider tool results and reasoning. A subset of messageTokens, not an additional budget category. */
479
+ toolMessageTokens?: number;
480
+ /** Per-tool result-message share; excludes assistant invocation overhead. */
481
+ toolMessageTokenCounts?: Record<string, number>;
478
482
  };
479
483
 
480
484
  export type EventStreamOptions = {
@@ -127,8 +127,11 @@ export interface SummarizationNodeInput {
127
127
  * is compacting to recover. When summarization is not enabled, this
128
128
  * variant performs no model call — the corrected budget alone is what the
129
129
  * retry needs.
130
+ * - `manual`: the host asked for a summary outright (a summarize-only run).
131
+ * The recency window is not applied unless the host configured
132
+ * `retainRecent` explicitly, so the summary replaces the whole history.
130
133
  */
131
- reason?: 'trigger' | 'overflow';
134
+ reason?: 'trigger' | 'overflow' | 'manual';
132
135
  /**
133
136
  * Whether an overflow recovery may spend a summarization model call.
134
137
  *
@@ -2,7 +2,9 @@
2
2
  // src/utils/events.ts
3
3
  import { dispatchCustomEvent } from '@langchain/core/callbacks/dispatch';
4
4
  import type { RunnableConfig } from '@langchain/core/runnables';
5
+ import type { ToolExecuteBatchRequest } from '@/types/tools';
5
6
  import type { AgentLogEvent } from '@/types/graph';
7
+ import { traceHostToolResults } from '@/langfuse';
6
8
  import { GraphEvents } from '@/common';
7
9
 
8
10
  /**
@@ -15,6 +17,23 @@ export async function safeDispatchCustomEvent(
15
17
  config?: RunnableConfig
16
18
  ): Promise<boolean | void> {
17
19
  try {
20
+ if (event === GraphEvents.ON_TOOL_EXECUTE) {
21
+ const request = payload as ToolExecuteBatchRequest;
22
+ payload = {
23
+ ...request,
24
+ resolve: (
25
+ results: Parameters<ToolExecuteBatchRequest['resolve']>[0]
26
+ ): void => {
27
+ void traceHostToolResults(request, results, config).then(
28
+ () => request.resolve(results),
29
+ () => {
30
+ console.warn('Failed to record host tool execution metadata');
31
+ request.resolve(results);
32
+ }
33
+ );
34
+ },
35
+ } satisfies ToolExecuteBatchRequest;
36
+ }
18
37
  await dispatchCustomEvent(event, payload, config);
19
38
  return true;
20
39
  } catch (e) {