From fb7b2b8811e3af32e8b79c0bda0878e387542bbe Mon Sep 17 00:00:00 2001 From: Justin Blumencranz <96924014+j15z@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:30:56 -0700 Subject: [PATCH] fix(files): guide retries after consumed edit intent --- .../tools/server/files/edit-content.test.ts | 58 +++++++++++++ .../tools/server/files/edit-content.ts | 87 +++++++++++-------- 2 files changed, 107 insertions(+), 38 deletions(-) diff --git a/apps/sim/lib/copilot/tools/server/files/edit-content.test.ts b/apps/sim/lib/copilot/tools/server/files/edit-content.test.ts index 432adbefa1d..90ffbbcc830 100644 --- a/apps/sim/lib/copilot/tools/server/files/edit-content.test.ts +++ b/apps/sim/lib/copilot/tools/server/files/edit-content.test.ts @@ -124,4 +124,62 @@ describe('edit_content', () => { { fileId: 'pdf-1' } ) }) + + it('tells the agent to create a new intent when an edit fails after consuming one', async () => { + consumeLatestFileIntentMock.mockResolvedValue({ + operation: 'patch', + fileId: 'pdf-1', + workspaceId: 'workspace-1', + userId: 'user-1', + fileRecord: { id: 'pdf-1', name: 'report.pdf' }, + createdAt: Date.now(), + }) + + const result = await editContentServerTool.execute({ content: 'replacement' }, context) + + expect(result).toEqual({ + success: false, + message: + 'Patch intent missing edit metadata. The workspace_file intent was consumed; call workspace_file again before retrying edit_content.', + }) + }) + + it('keeps the existing first-use guidance when no intent was consumed', async () => { + consumeLatestFileIntentMock.mockResolvedValue(undefined) + + const result = await editContentServerTool.execute({ content: 'replacement' }, context) + + expect(result).toEqual({ + 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.', + }) + }) + + it('adds the recovery guidance when document compilation returns an error', async () => { + compileDocForWriteMock.mockResolvedValue({ + ok: false, + message: 'PDF compilation failed', + }) + + const result = await editContentServerTool.execute({ content: 'source' }, context) + + expect(result).toEqual({ + success: false, + message: + 'PDF compilation failed. The workspace_file intent was consumed; call workspace_file again before retrying edit_content.', + }) + }) + + it('adds the recovery guidance when editing throws after consuming an intent', async () => { + compileDocForWriteMock.mockRejectedValue(new Error('sandbox unavailable')) + + const result = await editContentServerTool.execute({ content: 'source' }, context) + + expect(result).toEqual({ + success: false, + message: + 'Failed to edit file content. The workspace_file intent was consumed; call workspace_file again before retrying edit_content.', + }) + }) }) diff --git a/apps/sim/lib/copilot/tools/server/files/edit-content.ts b/apps/sim/lib/copilot/tools/server/files/edit-content.ts index 1a0fe9ec514..b7d6ba807a9 100644 --- a/apps/sim/lib/copilot/tools/server/files/edit-content.ts +++ b/apps/sim/lib/copilot/tools/server/files/edit-content.ts @@ -29,6 +29,18 @@ type EditContentResult = { data?: Record } +const CONSUMED_FILE_INTENT_RETRY_GUIDANCE = + 'The workspace_file intent was consumed; call workspace_file again before retrying edit_content.' + +function consumedFileIntentFailure(message: string): EditContentResult { + const normalizedMessage = message.trimEnd() + const separator = /[.!?]$/.test(normalizedMessage) ? ' ' : '. ' + return { + success: false, + message: `${normalizedMessage}${separator}${CONSUMED_FILE_INTENT_RETRY_GUIDANCE}`, + } +} + export const editContentServerTool: BaseServerTool = { name: 'edit_content', async execute(params: EditContentArgs, context?: ServerToolContext): Promise { @@ -105,17 +117,16 @@ export const editContentServerTool: BaseServerTool