From d6b3950734960ab2f1d078b048bf5ba58b784eea Mon Sep 17 00:00:00 2001 From: Darren Xu Date: Sat, 13 Jun 2026 16:43:17 -0700 Subject: [PATCH] fix(tavily): keep web search contract executable Keep the Tavily public artifact lightweight while lazily executing through the provider runtime. Verified with focused Tavily/provider artifact tests and clean CI. --- .../tavily/src/tavily-search-provider.ts | 25 +++++---------- extensions/tavily/src/tavily-tools.test.ts | 29 +++++++++++++++++ extensions/tavily/web-search-contract-api.ts | 31 +++++++++++++++++-- extensions/tavily/web-search-shared.ts | 17 ++++++++++ 4 files changed, 81 insertions(+), 21 deletions(-) diff --git a/extensions/tavily/src/tavily-search-provider.ts b/extensions/tavily/src/tavily-search-provider.ts index 068f1c9370f6..ad8bf1e9a7da 100644 --- a/extensions/tavily/src/tavily-search-provider.ts +++ b/extensions/tavily/src/tavily-search-provider.ts @@ -1,7 +1,11 @@ // Tavily provider module implements model/runtime integration. import { readPositiveIntegerParam } from "openclaw/plugin-sdk/param-readers"; import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-contract"; -import { buildTavilyWebSearchProviderBase } from "../web-search-shared.js"; +import { + buildTavilyWebSearchProviderBase, + TAVILY_GENERIC_SEARCH_DESCRIPTION, + TAVILY_GENERIC_SEARCH_SCHEMA, +} from "../web-search-shared.js"; type TavilyClientModule = typeof import("./tavily-client.js"); @@ -12,27 +16,12 @@ function loadTavilyClientModule(): Promise { return tavilyClientModulePromise; } -const GenericTavilySearchSchema = { - type: "object", - properties: { - query: { type: "string", description: "Search query string." }, - count: { - type: "integer", - description: "Number of results to return (1-20).", - minimum: 1, - maximum: 20, - }, - }, - additionalProperties: false, -} satisfies Record; - export function createTavilyWebSearchProvider(): WebSearchProviderPlugin { return { ...buildTavilyWebSearchProviderBase(), createTool: (ctx) => ({ - description: - "Search the web using Tavily. Returns structured results with snippets. Use tavily_search for Tavily-specific options like search depth, topic filtering, or AI answers.", - parameters: GenericTavilySearchSchema, + description: TAVILY_GENERIC_SEARCH_DESCRIPTION, + parameters: TAVILY_GENERIC_SEARCH_SCHEMA, execute: async (args) => { const { runTavilySearch } = await loadTavilyClientModule(); return await runTavilySearch({ diff --git a/extensions/tavily/src/tavily-tools.test.ts b/extensions/tavily/src/tavily-tools.test.ts index 9ff854a33966..1cc0eb80a6c9 100644 --- a/extensions/tavily/src/tavily-tools.test.ts +++ b/extensions/tavily/src/tavily-tools.test.ts @@ -47,6 +47,7 @@ function fakeApi(): OpenClawPluginApi { describe("tavily tools", () => { let createTavilyWebSearchProvider: typeof import("./tavily-search-provider.js").createTavilyWebSearchProvider; + let createTavilyContractWebSearchProvider: typeof import("../web-search-contract-api.js").createTavilyWebSearchProvider; let createTavilySearchTool: typeof import("./tavily-search-tool.js").createTavilySearchTool; let createTavilyExtractTool: typeof import("./tavily-extract-tool.js").createTavilyExtractTool; let tavilyClientTesting: typeof import("./tavily-client.js").testing; @@ -54,6 +55,8 @@ describe("tavily tools", () => { beforeAll(async () => { ({ createTavilyWebSearchProvider } = await import("./tavily-search-provider.js")); + ({ createTavilyWebSearchProvider: createTavilyContractWebSearchProvider } = + await import("../web-search-contract-api.js")); ({ createTavilySearchTool } = await import("./tavily-search-tool.js")); ({ createTavilyExtractTool } = await import("./tavily-extract-tool.js")); ({ testing: tavilyClientTesting } = @@ -110,6 +113,32 @@ describe("tavily tools", () => { }); }); + it("keeps the contract web search provider executable", async () => { + const provider = createTavilyContractWebSearchProvider(); + const tool = provider.createTool({ + config: { test: "contract" }, + } as never); + if (!tool) { + throw new Error("Expected contract provider tool definition"); + } + + const result = await tool.execute({ + query: "runtime registration", + count: 3, + }); + + expect(runTavilySearch).toHaveBeenCalledWith({ + cfg: { test: "contract" }, + query: "runtime registration", + maxResults: 3, + }); + expect(result).toEqual({ + cfg: { test: "contract" }, + query: "runtime registration", + maxResults: 3, + }); + }); + it("normalizes generic Tavily search count before dispatch", async () => { const provider = createTavilyWebSearchProvider(); const tool = provider.createTool({ diff --git a/extensions/tavily/web-search-contract-api.ts b/extensions/tavily/web-search-contract-api.ts index a8c6dbad0970..34845eaef4fd 100644 --- a/extensions/tavily/web-search-contract-api.ts +++ b/extensions/tavily/web-search-contract-api.ts @@ -1,10 +1,35 @@ // Tavily API module exposes the plugin public contract. -import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-contract"; -import { buildTavilyWebSearchProviderBase } from "./web-search-shared.js"; +import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-config-contract"; +import { + buildTavilyWebSearchProviderBase, + TAVILY_GENERIC_SEARCH_DESCRIPTION, + TAVILY_GENERIC_SEARCH_SCHEMA, +} from "./web-search-shared.js"; + +type TavilySearchProviderModule = typeof import("./src/tavily-search-provider.js"); + +let tavilySearchProviderModulePromise: Promise | undefined; + +function loadTavilySearchProviderModule(): Promise { + tavilySearchProviderModulePromise ??= import("./src/tavily-search-provider.js"); + return tavilySearchProviderModulePromise; +} export function createTavilyWebSearchProvider(): WebSearchProviderPlugin { return { ...buildTavilyWebSearchProviderBase(), - createTool: () => null, + createTool: (ctx) => ({ + description: TAVILY_GENERIC_SEARCH_DESCRIPTION, + parameters: TAVILY_GENERIC_SEARCH_SCHEMA, + execute: async (args) => { + const { createTavilyWebSearchProvider: createRuntimeProvider } = + await loadTavilySearchProviderModule(); + const tool = createRuntimeProvider().createTool(ctx); + if (!tool) { + throw new Error("Tavily web_search provider did not create a runtime tool."); + } + return await tool.execute(args); + }, + }), }; } diff --git a/extensions/tavily/web-search-shared.ts b/extensions/tavily/web-search-shared.ts index 0d180c8e406c..5cdac9689972 100644 --- a/extensions/tavily/web-search-shared.ts +++ b/extensions/tavily/web-search-shared.ts @@ -6,6 +6,23 @@ import { export const TAVILY_CREDENTIAL_PATH = "plugins.entries.tavily.config.webSearch.apiKey"; +export const TAVILY_GENERIC_SEARCH_DESCRIPTION = + "Search the web using Tavily. Returns structured results with snippets. Use tavily_search for Tavily-specific options like search depth, topic filtering, or AI answers."; + +export const TAVILY_GENERIC_SEARCH_SCHEMA = { + type: "object", + properties: { + query: { type: "string", description: "Search query string." }, + count: { + type: "integer", + description: "Number of results to return (1-20).", + minimum: 1, + maximum: 20, + }, + }, + additionalProperties: false, +} satisfies Record; + export function buildTavilyWebSearchProviderBase(): Omit { return { id: "tavily",