@librechat/agents 3.1.70 → 3.1.71-dev.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/cjs/graphs/Graph.cjs +52 -0
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/llm/invoke.cjs +13 -2
  4. package/dist/cjs/llm/invoke.cjs.map +1 -1
  5. package/dist/cjs/main.cjs +4 -0
  6. package/dist/cjs/main.cjs.map +1 -1
  7. package/dist/cjs/messages/prune.cjs +9 -2
  8. package/dist/cjs/messages/prune.cjs.map +1 -1
  9. package/dist/cjs/run.cjs +4 -0
  10. package/dist/cjs/run.cjs.map +1 -1
  11. package/dist/cjs/tools/BashExecutor.cjs +43 -0
  12. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  13. package/dist/cjs/tools/ToolNode.cjs +482 -45
  14. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  15. package/dist/cjs/tools/toolOutputReferences.cjs +657 -0
  16. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -0
  17. package/dist/cjs/utils/truncation.cjs +28 -0
  18. package/dist/cjs/utils/truncation.cjs.map +1 -1
  19. package/dist/esm/graphs/Graph.mjs +52 -0
  20. package/dist/esm/graphs/Graph.mjs.map +1 -1
  21. package/dist/esm/llm/invoke.mjs +13 -2
  22. package/dist/esm/llm/invoke.mjs.map +1 -1
  23. package/dist/esm/main.mjs +2 -2
  24. package/dist/esm/messages/prune.mjs +9 -2
  25. package/dist/esm/messages/prune.mjs.map +1 -1
  26. package/dist/esm/run.mjs +4 -0
  27. package/dist/esm/run.mjs.map +1 -1
  28. package/dist/esm/tools/BashExecutor.mjs +42 -1
  29. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  30. package/dist/esm/tools/ToolNode.mjs +482 -45
  31. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  32. package/dist/esm/tools/toolOutputReferences.mjs +649 -0
  33. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -0
  34. package/dist/esm/utils/truncation.mjs +27 -1
  35. package/dist/esm/utils/truncation.mjs.map +1 -1
  36. package/dist/types/graphs/Graph.d.ts +28 -0
  37. package/dist/types/llm/invoke.d.ts +9 -0
  38. package/dist/types/run.d.ts +1 -0
  39. package/dist/types/tools/BashExecutor.d.ts +31 -0
  40. package/dist/types/tools/ToolNode.d.ts +84 -3
  41. package/dist/types/tools/toolOutputReferences.d.ts +236 -0
  42. package/dist/types/types/index.d.ts +1 -0
  43. package/dist/types/types/messages.d.ts +26 -0
  44. package/dist/types/types/run.d.ts +9 -1
  45. package/dist/types/types/tools.d.ts +70 -0
  46. package/dist/types/utils/truncation.d.ts +21 -0
  47. package/package.json +1 -1
  48. package/src/graphs/Graph.ts +55 -0
  49. package/src/llm/invoke.test.ts +442 -0
  50. package/src/llm/invoke.ts +23 -2
  51. package/src/messages/prune.ts +9 -2
  52. package/src/run.ts +4 -0
  53. package/src/specs/prune.test.ts +413 -0
  54. package/src/tools/BashExecutor.ts +45 -0
  55. package/src/tools/ToolNode.ts +631 -55
  56. package/src/tools/__tests__/BashExecutor.test.ts +36 -0
  57. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +1438 -0
  58. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +419 -0
  59. package/src/tools/__tests__/toolOutputReferences.test.ts +415 -0
  60. package/src/tools/toolOutputReferences.ts +813 -0
  61. package/src/types/index.ts +1 -0
  62. package/src/types/messages.ts +27 -0
  63. package/src/types/run.ts +9 -1
  64. package/src/types/tools.ts +71 -0
  65. package/src/utils/__tests__/truncation.test.ts +66 -0
  66. package/src/utils/truncation.ts +30 -0
@@ -1,6 +1,7 @@
1
1
  // src/types/index.ts
2
2
  export * from './graph';
3
3
  export * from './llm';
4
+ export * from './messages';
4
5
  export * from './run';
5
6
  export * from './skill';
6
7
  export * from './stream';
@@ -2,3 +2,30 @@ import type Anthropic from '@anthropic-ai/sdk';
2
2
  import type { BaseMessage } from '@langchain/core/messages';
3
3
  export type AnthropicMessages = Array<AnthropicMessage | BaseMessage>;
4
4
  export type AnthropicMessage = Anthropic.MessageParam;
5
+
6
+ /**
7
+ * Per-message ref metadata stamped onto a `ToolMessage` at execution
8
+ * time. Read by `annotateMessagesForLLM` to apply transient annotation
9
+ * to a copy of the message right before it goes on the wire to the
10
+ * provider. Never read after the run-scoped registry has been cleared.
11
+ *
12
+ * Lives in `ToolMessage.additional_kwargs`. LangChain's provider
13
+ * serializers don't transmit `additional_kwargs` to provider APIs, so
14
+ * the metadata never leaks even if you forget to clean it.
15
+ */
16
+ export interface ToolMessageRefMetadata {
17
+ /** Key under which this message's untruncated output was registered. */
18
+ _refKey?: string;
19
+ /**
20
+ * Registry bucket scope under which `_refKey` was stored. For named
21
+ * runs this equals `config.configurable.run_id`; for anonymous
22
+ * invocations (no `run_id`) ToolNode mints a per-batch synthetic
23
+ * scope (`\0anon-<n>`) so concurrent batches don't collide. Stamping
24
+ * the scope on the message itself lets `annotateMessagesForLLM`
25
+ * recover it without re-deriving from config — which is impossible
26
+ * for the anonymous case, since the scope is internal to ToolNode.
27
+ */
28
+ _refScope?: string;
29
+ /** Placeholders the model used that could not be resolved this batch. */
30
+ _unresolvedRefs?: string[];
31
+ }
package/src/types/run.ts CHANGED
@@ -11,7 +11,7 @@ import type * as s from '@/types/stream';
11
11
  import type * as e from '@/common/enum';
12
12
  import type * as g from '@/types/graph';
13
13
  import type * as l from '@/types/llm';
14
- import type { ToolSessionMap } from '@/types/tools';
14
+ import type { ToolSessionMap, ToolOutputReferencesConfig } from '@/types/tools';
15
15
  import type { HookRegistry } from '@/hooks';
16
16
 
17
17
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -146,6 +146,14 @@ export type RunConfig = {
146
146
  * at run start, so ToolNode can inject session_id + files into tool calls.
147
147
  */
148
148
  initialSessions?: ToolSessionMap;
149
+ /**
150
+ * Run-scoped tool output reference configuration. When `enabled` is
151
+ * `true`, tool outputs are registered under stable keys
152
+ * (`tool<idx>turn<turn>`) and subsequent tool calls can pipe previous
153
+ * outputs into their arguments via `{{tool<idx>turn<turn>}}`
154
+ * placeholders. Disabled by default so existing runs are unaffected.
155
+ */
156
+ toolOutputReferences?: ToolOutputReferencesConfig;
149
157
  };
150
158
 
151
159
  export type ProvidedCallbacks =
@@ -3,6 +3,7 @@ import type { StructuredToolInterface } from '@langchain/core/tools';
3
3
  import type { RunnableToolLike } from '@langchain/core/runnables';
4
4
  import type { ToolCall } from '@langchain/core/messages/tool';
5
5
  import type { HookRegistry } from '@/hooks';
6
+ import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
6
7
  import type { MessageContentComplex, ToolErrorData } from './stream';
7
8
 
8
9
  /** Replacement type for `import type { ToolCall } from '@langchain/core/messages/tool'` in order to have stringified args typed */
@@ -62,6 +63,22 @@ export type ToolNodeOptions = {
62
63
  * When provided, takes precedence over the value computed from maxContextTokens.
63
64
  */
64
65
  maxToolResultChars?: number;
66
+ /**
67
+ * Run-scoped tool output reference configuration. When `enabled` is
68
+ * `true`, ToolNode registers successful outputs and substitutes
69
+ * `{{tool<idx>turn<turn>}}` placeholders found in string args.
70
+ *
71
+ * Ignored when `toolOutputRegistry` is also provided (host-supplied
72
+ * registry wins).
73
+ */
74
+ toolOutputReferences?: ToolOutputReferencesConfig;
75
+ /**
76
+ * Pre-constructed registry instance shared across ToolNodes for the
77
+ * run. Graphs pass the same registry to every ToolNode they compile
78
+ * so cross-agent `{{tool<i>turn<n>}}` substitutions resolve. Takes
79
+ * precedence over `toolOutputReferences` when both are set.
80
+ */
81
+ toolOutputRegistry?: ToolOutputReferenceRegistry;
65
82
  };
66
83
 
67
84
  export type ToolNodeConstructorParams = ToolRefs & ToolNodeOptions;
@@ -234,6 +251,60 @@ export type ToolExecuteResult = {
234
251
  /** Map of tool names to tool definitions */
235
252
  export type LCToolRegistry = Map<string, LCTool>;
236
253
 
254
+ /**
255
+ * Run-scoped configuration for tool output references.
256
+ *
257
+ * When enabled, each successful tool result is registered under a stable
258
+ * key (`tool<idx>turn<turn>`). Later tool calls can pipe a previous
259
+ * output into their arguments by including the literal placeholder
260
+ * `{{tool<idx>turn<turn>}}` anywhere in a string argument; ToolNode
261
+ * substitutes it with the stored output immediately before invoking
262
+ * the tool.
263
+ *
264
+ * The registry stores the *raw, untruncated* tool output (subject to
265
+ * its own size caps) so a later substitution can pipe the full payload
266
+ * into the next tool even when the LLM only saw a head+tail-truncated
267
+ * preview in `ToolMessage.content`. Size limits are decoupled from the
268
+ * LLM-visible truncation budget and default to 5 MB total.
269
+ *
270
+ * Known limitations:
271
+ * - Tools that return a `ToolMessage` with array-type content
272
+ * (multi-part content blocks such as text + image) are not
273
+ * registered and cannot be cited via `{{tool<i>turn<n>}}`. A
274
+ * warning is logged so the missing reference is visible.
275
+ * - When a `PostToolUse` hook replaces `ToolMessage.content`, the
276
+ * *post-hook* content is what gets stored in the registry (and
277
+ * what the model sees), so `{{…}}` substitutions deliver the
278
+ * hooked output rather than the raw tool return. This matches the
279
+ * hook's "authoritative" role for output shaping.
280
+ */
281
+ export type ToolOutputReferencesConfig = {
282
+ /** Enable the registry and placeholder substitution. Defaults to `false`. */
283
+ enabled?: boolean;
284
+ /**
285
+ * Maximum characters stored (and substituted) per registered output.
286
+ * Applied to the *raw* output before storage. Defaults to
287
+ * `HARD_MAX_TOOL_RESULT_CHARS` (~400 KB) — matching the
288
+ * LLM-visible tool-result truncation budget, which is also a safe
289
+ * payload size for shell `ARG_MAX` limits when a `{{…}}` expansion
290
+ * gets piped into a bash `command`. Hosts that want to preserve
291
+ * fuller fidelity (for example for non-bash API consumers) can
292
+ * raise this up to `maxTotalSize` (defaults to 5 MB) — be aware
293
+ * that large single-output substitutions may exceed shell
294
+ * argument-size limits on typical Linux/macOS.
295
+ */
296
+ maxOutputSize?: number;
297
+ /**
298
+ * Hard cap on total characters retained across all registered outputs
299
+ * for the run. When exceeded, the oldest entries are evicted FIFO
300
+ * until the total fits. The effective per-output cap is
301
+ * `min(maxOutputSize, maxTotalSize)` so a single stored output can
302
+ * never exceed the aggregate bound. Defaults to
303
+ * `calculateMaxTotalToolOutputSize(maxOutputSize)` (5 MB).
304
+ */
305
+ maxTotalSize?: number;
306
+ };
307
+
237
308
  export type ProgrammaticCache = { toolMap: ToolMap; toolDefs: LCTool[] };
238
309
 
239
310
  /** Search mode: code_interpreter uses external sandbox, local uses safe substring matching */
@@ -0,0 +1,66 @@
1
+ import { describe, it, expect } from '@jest/globals';
2
+ import {
3
+ HARD_MAX_TOOL_RESULT_CHARS,
4
+ HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE,
5
+ calculateMaxToolResultChars,
6
+ calculateMaxTotalToolOutputSize,
7
+ } from '@/utils/truncation';
8
+
9
+ describe('truncation helpers', () => {
10
+ describe('calculateMaxToolResultChars', () => {
11
+ it('returns the hard cap when context tokens are missing', () => {
12
+ expect(calculateMaxToolResultChars()).toBe(HARD_MAX_TOOL_RESULT_CHARS);
13
+ expect(calculateMaxToolResultChars(undefined)).toBe(
14
+ HARD_MAX_TOOL_RESULT_CHARS
15
+ );
16
+ expect(calculateMaxToolResultChars(0)).toBe(HARD_MAX_TOOL_RESULT_CHARS);
17
+ expect(calculateMaxToolResultChars(-100)).toBe(
18
+ HARD_MAX_TOOL_RESULT_CHARS
19
+ );
20
+ });
21
+
22
+ it('computes 30% of context-window characters for normal inputs', () => {
23
+ // 100k tokens * 0.3 = 30k tokens * 4 chars/token = 120k chars
24
+ expect(calculateMaxToolResultChars(100_000)).toBe(120_000);
25
+ });
26
+
27
+ it('clamps to the hard cap for large context windows', () => {
28
+ // 1M tokens * 0.3 * 4 = 1.2M chars, exceeds 400k cap
29
+ expect(calculateMaxToolResultChars(1_000_000)).toBe(
30
+ HARD_MAX_TOOL_RESULT_CHARS
31
+ );
32
+ });
33
+ });
34
+
35
+ describe('calculateMaxTotalToolOutputSize', () => {
36
+ it('returns the absolute hard cap when no per-output is provided', () => {
37
+ expect(calculateMaxTotalToolOutputSize()).toBe(
38
+ HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE
39
+ );
40
+ expect(calculateMaxTotalToolOutputSize(0)).toBe(
41
+ HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE
42
+ );
43
+ expect(calculateMaxTotalToolOutputSize(-1)).toBe(
44
+ HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE
45
+ );
46
+ });
47
+
48
+ it('doubles the per-output cap by default', () => {
49
+ expect(calculateMaxTotalToolOutputSize(100_000)).toBe(200_000);
50
+ expect(calculateMaxTotalToolOutputSize(1)).toBe(2);
51
+ });
52
+
53
+ it('clamps the doubled value to HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE', () => {
54
+ // 4M * 2 = 8M, exceeds 5M
55
+ expect(calculateMaxTotalToolOutputSize(4_000_000)).toBe(
56
+ HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE
57
+ );
58
+ // Right at the boundary: 2.5M * 2 = 5M (no clamp).
59
+ expect(calculateMaxTotalToolOutputSize(2_500_000)).toBe(5_000_000);
60
+ // Just past it: 2_500_001 * 2 = 5_000_002 -> clamped.
61
+ expect(calculateMaxTotalToolOutputSize(2_500_001)).toBe(
62
+ HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE
63
+ );
64
+ });
65
+ });
66
+ });
@@ -12,6 +12,16 @@
12
12
  */
13
13
  export const HARD_MAX_TOOL_RESULT_CHARS = 400_000;
14
14
 
15
+ /**
16
+ * Absolute hard cap on the aggregate size (characters) of all registered
17
+ * tool outputs kept for `{{tool<i>turn<n>}}` substitution. Set at 5 MB
18
+ * because the registry stores *raw, untruncated* tool output — full
19
+ * fidelity for piping into downstream bash/jq — so the budget needs
20
+ * enough headroom to keep a handful of large responses without
21
+ * ballooning unbounded.
22
+ */
23
+ export const HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE = 5_000_000;
24
+
15
25
  /**
16
26
  * Computes the dynamic max tool result size based on the model's context window.
17
27
  * Uses 30% of the context window (in estimated characters, ~4 chars/token)
@@ -32,6 +42,26 @@ export function calculateMaxToolResultChars(
32
42
  );
33
43
  }
34
44
 
45
+ /**
46
+ * Computes the default aggregate size (characters) for the tool output
47
+ * reference registry based on the per-output budget. Mirrors
48
+ * `calculateMaxToolResultChars`'s shape: a multiple of the per-output
49
+ * cap, clamped to `HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE`.
50
+ *
51
+ * @param maxOutputSize - Per-output maximum characters (e.g., the
52
+ * ToolNode's `maxToolResultChars`). When omitted or non-positive,
53
+ * falls back to the absolute total cap.
54
+ * @returns Maximum total characters retained across the registry.
55
+ */
56
+ export function calculateMaxTotalToolOutputSize(
57
+ maxOutputSize?: number
58
+ ): number {
59
+ if (maxOutputSize == null || maxOutputSize <= 0) {
60
+ return HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE;
61
+ }
62
+ return Math.min(maxOutputSize * 2, HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE);
63
+ }
64
+
35
65
  /**
36
66
  * Truncates a tool-call input (the arguments/payload of a tool_use block)
37
67
  * using head+tail strategy. Returns an object with `_truncated` (the