@librechat/agents 3.6.10 → 3.6.12

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 (64) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +63 -10
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +48 -119
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/llm/contextPressureMeter.cjs +132 -0
  6. package/dist/cjs/llm/contextPressureMeter.cjs.map +1 -0
  7. package/dist/cjs/llm/invoke.cjs +46 -75
  8. package/dist/cjs/llm/invoke.cjs.map +1 -1
  9. package/dist/cjs/llm/prepareProviderRequest.cjs +108 -0
  10. package/dist/cjs/llm/prepareProviderRequest.cjs.map +1 -0
  11. package/dist/cjs/main.cjs +4 -0
  12. package/dist/cjs/messages/content.cjs +5 -4
  13. package/dist/cjs/messages/content.cjs.map +1 -1
  14. package/dist/cjs/stream.cjs +1 -0
  15. package/dist/cjs/stream.cjs.map +1 -1
  16. package/dist/cjs/tools/CallerCapabilities.cjs +31 -0
  17. package/dist/cjs/tools/CallerCapabilities.cjs.map +1 -1
  18. package/dist/cjs/tools/ToolNode.cjs +23 -6
  19. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  20. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +26 -2
  21. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  22. package/dist/esm/agents/AgentContext.mjs +65 -12
  23. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  24. package/dist/esm/graphs/Graph.mjs +51 -122
  25. package/dist/esm/graphs/Graph.mjs.map +1 -1
  26. package/dist/esm/llm/contextPressureMeter.mjs +132 -0
  27. package/dist/esm/llm/contextPressureMeter.mjs.map +1 -0
  28. package/dist/esm/llm/invoke.mjs +48 -75
  29. package/dist/esm/llm/invoke.mjs.map +1 -1
  30. package/dist/esm/llm/prepareProviderRequest.mjs +105 -0
  31. package/dist/esm/llm/prepareProviderRequest.mjs.map +1 -0
  32. package/dist/esm/main.mjs +3 -2
  33. package/dist/esm/messages/content.mjs +5 -4
  34. package/dist/esm/messages/content.mjs.map +1 -1
  35. package/dist/esm/stream.mjs +1 -0
  36. package/dist/esm/stream.mjs.map +1 -1
  37. package/dist/esm/tools/CallerCapabilities.mjs +29 -1
  38. package/dist/esm/tools/CallerCapabilities.mjs.map +1 -1
  39. package/dist/esm/tools/ToolNode.mjs +24 -7
  40. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  41. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +26 -4
  42. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  43. package/dist/types/agents/AgentContext.d.ts +11 -0
  44. package/dist/types/index.d.ts +2 -0
  45. package/dist/types/llm/contextPressureMeter.d.ts +30 -0
  46. package/dist/types/llm/invoke.d.ts +31 -33
  47. package/dist/types/llm/prepareProviderRequest.d.ts +55 -0
  48. package/dist/types/tools/CallerCapabilities.d.ts +13 -0
  49. package/dist/types/tools/ToolNode.d.ts +9 -1
  50. package/dist/types/tools/local/resolveLocalExecutionTools.d.ts +6 -0
  51. package/dist/types/types/tools.d.ts +17 -0
  52. package/package.json +1 -1
  53. package/src/agents/AgentContext.ts +157 -15
  54. package/src/graphs/Graph.ts +75 -279
  55. package/src/index.ts +8 -0
  56. package/src/llm/contextPressureMeter.ts +284 -0
  57. package/src/llm/invoke.ts +136 -296
  58. package/src/llm/prepareProviderRequest.ts +357 -0
  59. package/src/messages/content.ts +3 -3
  60. package/src/stream.ts +6 -0
  61. package/src/tools/CallerCapabilities.ts +68 -0
  62. package/src/tools/ToolNode.ts +47 -4
  63. package/src/tools/local/resolveLocalExecutionTools.ts +77 -3
  64. package/src/types/tools.ts +18 -0
@@ -0,0 +1,357 @@
1
+ import type { RunnableConfig } from '@langchain/core/runnables';
2
+ import type { BaseMessage } from '@langchain/core/messages';
3
+ import type { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
4
+ import type * as t from '@/types';
5
+ import {
6
+ projectCacheControlledToolOutputsToText,
7
+ projectComputerCallOutputsToText,
8
+ projectOpenAIChatToolMessageContent,
9
+ projectOpenAIResponsesToolMessageContent,
10
+ projectOpenRouterToolMessageContent,
11
+ projectSingleTextToolOutputsToText,
12
+ projectStructuredToolOutputsToText,
13
+ projectToolStreamContentForProvider,
14
+ } from '@/messages/core';
15
+ import {
16
+ coalesceAdjacentUserTurns,
17
+ strictAlternationProviders,
18
+ appendPredecessorHandoffCue,
19
+ removePredecessorHandoffCue,
20
+ } from '@/messages';
21
+ import {
22
+ stripAnthropicCacheControl,
23
+ stripBedrockCacheControl,
24
+ } from '@/messages/cache';
25
+ import { annotateMessagesForLLM } from '@/tools/toolOutputReferences';
26
+ import { Providers } from '@/common';
27
+ import { isAnthropicLike, isOpenAILike } from '@/utils/llm';
28
+
29
+ const preparedProviderRequestBrand = Symbol('PreparedProviderRequest');
30
+
31
+ export type ProviderMessageProjectionMode =
32
+ | 'chat-messages'
33
+ | 'openai-responses';
34
+
35
+ export interface ProviderPayloadMeasurement {
36
+ readonly fits: boolean;
37
+ readonly projectedMessageTokens?: number;
38
+ readonly availableMessageTokens?: number;
39
+ readonly contextBudget?: number;
40
+ readonly effectiveInstructionTokens?: number;
41
+ }
42
+
43
+ export interface PreparedProviderRequest {
44
+ readonly model: t.ChatModel;
45
+ readonly modelId?: string;
46
+ readonly provider: Providers;
47
+ readonly projectionMode: ProviderMessageProjectionMode;
48
+ readonly messages: BaseMessage[];
49
+ readonly measurement?: ProviderPayloadMeasurement;
50
+ readonly [preparedProviderRequestBrand]: true;
51
+ }
52
+
53
+ type PreparedProviderRequestData = Omit<
54
+ PreparedProviderRequest,
55
+ typeof preparedProviderRequestBrand
56
+ >;
57
+
58
+ export interface ProviderRequestContext {
59
+ getOrCreateToolOutputRegistry?(): ToolOutputReferenceRegistry | undefined;
60
+ isRunProducedMessage?(message: BaseMessage): boolean;
61
+ }
62
+
63
+ export interface PrepareProviderRequestParams {
64
+ model: t.ChatModel;
65
+ messages: BaseMessage[];
66
+ provider: Providers;
67
+ context?: ProviderRequestContext;
68
+ config?: RunnableConfig;
69
+ maxToolResultChars?: number;
70
+ measure?: (messages: BaseMessage[]) => ProviderPayloadMeasurement;
71
+ }
72
+
73
+ export function usesNativeOpenAIResponses(
74
+ model: t.ChatModel,
75
+ provider: Providers,
76
+ callOptions?: unknown
77
+ ): boolean {
78
+ if (!isOpenAILike(provider)) {
79
+ return false;
80
+ }
81
+ let candidate: unknown = model;
82
+ let effectiveCallOptions = callOptions;
83
+ const seen = new Set<object>();
84
+ for (let depth = 0; depth < 20; depth++) {
85
+ if (candidate == null || typeof candidate !== 'object') {
86
+ return false;
87
+ }
88
+ if (seen.has(candidate)) {
89
+ return false;
90
+ }
91
+ seen.add(candidate);
92
+ const runnable = candidate as {
93
+ _useResponsesApi?: (options?: unknown) => boolean;
94
+ bound?: unknown;
95
+ defaultOptions?: unknown;
96
+ last?: unknown;
97
+ constructor?: { name?: unknown };
98
+ };
99
+ try {
100
+ if (
101
+ runnable.defaultOptions != null &&
102
+ typeof runnable.defaultOptions === 'object' &&
103
+ !Array.isArray(runnable.defaultOptions) &&
104
+ effectiveCallOptions != null &&
105
+ typeof effectiveCallOptions === 'object' &&
106
+ !Array.isArray(effectiveCallOptions)
107
+ ) {
108
+ effectiveCallOptions = {
109
+ ...(runnable.defaultOptions as Record<string, unknown>),
110
+ ...(effectiveCallOptions as Record<string, unknown>),
111
+ };
112
+ } else if (effectiveCallOptions == null) {
113
+ effectiveCallOptions = runnable.defaultOptions;
114
+ }
115
+ if (
116
+ runnable._useResponsesApi?.(effectiveCallOptions) === true ||
117
+ runnable._useResponsesApi?.(undefined) === true
118
+ ) {
119
+ return true;
120
+ }
121
+ } catch {
122
+ // Continue through RunnableSequence/RunnableBinding wrappers.
123
+ }
124
+ if (
125
+ typeof runnable.constructor?.name === 'string' &&
126
+ runnable.constructor.name.includes('Responses')
127
+ ) {
128
+ return true;
129
+ }
130
+ if (runnable.last != null && typeof runnable.last === 'object') {
131
+ candidate = runnable.last;
132
+ continue;
133
+ }
134
+ if (runnable.bound != null && typeof runnable.bound === 'object') {
135
+ candidate = runnable.bound;
136
+ continue;
137
+ }
138
+ return false;
139
+ }
140
+ return false;
141
+ }
142
+
143
+ function resolveProviderMessageProjectionMode(
144
+ model: t.ChatModel,
145
+ provider: Providers,
146
+ callOptions?: unknown
147
+ ): ProviderMessageProjectionMode {
148
+ return usesNativeOpenAIResponses(model, provider, callOptions)
149
+ ? 'openai-responses'
150
+ : 'chat-messages';
151
+ }
152
+
153
+ interface ProjectMessagesForProviderParams {
154
+ model: t.ChatModel;
155
+ messages: BaseMessage[];
156
+ provider: Providers;
157
+ maxToolResultChars?: number;
158
+ callOptions?: unknown;
159
+ }
160
+
161
+ function projectMessagesForProviderMode({
162
+ messages,
163
+ provider,
164
+ maxToolResultChars,
165
+ }: ProjectMessagesForProviderParams,
166
+ projectionMode: ProviderMessageProjectionMode
167
+ ): BaseMessage[] {
168
+ const nativeOpenAIResponses = projectionMode === 'openai-responses';
169
+ const providerInputMessages = projectToolStreamContentForProvider(
170
+ messages,
171
+ nativeOpenAIResponses ? 'native' : 'fallback',
172
+ maxToolResultChars
173
+ );
174
+ if (nativeOpenAIResponses) {
175
+ return projectOpenAIResponsesToolMessageContent(
176
+ stripAnthropicCacheControl(
177
+ stripBedrockCacheControl(providerInputMessages)
178
+ ),
179
+ maxToolResultChars
180
+ );
181
+ }
182
+ if (provider === Providers.OPENROUTER) {
183
+ return projectComputerCallOutputsToText(
184
+ projectOpenRouterToolMessageContent(
185
+ stripBedrockCacheControl(providerInputMessages),
186
+ maxToolResultChars
187
+ )
188
+ );
189
+ }
190
+ if (isOpenAILike(provider)) {
191
+ return projectComputerCallOutputsToText(
192
+ projectOpenAIChatToolMessageContent(
193
+ stripAnthropicCacheControl(
194
+ stripBedrockCacheControl(providerInputMessages)
195
+ ),
196
+ maxToolResultChars
197
+ )
198
+ );
199
+ }
200
+ if (provider === Providers.ANTHROPIC) {
201
+ return projectComputerCallOutputsToText(
202
+ projectSingleTextToolOutputsToText(
203
+ stripBedrockCacheControl(providerInputMessages),
204
+ maxToolResultChars
205
+ )
206
+ );
207
+ }
208
+ if (provider === Providers.BEDROCK) {
209
+ return stripAnthropicCacheControl(
210
+ projectComputerCallOutputsToText(
211
+ projectCacheControlledToolOutputsToText(
212
+ providerInputMessages,
213
+ maxToolResultChars
214
+ )
215
+ )
216
+ );
217
+ }
218
+ return projectComputerCallOutputsToText(
219
+ projectStructuredToolOutputsToText(
220
+ projectSingleTextToolOutputsToText(
221
+ stripAnthropicCacheControl(
222
+ stripBedrockCacheControl(providerInputMessages)
223
+ ),
224
+ maxToolResultChars
225
+ ),
226
+ maxToolResultChars
227
+ )
228
+ );
229
+ }
230
+
231
+ /** Produces the provider-facing representation before adapter serialization. */
232
+ export function projectMessagesForProvider(
233
+ params: ProjectMessagesForProviderParams
234
+ ): BaseMessage[] {
235
+ return projectMessagesForProviderMode(
236
+ params,
237
+ resolveProviderMessageProjectionMode(
238
+ params.model,
239
+ params.provider,
240
+ params.callOptions
241
+ )
242
+ );
243
+ }
244
+
245
+ /** Reads the serving model id through LangChain binding/sequence wrappers. */
246
+ export function resolveServingModelId(model: unknown): string | undefined {
247
+ const seen = new Set<unknown>();
248
+ let current: unknown = model;
249
+ while (current != null && typeof current === 'object' && !seen.has(current)) {
250
+ seen.add(current);
251
+ const wrapper = current as {
252
+ model?: unknown;
253
+ bound?: unknown;
254
+ last?: unknown;
255
+ steps?: unknown[];
256
+ };
257
+ if (typeof wrapper.model === 'string' && wrapper.model !== '') {
258
+ return wrapper.model;
259
+ }
260
+ current =
261
+ wrapper.bound ??
262
+ wrapper.last ??
263
+ (Array.isArray(wrapper.steps)
264
+ ? wrapper.steps[wrapper.steps.length - 1]
265
+ : undefined);
266
+ }
267
+ return undefined;
268
+ }
269
+
270
+ /**
271
+ * Finalizes one provider request and measures the exact message array that
272
+ * will be passed to LangChain. Source messages remain untouched.
273
+ */
274
+ export function prepareProviderRequest({
275
+ model,
276
+ messages,
277
+ provider,
278
+ context,
279
+ config,
280
+ maxToolResultChars,
281
+ measure,
282
+ }: PrepareProviderRequestParams): PreparedProviderRequest {
283
+ const projectionMode = resolveProviderMessageProjectionMode(
284
+ model,
285
+ provider,
286
+ config
287
+ );
288
+ const projected = projectMessagesForProviderMode(
289
+ {
290
+ model,
291
+ messages,
292
+ provider,
293
+ maxToolResultChars,
294
+ callOptions: config,
295
+ },
296
+ projectionMode
297
+ );
298
+ const registry = context?.getOrCreateToolOutputRegistry?.();
299
+ const runId = config?.configurable?.run_id as string | undefined;
300
+ const annotated = annotateMessagesForLLM(projected, registry, runId);
301
+ const isRunProduced = context?.isRunProducedMessage;
302
+ const modelId = resolveServingModelId(model);
303
+ const cued = isAnthropicLike(provider, {
304
+ model: modelId,
305
+ })
306
+ ? appendPredecessorHandoffCue(
307
+ annotated,
308
+ isRunProduced == null
309
+ ? undefined
310
+ : (message): boolean => isRunProduced.call(context, message)
311
+ )
312
+ : removePredecessorHandoffCue(annotated);
313
+ const preparedMessages = strictAlternationProviders.has(provider)
314
+ ? coalesceAdjacentUserTurns(cued)
315
+ : cued;
316
+
317
+ const request: PreparedProviderRequestData = {
318
+ model,
319
+ modelId,
320
+ provider,
321
+ projectionMode,
322
+ messages: preparedMessages,
323
+ measurement: measure?.(preparedMessages),
324
+ };
325
+ Object.defineProperty(request, preparedProviderRequestBrand, {
326
+ value: true,
327
+ enumerable: false,
328
+ });
329
+ return Object.freeze(request) as PreparedProviderRequest;
330
+ }
331
+
332
+ export function assertPreparedProviderRequestFor(
333
+ request: PreparedProviderRequest,
334
+ model: t.ChatModel,
335
+ provider: Providers,
336
+ config?: RunnableConfig
337
+ ): void {
338
+ if (
339
+ !Object.prototype.hasOwnProperty.call(request, preparedProviderRequestBrand)
340
+ ) {
341
+ throw new Error('Invalid prepared provider request');
342
+ }
343
+ if (request.model !== model) {
344
+ throw new Error('Prepared provider request does not match serving model');
345
+ }
346
+ if (request.provider !== provider) {
347
+ throw new Error('Prepared provider request does not match serving provider');
348
+ }
349
+ if (
350
+ request.projectionMode !==
351
+ resolveProviderMessageProjectionMode(model, provider, config)
352
+ ) {
353
+ throw new Error(
354
+ 'Prepared provider request does not match invocation options'
355
+ );
356
+ }
357
+ }
@@ -3,6 +3,7 @@ import type {
3
3
  MessageContentComplex,
4
4
  } from '@langchain/core/messages';
5
5
  import { ContentTypes } from '@/common';
6
+ import { cloneMessage } from './cache';
6
7
 
7
8
  /**
8
9
  * Whether {@link formatContentStrings} will flatten this message's content:
@@ -42,13 +43,12 @@ export const formatContentStrings = (
42
43
  const blocks = message.content as MessageContentComplex[];
43
44
  const content = blocks.reduce((acc, curr) => {
44
45
  if (curr.type === ContentTypes.TEXT) {
45
- return `${acc}${curr[ContentTypes.TEXT] || ''}\n`;
46
+ return `${acc}${curr[ContentTypes.TEXT] ?? ''}\n`;
46
47
  }
47
48
  return acc;
48
49
  }, '');
49
50
 
50
- message.content = content.trim();
51
- result.push(message);
51
+ result.push(cloneMessage(message, content.trim()));
52
52
  }
53
53
 
54
54
  return result;
package/src/stream.ts CHANGED
@@ -820,6 +820,12 @@ function startEagerToolExecutions(args: {
820
820
  toolCalls: entries.map((entry) => entry.request),
821
821
  userId: graph.config?.configurable?.user_id as string | undefined,
822
822
  agentId: agentContext?.agentId,
823
+ callerCapabilityProjection:
824
+ (
825
+ agentContext as
826
+ | Partial<Pick<AgentContext, 'getCallerCapabilityProjectionSnapshot'>>
827
+ | undefined
828
+ )?.getCallerCapabilityProjectionSnapshot?.(),
823
829
  configurable: graph.config?.configurable as
824
830
  | Record<string, unknown>
825
831
  | undefined,
@@ -14,6 +14,74 @@ export type CallerCapabilityProjection = {
14
14
  codeExecutionOnlyTools: t.LCTool[];
15
15
  };
16
16
 
17
+ /**
18
+ * Combines event schemas with runtime capability metadata. Matching runtime
19
+ * entries override caller/defer policy without replacing the event schema;
20
+ * runtime-only definitions are appended intact.
21
+ */
22
+ export function mergeCallerCapabilityDefinitions(
23
+ toolDefs: Iterable<t.LCTool> | null | undefined,
24
+ overrides: Iterable<t.LCTool> | null | undefined
25
+ ): t.LCTool[] {
26
+ const runtimeDefinitions = Array.from(overrides ?? []);
27
+ const merged = new Map(
28
+ applyCallerCapabilityDefinitionOverrides(
29
+ toolDefs ?? [],
30
+ runtimeDefinitions
31
+ ).map((toolDef) => [toolDef.name, toolDef])
32
+ );
33
+ for (const override of runtimeDefinitions) {
34
+ if (!merged.has(override.name)) {
35
+ merged.set(override.name, override);
36
+ }
37
+ }
38
+ return Array.from(merged.values());
39
+ }
40
+
41
+ /**
42
+ * Applies runtime caller metadata to schema-only event definitions without
43
+ * adding registry-only tools or replacing their model-facing schemas.
44
+ */
45
+ export function applyCallerCapabilityDefinitionOverrides(
46
+ toolDefs: Iterable<t.LCTool>,
47
+ overrides: Iterable<t.LCTool> | null | undefined
48
+ ): t.LCTool[] {
49
+ const overridesByName = new Map<string, t.LCTool>();
50
+ for (const override of overrides ?? []) {
51
+ overridesByName.set(override.name, override);
52
+ }
53
+ return Array.from(toolDefs, (toolDef) => {
54
+ const override = overridesByName.get(toolDef.name);
55
+ if (override == null) {
56
+ return toolDef;
57
+ }
58
+ return {
59
+ ...toolDef,
60
+ allowed_callers: override.allowed_callers,
61
+ defer_loading: override.defer_loading,
62
+ };
63
+ });
64
+ }
65
+
66
+ /** Converts the live projection into the versioned event transport shape. */
67
+ export function createCallerCapabilityProjectionSnapshot(
68
+ projection: CallerCapabilityProjection
69
+ ): t.CallerCapabilityProjectionSnapshot {
70
+ return {
71
+ version: 1,
72
+ directToolNames: projection.directTools.map((toolDef) => toolDef.name),
73
+ codeExecutionToolNames: projection.codeExecutionTools.map(
74
+ (toolDef) => toolDef.name
75
+ ),
76
+ directOnlyToolNames: projection.directOnlyTools.map(
77
+ (toolDef) => toolDef.name
78
+ ),
79
+ codeExecutionOnlyToolNames: projection.codeExecutionOnlyTools.map(
80
+ (toolDef) => toolDef.name
81
+ ),
82
+ };
83
+ }
84
+
17
85
  export function getAllowedCallers(
18
86
  toolDef: t.LCTool
19
87
  ): readonly t.AllowedCaller[] {
@@ -95,8 +95,11 @@ import {
95
95
  resolveLocalExecutionTools,
96
96
  } from '@/tools/local';
97
97
  import {
98
+ type CallerCapabilityProjection,
99
+ createCallerCapabilityProjectionSnapshot,
98
100
  isToolDefinitionActive,
99
101
  isProgrammaticControlTool,
102
+ mergeCallerCapabilityDefinitions,
100
103
  resolveCallerCapabilityProjection,
101
104
  } from '@/tools/CallerCapabilities';
102
105
  import { stripCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';
@@ -698,6 +701,10 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
698
701
  >();
699
702
  /** Tool registry for filtering (lazy computation of programmatic maps) */
700
703
  private toolRegistry?: t.LCToolRegistry;
704
+ /** Schema-only definitions used when event mode has no runtime registry. */
705
+ private toolDefinitions?: t.LCToolRegistry;
706
+ /** Tool-map entries created or replaced by the local execution resolver. */
707
+ private localImplementationNames = new Set<string>();
701
708
  /** Reads deferred-tool discovery state from the owning agent context. */
702
709
  private getDiscoveredToolNames?: () => readonly string[];
703
710
  /** Reference to Graph's sessions map for automatic session injection */
@@ -803,6 +810,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
803
810
  handleToolErrors,
804
811
  loadRuntimeTools,
805
812
  toolRegistry,
813
+ toolDefinitions,
806
814
  getDiscoveredToolNames,
807
815
  sessions,
808
816
  codeSessionKey,
@@ -879,6 +887,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
879
887
  toolRegistry,
880
888
  toolExecution,
881
889
  });
890
+ this.toolDefinitions = toolDefinitions;
882
891
  this.getDiscoveredToolNames = getDiscoveredToolNames;
883
892
  this.sessions = sessions;
884
893
  this.codeSessionKey = codeSessionKey ?? Constants.EXECUTE_CODE;
@@ -999,6 +1008,7 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
999
1008
  });
1000
1009
 
1001
1010
  this.toolMap = resolved.toolMap;
1011
+ this.localImplementationNames = resolved.localImplementationNames;
1002
1012
  if (resolved.fileCheckpointer != null) {
1003
1013
  this.fileCheckpointer = resolved.fileCheckpointer;
1004
1014
  }
@@ -1105,27 +1115,58 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
1105
1115
  this.settledInterruptingResults.clear();
1106
1116
  }
1107
1117
 
1118
+ /** Returns the live caller projection used by direct and event execution. */
1119
+ private getCallerCapabilityProjection(): CallerCapabilityProjection {
1120
+ const discoveredToolNames = new Set(
1121
+ this.getDiscoveredToolNames?.() ?? []
1122
+ );
1123
+ return resolveCallerCapabilityProjection(
1124
+ mergeCallerCapabilityDefinitions(
1125
+ this.toolDefinitions?.values(),
1126
+ this.toolRegistry?.values()
1127
+ ),
1128
+ (toolDef) => isToolDefinitionActive(toolDef, discoveredToolNames)
1129
+ );
1130
+ }
1131
+
1132
+ /** Serializes the live caller projection for event-driven hosts. */
1133
+ private getCallerCapabilityProjectionSnapshot(): t.CallerCapabilityProjectionSnapshot {
1134
+ return createCallerCapabilityProjectionSnapshot(
1135
+ this.getCallerCapabilityProjection()
1136
+ );
1137
+ }
1138
+
1108
1139
  /** Returns active tools projected by their effective caller capabilities. */
1109
1140
  private getProgrammaticTools(): t.ProgrammaticCache {
1110
1141
  const toolMap: t.ToolMap = new Map();
1142
+ const toolDefs: t.LCTool[] = [];
1111
1143
  const discoveredToolNames = new Set(
1112
1144
  this.getDiscoveredToolNames?.() ?? []
1113
1145
  );
1114
- const capabilities = resolveCallerCapabilityProjection(
1146
+ const executableCapabilities = resolveCallerCapabilityProjection(
1115
1147
  this.toolRegistry?.values() ?? [],
1116
1148
  (toolDef) => isToolDefinitionActive(toolDef, discoveredToolNames)
1117
1149
  );
1118
- for (const toolDef of capabilities.codeExecutionTools) {
1150
+ for (const toolDef of executableCapabilities.codeExecutionTools) {
1151
+ if (
1152
+ this.eventDrivenMode &&
1153
+ this.directToolNames?.has(toolDef.name) !== true &&
1154
+ !this.localImplementationNames.has(toolDef.name)
1155
+ ) {
1156
+ continue;
1157
+ }
1119
1158
  const tool = this.toolMap.get(toolDef.name);
1120
1159
  if (tool != null) {
1121
1160
  toolMap.set(toolDef.name, tool);
1161
+ toolDefs.push(toolDef);
1122
1162
  }
1123
1163
  }
1164
+ const activeCapabilities = this.getCallerCapabilityProjection();
1124
1165
 
1125
1166
  return {
1126
1167
  toolMap,
1127
- toolDefs: capabilities.codeExecutionTools,
1128
- disallowedToolDefs: capabilities.directOnlyTools
1168
+ toolDefs,
1169
+ disallowedToolDefs: activeCapabilities.directOnlyTools
1129
1170
  .filter((toolDef) => !isProgrammaticControlTool(toolDef.name))
1130
1171
  .map((toolDef) => ({ name: toolDef.name })),
1131
1172
  };
@@ -3455,6 +3496,8 @@ export class ToolNode<T = any> extends RunnableCallable<T, T> {
3455
3496
  // the eager path sends `agentContext.agentId` — this must
3456
3497
  // match it at the top level too.
3457
3498
  agentId: this.executingAgentId,
3499
+ callerCapabilityProjection:
3500
+ this.getCallerCapabilityProjectionSnapshot(),
3458
3501
  configurable: stripRunBreakerScope(
3459
3502
  config.configurable as Record<string, unknown> | undefined
3460
3503
  ),