/** * Failure vocabulary. * * Every one of these is *thrown*, never returned. pi only sets `isError: true` * on a tool result when `execute()` throws — a returned object with an `error` * field looks to the model exactly like a successful search that found nothing. * * `SearchChallengeError` is the one the agent is expected to act on rather than * just report: it means a human has to touch the browser before search can work * again, so its message says so in words the agent can pass to the operator. */ /** The search engine served a captcha, consent wall, or other human check. */ export class SearchChallengeError extends Error { override readonly name = "SearchChallengeError"; /** Machine-readable flavour of the challenge, for callers that branch on it. */ readonly challenge: string; /** The page the human needs to visit to clear it. */ readonly pageUrl: string; /** Which browser is blocked, e.g. "10.88.0.25:9223 (built-in default)". */ readonly endpoint: string; /** Which engine is blocked. */ readonly engine: string; /** Engines that are not blocked and could be tried instead. */ readonly alternatives: readonly string[]; constructor(options: { challenge: string; pageUrl: string; endpoint: string; engine: string; alternatives: readonly string[]; detail?: string | undefined; }) { super( [ `${options.engine} is blocked by a human verification challenge (${options.challenge}).`, options.detail ? `Page said: ${options.detail}` : null, // Retrying elsewhere comes first because it is the action the agent can // take on its own; a challenge on one engine says nothing about the // others, and asking the operator for help should be the fallback, not // the first move. options.alternatives.length > 0 ? `A challenge on one engine does not affect the others: retry the same query with engine set to ` + `${options.alternatives.join(", ")} before asking anyone for help.` : null, `If every engine is blocked, this needs a person — the challenge cannot be solved by the agent.`, `Ask the user to open ${options.pageUrl} in the Chrome at ${options.endpoint}, complete the challenge or`, `accept the consent dialog, then retry. The cookie it sets persists in that browser profile, so one pass`, `unblocks later searches.`, ] .filter((line) => line !== null) .join(" "), ); this.challenge = options.challenge; this.pageUrl = options.pageUrl; this.endpoint = options.endpoint; this.engine = options.engine; this.alternatives = options.alternatives; } } /** The chosen engine cannot express the requested recency window. */ export class RecencyUnsupportedError extends Error { override readonly name = "RecencyUnsupportedError"; } /** The browser could not be reached or spoke CDP badly. */ export class BrowserUnavailableError extends Error { override readonly name = "BrowserUnavailableError"; } /** The search itself ran but produced nothing usable. */ export class NoResultsError extends Error { override readonly name = "NoResultsError"; } /** The per-search deadline expired, or the user pressed Esc. */ export class SearchAbortedError extends Error { override readonly name = "SearchAbortedError"; } export const messageOf = (e: unknown): string => (e instanceof Error ? e.message : String(e));