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.
This commit is contained in:
Darren Xu
2026-06-13 16:43:17 -07:00
committed by GitHub
parent 61145dc252
commit d6b3950734
4 changed files with 81 additions and 21 deletions

View File

@@ -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<TavilyClientModule> {
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<string, unknown>;
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({

View File

@@ -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({

View File

@@ -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<TavilySearchProviderModule> | undefined;
function loadTavilySearchProviderModule(): Promise<TavilySearchProviderModule> {
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);
},
}),
};
}

View File

@@ -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<string, unknown>;
export function buildTavilyWebSearchProviderBase(): Omit<WebSearchProviderPlugin, "createTool"> {
return {
id: "tavily",