Table of Contents
- Five routes, one responsibility each
- Place seven common tools in the right layer first
- Classify the task before classifying the failure
- A 200 response still needs content acceptance
- Retry only errors that can recover on their own
- Budget and depth define when to stop
- Cache, deduplication, and provenance belong in the router
- An executable router shape
- The default route
- Update record
- References
🌏 中文版
The previous article in this series covers how a research agent plans work and combines sources. This article narrows the scope to the retrieval layer: when an agent receives “find this information” or “read this URL,” should it call Search, direct Fetch, a Crawler, a Browser, or a controlled stealth browser?
The answer is not “use the strongest tool.” Build a route that escalates from cheap, predictable, and auditable operations to expensive operations with more side-effect risk. Success cannot mean HTTP 200; login forms, challenge pages, soft 404s, and empty JavaScript shells can all return 200.
Five routes, one responsibility each
| Route | Responsibility | Good input | It should not own |
|---|---|---|---|
| Search | Discover candidate URLs and alternate sources | A question, entity, or unknown location | Treating snippets as full-text evidence |
| Fetch | Read the raw response from a known URL | HTML, JSON, text, or directly downloadable documents | Executing JavaScript or operating a page |
| Crawler | Expand links from a seed and extract content | Documentation sites, site sections, multipage lists | Simulating authenticated interaction |
| Browser | Execute JavaScript, wait for state, and operate safe UI | Client-rendered pages, consent, pagination, infinite scroll | Acting as the default HTTP client |
| Controlled stealth | Adjust browser characteristics within an authorized scope | False-positive bot detection on a dedicated or permitted site | Bypassing authorization, CAPTCHA, paywalls, or site policy |
Search and Fetch are alternative entry points, not fixed consecutive steps. If the user supplies a URL, Fetch first. If the user supplies only a question, Search first. Crawlers and Browsers are escalations. Stealth is not a fifth level with a higher success rate; it is a separate branch that requires explicit source authorization and policy approval.
This is also why SearXNG and Crawl4AI do not replace each other. The former answers “where might the answer be?” while the latter answers “what readable material exists inside this known site?”
Place seven common tools in the right layer first
The table assigns each tool its primary identity in the router; it is not an inventory of every feature the product offers. Decide whether the missing capability is candidate discovery, single-page reading, site traversal, or managed website automation before choosing a product. Do not arrange all seven names into one fixed fallback chain.
| Tool | Primary layer | Input to give it | Handoff or exit condition |
|---|---|---|---|
| Exa | Search API | A question, topic, or need for candidate sources | Hand URLs to a Reader or Crawler once enough candidates exist; stop if search results already satisfy the source contract |
| Tavily | Search API | A research question and search query | End discovery when candidate coverage passes; the presence of separate Extract and Crawl APIs does not remove the route boundaries |
| Linkup | Search API | A query or discovery task that requests structured output | Move on when URLs and sources are sufficient; if credible sources are missing, revise the query or provider before starting a site-wide crawl |
| Jina Reader | URL Reader | One known, publicly readable URL | Stop when the Markdown passes content acceptance; move to a Crawler or Browser for traversal, authentication, or cross-page work |
| Firecrawl | Managed crawl and extraction API | A known URL, site seed, or multipage retrieval job | Stop when coverage and required fields pass; exit at an authorization boundary, challenge, or budget limit |
| Crawl4AI | Self-hosted Crawler | A known site, browser configuration, and extraction rules | Stop at maxPages, maxDepth, or the content threshold; evaluate a platform layer only when managed scheduling or site-specific workflows are required |
| Apify | Managed automation platform | A repeatable website job, Actor or Task, and run configuration | Stop when the Actor output satisfies the schema; if no suitable compliant workflow exists, do not rotate Actors indefinitely just to force a result |
Valid combinations usually cross layers. Exa/Tavily/Linkup → Jina Reader fits “discover sources, then read a small set of public pages.” A Search API followed by Firecrawl or Crawl4AI fits “the candidate sites are known, but the answer spans pages.” Apify belongs on the branch where the work has become site-specific, repeatable, and suited to managed execution.
Every layer inherits this article's exit conditions: stop when content passes, when authorization or policy denies access, or when the remaining budget cannot finish the next stage.
Classify the task before classifying the failure
The router's first decision is the task shape, not the tool:
discover: no trusted URL; find candidate sources.read: a URL exists; retrieve one page.traverse: read a section, documentation tree, or paginated set.interact: the answer appears only after client-side rendering or safe UI interaction.authenticated: use a user-authorized session after verifying scope and data isolation.
The second decision is why the previous stage failed. Do not compress every condition into fetch_failed; that leaves the agent blindly swapping tools.
| Signal | Classification | Next action | Do not |
|---|---|---|---|
DNS, connection reset, 502, 503, 504 | Transient transport failure | Retry within bounds, then try another source | Retry forever |
429 | Rate limiting | Honor Retry-After; reduce concurrency | Immediately rotate identities or many IPs |
301, 302, 307, 308 | Redirect | Follow a bounded chain and retain it | Forward sensitive headers across unsafe boundaries |
401 | Unauthenticated | Enter the auth lane only with an authorized session | Guess credentials or borrow cookies |
403, 451, robots denial | Policy or access restriction | Stop or find a permitted public alternative | Treat every denial as bot detection to bypass |
404, 410 | Missing source | Search for the canonical or replacement source | Retry the same URL |
200 with little body, navigation only, or login form | Content failure | Classify soft 404, auth wall, or extraction failure | Declare success |
200 with a JS shell and data after scripts run | Rendering failure | Use Browser, or an allowed known API | Guess readiness with a fixed sleep |
| Challenge or CAPTCHA | Challenge | Stop and report; handle only in an explicitly authorized environment | Automatically defeat or evade it |
RFC 9110 defines HTTP status semantics, but status is not a content-quality classifier. For example, 403 means that the server understood the request and refuses to fulfill it; it does not mean “try a browser that looks more human.” RFC 6585 states that a 429 response may include Retry-After; the router should use it instead of immediately resending.
A 200 response still needs content acceptance
Every retrieval should pass four layers:
- Transport: the response is readable, redirects do not loop, and the content type is supported.
- Page identity: canonical URL, title, primary language, and expected origin agree.
- Content quality: body length, text density, required fields, and query relevance meet task-specific thresholds.
- Policy: no auth boundary was crossed, no form was submitted, no prohibited path was accessed, and no challenge page was cited.
Thresholds belong to the task. A global minChars = 500 is wrong: an exchange-rate lookup may need one number, while a specification task needs sections and a version. Let the task declare required_fields, expected_content_types, and quality_checks; the router should execute that contract.
A Browser should not use “sleep three seconds” as its success condition. Playwright auto-waiting checks element state before actions. For retrieval, wait for an observable selector, response, or page state. The readiness condition belongs to the task contract, not a hard-coded delay.
Retry only errors that can recover on their own
Retryable conditions commonly include timeouts, connection resets, 408, 429, and selected 5xx responses. Non-retryable conditions commonly include unsupported formats, explicit 401/403/404/410/451 responses, policy denial, and confirmed login or challenge pages.
Even retryable conditions need all of the following:
- A per-stage
maxAttempts, such as two Fetch attempts and one Browser attempt. - Exponential backoff with jitter so workers do not collide again in lockstep.
- A task deadline; do not begin an attempt that cannot finish in the remaining time.
- An idempotency constraint; this route is read-only by default and must not replay state-changing actions.
Switching tools is not a retry. Escalating Fetch to Browser adds latency, CPU, memory, cookies, and interaction risk, so record it as an escalation.
Budget and depth define when to stop
A fallback tree without budgets becomes brute-force search. Every task needs at least:
type RetrievalBudget = {
deadlineMs: number
maxRequests: number
maxSearchQueries: number
maxPages: number
maxDepth: number
maxBrowserStarts: number
maxCostUsd?: number
}
maxDepth limits how far a crawler walks from its seed. maxPages limits total expansion. maxBrowserStarts prevents launching a browser for every candidate. On exhaustion, return budget_exhausted with partial evidence; do not disguise it as “not found.”
Define positive stopping conditions too. Stop when required facts, source count, and freshness all pass. Reading more pages does not automatically make an answer more reliable.
Cache, deduplication, and provenance belong in the router
A cache key needs at least canonical URL, representation variant, and auth scope. URL alone is unsafe for authenticated content because it can return user A's page to user B. RFC 9111 separates fresh, stale, and validated responses. Prefer conditional requests with ETag or Last-Modified instead of always refetching or always trusting old data.
Deduplicate at three levels:
- URL normalization removes tracking parameters and fragments while preserving query parameters that change content.
- Redirect and canonical identity merge aliases while retaining the original chain.
- Content fingerprints process the same body once while preserving provenance when multiple origins syndicate it.
Every artifact should record requested_url, final_url, retrieval time, route, status, content type, content hash, cache state, auth scope, parent page, and search query. This provenance lets a claim trace back to a specific representation. W3C PROV-O provides a general model for entities, activities, agents, and derivations.
An executable router shape
The code below omits provider SDKs but retains the contracts that control behavior: classification, budget, policy, and trace.
async function retrieve(task: Task, ctx: Context): Promise<Result> {
const trace = ctx.trace.start(task)
const candidates = task.url
? [{ url: normalize(task.url), discoveredBy: "user" }]
: await searchWithBudget(task.query, ctx.budget, trace)
for (const candidate of dedupe(candidates)) {
if (!ctx.policy.mayFetch(candidate.url)) continue
const cached = await ctx.cache.get(candidate.url, ctx.authScope)
const fetched = await boundedFetch(candidate, cached, ctx, trace)
const fetchVerdict = classify(fetched, task)
if (fetchVerdict.kind === "usable") {
const result = task.mode === "traverse"
? await crawlWithinBudget(candidate, task, ctx, trace)
: toArtifact(fetched, trace)
if (satisfies(result, task)) return trace.complete(result)
}
if (fetchVerdict.kind === "js-shell" && ctx.budget.browserStartsLeft > 0) {
const rendered = await browseReadOnly(candidate, task.waitFor, ctx, trace)
const browserVerdict = classify(rendered, task)
if (browserVerdict.kind === "usable" && satisfies(rendered, task)) {
return trace.complete(toArtifact(rendered, trace))
}
if (browserVerdict.kind === "challenge") {
return trace.stop("challenge", { retryable: false })
}
}
if (fetchVerdict.kind === "auth-required") {
if (!task.requiresAuth || !ctx.authScope) continue
const authorized = await browseWithAuthorizedSession(candidate, ctx, trace)
if (satisfies(authorized, task)) return trace.complete(authorized)
}
if (!fetchVerdict.retryable) trace.recordStop(candidate, fetchVerdict.kind)
if (ctx.budget.exhausted()) return trace.stop("budget_exhausted")
}
return trace.stop("no_acceptable_source")
}
In production, boundedFetch owns redirect caps, timeout, Retry-After, backoff, and cache validation. classify owns soft 404, login-page, challenge, JS-shell, and content-quality detection. Keeping them separate prevents transport retries and tool escalation from collapsing into one ambiguous loop.
The default route
A safe, practical default is:
Question only → Search → Fetch candidates
Known URL → Fetch
Site section → Fetch seed → Crawler
JS shell or safe interaction → Browser
Authenticated content → Authorized Browser session with auth-scoped cache
Challenge or explicit denial → Stop or switch to a permitted public source
Every stage → Stop when content passes; also stop when budget is exhausted
The goal is not to make an agent “get into every site.” The goal is to give every escalation an explainable failure signal, cost, and policy reason. The next article turns these contracts into a fixed corpus and regression gates: How to Evaluate Agent Search Quality: Building a Web Retrieval Benchmark.
Update record
- 2026-08-22: Added a cross-layer selection matrix, valid combinations, and exit conditions for Exa, Tavily, Linkup, Jina Reader, Firecrawl, Crawl4AI, and Apify.
References
- RFC 9110: HTTP Semantics
- RFC 9111: HTTP Caching
- RFC 6585: Additional HTTP Status Codes
- RFC 9309: Robots Exclusion Protocol
- Playwright: Auto-waiting
- W3C PROV-O: The PROV Ontology
- Exa Search API Guide
- Tavily Search API
- Linkup Search Overview
- Jina AI Reader API
- Firecrawl Advanced Scraping Guide
- Crawl4AI Quick Start
- Apify Actors
- On this site: Building a Web Retrieval Benchmark
Loading...