If your agent already runs on LangChain or AutoGen you do not need to rebuild it to give it a browser. There is no SDK to install: the API is plain HTTPS with a Bearer header, so the adapter is a function. This guide shows the minimal wiring for each, and why it should point at dispatch rather than run.
How does a browser fit into a tool-using agent?
Both frameworks drive a model that decides which tools to call. You register one tool that takes a goal, a URL and a success condition; when the agent decides it needs to act on the web it calls that tool, and the whole observe-plan-act loop happens server-side. The agent's own model chooses the goal; it does not drive the browser step by step.
Why point the adapter at dispatch?
Because it makes every call cache-first without changing anything in your framework. The first time the agent asks for something new, dispatch compiles and caches a skill; every similar call after that replays it for 2 credits with no planner call. Pointing at /run instead re-reasons every time, which is correct only when you specifically want an uncached run.
Wiring up LangChain
One decorated function. Note the body shape: `url`, `prompt` and `success` — the third is required, and it is what stops the agent from reporting a green run that did nothing.
import os, requests
from langchain_core.tools import tool
@tool
def twin_browser(url: str, prompt: str, success_text: str) -> dict:
"""Act on a web page. Give the target URL, a natural-language goal, and
the text that will be visible on the page when the goal has succeeded."""
r = requests.post(
"https://twin-browser.com/api/v1/dispatch",
headers={"Authorization": f"Bearer {os.environ['TWIN_API_KEY']}"},
json={
"url": url,
"prompt": prompt,
"success": {"kind": "textVisible", "value": success_text},
},
timeout=180,
)
r.raise_for_status()
return r.json() # → { "mode": "cache-hit" | …, "success": …, "steps": … }
# agent = create_react_agent(llm, tools=[twin_browser])Wiring up AutoGen
The same function, registered as a callable tool on the assistant. AutoGen invokes it when the conversation calls for a browser action.
import os, requests
from autogen import AssistantAgent, register_function
def twin_browser(url: str, prompt: str, success_text: str) -> dict:
"""Run a browser goal and return the structured result."""
r = requests.post(
"https://twin-browser.com/api/v1/dispatch",
headers={"Authorization": f"Bearer {os.environ['TWIN_API_KEY']}"},
json={
"url": url,
"prompt": prompt,
"success": {"kind": "textVisible", "value": success_text},
},
timeout=180,
)
return r.json()
assistant = AssistantAgent("assistant", llm_config=llm_config)
register_function(
twin_browser, caller=assistant, executor=user_proxy,
description="Act on a web page from a URL, a goal and a success condition.",
)Handling the paused case
A sign-in goal can come back with status "paused" and a sessionId rather than a result. Your tool should return that to the agent as-is rather than raising: the agent can then decide to ask the human for a code (POST /api/v1/runs/{id}/resume) or to hand over a connect link. Swallowing the pause is how a 2FA wall turns into a silent failure.
Timeouts
A synchronous browser run can take a minute or more. Set a generous client timeout, and for work that may exceed it, POST to /api/v1/jobs instead and give the tool a job id to poll — that is the asynchronous shape, and it is a different guide.