@librechat/agents 3.2.57 → 3.2.59

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 (120) 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 +2 -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/main.cjs +7 -0
  10. package/dist/cjs/run.cjs +2 -2
  11. package/dist/cjs/run.cjs.map +1 -1
  12. package/dist/cjs/stream.cjs +2 -1
  13. package/dist/cjs/stream.cjs.map +1 -1
  14. package/dist/cjs/tools/BashExecutor.cjs +58 -9
  15. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  16. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +4 -2
  17. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  18. package/dist/cjs/tools/CodeExecutor.cjs +57 -7
  19. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  20. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +9 -3
  21. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  22. package/dist/cjs/tools/ToolNode.cjs +19 -1
  23. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  24. package/dist/cjs/tools/eagerEventExecution.cjs +18 -1
  25. package/dist/cjs/tools/eagerEventExecution.cjs.map +1 -1
  26. package/dist/cjs/tools/search/keenable-search.cjs +68 -0
  27. package/dist/cjs/tools/search/keenable-search.cjs.map +1 -0
  28. package/dist/cjs/tools/search/rerankers.cjs +28 -11
  29. package/dist/cjs/tools/search/rerankers.cjs.map +1 -1
  30. package/dist/cjs/tools/search/search.cjs +30 -4
  31. package/dist/cjs/tools/search/search.cjs.map +1 -1
  32. package/dist/cjs/tools/search/tool.cjs +14 -6
  33. package/dist/cjs/tools/search/tool.cjs.map +1 -1
  34. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +12 -1
  35. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  36. package/dist/cjs/utils/title.cjs +9 -9
  37. package/dist/cjs/utils/title.cjs.map +1 -1
  38. package/dist/esm/agents/AgentContext.mjs +7 -1
  39. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  40. package/dist/esm/graphs/Graph.mjs +2 -2
  41. package/dist/esm/graphs/Graph.mjs.map +1 -1
  42. package/dist/esm/langfuseToolOutputTracing.mjs +4 -0
  43. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  44. package/dist/esm/langfuseTraceShaping.mjs +171 -0
  45. package/dist/esm/langfuseTraceShaping.mjs.map +1 -0
  46. package/dist/esm/main.mjs +3 -3
  47. package/dist/esm/run.mjs +2 -2
  48. package/dist/esm/run.mjs.map +1 -1
  49. package/dist/esm/stream.mjs +2 -1
  50. package/dist/esm/stream.mjs.map +1 -1
  51. package/dist/esm/tools/BashExecutor.mjs +56 -10
  52. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  53. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +4 -2
  54. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  55. package/dist/esm/tools/CodeExecutor.mjs +54 -8
  56. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  57. package/dist/esm/tools/ProgrammaticToolCalling.mjs +9 -3
  58. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  59. package/dist/esm/tools/ToolNode.mjs +20 -2
  60. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  61. package/dist/esm/tools/eagerEventExecution.mjs +18 -2
  62. package/dist/esm/tools/eagerEventExecution.mjs.map +1 -1
  63. package/dist/esm/tools/search/keenable-search.mjs +66 -0
  64. package/dist/esm/tools/search/keenable-search.mjs.map +1 -0
  65. package/dist/esm/tools/search/rerankers.mjs +28 -11
  66. package/dist/esm/tools/search/rerankers.mjs.map +1 -1
  67. package/dist/esm/tools/search/search.mjs +30 -4
  68. package/dist/esm/tools/search/search.mjs.map +1 -1
  69. package/dist/esm/tools/search/tool.mjs +14 -6
  70. package/dist/esm/tools/search/tool.mjs.map +1 -1
  71. package/dist/esm/tools/subagent/SubagentExecutor.mjs +12 -1
  72. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  73. package/dist/esm/utils/title.mjs +9 -9
  74. package/dist/esm/utils/title.mjs.map +1 -1
  75. package/dist/types/langfuseTraceShaping.d.ts +20 -0
  76. package/dist/types/tools/BashExecutor.d.ts +13 -0
  77. package/dist/types/tools/CodeExecutor.d.ts +14 -0
  78. package/dist/types/tools/ToolNode.d.ts +3 -0
  79. package/dist/types/tools/eagerEventExecution.d.ts +8 -0
  80. package/dist/types/tools/search/keenable-search.d.ts +4 -0
  81. package/dist/types/tools/search/rerankers.d.ts +7 -2
  82. package/dist/types/tools/search/types.d.ts +38 -1
  83. package/dist/types/types/graph.d.ts +22 -0
  84. package/dist/types/types/tools.d.ts +58 -0
  85. package/package.json +1 -1
  86. package/src/agents/AgentContext.ts +9 -0
  87. package/src/agents/__tests__/AgentContext.test.ts +40 -0
  88. package/src/graphs/Graph.ts +25 -2
  89. package/src/graphs/__tests__/composition.smoke.test.ts +53 -0
  90. package/src/langfuseToolOutputTracing.ts +11 -0
  91. package/src/langfuseTraceShaping.ts +280 -0
  92. package/src/llm/anthropic/inherited-stream-events.spec.ts +6 -3
  93. package/src/run.ts +3 -3
  94. package/src/specs/langfuse-routing.integration.test.ts +1 -1
  95. package/src/specs/langfuse-trace-shaping.test.ts +194 -0
  96. package/src/stream.ts +17 -1
  97. package/src/tools/BashExecutor.ts +107 -14
  98. package/src/tools/BashProgrammaticToolCalling.ts +20 -1
  99. package/src/tools/CodeExecutor.ts +113 -9
  100. package/src/tools/ProgrammaticToolCalling.ts +27 -1
  101. package/src/tools/ToolNode.ts +36 -4
  102. package/src/tools/__tests__/BashExecutor.test.ts +39 -0
  103. package/src/tools/__tests__/CodeExecutor.stateful.test.ts +113 -0
  104. package/src/tools/__tests__/SubagentExecutor.test.ts +44 -0
  105. package/src/tools/__tests__/ToolNode.session.test.ts +86 -0
  106. package/src/tools/__tests__/eagerEventExecution.session.test.ts +92 -0
  107. package/src/tools/__tests__/hitl.test.ts +85 -0
  108. package/src/tools/eagerEventExecution.ts +32 -5
  109. package/src/tools/search/jina-reranker.test.ts +70 -1
  110. package/src/tools/search/keenable-search.ts +98 -0
  111. package/src/tools/search/keenable.test.ts +183 -0
  112. package/src/tools/search/rerankers.ts +41 -4
  113. package/src/tools/search/search.ts +58 -2
  114. package/src/tools/search/source-processing.test.ts +86 -0
  115. package/src/tools/search/tool.ts +29 -4
  116. package/src/tools/search/types.ts +42 -1
  117. package/src/tools/subagent/SubagentExecutor.ts +11 -0
  118. package/src/types/graph.ts +22 -0
  119. package/src/types/tools.ts +65 -0
  120. package/src/utils/title.ts +9 -9
@@ -0,0 +1,20 @@
1
+ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
2
+ /**
3
+ * LangGraph plumbing observations that add noise without information:
4
+ * the duplicated `__start__` channel-seed nodes and anonymous
5
+ * `RunnableLambda` pass-throughs (Langfuse team feedback items 4 & 5).
6
+ */
7
+ export declare function shouldDropLangfuseSpan(spanName: string): boolean;
8
+ /**
9
+ * Reshapes spans per Langfuse-team feedback before export:
10
+ * - `agent=<id>` / `tools=<id>` node names carry the ephemeral agent id
11
+ * (`provider__model`) — strip it so switching models doesn't break
12
+ * name-based logic (item 1).
13
+ * - Tool node spans are renamed to the actual tool name(s) and their
14
+ * input scoped to the pending tool-call args instead of the whole
15
+ * chat history (items 3 & 4).
16
+ * - The root span (and trace) input/output become the user question and
17
+ * assistant response so the session view reads as a conversation
18
+ * (item 2).
19
+ */
20
+ export declare function shapeLangfuseSpan(span: ReadableSpan): void;
@@ -19,6 +19,15 @@ export declare const BashExecutionToolSchema: {
19
19
  readonly required: readonly ["command"];
20
20
  };
21
21
  export declare const BashExecutionToolDescription: string;
22
+ /**
23
+ * Bash statefulness is filesystem-tier: on a warm session the machine (files
24
+ * including /tmp, installed packages, background processes) persists between
25
+ * calls, but each call may start a fresh shell — so shell variables and cwd
26
+ * are NOT reliable, and the machine can be reset at any time. Only /mnt/data
27
+ * is durable.
28
+ */
29
+ export declare const STATEFUL_BASH_NOTE = "Session state (best-effort): commands in this conversation usually run on the same machine, so files (including /tmp), installed packages, and running background processes from earlier calls typically persist. Each call may still start a fresh shell \u2014 do not rely on shell variables or the working directory carrying over \u2014 and the machine may be reset at any time. Only /mnt/data is durable.";
30
+ export declare const StatefulBashExecutionToolDescription: string;
22
31
  /**
23
32
  * Supplemental prompt documenting the tool-output reference feature.
24
33
  *
@@ -39,7 +48,11 @@ export declare const BashToolOutputReferencesGuide: string;
39
48
  */
40
49
  export declare function buildBashExecutionToolDescription(options?: {
41
50
  enableToolOutputReferences?: boolean;
51
+ statefulSessions?: boolean;
42
52
  }): string;
53
+ export declare function buildBashExecutionToolSchema(opts?: {
54
+ statefulSessions?: boolean;
55
+ }): typeof BashExecutionToolSchema;
43
56
  export declare const BashExecutionToolName = Constants.BASH_TOOL;
44
57
  /**
45
58
  * Default bash tool definition using the base description.
@@ -38,6 +38,20 @@ export declare function buildCodeApiHttpErrorMessage(method: string, endpoint: s
38
38
  text: () => Promise<string>;
39
39
  }): Promise<string>;
40
40
  export declare const CodeExecutionToolDescription: string;
41
+ /**
42
+ * Best-effort statefulness note. Deliberately hedged: warm reuse is an
43
+ * optimization, not a guarantee (the runtime may be reset on idle timeout,
44
+ * eviction, or the 8h VM lifetime), so the model must never depend on carried
45
+ * state for correctness and must persist anything durable to /mnt/data.
46
+ */
47
+ export declare const STATEFUL_ENV_NOTE = "Session state (best-effort): consecutive executions in this conversation usually share one runtime, so variables, imports, and in-memory data from earlier successful calls are typically still available. The runtime may be reset at any time, so treat carried-over state as an optimization, never a guarantee. Anything that must survive MUST be written to /mnt/data. If a NameError/ImportError signals lost state, re-run the needed setup and continue.";
48
+ export declare const StatefulCodeExecutionToolDescription: string;
49
+ export declare function buildCodeExecutionToolDescription(opts?: {
50
+ statefulSessions?: boolean;
51
+ }): string;
52
+ export declare function buildCodeExecutionToolSchema(opts?: {
53
+ statefulSessions?: boolean;
54
+ }): typeof CodeExecutionToolSchema;
41
55
  export declare const CodeExecutionToolName = Constants.EXECUTE_CODE;
42
56
  export declare const CodeExecutionToolDefinition: {
43
57
  readonly name: Constants.EXECUTE_CODE;
@@ -314,6 +314,9 @@ export declare class ToolNode<T = any> extends RunnableCallable<T, T> {
314
314
  * opt-in so only host-declared tools can influence the shared session.
315
315
  */
316
316
  private participatesInCodeSession;
317
+ /** Delegates to the shared resolver so the direct and event-driven planning
318
+ * paths derive the runtime session hint identically. */
319
+ private resolveRuntimeSessionHint;
317
320
  private storeCodeSessionFromResults;
318
321
  /**
319
322
  * Post-processes standard runTool outputs: dispatches ON_RUN_STEP_COMPLETED
@@ -9,7 +9,15 @@ export type ToolExecutionPlanCall = {
9
9
  args: unknown;
10
10
  stepId?: string;
11
11
  codeSessionContext?: t.ToolCallRequest['codeSessionContext'];
12
+ runtimeSessionHint?: string;
12
13
  };
14
+ /**
15
+ * Stateful runtime session hint for the remote sandbox: only when
16
+ * `toolExecution.sandbox.statefulSessions` is on; explicit host hint else the
17
+ * conversation `thread_id`. Undefined disables the wire field. Shared by the
18
+ * direct ToolNode path and both event-driven planners so they stay in lockstep.
19
+ */
20
+ export declare function resolveRuntimeSessionHint(toolExecution: t.ToolExecutionConfig | undefined, threadId: string | undefined): string | undefined;
13
21
  export type ToolExecutionRequestPlan = {
14
22
  allRequests: t.ToolCallRequest[];
15
23
  requests: t.ToolCallRequest[];
@@ -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;
@@ -278,6 +278,15 @@ export type CodeExecutionToolParams = undefined | {
278
278
  files?: CodeEnvFile[];
279
279
  /** Optional host-supplied Code API auth headers. */
280
280
  authHeaders?: CodeApiAuthHeaders;
281
+ /**
282
+ * Advertise best-effort stateful sessions in the tool description
283
+ * (variables/files may persist between calls, may reset). Prompt text
284
+ * only, and it must be set here because the description is bound to the
285
+ * LLM at construction time. Pair it with the run-scoped
286
+ * `toolExecution.sandbox.statefulSessions` gate, which drives the wire
287
+ * hint — set both from one flag so the prompt and the backend agree.
288
+ */
289
+ statefulSessions?: boolean;
281
290
  };
282
291
  export type CodeApiAuthHeaderMap = Record<string, string>;
283
292
  export type CodeApiAuthHeaders = CodeApiAuthHeaderMap | (() => CodeApiAuthHeaderMap | Promise<CodeApiAuthHeaderMap>);
@@ -326,6 +335,13 @@ export type ExecuteResult = {
326
335
  stdout: string;
327
336
  stderr: string;
328
337
  files?: FileRefs;
338
+ /**
339
+ * Durable runtime session id echoed by a stateful Code API backend
340
+ * (hash of tenant+user+hint). Additive; absent on stateless servers.
341
+ */
342
+ runtime_session_id?: string;
343
+ /** Whether this execution reused a warm runtime session or started fresh. */
344
+ runtime_status?: 'new' | 'reused';
329
345
  };
330
346
  /** JSON Schema type definition for tool parameters */
331
347
  export type JsonSchemaType = {
@@ -381,6 +397,12 @@ export type ToolCallRequest = {
381
397
  session_id: string;
382
398
  files?: CodeEnvFile[];
383
399
  };
400
+ /**
401
+ * Stable runtime session hint for stateful sandbox sessions. Orthogonal to
402
+ * `codeSessionContext` (which threads the transient exec-session for file
403
+ * continuity): the hint identifies the durable server-side runtime session.
404
+ */
405
+ runtimeSessionHint?: string;
384
406
  };
385
407
  /** Batch request containing ALL tool calls for a graph step */
386
408
  export type ToolExecuteBatchRequest = {
@@ -896,6 +918,31 @@ export type CloudflareSandboxExecutionConfig = {
896
918
  /** Run a fast per-file syntax check after successful edits/writes. */
897
919
  postEditSyntaxCheck?: LocalExecutionConfig['postEditSyntaxCheck'];
898
920
  };
921
+ export type SandboxExecutionConfig = {
922
+ /**
923
+ * Opt into best-effort stateful runtime sessions on the remote Code API
924
+ * (its warm per-session MicroVM backend). This gate is run-scoped: it only
925
+ * controls the wire behavior (ToolNode injecting the session hint on
926
+ * execute_code/bash calls). The transport is otherwise unchanged.
927
+ *
928
+ * It does NOT change the model-facing tool description. Tool descriptions are
929
+ * bound to the LLM at construction time (`createCodeExecutionTool` /
930
+ * `createBashExecutionTool`), before this run config is applied inside the
931
+ * graph, so they can only be adjusted via the tools' own `statefulSessions`
932
+ * factory param. Set BOTH from one flag (as LibreChat does): with this on but
933
+ * the factory param off, the backend runs statefully while the model is still
934
+ * told the environment is stateless (non-corrupting — the model just won't
935
+ * exploit persistence).
936
+ */
937
+ statefulSessions?: boolean;
938
+ /**
939
+ * Stable identity for the runtime session (e.g. the conversation id). The
940
+ * server derives the real session id as hash(tenant, user, hint), so this
941
+ * is never a security boundary. Falls back to `configurable.thread_id` when
942
+ * omitted.
943
+ */
944
+ runtimeSessionHint?: string;
945
+ };
899
946
  export type ToolExecutionConfig = {
900
947
  /** `sandbox` preserves the remote Code API behavior and is the default. */
901
948
  engine?: ToolExecutionEngine;
@@ -903,6 +950,8 @@ export type ToolExecutionConfig = {
903
950
  local?: LocalExecutionConfig;
904
951
  /** Cloudflare Sandbox execution settings used when `engine` is `cloudflare-sandbox`. */
905
952
  cloudflare?: CloudflareSandboxExecutionConfig;
953
+ /** Remote sandbox settings; applies when `engine` is `sandbox` or omitted. */
954
+ sandbox?: SandboxExecutionConfig;
906
955
  };
907
956
  export type ProgrammaticCache = {
908
957
  toolMap: ToolMap;
@@ -1005,6 +1054,9 @@ export type ProgrammaticExecutionResponse = {
1005
1054
  stdout?: string;
1006
1055
  stderr?: string;
1007
1056
  files?: FileRefs;
1057
+ /** Durable runtime session echo from a stateful backend (additive). */
1058
+ runtime_session_id?: string;
1059
+ runtime_status?: 'new' | 'reused';
1008
1060
  /** Present when status='error' */
1009
1061
  error?: string;
1010
1062
  };
@@ -1015,6 +1067,9 @@ export type ProgrammaticExecutionArtifact = {
1015
1067
  /** Execution session — see `CodeSessionContext.session_id`. */
1016
1068
  session_id?: string;
1017
1069
  files?: FileRefs;
1070
+ /** Durable runtime session echo from a stateful backend (additive). */
1071
+ runtime_session_id?: string;
1072
+ runtime_status?: 'new' | 'reused';
1018
1073
  };
1019
1074
  /** Parameters for creating a bash execution tool (same API as CodeExecutor, bash-only) */
1020
1075
  export type BashExecutionToolParams = CodeExecutionToolParams;
@@ -1062,6 +1117,9 @@ export type CodeExecutionArtifact = {
1062
1117
  /** Execution session — see `CodeSessionContext.session_id`. */
1063
1118
  session_id?: string;
1064
1119
  files?: FileRefs;
1120
+ /** Durable runtime session echo from a stateful backend (additive). */
1121
+ runtime_session_id?: string;
1122
+ runtime_status?: 'new' | 'reused';
1065
1123
  };
1066
1124
  /**
1067
1125
  * Generic session context union type for different tool types.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.2.57",
3
+ "version": "3.2.59",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -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({
@@ -1140,7 +1140,15 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1140
1140
  getToolCount(): number {
1141
1141
  const context = this.agentContexts.get(this.defaultAgentId);
1142
1142
  return (
1143
- (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)
1144
1152
  );
1145
1153
  }
1146
1154
 
@@ -1286,10 +1294,25 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1286
1294
  graphTools && graphTools.length > 0
1287
1295
  ? [...baseTools, ...graphTools]
1288
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
+ */
1289
1305
  const traditionalToolMap =
1290
1306
  graphTools && graphTools.length > 0
1291
1307
  ? new Map([
1292
- ...(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
+ )),
1293
1316
  ...graphTools
1294
1317
  .filter((t): t is t.GenericTool & { name: string } => 'name' in t)
1295
1318
  .map((t) => [t.name, t] as [string, t.GenericTool]),
@@ -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);