Skip to content

Repository files navigation

Agent Interaction Layer

A reading and navigation layer for LLM agents. You give it a URL and a goal in plain language; it drives a real browser and gives you back an answer.

The agent never sees HTML, CSS selectors, or pagination. That is the point: a model asked to "find the cheapest book in the Mystery category" should not have to spend its context on markup.

browse(
    url="https://en.wikipedia.org/wiki/Tokyo",
    goal="find the current population",
)
# -> "Answer: 14,254,039  [0 steps · 1 pages · 7.8s · 1 model calls · $0.0045]"

It is exposed over MCP, so any MCP client (Claude Desktop, Claude Code, your own) can call it as a tool.


Design

Everything a page contains reaches the model through one narrow channel, and every browser mutation leaves through another. Those two boundaries are what make the layer site-agnostic and what make safety enforceable.

                                   ┌───────────────────┐
                                   │   MCP client      │
                                   │  browse(url,goal) │
                                   └─────────┬─────────┘
                                             │
                                   ┌─────────▼─────────┐
                                   │    controller     │  the loop
                                   └─────────┬─────────┘
              ┌───────────────┬──────────────┼──────────────┬───────────────┐
              │               │              │              │               │
       ┌──────▼─────┐  ┌──────▼─────┐  ┌─────▼──────┐ ┌─────▼─────┐  ┌──────▼─────┐
       │ perception │  │observation │  │   safety   │ │ executor  │  │   memory   │
       │  page ->   │  │ what we've │  │ may this   │ │  the only │  │  routes    │
       │  elements  │  │    seen    │  │  action    │ │  module   │  │  that      │
       │  + text    │  │            │  │    run?    │ │  touching │  │  worked    │
       └──────┬─────┘  └────────────┘  └────────────┘ │  the page │  └────────────┘
              │                                       └─────┬─────┘
              └───────────────► browser ◄───────────────────┘
                              (session)

One pass of the loop is five ordered stages: perceive → decide → validate → execute → assess. Every way a run can end is one Termination value, produced in one place.

Module Job
controller.py The loop and every termination condition
perception.py Page → elements + readable text. Three layers: accessibility tree (CDP) → DOM → vision
observation.py What has been seen, across pages. Numbers kept with the line that gives them meaning
executor.py The only module that mutates the browser
safety.py May this action run? ALLOW / REJECT / HANDOFF / HALT
session.py Browser lifetime, per-conversation isolation
llm.py Model routing, retries, JSON recovery, cost accounting
memory.py Routes that worked before, per domain
server.py The MCP surface

Why the model does the interpreting

An earlier version extracted answers itself — re.findall(r'£(\d+\.\d{2})'), a max_value_book_title, a hardcoded list of category names — and then overrode the model's answer with what those rules found.

That works on exactly the site it was written against. A page of Hacker News scores contains no £, so the tracker saw nothing at all.

Now the layer remembers what it saw and does not interpret it. Each page contributes its text and any numbers, each kept with the line it appeared on. The accumulated evidence goes into the prompt, and the model — which is good at "which of these is cheapest" and needs no regex to do it — decides. The answer is then checked for grounding in observed text rather than substituted.

Consequently observation.py has no notion of currency, of products, or of what a goal is asking for. Tests assert its executable code contains no price, book, or site name.


Token efficiency

Page text is the largest part of every prompt, so the layer sends each page's content once rather than on every step. The agent re-reads its current page each time it decides; resending it pays for the same tokens repeatedly.

Measured across four real sites, six steps each:

tokens / 15-step run
resending page text every step 39,130
sending each page once 21,745

~44% fewer input tokens. On dense pages (Wikipedia, Hacker News) a repeat step costs 60% less.

Dedup is per URL, never across pages. Suppressing a line because a different page carried it deletes content from a page the model is seeing for the first time and shifts its order — a listing whose top entry also appears elsewhere arrives headed by its second entry, and "the first X" is answered wrongly from a truthful reading of what was supplied. That regression was caught live, and the cross-page saving turned out to be worth almost nothing anyway.

Model routing: a cheap model handles early navigation steps, a stronger one takes over for comparison and synthesis. A typical run costs $0.005–$0.04.


Safety

safety.py sees every action before the executor does, and answers with one of four verdicts. REJECT is recoverable — pick a different element. HANDOFF and HALT end the run.

Some actions are never automated, whatever the model asks for:

  • Typing into a credential field — password, card number, CVV, one-time code — detected by label, placeholder, name, and input_type.
  • Clicking through a transaction — Place order, Pay now, Confirm booking, Delete account.

Both produce HANDOFF: the run stops and reports what it was about to do, for a human to decide.

Matching is on whole phrases. submit alone flags "Submit search"; pay alone flags "PayPal". Tests pin both directions — 12 labels that must hand off, 8 that must not.


Install

Requires Python 3.13+.

git clone https://github.com/samarthm04/agent-interface-layer.git
cd agent-interface-layer
python3.13 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/playwright install chromium

Set your API key (console.anthropic.com):

export ANTHROPIC_API_KEY=sk-ant-...

Run the demo

Six scenarios against four real sites, reporting steps, time, model calls and cost for each:

.venv/bin/python example_usage.py

A browser window opens so you can watch. Options:

AIL_HEADLESS=1 no visible windows (faster)
AIL_ONLY=e,f run only the named scenarios

Web UI

One text box. Describe a task in plain language — the site is picked out of the sentence, so there is no separate URL field.

.venv/bin/python web/app.py

Then open http://127.0.0.1:8420.

Tasks in a tab share one browser, so a follow-up that names no site at all ("on the same page, what year was it founded") continues where the last one left off. New browser session drops that state.

See the difference for yourself

Compare with / without layer runs the identical task twice, side by side: once through AgentController, once through naive_agent.py — a bare loop with the same browser, executor, safety guard, model and action schema, whose only differences are the ones this layer exists to make:

AgentController NaiveAgent
page text new lines only, deduped per page the full page, every step
cross-page memory accumulated evidence none — raw history only
DONE claims checked for grounding accepted on first ask

Both sides stream their steps live with per-step token counts, so you watch the gap open rather than take a number on faith. One measured run — "go to books.toscrape.com and find the cheapest book in the Mystery category":

With layer Without
Input tokens 5.6k 23.7k
Steps 2 10 (hit the limit)
Cost $0.0076 $0.1262
Answer ✅ Tastes Like Fear — £10.69 ❌ never finished

−77% input tokens, −94% cost. The traces show why: without accumulated evidence the naive loop cannot remember what it already saw, so it re-treads (next, previous, then five navigate calls) while its per-step cost climbs 1.4k → 3.2k tokens as raw page text piles up. The layered run stays flat at ~1.9k and finishes in two steps.

A comparison runs the task twice, for real — roughly 2× the time and cost of a single run. AIL_COMPARE_MAX_STEPS (default 10) caps both sides equally.


Use it as an MCP tool

.venv/bin/python server.py

For Claude Desktop, add to claude_desktop_config.json:

{
  "mcpServers": {
    "agent-interaction-layer": {
      "command": "/absolute/path/to/agent-interface-layer/.venv/bin/python",
      "args": ["/absolute/path/to/agent-interface-layer/server.py"],
      "env": {
        "ANTHROPIC_API_KEY": "sk-ant-...",
        "AIL_HEADLESS": "1"
      }
    }
  }
}

Two tools are exposed:

  • browse(url, goal, conversation_id="") — read and navigate a site to answer a goal. Repeat calls with the same conversation_id reuse one browser, keeping cookies, logins and page position.
  • close_browser(conversation_id="") — release a browser. Optional; they close on their own after 30 minutes idle.

browse returns text in every case, including failure. A run can also stop at its step or time budget, or halt for a human decision — read the reply rather than assuming it answered.

Server configuration: AIL_HEADLESS, AIL_MAX_STEPS (default 20), AIL_MAX_SECONDS (default 300).


Tests

.venv/bin/python -m pytest          # 625 tests, ~130s, no network, no API calls
.venv/bin/python -m pytest -m live  # 4 tests against the real API

The suite runs against a local fixture site and a scripted stand-in for the model, so it costs nothing and does not depend on live sites staying unchanged. The fixture site is deliberately adversarial: both price extremes sit on the last page, a decoy number precedes the real one in prose, ranked scores carry no currency symbol, and one page renders its content only after a timer.


Known limitations

Exhaustive search over large collections. Asked for the most expensive book across 50 pages, the agent reads a handful and reports the highest it found — honestly labelled "found", but not the true maximum. It is reliable on scoped goals and on collections it can cover; it does not yet reason well about when it has seen enough.

Sites that truncate in listings. Where a listing shows A Light in the ... and the full title only on the detail page, the answer carries the truncated form unless the agent clicks through.

No login flows. The agent stops at credential fields by design. For a site behind a login, sign in by hand once and every later run on that conversation starts from that state:

.venv/bin/python seed_profile.py https://example.com my-conversation

One page at a time. No parallel exploration.

About

A perception-driven, safety-constrained agent loop that enables LLMs to interact with arbitrary web interfaces using accessibility, DOM, and vision layers—fully abstracted behind MCP.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages