FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

A4: browser_execute tool + workspace wiring by Alezander9 · Pull Request #6 · browser-use/browsercode · GitHub

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension .json  (2) .lock  (1) .ts  (4) .txt  (1) All 4 file types selected
Only manifest files
Viewed files
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Unified
Split
Hide whitespace
Diff view
Unified
Split
Hide whitespace
5 changes: 5 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions packages/bcode-browser/package.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"exports": {
"./*": "./src/*.ts"
},
"dependencies": {
"effect": "catalog:",
"zod": "catalog:"
},
"devDependencies": {
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:"
Expand Down
98 changes: 98 additions & 0 deletions packages/bcode-browser/src/browser-execute.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// browser_execute — single-tool browser interface (decisions.md §3.2).
//
// Spawns the vendored harness with a Python snippet:
//
// uv run --project <HARNESS_DIR> python run.py -c "<code>"
//
// The harness manages the daemon itself via admin.ensure_daemon(). We just
// pipe stdout+stderr back. BU_NAME is namespaced by sessionID so parallel
// sub-agents (each with their own session) get isolated daemons + browsers.
//
// Level 1 per decisions.md §1c — substantial implementation lives here. The
// Level-2 hook in packages/opencode is a one-line wrapper.

import { Effect, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import z from "zod"
import { HARNESS_DIR } from "./harness"

const DEFAULT_TIMEOUT_MS = 60 * 1000
const MAX_TIMEOUT_MS = 10 * 60 * 1000

export const parameters = z.object({
python: z.string().describe("Python source to execute against the browser harness."),
timeout: z
.number()
.describe(`Timeout in milliseconds. Default ${DEFAULT_TIMEOUT_MS}, max ${MAX_TIMEOUT_MS}.`)
.optional(),
})

export type Parameters = z.infer<typeof parameters>

export interface ExecuteContext {
readonly sessionID: string
}

export interface ExecuteResult {
readonly output: string
readonly exitCode: number
}

const UV_MISSING_HINT =
"uv is not installed or not on PATH. Install it once: curl -fsSL https://astral.sh/uv/install.sh | sh"

// Spawn errors flow through effect's PlatformError; ENOENT lives on the wrapped
// cause's `.code`. Walk the cause chain so we detect it regardless of nesting.
const isUvMissing = (err: unknown): boolean => {
let cur: unknown = err
for (let i = 0; i < 5 && cur; i++) {
if (typeof cur === "object" && cur !== null && (cur as { code?: string }).code === "ENOENT") return true
cur = (cur as { cause?: unknown }).cause
}
return false
}

export const make = Effect.fn("BrowserExecute.make")(function* () {
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner

const execute = (args: Parameters, ctx: ExecuteContext) =>
Effect.gen(function* () {
const proc = ChildProcess.make(
"uv",
["run", "--project", HARNESS_DIR, "python", "run.py", "-c", args.python],
{
cwd: HARNESS_DIR,
extendEnv: true,
env: { BU_NAME: ctx.sessionID },
stdin: "ignore",
},
)

// uv not on PATH (ENOENT) — surface as exit 127 with the install hint
// so both the agent (via output) and the user (via TUI) can act on it.
// 127 mirrors POSIX "command not found". Other spawn failures rethrow.
const spawned = yield* spawner.spawn(proc).pipe(
Effect.catch((err) =>
isUvMissing(err) ? Effect.succeed("uv-missing" as const) : Effect.fail(new Error(`failed to spawn uv: ${err}`)),
),
)
if (spawned === "uv-missing") return { output: UV_MISSING_HINT, exitCode: 127 } satisfies ExecuteResult

const [output, exitCode] = yield* Effect.all(
[Stream.mkString(Stream.decodeText(spawned.all)), spawned.exitCode],
{ concurrency: 2 },
)

return { output, exitCode } satisfies ExecuteResult
}).pipe(
Effect.scoped,
Effect.timeoutOrElse({
duration: Math.min(args.timeout ?? DEFAULT_TIMEOUT_MS, MAX_TIMEOUT_MS),
orElse: () => Effect.fail(new Error("browser_execute timed out")),
}),
)

return { parameters, execute }
})

export * as BrowserExecute from "./browser-execute"
19 changes: 19 additions & 0 deletions packages/bcode-browser/src/harness.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Harness directory resolver.
//
// The vendored browser-harness lives at `packages/bcode-browser/harness/`,
// which is a sibling of this `src/` directory. We resolve it from
// `import.meta.url` so callers don't have to know the workspace layout.
//
// In dev mode (running from source) this points at the in-tree harness.
// For built binaries (Phase C distribution) the build script will embed or
// copy the harness next to the binary; the resolution there will need to
// switch on packaging mode. Not built yet — explicit TODO when Phase C lands.

import path from "path"
import { fileURLToPath } from "url"

const __dirname = path.dirname(fileURLToPath(import.meta.url))

export const HARNESS_DIR = path.resolve(__dirname, "..", "harness")

export * as Harness from "./harness"
1 change: 1 addition & 0 deletions packages/opencode/package.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"@ai-sdk/vercel": "2.0.39",
"@ai-sdk/xai": "3.0.82",
"@aws-sdk/credential-providers": "3.993.0",
"@browser-use/bcode-browser": "workspace:*",
"@clack/prompts": "1.0.0-alpha.1",
"@effect/opentelemetry": "catalog:",
"@effect/platform-node": "catalog:",
Expand Down
29 changes: 29 additions & 0 deletions packages/opencode/src/tool/browser-execute.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// browser_execute — Level-2 hook (decisions.md §1c).
//
// Adapter only. All logic lives in @browser-use/bcode-browser/browser-execute.

import { Effect } from "effect"
import type z from "zod"
import { BrowserExecute } from "@browser-use/bcode-browser/browser-execute"
import * as Tool from "./tool"
import DESCRIPTION from "./browser-execute.txt"

export const BrowserExecuteTool = Tool.define(
"browser_execute",
Effect.gen(function* () {
const impl = yield* BrowserExecute.make()
return {
description: DESCRIPTION,
parameters: impl.parameters,
execute: (args: z.infer<typeof impl.parameters>, ctx: Tool.Context) =>
Effect.gen(function* () {
const result = yield* impl.execute(args, { sessionID: ctx.sessionID })
return {
title: "browser_execute",
output: result.output,
metadata: { exitCode: result.exitCode },
}
}).pipe(Effect.orDie),
}
}),
)
26 changes: 26 additions & 0 deletions packages/opencode/src/tool/browser-execute.txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Execute Python code against a connected web browser via the BrowserCode harness.

This is the single tool for all browser interaction. The agent writes Python that
imperatively drives the browser using helpers preloaded into the script's namespace
(`goto`, `click`, `type_text`, `screenshot`, `js`, `cdp`, `new_tab`, `switch_tab`,
`wait_for_load`, `page_info`, `http_get`, etc.).

Read `packages/bcode-browser/harness/SKILL.md` for the full helper surface and
recommended workflow. Read `packages/bcode-browser/harness/helpers.py` for exact
signatures.

State (CDP session, tab attachments, event buffer) is held by a long-lived daemon
keyed to your session id, so consecutive `browser_execute` calls share the same
browser. Editing `helpers.py` between calls takes effect on the very next call.

Coordinate-based interaction is the default — `click(x, y)` rather than selector
indices. `Input.dispatchMouseEvent` passes through iframes, shadow DOM, and
cross-origin at the compositor level.

Output is whatever the script writes to stdout/stderr. Wrap multi-step flows in
one call when possible — that's the design.

Example:
goto("https://example.com")
wait_for_load()
print(page_info())
4 changes: 4 additions & 0 deletions packages/opencode/src/tool/registry.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TaskTool } from "./task"
import { TodoWriteTool } from "./todo"
import { WebFetchTool } from "./webfetch"
import { WriteTool } from "./write"
import { BrowserExecuteTool } from "./browser-execute"
import { InvalidTool } from "./invalid"
import { SkillTool } from "./skill"
import * as Tool from "./tool"
Expand Down Expand Up @@ -113,6 +114,7 @@ export const layer: Layer.Layer<
const greptool = yield* GrepTool
const patchtool = yield* ApplyPatchTool
const skilltool = yield* SkillTool
const browserExecute = yield* BrowserExecuteTool
const agent = yield* Agent.Service

const state = yield* InstanceState.make<State>(
Expand Down Expand Up @@ -190,6 +192,7 @@ export const layer: Layer.Layer<
search: Tool.init(websearch),
code: Tool.init(codesearch),
skill: Tool.init(skilltool),
browserExecute: Tool.init(browserExecute),
patch: Tool.init(patchtool),
question: Tool.init(question),
lsp: Tool.init(lsptool),
Expand All @@ -213,6 +216,7 @@ export const layer: Layer.Layer<
tool.search,
tool.code,
tool.skill,
tool.browserExecute,
tool.patch,
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [tool.lsp] : []),
...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [tool.plan] : []),
Expand Down

Back | FazBrowse Home | New Git URL