@librechat/agents 3.1.89 → 3.1.91

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 (145) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +9 -5
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +53 -14
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/hooks/executeHooks.cjs +14 -7
  6. package/dist/cjs/hooks/executeHooks.cjs.map +1 -1
  7. package/dist/cjs/langfuse.cjs +234 -0
  8. package/dist/cjs/langfuse.cjs.map +1 -0
  9. package/dist/cjs/llm/anthropic/index.cjs +8 -2
  10. package/dist/cjs/llm/anthropic/index.cjs.map +1 -1
  11. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +34 -0
  12. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/main.cjs +34 -0
  14. package/dist/cjs/main.cjs.map +1 -1
  15. package/dist/cjs/run.cjs +44 -27
  16. package/dist/cjs/run.cjs.map +1 -1
  17. package/dist/cjs/stream.cjs +10 -3
  18. package/dist/cjs/stream.cjs.map +1 -1
  19. package/dist/cjs/tools/BashExecutor.cjs +10 -9
  20. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  21. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs +12 -8
  22. package/dist/cjs/tools/BashProgrammaticToolCalling.cjs.map +1 -1
  23. package/dist/cjs/tools/CodeExecutor.cjs +35 -11
  24. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  25. package/dist/cjs/tools/CodeSessionFileSummary.cjs +63 -0
  26. package/dist/cjs/tools/CodeSessionFileSummary.cjs.map +1 -0
  27. package/dist/cjs/tools/ProgrammaticToolCalling.cjs +16 -12
  28. package/dist/cjs/tools/ProgrammaticToolCalling.cjs.map +1 -1
  29. package/dist/cjs/tools/ToolNode.cjs +8 -5
  30. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  31. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs +380 -0
  32. package/dist/cjs/tools/cloudflare/CloudflareBridgeRuntime.cjs.map +1 -0
  33. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs +997 -0
  34. package/dist/cjs/tools/cloudflare/CloudflareProgrammaticToolCalling.cjs.map +1 -0
  35. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs +575 -0
  36. package/dist/cjs/tools/cloudflare/CloudflareSandboxExecutionEngine.cjs.map +1 -0
  37. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs +165 -0
  38. package/dist/cjs/tools/cloudflare/CloudflareSandboxTools.cjs.map +1 -0
  39. package/dist/cjs/tools/local/LocalExecutionEngine.cjs +17 -5
  40. package/dist/cjs/tools/local/LocalExecutionEngine.cjs.map +1 -1
  41. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs +110 -6
  42. package/dist/cjs/tools/local/resolveLocalExecutionTools.cjs.map +1 -1
  43. package/dist/cjs/tools/subagent/SubagentExecutor.cjs +319 -29
  44. package/dist/cjs/tools/subagent/SubagentExecutor.cjs.map +1 -1
  45. package/dist/esm/agents/AgentContext.mjs +9 -5
  46. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  47. package/dist/esm/graphs/Graph.mjs +53 -14
  48. package/dist/esm/graphs/Graph.mjs.map +1 -1
  49. package/dist/esm/hooks/executeHooks.mjs +14 -7
  50. package/dist/esm/hooks/executeHooks.mjs.map +1 -1
  51. package/dist/esm/langfuse.mjs +226 -0
  52. package/dist/esm/langfuse.mjs.map +1 -0
  53. package/dist/esm/llm/anthropic/index.mjs +9 -3
  54. package/dist/esm/llm/anthropic/index.mjs.map +1 -1
  55. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +33 -1
  56. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  57. package/dist/esm/main.mjs +7 -2
  58. package/dist/esm/main.mjs.map +1 -1
  59. package/dist/esm/run.mjs +44 -27
  60. package/dist/esm/run.mjs.map +1 -1
  61. package/dist/esm/stream.mjs +10 -3
  62. package/dist/esm/stream.mjs.map +1 -1
  63. package/dist/esm/tools/BashExecutor.mjs +11 -10
  64. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  65. package/dist/esm/tools/BashProgrammaticToolCalling.mjs +13 -9
  66. package/dist/esm/tools/BashProgrammaticToolCalling.mjs.map +1 -1
  67. package/dist/esm/tools/CodeExecutor.mjs +29 -12
  68. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  69. package/dist/esm/tools/CodeSessionFileSummary.mjs +60 -0
  70. package/dist/esm/tools/CodeSessionFileSummary.mjs.map +1 -0
  71. package/dist/esm/tools/ProgrammaticToolCalling.mjs +17 -13
  72. package/dist/esm/tools/ProgrammaticToolCalling.mjs.map +1 -1
  73. package/dist/esm/tools/ToolNode.mjs +8 -5
  74. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  75. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs +378 -0
  76. package/dist/esm/tools/cloudflare/CloudflareBridgeRuntime.mjs.map +1 -0
  77. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs +994 -0
  78. package/dist/esm/tools/cloudflare/CloudflareProgrammaticToolCalling.mjs.map +1 -0
  79. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs +566 -0
  80. package/dist/esm/tools/cloudflare/CloudflareSandboxExecutionEngine.mjs.map +1 -0
  81. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs +155 -0
  82. package/dist/esm/tools/cloudflare/CloudflareSandboxTools.mjs.map +1 -0
  83. package/dist/esm/tools/local/LocalExecutionEngine.mjs +17 -6
  84. package/dist/esm/tools/local/LocalExecutionEngine.mjs.map +1 -1
  85. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs +111 -7
  86. package/dist/esm/tools/local/resolveLocalExecutionTools.mjs.map +1 -1
  87. package/dist/esm/tools/subagent/SubagentExecutor.mjs +320 -31
  88. package/dist/esm/tools/subagent/SubagentExecutor.mjs.map +1 -1
  89. package/dist/types/agents/AgentContext.d.ts +4 -1
  90. package/dist/types/graphs/Graph.d.ts +6 -5
  91. package/dist/types/index.d.ts +1 -0
  92. package/dist/types/langfuse.d.ts +48 -0
  93. package/dist/types/llm/anthropic/index.d.ts +3 -1
  94. package/dist/types/llm/anthropic/utils/message_inputs.d.ts +4 -0
  95. package/dist/types/tools/BashExecutor.d.ts +3 -3
  96. package/dist/types/tools/CodeExecutor.d.ts +10 -3
  97. package/dist/types/tools/CodeSessionFileSummary.d.ts +3 -0
  98. package/dist/types/tools/ProgrammaticToolCalling.d.ts +4 -4
  99. package/dist/types/tools/cloudflare/CloudflareBridgeRuntime.d.ts +23 -0
  100. package/dist/types/tools/cloudflare/CloudflareProgrammaticToolCalling.d.ts +4 -0
  101. package/dist/types/tools/cloudflare/CloudflareSandboxExecutionEngine.d.ts +21 -0
  102. package/dist/types/tools/cloudflare/CloudflareSandboxTools.d.ts +22 -0
  103. package/dist/types/tools/cloudflare/index.d.ts +4 -0
  104. package/dist/types/tools/local/LocalExecutionEngine.d.ts +1 -0
  105. package/dist/types/tools/subagent/SubagentExecutor.d.ts +8 -5
  106. package/dist/types/types/graph.d.ts +8 -0
  107. package/dist/types/types/tools.d.ts +120 -5
  108. package/package.json +4 -4
  109. package/src/__tests__/stream.eagerEventExecution.test.ts +66 -0
  110. package/src/agents/AgentContext.ts +13 -3
  111. package/src/graphs/Graph.ts +60 -16
  112. package/src/hooks/__tests__/executeHooks.test.ts +38 -0
  113. package/src/hooks/executeHooks.ts +27 -7
  114. package/src/index.ts +1 -0
  115. package/src/langfuse.ts +358 -0
  116. package/src/llm/anthropic/index.ts +27 -3
  117. package/src/llm/anthropic/llm.spec.ts +60 -1
  118. package/src/llm/anthropic/utils/message_inputs.ts +46 -0
  119. package/src/run.ts +60 -38
  120. package/src/specs/langfuse-config.test.ts +57 -0
  121. package/src/specs/langfuse-metadata.test.ts +19 -1
  122. package/src/stream.ts +13 -3
  123. package/src/tools/BashExecutor.ts +21 -10
  124. package/src/tools/BashProgrammaticToolCalling.ts +21 -9
  125. package/src/tools/CodeExecutor.ts +55 -12
  126. package/src/tools/CodeSessionFileSummary.ts +80 -0
  127. package/src/tools/ProgrammaticToolCalling.ts +25 -12
  128. package/src/tools/ToolNode.ts +8 -5
  129. package/src/tools/__tests__/BashExecutor.test.ts +9 -0
  130. package/src/tools/__tests__/CloudflareSandboxExecution.test.ts +537 -0
  131. package/src/tools/__tests__/CodeApiAuthHeaders.test.ts +43 -0
  132. package/src/tools/__tests__/ProgrammaticToolCalling.test.ts +100 -16
  133. package/src/tools/__tests__/SubagentExecutor.test.ts +540 -6
  134. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +52 -0
  135. package/src/tools/__tests__/subagentHooks.test.ts +237 -0
  136. package/src/tools/cloudflare/CloudflareBridgeRuntime.ts +480 -0
  137. package/src/tools/cloudflare/CloudflareProgrammaticToolCalling.ts +1162 -0
  138. package/src/tools/cloudflare/CloudflareSandboxExecutionEngine.ts +744 -0
  139. package/src/tools/cloudflare/CloudflareSandboxTools.ts +225 -0
  140. package/src/tools/cloudflare/index.ts +4 -0
  141. package/src/tools/local/LocalExecutionEngine.ts +20 -4
  142. package/src/tools/local/resolveLocalExecutionTools.ts +169 -7
  143. package/src/tools/subagent/SubagentExecutor.ts +514 -36
  144. package/src/types/graph.ts +9 -0
  145. package/src/types/tools.ts +143 -5
@@ -288,6 +288,12 @@ export interface SubagentUpdateEvent {
288
288
  /** ISO timestamp for ordering / display. */
289
289
  timestamp: string;
290
290
  }
291
+ export interface LangfuseConfig {
292
+ enabled?: boolean;
293
+ publicKey?: string;
294
+ secretKey?: string;
295
+ baseUrl?: string;
296
+ }
291
297
  export interface AgentInputs {
292
298
  agentId: string;
293
299
  /** Human-readable name for the agent (used in handoff context). Defaults to agentId if not provided. */
@@ -301,6 +307,8 @@ export interface AgentInputs {
301
307
  streamBuffer?: number;
302
308
  maxContextTokens?: number;
303
309
  clientOptions?: ClientOptions;
310
+ /** Per-agent Langfuse tracing configuration. */
311
+ langfuse?: LangfuseConfig;
304
312
  /** Dynamic system tail appended after stable instructions without provider cache markers. */
305
313
  additional_instructions?: string;
306
314
  reasoningKey?: 'reasoning_content' | 'reasoning';
@@ -284,9 +284,8 @@ export type FileRef = {
284
284
  * `true` when the codeapi sandbox echoed this entry as an unchanged
285
285
  * passthrough of an input the caller already owns (skill files,
286
286
  * downloaded inputs whose hash matched the baseline, inherited
287
- * `.dirkeep` markers). The tool-result formatter renders these as
288
- * "Available files" rather than "Generated files" so the LLM doesn't
289
- * conflate infrastructure inputs with newly-produced outputs.
287
+ * `.dirkeep` markers). The host can use this flag to skip
288
+ * post-processing work for files the caller already owns.
290
289
  */
291
290
  inherited?: true;
292
291
  };
@@ -468,10 +467,11 @@ export type ToolOutputReferencesConfig = {
468
467
  */
469
468
  maxTotalSize?: number;
470
469
  };
471
- export type ToolExecutionEngine = 'sandbox' | 'local';
470
+ export type ToolExecutionEngine = 'sandbox' | 'local' | 'cloudflare-sandbox';
472
471
  /**
473
472
  * Records pre-write file contents so callers can rewind edits/writes
474
- * made by the local engine. Implementations live in `src/tools/local`.
473
+ * made by local-compatible coding-tool engines. Implementations live
474
+ * in `src/tools/local`.
475
475
  */
476
476
  export interface LocalFileCheckpointer {
477
477
  /**
@@ -587,6 +587,12 @@ export type LocalExecConfig = {
587
587
  * overridden together for non-host engines.
588
588
  */
589
589
  fs?: import('@/tools/local/workspaceFS').WorkspaceFS;
590
+ /**
591
+ * Set by custom execution backends that already provide their own
592
+ * sandbox boundary. Suppresses the local host-sandbox warning while
593
+ * preserving the warning for plain host `child_process` execution.
594
+ */
595
+ sandboxed?: boolean;
590
596
  };
591
597
  export type LocalExecutionConfig = {
592
598
  /**
@@ -741,11 +747,120 @@ export type LocalExecutionConfig = {
741
747
  timeoutMs?: number;
742
748
  };
743
749
  };
750
+ export type CloudflareSandboxExecOptions = {
751
+ cwd?: string;
752
+ env?: Record<string, string | undefined>;
753
+ timeout?: number;
754
+ stream?: boolean;
755
+ onOutput?: (stream: 'stdout' | 'stderr', data: string) => void;
756
+ signal?: AbortSignal;
757
+ };
758
+ export type CloudflareSandboxExecResult = {
759
+ success?: boolean;
760
+ exitCode: number;
761
+ stdout: string;
762
+ stderr: string;
763
+ command?: string;
764
+ duration?: number;
765
+ timestamp?: string;
766
+ };
767
+ export type CloudflareSandboxReadFileResult = string | Buffer | Uint8Array | {
768
+ content: string | Buffer | Uint8Array | ReadableStream<Uint8Array>;
769
+ size?: number;
770
+ mimeType?: string;
771
+ encoding?: 'utf-8' | 'utf8' | 'base64';
772
+ isBinary?: boolean;
773
+ };
774
+ export type CloudflareSandboxListFilesOptions = {
775
+ recursive?: boolean;
776
+ includeHidden?: boolean;
777
+ };
778
+ export type CloudflareSandboxFileInfo = {
779
+ name: string;
780
+ absolutePath?: string;
781
+ relativePath?: string;
782
+ type?: 'file' | 'directory' | 'symlink' | 'other';
783
+ size?: number;
784
+ modifiedAt?: string;
785
+ mode?: string;
786
+ };
787
+ export type CloudflareSandboxListFilesResult = CloudflareSandboxFileInfo[] | {
788
+ files: CloudflareSandboxFileInfo[];
789
+ count?: number;
790
+ path?: string;
791
+ success?: boolean;
792
+ };
793
+ export interface CloudflareSandboxRuntime {
794
+ exec(command: string, options?: CloudflareSandboxExecOptions): Promise<CloudflareSandboxExecResult>;
795
+ readFile(path: string, options?: {
796
+ encoding?: string;
797
+ }): Promise<CloudflareSandboxReadFileResult>;
798
+ writeFile(path: string, content: string | ReadableStream<Uint8Array>, options?: {
799
+ encoding?: string;
800
+ }): Promise<unknown>;
801
+ mkdir(path: string, options?: {
802
+ recursive?: boolean;
803
+ }): Promise<unknown>;
804
+ listFiles(path: string, options?: CloudflareSandboxListFilesOptions): Promise<CloudflareSandboxListFilesResult>;
805
+ deleteFile(path: string): Promise<unknown>;
806
+ }
807
+ export type CloudflareSandboxExecutionConfig = {
808
+ sandbox: CloudflareSandboxRuntime | (() => CloudflareSandboxRuntime | Promise<CloudflareSandboxRuntime>);
809
+ /** Working directory inside the sandbox. Defaults to `/workspace`. */
810
+ workspaceRoot?: string;
811
+ /** Extra environment variables merged into sandbox command executions. */
812
+ env?: Record<string, string | undefined>;
813
+ /** Default timeout for sandbox commands, in milliseconds. */
814
+ timeoutMs?: number;
815
+ /** Maximum stdout/stderr characters surfaced to the model. */
816
+ maxOutputChars?: number;
817
+ /**
818
+ * Add the built-in coding suite when `engine` is `cloudflare-sandbox`.
819
+ * Defaults to true, matching the local execution backend.
820
+ */
821
+ includeCodingTools?: boolean;
822
+ /**
823
+ * Optional exact coding-tool names to expose when `includeCodingTools`
824
+ * is on. Defaults to the full local-parity Cloudflare bundle. Use this
825
+ * to publish a bash-only sandbox surface, for example file/search tools
826
+ * plus `bash_tool` and `run_tools_with_bash`, without exposing
827
+ * `execute_code`.
828
+ */
829
+ codingToolNames?: readonly string[];
830
+ /** Optional shell executable for bash-style tools. Defaults to `bash`. */
831
+ shell?: string;
832
+ /**
833
+ * Optional project-level compile check configuration. Mirrors the local
834
+ * execution backend.
835
+ */
836
+ compileCheck?: LocalExecutionConfig['compileCheck'];
837
+ /** Optional read-only guard for mutating coding tools. */
838
+ readOnly?: boolean;
839
+ /** Permit dangerous commands that the validator otherwise blocks. */
840
+ allowDangerousCommands?: LocalExecutionConfig['allowDangerousCommands'];
841
+ /** Tree-sitter-bash AST validation pass for bash commands. */
842
+ bashAst?: LocalExecutionConfig['bashAst'];
843
+ /**
844
+ * Enable per-Run file checkpointing for `edit_file` / `write_file`
845
+ * against the Cloudflare Sandbox workspace.
846
+ */
847
+ fileCheckpointing?: LocalExecutionConfig['fileCheckpointing'];
848
+ /** Maximum bytes to read in `read_file` before returning a stub. */
849
+ maxReadBytes?: LocalExecutionConfig['maxReadBytes'];
850
+ /** Controls whether `read_file` returns binary files as attachments. */
851
+ attachReadAttachments?: LocalExecutionConfig['attachReadAttachments'];
852
+ /** Maximum pre-encoding byte size to embed inline. */
853
+ maxAttachmentBytes?: LocalExecutionConfig['maxAttachmentBytes'];
854
+ /** Run a fast per-file syntax check after successful edits/writes. */
855
+ postEditSyntaxCheck?: LocalExecutionConfig['postEditSyntaxCheck'];
856
+ };
744
857
  export type ToolExecutionConfig = {
745
858
  /** `sandbox` preserves the remote Code API behavior and is the default. */
746
859
  engine?: ToolExecutionEngine;
747
860
  /** Local process execution settings used when `engine` is `local`. */
748
861
  local?: LocalExecutionConfig;
862
+ /** Cloudflare Sandbox execution settings used when `engine` is `cloudflare-sandbox`. */
863
+ cloudflare?: CloudflareSandboxExecutionConfig;
749
864
  };
750
865
  export type ProgrammaticCache = {
751
866
  toolMap: ToolMap;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.1.89",
3
+ "version": "3.1.91",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -227,9 +227,9 @@
227
227
  "@langchain/openai": "1.4.5",
228
228
  "@langchain/textsplitters": "^1.0.1",
229
229
  "@langchain/xai": "^1.3.17",
230
- "@langfuse/langchain": "^4.3.0",
231
- "@langfuse/otel": "^4.3.0",
232
- "@langfuse/tracing": "^4.3.0",
230
+ "@langfuse/langchain": "^4.6.1",
231
+ "@langfuse/otel": "^4.6.1",
232
+ "@langfuse/tracing": "^4.6.1",
233
233
  "@opentelemetry/sdk-node": "^0.218.0",
234
234
  "@scarf/scarf": "^1.4.0",
235
235
  "@types/diff": "^7.0.2",
@@ -2847,6 +2847,72 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
2847
2847
  expect(graph.eagerEventToolCallChunks.size).toBe(0);
2848
2848
  });
2849
2849
 
2850
+ it('does not prestart streamed Cloudflare sandbox direct coding tools', async () => {
2851
+ const graph = createGraph({
2852
+ toolExecution: {
2853
+ engine: 'cloudflare-sandbox',
2854
+ cloudflare: { sandbox: {} },
2855
+ } as StandardGraph['toolExecution'],
2856
+ getAgentContext: jest.fn(
2857
+ (): Partial<AgentContext> => ({
2858
+ provider: Providers.OPENAI,
2859
+ reasoningKey: 'reasoning_content',
2860
+ toolDefinitions: [{ name: Constants.BASH_TOOL }, { name: 'weather' }],
2861
+ graphTools: [],
2862
+ agentId: 'agent_1',
2863
+ })
2864
+ ) as unknown as StandardGraph['getAgentContext'],
2865
+ });
2866
+ const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
2867
+ const handler = new ChatModelStreamHandler();
2868
+ const metadata = { langgraph_node: 'agent' };
2869
+
2870
+ await handler.handle(
2871
+ GraphEvents.CHAT_MODEL_STREAM,
2872
+ {
2873
+ chunk: {
2874
+ content: '',
2875
+ tool_call_chunks: [
2876
+ {
2877
+ id: 'call_weather',
2878
+ name: 'weather',
2879
+ args: '{"city":"NYC"}',
2880
+ index: 0,
2881
+ },
2882
+ ],
2883
+ } as unknown as t.StreamChunk,
2884
+ },
2885
+ metadata,
2886
+ graph
2887
+ );
2888
+ await handler.handle(
2889
+ GraphEvents.CHAT_MODEL_STREAM,
2890
+ {
2891
+ chunk: {
2892
+ content: '',
2893
+ tool_call_chunks: [
2894
+ {
2895
+ id: 'call_bash',
2896
+ name: Constants.BASH_TOOL,
2897
+ args: '{"command":"echo ok"}',
2898
+ index: 1,
2899
+ },
2900
+ ],
2901
+ } as unknown as t.StreamChunk,
2902
+ },
2903
+ metadata,
2904
+ graph
2905
+ );
2906
+
2907
+ expect(dispatchSpy).not.toHaveBeenCalledWith(
2908
+ GraphEvents.ON_TOOL_EXECUTE,
2909
+ expect.anything(),
2910
+ expect.anything()
2911
+ );
2912
+ expect(graph.eagerEventToolExecutions.size).toBe(0);
2913
+ expect(graph.eagerEventToolCallChunks.size).toBe(0);
2914
+ });
2915
+
2850
2916
  it('prestarts streamed remote bash tools when the next Anthropic tool call begins', async () => {
2851
2917
  const graph = createGraph({
2852
2918
  getAgentContext: jest.fn(
@@ -53,6 +53,7 @@ export class AgentContext {
53
53
  name,
54
54
  provider,
55
55
  clientOptions,
56
+ langfuse,
56
57
  tools,
57
58
  toolMap,
58
59
  toolEnd,
@@ -80,6 +81,7 @@ export class AgentContext {
80
81
  name: name ?? agentId,
81
82
  provider,
82
83
  clientOptions,
84
+ langfuse,
83
85
  maxContextTokens,
84
86
  streamBuffer,
85
87
  tools,
@@ -149,6 +151,8 @@ export class AgentContext {
149
151
  provider: Providers;
150
152
  /** Client options for this agent */
151
153
  clientOptions?: t.ClientOptions;
154
+ /** Per-agent Langfuse tracing configuration. */
155
+ langfuse?: t.LangfuseConfig;
152
156
  /** Token count map indexed by message position */
153
157
  indexTokenCountMap: Record<string, number | undefined> = {};
154
158
  /** Canonical pre-run token map used to restore token accounting on reset */
@@ -309,6 +313,7 @@ export class AgentContext {
309
313
  name,
310
314
  provider,
311
315
  clientOptions,
316
+ langfuse,
312
317
  maxContextTokens,
313
318
  streamBuffer,
314
319
  tokenCounter,
@@ -332,6 +337,7 @@ export class AgentContext {
332
337
  name?: string;
333
338
  provider: Providers;
334
339
  clientOptions?: t.ClientOptions;
340
+ langfuse?: t.LangfuseConfig;
335
341
  maxContextTokens?: number;
336
342
  streamBuffer?: number;
337
343
  tokenCounter?: t.TokenCounter;
@@ -355,6 +361,7 @@ export class AgentContext {
355
361
  this.name = name;
356
362
  this.provider = provider;
357
363
  this.clientOptions = clientOptions;
364
+ this.langfuse = langfuse;
358
365
  this.maxContextTokens = maxContextTokens;
359
366
  this.streamBuffer = streamBuffer;
360
367
  this.tokenCounter = tokenCounter;
@@ -458,11 +465,14 @@ export class AgentContext {
458
465
  }
459
466
 
460
467
  private hasAvailableTool(name: string): boolean {
461
- if (this.toolDefinitions?.some((tool) => tool.name === name)) return true;
462
- if (this.tools?.some((tool) => 'name' in tool && tool.name === name)) {
468
+ if (this.toolDefinitions?.some((tool) => tool.name === name) === true)
469
+ return true;
470
+ if (
471
+ this.tools?.some((tool) => 'name' in tool && tool.name === name) === true
472
+ ) {
463
473
  return true;
464
474
  }
465
- if (this.toolMap?.has(name)) return true;
475
+ if (this.toolMap?.has(name) === true) return true;
466
476
  return this.toolRegistry?.has(name) === true;
467
477
  }
468
478
 
@@ -58,8 +58,10 @@ import { createFakeStreamingLLM } from '@/llm/fake';
58
58
  import { handleToolCalls } from '@/tools/handlers';
59
59
  import { resolveLocalToolsForBinding } from '@/tools/local';
60
60
  import { createLocalCodingToolBundle } from '@/tools/local/LocalCodingTools';
61
+ import { createCloudflareCodingToolBundle } from '@/tools/cloudflare';
61
62
  import { isThinkingEnabled } from '@/llm/request';
62
63
  import { initializeModel } from '@/llm/init';
64
+ import { createLangfuseHandler, disposeLangfuseHandler } from '@/langfuse';
63
65
  import { HandlerRegistry } from '@/events';
64
66
  import { ChatOpenAI } from '@/llm/openai';
65
67
  import { partitionAndMarkOpenRouterToolCache } from '@/llm/openrouter/toolCache';
@@ -337,11 +339,12 @@ export abstract class Graph<
337
339
  /**
338
340
  * Single per-Run file checkpointer shared across every ToolNode the
339
341
  * graph compiles. Lazily constructed when
340
- * `toolExecution.local.fileCheckpointing === true` so multi-agent
341
- * graphs see ONE snapshot store, not one-per-agent. Returns
342
- * undefined when checkpointing is disabled or the local engine
343
- * isn't selected. Exposed via `Run.getFileCheckpointer()` /
344
- * `Run.rewindFiles()`.
342
+ * `toolExecution.local.fileCheckpointing === true` or
343
+ * `toolExecution.cloudflare.fileCheckpointing === true` so
344
+ * multi-agent graphs see ONE snapshot store, not one-per-agent.
345
+ * Returns undefined when checkpointing is disabled or a supported
346
+ * coding-tool engine isn't selected. Exposed via
347
+ * `Run.getFileCheckpointer()` / `Run.rewindFiles()`.
345
348
  */
346
349
  private _fileCheckpointer?: t.LocalFileCheckpointer;
347
350
  /**
@@ -364,20 +367,32 @@ export abstract class Graph<
364
367
  if (this._fileCheckpointer != null) {
365
368
  return this._fileCheckpointer;
366
369
  }
367
- if (
368
- this.toolExecution?.engine !== 'local' ||
369
- this.toolExecution.local?.fileCheckpointing !== true
370
- ) {
371
- return undefined;
372
- }
373
370
  // Eagerly create via the bundle factory so the construction path
374
371
  // matches the bundle-only callers (and future bundle-internal
375
372
  // cleanup hooks fire). The bundle factory itself accepts a pre-
376
373
  // supplied checkpointer when present, so re-injecting this one
377
374
  // into every ToolNode is idempotent.
378
- const bundle = createLocalCodingToolBundle(this.toolExecution.local ?? {});
379
- this._fileCheckpointer = bundle.checkpointer;
380
- return this._fileCheckpointer;
375
+ if (
376
+ this.toolExecution?.engine === 'local' &&
377
+ this.toolExecution.local?.fileCheckpointing === true
378
+ ) {
379
+ const bundle = createLocalCodingToolBundle(
380
+ this.toolExecution.local ?? {}
381
+ );
382
+ this._fileCheckpointer = bundle.checkpointer;
383
+ return this._fileCheckpointer;
384
+ }
385
+ if (
386
+ this.toolExecution?.engine === 'cloudflare-sandbox' &&
387
+ this.toolExecution.cloudflare?.fileCheckpointing === true
388
+ ) {
389
+ const bundle = createCloudflareCodingToolBundle(
390
+ this.toolExecution.cloudflare
391
+ );
392
+ this._fileCheckpointer = bundle.checkpointer;
393
+ return this._fileCheckpointer;
394
+ }
395
+ return undefined;
381
396
  }
382
397
  }
383
398
 
@@ -1318,6 +1333,26 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1318
1333
  { force: true }
1319
1334
  );
1320
1335
 
1336
+ const langfuseHandler = createLangfuseHandler({
1337
+ langfuse: agentContext.langfuse,
1338
+ userId: config.configurable?.user_id as string | undefined,
1339
+ sessionId: config.configurable?.thread_id as string | undefined,
1340
+ traceMetadata: {
1341
+ messageId: this.runId,
1342
+ parentMessageId: config.configurable?.requestBody?.parentMessageId,
1343
+ agentId,
1344
+ agentName: agentContext.name,
1345
+ },
1346
+ });
1347
+ const invokeConfig = langfuseHandler
1348
+ ? {
1349
+ ...config,
1350
+ callbacks: ((config.callbacks as t.ProvidedCallbacks) ?? []).concat(
1351
+ [langfuseHandler]
1352
+ ),
1353
+ }
1354
+ : config;
1355
+
1321
1356
  try {
1322
1357
  result = await attemptInvoke(
1323
1358
  {
@@ -1326,17 +1361,19 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1326
1361
  provider: agentContext.provider,
1327
1362
  context: this,
1328
1363
  },
1329
- config
1364
+ invokeConfig
1330
1365
  );
1331
1366
  } catch (primaryError) {
1332
1367
  result = await tryFallbackProviders({
1333
1368
  fallbacks,
1334
1369
  tools: agentContext.tools,
1335
1370
  messages: finalMessages,
1336
- config,
1371
+ config: invokeConfig,
1337
1372
  primaryError,
1338
1373
  context: this,
1339
1374
  });
1375
+ } finally {
1376
+ await disposeLangfuseHandler(langfuseHandler);
1340
1377
  }
1341
1378
 
1342
1379
  if (!result) {
@@ -1562,6 +1599,13 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
1562
1599
  maxDepth: effectiveSubagentDepth,
1563
1600
  createChildGraph: (input): StandardGraph => {
1564
1601
  const childGraph = new StandardGraph(input);
1602
+ childGraph.hookRegistry = this.hookRegistry;
1603
+ /**
1604
+ * Do not propagate `humanInTheLoop` into the child graph yet:
1605
+ * nested subagent interrupts need a stable child checkpoint and
1606
+ * resume bridge. Child hooks still fire; `ask` decisions fail
1607
+ * closed inside the subagent until that flow is implemented.
1608
+ */
1565
1609
  childGraph.toolOutputReferences = this.toolOutputReferences;
1566
1610
  childGraph.eagerEventToolExecution = this.eagerEventToolExecution;
1567
1611
  childGraph.toolExecution = this.toolExecution;
@@ -109,6 +109,44 @@ describe('executeHooks', () => {
109
109
  consoleWarnSpy.mockRestore();
110
110
  });
111
111
 
112
+ describe('abort listener management', () => {
113
+ it('uses one abort listener for many hooks on one matcher', async () => {
114
+ const registry = new HookRegistry();
115
+ const listenerCounts = new Map<AbortSignal, number>();
116
+ let maxAbortListeners = 0;
117
+ const addEventListenerSpy = jest
118
+ .spyOn(AbortSignal.prototype, 'addEventListener')
119
+ .mockImplementation(function (
120
+ this: AbortSignal,
121
+ type: string,
122
+ _listener: EventListenerOrEventListenerObject | null
123
+ ): void {
124
+ if (type !== 'abort') {
125
+ return;
126
+ }
127
+ const count = (listenerCounts.get(this) ?? 0) + 1;
128
+ listenerCounts.set(this, count);
129
+ maxAbortListeners = Math.max(maxAbortListeners, count);
130
+ });
131
+ const hooks = Array.from({ length: 12 }, () =>
132
+ runStartHook(async (): Promise<RunStartHookOutput> => ({}))
133
+ );
134
+
135
+ try {
136
+ registry.register('RunStart', { hooks });
137
+
138
+ await executeHooks({
139
+ registry,
140
+ input: runStartInput(),
141
+ timeoutMs: 1000,
142
+ });
143
+ expect(maxAbortListeners).toBe(1);
144
+ } finally {
145
+ addEventListenerSpy.mockRestore();
146
+ }
147
+ });
148
+ });
149
+
112
150
  describe('empty matcher set', () => {
113
151
  it('returns an empty aggregated result when no matchers are registered', async () => {
114
152
  const registry = new HookRegistry();
@@ -46,6 +46,11 @@ interface HookOutcome {
46
46
  timedOut: boolean;
47
47
  }
48
48
 
49
+ interface AbortRace {
50
+ promise: Promise<never>;
51
+ cleanup: () => void;
52
+ }
53
+
49
54
  function freshResult(): AggregatedHookResult {
50
55
  return {
51
56
  additionalContexts: [],
@@ -110,10 +115,10 @@ async function runHook(
110
115
  hook: WideCallback,
111
116
  input: HookInput,
112
117
  signal: AbortSignal,
118
+ abortPromise: Promise<never>,
113
119
  matcher: WideMatcher
114
120
  ): Promise<HookOutcome> {
115
121
  const hookPromise = Promise.resolve().then(() => hook(input, signal));
116
- const { promise: abortPromise, cleanup } = makeAbortPromise(signal);
117
122
  try {
118
123
  const output = await Promise.race([hookPromise, abortPromise]);
119
124
  return { matcher, output, error: null, timedOut: false };
@@ -124,8 +129,22 @@ async function runHook(
124
129
  error: describeError(err),
125
130
  timedOut: isTimeout(err),
126
131
  };
132
+ }
133
+ }
134
+
135
+ async function runMatcherHooks(
136
+ matcher: WideMatcher,
137
+ input: HookInput,
138
+ signal: AbortSignal
139
+ ): Promise<HookOutcome[]> {
140
+ const abortRace: AbortRace = makeAbortPromise(signal);
141
+ const tasks = matcher.hooks.map((hook) =>
142
+ runHook(hook, input, signal, abortRace.promise, matcher)
143
+ );
144
+ try {
145
+ return await Promise.all(tasks);
127
146
  } finally {
128
- cleanup();
147
+ abortRace.cleanup();
129
148
  }
130
149
  }
131
150
 
@@ -373,7 +392,7 @@ export async function executeHooks(
373
392
  }
374
393
 
375
394
  // --- SYNC CRITICAL SECTION: once-matcher removal must complete before any await ---
376
- const tasks: Promise<HookOutcome>[] = [];
395
+ const tasks: Promise<HookOutcome[]>[] = [];
377
396
  for (const matcher of matchers) {
378
397
  if (!matchesQuery(matcher.pattern, matchQuery)) {
379
398
  continue;
@@ -381,18 +400,19 @@ export async function executeHooks(
381
400
  if (matcher.once === true) {
382
401
  registry.removeMatcher(event, matcher, sessionId);
383
402
  }
403
+ if (matcher.hooks.length === 0) {
404
+ continue;
405
+ }
384
406
  const perHookTimeout = matcher.timeout ?? timeoutMs;
385
407
  const matcherSignal = combineSignals(signal, perHookTimeout);
386
- for (const hook of matcher.hooks) {
387
- tasks.push(runHook(hook, input, matcherSignal, matcher));
388
- }
408
+ tasks.push(runMatcherHooks(matcher, input, matcherSignal));
389
409
  }
390
410
  // --- END SYNC CRITICAL SECTION ---
391
411
  if (tasks.length === 0) {
392
412
  return freshResult();
393
413
  }
394
414
 
395
- const outcomes = await Promise.all(tasks);
415
+ const outcomes = (await Promise.all(tasks)).flat();
396
416
  reportErrors(outcomes, event, logger);
397
417
  const aggregated = fold(outcomes);
398
418
  /**
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ export * from './tools/ToolNode';
27
27
  export * from './tools/schema';
28
28
  export * from './tools/handlers';
29
29
  export * from './tools/local';
30
+ export * from './tools/cloudflare';
30
31
  export * from './tools/search';
31
32
 
32
33
  /* Misc. */