summaryrefslogtreecommitdiff
path: root/src/errors.ts
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-08-03 21:18:02 +0100
committerIgor Soarez <igor@soarez.org>2026-08-03 21:18:02 +0100
commit495de0d5283dd3e4a6ef715b596c4a2892e95915 (patch)
treeaae2ec78d59cb96d8e90c56ab7626a6886d30be2 /src/errors.ts
Web search for pi through an existing Chrome over CDP
Attaches to a browser that is already running — never launches one — using the same endpoint configuration as pi-browser-harness, so a single /browser-target choice governs both packages. One tool, castle_cdp_search, deliberately not named web_search so it coexists with pi-web-access rather than shadowing it. Notes from validating against castle's Chrome: - tbs=qdr:*, the parameter Google's own Tools menu writes, renders an empty page on this profile; the older as_qdr=* works. Any date filter combined with udm=14 is also empty, so recency drops udm. - Target.createTarget must not be raced against the abort signal: raceAbort abandons the promise but cannot cancel the command, and the command's side effect is a tab nothing is left holding. - A search cancelled while queued has to be removed from the semaphore queue, or the slot handed to it later is never counted back. - A decaying rate-limit block stops serving /sorry/ and returns an empty results page instead, indistinguishable from a genuine zero-hit search.
Diffstat (limited to 'src/errors.ts')
-rw-r--r--src/errors.ts56
1 files changed, 56 insertions, 0 deletions
diff --git a/src/errors.ts b/src/errors.ts
new file mode 100644
index 0000000..d003e4f
--- /dev/null
+++ b/src/errors.ts
@@ -0,0 +1,56 @@
+/**
+ * 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;
+
+ constructor(options: { challenge: string; pageUrl: string; endpoint: string; detail?: string | undefined }) {
+ super(
+ [
+ `Search is blocked by a human verification challenge (${options.challenge}).`,
+ options.detail ? `Page said: ${options.detail}` : null,
+ `This cannot be solved by the agent — a person has to clear it in the browser at ${options.endpoint}.`,
+ `Ask the user to open ${options.pageUrl} in that Chrome, complete the challenge or accept the consent dialog,`,
+ `then retry the search. 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;
+ }
+}
+
+/** 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));