Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
c2ed35d
feat(langchain): add Stagehand code-mode MCP example
shrey150 Aug 6, 2026
d57bce1
Merge updated CrewAI integration parent
shrey150 Aug 6, 2026
9c819cd
Merge updated CrewAI integration parent
shrey150 Aug 7, 2026
c9fa671
Merge updated CrewAI integration parent
shrey150 Aug 7, 2026
ced3e2c
Merge refreshed CrewAI integration parent
shrey150 Aug 7, 2026
3f6107f
Merge remote-tracking branch 'origin/shrey/stg-2765-codemode-crewai' …
shrey150 Aug 8, 2026
8d9cc5f
docs: sandbox the Deep Agents code-mode MCP
shrey150 Aug 8, 2026
2e20aa4
Merge remote-tracking branch 'origin/shrey/stg-2765-codemode-crewai' …
shrey150 Aug 8, 2026
0c89f77
fix: preserve CI Chrome launch settings
shrey150 Aug 8, 2026
da13c9b
Merge remote-tracking branch 'origin/shrey/stg-2765-codemode-crewai' …
shrey150 Aug 8, 2026
a16cb8a
fix: filter Deep Agents bridge overrides
shrey150 Aug 8, 2026
2a6d8cc
Merge remote-tracking branch 'origin/shrey/stg-2765-codemode-crewai' …
shrey150 Aug 8, 2026
0b5bccf
refactor(langchain): use sandboxed code-mode MCP
shrey150 Aug 8, 2026
095b04b
Merge latest CrewAI parent into LangChain
shrey150 Aug 8, 2026
0d5c95a
fix(langchain): gate live proof on credentials
shrey150 Aug 8, 2026
e6cff47
Merge latest CrewAI review fixes into LangChain
shrey150 Aug 8, 2026
c29ce4a
fix(langchain): harden session and agent contracts
shrey150 Aug 8, 2026
2fe3165
Merge latest CrewAI review fixes into LangChain
shrey150 Aug 8, 2026
fddca9e
Merge latest CrewAI contract tests into LangChain
shrey150 Aug 8, 2026
a05413b
fix(langchain): classify session cleanup failures
shrey150 Aug 8, 2026
dde6fd4
Merge latest CrewAI lifecycle fixes into LangChain
shrey150 Aug 8, 2026
1dbce0b
Merge deterministic CrewAI stack test into LangChain
shrey150 Aug 8, 2026
c91a195
Merge latest CrewAI review fixes into LangChain
shrey150 Aug 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/codemode-framework-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,65 @@ jobs:
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

langchain:
name: LangChain Deep Agents
if: >-
github.event_name == 'push' ||
github.event.pull_request.head.repo.full_name == github.repository ||
contains(github.event.pull_request.labels.*.name, 'safe-to-test')
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0

- uses: ./.github/actions/setup-node-pnpm
with:
use-prebuilt-artifacts: "false"

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
with:
python-version: "3.12"
cache: pip
cache-dependency-path: packages/integrations/examples/langchain/requirements.txt

- run: python -m pip install -r packages/integrations/examples/langchain/requirements.txt
- run: >-
python -m py_compile
packages/integrations/examples/shared/vercel_sandbox_lease.py
packages/integrations/examples/shared/test_vercel_sandbox_lease.py
packages/integrations/examples/langchain/agent.py
packages/integrations/examples/langchain/sandbox.py
packages/integrations/examples/langchain/test_agent.py
packages/integrations/examples/langchain/test_e2e.py
packages/integrations/examples/langchain/e2e.py
- run: python packages/integrations/examples/shared/test_vercel_sandbox_lease.py
- run: python packages/integrations/examples/langchain/test_agent.py
- run: python packages/integrations/examples/langchain/test_e2e.py
- run: pnpm --filter @browserbasehq/stagehand-integrations-example-vercel-sandbox pack:artifacts
- name: Detect LangChain live test credentials
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
id: langchain-live-credentials
uses: ./.github/actions/detect-codemode-live-credentials
with:
require-openai: "true"
env:
BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }}
BROWSERBASE_PROJECT_ID: ${{ secrets.BROWSERBASE_PROJECT_ID }}
VERCEL_OIDC_TOKEN: ${{ secrets.VERCEL_OIDC_TOKEN }}
VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Run LangChain live sandbox proof
if: steps.langchain-live-credentials.outputs.available == 'true'
run: python packages/integrations/examples/langchain/e2e.py
env:
STAGEHAND_SANDBOX_ARTIFACTS: ${{ github.workspace }}/packages/integrations/examples/vercel-sandbox/.artifacts
BROWSERBASE_API_KEY: ${{ secrets.BROWSERBASE_API_KEY }}
BROWSERBASE_PROJECT_ID: ${{ secrets.BROWSERBASE_PROJECT_ID }}
VERCEL_OIDC_TOKEN: ${{ secrets.VERCEL_OIDC_TOKEN }}
VERCEL_TEAM_ID: ${{ secrets.VERCEL_TEAM_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
2 changes: 2 additions & 0 deletions packages/integrations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,5 @@ implementation modules and an in-process arbitrary-code executor are not public
- [Mastra](./examples/mastra) consumes that connection with one persistent remote MCP client.
- [CrewAI](./examples/crewai) keeps its context-managed MCP adapter open across every tool call in
one crew execution.
- [LangChain Deep Agents](./examples/langchain) uses one explicit MCP session so every tool call
reaches the same browser.
75 changes: 75 additions & 0 deletions packages/integrations/examples/langchain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# LangChain Deep Agents with Stagehand code mode

This example gives a Deep Agent one browser tool, `code_execute`, without running generated
JavaScript in the Python agent process. A small trusted Python lease owner starts the
package-installed [Vercel Sandbox example](../vercel-sandbox) and receives its `{ url, token }`
connection. LangChain then connects directly through authenticated Streamable HTTP:

```text
Deep Agent -> persistent LangChain MCP session -> authenticated HTTPS -> Vercel Sandbox
`-> Stagehand MCP
`-> generated JavaScript
```

The integration discovers the package's canonical tool description and uses it as the agent system
prompt. It does not copy the executor, schema, or code-mode skill.

## Setup

Create a Python 3.12 environment and install the exact tested dependencies:

```bash
python3.12 -m venv .venv
. .venv/bin/activate
python -m pip install -r packages/integrations/examples/langchain/requirements.txt
```

Build and pack the exact Stagehand packages under review:

```bash
pnpm install
pnpm --filter @browserbasehq/stagehand-integrations-example-vercel-sandbox pack:artifacts
```

## Run the end-to-end proof

```bash
STAGEHAND_SANDBOX_ARTIFACTS="$PWD/packages/integrations/examples/vercel-sandbox/.artifacts" \
BROWSERBASE_API_KEY=<api-key> \
BROWSERBASE_PROJECT_ID=<project-id> \
VERCEL_OIDC_TOKEN=<oidc-token> \
OPENAI_API_KEY=<openai-key> \
python packages/integrations/examples/langchain/e2e.py
```

For external CI, replace `VERCEL_OIDC_TOKEN` with `VERCEL_TEAM_ID`, `VERCEL_PROJECT_ID`, and
`VERCEL_TOKEN`. `STAGEHAND_LANGCHAIN_MODEL` selects the host model for the standalone agent and
defaults to `openai:gpt-5-mini`.

The proof uses one live package-installed sandbox and one explicit LangChain MCP session. It:

1. invokes `code_execute` twice directly and requires the same page ID and DOM marker;
2. requires a real Deep Agent model to select `code_execute` inside that same session;
3. invokes the tool again to independently verify the model's browser-side change;
4. proves the model key and a host-only marker are absent inside generated code; and
5. closes the LangChain MCP session before ending the sandbox lease, emitting `PASS` only afterward.

The explicit `client.session("stagehand")` context and `load_mcp_tools(session)` call are important.
LangChain's convenience tool loader is stateless by default and would otherwise create a fresh MCP
session for each invocation, losing Stagehand's browser state.

## Run a Deep Agent

```bash
STAGEHAND_SANDBOX_ARTIFACTS="$PWD/packages/integrations/examples/vercel-sandbox/.artifacts" \
BROWSERBASE_API_KEY=<api-key> \
BROWSERBASE_PROJECT_ID=<project-id> \
VERCEL_OIDC_TOKEN=<oidc-token> \
OPENAI_API_KEY=<openai-key> \
python packages/integrations/examples/langchain/agent.py \
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
"Open https://example.com and return its title and URL."
```

The lease subprocess receives only Browserbase, Vercel, artifact, and runtime variables from its
allowlist. Outer model-provider credentials are intentionally excluded, and the sandbox foundation
brokers the Browserbase credential at its egress boundary.
213 changes: 213 additions & 0 deletions packages/integrations/examples/langchain/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
from __future__ import annotations

import asyncio
import os
import sys
from builtins import BaseExceptionGroup
from collections.abc import AsyncIterator, Sequence
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any, Protocol

from deepagents import create_deep_agent
from langchain_core.tools import BaseTool
from langchain_mcp_adapters.client import MultiServerMCPClient
from langchain_mcp_adapters.tools import load_mcp_tools
from mcp import ClientSession

DEFAULT_STAGEHAND_MODEL = "openai:gpt-5-mini"


class StagehandLangChainConnectionError(RuntimeError):
"""LangChain could not establish the authenticated Stagehand MCP connection."""


class StagehandLangChainToolContractError(RuntimeError):
"""The remote Stagehand MCP tool contract was not the expected code-mode API."""


class StagehandLangChainCleanupError(RuntimeError):
"""LangChain could not close its Stagehand MCP session."""


class StagehandLangChainSetupError(RuntimeError):
"""LangChain could not construct the Stagehand browser agent."""


class StagehandLangChainRunError(RuntimeError):
"""The LangChain Stagehand browser run failed."""


class StagehandSandboxConnection(Protocol):
url: str
token: str


def create_stagehand_mcp_client(
connection: StagehandSandboxConnection,
) -> MultiServerMCPClient:
"""Create a native authenticated Streamable HTTP client."""
return MultiServerMCPClient(
{
"stagehand": {
"transport": "streamable_http",
"url": connection.url,
"headers": {"Authorization": f"Bearer {connection.token}"},
}
},
tool_name_prefix=False,
handle_tool_errors=True,
)


async def load_stagehand_code_tool(session: ClientSession) -> BaseTool:
"""Discover the one canonical tool without creating another MCP session."""
try:
tools = await load_mcp_tools(session)
except Exception: # noqa: BLE001 -- MCP discovery is an untyped boundary.
raise StagehandLangChainConnectionError(
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
"Could not discover the Stagehand MCP tool."
) from None
tool_names = [tool.name for tool in tools]
if tool_names != ["code_execute"]:
raise StagehandLangChainToolContractError(
"The Stagehand MCP server returned an invalid tool contract."
)
if "# Stagehand V4 code-mode syntax" not in tools[0].description:
raise StagehandLangChainToolContractError(
"The Stagehand MCP server returned an invalid tool contract."
)
return tools[0]


@asynccontextmanager
async def stagehand_code_session(
connection: StagehandSandboxConnection,
) -> AsyncIterator[BaseTool]:
"""Keep one remote MCP session open for every tool call in an agent run."""
try:
client = create_stagehand_mcp_client(connection)
except Exception: # noqa: BLE001 -- client construction is an untyped boundary.
raise StagehandLangChainConnectionError(
"Could not connect LangChain to the Stagehand MCP server."
) from None

primary_error: BaseException | None = None
session_entered = False
try:
async with client.session("stagehand") as session:
session_entered = True
try:
code_tool = await load_stagehand_code_tool(session)
yield code_tool
except BaseException as error:
primary_error = error
raise
except (
StagehandLangChainConnectionError,
StagehandLangChainToolContractError,
):
raise
except BaseException as error:
if primary_error is not None:
if error is primary_error:
raise
raise BaseExceptionGroup(
"LangChain run and MCP cleanup both failed",
[
primary_error,
StagehandLangChainCleanupError(
"Could not close the LangChain Stagehand MCP session."
),
],
) from None
if not isinstance(error, Exception):
raise
if session_entered:
raise StagehandLangChainCleanupError(
"Could not close the LangChain Stagehand MCP session."
) from None
raise StagehandLangChainConnectionError(
"Could not connect LangChain to the Stagehand MCP server."
) from None


def build_stagehand_agent(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The newly added agent-construction and session-scoping paths (build_stagehand_agent, run_stagehand_agent, stagehand_code_session) have no focused unit tests — the existing tests cover only the client shape, tool-description validation, and the tool-call counter. These paths are currently validated only through the live, model-dependent e2e. Adding focused tests that verify build_stagehand_agent passes the tool description as the system prompt, rejects non-code_execute tools, and that stagehand_code_session scopes the loaded tool to one session would encode the contract and protect against regressions independently of the flaky real-model run.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/integrations/examples/langchain/agent.py, line 63:

<comment>The newly added agent-construction and session-scoping paths (`build_stagehand_agent`, `run_stagehand_agent`, `stagehand_code_session`) have no focused unit tests — the existing tests cover only the client shape, tool-description validation, and the tool-call counter. These paths are currently validated only through the live, model-dependent e2e. Adding focused tests that verify `build_stagehand_agent` passes the tool description as the system prompt, rejects non-`code_execute` tools, and that `stagehand_code_session` scopes the loaded tool to one session would encode the contract and protect against regressions independently of the flaky real-model run.</comment>

<file context>
@@ -3,120 +3,105 @@
+        yield await load_stagehand_code_tool(session)
+
+
+def build_stagehand_agent(
+    code_tool: BaseTool,
+    model: str | Any = DEFAULT_STAGEHAND_MODEL,
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in c29ce4a8. The focused suite now verifies canonical tool-description → system-prompt wiring, rejects non-code_execute tools without reflecting remote names, proves one named MCP session owns the loaded tool, and exercises run_stagehand_agent input/config wiring. The exact pinned environment passes 9/9 agent/session tests.

code_tool: BaseTool,
model: str | Any = DEFAULT_STAGEHAND_MODEL,
) -> Any:
if code_tool.name != "code_execute":
raise StagehandLangChainToolContractError(
"The Stagehand MCP server returned an invalid tool contract."
)
try:
return create_deep_agent(
model=model,
tools=[code_tool],
system_prompt=code_tool.description,
)
except Exception: # noqa: BLE001 -- Deep Agent construction is untyped.
raise StagehandLangChainSetupError(
"Could not configure the LangChain Stagehand agent."
) from None


def stagehand_tool_call_count(result: dict[str, Any]) -> int:
"""Count model-selected code_execute calls in a LangGraph result."""
count = 0
for message in result.get("messages", []):
for call in getattr(message, "tool_calls", []):
name = (
call.get("name")
if isinstance(call, dict)
else getattr(call, "name", None)
)
if name == "code_execute":
count += 1
return count


async def run_stagehand_agent(
connection: StagehandSandboxConnection,
prompt: str,
model: str | Any = DEFAULT_STAGEHAND_MODEL,
) -> dict[str, Any]:
async with stagehand_code_session(connection) as code_tool:
try:
return await build_stagehand_agent(code_tool, model).ainvoke(
{"messages": [{"role": "user", "content": prompt}]},
config={"recursion_limit": 20},
)
except (
StagehandLangChainSetupError,
StagehandLangChainToolContractError,
):
raise
except Exception: # noqa: BLE001 -- Deep Agent execution is untyped.
raise StagehandLangChainRunError(
"The LangChain Stagehand agent run failed."
) from None


def _last_message_text(result: dict[str, Any]) -> str:
messages: Sequence[Any] = result.get("messages", [])
if not messages:
return str(result)
content = getattr(messages[-1], "content", messages[-1])
return content if isinstance(content, str) else str(content)


async def _main() -> None:
if len(sys.argv) < 2:
raise SystemExit(f'Usage: {Path(sys.argv[0]).name} "<browser task>"')
from sandbox import StagehandSandboxLease

prompt = " ".join(sys.argv[1:])
model = os.environ.get("STAGEHAND_LANGCHAIN_MODEL", DEFAULT_STAGEHAND_MODEL)
with StagehandSandboxLease() as connection:
result = await run_stagehand_agent(connection, prompt, model=model)
print(_last_message_text(result))


if __name__ == "__main__":
asyncio.run(_main())
Loading
Loading