diff --git a/packages/docs/docs.json b/packages/docs/docs.json
index a62b1e8fe..d2f2072ed 100644
--- a/packages/docs/docs.json
+++ b/packages/docs/docs.json
@@ -52,6 +52,7 @@
"v4/integrations/claude-code",
"v4/integrations/codex",
"v4/integrations/eve",
+ "v4/integrations/flue",
"v4/integrations/deep-agents",
"v4/integrations/crewai",
"v4/integrations/mastra",
diff --git a/packages/docs/images/integrations/flue.svg b/packages/docs/images/integrations/flue.svg
new file mode 100644
index 000000000..d528e8eb9
--- /dev/null
+++ b/packages/docs/images/integrations/flue.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/docs/v4/best-practices/mcp-integrations.mdx b/packages/docs/v4/best-practices/mcp-integrations.mdx
index 138402ab8..7a7f1f557 100644
--- a/packages/docs/v4/best-practices/mcp-integrations.mdx
+++ b/packages/docs/v4/best-practices/mcp-integrations.mdx
@@ -7,7 +7,7 @@ description: "Call Model Context Protocol (MCP) servers alongside Stagehand's pr
Stagehand v4 does not include an autonomous agent or a general-purpose MCP client. To call third-party MCP tools alongside Stagehand, orchestrate both from your own code as shown below.
-Stagehand also provides experimental [integrations](/v4/integrations/overview) that expose a persistent Stagehand browser to CrewAI, Deep Agents, Mastra, and the Vercel AI SDK over MCP. The Eve integration exposes the same tools natively.
+Stagehand also provides experimental [integrations](/v4/integrations/overview) that expose a persistent Stagehand browser to CrewAI, Deep Agents, Mastra, and the Vercel AI SDK over MCP. The Eve, Flue, and Pi integrations expose the same tools natively.
Give an agent the `run`, `snapshot`, and `screenshot` browser tools.
diff --git a/packages/docs/v4/integrations/flue.mdx b/packages/docs/v4/integrations/flue.mdx
new file mode 100644
index 000000000..b4399c0cc
--- /dev/null
+++ b/packages/docs/v4/integrations/flue.mdx
@@ -0,0 +1,84 @@
+---
+title: "Flue"
+description: "Give a Flue agent native Stagehand browser tools backed by one persistent browser session."
+---
+
+The Stagehand integration binds `run`, `snapshot`, and `screenshot` as native in-process [Flue](https://flueframework.com/) tools backed by one browser session, without starting an MCP server or bridge process.
+
+
+Stagehand ships this experimental integration from the repository rather than publishing it as a standalone adapter.
+
+
+## Prerequisites
+
+- Node.js 24 or newer
+- pnpm 11.10.0
+- A model-provider credential for Flue
+- A current Google Chrome installation for local browser mode
+
+## Quickstart
+
+
+
+```bash
+git clone https://github.com/browserbase/stagehand.git
+cd stagehand
+pnpm install --frozen-lockfile
+pnpm exec turbo run build \
+ --filter @browserbasehq/stagehand-integrations
+```
+
+
+The example uses an OpenAI model by default:
+
+```bash
+export OPENAI_API_KEY="your-openai-api-key"
+```
+
+Set `FLUE_STAGEHAND_MODEL` to select another Flue model.
+
+
+The example defaults to Browserbase when `BROWSERBASE_API_KEY` is set, otherwise it uses local Chrome:
+
+```bash
+export STAGEHAND_BROWSER="browserbase"
+export BROWSERBASE_API_KEY="your-browserbase-api-key"
+```
+
+
+```bash
+pnpm --filter @browserbasehq/stagehand-integrations-example-flue-facade start \
+ "Open https://example.com and report the page title."
+```
+
+Flue uses the `run` tool and should report `Example Domain` before the process exits.
+
+
+
+## Configuration
+
+| Variable | Purpose |
+| --- | --- |
+| `FLUE_STAGEHAND_MODEL` | Flue agent model. Defaults to `openai/gpt-5.6-luna`. |
+| `OPENAI_API_KEY` | Credential for the example's default Flue model. |
+| `STAGEHAND_BROWSER` | Select `local` or `browserbase`. Defaults to `browserbase` when `BROWSERBASE_API_KEY` is set, otherwise `local`. |
+| `BROWSERBASE_API_KEY` | Required for Browserbase. |
+| `BROWSERBASE_PROJECT_ID` | Optional Browserbase project ID. |
+| `STAGEHAND_MODEL_NAME` | Optional. The facade tools are deterministic and do not call a Stagehand model today. |
+| `STAGEHAND_MODEL_API_KEY` | Optional credential for `STAGEHAND_MODEL_NAME`; the native integration can infer a supported provider key. |
+
+## Screenshot behavior
+
+Flue's current tool loop is text-only. This integration saves each screenshot to a temporary file and returns the path to the agent. Open the returned path to inspect the image.
+
+## Session lifecycle
+
+One Flue CLI run owns one lazily-created browser session. All three tools reuse it, so navigation, authentication, and snapshot IDs survive between calls. The browser closes when the run finishes or receives an interrupt.
+
+
+`run` executes model-authored JavaScript in the browser. Use Browserbase for untrusted tasks and review the [integration security boundary](/v4/integrations/overview#security-boundary).
+
+
+
+ Read the native tool bindings and browser lifecycle implementation.
+
diff --git a/packages/docs/v4/integrations/overview.mdx b/packages/docs/v4/integrations/overview.mdx
index 9aaee8545..7c773310e 100644
--- a/packages/docs/v4/integrations/overview.mdx
+++ b/packages/docs/v4/integrations/overview.mdx
@@ -1,7 +1,7 @@
---
title: "Integrations"
sidebarTitle: "Overview"
-description: "Connect Claude Code, Codex, CrewAI, Deep Agents, Eve, Mastra, fx, Pi, or the Vercel AI SDK to a persistent Stagehand browser."
+description: "Connect Claude Code, Codex, CrewAI, Deep Agents, Eve, Flue, Mastra, fx, Pi, or the Vercel AI SDK to a persistent Stagehand browser."
---
Each integration gives your agent one persistent browser and three tools: `run`, `snapshot`, and `screenshot`. Your agent decides how to navigate and interact while Stagehand manages the browser session.
@@ -22,6 +22,9 @@ Stagehand ships these experimental integrations from the monorepo and does not p
Give Vercel's framework for building durable agents native Stagehand tools.
+
+ Give a Flue agent native Stagehand tools backed by one persistent browser.
+
Run locally over MCP or deploy native tools with Managed Deep Agents.
@@ -70,7 +73,7 @@ Every integration exposes the same browser capabilities.
```
- Capture the active page as a PNG or JPEG for visual inspection. Frameworks that support multimodal tool results receive the image directly; the CrewAI adapter writes it to a temporary file because its current tool loop is text-only.
+ Capture the active page as a PNG or JPEG for visual inspection. Frameworks that support multimodal tool results receive the image directly; the CrewAI and Flue adapters write it to a temporary file because their current tool loops are text-only.
@@ -78,7 +81,7 @@ Every integration exposes the same browser capabilities.
The tools share a browser for the lifetime of the integration's client session. A navigation performed by `run` is visible to the next `snapshot`, and authentication and page state remain available across calls.
-The Claude Code, Codex, CrewAI, Mastra, fx, Vercel AI SDK, and local Deep Agents examples keep one MCP client session open so the stdio server and browser stay alive. Eve, Pi, and Managed Deep Agents bind equivalent tools in-process.
+The Claude Code, Codex, CrewAI, Mastra, fx, Vercel AI SDK, and local Deep Agents examples keep one MCP client session open so the stdio server and browser stay alive. Eve, Flue, Pi, and Managed Deep Agents bind equivalent tools in-process.
The private [`core/` workspace package](https://github.com/browserbase/stagehand/tree/main/packages/integrations/core) owns the shared TypeScript tool contract, browser runtime, native bindings, and stdio MCP server. The TypeScript integrations and CrewAI use this package.
@@ -88,7 +91,7 @@ Do not create a new MCP process for every tool call. Doing so starts a new brows
## Run the integrations from source
-Claude Code, Codex, CrewAI, Mastra, fx, and the Vercel AI SDK use the shared TypeScript Stagehand facade MCP server. Eve and Pi use native in-process bindings from the same package. These integrations require Node.js 24 or newer and [pnpm](https://pnpm.io/installation) 11.10.0. CrewAI also requires Python 3.11–3.13 and [uv](https://docs.astral.sh/uv/).
+Claude Code, Codex, CrewAI, Mastra, fx, and the Vercel AI SDK use the shared TypeScript Stagehand facade MCP server. Eve, Flue, and Pi use native in-process bindings from the same package. These integrations require Node.js 24 or newer and [pnpm](https://pnpm.io/installation) 11.10.0. CrewAI also requires Python 3.11–3.13 and [uv](https://docs.astral.sh/uv/).
@@ -126,7 +129,7 @@ export BROWSERBASE_API_KEY="your-browserbase-api-key"
| `BROWSERBASE_API_KEY` | Required for Browserbase. |
| `BROWSERBASE_PROJECT_ID` | Optional Browserbase project ID for the TypeScript integrations. |
| `STAGEHAND_MODEL_NAME` | Optional model for Stagehand AI methods called from TypeScript integration tools. |
-| `STAGEHAND_MODEL_API_KEY` | Credential for `STAGEHAND_MODEL_NAME`. Eve and Pi can infer a supported provider key; the MCP examples require this variable. |
+| `STAGEHAND_MODEL_API_KEY` | Credential for `STAGEHAND_MODEL_NAME`. Eve, Flue, and Pi can infer a supported provider key; the MCP examples require this variable. |
The framework's agent model and Stagehand's optional browser model are separate. The agent model decides which tool to call. Configure a Stagehand model only when JavaScript passed to `run` calls AI methods such as `act`, `extract`, or `observe`.
diff --git a/packages/integrations/README.md b/packages/integrations/README.md
index 00665ba3d..0bb243d12 100644
--- a/packages/integrations/README.md
+++ b/packages/integrations/README.md
@@ -16,6 +16,7 @@ else, never restated.
| `crewai/` | Python CrewAI example over MCP/stdio (uv project). |
| `deepagents/` | Python LangChain Deep Agents integrations: a local stdio MCP server and a Managed Deep Agents project with native tools. |
| `eve/` | Eve example with the tools bound natively via `defineTool` (Eve has no external-process tool mounting). |
+| `flue/` | Flue example with the tools bound natively in-process (Flue has no external-process tool mounting). |
| `fx/` | fx configuration templates and skill — fx consumes the facade via its user-global MCP config. |
| `mastra/` | Mastra example over MCP/stdio via Mastra's `MCPClient`. |
| `pi/` | Pi extension registering the tools natively (Pi ships without built-in MCP). |
diff --git a/packages/integrations/flue/README.md b/packages/integrations/flue/README.md
new file mode 100644
index 000000000..1ff2c864d
--- /dev/null
+++ b/packages/integrations/flue/README.md
@@ -0,0 +1,51 @@
+# Flue + Stagehand facade (native tools)
+
+This example gives a Flue agent the native tools `run`, `snapshot`, and `screenshot`. The tools
+share a Stagehand session directly; no MCP connection or bridge process is required.
+
+## Setup
+
+Use Node.js 24 or later. From the repository root, build the integrations package before running
+the example:
+
+```bash
+pnpm exec turbo run build --filter @browserbasehq/stagehand-integrations
+```
+
+Configure the environment as needed:
+
+| Variable | Purpose |
+| ------------------------- | ------------------------------------------------------------------------------------------------ |
+| `STAGEHAND_BROWSER` | Browser backend. Defaults to `browserbase` when `BROWSERBASE_API_KEY` is set, otherwise `local`. |
+| `BROWSERBASE_API_KEY` | Browserbase API key. |
+| `STAGEHAND_MODEL_NAME` | Optional Stagehand model name, such as `openai/gpt-5.6-luna`. |
+| `STAGEHAND_MODEL_API_KEY` | Optional explicit API key for `STAGEHAND_MODEL_NAME`; otherwise a supported provider key is inferred. |
+| `FLUE_STAGEHAND_MODEL` | Flue agent model; defaults to `openai/gpt-5.6-luna`. |
+| `OPENAI_API_KEY` | Used by the Flue agent model in the host process. |
+
+## Run
+
+```bash
+pnpm --filter @browserbasehq/stagehand-integrations-example-flue-facade test
+pnpm --filter @browserbasehq/stagehand-integrations-example-flue-facade typecheck
+pnpm --filter @browserbasehq/stagehand-integrations-example-flue-facade start \
+ "Open https://example.com and report the page title."
+```
+
+## Screenshots
+
+Flue's current tool loop is text-only. This example saves each screenshot to a temporary file and
+returns its path and MIME type to the agent; open the file to inspect it.
+
+## Security model
+
+`run(code)` executes model-authored JavaScript in the extension service worker:
+it runs browser-side, never in the host process. Browserbase is the
+recommended isolation boundary. The Flue process holds only the browser session
+handle; model-authored JavaScript does not execute inside the host process.
+
+## Session lifecycle
+
+One CLI run owns one lazily-created browser session. All three tools reuse that browser, so page
+state and snapshot IDs survive between tool calls. The browser closes when the Flue run finishes
+or is interrupted.
diff --git a/packages/integrations/flue/agent/agent.ts b/packages/integrations/flue/agent/agent.ts
new file mode 100644
index 000000000..e453dcbc6
--- /dev/null
+++ b/packages/integrations/flue/agent/agent.ts
@@ -0,0 +1,21 @@
+"use agent";
+
+import { useModel, useTool } from "@flue/runtime";
+
+import { FACADE_AGENT_INSTRUCTIONS } from "@browserbasehq/stagehand-integrations/facade";
+
+import runTool from "./tools/run.js";
+import screenshotTool from "./tools/screenshot.js";
+import snapshotTool from "./tools/snapshot.js";
+
+export const FLUE_STAGEHAND_INSTRUCTIONS = `${FACADE_AGENT_INSTRUCTIONS}
+
+The screenshot tool returns a temporary local file path and MIME type. Report that path so the user can inspect the image.`;
+
+export function StagehandAgent() {
+ useModel(process.env.FLUE_STAGEHAND_MODEL ?? "openai/gpt-5.6-luna");
+ useTool(runTool);
+ useTool(snapshotTool);
+ useTool(screenshotTool);
+ return FLUE_STAGEHAND_INSTRUCTIONS;
+}
diff --git a/packages/integrations/flue/agent/tools/run.ts b/packages/integrations/flue/agent/tools/run.ts
new file mode 100644
index 000000000..56db8697b
--- /dev/null
+++ b/packages/integrations/flue/agent/tools/run.ts
@@ -0,0 +1,43 @@
+import { defineTool } from "@flue/runtime";
+import * as v from "valibot";
+
+import {
+ CodeModeRunInputSchema,
+ RUN_TOOL_DESCRIPTION,
+} from "@browserbasehq/stagehand-integrations/facade";
+
+import { discardFacadeToolsIfUnhealthy, getFacadeTools } from "../../src/session.js";
+
+const runInput = v.strictObject({
+ code: v.optional(v.pipe(v.string(), v.minLength(1))),
+ actions: v.optional(v.pipe(v.array(v.record(v.string(), v.unknown())), v.minLength(1))),
+});
+
+export default defineTool({
+ name: "run",
+ description: RUN_TOOL_DESCRIPTION,
+ input: runInput,
+ async run({ data }) {
+ const input = CodeModeRunInputSchema.parse(data);
+ const tools = await getFacadeTools();
+ try {
+ const result =
+ input.code !== undefined
+ ? await tools.run(input.code)
+ : await tools.runActions(input.actions!);
+ return stringifyResult(result);
+ } catch (error) {
+ await discardFacadeToolsIfUnhealthy(tools);
+ throw error;
+ }
+ },
+});
+
+function stringifyResult(value: unknown): string {
+ if (typeof value === "string") return value;
+ try {
+ return JSON.stringify(value, null, 2) ?? String(value);
+ } catch {
+ return String(value);
+ }
+}
diff --git a/packages/integrations/flue/agent/tools/screenshot.ts b/packages/integrations/flue/agent/tools/screenshot.ts
new file mode 100644
index 000000000..e7fea1dae
--- /dev/null
+++ b/packages/integrations/flue/agent/tools/screenshot.ts
@@ -0,0 +1,31 @@
+import { defineTool } from "@flue/runtime";
+import * as v from "valibot";
+
+import {
+ SCREENSHOT_TOOL_DESCRIPTION,
+ ScreenshotInputSchema,
+} from "@browserbasehq/stagehand-integrations/facade";
+
+import { writeScreenshotArtifact } from "../../src/artifacts.js";
+import { discardFacadeToolsIfUnhealthy, getFacadeTools } from "../../src/session.js";
+
+export default defineTool({
+ name: "screenshot",
+ description: SCREENSHOT_TOOL_DESCRIPTION,
+ input: v.strictObject({
+ fullPage: v.optional(v.boolean()),
+ type: v.optional(v.picklist(["png", "jpeg"])),
+ quality: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(100))),
+ }),
+ async run({ data }) {
+ const input = ScreenshotInputSchema.parse(data);
+ const tools = await getFacadeTools();
+ try {
+ const image = await tools.screenshot(input);
+ return { output: await writeScreenshotArtifact(image) };
+ } catch (error) {
+ await discardFacadeToolsIfUnhealthy(tools);
+ throw error;
+ }
+ },
+});
diff --git a/packages/integrations/flue/agent/tools/snapshot.ts b/packages/integrations/flue/agent/tools/snapshot.ts
new file mode 100644
index 000000000..1026120a0
--- /dev/null
+++ b/packages/integrations/flue/agent/tools/snapshot.ts
@@ -0,0 +1,27 @@
+import { defineTool } from "@flue/runtime";
+import * as v from "valibot";
+
+import {
+ SNAPSHOT_TOOL_DESCRIPTION,
+ SnapshotInputSchema,
+} from "@browserbasehq/stagehand-integrations/facade";
+
+import { discardFacadeToolsIfUnhealthy, getFacadeTools } from "../../src/session.js";
+
+export default defineTool({
+ name: "snapshot",
+ description: SNAPSHOT_TOOL_DESCRIPTION,
+ input: v.strictObject({
+ includeIframes: v.optional(v.boolean()),
+ }),
+ async run({ data }) {
+ const input = SnapshotInputSchema.parse(data);
+ const tools = await getFacadeTools();
+ try {
+ return await tools.snapshot(input);
+ } catch (error) {
+ await discardFacadeToolsIfUnhealthy(tools);
+ throw error;
+ }
+ },
+});
diff --git a/packages/integrations/flue/package.json b/packages/integrations/flue/package.json
new file mode 100644
index 000000000..a47703c80
--- /dev/null
+++ b/packages/integrations/flue/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "@browserbasehq/stagehand-integrations-example-flue-facade",
+ "version": "4.0.1",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "start": "tsx src/run-agent.ts",
+ "test": "vitest run",
+ "test:unit": "vitest run",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@browserbasehq/stagehand": "workspace:*",
+ "@browserbasehq/stagehand-integrations": "workspace:*",
+ "@flue/runtime": "catalog:",
+ "dotenv": "catalog:",
+ "valibot": "catalog:"
+ },
+ "devDependencies": {
+ "@earendil-works/pi-ai": "catalog:",
+ "@types/node": "catalog:",
+ "tsx": "catalog:",
+ "typescript": "catalog:",
+ "vitest": "catalog:"
+ },
+ "engines": {
+ "node": ">=24"
+ }
+}
diff --git a/packages/integrations/flue/src/artifacts.ts b/packages/integrations/flue/src/artifacts.ts
new file mode 100644
index 000000000..54d267c43
--- /dev/null
+++ b/packages/integrations/flue/src/artifacts.ts
@@ -0,0 +1,21 @@
+import { mkdtemp, writeFile } from "node:fs/promises";
+import os from "node:os";
+import path from "node:path";
+
+let artifactDirectory: string | undefined;
+let screenshotNumber = 0;
+
+export async function writeScreenshotArtifact(image: {
+ data: string;
+ mimeType: string;
+}): Promise<{ path: string; mimeType: string }> {
+ artifactDirectory ??= await mkdtemp(path.join(os.tmpdir(), "stagehand-flue-"));
+ screenshotNumber += 1;
+ const extension = image.mimeType === "image/jpeg" ? "jpg" : "png";
+ const screenshotPath = path.join(
+ artifactDirectory,
+ `screenshot-${screenshotNumber}.${extension}`,
+ );
+ await writeFile(screenshotPath, Buffer.from(image.data, "base64"));
+ return { path: screenshotPath, mimeType: image.mimeType };
+}
diff --git a/packages/integrations/flue/src/run-agent.ts b/packages/integrations/flue/src/run-agent.ts
new file mode 100644
index 000000000..fff12d339
--- /dev/null
+++ b/packages/integrations/flue/src/run-agent.ts
@@ -0,0 +1,47 @@
+import { randomUUID } from "node:crypto";
+
+import { init } from "@flue/runtime";
+import { start } from "@flue/runtime/node";
+import dotenv from "dotenv";
+
+import { StagehandAgent } from "../agent/agent.js";
+import { closeFacadeSession } from "./session.js";
+
+dotenv.config();
+
+const task = process.argv.slice(2).join(" ").trim();
+if (!task) {
+ throw new Error('Usage: pnpm start "Open https://example.com and report the title."');
+}
+
+const flue = await start({ agents: [StagehandAgent] });
+const agent = init(StagehandAgent, { id: `stagehand-${randomUUID()}` });
+
+const interrupt = () => {
+ void agent.abort();
+};
+process.once("SIGINT", interrupt);
+process.once("SIGTERM", interrupt);
+
+let primaryError: unknown;
+try {
+ const receipt = await agent.dispatch(task);
+ const reply = await agent.read(receipt);
+ process.stdout.write(`${reply.text}\n`);
+} catch (error) {
+ primaryError = error;
+}
+
+process.removeListener("SIGINT", interrupt);
+process.removeListener("SIGTERM", interrupt);
+
+const cleanupErrors: unknown[] = [];
+await agent.abort().catch((error: unknown) => cleanupErrors.push(error));
+await closeFacadeSession().catch((error: unknown) => cleanupErrors.push(error));
+await flue.stop().catch((error: unknown) => cleanupErrors.push(error));
+
+if (primaryError !== undefined && cleanupErrors.length > 0) {
+ throw new AggregateError([primaryError, ...cleanupErrors], "Flue run and cleanup failed");
+}
+if (primaryError !== undefined) throw primaryError;
+if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors, "Flue cleanup failed");
diff --git a/packages/integrations/flue/src/session.ts b/packages/integrations/flue/src/session.ts
new file mode 100644
index 000000000..beca7e750
--- /dev/null
+++ b/packages/integrations/flue/src/session.ts
@@ -0,0 +1,94 @@
+import {
+ browserbase,
+ localBrowser,
+ Stagehand,
+ type StagehandBrowser,
+} from "@browserbasehq/stagehand";
+import {
+ StagehandFacadeTools,
+ stagehandFacadeConfigFromEnv,
+} from "@browserbasehq/stagehand-integrations/facade";
+
+type FacadeResources = {
+ browser: StagehandBrowser;
+ stagehand: Stagehand;
+ tools: StagehandFacadeTools;
+};
+
+let resources: FacadeResources | undefined;
+let resourcesPromise: Promise | undefined;
+let cleanupPromise: Promise = Promise.resolve();
+
+export async function getFacadeTools(): Promise {
+ if (resources && !resources.browser.closed) return resources.tools;
+ if (resources) discardFacadeTools(resources.tools);
+
+ resourcesPromise ??= cleanupPromise.then(createResources);
+ const pending = resourcesPromise;
+ try {
+ const created = await pending;
+ if (resourcesPromise === pending) resources = created;
+ return created.tools;
+ } catch (error) {
+ if (resourcesPromise === pending) resourcesPromise = undefined;
+ throw error;
+ } finally {
+ if (resourcesPromise === pending) resourcesPromise = undefined;
+ }
+}
+
+export async function discardFacadeToolsIfUnhealthy(expected: StagehandFacadeTools): Promise {
+ if (resources?.tools !== expected) return;
+
+ try {
+ if (!resources.browser.closed) await resources.browser.context.pages();
+ else discardFacadeTools(expected);
+ } catch {
+ discardFacadeTools(expected);
+ }
+}
+
+export async function closeFacadeSession(): Promise {
+ const pending = resourcesPromise;
+ if (pending) await pending.catch(() => undefined);
+
+ const current = resources;
+ resources = undefined;
+ resourcesPromise = undefined;
+
+ const errors: unknown[] = [];
+ await cleanupPromise.catch((error: unknown) => errors.push(error));
+ if (current) {
+ await current.stagehand.close().catch((error: unknown) => errors.push(error));
+ await current.browser.close().catch((error: unknown) => errors.push(error));
+ }
+ if (errors.length > 0)
+ throw new AggregateError(errors, "Failed to close the Flue facade session");
+}
+
+function discardFacadeTools(expected: StagehandFacadeTools): void {
+ if (resources?.tools !== expected) return;
+ const stale = resources;
+ resources = undefined;
+ cleanupPromise = cleanupPromise.then(() => closeResources(stale));
+}
+
+async function createResources(): Promise {
+ const config = stagehandFacadeConfigFromEnv();
+ const browser =
+ config.browser.type === "browserbase"
+ ? await browserbase.launch(config.browser.launchOptions)
+ : await localBrowser.launch(config.browser.launchOptions);
+ try {
+ const stagehand = await Stagehand.create({ browser, ...config.stagehand });
+ return { browser, stagehand, tools: new StagehandFacadeTools(stagehand) };
+ } catch (error) {
+ await browser.close().catch(() => undefined);
+ throw error;
+ }
+}
+
+async function closeResources(stale: FacadeResources): Promise {
+ await stale.stagehand.close().catch(() => undefined);
+ await stale.browser.close().catch(() => undefined);
+}
diff --git a/packages/integrations/flue/tests/tools.test.ts b/packages/integrations/flue/tests/tools.test.ts
new file mode 100644
index 000000000..22183cb82
--- /dev/null
+++ b/packages/integrations/flue/tests/tools.test.ts
@@ -0,0 +1,153 @@
+import { randomUUID } from "node:crypto";
+import { readFile, rm } from "node:fs/promises";
+import path from "node:path";
+
+import { fauxAssistantMessage, fauxProvider, fauxText, fauxToolCall } from "@earendil-works/pi-ai";
+import { init } from "@flue/runtime";
+import { start } from "@flue/runtime/node";
+import * as v from "valibot";
+import { afterEach, describe, expect, it, vi } from "vitest";
+
+import {
+ CodeModeRunInputSchema,
+ FACADE_AGENT_INSTRUCTIONS,
+ FACADE_TOOLS,
+} from "@browserbasehq/stagehand-integrations/facade";
+
+import { FLUE_STAGEHAND_INSTRUCTIONS, StagehandAgent } from "../agent/agent.js";
+import runTool from "../agent/tools/run.js";
+import screenshotTool from "../agent/tools/screenshot.js";
+import snapshotTool from "../agent/tools/snapshot.js";
+
+const fakeFacade = vi.hoisted(() => ({
+ run: vi.fn(async () => ({ title: "Example Domain" })),
+ runActions: vi.fn(async () => ({ completed: 1, url: "https://example.com" })),
+ snapshot: vi.fn(async () => "[1-1] link: Example Domain"),
+ screenshot: vi.fn(async () => ({
+ data: Buffer.from("fake-png").toString("base64"),
+ mimeType: "image/png" as const,
+ })),
+}));
+
+vi.mock("../src/session.js", () => ({
+ getFacadeTools: async () => fakeFacade,
+ discardFacadeToolsIfUnhealthy: async () => undefined,
+}));
+
+const screenshotDirectories = new Set();
+const stagehandTools = [runTool, snapshotTool, screenshotTool];
+
+afterEach(async () => {
+ vi.clearAllMocks();
+ await Promise.all(
+ [...screenshotDirectories].map((directory) => rm(directory, { recursive: true, force: true })),
+ );
+ screenshotDirectories.clear();
+});
+
+describe("Flue Stagehand facade tools", () => {
+ it("uses the facade descriptions", () => {
+ expect(stagehandTools.map((tool) => tool.name)).toEqual(FACADE_TOOLS.map((tool) => tool.name));
+ expect(runTool.description).toBe(FACADE_TOOLS[0].description);
+ expect(snapshotTool.description).toBe(FACADE_TOOLS[1].description);
+ expect(screenshotTool.description).toBe(FACADE_TOOLS[2].description);
+ });
+
+ it("keeps the canonical facade instructions", () => {
+ expect(FLUE_STAGEHAND_INSTRUCTIONS).toContain(FACADE_AGENT_INSTRUCTIONS);
+ expect(FLUE_STAGEHAND_INSTRUCTIONS).toContain("temporary local file path");
+ expect(FLUE_STAGEHAND_INSTRUCTIONS).toContain("Report that path");
+ });
+
+ it("validates run input before opening a browser", async () => {
+ expect(CodeModeRunInputSchema.safeParse({ code: "return 1;" }).success).toBe(true);
+ await expect(runTool.run(runContext({}))).rejects.toThrow(
+ "run requires exactly one of code or actions",
+ );
+ await expect(
+ runTool.run(runContext({ code: "return 1;", actions: [{ op: "click", id: "1-1" }] })),
+ ).rejects.toThrow("run requires exactly one of code or actions");
+ expect(() => v.parse(runTool.input, { code: "return 1;", extra: true })).toThrow();
+ expect(() => v.parse(snapshotTool.input, { includeIframes: false, extra: true })).toThrow();
+ expect(() => v.parse(screenshotTool.input, { type: "png", extra: true })).toThrow();
+ });
+
+ it("routes valid inputs through one facade tool set", async () => {
+ await expect(runTool.run(runContext({ code: "return await page.title();" }))).resolves.toBe(
+ '{\n "title": "Example Domain"\n}',
+ );
+ await expect(
+ runTool.run(runContext({ actions: [{ op: "click", id: "1-1" }] })),
+ ).resolves.toContain('"completed": 1');
+ await expect(snapshotTool.run(snapshotContext({ includeIframes: false }))).resolves.toBe(
+ "[1-1] link: Example Domain",
+ );
+
+ expect(fakeFacade.run).toHaveBeenCalledWith("return await page.title();");
+ expect(fakeFacade.runActions).toHaveBeenCalledWith([{ op: "click", id: "1-1" }]);
+ expect(fakeFacade.snapshot).toHaveBeenCalledWith({ includeIframes: false });
+ });
+
+ it("writes screenshots to an inspectable temporary file", async () => {
+ const result = await screenshotTool.run(screenshotContext({ type: "png" }));
+ if (typeof result !== "object" || result === null || !("output" in result)) {
+ throw new Error("Expected a screenshot output envelope");
+ }
+
+ const output = result.output as { path: string; mimeType: string };
+ screenshotDirectories.add(path.dirname(output.path));
+ expect(output.mimeType).toBe("image/png");
+ await expect(readFile(output.path, "utf8")).resolves.toBe("fake-png");
+ });
+
+ it("runs the native tools through a real Flue agent loop", async () => {
+ const previousModel = process.env.FLUE_STAGEHAND_MODEL;
+ process.env.FLUE_STAGEHAND_MODEL = "faux/faux-1";
+ const faux = fauxProvider();
+ faux.setResponses([
+ (context) => {
+ expect(context.tools?.map((tool) => tool.name)).toEqual(
+ expect.arrayContaining(FACADE_TOOLS.map((tool) => tool.name)),
+ );
+ return fauxAssistantMessage(fauxToolCall("run", { code: "return await page.title();" }), {
+ stopReason: "toolUse",
+ });
+ },
+ fauxAssistantMessage(fauxText("Example Domain")),
+ ]);
+
+ let flue: Awaited> | undefined;
+ try {
+ flue = await start({ agents: [StagehandAgent], providers: [faux.provider] });
+ const agent = init(StagehandAgent, { id: `stagehand-test-${randomUUID()}` });
+ const receipt = await agent.dispatch("Open example.com and report its title.");
+ const reply = await agent.read(receipt, { signal: AbortSignal.timeout(10_000) });
+ expect(reply.text).toBe("Example Domain");
+ expect(fakeFacade.run).toHaveBeenCalledWith("return await page.title();");
+ } finally {
+ await flue?.stop();
+ if (previousModel === undefined) delete process.env.FLUE_STAGEHAND_MODEL;
+ else process.env.FLUE_STAGEHAND_MODEL = previousModel;
+ }
+ });
+});
+
+function runContext(data: unknown): Parameters[0] {
+ return baseContext(data) as Parameters[0];
+}
+
+function snapshotContext(data: unknown): Parameters[0] {
+ return baseContext(data) as Parameters[0];
+}
+
+function screenshotContext(data: unknown): Parameters[0] {
+ return baseContext(data) as Parameters[0];
+}
+
+function baseContext(data: unknown) {
+ return {
+ data,
+ toolCallId: "test-call",
+ log: { info: () => undefined, warn: () => undefined, error: () => undefined },
+ };
+}
diff --git a/packages/integrations/flue/tsconfig.json b/packages/integrations/flue/tsconfig.json
new file mode 100644
index 000000000..17ff1a30f
--- /dev/null
+++ b/packages/integrations/flue/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "../../../tsconfig.json",
+ "compilerOptions": {
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "target": "ES2022",
+ "types": ["node"],
+ "rootDir": ".",
+ "noEmit": true,
+ "skipLibCheck": true
+ },
+ "include": ["agent/**/*.ts", "src/**/*.ts", "tests/**/*.ts", "vitest.config.ts"],
+ "exclude": ["dist", "node_modules"]
+}
diff --git a/packages/integrations/flue/vitest.config.ts b/packages/integrations/flue/vitest.config.ts
new file mode 100644
index 000000000..d82214c96
--- /dev/null
+++ b/packages/integrations/flue/vitest.config.ts
@@ -0,0 +1,9 @@
+import { defineConfig } from "vitest/config";
+
+export default defineConfig({
+ test: {
+ include: ["tests/**/*.test.ts"],
+ hookTimeout: 20_000,
+ testTimeout: 20_000,
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 62ef42254..1b61076fc 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -247,9 +247,15 @@ catalogs:
'@changesets/cli':
specifier: 2.31.1
version: 2.31.1
+ '@earendil-works/pi-ai':
+ specifier: 0.83.0
+ version: 0.83.0
'@earendil-works/pi-coding-agent':
specifier: 0.84.2
version: 0.84.2
+ '@flue/runtime':
+ specifier: 2.0.3
+ version: 2.0.3
'@mastra/core':
specifier: ^1.55.0
version: 1.57.0
@@ -349,6 +355,9 @@ catalogs:
typescript:
specifier: ^5
version: 5.9.3
+ valibot:
+ specifier: ^1.4.0
+ version: 1.4.2
vitest:
specifier: 4.1.9
version: 4.1.9
@@ -679,6 +688,40 @@ importers:
specifier: 'catalog:'
version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0))
+ packages/integrations/flue:
+ dependencies:
+ '@browserbasehq/stagehand':
+ specifier: workspace:*
+ version: link:../../sdk-ts
+ '@browserbasehq/stagehand-integrations':
+ specifier: workspace:*
+ version: link:../core
+ '@flue/runtime':
+ specifier: 'catalog:'
+ version: 2.0.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(typescript@5.9.3)(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3)
+ dotenv:
+ specifier: 'catalog:'
+ version: 17.4.2
+ valibot:
+ specifier: 'catalog:'
+ version: 1.4.2(typescript@5.9.3)
+ devDependencies:
+ '@earendil-works/pi-ai':
+ specifier: 'catalog:'
+ version: 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3)
+ '@types/node':
+ specifier: 'catalog:'
+ version: 24.13.2
+ tsx:
+ specifier: 'catalog:'
+ version: 4.23.1
+ typescript:
+ specifier: 'catalog:'
+ version: 5.9.3
+ vitest:
+ specifier: 'catalog:'
+ version: 4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0))
+
packages/integrations/mastra:
dependencies:
'@ai-sdk/openai':
@@ -1505,10 +1548,19 @@ packages:
'@changesets/write@0.4.0':
resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
+ '@earendil-works/pi-agent-core@0.83.0':
+ resolution: {integrity: sha512-RorGp9OH5l3ElpuC5a5ZQ2eWcchZGXflXRzVGkV99y3y6tT+LLNyxoYIdVKvTKWEObwhExeQbTH0fI2tE4iX4g==}
+ engines: {node: '>=22.19.0'}
+
'@earendil-works/pi-agent-core@0.84.2':
resolution: {integrity: sha512-8Pn3wSCxj0cfo5I6jxQYVB/3uuQRmHhAlEclyjqpOuMEdQMIODHizRogv56FLdbU+dTiGnybeHQ2N+sV1/L2YA==}
engines: {node: '>=22.19.0'}
+ '@earendil-works/pi-ai@0.83.0':
+ resolution: {integrity: sha512-m3IZD4g3er0V8TC9+Vpgw/sjTKqcJlkcIBy/JvsgRubuuik3tAVzyugUg4rVrShIkkOT69mEd34NEqKUIsl6JQ==}
+ engines: {node: '>=22.19.0'}
+ hasBin: true
+
'@earendil-works/pi-ai@0.84.2':
resolution: {integrity: sha512-6MzsrYIYNVlE7SfpbL2yYb67Qo58p/7Q+xWG1RZvoX1P80aRCHSod2/13aFpxkow1lPO2LEh3c495J0Gwmyjig==}
engines: {node: '>=22.19.0'}
@@ -1871,6 +1923,10 @@ packages:
'@floating-ui/utils@0.2.12':
resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
+ '@flue/runtime@2.0.3':
+ resolution: {integrity: sha512-RfWyZG9x2hlDb1264XTESX42tzzG3AA8er3XjaTIxIMh28pTD91zKd9Jh6PFXKeTkZegLsGiJKDxmiCddlyeug==}
+ engines: {node: '>=22.19.0'}
+
'@google/genai@1.52.0':
resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==}
engines: {node: '>=20.0.0'}
@@ -1886,6 +1942,12 @@ packages:
peerDependencies:
hono: ^4
+ '@hono/node-server@2.1.1':
+ resolution: {integrity: sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg==}
+ engines: {node: '>=20'}
+ peerDependencies:
+ hono: ^4
+
'@img/colour@1.1.0':
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
engines: {node: '>=18'}
@@ -2493,6 +2555,22 @@ packages:
'@mintlify/validation@0.1.823':
resolution: {integrity: sha512-rKGLBAKPK9pMSbAMfXo4flj5mdlZVHCPGbSe3K0XUU+wZQtdnY5dirvgSCdkOPe6T6uCN0WyScHNnk+wH6t7vw==}
+ '@mistralai/mistralai@2.2.6':
+ resolution: {integrity: sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==}
+ peerDependencies:
+ '@opentelemetry/api': ^1.9.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+
+ '@modelcontextprotocol/client@2.0.0':
+ resolution: {integrity: sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==}
+ engines: {node: '>=20'}
+
+ '@modelcontextprotocol/core@2.0.0':
+ resolution: {integrity: sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==}
+ engines: {node: '>=20'}
+
'@modelcontextprotocol/ext-apps@1.7.5':
resolution: {integrity: sha512-TjPH2S2y5UEGKhmI6+XGFuqfqOV4ppe1x6DA3txnUaEWkgtA4G5vo14jGKFZmegdkZ1H4QMLyujLvoU1BEdnAg==}
engines: {node: '>=20'}
@@ -3248,10 +3326,6 @@ packages:
resolution: {integrity: sha512-hCynhm22wMJ8wTF9crcwu8mxggtUrSLLJgDcGUvYFBqpofxycYJCGKOMYg4xtPPFtgNiDJSYmhsWLTrcU/g59Q==}
engines: {node: '>=18.0.0'}
- '@smithy/types@4.16.1':
- resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==}
- engines: {node: '>=18.0.0'}
-
'@smithy/types@4.17.0':
resolution: {integrity: sha512-Aw4joiM0ZdErpo39lCj8phT2lxoiKZV+KZzBxnnQhWVtU2Is/WffQSL04uUWRcXUse9Ln8vXZK6V/FwqRVnQpg==}
engines: {node: '>=18.0.0'}
@@ -3490,6 +3564,11 @@ packages:
'@ungap/structured-clone@1.3.3':
resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==}
+ '@valibot/to-json-schema@1.7.1':
+ resolution: {integrity: sha512-3qkmU6KXWh8GIThEAW3kuRHPQBMjWkKy+Ppz3WkUucx53DTpOa6siMn4xDGSOhlVyMrDaJTCTMLYPZVAIk1P0A==}
+ peerDependencies:
+ valibot: ^1.4.0
+
'@vercel/detect-agent@1.2.3':
resolution: {integrity: sha512-VYNCgUc0nOmC4WJmWw9GkrKdfr8Zl4/rxhC5SvgacBgxiW9W/9NRttUoHHXV8xdII3MaRgkZZVX8Ikzc/Jmjag==}
engines: {node: '>=14'}
@@ -5564,6 +5643,10 @@ packages:
resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==}
hasBin: true
+ js-yaml@5.3.0:
+ resolution: {integrity: sha512-muutsYr+e2+d3rTgUGslq5rxbBlUy3cJ61IsHag2QNDQV+7zXWjkUpmALIajhrlLlrgRUiymj6U3zUr/TMK84Q==}
+ hasBin: true
+
jsep@1.4.0:
resolution: {integrity: sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==}
engines: {node: '>= 10.16.0'}
@@ -5640,6 +5723,9 @@ packages:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'}
+ layerr@3.0.0:
+ resolution: {integrity: sha512-tv754Ki2dXpPVApOrjTyRo4/QegVb9eVFq4mjqp4+NM5NaX7syQvN5BBNfV/ZpAHCEHV24XdUVrBAoka4jt3pA==}
+
lazystream@1.0.1:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'}
@@ -6440,6 +6526,18 @@ packages:
zod:
optional: true
+ openai@6.26.0:
+ resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==}
+ hasBin: true
+ peerDependencies:
+ ws: ^8.18.0
+ zod: ^3.25 || ^4.0
+ peerDependenciesMeta:
+ ws:
+ optional: true
+ zod:
+ optional: true
+
openai@6.40.0:
resolution: {integrity: sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==}
peerDependencies:
@@ -7722,6 +7820,10 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
+ ulidx@2.4.1:
+ resolution: {integrity: sha512-xY7c8LPyzvhvew0Fn+Ek3wBC9STZAuDI/Y5andCKi9AX6/jvfaX45PhsDX8oxgPL0YFp0Jhr8qWMbS/p9375Xg==}
+ engines: {node: '>=16'}
+
unbox-primitive@1.1.0:
resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
engines: {node: '>= 0.4'}
@@ -7921,6 +8023,14 @@ packages:
deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
+ valibot@1.4.2:
+ resolution: {integrity: sha512-gjdCvJ6d3RyHAneqxMYMW9QMCwYMb3jpOO0IyHZV1bnRHFBHrX3VkIILt5XYR0WhwHiH7Mty8ovuPZ/O3gamrg==}
+ peerDependencies:
+ typescript: '>=5'
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
@@ -8688,10 +8798,10 @@ snapshots:
'@aws-sdk/middleware-websocket': 3.972.50
'@aws-sdk/token-providers': 3.1048.0
'@aws-sdk/types': 3.974.3
- '@smithy/core': 3.30.0
+ '@smithy/core': 3.32.0
'@smithy/fetch-http-handler': 5.7.0
- '@smithy/node-http-handler': 4.7.3
- '@smithy/types': 4.16.1
+ '@smithy/node-http-handler': 4.10.0
+ '@smithy/types': 4.17.0
tslib: 2.8.1
'@aws-sdk/core@3.977.7':
@@ -8793,14 +8903,14 @@ snapshots:
dependencies:
'@aws-sdk/types': 3.974.3
'@smithy/core': 3.32.0
- '@smithy/types': 4.16.1
+ '@smithy/types': 4.17.0
tslib: 2.8.1
'@aws-sdk/middleware-eventstream@3.972.27':
dependencies:
'@aws-sdk/types': 3.974.3
'@smithy/core': 3.32.0
- '@smithy/types': 4.16.1
+ '@smithy/types': 4.17.0
tslib: 2.8.1
'@aws-sdk/middleware-websocket@3.972.50':
@@ -8810,7 +8920,7 @@ snapshots:
'@smithy/core': 3.32.0
'@smithy/fetch-http-handler': 5.7.0
'@smithy/signature-v4': 5.7.0
- '@smithy/types': 4.16.1
+ '@smithy/types': 4.17.0
tslib: 2.8.1
'@aws-sdk/nested-clients@3.997.42':
@@ -8836,8 +8946,8 @@ snapshots:
'@aws-sdk/core': 3.977.7
'@aws-sdk/nested-clients': 3.997.42
'@aws-sdk/types': 3.974.3
- '@smithy/core': 3.30.0
- '@smithy/types': 4.16.1
+ '@smithy/core': 3.32.0
+ '@smithy/types': 4.17.0
tslib: 2.8.1
'@aws-sdk/token-providers@3.1108.0':
@@ -9174,6 +9284,21 @@ snapshots:
human-id: 4.2.0
prettier: 2.8.8
+ '@earendil-works/pi-agent-core@0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3)':
+ dependencies:
+ '@earendil-works/pi-ai': 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3)
+ diff: 8.0.4
+ ignore: 7.0.5
+ typebox: 1.3.7
+ yaml: 2.9.0
+ transitivePeerDependencies:
+ - '@modelcontextprotocol/sdk'
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ - ws
+ - zod
+
'@earendil-works/pi-agent-core@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3)':
dependencies:
'@earendil-works/pi-ai': 0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3)
@@ -9190,6 +9315,27 @@ snapshots:
- ws
- zod
+ '@earendil-works/pi-ai@0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3)':
+ dependencies:
+ '@anthropic-ai/sdk': 0.91.1(zod@4.4.3)
+ '@aws-sdk/client-bedrock-runtime': 3.1048.0
+ '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)
+ '@mistralai/mistralai': 2.2.6(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)
+ '@opentelemetry/api': 1.9.0
+ '@smithy/node-http-handler': 4.7.3
+ http-proxy-agent: 7.0.2
+ https-proxy-agent: 7.0.6
+ openai: 6.26.0(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3)
+ partial-json: 0.1.7
+ typebox: 1.3.7
+ transitivePeerDependencies:
+ - '@modelcontextprotocol/sdk'
+ - bufferutil
+ - supports-color
+ - utf-8-validate
+ - ws
+ - zod
+
'@earendil-works/pi-ai@0.84.2(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3)':
dependencies:
'@anthropic-ai/sdk': 0.91.1(zod@4.4.3)
@@ -9448,6 +9594,26 @@ snapshots:
'@floating-ui/utils@0.2.12': {}
+ '@flue/runtime@2.0.3(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(typescript@5.9.3)(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3)':
+ dependencies:
+ '@earendil-works/pi-agent-core': 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3)
+ '@earendil-works/pi-ai': 0.83.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3)
+ '@hono/node-server': 2.1.1(hono@4.12.32)
+ '@modelcontextprotocol/client': 2.0.0
+ '@valibot/to-json-schema': 1.7.1(valibot@1.4.2(typescript@5.9.3))
+ hono: 4.12.32
+ js-yaml: 5.3.0
+ ulidx: 2.4.1
+ valibot: 1.4.2(typescript@5.9.3)
+ transitivePeerDependencies:
+ - '@modelcontextprotocol/sdk'
+ - bufferutil
+ - supports-color
+ - typescript
+ - utf-8-validate
+ - ws
+ - zod
+
'@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(bufferutil@4.1.0)':
dependencies:
google-auth-library: 10.9.1
@@ -9465,6 +9631,10 @@ snapshots:
dependencies:
hono: 4.12.32
+ '@hono/node-server@2.1.1(hono@4.12.32)':
+ dependencies:
+ hono: 4.12.32
+
'@img/colour@1.1.0': {}
'@img/sharp-darwin-arm64@0.33.5':
@@ -10290,6 +10460,32 @@ snapshots:
- supports-color
- typescript
+ '@mistralai/mistralai@2.2.6(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)':
+ dependencies:
+ '@opentelemetry/semantic-conventions': 1.43.0
+ ws: 8.21.0(bufferutil@4.1.0)
+ zod: 4.4.3
+ zod-to-json-schema: 3.25.2(zod@4.4.3)
+ optionalDependencies:
+ '@opentelemetry/api': 1.9.0
+ transitivePeerDependencies:
+ - bufferutil
+ - utf-8-validate
+
+ '@modelcontextprotocol/client@2.0.0':
+ dependencies:
+ '@modelcontextprotocol/core': 2.0.0
+ cross-spawn: 7.0.6
+ eventsource: 3.0.7
+ eventsource-parser: 3.1.0
+ jose: 6.2.4
+ pkce-challenge: 5.0.1
+ zod: 4.4.3
+
+ '@modelcontextprotocol/core@2.0.0':
+ dependencies:
+ zod: 4.4.3
+
'@modelcontextprotocol/ext-apps@1.7.5(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(react-dom@18.3.1(react@19.2.3))(react@19.2.3)(zod@4.4.3)':
dependencies:
'@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3)
@@ -10836,8 +11032,9 @@ snapshots:
'@smithy/core@3.30.0':
dependencies:
- '@smithy/types': 4.16.1
+ '@smithy/types': 4.17.0
tslib: 2.8.1
+ optional: true
'@smithy/core@3.32.0':
dependencies:
@@ -10874,8 +11071,8 @@ snapshots:
'@smithy/node-http-handler@4.7.3':
dependencies:
- '@smithy/core': 3.30.0
- '@smithy/types': 4.16.1
+ '@smithy/core': 3.32.0
+ '@smithy/types': 4.17.0
tslib: 2.8.1
'@smithy/signature-v4@5.7.0':
@@ -10884,10 +11081,6 @@ snapshots:
'@smithy/types': 4.17.0
tslib: 2.8.1
- '@smithy/types@4.16.1':
- dependencies:
- tslib: 2.8.1
-
'@smithy/types@4.17.0':
dependencies:
tslib: 2.8.1
@@ -11201,6 +11394,10 @@ snapshots:
'@ungap/structured-clone@1.3.3': {}
+ '@valibot/to-json-schema@1.7.1(valibot@1.4.2(typescript@5.9.3))':
+ dependencies:
+ valibot: 1.4.2(typescript@5.9.3)
+
'@vercel/detect-agent@1.2.3': {}
'@vercel/functions@1.6.0(@aws-sdk/credential-provider-web-identity@3.972.74)':
@@ -13606,6 +13803,10 @@ snapshots:
dependencies:
argparse: 2.0.1
+ js-yaml@5.3.0:
+ dependencies:
+ argparse: 2.0.1
+
jsep@1.4.0: {}
jsesc@3.1.0: {}
@@ -13680,6 +13881,8 @@ snapshots:
kind-of@6.0.3: {}
+ layerr@3.0.0: {}
+
lazystream@1.0.1:
dependencies:
readable-stream: 2.3.8
@@ -14645,6 +14848,11 @@ snapshots:
transitivePeerDependencies:
- encoding
+ openai@6.26.0(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3):
+ optionalDependencies:
+ ws: 8.21.0(bufferutil@4.1.0)
+ zod: 4.4.3
+
openai@6.40.0(ws@8.21.0(bufferutil@4.1.0))(zod@4.4.3):
optionalDependencies:
ws: 8.21.0(bufferutil@4.1.0)
@@ -16326,6 +16534,10 @@ snapshots:
typescript@5.9.3: {}
+ ulidx@2.4.1:
+ dependencies:
+ layerr: 3.0.0
+
unbox-primitive@1.1.0:
dependencies:
call-bound: 1.0.4
@@ -16473,6 +16685,10 @@ snapshots:
uuid@9.0.1: {}
+ valibot@1.4.2(typescript@5.9.3):
+ optionalDependencies:
+ typescript: 5.9.3
+
vary@1.1.2: {}
vfile-location@5.0.3:
@@ -16525,6 +16741,34 @@ snapshots:
tsx: 4.23.1
yaml: 2.9.0
+ vitest@4.1.9(@opentelemetry/api@1.9.0)(@types/node@24.13.2)(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)):
+ dependencies:
+ '@vitest/expect': 4.1.9
+ '@vitest/mocker': 4.1.9(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0))
+ '@vitest/pretty-format': 4.1.9
+ '@vitest/runner': 4.1.9
+ '@vitest/snapshot': 4.1.9
+ '@vitest/spy': 4.1.9
+ '@vitest/utils': 4.1.9
+ es-module-lexer: 2.3.0
+ expect-type: 1.4.0
+ magic-string: 0.30.21
+ obug: 2.1.3
+ pathe: 2.0.3
+ picomatch: 4.0.5
+ std-env: 4.1.0
+ tinybench: 2.9.0
+ tinyexec: 1.2.4
+ tinyglobby: 0.2.17
+ tinyrainbow: 3.1.0
+ vite: 8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)
+ why-is-node-running: 2.3.0
+ optionalDependencies:
+ '@opentelemetry/api': 1.9.0
+ '@types/node': 24.13.2
+ transitivePeerDependencies:
+ - msw
+
vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.13.2)(vite@8.1.3(@types/node@24.13.2)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.23.1)(yaml@2.9.0)):
dependencies:
'@vitest/expect': 4.1.9
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 36a3d2ff2..bc2d7158e 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -33,11 +33,14 @@ catalog:
"@openai/codex-sdk": 0.147.0
"@anthropic-ai/claude-agent-sdk": 0.3.224
typebox: 1.3.7
+ "@earendil-works/pi-ai": 0.83.0
"@earendil-works/pi-coding-agent": 0.84.2
"@mastra/core": ^1.55.0
"@mastra/mcp": ^1.15.0
"@ai-sdk/mcp": 2.0.22
eve: 0.29.4
+ "@flue/runtime": 2.0.3
+ valibot: ^1.4.0
"@ai-sdk/provider": ^4.0.2
"@ai-sdk/openai": ^4.0.8
"@ai-sdk/anthropic": ^4.0.8
diff --git a/turbo.json b/turbo.json
index 32b50787d..e5cd3101e 100644
--- a/turbo.json
+++ b/turbo.json
@@ -99,6 +99,9 @@
"@browserbasehq/stagehand-integrations-example-codex-facade#typecheck": {
"dependsOn": ["^build"]
},
+ "@browserbasehq/stagehand-integrations-example-flue-facade#typecheck": {
+ "dependsOn": ["^build"]
+ },
"@browserbasehq/stagehand-docs#typecheck": {},
"test:unit": {
"dependsOn": ["^build"],