refactor(tool-provider): rename NativeModelToolProvider to NativeToolBinderService (#19966)

**Stacked on top of #19962.**

## Summary

- `NativeModelToolProvider` lived under `providers/` and had the
`*-tool.provider.ts` suffix, but it never implemented `ToolProvider`,
wasn't in `TOOL_PROVIDERS`, had no descriptors, and wasn't executed by
`ToolExecutorService`. The shape misled readers.
- It's actually a **parallel concept**: a binder that produces
SDK-native tool objects (Anthropic `webSearch`, OpenAI `webSearch`,
etc.) which the AI SDK passes straight to the model. Opaque, not
serializable, never in the catalog, never dispatched by the executor.
- This PR renames + moves it to reflect that.

## Renames

| Before | After |
|---|---|
| `NativeModelToolProvider` (class) | `NativeToolBinderService` |
| `NativeToolProvider` (interface) | `NativeToolBinder` |
| `generateTools(context)` (method) | `bind(context)` |
| `providers/native-model-tool.provider.ts` |
`native/native-tool-binder.service.ts` |
| `interfaces/native-tool-provider.interface.ts` |
`native/native-tool-binder.interface.ts` |

## What doesn't change

- `ToolCategory.NATIVE_MODEL` enum stays (still used by
`getToolsByCategories`).
- `isAvailable()` signature unchanged.
- `WebSearchService.shouldUseNativeSearch()` toggle untouched — that's
product-level and belongs to a separate PR that handles the Exa
coexistence story.
- No behavior change. Pure rename + move.

## Why this matters for the broader architecture

This rename makes the native/binder concept **visible in the type system
and directory structure**. That's what later enables coexisting native +
custom tools (e.g., `web_search` native alongside `exa_web_search`
custom) without the current naming collision, because native tools are
no longer masquerading as a registry provider.

## Stats

- 5 files, +30 / −28.
- Blast radius: 4 files modified, 1 file renamed (git tracks as rename).
- Typecheck clean (7 pre-existing unrelated errors, zero new).
- Prettier clean.

## Test plan

- [ ] `npx nx typecheck twenty-server` passes
- [ ] AI chat: native `web_search` still works end-to-end when enabled
- [ ] Workflow AI agent: `ToolCategory.NATIVE_MODEL` still works (goes
through `bind()` now)
- [ ] MCP: unaffected (doesn't use native tools)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-04-22 13:43:30 +02:00
committed by GitHub
parent 2a5d5b36db
commit 44309a6fd9
5 changed files with 30 additions and 28 deletions
@@ -1,12 +0,0 @@
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>;
}
@@ -0,0 +1,18 @@
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';
// Parallel to ToolProvider, not a variant of it. A binder produces SDK-native
// tool objects (Anthropic webSearch, OpenAI webSearch, etc.) that the AI SDK
// passes straight to the model. These tools are opaque — they can't be
// serialized into descriptors, don't appear in the tool catalog, and aren't
// executed by ToolExecutorService. They're merged directly into the ToolSet
// handed to streamText.
export interface NativeToolBinder {
readonly category: ToolCategory;
isAvailable(context: ToolProviderContext): Promise<boolean>;
bind(context: ToolProviderContext): Promise<ToolSet>;
}
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import { type ToolSet } from 'ai';
import { isDefined } from 'twenty-shared/utils';
import { type NativeToolProvider } from 'src/engine/core-modules/tool-provider/interfaces/native-tool-provider.interface';
import { type NativeToolBinder } from 'src/engine/core-modules/tool-provider/native/native-tool-binder.interface';
import { type ToolProviderContext } from 'src/engine/core-modules/tool-provider/interfaces/tool-provider-context.type';
import { ToolCategory } from 'twenty-shared/ai';
@@ -11,10 +11,8 @@ import { WebSearchService } from 'src/engine/core-modules/web-search/web-search.
import { AiModelConfigService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-config.service';
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
// SDK-native tools (anthropic webSearch, etc.) are opaque and not serializable.
// This provider keeps generateTools() and is excluded from the descriptor system.
@Injectable()
export class NativeModelToolProvider implements NativeToolProvider {
export class NativeToolBinderService implements NativeToolBinder {
readonly category = ToolCategory.NATIVE_MODEL;
constructor(
@@ -27,7 +25,7 @@ export class NativeModelToolProvider implements NativeToolProvider {
return isDefined(context.agent);
}
async generateTools(context: ToolProviderContext): Promise<ToolSet> {
async bind(context: ToolProviderContext): Promise<ToolSet> {
if (!context.agent) {
return {};
}
@@ -2,15 +2,14 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
import { type ToolSet, jsonSchema } from 'ai';
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 { NativeToolBinderService } from 'src/engine/core-modules/tool-provider/native/native-tool-binder.service';
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 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';
@@ -31,7 +30,7 @@ export class ToolRegistryService {
constructor(
@Inject(TOOL_PROVIDERS)
private readonly providers: ToolProvider[],
private readonly nativeModelToolProvider: NativeModelToolProvider,
private readonly nativeToolBinder: NativeToolBinderService,
private readonly toolExecutorService: ToolExecutorService,
) {}
@@ -329,10 +328,8 @@ export class ToolRegistryService {
});
if (categories?.includes(ToolCategory.NATIVE_MODEL)) {
if (await this.nativeModelToolProvider.isAvailable(context)) {
const nativeTools = await (
this.nativeModelToolProvider as NativeToolProvider
).generateTools(context);
if (await this.nativeToolBinder.isAvailable(context)) {
const nativeTools = await this.nativeToolBinder.bind(context);
Object.assign(toolSet, nativeTools);
}
@@ -8,7 +8,7 @@ import { DashboardToolProvider } from 'src/engine/core-modules/tool-provider/pro
import { DatabaseToolProvider } from 'src/engine/core-modules/tool-provider/providers/database-tool.provider';
import { LogicFunctionToolProvider } from 'src/engine/core-modules/tool-provider/providers/logic-function-tool.provider';
import { MetadataToolProvider } from 'src/engine/core-modules/tool-provider/providers/metadata-tool.provider';
import { NativeModelToolProvider } from 'src/engine/core-modules/tool-provider/providers/native-model-tool.provider';
import { NativeToolBinderService } from 'src/engine/core-modules/tool-provider/native/native-tool-binder.service';
import { ViewFieldToolProvider } from 'src/engine/core-modules/tool-provider/providers/view-field-tool.provider';
import { ViewToolProvider } from 'src/engine/core-modules/tool-provider/providers/view-tool.provider';
import { WorkflowToolProvider } from 'src/engine/core-modules/tool-provider/providers/workflow-tool.provider';
@@ -63,14 +63,15 @@ import { ToolRegistryService } from './services/tool-registry.service';
DashboardToolProvider,
DatabaseToolProvider,
MetadataToolProvider,
NativeModelToolProvider,
NativeToolBinderService,
LogicFunctionToolProvider,
ViewFieldToolProvider,
ViewToolProvider,
WorkflowToolProvider,
{
// TOOL_PROVIDERS contains only providers implementing ToolProvider (generateDescriptors).
// NativeModelToolProvider is excluded -- it's injected separately in the registry.
// TOOL_PROVIDERS contains only providers implementing ToolProvider
// (registry tools with descriptors). The native tool binder is a
// parallel concept and is injected directly into ToolRegistryService.
provide: TOOL_PROVIDERS,
useFactory: (
actionProvider: ActionToolProvider,