@librechat/agents 3.2.60 → 3.2.62

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 (116) hide show
  1. package/dist/cjs/common/enum.cjs +2 -0
  2. package/dist/cjs/common/enum.cjs.map +1 -1
  3. package/dist/cjs/hooks/HookRegistry.cjs +32 -0
  4. package/dist/cjs/hooks/HookRegistry.cjs.map +1 -1
  5. package/dist/cjs/hooks/executeHooks.cjs +6 -0
  6. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  7. package/dist/cjs/hooks/index.cjs +12 -0
  8. package/dist/cjs/hooks/index.cjs.map +1 -0
  9. package/dist/cjs/hooks/types.cjs.map +1 -1
  10. package/dist/cjs/langfuse.cjs +62 -1
  11. package/dist/cjs/langfuse.cjs.map +1 -1
  12. package/dist/cjs/llm/bedrock/index.cjs +266 -43
  13. package/dist/cjs/llm/bedrock/index.cjs.map +1 -1
  14. package/dist/cjs/llm/openai/index.cjs +274 -7
  15. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  16. package/dist/cjs/llm/openai/streamMetadata.cjs +69 -0
  17. package/dist/cjs/llm/openai/streamMetadata.cjs.map +1 -0
  18. package/dist/cjs/llm/openrouter/index.cjs +5 -6
  19. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  20. package/dist/cjs/main.cjs +6 -1
  21. package/dist/cjs/messages/format.cjs +61 -0
  22. package/dist/cjs/messages/format.cjs.map +1 -1
  23. package/dist/cjs/messages/prune.cjs +31 -19
  24. package/dist/cjs/messages/prune.cjs.map +1 -1
  25. package/dist/cjs/stream.cjs +13 -3
  26. package/dist/cjs/stream.cjs.map +1 -1
  27. package/dist/cjs/tools/ToolNode.cjs +75 -10
  28. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  29. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -0
  30. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  31. package/dist/cjs/utils/tokens.cjs +111 -0
  32. package/dist/cjs/utils/tokens.cjs.map +1 -1
  33. package/dist/esm/common/enum.mjs +2 -0
  34. package/dist/esm/common/enum.mjs.map +1 -1
  35. package/dist/esm/hooks/HookRegistry.mjs +32 -0
  36. package/dist/esm/hooks/HookRegistry.mjs.map +1 -1
  37. package/dist/esm/hooks/executeHooks.mjs +6 -0
  38. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  39. package/dist/esm/hooks/index.mjs +12 -1
  40. package/dist/esm/hooks/index.mjs.map +1 -0
  41. package/dist/esm/hooks/types.mjs.map +1 -1
  42. package/dist/esm/langfuse.mjs +62 -1
  43. package/dist/esm/langfuse.mjs.map +1 -1
  44. package/dist/esm/llm/bedrock/index.mjs +266 -43
  45. package/dist/esm/llm/bedrock/index.mjs.map +1 -1
  46. package/dist/esm/llm/openai/index.mjs +274 -7
  47. package/dist/esm/llm/openai/index.mjs.map +1 -1
  48. package/dist/esm/llm/openai/streamMetadata.mjs +69 -0
  49. package/dist/esm/llm/openai/streamMetadata.mjs.map +1 -0
  50. package/dist/esm/llm/openrouter/index.mjs +5 -6
  51. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  52. package/dist/esm/main.mjs +3 -3
  53. package/dist/esm/messages/format.mjs +61 -0
  54. package/dist/esm/messages/format.mjs.map +1 -1
  55. package/dist/esm/messages/prune.mjs +31 -19
  56. package/dist/esm/messages/prune.mjs.map +1 -1
  57. package/dist/esm/stream.mjs +13 -3
  58. package/dist/esm/stream.mjs.map +1 -1
  59. package/dist/esm/tools/ToolNode.mjs +75 -10
  60. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  61. package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -0
  62. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  63. package/dist/esm/utils/tokens.mjs +108 -1
  64. package/dist/esm/utils/tokens.mjs.map +1 -1
  65. package/dist/types/common/enum.d.ts +3 -1
  66. package/dist/types/hooks/HookRegistry.d.ts +10 -0
  67. package/dist/types/hooks/index.d.ts +7 -0
  68. package/dist/types/hooks/types.d.ts +18 -1
  69. package/dist/types/llm/bedrock/index.d.ts +5 -0
  70. package/dist/types/llm/openai/index.d.ts +19 -0
  71. package/dist/types/llm/openai/streamMetadata.d.ts +16 -0
  72. package/dist/types/messages/prune.d.ts +4 -3
  73. package/dist/types/tools/ToolNode.d.ts +1 -0
  74. package/dist/types/types/llm.d.ts +12 -2
  75. package/dist/types/types/tools.d.ts +1 -1
  76. package/dist/types/utils/tokens.d.ts +30 -0
  77. package/package.json +9 -9
  78. package/src/__tests__/stream.eagerEventExecution.test.ts +100 -2
  79. package/src/common/enum.ts +2 -0
  80. package/src/hooks/HookRegistry.ts +45 -0
  81. package/src/hooks/__tests__/HookRegistry.test.ts +48 -0
  82. package/src/hooks/__tests__/executeHooks.test.ts +85 -2
  83. package/src/hooks/executeHooks.ts +15 -0
  84. package/src/hooks/index.ts +7 -0
  85. package/src/hooks/types.ts +18 -1
  86. package/src/langfuse.ts +134 -1
  87. package/src/llm/bedrock/index.ts +434 -83
  88. package/src/llm/bedrock/streamSealDispatch.test.ts +97 -0
  89. package/src/llm/custom-chat-models.smoke.test.ts +7 -0
  90. package/src/llm/openai/index.ts +604 -6
  91. package/src/llm/openai/managedRequests.test.ts +182 -0
  92. package/src/llm/openai/streamMetadata.spec.ts +86 -0
  93. package/src/llm/openai/streamMetadata.ts +95 -0
  94. package/src/llm/openai/streamMetadataDedup.spec.ts +166 -0
  95. package/src/llm/openrouter/index.ts +9 -5
  96. package/src/messages/format.ts +96 -3
  97. package/src/messages/formatAgentMessages.steer.test.ts +326 -0
  98. package/src/messages/labelContentByAgent.test.ts +75 -0
  99. package/src/messages/prune.ts +56 -30
  100. package/src/specs/anthropic.simple.test.ts +4 -2
  101. package/src/specs/cache.simple.test.ts +17 -4
  102. package/src/specs/langfuse-callbacks.test.ts +61 -0
  103. package/src/specs/openai.simple.test.ts +4 -2
  104. package/src/specs/spec.utils.ts +12 -0
  105. package/src/specs/summarization.test.ts +9 -13
  106. package/src/specs/token-accounting-pipeline.test.ts +130 -4
  107. package/src/specs/tokens.test.ts +214 -0
  108. package/src/stream.ts +22 -3
  109. package/src/tools/ToolNode.ts +107 -12
  110. package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +554 -0
  111. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +49 -2
  112. package/src/tools/__tests__/hitl.test.ts +112 -0
  113. package/src/tools/subagent/SubagentExecutor.ts +2 -0
  114. package/src/types/llm.ts +12 -2
  115. package/src/types/tools.ts +1 -1
  116. package/src/utils/tokens.ts +181 -3
@@ -155,7 +155,11 @@ describe('executeHooks', () => {
155
155
  input: preToolUseInput('Bash'),
156
156
  matchQuery: 'Bash',
157
157
  });
158
- expect(result).toEqual({ additionalContexts: [], errors: [] });
158
+ expect(result).toEqual({
159
+ additionalContexts: [],
160
+ injectedMessages: [],
161
+ errors: [],
162
+ });
159
163
  });
160
164
 
161
165
  it('returns an empty result when no matcher pattern matches the query', async () => {
@@ -176,7 +180,11 @@ describe('executeHooks', () => {
176
180
  matchQuery: 'Bash',
177
181
  });
178
182
  expect(called).toBe(false);
179
- expect(result).toEqual({ additionalContexts: [], errors: [] });
183
+ expect(result).toEqual({
184
+ additionalContexts: [],
185
+ injectedMessages: [],
186
+ errors: [],
187
+ });
180
188
  });
181
189
  });
182
190
 
@@ -439,6 +447,81 @@ describe('executeHooks', () => {
439
447
  });
440
448
  });
441
449
 
450
+ describe('injectedMessages accumulation', () => {
451
+ it('accumulates injectedMessages from every hook in registration order', async () => {
452
+ const registry = new HookRegistry();
453
+ registry.register('PreToolUse', {
454
+ hooks: [
455
+ preToolHook(
456
+ async (): Promise<PreToolUseHookOutput> => ({
457
+ injectedMessages: [
458
+ { role: 'user', content: 'steer one', source: 'steer' },
459
+ ],
460
+ })
461
+ ),
462
+ preToolHook(
463
+ async (): Promise<PreToolUseHookOutput> => emptyPreOutput
464
+ ),
465
+ ],
466
+ });
467
+ registry.register('PreToolUse', {
468
+ hooks: [
469
+ preToolHook(
470
+ async (): Promise<PreToolUseHookOutput> => ({
471
+ injectedMessages: [
472
+ { role: 'user', content: 'steer two', source: 'steer' },
473
+ { role: 'system', content: 'hint', source: 'hook' },
474
+ ],
475
+ })
476
+ ),
477
+ ],
478
+ });
479
+ const result = await executeHooks({
480
+ registry,
481
+ input: preToolUseInput('Bash'),
482
+ matchQuery: 'Bash',
483
+ });
484
+ expect(result.injectedMessages).toEqual([
485
+ { role: 'user', content: 'steer one', source: 'steer' },
486
+ { role: 'user', content: 'steer two', source: 'steer' },
487
+ { role: 'system', content: 'hint', source: 'hook' },
488
+ ]);
489
+ });
490
+
491
+ it('returns an empty array when no hook sets injectedMessages', async () => {
492
+ const registry = new HookRegistry();
493
+ registry.register('PreToolUse', { hooks: [noopPreHook] });
494
+ const result = await executeHooks({
495
+ registry,
496
+ input: preToolUseInput('Bash'),
497
+ matchQuery: 'Bash',
498
+ });
499
+ expect(result.injectedMessages).toEqual([]);
500
+ });
501
+
502
+ it('skips injectedMessages from fire-and-forget (async) outputs', async () => {
503
+ const registry = new HookRegistry();
504
+ registry.register('PreToolUse', {
505
+ hooks: [
506
+ preToolHook(
507
+ async (): Promise<PreToolUseHookOutput> => ({
508
+ async: true,
509
+ injectedMessages: [
510
+ { role: 'user', content: 'ignored', source: 'steer' },
511
+ ],
512
+ })
513
+ ),
514
+ ],
515
+ });
516
+ const result = await executeHooks({
517
+ registry,
518
+ input: preToolUseInput('Bash'),
519
+ matchQuery: 'Bash',
520
+ });
521
+ expect(result.injectedMessages).toEqual([]);
522
+ });
523
+ });
524
+
442
525
  describe('updatedInput handling', () => {
443
526
  it('last-writer-wins on updatedInput follows registration order', async () => {
444
527
  const registry = new HookRegistry();
@@ -54,6 +54,7 @@ interface AbortRace {
54
54
  function freshResult(): AggregatedHookResult {
55
55
  return {
56
56
  additionalContexts: [],
57
+ injectedMessages: [],
57
58
  errors: [],
58
59
  };
59
60
  }
@@ -244,6 +245,19 @@ function applyContext(agg: AggregatedHookResult, output: HookOutput): void {
244
245
  }
245
246
  }
246
247
 
248
+ function applyInjectedMessages(
249
+ agg: AggregatedHookResult,
250
+ output: HookOutput
251
+ ): void {
252
+ if (
253
+ output.injectedMessages === undefined ||
254
+ output.injectedMessages.length === 0
255
+ ) {
256
+ return;
257
+ }
258
+ agg.injectedMessages.push(...output.injectedMessages);
259
+ }
260
+
247
261
  function applyStopFlag(agg: AggregatedHookResult, output: HookOutput): void {
248
262
  if (output.preventContinuation !== true) {
249
263
  return;
@@ -310,6 +324,7 @@ function fold(outcomes: readonly HookOutcome[]): AggregatedHookResult {
310
324
  continue;
311
325
  }
312
326
  applyContext(agg, output);
327
+ applyInjectedMessages(agg, output);
313
328
  applyStopFlag(agg, output);
314
329
  applyDecision(agg, output);
315
330
  applyUpdatedInput(agg, output);
@@ -9,6 +9,13 @@
9
9
  export { HookRegistry } from './HookRegistry';
10
10
  export type { HookHaltSignal } from './HookRegistry';
11
11
  export { executeHooks, DEFAULT_HOOK_TIMEOUT_MS } from './executeHooks';
12
+ /**
13
+ * Feature probe for hosts: hook outputs support `injectedMessages`
14
+ * (per-message graph-state injection at the `PostToolBatch` boundary).
15
+ * Hosts must gate drain-style hooks on this so a queued message can never
16
+ * be consumed by an SDK version that would silently drop it.
17
+ */
18
+ export const HOOK_INJECTED_MESSAGES_CAPABLE = true;
12
19
  export {
13
20
  matchesQuery,
14
21
  hasNestedQuantifier,
@@ -1,5 +1,6 @@
1
1
  // src/hooks/types.ts
2
2
  import type { BaseMessage } from '@langchain/core/messages';
3
+ import type { InjectedMessage } from '@/types/tools';
3
4
 
4
5
  /**
5
6
  * Closed set of hook lifecycle events supported by the hooks system.
@@ -134,7 +135,9 @@ export interface PostToolBatchEntry {
134
135
  *
135
136
  * Order: fires AFTER all per-tool PostToolUse / PostToolUseFailure hooks
136
137
  * for the same batch have completed, BEFORE the next model call. Pass an
137
- * `additionalContext` to inject context for that next model turn.
138
+ * `additionalContext` to inject context for that next model turn, or
139
+ * `injectedMessages` to inject standalone per-message user speech (e.g.
140
+ * mid-run steering) that must not be consolidated with hook context.
138
141
  */
139
142
  export interface PostToolBatchHookInput extends BaseHookInput {
140
143
  hook_event_name: 'PostToolBatch';
@@ -246,6 +249,18 @@ export type HookInputByEvent = {
246
249
  export interface BaseHookOutput {
247
250
  /** Context string to inject into the conversation. Accumulated across hooks. */
248
251
  additionalContext?: string;
252
+ /**
253
+ * Messages to inject into graph state, one `HumanMessage` per entry
254
+ * (converted via `ToolNode.convertInjectedMessages`, which preserves
255
+ * `role`/`source`/`isMeta` in `additional_kwargs`). Unlike
256
+ * `additionalContext` — which is consolidated across hooks into a single
257
+ * system-flavored message — each entry keeps its own identity and role,
258
+ * making this the channel for injecting verbatim user speech (e.g. a
259
+ * mid-run steering message). Accumulated across hooks in registration
260
+ * order. Currently consumed only at the `PostToolBatch` dispatch site;
261
+ * other events ignore the field.
262
+ */
263
+ injectedMessages?: InjectedMessage[];
249
264
  /** True to prevent the next model turn. Any hook can set this. */
250
265
  preventContinuation?: boolean;
251
266
  /** Reason reported alongside `preventContinuation`. */
@@ -508,6 +523,8 @@ export interface AggregatedHookResult {
508
523
  updatedOutput?: unknown;
509
524
  /** Accumulated `additionalContext` strings from every hook, in order. */
510
525
  additionalContexts: string[];
526
+ /** Accumulated `injectedMessages` from every hook, in registration order. */
527
+ injectedMessages: InjectedMessage[];
511
528
  /** True if any hook returned `preventContinuation`. */
512
529
  preventContinuation?: boolean;
513
530
  /**
package/src/langfuse.ts CHANGED
@@ -1,9 +1,20 @@
1
1
  import { CallbackHandler } from '@langfuse/langchain';
2
2
  import { context as otelContext } from '@opentelemetry/api';
3
+ import { AIMessage, AIMessageChunk } from '@langchain/core/messages';
3
4
  import {
4
5
  getLangfuseTracerProvider,
5
6
  propagateAttributes,
6
7
  } from '@langfuse/tracing';
8
+ import type {
9
+ AIMessageChunkFields,
10
+ AIMessageFields,
11
+ UsageMetadata,
12
+ } from '@langchain/core/messages';
13
+ import type {
14
+ ChatGeneration,
15
+ Generation,
16
+ LLMResult,
17
+ } from '@langchain/core/outputs';
7
18
  import type { PropagateAttributesParams } from '@langfuse/tracing';
8
19
  import type * as t from '@/types';
9
20
  import {
@@ -43,6 +54,116 @@ type FlushableTracerProvider = {
43
54
  forceFlush?: () => Promise<void> | void;
44
55
  };
45
56
 
57
+ type BedrockResponseUsage = {
58
+ inputTokens?: number;
59
+ cacheReadInputTokens?: number;
60
+ cacheWriteInputTokens?: number;
61
+ };
62
+
63
+ type BedrockResponseMetadata = {
64
+ metadata?: {
65
+ usage?: BedrockResponseUsage;
66
+ };
67
+ };
68
+
69
+ function getLangfuseBedrockUsage(
70
+ message: AIMessage | AIMessageChunk
71
+ ): UsageMetadata | undefined {
72
+ const usageMetadata = message.usage_metadata;
73
+ const bedrockUsage = (message.response_metadata as BedrockResponseMetadata)
74
+ .metadata?.usage;
75
+ if (
76
+ usageMetadata == null ||
77
+ bedrockUsage == null ||
78
+ usageMetadata.input_tokens !== bedrockUsage.inputTokens
79
+ ) {
80
+ return usageMetadata;
81
+ }
82
+
83
+ const cacheRead = bedrockUsage.cacheReadInputTokens ?? 0;
84
+ const cacheCreation = bedrockUsage.cacheWriteInputTokens ?? 0;
85
+ if (cacheRead === 0 && cacheCreation === 0) {
86
+ return usageMetadata;
87
+ }
88
+
89
+ return {
90
+ ...usageMetadata,
91
+ input_tokens: usageMetadata.input_tokens + cacheRead + cacheCreation,
92
+ };
93
+ }
94
+
95
+ function cloneMessageWithUsage(
96
+ message: AIMessage | AIMessageChunk,
97
+ usageMetadata: UsageMetadata
98
+ ): AIMessage | AIMessageChunk {
99
+ const fields: AIMessageFields = {
100
+ content: message.content,
101
+ additional_kwargs: message.additional_kwargs,
102
+ response_metadata: message.response_metadata,
103
+ id: message.id,
104
+ name: message.name,
105
+ tool_calls: message.tool_calls,
106
+ invalid_tool_calls: message.invalid_tool_calls,
107
+ usage_metadata: usageMetadata,
108
+ };
109
+
110
+ if (message instanceof AIMessageChunk) {
111
+ const chunkFields: AIMessageChunkFields = {
112
+ ...fields,
113
+ tool_call_chunks: message.tool_call_chunks,
114
+ };
115
+ return new AIMessageChunk(chunkFields);
116
+ }
117
+
118
+ return new AIMessage(fields);
119
+ }
120
+
121
+ function normalizeGenerationForLangfuse(generation: Generation): Generation {
122
+ if (!('message' in generation)) {
123
+ return generation;
124
+ }
125
+
126
+ const message = (generation as ChatGeneration).message;
127
+ if (!(message instanceof AIMessage || message instanceof AIMessageChunk)) {
128
+ return generation;
129
+ }
130
+
131
+ const usageMetadata = getLangfuseBedrockUsage(message);
132
+ if (usageMetadata == null || usageMetadata === message.usage_metadata) {
133
+ return generation;
134
+ }
135
+
136
+ const chatGeneration: ChatGeneration = {
137
+ ...(generation as ChatGeneration),
138
+ message: cloneMessageWithUsage(message, usageMetadata),
139
+ };
140
+ return chatGeneration;
141
+ }
142
+
143
+ function normalizeBedrockUsageForLangfuse(output: LLMResult): LLMResult {
144
+ if (output.generations.length === 0) {
145
+ return output;
146
+ }
147
+
148
+ const listIndex = output.generations.length - 1;
149
+ const generationList = output.generations[listIndex];
150
+ if (generationList.length === 0) {
151
+ return output;
152
+ }
153
+
154
+ const generationIndex = generationList.length - 1;
155
+ const generation = generationList[generationIndex];
156
+ const normalized = normalizeGenerationForLangfuse(generation);
157
+ if (normalized === generation) {
158
+ return output;
159
+ }
160
+
161
+ const generations = [...output.generations];
162
+ generations[listIndex] = [...generationList];
163
+ generations[listIndex][generationIndex] = normalized;
164
+ return { ...output, generations };
165
+ }
166
+
46
167
  class ScopedLangfuseCallbackHandler extends CallbackHandler {
47
168
  private readonly langfuse?: t.LangfuseConfig;
48
169
  private readonly traceIdSeed?: string;
@@ -107,6 +228,18 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
107
228
  return this.withRuntimeContext(() => super.handleLLMStart(...args));
108
229
  }
109
230
 
231
+ override handleLLMEnd(
232
+ output: LLMResult,
233
+ runId: string,
234
+ parentRunId?: string
235
+ ): Promise<void> {
236
+ return super.handleLLMEnd(
237
+ normalizeBedrockUsageForLangfuse(output),
238
+ runId,
239
+ parentRunId
240
+ );
241
+ }
242
+
110
243
  override handleToolStart(
111
244
  ...args: Parameters<CallbackHandler['handleToolStart']>
112
245
  ): ReturnType<CallbackHandler['handleToolStart']> {
@@ -351,7 +484,7 @@ export function isLangfuseCallbackHandler(value: unknown): boolean {
351
484
  export async function disposeLangfuseHandler(value: unknown): Promise<void> {
352
485
  if (
353
486
  value == null ||
354
- !parseBooleanEnv(process.env[LANGFUSE_FORCE_FLUSH_ON_DISPOSE])
487
+ parseBooleanEnv(process.env[LANGFUSE_FORCE_FLUSH_ON_DISPOSE]) !== true
355
488
  ) {
356
489
  return;
357
490
  }