---
title: HARvestGym
emoji: πΈοΈ
colorFrom: blue
colorTo: purple
sdk: docker
pinned: false
tags:
- openenv
- reinforcement-learning
- api-agent
- web-tasks
base_path: /web
---
# HARvestGym
*Core idea: Trains LLMs to reverse-engineer and complete web tasks through raw HTTP APIs. No browser. No docs. Just a URL and a task.*
### Can a small model learn to explore the API surface of any web application β and complete real tasks through those APIs, without ever opening a browser?
Web applications are full of APIs. Every click in a browser triggers an HTTP call with a precise schema, a specific authentication header, an exact sequence of prerequisites. **HARvestGym trains a small model to do all of that directly** β given a task and a URL, it discovers the relevant endpoints, understands what each one needs, chains the calls in the right order, and completes the task without any browser.
The model starts with nothing: no schema, no documentation, no endpoint list. It uses tools to explore β issuing requests, inspecting responses, building up its own understanding of how the application works. This is what a developer does when they reverse-engineer an API. The model learns to do the same.
Given a URL and a task string, the agent must discover which endpoints exist, figure out schemas and parameter dependencies, and execute the right sequence. Zero prior knowledge.
## What the Model (Policy) Is Learning
Given: a natural language task + a live web application URL. No prior knowledge of the application.
The model calls `browser_agent` first β this returns the list of API endpoints the browser used to complete the task. The model now has a map: it knows what endpoints exist. What it does not know:
- which of those endpoints are actually needed for this specific task
- in what order they must be called (you cannot add to a cart before the cart exists)
- where each required parameter value comes from
- how to re-authenticate if a session expires mid-episode
The model must learn to:
1. **Discover endpoints** β by using a browser agent tool that completes the same task in a real browser while recording all network traffic, then filtering that traffic to extract only the meaningful application API calls (stripping out CDN requests, analytics, static assets). The browser agent runs once and generates the raw discovery data; the model uses this as its starting context.
2. **Select the right endpoints** β from the browser agent's list, identify the subset relevant to the current task (not every observed endpoint is needed)
3. **Sequence calls correctly** β determine the prerequisite order (create cart β find product β add item), including calls that must happen before others even though the task description doesn't say so
4. **Thread parameters** β this is the hardest part. APIs form a dependency graph:
- Some values come from a previous response (`cart_id` from step 1 β path param in step 3)
- Some values come from the authentication flow (`form_key`, `Bearer token` β header in every subsequent call)
- Some values come from the task description (`product name` β search query β `sku` β body of add-item call)
- The ground truth catalog defines these relationships precisely; the model learns to navigate them
5. **Handle auth and errors** β detect 401 / session-expired responses, re-authenticate, and continue; interpret 4xx errors and adjust the next call accordingly
---
## Architecture
```
TRAINING LOOP
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
β Task + App URL β
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Policy Model (RL Agent) β β
β β small model β no prior knowledge of the app β β
β β β β
β β Observation: task + history + session_state + last_result β β
β β β β
β β Step 1 βββΊ browser_agent(task, url) β β
β β Step 2+ βββΊ search_endpoints(query) β β
β β βββΊ curl_exec(command) β β
β β βββΊ search_episode_data(query) β β
β β βββΊ done(result) β β
β ββββββββββ¬ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββββββ΄βββββββββββββββββββββββββββββββ β
β β β β
β βΌ βΌ β
β βββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββ β
β β Browser Agent β β Environment β β
β β (step 1 only) β β β β
β β β β β’ Executes curl_exec via subprocessβ β
β β Training: β β β’ Auto-injects session cookies β β
β β Load pre-recorded β β β’ Smart-truncates response bodies β β
β β cached HAR from β β β’ Indexes full responses into β β
β β disk or launch β β per-episode BM25 + GEMMA store β β
β β on real browser β β β’ Manages session_state: cookies, β β
β β β β CSRF tokens, auth headers β β
β β Inference: β ββββββββββββββββ¬βββββββββββββββββββββββ β
β β Launch real browserβ β β
β β via Playwright + β β HTTP calls (always live) β
β β bu-30b-a3b-preview β βΌ β
β β β βββββββββββββββββββββββββββββββββββββββ β
β β Both paths produce: β β WebArena EC2 (live apps) β β
β β β’ Filtered HAR β β β β
β β β’ OpenAPI-like specβ β :7770 Shopping (Magento 2) β β
β β β’ GEMMA embeddings β β :7780 Shopping Admin β β
β β for search_ β β :9999 Forum (Postmill) β β
β β endpoints() β β :8888 Wikipedia (Kiwix) β β
β βββββββββββββββββββββββ β :3000 Map (OpenStreetMap) β β
β ββββββββββββββββ¬βββββββββββββββββββββββ β
β β β
β β episode trajectory β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββ β
β β Deterministic Judge β β
β β β β
β β Per-template programmatic grader: β β
β β β’ Inspects episode trajectory β β
β β β’ Optionally probes live app state β β
β β β’ Verifies parameter sourcing β β
β β (TASK_SPEC / PREV_CALL / β β
β β AUTH_FLOW / STATIC / DERIVED) β β
β β β’ Scores [0.0 β 1.0] β β
β ββββββββββββββββ¬βββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββ β
β β Reward Signal β β
β β β β
β β Per-step: β β
β β +0.2 valid API call (2xx) β β
β β +0.1 new path explored β β
β β +0.25 correct param sourcing β β
β β β0.15 repeated identical call β β
β β β0.3 browser_agent called again β β
β β β β
β β Episode end: β β
β β +2.0β+5.0 task complete (easyβhardβ β
β β β1.5 task failed β β
β ββββββββββββββββ¬βββββββββββββββββββββββ β
β β β
β βΌ β
β βββββββββββββββββββββββββββββββββββββββ β
β β GRPO (via HF TRL) β β
β β β β
β β 8 parallel rollouts per prompt β β
β β Computes advantages without β β
β β a value function β β
β β Updates policy weights β β
β βββββββββββββββββββββββββββββββββββββββ β
β β β
β ββββΊ updated Policy Model β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
```
### Data Flow: Browser Agent β Search Index β Execution
```
HAR File (cached using Browser Agent) βββΊ filter_har_entries()
β
βΌ
drop: CDN, analytics, static assets
keep: {method, path, request_body,
response_body, status_code}
β
βΌ
extract_openapi_spec()
β structured endpoint catalog
{path, method, params, auth, response_fields}
β
ββββββββ΄βββββββ
β β
βΌ βΌ
build_GEMMA_embeddings return summary list
(search_endpoints to RL agent:
index β full schemas) [GET /products,
POST /guest-carts, ...]
β
βΌ
search_endpoints("create guest cart")
β top-3 endpoint schemas with:
β’ path params + sources
β’ body params + sources
β’ auth requirements
β’ response field names
```
### Episode Response Indexing
```
curl_exec(command)
β
ββββΊ subprocess: execute against live EC2
β
ββββΊ index_full_response()
β BM25 index ββ keyword match (IDs, SKUs, tokens)
β GEMMA embed ββ semantic match (paraphrases)
β (indexes BEFORE truncation β all items stored)
β
ββββΊ smart_truncate()
non-JSON HTML β 3,000 chars
JSON primitive β never truncated
error (4xx/5xx) β never truncated
small JSON β returned as-is
large array β first 2 items shown
+ _list_truncated annotation
+ hint to call search_episode_data()
```
### Parameter Dependency Graph (what the judge tracks)
```
Task: "Add 'Radiant Tee' to a guest cart"
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β TASK_SPEC βββββββββββββββββββββββββββββββββββββββββββ β
β "Radiant Tee" (product name) β β
β β β β
β βΌ β β
β GET /rest/V1/products?name=Radiant+Tee β β
β β items[0].sku = "MH01" (PREV_CALL) βββ β β
β β β β
β POST /rest/V1/guest-carts β β β
β β body = "cart-abc123" (PREV_CALL) βββΌβββΌββΊβ
β β β β
β POST /rest/V1/guest-carts/{cartId}/items β β β
β path: cartId βββββββ "cart-abc123" ββββββββ β β
β body: sku βββββββ "MH01" ββββββββββ β
β body: qty βββββββ TASK_SPEC (quantity) β
β body: quote_id βββββββ DERIVED (= cartId) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Source types tracked by the judge:
TASK_SPEC β value stated in the task string
PREV_CALL β value from a prior curl response in this episode
AUTH_FLOW β value from a session/token auth step
STATIC β fixed application constant (e.g. store_id = 1)
DERIVED β computed from another param (e.g. quote_id = cart_id)
```
### Curriculum: Complexity Tiers
```
Easy ββββββββββββββββββββββββ graduate when P(success) > 0.7
β Single call, no auth β
β Templates 1, 2 β
β 1 API call required β
β βΌ
Medium ββββββββββββββββββββββββ graduate when P(success) > 0.7
β Auth + 1β2 dependent calls β
β Templates 3, 4 β
β 2β3 API calls required β
β βΌ
Hard ββββββββββββββββββββββββββ final tier
Multi-step chain, full auth, ID threading
Templates 5, 6, 7
4β8+ API calls required
Reward scaling: Γ2.5 vs Easy
```
### The RL Agent's Tool: Browser Agent
The RL agent has access to a **browser agent tool** powered by `[browser-use/bu-30b-a3b-preview](https://huggingface.co/browser-use/bu-30b-a3b-preview)` β a 30B MoE vision-language model (3B active parameters) purpose-built for web task completion, served via the [browser-use](https://github.com/browser-use/browser-use) library on Playwright. When the RL agent calls this tool with a natural language task, the browser agent:
1. Opens the target application in a real browser
2. Completes the task by clicking, typing, and navigating β exactly as a human would
3. All HTTP traffic is intercepted via Playwright network events
4. Returns the intercepted traffic, filtered down to only the application's own API calls
The filtering step strips analytics pings, CDN requests, font loads, JS/CSS bundles and returns only `{method, path, request_body, response_body, status_code}` tuples for the app's actual API endpoints.
**Training vs. inference β what gets cached:**
- The browser agent output (filtered endpoint list) is pre-computed once per task and cached. During training, the RL model receives this cached result instantly β no live browser session runs.
- The RL agent's own `curl_exec` calls **always hit the real live WebArena server** β during both training and inference. No API response is mocked or cached.
- At inference, the browser agent runs live to handle novel tasks or changed application state.
Full architecture and code: `[BROWSER_AGENT.md](BROWSER_AGENT.md)`
### Ground Truth: From the Codebase, Not the Browser
The browser agent shows *what* API calls happen. It does not explain *why* β specifically, it does not document where each parameter comes from or what field constraints exist. That comes from the application codebase.
For each WebArena application, we perform a one-time static analysis (using a large model against the Docker image source) to produce a **ground truth API catalog** β a precise, hard-coded document specifying:
```
endpoint: POST /rest/V1/guest-carts/{cartId}/items
method: POST
auth: None (guest cart)
path_params:
cartId: [string] obtained from: POST /rest/V1/guest-carts β response body
body:
cartItem.sku: [string] the product's SKU, from: GET /rest/V1/products β items[].sku
cartItem.qty: [number] quantity, from: task specification
cartItem.quote_id: [string] same as cartId
```
This is what the judge compares against. The ground truth defines the complete parameter relationship graph for each application.
Full extraction process: `[GROUND_TRUTH_EXTRACTION.md](GROUND_TRUTH_EXTRACTION.md)`
### The Training Loop
```
Task (natural language) + App URL
β
βΌ
Policy Model (sees: task + history of all prior actions/results + session_state + findings)
β calls tools to explore and execute
βββΊ browser_agent(task, url) β filtered API call list (cached during training)
βββΊ search_endpoints(query) β full schema for a specific endpoint
βββΊ curl_exec(command) β execute HTTP call, get {status, headers, body}
βββΊ search_episode_data(q) β search prior response bodies in this episode
βββΊ done(result) β declare task complete
β
βΌ
Live WebArena App (EC2) ββββ real HTTP responses (always live, never mocked)
β
βΌ
Judge (compares against ground truth API catalog)
β
βΌ
Reward Signal βββΊ GRPO βββΊ updated policy
```
---
## Target Applications
All running on a single AWS EC2 instance. Real production software, no simulation.
| App | Port | URL | Software |
| -------------- | ---- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| Shopping | 7770 | [http://ec2-16-59-2-56.us-east-2.compute.amazonaws.com:7770/](http://ec2-16-59-2-56.us-east-2.compute.amazonaws.com:7770/) | Magento 2 β open-source e-commerce platform |
| Shopping Admin | 7780 | [http://ec2-16-59-2-56.us-east-2.compute.amazonaws.com:7780/](http://ec2-16-59-2-56.us-east-2.compute.amazonaws.com:7780/) | Magento 2 Admin β backend panel for the same store |
| Forum | 9999 | [http://ec2-16-59-2-56.us-east-2.compute.amazonaws.com:9999/](http://ec2-16-59-2-56.us-east-2.compute.amazonaws.com:9999/) | Postmill β open-source Reddit-like link aggregation forum |
| Wikipedia | 8888 | [http://ec2-16-59-2-56.us-east-2.compute.amazonaws.com:8888/](http://ec2-16-59-2-56.us-east-2.compute.amazonaws.com:8888/) | Kiwix β read-only offline mirror of English Wikipedia |
| Map | 3000 | [http://ec2-16-59-2-56.us-east-2.compute.amazonaws.com:3000/](http://ec2-16-59-2-56.us-east-2.compute.amazonaws.com:3000/) | OpenStreetMap β open-source collaborative mapping platform |
Source: [WebArena environment_docker](https://github.com/web-arena-x/webarena/tree/main/environment_docker)
---
## Spaces
### Observation Space
What the model sees at each step:
```python
class Observation(BaseModel):
task: str # Natural language task
app_base_url: str # Root URL of the target application
last_tool_result: Any # Result of last tool call:
# search_endpoints β list of endpoint schema strings
# curl_exec β {status_code, headers, body (smart-truncated)}
# search_episode_data β list of matching JSON object strings
history: list[dict] # Full episode trajectory: list of {action, tool_result} pairs
# from all prior steps. The model sees what it already tried,
# enabling value threading (read a cart_id from step 2's response
# and use it in step 5's curl call) and loop avoidance.
session_state: dict # Auto-managed by environment: cookies, tokens, CSRF values
# extracted from all prior HTTP Set-Cookie and response bodies
# e.g. {"PHPSESSID": "abc", "form_key": "xyz", "cart_id": "123"}
step_count: int
max_steps: int # 20
```
`session_state` is maintained by the environment. The model never parses `Set-Cookie` headers β the environment extracts tokens automatically and makes them available. The model decides *when* to authenticate and *which* session values to use; the environment handles *extraction*.
**curl execution:** The agent outputs a curl command string. The environment parses it and executes it via subprocess against the live EC2 server β the agent machine never has a direct network connection to WebArena. The environment also injects cookies from `session_state` automatically before each call.
**Response truncation β smart array truncation, not byte cutoff:** HTTP response bodies are processed by a pure Python function before being returned to the model. Rules applied in order:
1. **Non-JSON body** (HTML, CSS, JS, plain text): truncate to 3,000 characters. HTML from form-serving pages (login, post creation) is kept longer than pure prose because CSRF tokens and `` fields are embedded inside the markup and the model needs to locate them. See the [HTML / Form-Submission Handling](#html--form-submission-handling) section below for how the model is expected to work with HTML responses.
2. **JSON primitive** (string, number, boolean): never truncated β these are tokens, IDs, confirmations.
3. **Error response (4xx / 5xx)**: never truncated β the model needs every word to self-correct.
4. **JSON object or array with no large arrays** (< 3 dict items per array): returned as-is.
5. **JSON with a large array field** (β₯ 3 dict items): keep first 2 items, drop the rest, and add a `_list_truncated` annotation:
```json
{
"items": [
{"sku": "MH01", "name": "Radiant Tee", "price": 22.0},
{"sku": "MH02", "name": "Breathe-Easy Tank", "price": 34.0}
],
"_list_truncated": {
"field": "items",
"shown": 2,
"total": 50,
"note": "Showing 2 of 50 items. Use search_episode_data() to find a specific item from this response."
}
}
```
**Episode response indexing:** Every `curl_exec` call indexes the full request and response bodies into a per-episode hybrid index (BM25 for keyword matching + GEMMA semantic embeddings for paraphrase handling). When a list is truncated, all items (not just the 2 shown) are indexed. The model can retrieve any specific object using `search_episode_data("keyword or natural language query")` without needing a filtered API endpoint to exist. See `TOOLS.md` for the full indexing algorithm.
### Action Space
The model outputs a single tool call per step. Full technical specifications for all tools (document construction, truncation implementation, index architecture, caveats) are in `[TOOLS.md](./TOOLS.md)`.
| Tool | Input | What It Does | Output |
| ---------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `browser_agent(task, url)` | Task string + app base URL | Checks for pre-recorded HAR; if found, processes it β otherwise launches live browser to perform task and record traffic. Extracts OpenAPI-like spec, builds GEMMA embeddings for search. | Summary list of API endpoint names + methods (e.g. `GET /products`). No schemas/headers. Use `search_endpoints()` for details. |
| `search_endpoints(query)` | Natural language query | Semantic search over GEMMA-embedded endpoint spec built by `browser_agent`. Returns full parameter details for matching endpoints. | Top-3 endpoint schemas (method, path, auth, params with sources, response fields) |
| `curl_exec(command)` | Full curl command string | Executes HTTP call against live EC2 server, indexes full response into episode BM25 store, returns truncated observation. | `{status_code, headers, body}` β body smart-truncated; full body indexed to episode store |
| `search_episode_data(query)` | Keyword or natural language query | Hybrid BM25 + GEMMA semantic search over all request/response bodies from prior `curl_exec` calls in this episode. | Top-5 JSON objects from this episode's request/response history |
| `done(result?)` | Optional result string | Signals task complete, triggers judge evaluation. | Ends episode |
`browser_agent` is called **exactly once per episode at step 1**. During training, it loads a cached pre-recorded HAR file(if available); at inference, it will launch a live browser session. It returns the deduplicated list of API endpoint patterns observed in the network traffic. **If called again after step 1, the call executes normally but a β0.3 penalty is applied to the reward.** `search_endpoints` then provides the full schema for any specific endpoint the model wants to call β searching the GEMMA embeddings built by `browser_agent` from the HAR data.
`curl_exec` is the primary HTTP action β one string that encodes method, URL, headers, and body together, exactly as API documentation is written. This lets the model leverage its pretrained knowledge of `curl` syntax while producing calls that are self-documenting.
```bash
# Step 1 β Discover which endpoint creates a guest cart
# (model calls search_endpoints first, sees: POST /rest/V1/guest-carts)
# Step 2 β Create guest cart
curl -X POST 'http://ec2-.../rest/V1/guest-carts' -H 'Content-Type: application/json'
# β body: "cart-abc123" (plain string β never truncated)
# Step 3 β Find the product SKU (list response, truncated to 2 items + note)
curl 'http://ec2-.../rest/V1/products?searchCriteria[filter_groups][0][filters][0][field]=name&searchCriteria[filter_groups][0][filters][0][value]=Radiant+Tee'
# β body: {"items":[{"sku":"MH01","name":"Radiant Tee","price":22.0}],"total_count":1}
# (1 item β not truncated; if 200 items, all 200 indexed, 2 shown in context)
# Step 4 β Add item (model reads cart-abc123 from step 2, MH01 from step 3 β all in history)
curl -X POST 'http://ec2-.../rest/V1/guest-carts/cart-abc123/items' \
-H 'Content-Type: application/json' \
-d '{"cartItem":{"sku":"MH01","qty":1,"quote_id":"cart-abc123"}}'
```
Values from prior responses (cart IDs, SKUs, tokens) are threaded directly from the growing episode history. `session_state` tokens (cookies, CSRF values) are auto-injected by the environment. If a list response was truncated and the model needs a specific item not shown in the 2-item sample, it calls `search_episode_data("Radiant Tee sku")` β all 200 items are indexed, even though only 2 were shown in context.
### Prompt Structure:
```
SYSTEM: You are an API agent. Complete the task using only the tools available:
browser_agent, search_endpoints, curl_exec, search_episode_data, done.
When a response is HTML, look for JSON data embedded in