diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts index 8748fe2004..9a1071e8d0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.workspace-service.ts @@ -175,7 +175,10 @@ export class WorkflowSchemaWorkspaceService { workspaceId: string; workflowVersionId: string; }): Promise { - const BACKEND_ENRICHED_TYPES = [WorkflowActionType.ITERATOR]; + const BACKEND_ENRICHED_TYPES = [ + WorkflowActionType.ITERATOR, + WorkflowActionType.AI_AGENT, + ]; if (!BACKEND_ENRICHED_TYPES.includes(step.type)) { return step; diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/update-agent.tool.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/update-agent.tool.spec.ts new file mode 100644 index 0000000000..76a8ec58ed --- /dev/null +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/__tests__/update-agent.tool.spec.ts @@ -0,0 +1,151 @@ +import { createUpdateAgentTool } from 'src/modules/workflow/workflow-tools/tools/update-agent.tool'; + +const AGENT_ID = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'; +const WORKSPACE_ID = 'workspace-id'; + +const buildAiAgentStep = (agentId: string, stepId = 'step-1') => ({ + id: stepId, + name: 'AI Agent', + type: 'AI_AGENT', + valid: true, + settings: { input: { agentId }, outputSchema: {} }, +}); + +const buildTool = ({ + draftVersions = [], +}: { + draftVersions?: { id: string; status: string; steps: unknown[] | null }[]; +} = {}) => { + const agentService = { + updateOneAgent: jest.fn().mockResolvedValue({ id: AGENT_ID }), + }; + const workflowVersionStepService = { + updateWorkflowVersionStep: jest.fn().mockResolvedValue(undefined), + }; + const workflowVersionRepository = { + find: jest.fn().mockResolvedValue(draftVersions), + }; + const globalWorkspaceOrmManager = { + getRepository: jest.fn().mockResolvedValue(workflowVersionRepository), + }; + const flatEntityMapsCacheService = { + invalidateFlatEntityMaps: jest.fn().mockResolvedValue(undefined), + }; + + const tool = createUpdateAgentTool( + { + agentService, + workflowVersionStepService, + globalWorkspaceOrmManager, + flatEntityMapsCacheService, + } as never, + { workspaceId: WORKSPACE_ID }, + ); + + return { + tool, + agentService, + workflowVersionStepService, + globalWorkspaceOrmManager, + flatEntityMapsCacheService, + }; +}; + +describe('createUpdateAgentTool', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should resync the linked AI_AGENT step output schema when responseFormat changes', async () => { + const step = buildAiAgentStep(AGENT_ID); + const { tool, workflowVersionStepService, flatEntityMapsCacheService } = + buildTool({ + draftVersions: [{ id: 'version-1', status: 'DRAFT', steps: [step] }], + }); + + const result = (await tool.execute({ + agentId: AGENT_ID, + responseFormat: { + type: 'json', + schema: { + type: 'object', + properties: { summary: { type: 'string' } }, + }, + }, + } as never)) as Record; + + expect(result.success).toBe(true); + expect( + flatEntityMapsCacheService.invalidateFlatEntityMaps, + ).toHaveBeenCalledWith({ + workspaceId: WORKSPACE_ID, + flatMapsKeys: ['flatAgentMaps'], + }); + expect( + workflowVersionStepService.updateWorkflowVersionStep, + ).toHaveBeenCalledWith({ + workspaceId: WORKSPACE_ID, + workflowVersionId: 'version-1', + step, + }); + }); + + it('should not resync when responseFormat is not provided', async () => { + const step = buildAiAgentStep(AGENT_ID); + const { tool, workflowVersionStepService, globalWorkspaceOrmManager } = + buildTool({ + draftVersions: [{ id: 'version-1', status: 'DRAFT', steps: [step] }], + }); + + const result = (await tool.execute({ + agentId: AGENT_ID, + prompt: 'You are a helpful assistant', + } as never)) as Record; + + expect(result.success).toBe(true); + expect(globalWorkspaceOrmManager.getRepository).not.toHaveBeenCalled(); + expect( + workflowVersionStepService.updateWorkflowVersionStep, + ).not.toHaveBeenCalled(); + }); + + it('should skip steps referencing a different agent', async () => { + const { tool, workflowVersionStepService } = buildTool({ + draftVersions: [ + { + id: 'version-1', + status: 'DRAFT', + steps: [buildAiAgentStep('another-agent-id')], + }, + ], + }); + + await tool.execute({ + agentId: AGENT_ID, + responseFormat: { type: 'text' }, + } as never); + + expect( + workflowVersionStepService.updateWorkflowVersionStep, + ).not.toHaveBeenCalled(); + }); + + it('should still report agent update success when the resync fails', async () => { + const step = buildAiAgentStep(AGENT_ID); + const { tool, workflowVersionStepService } = buildTool({ + draftVersions: [{ id: 'version-1', status: 'DRAFT', steps: [step] }], + }); + + workflowVersionStepService.updateWorkflowVersionStep.mockRejectedValue( + new Error('boom'), + ); + + const result = (await tool.execute({ + agentId: AGENT_ID, + responseFormat: { type: 'text' }, + } as never)) as Record; + + expect(result.success).toBe(true); + expect(result.message).toContain('failed to resync'); + }); +}); diff --git a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-agent.tool.ts b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-agent.tool.ts index 179d3a30ae..9f1ff3b5d0 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-agent.tool.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-tools/tools/update-agent.tool.ts @@ -1,8 +1,13 @@ import { isDefined } from 'twenty-shared/utils'; +import { WorkflowActionType } from 'twenty-shared/workflow'; import { z } from 'zod'; import { type AgentResponseFormat } from 'src/engine/metadata-modules/ai/ai-agent/types/agent-response-format.type'; import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/types/model-id.type'; +import { + WorkflowVersionStatus, + type WorkflowVersionWorkspaceEntity, +} from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity'; import { type WorkflowToolContext, type WorkflowToolDependencies, @@ -53,8 +58,64 @@ const updateAgentSchema = z.object({ ), }); +const resyncAiAgentStepOutputSchemas = async ( + deps: Pick< + WorkflowToolDependencies, + | 'workflowVersionStepService' + | 'globalWorkspaceOrmManager' + | 'flatEntityMapsCacheService' + >, + { workspaceId, agentId }: { workspaceId: string; agentId: string }, +): Promise => { + await deps.flatEntityMapsCacheService.invalidateFlatEntityMaps({ + workspaceId, + flatMapsKeys: ['flatAgentMaps'], + }); + + const workflowVersionRepository = + await deps.globalWorkspaceOrmManager.getRepository( + workspaceId, + 'workflowVersion', + { shouldBypassPermissionChecks: true }, + ); + + const draftVersions = await workflowVersionRepository.find({ + where: { status: WorkflowVersionStatus.DRAFT }, + }); + + for (const version of draftVersions) { + const steps = version.steps; + + if (!isDefined(steps)) { + continue; + } + + const matchingStep = steps.find( + (step) => + step.type === WorkflowActionType.AI_AGENT && + step.settings?.input?.agentId === agentId, + ); + + if (!isDefined(matchingStep)) { + continue; + } + + await deps.workflowVersionStepService.updateWorkflowVersionStep({ + workspaceId, + workflowVersionId: version.id, + step: matchingStep, + }); + } +}; + export const createUpdateAgentTool = ( - deps: Pick, + deps: Pick< + WorkflowToolDependencies, + | 'agentService' + | 'workflowVersionStepService' + | 'globalWorkspaceOrmManager' + | 'flatEntityMapsCacheService' + >, context: WorkflowToolContext, ) => ({ name: 'update_agent' as const, @@ -88,6 +149,18 @@ To find the agentId, look at the AI_AGENT step's settings.input.agentId field.`, workspaceId, }); + if (isDefined(responseFormat)) { + try { + await resyncAiAgentStepOutputSchemas(deps, { workspaceId, agentId }); + } catch (resyncError) { + return { + success: true, + message: `Successfully updated agent ${agentId}, but failed to resync workflow step output schema: ${resyncError.message}`, + agentId: updatedAgent.id, + }; + } + } + return { success: true, message: `Successfully updated agent ${agentId}`,