summaryrefslogtreecommitdiff
path: root/src/engine-target.ts
blob: db4d81f3bb3f28e1bb625cca042b424ecfdabbae (plain)
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(", ")}`;