mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:50:43 +00:00
fix(web-search): late bind managed runtime config
This commit is contained in:
@@ -26,6 +26,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/configure: preserve the existing default model when adding or reauthing a provider whose plugin returns a default-model config patch. Fixes #50268. Thanks @rixcorp-oc.
|
||||
- Slack/message tool: let `read` fetch an exact Slack message timestamp, including a specific thread reply when paired with `threadId`, instead of returning only the parent thread or recent channel history. Fixes #53943. Thanks @zomars.
|
||||
- Web search: point missing-key errors to `web_fetch` for known URLs and the browser tool for interactive pages. Thanks @zhaoyang97.
|
||||
- Web search: late-bind managed agent `web_search` calls to the current runtime config snapshot, so existing sessions do not keep stale unresolved SecretRefs after secrets reload. Fixes #75420. Thanks @richardmqq.
|
||||
- Heartbeat: strip legacy `[TOOL_CALL]...[/TOOL_CALL]` and `[TOOL_RESULT]...[/TOOL_RESULT]` pseudo-call blocks from heartbeat replies before channel delivery. Fixes #54138. Thanks @Deniable9570.
|
||||
- macOS/Voice Wake: send wake-word and Push-to-Talk transcripts through the selected macOS session target instead of always falling back to main WebChat. Fixes #51040. Thanks @carl-jeffrolc.
|
||||
- Providers/xAI: give Grok `web_search` a 60s default timeout, harden malformed xAI Responses parsing, and return structured timeout errors instead of aborting the tool call. Fixes #58063 and #58733. Thanks @dnishimura, @marvcasasola-svg, and @Nanako0129.
|
||||
|
||||
@@ -194,6 +194,7 @@ export function createOpenClawTools(
|
||||
config: options?.config,
|
||||
sandboxed: options?.sandboxed,
|
||||
runtimeWebSearch: runtimeWebTools?.search,
|
||||
lateBindRuntimeConfig: true,
|
||||
});
|
||||
const webFetchTool = createWebFetchTool({
|
||||
config: options?.config,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { resolveWebSearchProviderId, runWebSearch } from "../../web-search/runtime.js";
|
||||
import type { AnyAgentTool } from "./common.js";
|
||||
@@ -72,20 +73,11 @@ export function createWebSearchTool(options?: {
|
||||
config?: OpenClawConfig;
|
||||
sandboxed?: boolean;
|
||||
runtimeWebSearch?: RuntimeWebSearchMetadata;
|
||||
lateBindRuntimeConfig?: boolean;
|
||||
}): AnyAgentTool | null {
|
||||
if (isWebSearchDisabled(options?.config)) {
|
||||
return null;
|
||||
}
|
||||
const runtimeProviderId =
|
||||
options?.runtimeWebSearch?.selectedProvider ?? options?.runtimeWebSearch?.providerConfigured;
|
||||
const preferRuntimeProviders =
|
||||
Boolean(runtimeProviderId) &&
|
||||
!resolveManifestContractOwnerPluginId({
|
||||
contract: "webSearchProviders",
|
||||
value: runtimeProviderId,
|
||||
origin: "bundled",
|
||||
config: options?.config,
|
||||
});
|
||||
|
||||
return {
|
||||
label: "Web Search",
|
||||
@@ -94,10 +86,25 @@ export function createWebSearchTool(options?: {
|
||||
"Search the web. Returns provider-normalized results for current information lookup.",
|
||||
parameters: WebSearchSchema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
const runtimeWebSearch =
|
||||
options?.lateBindRuntimeConfig === true
|
||||
? getActiveRuntimeWebToolsMetadata()?.search
|
||||
: options?.runtimeWebSearch;
|
||||
const runtimeProviderId =
|
||||
runtimeWebSearch?.selectedProvider ?? runtimeWebSearch?.providerConfigured;
|
||||
const config = options?.lateBindRuntimeConfig === true ? undefined : options?.config;
|
||||
const preferRuntimeProviders =
|
||||
Boolean(runtimeProviderId) &&
|
||||
!resolveManifestContractOwnerPluginId({
|
||||
contract: "webSearchProviders",
|
||||
value: runtimeProviderId,
|
||||
origin: "bundled",
|
||||
config,
|
||||
});
|
||||
const result = await runWebSearch({
|
||||
config: options?.config,
|
||||
config,
|
||||
sandboxed: options?.sandboxed,
|
||||
runtimeWebSearch: options?.runtimeWebSearch,
|
||||
runtimeWebSearch,
|
||||
preferRuntimeProviders,
|
||||
args: asToolParamsRecord(args),
|
||||
});
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import { clearActiveRuntimeWebToolsMetadata } from "../../secrets/runtime-web-tools-state.js";
|
||||
import {
|
||||
clearActiveRuntimeWebToolsMetadata,
|
||||
setActiveRuntimeWebToolsMetadata,
|
||||
} from "../../secrets/runtime-web-tools-state.js";
|
||||
import { createWebFetchTool, createWebSearchTool } from "./web-tools.js";
|
||||
|
||||
const runWebSearchCalls = vi.hoisted(
|
||||
() => [] as Array<{ config?: unknown; runtimeWebSearch?: unknown }>,
|
||||
);
|
||||
|
||||
vi.mock("../../web-search/runtime.js", async () => {
|
||||
const { getActivePluginRegistry } = await import("../../plugins/runtime.js");
|
||||
const { getActiveRuntimeWebToolsMetadata } =
|
||||
await import("../../secrets/runtime-web-tools-state.js");
|
||||
const resolveRuntimeDefinition = (options?: {
|
||||
config?: unknown;
|
||||
runtimeWebSearch?: { selectedProvider?: string; providerConfigured?: string };
|
||||
}) => {
|
||||
const providerId =
|
||||
options?.runtimeWebSearch?.selectedProvider ?? options?.runtimeWebSearch?.providerConfigured;
|
||||
options?.runtimeWebSearch?.selectedProvider ??
|
||||
options?.runtimeWebSearch?.providerConfigured ??
|
||||
getActiveRuntimeWebToolsMetadata()?.search?.selectedProvider ??
|
||||
getActiveRuntimeWebToolsMetadata()?.search?.providerConfigured;
|
||||
const registration = getActivePluginRegistry()?.webSearchProviders.find(
|
||||
(entry) => entry.provider.id === providerId,
|
||||
);
|
||||
@@ -33,9 +45,14 @@ vi.mock("../../web-search/runtime.js", async () => {
|
||||
resolveWebSearchDefinition: resolveRuntimeDefinition,
|
||||
resolveWebSearchProviderId: () => "",
|
||||
runWebSearch: async (options: {
|
||||
config?: unknown;
|
||||
args: Record<string, unknown>;
|
||||
runtimeWebSearch?: unknown;
|
||||
}) => {
|
||||
runWebSearchCalls.push({
|
||||
config: options.config,
|
||||
runtimeWebSearch: options.runtimeWebSearch,
|
||||
});
|
||||
const resolved = resolveRuntimeDefinition(options as never);
|
||||
if (!resolved) {
|
||||
throw new Error("web_search is disabled or no provider is available.");
|
||||
@@ -51,6 +68,7 @@ vi.mock("../../web-search/runtime.js", async () => {
|
||||
beforeEach(() => {
|
||||
setActivePluginRegistry(createEmptyPluginRegistry());
|
||||
clearActiveRuntimeWebToolsMetadata();
|
||||
runWebSearchCalls.length = 0;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -114,4 +132,91 @@ describe("web tools defaults", () => {
|
||||
expect(tool?.description).toContain("Search the web");
|
||||
expect(result?.details).toMatchObject({ ok: true });
|
||||
});
|
||||
|
||||
it("late-binds managed web_search execution to the current runtime snapshot", async () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
registry.webSearchProviders.push(
|
||||
{
|
||||
pluginId: "stale-search",
|
||||
pluginName: "Stale Search",
|
||||
source: "test",
|
||||
provider: {
|
||||
id: "stale",
|
||||
label: "Stale Search",
|
||||
hint: "Stale runtime provider",
|
||||
envVars: [],
|
||||
placeholder: "stale-...",
|
||||
signupUrl: "https://example.com/stale",
|
||||
autoDetectOrder: 1,
|
||||
credentialPath: "tools.web.search.stale.apiKey",
|
||||
getCredentialValue: () => "configured",
|
||||
setCredentialValue: () => {},
|
||||
createTool: () => ({
|
||||
description: "stale runtime tool",
|
||||
parameters: {},
|
||||
execute: async () => ({ provider: "stale" }),
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
pluginId: "fresh-search",
|
||||
pluginName: "Fresh Search",
|
||||
source: "test",
|
||||
provider: {
|
||||
id: "fresh",
|
||||
label: "Fresh Search",
|
||||
hint: "Fresh runtime provider",
|
||||
envVars: [],
|
||||
placeholder: "fresh-...",
|
||||
signupUrl: "https://example.com/fresh",
|
||||
autoDetectOrder: 2,
|
||||
credentialPath: "tools.web.search.fresh.apiKey",
|
||||
getCredentialValue: () => "configured",
|
||||
setCredentialValue: () => {},
|
||||
createTool: () => ({
|
||||
description: "fresh runtime tool",
|
||||
parameters: {},
|
||||
execute: async () => ({ provider: "fresh" }),
|
||||
}),
|
||||
},
|
||||
},
|
||||
);
|
||||
setActivePluginRegistry(registry);
|
||||
setActiveRuntimeWebToolsMetadata({
|
||||
search: {
|
||||
providerConfigured: "fresh",
|
||||
providerSource: "configured",
|
||||
selectedProvider: "fresh",
|
||||
selectedProviderKeySource: "config",
|
||||
diagnostics: [],
|
||||
},
|
||||
fetch: {
|
||||
providerSource: "none",
|
||||
diagnostics: [],
|
||||
},
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
const tool = createWebSearchTool({
|
||||
config: { tools: { web: { search: { provider: "stale" } } } },
|
||||
sandboxed: true,
|
||||
runtimeWebSearch: {
|
||||
providerConfigured: "stale",
|
||||
providerSource: "configured",
|
||||
selectedProvider: "stale",
|
||||
selectedProviderKeySource: "config",
|
||||
diagnostics: [],
|
||||
},
|
||||
lateBindRuntimeConfig: true,
|
||||
});
|
||||
|
||||
const result = await tool?.execute?.("call-runtime-provider", {});
|
||||
|
||||
expect(result?.details).toMatchObject({ provider: "fresh" });
|
||||
expect(runWebSearchCalls).toHaveLength(1);
|
||||
expect(runWebSearchCalls[0]?.config).toBeUndefined();
|
||||
expect(runWebSearchCalls[0]?.runtimeWebSearch).toMatchObject({
|
||||
selectedProvider: "fresh",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user