Files
openclaw/extensions/codex/src/web-search-provider.runtime.ts
Peter Steinberger b080dd1e76 refactor: consolidate coercion contracts (#122458)
* refactor: consolidate coercion contracts

Centralize exact string, record, numeric, date, Boolean, argument, and structured-error coercions while preserving call-site semantics.

Migrate canonical-name collisions and deprecated internal SDK bypasses, deleting 55 net production/tooling lines. Expand declaration ownership enforcement to 101 allowed helpers and add a narrow export-completeness audit.

* fix: preserve standalone script coercions

Keep copied Control UI tooling self-contained and retain the trusted release harness module-relative source seam when the harness runs against an old target cwd.
2026-08-11 23:26:37 -07:00

98 lines
3.7 KiB
TypeScript

import {
readStringParam,
resolveSearchTimeoutSeconds,
type SearchConfigRecord,
type WebSearchProviderToolExecutionContext,
wrapWebContent,
} from "openclaw/plugin-sdk/provider-web-search";
import type { WebSearchProviderPlugin } from "openclaw/plugin-sdk/provider-web-search-contract";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
import {
runBoundedCodexAppServerTurn,
type CodexBoundedTurnOptions,
} from "./app-server/bounded-turn.js";
import { isJsonObject, type CodexThreadItem, type JsonObject } from "./app-server/protocol.js";
import { buildCodexNativeWebSearchThreadConfig } from "./app-server/web-search.js";
type WebSearchProviderContext = Parameters<WebSearchProviderPlugin["createTool"]>[0];
export async function executeCodexWebSearchProviderTool(
ctx: WebSearchProviderContext,
args: Record<string, unknown>,
executionContext: WebSearchProviderToolExecutionContext | undefined,
options: CodexBoundedTurnOptions,
): Promise<Record<string, unknown>> {
const query = readStringParam(args, "query", { required: true });
const start = Date.now();
const result = await runBoundedCodexAppServerTurn({
config: ctx.config,
model: { mode: "live-default" },
modelProvider: "openai",
timeoutMs: resolveSearchTimeoutSeconds(ctx.searchConfig as SearchConfigRecord) * 1_000,
signal: executionContext?.signal,
agentDir: ctx.agentDir,
options,
taskLabel: "hosted search",
developerInstructions:
"You are OpenClaw's bounded web-search worker. You must use Codex hosted web_search to answer the user's search query. Return a concise grounded answer with source URLs. Do not call other tools, edit files, or ask follow-up questions.",
input: [{ type: "text", text: query, text_elements: [] }],
requiredModalities: ["text"],
isolation: "private-stdio",
threadConfig: buildCodexNativeWebSearchThreadConfig(ctx.config),
});
const searches = result.items
.filter((item) => item.type === "webSearch")
.map(summarizeCodexWebSearchItem);
if (searches.length === 0) {
throw new Error("Codex hosted search completed without invoking web search.");
}
return {
query,
provider: "codex",
model: result.model,
tookMs: Date.now() - start,
externalContent: {
untrusted: true,
source: "web_search",
provider: "codex",
wrapped: true,
},
content: wrapWebContent(result.text, "web_search"),
searches,
};
}
function summarizeCodexWebSearchItem(item: CodexThreadItem): Record<string, unknown> {
const action = isJsonObject(item.action) ? item.action : undefined;
const actionType = readNonEmptyString(action, "type");
const queries = actionType === "search" ? readNonEmptyStringArray(action, "queries") : [];
const query =
normalizeOptionalString(item.query) ??
(actionType === "search" ? readNonEmptyString(action, "query") : undefined) ??
queries[0];
const url = readNonEmptyString(action, "url");
const pattern = readNonEmptyString(action, "pattern");
return {
...(query ? { query } : {}),
...(queries.length > 0 ? { queries } : {}),
...(actionType && actionType !== "search" ? { action: actionType } : {}),
...(url ? { url } : {}),
...(pattern ? { pattern } : {}),
};
}
function readNonEmptyString(record: JsonObject | undefined, key: string): string | undefined {
return record ? normalizeOptionalString(record[key]) : undefined;
}
function readNonEmptyStringArray(record: JsonObject | undefined, key: string): string[] {
const value = record?.[key];
if (!Array.isArray(value)) {
return [];
}
return value.flatMap((entry) => {
const normalized = normalizeOptionalString(entry);
return normalized ? [normalized] : [];
});
}