From a276f3277f002db029bda763f87efd27fbd31249 Mon Sep 17 00:00:00 2001 From: Etienne <45695613+etiennejouan@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:48:24 +0200 Subject: [PATCH] feat(workflow): pin concrete model on AI agent node creation and exclude interactive tools from workflow runs (#23447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context The AI Agent workflow node's model dropdown could show a model that was not the one used at run time (e.g. the node displayed "Claude Haiku 4.5" while the run log showed `openai/gpt-5.6-sol`). Root cause: workflow agents were created with `modelId: AUTO_SELECT_SMART_MODEL_ID`. The builder's model `Select` cannot represent that value — auto-select ids are filtered out of the options (`useWorkspaceAiModelAvailability`) and the pinned "default" option remaps its value to the resolved concrete model id (`useAiModelOptions`) — so `Select` silently fell back to `options[0]`, the alphabetically first enabled model. Meanwhile the runtime correctly resolved auto-select to the instance's default smart model. ## What this PR does ### 1. New workflow agents store a concrete model id `WorkflowVersionStepOperationsWorkspaceService` now reads the workspace's `fastModel` setting, expands it through `AiModelRegistryService.getEffectiveModelConfig`, validates it with `validateModelAvailability`, and stores the concrete model id — so the dropdown displays the model that will actually run, and workflow agents default to the cheaper fast tier instead of the smart one. Falls back to `AUTO_SELECT_FAST_MODEL_ID` if the lookup or validation fails (workspace missing, no AI provider configured, model disabled), so node creation never breaks. ### 2. Exclude `search_help_center` and `navigate_app` from workflow agent runs `ActionToolProvider` adds both tools unconditionally, but they only make sense in an interactive chat session (navigation targets the user's browser; help-center search is a support tool). They are now excluded via `WORKFLOW_AGENT_EXCLUDED_TOOL_NAMES` in `AgentAsyncExecutorService`, alongside the existing output-navigation exclusions. Chat agents are unaffected. ## Test coverage - Existing specs for `WorkflowVersionStepOperationsWorkspaceService` and `AgentAsyncExecutorService` updated/passing (new constructor deps mocked). - `nx typecheck twenty-server` and `lint:diff-with-main` pass. Review in cubic --- ...orkflow-agent-excluded-tool-names.const.ts | 6 ++++ ...-step-operations.workspace-service.spec.ts | 21 ++++++++++++ ...rsion-step-operations.workspace-service.ts | 32 ++++++++++++++++--- .../workflow-version-step.module.ts | 7 +++- 4 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/constants/workflow-agent-excluded-tool-names.const.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/constants/workflow-agent-excluded-tool-names.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/constants/workflow-agent-excluded-tool-names.const.ts new file mode 100644 index 0000000000..db8dc6154c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/constants/workflow-agent-excluded-tool-names.const.ts @@ -0,0 +1,6 @@ +import { type ActionToolId } from 'src/engine/core-modules/tool-provider/constants/action-tool-label.constant'; + +export const WORKFLOW_AGENT_EXCLUDED_TOOL_NAMES = [ + 'search_help_center', + 'navigate_app', +] as const satisfies readonly ActionToolId[]; diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts index ecd3940a6e..e49ad60f00 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts @@ -6,7 +6,9 @@ import { SEED_WORKFLOW_ACTION_TRIGGER_SETTINGS } from 'twenty-shared/logic-funct import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service'; import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service'; +import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service'; import { WorkflowVersionCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-version-core-sync.service'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity'; @@ -34,6 +36,8 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => { let workflowCommonWorkspaceService: jest.Mocked; let aiAgentRoleService: jest.Mocked; let workspaceCacheService: jest.Mocked; + let aiModelRegistryService: jest.Mocked; + let workspaceRepository: jest.Mocked; beforeEach(async () => { codeStepBuildService = { @@ -123,6 +127,15 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => { flush: jest.fn(), } as unknown as jest.Mocked; + aiModelRegistryService = { + getEffectiveModelConfig: jest.fn(), + validateModelAvailability: jest.fn(), + } as unknown as jest.Mocked; + + workspaceRepository = { + findOneBy: jest.fn().mockResolvedValue(null), + }; + const module: TestingModule = await Test.createTestingModule({ providers: [ WorkflowVersionStepOperationsWorkspaceService, @@ -150,6 +163,14 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => { provide: getRepositoryToken(ObjectMetadataEntity), useValue: objectMetadataRepository, }, + { + provide: getRepositoryToken(WorkspaceEntity), + useValue: workspaceRepository, + }, + { + provide: AiModelRegistryService, + useValue: aiModelRegistryService, + }, { provide: WorkflowCommonWorkspaceService, useValue: workflowCommonWorkspaceService, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts index ceec1313b7..96dec4bb57 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts @@ -10,9 +10,9 @@ import { import { isDefined, isValidUuid } from 'twenty-shared/utils'; import { IF_ELSE_BRANCH_POSITION_OFFSETS, + WorkflowActionType, getFunctionInputFromInputSchema, type StepIfElseBranch, - WorkflowActionType, } from 'twenty-shared/workflow'; import { Repository } from 'typeorm'; import { v4 } from 'uuid'; @@ -20,14 +20,16 @@ import { v4 } from 'uuid'; import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position.input'; import { WorkflowVersionCoreSyncService } from 'src/engine/core-modules/workflow/services/workflow-version-core-sync.service'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service'; import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service'; +import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service'; import { LogicFunctionException, LogicFunctionExceptionCode, } from 'src/engine/metadata-modules/logic-function/logic-function.exception'; +import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service'; import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; @@ -50,7 +52,6 @@ import { type WorkflowEmptyAction, type WorkflowFormAction, } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type'; -import { AUTO_SELECT_SMART_MODEL_ID } from 'twenty-shared/constants'; const BASE_STEP_DEFINITION: BaseWorkflowActionSettings = { outputSchema: {}, errorHandlingOptions: { @@ -77,10 +78,13 @@ export class WorkflowVersionStepOperationsWorkspaceService { private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService, private readonly codeStepBuildService: CodeStepBuildService, private readonly agentService: AgentService, + private readonly aiModelRegistryService: AiModelRegistryService, @InjectWorkspaceScopedRepository(RoleTargetEntity) private readonly roleTargetRepository: WorkspaceScopedRepository, @InjectRepository(ObjectMetadataEntity) private readonly objectMetadataRepository: Repository, + @InjectRepository(WorkspaceEntity) + private readonly workspaceRepository: Repository, private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, private readonly aiAgentRoleService: AiAgentRoleService, private readonly workspaceCacheService: WorkspaceCacheService, @@ -88,6 +92,26 @@ export class WorkflowVersionStepOperationsWorkspaceService { private readonly workflowVersionCoreSyncService: WorkflowVersionCoreSyncService, ) {} + private async getWorkspaceDefaultFastModelId( + workspaceId: string, + ): Promise { + const workspace = await this.workspaceRepository.findOneBy({ + id: workspaceId, + }); + + if (!isDefined(workspace)) { + throw new WorkflowVersionStepException( + `Workspace ${workspaceId} not found`, + WorkflowVersionStepExceptionCode.NOT_FOUND, + ); + } + + const effectiveModelConfig = + this.aiModelRegistryService.getEffectiveModelConfig(workspace.fastModel); + + return effectiveModelConfig.modelId; + } + async runWorkflowVersionStepDeletionSideEffects({ step, workspaceId, @@ -547,7 +571,7 @@ export class WorkflowVersionStepOperationsWorkspaceService { description: '', prompt: 'You are a helpful AI assistant. Complete the task based on the workflow context.', - modelId: AUTO_SELECT_SMART_MODEL_ID, + modelId: await this.getWorkspaceDefaultFastModelId(workspaceId), responseFormat: { type: 'text' }, isCustom: true, }, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts index ccf425079b..a3115d69d3 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { WorkflowVersionCoreModule } from 'src/engine/core-modules/workflow/workflow-version-core.module'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module'; import { AiAgentModule } from 'src/engine/metadata-modules/ai/ai-agent/ai-agent.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; @@ -29,7 +30,11 @@ import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workfl AiAgentRoleModule, AiAgentModule, WorkspaceCacheModule, - TypeOrmModule.forFeature([ObjectMetadataEntity, RoleTargetEntity]), + TypeOrmModule.forFeature([ + ObjectMetadataEntity, + RoleTargetEntity, + WorkspaceEntity, + ]), WorkspaceManyOrAllFlatEntityMapsCacheModule, WorkflowVersionCoreModule, ],