@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.
- package/dist/cjs/agents/AgentContext.cjs +7 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +7 -2
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/langfuseToolOutputTracing.cjs +4 -0
- package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
- package/dist/cjs/langfuseTraceShaping.cjs +172 -0
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -0
- package/dist/cjs/run.cjs +6 -2
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/stream.cjs +3 -2
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +19 -5
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-search.cjs +68 -0
- package/dist/cjs/tools/search/keenable-search.cjs.map +1 -0
- package/dist/cjs/tools/search/rerankers.cjs +28 -11
- package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
- package/dist/cjs/tools/search/search.cjs +30 -4
- package/dist/cjs/tools/search/search.cjs.map +1 -1
- package/dist/cjs/tools/search/tool.cjs +14 -6
- package/dist/cjs/tools/search/tool.cjs.map +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +12 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
- package/dist/cjs/utils/title.cjs +9 -9
- package/dist/cjs/utils/title.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +7 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +7 -2
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/langfuseToolOutputTracing.mjs +4 -0
- package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
- package/dist/esm/langfuseTraceShaping.mjs +171 -0
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -0
- package/dist/esm/run.mjs +6 -2
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/stream.mjs +3 -2
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +19 -5
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-search.mjs +66 -0
- package/dist/esm/tools/search/keenable-search.mjs.map +1 -0
- package/dist/esm/tools/search/rerankers.mjs +28 -11
- package/dist/esm/tools/search/rerankers.mjs.map +1 -1
- package/dist/esm/tools/search/search.mjs +30 -4
- package/dist/esm/tools/search/search.mjs.map +1 -1
- package/dist/esm/tools/search/tool.mjs +14 -6
- package/dist/esm/tools/search/tool.mjs.map +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +12 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
- package/dist/esm/utils/title.mjs +9 -9
- package/dist/esm/utils/title.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +1 -0
- package/dist/types/langfuseTraceShaping.d.ts +20 -0
- package/dist/types/run.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +11 -1
- package/dist/types/tools/search/keenable-search.d.ts +4 -0
- package/dist/types/tools/search/rerankers.d.ts +7 -2
- package/dist/types/tools/search/types.d.ts +38 -1
- package/dist/types/types/graph.d.ts +22 -0
- package/dist/types/types/run.d.ts +9 -0
- package/dist/types/types/tools.d.ts +6 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +44 -0
- package/src/agents/AgentContext.ts +9 -0
- package/src/agents/__tests__/AgentContext.test.ts +40 -0
- package/src/graphs/Graph.ts +31 -3
- package/src/graphs/__tests__/composition.smoke.test.ts +53 -0
- package/src/langfuseToolOutputTracing.ts +11 -0
- package/src/langfuseTraceShaping.ts +280 -0
- package/src/llm/anthropic/inherited-stream-events.spec.ts +6 -3
- package/src/run.ts +7 -3
- package/src/specs/langfuse-routing.integration.test.ts +1 -1
- package/src/specs/langfuse-trace-shaping.test.ts +194 -0
- package/src/stream.ts +10 -2
- package/src/tools/ToolNode.ts +28 -4
- package/src/tools/__tests__/SubagentExecutor.test.ts +44 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +160 -0
- package/src/tools/__tests__/hitl.test.ts +85 -0
- package/src/tools/search/jina-reranker.test.ts +70 -1
- package/src/tools/search/keenable-search.ts +98 -0
- package/src/tools/search/keenable.test.ts +183 -0
- package/src/tools/search/rerankers.ts +41 -4
- package/src/tools/search/search.ts +58 -2
- package/src/tools/search/source-processing.test.ts +86 -0
- package/src/tools/search/tool.ts +29 -4
- package/src/tools/search/types.ts +42 -1
- package/src/tools/subagent/SubagentExecutor.ts +11 -0
- package/src/types/graph.ts +22 -0
- package/src/types/run.ts +9 -0
- package/src/types/tools.ts +6 -0
- 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
|
|
@@ -8,16 +8,20 @@ export declare abstract class BaseReranker {
|
|
|
8
8
|
}
|
|
9
9
|
export declare class JinaReranker extends BaseReranker {
|
|
10
10
|
private apiUrl;
|
|
11
|
-
|
|
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
|
-
|
|
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
|
@@ -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({
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -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) +
|
|
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 ??
|
|
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);
|