Clean up tool output architecture: remove wrappers, enforce ToolOutput everywhere (#19321)
## Summary
- **Remove `ExecuteToolResult` wrapper** — `execute_tool` is now a
transparent dispatcher that returns the raw `ToolOutput` from underlying
tools. No more `{ toolName, result }` envelope.
- **Type the entire execution chain as `Promise<ToolOutput>`** — from
`ToolExecutorService.dispatch()` through `resolveAndExecute()` to
`execute_tool.execute()`. Zero `Promise<unknown>` remaining in the tool
layer.
- **Use `Extract<ToolExecutionRef, ...>`** for dispatch methods,
enabling exhaustive switch checking and removing `as never` casts.
- **Relax `ToolOutput.result` to accept `null`** — removes `??
undefined` hacks at the boundary with logic function results.
- **Enforce 1-export-per-file** across tool type/interface files (split
`tool-descriptor.type.ts`, `tool-provider.interface.ts`, `tool.type.ts`,
`tool-output.type.ts`, `tool-executor.service.ts`).
- **Simplify error handling** — `wrapWithErrorHandler` and all
meta-errors (tool not found, tool excluded) now return consistent
`ToolOutput` shape with `error` as a plain string.
- **Frontend reads output directly** — removed `unwrapToolOutput`
utility; `ToolStepRenderer` and `ThinkingStepsDisplay` extract
`message`/`error` from the raw output with simple type guards.
- **Add permission error detection** for email tools via
`isInsufficientPermissionsError`, guiding the AI model to suggest
account reconnection instead of hallucinating about visibility settings.
## Test plan
- [ ] AI chat tool calls return visible output (not "null") in the UI
- [ ] Tool errors display correctly in the JSON tree
- [ ] Email draft/send tools return actionable permission errors
- [ ] Code interpreter output renders correctly
- [ ] Thinking steps display tool outputs properly
Made with [Cursor](https://cursor.com)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+3
@@ -0,0 +1,3 @@
|
||||
import { type CodeExecutionData } from 'twenty-shared/ai';
|
||||
|
||||
export type CodeExecutionStreamEmitter = (data: CodeExecutionData) => void;
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export type GenerateDescriptorOptions = {
|
||||
includeSchemas?: boolean;
|
||||
};
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { type ToolSet } from 'ai';
|
||||
import { type ToolCategory } from 'twenty-shared/ai';
|
||||
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
|
||||
export interface NativeToolProvider {
|
||||
readonly category: ToolCategory;
|
||||
|
||||
isAvailable(context: ToolProviderContext): Promise<boolean>;
|
||||
|
||||
generateTools(context: ToolProviderContext): Promise<ToolSet>;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
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<ToolOutput>;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { type ActorMetadata } from 'twenty-shared/types';
|
||||
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { type CodeExecutionStreamEmitter } from 'src/engine/core-modules/tool-provider/interfaces/code-execution-stream-emitter.type';
|
||||
import { type FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
export type ToolProviderContext = {
|
||||
workspaceId: string;
|
||||
roleId: string;
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
authContext?: WorkspaceAuthContext;
|
||||
actorContext?: ActorMetadata;
|
||||
userId?: string;
|
||||
userWorkspaceId?: string;
|
||||
agent?: FlatAgentWithRoleId | null;
|
||||
onCodeExecutionUpdate?: CodeExecutionStreamEmitter;
|
||||
};
|
||||
+5
-47
@@ -1,41 +1,9 @@
|
||||
import { type ToolSet } from 'ai';
|
||||
import { type CodeExecutionData, type ToolCategory } from 'twenty-shared/ai';
|
||||
import { type ActorMetadata } from 'twenty-shared/types';
|
||||
import { type ToolCategory } from 'twenty-shared/ai';
|
||||
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { type FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
export type CodeExecutionStreamEmitter = (data: CodeExecutionData) => void;
|
||||
|
||||
// Unified context for tool generation - used by all consumers
|
||||
export type ToolProviderContext = {
|
||||
workspaceId: string;
|
||||
roleId: string;
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
// Optional fields for different use cases
|
||||
authContext?: WorkspaceAuthContext;
|
||||
actorContext?: ActorMetadata;
|
||||
userId?: string;
|
||||
userWorkspaceId?: string;
|
||||
agent?: FlatAgentWithRoleId | null;
|
||||
onCodeExecutionUpdate?: CodeExecutionStreamEmitter;
|
||||
};
|
||||
|
||||
// Options for tool retrieval
|
||||
export type ToolRetrievalOptions = {
|
||||
categories?: ToolCategory[];
|
||||
excludeTools?: string[];
|
||||
wrapWithErrorContext?: boolean;
|
||||
};
|
||||
|
||||
export type GenerateDescriptorOptions = {
|
||||
includeSchemas?: boolean; // defaults to true for backward compat
|
||||
};
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
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';
|
||||
|
||||
export interface ToolProvider {
|
||||
readonly category: ToolCategory;
|
||||
@@ -47,13 +15,3 @@ export interface ToolProvider {
|
||||
options?: GenerateDescriptorOptions,
|
||||
): Promise<(ToolIndexEntry | ToolDescriptor)[]>;
|
||||
}
|
||||
|
||||
// NativeModelToolProvider is special: SDK-native tools are opaque and not
|
||||
// serializable. It keeps the old generateTools() contract.
|
||||
export interface NativeToolProvider {
|
||||
readonly category: ToolCategory;
|
||||
|
||||
isAvailable(context: ToolProviderContext): Promise<boolean>;
|
||||
|
||||
generateTools(context: ToolProviderContext): Promise<ToolSet>;
|
||||
}
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import { type ToolCategory } from 'twenty-shared/ai';
|
||||
|
||||
export type ToolRetrievalOptions = {
|
||||
categories?: ToolCategory[];
|
||||
excludeTools?: string[];
|
||||
wrapWithErrorContext?: boolean;
|
||||
};
|
||||
+7
-13
@@ -3,21 +3,15 @@ import { Injectable } from '@nestjs/common';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import {
|
||||
type StaticToolHandler,
|
||||
ToolExecutorService,
|
||||
} from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
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';
|
||||
import { CodeInterpreterTool } from 'src/engine/core-modules/tool/tools/code-interpreter-tool/code-interpreter-tool';
|
||||
import { DraftEmailTool } from 'src/engine/core-modules/tool/tools/email-tool/draft-email-tool';
|
||||
|
||||
+5
-9
@@ -2,20 +2,16 @@ import { Inject, Injectable, OnModuleInit, Optional } from '@nestjs/common';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
|
||||
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,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.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 { 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 { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
|
||||
+5
-9
@@ -7,11 +7,9 @@ import {
|
||||
import { camelToSnakeCase, isDefined } from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
|
||||
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
|
||||
import { generateCreateManyRecordInputSchema } from 'src/engine/core-modules/record-crud/utils/generate-create-many-record-input-schema.util';
|
||||
@@ -22,10 +20,8 @@ import { DeleteToolInputSchema } from 'src/engine/core-modules/record-crud/zod-s
|
||||
import { FindOneToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/find-one-tool.zod-schema';
|
||||
import { generateFindToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.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 { isFavoriteRelatedObject } from 'src/engine/metadata-modules/ai/ai-agent/utils/is-favorite-related-object.util';
|
||||
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';
|
||||
|
||||
+5
-9
@@ -3,17 +3,13 @@ import { Injectable } from '@nestjs/common';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { DEFAULT_TOOL_INPUT_SCHEMA } from 'twenty-shared/logic-function';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.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 { 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';
|
||||
|
||||
|
||||
+5
-9
@@ -2,18 +2,14 @@ import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
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,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.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 { toolSetToDescriptors } from 'src/engine/core-modules/tool-provider/utils/tool-set-to-descriptors.util';
|
||||
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';
|
||||
|
||||
+2
-4
@@ -3,10 +3,8 @@ import { Injectable } from '@nestjs/common';
|
||||
import { type ToolSet } from 'ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type NativeToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type NativeToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/native-tool-provider.interface';
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { AgentModelConfigService } from 'src/engine/metadata-modules/ai/ai-models/services/agent-model-config.service';
|
||||
|
||||
+5
-9
@@ -2,18 +2,14 @@ import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
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,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.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 { 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 { ViewFieldToolsFactory } from 'src/engine/metadata-modules/view-field/tools/view-field-tools.factory';
|
||||
|
||||
+5
-9
@@ -2,18 +2,14 @@ import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
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,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.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 { 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 { ViewFilterToolsFactory } from 'src/engine/metadata-modules/view-filter/tools/view-filter-tools.factory';
|
||||
|
||||
+5
-9
@@ -2,20 +2,16 @@ import { Inject, Injectable, OnModuleInit, Optional } from '@nestjs/common';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import {
|
||||
type GenerateDescriptorOptions,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type GenerateDescriptorOptions } from 'src/engine/core-modules/tool-provider/interfaces/generate-descriptor-options.type';
|
||||
import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
|
||||
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,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.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 { 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 { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
|
||||
+19
-32
@@ -1,13 +1,12 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { type FlatWorkspace } from 'src/engine/core-modules/workspace/types/flat-workspace.type';
|
||||
import { fromUserEntityToFlat } from 'src/engine/core-modules/user/utils/from-user-entity-to-flat.util';
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
|
||||
import {
|
||||
AuthException,
|
||||
@@ -20,29 +19,20 @@ import { CreateManyRecordsService } from 'src/engine/core-modules/record-crud/se
|
||||
import { CreateRecordService } from 'src/engine/core-modules/record-crud/services/create-record.service';
|
||||
import { DeleteRecordService } from 'src/engine/core-modules/record-crud/services/delete-record.service';
|
||||
import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service';
|
||||
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 ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
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 { 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';
|
||||
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
import { stripLoadingMessage } from 'src/engine/core-modules/tool/utils/wrap-tool-for-execution.util';
|
||||
import { UserEntity } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
|
||||
// Handler for individually registered static tools (e.g., action tools)
|
||||
export interface StaticToolHandler {
|
||||
execute(args: ToolInput, context: ToolProviderContext): Promise<unknown>;
|
||||
}
|
||||
|
||||
// Generator that produces a ToolSet on demand for a category (workflow, view, etc.)
|
||||
// Used as a fallback when no per-tool handler is registered.
|
||||
export type CategoryToolGenerator = (
|
||||
context: ToolProviderContext,
|
||||
) => Promise<ToolSet>;
|
||||
|
||||
@Injectable()
|
||||
export class ToolExecutorService {
|
||||
private readonly logger = new Logger(ToolExecutorService.name);
|
||||
@@ -84,7 +74,7 @@ export class ToolExecutorService {
|
||||
descriptor: ToolIndexEntry | ToolDescriptor,
|
||||
args: Record<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<unknown> {
|
||||
): Promise<ToolOutput> {
|
||||
const cleanArgs = stripLoadingMessage(args);
|
||||
|
||||
switch (descriptor.executionRef.kind) {
|
||||
@@ -106,10 +96,10 @@ export class ToolExecutorService {
|
||||
}
|
||||
|
||||
private async dispatchDatabaseCrud(
|
||||
ref: { objectNameSingular: string; operation: string },
|
||||
ref: Extract<ToolExecutionRef, { kind: 'database_crud' }>,
|
||||
args: Record<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<unknown> {
|
||||
): Promise<ToolOutput> {
|
||||
const authContext =
|
||||
context.authContext ?? (await this.buildAuthContext(context));
|
||||
|
||||
@@ -120,7 +110,7 @@ export class ToolExecutorService {
|
||||
return this.findRecordsService.execute({
|
||||
objectName: ref.objectNameSingular,
|
||||
filter,
|
||||
orderBy: orderBy as never,
|
||||
orderBy: orderBy as FindRecordsParams['orderBy'],
|
||||
limit: limit as number | undefined,
|
||||
offset: offset as number | undefined,
|
||||
authContext,
|
||||
@@ -191,9 +181,6 @@ export class ToolExecutorService {
|
||||
rolePermissionConfig: context.rolePermissionConfig,
|
||||
soft: true,
|
||||
});
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown database_crud operation: ${ref.operation}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +188,7 @@ export class ToolExecutorService {
|
||||
descriptor: ToolIndexEntry | ToolDescriptor,
|
||||
args: Record<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<unknown> {
|
||||
): Promise<ToolOutput> {
|
||||
if (descriptor.executionRef.kind !== 'static') {
|
||||
throw new Error('Expected static executionRef');
|
||||
}
|
||||
@@ -231,19 +218,17 @@ export class ToolExecutorService {
|
||||
);
|
||||
}
|
||||
|
||||
// The tool's execute expects (args, ToolExecutionOptions). Pass args with
|
||||
// a dummy loadingMessage since the tool's internal strip is harmless.
|
||||
return tool.execute(
|
||||
{ loadingMessage: '', ...args },
|
||||
{ toolCallId: '', messages: [] },
|
||||
);
|
||||
) as Promise<ToolOutput>;
|
||||
}
|
||||
|
||||
private async dispatchLogicFunction(
|
||||
ref: { logicFunctionId: string },
|
||||
ref: Extract<ToolExecutionRef, { kind: 'logic_function' }>,
|
||||
args: Record<string, unknown>,
|
||||
context: ToolProviderContext,
|
||||
): Promise<unknown> {
|
||||
): Promise<ToolOutput> {
|
||||
const result = await this.logicFunctionExecutorService.execute({
|
||||
logicFunctionId: ref.logicFunctionId,
|
||||
workspaceId: context.workspaceId,
|
||||
@@ -253,13 +238,15 @@ export class ToolExecutorService {
|
||||
if (result.error) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Logic function execution failed',
|
||||
error: result.error.errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
result: result.data,
|
||||
message: 'Logic function executed successfully',
|
||||
result: result.data ?? undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+17
-41
@@ -2,34 +2,24 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { type ToolExecutionOptions, type ToolSet, jsonSchema } from 'ai';
|
||||
|
||||
import {
|
||||
type NativeToolProvider,
|
||||
type ToolProvider,
|
||||
type ToolProviderContext,
|
||||
type ToolRetrievalOptions,
|
||||
} from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type NativeToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/native-tool-provider.interface';
|
||||
import { type ToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
|
||||
import { type ToolRetrievalOptions } from 'src/engine/core-modules/tool-provider/interfaces/tool-retrieval-options.type';
|
||||
|
||||
import { TOOL_PROVIDERS } from 'src/engine/core-modules/tool-provider/constants/tool-providers.token';
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { compactToolOutput } from 'src/engine/core-modules/tool-provider/output-serialization/compact-tool-output.util';
|
||||
import { NativeModelToolProvider } from 'src/engine/core-modules/tool-provider/providers/native-model-tool.provider';
|
||||
import { ToolExecutorService } from 'src/engine/core-modules/tool-provider/services/tool-executor.service';
|
||||
import { type ExecuteToolResult } from 'src/engine/core-modules/tool-provider/tools/execute-tool.tool';
|
||||
import { type LearnToolsAspect } from 'src/engine/core-modules/tool-provider/tools/learn-tools.tool';
|
||||
import { type ToolContext } from 'src/engine/core-modules/tool-provider/types/tool-context.type';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.type';
|
||||
import {
|
||||
generateErrorSuggestion,
|
||||
wrapWithErrorHandler,
|
||||
} from 'src/engine/core-modules/tool-provider/utils/tool-error.util';
|
||||
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 { wrapWithErrorHandler } from 'src/engine/core-modules/tool-provider/utils/tool-error.util';
|
||||
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
import { wrapJsonSchemaForExecution } from 'src/engine/core-modules/tool/utils/wrap-tool-for-execution.util';
|
||||
import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config';
|
||||
|
||||
export { type ToolContext } from 'src/engine/core-modules/tool-provider/types/tool-context.type';
|
||||
|
||||
@Injectable()
|
||||
export class ToolRegistryService {
|
||||
private readonly logger = new Logger(ToolRegistryService.name);
|
||||
@@ -125,7 +115,7 @@ export class ToolRegistryService {
|
||||
|
||||
const executeFn = async (
|
||||
args: Record<string, unknown>,
|
||||
): Promise<unknown> =>
|
||||
): Promise<ToolOutput> =>
|
||||
this.toolExecutorService.dispatch(descriptor, args, context);
|
||||
|
||||
toolSet[descriptor.name] = {
|
||||
@@ -220,7 +210,7 @@ export class ToolRegistryService {
|
||||
args: Record<string, unknown>,
|
||||
context: ToolContext,
|
||||
_options: ToolExecutionOptions,
|
||||
): Promise<ExecuteToolResult> {
|
||||
): Promise<ToolOutput> {
|
||||
try {
|
||||
const fullContext = this.buildContextFromToolContext(context);
|
||||
|
||||
@@ -229,25 +219,13 @@ export class ToolRegistryService {
|
||||
|
||||
if (!entry) {
|
||||
return {
|
||||
toolName,
|
||||
error: {
|
||||
message: `Tool "${toolName}" not found. Check the tool catalog for correct names.`,
|
||||
suggestion:
|
||||
'Use learn_tools to discover available tools and their correct names.',
|
||||
},
|
||||
success: false,
|
||||
message: `Tool "${toolName}" not found`,
|
||||
error: `Tool "${toolName}" not found. Use learn_tools to discover available tools.`,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await this.toolExecutorService.dispatch(
|
||||
entry,
|
||||
args,
|
||||
fullContext,
|
||||
);
|
||||
|
||||
return {
|
||||
toolName,
|
||||
result: compactToolOutput(result),
|
||||
};
|
||||
return await this.toolExecutorService.dispatch(entry, args, fullContext);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
@@ -255,11 +233,9 @@ export class ToolRegistryService {
|
||||
this.logger.error(`Error executing tool "${toolName}": ${errorMessage}`);
|
||||
|
||||
return {
|
||||
toolName,
|
||||
error: {
|
||||
message: errorMessage,
|
||||
suggestion: generateErrorSuggestion(toolName, errorMessage),
|
||||
},
|
||||
success: false,
|
||||
message: `Failed to execute ${toolName}`,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+6
-18
@@ -4,6 +4,7 @@ import { z } from 'zod';
|
||||
|
||||
import { type ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { type ToolContext } from 'src/engine/core-modules/tool-provider/types/tool-context.type';
|
||||
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
|
||||
export const EXECUTE_TOOL_TOOL_NAME = 'execute_tool';
|
||||
|
||||
@@ -40,15 +41,6 @@ export const executeToolInputSchema = jsonSchema<ExecuteToolInput>(
|
||||
},
|
||||
);
|
||||
|
||||
export type ExecuteToolResult = {
|
||||
toolName: string;
|
||||
result?: unknown;
|
||||
error?: {
|
||||
message: string;
|
||||
suggestion: string;
|
||||
};
|
||||
};
|
||||
|
||||
export const createExecuteToolTool = (
|
||||
toolRegistry: ToolRegistryService,
|
||||
context: ToolContext,
|
||||
@@ -61,25 +53,21 @@ export const createExecuteToolTool = (
|
||||
execute: async (
|
||||
parameters: ExecuteToolInput,
|
||||
options: ToolExecutionOptions,
|
||||
): Promise<ExecuteToolResult> => {
|
||||
): Promise<ToolOutput> => {
|
||||
const { toolName, arguments: args } = parameters;
|
||||
|
||||
if (excludeTools?.has(toolName)) {
|
||||
return {
|
||||
toolName,
|
||||
error: {
|
||||
message: `Tool "${toolName}" is not available in this context.`,
|
||||
suggestion: 'Use get_tool_catalog to see which tools are available.',
|
||||
},
|
||||
success: false,
|
||||
message: `Tool "${toolName}" is not available`,
|
||||
error: `Tool "${toolName}" is not available in this context. Use get_tool_catalog to see which tools are available.`,
|
||||
};
|
||||
}
|
||||
|
||||
const directTool = directTools?.[toolName];
|
||||
|
||||
if (directTool?.execute) {
|
||||
const result = await directTool.execute(args, options);
|
||||
|
||||
return { toolName, result };
|
||||
return directTool.execute(args, options) as Promise<ToolOutput>;
|
||||
}
|
||||
|
||||
return toolRegistry.resolveAndExecute(toolName, args, context, options);
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||
|
||||
import { ToolCategory } from 'twenty-shared/ai';
|
||||
import { type ToolRegistryService } from 'src/engine/core-modules/tool-provider/services/tool-registry.service';
|
||||
import { type ToolIndexEntry } 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';
|
||||
|
||||
export const GET_TOOL_CATALOG_TOOL_NAME = 'get_tool_catalog';
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ export {
|
||||
createExecuteToolTool,
|
||||
executeToolInputSchema,
|
||||
type ExecuteToolInput,
|
||||
type ExecuteToolResult,
|
||||
} from './execute-tool.tool';
|
||||
|
||||
export {
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
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<ToolSet>;
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
export type DatabaseCrudOperation =
|
||||
| 'find'
|
||||
| 'find_one'
|
||||
| 'create'
|
||||
| 'create_many'
|
||||
| 'update'
|
||||
| 'update_many'
|
||||
| 'delete';
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { type ActorMetadata } from 'twenty-shared/types';
|
||||
|
||||
import { type CodeExecutionStreamEmitter } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
import { type CodeExecutionStreamEmitter } from 'src/engine/core-modules/tool-provider/interfaces/code-execution-stream-emitter.type';
|
||||
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
|
||||
|
||||
+1
-31
@@ -1,35 +1,5 @@
|
||||
import { type ToolCategory } from 'twenty-shared/ai';
|
||||
import { type ToolIndexEntry } from 'src/engine/core-modules/tool-provider/types/tool-index-entry.type';
|
||||
|
||||
export type DatabaseCrudOperation =
|
||||
| 'find'
|
||||
| 'find_one'
|
||||
| 'create'
|
||||
| 'create_many'
|
||||
| 'update'
|
||||
| 'update_many'
|
||||
| 'delete';
|
||||
|
||||
export type ToolExecutionRef =
|
||||
| {
|
||||
kind: 'database_crud';
|
||||
objectNameSingular: string;
|
||||
operation: DatabaseCrudOperation;
|
||||
}
|
||||
| { kind: 'static'; toolId: string }
|
||||
| { kind: 'logic_function'; logicFunctionId: string };
|
||||
|
||||
// Lightweight entry for catalog/index (no schema)
|
||||
export type ToolIndexEntry = {
|
||||
name: string;
|
||||
description: string;
|
||||
category: ToolCategory;
|
||||
executionRef: ToolExecutionRef;
|
||||
objectName?: string;
|
||||
operation?: string;
|
||||
icon?: string;
|
||||
};
|
||||
|
||||
// Full descriptor with schema (on-demand)
|
||||
export type ToolDescriptor = ToolIndexEntry & {
|
||||
inputSchema: object;
|
||||
};
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { type DatabaseCrudOperation } from 'src/engine/core-modules/tool-provider/types/database-crud-operation.type';
|
||||
|
||||
export type ToolExecutionRef =
|
||||
| {
|
||||
kind: 'database_crud';
|
||||
objectNameSingular: string;
|
||||
operation: DatabaseCrudOperation;
|
||||
}
|
||||
| { kind: 'static'; toolId: string }
|
||||
| { kind: 'logic_function'; logicFunctionId: string };
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import { type ToolCategory } from 'twenty-shared/ai';
|
||||
|
||||
import { type ToolExecutionRef } from 'src/engine/core-modules/tool-provider/types/tool-execution-ref.type';
|
||||
|
||||
export type ToolIndexEntry = {
|
||||
name: string;
|
||||
description: string;
|
||||
category: ToolCategory;
|
||||
executionRef: ToolExecutionRef;
|
||||
objectName?: string;
|
||||
operation?: string;
|
||||
icon?: string;
|
||||
};
|
||||
+5
-45
@@ -1,46 +1,9 @@
|
||||
export const generateErrorSuggestion = (
|
||||
_toolName: string,
|
||||
errorMessage: string,
|
||||
): string => {
|
||||
const lowerError = errorMessage.toLowerCase();
|
||||
|
||||
if (
|
||||
lowerError.includes('not found') ||
|
||||
lowerError.includes('does not exist')
|
||||
) {
|
||||
return 'Verify the ID or name exists with a search query first';
|
||||
}
|
||||
|
||||
if (
|
||||
lowerError.includes('permission') ||
|
||||
lowerError.includes('forbidden') ||
|
||||
lowerError.includes('unauthorized')
|
||||
) {
|
||||
return 'This operation requires elevated permissions or a different role';
|
||||
}
|
||||
|
||||
if (lowerError.includes('invalid') || lowerError.includes('validation')) {
|
||||
return 'Check the tool schema for valid parameter formats and types';
|
||||
}
|
||||
|
||||
if (
|
||||
lowerError.includes('duplicate') ||
|
||||
lowerError.includes('already exists')
|
||||
) {
|
||||
return 'A record with this identifier already exists. Try updating instead of creating';
|
||||
}
|
||||
|
||||
if (lowerError.includes('required') || lowerError.includes('missing')) {
|
||||
return 'Required fields are missing. Check which fields are mandatory for this operation';
|
||||
}
|
||||
|
||||
return 'Try adjusting the parameters or using a different approach';
|
||||
};
|
||||
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
|
||||
export const wrapWithErrorHandler = (
|
||||
toolName: string,
|
||||
executeFn: (args: Record<string, unknown>) => Promise<unknown>,
|
||||
): ((args: Record<string, unknown>) => Promise<unknown>) => {
|
||||
executeFn: (args: Record<string, unknown>) => Promise<ToolOutput>,
|
||||
): ((args: Record<string, unknown>) => Promise<ToolOutput>) => {
|
||||
return async (args: Record<string, unknown>) => {
|
||||
try {
|
||||
return await executeFn(args);
|
||||
@@ -50,11 +13,8 @@ export const wrapWithErrorHandler = (
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: errorMessage,
|
||||
tool: toolName,
|
||||
suggestion: generateErrorSuggestion(toolName, errorMessage),
|
||||
},
|
||||
message: `Failed to execute ${toolName}`,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
+2
-4
@@ -2,10 +2,8 @@ import { type ToolSet } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { type ToolCategory } from 'twenty-shared/ai';
|
||||
import {
|
||||
type ToolDescriptor,
|
||||
type ToolIndexEntry,
|
||||
} from 'src/engine/core-modules/tool-provider/types/tool-descriptor.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';
|
||||
|
||||
export type ToolSetToDescriptorsOptions = {
|
||||
includeSchemas?: boolean;
|
||||
|
||||
+2
-4
@@ -34,10 +34,8 @@ import {
|
||||
} from 'src/engine/core-modules/tool/tools/code-interpreter-tool/types/code-interpreter-input.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';
|
||||
import {
|
||||
type Tool,
|
||||
type ToolExecutionContext,
|
||||
} from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { type ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool-execution-context.type';
|
||||
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type';
|
||||
|
||||
|
||||
+13
-4
@@ -3,13 +3,12 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { EmailComposerService } from 'src/engine/core-modules/tool/tools/email-tool/email-composer.service';
|
||||
import { EmailToolInputZodSchema } from 'src/engine/core-modules/tool/tools/email-tool/email-tool.schema';
|
||||
import { EmailToolException } from 'src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception';
|
||||
import { isInsufficientPermissionsError } from 'src/engine/core-modules/tool/tools/email-tool/utils/is-insufficient-permissions-error.util';
|
||||
import { type ComposedEmail } from 'src/engine/core-modules/tool/tools/email-tool/types/composed-email.type';
|
||||
import { type EmailToolInput } from 'src/engine/core-modules/tool/tools/email-tool/types/email-tool-input.type';
|
||||
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
import {
|
||||
type Tool,
|
||||
type ToolExecutionContext,
|
||||
} from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { type ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool-execution-context.type';
|
||||
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { MessagingMessageOutboundService } from 'src/modules/messaging/message-outbound-manager/services/messaging-message-outbound.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -70,6 +69,16 @@ export class DraftEmailTool implements Tool {
|
||||
|
||||
this.logger.error(`Failed to create draft: ${error}`);
|
||||
|
||||
if (isInsufficientPermissionsError(error)) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to create draft due to insufficient permissions',
|
||||
error:
|
||||
'The connected email account does not have permission to create drafts. ' +
|
||||
'The user should disconnect and reconnect their account in Settings > Accounts to grant the required permissions.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to create draft',
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ import {
|
||||
import { EmailComposerResult } from 'src/engine/core-modules/tool/tools/email-tool/types/email-composer-result.type';
|
||||
import { EmailToolInput } from 'src/engine/core-modules/tool/tools/email-tool/types/email-tool-input.type';
|
||||
import { parseCommaSeparatedEmails } from 'src/engine/core-modules/tool/tools/email-tool/utils/parse-comma-separated-emails.util';
|
||||
import { ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { type ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool-execution-context.type';
|
||||
import { ConnectedAccountDataAccessService } from 'src/engine/metadata-modules/connected-account/data-access/services/connected-account-data-access.service';
|
||||
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
|
||||
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
|
||||
|
||||
+13
-4
@@ -3,13 +3,12 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { EmailComposerService } from 'src/engine/core-modules/tool/tools/email-tool/email-composer.service';
|
||||
import { EmailToolInputZodSchema } from 'src/engine/core-modules/tool/tools/email-tool/email-tool.schema';
|
||||
import { EmailToolException } from 'src/engine/core-modules/tool/tools/email-tool/exceptions/email-tool.exception';
|
||||
import { isInsufficientPermissionsError } from 'src/engine/core-modules/tool/tools/email-tool/utils/is-insufficient-permissions-error.util';
|
||||
import { type ComposedEmail } from 'src/engine/core-modules/tool/tools/email-tool/types/composed-email.type';
|
||||
import { type EmailToolInput } from 'src/engine/core-modules/tool/tools/email-tool/types/email-tool-input.type';
|
||||
import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
import {
|
||||
type Tool,
|
||||
type ToolExecutionContext,
|
||||
} from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { type ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool-execution-context.type';
|
||||
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { MessagingMessageOutboundService } from 'src/modules/messaging/message-outbound-manager/services/messaging-message-outbound.service';
|
||||
|
||||
@Injectable()
|
||||
@@ -70,6 +69,16 @@ export class SendEmailTool implements Tool {
|
||||
|
||||
this.logger.error(`Failed to send email: ${error}`);
|
||||
|
||||
if (isInsufficientPermissionsError(error)) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to send email due to insufficient permissions',
|
||||
error:
|
||||
'The connected email account does not have permission to send emails. ' +
|
||||
'The user should disconnect and reconnect their account in Settings > Accounts to grant the required permissions.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to send email',
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Detects permission/scope errors from email provider APIs (Google, Microsoft).
|
||||
// These typically occur when the OAuth consent didn't include the required
|
||||
// scope for the operation (e.g., gmail.compose for drafts).
|
||||
export const isInsufficientPermissionsError = (error: unknown): boolean => {
|
||||
if (!(error instanceof Error)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = error.message.toLowerCase();
|
||||
|
||||
if (
|
||||
message.includes('insufficient permission') ||
|
||||
message.includes('insufficient authentication scopes') ||
|
||||
message.includes('access denied') ||
|
||||
message.includes('forbidden')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const response = (error as { response?: { status?: number } }).response;
|
||||
|
||||
return response?.status === 401 || response?.status === 403;
|
||||
};
|
||||
@@ -9,10 +9,8 @@ import { HttpRequestInputZodSchema } from 'src/engine/core-modules/tool/tools/ht
|
||||
import { type HttpRequestInput } from 'src/engine/core-modules/tool/tools/http-tool/types/http-request-input.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';
|
||||
import {
|
||||
type Tool,
|
||||
type ToolExecutionContext,
|
||||
} from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { type ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool-execution-context.type';
|
||||
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
|
||||
@Injectable()
|
||||
export class HttpTool implements Tool {
|
||||
|
||||
+2
-4
@@ -12,10 +12,8 @@ import {
|
||||
} from 'src/engine/core-modules/tool/tools/navigate-tool/navigate-app-tool.schema';
|
||||
import { type ToolInput } from 'src/engine/core-modules/tool/types/tool-input.type';
|
||||
import { ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type';
|
||||
import {
|
||||
type Tool,
|
||||
type ToolExecutionContext,
|
||||
} from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { type ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool-execution-context.type';
|
||||
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
+2
-4
@@ -6,10 +6,8 @@ import { SecureHttpClientService } from 'src/engine/core-modules/secure-http-cli
|
||||
import { SearchHelpCenterInputZodSchema } from 'src/engine/core-modules/tool/tools/search-help-center-tool/search-help-center-tool.schema';
|
||||
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,
|
||||
type ToolExecutionContext,
|
||||
} from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { type ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool-execution-context.type';
|
||||
import { type Tool } from 'src/engine/core-modules/tool/types/tool.type';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
export type RecordReference = {
|
||||
objectNameSingular: string;
|
||||
recordId: string;
|
||||
displayName: string;
|
||||
};
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
import { type CodeExecutionStreamEmitter } from 'src/engine/core-modules/tool-provider/interfaces/code-execution-stream-emitter.type';
|
||||
|
||||
export type ToolExecutionContext = {
|
||||
workspaceId: string;
|
||||
userId?: string;
|
||||
userWorkspaceId?: string;
|
||||
onCodeExecutionUpdate?: CodeExecutionStreamEmitter;
|
||||
};
|
||||
@@ -1,8 +1,4 @@
|
||||
export type RecordReference = {
|
||||
objectNameSingular: string;
|
||||
recordId: string;
|
||||
displayName: string;
|
||||
};
|
||||
import { type RecordReference } from 'src/engine/core-modules/tool/types/record-reference.type';
|
||||
|
||||
export type ToolOutput<T = object> = {
|
||||
success: boolean;
|
||||
@@ -12,6 +8,5 @@ export type ToolOutput<T = object> = {
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
headers?: Record<string, string>;
|
||||
// Record references for linking to created/found records
|
||||
recordReferences?: RecordReference[];
|
||||
};
|
||||
|
||||
@@ -1,18 +1,10 @@
|
||||
import { type FlexibleSchema } from '@ai-sdk/provider-utils';
|
||||
import { type PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { type CodeExecutionStreamEmitter } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider.interface';
|
||||
|
||||
import { type ToolExecutionContext } from 'src/engine/core-modules/tool/types/tool-execution-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 type ToolExecutionContext = {
|
||||
workspaceId: string;
|
||||
userId?: string;
|
||||
userWorkspaceId?: string;
|
||||
onCodeExecutionUpdate?: CodeExecutionStreamEmitter;
|
||||
};
|
||||
|
||||
export type Tool = {
|
||||
description: string;
|
||||
inputSchema: FlexibleSchema<unknown>;
|
||||
|
||||
Reference in New Issue
Block a user