aboutsummaryrefslogtreecommitdiff
path: root/castle-search/src/errors.ts
diff options
context:
space:
mode:
Diffstat (limited to 'castle-search/src/errors.ts')
-rw-r--r--castle-search/src/errors.ts83
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));