Most agent frameworks treat output as a best-effort string. PydanticAI does not. Built by the Pydantic team, it enforces typed tool parameters and structured output schemas at runtime—so your agent either returns a valid ResearchResult or retries, never a half-formed blob of text.
Pair that with Anchor's managed cloud browser infrastructure—isolated sessions, built-in anti-detect, residential IPs, native CAPTCHA solving—and you get a stack where the browser layer is handled for you and your agent logic stays clean and testable.
What We're Building
A research agent that accepts a plain-English query, opens an Anchor browser session, navigates relevant pages, and returns a typed ResearchResult with a summary, source URLs, and a confidence score. The LLM can't hallucinate a field that doesn't exist in your schema—PydanticAI enforces it.
Setup
pip install pydantic-ai anchor-browser playwright
playwright install chromium
export ANCHOR_API_KEY=your_anchor_key
export ANTHROPIC_API_KEY=your_anthropic_key
Imports and Client Initialization
import os
import asyncio
from dataclasses import dataclass
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
from anchor_browser import AnchorClient
from playwright.async_api import async_playwright
Define Dependencies and Output Schema
PydanticAI's dependency injection threads context through every tool call without globals. The output schema is a plain Pydantic model—nothing framework-specific.
@dataclass
class BrowserDeps:
anchor: AnchorClient
cdp_url: str
class ResearchResult(BaseModel):
summary: str = Field(description="Concise answer to the research question")
sources: list[str] = Field(description="URLs consulted during research")
confidence: float = Field(ge=0.0, le=1.0, description="Confidence score 0–1")
Define the Browser Tool
The @agent.tool decorator registers the function as an LLM-callable tool. The docstring becomes the description the model sees—no separate schema definition needed.
agent = Agent(
"anthropic:claude-sonnet-4-6",
deps_type=BrowserDeps,
output_type=ResearchResult,
instructions=(
"You are a research assistant with access to a live web browser. "
"Navigate relevant pages, extract facts, and return a structured summary."
),
)
@agent.tool
async def browse(ctx: RunContext[BrowserDeps], url: str) -> str:
# Navigate to a URL; returns title + body text (first 4000 chars).
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp(ctx.deps.cdp_url)
context = browser.contexts[0]
page = context.pages[0] if context.pages else await context.new_page()
await page.goto(url, wait_until="domcontentloaded")
title = await page.title()
text = await page.inner_text("body")
await browser.close()
return f"Title: {title}\n\n{text[:4000]}"
Run It
async def main(query: str) -> ResearchResult:
anchor = AnchorClient(api_key=os.environ["ANCHOR_API_KEY"])
session = anchor.sessions.create(
proxy_country="us",
options={"adblock": True},
)
deps = BrowserDeps(anchor=anchor, cdp_url=session.data.cdp_url)
result = await agent.run(query, deps=deps)
anchor.sessions.delete(session.data.id)
return result.output
if __name__ == "__main__":
result = asyncio.run(
main("What are the key features of Anchor Browser's VPN for AI agents?")
)
print(result.summary)
print("Sources:", result.sources)
print(f"Confidence: {result.confidence:.0%}")
PydanticAI automatically retries the final structured-output step if the model returns something that doesn't match ResearchResult. You never need to write a JSON parser or a try/except around the response.
Why PydanticAI + Anchor Works Well Together
Structured output, always. PydanticAI validates LLM output against your schema and retries on failures. You get a ResearchResult object with guaranteed types—not a string to parse and sanitize.
Dependency injection done right. The BrowserDeps dataclass holds your Anchor session and gets threaded through every tool call via RunContext. No module-level state, no threading issues.
Model-agnostic by design. Swap anthropic:claude-sonnet-4-6 for openai:gpt-4o or google:gemini-2.5-flash without touching your tools or output schema. Test against a cheap model, deploy on a capable one.
Session isolation. Each agent.run() gets its own Anchor session—no cookie bleed, no shared fingerprint state between concurrent runs. Anchor's per-session residential IPs mean your agent looks like a real human on every request.
Production Tips
Session pooling. For high-throughput pipelines, pre-warm a pool of Anchor sessions at startup and check them out per run. Cold-starting a browser per query adds latency you don't need.
Retry budgets. PydanticAI retries tool calls on validation failures. Cap them with max_retries=2 on your Agent to prevent runaway costs when a target page returns garbage.
Authentication flows. Pass credentials via BrowserDeps and use Anchor's OmniConnect for sites that require OAuth or 2FA. The authenticated session state persists across tool calls.
Geo-routing. Anchor exposes proxy_country at session creation time. Spin up region-specific sessions to scrape country-gated pricing pages or localized content without managing proxy infrastructure yourself.
What's Next
- Add a
searchtool alongsidebrowseso the agent discovers URLs before it visits them - Use PydanticAI's streaming API (
agent.run_stream()) to surface partial results as the agent browses - Combine with Anchor Replicate to replay human-recorded workflows as structured agent subtasks
Get Started
PydanticAI docs · Anchor documentation · Start your free Anchor trial →



