@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
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Tool output reference registry.
3
+ *
4
+ * When enabled via `RunConfig.toolOutputReferences.enabled`, ToolNode
5
+ * stores each successful tool output under a stable key
6
+ * (`tool<idx>turn<turn>`) where `idx` is the tool's position within a
7
+ * ToolNode batch and `turn` is the batch index within the run
8
+ * (incremented once per ToolNode invocation).
9
+ *
10
+ * Subsequent tool calls can pipe a previous output into their args by
11
+ * embedding `{{tool<idx>turn<turn>}}` inside any string argument;
12
+ * {@link ToolOutputReferenceRegistry.resolve} walks the args and
13
+ * substitutes the placeholders immediately before invocation.
14
+ *
15
+ * The registry stores the *raw, untruncated* tool output so a later
16
+ * `{{…}}` substitution pipes the full payload into the next tool —
17
+ * even when the LLM only saw a head+tail-truncated preview in
18
+ * `ToolMessage.content`. Outputs are stored without any annotation
19
+ * (the `_ref` key or the `[ref: ...]` prefix seen by the LLM is
20
+ * strictly a UX signal attached to `ToolMessage.content`). Keeping the
21
+ * registry pristine means downstream bash/jq piping receives the
22
+ * complete, verbatim output with no injected fields.
23
+ */
24
+ import type { BaseMessage } from '@langchain/core/messages';
25
+ /**
26
+ * Non-global matcher for a single `{{tool<i>turn<n>}}` placeholder.
27
+ * Exported for consumers that want to detect references (e.g., syntax
28
+ * highlighting, docs). The stateful `g` variant lives inside the
29
+ * registry so nobody trips on `lastIndex`.
30
+ */
31
+ export declare const TOOL_OUTPUT_REF_PATTERN: RegExp;
32
+ /** Object key used when a parsed-object output has `_ref` injected. */
33
+ export declare const TOOL_OUTPUT_REF_KEY = "_ref";
34
+ /**
35
+ * Object key used to carry unresolved reference warnings on a parsed-
36
+ * object output. Using a dedicated field instead of a trailing text
37
+ * line keeps the annotated `ToolMessage.content` parseable as JSON for
38
+ * downstream consumers that rely on the object shape.
39
+ */
40
+ export declare const TOOL_OUTPUT_UNRESOLVED_KEY = "_unresolved_refs";
41
+ /** Single-line prefix prepended to non-object tool outputs so the LLM sees the reference key. */
42
+ export declare function buildReferencePrefix(key: string): string;
43
+ /** Stable registry key for a tool output. */
44
+ export declare function buildReferenceKey(toolIndex: number, turn: number): string;
45
+ export type ToolOutputReferenceRegistryOptions = {
46
+ /** Maximum characters stored per registered output. */
47
+ maxOutputSize?: number;
48
+ /** Maximum total characters retained across all registered outputs. */
49
+ maxTotalSize?: number;
50
+ /**
51
+ * Upper bound on the number of concurrently-tracked runs. When
52
+ * exceeded, the oldest run bucket is evicted (FIFO). Defaults to 32.
53
+ */
54
+ maxActiveRuns?: number;
55
+ };
56
+ /**
57
+ * Result of resolving placeholders in tool args.
58
+ */
59
+ export type ResolveResult<T> = {
60
+ /** Arguments with placeholders replaced. Same shape as the input. */
61
+ resolved: T;
62
+ /** Reference keys that were referenced but had no stored value. */
63
+ unresolved: string[];
64
+ };
65
+ /**
66
+ * Read-only view over a frozen registry snapshot. Returned by
67
+ * {@link ToolOutputReferenceRegistry.snapshot} for callers that need
68
+ * to resolve placeholders against the registry state at a specific
69
+ * point in time, ignoring any subsequent registrations.
70
+ */
71
+ export interface ToolOutputResolveView {
72
+ resolve<T>(args: T): ResolveResult<T>;
73
+ }
74
+ /**
75
+ * Pre-resolved arg map keyed by `toolCallId`. Used by the mixed
76
+ * direct+event dispatch path to feed event calls' resolved args
77
+ * (captured pre-batch) into the dispatcher without re-resolving
78
+ * against the now-stale live registry.
79
+ */
80
+ export type PreResolvedArgsMap = Map<string, {
81
+ resolved: Record<string, unknown>;
82
+ unresolved: string[];
83
+ }>;
84
+ /**
85
+ * Per-call sink for resolved args, keyed by `toolCallId`. Threaded
86
+ * as a per-batch local map so concurrent `ToolNode.run()` calls do
87
+ * not race on shared sink state.
88
+ */
89
+ export type ResolvedArgsByCallId = Map<string, Record<string, unknown>>;
90
+ /**
91
+ * Ordered map of reference-key → stored output, partitioned by run so
92
+ * concurrent / interleaved runs sharing one registry cannot leak
93
+ * outputs between each other.
94
+ *
95
+ * Each public method takes a `runId` which selects the run's bucket.
96
+ * Hosts typically get one registry per run via `Graph`, in which
97
+ * case only a single bucket is ever populated; the partitioning
98
+ * exists so the registry also behaves correctly when a single
99
+ * instance is reused directly.
100
+ */
101
+ export declare class ToolOutputReferenceRegistry {
102
+ private runStates;
103
+ private readonly maxOutputSize;
104
+ private readonly maxTotalSize;
105
+ private readonly maxActiveRuns;
106
+ /**
107
+ * Local stateful matcher used only by `replaceInString`. Kept
108
+ * off-module so callers of the exported `TOOL_OUTPUT_REF_PATTERN`
109
+ * never see a stale `lastIndex`.
110
+ */
111
+ private static readonly PLACEHOLDER_MATCHER;
112
+ constructor(options?: ToolOutputReferenceRegistryOptions);
113
+ private keyFor;
114
+ private getOrCreate;
115
+ /** Registers (or replaces) the output stored under `key` for `runId`. */
116
+ set(runId: string | undefined, key: string, value: string): void;
117
+ /** Returns the stored value for `key` in `runId`'s bucket, or `undefined`. */
118
+ get(runId: string | undefined, key: string): string | undefined;
119
+ /**
120
+ * Returns `true` when `key` is currently stored in `runId`'s bucket.
121
+ * Used by {@link annotateMessagesForLLM} to gate transient annotation
122
+ * on whether the registry still owns the referenced output (a stale
123
+ * `_refKey` from a prior run silently no-ops here).
124
+ */
125
+ has(runId: string | undefined, key: string): boolean;
126
+ /** Total number of registered outputs across every run bucket. */
127
+ get size(): number;
128
+ /** Maximum characters retained per output (post-clip). */
129
+ get perOutputLimit(): number;
130
+ /** Maximum total characters retained *per run*. */
131
+ get totalLimit(): number;
132
+ /** Drops every run's state. */
133
+ clear(): void;
134
+ /**
135
+ * Explicitly release `runId`'s state. Safe to call when a run has
136
+ * finished. Hosts sharing one registry across runs should call this
137
+ * to reclaim memory deterministically; otherwise LRU eviction kicks
138
+ * in when `maxActiveRuns` runs accumulate.
139
+ */
140
+ releaseRun(runId: string | undefined): void;
141
+ /**
142
+ * Claims the next batch turn synchronously from `runId`'s bucket.
143
+ *
144
+ * Must be called once at the start of each ToolNode batch before
145
+ * any `await`, so concurrent invocations within the same run see
146
+ * distinct turn values (reads are effectively atomic by JS's
147
+ * single-threaded execution of the sync prefix).
148
+ *
149
+ * If `runId` is missing the anonymous bucket is dropped and a
150
+ * fresh one created so each anonymous call behaves as its own run.
151
+ */
152
+ nextTurn(runId: string | undefined): number;
153
+ /**
154
+ * Records that `toolName` has been warned about in `runId` (returns
155
+ * `true` on the first call per run, `false` after). Used by
156
+ * ToolNode to emit one log line per offending tool per run when a
157
+ * `ToolMessage.content` isn't a string.
158
+ */
159
+ claimWarnOnce(runId: string | undefined, toolName: string): boolean;
160
+ /**
161
+ * Walks `args` and replaces every `{{tool<i>turn<n>}}` placeholder in
162
+ * string values with the stored output *from `runId`'s bucket*. Non-
163
+ * string values and object keys are left untouched. Unresolved
164
+ * references are left in-place and reported so the caller can
165
+ * surface them to the LLM. When no placeholder appears anywhere in
166
+ * the serialized args, the original input is returned without
167
+ * walking the tree.
168
+ */
169
+ resolve<T>(runId: string | undefined, args: T): ResolveResult<T>;
170
+ /**
171
+ * Captures a frozen snapshot of `runId`'s current entries and
172
+ * returns a view that resolves placeholders against *only* that
173
+ * snapshot. The snapshot is decoupled from the live registry, so
174
+ * subsequent `set()` calls (for example, same-turn direct outputs
175
+ * registering while an event branch is still in flight) are
176
+ * invisible to the snapshot's `resolve`. Used by the mixed
177
+ * direct+event dispatch path to preserve same-turn isolation when
178
+ * a `PreToolUse` hook rewrites event args after directs have
179
+ * completed.
180
+ */
181
+ snapshot(runId: string | undefined): ToolOutputResolveView;
182
+ private resolveAgainst;
183
+ private transform;
184
+ private replaceInString;
185
+ private evictWithinBucket;
186
+ }
187
+ /**
188
+ * Annotates `content` with a reference key and/or unresolved-ref
189
+ * warnings so the LLM sees both alongside the tool output.
190
+ *
191
+ * Behavior:
192
+ * - If `content` parses as a plain (non-array, non-null) JSON object
193
+ * and the object does not already have a conflicting `_ref` key,
194
+ * the reference key and (when present) `_unresolved_refs` array
195
+ * are injected as object fields, preserving JSON validity for
196
+ * downstream consumers that parse the output.
197
+ * - Otherwise (string output, JSON array/primitive, parse failure,
198
+ * or `_ref` collision), a `[ref: <key>]\n` prefix line is
199
+ * prepended and unresolved refs are appended as a trailing
200
+ * `[unresolved refs: …]` line.
201
+ *
202
+ * The annotated string is what the LLM sees as `ToolMessage.content`.
203
+ * The *original* (un-annotated) value is what gets stored in the
204
+ * registry, so downstream piping remains pristine.
205
+ *
206
+ * @param content Raw (post-truncation) tool output.
207
+ * @param key Reference key for this output, or undefined when
208
+ * there is nothing to register (errors etc.).
209
+ * @param unresolved Reference keys that failed to resolve during
210
+ * argument substitution. Surfaced so the LLM can
211
+ * self-correct its next tool call.
212
+ */
213
+ export declare function annotateToolOutputWithReference(content: string, key: string | undefined, unresolved?: string[]): string;
214
+ /**
215
+ * Lazy projection that, given a registry and a runId, returns a new
216
+ * `messages` array where each `ToolMessage` carrying ref metadata is
217
+ * projected into a transient copy with annotated content (when the ref
218
+ * is live in the registry) and with the framework-owned `additional_
219
+ * kwargs` keys (`_refKey`, `_refScope`, `_unresolvedRefs`) stripped
220
+ * regardless of whether annotation applied. The original input array
221
+ * and its messages are never mutated.
222
+ *
223
+ * Annotation is gated on registry presence: a stale `_refKey` from a
224
+ * prior run (e.g. one that survived in persisted history) silently
225
+ * no-ops on the *content* side. The strip-metadata side still runs so
226
+ * stale framework keys never leak onto the wire under any custom or
227
+ * future provider serializer that might transmit `additional_kwargs`.
228
+ * `_unresolvedRefs` is always meaningful and is not gated.
229
+ *
230
+ * **Feature-disabled fast path:** when the host hasn't enabled the
231
+ * tool-output-reference feature, the registry is `undefined` and this
232
+ * function returns the input array reference-equal *without iterating
233
+ * a single message*. The loop is exclusive to the feature-enabled
234
+ * code path.
235
+ */
236
+ export declare function annotateMessagesForLLM(messages: BaseMessage[], registry: ToolOutputReferenceRegistry | undefined, runId: string | undefined): BaseMessage[];
@@ -1,5 +1,6 @@
1
1
  export * from './graph';
2
2
  export * from './llm';
3
+ export * from './messages';
3
4
  export * from './run';
4
5
  export * from './skill';
5
6
  export * from './stream';
@@ -2,3 +2,29 @@ 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
+ * Per-message ref metadata stamped onto a `ToolMessage` at execution
7
+ * time. Read by `annotateMessagesForLLM` to apply transient annotation
8
+ * to a copy of the message right before it goes on the wire to the
9
+ * provider. Never read after the run-scoped registry has been cleared.
10
+ *
11
+ * Lives in `ToolMessage.additional_kwargs`. LangChain's provider
12
+ * serializers don't transmit `additional_kwargs` to provider APIs, so
13
+ * the metadata never leaks even if you forget to clean it.
14
+ */
15
+ export interface ToolMessageRefMetadata {
16
+ /** Key under which this message's untruncated output was registered. */
17
+ _refKey?: string;
18
+ /**
19
+ * Registry bucket scope under which `_refKey` was stored. For named
20
+ * runs this equals `config.configurable.run_id`; for anonymous
21
+ * invocations (no `run_id`) ToolNode mints a per-batch synthetic
22
+ * scope (`\0anon-<n>`) so concurrent batches don't collide. Stamping
23
+ * the scope on the message itself lets `annotateMessagesForLLM`
24
+ * recover it without re-deriving from config — which is impossible
25
+ * for the anonymous case, since the scope is internal to ToolNode.
26
+ */
27
+ _refScope?: string;
28
+ /** Placeholders the model used that could not be resolved this batch. */
29
+ _unresolvedRefs?: string[];
30
+ }
@@ -7,7 +7,7 @@ import type * as s from '@/types/stream';
7
7
  import type * as e from '@/common/enum';
8
8
  import type * as g from '@/types/graph';
9
9
  import type * as l from '@/types/llm';
10
- import type { ToolSessionMap } from '@/types/tools';
10
+ import type { ToolSessionMap, ToolOutputReferencesConfig } from '@/types/tools';
11
11
  import type { HookRegistry } from '@/hooks';
12
12
  export type ZodObjectAny = z.ZodObject<any, any, any, any>;
13
13
  export type BaseGraphConfig = {
@@ -134,6 +134,14 @@ export type RunConfig = {
134
134
  * at run start, so ToolNode can inject session_id + files into tool calls.
135
135
  */
136
136
  initialSessions?: ToolSessionMap;
137
+ /**
138
+ * Run-scoped tool output reference configuration. When `enabled` is
139
+ * `true`, tool outputs are registered under stable keys
140
+ * (`tool<idx>turn<turn>`) and subsequent tool calls can pipe previous
141
+ * outputs into their arguments via `{{tool<idx>turn<turn>}}`
142
+ * placeholders. Disabled by default so existing runs are unaffected.
143
+ */
144
+ toolOutputReferences?: ToolOutputReferencesConfig;
137
145
  };
138
146
  export type ProvidedCallbacks = (BaseCallbackHandler | CallbackHandlerMethods)[] | undefined;
139
147
  export type TokenCounter = (message: BaseMessage) => number;
@@ -2,6 +2,7 @@ import type { StructuredToolInterface } from '@langchain/core/tools';
2
2
  import type { RunnableToolLike } from '@langchain/core/runnables';
3
3
  import type { ToolCall } from '@langchain/core/messages/tool';
4
4
  import type { HookRegistry } from '@/hooks';
5
+ import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
5
6
  import type { MessageContentComplex, ToolErrorData } from './stream';
6
7
  /** Replacement type for `import type { ToolCall } from '@langchain/core/messages/tool'` in order to have stringified args typed */
7
8
  export type CustomToolCall = {
@@ -52,6 +53,22 @@ export type ToolNodeOptions = {
52
53
  * When provided, takes precedence over the value computed from maxContextTokens.
53
54
  */
54
55
  maxToolResultChars?: number;
56
+ /**
57
+ * Run-scoped tool output reference configuration. When `enabled` is
58
+ * `true`, ToolNode registers successful outputs and substitutes
59
+ * `{{tool<idx>turn<turn>}}` placeholders found in string args.
60
+ *
61
+ * Ignored when `toolOutputRegistry` is also provided (host-supplied
62
+ * registry wins).
63
+ */
64
+ toolOutputReferences?: ToolOutputReferencesConfig;
65
+ /**
66
+ * Pre-constructed registry instance shared across ToolNodes for the
67
+ * run. Graphs pass the same registry to every ToolNode they compile
68
+ * so cross-agent `{{tool<i>turn<n>}}` substitutions resolve. Takes
69
+ * precedence over `toolOutputReferences` when both are set.
70
+ */
71
+ toolOutputRegistry?: ToolOutputReferenceRegistry;
55
72
  };
56
73
  export type ToolNodeConstructorParams = ToolRefs & ToolNodeOptions;
57
74
  export type ToolEndEvent = {
@@ -199,6 +216,59 @@ export type ToolExecuteResult = {
199
216
  };
200
217
  /** Map of tool names to tool definitions */
201
218
  export type LCToolRegistry = Map<string, LCTool>;
219
+ /**
220
+ * Run-scoped configuration for tool output references.
221
+ *
222
+ * When enabled, each successful tool result is registered under a stable
223
+ * key (`tool<idx>turn<turn>`). Later tool calls can pipe a previous
224
+ * output into their arguments by including the literal placeholder
225
+ * `{{tool<idx>turn<turn>}}` anywhere in a string argument; ToolNode
226
+ * substitutes it with the stored output immediately before invoking
227
+ * the tool.
228
+ *
229
+ * The registry stores the *raw, untruncated* tool output (subject to
230
+ * its own size caps) so a later substitution can pipe the full payload
231
+ * into the next tool even when the LLM only saw a head+tail-truncated
232
+ * preview in `ToolMessage.content`. Size limits are decoupled from the
233
+ * LLM-visible truncation budget and default to 5 MB total.
234
+ *
235
+ * Known limitations:
236
+ * - Tools that return a `ToolMessage` with array-type content
237
+ * (multi-part content blocks such as text + image) are not
238
+ * registered and cannot be cited via `{{tool<i>turn<n>}}`. A
239
+ * warning is logged so the missing reference is visible.
240
+ * - When a `PostToolUse` hook replaces `ToolMessage.content`, the
241
+ * *post-hook* content is what gets stored in the registry (and
242
+ * what the model sees), so `{{…}}` substitutions deliver the
243
+ * hooked output rather than the raw tool return. This matches the
244
+ * hook's "authoritative" role for output shaping.
245
+ */
246
+ export type ToolOutputReferencesConfig = {
247
+ /** Enable the registry and placeholder substitution. Defaults to `false`. */
248
+ enabled?: boolean;
249
+ /**
250
+ * Maximum characters stored (and substituted) per registered output.
251
+ * Applied to the *raw* output before storage. Defaults to
252
+ * `HARD_MAX_TOOL_RESULT_CHARS` (~400 KB) — matching the
253
+ * LLM-visible tool-result truncation budget, which is also a safe
254
+ * payload size for shell `ARG_MAX` limits when a `{{…}}` expansion
255
+ * gets piped into a bash `command`. Hosts that want to preserve
256
+ * fuller fidelity (for example for non-bash API consumers) can
257
+ * raise this up to `maxTotalSize` (defaults to 5 MB) — be aware
258
+ * that large single-output substitutions may exceed shell
259
+ * argument-size limits on typical Linux/macOS.
260
+ */
261
+ maxOutputSize?: number;
262
+ /**
263
+ * Hard cap on total characters retained across all registered outputs
264
+ * for the run. When exceeded, the oldest entries are evicted FIFO
265
+ * until the total fits. The effective per-output cap is
266
+ * `min(maxOutputSize, maxTotalSize)` so a single stored output can
267
+ * never exceed the aggregate bound. Defaults to
268
+ * `calculateMaxTotalToolOutputSize(maxOutputSize)` (5 MB).
269
+ */
270
+ maxTotalSize?: number;
271
+ };
202
272
  export type ProgrammaticCache = {
203
273
  toolMap: ToolMap;
204
274
  toolDefs: LCTool[];
@@ -10,6 +10,15 @@
10
10
  * larger than this is almost certainly a bug (e.g., dumping a binary file).
11
11
  */
12
12
  export declare const HARD_MAX_TOOL_RESULT_CHARS = 400000;
13
+ /**
14
+ * Absolute hard cap on the aggregate size (characters) of all registered
15
+ * tool outputs kept for `{{tool<i>turn<n>}}` substitution. Set at 5 MB
16
+ * because the registry stores *raw, untruncated* tool output — full
17
+ * fidelity for piping into downstream bash/jq — so the budget needs
18
+ * enough headroom to keep a handful of large responses without
19
+ * ballooning unbounded.
20
+ */
21
+ export declare const HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE = 5000000;
13
22
  /**
14
23
  * Computes the dynamic max tool result size based on the model's context window.
15
24
  * Uses 30% of the context window (in estimated characters, ~4 chars/token)
@@ -19,6 +28,18 @@ export declare const HARD_MAX_TOOL_RESULT_CHARS = 400000;
19
28
  * @returns Maximum allowed characters for a single tool result.
20
29
  */
21
30
  export declare function calculateMaxToolResultChars(contextWindowTokens?: number): number;
31
+ /**
32
+ * Computes the default aggregate size (characters) for the tool output
33
+ * reference registry based on the per-output budget. Mirrors
34
+ * `calculateMaxToolResultChars`'s shape: a multiple of the per-output
35
+ * cap, clamped to `HARD_MAX_TOTAL_TOOL_OUTPUT_SIZE`.
36
+ *
37
+ * @param maxOutputSize - Per-output maximum characters (e.g., the
38
+ * ToolNode's `maxToolResultChars`). When omitted or non-positive,
39
+ * falls back to the absolute total cap.
40
+ * @returns Maximum total characters retained across the registry.
41
+ */
42
+ export declare function calculateMaxTotalToolOutputSize(maxOutputSize?: number): number;
22
43
  /**
23
44
  * Truncates a tool-call input (the arguments/payload of a tool_use block)
24
45
  * using head+tail strategy. Returns an object with `_truncated` (the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.1.70",
3
+ "version": "3.1.71-dev.1",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -43,6 +43,7 @@ import {
43
43
  import { SubagentExecutor, resolveSubagentConfigs } from '@/tools/subagent';
44
44
  import { buildSubagentToolParams } from '@/tools/SubagentTool';
45
45
  import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
46
+ import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
46
47
  import { safeDispatchCustomEvent, emitAgentLog } from '@/utils/events';
47
48
  import { attemptInvoke, tryFallbackProviders } from '@/llm/invoke';
48
49
  import { shouldTriggerSummarization } from '@/summarization';
@@ -128,6 +129,19 @@ export abstract class Graph<
128
129
  invokedToolIds?: Set<string>;
129
130
  handlerRegistry: HandlerRegistry | undefined;
130
131
  hookRegistry: HookRegistry | undefined;
132
+ /**
133
+ * Run-scoped config for the tool output reference registry. Threaded
134
+ * from `RunConfig.toolOutputReferences` down into every ToolNode this
135
+ * graph compiles.
136
+ */
137
+ toolOutputReferences: t.ToolOutputReferencesConfig | undefined;
138
+ /**
139
+ * Shared registry instance used by every ToolNode compiled from this
140
+ * graph. Lazily constructed on first access so multi-agent graphs
141
+ * produce one registry per run (not one per agent), letting cross-
142
+ * agent `{{tool<i>turn<n>}}` substitutions resolve.
143
+ */
144
+ private _toolOutputRegistry?: ToolOutputReferenceRegistry;
131
145
  /**
132
146
  * Tool session contexts for automatic state persistence across tool invocations.
133
147
  * Keyed by tool name (e.g., Constants.EXECUTE_CODE).
@@ -153,8 +167,47 @@ export abstract class Graph<
153
167
  this.invokedToolIds = undefined;
154
168
  this.handlerRegistry = undefined;
155
169
  this.hookRegistry = undefined;
170
+ this.toolOutputReferences = undefined;
171
+ /**
172
+ * ToolNodes compiled from this graph captured the registry
173
+ * instance at construction time, so simply dropping the Graph's
174
+ * own reference would leave their captured reference — and every
175
+ * stored `tool<i>turn<n>` entry, plus up to `maxTotalSize` of raw
176
+ * output — alive across subsequent `processStream()` calls. Wipe
177
+ * the registry's contents first so subsequent runs start fresh.
178
+ */
179
+ this._toolOutputRegistry?.clear();
180
+ this._toolOutputRegistry = undefined;
156
181
  this.sessions.clear();
157
182
  }
183
+
184
+ /**
185
+ * Returns the shared `ToolOutputReferenceRegistry` for this run,
186
+ * constructing it on first access. Returns `undefined` when the
187
+ * feature is disabled. All ToolNodes compiled from this graph share
188
+ * this single instance so cross-agent `{{…}}` references resolve.
189
+ *
190
+ * @internal Public so `attemptInvoke` can read it through the typed
191
+ * `InvokeContext` and project ToolMessages into LLM-facing annotated
192
+ * copies right before each provider call (see
193
+ * `annotateMessagesForLLM`). Host code should not call this directly
194
+ * — registry mutations outside the ToolNode lifecycle break the
195
+ * partitioning, eviction, and turn-counter invariants.
196
+ */
197
+ public getOrCreateToolOutputRegistry():
198
+ | ToolOutputReferenceRegistry
199
+ | undefined {
200
+ if (this.toolOutputReferences?.enabled !== true) {
201
+ return undefined;
202
+ }
203
+ if (this._toolOutputRegistry == null) {
204
+ this._toolOutputRegistry = new ToolOutputReferenceRegistry({
205
+ maxOutputSize: this.toolOutputReferences.maxOutputSize,
206
+ maxTotalSize: this.toolOutputReferences.maxTotalSize,
207
+ });
208
+ }
209
+ return this._toolOutputRegistry;
210
+ }
158
211
  }
159
212
 
160
213
  export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
@@ -516,6 +569,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
516
569
  directToolNames: directToolNames.size > 0 ? directToolNames : undefined,
517
570
  maxContextTokens: agentContext?.maxContextTokens,
518
571
  maxToolResultChars: agentContext?.maxToolResultChars,
572
+ toolOutputRegistry: this.getOrCreateToolOutputRegistry(),
519
573
  errorHandler: (data, metadata) =>
520
574
  StandardGraph.handleToolCallErrorStatic(this, data, metadata),
521
575
  });
@@ -547,6 +601,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
547
601
  sessions: this.sessions,
548
602
  maxContextTokens: agentContext?.maxContextTokens,
549
603
  maxToolResultChars: agentContext?.maxToolResultChars,
604
+ toolOutputRegistry: this.getOrCreateToolOutputRegistry(),
550
605
  });
551
606
  }
552
607