@librechat/agents 3.2.65 → 3.2.66

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 (56) hide show
  1. package/dist/cjs/graphs/Graph.cjs +15 -2
  2. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  3. package/dist/cjs/instrumentation.cjs +15 -3
  4. package/dist/cjs/instrumentation.cjs.map +1 -1
  5. package/dist/cjs/langfuseToolOutputTracing.cjs +1 -2
  6. package/dist/cjs/langfuseToolOutputTracing.cjs.map +1 -1
  7. package/dist/cjs/langfuseTraceShaping.cjs +51 -24
  8. package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +8 -0
  10. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +8 -0
  12. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/tools/BashExecutor.cjs +9 -8
  14. package/dist/cjs/tools/BashExecutor.cjs.map +1 -1
  15. package/dist/cjs/tools/CodeExecutor.cjs +9 -7
  16. package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
  17. package/dist/esm/graphs/Graph.mjs +15 -2
  18. package/dist/esm/graphs/Graph.mjs.map +1 -1
  19. package/dist/esm/instrumentation.mjs +15 -3
  20. package/dist/esm/instrumentation.mjs.map +1 -1
  21. package/dist/esm/langfuseToolOutputTracing.mjs +1 -2
  22. package/dist/esm/langfuseToolOutputTracing.mjs.map +1 -1
  23. package/dist/esm/langfuseTraceShaping.mjs +51 -24
  24. package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
  25. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +8 -0
  26. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  27. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +8 -0
  28. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  29. package/dist/esm/tools/BashExecutor.mjs +9 -8
  30. package/dist/esm/tools/BashExecutor.mjs.map +1 -1
  31. package/dist/esm/tools/CodeExecutor.mjs +9 -7
  32. package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
  33. package/dist/types/graphs/Graph.d.ts +2 -0
  34. package/dist/types/langfuseTraceShaping.d.ts +12 -6
  35. package/dist/types/tools/BashExecutor.d.ts +7 -6
  36. package/dist/types/tools/CodeExecutor.d.ts +7 -5
  37. package/dist/types/types/graph.d.ts +10 -3
  38. package/package.json +1 -1
  39. package/src/graphs/Graph.ts +21 -3
  40. package/src/instrumentation.ts +20 -0
  41. package/src/langfuseToolOutputTracing.ts +2 -4
  42. package/src/langfuseTraceShaping.ts +73 -20
  43. package/src/llm/anthropic/utils/cross-provider-server-tools.test.ts +110 -0
  44. package/src/llm/anthropic/utils/message_inputs.ts +15 -0
  45. package/src/llm/bedrock/utils/cross-provider-server-tools.test.ts +122 -0
  46. package/src/llm/bedrock/utils/message_inputs.ts +13 -0
  47. package/src/specs/langfuse-instrumentation.test.ts +64 -0
  48. package/src/specs/langfuse-routing.integration.test.ts +49 -4
  49. package/src/specs/langfuse-tool-output-tracing.test.ts +5 -4
  50. package/src/specs/langfuse-trace-shaping.test.ts +80 -9
  51. package/src/specs/subagent.test.ts +180 -0
  52. package/src/tools/BashExecutor.ts +9 -8
  53. package/src/tools/CodeExecutor.ts +9 -7
  54. package/src/tools/__tests__/BashExecutor.test.ts +16 -5
  55. package/src/tools/__tests__/CodeExecutor.stateful.test.ts +17 -6
  56. package/src/types/graph.ts +10 -3
@@ -1 +1 @@
1
- {"version":3,"file":"BashExecutor.mjs","names":[],"sources":["../../../src/tools/BashExecutor.ts"],"sourcesContent":["import { config } from 'dotenv';\nimport fetch, { RequestInit } from 'node-fetch';\nimport { HttpsProxyAgent } from 'https-proxy-agent';\nimport { tool, DynamicStructuredTool } from '@langchain/core/tools';\nimport type * as t from '@/types';\nimport {\n BASH_SHELL_GUIDANCE,\n CODE_ARTIFACT_PATH_GUIDANCE,\n appendFailedExecutionFileReminder,\n appendTmpScratchReminder,\n appendCodeSessionFileSummary,\n emptyOutputMessage,\n buildCodeApiHttpErrorMessage,\n getCodeBaseURL,\n resolveCodeApiAuthHeaders,\n} from './CodeExecutor';\nimport { Constants } from '@/common';\n\nconfig();\n\nconst baseEndpoint = getCodeBaseURL();\nconst EXEC_ENDPOINT = `${baseEndpoint}/exec`;\n\nexport const BashExecutionToolSchema = {\n type: 'object',\n properties: {\n command: {\n type: 'string',\n description: `The bash command or script to execute.\n- The environment is stateless; variables and state don't persist between executions.\n- Prior /mnt/data files are available and can be modified in place.\n- ${CODE_ARTIFACT_PATH_GUIDANCE}\n- ${BASH_SHELL_GUIDANCE}\n- Input code **IS ALREADY** displayed to the user, so **DO NOT** repeat it in your response unless asked.\n- Output code **IS NOT** displayed to the user, so **DO** write all desired output explicitly.\n- IMPORTANT: You MUST explicitly print/output ALL results you want the user to see.\n- Use \\`echo\\`, \\`printf\\`, or \\`cat\\` for all outputs.`,\n },\n args: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Additional arguments to execute the command with. This should only be used if the input command requires additional arguments to run.',\n },\n },\n required: ['command'],\n} as const;\n\nexport const BashExecutionToolDescription = `\nRuns bash commands and returns stdout/stderr output from a stateless execution environment, similar to running scripts in a command-line interface. Each execution is isolated and independent.\n\nUsage:\n- No network access available.\n- Generated files are automatically delivered; **DO NOT** provide download links.\n- ${CODE_ARTIFACT_PATH_GUIDANCE}\n- ${BASH_SHELL_GUIDANCE}\n- NEVER use this tool to execute malicious commands.\n`.trim();\n\n/**\n * Bash statefulness is filesystem-tier: on a warm session the machine (files\n * including /tmp, installed packages, background processes) persists between\n * calls, but each call may start a fresh shell — so shell variables and cwd\n * are NOT reliable, and the machine can be reset at any time. Only /mnt/data\n * is durable.\n */\nexport const STATEFUL_BASH_NOTE =\n '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 — do not rely on shell variables or the working directory carrying over — and the machine may be reset at any time. Only /mnt/data is durable.';\n\nexport const StatefulBashExecutionToolDescription = `\nRuns bash commands and returns stdout/stderr output from a session-based execution environment, similar to a long-running machine.\n\n${STATEFUL_BASH_NOTE}\n\nUsage:\n- No network access available.\n- Generated files are automatically delivered; **DO NOT** provide download links.\n- ${CODE_ARTIFACT_PATH_GUIDANCE}\n- ${BASH_SHELL_GUIDANCE}\n- NEVER use this tool to execute malicious commands.\n`.trim();\n\n/**\n * Supplemental prompt documenting the tool-output reference feature.\n *\n * Hosts should append this (separated by a blank line) to the base\n * {@link BashExecutionToolDescription} only when\n * `RunConfig.toolOutputReferences.enabled` is `true`. When the feature\n * is disabled, including this text would tell the LLM to emit\n * `{{tool0turn0}}` placeholders that pass through unsubstituted and\n * leak into the shell.\n */\nexport const BashToolOutputReferencesGuide = `\nReferencing previous tool outputs:\n- Every successful tool result is tagged with a reference key of the form \\`tool<idx>turn<turn>\\` (e.g., \\`tool0turn0\\`). The key appears either as a \\`[ref: tool0turn0]\\` prefix line or, when the output is a JSON object, as a \\`_ref\\` field on the object.\n- To pipe a previous tool output into this tool, embed the placeholder \\`{{tool<idx>turn<turn>}}\\` literally anywhere in the \\`command\\` string (or any string arg). It will be substituted with the stored output verbatim before the command runs.\n- The substituted value is the original output string (no \\`[ref: …]\\` prefix, no \\`_ref\\` key), so it is safe to pipe directly into \\`jq\\`, \\`grep\\`, \\`awk\\`, etc.\n- Example (simple ASCII output): \\`echo '{{tool0turn0}}' | jq '.foo'\\` takes the full output of the first tool from the first turn and pipes it into jq.\n- For payloads that may contain quotes, parentheses, backticks, or arbitrary bytes (random/binary data, JSON with embedded quotes, multi-line strings), prefer a quoted-delimiter heredoc over \\`echo '…'\\`. The heredoc body is not interpreted by the shell, so substituted payloads pass through unchanged.\n- Heredoc example: \\`wc -c << 'EOF'\\\\n{{tool0turn0}}\\\\nEOF\\` (the quotes around \\`'EOF'\\` disable interpolation inside the body).\n- Unknown reference keys are left in place and surfaced as \\`[unresolved refs: …]\\` after the output.\n`.trim();\n\n/**\n * Composes the bash tool description, optionally appending the\n * tool-output references guide. Hosts that enable\n * `RunConfig.toolOutputReferences` should pass `enableToolOutputReferences: true`\n * when registering the tool so the LLM learns the `{{…}}` syntax it\n * will actually be able to use.\n */\nexport function buildBashExecutionToolDescription(options?: {\n enableToolOutputReferences?: boolean;\n statefulSessions?: boolean;\n}): string {\n const base =\n options?.statefulSessions === true\n ? StatefulBashExecutionToolDescription\n : BashExecutionToolDescription;\n if (options?.enableToolOutputReferences === true) {\n return `${base}\\n\\n${BashToolOutputReferencesGuide}`;\n }\n return base;\n}\n\nconst STATELESS_BASH_PARAM_NOTE =\n 'The environment is stateless; variables and state don\\'t persist between executions.';\nconst STATEFUL_BASH_PARAM_NOTE =\n 'Files, installed packages, and background processes usually persist between calls, but each call may start a fresh shell (do not rely on shell variables or cwd) and the machine may reset. Only /mnt/data is durable.';\n\nexport function buildBashExecutionToolSchema(opts?: {\n statefulSessions?: boolean;\n}): typeof BashExecutionToolSchema {\n const note =\n opts?.statefulSessions === true\n ? STATEFUL_BASH_PARAM_NOTE\n : STATELESS_BASH_PARAM_NOTE;\n const commandDescription =\n BashExecutionToolSchema.properties.command.description.replace(\n STATELESS_BASH_PARAM_NOTE,\n note\n );\n return {\n ...BashExecutionToolSchema,\n properties: {\n ...BashExecutionToolSchema.properties,\n command: {\n ...BashExecutionToolSchema.properties.command,\n description: commandDescription,\n },\n },\n } as typeof BashExecutionToolSchema;\n}\n\nexport const BashExecutionToolName = Constants.BASH_TOOL;\n\n/**\n * Default bash tool definition using the base description.\n *\n * When `RunConfig.toolOutputReferences.enabled` is `true`, build a\n * reference-aware description with\n * {@link buildBashExecutionToolDescription}\n * (`{ enableToolOutputReferences: true }`) and construct a custom\n * definition using it — using this constant as-is leaves the LLM\n * unaware of the `{{tool<i>turn<n>}}` syntax.\n */\nexport const BashExecutionToolDefinition = {\n name: BashExecutionToolName,\n description: BashExecutionToolDescription,\n schema: BashExecutionToolSchema,\n} as const;\n\nfunction createBashExecutionTool(\n params: t.BashExecutionToolParams | null = {}\n): DynamicStructuredTool {\n return tool(\n async (rawInput, config) => {\n /* `statefulSessions` is prompt-only — keep it out of the wire body. */\n const {\n authHeaders,\n statefulSessions: _statefulSessions,\n ...executionParams\n } = params ?? {};\n void _statefulSessions;\n /* Drop any model-supplied `runtime_session_hint` from the raw args: the\n * hint must only come from ToolNode's injected `_runtime_session_hint`\n * (below), never from the tool call itself. */\n const {\n command,\n runtime_session_hint: _ignoredModelHint,\n ...rest\n } = rawInput as {\n command: string;\n runtime_session_hint?: unknown;\n args?: string[];\n };\n void _ignoredModelHint;\n const { session_id, _injected_files, _runtime_session_hint } =\n (config.toolCall ?? {}) as {\n session_id?: string;\n _injected_files?: t.CodeEnvFile[];\n _runtime_session_hint?: string;\n };\n\n const postData: Record<string, unknown> = {\n lang: 'bash',\n code: command,\n ...rest,\n ...executionParams,\n };\n\n if (\n typeof _runtime_session_hint === 'string' &&\n _runtime_session_hint !== ''\n ) {\n postData.runtime_session_hint = _runtime_session_hint;\n }\n\n /* See `CodeExecutor.ts` for the rationale — `/files/<session_id>`\n * HTTP fallback was removed because codeapi's sessionAuth requires\n * kind/id query params unavailable at this point. */\n if (_injected_files && _injected_files.length > 0) {\n postData.files = _injected_files;\n } else if (\n session_id != null &&\n session_id.length > 0 &&\n !Array.isArray(postData.files)\n ) {\n // eslint-disable-next-line no-console\n console.debug(\n `[BashExecutor] No injected files for session_id=${session_id} — exec will run without input files`\n );\n }\n\n try {\n const resolvedAuthHeaders =\n await resolveCodeApiAuthHeaders(authHeaders);\n const fetchOptions: RequestInit = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'LibreChat/1.0',\n ...resolvedAuthHeaders,\n },\n body: JSON.stringify(postData),\n };\n\n if (process.env.PROXY != null && process.env.PROXY !== '') {\n fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY);\n }\n const response = await fetch(EXEC_ENDPOINT, fetchOptions);\n if (!response.ok) {\n throw new Error(\n await buildCodeApiHttpErrorMessage('POST', EXEC_ENDPOINT, response)\n );\n }\n\n const result: t.ExecuteResult = await response.json();\n let formattedOutput = '';\n if (result.stdout) {\n formattedOutput += `stdout:\\n${result.stdout}\\n`;\n } else {\n formattedOutput += emptyOutputMessage;\n }\n if (result.stderr) formattedOutput += `stderr:\\n${result.stderr}\\n`;\n\n const outputWithReminder = appendTmpScratchReminder(\n formattedOutput,\n command\n );\n const hasFiles = result.files != null && result.files.length > 0;\n const runtimeEcho =\n result.runtime_session_id != null\n ? {\n runtime_session_id: result.runtime_session_id,\n runtime_status: result.runtime_status,\n }\n : {};\n return [\n appendCodeSessionFileSummary(outputWithReminder, result.files),\n (hasFiles\n ? {\n session_id: result.session_id,\n files: result.files,\n ...runtimeEcho,\n }\n : {\n session_id: result.session_id,\n ...runtimeEcho,\n }) satisfies t.CodeExecutionArtifact,\n ];\n } catch (error) {\n const messageWithReminder = appendFailedExecutionFileReminder(\n (error as Error | undefined)?.message ?? '',\n command\n );\n throw new Error(`Execution error:\\n\\n${messageWithReminder}`);\n }\n },\n {\n name: BashExecutionToolName,\n description: buildBashExecutionToolDescription({\n statefulSessions: params?.statefulSessions,\n }),\n schema: buildBashExecutionToolSchema(params ?? undefined),\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n\nexport { createBashExecutionTool };\n"],"mappings":"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"}
1
+ {"version":3,"file":"BashExecutor.mjs","names":[],"sources":["../../../src/tools/BashExecutor.ts"],"sourcesContent":["import { config } from 'dotenv';\nimport fetch, { RequestInit } from 'node-fetch';\nimport { HttpsProxyAgent } from 'https-proxy-agent';\nimport { tool, DynamicStructuredTool } from '@langchain/core/tools';\nimport type * as t from '@/types';\nimport {\n BASH_SHELL_GUIDANCE,\n CODE_ARTIFACT_PATH_GUIDANCE,\n appendFailedExecutionFileReminder,\n appendTmpScratchReminder,\n appendCodeSessionFileSummary,\n emptyOutputMessage,\n buildCodeApiHttpErrorMessage,\n getCodeBaseURL,\n resolveCodeApiAuthHeaders,\n} from './CodeExecutor';\nimport { Constants } from '@/common';\n\nconfig();\n\nconst baseEndpoint = getCodeBaseURL();\nconst EXEC_ENDPOINT = `${baseEndpoint}/exec`;\n\nexport const BashExecutionToolSchema = {\n type: 'object',\n properties: {\n command: {\n type: 'string',\n description: `The bash command or script to execute.\n- The environment is stateless; variables and state don't persist between executions.\n- Prior /mnt/data files are available and can be modified in place.\n- ${CODE_ARTIFACT_PATH_GUIDANCE}\n- ${BASH_SHELL_GUIDANCE}\n- Input code **IS ALREADY** displayed to the user, so **DO NOT** repeat it in your response unless asked.\n- Output code **IS NOT** displayed to the user, so **DO** write all desired output explicitly.\n- IMPORTANT: You MUST explicitly print/output ALL results you want the user to see.\n- Use \\`echo\\`, \\`printf\\`, or \\`cat\\` for all outputs.`,\n },\n args: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Additional arguments to execute the command with. This should only be used if the input command requires additional arguments to run.',\n },\n },\n required: ['command'],\n} as const;\n\nexport const BashExecutionToolDescription = `\nRuns bash commands and returns stdout/stderr output from a stateless execution environment, similar to running scripts in a command-line interface. Each execution is isolated and independent.\n\nUsage:\n- No network access available.\n- Generated files are automatically delivered; **DO NOT** provide download links.\n- ${CODE_ARTIFACT_PATH_GUIDANCE}\n- ${BASH_SHELL_GUIDANCE}\n- NEVER use this tool to execute malicious commands.\n`.trim();\n\n/**\n * Bash statefulness is filesystem-tier and scoped to `/mnt/data`. The machine\n * is warm across calls, but each call runs in a fresh sandbox (new process\n * tree + private /tmp), so background processes are reaped when the call ends\n * and anything written outside /mnt/data is discarded. The note must not\n * promise otherwise: a model told background processes survive will start a\n * server in one call and assume it is listening in the next.\n */\nexport const STATEFUL_BASH_NOTE =\n 'Session state: commands in this conversation run on the same warm machine, so files written to /mnt/data persist between calls. Each call runs in a fresh, isolated sandbox: shell variables, the working directory, /tmp, and background processes do NOT survive after the call returns — a process started in one call is terminated when that call ends. Only /mnt/data is durable (the machine itself may also be reset at any time).';\n\nexport const StatefulBashExecutionToolDescription = `\nRuns bash commands and returns stdout/stderr output. Commands in this conversation share one warm machine with a persistent /mnt/data, but each command runs in its own isolated sandbox (not a persistent shell session).\n\n${STATEFUL_BASH_NOTE}\n\nUsage:\n- No network access available.\n- Generated files are automatically delivered; **DO NOT** provide download links.\n- ${CODE_ARTIFACT_PATH_GUIDANCE}\n- ${BASH_SHELL_GUIDANCE}\n- NEVER use this tool to execute malicious commands.\n`.trim();\n\n/**\n * Supplemental prompt documenting the tool-output reference feature.\n *\n * Hosts should append this (separated by a blank line) to the base\n * {@link BashExecutionToolDescription} only when\n * `RunConfig.toolOutputReferences.enabled` is `true`. When the feature\n * is disabled, including this text would tell the LLM to emit\n * `{{tool0turn0}}` placeholders that pass through unsubstituted and\n * leak into the shell.\n */\nexport const BashToolOutputReferencesGuide = `\nReferencing previous tool outputs:\n- Every successful tool result is tagged with a reference key of the form \\`tool<idx>turn<turn>\\` (e.g., \\`tool0turn0\\`). The key appears either as a \\`[ref: tool0turn0]\\` prefix line or, when the output is a JSON object, as a \\`_ref\\` field on the object.\n- To pipe a previous tool output into this tool, embed the placeholder \\`{{tool<idx>turn<turn>}}\\` literally anywhere in the \\`command\\` string (or any string arg). It will be substituted with the stored output verbatim before the command runs.\n- The substituted value is the original output string (no \\`[ref: …]\\` prefix, no \\`_ref\\` key), so it is safe to pipe directly into \\`jq\\`, \\`grep\\`, \\`awk\\`, etc.\n- Example (simple ASCII output): \\`echo '{{tool0turn0}}' | jq '.foo'\\` takes the full output of the first tool from the first turn and pipes it into jq.\n- For payloads that may contain quotes, parentheses, backticks, or arbitrary bytes (random/binary data, JSON with embedded quotes, multi-line strings), prefer a quoted-delimiter heredoc over \\`echo '…'\\`. The heredoc body is not interpreted by the shell, so substituted payloads pass through unchanged.\n- Heredoc example: \\`wc -c << 'EOF'\\\\n{{tool0turn0}}\\\\nEOF\\` (the quotes around \\`'EOF'\\` disable interpolation inside the body).\n- Unknown reference keys are left in place and surfaced as \\`[unresolved refs: …]\\` after the output.\n`.trim();\n\n/**\n * Composes the bash tool description, optionally appending the\n * tool-output references guide. Hosts that enable\n * `RunConfig.toolOutputReferences` should pass `enableToolOutputReferences: true`\n * when registering the tool so the LLM learns the `{{…}}` syntax it\n * will actually be able to use.\n */\nexport function buildBashExecutionToolDescription(options?: {\n enableToolOutputReferences?: boolean;\n statefulSessions?: boolean;\n}): string {\n const base =\n options?.statefulSessions === true\n ? StatefulBashExecutionToolDescription\n : BashExecutionToolDescription;\n if (options?.enableToolOutputReferences === true) {\n return `${base}\\n\\n${BashToolOutputReferencesGuide}`;\n }\n return base;\n}\n\nconst STATELESS_BASH_PARAM_NOTE =\n 'The environment is stateless; variables and state don\\'t persist between executions.';\nconst STATEFUL_BASH_PARAM_NOTE =\n 'Files written to /mnt/data persist between calls on the same warm machine. Each call runs in a fresh sandbox: shell variables, cwd, /tmp, and background processes do NOT survive the call. Only /mnt/data is durable.';\n\nexport function buildBashExecutionToolSchema(opts?: {\n statefulSessions?: boolean;\n}): typeof BashExecutionToolSchema {\n const note =\n opts?.statefulSessions === true\n ? STATEFUL_BASH_PARAM_NOTE\n : STATELESS_BASH_PARAM_NOTE;\n const commandDescription =\n BashExecutionToolSchema.properties.command.description.replace(\n STATELESS_BASH_PARAM_NOTE,\n note\n );\n return {\n ...BashExecutionToolSchema,\n properties: {\n ...BashExecutionToolSchema.properties,\n command: {\n ...BashExecutionToolSchema.properties.command,\n description: commandDescription,\n },\n },\n } as typeof BashExecutionToolSchema;\n}\n\nexport const BashExecutionToolName = Constants.BASH_TOOL;\n\n/**\n * Default bash tool definition using the base description.\n *\n * When `RunConfig.toolOutputReferences.enabled` is `true`, build a\n * reference-aware description with\n * {@link buildBashExecutionToolDescription}\n * (`{ enableToolOutputReferences: true }`) and construct a custom\n * definition using it — using this constant as-is leaves the LLM\n * unaware of the `{{tool<i>turn<n>}}` syntax.\n */\nexport const BashExecutionToolDefinition = {\n name: BashExecutionToolName,\n description: BashExecutionToolDescription,\n schema: BashExecutionToolSchema,\n} as const;\n\nfunction createBashExecutionTool(\n params: t.BashExecutionToolParams | null = {}\n): DynamicStructuredTool {\n return tool(\n async (rawInput, config) => {\n /* `statefulSessions` is prompt-only — keep it out of the wire body. */\n const {\n authHeaders,\n statefulSessions: _statefulSessions,\n ...executionParams\n } = params ?? {};\n void _statefulSessions;\n /* Drop any model-supplied `runtime_session_hint` from the raw args: the\n * hint must only come from ToolNode's injected `_runtime_session_hint`\n * (below), never from the tool call itself. */\n const {\n command,\n runtime_session_hint: _ignoredModelHint,\n ...rest\n } = rawInput as {\n command: string;\n runtime_session_hint?: unknown;\n args?: string[];\n };\n void _ignoredModelHint;\n const { session_id, _injected_files, _runtime_session_hint } =\n (config.toolCall ?? {}) as {\n session_id?: string;\n _injected_files?: t.CodeEnvFile[];\n _runtime_session_hint?: string;\n };\n\n const postData: Record<string, unknown> = {\n lang: 'bash',\n code: command,\n ...rest,\n ...executionParams,\n };\n\n if (\n typeof _runtime_session_hint === 'string' &&\n _runtime_session_hint !== ''\n ) {\n postData.runtime_session_hint = _runtime_session_hint;\n }\n\n /* See `CodeExecutor.ts` for the rationale — `/files/<session_id>`\n * HTTP fallback was removed because codeapi's sessionAuth requires\n * kind/id query params unavailable at this point. */\n if (_injected_files && _injected_files.length > 0) {\n postData.files = _injected_files;\n } else if (\n session_id != null &&\n session_id.length > 0 &&\n !Array.isArray(postData.files)\n ) {\n // eslint-disable-next-line no-console\n console.debug(\n `[BashExecutor] No injected files for session_id=${session_id} — exec will run without input files`\n );\n }\n\n try {\n const resolvedAuthHeaders =\n await resolveCodeApiAuthHeaders(authHeaders);\n const fetchOptions: RequestInit = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'LibreChat/1.0',\n ...resolvedAuthHeaders,\n },\n body: JSON.stringify(postData),\n };\n\n if (process.env.PROXY != null && process.env.PROXY !== '') {\n fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY);\n }\n const response = await fetch(EXEC_ENDPOINT, fetchOptions);\n if (!response.ok) {\n throw new Error(\n await buildCodeApiHttpErrorMessage('POST', EXEC_ENDPOINT, response)\n );\n }\n\n const result: t.ExecuteResult = await response.json();\n let formattedOutput = '';\n if (result.stdout) {\n formattedOutput += `stdout:\\n${result.stdout}\\n`;\n } else {\n formattedOutput += emptyOutputMessage;\n }\n if (result.stderr) formattedOutput += `stderr:\\n${result.stderr}\\n`;\n\n const outputWithReminder = appendTmpScratchReminder(\n formattedOutput,\n command\n );\n const hasFiles = result.files != null && result.files.length > 0;\n const runtimeEcho =\n result.runtime_session_id != null\n ? {\n runtime_session_id: result.runtime_session_id,\n runtime_status: result.runtime_status,\n }\n : {};\n return [\n appendCodeSessionFileSummary(outputWithReminder, result.files),\n (hasFiles\n ? {\n session_id: result.session_id,\n files: result.files,\n ...runtimeEcho,\n }\n : {\n session_id: result.session_id,\n ...runtimeEcho,\n }) satisfies t.CodeExecutionArtifact,\n ];\n } catch (error) {\n const messageWithReminder = appendFailedExecutionFileReminder(\n (error as Error | undefined)?.message ?? '',\n command\n );\n throw new Error(`Execution error:\\n\\n${messageWithReminder}`);\n }\n },\n {\n name: BashExecutionToolName,\n description: buildBashExecutionToolDescription({\n statefulSessions: params?.statefulSessions,\n }),\n schema: buildBashExecutionToolSchema(params ?? undefined),\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n\nexport { createBashExecutionTool };\n"],"mappings":"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"}
@@ -96,14 +96,16 @@ Usage:
96
96
  - NEVER use this tool to execute malicious code.
97
97
  `.trim();
98
98
  /**
99
- * Best-effort statefulness note. Deliberately hedged: warm reuse is an
100
- * optimization, not a guarantee (the runtime may be reset on idle timeout,
101
- * eviction, or the 8h VM lifetime), so the model must never depend on carried
102
- * state for correctness and must persist anything durable to /mnt/data.
99
+ * Statefulness here is FILESYSTEM-tier, not runtime-tier. Executions in a
100
+ * session reuse one warm machine, so `/mnt/data` carries across calls but
101
+ * every execution is a brand-new interpreter process in a fresh sandbox, so
102
+ * variables and imports never survive. The note must not imply otherwise: a
103
+ * model told its in-memory state persists writes `df = ...` in one call and
104
+ * `df.head()` in the next, then hits a NameError it was told to treat as rare.
103
105
  */
104
- 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.";
106
+ const STATEFUL_ENV_NOTE = "Session state: executions in this conversation run on the same warm machine, so files persist between calls but each execution is a NEW process. Variables, imports, and in-memory data NEVER carry over: every call must re-import and rebuild the state it needs. Only /mnt/data is durable (the machine itself may also be reset at any time), so write anything that must survive there and read it back next call.";
105
107
  const StatefulCodeExecutionToolDescription = `
106
- Runs code and returns stdout/stderr output from a session-based execution environment, similar to a long-running command-line session.
108
+ Runs code and returns stdout/stderr output. Executions in this conversation share one warm machine with a persistent /mnt/data, but each execution runs as a separate process (not a notebook-style kernel).
107
109
 
108
110
  ${STATEFUL_ENV_NOTE}
109
111
 
@@ -117,7 +119,7 @@ function buildCodeExecutionToolDescription(opts) {
117
119
  return opts?.statefulSessions === true ? StatefulCodeExecutionToolDescription : CodeExecutionToolDescription;
118
120
  }
119
121
  const STATELESS_CODE_PARAM_NOTE = "The environment is stateless; variables and imports don't persist between executions.";
120
- const STATEFUL_CODE_PARAM_NOTE = "Executions in this conversation usually share one runtime: variables and imports from prior successful calls are typically still defined, but the runtime may reset between calls. Rebuild state on NameError/ImportError; persist anything important to /mnt/data.";
122
+ const STATEFUL_CODE_PARAM_NOTE = "Executions in this conversation share one warm machine, so files written to /mnt/data persist between calls. Each execution is a new process: variables and imports do NOT carry over re-import and reload from /mnt/data every call.";
121
123
  function buildCodeExecutionToolSchema(opts) {
122
124
  const note = opts?.statefulSessions === true ? STATEFUL_CODE_PARAM_NOTE : STATELESS_CODE_PARAM_NOTE;
123
125
  const codeDescription = CodeExecutionToolSchema.properties.code.description.replace(STATELESS_CODE_PARAM_NOTE, note);
@@ -1 +1 @@
1
- {"version":3,"file":"CodeExecutor.mjs","names":[],"sources":["../../../src/tools/CodeExecutor.ts"],"sourcesContent":["import { config } from 'dotenv';\nimport fetch, { RequestInit } from 'node-fetch';\nimport { HttpsProxyAgent } from 'https-proxy-agent';\nimport { getEnvironmentVariable } from '@langchain/core/utils/env';\nimport { tool, DynamicStructuredTool } from '@langchain/core/tools';\nimport type * as t from '@/types';\nimport { appendCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';\nimport { EnvVar, Constants } from '@/common';\n\nexport {\n appendCodeSessionFileSummary,\n stripCodeSessionFileSummary,\n} from '@/tools/CodeSessionFileSummary';\n\nconfig();\n\nexport const getCodeBaseURL = (): string =>\n getEnvironmentVariable(EnvVar.CODE_BASEURL) ??\n Constants.OFFICIAL_CODE_BASEURL;\n\nexport const emptyOutputMessage =\n 'stdout: Empty. Ensure you\\'re writing output explicitly.\\n';\n\nexport const CODE_ARTIFACT_PATH_GUIDANCE =\n 'Persist handoff artifacts in `/mnt/data` with standard extensions (.json/.txt/.csv/.tsv/.log/.parquet/.png/.jpg/.pdf/.xlsx); failed executions do not register new files; `/tmp` and odd extensions are same-call scratch only, not later-call storage.';\n\nexport const BASH_SHELL_GUIDANCE =\n 'Bash: multi-line files use heredoc/printf; run Python via python3 -c/heredoc, not bare Python.';\n\nconst TMP_PATH_PATTERN = /(^|[^A-Za-z0-9_])\\/tmp(?:\\/|\\b)/;\nconst MNT_DATA_PATH_PATTERN = /(^|[^A-Za-z0-9_])\\/mnt\\/data(?:\\/|\\b)/;\n\nexport const TMP_SCRATCH_OUTPUT_REMINDER =\n 'Note: /tmp files are same-call scratch only and were not persisted; use /mnt/data for files needed later.';\n\nexport const FAILED_EXECUTION_FILE_REMINDER =\n 'Note: any files written during this failed call were not registered for later calls; fix the error and rerun before relying on them.';\n\nexport function appendTmpScratchReminder(output: string, code: string): string {\n if (!TMP_PATH_PATTERN.test(code)) {\n return output;\n }\n return `${output.trimEnd()}\\n${TMP_SCRATCH_OUTPUT_REMINDER}\\n`;\n}\n\nexport function appendFailedExecutionFileReminder(\n output: string,\n code: string\n): string {\n if (\n !MNT_DATA_PATH_PATTERN.test(code) ||\n output.includes(FAILED_EXECUTION_FILE_REMINDER)\n ) {\n return output;\n }\n return `${output.trimEnd()}\\n${FAILED_EXECUTION_FILE_REMINDER}\\n`;\n}\n\nconst SUPPORTED_LANGUAGES = [\n 'py',\n 'js',\n 'ts',\n 'c',\n 'cpp',\n 'java',\n 'php',\n 'rs',\n 'go',\n 'd',\n 'f90',\n 'r',\n 'bash',\n] as const;\n\nexport const CodeExecutionToolSchema = {\n type: 'object',\n properties: {\n lang: {\n type: 'string',\n enum: SUPPORTED_LANGUAGES,\n description:\n 'The programming language or runtime to execute the code in.',\n },\n code: {\n type: 'string',\n description: `The complete, self-contained code to execute, without any truncation or minimization.\n- The environment is stateless; variables and imports don't persist between executions.\n- Prior /mnt/data files are available and can be modified in place.\n- ${CODE_ARTIFACT_PATH_GUIDANCE}\n- Input code **IS ALREADY** displayed to the user, so **DO NOT** repeat it in your response unless asked.\n- Output code **IS NOT** displayed to the user, so **DO** write all desired output explicitly.\n- IMPORTANT: You MUST explicitly print/output ALL results you want the user to see.\n- py: This is not a Jupyter notebook environment. Use \\`print()\\` for all outputs.\n- py: Matplotlib: Use \\`plt.savefig()\\` to save plots as files.\n- js: use the \\`console\\` or \\`process\\` methods for all outputs.\n- r: IMPORTANT: No X11 display available. ALL graphics MUST use Cairo library (library(Cairo)).\n- Other languages: use appropriate output functions.`,\n },\n args: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Additional arguments to execute the code with. This should only be used if the input code requires additional arguments to run.',\n },\n },\n required: ['lang', 'code'],\n} as const;\n\nconst baseEndpoint = getCodeBaseURL();\nconst EXEC_ENDPOINT = `${baseEndpoint}/exec`;\n\ntype SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];\n\nexport async function resolveCodeApiAuthHeaders(\n authHeaders?: t.CodeApiAuthHeaders\n): Promise<t.CodeApiAuthHeaderMap> {\n if (authHeaders == null) {\n return {};\n }\n if (typeof authHeaders === 'function') {\n return authHeaders();\n }\n return authHeaders;\n}\n\nexport async function buildCodeApiHttpErrorMessage(\n method: string,\n endpoint: string,\n response: { status: number; text: () => Promise<string> }\n): Promise<string> {\n let responseBody = '';\n try {\n responseBody = await response.text();\n } catch {\n responseBody = '';\n }\n const body = responseBody.trim();\n const bodySuffix = body === '' ? '' : `, body: ${body.slice(0, 1000)}`;\n return `CodeAPI request failed: ${method} ${endpoint} returned ${response.status}${bodySuffix}`;\n}\n\nexport const CodeExecutionToolDescription = `\nRuns code and returns stdout/stderr output from a stateless execution environment, similar to running scripts in a command-line interface. Each execution is isolated and independent.\n\nUsage:\n- No network access available.\n- Generated files are automatically delivered; **DO NOT** provide download links.\n- ${CODE_ARTIFACT_PATH_GUIDANCE}\n- NEVER use this tool to execute malicious code.\n`.trim();\n\n/**\n * Best-effort statefulness note. Deliberately hedged: warm reuse is an\n * optimization, not a guarantee (the runtime may be reset on idle timeout,\n * eviction, or the 8h VM lifetime), so the model must never depend on carried\n * state for correctness and must persist anything durable to /mnt/data.\n */\nexport const STATEFUL_ENV_NOTE =\n '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.';\n\nexport const StatefulCodeExecutionToolDescription = `\nRuns code and returns stdout/stderr output from a session-based execution environment, similar to a long-running command-line session.\n\n${STATEFUL_ENV_NOTE}\n\nUsage:\n- No network access available.\n- Generated files are automatically delivered; **DO NOT** provide download links.\n- ${CODE_ARTIFACT_PATH_GUIDANCE}\n- NEVER use this tool to execute malicious code.\n`.trim();\n\nexport function buildCodeExecutionToolDescription(opts?: {\n statefulSessions?: boolean;\n}): string {\n return opts?.statefulSessions === true\n ? StatefulCodeExecutionToolDescription\n : CodeExecutionToolDescription;\n}\n\nconst STATELESS_CODE_PARAM_NOTE =\n 'The environment is stateless; variables and imports don\\'t persist between executions.';\nconst STATEFUL_CODE_PARAM_NOTE =\n 'Executions in this conversation usually share one runtime: variables and imports from prior successful calls are typically still defined, but the runtime may reset between calls. Rebuild state on NameError/ImportError; persist anything important to /mnt/data.';\n\nexport function buildCodeExecutionToolSchema(opts?: {\n statefulSessions?: boolean;\n}): typeof CodeExecutionToolSchema {\n const note =\n opts?.statefulSessions === true\n ? STATEFUL_CODE_PARAM_NOTE\n : STATELESS_CODE_PARAM_NOTE;\n const codeDescription =\n CodeExecutionToolSchema.properties.code.description.replace(\n STATELESS_CODE_PARAM_NOTE,\n note\n );\n return {\n ...CodeExecutionToolSchema,\n properties: {\n ...CodeExecutionToolSchema.properties,\n code: {\n ...CodeExecutionToolSchema.properties.code,\n description: codeDescription,\n },\n },\n } as typeof CodeExecutionToolSchema;\n}\n\nexport const CodeExecutionToolName = Constants.EXECUTE_CODE;\n\nexport const CodeExecutionToolDefinition = {\n name: CodeExecutionToolName,\n description: CodeExecutionToolDescription,\n schema: CodeExecutionToolSchema,\n} as const;\n\nfunction createCodeExecutionTool(\n params: t.CodeExecutionToolParams | null = {}\n): DynamicStructuredTool {\n return tool(\n async (rawInput, config) => {\n /* `statefulSessions` is a prompt-only flag (drives the description);\n * keep it out of the wire body. */\n const {\n authHeaders,\n statefulSessions: _statefulSessions,\n ...executionParams\n } = params ?? {};\n void _statefulSessions;\n /* Drop any model-supplied `runtime_session_hint` from the raw args: the\n * hint is host-controlled and must only ever come from ToolNode's\n * injected `_runtime_session_hint` (below). Spreading `...rest` into\n * postData would otherwise let a tool call opt itself into / pick a\n * stateful runtime even when statefulSessions is off. */\n const {\n lang,\n code,\n runtime_session_hint: _ignoredModelHint,\n ...rest\n } = rawInput as {\n lang: SupportedLanguage;\n code: string;\n runtime_session_hint?: unknown;\n args?: string[];\n };\n void _ignoredModelHint;\n /**\n * Extract session context from config.toolCall (injected by ToolNode).\n * - session_id: associates with the previous run.\n * - _injected_files: File refs to pass directly (avoids /files endpoint race condition).\n */\n const { session_id, _injected_files, _runtime_session_hint } =\n (config.toolCall ?? {}) as {\n session_id?: string;\n _injected_files?: t.CodeEnvFile[];\n _runtime_session_hint?: string;\n };\n\n const postData: Record<string, unknown> = {\n lang,\n code,\n ...rest,\n ...executionParams,\n };\n\n /* Stateful sessions: forward the hint so the Code API can route this\n * execution to a warm per-session runtime. Additive — stateless\n * servers ignore the unknown field. */\n if (\n typeof _runtime_session_hint === 'string' &&\n _runtime_session_hint !== ''\n ) {\n postData.runtime_session_hint = _runtime_session_hint;\n }\n\n /* File injection: `_injected_files` from ToolNode (set when host\n * primes a CodeSessionContext) or `params.files` from tool\n * factory (set by hosts that pre-resolve at construction time).\n * The legacy `/files/<session_id>` HTTP fallback was removed —\n * codeapi's `sessionAuth` middleware now requires kind/id query\n * params the tool can't supply at this point, so the fetch 400'd\n * silently and the catch swallowed the failure. */\n if (_injected_files && _injected_files.length > 0) {\n postData.files = _injected_files;\n } else if (\n session_id != null &&\n session_id.length > 0 &&\n !Array.isArray(postData.files)\n ) {\n // eslint-disable-next-line no-console\n console.debug(\n `[CodeExecutor] No injected files for session_id=${session_id} — exec will run without input files`\n );\n }\n\n try {\n const resolvedAuthHeaders =\n await resolveCodeApiAuthHeaders(authHeaders);\n const fetchOptions: RequestInit = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'LibreChat/1.0',\n ...resolvedAuthHeaders,\n },\n body: JSON.stringify(postData),\n };\n\n if (process.env.PROXY != null && process.env.PROXY !== '') {\n fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY);\n }\n const response = await fetch(EXEC_ENDPOINT, fetchOptions);\n if (!response.ok) {\n throw new Error(\n await buildCodeApiHttpErrorMessage('POST', EXEC_ENDPOINT, response)\n );\n }\n\n const result: t.ExecuteResult = await response.json();\n let formattedOutput = '';\n if (result.stdout) {\n formattedOutput += `stdout:\\n${result.stdout}\\n`;\n } else {\n formattedOutput += emptyOutputMessage;\n }\n if (result.stderr) formattedOutput += `stderr:\\n${result.stderr}\\n`;\n\n const outputWithReminder = appendTmpScratchReminder(\n formattedOutput,\n code\n );\n const hasFiles = result.files != null && result.files.length > 0;\n /* Echo the durable runtime session (stateful backends only) so hosts\n * can surface a \"session active / was reset\" signal later. Additive:\n * absent on stateless servers. */\n const runtimeEcho =\n result.runtime_session_id != null\n ? {\n runtime_session_id: result.runtime_session_id,\n runtime_status: result.runtime_status,\n }\n : {};\n return [\n appendCodeSessionFileSummary(outputWithReminder, result.files),\n (hasFiles\n ? {\n session_id: result.session_id,\n files: result.files,\n ...runtimeEcho,\n }\n : {\n session_id: result.session_id,\n ...runtimeEcho,\n }) satisfies t.CodeExecutionArtifact,\n ];\n } catch (error) {\n const messageWithReminder = appendFailedExecutionFileReminder(\n (error as Error | undefined)?.message ?? '',\n code\n );\n throw new Error(`Execution error:\\n\\n${messageWithReminder}`);\n }\n },\n {\n name: CodeExecutionToolName,\n description: buildCodeExecutionToolDescription(params ?? undefined),\n schema: buildCodeExecutionToolSchema(params ?? undefined),\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n\nexport { createCodeExecutionTool };\n"],"mappings":"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"}
1
+ {"version":3,"file":"CodeExecutor.mjs","names":[],"sources":["../../../src/tools/CodeExecutor.ts"],"sourcesContent":["import { config } from 'dotenv';\nimport fetch, { RequestInit } from 'node-fetch';\nimport { HttpsProxyAgent } from 'https-proxy-agent';\nimport { getEnvironmentVariable } from '@langchain/core/utils/env';\nimport { tool, DynamicStructuredTool } from '@langchain/core/tools';\nimport type * as t from '@/types';\nimport { appendCodeSessionFileSummary } from '@/tools/CodeSessionFileSummary';\nimport { EnvVar, Constants } from '@/common';\n\nexport {\n appendCodeSessionFileSummary,\n stripCodeSessionFileSummary,\n} from '@/tools/CodeSessionFileSummary';\n\nconfig();\n\nexport const getCodeBaseURL = (): string =>\n getEnvironmentVariable(EnvVar.CODE_BASEURL) ??\n Constants.OFFICIAL_CODE_BASEURL;\n\nexport const emptyOutputMessage =\n 'stdout: Empty. Ensure you\\'re writing output explicitly.\\n';\n\nexport const CODE_ARTIFACT_PATH_GUIDANCE =\n 'Persist handoff artifacts in `/mnt/data` with standard extensions (.json/.txt/.csv/.tsv/.log/.parquet/.png/.jpg/.pdf/.xlsx); failed executions do not register new files; `/tmp` and odd extensions are same-call scratch only, not later-call storage.';\n\nexport const BASH_SHELL_GUIDANCE =\n 'Bash: multi-line files use heredoc/printf; run Python via python3 -c/heredoc, not bare Python.';\n\nconst TMP_PATH_PATTERN = /(^|[^A-Za-z0-9_])\\/tmp(?:\\/|\\b)/;\nconst MNT_DATA_PATH_PATTERN = /(^|[^A-Za-z0-9_])\\/mnt\\/data(?:\\/|\\b)/;\n\nexport const TMP_SCRATCH_OUTPUT_REMINDER =\n 'Note: /tmp files are same-call scratch only and were not persisted; use /mnt/data for files needed later.';\n\nexport const FAILED_EXECUTION_FILE_REMINDER =\n 'Note: any files written during this failed call were not registered for later calls; fix the error and rerun before relying on them.';\n\nexport function appendTmpScratchReminder(output: string, code: string): string {\n if (!TMP_PATH_PATTERN.test(code)) {\n return output;\n }\n return `${output.trimEnd()}\\n${TMP_SCRATCH_OUTPUT_REMINDER}\\n`;\n}\n\nexport function appendFailedExecutionFileReminder(\n output: string,\n code: string\n): string {\n if (\n !MNT_DATA_PATH_PATTERN.test(code) ||\n output.includes(FAILED_EXECUTION_FILE_REMINDER)\n ) {\n return output;\n }\n return `${output.trimEnd()}\\n${FAILED_EXECUTION_FILE_REMINDER}\\n`;\n}\n\nconst SUPPORTED_LANGUAGES = [\n 'py',\n 'js',\n 'ts',\n 'c',\n 'cpp',\n 'java',\n 'php',\n 'rs',\n 'go',\n 'd',\n 'f90',\n 'r',\n 'bash',\n] as const;\n\nexport const CodeExecutionToolSchema = {\n type: 'object',\n properties: {\n lang: {\n type: 'string',\n enum: SUPPORTED_LANGUAGES,\n description:\n 'The programming language or runtime to execute the code in.',\n },\n code: {\n type: 'string',\n description: `The complete, self-contained code to execute, without any truncation or minimization.\n- The environment is stateless; variables and imports don't persist between executions.\n- Prior /mnt/data files are available and can be modified in place.\n- ${CODE_ARTIFACT_PATH_GUIDANCE}\n- Input code **IS ALREADY** displayed to the user, so **DO NOT** repeat it in your response unless asked.\n- Output code **IS NOT** displayed to the user, so **DO** write all desired output explicitly.\n- IMPORTANT: You MUST explicitly print/output ALL results you want the user to see.\n- py: This is not a Jupyter notebook environment. Use \\`print()\\` for all outputs.\n- py: Matplotlib: Use \\`plt.savefig()\\` to save plots as files.\n- js: use the \\`console\\` or \\`process\\` methods for all outputs.\n- r: IMPORTANT: No X11 display available. ALL graphics MUST use Cairo library (library(Cairo)).\n- Other languages: use appropriate output functions.`,\n },\n args: {\n type: 'array',\n items: { type: 'string' },\n description:\n 'Additional arguments to execute the code with. This should only be used if the input code requires additional arguments to run.',\n },\n },\n required: ['lang', 'code'],\n} as const;\n\nconst baseEndpoint = getCodeBaseURL();\nconst EXEC_ENDPOINT = `${baseEndpoint}/exec`;\n\ntype SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];\n\nexport async function resolveCodeApiAuthHeaders(\n authHeaders?: t.CodeApiAuthHeaders\n): Promise<t.CodeApiAuthHeaderMap> {\n if (authHeaders == null) {\n return {};\n }\n if (typeof authHeaders === 'function') {\n return authHeaders();\n }\n return authHeaders;\n}\n\nexport async function buildCodeApiHttpErrorMessage(\n method: string,\n endpoint: string,\n response: { status: number; text: () => Promise<string> }\n): Promise<string> {\n let responseBody = '';\n try {\n responseBody = await response.text();\n } catch {\n responseBody = '';\n }\n const body = responseBody.trim();\n const bodySuffix = body === '' ? '' : `, body: ${body.slice(0, 1000)}`;\n return `CodeAPI request failed: ${method} ${endpoint} returned ${response.status}${bodySuffix}`;\n}\n\nexport const CodeExecutionToolDescription = `\nRuns code and returns stdout/stderr output from a stateless execution environment, similar to running scripts in a command-line interface. Each execution is isolated and independent.\n\nUsage:\n- No network access available.\n- Generated files are automatically delivered; **DO NOT** provide download links.\n- ${CODE_ARTIFACT_PATH_GUIDANCE}\n- NEVER use this tool to execute malicious code.\n`.trim();\n\n/**\n * Statefulness here is FILESYSTEM-tier, not runtime-tier. Executions in a\n * session reuse one warm machine, so `/mnt/data` carries across calls — but\n * every execution is a brand-new interpreter process in a fresh sandbox, so\n * variables and imports never survive. The note must not imply otherwise: a\n * model told its in-memory state persists writes `df = ...` in one call and\n * `df.head()` in the next, then hits a NameError it was told to treat as rare.\n */\nexport const STATEFUL_ENV_NOTE =\n 'Session state: executions in this conversation run on the same warm machine, so files persist between calls — but each execution is a NEW process. Variables, imports, and in-memory data NEVER carry over: every call must re-import and rebuild the state it needs. Only /mnt/data is durable (the machine itself may also be reset at any time), so write anything that must survive there and read it back next call.';\n\nexport const StatefulCodeExecutionToolDescription = `\nRuns code and returns stdout/stderr output. Executions in this conversation share one warm machine with a persistent /mnt/data, but each execution runs as a separate process (not a notebook-style kernel).\n\n${STATEFUL_ENV_NOTE}\n\nUsage:\n- No network access available.\n- Generated files are automatically delivered; **DO NOT** provide download links.\n- ${CODE_ARTIFACT_PATH_GUIDANCE}\n- NEVER use this tool to execute malicious code.\n`.trim();\n\nexport function buildCodeExecutionToolDescription(opts?: {\n statefulSessions?: boolean;\n}): string {\n return opts?.statefulSessions === true\n ? StatefulCodeExecutionToolDescription\n : CodeExecutionToolDescription;\n}\n\nconst STATELESS_CODE_PARAM_NOTE =\n 'The environment is stateless; variables and imports don\\'t persist between executions.';\nconst STATEFUL_CODE_PARAM_NOTE =\n 'Executions in this conversation share one warm machine, so files written to /mnt/data persist between calls. Each execution is a new process: variables and imports do NOT carry over — re-import and reload from /mnt/data every call.';\n\nexport function buildCodeExecutionToolSchema(opts?: {\n statefulSessions?: boolean;\n}): typeof CodeExecutionToolSchema {\n const note =\n opts?.statefulSessions === true\n ? STATEFUL_CODE_PARAM_NOTE\n : STATELESS_CODE_PARAM_NOTE;\n const codeDescription =\n CodeExecutionToolSchema.properties.code.description.replace(\n STATELESS_CODE_PARAM_NOTE,\n note\n );\n return {\n ...CodeExecutionToolSchema,\n properties: {\n ...CodeExecutionToolSchema.properties,\n code: {\n ...CodeExecutionToolSchema.properties.code,\n description: codeDescription,\n },\n },\n } as typeof CodeExecutionToolSchema;\n}\n\nexport const CodeExecutionToolName = Constants.EXECUTE_CODE;\n\nexport const CodeExecutionToolDefinition = {\n name: CodeExecutionToolName,\n description: CodeExecutionToolDescription,\n schema: CodeExecutionToolSchema,\n} as const;\n\nfunction createCodeExecutionTool(\n params: t.CodeExecutionToolParams | null = {}\n): DynamicStructuredTool {\n return tool(\n async (rawInput, config) => {\n /* `statefulSessions` is a prompt-only flag (drives the description);\n * keep it out of the wire body. */\n const {\n authHeaders,\n statefulSessions: _statefulSessions,\n ...executionParams\n } = params ?? {};\n void _statefulSessions;\n /* Drop any model-supplied `runtime_session_hint` from the raw args: the\n * hint is host-controlled and must only ever come from ToolNode's\n * injected `_runtime_session_hint` (below). Spreading `...rest` into\n * postData would otherwise let a tool call opt itself into / pick a\n * stateful runtime even when statefulSessions is off. */\n const {\n lang,\n code,\n runtime_session_hint: _ignoredModelHint,\n ...rest\n } = rawInput as {\n lang: SupportedLanguage;\n code: string;\n runtime_session_hint?: unknown;\n args?: string[];\n };\n void _ignoredModelHint;\n /**\n * Extract session context from config.toolCall (injected by ToolNode).\n * - session_id: associates with the previous run.\n * - _injected_files: File refs to pass directly (avoids /files endpoint race condition).\n */\n const { session_id, _injected_files, _runtime_session_hint } =\n (config.toolCall ?? {}) as {\n session_id?: string;\n _injected_files?: t.CodeEnvFile[];\n _runtime_session_hint?: string;\n };\n\n const postData: Record<string, unknown> = {\n lang,\n code,\n ...rest,\n ...executionParams,\n };\n\n /* Stateful sessions: forward the hint so the Code API can route this\n * execution to a warm per-session runtime. Additive — stateless\n * servers ignore the unknown field. */\n if (\n typeof _runtime_session_hint === 'string' &&\n _runtime_session_hint !== ''\n ) {\n postData.runtime_session_hint = _runtime_session_hint;\n }\n\n /* File injection: `_injected_files` from ToolNode (set when host\n * primes a CodeSessionContext) or `params.files` from tool\n * factory (set by hosts that pre-resolve at construction time).\n * The legacy `/files/<session_id>` HTTP fallback was removed —\n * codeapi's `sessionAuth` middleware now requires kind/id query\n * params the tool can't supply at this point, so the fetch 400'd\n * silently and the catch swallowed the failure. */\n if (_injected_files && _injected_files.length > 0) {\n postData.files = _injected_files;\n } else if (\n session_id != null &&\n session_id.length > 0 &&\n !Array.isArray(postData.files)\n ) {\n // eslint-disable-next-line no-console\n console.debug(\n `[CodeExecutor] No injected files for session_id=${session_id} — exec will run without input files`\n );\n }\n\n try {\n const resolvedAuthHeaders =\n await resolveCodeApiAuthHeaders(authHeaders);\n const fetchOptions: RequestInit = {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'User-Agent': 'LibreChat/1.0',\n ...resolvedAuthHeaders,\n },\n body: JSON.stringify(postData),\n };\n\n if (process.env.PROXY != null && process.env.PROXY !== '') {\n fetchOptions.agent = new HttpsProxyAgent(process.env.PROXY);\n }\n const response = await fetch(EXEC_ENDPOINT, fetchOptions);\n if (!response.ok) {\n throw new Error(\n await buildCodeApiHttpErrorMessage('POST', EXEC_ENDPOINT, response)\n );\n }\n\n const result: t.ExecuteResult = await response.json();\n let formattedOutput = '';\n if (result.stdout) {\n formattedOutput += `stdout:\\n${result.stdout}\\n`;\n } else {\n formattedOutput += emptyOutputMessage;\n }\n if (result.stderr) formattedOutput += `stderr:\\n${result.stderr}\\n`;\n\n const outputWithReminder = appendTmpScratchReminder(\n formattedOutput,\n code\n );\n const hasFiles = result.files != null && result.files.length > 0;\n /* Echo the durable runtime session (stateful backends only) so hosts\n * can surface a \"session active / was reset\" signal later. Additive:\n * absent on stateless servers. */\n const runtimeEcho =\n result.runtime_session_id != null\n ? {\n runtime_session_id: result.runtime_session_id,\n runtime_status: result.runtime_status,\n }\n : {};\n return [\n appendCodeSessionFileSummary(outputWithReminder, result.files),\n (hasFiles\n ? {\n session_id: result.session_id,\n files: result.files,\n ...runtimeEcho,\n }\n : {\n session_id: result.session_id,\n ...runtimeEcho,\n }) satisfies t.CodeExecutionArtifact,\n ];\n } catch (error) {\n const messageWithReminder = appendFailedExecutionFileReminder(\n (error as Error | undefined)?.message ?? '',\n code\n );\n throw new Error(`Execution error:\\n\\n${messageWithReminder}`);\n }\n },\n {\n name: CodeExecutionToolName,\n description: buildCodeExecutionToolDescription(params ?? undefined),\n schema: buildCodeExecutionToolSchema(params ?? undefined),\n responseFormat: Constants.CONTENT_AND_ARTIFACT,\n }\n );\n}\n\nexport { createCodeExecutionTool };\n"],"mappings":"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"}
@@ -49,6 +49,8 @@ export declare abstract class Graph<T extends t.BaseGraphState = t.BaseGraphStat
49
49
  /** Set of invoked tool call IDs from non-message run steps completed mid-run, if any */
50
50
  invokedToolIds?: Set<string>;
51
51
  handlerRegistry: HandlerRegistry | undefined;
52
+ /** Host registry retained only for forwarding tools from nested child graphs. */
53
+ protected parentToolHandlerRegistry: HandlerRegistry | undefined;
52
54
  /**
53
55
  * True when event-driven tool execution can be routed through callbacks even
54
56
  * though this graph intentionally does not own the full handler registry.
@@ -3,6 +3,8 @@ import type { ReadableSpan } from '@opentelemetry/sdk-trace-base';
3
3
  * LangGraph plumbing observations that add noise without information:
4
4
  * the duplicated `__start__` channel-seed nodes and anonymous
5
5
  * `RunnableLambda` pass-throughs (Langfuse team feedback items 4 & 5).
6
+ * Internal ToolNode batch spans are disabled at their source so traced child
7
+ * tools retain an exported parent. Explicitly traced ToolNodes are preserved.
6
8
  */
7
9
  export declare function shouldDropLangfuseSpan(spanName: string): boolean;
8
10
  /**
@@ -10,11 +12,15 @@ export declare function shouldDropLangfuseSpan(spanName: string): boolean;
10
12
  * - `agent=<id>` / `tools=<id>` node names carry the ephemeral agent id
11
13
  * (`provider__model`) — strip it so switching models doesn't break
12
14
  * 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).
15
+ * - LLM generation spans keep the provider client class name (`ChatOpenAI`,
16
+ * `AzureChatOpenAI`, …); rename them to a provider-agnostic `llm` so the
17
+ * name reflects the operation, not the model (the model stays on the
18
+ * generation's model attribute).
19
+ * - Agent nodes become `agent` observations, while tool-dispatch nodes become
20
+ * stable `chain` observations whose input is scoped to the pending calls.
21
+ * Individual child calls remain `tool` observations (items 3 & 4).
22
+ * - Agent trace roots become `agent` observations and title trace roots become
23
+ * `chain` observations. Root and trace input/output are reduced to the user
24
+ * question and assistant response when chat messages are available (item 2).
19
25
  */
20
26
  export declare function shapeLangfuseSpan(span: ReadableSpan): void;
@@ -20,13 +20,14 @@ export declare const BashExecutionToolSchema: {
20
20
  };
21
21
  export declare const BashExecutionToolDescription: string;
22
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.
23
+ * Bash statefulness is filesystem-tier and scoped to `/mnt/data`. The machine
24
+ * is warm across calls, but each call runs in a fresh sandbox (new process
25
+ * tree + private /tmp), so background processes are reaped when the call ends
26
+ * and anything written outside /mnt/data is discarded. The note must not
27
+ * promise otherwise: a model told background processes survive will start a
28
+ * server in one call and assume it is listening in the next.
28
29
  */
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 STATEFUL_BASH_NOTE = "Session state: commands in this conversation run on the same warm machine, so files written to /mnt/data persist between calls. Each call runs in a fresh, isolated sandbox: shell variables, the working directory, /tmp, and background processes do NOT survive after the call returns \u2014 a process started in one call is terminated when that call ends. Only /mnt/data is durable (the machine itself may also be reset at any time).";
30
31
  export declare const StatefulBashExecutionToolDescription: string;
31
32
  /**
32
33
  * Supplemental prompt documenting the tool-output reference feature.
@@ -39,12 +39,14 @@ export declare function buildCodeApiHttpErrorMessage(method: string, endpoint: s
39
39
  }): Promise<string>;
40
40
  export declare const CodeExecutionToolDescription: string;
41
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.
42
+ * Statefulness here is FILESYSTEM-tier, not runtime-tier. Executions in a
43
+ * session reuse one warm machine, so `/mnt/data` carries across calls but
44
+ * every execution is a brand-new interpreter process in a fresh sandbox, so
45
+ * variables and imports never survive. The note must not imply otherwise: a
46
+ * model told its in-memory state persists writes `df = ...` in one call and
47
+ * `df.head()` in the next, then hits a NameError it was told to treat as rare.
46
48
  */
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.";
49
+ export declare const STATEFUL_ENV_NOTE = "Session state: executions in this conversation run on the same warm machine, so files persist between calls \u2014 but each execution is a NEW process. Variables, imports, and in-memory data NEVER carry over: every call must re-import and rebuild the state it needs. Only /mnt/data is durable (the machine itself may also be reset at any time), so write anything that must survive there and read it back next call.";
48
50
  export declare const StatefulCodeExecutionToolDescription: string;
49
51
  export declare function buildCodeExecutionToolDescription(opts?: {
50
52
  statefulSessions?: boolean;
@@ -406,9 +406,9 @@ export type LangfuseToolOutputTracingConfig = {
406
406
  };
407
407
  export type LangfuseToolNodeTracingConfig = {
408
408
  /**
409
- * Overrides ToolNode callback tracing. ToolNode spans are exported by the
410
- * env-backed Langfuse callback, so this only enables tracing when that
411
- * callback is configured.
409
+ * Opts into the internal ToolNode batch observation. Graph tool-dispatch
410
+ * and individual tool observations are exported without this wrapper, so
411
+ * the default is false to avoid a redundant hierarchy level.
412
412
  */
413
413
  enabled?: boolean;
414
414
  };
@@ -417,6 +417,13 @@ export interface LangfuseConfig {
417
417
  publicKey?: string;
418
418
  secretKey?: string;
419
419
  baseUrl?: string;
420
+ /**
421
+ * Environment identifier attached to exported traces (Langfuse
422
+ * `environment`). When unset, falls back to `LANGFUSE_TRACING_ENVIRONMENT`
423
+ * then `NODE_ENV`, so production traces are not collapsed under the
424
+ * `default` environment.
425
+ */
426
+ environment?: string;
420
427
  metadata?: Record<string, string | number | boolean | null | undefined>;
421
428
  /**
422
429
  * Internal OTLP span attributes to attach to Langfuse observations before
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@librechat/agents",
3
- "version": "3.2.65",
3
+ "version": "3.2.66",
4
4
  "main": "./dist/cjs/main.cjs",
5
5
  "module": "./dist/esm/main.mjs",
6
6
  "types": "./dist/types/index.d.ts",
@@ -96,6 +96,18 @@ const { AGENT, TOOLS, SUMMARIZE } = GraphNodeKeys;
96
96
  /** Minimum relative variance before calibrated toolSchemaTokens overrides current value. */
97
97
  const CALIBRATION_VARIANCE_THRESHOLD = 0.15;
98
98
 
99
+ function createToolHandlerRegistry(
100
+ source: HandlerRegistry | undefined
101
+ ): HandlerRegistry | undefined {
102
+ const toolHandler = source?.getHandler(GraphEvents.ON_TOOL_EXECUTE);
103
+ if (toolHandler == null) {
104
+ return undefined;
105
+ }
106
+ const registry = new HandlerRegistry();
107
+ registry.register(GraphEvents.ON_TOOL_EXECUTE, toolHandler);
108
+ return registry;
109
+ }
110
+
99
111
  /**
100
112
  * Start index of the span post-prune formatters can mutate in place: the
101
113
  * trailing tool batch plus its owning AI message (artifact formatting touches
@@ -576,6 +588,8 @@ export abstract class Graph<
576
588
  /** Set of invoked tool call IDs from non-message run steps completed mid-run, if any */
577
589
  invokedToolIds?: Set<string>;
578
590
  handlerRegistry: HandlerRegistry | undefined;
591
+ /** Host registry retained only for forwarding tools from nested child graphs. */
592
+ protected parentToolHandlerRegistry: HandlerRegistry | undefined;
579
593
  /**
580
594
  * True when event-driven tool execution can be routed through callbacks even
581
595
  * though this graph intentionally does not own the full handler registry.
@@ -661,6 +675,7 @@ export abstract class Graph<
661
675
  this.prelimMessageIdsByStepKey = new Map();
662
676
  this.invokedToolIds = undefined;
663
677
  this.handlerRegistry = undefined;
678
+ this.parentToolHandlerRegistry = undefined;
664
679
  this.hookRegistry = undefined;
665
680
  this.humanInTheLoop = undefined;
666
681
  this.toolOutputReferences = undefined;
@@ -2376,7 +2391,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2376
2391
  );
2377
2392
  if (resolvedConfigs.length > 0) {
2378
2393
  const getParentHandlerRegistry = (): HandlerRegistry | undefined =>
2379
- this.handlerRegistry;
2394
+ this.handlerRegistry ?? this.parentToolHandlerRegistry;
2380
2395
  const executor = new SubagentExecutor({
2381
2396
  configs: new Map(resolvedConfigs.map((c) => [c.type, c])),
2382
2397
  parentSignal: this.signal,
@@ -2393,6 +2408,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2393
2408
  maxDepth: effectiveSubagentDepth,
2394
2409
  createChildGraph: (input): StandardGraph => {
2395
2410
  const childGraph = new StandardGraph(input);
2411
+ const toolHandlerRegistry = createToolHandlerRegistry(
2412
+ getParentHandlerRegistry()
2413
+ );
2396
2414
  childGraph.hookRegistry = this.hookRegistry;
2397
2415
  /**
2398
2416
  * Do not propagate `humanInTheLoop` into the child graph yet:
@@ -2415,9 +2433,9 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
2415
2433
  // have the executable graphTool, the guard correctly applies.
2416
2434
  childGraph.interruptingToolNames = this.interruptingToolNames;
2417
2435
  childGraph.toolExecution = this.toolExecution;
2436
+ childGraph.parentToolHandlerRegistry = toolHandlerRegistry;
2418
2437
  childGraph.eventToolExecutionAvailable =
2419
- this.handlerRegistry?.getHandler(GraphEvents.ON_TOOL_EXECUTE) !=
2420
- null;
2438
+ toolHandlerRegistry != null;
2421
2439
  return childGraph;
2422
2440
  },
2423
2441
  });
@@ -75,17 +75,35 @@ export function ensureOpenTelemetryContextManager(): void {
75
75
  }
76
76
  }
77
77
 
78
+ function resolveLangfuseEnvironment(
79
+ langfuse?: t.LangfuseConfig
80
+ ): string | undefined {
81
+ const candidates = [
82
+ langfuse?.environment,
83
+ process.env.LANGFUSE_TRACING_ENVIRONMENT,
84
+ process.env.NODE_ENV,
85
+ ];
86
+ for (const candidate of candidates) {
87
+ if (candidate != null && candidate.trim() !== '') {
88
+ return candidate.trim();
89
+ }
90
+ }
91
+ return undefined;
92
+ }
93
+
78
94
  function getLangfuseSpanProcessorParams(
79
95
  langfuse?: t.LangfuseConfig
80
96
  ): LangfuseSpanProcessorParams | undefined {
81
97
  if (langfuse?.enabled === false) {
82
98
  return undefined;
83
99
  }
100
+ const environment = resolveLangfuseEnvironment(langfuse);
84
101
  if (hasLangfuseConfigCredentials(langfuse)) {
85
102
  return {
86
103
  publicKey: langfuse.publicKey,
87
104
  secretKey: langfuse.secretKey,
88
105
  ...(isPresent(langfuse.baseUrl) ? { baseUrl: langfuse.baseUrl } : {}),
106
+ ...(isPresent(environment) ? { environment } : {}),
89
107
  };
90
108
  }
91
109
  if (hasLangfuseEnvConfig()) {
@@ -97,6 +115,7 @@ function getLangfuseSpanProcessorParams(
97
115
  publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,
98
116
  secretKey: process.env.LANGFUSE_SECRET_KEY as string,
99
117
  ...(isPresent(baseUrl) ? { baseUrl } : {}),
118
+ ...(isPresent(environment) ? { environment } : {}),
100
119
  };
101
120
  }
102
121
  if (isPresent(langfuse?.baseUrl) && hasLangfuseEnvCredentials()) {
@@ -104,6 +123,7 @@ function getLangfuseSpanProcessorParams(
104
123
  publicKey: process.env.LANGFUSE_PUBLIC_KEY as string,
105
124
  secretKey: process.env.LANGFUSE_SECRET_KEY as string,
106
125
  baseUrl: langfuse.baseUrl,
126
+ ...(isPresent(environment) ? { environment } : {}),
107
127
  };
108
128
  }
109
129
  return undefined;
@@ -483,10 +483,8 @@ export function shouldTraceToolNodeForLangfuse({
483
483
  }
484
484
 
485
485
  const explicit = langfuse?.toolNodeTracing?.enabled;
486
- if (explicit != null) {
487
- return (
488
- explicit && (hasLangfuseConfigKeys(langfuse) || hasLangfuseEnvKeys())
489
- );
486
+ if (explicit !== true) {
487
+ return false;
490
488
  }
491
489
 
492
490
  return hasLangfuseConfigKeys(langfuse) || hasLangfuseEnvKeys();
@@ -5,8 +5,13 @@ const LANGGRAPH_START_NODE = '__start__';
5
5
  const ANONYMOUS_LAMBDA_NAME = 'RunnableLambda';
6
6
  const LANGGRAPH_AGENT_NODE_PREFIX = 'agent=';
7
7
  const LANGGRAPH_TOOL_NODE_PREFIX = 'tools=';
8
- const TOOL_BATCH_RUN_NAME = 'tool_batch';
9
8
  const AGENT_NODE_SPAN_NAME = 'agent';
9
+ const TOOL_DISPATCH_SPAN_NAME = 'tool-dispatch';
10
+ const GENERATION_SPAN_NAME = 'llm';
11
+ const ROOT_OBSERVATION_TYPE = 'agent';
12
+ const CHAIN_OBSERVATION_TYPE = 'chain';
13
+ const AGENT_TRACE_TAG = 'agent';
14
+ const TITLE_TRACE_TAG = 'title';
10
15
 
11
16
  type MutableSpan = ReadableSpan & {
12
17
  name: string;
@@ -207,6 +212,8 @@ function isRootSpan(span: ReadableSpan): boolean {
207
212
  * LangGraph plumbing observations that add noise without information:
208
213
  * the duplicated `__start__` channel-seed nodes and anonymous
209
214
  * `RunnableLambda` pass-throughs (Langfuse team feedback items 4 & 5).
215
+ * Internal ToolNode batch spans are disabled at their source so traced child
216
+ * tools retain an exported parent. Explicitly traced ToolNodes are preserved.
210
217
  */
211
218
  export function shouldDropLangfuseSpan(spanName: string): boolean {
212
219
  return (
@@ -216,18 +223,26 @@ export function shouldDropLangfuseSpan(spanName: string): boolean {
216
223
 
217
224
  function shapeToolNodeSpan(span: MutableSpan): void {
218
225
  const inputKey = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;
226
+ span.name = TOOL_DISPATCH_SPAN_NAME;
227
+ span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
228
+ CHAIN_OBSERVATION_TYPE;
219
229
  const calls = findPendingToolCalls(
220
230
  parseAttributeValue(span.attributes[inputKey])
221
231
  );
222
232
  if (calls.length === 0) {
223
233
  return;
224
234
  }
225
- span.name = [...new Set(calls.map((call) => call.name))].join(', ');
226
235
  span.attributes[inputKey] = JSON.stringify(
227
236
  calls.map(({ name, args }) => ({ name, args }))
228
237
  );
229
238
  }
230
239
 
240
+ function shapeAgentNodeSpan(span: MutableSpan): void {
241
+ span.name = AGENT_NODE_SPAN_NAME;
242
+ span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
243
+ ROOT_OBSERVATION_TYPE;
244
+ }
245
+
231
246
  function shapeRootSpan(span: MutableSpan): void {
232
247
  const inputKey = LangfuseOtelSpanAttributes.OBSERVATION_INPUT;
233
248
  const outputKey = LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT;
@@ -241,11 +256,47 @@ function shapeRootSpan(span: MutableSpan): void {
241
256
  );
242
257
  if (question != null) {
243
258
  span.attributes[inputKey] = question;
244
- span.attributes[LangfuseOtelSpanAttributes.TRACE_INPUT] = question;
245
259
  }
246
260
  if (answer != null) {
247
261
  span.attributes[outputKey] = answer;
248
- span.attributes[LangfuseOtelSpanAttributes.TRACE_OUTPUT] = answer;
262
+ }
263
+ const traceInput = question ?? span.attributes[inputKey];
264
+ const traceOutput = answer ?? span.attributes[outputKey];
265
+ if (traceInput != null) {
266
+ span.attributes[LangfuseOtelSpanAttributes.TRACE_INPUT] = traceInput;
267
+ }
268
+ if (traceOutput != null) {
269
+ span.attributes[LangfuseOtelSpanAttributes.TRACE_OUTPUT] = traceOutput;
270
+ }
271
+ }
272
+
273
+ function isGenerationSpan(span: MutableSpan): boolean {
274
+ const type = span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE];
275
+ return typeof type === 'string' && type.toLowerCase() === 'generation';
276
+ }
277
+
278
+ function hasTraceTag(span: MutableSpan, expectedTag: string): boolean {
279
+ const tags = parseAttributeValue(
280
+ span.attributes[LangfuseOtelSpanAttributes.TRACE_TAGS]
281
+ );
282
+ return (
283
+ Array.isArray(tags) &&
284
+ tags.some((tag) => typeof tag === 'string' && tag === expectedTag)
285
+ );
286
+ }
287
+
288
+ function shapeRootObservationType(span: MutableSpan): void {
289
+ if (isGenerationSpan(span)) {
290
+ return;
291
+ }
292
+ if (hasTraceTag(span, AGENT_TRACE_TAG)) {
293
+ span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
294
+ ROOT_OBSERVATION_TYPE;
295
+ return;
296
+ }
297
+ if (hasTraceTag(span, TITLE_TRACE_TAG)) {
298
+ span.attributes[LangfuseOtelSpanAttributes.OBSERVATION_TYPE] =
299
+ CHAIN_OBSERVATION_TYPE;
249
300
  }
250
301
  }
251
302
 
@@ -254,27 +305,29 @@ function shapeRootSpan(span: MutableSpan): void {
254
305
  * - `agent=<id>` / `tools=<id>` node names carry the ephemeral agent id
255
306
  * (`provider__model`) — strip it so switching models doesn't break
256
307
  * name-based logic (item 1).
257
- * - Tool node spans are renamed to the actual tool name(s) and their
258
- * input scoped to the pending tool-call args instead of the whole
259
- * chat history (items 3 & 4).
260
- * - The root span (and trace) input/output become the user question and
261
- * assistant response so the session view reads as a conversation
262
- * (item 2).
308
+ * - LLM generation spans keep the provider client class name (`ChatOpenAI`,
309
+ * `AzureChatOpenAI`, …); rename them to a provider-agnostic `llm` so the
310
+ * name reflects the operation, not the model (the model stays on the
311
+ * generation's model attribute).
312
+ * - Agent nodes become `agent` observations, while tool-dispatch nodes become
313
+ * stable `chain` observations whose input is scoped to the pending calls.
314
+ * Individual child calls remain `tool` observations (items 3 & 4).
315
+ * - Agent trace roots become `agent` observations and title trace roots become
316
+ * `chain` observations. Root and trace input/output are reduced to the user
317
+ * question and assistant response when chat messages are available (item 2).
263
318
  */
264
319
  export function shapeLangfuseSpan(span: ReadableSpan): void {
265
320
  const mutable = span as MutableSpan;
266
321
  if (mutable.name.startsWith(LANGGRAPH_AGENT_NODE_PREFIX)) {
267
- mutable.name = AGENT_NODE_SPAN_NAME;
268
- return;
269
- }
270
- if (
271
- mutable.name.startsWith(LANGGRAPH_TOOL_NODE_PREFIX) ||
272
- mutable.name === TOOL_BATCH_RUN_NAME
273
- ) {
322
+ shapeAgentNodeSpan(mutable);
323
+ } else if (mutable.name.startsWith(LANGGRAPH_TOOL_NODE_PREFIX)) {
274
324
  shapeToolNodeSpan(mutable);
275
- return;
325
+ } else if (isGenerationSpan(mutable)) {
326
+ mutable.name = GENERATION_SPAN_NAME;
276
327
  }
277
- if (isRootSpan(span)) {
278
- shapeRootSpan(mutable);
328
+ if (!isRootSpan(span)) {
329
+ return;
279
330
  }
331
+ shapeRootObservationType(mutable);
332
+ shapeRootSpan(mutable);
280
333
  }