diff --git a/api/server/controllers/agents/__tests__/callbacks.spec.js b/api/server/controllers/agents/__tests__/callbacks.spec.js index 65bcfccfe6..20fcf54a6c 100644 --- a/api/server/controllers/agents/__tests__/callbacks.spec.js +++ b/api/server/controllers/agents/__tests__/callbacks.spec.js @@ -7,8 +7,9 @@ jest.mock('nanoid', () => ({ jest.mock('@librechat/api', () => ({ sendEvent: jest.fn(), + HOST_FILE_AUTHORING_ARTIFACT_KEY: '__librechat_file_authoring', isCodeSessionToolName: jest.fn((name) => - ['execute_code', 'bash_tool', 'read_file', 'create_file', 'edit_file'].includes(name), + ['execute_code', 'bash_tool', 'read_file'].includes(name), ), })); @@ -374,12 +375,14 @@ describe('createToolEndCallback', () => { fileId, name, toolName = 'execute_code', + hostFileAuthoring = false, }) { return { output: { name: toolName, tool_call_id: toolCallId, artifact: { + ...(hostFileAuthoring ? { __librechat_file_authoring: true } : {}), session_id: 'sess-1', files: [{ id: fileId, name, session_id: 'sess-1' }], }, @@ -607,6 +610,7 @@ describe('createToolEndCallback', () => { fileId: 'fid-created', name: 'created.txt', toolName: 'create_file', + hostFileAuthoring: true, }); await toolEndCallback({ output: event.output }, event.metadata); await Promise.all(artifactPromises); @@ -622,6 +626,25 @@ describe('createToolEndCallback', () => { ); expect(res.write).toHaveBeenCalledTimes(1); }); + + it('does not process arbitrary user tool artifacts named create_file as code outputs', async () => { + res.headersSent = true; + const toolEndCallback = createToolEndCallback({ req, res, artifactPromises }); + const event = makeCodeExecutionEvent({ + runId: 'run-user-create', + threadId: 'thread789', + toolCallId: 'tool-user-create', + fileId: 'fid-user-created', + name: 'created.txt', + toolName: 'create_file', + }); + + await toolEndCallback({ output: event.output }, event.metadata); + await Promise.all(artifactPromises); + + expect(processCodeOutput).not.toHaveBeenCalled(); + expect(res.write).not.toHaveBeenCalled(); + }); }); }); diff --git a/api/server/controllers/agents/callbacks.js b/api/server/controllers/agents/callbacks.js index b4cc8bd0f2..0a8f8854f7 100644 --- a/api/server/controllers/agents/callbacks.js +++ b/api/server/controllers/agents/callbacks.js @@ -12,14 +12,19 @@ const { GenerationJobManager, writeAttachmentEvent, createToolExecuteHandler, + HOST_FILE_AUTHORING_ARTIFACT_KEY, isCodeSessionToolName, } = require('@librechat/api'); const { processFileCitations } = require('~/server/services/Files/Citations'); const { processCodeOutput, runPreviewFinalize } = require('~/server/services/Files/Code/process'); const { saveBase64Image } = require('~/server/services/Files/process'); -function isCodeArtifactToolName(name) { - return isCodeSessionToolName(name); +function isHostFileAuthoringArtifact(artifact) { + return artifact?.[HOST_FILE_AUTHORING_ARTIFACT_KEY] === true; +} + +function isCodeArtifactToolOutput(output) { + return isCodeSessionToolName(output.name) || isHostFileAuthoringArtifact(output.artifact); } class ModelEndHandler { @@ -627,7 +632,7 @@ function createToolEndCallback({ req, res, artifactPromises, streamId = null }) return; } - if (!isCodeArtifactToolName(output.name)) { + if (!isCodeArtifactToolOutput(output)) { return; } @@ -894,7 +899,7 @@ function createResponsesToolEndCallback({ req, res, tracker, artifactPromises }) return; } - if (!isCodeArtifactToolName(output.name)) { + if (!isCodeArtifactToolOutput(output)) { return; } diff --git a/packages/api/src/agents/handlers.spec.ts b/packages/api/src/agents/handlers.spec.ts index 63dbae81fb..bf06244f87 100644 --- a/packages/api/src/agents/handlers.spec.ts +++ b/packages/api/src/agents/handlers.spec.ts @@ -386,7 +386,8 @@ describe('createToolExecuteHandler', () => { describe('host file authoring collisions', () => { it('invokes a loaded user tool named create_file when host file authoring is not active', async () => { const capturedArgs: unknown[] = []; - const tool = createMockTool('create_file', [], { capturedArgs }); + const capturedConfigs: Record[] = []; + const tool = createMockTool('create_file', capturedConfigs, { capturedArgs }); const loadTools: ToolExecuteOptions['loadTools'] = jest.fn(async () => ({ loadedTools: [tool] as never[], configurable: { fileAuthoringToolNames: new Set() }, @@ -398,12 +399,18 @@ describe('createToolExecuteHandler', () => { id: 'call_user_create_file', name: 'create_file', args: { custom: true }, + codeSessionContext: { + session_id: 'ignored-session', + files: [{ id: 'ignored-file', name: 'ignored.txt', session_id: 'ignored-session' }], + }, }, ]); expect(result.status).toBe('success'); expect(result.content).toContain('create_file executed'); expect(capturedArgs).toEqual([{ custom: true }]); + expect(capturedConfigs[0].session_id).toBeUndefined(); + expect(capturedConfigs[0]._injected_files).toBeUndefined(); }); }); @@ -1331,6 +1338,56 @@ describe('createToolExecuteHandler', () => { ); }); + it('rejects bundled skill file writes when the skill version changed after reading', async () => { + const getSkillByName = jest + .fn() + .mockResolvedValueOnce({ + _id: SKILL_ID, + name: 'edit-skill', + body: '# Existing', + fileCount: 1, + version: 1, + }) + .mockResolvedValueOnce({ + _id: SKILL_ID, + name: 'edit-skill', + body: '# Existing changed elsewhere', + fileCount: 1, + version: 2, + }); + const saveSkillFileContent = jest.fn(); + const handler = makeAuthoringHandler({ + getSkillByName, + getSkillFileByPath: jest.fn(async () => ({ + content: 'hello old\n', + isBinary: false, + mimeType: 'text/markdown', + bytes: 10, + filepath: '/tmp/a.md', + source: 'local', + relativePath: 'references/a.md', + })), + saveSkillFileContent, + }); + + const [result] = await invokeHandler(handler, [ + { + id: 'call_edit_stale_bundled_file', + name: 'edit_file', + args: { + file_path: 'skills/edit-skill/references/a.md', + old_text: 'hello old', + new_text: 'hello new', + }, + }, + ]); + + expect(result.status).toBe('error'); + expect(result.errorMessage).toContain('changed while editing'); + expect(result.errorMessage).toContain('skills/edit-skill/references/a.md'); + expect(saveSkillFileContent).not.toHaveBeenCalled(); + }); + it('passes structured frontmatter when editing SKILL.md', async () => { const oldBody = '---\nname: runtime-skill\ndescription: Use before\naction: ignored\n---\n# Body\n'; @@ -1745,6 +1802,87 @@ describe('createToolExecuteHandler', () => { ); }); + it('propagates newly created sandbox sessions to queued same-path authoring calls', async () => { + let readCount = 0; + let writeCount = 0; + const sandboxFiles = [ + { id: 'file-queued', name: 'queued.txt', storage_session_id: 'sess-new' }, + ]; + const readSandboxFile = jest.fn( + async ({ + session_id, + files, + }: { + session_id?: string; + files?: Array<{ id: string; name: string; storage_session_id?: string }>; + }) => { + readCount++; + if (readCount === 1) { + throw new Error('cat: /mnt/data/queued.txt: No such file or directory'); + } + + expect(session_id).toBe('sess-new'); + expect(files).toEqual(sandboxFiles); + return { content: 'hello world\n' }; + }, + ); + const writeSandboxFile = jest.fn( + async ({ + session_id, + files, + content, + }: { + session_id?: string; + files?: Array<{ id: string; name: string; storage_session_id?: string }>; + content: string; + }) => { + writeCount++; + if (writeCount === 1) { + expect(session_id).toBeUndefined(); + expect(files).toBeUndefined(); + expect(content).toBe('hello world\n'); + } else { + expect(session_id).toBe('sess-new'); + expect(files).toEqual(sandboxFiles); + expect(content).toBe('goodbye world\n'); + } + return { + stdout: `WROTE ${content.length} bytes to /mnt/data/queued.txt\n`, + session_id: 'sess-new', + files: sandboxFiles, + }; + }, + ); + const handler = makeSandboxAuthoringHandler({ + readSandboxFile, + writeSandboxFile, + }); + + const results = await invokeHandler(handler, [ + { + id: 'call_create_queued_sandbox', + name: 'create_file', + args: { + file_path: '/mnt/data/queued.txt', + content: 'hello world\n', + }, + }, + { + id: 'call_edit_queued_sandbox', + name: 'edit_file', + args: { + file_path: '/mnt/data/queued.txt', + old_text: 'hello world', + new_text: 'goodbye world', + }, + }, + ]); + + expect(results.map((result) => result.status)).toEqual(['success', 'success']); + expect(readSandboxFile).toHaveBeenCalledTimes(2); + expect(writeSandboxFile).toHaveBeenCalledTimes(2); + }); + it('rejects non-skill paths when code execution is unavailable', async () => { const writeSandboxFile = jest.fn(); const loadTools: ToolExecuteOptions['loadTools'] = jest.fn(async () => ({ diff --git a/packages/api/src/agents/handlers.ts b/packages/api/src/agents/handlers.ts index 664d0f8c86..a26ff7d495 100644 --- a/packages/api/src/agents/handlers.ts +++ b/packages/api/src/agents/handlers.ts @@ -19,7 +19,12 @@ import { logAxiosError, runOutsideTracing } from '~/utils'; import { buildSkillPrimeMessage } from './skills'; import { cleanCodeToolOutput } from './cleanup'; import { primeSkillFiles } from './skillFiles'; -import { CREATE_FILE_TOOL_NAME, EDIT_FILE_TOOL_NAME, isCodeSessionToolName } from './tools'; +import { + CREATE_FILE_TOOL_NAME, + EDIT_FILE_TOOL_NAME, + HOST_FILE_AUTHORING_ARTIFACT_KEY, + isCodeSessionToolName, +} from './tools'; import { parseFrontmatter } from '../skills/import'; export interface ToolEndCallbackData { @@ -210,7 +215,7 @@ export interface ToolExecuteOptions { readSandboxFile?: (params: { file_path: string; session_id?: string; - files?: Array<{ id: string; name: string; session_id?: string }>; + files?: Array<{ id: string; name: string; session_id?: string; storage_session_id?: string }>; req?: ServerRequest; }) => Promise<{ content: string } | null>; /** @@ -223,7 +228,7 @@ export interface ToolExecuteOptions { file_path: string; content: string; session_id?: string; - files?: Array<{ id: string; name: string; session_id?: string }>; + files?: Array<{ id: string; name: string; session_id?: string; storage_session_id?: string }>; req?: ServerRequest; }) => Promise<{ stdout?: string; @@ -471,7 +476,7 @@ type LoadedSandboxText = LoadedSkillText; type SandboxSessionContext = { session_id?: string; - files?: Array<{ id: string; name: string; session_id?: string }>; + files?: Array<{ id: string; name: string; session_id?: string; storage_session_id?: string }>; }; const MIME_MAP: Readonly> = Object.freeze({ @@ -1172,8 +1177,66 @@ async function handleSandboxFileFallback( } } -function sandboxSessionContext(tc: ToolCallRequest): SandboxSessionContext | undefined { - return tc.codeSessionContext as SandboxSessionContext | undefined; +function sandboxSessionContext( + tc: ToolCallRequest, + override?: SandboxSessionContext, +): SandboxSessionContext | undefined { + return override ?? (tc.codeSessionContext as SandboxSessionContext | undefined); +} + +function cloneSandboxSessionContext( + context: SandboxSessionContext | undefined, +): SandboxSessionContext { + return { + ...(context?.session_id ? { session_id: context.session_id } : {}), + ...(context?.files ? { files: context.files.map((file) => ({ ...file })) } : {}), + }; +} + +function mergeSandboxSessionArtifact( + context: SandboxSessionContext, + artifact: ToolExecuteResult['artifact'], +): void { + if (!artifact || typeof artifact !== 'object') { + return; + } + const value = artifact as { + session_id?: unknown; + files?: unknown; + }; + if (typeof value.session_id === 'string' && value.session_id.length > 0) { + context.session_id = value.session_id; + } + if (!Array.isArray(value.files)) { + return; + } + + const files: SandboxSessionContext['files'] = []; + for (const file of value.files) { + if (!file || typeof file !== 'object') { + continue; + } + const ref = file as { + id?: unknown; + name?: unknown; + session_id?: unknown; + storage_session_id?: unknown; + }; + if (typeof ref.id !== 'string' || typeof ref.name !== 'string') { + continue; + } + files.push({ + id: ref.id, + name: ref.name, + ...(typeof ref.session_id === 'string' ? { session_id: ref.session_id } : {}), + ...(typeof ref.storage_session_id === 'string' + ? { storage_session_id: ref.storage_session_id } + : {}), + }); + } + if (files.length > 0) { + context.files = files; + } } function isSandboxMissingFileError(error: unknown): boolean { @@ -1203,11 +1266,13 @@ async function loadSandboxTextForAuthoring({ tc, options, req, + sandboxContext, }: { filePath: string; tc: ToolCallRequest; options: ToolExecuteOptions; req?: ServerRequest; + sandboxContext?: SandboxSessionContext; }): Promise { const ext = lowercaseExtension(filePath); if (BINARY_EXTENSIONS_NEVER_READABLE.has(ext)) { @@ -1220,7 +1285,7 @@ async function loadSandboxTextForAuthoring({ }; } - const ctx = sandboxSessionContext(tc); + const ctx = sandboxSessionContext(tc, sandboxContext); try { const result = await options.readSandboxFile({ file_path: filePath, @@ -1275,6 +1340,7 @@ async function writeSandboxTextForAuthoring({ content, oldContent, created, + sandboxContext, }: { tc: ToolCallRequest; options: ToolExecuteOptions; @@ -1283,6 +1349,7 @@ async function writeSandboxTextForAuthoring({ content: string; oldContent?: string; created: boolean; + sandboxContext?: SandboxSessionContext; }): AuthoringResult { if (!options.writeSandboxFile) { return errorResult( @@ -1290,7 +1357,7 @@ async function writeSandboxTextForAuthoring({ `Sandbox file writing is not configured. Use \`bash_tool\` to write "${filePath}".`, ); } - const ctx = sandboxSessionContext(tc); + const ctx = sandboxSessionContext(tc, sandboxContext); let writeResult: Awaited>>; try { writeResult = await options.writeSandboxFile({ @@ -1318,6 +1385,7 @@ async function writeSandboxTextForAuthoring({ const summary = `${action} ${filePath} (${content.length} chars).`; return successResult(tc, diff ? `${summary}\n\n${diff}` : summary, { path: filePath, + [HOST_FILE_AUTHORING_ARTIFACT_KEY]: true, bytes_written: Buffer.byteLength(content, 'utf8'), created, ...(diff ? { diff } : {}), @@ -1369,6 +1437,13 @@ function isHostFileAuthoringToolCall( return getFileAuthoringToolNames(mergedConfigurable)?.has(toolName) === true; } +function isCodeSessionAwareToolCall( + toolName: string, + mergedConfigurable: Record, +): boolean { + return isCodeSessionToolName(toolName, getFileAuthoringToolNames(mergedConfigurable)); +} + function isSkillPrimedForAuthoring( skillName: string, mergedConfigurable: Record, @@ -1686,6 +1761,34 @@ async function inspectBundledSkillFileForCreate({ return { status: 'present', oldContent: loaded.content }; } +async function ensureBundledSkillVersionCurrent({ + tc, + options, + skill, + displayPath, +}: { + tc: ToolCallRequest; + options: ToolExecuteOptions; + skill: AuthoringSkill; + displayPath: string; +}): Promise { + if (!options.getSkillByName) { + return null; + } + + const current = await options.getSkillByName(skill.name, [skill._id], {}); + if (!current) { + return errorResult(tc, `Skill "${skill.name}" not found or not accessible.`); + } + if (current.version !== skill.version) { + return errorResult( + tc, + `Skill "${skill.name}" changed while editing. Re-read ${displayPath} and retry.`, + ); + } + return null; +} + async function writeSkillMd({ tc, options, @@ -1831,6 +1934,15 @@ async function writeBundledSkillFile({ if (!req || !options.saveSkillFileContent) { return errorResult(tc, 'Skill file writing is not configured.'); } + const staleDenied = await ensureBundledSkillVersionCurrent({ + tc, + options, + skill, + displayPath, + }); + if (staleDenied) { + return staleDenied; + } await options.saveSkillFileContent({ req, @@ -1858,6 +1970,7 @@ async function handleSandboxCreateFileCall({ filePath, content, overwrite, + sandboxContext, }: { tc: ToolCallRequest; options: ToolExecuteOptions; @@ -1865,13 +1978,20 @@ async function handleSandboxCreateFileCall({ filePath: string; content: string; overwrite: boolean; + sandboxContext?: SandboxSessionContext; }): AuthoringResult { const pathError = invalidSandboxAuthoringPath(filePath); if (pathError) { return errorResult(tc, pathError); } - const current = await loadSandboxTextForAuthoring({ filePath, tc, options, req }); + const current = await loadSandboxTextForAuthoring({ + filePath, + tc, + options, + req, + sandboxContext, + }); if (current.status === 'error') { return errorResult(tc, current.message); } @@ -1887,6 +2007,7 @@ async function handleSandboxCreateFileCall({ content, oldContent: current.status === 'loaded' ? current.content : undefined, created: current.status === 'missing', + sandboxContext, }); } @@ -1896,19 +2017,27 @@ async function handleSandboxEditFileCall({ req, filePath, edits, + sandboxContext, }: { tc: ToolCallRequest; options: ToolExecuteOptions; req?: ServerRequest; filePath: string; edits: TextEdit[]; + sandboxContext?: SandboxSessionContext; }): AuthoringResult { const pathError = invalidSandboxAuthoringPath(filePath); if (pathError) { return errorResult(tc, pathError); } - const current = await loadSandboxTextForAuthoring({ filePath, tc, options, req }); + const current = await loadSandboxTextForAuthoring({ + filePath, + tc, + options, + req, + sandboxContext, + }); if (current.status === 'missing') { return errorResult(tc, `File not found: "${filePath}"`); } @@ -1934,6 +2063,7 @@ async function handleSandboxEditFileCall({ content: edited.content, oldContent: current.content, created: false, + sandboxContext, }); if (result.status === 'success') { result.artifact = { @@ -1952,6 +2082,7 @@ async function handleCreateFileCall( options: ToolExecuteOptions, req?: ServerRequest, sourceConfigurable?: Record, + sandboxContext?: SandboxSessionContext, ): AuthoringResult { const args = tc.args as { file_path?: unknown; content?: unknown; overwrite?: unknown }; if (typeof args.file_path !== 'string' || args.file_path.length === 0) { @@ -1979,6 +2110,7 @@ async function handleCreateFileCall( filePath: args.file_path, content: args.content, overwrite, + sandboxContext, }); } @@ -2045,6 +2177,7 @@ async function handleEditFileCall( mergedConfigurable: Record, options: ToolExecuteOptions, req?: ServerRequest, + sandboxContext?: SandboxSessionContext, ): AuthoringResult { const args = tc.args as { file_path?: unknown; @@ -2074,6 +2207,7 @@ async function handleEditFileCall( req, filePath: args.file_path, edits, + sandboxContext, }); } @@ -2795,14 +2929,21 @@ export function createToolExecuteHandler(options: ToolExecuteOptions): EventHand loadedConfigurable, ); const authoringQueues = new Map>(); + const sandboxAuthoringContexts = new Map(); const results: ToolExecuteResult[] = await Promise.all( toolCalls.map(async (tc: ToolCallRequest) => { - const execute = async (): Promise => { + const execute = async ( + sandboxContext?: SandboxSessionContext, + ): Promise => { const isFileAuthoringCall = isHostFileAuthoringToolCall( tc.name, mergedConfigurable, ); + const isSandboxFileAuthoringCall = + isFileAuthoringCall && + typeof (tc.args as { file_path?: unknown }).file_path === 'string' && + !(tc.args as { file_path: string }).file_path.startsWith(SKILL_FILE_PREFIX); if ( tc.name === Constants.SKILL_TOOL || tc.name === Constants.READ_FILE || @@ -2832,6 +2973,7 @@ export function createToolExecuteHandler(options: ToolExecuteOptions): EventHand options, req, sourceConfigurable, + sandboxContext, ); } else if (tc.name === EDIT_FILE_TOOL_NAME && isFileAuthoringCall) { handlerResult = await handleEditFileCall( @@ -2839,6 +2981,7 @@ export function createToolExecuteHandler(options: ToolExecuteOptions): EventHand mergedConfigurable, options, req, + sandboxContext, ); } else { handlerResult = errorResult(tc, `Tool ${tc.name} not found`); @@ -2857,6 +3000,14 @@ export function createToolExecuteHandler(options: ToolExecuteOptions): EventHand }; } + if ( + isSandboxFileAuthoringCall && + handlerResult.status === 'success' && + sandboxContext + ) { + mergeSandboxSessionArtifact(sandboxContext, handlerResult.artifact); + } + if (toolEndCallback && handlerResult.artifact) { await toolEndCallback( { @@ -2903,7 +3054,10 @@ export function createToolExecuteHandler(options: ToolExecuteOptions): EventHand turn: tc.turn, }; - if (tc.codeSessionContext && isCodeSessionToolName(tc.name)) { + if ( + tc.codeSessionContext && + isCodeSessionAwareToolCall(tc.name, mergedConfigurable) + ) { toolCallConfig.session_id = tc.codeSessionContext.session_id; if (tc.codeSessionContext.files && tc.codeSessionContext.files.length > 0) { toolCallConfig._injected_files = tc.codeSessionContext.files; @@ -3011,7 +3165,8 @@ export function createToolExecuteHandler(options: ToolExecuteOptions): EventHand // (model context, SSE forwarding, persistence) see it. // Non-code-execution tools pass through unchanged. const cleanedContent = - isCodeSessionToolName(tc.name) && typeof result.content === 'string' + isCodeSessionAwareToolCall(tc.name, mergedConfigurable) && + typeof result.content === 'string' ? cleanCodeToolOutput(result.content) : result.content; @@ -3063,8 +3218,18 @@ export function createToolExecuteHandler(options: ToolExecuteOptions): EventHand if (!queueKey) { return await execute(); } + let sandboxContext: SandboxSessionContext | undefined; + if (queueKey.startsWith('sandbox:')) { + sandboxContext = + sandboxAuthoringContexts.get(queueKey) ?? + cloneSandboxSessionContext(sandboxSessionContext(tc)); + sandboxAuthoringContexts.set(queueKey, sandboxContext); + } const previous = authoringQueues.get(queueKey) ?? Promise.resolve(); - const resultPromise = previous.then(execute, execute); + const resultPromise = previous.then( + () => execute(sandboxContext), + () => execute(sandboxContext), + ); authoringQueues.set( queueKey, resultPromise.then( diff --git a/packages/api/src/agents/tools.spec.ts b/packages/api/src/agents/tools.spec.ts index 585a353cd3..9a4c8d268b 100644 --- a/packages/api/src/agents/tools.spec.ts +++ b/packages/api/src/agents/tools.spec.ts @@ -46,6 +46,7 @@ import { BuildToolSetConfig, registerCodeExecutionTools, registerFileAuthoringTools, + FILE_AUTHORING_TOOL_NAMES, isFileAuthoringToolDefinition, isCodeSessionToolName, } from './tools'; @@ -437,8 +438,10 @@ describe('registerFileAuthoringTools', () => { it('recognizes host-side file authoring tools as code-session-aware without mutating the shared set', () => { expect(isCodeSessionToolName('bash_tool')).toBe(true); - expect(isCodeSessionToolName('create_file')).toBe(true); - expect(isCodeSessionToolName('edit_file')).toBe(true); + expect(isCodeSessionToolName('create_file')).toBe(false); + expect(isCodeSessionToolName('edit_file')).toBe(false); + expect(isCodeSessionToolName('create_file', FILE_AUTHORING_TOOL_NAMES)).toBe(true); + expect(isCodeSessionToolName('edit_file', FILE_AUTHORING_TOOL_NAMES)).toBe(true); expect(CODE_EXECUTION_TOOLS.has('create_file')).toBe(false); expect(CODE_EXECUTION_TOOLS.has('edit_file')).toBe(false); }); diff --git a/packages/api/src/agents/tools.ts b/packages/api/src/agents/tools.ts index df43826006..d37dd80ca8 100644 --- a/packages/api/src/agents/tools.ts +++ b/packages/api/src/agents/tools.ts @@ -8,13 +8,17 @@ import type { LCTool, LCToolRegistry } from '@librechat/agents'; export const CREATE_FILE_TOOL_NAME = 'create_file'; export const EDIT_FILE_TOOL_NAME = 'edit_file'; +export const HOST_FILE_AUTHORING_ARTIFACT_KEY = '__librechat_file_authoring'; export const FILE_AUTHORING_TOOL_NAMES: ReadonlySet = new Set([ CREATE_FILE_TOOL_NAME, EDIT_FILE_TOOL_NAME, ]); -export function isCodeSessionToolName(name: string): boolean { - return CODE_EXECUTION_TOOLS.has(name) || FILE_AUTHORING_TOOL_NAMES.has(name); +export function isCodeSessionToolName( + name: string, + hostFileAuthoringToolNames?: ReadonlySet, +): boolean { + return CODE_EXECUTION_TOOLS.has(name) || hostFileAuthoringToolNames?.has(name) === true; } interface ToolDefLike {