🎬 fix: Code Session Context In Event Driven Mode (#11673)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run

* fix: Update parseTextParts to handle undefined content parts

- Modified the parseTextParts function to accept an array of content parts that may include undefined values.
- Implemented optional chaining to safely check for the type of each part, preventing potential runtime errors when accessing properties of undefined elements.

* refactor: Tool Call Configuration with Session Context

- Added support for including session ID and injected files in the tool call configuration when a code session context is present.
- Improved handling of tool call configurations to accommodate additional context data, enhancing the functionality of the tool execution handler.

* chore: Update @librechat/agents to version 3.1.37 in package.json and package-lock.json

* test: Add unit tests for createToolExecuteHandler

- Introduced a new test suite for the createToolExecuteHandler function, validating the handling of session context in tool calls.
- Added tests to ensure correct passing of session IDs and injected files based on the presence of codeSessionContext.
- Included scenarios for handling multiple tool calls and ensuring non-code execution tools are unaffected by session context.

* test: Update createToolExecuteHandler tests for session context handling

- Renamed test to clarify that it checks for the absence of session context in non-code-execution tools.
- Updated assertions to ensure that session_id and _injected_files are undefined when non-code-execution tools are invoked, enhancing test accuracy.
This commit is contained in:
Danny Avila 2026-02-07 03:09:55 -05:00 committed by GitHub
parent 968e97b4d2
commit a771d70b10
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 294 additions and 17 deletions

View file

@ -87,7 +87,7 @@
"@google/genai": "^1.19.0",
"@keyv/redis": "^4.3.3",
"@langchain/core": "^0.3.80",
"@librechat/agents": "^3.1.36",
"@librechat/agents": "^3.1.37",
"@librechat/data-schemas": "*",
"@modelcontextprotocol/sdk": "^1.26.0",
"@smithy/node-http-handler": "^4.4.5",

View file

@ -0,0 +1,178 @@
import { Constants } from '@librechat/agents';
import type {
ToolExecuteBatchRequest,
ToolExecuteResult,
ToolCallRequest,
} from '@librechat/agents';
import { createToolExecuteHandler, ToolExecuteOptions } from './handlers';
function createMockTool(name: string, capturedConfigs: Record<string, unknown>[]) {
return {
name,
invoke: jest.fn(async (_args: unknown, config: Record<string, unknown>) => {
capturedConfigs.push({ ...(config.toolCall as Record<string, unknown>) });
return {
content: `stdout:\n${name} executed\n`,
artifact: { session_id: `result-session-${name}`, files: [] },
};
}),
};
}
function createHandler(
capturedConfigs: Record<string, unknown>[],
toolNames: string[] = [Constants.EXECUTE_CODE],
) {
const mockTools = toolNames.map((name) => createMockTool(name, capturedConfigs));
const loadTools: ToolExecuteOptions['loadTools'] = jest.fn(async () => ({
loadedTools: mockTools as never[],
}));
return createToolExecuteHandler({ loadTools });
}
function invokeHandler(
handler: ReturnType<typeof createToolExecuteHandler>,
toolCalls: ToolCallRequest[],
): Promise<ToolExecuteResult[]> {
return new Promise((resolve, reject) => {
const request: ToolExecuteBatchRequest = {
toolCalls,
resolve,
reject,
};
handler.handle('on_tool_execute', request);
});
}
describe('createToolExecuteHandler', () => {
describe('code execution session context passthrough', () => {
it('passes session_id and _injected_files from codeSessionContext to toolCallConfig', async () => {
const capturedConfigs: Record<string, unknown>[] = [];
const handler = createHandler(capturedConfigs);
const toolCalls: ToolCallRequest[] = [
{
id: 'call_1',
name: Constants.EXECUTE_CODE,
args: { lang: 'python', code: 'print("hi")' },
codeSessionContext: {
session_id: 'prev-session-abc',
files: [
{ session_id: 'prev-session-abc', id: 'f1', name: 'data.parquet' },
{ session_id: 'prev-session-abc', id: 'f2', name: 'chart.png' },
],
},
},
];
await invokeHandler(handler, toolCalls);
expect(capturedConfigs).toHaveLength(1);
expect(capturedConfigs[0].session_id).toBe('prev-session-abc');
expect(capturedConfigs[0]._injected_files).toEqual([
{ session_id: 'prev-session-abc', id: 'f1', name: 'data.parquet' },
{ session_id: 'prev-session-abc', id: 'f2', name: 'chart.png' },
]);
});
it('passes session_id without _injected_files when session has no files', async () => {
const capturedConfigs: Record<string, unknown>[] = [];
const handler = createHandler(capturedConfigs);
const toolCalls: ToolCallRequest[] = [
{
id: 'call_2',
name: Constants.EXECUTE_CODE,
args: { lang: 'python', code: 'import pandas' },
codeSessionContext: {
session_id: 'session-no-files',
},
},
];
await invokeHandler(handler, toolCalls);
expect(capturedConfigs).toHaveLength(1);
expect(capturedConfigs[0].session_id).toBe('session-no-files');
expect(capturedConfigs[0]._injected_files).toBeUndefined();
});
it('does not inject session context when codeSessionContext is absent', async () => {
const capturedConfigs: Record<string, unknown>[] = [];
const handler = createHandler(capturedConfigs);
const toolCalls: ToolCallRequest[] = [
{
id: 'call_3',
name: Constants.EXECUTE_CODE,
args: { lang: 'python', code: 'x = 1' },
},
];
await invokeHandler(handler, toolCalls);
expect(capturedConfigs).toHaveLength(1);
expect(capturedConfigs[0].session_id).toBeUndefined();
expect(capturedConfigs[0]._injected_files).toBeUndefined();
});
it('passes session context independently for multiple code execution calls', async () => {
const capturedConfigs: Record<string, unknown>[] = [];
const handler = createHandler(capturedConfigs);
const toolCalls: ToolCallRequest[] = [
{
id: 'call_a',
name: Constants.EXECUTE_CODE,
args: { lang: 'python', code: 'step_1()' },
codeSessionContext: {
session_id: 'session-A',
files: [{ session_id: 'session-A', id: 'fa', name: 'a.csv' }],
},
},
{
id: 'call_b',
name: Constants.EXECUTE_CODE,
args: { lang: 'python', code: 'step_2()' },
codeSessionContext: {
session_id: 'session-A',
files: [{ session_id: 'session-A', id: 'fa', name: 'a.csv' }],
},
},
];
await invokeHandler(handler, toolCalls);
expect(capturedConfigs).toHaveLength(2);
for (const config of capturedConfigs) {
expect(config.session_id).toBe('session-A');
expect(config._injected_files).toEqual([
{ session_id: 'session-A', id: 'fa', name: 'a.csv' },
]);
}
});
it('does not pass session context to non-code-execution tools', async () => {
const capturedConfigs: Record<string, unknown>[] = [];
const handler = createHandler(capturedConfigs, ['web_search']);
const toolCalls: ToolCallRequest[] = [
{
id: 'call_ws',
name: 'web_search',
args: { query: 'test' },
codeSessionContext: {
session_id: 'should-be-ignored',
files: [{ session_id: 'x', id: 'y', name: 'z' }],
},
},
];
await invokeHandler(handler, toolCalls);
expect(capturedConfigs).toHaveLength(1);
expect(capturedConfigs[0].session_id).toBeUndefined();
expect(capturedConfigs[0]._injected_files).toBeUndefined();
});
});
});

View file

@ -85,6 +85,17 @@ export function createToolExecuteHandler(options: ToolExecuteOptions): EventHand
turn: tc.turn,
};
if (
tc.codeSessionContext &&
(tc.name === Constants.EXECUTE_CODE ||
tc.name === Constants.PROGRAMMATIC_TOOL_CALLING)
) {
toolCallConfig.session_id = tc.codeSessionContext.session_id;
if (tc.codeSessionContext.files && tc.codeSessionContext.files.length > 0) {
toolCallConfig._injected_files = tc.codeSessionContext.files;
}
}
if (tc.name === Constants.PROGRAMMATIC_TOOL_CALLING) {
const toolRegistry = mergedConfigurable?.toolRegistry as LCToolRegistry | undefined;
const ptcToolMap = mergedConfigurable?.ptcToolMap as