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.84",
"version": "3.1.85",
"main": "./dist/cjs/main.cjs",
"module": "./dist/esm/main.mjs",
"types": "./dist/types/index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions src/common/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,4 +261,5 @@ export enum TitleMethod {

export enum EnvVar {
CODE_BASEURL = 'LIBRECHAT_CODE_BASEURL',
CODE_API_RUN_TIMEOUT_MS = 'CODE_API_RUN_TIMEOUT_MS',
}
53 changes: 31 additions & 22 deletions src/tools/BashProgrammaticToolCalling.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { config } from 'dotenv';
import { tool, DynamicStructuredTool } from '@langchain/core/tools';
import type { ToolCall } from '@langchain/core/messages/tool';
import type { ProgrammaticToolCallingJsonSchema } from './ptcTimeout';
import type * as t from '@/types';
import {
makeRequest,
executeTools,
formatCompletedResponse,
} from './ProgrammaticToolCalling';
import { getCodeBaseURL } from './CodeExecutor';
import {
clampCodeApiRunTimeoutMs,
createCodeApiRunTimeoutSchema,
resolveCodeApiRunTimeoutMs,
} from './ptcTimeout';
import { Constants } from '@/common';

config();
Expand All @@ -17,7 +23,7 @@ config();
// ============================================================================

const DEFAULT_MAX_ROUND_TRIPS = 20;
const DEFAULT_TIMEOUT = 60000;
const DEFAULT_RUN_TIMEOUT_MS = resolveCodeApiRunTimeoutMs();

/** Bash reserved words that get `_tool` suffix when used as function names */
const BASH_RESERVED = new Set([
Expand Down Expand Up @@ -60,7 +66,8 @@ const CORE_RULES = `Rules:
- Tools are pre-defined as bash functions—DO NOT redefine them
- Each tool function accepts a JSON string argument
- Only echo/printf output returns to the model
- Generated files are automatically available in /mnt/data/ for subsequent executions`;
- Generated files are automatically available in /mnt/data/ for subsequent executions
- timeout caps one sandbox run/replay iteration, not the total multi-round-trip workflow`;

const ADDITIONAL_RULES =
'- Tool names normalized: hyphens→underscores, reserved words get `_tool` suffix';
Expand Down Expand Up @@ -92,25 +99,25 @@ ${CORE_RULES}`;
// Schema
// ============================================================================

export const BashProgrammaticToolCallingSchema = {
type: 'object',
properties: {
code: {
type: 'string',
minLength: 1,
description: CODE_PARAM_DESCRIPTION,
},
timeout: {
type: 'integer',
minimum: 1000,
maximum: 300000,
default: DEFAULT_TIMEOUT,
description:
'Maximum execution time in milliseconds. Default: 60 seconds. Max: 5 minutes.',
export function createBashProgrammaticToolCallingSchema(
maxRunTimeoutMs = DEFAULT_RUN_TIMEOUT_MS
): ProgrammaticToolCallingJsonSchema {
return {
type: 'object',
properties: {
code: {
type: 'string',
minLength: 1,
description: CODE_PARAM_DESCRIPTION,
},
timeout: createCodeApiRunTimeoutSchema(maxRunTimeoutMs),
},
},
required: ['code'],
} as const;
required: ['code'],
} as const;
}

export const BashProgrammaticToolCallingSchema =
createBashProgrammaticToolCallingSchema();

export const BashProgrammaticToolCallingName =
Constants.BASH_PROGRAMMATIC_TOOL_CALLING;
Expand Down Expand Up @@ -242,14 +249,16 @@ export function createBashProgrammaticToolCallingTool(
): DynamicStructuredTool {
const baseUrl = initParams.baseUrl ?? getCodeBaseURL();
const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;
const maxRunTimeoutMs = resolveCodeApiRunTimeoutMs(initParams.runTimeoutMs);
const proxy = initParams.proxy ?? process.env.PROXY;
const debug = initParams.debug ?? process.env.BASH_PTC_DEBUG === 'true';
const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;

return tool(
async (rawParams, config) => {
const params = rawParams as { code: string; timeout?: number };
const { code, timeout = DEFAULT_TIMEOUT } = params;
const { code } = params;
const timeout = clampCodeApiRunTimeoutMs(params.timeout, maxRunTimeoutMs);

const toolCall = (config.toolCall ?? {}) as ToolCall &
Partial<t.ProgrammaticCache> & {
Expand Down Expand Up @@ -382,7 +391,7 @@ export function createBashProgrammaticToolCallingTool(
{
name: Constants.BASH_PROGRAMMATIC_TOOL_CALLING,
description: BashProgrammaticToolCallingDescription,
schema: BashProgrammaticToolCallingSchema,
schema: createBashProgrammaticToolCallingSchema(maxRunTimeoutMs),
responseFormat: Constants.CONTENT_AND_ARTIFACT,
}
);
Expand Down
54 changes: 31 additions & 23 deletions src/tools/ProgrammaticToolCalling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,27 @@ import fetch, { RequestInit } from 'node-fetch';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { tool, DynamicStructuredTool } from '@langchain/core/tools';
import type { ToolCall } from '@langchain/core/messages/tool';
import type { ProgrammaticToolCallingJsonSchema } from './ptcTimeout';
import type * as t from '@/types';
import {
buildCodeApiHttpErrorMessage,
emptyOutputMessage,
getCodeBaseURL,
resolveCodeApiAuthHeaders,
} from './CodeExecutor';
import {
clampCodeApiRunTimeoutMs,
createCodeApiRunTimeoutSchema,
resolveCodeApiRunTimeoutMs,
} from './ptcTimeout';
import { Constants } from '@/common';

config();

/** Default max round-trips to prevent infinite loops */
const DEFAULT_MAX_ROUND_TRIPS = 20;

/** Default execution timeout in milliseconds */
const DEFAULT_TIMEOUT = 60000;
const DEFAULT_RUN_TIMEOUT_MS = resolveCodeApiRunTimeoutMs();

// ============================================================================
// Description Components (Single Source of Truth)
Expand All @@ -35,7 +40,8 @@ const CORE_RULES = `Rules:
- Just write code with await—auto-wrapped in async context
- DO NOT define async def main() or call asyncio.run()
- Tools are pre-defined—DO NOT write function definitions
- Only print() output returns to the model`;
- Only print() output returns to the model
- timeout caps one sandbox run/replay iteration, not the total multi-round-trip workflow`;

const ADDITIONAL_RULES = `- Generated files are automatically available in /mnt/data/ for subsequent executions
- Tool names normalized: hyphens→underscores, keywords get \`_tool\` suffix`;
Expand Down Expand Up @@ -68,25 +74,25 @@ ${EXAMPLES}

${CORE_RULES}`;

export const ProgrammaticToolCallingSchema = {
type: 'object',
properties: {
code: {
type: 'string',
minLength: 1,
description: CODE_PARAM_DESCRIPTION,
},
timeout: {
type: 'integer',
minimum: 1000,
maximum: 300000,
default: DEFAULT_TIMEOUT,
description:
'Maximum execution time in milliseconds. Default: 60 seconds. Max: 5 minutes.',
export function createProgrammaticToolCallingSchema(
maxRunTimeoutMs = DEFAULT_RUN_TIMEOUT_MS
): ProgrammaticToolCallingJsonSchema {
return {
type: 'object',
properties: {
code: {
type: 'string',
minLength: 1,
description: CODE_PARAM_DESCRIPTION,
},
timeout: createCodeApiRunTimeoutSchema(maxRunTimeoutMs),
},
},
required: ['code'],
} as const;
required: ['code'],
} as const;
}

export const ProgrammaticToolCallingSchema =
createProgrammaticToolCallingSchema();

export const ProgrammaticToolCallingName = Constants.PROGRAMMATIC_TOOL_CALLING;

Expand Down Expand Up @@ -731,14 +737,16 @@ export function createProgrammaticToolCallingTool(
): DynamicStructuredTool {
const baseUrl = initParams.baseUrl ?? getCodeBaseURL();
const maxRoundTrips = initParams.maxRoundTrips ?? DEFAULT_MAX_ROUND_TRIPS;
const maxRunTimeoutMs = resolveCodeApiRunTimeoutMs(initParams.runTimeoutMs);
const proxy = initParams.proxy ?? process.env.PROXY;
const debug = initParams.debug ?? process.env.PTC_DEBUG === 'true';
const EXEC_ENDPOINT = `${baseUrl}/exec/programmatic`;

return tool(
async (rawParams, config) => {
const params = rawParams as { code: string; timeout?: number };
const { code, timeout = DEFAULT_TIMEOUT } = params;
const { code } = params;
const timeout = clampCodeApiRunTimeoutMs(params.timeout, maxRunTimeoutMs);

// Extra params injected by ToolNode (follows web_search pattern).
const toolCall = (config.toolCall ?? {}) as ToolCall &
Expand Down Expand Up @@ -873,7 +881,7 @@ export function createProgrammaticToolCallingTool(
{
name: Constants.PROGRAMMATIC_TOOL_CALLING,
description: ProgrammaticToolCallingDescription,
schema: ProgrammaticToolCallingSchema,
schema: createProgrammaticToolCallingSchema(maxRunTimeoutMs),
responseFormat: Constants.CONTENT_AND_ARTIFACT,
}
);
Expand Down
103 changes: 103 additions & 0 deletions src/tools/__tests__/CodeApiAuthHeaders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ import {
makeRequest,
} from '../ProgrammaticToolCalling';
import { createBashProgrammaticToolCallingTool } from '../BashProgrammaticToolCalling';
import {
clampCodeApiRunTimeoutMs,
createCodeApiRunTimeoutSchema,
} from '../ptcTimeout';
import {
createLocalProgrammaticToolCallingTool,
createLocalBashProgrammaticToolCallingTool,
} from '../local/LocalProgrammaticToolCalling';

jest.mock('node-fetch', () => ({
__esModule: true,
Expand All @@ -23,8 +31,33 @@ type FetchMock = jest.MockedFunction<
(url: unknown, init?: unknown) => Promise<unknown>
>;

type CodeApiRequestBody = {
timeout?: number;
};

type TimeoutSchemaForTest = {
default: number;
maximum: number;
description: string;
};

type ToolSchemaForTest = {
properties: {
timeout: TimeoutSchemaForTest;
};
};

const fetchMock = fetch as unknown as FetchMock;

function requestBodyAt(callIndex: number): CodeApiRequestBody {
const init = fetchMock.mock.calls[callIndex]?.[1] as RequestInit;
return JSON.parse(init.body as string) as CodeApiRequestBody;
}

function timeoutSchemaForTest(toolSchema: unknown): TimeoutSchemaForTest {
return (toolSchema as ToolSchemaForTest).properties.timeout;
}

function jsonResponse(body: unknown): unknown {
return {
ok: true,
Expand Down Expand Up @@ -203,6 +236,76 @@ describe('CodeAPI auth header injection', () => {
}
});

it('defaults programmatic timeout to the configured CodeAPI run cap', async () => {
const tool = createProgrammaticToolCallingTool({
runTimeoutMs: 15000,
});

await tool.invoke(
{ code: 'result = await lookup_user()\nprint(result)' },
{
toolCall: {
name: 'programmatic_code_execution',
args: {},
toolMap: toolMap(),
toolDefs,
},
}
);

expect(requestBodyAt(0).timeout).toBe(15000);
});

it('defaults bash programmatic timeout to the configured CodeAPI run cap', async () => {
const tool = createBashProgrammaticToolCallingTool({
runTimeoutMs: 15000,
});

await tool.invoke(
{ code: 'lookup_user "{}"' },
{
toolCall: {
name: 'bash_programmatic_code_execution',
args: {},
toolMap: toolMap(),
toolDefs,
},
}
);

expect(requestBodyAt(0).timeout).toBe(15000);
});

it('describes the PTC timeout as a single sandbox run cap', () => {
const schema = createCodeApiRunTimeoutSchema(15000);

expect(clampCodeApiRunTimeoutMs(60000, 15000)).toBe(15000);
expect(schema.default).toBe(15000);
expect(schema.maximum).toBe(15000);
expect(schema.description).toContain('one sandbox run');
expect(schema.description).toContain('not the total multi-round-trip');
});

it('keeps local programmatic timeout schemas aligned with local execution defaults', () => {
const pythonTimeout = timeoutSchemaForTest(
createLocalProgrammaticToolCallingTool().schema
);
const bashTimeout = timeoutSchemaForTest(
createLocalBashProgrammaticToolCallingTool().schema
);
const configuredTimeout = timeoutSchemaForTest(
createLocalProgrammaticToolCallingTool({ timeoutMs: 120000 }).schema
);

expect(pythonTimeout.default).toBe(60000);
expect(pythonTimeout.maximum).toBe(300000);
expect(pythonTimeout.description).toContain('local execution time');
expect(bashTimeout.default).toBe(60000);
expect(bashTimeout.maximum).toBe(300000);
expect(configuredTimeout.default).toBe(120000);
expect(configuredTimeout.maximum).toBe(300000);
});

it('forwards Authorization for bash programmatic requests', async () => {
const tool = createBashProgrammaticToolCallingTool({
authHeaders: { Authorization: 'Bearer bash-ptc-token' },
Expand Down
Loading
Loading