From b77c44fd20e4593a47363134757e1d8dc4f114ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Wed, 22 Apr 2026 12:32:03 +0200 Subject: [PATCH] refactor(tool-provider): dedupe descriptor/generator paths (#19960) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Every tool provider used to implement `generateDescriptors()` **and** register a category generator at `onModuleInit()` that re-ran the same factories at execute time. `ToolExecutorService` carried two registries (`staticToolHandlers`, `categoryGenerators`) to route between them. - Providers now own execution of their own tools via a new `executeStaticTool()` method. `ToolExecutorService` drops both maps and delegates by `descriptor.category`. Each factory-backed provider has a single `buildToolSet()` used by both descriptor generation and execution. - Extracts `resolveObjectIcon` shared util (was duplicated verbatim in workflow + dashboard providers), and deletes the orphaned `ToolGeneratorModule` whose consumers were removed in the earlier AI chat simplification refactor. No behavior change. Same factories run, same permission checks, same tools execute. Net diff: 18 files, +311 / −480. ## Key changes - `ToolProvider` interface gains `executeStaticTool(name, args, context)`. - `ToolExecutorService` loses its `staticToolHandlers` and `categoryGenerators` maps, injects `TOOL_PROVIDERS`, and does `providers.find(p => p.category === descriptor.category).executeStaticTool(...)` for `kind: 'static'` descriptors. - `ActionToolProvider` drops the register-handler loop in its constructor; `executeStaticTool` looks up in the existing `toolMap`. - `View`, `Metadata`, `Workflow`, `Dashboard`, `ViewField` providers each have a single `buildToolSet(context)` private method used by both `generateDescriptors` and `executeStaticTool`. No more `onModuleInit`, no `ToolExecutorService` dependency. - `DatabaseToolProvider` and `LogicFunctionToolProvider` implement `executeStaticTool` with an invariant-violation throw — they only emit `database_crud` / `logic_function` kinds, so the static-tool path is unreachable for them. - Deletes `tool-generator/` (dead code — zero consumers). ## Dependency graph before/after **Before:** provider → `ToolExecutorService` (for `register*` calls) **After:** `ToolExecutorService` → `TOOL_PROVIDERS` → providers. Cleaner, no cycle. ## Test plan - [ ] `npx nx typecheck twenty-server` passes (verified: same 7 pre-existing unrelated errors) - [ ] `npx nx lint twenty-server` passes - [ ] AI chat: trigger a tool call that hits `execute_tool` fallback (e.g. a view/metadata tool not in the preloaded set) — verify it still executes - [ ] AI chat: trigger a preloaded action tool (e.g. `search_help_center`) — verify it still executes - [ ] MCP: `tools/list` and `tools/call` for both preloaded and catalog-discovered tools - [ ] Workflow AI agent: run a workflow with AI agent step that calls DATABASE_CRUD tools - [ ] Verify the `web_search` / `code_interpreter` tools (if enabled) still dispatch correctly --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .../per-object-tool-generator.service.ts | 134 ------------------ .../tool-generator/tool-generator.module.ts | 13 -- .../types/tool-generator.types.ts | 33 ----- .../static-tool-handler.interface.ts | 7 - .../interfaces/tool-provider.interface.ts | 13 ++ .../providers/action-tool.provider.ts | 41 +++--- .../providers/dashboard-tool.provider.ts | 82 +++++------ .../providers/database-tool.provider.ts | 15 ++ .../providers/logic-function-tool.provider.ts | 15 ++ .../providers/metadata-tool.provider.ts | 49 ++++--- .../providers/view-field-tool.provider.ts | 78 +++++----- .../providers/view-tool.provider.ts | 103 +++++--------- .../providers/workflow-tool.provider.ts | 82 +++++------ .../services/tool-executor.service.ts | 70 ++++----- .../tool-provider/tool-provider.module.ts | 2 - .../types/category-tool-generator.type.ts | 7 - .../utils/execute-tool-from-tool-set.util.ts | 32 +++++ .../utils/resolve-object-icon.util.ts | 22 +++ 18 files changed, 320 insertions(+), 478 deletions(-) delete mode 100644 packages/twenty-server/src/engine/core-modules/tool-generator/services/per-object-tool-generator.service.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/tool-generator/tool-generator.module.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/tool-generator/types/tool-generator.types.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/tool-provider/interfaces/static-tool-handler.interface.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/tool-provider/types/category-tool-generator.type.ts create mode 100644 packages/twenty-server/src/engine/core-modules/tool-provider/utils/execute-tool-from-tool-set.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/tool-provider/utils/resolve-object-icon.util.ts diff --git a/packages/twenty-server/src/engine/core-modules/tool-generator/services/per-object-tool-generator.service.ts b/packages/twenty-server/src/engine/core-modules/tool-generator/services/per-object-tool-generator.service.ts deleted file mode 100644 index 01f8234270..0000000000 --- a/packages/twenty-server/src/engine/core-modules/tool-generator/services/per-object-tool-generator.service.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { type ToolSet } from 'ai'; -import { isDefined } from 'twenty-shared/utils'; - -import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util'; -import { - type ObjectWithPermission, - type ToolFactory, - type ToolGeneratorContext, -} from 'src/engine/core-modules/tool-generator/types/tool-generator.types'; -import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util'; -import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; -import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util'; -import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; - -@Injectable() -export class PerObjectToolGeneratorService { - private readonly logger = new Logger(PerObjectToolGeneratorService.name); - - constructor( - private readonly workspaceCacheService: WorkspaceCacheService, - private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, - ) {} - - // Generate tools by iterating over workspace objects once and applying all factories - async generate( - context: ToolGeneratorContext, - factories: ToolFactory[], - ): Promise { - const objects = await this.getObjectsWithPermissions( - context.workspaceId, - context.rolePermissionConfig, - ); - - const tools: ToolSet = {}; - - for (const objectWithPermission of objects) { - for (const factory of factories) { - Object.assign(tools, factory(objectWithPermission, context)); - } - } - - this.logger.log( - `Generated ${Object.keys(tools).length} tools from ${factories.length} factories for ${objects.length} objects`, - ); - - return tools; - } - - // Get workspace objects with their permissions - async getObjectsWithPermissions( - workspaceId: string, - rolePermissionConfig: RolePermissionConfig, - ): Promise { - const { rolesPermissions } = - await this.workspaceCacheService.getOrRecompute(workspaceId, [ - 'rolesPermissions', - ]); - - let objectPermissions; - - if ('unionOf' in rolePermissionConfig) { - if (rolePermissionConfig.unionOf.length === 1) { - objectPermissions = rolesPermissions[rolePermissionConfig.unionOf[0]]; - } else { - throw new Error( - 'Union permission logic for multiple roles not yet implemented', - ); - } - } else if ('intersectionOf' in rolePermissionConfig) { - const allRolePermissions = rolePermissionConfig.intersectionOf.map( - (roleId: string) => rolesPermissions[roleId], - ); - - objectPermissions = - allRolePermissions.length === 1 - ? allRolePermissions[0] - : computePermissionIntersection(allRolePermissions); - } else { - return []; - } - - const { flatObjectMetadataMaps, flatFieldMetadataMaps } = - await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps', 'flatFieldMetadataMaps'], - }, - ); - - const allFlatObjects = Object.values( - flatObjectMetadataMaps.byUniversalIdentifier, - ) - .filter(isDefined) - .filter((obj) => obj.isActive && !obj.isSystem); - - const allObjectMetadata = allFlatObjects.map((flatObject) => ({ - ...flatObject, - fields: getFlatFieldsFromFlatObjectMetadata( - flatObject, - flatFieldMetadataMaps, - ), - })); - - // Filter out workflow-related objects - const filteredObjectMetadata = allObjectMetadata.filter( - (objectMetadata) => !isWorkflowRelatedObject(objectMetadata), - ); - - // Map to ObjectWithPermission - const result: ObjectWithPermission[] = []; - - for (const objectMetadata of filteredObjectMetadata) { - const permission = objectPermissions[objectMetadata.id]; - - if (!permission) { - continue; - } - - result.push({ - objectMetadata, - restrictedFields: permission.restrictedFields, - canCreate: permission.canUpdateObjectRecords, - canRead: permission.canReadObjectRecords, - canUpdate: permission.canUpdateObjectRecords, - canDelete: permission.canSoftDeleteObjectRecords, - }); - } - - return result; - } -} diff --git a/packages/twenty-server/src/engine/core-modules/tool-generator/tool-generator.module.ts b/packages/twenty-server/src/engine/core-modules/tool-generator/tool-generator.module.ts deleted file mode 100644 index 5a96bddd96..0000000000 --- a/packages/twenty-server/src/engine/core-modules/tool-generator/tool-generator.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; - -import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; -import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; - -import { PerObjectToolGeneratorService } from './services/per-object-tool-generator.service'; - -@Module({ - imports: [WorkspaceCacheModule, WorkspaceManyOrAllFlatEntityMapsCacheModule], - providers: [PerObjectToolGeneratorService], - exports: [PerObjectToolGeneratorService], -}) -export class ToolGeneratorModule {} diff --git a/packages/twenty-server/src/engine/core-modules/tool-generator/types/tool-generator.types.ts b/packages/twenty-server/src/engine/core-modules/tool-generator/types/tool-generator.types.ts deleted file mode 100644 index 6e421da62b..0000000000 --- a/packages/twenty-server/src/engine/core-modules/tool-generator/types/tool-generator.types.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { type ToolSet } from 'ai'; -import { - type ActorMetadata, - type RestrictedFieldsPermissions, -} from 'twenty-shared/types'; - -import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type'; -import { type ObjectMetadataForToolSchema } from 'src/engine/core-modules/record-crud/types/object-metadata-for-tool-schema.type'; -import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; - -// Object metadata enriched with permission information -export type ObjectWithPermission = { - objectMetadata: ObjectMetadataForToolSchema; - restrictedFields: RestrictedFieldsPermissions; - canCreate: boolean; - canRead: boolean; - canUpdate: boolean; - canDelete: boolean; -}; - -// Context passed to tool factories -export type ToolGeneratorContext = { - workspaceId: string; - authContext?: WorkspaceAuthContext; - rolePermissionConfig: RolePermissionConfig; - actorContext?: ActorMetadata; -}; - -// Factory function type for generating tools from an object -export type ToolFactory = ( - objectWithPermission: ObjectWithPermission, - context: ToolGeneratorContext, -) => ToolSet; diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/interfaces/static-tool-handler.interface.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/interfaces/static-tool-handler.interface.ts deleted file mode 100644 index da897e0fb4..0000000000 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/interfaces/static-tool-handler.interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type'; -import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type'; -import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; - -export interface StaticToolHandler { - execute(args: ToolInput, context: ToolProviderContext): Promise; -} diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/interfaces/tool-provider.interface.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/interfaces/tool-provider.interface.ts index 1deacb3524..1652fa23d0 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/interfaces/tool-provider.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/interfaces/tool-provider.interface.ts @@ -4,6 +4,7 @@ import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-pro import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type'; import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type'; +import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; export interface ToolProvider { readonly category: ToolCategory; @@ -14,4 +15,16 @@ export interface ToolProvider { context: ToolProviderContext, options?: GenerateDescriptorOptions, ): Promise<(ToolIndexEntry | ToolDescriptor)[]>; + + // Execute a tool whose descriptor has `executionRef.kind === 'static'` and + // `descriptor.category === this.category`. Providers own the execution of + // the tools they emit. + // + // Providers that never emit 'static' descriptors (database CRUD, logic + // functions) should throw — the call is unreachable by construction. + executeStaticTool( + toolName: string, + args: Record, + context: ToolProviderContext, + ): Promise; } diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/action-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/action-tool.provider.ts index c24c11533a..322dbdabc5 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/action-tool.provider.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/action-tool.provider.ts @@ -8,8 +8,6 @@ import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfa import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type'; import { ToolCategory } from 'twenty-shared/ai'; -import { type StaticToolHandler } from 'src/engine/core-modules/tool-provider/interfaces/static-tool-handler.interface'; -import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service'; import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type'; import { CodeInterpreterService } from 'src/engine/core-modules/code-interpreter/code-interpreter.service'; @@ -20,7 +18,7 @@ import { HttpTool } from 'src/engine/core-modules/tool/tools/http-tool/http-tool import { NavigateAppTool } from 'src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool'; import { SearchHelpCenterTool } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool'; import { WebSearchTool } from 'src/engine/core-modules/tool/tools/web-search-tool/web-search-tool'; -import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type'; +import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { type Tool } from 'src/engine/core-modules/tool/types/tool.type'; import { WebSearchService } from 'src/engine/core-modules/web-search/web-search.service'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; @@ -42,7 +40,6 @@ export class ActionToolProvider implements ToolProvider { private readonly codeInterpreterService: CodeInterpreterService, private readonly webSearchService: WebSearchService, private readonly permissionsService: PermissionsService, - private readonly toolExecutorService: ToolExecutorService, ) { this.toolMap = new Map([ ['http_request', this.httpTool], @@ -53,21 +50,6 @@ export class ActionToolProvider implements ToolProvider { ['navigate_app', this.navigateAppTool], ['web_search', this.webSearchTool], ]); - - // Register each action tool as a static handler in the executor - for (const [toolId, tool] of this.toolMap) { - const handler: StaticToolHandler = { - execute: async (args: ToolInput, context: ToolProviderContext) => - tool.execute(args, { - workspaceId: context.workspaceId, - userId: context.userId, - userWorkspaceId: context.userWorkspaceId, - onCodeExecutionUpdate: context.onCodeExecutionUpdate, - }), - }; - - this.toolExecutorService.registerStaticHandler(toolId, handler); - } } async isAvailable(_context: ToolProviderContext): Promise { @@ -155,6 +137,27 @@ export class ActionToolProvider implements ToolProvider { return descriptors; } + async executeStaticTool( + toolName: string, + args: Record, + context: ToolProviderContext, + ): Promise { + const tool = this.toolMap.get(toolName); + + if (!tool) { + throw new Error( + `Unknown action tool "${toolName}" (category: ${this.category})`, + ); + } + + return tool.execute(args, { + workspaceId: context.workspaceId, + userId: context.userId, + userWorkspaceId: context.userWorkspaceId, + onCodeExecutionUpdate: context.onCodeExecutionUpdate, + }); + } + private buildDescriptor( toolId: string, tool: Tool, diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/dashboard-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/dashboard-tool.provider.ts index d322c341d6..db8bf9c2be 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/dashboard-tool.provider.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/dashboard-tool.provider.ts @@ -1,5 +1,6 @@ -import { Inject, Injectable, OnModuleInit, Optional } from '@nestjs/common'; +import { Inject, Injectable, Optional } from '@nestjs/common'; +import { type ToolSet } from 'ai'; import { PermissionFlagType } from 'twenty-shared/constants'; import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type'; @@ -9,16 +10,18 @@ import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/ import { DASHBOARD_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/dashboard-tool-service.token'; import { ToolCategory } from 'twenty-shared/ai'; import { CoreObjectNameSingular } from 'twenty-shared/types'; -import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service'; import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type'; +import { executeToolFromToolSet } from 'src/engine/core-modules/tool-provider/utils/execute-tool-from-tool-set.util'; +import { resolveObjectIcon } from 'src/engine/core-modules/tool-provider/utils/resolve-object-icon.util'; import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util'; -import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; +import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import type { DashboardToolWorkspaceService } from 'src/modules/dashboard/tools/services/dashboard-tool.workspace-service'; @Injectable() -export class DashboardToolProvider implements ToolProvider, OnModuleInit { +export class DashboardToolProvider implements ToolProvider { readonly category = ToolCategory.DASHBOARD; constructor( @@ -26,25 +29,9 @@ export class DashboardToolProvider implements ToolProvider, OnModuleInit { @Inject(DASHBOARD_TOOL_SERVICE_TOKEN) private readonly dashboardToolService: DashboardToolWorkspaceService | null, private readonly permissionsService: PermissionsService, - private readonly toolExecutorService: ToolExecutorService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} - onModuleInit(): void { - if (this.dashboardToolService) { - const service = this.dashboardToolService; - - this.toolExecutorService.registerCategoryGenerator( - ToolCategory.DASHBOARD, - async (context) => - service.generateDashboardTools( - context.workspaceId, - context.rolePermissionConfig, - ), - ); - } - } - async isAvailable(context: ToolProviderContext): Promise { if (!this.dashboardToolService) { return false; @@ -61,16 +48,14 @@ export class DashboardToolProvider implements ToolProvider, OnModuleInit { context: ToolProviderContext, options?: GenerateDescriptorOptions, ): Promise<(ToolIndexEntry | ToolDescriptor)[]> { - if (!this.dashboardToolService) { + const toolSet = await this.buildToolSet(context); + + if (!toolSet) { return []; } - const toolSet = await this.dashboardToolService.generateDashboardTools( - context.workspaceId, - context.rolePermissionConfig, - ); - - const icon = await this.resolveObjectIcon( + const icon = await resolveObjectIcon( + this.flatEntityMapsCacheService, context.workspaceId, CoreObjectNameSingular.Dashboard, ); @@ -81,22 +66,37 @@ export class DashboardToolProvider implements ToolProvider, OnModuleInit { }); } - private async resolveObjectIcon( - workspaceId: string, - nameSingular: string, - ): Promise { - const { flatObjectMetadataMaps } = - await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps'], - }, + async executeStaticTool( + toolName: string, + args: Record, + context: ToolProviderContext, + ): Promise { + const toolSet = await this.buildToolSet(context); + + if (!toolSet) { + throw new Error( + `Dashboard tool service is not available (tool: ${toolName})`, ); + } - const flatObject = Object.values( - flatObjectMetadataMaps.byUniversalIdentifier, - ).find((obj) => obj?.nameSingular === nameSingular); + return executeToolFromToolSet( + toolSet, + toolName, + args, + ToolCategory.DASHBOARD, + ); + } - return flatObject?.icon ?? undefined; + private async buildToolSet( + context: ToolProviderContext, + ): Promise { + if (!this.dashboardToolService) { + return null; + } + + return this.dashboardToolService.generateDashboardTools( + context.workspaceId, + context.rolePermissionConfig, + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/database-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/database-tool.provider.ts index dd77522186..45afaa2bb2 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/database-tool.provider.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/database-tool.provider.ts @@ -26,6 +26,7 @@ import { import { ToolCategory } from 'twenty-shared/ai'; import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type'; +import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-workflow-related-object.util'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util'; @@ -44,6 +45,20 @@ export class DatabaseToolProvider implements ToolProvider { return true; } + // Database CRUD tools emit `executionRef.kind === 'database_crud'` descriptors + // and are dispatched inline by ToolExecutorService. The static-tool path is + // unreachable for this provider; this method exists only to satisfy the + // interface. + async executeStaticTool( + toolName: string, + _args: Record, + _context: ToolProviderContext, + ): Promise { + throw new Error( + `DatabaseToolProvider does not emit static-kind descriptors (tool: ${toolName})`, + ); + } + async generateDescriptors( context: ToolProviderContext, options?: GenerateDescriptorOptions, diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/logic-function-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/logic-function-tool.provider.ts index 2281a92f63..30bd19de0a 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/logic-function-tool.provider.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/logic-function-tool.provider.ts @@ -10,6 +10,7 @@ import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/ import { ToolCategory } from 'twenty-shared/ai'; import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type'; +import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type'; @@ -25,6 +26,20 @@ export class LogicFunctionToolProvider implements ToolProvider { return true; } + // Logic function tools emit `executionRef.kind === 'logic_function'` + // descriptors and are dispatched inline by ToolExecutorService. The + // static-tool path is unreachable for this provider; this method exists + // only to satisfy the interface. + async executeStaticTool( + toolName: string, + _args: Record, + _context: ToolProviderContext, + ): Promise { + throw new Error( + `LogicFunctionToolProvider does not emit static-kind descriptors (tool: ${toolName})`, + ); + } + async generateDescriptors( context: ToolProviderContext, options?: GenerateDescriptorOptions, diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/metadata-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/metadata-tool.provider.ts index 890ba84f4d..9b61018b8e 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/metadata-tool.provider.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/metadata-tool.provider.ts @@ -1,5 +1,6 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { type ToolSet } from 'ai'; import { PermissionFlagType } from 'twenty-shared/constants'; import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type'; @@ -7,38 +8,25 @@ import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfa import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type'; import { ToolCategory } from 'twenty-shared/ai'; -import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service'; import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type'; +import { executeToolFromToolSet } from 'src/engine/core-modules/tool-provider/utils/execute-tool-from-tool-set.util'; import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util'; +import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { FieldMetadataToolsFactory } from 'src/engine/metadata-modules/field-metadata/tools/field-metadata-tools.factory'; import { ObjectMetadataToolsFactory } from 'src/engine/metadata-modules/object-metadata/tools/object-metadata-tools.factory'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; @Injectable() -export class MetadataToolProvider implements ToolProvider, OnModuleInit { +export class MetadataToolProvider implements ToolProvider { readonly category = ToolCategory.METADATA; constructor( private readonly objectMetadataToolsFactory: ObjectMetadataToolsFactory, private readonly fieldMetadataToolsFactory: FieldMetadataToolsFactory, private readonly permissionsService: PermissionsService, - private readonly toolExecutorService: ToolExecutorService, ) {} - onModuleInit(): void { - const objectFactory = this.objectMetadataToolsFactory; - const fieldFactory = this.fieldMetadataToolsFactory; - - this.toolExecutorService.registerCategoryGenerator( - ToolCategory.METADATA, - async (context) => ({ - ...objectFactory.generateTools(context.workspaceId), - ...fieldFactory.generateTools(context.workspaceId), - }), - ); - } - async isAvailable(context: ToolProviderContext): Promise { return this.permissionsService.checkRolesPermissions( context.rolePermissionConfig, @@ -51,14 +39,33 @@ export class MetadataToolProvider implements ToolProvider, OnModuleInit { context: ToolProviderContext, options?: GenerateDescriptorOptions, ): Promise<(ToolIndexEntry | ToolDescriptor)[]> { - const toolSet = { - ...this.objectMetadataToolsFactory.generateTools(context.workspaceId), - ...this.fieldMetadataToolsFactory.generateTools(context.workspaceId), - }; + const toolSet = this.buildToolSet(context); return toolSetToDescriptors(toolSet, ToolCategory.METADATA, { includeSchemas: options?.includeSchemas ?? true, icon: 'IconSettings', }); } + + async executeStaticTool( + toolName: string, + args: Record, + context: ToolProviderContext, + ): Promise { + const toolSet = this.buildToolSet(context); + + return executeToolFromToolSet( + toolSet, + toolName, + args, + ToolCategory.METADATA, + ); + } + + private buildToolSet(context: ToolProviderContext): ToolSet { + return { + ...this.objectMetadataToolsFactory.generateTools(context.workspaceId), + ...this.fieldMetadataToolsFactory.generateTools(context.workspaceId), + }; + } } diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-field-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-field-tool.provider.ts index 31cc564eb4..4b2ac1d90e 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-field-tool.provider.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-field-tool.provider.ts @@ -1,5 +1,6 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { type ToolSet } from 'ai'; import { PermissionFlagType } from 'twenty-shared/constants'; import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type'; @@ -7,49 +8,23 @@ import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfa import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type'; import { ToolCategory } from 'twenty-shared/ai'; -import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service'; import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type'; +import { executeToolFromToolSet } from 'src/engine/core-modules/tool-provider/utils/execute-tool-from-tool-set.util'; import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util'; +import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { ViewFieldToolsFactory } from 'src/engine/metadata-modules/view-field/tools/view-field-tools.factory'; @Injectable() -export class ViewFieldToolProvider implements ToolProvider, OnModuleInit { +export class ViewFieldToolProvider implements ToolProvider { readonly category = ToolCategory.VIEW_FIELD; constructor( private readonly viewFieldToolsFactory: ViewFieldToolsFactory, private readonly permissionsService: PermissionsService, - private readonly toolExecutorService: ToolExecutorService, ) {} - onModuleInit(): void { - const factory = this.viewFieldToolsFactory; - - this.toolExecutorService.registerCategoryGenerator( - ToolCategory.VIEW_FIELD, - async (context) => { - const readTools = factory.generateReadTools(context.workspaceId); - - const hasViewPermission = - await this.permissionsService.checkRolesPermissions( - context.rolePermissionConfig, - context.workspaceId, - PermissionFlagType.VIEWS, - ); - - if (hasViewPermission) { - const writeTools = factory.generateWriteTools(context.workspaceId); - - return { ...readTools, ...writeTools }; - } - - return readTools; - }, - ); - } - async isAvailable(_context: ToolProviderContext): Promise { return true; } @@ -58,11 +33,30 @@ export class ViewFieldToolProvider implements ToolProvider, OnModuleInit { context: ToolProviderContext, options?: GenerateDescriptorOptions, ): Promise<(ToolIndexEntry | ToolDescriptor)[]> { - const schemaOptions = { + const toolSet = await this.buildToolSet(context); + + return toolSetToDescriptors(toolSet, ToolCategory.VIEW_FIELD, { includeSchemas: options?.includeSchemas ?? true, icon: 'IconTable', - }; + }); + } + async executeStaticTool( + toolName: string, + args: Record, + context: ToolProviderContext, + ): Promise { + const toolSet = await this.buildToolSet(context); + + return executeToolFromToolSet( + toolSet, + toolName, + args, + ToolCategory.VIEW_FIELD, + ); + } + + private async buildToolSet(context: ToolProviderContext): Promise { const readTools = this.viewFieldToolsFactory.generateReadTools( context.workspaceId, ); @@ -74,22 +68,14 @@ export class ViewFieldToolProvider implements ToolProvider, OnModuleInit { PermissionFlagType.VIEWS, ); - if (hasViewPermission) { - const writeTools = this.viewFieldToolsFactory.generateWriteTools( - context.workspaceId, - ); - - return toolSetToDescriptors( - { ...readTools, ...writeTools }, - ToolCategory.VIEW_FIELD, - schemaOptions, - ); + if (!hasViewPermission) { + return readTools; } - return toolSetToDescriptors( - readTools, - ToolCategory.VIEW_FIELD, - schemaOptions, + const writeTools = this.viewFieldToolsFactory.generateWriteTools( + context.workspaceId, ); + + return { ...readTools, ...writeTools }; } } diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-tool.provider.ts index bfac01c718..1ba3058844 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-tool.provider.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/view-tool.provider.ts @@ -1,5 +1,6 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { type ToolSet } from 'ai'; import { PermissionFlagType } from 'twenty-shared/constants'; import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type'; @@ -7,17 +8,18 @@ import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfa import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type'; import { ToolCategory } from 'twenty-shared/ai'; -import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service'; import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type'; +import { executeToolFromToolSet } from 'src/engine/core-modules/tool-provider/utils/execute-tool-from-tool-set.util'; import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util'; +import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { ViewFilterToolsFactory } from 'src/engine/metadata-modules/view-filter/tools/view-filter-tools.factory'; import { ViewSortToolsFactory } from 'src/engine/metadata-modules/view-sort/tools/view-sort-tools.factory'; import { ViewToolsFactory } from 'src/engine/metadata-modules/view/tools/view-tools.factory'; @Injectable() -export class ViewToolProvider implements ToolProvider, OnModuleInit { +export class ViewToolProvider implements ToolProvider { readonly category = ToolCategory.VIEW; constructor( @@ -25,54 +27,8 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit { private readonly viewFilterToolsFactory: ViewFilterToolsFactory, private readonly viewSortToolsFactory: ViewSortToolsFactory, private readonly permissionsService: PermissionsService, - private readonly toolExecutorService: ToolExecutorService, ) {} - onModuleInit(): void { - const viewFactory = this.viewToolsFactory; - const filterFactory = this.viewFilterToolsFactory; - const sortFactory = this.viewSortToolsFactory; - - this.toolExecutorService.registerCategoryGenerator( - ToolCategory.VIEW, - async (context) => { - const workspaceMemberId = context.actorContext?.workspaceMemberId; - - const readTools = { - ...viewFactory.generateReadTools( - context.workspaceId, - workspaceMemberId ?? undefined, - workspaceMemberId ?? undefined, - ), - ...filterFactory.generateReadTools(context.workspaceId), - ...sortFactory.generateReadTools(context.workspaceId), - }; - - const hasViewPermission = - await this.permissionsService.checkRolesPermissions( - context.rolePermissionConfig, - context.workspaceId, - PermissionFlagType.VIEWS, - ); - - if (hasViewPermission) { - const writeTools = { - ...viewFactory.generateWriteTools( - context.workspaceId, - workspaceMemberId ?? undefined, - ), - ...filterFactory.generateWriteTools(context.workspaceId), - ...sortFactory.generateWriteTools(context.workspaceId), - }; - - return { ...readTools, ...writeTools }; - } - - return readTools; - }, - ); - } - async isAvailable(_context: ToolProviderContext): Promise { return true; } @@ -81,10 +37,25 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit { context: ToolProviderContext, options?: GenerateDescriptorOptions, ): Promise<(ToolIndexEntry | ToolDescriptor)[]> { - const workspaceMemberId = context.actorContext?.workspaceMemberId; - const schemaOptions = { + const toolSet = await this.buildToolSet(context); + + return toolSetToDescriptors(toolSet, ToolCategory.VIEW, { includeSchemas: options?.includeSchemas ?? true, - }; + }); + } + + async executeStaticTool( + toolName: string, + args: Record, + context: ToolProviderContext, + ): Promise { + const toolSet = await this.buildToolSet(context); + + return executeToolFromToolSet(toolSet, toolName, args, ToolCategory.VIEW); + } + + private async buildToolSet(context: ToolProviderContext): Promise { + const workspaceMemberId = context.actorContext?.workspaceMemberId; const readTools = { ...this.viewToolsFactory.generateReadTools( @@ -103,23 +74,19 @@ export class ViewToolProvider implements ToolProvider, OnModuleInit { PermissionFlagType.VIEWS, ); - if (hasViewPermission) { - const writeTools = { - ...this.viewToolsFactory.generateWriteTools( - context.workspaceId, - workspaceMemberId ?? undefined, - ), - ...this.viewFilterToolsFactory.generateWriteTools(context.workspaceId), - ...this.viewSortToolsFactory.generateWriteTools(context.workspaceId), - }; - - return toolSetToDescriptors( - { ...readTools, ...writeTools }, - ToolCategory.VIEW, - schemaOptions, - ); + if (!hasViewPermission) { + return readTools; } - return toolSetToDescriptors(readTools, ToolCategory.VIEW, schemaOptions); + const writeTools = { + ...this.viewToolsFactory.generateWriteTools( + context.workspaceId, + workspaceMemberId ?? undefined, + ), + ...this.viewFilterToolsFactory.generateWriteTools(context.workspaceId), + ...this.viewSortToolsFactory.generateWriteTools(context.workspaceId), + }; + + return { ...readTools, ...writeTools }; } } diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/workflow-tool.provider.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/workflow-tool.provider.ts index a9fd793c5d..4fdba2b6ed 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/providers/workflow-tool.provider.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/providers/workflow-tool.provider.ts @@ -1,5 +1,6 @@ -import { Inject, Injectable, OnModuleInit, Optional } from '@nestjs/common'; +import { Inject, Injectable, Optional } from '@nestjs/common'; +import { type ToolSet } from 'ai'; import { PermissionFlagType } from 'twenty-shared/constants'; import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type'; @@ -9,16 +10,18 @@ import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/ import { WORKFLOW_TOOL_SERVICE_TOKEN } from 'src/engine/core-modules/tool-provider/constants/workflow-tool-service.token'; import { ToolCategory } from 'twenty-shared/ai'; import { CoreObjectNameSingular } from 'twenty-shared/types'; -import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service'; import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type'; +import { executeToolFromToolSet } from 'src/engine/core-modules/tool-provider/utils/execute-tool-from-tool-set.util'; +import { resolveObjectIcon } from 'src/engine/core-modules/tool-provider/utils/resolve-object-icon.util'; import { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util'; -import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; +import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import type { WorkflowToolWorkspaceService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service'; @Injectable() -export class WorkflowToolProvider implements ToolProvider, OnModuleInit { +export class WorkflowToolProvider implements ToolProvider { readonly category = ToolCategory.WORKFLOW; constructor( @@ -26,25 +29,9 @@ export class WorkflowToolProvider implements ToolProvider, OnModuleInit { @Inject(WORKFLOW_TOOL_SERVICE_TOKEN) private readonly workflowToolService: WorkflowToolWorkspaceService | null, private readonly permissionsService: PermissionsService, - private readonly toolExecutorService: ToolExecutorService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} - onModuleInit(): void { - if (this.workflowToolService) { - const service = this.workflowToolService; - - this.toolExecutorService.registerCategoryGenerator( - ToolCategory.WORKFLOW, - async (context) => - service.generateWorkflowTools( - context.workspaceId, - context.rolePermissionConfig, - ), - ); - } - } - async isAvailable(context: ToolProviderContext): Promise { if (!this.workflowToolService) { return false; @@ -61,16 +48,14 @@ export class WorkflowToolProvider implements ToolProvider, OnModuleInit { context: ToolProviderContext, options?: GenerateDescriptorOptions, ): Promise<(ToolIndexEntry | ToolDescriptor)[]> { - if (!this.workflowToolService) { + const toolSet = await this.buildToolSet(context); + + if (!toolSet) { return []; } - const toolSet = await this.workflowToolService.generateWorkflowTools( - context.workspaceId, - context.rolePermissionConfig, - ); - - const icon = await this.resolveObjectIcon( + const icon = await resolveObjectIcon( + this.flatEntityMapsCacheService, context.workspaceId, CoreObjectNameSingular.Workflow, ); @@ -81,22 +66,37 @@ export class WorkflowToolProvider implements ToolProvider, OnModuleInit { }); } - private async resolveObjectIcon( - workspaceId: string, - nameSingular: string, - ): Promise { - const { flatObjectMetadataMaps } = - await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: ['flatObjectMetadataMaps'], - }, + async executeStaticTool( + toolName: string, + args: Record, + context: ToolProviderContext, + ): Promise { + const toolSet = await this.buildToolSet(context); + + if (!toolSet) { + throw new Error( + `Workflow tool service is not available (tool: ${toolName})`, ); + } - const flatObject = Object.values( - flatObjectMetadataMaps.byUniversalIdentifier, - ).find((obj) => obj?.nameSingular === nameSingular); + return executeToolFromToolSet( + toolSet, + toolName, + args, + ToolCategory.WORKFLOW, + ); + } - return flatObject?.icon ?? undefined; + private async buildToolSet( + context: ToolProviderContext, + ): Promise { + if (!this.workflowToolService) { + return null; + } + + return this.workflowToolService.generateWorkflowTools( + context.workspaceId, + context.rolePermissionConfig, + ); } } diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-executor.service.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-executor.service.ts index 32b6992a75..635382b00a 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-executor.service.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-executor.service.ts @@ -1,4 +1,4 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { type AggregateOperations } from 'twenty-shared/types'; @@ -26,9 +26,8 @@ import { GroupByRecordsService } from 'src/engine/core-modules/record-crud/servi import { type FindRecordsParams } from 'src/engine/core-modules/record-crud/types/find-records-params.type'; import { UpdateManyRecordsService } from 'src/engine/core-modules/record-crud/services/update-many-records.service'; import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service'; -import { type ToolCategory } from 'twenty-shared/ai'; -import { type StaticToolHandler } from 'src/engine/core-modules/tool-provider/interfaces/static-tool-handler.interface'; -import { type CategoryToolGenerator } from 'src/engine/core-modules/tool-provider/types/category-tool-generator.type'; +import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/tool-providers.token'; +import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface'; import { type ToolDescriptor } from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type'; import { type ToolExecutionRef } from 'src/engine/core-modules/tool-provider/types/tool-execution-ref.type'; import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type'; @@ -40,16 +39,9 @@ import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/works export class ToolExecutorService { private readonly logger = new Logger(ToolExecutorService.name); - // Per-tool handlers (action tools, etc.) - private readonly staticToolHandlers = new Map(); - - // Category-level ToolSet generators (workflow, view, dashboard, metadata) - private readonly categoryGenerators = new Map< - ToolCategory, - CategoryToolGenerator - >(); - constructor( + @Inject(TOOL_PROVIDERS) + private readonly providers: ToolProvider[], private readonly findRecordsService: FindRecordsService, private readonly groupByRecordsService: GroupByRecordsService, private readonly createRecordService: CreateRecordService, @@ -63,17 +55,6 @@ export class ToolExecutorService { private readonly userRepository: Repository, ) {} - registerStaticHandler(toolId: string, handler: StaticToolHandler): void { - this.staticToolHandlers.set(toolId, handler); - } - - registerCategoryGenerator( - category: ToolCategory, - generator: CategoryToolGenerator, - ): void { - this.categoryGenerators.set(category, generator); - } - async dispatch( descriptor: ToolIndexEntry | ToolDescriptor, args: Record | undefined, @@ -222,35 +203,32 @@ export class ToolExecutorService { throw new Error('Expected static executionRef'); } - // Per-tool handler first (action tools) - const handler = this.staticToolHandlers.get(descriptor.executionRef.toolId); + const provider = this.providers.find( + (candidate) => candidate.category === descriptor.category, + ); - if (handler) { - return handler.execute(args, context); - } - - // Category-level generator fallback (workflow, view, dashboard, metadata) - const generator = this.categoryGenerators.get(descriptor.category); - - if (!generator) { + if (!provider) { throw new Error( - `No handler or generator for static tool: ${descriptor.executionRef.toolId}`, + `No provider registered for category "${descriptor.category}" (tool: ${descriptor.executionRef.toolId})`, ); } - const toolSet = await generator(context); - const tool = toolSet[descriptor.name]; - - if (!tool?.execute) { - throw new Error( - `Tool ${descriptor.name} not found in generated ToolSet for category ${descriptor.category}`, - ); + // Defense-in-depth: catalog and by-name lookups already filter by + // `isAvailable`, but re-verify at dispatch so the gate is enforced in + // one place regardless of how the descriptor reached us. + if (!(await provider.isAvailable(context))) { + return { + success: false, + message: `Tool "${descriptor.name}" is not available`, + error: `Tool "${descriptor.name}" is not available in this context. Use get_tool_catalog to see available tools.`, + }; } - return tool.execute( - { loadingMessage: '', ...args }, - { toolCallId: '', messages: [] }, - ) as Promise; + return provider.executeStaticTool( + descriptor.executionRef.toolId, + args, + context, + ); } private async dispatchLogicFunction( diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts index a2cc6e2323..1b902518ae 100644 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/tool-provider.module.ts @@ -2,7 +2,6 @@ import { forwardRef, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { RecordCrudModule } from 'src/engine/core-modules/record-crud/record-crud.module'; -import { ToolGeneratorModule } from 'src/engine/core-modules/tool-generator/tool-generator.module'; import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/tool-providers.token'; import { ActionToolProvider } from 'src/engine/core-modules/tool-provider/providers/action-tool.provider'; import { DashboardToolProvider } from 'src/engine/core-modules/tool-provider/providers/dashboard-tool.provider'; @@ -41,7 +40,6 @@ import { ToolRegistryService } from './services/tool-registry.service'; @Module({ imports: [ ToolModule, - ToolGeneratorModule, RecordCrudModule, AiModelsModule, forwardRef(() => AiAgentExecutionModule), diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/types/category-tool-generator.type.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/types/category-tool-generator.type.ts deleted file mode 100644 index 0988d7c1b8..0000000000 --- a/packages/twenty-server/src/engine/core-modules/tool-provider/types/category-tool-generator.type.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type ToolSet } from 'ai'; - -import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type'; - -export type CategoryToolGenerator = ( - context: ToolProviderContext, -) => Promise; diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/utils/execute-tool-from-tool-set.util.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/utils/execute-tool-from-tool-set.util.ts new file mode 100644 index 0000000000..9018224c17 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/utils/execute-tool-from-tool-set.util.ts @@ -0,0 +1,32 @@ +import { type ToolSet } from 'ai'; +import { type ToolCategory } from 'twenty-shared/ai'; + +import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; + +// Invokes a tool from a factory-generated ToolSet by name. Used by providers +// whose tools are produced as opaque AI-SDK ToolSet closures (view, metadata, +// workflow, dashboard, view-field) and which therefore cannot dispatch by +// executionRef alone. +// +// The factory closures expect a `loadingMessage` field (added by the chat UX +// wrapper) and a ToolExecutionOptions object; neither is meaningful when the +// executor is invoking them internally, so we pass empty defaults. +export const executeToolFromToolSet = async ( + toolSet: ToolSet, + toolName: string, + args: Record, + category: ToolCategory, +): Promise => { + const tool = toolSet[toolName]; + + if (!tool?.execute) { + throw new Error( + `Tool "${toolName}" not found in ToolSet for category "${category}"`, + ); + } + + return tool.execute( + { loadingMessage: '', ...args }, + { toolCallId: '', messages: [] }, + ) as Promise; +}; diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/utils/resolve-object-icon.util.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/utils/resolve-object-icon.util.ts new file mode 100644 index 0000000000..ea923f7bc6 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/tool-provider/utils/resolve-object-icon.util.ts @@ -0,0 +1,22 @@ +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; + +// Looks up a core object's icon by its nameSingular. Shared by providers that +// tag descriptor batches with the icon of the underlying object (workflows, +// dashboards) so the UI can render per-tool icons. +export const resolveObjectIcon = async ( + flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + workspaceId: string, + nameSingular: string, +): Promise => { + const { flatObjectMetadataMaps } = + await flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({ + workspaceId, + flatMapsKeys: ['flatObjectMetadataMaps'], + }); + + const flatObject = Object.values( + flatObjectMetadataMaps.byUniversalIdentifier, + ).find((obj) => obj?.nameSingular === nameSingular); + + return flatObject?.icon ?? undefined; +};