A2A Protocol

browsy implements Google's Agent-to-Agent (A2A) protocol, enabling agent discovery and task delegation over HTTP. Any A2A-compatible agent can discover browsy's capabilities and delegate web browsing tasks to it.

Overview

A2A is a standard for agents to find and communicate with each other. browsy's A2A support consists of two parts:

  1. Agent card -- a JSON manifest at a well-known URL describing browsy's capabilities.
  2. Task execution -- an endpoint that accepts goals, executes them as browsing tasks, and streams status events back via SSE.

Both are served automatically by browsy serve.

browsy serve --port 3847

Agent card

The agent card is served at GET /.well-known/agent.json and describes browsy's identity and capabilities.

curl http://localhost:3847/.well-known/agent.json

Response:

{
  "name": "browsy",
  "description": "Zero-render browser engine for AI agents. Navigates, extracts, and interacts with web pages without rendering pixels.",
  "url": "http://localhost:3847",
  "version": "1.0",
  "capabilities": {
    "streaming": true,
    "pushNotifications": false
  },
  "skills": [
    {
      "id": "web-browse",
      "name": "Web Browsing",
      "description": "Navigate to URLs, interact with pages, extract content, fill forms, and search the web.",
      "tags": ["browse", "scrape", "extract", "search", "login", "forms"]
    }
  ]
}

Agents discover browsy by fetching this card and inspecting the skills array. The streaming: true capability indicates that task responses are delivered as Server-Sent Events (SSE).

Task execution

POST /a2a/tasks

Submit a task for browsy to execute. The response is an SSE event stream with status updates.

Request body:

FieldTypeRequiredDescription
goalstringyesNatural language description of the task
paramsobjectnoStructured parameters (see below)

Params fields:

FieldTypeDescription
urlstringTarget URL to browse
credentialsobject{ "username": "...", "password": "..." } for login tasks
search_querystringQuery string for search tasks
extractstringWhat to extract from the page (e.g., "tables", "links", "text")

browsy infers the task intent from the goal text and params fields. Explicit params take priority over goal parsing.

Intent detection

browsy maps each task to one of these intents:

IntentTriggerBehavior
Searchsearch_query param, or goal contains "search"Performs a web search, returns results
Logincredentials param, or goal contains "login"/"sign in"Navigates to URL, fills login form, submits
Extractextract param (not "tables"), or goal contains "extract"/"scrape"Navigates to URL, returns page content
ExtractTablesextract: "tables", or goal contains "table"Navigates to URL, extracts structured table data
FillFormGoal contains "fill"/"form"/"submit"Navigates to URL, interacts with form elements
BrowseDefault fallbackNavigates to URL, returns the Spatial DOM

SSE event stream

The response uses Content-Type: text/event-stream. Each event is a JSON object with the following structure:

data: {"id":"task_abc123","status":"working","steps":[{"description":"Navigating to https://example.com"}]}

data: {"id":"task_abc123","status":"completed","steps":[{"description":"Navigating to https://example.com"},{"description":"Page loaded: Example Domain (3 elements)"}],"result":{"page_type":"Other","title":"Example Domain","elements":3}}

Event fields:

FieldTypeDescription
idstringUnique task identifier
statusstring"working", "completed", or "failed"
stepsarrayList of { "description": "..." } objects showing progress
resultobjectPresent when status is "completed". Contains extracted data
errorstringPresent when status is "failed". Describes what went wrong

The stream always ends with a terminal event ("completed" or "failed").

Examples

Browse a page

curl -N http://localhost:3847/a2a/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "goal": "Browse the Hacker News front page",
    "params": { "url": "https://news.ycombinator.com" }
  }'

Event stream:

data: {"id":"task_1","status":"working","steps":[{"description":"Navigating to https://news.ycombinator.com"}]}

data: {"id":"task_1","status":"completed","steps":[{"description":"Navigating to https://news.ycombinator.com"},{"description":"Page loaded: Hacker News (120 elements)"}],"result":{"page_type":"List","title":"Hacker News","elements":120}}

Search the web

curl -N http://localhost:3847/a2a/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "goal": "Search for Rust web frameworks",
    "params": { "search_query": "rust web framework 2026" }
  }'

Login to a site

curl -N http://localhost:3847/a2a/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "goal": "Login to the application",
    "params": {
      "url": "https://app.example.com/login",
      "credentials": { "username": "user@example.com", "password": "secret" }
    }
  }'

Event stream:

data: {"id":"task_3","status":"working","steps":[{"description":"Navigating to https://app.example.com/login"}]}

data: {"id":"task_3","status":"working","steps":[{"description":"Navigating to https://app.example.com/login"},{"description":"Login page detected, submitting credentials"}]}

data: {"id":"task_3","status":"completed","steps":[{"description":"Navigating to https://app.example.com/login"},{"description":"Login page detected, submitting credentials"},{"description":"Login successful, redirected to Dashboard"}],"result":{"page_type":"Dashboard","title":"Dashboard - App"}}

Extract table data

curl -N http://localhost:3847/a2a/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "goal": "Extract the pricing table",
    "params": {
      "url": "https://example.com/pricing",
      "extract": "tables"
    }
  }'

Extract page content

curl -N http://localhost:3847/a2a/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "goal": "Extract the main article text",
    "params": {
      "url": "https://example.com/blog/post",
      "extract": "text"
    }
  }'

Fill a form

curl -N http://localhost:3847/a2a/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "goal": "Fill out the contact form with name John and email john@example.com",
    "params": { "url": "https://example.com/contact" }
  }'

Task status polling

A stub endpoint exists for polling task status by ID:

GET /a2a/tasks/{task_id}
curl http://localhost:3847/a2a/tasks/task_abc123

This returns the last known state of the task. Since tasks execute synchronously over SSE, polling is primarily useful for checking whether a task completed after a disconnection.

Error handling

When a task fails, the final SSE event includes an error field:

data: {"id":"task_5","status":"failed","steps":[{"description":"Navigating to https://invalid.example"}],"error":"Network error: DNS resolution failed"}

Common failure causes:

ErrorCause
Network errorDNS failure, connection refused, timeout
CAPTCHA detectedTarget page requires human verification
No login form foundLogin intent but page has no detected login action
Element not foundForm interaction referenced a nonexistent element