ifURI examples
← all examples

14 — LLM-over-URI agent (drive Chrome + tools by URI)

An LLM-style decision loop over a URI registry: compile connectors into one registry (the agent's *action space*), let a planner pick {uri, payload} steps, run them under policy (query freely, command only when permitted), and feed each result into the next decision.

This is the pattern from ../AUTOMATION-INTEGRATIONS.md: registry → LLM picks a URI → policy gate → run → result → repeat.

Run

python3 agent.py "check and read https://example.com"
# queries (time/http/dom) run; the log COMMAND is skipped (gated)

python3 agent.py "check and read https://example.com" --allow-commands
# now the log:// command runs too
python3 agent.py "..." --json     # machine-readable trace

Ready-to-run YAML flow

The plan this agent produces is also committed as a runnable flow of real URIs — run it directly, no planner/LLM:

python3 agent.py --flow flows/web-recon.yaml --allow-commands
# time:// -> httpcheck:// -> browser:// -> log://   (see flows/web-recon.yaml)

Example output:

  ✓ time://host/clock/query/now           stamp the run
      -> {"ok": true, "utc": "2026-…Z"}
  ✓ httpcheck://host/url/query/status      is the site up?
      -> {"ok": true, "status": 200, ...}
  ✓ browser://chrome/page/query/dom        read the page
      -> {"ok": true, "bytes": 561, "sample": "<!DOCTYPE html> ...Example Domain..."}
  · log://host/run/command/write           record the run  (gated)

Pieces

time, httpcheck, browser://chrome and log routes. The browser route reads a page via headless Chrome (chrome --headless --dump-dom), with a graceful dry-run when no Chrome is installed. In production these are real connectors (time-tools, http-check, a Chrome CDP browser-control, sqlite-context).

plan() is deterministic so the demo runs in CI.

Reusing the real browser-control connector

The browser route doesn't have to be the tools.py stub. When the sibling package urirun-connector-browser-control is checked out next to this repo, agent.py reuses it for the whole browser:// surface — the same registry, but backed by the packaged connector instead of the demo stub:

without connector:  browser://chrome/page/query/dom            (tools.py stub)
with connector:     browser://chrome/page/query/dom            ┐
                    browser://chrome/page/query/text           │ urirun-connector-
                    browser://chrome/page/command/screenshot   │ browser-control
                    browser://desktop/page/command/open        │
                    browser://desktop/page/command/screenshot  ┘

The connector's routes run out-of-process (it ships local-function-subprocess handlers via the shared python -m urirun.exec runner), so they're registry-portable and the agent drives them as external tools — no in-process import of the connector at call time, and the routes survive being compiled into a registry. agent.browser_control_bindings() loads the connector straight from the checkout (adds the package dir to sys.path/PYTHONPATH, no install needed) only to read those bindings. load_registry() then drops the inline browser:// stub and merges in the connector's routes; the planner prefers the connector-only page/query/text route, and run_step unwraps the handler result from the run envelope (result.value). The run banner reports which backend is active:

python3 agent.py "check and read https://example.com"
# action space: 8 routes  (browser: urirun-connector-browser-control)
#   ✓ browser://chrome/page/query/text   read the page
#       -> {"connector": "browser-control", "ok": true, ...}

If the connector isn't present the agent silently falls back to the tools.py stub (browser: tools.py (inline stub)), so the example still runs standalone. This is the point of the URI + registry substrate: a connector is *plugged into* the agent's action space without changing the loop — see ../18-connector-transport.

Built into urirun

This loop now ships as a runtime command — point it at any compiled registry:

tools.py bindings > b.json && urirun compile b.json --out reg.json
urirun agent space reg.json                                   # the action space
urirun agent run reg.json --goal "check https://example.com" \
  --planner agent:plan --allow-commands                       # run a pluggable planner

--planner module:function takes (goal, action_space) and returns the [{uri, payload}] steps — wire it to an LLM. agent.py here is the long-form equivalent.

Swap in a real LLM

Replace plan(goal, routes) with a model call: send the goal plus the action space (action_space(registry) — the same data as urirun's MCP tools) and have the model return the [{uri, payload}] steps. Anything MCP/A2A-speaking can consume the registry directly; or use the llm connector (llm://host/chat/command/complete) inside the loop.

Why URI + registry is a good agent substrate

Real Chrome control (CDP)

chrome --headless --dump-dom is the one-shot shown here. For full control use the Chrome DevTools Protocol (chrome --remote-debugging-port=9222) and map: Page.navigatebrowser://chrome/page/command/navigate, Runtime.evaluate…/page/query/eval, Page.captureScreenshot…/page/command/screenshot, Input.dispatchMouseEvent…/page/command/click. query = read DOM/eval, command = navigate/click (gated). See ../AUTOMATION-INTEGRATIONS.md §4.

Test

pytest test_agent.py -q

Files

MakefileREADME.mdagent-run.logagent.pytest_agent.pytools.py.benchmarks/flows/

View on GitHub →