@librechat/agents 3.9.3 → 3.9.5

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 (125) hide show
  1. package/README.md +32 -0
  2. package/dist/cjs/common/enum.cjs +2 -0
  3. package/dist/cjs/common/enum.cjs.map +1 -1
  4. package/dist/cjs/events.cjs +11 -0
  5. package/dist/cjs/events.cjs.map +1 -1
  6. package/dist/cjs/graphs/Graph.cjs +41 -1
  7. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  8. package/dist/cjs/graphs/acceptedModelResponse.cjs +168 -0
  9. package/dist/cjs/graphs/acceptedModelResponse.cjs.map +1 -0
  10. package/dist/cjs/llm/invoke.cjs +10 -5
  11. package/dist/cjs/llm/invoke.cjs.map +1 -1
  12. package/dist/cjs/llm/streamLimits.cjs +1 -1
  13. package/dist/cjs/llm/streamLimits.cjs.map +1 -1
  14. package/dist/cjs/main.cjs +2 -0
  15. package/dist/cjs/messages/fading.cjs +14 -6
  16. package/dist/cjs/messages/fading.cjs.map +1 -1
  17. package/dist/cjs/messages/prune.cjs +95 -36
  18. package/dist/cjs/messages/prune.cjs.map +1 -1
  19. package/dist/cjs/openai/index.cjs +2 -0
  20. package/dist/cjs/openai/index.cjs.map +1 -1
  21. package/dist/cjs/openai/toolProjection.cjs +196 -0
  22. package/dist/cjs/openai/toolProjection.cjs.map +1 -0
  23. package/dist/cjs/run.cjs +8 -1
  24. package/dist/cjs/run.cjs.map +1 -1
  25. package/dist/cjs/session/AgentSession.cjs +1 -1
  26. package/dist/cjs/session/AgentSession.cjs.map +1 -1
  27. package/dist/cjs/stream.cjs +9 -4
  28. package/dist/cjs/stream.cjs.map +1 -1
  29. package/dist/cjs/tools/ArtifactTruncation.cjs +45 -0
  30. package/dist/cjs/tools/ArtifactTruncation.cjs.map +1 -0
  31. package/dist/cjs/tools/BashExecutor.cjs +6 -1
  32. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  33. package/dist/cjs/tools/CodeExecutor.cjs +6 -1
  34. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  35. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +5 -1
  36. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  37. package/dist/cjs/tools/ToolNode.cjs +2 -1
  38. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  39. package/dist/cjs/tools/subagent/SubagentReplay.cjs +4 -1
  40. package/dist/cjs/tools/subagent/SubagentReplay.cjs.map +1 -1
  41. package/dist/cjs/utils/acceptedToolArguments.cjs +143 -0
  42. package/dist/cjs/utils/acceptedToolArguments.cjs.map +1 -0
  43. package/dist/esm/common/enum.mjs +2 -0
  44. package/dist/esm/common/enum.mjs.map +1 -1
  45. package/dist/esm/events.mjs +11 -0
  46. package/dist/esm/events.mjs.map +1 -1
  47. package/dist/esm/graphs/Graph.mjs +41 -1
  48. package/dist/esm/graphs/Graph.mjs.map +1 -1
  49. package/dist/esm/graphs/acceptedModelResponse.mjs +165 -0
  50. package/dist/esm/graphs/acceptedModelResponse.mjs.map +1 -0
  51. package/dist/esm/llm/invoke.mjs +10 -5
  52. package/dist/esm/llm/invoke.mjs.map +1 -1
  53. package/dist/esm/llm/streamLimits.mjs +1 -1
  54. package/dist/esm/llm/streamLimits.mjs.map +1 -1
  55. package/dist/esm/main.mjs +3 -3
  56. package/dist/esm/messages/fading.mjs +14 -7
  57. package/dist/esm/messages/fading.mjs.map +1 -1
  58. package/dist/esm/messages/prune.mjs +95 -37
  59. package/dist/esm/messages/prune.mjs.map +1 -1
  60. package/dist/esm/openai/index.mjs +2 -1
  61. package/dist/esm/openai/index.mjs.map +1 -1
  62. package/dist/esm/openai/toolProjection.mjs +196 -0
  63. package/dist/esm/openai/toolProjection.mjs.map +1 -0
  64. package/dist/esm/run.mjs +8 -1
  65. package/dist/esm/run.mjs.map +1 -1
  66. package/dist/esm/session/AgentSession.mjs +1 -1
  67. package/dist/esm/session/AgentSession.mjs.map +1 -1
  68. package/dist/esm/stream.mjs +9 -4
  69. package/dist/esm/stream.mjs.map +1 -1
  70. package/dist/esm/tools/ArtifactTruncation.mjs +44 -0
  71. package/dist/esm/tools/ArtifactTruncation.mjs.map +1 -0
  72. package/dist/esm/tools/BashExecutor.mjs +6 -1
  73. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  74. package/dist/esm/tools/CodeExecutor.mjs +6 -1
  75. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  76. package/dist/esm/tools/ProgrammaticToolCalling.mjs +5 -1
  77. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  78. package/dist/esm/tools/ToolNode.mjs +2 -1
  79. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  80. package/dist/esm/tools/subagent/SubagentReplay.mjs +5 -2
  81. package/dist/esm/tools/subagent/SubagentReplay.mjs.map +1 -1
  82. package/dist/esm/utils/acceptedToolArguments.mjs +142 -0
  83. package/dist/esm/utils/acceptedToolArguments.mjs.map +1 -0
  84. package/dist/types/common/enum.d.ts +4 -0
  85. package/dist/types/graphs/Graph.d.ts +3 -1
  86. package/dist/types/graphs/acceptedModelResponse.d.ts +15 -0
  87. package/dist/types/messages/fading.d.ts +14 -2
  88. package/dist/types/messages/prune.d.ts +7 -0
  89. package/dist/types/openai/arguments.d.ts +2 -0
  90. package/dist/types/openai/index.d.ts +2 -0
  91. package/dist/types/openai/toolProjection.d.ts +30 -0
  92. package/dist/types/run.d.ts +1 -0
  93. package/dist/types/tools/ArtifactTruncation.d.ts +3 -0
  94. package/dist/types/tools/ToolNode.d.ts +1 -1
  95. package/dist/types/types/graph.d.ts +5 -3
  96. package/dist/types/types/run.d.ts +5 -0
  97. package/dist/types/types/stream.d.ts +22 -1
  98. package/dist/types/types/tools.d.ts +14 -0
  99. package/dist/types/utils/acceptedToolArguments.d.ts +10 -0
  100. package/package.json +1 -1
  101. package/src/common/enum.ts +5 -0
  102. package/src/events.ts +24 -1
  103. package/src/graphs/Graph.ts +95 -0
  104. package/src/graphs/acceptedModelResponse.ts +307 -0
  105. package/src/llm/invoke.ts +39 -14
  106. package/src/llm/streamLimits.ts +1 -1
  107. package/src/messages/fading.ts +30 -5
  108. package/src/messages/prune.ts +168 -55
  109. package/src/openai/arguments.ts +2 -0
  110. package/src/openai/index.ts +6 -0
  111. package/src/openai/toolProjection.ts +318 -0
  112. package/src/run.ts +21 -0
  113. package/src/session/AgentSession.ts +2 -2
  114. package/src/stream.ts +21 -1
  115. package/src/tools/ArtifactTruncation.ts +85 -0
  116. package/src/tools/BashExecutor.ts +22 -5
  117. package/src/tools/CodeExecutor.ts +18 -1
  118. package/src/tools/ProgrammaticToolCalling.ts +17 -3
  119. package/src/tools/ToolNode.ts +5 -0
  120. package/src/tools/subagent/SubagentReplay.ts +10 -9
  121. package/src/types/graph.ts +15 -9
  122. package/src/types/run.ts +5 -0
  123. package/src/types/stream.ts +26 -0
  124. package/src/types/tools.ts +21 -0
  125. package/src/utils/acceptedToolArguments.ts +204 -0
@@ -0,0 +1,142 @@
1
+ import { types } from "node:util";
2
+ //#region src/utils/acceptedToolArguments.ts
3
+ const MAX_ARGUMENT_DEPTH = 64;
4
+ const INVALID = "Accepted tool call arguments are not JSON serializable";
5
+ const LIMIT = "Tool projection buffer limit exceeded";
6
+ function invalid() {
7
+ throw new Error(INVALID);
8
+ }
9
+ function hasJSONPrototype(value, array) {
10
+ const proto = Object.getPrototypeOf(value);
11
+ if (proto === null) return true;
12
+ if (typeof proto !== "object" || types.isProxy(proto)) return false;
13
+ const ctor = Object.getOwnPropertyDescriptor(proto, "constructor")?.value;
14
+ if (typeof ctor !== "function" || types.isProxy(ctor)) return false;
15
+ return Object.getOwnPropertyDescriptor(ctor, "prototype")?.value === proto && Function.prototype.toString.call(ctor) === Function.prototype.toString.call(array ? Array : Object);
16
+ }
17
+ function serializeToolArguments(value, maxBytes) {
18
+ if (maxBytes < 2) throw new Error(LIMIT);
19
+ const parts = [];
20
+ const ancestors = /* @__PURE__ */ new Set();
21
+ let remaining = maxBytes;
22
+ const append = (text) => {
23
+ remaining -= Buffer.byteLength(text, "utf8");
24
+ if (remaining < 0) throw new Error(LIMIT);
25
+ parts.push(text);
26
+ };
27
+ const string = (text) => {
28
+ if (text.length + 2 > remaining) throw new Error(LIMIT);
29
+ append(JSON.stringify(text));
30
+ };
31
+ const encode = (input, depth) => {
32
+ if (depth > MAX_ARGUMENT_DEPTH) invalid();
33
+ if (input === null) {
34
+ append("null");
35
+ return;
36
+ }
37
+ if (typeof input === "string") {
38
+ string(input);
39
+ return;
40
+ }
41
+ if (typeof input === "boolean") {
42
+ append(input ? "true" : "false");
43
+ return;
44
+ }
45
+ if (typeof input === "number") {
46
+ if (!Number.isFinite(input)) invalid();
47
+ append(Object.is(input, -0) ? "-0" : String(input));
48
+ return;
49
+ }
50
+ if (typeof input !== "object") invalid();
51
+ if (types.isProxy(input) || ancestors.has(input)) invalid();
52
+ const array = Array.isArray(input);
53
+ if (!hasJSONPrototype(input, array)) invalid();
54
+ const keys = Reflect.ownKeys(input);
55
+ if (keys.length > remaining + (array ? 1 : 0)) throw new Error(LIMIT);
56
+ ancestors.add(input);
57
+ if (array) {
58
+ if (keys.length !== input.length + 1) invalid();
59
+ append("[");
60
+ for (let i = 0; i < input.length; i++) {
61
+ const descriptor = Object.getOwnPropertyDescriptor(input, String(i));
62
+ if (descriptor == null || descriptor.enumerable !== true || !("value" in descriptor)) invalid();
63
+ if (i !== 0) append(",");
64
+ encode(descriptor.value, depth + 1);
65
+ }
66
+ append("]");
67
+ } else {
68
+ append("{");
69
+ for (let i = 0; i < keys.length; i++) {
70
+ const key = keys[i];
71
+ if (typeof key !== "string") invalid();
72
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
73
+ if (descriptor == null || descriptor.enumerable !== true || !("value" in descriptor)) invalid();
74
+ if (i !== 0) append(",");
75
+ string(key);
76
+ append(":");
77
+ encode(descriptor.value, depth + 1);
78
+ }
79
+ append("}");
80
+ }
81
+ ancestors.delete(input);
82
+ };
83
+ if (value != null && typeof value === "object" && types.isProxy(value)) invalid();
84
+ if (value == null || typeof value !== "object" || Array.isArray(value)) throw new Error("Accepted tool call arguments must be an object");
85
+ encode(value, 0);
86
+ return parts.join("");
87
+ }
88
+ function cloneToolArguments(value) {
89
+ const copies = /* @__PURE__ */ new Map();
90
+ const ancestors = /* @__PURE__ */ new Set();
91
+ const stack = [];
92
+ const copy = (input) => {
93
+ if (input === null || typeof input === "string" || typeof input === "boolean") return input;
94
+ if (typeof input === "number") {
95
+ if (!Number.isFinite(input)) invalid();
96
+ return input;
97
+ }
98
+ if (typeof input !== "object" || types.isProxy(input) || ancestors.has(input)) invalid();
99
+ const existing = copies.get(input);
100
+ if (existing != null) return existing;
101
+ const array = Array.isArray(input);
102
+ if (!hasJSONPrototype(input, array)) invalid();
103
+ const keys = Reflect.ownKeys(input);
104
+ if (array && keys.length !== input.length + 1) invalid();
105
+ const output = array ? [] : {};
106
+ copies.set(input, output);
107
+ ancestors.add(input);
108
+ stack.push({
109
+ input,
110
+ output,
111
+ keys: array ? keys.filter((key) => key !== "length") : keys,
112
+ index: 0
113
+ });
114
+ return output;
115
+ };
116
+ if (value == null || typeof value !== "object" || types.isProxy(value) || Array.isArray(value)) invalid();
117
+ const result = copy(value);
118
+ while (stack.length > 0) {
119
+ const frame = stack[stack.length - 1];
120
+ if (frame.index === frame.keys.length) {
121
+ ancestors.delete(frame.input);
122
+ stack.pop();
123
+ continue;
124
+ }
125
+ const key = frame.keys[frame.index++];
126
+ if (typeof key !== "string") invalid();
127
+ if (Array.isArray(frame.input) && key !== String(frame.index - 1)) invalid();
128
+ const descriptor = Object.getOwnPropertyDescriptor(frame.input, key);
129
+ if (descriptor == null || !("value" in descriptor) || descriptor.enumerable !== true) invalid();
130
+ Object.defineProperty(frame.output, key, {
131
+ value: copy(descriptor.value),
132
+ enumerable: true,
133
+ writable: true,
134
+ configurable: true
135
+ });
136
+ }
137
+ return result;
138
+ }
139
+ //#endregion
140
+ export { cloneToolArguments, serializeToolArguments };
141
+
142
+ //# sourceMappingURL=acceptedToolArguments.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"acceptedToolArguments.mjs","names":[],"sources":["../../../src/utils/acceptedToolArguments.ts"],"mappings":"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"}
@@ -5,6 +5,10 @@
5
5
  * @enum {string}
6
6
  */
7
7
  export declare enum GraphEvents {
8
+ /** Accepted graph model result, after fallback selection. Registry-only, not a provider callback. */
9
+ ON_MODEL_RESPONSE = "on_model_response",
10
+ /** Registry-only: ToolNode has taken ownership of an accepted message's calls. */
11
+ ON_MODEL_TOOLS_CLAIMED = "on_model_tools_claimed",
8
12
  /** [Custom] Agent update event in multi-agent graph/workflow */
9
13
  ON_AGENT_UPDATE = "on_agent_update",
10
14
  /** [Custom] Delta event for run steps (message creation and tool calls) */
@@ -92,6 +92,8 @@ export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphStat
92
92
  callerSignal?: AbortSignal;
93
93
  /** Set of invoked tool call IDs from non-message run steps completed mid-run, if any */
94
94
  invokedToolIds?: Set<string>;
95
+ /** Explicit host policy. Never inferred from missing ToolNode claims. */
96
+ clientDelegatedToolNames?: ReadonlySet<string>;
95
97
  handlerRegistry: HandlerRegistry | undefined;
96
98
  /** Host registry retained only for forwarding tools from nested child graphs. */
97
99
  protected parentToolHandlerRegistry: HandlerRegistry | undefined;
@@ -439,7 +441,7 @@ export declare class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode>
439
441
  * from a discard.
440
442
  */
441
443
  private preemptRestartPending;
442
- constructor({ runId, signal, agents, langfuse, tokenCounter, indexTokenCountMap, calibrationRatio, fadingTier, fadingTiers, subagentUsageSink, subagentTasks, subagentScope, subagentExecutionContext, subagentContext, preemption, streamLimits, toolExecution, }: t.StandardGraphInput, dependencies?: GraphFactoryDependencies);
444
+ constructor({ runId, signal, agents, langfuse, tokenCounter, indexTokenCountMap, calibrationRatio, fadingTier, fadingTiers, subagentUsageSink, subagentTasks, subagentScope, subagentExecutionContext, subagentContext, preemption, streamLimits, toolExecution, clientDelegatedToolNames, }: t.StandardGraphInput, dependencies?: GraphFactoryDependencies);
443
445
  /**
444
446
  * A compaction applies its remove-all inside the agent subgraph, so the
445
447
  * subgraph's result carries only the retained tail, and an outer reducer
@@ -0,0 +1,15 @@
1
+ import type { AIMessageChunk } from '@langchain/core/messages';
2
+ import type { ModelResponseEvent } from '@/types';
3
+ export declare class InvalidModelToolCallError extends Error {
4
+ constructor(message: string);
5
+ }
6
+ /** Detach executable calls before dispatch. Invalid diagnostics remain available
7
+ * for ToolNode to synthesize paired error results; accepted projection rejects them.
8
+ */
9
+ export declare function detachValidatedModelToolCalls(message: AIMessageChunk, partial?: boolean): void;
10
+ /** Only the accepted final response becomes a host-visible model result. */
11
+ export declare function snapshotAcceptedModelResponse(finalResponse: AIMessageChunk, id: string, agentId: string, providerExecutedIds?: ReadonlySet<string>, clientDelegatedToolNames?: ReadonlySet<string>): ModelResponseEvent;
12
+ /** Providers can mutate and re-yield records. Inspect a per-emission snapshot,
13
+ * leaving their originals intact, but keep producer/consumer charge identity.
14
+ */
15
+ export declare function snapshotValidatedModelChunk(message: AIMessageChunk, partial?: boolean): AIMessageChunk;
@@ -1,5 +1,10 @@
1
1
  import type { FadingTier } from '@/types/graph';
2
- export declare const FADING_TIER_VERSION = 1;
2
+ /**
3
+ * Version 2: exchange width counts only the current turn. Version 1 tiers may
4
+ * have latched on history whose steps storage had merged into one assistant
5
+ * message, so they are discarded and re-derived once.
6
+ */
7
+ export declare const FADING_TIER_VERSION = 2;
3
8
  /** Context pressure at which observation masking activates. */
4
9
  export declare const PRESSURE_THRESHOLD_MASKING = 0.8;
5
10
  /** Smallest token budget the ladder can shrink to; keeps the emergency floor near 200 chars. */
@@ -12,7 +17,7 @@ export type FadingSignals = {
12
17
  /** (pruningBudget − instruction tokens) ÷ calibrationRatio, in raw token space. */
13
18
  effectiveRawTokens: number;
14
19
  summarizationEnabled: boolean;
15
- /** Largest number of parallel calls observed in one assistant exchange. */
20
+ /** Largest number of parallel calls in one assistant exchange of the current turn. */
16
21
  toolExchangeWidth?: number;
17
22
  /** Recovery paths force at least this rung on the current window's ladder. */
18
23
  minRung?: number;
@@ -29,6 +34,13 @@ export type FadingCaps = {
29
34
  /** Tier for a conversation that has never faded: the whole window, nothing masked. */
30
35
  export declare function createFadingTier(window: number): FadingTier;
31
36
  export declare function isFadingTier(value: unknown): value is FadingTier;
37
+ /**
38
+ * A well-formed tier persisted before version 2. Stored snapshots that carry
39
+ * one (resume manifests, session files) stay valid; every consumer then drops
40
+ * it through `isFadingTier`, so the tier re-derives once instead of the
41
+ * snapshot being rejected as corrupt.
42
+ */
43
+ export declare function isLegacyFadingTier(value: unknown): boolean;
32
44
  /** Deepest rung for a window: the point where the budget reaches its floor. */
33
45
  export declare function maxFadingRung(window: number): number;
34
46
  /** Token budget at a rung: the window halved per rung, never below the floor. */
@@ -250,6 +250,13 @@ export declare function preFlightTruncateToolResults(params: {
250
250
  indexTokenCountMap: Record<string, number | undefined>;
251
251
  tokenCounter: TokenCounter;
252
252
  }): number;
253
+ /**
254
+ * Leads every shortened tool-call input. Fading only shortens calls already in
255
+ * history, which ran with their full input; a model that reads a shortened copy
256
+ * as a call that was cut off re-issues it, and a side-effecting tool (sending an
257
+ * email) then runs again. The note says what actually happened.
258
+ */
259
+ export declare const TOOL_INPUT_ELISION_NOTE = "Completed call; input shortened to save context.";
253
260
  /**
254
261
  * Serializes one structured tool-call input as valid, bounded JSON without
255
262
  * invoking user-defined accessors or `toJSON`.
@@ -0,0 +1,2 @@
1
+ /** OpenAI-only compatibility import; validation belongs to the graph-safe utility. */
2
+ export { serializeToolArguments } from '@/utils/acceptedToolArguments';
@@ -73,3 +73,5 @@ export declare function createChatCompletionUsageChunk(context: OpenAIResponseCo
73
73
  export declare function writeOpenAISSE(writer: OpenAICompatibleWriter, data: OpenAIChatCompletionChunk | '[DONE]'): Promise<void>;
74
74
  export declare function createOpenAIHandlers(config: OpenAIHandlerConfig): Record<string, t.EventHandler>;
75
75
  export declare function sendOpenAIFinalChunk(config: OpenAIHandlerConfig, finishReason?: OpenAIChatCompletionChunkChoice['finish_reason']): Promise<void>;
76
+ export { createOpenAIToolCallStream } from './toolProjection';
77
+ export type { OpenAIToolCallStream, OpenAIToolCallStreamConfig, } from './toolProjection';
@@ -0,0 +1,30 @@
1
+ import type { OpenAIChatCompletionChunkChoice, OpenAIToolCall, OpenAIStreamTracker } from './index';
2
+ import type { EventHandler } from '@/types';
3
+ interface OpenAIToolCallStreamOptions {
4
+ /** Synchronous framing only. Async transport/backpressure is a separate boundary. */
5
+ emit?: (delta: OpenAIChatCompletionChunkChoice['delta']) => void;
6
+ signal?: AbortSignal;
7
+ /** Bound retained complete-before-publish output, not model execution. Default: 1024. */
8
+ maxToolCalls?: number;
9
+ /** UTF-8 bytes of serialized arguments, names and IDs retained across accepted responses. Default: 4 MiB. */
10
+ maxBufferedBytes?: number;
11
+ }
12
+ /** Streaming hosts share the finalizer's tracker; map-only hosts collect JSON output. */
13
+ export type OpenAIToolCallStreamConfig = OpenAIToolCallStreamOptions & ({
14
+ tracker: OpenAIStreamTracker;
15
+ toolCalls?: never;
16
+ emit: NonNullable<OpenAIToolCallStreamOptions['emit']>;
17
+ } | {
18
+ toolCalls: Map<number, OpenAIToolCall>;
19
+ tracker?: never;
20
+ });
21
+ export interface OpenAIToolCallStream {
22
+ /** Pass directly to Run.create({ customHandlers: stream.handlers }). */
23
+ handlers: Record<string, EventHandler>;
24
+ /** Publish only after the host verifies that the entire run completed successfully. */
25
+ finish: () => void;
26
+ abort: () => void;
27
+ }
28
+ /** Serializes finalized, accepted tool calls. No provider fragments or attempt inference. */
29
+ export declare function createOpenAIToolCallStream(config: OpenAIToolCallStreamConfig): OpenAIToolCallStream;
30
+ export {};
@@ -14,6 +14,7 @@ export declare class Run<_T extends t.BaseGraphState> {
14
14
  private langfuse?;
15
15
  private toolOutputReferences?;
16
16
  private eagerEventToolExecution?;
17
+ private clientDelegatedToolNames?;
17
18
  private codeSessionToolNames?;
18
19
  private interruptingToolNames?;
19
20
  private toolExecution?;
@@ -0,0 +1,3 @@
1
+ import type { ArtifactTruncation } from '@/types';
2
+ export declare function normalizeArtifactTruncation(value: unknown): ArtifactTruncation | undefined;
3
+ export declare function appendArtifactTruncationWarning(output: string, truncation: ArtifactTruncation | undefined): string;
@@ -229,7 +229,7 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
229
229
  */
230
230
  private anonBatchCounter;
231
231
  private handoffRouting?;
232
- constructor({ handoffRouting, tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, toolDefinitions, getDiscoveredToolNames, sessions, codeSessionKey, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, eagerEventToolSuppressions, agentId, executingAgentId, executionContext, executingAgentName, rootAgentId, rootAgentName, directToolNames, interruptingToolNames, codeSessionToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, getBreakerSignal, getRunScope, preparedSubagents, restoreRunStepResumeState, createRunStepResumeState, }: t.ToolNodeConstructorParams);
232
+ constructor({ handoffRouting, tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, toolDefinitions, getDiscoveredToolNames, sessions, codeSessionKey, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, eagerEventToolSuppressions, agentId, executingAgentId, executionContext, executingAgentName, rootAgentId, rootAgentName, directToolNames, interruptingToolNames, codeSessionToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, getBreakerSignal, getRunScope, preparedSubagents, restoreRunStepResumeState, createRunStepResumeState, onToolCallsClaimed, }: t.ToolNodeConstructorParams);
233
233
  invoke(input: any, options?: Partial<RunnableConfig>): Promise<any>;
234
234
  private withToolScope;
235
235
  /**
@@ -4,8 +4,8 @@ import type { START, StateGraph, StateGraphArgs } from '@langchain/langgraph';
4
4
  import type { RunnableConfig, Runnable } from '@langchain/core/runnables';
5
5
  import type { ChatGenerationChunk } from '@langchain/core/outputs';
6
6
  import type { GoogleAIToolType } from '@langchain/google-common';
7
+ import type { RunStep, ModelResponseEvent, ModelToolsClaimedEvent, RunStepDeltaEvent, RunStepResumeState, RunStepClosedEvent, MessageDeltaEvent, ReasoningDeltaEvent } from '@/types/stream';
7
8
  import type { SummarizationNodeInput, SummarizeCompleteEvent, CompactionSemanticIndex, SummarizationConfig, SummarizeStartEvent, SummarizeDeltaEvent } from '@/types/summarize';
8
- import type { RunStep, RunStepDeltaEvent, RunStepResumeState, RunStepClosedEvent, MessageDeltaEvent, ReasoningDeltaEvent } from '@/types/stream';
9
9
  import type { ToolMap, ToolSessionMap, ToolEndEvent, GenericTool, LCTool, ToolExecutionConfig, ToolExecuteBatchRequest } from '@/types/tools';
10
10
  import type { TokenCounter, StreamLimits, StreamPreemption, TokenBudgetBreakdown } from '@/types/run';
11
11
  import type { SubagentTaskConfig } from '@/types/subagentTasks';
@@ -127,7 +127,7 @@ export interface ContextUsageEvent {
127
127
  * `RunConfig.fadingTiers[agentId]`.
128
128
  */
129
129
  export interface FadingTier {
130
- v: 1;
130
+ v: 2;
131
131
  /** Token budget the caps derive from, in raw token space. Never grows;
132
132
  * clamped to the current context window when seeded. */
133
133
  budgetTokens: number;
@@ -139,7 +139,7 @@ export interface FadingTier {
139
139
  /** Latched fading tiers keyed by agent ID. */
140
140
  export type FadingTiers = Record<string, FadingTier>;
141
141
  export interface EventHandler {
142
- handle(event: string, data: StreamEventData | ModelEndData | RunStep | RunStepDeltaEvent | RunStepClosedEvent | MessageDeltaEvent | ReasoningDeltaEvent | SummarizeStartEvent | SummarizeDeltaEvent | SummarizeCompleteEvent | SubagentUpdateEvent | AgentLogEvent | ContextUsageEvent | ToolExecuteBatchRequest | {
142
+ handle(event: string, data: StreamEventData | ModelEndData | ModelResponseEvent | ModelToolsClaimedEvent | RunStep | RunStepDeltaEvent | RunStepClosedEvent | MessageDeltaEvent | ReasoningDeltaEvent | SummarizeStartEvent | SummarizeDeltaEvent | SummarizeCompleteEvent | SubagentUpdateEvent | AgentLogEvent | ContextUsageEvent | ToolExecuteBatchRequest | {
143
143
  result: ToolEndEvent;
144
144
  }, metadata?: Record<string, unknown>, graph?: StandardGraph | MultiAgentGraph): void | Promise<void>;
145
145
  }
@@ -291,6 +291,8 @@ export type StandardGraphInput = {
291
291
  agents: AgentInputs[];
292
292
  /** Execution backend used to resolve the effective tool registry. */
293
293
  toolExecution?: ToolExecutionConfig;
294
+ /** Trusted single-agent client delegation policy; mixed batches fail closed. */
295
+ clientDelegatedToolNames?: readonly string[];
294
296
  langfuse?: LangfuseConfig;
295
297
  tokenCounter?: TokenCounter;
296
298
  indexTokenCountMap?: Record<string, number>;
@@ -260,6 +260,11 @@ export type RunConfig = {
260
260
  */
261
261
  langfuse?: g.LangfuseConfig;
262
262
  customHandlers?: Record<string, g.EventHandler>;
263
+ /** Explicit client-owned tools for a single-agent graph. A batch mixing
264
+ * client and SDK/provider calls fails closed; omitted means SDK ownership.
265
+ * Hosts must register the corresponding model-facing tool schemas.
266
+ */
267
+ clientDelegatedToolNames?: readonly string[];
263
268
  /**
264
269
  * Receives token usage for every model call made inside subagent child
265
270
  * runs (including nested subagents). Child graphs execute via `invoke()`
@@ -1,4 +1,4 @@
1
- import type { MessageContentImageUrl, MessageContentText, ToolMessage, BaseMessage } from '@langchain/core/messages';
1
+ import type { AIMessageChunk, MessageContentImageUrl, MessageContentText, ToolMessage, BaseMessage } from '@langchain/core/messages';
2
2
  import type { ToolCall, ToolCallChunk } from '@langchain/core/messages/tool';
3
3
  import type { LLMResult, Generation } from '@langchain/core/outputs';
4
4
  import type { Command } from '@langchain/langgraph';
@@ -8,6 +8,21 @@ import type { AssistantTextPhase } from '@/types/assistantPhase';
8
8
  import type { SummarizeCompleteEvent } from '@/types/summarize';
9
9
  import type { ToolEndEvent } from '@/types/tools';
10
10
  import { StepTypes, ContentTypes, GraphEvents } from '@/common/enum';
11
+ /** One accepted model result, detached from execution state before host dispatch.
12
+ * Provider chunks, failed attempts and UI run-step events are not this contract. */
13
+ export interface ModelResponseEvent {
14
+ type: 'model_response';
15
+ /** Graph-generated acceptance ID, not a provider ID or run-step index. */
16
+ id: string;
17
+ agentId: string;
18
+ /** Graph-state message identity used to correlate ToolNode ownership. */
19
+ messageId?: string;
20
+ toolCalls: ReadonlyArray<ToolCall>;
21
+ /** Same index as toolCalls. Only a trusted graph decision of 'client'
22
+ * permits this call on the OpenAI client wire; absence fails closed. */
23
+ toolCallDispositions: ReadonlyArray<'sdk' | 'provider' | 'client'>;
24
+ invalidToolCalls: ReadonlyArray<NonNullable<AIMessageChunk['invalid_tool_calls']>[number]>;
25
+ }
11
26
  export type HandleLLMEnd = (output: LLMResult, runId: string, parentRunId?: string, tags?: string[]) => void;
12
27
  export type MetadataAggregatorResult = {
13
28
  handleLLMEnd: HandleLLMEnd;
@@ -442,3 +457,9 @@ export type ContentAggregatorResult = {
442
457
  contentParts: Array<MessageContentComplex | undefined>;
443
458
  aggregateContent: ContentAggregator;
444
459
  };
460
+ /** Ownership, not successful completion. Interrupted/failed batches remain graph-owned. */
461
+ export interface ModelToolsClaimedEvent {
462
+ type: 'model_tools_claimed';
463
+ agentId: string;
464
+ messageId: string;
465
+ }
@@ -293,6 +293,8 @@ export type ToolNodeOptions = {
293
293
  restoreRunStepResumeState?: (state?: RunStepResumeState, config?: RunnableConfig) => void;
294
294
  /** SDK-owned checkpoint snapshot for open run-step lifecycle state. */
295
295
  createRunStepResumeState?: () => RunStepResumeState;
296
+ /** Internal ownership bridge, awaited before ToolNode can execute a batch. */
297
+ onToolCallsClaimed?: (messageId: string, config: RunnableConfig) => Promise<void>;
296
298
  };
297
299
  export type ToolNodeConstructorParams = ToolRefs & ToolNodeOptions;
298
300
  export type ToolEndEvent = {
@@ -462,6 +464,14 @@ export type ArtifactDeliveryFailure = {
462
464
  delivered: number;
463
465
  failed: number;
464
466
  };
467
+ export type ArtifactTruncationReason = 'max_files' | 'depth' | 'size' | 'path' | 'unreadable';
468
+ export type ArtifactTruncation = {
469
+ code: 'artifact_truncated';
470
+ reasons: Partial<Record<ArtifactTruncationReason, number>>;
471
+ /** Up to 20 relative paths omitted from the response. */
472
+ skipped: string[];
473
+ skipped_count: number;
474
+ };
465
475
  export type ExecuteResult = {
466
476
  /**
467
477
  * Execution session id — the (transient) sandbox run that produced
@@ -475,6 +485,7 @@ export type ExecuteResult = {
475
485
  /** Persisted input paths explicitly removed during this execution. */
476
486
  deleted_files?: string[];
477
487
  artifact_delivery?: ArtifactDeliveryFailure;
488
+ artifact_truncation?: ArtifactTruncation;
478
489
  /**
479
490
  * Durable runtime session id echoed by a stateful Code API backend
480
491
  * (hash of tenant+user+hint). Additive; absent on stateless servers.
@@ -1243,6 +1254,7 @@ export type ProgrammaticExecutionResponse = {
1243
1254
  /** Persisted input paths explicitly removed during this execution. */
1244
1255
  deleted_files?: string[];
1245
1256
  artifact_delivery?: ArtifactDeliveryFailure;
1257
+ artifact_truncation?: ArtifactTruncation;
1246
1258
  /** Durable runtime session echo from a stateful backend (additive). */
1247
1259
  runtime_session_id?: string;
1248
1260
  runtime_status?: 'new' | 'reused';
@@ -1259,6 +1271,7 @@ export type ProgrammaticExecutionArtifact = {
1259
1271
  /** Persisted input paths explicitly removed during this execution. */
1260
1272
  deleted_files?: string[];
1261
1273
  artifact_delivery?: ArtifactDeliveryFailure;
1274
+ artifact_truncation?: ArtifactTruncation;
1262
1275
  /** Durable runtime session echo from a stateful backend (additive). */
1263
1276
  runtime_session_id?: string;
1264
1277
  runtime_status?: 'new' | 'reused';
@@ -1326,6 +1339,7 @@ export type CodeExecutionArtifact = {
1326
1339
  /** Persisted input paths explicitly removed during this execution. */
1327
1340
  deleted_files?: string[];
1328
1341
  artifact_delivery?: ArtifactDeliveryFailure;
1342
+ artifact_truncation?: ArtifactTruncation;
1329
1343
  /** Durable runtime session echo from a stateful backend (additive). */
1330
1344
  runtime_session_id?: string;
1331
1345
  runtime_status?: 'new' | 'reused';
@@ -0,0 +1,10 @@
1
+ /** Encode JSON data, not arbitrary JS values. No getters, proxies or toJSON hooks run.
2
+ * Repeated references expand like JSON (and count against the budget); cycles fail.
3
+ * Limits bound output and traversal, including deeply nested/alias-heavy input.
4
+ */
5
+ export declare function serializeToolArguments(value: unknown, maxBytes: number): string;
6
+ /** Detach JSON data without encoding it. Unlike projection, invocation has no
7
+ * formatting budget: preserve shared references and traverse iteratively so a
8
+ * deep or alias-heavy object cannot expand exponentially during validation.
9
+ */
10
+ export declare function cloneToolArguments(value: unknown): Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.9.3",
3
+ "version": "3.9.5",
4
4
  "reova": {
5
5
  "enabled": true,
6
6
  "endpoint": "https://telemetry.reo.dev/data"
@@ -7,6 +7,11 @@
7
7
  export enum GraphEvents {
8
8
  /* Custom Events */
9
9
 
10
+ /** Accepted graph model result, after fallback selection. Registry-only, not a provider callback. */
11
+ ON_MODEL_RESPONSE = 'on_model_response',
12
+ /** Registry-only: ToolNode has taken ownership of an accepted message's calls. */
13
+ ON_MODEL_TOOLS_CLAIMED = 'on_model_tools_claimed',
14
+
10
15
  /** [Custom] Agent update event in multi-agent graph/workflow */
11
16
  ON_AGENT_UPDATE = 'on_agent_update',
12
17
  /** [Custom] Delta event for run steps (message creation and tool calls) */
package/src/events.ts CHANGED
@@ -8,7 +8,7 @@ import type { Logger } from 'winston';
8
8
  import type { MultiAgentGraph, StandardGraph } from '@/graphs';
9
9
  import type * as t from '@/types';
10
10
  import { dispatchesChatModelStream, SDK_STREAM_DISPATCH } from '@/stream';
11
- import { Constants } from '@/common';
11
+ import { Constants, GraphEvents } from '@/common';
12
12
 
13
13
  export class HandlerRegistry {
14
14
  private handlers: Map<string, t.EventHandler> = new Map();
@@ -26,12 +26,23 @@ export function composeEventHandlers(
26
26
  ...handlerSets: Array<Record<string, t.EventHandler> | undefined>
27
27
  ): Record<string, t.EventHandler> {
28
28
  const composed: Partial<Record<string, t.EventHandler>> = {};
29
+ const acceptedObservers = new Map<string, t.EventHandler[]>();
29
30
 
30
31
  for (const handlerSet of handlerSets) {
31
32
  if (!handlerSet) {
32
33
  continue;
33
34
  }
34
35
  for (const [eventType, handler] of Object.entries(handlerSet)) {
36
+ if (
37
+ eventType === GraphEvents.ON_MODEL_RESPONSE ||
38
+ eventType === GraphEvents.ON_MODEL_TOOLS_CLAIMED
39
+ ) {
40
+ const observers = acceptedObservers.get(eventType) ?? [];
41
+ observers.push(handler);
42
+ acceptedObservers.set(eventType, observers);
43
+ composed[eventType] = handler;
44
+ continue;
45
+ }
35
46
  const previous = composed[eventType];
36
47
  if (previous === undefined) {
37
48
  composed[eventType] = handler;
@@ -61,6 +72,18 @@ export function composeEventHandlers(
61
72
  }
62
73
  }
63
74
 
75
+ for (const [eventType, observers] of acceptedObservers) {
76
+ composed[eventType] = {
77
+ handle: async (event, data, metadata, graph): Promise<void> => {
78
+ // Clone from the graph-owned snapshot, not from any preceding observer's
79
+ // possibly mutated argument tree. One bounded copy per observer.
80
+ for (const observer of observers) {
81
+ await observer.handle(event, structuredClone(data), metadata, graph);
82
+ }
83
+ },
84
+ };
85
+ }
86
+
64
87
  return composed as Record<string, t.EventHandler>;
65
88
  }
66
89