fix(web): keep agent search runtime-first

This commit is contained in:
Vincent Koc
2026-06-24 09:07:45 +08:00
parent 2a484a3ff1
commit bf13efd818
3 changed files with 20 additions and 46 deletions

View File

@@ -35,20 +35,10 @@ type RunWebSearchParams = {
};
};
type ProviderResolutionParams = {
value?: string;
};
function firstRunWebSearchParams(): RunWebSearchParams | undefined {
return mocks.runWebSearch.mock.calls[0]?.[0] as RunWebSearchParams | undefined;
}
function firstProviderResolutionParams(): ProviderResolutionParams | undefined {
return mocks.resolveManifestContractOwnerPluginId.mock.calls[0]?.[0] as
| ProviderResolutionParams
| undefined;
}
describe("web_search late-bound runtime fallback", () => {
beforeEach(() => {
mocks.runWebSearch.mockReset();
@@ -106,7 +96,7 @@ describe("web_search late-bound runtime fallback", () => {
await tool?.execute("call-search", { query: "openclaw" }, undefined);
expect(firstProviderResolutionParams()?.value).toBe("brave");
expect(mocks.resolveManifestContractOwnerPluginId).not.toHaveBeenCalled();
expect(firstRunWebSearchParams()?.preferRuntimeProviders).toBe(true);
});
@@ -122,7 +112,7 @@ describe("web_search late-bound runtime fallback", () => {
expect(firstRunWebSearchParams()?.preferRuntimeProviders).toBe(true);
});
it("does not prefer runtime providers when the configured provider is a bundled manifest owner", async () => {
it("keeps runtime provider discovery enabled when configured search provider has a manifest owner", async () => {
mocks.resolveManifestContractOwnerPluginId.mockReturnValue("openclaw-bundled-brave");
const config = {
tools: { web: { search: { provider: "brave" } } },
@@ -134,7 +124,8 @@ describe("web_search late-bound runtime fallback", () => {
await tool?.execute("call-search", { query: "openclaw" }, undefined);
expect(firstRunWebSearchParams()?.preferRuntimeProviders).toBe(false);
expect(mocks.resolveManifestContractOwnerPluginId).not.toHaveBeenCalled();
expect(firstRunWebSearchParams()?.preferRuntimeProviders).toBe(true);
});
it("prefers active runtime metadata over options.runtimeWebSearch when present", async () => {

View File

@@ -78,11 +78,8 @@ describe("web tool runtime context", () => {
expect(resolved.config).toBe(runtimeConfig);
expect(resolved.runtimeWebSearch?.selectedProvider).toBe("perplexity");
const ownerLookup = latestOwnerLookupParams();
expect(ownerLookup.contract).toBe("webSearchProviders");
expect(ownerLookup.value).toBe("perplexity");
expect(ownerLookup).not.toHaveProperty("origin");
expect(ownerLookup.config).toBe(runtimeConfig);
expect(resolved.preferRuntimeProviders).toBe(true);
expect(mocks.resolveManifestContractOwnerPluginId).not.toHaveBeenCalled();
});
it("falls back to captured search config and runtime metadata when active globals are missing", async () => {
@@ -104,28 +101,20 @@ describe("web tool runtime context", () => {
expect(resolved.config).toBe(capturedConfig);
expect(resolved.runtimeWebSearch?.selectedProvider).toBe("brave");
const ownerLookup = latestOwnerLookupParams();
expect(ownerLookup.contract).toBe("webSearchProviders");
expect(ownerLookup.value).toBe("brave");
expect(ownerLookup).not.toHaveProperty("origin");
expect(ownerLookup.config).toBe(capturedConfig);
expect(resolved.preferRuntimeProviders).toBe(true);
expect(mocks.resolveManifestContractOwnerPluginId).not.toHaveBeenCalled();
});
it("uses configured provider ids when runtime metadata is absent", () => {
resolveWebSearchToolRuntimeContext({
it("keeps search runtime discovery enabled when runtime metadata is absent", () => {
const resolved = resolveWebSearchToolRuntimeContext({
config: { tools: { web: { search: { provider: "Brave" } } } },
});
const ownerLookup = latestOwnerLookupParams();
expect(ownerLookup.contract).toBe("webSearchProviders");
expect(ownerLookup.value).toBe("brave");
expect(ownerLookup).not.toHaveProperty("origin");
expect(ownerLookup.config).toEqual({
tools: { web: { search: { provider: "Brave" } } },
});
expect(resolved.preferRuntimeProviders).toBe(true);
expect(mocks.resolveManifestContractOwnerPluginId).not.toHaveBeenCalled();
});
it("treats resolved global provider owners as explicit selections", async () => {
it("keeps search runtime discovery enabled for manifest-owned configured providers", async () => {
mocks.resolveManifestContractOwnerPluginId.mockReturnValue("brave");
const { resolveWebSearchToolRuntimeContext: resolveWebSearchToolRuntimeContextLocal } =
await import("./web-tool-runtime-context.js");
@@ -134,10 +123,8 @@ describe("web tool runtime context", () => {
config: { tools: { web: { search: { provider: "brave" } } } },
});
expect(resolved.preferRuntimeProviders).toBe(false);
expect(mocks.resolveManifestContractOwnerPluginId.mock.calls.at(-1)?.[0]).not.toHaveProperty(
"origin",
);
expect(resolved.preferRuntimeProviders).toBe(true);
expect(mocks.resolveManifestContractOwnerPluginId).not.toHaveBeenCalled();
});
it("keeps runtime providers disabled for bundled fetch owners", async () => {

View File

@@ -16,8 +16,6 @@ type WebProviderKind = "fetch" | "search";
type WebProviderRuntimeMetadata = RuntimeWebFetchMetadata | RuntimeWebSearchMetadata;
type WebProviderContract = "webFetchProviders" | "webSearchProviders";
type ResolvedWebToolRuntimeContext<TMetadata extends WebProviderRuntimeMetadata> = {
config?: OpenClawConfig;
preferRuntimeProviders: boolean;
@@ -36,23 +34,21 @@ function resolveRuntimeWebProviderId(metadata: WebProviderRuntimeMetadata | unde
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) {
// Agent-side web_search must use the live runtime registry; runWebSearch
// applies manifest ownership only as a load-scope hint after that.
if (!params.providerSelectionId || params.kind === "search") {
return true;
}
// Built-in providers are handled by core; plugin-owned selections should route through plugins.
return !resolveManifestContractOwnerPluginId({
contract: resolveWebProviderContract(params.kind),
contract: "webFetchProviders",
value: params.providerSelectionId,
...(params.kind === "fetch" ? { origin: "bundled" as const } : {}),
origin: "bundled",
config: params.config,
});
}