Skip to content

Commit 3f92853

Browse files
authored
🏠 fix: Route Standalone File Writes to BYOM Workers (#15675)
1 parent 87097e2 commit 3f92853

2 files changed

Lines changed: 78 additions & 23 deletions

File tree

api/server/services/ToolService.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ const {
4949
createGitIdentityProgrammaticBashTool,
5050
resolveCodeExecutionContext,
5151
resolveCallerCapabilityProjectionSnapshot,
52+
CREATE_FILE_TOOL_NAME,
53+
EDIT_FILE_TOOL_NAME,
5254
LIST_WORKSPACE_FILES_TOOL_NAME,
5355
SEARCH_WORKSPACE_TOOL_NAME,
5456
getTransactionsConfig,
@@ -2013,7 +2015,7 @@ async function loadToolsForExecution({
20132015
isWorkspaceListRequested;
20142016
const isSkillToolRequested = toolNames.includes(AgentConstants.SKILL_TOOL);
20152017
const isSandboxFileToolRequested = toolNames.some((name) =>
2016-
[AgentConstants.READ_FILE, AgentConstants.CREATE_FILE, AgentConstants.EDIT_FILE].includes(name),
2018+
[AgentConstants.READ_FILE, CREATE_FILE_TOOL_NAME, EDIT_FILE_TOOL_NAME].includes(name),
20172019
);
20182020

20192021
let enabledCapabilities;

api/server/services/__tests__/ToolService.spec.js

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2529,43 +2529,96 @@ describe('ToolService - Action Capability Gating', () => {
25292529
}
25302530
});
25312531

2532-
it('resolves stateful routing for host file tools with the controller conversation ID', async () => {
2533-
const capabilities = [
2534-
AgentCapabilities.tools,
2535-
AgentCapabilities.execute_code,
2536-
AgentCapabilities.stateful_code_sessions,
2537-
];
2538-
const req = createMockReq(capabilities);
2539-
req.body = {};
2540-
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
2541-
process.env.LIBRECHAT_CODE_BASEURL_STATEFUL = 'http://code-stateful.test/v1';
2532+
it.each(['read_file', 'create_file', 'edit_file'])(
2533+
'resolves stateful routing for standalone %s with the controller conversation ID',
2534+
async (toolName) => {
2535+
const capabilities = [
2536+
AgentCapabilities.tools,
2537+
AgentCapabilities.execute_code,
2538+
AgentCapabilities.stateful_code_sessions,
2539+
];
2540+
const req = createMockReq(capabilities);
2541+
req.body = {};
2542+
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
2543+
process.env.LIBRECHAT_CODE_BASEURL_STATEFUL = 'http://code-stateful.test/v1';
2544+
2545+
try {
2546+
const result = await loadToolsForExecution({
2547+
req,
2548+
res: {},
2549+
conversationId: 'resolved-api-conversation',
2550+
agent: {
2551+
id: 'stateful-agent',
2552+
tools: [Tools.execute_code],
2553+
stateful_code_sessions: true,
2554+
stateful_code_environment: 'conversation',
2555+
},
2556+
toolNames: [toolName],
2557+
actionsEnabled: false,
2558+
});
2559+
2560+
expect(result.configurable.codeExecutionContext.executionProfile).toBe('stateful');
2561+
expect(mockResolveCodeExecutionContext).toHaveBeenLastCalledWith(
2562+
expect.objectContaining({
2563+
statefulSessions: true,
2564+
conversationId: 'resolved-api-conversation',
2565+
}),
2566+
);
2567+
} finally {
2568+
delete process.env.LIBRECHAT_CODE_BASEURL_STATEFUL;
2569+
}
2570+
},
2571+
);
2572+
2573+
it.each(['read_file', 'create_file', 'edit_file'])(
2574+
'keeps standalone %s on the selected attached worker',
2575+
async (toolName) => {
2576+
const capabilities = [
2577+
AgentCapabilities.tools,
2578+
AgentCapabilities.execute_code,
2579+
AgentCapabilities.stateful_code_sessions,
2580+
];
2581+
const req = createMockReq(capabilities);
2582+
req.config.endpoints.agents.statefulCodeSessions = {
2583+
environments: [
2584+
{
2585+
id: 'personal-machine',
2586+
type: 'attached',
2587+
baseURL: 'https://attached-code.test/v1',
2588+
workerId: 'worker-personal',
2589+
},
2590+
],
2591+
};
2592+
mockGetEndpointsConfig.mockResolvedValue(createEndpointsConfig(capabilities));
2593+
mockResolveCodeExecutionContext.mockImplementationOnce(
2594+
jest.requireActual('@librechat/api').resolveCodeExecutionContext,
2595+
);
25422596

2543-
try {
25442597
const result = await loadToolsForExecution({
25452598
req,
25462599
res: {},
25472600
conversationId: 'resolved-api-conversation',
25482601
agent: {
2549-
id: 'stateful-agent',
2602+
id: 'attached-agent',
25502603
tools: [Tools.execute_code],
25512604
stateful_code_sessions: true,
2552-
stateful_code_environment: 'conversation',
2605+
code_environment_id: 'personal-machine',
25532606
},
2554-
toolNames: [AgentConstants.READ_FILE],
2607+
toolNames: [toolName],
25552608
actionsEnabled: false,
25562609
});
25572610

2558-
expect(result.configurable.codeExecutionContext.executionProfile).toBe('stateful');
2559-
expect(mockResolveCodeExecutionContext).toHaveBeenLastCalledWith(
2611+
expect(result.configurable.codeExecutionContext).toEqual(
25602612
expect.objectContaining({
2561-
statefulSessions: true,
2562-
conversationId: 'resolved-api-conversation',
2613+
baseUrl: 'https://attached-code.test/v1',
2614+
executionProfile: 'stateful',
2615+
environmentType: 'attached',
2616+
environmentId: 'personal-machine',
2617+
bridgeWorkerId: 'worker-personal',
25632618
}),
25642619
);
2565-
} finally {
2566-
delete process.env.LIBRECHAT_CODE_BASEURL_STATEFUL;
2567-
}
2568-
});
2620+
},
2621+
);
25692622

25702623
it('preserves attached routing when workspace search is the only requested tool', async () => {
25712624
const capabilities = [

0 commit comments

Comments
 (0)