mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
refactor(web-tools): share runtime provider context
This commit is contained in:
@@ -34,6 +34,7 @@ import {
|
||||
resolveTimeoutSeconds,
|
||||
writeCache,
|
||||
} from "./web-shared.js";
|
||||
import { resolveWebFetchToolRuntimeContext } from "./web-tool-runtime-context.js";
|
||||
|
||||
const EXTRACT_MODES = ["markdown", "text"] as const;
|
||||
|
||||
@@ -631,11 +632,17 @@ export function createWebFetchTool(options?: {
|
||||
const resolveProviderFallback = async () => {
|
||||
if (!providerFallbackResolved) {
|
||||
const { resolveWebFetchDefinition } = await loadWebFetchRuntime();
|
||||
const { config, preferRuntimeProviders, runtimeWebFetch } = resolveWebFetchToolRuntimeContext(
|
||||
{
|
||||
config: options?.config,
|
||||
runtimeWebFetch: options?.runtimeWebFetch,
|
||||
},
|
||||
);
|
||||
providerFallbackCache = resolveWebFetchDefinition({
|
||||
config: options?.config,
|
||||
config,
|
||||
sandboxed: options?.sandboxed,
|
||||
runtimeWebFetch: options?.runtimeWebFetch,
|
||||
preferRuntimeProviders: true,
|
||||
runtimeWebFetch,
|
||||
preferRuntimeProviders,
|
||||
});
|
||||
providerFallbackResolved = true;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { resolveManifestContractOwnerPluginId } from "../../plugins/plugin-registry.js";
|
||||
import { getActiveRuntimeWebToolsMetadata } from "../../secrets/runtime-web-tools-state.js";
|
||||
import type { RuntimeWebSearchMetadata } from "../../secrets/runtime-web-tools.types.js";
|
||||
import { getActiveSecretsRuntimeSnapshot } from "../../secrets/runtime.js";
|
||||
import { resolveWebSearchProviderId, runWebSearch } from "../../web-search/runtime.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
import { asToolParamsRecord, jsonResult } from "./common.js";
|
||||
import { MAX_SEARCH_COUNT, SEARCH_CACHE } from "./web-search-provider-common.js";
|
||||
import { resolveWebSearchToolRuntimeContext } from "./web-tool-runtime-context.js";
|
||||
|
||||
const WebSearchSchema = {
|
||||
type: "object",
|
||||
@@ -87,32 +85,11 @@ export function createWebSearchTool(options?: {
|
||||
"Search the web. Returns provider-normalized results for current information lookup.",
|
||||
parameters: WebSearchSchema,
|
||||
execute: async (_toolCallId, args, signal) => {
|
||||
const runtimeWebSearch =
|
||||
options?.lateBindRuntimeConfig === true
|
||||
? (getActiveRuntimeWebToolsMetadata()?.search ?? options?.runtimeWebSearch)
|
||||
: options?.runtimeWebSearch;
|
||||
const runtimeProviderId =
|
||||
runtimeWebSearch?.selectedProvider ?? runtimeWebSearch?.providerConfigured;
|
||||
const config =
|
||||
options?.lateBindRuntimeConfig === true
|
||||
? (getActiveSecretsRuntimeSnapshot()?.config ?? options?.config)
|
||||
: options?.config;
|
||||
// The active gateway plugin registry may omit the configured search
|
||||
// provider; fall back to the provider id captured in config so the
|
||||
// first-class assistant tool still resolves the right plugin instead of
|
||||
// reporting "no provider available".
|
||||
const configuredProviderId =
|
||||
typeof config?.tools?.web?.search?.provider === "string"
|
||||
? config.tools.web.search.provider.trim().toLowerCase()
|
||||
: "";
|
||||
const providerSelectionId = runtimeProviderId || configuredProviderId;
|
||||
const preferRuntimeProviders =
|
||||
!providerSelectionId ||
|
||||
!resolveManifestContractOwnerPluginId({
|
||||
contract: "webSearchProviders",
|
||||
value: providerSelectionId,
|
||||
origin: "bundled",
|
||||
config,
|
||||
const { config, preferRuntimeProviders, runtimeWebSearch } =
|
||||
resolveWebSearchToolRuntimeContext({
|
||||
config: options?.config,
|
||||
lateBindRuntimeConfig: options?.lateBindRuntimeConfig,
|
||||
runtimeWebSearch: options?.runtimeWebSearch,
|
||||
});
|
||||
const result = await runWebSearch({
|
||||
config,
|
||||
|
||||
144
src/agents/tools/web-tool-runtime-context.test.ts
Normal file
144
src/agents/tools/web-tool-runtime-context.test.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
getActiveRuntimeWebToolsMetadata: vi.fn(),
|
||||
getActiveSecretsRuntimeSnapshot: vi.fn(),
|
||||
resolveManifestContractOwnerPluginId: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../plugins/plugin-registry.js", () => ({
|
||||
resolveManifestContractOwnerPluginId: mocks.resolveManifestContractOwnerPluginId,
|
||||
}));
|
||||
|
||||
vi.mock("../../secrets/runtime-web-tools-state.js", () => ({
|
||||
getActiveRuntimeWebToolsMetadata: mocks.getActiveRuntimeWebToolsMetadata,
|
||||
}));
|
||||
|
||||
vi.mock("../../secrets/runtime.js", () => ({
|
||||
getActiveSecretsRuntimeSnapshot: mocks.getActiveSecretsRuntimeSnapshot,
|
||||
}));
|
||||
|
||||
describe("web tool runtime context", () => {
|
||||
beforeEach(() => {
|
||||
mocks.getActiveRuntimeWebToolsMetadata.mockReset();
|
||||
mocks.getActiveRuntimeWebToolsMetadata.mockReturnValue(null);
|
||||
mocks.getActiveSecretsRuntimeSnapshot.mockReset();
|
||||
mocks.getActiveSecretsRuntimeSnapshot.mockReturnValue(null);
|
||||
mocks.resolveManifestContractOwnerPluginId.mockReset();
|
||||
mocks.resolveManifestContractOwnerPluginId.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
it("late-binds search config and metadata from active runtime before captured options", async () => {
|
||||
const runtimeConfig = {
|
||||
tools: { web: { search: { provider: "perplexity" } } },
|
||||
};
|
||||
mocks.getActiveSecretsRuntimeSnapshot.mockReturnValue({ config: runtimeConfig });
|
||||
mocks.getActiveRuntimeWebToolsMetadata.mockReturnValue({
|
||||
search: {
|
||||
providerConfigured: "perplexity",
|
||||
providerSource: "configured",
|
||||
selectedProvider: "perplexity",
|
||||
selectedProviderKeySource: "config",
|
||||
diagnostics: [],
|
||||
},
|
||||
fetch: {
|
||||
providerSource: "none",
|
||||
diagnostics: [],
|
||||
},
|
||||
diagnostics: [],
|
||||
});
|
||||
const { resolveWebSearchToolRuntimeContext } = await import("./web-tool-runtime-context.js");
|
||||
|
||||
const resolved = resolveWebSearchToolRuntimeContext({
|
||||
config: { tools: { web: { search: { provider: "brave" } } } },
|
||||
lateBindRuntimeConfig: true,
|
||||
runtimeWebSearch: {
|
||||
providerConfigured: "brave",
|
||||
providerSource: "configured",
|
||||
selectedProvider: "brave",
|
||||
selectedProviderKeySource: "config",
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved.config).toBe(runtimeConfig);
|
||||
expect(resolved.runtimeWebSearch).toMatchObject({ selectedProvider: "perplexity" });
|
||||
expect(mocks.resolveManifestContractOwnerPluginId).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
contract: "webSearchProviders",
|
||||
value: "perplexity",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("falls back to captured search config and runtime metadata when active globals are missing", async () => {
|
||||
const capturedConfig = {
|
||||
tools: { web: { search: { provider: "brave" } } },
|
||||
};
|
||||
const { resolveWebSearchToolRuntimeContext } = await import("./web-tool-runtime-context.js");
|
||||
|
||||
const resolved = resolveWebSearchToolRuntimeContext({
|
||||
config: capturedConfig,
|
||||
lateBindRuntimeConfig: true,
|
||||
runtimeWebSearch: {
|
||||
providerConfigured: "brave",
|
||||
providerSource: "configured",
|
||||
selectedProvider: "brave",
|
||||
selectedProviderKeySource: "config",
|
||||
diagnostics: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolved.config).toBe(capturedConfig);
|
||||
expect(resolved.runtimeWebSearch).toMatchObject({ selectedProvider: "brave" });
|
||||
expect(mocks.resolveManifestContractOwnerPluginId).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
contract: "webSearchProviders",
|
||||
value: "brave",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses configured provider ids when runtime metadata is absent", async () => {
|
||||
const { resolveWebSearchToolRuntimeContext } = await import("./web-tool-runtime-context.js");
|
||||
|
||||
resolveWebSearchToolRuntimeContext({
|
||||
config: { tools: { web: { search: { provider: "Brave" } } } },
|
||||
});
|
||||
|
||||
expect(mocks.resolveManifestContractOwnerPluginId).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
contract: "webSearchProviders",
|
||||
value: "brave",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps runtime providers disabled for bundled fetch owners", async () => {
|
||||
mocks.resolveManifestContractOwnerPluginId.mockReturnValue("firecrawl");
|
||||
const { resolveWebFetchToolRuntimeContext } = await import("./web-tool-runtime-context.js");
|
||||
|
||||
const resolved = resolveWebFetchToolRuntimeContext({
|
||||
config: { tools: { web: { fetch: { provider: "firecrawl" } } } },
|
||||
});
|
||||
|
||||
expect(resolved.preferRuntimeProviders).toBe(false);
|
||||
expect(mocks.resolveManifestContractOwnerPluginId).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
contract: "webFetchProviders",
|
||||
value: "firecrawl",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps runtime provider discovery enabled when no provider is selected", async () => {
|
||||
const { resolveWebFetchToolRuntimeContext } = await import("./web-tool-runtime-context.js");
|
||||
|
||||
const resolved = resolveWebFetchToolRuntimeContext({
|
||||
config: {},
|
||||
});
|
||||
|
||||
expect(resolved.preferRuntimeProviders).toBe(true);
|
||||
expect(mocks.resolveManifestContractOwnerPluginId).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
123
src/agents/tools/web-tool-runtime-context.ts
Normal file
123
src/agents/tools/web-tool-runtime-context.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { resolveManifestContractOwnerPluginId } from "../../plugins/plugin-registry.js";
|
||||
import { getActiveRuntimeWebToolsMetadata } from "../../secrets/runtime-web-tools-state.js";
|
||||
import type {
|
||||
RuntimeWebFetchMetadata,
|
||||
RuntimeWebSearchMetadata,
|
||||
} from "../../secrets/runtime-web-tools.types.js";
|
||||
import { getActiveSecretsRuntimeSnapshot } from "../../secrets/runtime.js";
|
||||
|
||||
type WebProviderKind = "fetch" | "search";
|
||||
|
||||
type WebProviderRuntimeMetadata = RuntimeWebFetchMetadata | RuntimeWebSearchMetadata;
|
||||
|
||||
type WebProviderContract = "webFetchProviders" | "webSearchProviders";
|
||||
|
||||
type ResolvedWebToolRuntimeContext<TMetadata extends WebProviderRuntimeMetadata> = {
|
||||
config?: OpenClawConfig;
|
||||
preferRuntimeProviders: boolean;
|
||||
runtimeMetadata?: TMetadata;
|
||||
};
|
||||
|
||||
function resolveConfiguredWebProviderId(
|
||||
config: OpenClawConfig | undefined,
|
||||
kind: WebProviderKind,
|
||||
): string {
|
||||
const provider = config?.tools?.web?.[kind]?.provider;
|
||||
return typeof provider === "string" ? provider.trim().toLowerCase() : "";
|
||||
}
|
||||
|
||||
function resolveRuntimeWebProviderId(metadata: WebProviderRuntimeMetadata | undefined): string {
|
||||
return metadata?.selectedProvider ?? metadata?.providerConfigured ?? "";
|
||||
}
|
||||
|
||||
function resolveWebProviderContract(kind: WebProviderKind): WebProviderContract {
|
||||
return kind === "fetch" ? "webFetchProviders" : "webSearchProviders";
|
||||
}
|
||||
|
||||
function shouldPreferRuntimeProviders(params: {
|
||||
config?: OpenClawConfig;
|
||||
kind: WebProviderKind;
|
||||
providerSelectionId: string;
|
||||
}): boolean {
|
||||
if (!params.providerSelectionId) {
|
||||
return true;
|
||||
}
|
||||
return !resolveManifestContractOwnerPluginId({
|
||||
contract: resolveWebProviderContract(params.kind),
|
||||
value: params.providerSelectionId,
|
||||
origin: "bundled",
|
||||
config: params.config,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveWebToolRuntimeContext<TMetadata extends WebProviderRuntimeMetadata>(params: {
|
||||
capturedConfig?: OpenClawConfig;
|
||||
capturedRuntimeMetadata?: TMetadata;
|
||||
kind: WebProviderKind;
|
||||
lateBindRuntimeConfig?: boolean;
|
||||
}): ResolvedWebToolRuntimeContext<TMetadata> {
|
||||
const activeWebTools =
|
||||
params.lateBindRuntimeConfig === true ? getActiveRuntimeWebToolsMetadata() : null;
|
||||
const runtimeMetadata = (activeWebTools?.[params.kind] ?? params.capturedRuntimeMetadata) as
|
||||
| TMetadata
|
||||
| undefined;
|
||||
const config =
|
||||
params.lateBindRuntimeConfig === true
|
||||
? (getActiveSecretsRuntimeSnapshot()?.config ?? params.capturedConfig)
|
||||
: params.capturedConfig;
|
||||
const providerSelectionId =
|
||||
resolveRuntimeWebProviderId(runtimeMetadata) ||
|
||||
resolveConfiguredWebProviderId(config, params.kind);
|
||||
return {
|
||||
config,
|
||||
preferRuntimeProviders: shouldPreferRuntimeProviders({
|
||||
config,
|
||||
kind: params.kind,
|
||||
providerSelectionId,
|
||||
}),
|
||||
runtimeMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveWebSearchToolRuntimeContext(params: {
|
||||
config?: OpenClawConfig;
|
||||
lateBindRuntimeConfig?: boolean;
|
||||
runtimeWebSearch?: RuntimeWebSearchMetadata;
|
||||
}): ResolvedWebToolRuntimeContext<RuntimeWebSearchMetadata> & {
|
||||
runtimeWebSearch?: RuntimeWebSearchMetadata;
|
||||
} {
|
||||
const resolved = resolveWebToolRuntimeContext({
|
||||
capturedConfig: params.config,
|
||||
capturedRuntimeMetadata: params.runtimeWebSearch,
|
||||
kind: "search",
|
||||
lateBindRuntimeConfig: params.lateBindRuntimeConfig,
|
||||
});
|
||||
return {
|
||||
config: resolved.config,
|
||||
preferRuntimeProviders: resolved.preferRuntimeProviders,
|
||||
runtimeMetadata: resolved.runtimeMetadata,
|
||||
runtimeWebSearch: resolved.runtimeMetadata,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveWebFetchToolRuntimeContext(params: {
|
||||
config?: OpenClawConfig;
|
||||
lateBindRuntimeConfig?: boolean;
|
||||
runtimeWebFetch?: RuntimeWebFetchMetadata;
|
||||
}): ResolvedWebToolRuntimeContext<RuntimeWebFetchMetadata> & {
|
||||
runtimeWebFetch?: RuntimeWebFetchMetadata;
|
||||
} {
|
||||
const resolved = resolveWebToolRuntimeContext({
|
||||
capturedConfig: params.config,
|
||||
capturedRuntimeMetadata: params.runtimeWebFetch,
|
||||
kind: "fetch",
|
||||
lateBindRuntimeConfig: params.lateBindRuntimeConfig,
|
||||
});
|
||||
return {
|
||||
config: resolved.config,
|
||||
preferRuntimeProviders: resolved.preferRuntimeProviders,
|
||||
runtimeMetadata: resolved.runtimeMetadata,
|
||||
runtimeWebFetch: resolved.runtimeMetadata,
|
||||
};
|
||||
}
|
||||
@@ -64,6 +64,11 @@ type WebProviderRuntimeContext = {
|
||||
onlyPluginIds?: string[];
|
||||
};
|
||||
|
||||
type RuntimeRegistryWebProviderResolution<TEntry> = {
|
||||
providers: TEntry[];
|
||||
shouldReturn: boolean;
|
||||
};
|
||||
|
||||
function resolveWebProviderRuntimeContext<TEntry>(
|
||||
params: ResolvePluginWebProvidersParams,
|
||||
deps: ResolveWebProviderRuntimeDeps<TEntry>,
|
||||
@@ -125,6 +130,25 @@ function resolveWebProviderLoadOptions(
|
||||
);
|
||||
}
|
||||
|
||||
function resolveRuntimeRegistryWebProviders<TEntry>(params: {
|
||||
hasExplicitEmptyScope: boolean;
|
||||
mapRegistryProviders: ResolveWebProviderRuntimeDeps<TEntry>["mapRegistryProviders"];
|
||||
onlyPluginIds?: readonly string[];
|
||||
registry: PluginRegistry | undefined;
|
||||
}): RuntimeRegistryWebProviderResolution<TEntry> | undefined {
|
||||
if (!params.registry) {
|
||||
return undefined;
|
||||
}
|
||||
const providers = params.mapRegistryProviders({
|
||||
registry: params.registry,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
});
|
||||
return {
|
||||
providers,
|
||||
shouldReturn: providers.length > 0 || params.hasExplicitEmptyScope,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePluginWebProviders<TEntry>(
|
||||
params: ResolvePluginWebProvidersParams,
|
||||
deps: ResolveWebProviderRuntimeDeps<TEntry>,
|
||||
@@ -188,14 +212,16 @@ export function resolvePluginWebProviders<TEntry>(
|
||||
});
|
||||
const scopedPluginIds = context.onlyPluginIds;
|
||||
const hasExplicitEmptyScope = scopedPluginIds !== undefined && scopedPluginIds.length === 0;
|
||||
if (compatible) {
|
||||
const resolved = deps.mapRegistryProviders({
|
||||
registry: compatible,
|
||||
onlyPluginIds: context.onlyPluginIds,
|
||||
});
|
||||
if (resolved.length > 0 || hasExplicitEmptyScope) {
|
||||
return resolved;
|
||||
}
|
||||
const compatibleProviders = resolveRuntimeRegistryWebProviders({
|
||||
hasExplicitEmptyScope,
|
||||
mapRegistryProviders: deps.mapRegistryProviders,
|
||||
onlyPluginIds: context.onlyPluginIds,
|
||||
registry: compatible,
|
||||
});
|
||||
if (compatibleProviders?.shouldReturn) {
|
||||
return compatibleProviders.providers;
|
||||
}
|
||||
if (compatibleProviders) {
|
||||
// The active gateway plugin registry may be otherwise compatible with this
|
||||
// config while contributing zero web providers (for example when channels,
|
||||
// memory, harnesses, and sidecars are loaded but Brave/web providers are
|
||||
@@ -225,16 +251,16 @@ export function resolveRuntimeWebProviders<TEntry>(
|
||||
workspaceDir: params.workspaceDir,
|
||||
requiredPluginIds: params.onlyPluginIds,
|
||||
});
|
||||
if (runtimeRegistry) {
|
||||
const resolved = deps.mapRegistryProviders({
|
||||
registry: runtimeRegistry,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
});
|
||||
const hasExplicitEmptyScope =
|
||||
params.onlyPluginIds !== undefined && params.onlyPluginIds.length === 0;
|
||||
if (resolved.length > 0 || hasExplicitEmptyScope) {
|
||||
return resolved;
|
||||
}
|
||||
const hasExplicitEmptyScope =
|
||||
params.onlyPluginIds !== undefined && params.onlyPluginIds.length === 0;
|
||||
const runtimeProviders = resolveRuntimeRegistryWebProviders({
|
||||
hasExplicitEmptyScope,
|
||||
mapRegistryProviders: deps.mapRegistryProviders,
|
||||
onlyPluginIds: params.onlyPluginIds,
|
||||
registry: runtimeRegistry,
|
||||
});
|
||||
if (runtimeProviders?.shouldReturn) {
|
||||
return runtimeProviders.providers;
|
||||
}
|
||||
return resolvePluginWebProviders(params, deps);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user