@librechat/agents 3.7.7 → 3.7.9

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 (167) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +26 -5
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/common/constants.cjs +12 -0
  4. package/dist/cjs/common/constants.cjs.map +1 -1
  5. package/dist/cjs/graphs/Graph.cjs +56 -11
  6. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  7. package/dist/cjs/graphs/MultiAgentGraph.cjs +1 -1
  8. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  9. package/dist/cjs/hooks/executeHooks.cjs +8 -0
  10. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  11. package/dist/cjs/hooks/index.cjs +2 -0
  12. package/dist/cjs/hooks/index.cjs.map +1 -1
  13. package/dist/cjs/hooks/types.cjs +1 -0
  14. package/dist/cjs/hooks/types.cjs.map +1 -1
  15. package/dist/cjs/langfuse.cjs +43 -3
  16. package/dist/cjs/langfuse.cjs.map +1 -1
  17. package/dist/cjs/langfuseTraceShaping.cjs +75 -0
  18. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  19. package/dist/cjs/llm/bedrock/toolCache.cjs +1 -1
  20. package/dist/cjs/llm/fake.cjs +9 -5
  21. package/dist/cjs/llm/fake.cjs.map +1 -1
  22. package/dist/cjs/llm/invoke.cjs +3 -7
  23. package/dist/cjs/llm/invoke.cjs.map +1 -1
  24. package/dist/cjs/llm/openrouter/toolCache.cjs +1 -1
  25. package/dist/cjs/llm/prepareProviderRequest.cjs +2 -2
  26. package/dist/cjs/llm/providers.cjs +1 -1
  27. package/dist/cjs/llm/request.cjs +6 -0
  28. package/dist/cjs/llm/request.cjs.map +1 -1
  29. package/dist/cjs/llm/truncation.cjs +1 -0
  30. package/dist/cjs/main.cjs +22 -13
  31. package/dist/cjs/messages/format.cjs +375 -3
  32. package/dist/cjs/messages/format.cjs.map +1 -1
  33. package/dist/cjs/messages/index.cjs +1 -1
  34. package/dist/cjs/messages/prune.cjs +1 -1
  35. package/dist/cjs/run.cjs +217 -52
  36. package/dist/cjs/run.cjs.map +1 -1
  37. package/dist/cjs/session/AgentSession.cjs +1 -1
  38. package/dist/cjs/session/JsonlSessionStore.cjs +1 -1
  39. package/dist/cjs/stream.cjs +1 -1
  40. package/dist/cjs/summarization/index.cjs +1 -0
  41. package/dist/cjs/summarization/index.cjs.map +1 -1
  42. package/dist/cjs/summarization/node.cjs +58 -85
  43. package/dist/cjs/summarization/node.cjs.map +1 -1
  44. package/dist/cjs/summarization/semanticIndex.cjs +366 -0
  45. package/dist/cjs/summarization/semanticIndex.cjs.map +1 -0
  46. package/dist/cjs/summarization/shared.cjs +83 -0
  47. package/dist/cjs/summarization/shared.cjs.map +1 -0
  48. package/dist/cjs/tools/ToolNode.cjs +5 -5
  49. package/dist/cjs/tools/local/CompileCheckTool.cjs +1 -1
  50. package/dist/cjs/tools/local/LocalCodingTools.cjs +1 -1
  51. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +1 -1
  52. package/dist/cjs/tools/runStepResume.cjs +1 -1
  53. package/dist/cjs/tools/runStepResume.cjs.map +1 -1
  54. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +8 -12
  55. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  56. package/dist/cjs/tools/subagent/childGraphConfig.cjs +2 -1
  57. package/dist/cjs/tools/subagent/childGraphConfig.cjs.map +1 -1
  58. package/dist/cjs/utils/index.cjs +1 -1
  59. package/dist/cjs/utils/tokens.cjs +8 -1
  60. package/dist/cjs/utils/tokens.cjs.map +1 -1
  61. package/dist/esm/agents/AgentContext.mjs +26 -5
  62. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  63. package/dist/esm/common/constants.mjs +11 -1
  64. package/dist/esm/common/constants.mjs.map +1 -1
  65. package/dist/esm/graphs/Graph.mjs +56 -11
  66. package/dist/esm/graphs/Graph.mjs.map +1 -1
  67. package/dist/esm/graphs/MultiAgentGraph.mjs +1 -1
  68. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  69. package/dist/esm/hooks/executeHooks.mjs +8 -1
  70. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  71. package/dist/esm/hooks/index.mjs +2 -1
  72. package/dist/esm/hooks/index.mjs.map +1 -1
  73. package/dist/esm/hooks/types.mjs +1 -0
  74. package/dist/esm/hooks/types.mjs.map +1 -1
  75. package/dist/esm/langfuse.mjs +43 -3
  76. package/dist/esm/langfuse.mjs.map +1 -1
  77. package/dist/esm/langfuseTraceShaping.mjs +75 -0
  78. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  79. package/dist/esm/llm/bedrock/toolCache.mjs +1 -1
  80. package/dist/esm/llm/fake.mjs +9 -5
  81. package/dist/esm/llm/fake.mjs.map +1 -1
  82. package/dist/esm/llm/invoke.mjs +3 -7
  83. package/dist/esm/llm/invoke.mjs.map +1 -1
  84. package/dist/esm/llm/openrouter/toolCache.mjs +1 -1
  85. package/dist/esm/llm/prepareProviderRequest.mjs +2 -2
  86. package/dist/esm/llm/providers.mjs +1 -1
  87. package/dist/esm/llm/request.mjs +6 -1
  88. package/dist/esm/llm/request.mjs.map +1 -1
  89. package/dist/esm/llm/truncation.mjs +1 -1
  90. package/dist/esm/main.mjs +15 -14
  91. package/dist/esm/messages/format.mjs +375 -3
  92. package/dist/esm/messages/format.mjs.map +1 -1
  93. package/dist/esm/messages/index.mjs +1 -1
  94. package/dist/esm/messages/prune.mjs +1 -1
  95. package/dist/esm/run.mjs +217 -52
  96. package/dist/esm/run.mjs.map +1 -1
  97. package/dist/esm/session/AgentSession.mjs +1 -1
  98. package/dist/esm/session/JsonlSessionStore.mjs +1 -1
  99. package/dist/esm/stream.mjs +1 -1
  100. package/dist/esm/summarization/index.mjs +1 -0
  101. package/dist/esm/summarization/index.mjs.map +1 -1
  102. package/dist/esm/summarization/node.mjs +57 -84
  103. package/dist/esm/summarization/node.mjs.map +1 -1
  104. package/dist/esm/summarization/semanticIndex.mjs +363 -0
  105. package/dist/esm/summarization/semanticIndex.mjs.map +1 -0
  106. package/dist/esm/summarization/shared.mjs +79 -0
  107. package/dist/esm/summarization/shared.mjs.map +1 -0
  108. package/dist/esm/tools/ToolNode.mjs +5 -5
  109. package/dist/esm/tools/local/CompileCheckTool.mjs +1 -1
  110. package/dist/esm/tools/local/LocalCodingTools.mjs +1 -1
  111. package/dist/esm/tools/local/LocalExecutionEngine.mjs +1 -1
  112. package/dist/esm/tools/runStepResume.mjs +1 -1
  113. package/dist/esm/tools/runStepResume.mjs.map +1 -1
  114. package/dist/esm/tools/subagent/SubagentExecutor.mjs +8 -12
  115. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  116. package/dist/esm/tools/subagent/childGraphConfig.mjs +2 -1
  117. package/dist/esm/tools/subagent/childGraphConfig.mjs.map +1 -1
  118. package/dist/esm/utils/index.mjs +1 -1
  119. package/dist/esm/utils/tokens.mjs +8 -2
  120. package/dist/esm/utils/tokens.mjs.map +1 -1
  121. package/dist/types/agents/AgentContext.d.ts +27 -1
  122. package/dist/types/common/constants.d.ts +17 -0
  123. package/dist/types/graphs/Graph.d.ts +24 -0
  124. package/dist/types/hooks/executeHooks.d.ts +5 -2
  125. package/dist/types/hooks/index.d.ts +7 -2
  126. package/dist/types/hooks/types.d.ts +45 -7
  127. package/dist/types/langfuse.d.ts +3 -1
  128. package/dist/types/llm/fake.d.ts +12 -2
  129. package/dist/types/llm/request.d.ts +10 -0
  130. package/dist/types/messages/format.d.ts +17 -2
  131. package/dist/types/run.d.ts +19 -6
  132. package/dist/types/summarization/index.d.ts +6 -0
  133. package/dist/types/summarization/node.d.ts +0 -4
  134. package/dist/types/summarization/semanticIndex.d.ts +21 -0
  135. package/dist/types/summarization/shared.d.ts +25 -0
  136. package/dist/types/types/graph.d.ts +9 -1
  137. package/dist/types/types/run.d.ts +8 -0
  138. package/dist/types/types/stream.d.ts +6 -0
  139. package/dist/types/types/summarize.d.ts +44 -0
  140. package/dist/types/utils/tokens.d.ts +9 -0
  141. package/package.json +2 -1
  142. package/src/agents/AgentContext.ts +63 -6
  143. package/src/common/constants.ts +19 -0
  144. package/src/graphs/Graph.ts +86 -8
  145. package/src/hooks/HookRegistry.ts +3 -1
  146. package/src/hooks/executeHooks.ts +20 -2
  147. package/src/hooks/index.ts +12 -1
  148. package/src/hooks/types.ts +47 -4
  149. package/src/langfuse.ts +70 -0
  150. package/src/langfuseTraceShaping.ts +94 -0
  151. package/src/llm/fake.ts +35 -4
  152. package/src/llm/invoke.ts +13 -29
  153. package/src/llm/request.ts +24 -0
  154. package/src/messages/format.ts +839 -5
  155. package/src/run.ts +393 -187
  156. package/src/summarization/index.ts +11 -0
  157. package/src/summarization/node.ts +202 -158
  158. package/src/summarization/semanticIndex.ts +662 -0
  159. package/src/summarization/shared.ts +130 -0
  160. package/src/tools/runStepResume.ts +9 -0
  161. package/src/tools/subagent/SubagentExecutor.ts +4 -27
  162. package/src/tools/subagent/childGraphConfig.ts +3 -0
  163. package/src/types/graph.ts +9 -0
  164. package/src/types/run.ts +8 -0
  165. package/src/types/stream.ts +6 -0
  166. package/src/types/summarize.ts +57 -0
  167. package/src/utils/tokens.ts +31 -8
package/src/langfuse.ts CHANGED
@@ -79,6 +79,8 @@ type LangfuseHandlerParams = {
79
79
  * only adopted when stamped with the same run (see
80
80
  * `LangfuseRuntimeContext.runId`). */
81
81
  runId?: string;
82
+ /** Keep this root open while one processStream call executes graph segments. */
83
+ deferRootRunId?: string;
82
84
  /** The run's resolved tool-output policy — for multi-agent streams the
83
85
  * conservative aggregate across agents, which `this.langfuse` (the
84
86
  * primary agent's config) cannot reproduce. Applied when a foreign
@@ -293,9 +295,19 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
293
295
  private readonly agentId?: string;
294
296
  private readonly parentSpanContext?: SpanContext;
295
297
  private readonly runId?: string;
298
+ private readonly deferredRootRunId?: string;
296
299
  private readonly identity: HandlerIdentity;
297
300
  private readonly toolOutputTracing?: ResolvedLangfuseToolOutputTracingConfig;
298
301
  private readonly trackedRunIds = new Set<string>();
302
+ private deferredRootStarted = false;
303
+ private deferredRootOutcome:
304
+ | {
305
+ type: 'end';
306
+ output: Parameters<CallbackHandler['handleChainEnd']>[0];
307
+ parentRunId?: string;
308
+ }
309
+ | { type: 'error'; error: Error; parentRunId?: string }
310
+ | undefined;
299
311
 
300
312
  constructor(params?: AgentLangfuseHandlerParams) {
301
313
  const {
@@ -306,6 +318,7 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
306
318
  parentSpanContext,
307
319
  inheritTraceIdentity,
308
320
  runId,
321
+ deferRootRunId,
309
322
  toolOutputTracing,
310
323
  traceName,
311
324
  ...handlerParams
@@ -326,6 +339,10 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
326
339
  this.agentId = agentId;
327
340
  this.parentSpanContext = parentSpanContext;
328
341
  this.runId = runId;
342
+ this.deferredRootRunId = deferRootRunId;
343
+ if (deferRootRunId != null) {
344
+ this.awaitHandlers = true;
345
+ }
329
346
  this.toolOutputTracing = toolOutputTracing;
330
347
  this.identity = {
331
348
  userId: inheritTraceIdentity === true ? undefined : handlerParams.userId,
@@ -528,6 +545,17 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
528
545
  override handleChainStart(
529
546
  ...args: Parameters<CallbackHandler['handleChainStart']>
530
547
  ): ReturnType<CallbackHandler['handleChainStart']> {
548
+ const [, , runId, parentRunId] = args;
549
+ if (
550
+ runId === this.deferredRootRunId &&
551
+ parentRunId == null &&
552
+ this.deferredRootStarted
553
+ ) {
554
+ return Promise.resolve();
555
+ }
556
+ if (runId === this.deferredRootRunId && parentRunId == null) {
557
+ this.deferredRootStarted = true;
558
+ }
531
559
  return this.withRuntimeContext(
532
560
  () => super.handleChainStart(...args),
533
561
  this.startsDetachedRun(args[2], args[3]),
@@ -539,6 +567,13 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
539
567
  ...args: Parameters<CallbackHandler['handleChainError']>
540
568
  ): ReturnType<CallbackHandler['handleChainError']> {
541
569
  const [error, runId, parentRunId] = args;
570
+ if (runId === this.deferredRootRunId && parentRunId == null) {
571
+ this.deferredRootOutcome = {
572
+ type: 'error',
573
+ error: error instanceof Error ? error : new Error(String(error)),
574
+ };
575
+ return Promise.resolve();
576
+ }
542
577
  if (error != null && parentRunId != null && isGraphInterrupt(error)) {
543
578
  return super.handleChainEnd(
544
579
  GRAPH_INTERRUPT_CONTROL_FLOW,
@@ -556,6 +591,36 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
556
591
  return super.handleChainError(...args);
557
592
  }
558
593
 
594
+ override handleChainEnd(
595
+ ...args: Parameters<CallbackHandler['handleChainEnd']>
596
+ ): ReturnType<CallbackHandler['handleChainEnd']> {
597
+ const [output, runId, parentRunId] = args;
598
+ if (runId === this.deferredRootRunId && parentRunId == null) {
599
+ this.deferredRootOutcome = { type: 'end', output };
600
+ return Promise.resolve();
601
+ }
602
+ return super.handleChainEnd(...args);
603
+ }
604
+
605
+ async finishDeferredRoot(): Promise<void> {
606
+ const runId = this.deferredRootRunId;
607
+ const outcome = this.deferredRootOutcome;
608
+ if (!this.deferredRootStarted || runId == null || outcome == null) {
609
+ return;
610
+ }
611
+ this.deferredRootStarted = false;
612
+ this.deferredRootOutcome = undefined;
613
+ if (outcome.type === 'error') {
614
+ await super.handleChainError(
615
+ outcome.error,
616
+ runId,
617
+ outcome.parentRunId
618
+ );
619
+ return;
620
+ }
621
+ await super.handleChainEnd(outcome.output, runId, outcome.parentRunId);
622
+ }
623
+
559
624
  override handleAgentAction(
560
625
  ...args: Parameters<CallbackHandler['handleAgentAction']>
561
626
  ): ReturnType<CallbackHandler['handleAgentAction']> {
@@ -792,6 +857,7 @@ export function createLangfuseHandler({
792
857
  parentSpanContext,
793
858
  inheritTraceIdentity,
794
859
  runId,
860
+ deferRootRunId,
795
861
  toolOutputTracing,
796
862
  traceName,
797
863
  }: AgentLangfuseHandlerParams): CallbackHandler | undefined {
@@ -813,6 +879,7 @@ export function createLangfuseHandler({
813
879
  parentSpanContext,
814
880
  inheritTraceIdentity,
815
881
  runId,
882
+ deferRootRunId,
816
883
  toolOutputTracing,
817
884
  traceName,
818
885
  });
@@ -865,6 +932,9 @@ export function isLangfuseCallbackHandler(value: unknown): boolean {
865
932
  }
866
933
 
867
934
  export async function disposeLangfuseHandler(value: unknown): Promise<void> {
935
+ if (value instanceof ScopedLangfuseCallbackHandler) {
936
+ await value.finishDeferredRoot();
937
+ }
868
938
  if (
869
939
  value == null ||
870
940
  parseBooleanEnv(process.env[LANGFUSE_FORCE_FLUSH_ON_DISPOSE]) !== true
@@ -35,6 +35,11 @@ const DEPRECATED_TRACE_INPUT_ATTRIBUTE = 'langfuse.trace.input';
35
35
  const DEPRECATED_TRACE_OUTPUT_ATTRIBUTE = 'langfuse.trace.output';
36
36
  const OBSERVATION_METADATA_LANGGRAPH_NODE = `${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.langgraph_node`;
37
37
  const OBSERVATION_METADATA_OPERATION = `${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.${LANGFUSE_OPERATION_METADATA_KEY}`;
38
+ const OBSERVATION_METADATA_COMPACTION_SEMANTIC_INDEX_ENTRIES = `${LangfuseOtelSpanAttributes.OBSERVATION_METADATA}.compaction_semantic_index_entries`;
39
+ const COMPACTION_SEMANTIC_INDEX_OPEN = '<compaction-semantic-index>';
40
+ const COMPACTION_SEMANTIC_INDEX_CLOSE = '</compaction-semantic-index>';
41
+ const REDACTED_COMPACTION_SEMANTIC_INDEX =
42
+ '<compaction-semantic-index redacted="true" />\n\n';
38
43
 
39
44
  type MutableSpan = ReadableSpan & {
40
45
  name: string;
@@ -66,6 +71,94 @@ function parseAttributeValue(value: unknown): unknown {
66
71
  }
67
72
  }
68
73
 
74
+ type CompactionSemanticIndexRedaction = {
75
+ value: unknown;
76
+ redacted: boolean;
77
+ };
78
+
79
+ function redactCompactionSemanticIndexText(
80
+ value: string
81
+ ): CompactionSemanticIndexRedaction {
82
+ if (!value.startsWith(COMPACTION_SEMANTIC_INDEX_OPEN)) {
83
+ return { value, redacted: false };
84
+ }
85
+ const start = 0;
86
+ const close = value.indexOf(COMPACTION_SEMANTIC_INDEX_CLOSE, start);
87
+ if (close < 0) {
88
+ return { value, redacted: false };
89
+ }
90
+ let end = close + COMPACTION_SEMANTIC_INDEX_CLOSE.length;
91
+ while (end < value.length && /\s/.test(value[end])) {
92
+ end++;
93
+ }
94
+ return {
95
+ value:
96
+ value.slice(0, start) +
97
+ REDACTED_COMPACTION_SEMANTIC_INDEX +
98
+ value.slice(end),
99
+ redacted: true,
100
+ };
101
+ }
102
+
103
+ function redactCompactionSemanticIndexValue(
104
+ value: unknown
105
+ ): CompactionSemanticIndexRedaction {
106
+ if (typeof value === 'string') {
107
+ return redactCompactionSemanticIndexText(value);
108
+ }
109
+ if (Array.isArray(value)) {
110
+ for (let index = value.length - 1; index >= 0; index--) {
111
+ const nested = redactCompactionSemanticIndexValue(value[index]);
112
+ if (nested.redacted) {
113
+ const result = [...value];
114
+ result[index] = nested.value;
115
+ return { value: result, redacted: true };
116
+ }
117
+ }
118
+ return { value, redacted: false };
119
+ }
120
+ if (!isRecord(value)) {
121
+ return { value, redacted: false };
122
+ }
123
+ const entries = Object.entries(value);
124
+ for (let index = entries.length - 1; index >= 0; index--) {
125
+ const [key, nestedValue] = entries[index];
126
+ const nested = redactCompactionSemanticIndexValue(nestedValue);
127
+ if (nested.redacted) {
128
+ return {
129
+ value: { ...value, [key]: nested.value },
130
+ redacted: true,
131
+ };
132
+ }
133
+ }
134
+ return { value, redacted: false };
135
+ }
136
+
137
+ function redactCompactionSemanticIndexInput(span: MutableSpan): void {
138
+ const entryCount =
139
+ span.attributes[
140
+ OBSERVATION_METADATA_COMPACTION_SEMANTIC_INDEX_ENTRIES
141
+ ];
142
+ const numericEntryCount = Number(entryCount);
143
+ if (!Number.isFinite(numericEntryCount) || numericEntryCount <= 0) {
144
+ return;
145
+ }
146
+ const inputKey = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;
147
+ const input = span.attributes[inputKey];
148
+ if (input == null) {
149
+ return;
150
+ }
151
+ const parsed = parseAttributeValue(input);
152
+ const redaction = redactCompactionSemanticIndexValue(parsed);
153
+ if (!redaction.redacted) {
154
+ return;
155
+ }
156
+ span.attributes[inputKey] =
157
+ typeof redaction.value === 'string' && parsed === input
158
+ ? redaction.value
159
+ : JSON.stringify(redaction.value);
160
+ }
161
+
69
162
  function getMessageArray(
70
163
  value: unknown
71
164
  ): Record<string, unknown>[] | undefined {
@@ -594,6 +687,7 @@ export function shapeLangfuseSpan(span: ReadableSpan): void {
594
687
  const mutable = span as MutableSpan;
595
688
  delete mutable.attributes[DEPRECATED_TRACE_INPUT_ATTRIBUTE];
596
689
  delete mutable.attributes[DEPRECATED_TRACE_OUTPUT_ATTRIBUTE];
690
+ redactCompactionSemanticIndexInput(mutable);
597
691
  const isGraphObservation = isGraphSpan(mutable);
598
692
  if (mutable.name.startsWith(LANGGRAPH_AGENT_NODE_PREFIX)) {
599
693
  shapeAgentNodeSpan(mutable);
package/src/llm/fake.ts CHANGED
@@ -14,6 +14,15 @@ export class FakeChatModel extends FakeListChatModel {
14
14
  private splitStrategy: SplitStrategy;
15
15
  private toolCalls: ToolCall[] = [];
16
16
  private addedToolCalls: boolean = false;
17
+ /**
18
+ * Attached to the last streamed chunk of every response, mirroring a real
19
+ * provider's terminal `finish_reason`/`stop_reason` on `generationInfo`.
20
+ * Lets truncation-path tests (`getTruncationStopReason`) drive a genuine
21
+ * `Run`/`StandardGraph` without a live provider. `undefined` (the
22
+ * default) reproduces the previous behavior exactly: no metadata on any
23
+ * chunk.
24
+ */
25
+ private finalChunkGenerationInfo?: Record<string, unknown>;
17
26
 
18
27
  constructor({
19
28
  responses,
@@ -21,16 +30,19 @@ export class FakeChatModel extends FakeListChatModel {
21
30
  emitCustomEvent,
22
31
  splitStrategy = { type: 'regex', value: /(?<=\s+)|(?=\s+)/ },
23
32
  toolCalls = [],
33
+ finalChunkGenerationInfo,
24
34
  }: {
25
35
  responses: string[];
26
36
  sleep?: number;
27
37
  emitCustomEvent?: boolean;
28
38
  splitStrategy?: SplitStrategy;
29
39
  toolCalls?: ToolCall[];
40
+ finalChunkGenerationInfo?: Record<string, unknown>;
30
41
  }) {
31
42
  super({ responses, sleep, emitCustomEvent });
32
43
  this.splitStrategy = splitStrategy;
33
44
  this.toolCalls = toolCalls;
45
+ this.finalChunkGenerationInfo = finalChunkGenerationInfo;
34
46
  }
35
47
 
36
48
  private splitText(text: string): string[] {
@@ -47,14 +59,16 @@ export class FakeChatModel extends FakeListChatModel {
47
59
  }
48
60
  _createResponseChunk(
49
61
  text: string,
50
- tool_call_chunks?: ToolCallChunk[]
62
+ tool_call_chunks?: ToolCallChunk[],
63
+ responseMetadata?: Record<string, unknown>
51
64
  ): ChatGenerationChunk {
52
65
  return new ChatGenerationChunk({
53
66
  text,
54
- generationInfo: {},
67
+ generationInfo: responseMetadata ?? {},
55
68
  message: new AIMessageChunk({
56
69
  content: text,
57
70
  tool_call_chunks,
71
+ response_metadata: responseMetadata,
58
72
  additional_kwargs: tool_call_chunks
59
73
  ? {
60
74
  tool_calls: tool_call_chunks.map((toolCall) => ({
@@ -87,14 +101,31 @@ export class FakeChatModel extends FakeListChatModel {
87
101
  }
88
102
 
89
103
  const chunks = this.splitText(response);
90
- for await (const chunk of chunks) {
104
+ for (let i = 0; i < chunks.length; i++) {
105
+ const chunk = chunks[i];
91
106
  await this._sleepIfRequested();
92
107
 
93
108
  if (options.thrownErrorString != null && options.thrownErrorString) {
94
109
  throw new Error(options.thrownErrorString);
95
110
  }
96
111
 
97
- const responseChunk = super._createResponseChunk(chunk);
112
+ /**
113
+ * Only the true terminal chunk of the response gets the metadata: when
114
+ * this response also appends a trailing tool-call chunk below, THAT
115
+ * chunk is terminal instead (not currently exercised by any test —
116
+ * a caller wanting truncated-tool-call metadata should attach it
117
+ * there).
118
+ */
119
+ const isTerminalChunk =
120
+ i === chunks.length - 1 && this.toolCalls.length === 0;
121
+ const responseChunk =
122
+ isTerminalChunk && this.finalChunkGenerationInfo != null
123
+ ? this._createResponseChunk(
124
+ chunk,
125
+ undefined,
126
+ this.finalChunkGenerationInfo
127
+ )
128
+ : super._createResponseChunk(chunk);
98
129
  yield responseChunk;
99
130
  void runManager?.handleLLMNewToken(chunk);
100
131
  }
package/src/llm/invoke.ts CHANGED
@@ -27,6 +27,12 @@ import {
27
27
  STREAM_LIMIT_REDISPATCH_KEY,
28
28
  STREAM_LIMIT_ATTEMPT_KEY,
29
29
  } from '@/llm/streamLimits';
30
+ import {
31
+ inspectProviderMessageProjection,
32
+ ProviderMessageProjectionInvariantError,
33
+ resolveProviderMessageProjectionInvariantMode,
34
+ modifyDeltaProperties,
35
+ } from '@/messages';
30
36
  import {
31
37
  assertPreparedProviderRequestFor,
32
38
  prepareProviderRequest,
@@ -38,15 +44,10 @@ import {
38
44
  import { ChatModelStreamHandler, dispatchesChatModelStream } from '@/stream';
39
45
  import { Constants, ContentTypes, GraphEvents, Providers } from '@/common';
40
46
  import { assertNotTruncatedToolCall } from '@/llm/truncation';
47
+ import { resolveClientOptionsModel } from '@/llm/request';
41
48
  import { safeDispatchCustomEvent } from '@/utils/events';
42
49
  import { getContextOverflowInfo } from '@/utils/errors';
43
50
  import { appendCallbacks } from '@/utils/callbacks';
44
- import {
45
- inspectProviderMessageProjection,
46
- ProviderMessageProjectionInvariantError,
47
- resolveProviderMessageProjectionInvariantMode,
48
- modifyDeltaProperties,
49
- } from '@/messages';
50
51
  import { canSealPreempt } from '@/llm/preempt';
51
52
  import { initializeModel } from '@/llm/init';
52
53
 
@@ -715,8 +716,7 @@ async function attemptInvokeBody(
715
716
  resolveProviderMessageProjectionInvariantMode();
716
717
  let sealedRunId: string | undefined;
717
718
  let invocationConfig = config;
718
- const captureModelRunId =
719
- model.stream != null && context?.preemption != null;
719
+ const captureModelRunId = model.stream != null && context?.preemption != null;
720
720
  if (projectionInvariantMode !== 'off' || captureModelRunId) {
721
721
  invocationConfig = withModelStartHandler({
722
722
  config,
@@ -934,7 +934,10 @@ async function attemptInvokeBody(
934
934
  return { messages: [finalChunk as AIMessageChunk] };
935
935
  }
936
936
 
937
- const finalMessage = await model.invoke(messagesForProvider, invocationConfig);
937
+ const finalMessage = await model.invoke(
938
+ messagesForProvider,
939
+ invocationConfig
940
+ );
938
941
  if ((finalMessage.tool_calls?.length ?? 0) > 0) {
939
942
  finalMessage.tool_calls = finalMessage.tool_calls?.filter(
940
943
  (tool_call: ToolCall) => !!tool_call.name
@@ -997,25 +1000,6 @@ export function getFallbackOverflowCandidates(
997
1000
  return [...(fallbackOverflowCandidates.get(error) ?? [])];
998
1001
  }
999
1002
 
1000
- /**
1001
- * Best-effort read of the configured model name from client options.
1002
- * Providers disagree on the key (`model` vs `modelName`).
1003
- */
1004
- function extractClientOptionsModel(
1005
- clientOptions: t.ClientOptions | undefined
1006
- ): string | undefined {
1007
- const options = clientOptions as
1008
- | { model?: unknown; modelName?: unknown }
1009
- | undefined;
1010
- if (typeof options?.model === 'string' && options.model !== '') {
1011
- return options.model;
1012
- }
1013
- if (typeof options?.modelName === 'string' && options.modelName !== '') {
1014
- return options.modelName;
1015
- }
1016
- return undefined;
1017
- }
1018
-
1019
1003
  /**
1020
1004
  * Attempts each fallback provider in order until one succeeds.
1021
1005
  *
@@ -1106,7 +1090,7 @@ export async function tryFallbackProviders({
1106
1090
  * `ls_model_name`. The serving provider is stamped uniformly by
1107
1091
  * `attemptInvoke` (`INVOKED_PROVIDER`).
1108
1092
  */
1109
- const fbModelName = extractClientOptionsModel(fb.clientOptions);
1093
+ const fbModelName = resolveClientOptionsModel(fb.clientOptions);
1110
1094
  const fbConfig: RunnableConfig | undefined =
1111
1095
  fbModelName == null
1112
1096
  ? config
@@ -43,6 +43,30 @@ export function isThinkingEnabled(
43
43
  return false;
44
44
  }
45
45
 
46
+ /**
47
+ * Model configured on client options, under whichever of the two keys carries
48
+ * it. LangChain accepts `modelName` as an alias for `model` and this package
49
+ * writes both (see `buildSummarizationClientConfig`), while hosts configure
50
+ * agents through either. Reading only one key therefore does not fail loudly:
51
+ * it reports an unconfigured model, and every caller here treats that as a cue
52
+ * to fall back to a default, so a Claude agent configured through the alias is
53
+ * silently handled as something else.
54
+ */
55
+ export function resolveClientOptionsModel(
56
+ clientOptions: t.ClientOptions | undefined
57
+ ): string | undefined {
58
+ const options = clientOptions as
59
+ | { model?: unknown; modelName?: unknown }
60
+ | undefined;
61
+ if (typeof options?.model === 'string' && options.model !== '') {
62
+ return options.model;
63
+ }
64
+ if (typeof options?.modelName === 'string' && options.modelName !== '') {
65
+ return options.modelName;
66
+ }
67
+ return undefined;
68
+ }
69
+
46
70
  /**
47
71
  * Returns the correct key for setting max output tokens on the model
48
72
  * constructor options. Google/Vertex use `maxOutputTokens`, all others