Skip to content

browser: allow for tool executions on a single instance #1398

Description

@threepointone

Parent roadmap: #1439

Summary

Browser tools currently create a fresh Browser Run session for every browser_execute call, then close/delete it when the call finishes. That is fine for isolated one-shot inspections, but it makes multi-step browsing workflows awkward and loses the state that makes browser automation useful: tabs, cookies, login state, local storage, navigation history, console/network state, and human-observable context.

This issue tracks adding an option for Think / agents/browser tools to execute multiple tool calls against a single reusable browser instance when the caller opts into that behavior. It should also incorporate Browser Run Live View so users can watch, debug, and manually intervene in active browser sessions.

Verified current behavior

Current public docs say this directly:

  • docs/browse-the-web.md: browser_execute opens a fresh browser session for the call and closes it.
  • packages/think/src/tools/browser.ts: comment says each browser_execute call opens a fresh session, exposes cdp, and closes the session on completion.

Current code also does this:

  • packages/agents/src/browser/shared.ts
    • execute() calls connectBrowser(...) or connectUrl(...).
    • In finally, it calls session?.close().
  • packages/agents/src/browser/cdp-session.ts
    • connectBrowser() creates a Browser Run CDP WebSocket through the browser binding.
    • The CdpSession dispose callback calls DELETE /v1/devtools/browser/{sessionId} when the session closes.

So for the Browser Run binding path, each browser_execute tool call currently creates and tears down a browser session.

Why this matters

Many agent browsing tasks are naturally multi-step:

  • navigate to a site, inspect the page, then click/fill based on what was found
  • log in once, then perform several authenticated actions
  • debug a frontend by preserving console/network state across steps
  • run a performance profile, then inspect traces/results in later steps
  • ask a human to complete MFA/CAPTCHA/sensitive input, then resume automation
  • keep a page open while the model iterates on CDP commands

For those, forcing everything into one browser_execute code block is brittle. It increases prompt/code complexity and prevents the model from using normal observe/act/observe loops across tool calls.

Browser Run itself supports session reuse, Live View, Human in the Loop, and session recording. The agents browser tools should expose enough of that model to be useful without becoming a full browser IDE.

Relevant Browser Run features

Useful docs / features for this design:

Important constraints from those docs:

  • Browser Sessions default to one-minute inactivity timeout; keep_alive can extend idle timeout up to 10 minutes.
  • Reusing sessions avoids cold starts and reduces churn, but callers must close sessions when done.
  • Durable Objects are the recommended pattern for stateful browser session management when a specific user/task needs a specific browser instance.
  • Live View URLs (devtoolsFrontendUrl) let users watch/interact with a running Browser Run session through live.browser.run or native Chrome DevTools.
  • Live View URLs are time-limited; if they expire before opening, list targets again to generate a fresh URL.
  • Human-in-the-loop flows use Live View for MFA/CAPTCHA/sensitive input and then automation resumes.
  • Session recording is opt-in and only available after the session ends; useful for debugging failed browser runs.
  • Browser sessions count against browser-hour and concurrent-browser limits, so lifecycle/cleanup needs to be explicit.

Proposed direction

Add a stateful browser-session mode alongside the existing one-shot mode.

Keep one-shot as default

The current behavior is safe and simple. Keep it as the default:

createBrowserTools({ browser, loader })

browser_execute opens and closes a fresh session per call.

Add reusable session mode

Add an opt-in mode that lets multiple calls share one Browser Run session:

createBrowserTools({
  browser: this.env.BROWSER,
  loader: this.env.LOADER,
  session: {
    id: this.name, // or chat/session id
    mode: "reuse",
    keepAliveMs: 600_000,
    liveView: true,
    recording: false
  }
})

Names are illustrative; shape should be decided in implementation.

The important semantic shift: browser_execute should attach to the same underlying browser session across calls until closed/expired/reset.

Possible tool/API surface

Do not overbuild v1. A small surface is enough:

Existing tools

  • browser_search remains unchanged.
  • browser_execute runs against either a fresh session or the configured reusable session.

New optional tools or helpers

  • browser_session_info

    • Returns session id, active target/page list, current URLs/titles, whether Live View is available, and maybe devtoolsFrontendUrl values.
  • browser_new_session / browser_reset_session

    • Explicitly create/reset the persistent browser for a chat/user/task.
  • browser_close_session

    • Close the persistent browser to stop billing and clear state.
  • browser_live_view

    • Return a fresh Live View URL for the current tab/session.
    • Might be a tool result, a server-side callable, or a client-visible event rather than something only the model sees.
  • browser_recording_info

    • Optional later: expose session recording ids/URLs after close for debugging.

Alternative: keep the AI tool surface to browser_execute only, and expose lifecycle/live-view methods as server-side helpers/callables for UI code. That may be cleaner.

Live View / human-in-the-loop behavior

We should design Live View as a user-facing capability, not only a model-facing string.

Possible flow:

  1. Agent starts or reuses a browser session.
  2. Browser tools publish a session status event or return metadata including sessionId, target id, URL/title, and devtoolsFrontendUrl.
  3. UI renders an "Open Live View" action for the user.
  4. If the model needs help, it can ask the user to open the Live View and complete a step.
  5. Agent polls or waits for page/navigation/selector state, then continues.

Open questions:

  • Should Think broadcast Browser Run session metadata over chat/UI events?
  • Should Live View URLs be included in tool output, hidden metadata, or a separate event to avoid leaking them into model context/history?
  • How do we refresh expired Live View URLs?
  • Should there be a needsApproval / human handoff pattern for browser actions?
  • How do we represent "human took control" vs "agent has control"?

Session ownership models

Potential modes:

Per tool call (current)

Fresh session per browser_execute, close on completion. Default.

Per Think agent / chat

One browser session per Think DO instance or child chat. Good for multi-step user tasks and preserving auth/cookies within a chat.

Per parent directory / user

One browser shared across many child chats. Probably risky by default because browser state is user-global and may leak between tasks; maybe useful in a deliberate assistant app.

Explicit session id

Caller passes sessionId / sessionKey; framework maps that to Browser Run session lifecycle. Useful for advanced apps.

Recommendation: start with per Think/chat session and explicit close/reset.

Implementation questions

  • Where should persistent session state live? For Think, likely in the DO SQLite config/state; for generic agents/browser, possibly caller-managed.
  • Should this be implemented in agents/browser generically first, with Think just passing options through?
  • How does the Browser binding expose creating sessions with keep_alive, targets=true, recording=true, listing targets, and deleting sessions? Verify exact local binding URLs vs public CDP HTTP APIs.
  • Can connectBrowser() attach to an existing session id via the binding, or do we need a new helper?
  • Should CdpSession.close() disconnect from the CDP socket without deleting the Browser Run session in reuse mode?
  • How do we prevent concurrent browser_execute calls from racing on the same session? Queue per session? Reject concurrent calls? Multiple tabs?
  • Should one reusable browser support multiple targets/tabs across calls, or should each call operate on the current/default tab unless code creates more?
  • How do we clean up sessions on DO alarm / idle timeout / clear / delete chat?
  • How does this interact with Browser Run rate limits, concurrent browser billing, and idle close reasons?

Acceptance criteria

  • Current one-shot browser_execute behavior remains the default and remains covered by tests.
  • A caller can opt into a reusable Browser Run session for browser tools.
  • Multiple browser_execute calls can observe and mutate the same browser state across calls.
  • Reusable sessions can be explicitly closed/reset to avoid idle billing leaks.
  • The implementation exposes enough session metadata for a UI to show session id, current targets, URLs/titles, and Live View links.
  • Live View URLs can be refreshed when expired.
  • Docs explain one-shot vs reusable mode and when to choose each.
  • Docs call out Browser Run limits, keep-alive, pricing, idle timeout, and cleanup responsibilities.
  • Tests or examples verify at least: state persists across calls, close/reset clears state, and one-shot mode still closes sessions.

Nice-to-have follow-ups

  • Session recording support for failed runs / debug replay.
  • Human-in-the-loop helper flow for login/MFA/CAPTCHA/manual verification.
  • Browser session events in Think UI / useAgentChat metadata.
  • Per-session concurrency queue and target/tab management helpers.
  • Integration with Browser Run WebMCP / Playwright MCP where structured website tools are available.
  • A playground or assistant demo where the user can watch the agent browse through Live View.

Useful files

  • packages/agents/src/browser/shared.ts
  • packages/agents/src/browser/cdp-session.ts
  • packages/agents/src/browser/ai.ts
  • packages/think/src/tools/browser.ts
  • docs/browse-the-web.md
  • examples/ai-chat/README.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions