import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' import { assertServerToolNotAborted, type BaseServerTool, type ServerToolContext, } from '@/lib/copilot/tools/server/base-tool' import { isE2BDocEnabled } from '@/lib/core/config/env-flags' import { updateWorkspaceFileContent } from '@/lib/uploads/contexts/workspace/workspace-file-manager' import { getE2BDocFormat } from './doc-compile' import { buildEmbeddedImageRefWarning } from './embedded-image-refs' import { consumeLatestFileIntent } from './file-intent-store' import { compileDocForWrite, getDocumentFormatInfo, inferContentType } from './workspace-file' const logger = createLogger('EditContentServerTool') type EditContentArgs = { content: string } type EditContentResult = { success: boolean message: string data?: Record } export const editContentServerTool: BaseServerTool = { name: 'edit_content', async execute(params: EditContentArgs, context?: ServerToolContext): Promise { if (!context?.userId) { logger.error('Unauthorized attempt to use edit_content') throw new Error('Authentication required') } const workspaceId = context.workspaceId if (!workspaceId) { return { success: false, message: 'Workspace ID is required' } } const raw = params as Record const nested = raw.args as Record | undefined const content = typeof params.content === 'string' ? params.content : typeof nested?.content === 'string' ? (nested.content as string) : undefined if (content === undefined) { return { success: false, message: 'content is required for edit_content' } } // Consume the intent from THIS file subagent's channel (its outer tool_use // id), not just the latest in the message — otherwise two file agents // writing concurrently would each grab whichever workspace_file landed last // and write their content into the wrong file. Falls back to latest-in- // message when no channel id is present (main-agent / legacy calls). const intent = await consumeLatestFileIntent(workspaceId, { chatId: context.chatId, messageId: context.messageId, channelId: context.parentToolCallId, }) if (!intent) { return { success: false, message: 'No workspace_file context found. Call workspace_file first, wait for it to succeed, then call edit_content in the next step. Do not emit edit_content in parallel or in the same batch as workspace_file.', } } try { const { operation, fileRecord } = intent const docInfo = getDocumentFormatInfo(fileRecord.name) const e2bFmt = isE2BDocEnabled ? await getE2BDocFormat(fileRecord.name) : null let finalContent: string switch (operation) { case 'append': { const existing = intent.existingContent ?? '' // The JS engines (isolated-vm and E2B-node pptx/docx) use the `{ ... }` // block-append convention — block statements scope cleanly inside the // compile wrapper. Python docs (pdf/xlsx) are a single cohesive script, // so brace-wrapping would produce invalid Python; plain-concatenate. // Brace-wrap appended content for the JS engines (isolated-vm and // E2B-node pptx/docx); Python docs (pdf/xlsx) are one cohesive script. const braceWrap = e2bFmt ? e2bFmt.engine === 'node' : docInfo.isDoc finalContent = braceWrap ? existing ? `${existing}\n{\n${content}\n}` : content : existing ? `${existing}\n${content}` : content break } case 'update': { finalContent = content break } case 'patch': { const existing = intent.existingContent ?? '' if (!intent.edit) { return { success: false, message: 'Patch intent missing edit metadata' } } if (intent.edit.strategy === 'search_replace') { const search = intent.edit.search! const firstIdx = existing.indexOf(search) if (firstIdx === -1) { return { success: false, message: `Patch failed: search string not found in file "${fileRecord.name}"`, } } finalContent = intent.edit.replaceAll ? existing.split(search).join(content) : existing.slice(0, firstIdx) + content + existing.slice(firstIdx + search.length) } else if (intent.edit.strategy === 'anchored') { const lines = existing.split('\n') const defaultOccurrence = intent.edit.occurrence ?? 1 const findAnchorLine = ( anchor: string, occurrence = defaultOccurrence, afterIndex = -1 ): { index: number; error?: string } => { const trimmed = anchor.trim() let count = 0 for (let i = afterIndex + 1; i < lines.length; i++) { if (lines[i].trim() === trimmed) { count++ if (count === occurrence) return { index: i } } } if (count === 0) { return { index: -1, error: `Anchor line not found in "${fileRecord.name}": "${anchor.slice(0, 100)}"`, } } return { index: -1, error: `Anchor line occurrence ${occurrence} not found (only ${count} match${count > 1 ? 'es' : ''}) in "${fileRecord.name}": "${anchor.slice(0, 100)}"`, } } if (intent.edit.mode === 'replace_between') { if (!intent.edit.before_anchor || !intent.edit.after_anchor) { return { success: false, message: 'replace_between requires before_anchor and after_anchor', } } const before = findAnchorLine(intent.edit.before_anchor) if (before.error) return { success: false, message: `Patch failed: ${before.error}` } const after = findAnchorLine( intent.edit.after_anchor, defaultOccurrence, before.index ) if (after.error) return { success: false, message: `Patch failed: ${after.error}` } if (after.index <= before.index) { return { success: false, message: 'Patch failed: after_anchor must appear after before_anchor in the file', } } const newLines = [ ...lines.slice(0, before.index + 1), ...content.split('\n'), ...lines.slice(after.index), ] finalContent = newLines.join('\n') } else if (intent.edit.mode === 'insert_after') { if (!intent.edit.anchor) { return { success: false, message: 'insert_after requires anchor' } } const found = findAnchorLine(intent.edit.anchor) if (found.error) return { success: false, message: `Patch failed: ${found.error}` } const newLines = [ ...lines.slice(0, found.index + 1), ...content.split('\n'), ...lines.slice(found.index + 1), ] finalContent = newLines.join('\n') } else if (intent.edit.mode === 'delete_between') { if (!intent.edit.start_anchor || !intent.edit.end_anchor) { return { success: false, message: 'delete_between requires start_anchor and end_anchor', } } const start = findAnchorLine(intent.edit.start_anchor) if (start.error) return { success: false, message: `Patch failed: ${start.error}` } const end = findAnchorLine(intent.edit.end_anchor, defaultOccurrence, start.index) if (end.error) return { success: false, message: `Patch failed: ${end.error}` } if (end.index <= start.index) { return { success: false, message: 'Patch failed: end_anchor must appear after start_anchor in the file', } } const newLines = [...lines.slice(0, start.index), ...lines.slice(end.index)] finalContent = newLines.join('\n') } else { return { success: false, message: `Unknown anchored patch mode: "${intent.edit.mode}"`, } } } else { return { success: false, message: `Unknown patch strategy: "${intent.edit.strategy}"` } } break } default: return { success: false, message: `Unsupported operation in intent: ${operation}` } } // Compile once via the right engine (or isolated-vm fallback) and resolve // the source MIME to store. Shared with the create path. const compiled = await compileDocForWrite({ source: finalContent, fileName: fileRecord.name, workspaceId, ownerKey: `user:${context.userId}`, signal: context.abortSignal, fallbackMime: inferContentType(fileRecord.name, intent.contentType), }) if (!compiled.ok) { return { success: false, message: compiled.message } } const fileBuffer = Buffer.from(finalContent, 'utf-8') assertServerToolNotAborted(context) await updateWorkspaceFileContent( workspaceId, intent.fileId, context.userId, fileBuffer, compiled.sourceMime ) const verb = operation === 'append' ? 'appended to' : operation === 'update' ? 'updated' : 'patched' logger.info(`Workspace file ${verb} via copilot (edit_content)`, { fileId: intent.fileId, name: fileRecord.name, operation, size: fileBuffer.length, userId: context.userId, }) // Flag any `/api/files/view/` embeds the model just authored that won't render/export // (non-workspace or missing), so it can self-correct on the next step. const embedWarning = await buildEmbeddedImageRefWarning(content, workspaceId) return { success: true, message: `File "${fileRecord.name}" ${verb} successfully (${fileBuffer.length} bytes)${embedWarning}`, data: { id: intent.fileId, name: fileRecord.name, size: fileBuffer.length, contentType: compiled.sourceMime, }, } } catch (error) { const errorMessage = getErrorMessage(error, 'Unknown error occurred') logger.error('Error in edit_content tool', { operation: intent.operation, fileId: intent.fileId, error: errorMessage, userId: context.userId, }) return { success: false, message: `Failed to apply content: ${errorMessage}`, } } }, }