@librechat/agents 3.2.56 → 3.2.58

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 (92) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +7 -1
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +7 -2
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
  6. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  7. package/dist/cjs/langfuseTraceShaping.cjs +172 -0
  8. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -0
  9. package/dist/cjs/run.cjs +6 -2
  10. package/dist/cjs/run.cjs.map +1 -1
  11. package/dist/cjs/stream.cjs +3 -2
  12. package/dist/cjs/stream.cjs.map +1 -1
  13. package/dist/cjs/tools/ToolNode.cjs +19 -5
  14. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  15. package/dist/cjs/tools/search/keenable-search.cjs +68 -0
  16. package/dist/cjs/tools/search/keenable-search.cjs.map +1 -0
  17. package/dist/cjs/tools/search/rerankers.cjs +28 -11
  18. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  19. package/dist/cjs/tools/search/search.cjs +30 -4
  20. package/dist/cjs/tools/search/search.cjs.map +1 -1
  21. package/dist/cjs/tools/search/tool.cjs +14 -6
  22. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  23. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +12 -1
  24. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  25. package/dist/cjs/utils/title.cjs +9 -9
  26. package/dist/cjs/utils/title.cjs.map +1 -1
  27. package/dist/esm/agents/AgentContext.mjs +7 -1
  28. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  29. package/dist/esm/graphs/Graph.mjs +7 -2
  30. package/dist/esm/graphs/Graph.mjs.map +1 -1
  31. package/dist/esm/langfuseToolOutputTracing.mjs +4 -0
  32. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  33. package/dist/esm/langfuseTraceShaping.mjs +171 -0
  34. package/dist/esm/langfuseTraceShaping.mjs.map +1 -0
  35. package/dist/esm/run.mjs +6 -2
  36. package/dist/esm/run.mjs.map +1 -1
  37. package/dist/esm/stream.mjs +3 -2
  38. package/dist/esm/stream.mjs.map +1 -1
  39. package/dist/esm/tools/ToolNode.mjs +19 -5
  40. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  41. package/dist/esm/tools/search/keenable-search.mjs +66 -0
  42. package/dist/esm/tools/search/keenable-search.mjs.map +1 -0
  43. package/dist/esm/tools/search/rerankers.mjs +28 -11
  44. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  45. package/dist/esm/tools/search/search.mjs +30 -4
  46. package/dist/esm/tools/search/search.mjs.map +1 -1
  47. package/dist/esm/tools/search/tool.mjs +14 -6
  48. package/dist/esm/tools/search/tool.mjs.map +1 -1
  49. package/dist/esm/tools/subagent/SubagentExecutor.mjs +12 -1
  50. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  51. package/dist/esm/utils/title.mjs +9 -9
  52. package/dist/esm/utils/title.mjs.map +1 -1
  53. package/dist/types/graphs/Graph.d.ts +1 -0
  54. package/dist/types/langfuseTraceShaping.d.ts +20 -0
  55. package/dist/types/run.d.ts +1 -0
  56. package/dist/types/tools/ToolNode.d.ts +11 -1
  57. package/dist/types/tools/search/keenable-search.d.ts +4 -0
  58. package/dist/types/tools/search/rerankers.d.ts +7 -2
  59. package/dist/types/tools/search/types.d.ts +38 -1
  60. package/dist/types/types/graph.d.ts +22 -0
  61. package/dist/types/types/run.d.ts +9 -0
  62. package/dist/types/types/tools.d.ts +6 -0
  63. package/package.json +1 -1
  64. package/src/__tests__/stream.eagerEventExecution.test.ts +44 -0
  65. package/src/agents/AgentContext.ts +9 -0
  66. package/src/agents/__tests__/AgentContext.test.ts +40 -0
  67. package/src/graphs/Graph.ts +31 -3
  68. package/src/graphs/__tests__/composition.smoke.test.ts +53 -0
  69. package/src/langfuseToolOutputTracing.ts +11 -0
  70. package/src/langfuseTraceShaping.ts +280 -0
  71. package/src/llm/anthropic/inherited-stream-events.spec.ts +6 -3
  72. package/src/run.ts +7 -3
  73. package/src/specs/langfuse-routing.integration.test.ts +1 -1
  74. package/src/specs/langfuse-trace-shaping.test.ts +194 -0
  75. package/src/stream.ts +10 -2
  76. package/src/tools/ToolNode.ts +28 -4
  77. package/src/tools/__tests__/SubagentExecutor.test.ts +44 -0
  78. package/src/tools/__tests__/ToolNode.session.test.ts +160 -0
  79. package/src/tools/__tests__/hitl.test.ts +85 -0
  80. package/src/tools/search/jina-reranker.test.ts +70 -1
  81. package/src/tools/search/keenable-search.ts +98 -0
  82. package/src/tools/search/keenable.test.ts +183 -0
  83. package/src/tools/search/rerankers.ts +41 -4
  84. package/src/tools/search/search.ts +58 -2
  85. package/src/tools/search/source-processing.test.ts +86 -0
  86. package/src/tools/search/tool.ts +29 -4
  87. package/src/tools/search/types.ts +42 -1
  88. package/src/tools/subagent/SubagentExecutor.ts +11 -0
  89. package/src/types/graph.ts +22 -0
  90. package/src/types/run.ts +9 -0
  91. package/src/types/tools.ts +6 -0
  92. package/src/utils/title.ts +9 -9
@@ -91,6 +91,8 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
91
91
  private eventDrivenMode;
92
92
  /** Opt-in stream-layer prestart config for event-driven tools. */
93
93
  private eagerEventToolExecution?;
94
+ /** Host tools that write to the code sandbox and share its exec session. */
95
+ private codeSessionToolNames?;
94
96
  /** Shared per-run prestarted tool registry populated by ChatModelStreamHandler. */
95
97
  private eagerEventToolExecutions?;
96
98
  /** Shared per-run per-tool turn counter used by eager and normal event dispatch. */
@@ -145,7 +147,7 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
145
147
  * other's in-flight state.
146
148
  */
147
149
  private anonBatchCounter;
148
- constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, agentId, executingAgentId, directToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, }: t.ToolNodeConstructorParams);
150
+ constructor({ tools, toolMap, name, tags, trace, runLangfuse, agentLangfuse, errorHandler, toolCallStepIds, handleToolErrors, loadRuntimeTools, toolRegistry, sessions, eventDrivenMode, eagerEventToolExecution, eagerEventToolExecutions, eagerEventToolUsageCount, agentId, executingAgentId, directToolNames, codeSessionToolNames, maxContextTokens, maxToolResultChars, hookRegistry, humanInTheLoop, toolOutputReferences, toolOutputRegistry, toolExecution, fileCheckpointer, }: t.ToolNodeConstructorParams);
149
151
  invoke(input: any, options?: Partial<RunnableConfig>): Promise<any>;
150
152
  /**
151
153
  * Returns the run-scoped tool output registry, or `undefined` when
@@ -304,6 +306,14 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
304
306
  * Extracts code execution session context from tool results and stores in Graph.sessions.
305
307
  * Mirrors the session storage logic in handleRunToolCompletions for direct execution.
306
308
  */
309
+ /**
310
+ * True when a tool's successful result should fold its returned exec
311
+ * `session_id` into the shared code session: built-in `CODE_EXECUTION_TOOLS`,
312
+ * plus host-declared sandbox-writing tools (`codeSessionToolNames`, e.g.
313
+ * create_file/edit_file). Kept name-scoped rather than a blanket artifact
314
+ * opt-in so only host-declared tools can influence the shared session.
315
+ */
316
+ private participatesInCodeSession;
307
317
  private storeCodeSessionFromResults;
308
318
  /**
309
319
  * Post-processes standard runTool outputs: dispatches ON_RUN_STEP_COMPLETED
@@ -0,0 +1,4 @@
1
+ import type * as t from './types';
2
+ export declare const createKeenableAPI: (apiKey?: string, apiUrl?: string, options?: t.KeenableSearchOptions) => {
3
+ getSources: (params: t.GetSourcesParams) => Promise<t.SearchResult>;
4
+ };
@@ -8,16 +8,20 @@ export declare abstract class BaseReranker {
8
8
  }
9
9
  export declare class JinaReranker extends BaseReranker {
10
10
  private apiUrl;
11
- constructor({ apiKey, apiUrl, logger, }: {
11
+ private timeout;
12
+ constructor({ apiKey, apiUrl, timeout, logger, }: {
12
13
  apiKey?: string;
13
14
  apiUrl?: string;
15
+ timeout?: number;
14
16
  logger?: t.Logger;
15
17
  });
16
18
  rerank(query: string, documents: string[], topK?: number): Promise<t.Highlight[]>;
17
19
  }
18
20
  export declare class CohereReranker extends BaseReranker {
19
- constructor({ apiKey, logger, }: {
21
+ private timeout;
22
+ constructor({ apiKey, timeout, logger, }: {
20
23
  apiKey?: string;
24
+ timeout?: number;
21
25
  logger?: t.Logger;
22
26
  });
23
27
  rerank(query: string, documents: string[], topK?: number): Promise<t.Highlight[]>;
@@ -34,5 +38,6 @@ export declare const createReranker: (config: {
34
38
  jinaApiKey?: string;
35
39
  jinaApiUrl?: string;
36
40
  cohereApiKey?: string;
41
+ rerankerTimeout?: number;
37
42
  logger?: t.Logger;
38
43
  }) => BaseReranker | undefined;
@@ -2,7 +2,7 @@ import type { RunnableConfig } from '@langchain/core/runnables';
2
2
  import type { Logger as WinstonLogger } from 'winston';
3
3
  import type { BaseReranker } from './rerankers';
4
4
  import { DATE_RANGE } from './schema';
5
- export type SearchProvider = 'serper' | 'searxng' | 'tavily';
5
+ export type SearchProvider = 'serper' | 'searxng' | 'tavily' | 'keenable';
6
6
  export type ScraperProvider = 'firecrawl' | 'serper' | 'tavily';
7
7
  export type RerankerType = 'infinity' | 'jina' | 'cohere' | 'none';
8
8
  export interface Highlight {
@@ -100,6 +100,32 @@ export interface SearchConfig {
100
100
  tavilySearchUrl?: string;
101
101
  tavilyExtractUrl?: string;
102
102
  tavilySearchOptions?: TavilySearchOptions;
103
+ keenableApiKey?: string;
104
+ keenableApiUrl?: string;
105
+ keenableSearchOptions?: KeenableSearchOptions;
106
+ }
107
+ export interface KeenableSearchOptions {
108
+ maxResults?: number;
109
+ /** Restrict results to a single domain, e.g. "github.com". */
110
+ site?: string;
111
+ /** Sent as the X-Keenable-Title attribution header. Defaults to "LibreChat". */
112
+ attributionTitle?: string;
113
+ timeout?: number;
114
+ }
115
+ export interface KeenableSearchPayload {
116
+ query: string;
117
+ site?: string;
118
+ published_after?: string;
119
+ }
120
+ export interface KeenableSearchResult {
121
+ title?: string;
122
+ url?: string;
123
+ description?: string;
124
+ snippet?: string;
125
+ published_at?: string;
126
+ }
127
+ export interface KeenableSearchResponse {
128
+ results?: KeenableSearchResult[];
103
129
  }
104
130
  export type References = {
105
131
  links: MediaReference[];
@@ -120,6 +146,15 @@ export interface ProcessSourcesConfig {
120
146
  * chunker/reranker. Defaults to 50,000; also configurable via the
121
147
  * `SEARCH_MAX_CONTENT_LENGTH` env var. */
122
148
  maxContentLength?: number;
149
+ /** Chunk size (chars) for splitting scraped content before reranking.
150
+ * Defaults to 150; also configurable via the `SEARCH_CHUNK_SIZE` env var.
151
+ * Larger chunks send fewer documents to the reranker (lower cost/latency);
152
+ * highlights are expanded ±300-450 chars around each hit either way. */
153
+ chunkSize?: number;
154
+ /** Overlap (chars) between adjacent chunks. Defaults to 50; also
155
+ * configurable via the `SEARCH_CHUNK_OVERLAP` env var. Clamped below
156
+ * `chunkSize`. */
157
+ chunkOverlap?: number;
123
158
  strategies?: string[];
124
159
  filterContent?: boolean;
125
160
  reranker?: BaseReranker;
@@ -202,6 +237,8 @@ export interface SearchToolConfig extends SearchConfig, ProcessSourcesConfig, Fi
202
237
  jinaApiUrl?: string;
203
238
  cohereApiKey?: string;
204
239
  rerankerType?: RerankerType;
240
+ /** Timeout (ms) for rerank API requests. Defaults to 10,000. */
241
+ rerankerTimeout?: number;
205
242
  scraperProvider?: ScraperProvider;
206
243
  scraperTimeout?: number;
207
244
  serperScraperOptions?: SerperScraperConfig;
@@ -480,6 +480,28 @@ export interface AgentInputs {
480
480
  subagentConfigs?: SubagentConfig[];
481
481
  /** Maximum subagent nesting depth. Default 1 means top-level agents can spawn subagents but subagents cannot nest further. */
482
482
  maxSubagentDepth?: number;
483
+ /**
484
+ * Host-supplied tool instances that must execute IN-PROCESS inside the graph's
485
+ * ToolNode even when the run is event-driven (`toolDefinitions` non-empty). Each
486
+ * instance is bound to the model alongside the schema-only event tools and its
487
+ * name is marked direct, so calls bypass ON_TOOL_EXECUTE dispatch and run inside
488
+ * the Pregel task frame. This is the only execution mode where a tool body may
489
+ * raise a LangGraph `interrupt()` (e.g. a tool built on `askUserQuestion()`) —
490
+ * the host-side event handler runs outside the graph task, where `interrupt()`
491
+ * throws. Do NOT also list these tools in `toolDefinitions` (they would be bound
492
+ * twice). NOT inherited by SELF-SPAWNED subagent children (their config is a
493
+ * shallow spread of the parent's inputs, and child graphs compile without a
494
+ * checkpointer, so an interrupt-capable tool could never pause there) —
495
+ * `buildChildInputs` scrubs the inherited copy; an EXPLICIT child config that
496
+ * lists its own `graphTools` keeps them.
497
+ *
498
+ * Deliberately `GenericTool[]`, not `GraphTools`: the wider union admits
499
+ * schema-only shapes (OpenAI `BindToolsInput`, Google tool objects) that
500
+ * `initializeTools` cannot register in the ToolNode direct map — the model
501
+ * would bind a tool the SDK advertised as in-process but cannot execute.
502
+ * Every entry must be a real executable tool instance with a `name`.
503
+ */
504
+ graphTools?: GenericTool[];
483
505
  }
484
506
  export interface ContextPruningConfig {
485
507
  enabled?: boolean;
@@ -170,6 +170,15 @@ export type RunConfig = {
170
170
  * eager dispatch unsafe.
171
171
  */
172
172
  eagerEventToolExecution?: EagerEventToolExecutionConfig;
173
+ /**
174
+ * Names of host tools that write to the code-execution sandbox but are not
175
+ * built-in `CODE_EXECUTION_TOOLS` (e.g. LibreChat's create_file/edit_file).
176
+ * Their successful results fold the returned exec `session_id` into the
177
+ * shared code session, so a file such a tool writes is visible to later
178
+ * bash_tool/execute_code calls running in the same sandbox. Host-declared so
179
+ * the SDK stays name-agnostic.
180
+ */
181
+ codeSessionToolNames?: string[];
173
182
  /**
174
183
  * Selects the execution backend for built-in code tools. Omit this to keep
175
184
  * the remote LibreChat Code API sandbox. Set `{ engine: 'local' }` to run
@@ -100,6 +100,12 @@ export type ToolNodeOptions = {
100
100
  directToolNames?: Set<string>;
101
101
  /** Opt-in eager execution for event-driven tool calls. */
102
102
  eagerEventToolExecution?: EagerEventToolExecutionConfig;
103
+ /**
104
+ * Host tool names that write to the code-execution sandbox but are not
105
+ * built-in `CODE_EXECUTION_TOOLS`. Their exec `session_id` is folded into the
106
+ * shared code session so later bash_tool/execute_code calls see written files.
107
+ */
108
+ codeSessionToolNames?: string[];
103
109
  /** Shared per-run eager execution registry populated by the stream handler. */
104
110
  eagerEventToolExecutions?: Map<string, EagerEventToolExecution>;
105
111
  /** Shared per-run per-tool turn counter used by eager and normal event dispatch. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.2.56",
3
+ "version": "3.2.58",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -4772,6 +4772,50 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
4772
4772
  expect(graph.eagerEventToolExecutions.has('call_file')).toBe(false);
4773
4773
  });
4774
4774
 
4775
+ it('does not prestart codeSessionToolNames tools even without excludeToolNames', async () => {
4776
+ // A declared session-writing host tool is side-effecting, so it must not be
4777
+ // eagerly prestarted even when the host didn't also list it in excludeToolNames.
4778
+ const graph = createGraph({
4779
+ eagerEventToolExecution: { enabled: true },
4780
+ codeSessionToolNames: ['create_file', 'edit_file'],
4781
+ });
4782
+ const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
4783
+ jest
4784
+ .spyOn(events, 'safeDispatchCustomEvent')
4785
+ .mockImplementation(async (event, data): Promise<void> => {
4786
+ if (event !== GraphEvents.ON_TOOL_EXECUTE) {
4787
+ return;
4788
+ }
4789
+ const batch = data as t.ToolExecuteBatchRequest;
4790
+ toolExecuteCalls.push(batch);
4791
+ batch.resolve([
4792
+ { toolCallId: 'call_cf2', status: 'success', content: 'ok' },
4793
+ ]);
4794
+ });
4795
+
4796
+ await new ChatModelStreamHandler().handle(
4797
+ GraphEvents.CHAT_MODEL_STREAM,
4798
+ {
4799
+ chunk: {
4800
+ content: '',
4801
+ tool_calls: [
4802
+ {
4803
+ id: 'call_cf2',
4804
+ name: 'create_file',
4805
+ args: { path: '/mnt/data/y.py', content: 'print(2)' },
4806
+ },
4807
+ ],
4808
+ response_metadata: finalToolCallResponseMetadata,
4809
+ } as unknown as t.StreamChunk,
4810
+ },
4811
+ { langgraph_node: 'agent' },
4812
+ graph
4813
+ );
4814
+
4815
+ expect(toolExecuteCalls).toHaveLength(0);
4816
+ expect(graph.eagerEventToolExecutions.has('call_cf2')).toBe(false);
4817
+ });
4818
+
4775
4819
  it('keeps the direct-tool batch guard when an excluded tool is also direct', async () => {
4776
4820
  // edit_file is both a direct graph tool AND excluded from eager. A mixed
4777
4821
  // batch with a direct tool must suppress eager for the whole batch, so the
@@ -85,6 +85,7 @@ export class AgentContext {
85
85
  toolSchemaTokens,
86
86
  subagentConfigs,
87
87
  maxSubagentDepth,
88
+ graphTools,
88
89
  } = agentConfig;
89
90
 
90
91
  const agentContext = new AgentContext({
@@ -116,6 +117,14 @@ export class AgentContext {
116
117
  agentContext._sourceInputs = agentConfig;
117
118
  agentContext.subagentConfigs = subagentConfigs;
118
119
  agentContext.maxSubagentDepth = maxSubagentDepth;
120
+ /**
121
+ * Host-supplied direct tools (see `AgentInputs.graphTools`). Copied — never
122
+ * aliased — because the SDK later pushes graph-managed tools (handoff /
123
+ * subagent) into this same array and must not mutate the host's input.
124
+ */
125
+ if (graphTools && graphTools.length > 0) {
126
+ agentContext.graphTools = [...graphTools];
127
+ }
119
128
 
120
129
  if (initialSummary?.text != null && initialSummary.text !== '') {
121
130
  agentContext.setInitialSummary(
@@ -42,6 +42,46 @@ describe('AgentContext', () => {
42
42
  schema: { type: 'object', properties: {} },
43
43
  }) as unknown as t.GenericTool;
44
44
 
45
+ describe('fromConfig — host-supplied graphTools', () => {
46
+ it('copies graphTools onto the context without aliasing the host array', () => {
47
+ const askTool = createMockTool('ask_user_question');
48
+ const hostGraphTools = [askTool];
49
+ const ctx = createBasicContext({
50
+ agentConfig: { graphTools: hostGraphTools },
51
+ });
52
+
53
+ expect(ctx.graphTools).toEqual([askTool]);
54
+ expect(ctx.graphTools).not.toBe(hostGraphTools);
55
+
56
+ /** The SDK pushes graph-managed tools (handoff/subagent) into this
57
+ * array later — that must never mutate the host's input. */
58
+ (ctx.graphTools as t.GenericTool[]).push(createMockTool('subagent'));
59
+ expect(hostGraphTools).toHaveLength(1);
60
+ });
61
+
62
+ it('leaves graphTools undefined when the host supplies none (or an empty list)', () => {
63
+ expect(createBasicContext().graphTools).toBeUndefined();
64
+ expect(
65
+ createBasicContext({ agentConfig: { graphTools: [] } }).graphTools
66
+ ).toBeUndefined();
67
+ });
68
+
69
+ it('binds host graphTools to the model in event-driven mode', () => {
70
+ const askTool = createMockTool('ask_user_question');
71
+ const ctx = createBasicContext({
72
+ agentConfig: {
73
+ graphTools: [askTool],
74
+ toolDefinitions: [{ name: 'echo', description: 'host event tool' }],
75
+ },
76
+ });
77
+
78
+ const bound = ctx.getToolsForBinding() ?? [];
79
+ const names = (bound as Array<{ name?: string }>).map((t) => t.name);
80
+ expect(names).toContain('ask_user_question');
81
+ expect(names).toContain('echo');
82
+ });
83
+ });
84
+
45
85
  describe('System Runnable - Lazy Creation', () => {
46
86
  it('creates system runnable on first access', () => {
47
87
  const ctx = createBasicContext({
@@ -66,8 +66,8 @@ import {
66
66
  type CallbackEntry,
67
67
  } from '@/utils/callbacks';
68
68
  import { partitionAndMarkOpenRouterToolCache } from '@/llm/openrouter/toolCache';
69
- import { shouldTraceToolNodeForLangfuse } from '@/langfuseToolOutputTracing';
70
69
  import { ToolNode as CustomToolNode, toolsCondition } from '@/tools/ToolNode';
70
+ import { shouldTraceToolNodeForLangfuse } from '@/langfuseToolOutputTracing';
71
71
  import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
72
72
  import { SubagentExecutor, resolveSubagentConfigs } from '@/tools/subagent';
73
73
  import { ToolOutputReferenceRegistry } from '@/tools/toolOutputReferences';
@@ -608,6 +608,7 @@ export abstract class Graph<
608
608
  * consumes the settled promises while preserving final ToolMessage order.
609
609
  */
610
610
  eagerEventToolExecution: t.EagerEventToolExecutionConfig | undefined;
611
+ codeSessionToolNames: string[] | undefined;
611
612
  eagerEventToolExecutions: Map<string, t.EagerEventToolExecution> = new Map();
612
613
  eagerEventToolUsageCount: Map<string, number> = new Map();
613
614
  private eagerEventToolUsageCountsByAgentId: Map<string, Map<string, number>> =
@@ -656,6 +657,7 @@ export abstract class Graph<
656
657
  this.humanInTheLoop = undefined;
657
658
  this.toolOutputReferences = undefined;
658
659
  this.eagerEventToolExecution = undefined;
660
+ this.codeSessionToolNames = undefined;
659
661
  this.eagerEventToolExecutions.clear();
660
662
  this.clearEagerEventToolUsageCounts();
661
663
  this.eagerEventToolCallChunks.clear();
@@ -1138,7 +1140,15 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1138
1140
  getToolCount(): number {
1139
1141
  const context = this.agentContexts.get(this.defaultAgentId);
1140
1142
  return (
1141
- (context?.tools?.length ?? 0) + (context?.toolDefinitions?.length ?? 0)
1143
+ (context?.tools?.length ?? 0) +
1144
+ (context?.toolDefinitions?.length ?? 0) +
1145
+ /**
1146
+ * Graph-managed + host-supplied direct tools (handoff, subagent,
1147
+ * `AgentInputs.graphTools`) are bound to the model and token-accounted,
1148
+ * so a count that omits them under-reports the run's tool surface
1149
+ * (Codex #289 P3).
1150
+ */
1151
+ (context?.graphTools?.length ?? 0)
1142
1152
  );
1143
1153
  }
1144
1154
 
@@ -1260,6 +1270,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1260
1270
  hookRegistry: this.hookRegistry,
1261
1271
  humanInTheLoop: this.humanInTheLoop,
1262
1272
  eagerEventToolExecution: this.eagerEventToolExecution,
1273
+ codeSessionToolNames: this.codeSessionToolNames,
1263
1274
  eagerEventToolExecutions: this.eagerEventToolExecutions,
1264
1275
  eagerEventToolUsageCount: this.getEagerEventToolUsageCount(
1265
1276
  agentContext?.agentId
@@ -1283,10 +1294,25 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1283
1294
  graphTools && graphTools.length > 0
1284
1295
  ? [...baseTools, ...graphTools]
1285
1296
  : baseTools;
1297
+ /**
1298
+ * ToolNode treats a supplied `toolMap` as authoritative (it only derives
1299
+ * one from `tools` when the param is undefined), so when graphTools force
1300
+ * us to build a merged map here, an absent `currentToolMap` must be
1301
+ * seeded from the BASE tools first — otherwise ordinary tools stay bound
1302
+ * to the model but vanish from the execution map and every call to them
1303
+ * fails as an unknown tool (Codex #289 round 2).
1304
+ */
1286
1305
  const traditionalToolMap =
1287
1306
  graphTools && graphTools.length > 0
1288
1307
  ? new Map([
1289
- ...(currentToolMap ?? new Map()),
1308
+ ...(currentToolMap ??
1309
+ new Map(
1310
+ baseTools
1311
+ .filter(
1312
+ (t): t is t.GenericTool & { name: string } => 'name' in t
1313
+ )
1314
+ .map((t) => [t.name, t] as [string, t.GenericTool])
1315
+ )),
1290
1316
  ...graphTools
1291
1317
  .filter((t): t is t.GenericTool & { name: string } => 'name' in t)
1292
1318
  .map((t) => [t.name, t] as [string, t.GenericTool]),
@@ -1309,6 +1335,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1309
1335
  toolRegistry: agentContext?.toolRegistry,
1310
1336
  sessions: this.sessions,
1311
1337
  toolExecution: this.toolExecution,
1338
+ codeSessionToolNames: this.codeSessionToolNames,
1312
1339
  hookRegistry: this.hookRegistry,
1313
1340
  humanInTheLoop: this.humanInTheLoop,
1314
1341
  maxContextTokens: agentContext?.maxContextTokens,
@@ -2333,6 +2360,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2333
2360
  */
2334
2361
  childGraph.toolOutputReferences = this.toolOutputReferences;
2335
2362
  childGraph.eagerEventToolExecution = this.eagerEventToolExecution;
2363
+ childGraph.codeSessionToolNames = this.codeSessionToolNames;
2336
2364
  childGraph.toolExecution = this.toolExecution;
2337
2365
  childGraph.eventToolExecutionAvailable =
2338
2366
  this.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) !=
@@ -73,6 +73,59 @@ const expectCompiledWorkflow = (
73
73
  };
74
74
 
75
75
  describe('LangGraph composition smoke tests', () => {
76
+ it('getToolCount includes direct graph tools (host-supplied graphTools) alongside instances and definitions', () => {
77
+ const askLikeTool = { name: 'ask_user_question' } as unknown as NonNullable<
78
+ t.AgentInputs['graphTools']
79
+ >[number];
80
+ const graph = new StandardGraph({
81
+ runId: 'toolcount-smoke',
82
+ agents: [
83
+ {
84
+ ...makeAgent('agent'),
85
+ toolDefinitions: [
86
+ { name: 'evt1', description: 'event tool one' },
87
+ { name: 'evt2', description: 'event tool two' },
88
+ ],
89
+ graphTools: [askLikeTool],
90
+ },
91
+ ],
92
+ });
93
+ // 2 schema-only event tools + 1 in-process direct tool — all bound to the
94
+ // model and token-accounted, so all must be counted (Codex #289 P3).
95
+ expect(graph.getToolCount()).toBe(3);
96
+ });
97
+
98
+ it('keeps ordinary tools executable when graphTools are added without a host toolMap (traditional mode)', () => {
99
+ type HostTool = NonNullable<t.AgentInputs['graphTools']>[number];
100
+ const echoTool = { name: 'echo_tool' } as unknown as HostTool;
101
+ const askLikeTool = { name: 'ask_user_question' } as unknown as HostTool;
102
+ const graph = new StandardGraph({
103
+ runId: 'toolmap-merge-smoke',
104
+ agents: [
105
+ {
106
+ ...makeAgent('agent'),
107
+ tools: [echoTool],
108
+ graphTools: [askLikeTool],
109
+ },
110
+ ],
111
+ });
112
+ const agentContext = graph.agentContexts.get('agent');
113
+ const node = graph.initializeTools({
114
+ currentTools: agentContext?.tools,
115
+ currentToolMap: undefined,
116
+ agentContext,
117
+ });
118
+ /**
119
+ * ToolNode treats a provided toolMap as authoritative — if the merged map
120
+ * built for graphTools drops the base tools, they stay bound to the model
121
+ * but every call fails as an unknown tool (Codex #289 round 2).
122
+ */
123
+ const toolMap = (node as unknown as { toolMap: Map<string, unknown> })
124
+ .toolMap;
125
+ expect(toolMap.has('echo_tool')).toBe(true);
126
+ expect(toolMap.has('ask_user_question')).toBe(true);
127
+ });
128
+
76
129
  it('clears run-scoped eager tool state on reset', () => {
77
130
  const graph = new StandardGraph({
78
131
  runId: 'standard-eager-reset',
@@ -16,6 +16,10 @@ import {
16
16
  resolveLangfuseConfig,
17
17
  resolveToolOutputTracingConfig,
18
18
  } from '@/langfuseConfig';
19
+ import {
20
+ shapeLangfuseSpan,
21
+ shouldDropLangfuseSpan,
22
+ } from '@/langfuseTraceShaping';
19
23
  import { resolveToolOutputTracingConfigForSpan } from '@/langfuseRuntimeScope';
20
24
 
21
25
  export { LANGFUSE_TOOL_OUTPUT_REDACTION_TEXT, resolveLangfuseConfig };
@@ -407,6 +411,9 @@ class ToolOutputRedactingLangfuseSpanProcessor implements SpanProcessor {
407
411
  }
408
412
 
409
413
  onStart(span: Span, parentContext: Context): void {
414
+ if (shouldDropLangfuseSpan(span.name)) {
415
+ return;
416
+ }
410
417
  const config =
411
418
  resolveToolOutputTracingConfigForSpan(parentContext) ??
412
419
  this.fallbackConfig;
@@ -417,7 +424,11 @@ class ToolOutputRedactingLangfuseSpanProcessor implements SpanProcessor {
417
424
  }
418
425
 
419
426
  onEnd(span: ReadableSpan): void {
427
+ if (shouldDropLangfuseSpan(span.name)) {
428
+ return;
429
+ }
420
430
  classifyLangfuseToolNodeSpan(span);
431
+ shapeLangfuseSpan(span);
421
432
  const config = this.spanConfigs.get(span) ?? this.fallbackConfig;
422
433
  if (config != null) {
423
434
  redactLangfuseSpanToolOutputs(span, config);