diff --git a/api/package.json b/api/package.json index 7b13158680..7692c61515 100644 --- a/api/package.json +++ b/api/package.json @@ -44,7 +44,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.70", + "@librechat/agents": "^3.1.71", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", diff --git a/package-lock.json b/package-lock.json index 899c2cfde9..81a3085516 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59,7 +59,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.70", + "@librechat/agents": "^3.1.71", "@librechat/api": "*", "@librechat/data-schemas": "*", "@microsoft/microsoft-graph-client": "^3.0.7", @@ -11894,9 +11894,9 @@ } }, "node_modules/@librechat/agents": { - "version": "3.1.70", - "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.70.tgz", - "integrity": "sha512-+eZbU4hmPDHqo+1as+BLEEKFA3HcZkhTowSngJ5YwVi4v1sn9OyHhkxNz/svTKDZLHYzU8ONGPf1fHqpvWVw5A==", + "version": "3.1.71", + "resolved": "https://registry.npmjs.org/@librechat/agents/-/agents-3.1.71.tgz", + "integrity": "sha512-xAZQDlEfDJhPluBMugRoe6pZFUgfm+DIDIMCKKEg95DRcXiw8VNggkHfne/1Wf3xoA2l2Qh9RcSu/eARq55CEg==", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.73.0", @@ -44232,7 +44232,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.70", + "@librechat/agents": "^3.1.71", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.29.0", "@smithy/node-http-handler": "^4.4.5", diff --git a/packages/api/package.json b/packages/api/package.json index 8b26747483..92414ae51d 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -95,7 +95,7 @@ "@google/genai": "^1.19.0", "@keyv/redis": "^4.3.3", "@langchain/core": "^0.3.80", - "@librechat/agents": "^3.1.70", + "@librechat/agents": "^3.1.71", "@librechat/data-schemas": "*", "@modelcontextprotocol/sdk": "^1.29.0", "@smithy/node-http-handler": "^4.4.5", diff --git a/packages/api/src/agents/__tests__/initialize.test.ts b/packages/api/src/agents/__tests__/initialize.test.ts index 8ed8218f68..8ff07f64a0 100644 --- a/packages/api/src/agents/__tests__/initialize.test.ts +++ b/packages/api/src/agents/__tests__/initialize.test.ts @@ -1,3 +1,18 @@ +/** + * Stub `buildBashExecutionToolDescription` since the installed SDK version + * may pre-date the export. Kept as a partial mock so `Providers` and the + * rest of the namespace come through `jest.requireActual`. + */ +jest.mock('@librechat/agents', () => ({ + ...jest.requireActual('@librechat/agents'), + buildBashExecutionToolDescription: ({ + enableToolOutputReferences, + }: { + enableToolOutputReferences?: boolean; + } = {}): string => + enableToolOutputReferences === true ? 'bash {{toolturn}}' : 'bash', +})); + import { Providers } from '@librechat/agents'; import { EModelEndpoint } from 'librechat-data-provider'; import type { Agent } from 'librechat-data-provider'; diff --git a/packages/api/src/agents/__tests__/run-summarization.test.ts b/packages/api/src/agents/__tests__/run-summarization.test.ts index 4164d5ecf4..bae8ba9692 100644 --- a/packages/api/src/agents/__tests__/run-summarization.test.ts +++ b/packages/api/src/agents/__tests__/run-summarization.test.ts @@ -920,3 +920,155 @@ describe('subagentConfigs', () => { expect(childInputs.discoveredTools).toBeUndefined(); }); }); + +// --------------------------------------------------------------------------- +// Suite: toolOutputReferences gating +// --------------------------------------------------------------------------- +describe('toolOutputReferences gating', () => { + /** + * Captures the top-level `Run.create` config (not just agentInputs) so the + * test can assert presence/absence of the `toolOutputReferences` key. + */ + async function callAndCaptureRunConfig( + overrides?: Record, + ): Promise> { + const agents = [makeAgent(overrides)]; + const signal = new AbortController().signal; + + await createRun({ + agents: agents as never, + signal, + streaming: true, + streamUsage: true, + }); + + const createMock = Run.create as jest.Mock; + expect(createMock).toHaveBeenCalledTimes(1); + return createMock.mock.calls[0][0] as Record; + } + + it('passes toolOutputReferences when agent has codeEnvAvailable=true', async () => { + const callArgs = await callAndCaptureRunConfig({ codeEnvAvailable: true }); + expect(callArgs.toolOutputReferences).toEqual({ enabled: true }); + }); + + it('omits toolOutputReferences when codeEnvAvailable is false', async () => { + const callArgs = await callAndCaptureRunConfig({ codeEnvAvailable: false }); + expect(callArgs).not.toHaveProperty('toolOutputReferences'); + }); + + it('omits toolOutputReferences when codeEnvAvailable is unset', async () => { + const callArgs = await callAndCaptureRunConfig(); + expect(callArgs).not.toHaveProperty('toolOutputReferences'); + }); + + it('enables toolOutputReferences if any agent in a multi-agent run has codeEnvAvailable=true', async () => { + const signal = new AbortController().signal; + await createRun({ + agents: [ + makeAgent({ id: 'agent_a', codeEnvAvailable: false }), + makeAgent({ id: 'agent_b', codeEnvAvailable: true }), + ] as never, + signal, + streaming: true, + streamUsage: true, + }); + + const createMock = Run.create as jest.Mock; + const callArgs = createMock.mock.calls[0][0] as Record; + expect(callArgs.toolOutputReferences).toEqual({ enabled: true }); + }); + + it('enables toolOutputReferences when only a subagent has codeEnvAvailable=true', async () => { + /** + * Real scenario: a parent agent without `execute_code` spawns a + * subagent that does have it. The SDK's shared tool-output + * reference registry serves every ToolNode in the run, so the + * subagent's `bash_tool` benefits from the run-level flag — and + * without this gate looking at `subagentAgentConfigs`, the + * subagent's `{{toolturn}}` placeholders would pass + * through unsubstituted. + */ + const signal = new AbortController().signal; + const subagent = makeAgent({ id: 'agent_child', codeEnvAvailable: true }); + await createRun({ + agents: [ + makeAgent({ + id: 'agent_parent', + codeEnvAvailable: false, + subagents: { enabled: true, allowSelf: false, agent_ids: ['agent_child'] }, + subagentAgentConfigs: [subagent], + }), + ] as never, + signal, + streaming: true, + streamUsage: true, + }); + + const createMock = Run.create as jest.Mock; + const callArgs = createMock.mock.calls[0][0] as Record; + expect(callArgs.toolOutputReferences).toEqual({ enabled: true }); + }); + + it('enables toolOutputReferences when a transitively-nested subagent has codeEnvAvailable=true', async () => { + /** + * Multi-level delegation (parent → child → grandchild): only the + * grandchild has `codeEnvAvailable`. Verifies the recursion + * descends past one level of `subagentAgentConfigs`. + */ + const signal = new AbortController().signal; + const grandchild = makeAgent({ id: 'agent_grandchild', codeEnvAvailable: true }); + const child = makeAgent({ + id: 'agent_child', + codeEnvAvailable: false, + subagents: { enabled: true, allowSelf: false, agent_ids: ['agent_grandchild'] }, + subagentAgentConfigs: [grandchild], + }); + await createRun({ + agents: [ + makeAgent({ + id: 'agent_parent', + codeEnvAvailable: false, + subagents: { enabled: true, allowSelf: false, agent_ids: ['agent_child'] }, + subagentAgentConfigs: [child], + }), + ] as never, + signal, + streaming: true, + streamUsage: true, + }); + + const createMock = Run.create as jest.Mock; + const callArgs = createMock.mock.calls[0][0] as Record; + expect(callArgs.toolOutputReferences).toEqual({ enabled: true }); + }); + + it('terminates and omits toolOutputReferences for a cyclic agent tree with no codeenv', async () => { + /** + * Cycle safety: `A → B → A`, neither has `codeEnvAvailable`. The + * `visited` set in `anyAgentHasCodeEnv` must short-circuit the + * recursion — without it this would stack-overflow before + * `Run.create` is reached. Mirrors the cycle-safety pattern + * `buildSubagentConfigs` already uses elsewhere in this module. + */ + const signal = new AbortController().signal; + type CyclicAgent = ReturnType & { + subagentAgentConfigs?: ReturnType[]; + }; + const a = makeAgent({ id: 'agent_a', codeEnvAvailable: false }) as CyclicAgent; + const b = makeAgent({ id: 'agent_b', codeEnvAvailable: false }) as CyclicAgent; + a.subagentAgentConfigs = [b]; + b.subagentAgentConfigs = [a]; + + await createRun({ + agents: [a] as never, + signal, + streaming: true, + streamUsage: true, + }); + + const createMock = Run.create as jest.Mock; + const callArgs = createMock.mock.calls[0][0] as Record; + expect(callArgs).not.toHaveProperty('toolOutputReferences'); + }); +}); diff --git a/packages/api/src/agents/__tests__/skills.test.ts b/packages/api/src/agents/__tests__/skills.test.ts index 08b9c08bad..72101ecad4 100644 --- a/packages/api/src/agents/__tests__/skills.test.ts +++ b/packages/api/src/agents/__tests__/skills.test.ts @@ -24,6 +24,12 @@ jest.mock('@librechat/agents', () => ({ description: 'bash', schema: {}, }, + buildBashExecutionToolDescription: ({ + enableToolOutputReferences, + }: { + enableToolOutputReferences?: boolean; + } = {}): string => + enableToolOutputReferences === true ? 'bash {{toolturn}}' : 'bash', })); import { Types } from 'mongoose'; @@ -869,6 +875,26 @@ describe('injectSkillCatalog', () => { expect(definedNames).not.toContain('skill'); }); + it('registers bash_tool with the tool-output reference syntax guide when codeEnvAvailable', async () => { + /** + * Symmetry check with `initializeAgent`'s call: `injectSkillCatalog` + * must forward `enableToolOutputReferences` so that if call order + * ever flips (skills-first), the resulting `bash_tool` description + * still contains the `{{toolturn}}` syntax guide. Using + * the registry mock's stub of `buildBashExecutionToolDescription` + * (defined at the top of this file), the guide collapses to a + * `{{toolturn}}` literal substring when the flag is on + * and is absent when off. + */ + const owned = makeSkill('owned-skill', userObjectId); + const listSkillsByAccess = buildPager([[owned]]); + const result = await injectSkillCatalog( + baseParams({ listSkillsByAccess, codeEnvAvailable: true }), + ); + const bashDef = (result.toolDefinitions ?? []).find((d) => d.name === 'bash_tool'); + expect(bashDef?.description).toContain('{{toolturn}}'); + }); + it('does NOT register bash_tool when codeEnvAvailable is false (skills-only agent)', async () => { /* Narrowing regression: `initializeAgent` now passes the per-agent effective flag (admin cap AND `agent.tools.includes('execute_code')`). diff --git a/packages/api/src/agents/initialize.ts b/packages/api/src/agents/initialize.ts index 2f9f1bdf9f..3c8086b3a2 100644 --- a/packages/api/src/agents/initialize.ts +++ b/packages/api/src/agents/initialize.ts @@ -742,6 +742,7 @@ export async function initializeAgent( toolRegistry, toolDefinitions, includeBash: true, + enableToolOutputReferences: effectiveCodeEnvAvailable, }); toolDefinitions = codeExecResult.toolDefinitions; } else if (agentRequestsCodeExec) { diff --git a/packages/api/src/agents/run.ts b/packages/api/src/agents/run.ts index d86b15a470..8397306ede 100644 --- a/packages/api/src/agents/run.ts +++ b/packages/api/src/agents/run.ts @@ -239,6 +239,14 @@ type RunAgent = Omit & { toolDefinitions?: LCTool[]; /** Precomputed flag indicating if any tools have defer_loading enabled */ hasDeferredTools?: boolean; + /** + * Per-agent codeenv gate set by `initializeAgent`: admin-level + * `execute_code` capability AND the agent actually requested + * `execute_code` in its tools. Used here to enable + * `RunConfig.toolOutputReferences` only on runs where the bash tool + * is actually registered. + */ + codeEnvAvailable?: boolean; /** Optional per-agent summarization overrides */ summarization?: SummarizationConfig; /** @@ -502,6 +510,43 @@ function computeEffectiveMaxContextTokens( /** Identifier for the self-spawn subagent (reuses parent's AgentInputs in an isolated child graph). */ const SELF_SUBAGENT_TYPE = 'self'; +/** + * Recursive any-true check across the agent tree: returns `true` if this + * agent or any subagent (transitively) has the per-agent codeenv gate + * enabled. + * + * The SDK's tool-output reference registry is shared across every + * `ToolNode` compiled from the run's graph (parent + every subagent + * alike), so a single subagent with `bash_tool` registered is enough to + * make `RunConfig.toolOutputReferences` worth activating for the whole + * run — without it, the subagent's `{{toolturn}}` + * placeholders would pass through to the shell unsubstituted. + * + * Cycle-safe via a `visited` set, mirroring `buildSubagentConfigs`'s + * `ancestors` pattern. The bash tool description itself is still gated + * per-agent in `initializeAgent`, so only agents that actually have + * bash registered learn the `{{…}}` syntax — broadening the run-level + * registry gate doesn't broaden the model-facing surface. + */ +function anyAgentHasCodeEnv(agents: RunAgent[], visited: Set = new Set()): boolean { + for (const agent of agents) { + if (visited.has(agent.id)) { + continue; + } + visited.add(agent.id); + if (agent.codeEnvAvailable === true) { + return true; + } + if ( + agent.subagentAgentConfigs != null && + anyAgentHasCodeEnv(agent.subagentAgentConfigs, visited) + ) { + return true; + } + } + return false; +} + /** * Builds SubagentConfig entries for an agent: optional self-spawn plus any * explicit child agents loaded in `agent.subagentAgentConfigs`. Returns an empty @@ -818,6 +863,25 @@ export async function createRun({ (graphConfig as StandardGraphConfig).type = 'standard'; } + /** + * Enable tool-output references when the bash tool is actually + * present anywhere in this run — top-level agent OR any subagent + * (transitively). `codeEnvAvailable` on each `RunAgent` is the + * per-agent gate (admin `execute_code` capability AND the agent's + * own `tools` listing `execute_code`), so the feature follows the + * same activation as the bash-tool registration in + * `initializeAgent`. The walk into `subagentAgentConfigs` is + * load-bearing: a parent without `execute_code` can spawn a + * subagent that has it, and the SDK's shared registry serves + * every `ToolNode` compiled from this run's graph — so missing + * subagents in this gate would leave the child's + * `{{toolturn}}` placeholders unsubstituted. SDK + * defaults (~400 KB per output, 5 MB total) keep substituted + * payloads inside typical shell ARG_MAX limits, so no overrides + * are needed for the experimental rollout. + */ + const enableToolOutputReferences = anyAgentHasCodeEnv(agents); + return Run.create({ runId, graphConfig, @@ -826,5 +890,8 @@ export async function createRun({ indexTokenCountMap, initialSessions, calibrationRatio, + ...(enableToolOutputReferences && { + toolOutputReferences: { enabled: true }, + }), }); } diff --git a/packages/api/src/agents/skills.ts b/packages/api/src/agents/skills.ts index 2a0b88c1a1..42d29aa8f9 100644 --- a/packages/api/src/agents/skills.ts +++ b/packages/api/src/agents/skills.ts @@ -418,10 +418,23 @@ export async function injectSkillCatalog( toolRegistry?.set(skillToolDef.name, skillToolDef); } + /** + * Forward `enableToolOutputReferences` to keep the skills caller + * symmetric with `initializeAgent`'s call. Today `initializeAgent` + * registers `bash_tool` first and the registry `.has()` check makes + * this call a no-op — but if call order ever flips (skills-first), + * a missing flag here would silently produce a `bash_tool` + * description without the `{{toolturn}}` guide, and the + * `initializeAgent` pass would become the no-op. Mirror the gate + * `initializeAgent` uses (`effectiveCodeEnvAvailable`, which here + * is `codeEnvAvailable === true`) so both paths produce identical + * tool definitions regardless of which fires first. + */ const codeExecResult = registerCodeExecutionTools({ toolRegistry, toolDefinitions: workingDefs, includeBash: codeEnvAvailable === true, + enableToolOutputReferences: codeEnvAvailable === true, }); workingDefs = codeExecResult.toolDefinitions; diff --git a/packages/api/src/agents/tools.spec.ts b/packages/api/src/agents/tools.spec.ts index 161849dac6..da24255e9b 100644 --- a/packages/api/src/agents/tools.spec.ts +++ b/packages/api/src/agents/tools.spec.ts @@ -17,6 +17,17 @@ jest.mock('@librechat/agents', () => ({ description: 'bash', schema: { type: 'object', properties: {} }, }, + /** + * Deterministic stub mirroring the SDK's `buildBashExecutionToolDescription`: + * appends an LLM-facing reference-syntax marker only when + * `enableToolOutputReferences` is true. + */ + buildBashExecutionToolDescription: ({ + enableToolOutputReferences, + }: { + enableToolOutputReferences?: boolean; + } = {}): string => + enableToolOutputReferences === true ? 'bash {{toolturn}}' : 'bash', })); import type { LCTool, LCToolRegistry } from '@librechat/agents'; @@ -266,4 +277,96 @@ describe('registerCodeExecutionTools', () => { expect(result.registered.sort()).toEqual(['bash_tool', 'read_file']); }); }); + + describe('enableToolOutputReferences', () => { + const findBashDef = (defs: LCTool[]): LCTool | undefined => + defs.find((d) => d.name === 'bash_tool'); + + it('appends the {{toolturn}} guide when flag is true', () => { + const result = registerCodeExecutionTools({ + toolRegistry: makeRegistry(), + toolDefinitions: [], + includeBash: true, + enableToolOutputReferences: true, + }); + + const bash = findBashDef(result.toolDefinitions); + expect(bash?.description).toContain('{{toolturn}}'); + }); + + it('omits the {{toolturn}} guide when flag is false', () => { + const result = registerCodeExecutionTools({ + toolRegistry: makeRegistry(), + toolDefinitions: [], + includeBash: true, + enableToolOutputReferences: false, + }); + + const bash = findBashDef(result.toolDefinitions); + expect(bash?.description).not.toContain('{{toolturn}}'); + }); + + it('omits the guide by default when flag is unspecified', () => { + const result = registerCodeExecutionTools({ + toolRegistry: makeRegistry(), + toolDefinitions: [], + includeBash: true, + }); + + const bash = findBashDef(result.toolDefinitions); + expect(bash?.description).not.toContain('{{toolturn}}'); + }); + + it('returns the same frozen bash_tool reference across calls with the same flag', () => { + /** + * The two `bash_tool` variants are cached at module scope so + * repeated agent inits in the same process don't re-allocate + * + re-freeze + re-build the description on every call. + * Asserting reference equality across two fresh registries + * pins that contract — a regression that switches back to a + * per-call `Object.freeze` would fail this test. + */ + const a = registerCodeExecutionTools({ + toolRegistry: makeRegistry(), + toolDefinitions: [], + includeBash: true, + enableToolOutputReferences: true, + }); + const b = registerCodeExecutionTools({ + toolRegistry: makeRegistry(), + toolDefinitions: [], + includeBash: true, + enableToolOutputReferences: true, + }); + + expect(findBashDef(a.toolDefinitions)).toBe(findBashDef(b.toolDefinitions)); + }); + + it('returns distinct frozen references for the two flag variants', () => { + /** + * Sanity check on the two-singleton cache: the with-refs and + * without-refs definitions are distinct objects so toggling + * the flag in `registerCodeExecutionTools` actually picks up + * the alternate description, not the same cached reference. + */ + const withRefs = registerCodeExecutionTools({ + toolRegistry: makeRegistry(), + toolDefinitions: [], + includeBash: true, + enableToolOutputReferences: true, + }); + const withoutRefs = registerCodeExecutionTools({ + toolRegistry: makeRegistry(), + toolDefinitions: [], + includeBash: true, + enableToolOutputReferences: false, + }); + + const a = findBashDef(withRefs.toolDefinitions); + const b = findBashDef(withoutRefs.toolDefinitions); + expect(a).not.toBe(b); + expect(a?.description).toContain('{{toolturn}}'); + expect(b?.description).not.toContain('{{toolturn}}'); + }); + }); }); diff --git a/packages/api/src/agents/tools.ts b/packages/api/src/agents/tools.ts index 5582724ba9..55d333c79e 100644 --- a/packages/api/src/agents/tools.ts +++ b/packages/api/src/agents/tools.ts @@ -1,4 +1,8 @@ -import { BashExecutionToolDefinition, ReadFileToolDefinition } from '@librechat/agents'; +import { + BashExecutionToolDefinition, + ReadFileToolDefinition, + buildBashExecutionToolDescription, +} from '@librechat/agents'; import type { LCTool, LCToolRegistry } from '@librechat/agents'; interface ToolDefLike { @@ -60,6 +64,13 @@ export interface RegisterCodeExecutionToolsParams { * no-op so there is exactly one copy of each tool in `toolDefinitions`. */ includeBash: boolean; + /** + * When `true`, the registered `bash_tool` description includes the + * LLM-facing `{{toolturn}}` reference syntax guide so the + * model knows it can substitute prior tool outputs in subsequent + * commands. Paired with `RunConfig.toolOutputReferences` in `createRun`. + */ + enableToolOutputReferences?: boolean; } export interface RegisterCodeExecutionToolsResult { @@ -69,11 +80,11 @@ export interface RegisterCodeExecutionToolsResult { } /** - * Hoisted module-level definitions so `registerCodeExecutionTools` doesn't - * re-allocate on every call (including the common no-op second call in the - * same run). The shapes are derived entirely from static - * `@librechat/agents` exports — no per-request state — so a single frozen - * object per tool is safe to share across every agent init. + * Hoisted module-level definition for `read_file` so + * `registerCodeExecutionTools` doesn't re-allocate on every call. The + * shape is derived entirely from a static `@librechat/agents` export — + * no per-request state — so a single frozen object is safe to share + * across every agent init. */ const READ_FILE_DEF: LCTool = Object.freeze({ name: ReadFileToolDefinition.name, @@ -82,11 +93,33 @@ const READ_FILE_DEF: LCTool = Object.freeze({ responseFormat: ReadFileToolDefinition.responseFormat, }) as LCTool; -const BASH_TOOL_DEF: LCTool = Object.freeze({ - name: BashExecutionToolDefinition.name, - description: BashExecutionToolDefinition.description, - parameters: BashExecutionToolDefinition.schema as unknown as LCTool['parameters'], -}) as LCTool; +/** + * The `bash_tool` description varies along exactly one axis — whether + * the LLM-facing `{{toolturn}}` reference syntax guide is + * appended — so two frozen module-level singletons cover every call + * site. Both are shaped identically to the legacy `BASH_TOOL_DEF` + * constant; only `description` differs. Sharing references across + * every agent init avoids per-call `Object.freeze` + SDK + * `buildBashExecutionToolDescription` work, matching the no-allocation + * intent of the original constant while keeping the per-agent gate + * behavior introduced for tool-output references. + */ +function createBashToolDef(enableToolOutputReferences: boolean): LCTool { + return Object.freeze({ + name: BashExecutionToolDefinition.name, + description: buildBashExecutionToolDescription({ enableToolOutputReferences }), + parameters: BashExecutionToolDefinition.schema as unknown as LCTool['parameters'], + }) as LCTool; +} + +const BASH_TOOL_DEF_WITH_OUTPUT_REFS = createBashToolDef(true); +const BASH_TOOL_DEF_WITHOUT_OUTPUT_REFS = createBashToolDef(false); + +function buildBashToolDef(opts: { enableToolOutputReferences: boolean }): LCTool { + return opts.enableToolOutputReferences + ? BASH_TOOL_DEF_WITH_OUTPUT_REFS + : BASH_TOOL_DEF_WITHOUT_OUTPUT_REFS; +} /** * Idempotently registers the skill-flavored code-execution tool pair @@ -103,9 +136,11 @@ const BASH_TOOL_DEF: LCTool = Object.freeze({ export function registerCodeExecutionTools( params: RegisterCodeExecutionToolsParams, ): RegisterCodeExecutionToolsResult { - const { toolRegistry, toolDefinitions, includeBash } = params; + const { toolRegistry, toolDefinitions, includeBash, enableToolOutputReferences = false } = params; - const candidates: LCTool[] = includeBash ? [READ_FILE_DEF, BASH_TOOL_DEF] : [READ_FILE_DEF]; + const candidates: LCTool[] = includeBash + ? [READ_FILE_DEF, buildBashToolDef({ enableToolOutputReferences })] + : [READ_FILE_DEF]; const existingNames = new Set((toolDefinitions ?? []).map((d) => d.name));