diff options
| author | Igor Soarez <igor@soarez.org> | 2026-08-03 21:18:02 +0100 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-08-03 21:18:02 +0100 |
| commit | 495de0d5283dd3e4a6ef715b596c4a2892e95915 (patch) | |
| tree | aae2ec78d59cb96d8e90c56ab7626a6886d30be2 /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.ts | 56 |
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)); |
