castle-search/src/engine-target.ts (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
/**
* Which search engine to use, resolved the same way the CDP host is:
* environment variable, then a persisted choice, then a built-in default.
*
* Pure — no I/O — so the `/search-engine` command can report the configured
* intent without touching disk twice, and so it is testable.
*/
import { type EngineId, parseEngineId, engineAliases } from "./engines.ts";
/** Used when nothing else says otherwise. */
export const DEFAULT_ENGINE: EngineId = "google";
/** The environment variable that pins the engine for one process. */
export const ENGINE_ENV_VAR = "CCS_SEARCH_ENGINE";
export type EngineSource = "parameter" | "env" | "stored" | "default";
export type EngineResolution =
| { readonly kind: "ok"; readonly id: EngineId; readonly source: EngineSource }
| { readonly kind: "invalid"; readonly raw: string; readonly source: EngineSource };
/**
* Precedence: the per-call tool parameter, then `CCS_SEARCH_ENGINE`, then the
* `/search-engine` choice, then the built-in default.
*
* Note the tool parameter sits *above* the environment variable, which is the
* opposite of how the CDP target treats an explicit endpoint. That difference
* is deliberate. For the browser, an environment variable has to win, because
* quietly driving a different machine means automating the operator's own
* signed-in Chrome — a safety property. Choosing a different search engine has
* no equivalent hazard, and the ability to fall back to another engine when the
* first one is serving a captcha is the single most useful thing the agent can
* do with this tool. So the model is allowed to override the default; the
* operator sets what it starts from.
*/
export const resolveEngine = (
parameter: string | null | undefined,
env: NodeJS.ProcessEnv = process.env,
stored: string | null = null,
): EngineResolution => {
const candidates: Array<{ raw: string; source: EngineSource }> = [];
const fromParam = parameter?.trim();
if (fromParam) candidates.push({ raw: fromParam, source: "parameter" });
const fromEnv = env[ENGINE_ENV_VAR]?.trim();
if (fromEnv) candidates.push({ raw: fromEnv, source: "env" });
const fromStore = stored?.trim();
if (fromStore) candidates.push({ raw: fromStore, source: "stored" });
for (const candidate of candidates) {
const id = parseEngineId(candidate.raw);
// An unparseable value is reported rather than skipped over. Falling
// through to the next source would mean a typo in CCS_SEARCH_ENGINE
// silently searches Google instead of saying so.
if (!id) return { kind: "invalid", raw: candidate.raw, source: candidate.source };
return { kind: "ok", id, source: candidate.source };
}
return { kind: "ok", id: DEFAULT_ENGINE, source: "default" };
};
const sourceLabel = (source: EngineSource): string =>
source === "parameter"
? "engine parameter"
: source === "env"
? ENGINE_ENV_VAR
: source === "stored"
? "/search-engine"
: "built-in default";
export const describeEngineSource = (source: EngineSource): string => sourceLabel(source);
/** One-line description of a bad engine name, listing what would have worked. */
export const describeEngineFailure = (failure: Extract<EngineResolution, { kind: "invalid" }>): string =>
`unknown search engine "${failure.raw}" from ${sourceLabel(failure.source)} — ` +
`expected one of ${engineAliases().join(", ")}`;
|