Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@librechat/agents",
"version": "3.1.83",
"version": "3.1.84",
"main": "./dist/cjs/main.cjs",
"module": "./dist/esm/main.mjs",
"types": "./dist/types/index.d.ts",
Expand Down
35 changes: 32 additions & 3 deletions src/agents/AgentContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ANTHROPIC_TOOL_TOKEN_MULTIPLIER,
DEFAULT_TOOL_TOKEN_MULTIPLIER,
ContentTypes,
Constants,
Providers,
} from '@/common';
import { createSchemaOnlyTools } from '@/tools/schema';
Expand Down Expand Up @@ -389,7 +390,7 @@ export class AgentContext {
/**
* Builds instructions text for tools that are ONLY callable via programmatic code execution.
* These tools cannot be called directly by the LLM but are available through the
* run_tools_with_code tool.
* configured programmatic tool.
*
* Includes:
* - Code_execution-only tools that are NOT deferred
Expand All @@ -416,6 +417,7 @@ export class AgentContext {

if (programmaticOnlyTools.length === 0) return '';

const programmaticTool = this.getProgrammaticToolInstructionTarget();
const toolDescriptions = programmaticOnlyTools
.map((tool) => {
let desc = `- **${tool.name}**`;
Expand All @@ -431,12 +433,39 @@ export class AgentContext {

return (
'\n\n## Programmatic-Only Tools\n\n' +
'The following tools are available exclusively through the `run_tools_with_code` tool. ' +
'You cannot call these tools directly; instead, use `run_tools_with_code` with Python code that invokes them.\n\n' +
`The following tools are available exclusively through the \`${programmaticTool.name}\` tool. ` +
`You cannot call these tools directly; instead, use \`${programmaticTool.name}\` with ${programmaticTool.language} code that invokes them.\n\n` +
toolDescriptions
);
}

private getProgrammaticToolInstructionTarget(): {
name: string;
language: 'bash' | 'Python';
} {
if (this.hasAvailableTool(Constants.BASH_PROGRAMMATIC_TOOL_CALLING)) {
return {
name: Constants.BASH_PROGRAMMATIC_TOOL_CALLING,
language: 'bash',
};
}

if (this.hasAvailableTool(Constants.PROGRAMMATIC_TOOL_CALLING)) {
return { name: Constants.PROGRAMMATIC_TOOL_CALLING, language: 'Python' };
}

return { name: Constants.BASH_PROGRAMMATIC_TOOL_CALLING, language: 'bash' };
}

private hasAvailableTool(name: string): boolean {
if (this.toolDefinitions?.some((tool) => tool.name === name)) return true;
if (this.tools?.some((tool) => 'name' in tool && tool.name === name)) {
return true;
}
if (this.toolMap?.has(name)) return true;
return this.toolRegistry?.has(name) === true;
}

/**
* Gets the system runnable, creating it lazily if needed.
* Includes stable instructions, dynamic additional instructions, and
Expand Down
39 changes: 36 additions & 3 deletions src/agents/__tests__/AgentContext.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// src/agents/__tests__/AgentContext.test.ts
import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
import { AgentContext } from '../AgentContext';
import { Providers } from '@/common';
import { Constants, Providers } from '@/common';
import { addBedrockCacheControl } from '@/messages/cache';
import type * as t from '@/types';

Expand Down Expand Up @@ -593,7 +593,7 @@ describe('AgentContext', () => {
});

describe('buildProgrammaticOnlyToolsInstructions', () => {
it('includes code_execution-only tools in system message', () => {
it('includes code_execution-only tools in system message', async () => {
const toolRegistry: t.LCToolRegistry = new Map([
[
'programmatic_tool',
Expand All @@ -606,11 +606,44 @@ describe('AgentContext', () => {
]);

const ctx = createBasicContext({
agentConfig: { instructions: 'Base', toolRegistry },
agentConfig: {
instructions: 'Base',
toolDefinitions: [{ name: Constants.BASH_PROGRAMMATIC_TOOL_CALLING }],
toolRegistry,
},
});

const runnable = ctx.systemRunnable;
expect(runnable).toBeDefined();
const result = await runnable!.invoke([]);
expect(result[0].content).toContain('run_tools_with_bash');
expect(result[0].content).not.toContain('run_tools_with_code');
});

it('uses Python PTC guidance when only run_tools_with_code is available', async () => {
const toolRegistry: t.LCToolRegistry = new Map([
[
'programmatic_tool',
{
name: 'programmatic_tool',
description: 'Only callable via code execution',
allowed_callers: ['code_execution'],
},
],
]);

const ctx = createBasicContext({
agentConfig: {
instructions: 'Base',
toolDefinitions: [{ name: Constants.PROGRAMMATIC_TOOL_CALLING }],
toolRegistry,
},
});

const result = await ctx.systemRunnable!.invoke([]);
expect(result[0].content).toContain('run_tools_with_code');
expect(result[0].content).toContain('Python code');
expect(result[0].content).not.toContain('run_tools_with_bash');
});

it('excludes direct-callable tools from programmatic section', () => {
Expand Down
5 changes: 4 additions & 1 deletion src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ export class ToolEndHandler implements t.EventHandler {
return;
}

if (metadata[Constants.PROGRAMMATIC_TOOL_CALLING] === true) {
if (
metadata[Constants.PROGRAMMATIC_TOOL_CALLING] === true ||
metadata[Constants.BASH_PROGRAMMATIC_TOOL_CALLING] === true
) {
return;
}

Expand Down
17 changes: 14 additions & 3 deletions src/tools/BashExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import fetch, { RequestInit } from 'node-fetch';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { tool, DynamicStructuredTool } from '@langchain/core/tools';
import type * as t from '@/types';
import { emptyOutputMessage, getCodeBaseURL } from './CodeExecutor';
import {
emptyOutputMessage,
buildCodeApiHttpErrorMessage,
getCodeBaseURL,
resolveCodeApiAuthHeaders,
} from './CodeExecutor';
import { Constants } from '@/common';

config();
Expand Down Expand Up @@ -104,6 +109,7 @@ function createBashExecutionTool(
): DynamicStructuredTool {
return tool(
async (rawInput, config) => {
const { authHeaders, ...executionParams } = params ?? {};
const { command, ...rest } = rawInput as {
command: string;
args?: string[];
Expand All @@ -117,7 +123,7 @@ function createBashExecutionTool(
lang: 'bash',
code: command,
...rest,
...params,
...executionParams,
};

/* See `CodeExecutor.ts` for the rationale — `/files/<session_id>`
Expand All @@ -137,11 +143,14 @@ function createBashExecutionTool(
}

try {
const resolvedAuthHeaders =
await resolveCodeApiAuthHeaders(authHeaders);
const fetchOptions: RequestInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'LibreChat/1.0',
...resolvedAuthHeaders,
},
body: JSON.stringify(postData),
};
Expand All @@ -151,7 +160,9 @@ function createBashExecutionTool(
}
const response = await fetch(EXEC_ENDPOINT, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
throw new Error(
await buildCodeApiHttpErrorMessage('POST', EXEC_ENDPOINT, response)
);
}

const result: t.ExecuteResult = await response.json();
Expand Down
9 changes: 6 additions & 3 deletions src/tools/BashProgrammaticToolCalling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,8 @@ export function createBashProgrammaticToolCallingTool(
timeout,
...(files && files.length > 0 ? { files } : {}),
},
proxy
proxy,
initParams.authHeaders
);

// ====================================================================
Expand All @@ -339,7 +340,8 @@ export function createBashProgrammaticToolCallingTool(

const toolResults = await executeTools(
response.tool_calls ?? [],
toolMap
toolMap,
Constants.BASH_PROGRAMMATIC_TOOL_CALLING
);

response = await makeRequest(
Expand All @@ -348,7 +350,8 @@ export function createBashProgrammaticToolCallingTool(
continuation_token: response.continuation_token,
tool_results: toolResults,
},
proxy
proxy,
initParams.authHeaders
);
}

Expand Down
38 changes: 36 additions & 2 deletions src/tools/CodeExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,34 @@ const EXEC_ENDPOINT = `${baseEndpoint}/exec`;

type SupportedLanguage = (typeof SUPPORTED_LANGUAGES)[number];

export async function resolveCodeApiAuthHeaders(
authHeaders?: t.CodeApiAuthHeaders
): Promise<t.CodeApiAuthHeaderMap> {
if (authHeaders == null) {
return {};
}
if (typeof authHeaders === 'function') {
return authHeaders();
}
return authHeaders;
}

export async function buildCodeApiHttpErrorMessage(
method: string,
endpoint: string,
response: { status: number; text: () => Promise<string> }
): Promise<string> {
let responseBody = '';
try {
responseBody = await response.text();
} catch {
responseBody = '';
}
const body = responseBody.trim();
const bodySuffix = body === '' ? '' : `, body: ${body.slice(0, 1000)}`;
return `CodeAPI request failed: ${method} ${endpoint} returned ${response.status}${bodySuffix}`;
}

export const CodeExecutionToolDescription = `
Runs 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.

Expand All @@ -92,6 +120,7 @@ function createCodeExecutionTool(
): DynamicStructuredTool {
return tool(
async (rawInput, config) => {
const { authHeaders, ...executionParams } = params ?? {};
const { lang, code, ...rest } = rawInput as {
lang: SupportedLanguage;
code: string;
Expand All @@ -111,7 +140,7 @@ function createCodeExecutionTool(
lang,
code,
...rest,
...params,
...executionParams,
};

/* File injection: `_injected_files` from ToolNode (set when host
Expand All @@ -135,11 +164,14 @@ function createCodeExecutionTool(
}

try {
const resolvedAuthHeaders =
await resolveCodeApiAuthHeaders(authHeaders);
const fetchOptions: RequestInit = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'LibreChat/1.0',
...resolvedAuthHeaders,
},
body: JSON.stringify(postData),
};
Expand All @@ -149,7 +181,9 @@ function createCodeExecutionTool(
}
const response = await fetch(EXEC_ENDPOINT, fetchOptions);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
throw new Error(
await buildCodeApiHttpErrorMessage('POST', EXEC_ENDPOINT, response)
);
}

const result: t.ExecuteResult = await response.json();
Expand Down
Loading
Loading