diff options
Diffstat (limited to 'castle-search/src/errors.ts')
| -rw-r--r-- | castle-search/src/errors.ts | 83 |
1 files changed, 83 insertions, 0 deletions
diff --git a/castle-search/src/errors.ts b/castle-search/src/errors.ts new file mode 100644 index 0000000..2318b27 --- /dev/null +++ b/castle-search/src/errors.ts @@ -0,0 +1,83 @@ +/** + * 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)); |
