This commit is contained in:
Harrison Chase 2026-03-07 13:24:22 -08:00
parent 5c493ea1c7
commit da5c0f36f6
2 changed files with 753 additions and 0 deletions

437
CUSTOMIZATION.md Normal file
View file

@ -0,0 +1,437 @@
# Customization Guide
Open SWE is designed to be forked and customized for your org. The core agent is assembled in a single function — `get_agent()` in `agent/server.py` — where you can swap out the sandbox, model, tools, and triggers.
```python
# agent/server.py — the key lines
return create_deep_agent(
model=make_model("anthropic:claude-opus-4-6", temperature=0, max_tokens=20_000),
system_prompt=construct_system_prompt(repo_dir, ...),
tools=[http_request, fetch_url, commit_and_open_pr, linear_comment, slack_thread_reply],
backend=sandbox_backend,
middleware=[
ToolErrorMiddleware(),
check_message_queue_before_model,
ensure_no_empty_msg,
open_pr_if_needed,
],
)
```
---
## 1. Sandbox
By default, Open SWE runs each task in a [LangSmith cloud sandbox](https://docs.smith.langchain.com/) — an isolated Linux environment where the agent clones the repo and executes commands. Sandbox creation and connection is handled in `agent/integrations/langsmith.py`.
### Using a custom sandbox template
Set environment variables to use a custom Docker image:
```bash
DEFAULT_SANDBOX_TEMPLATE_NAME="my-template" # Template registered in LangSmith
DEFAULT_SANDBOX_TEMPLATE_IMAGE="my-org/my-image:latest" # Docker image
```
This is useful for pre-installing languages, frameworks, or internal tools that your repos depend on — reducing setup time per agent run.
### Using a different sandbox provider
The `deepagents` ecosystem includes several sandbox providers out of the box. To swap providers, replace the `create_langsmith_sandbox()` call in `agent/server.py` with one of the following:
#### Modal
```bash
pip install langchain-modal
```
```python
import modal
from langchain_modal import ModalSandbox
app = modal.App.lookup("open-swe")
sandbox_backend = ModalSandbox(sandbox=modal.Sandbox.create(app=app))
```
This is what Ramp uses for their Inspect agent — container-based isolation with fast spin-up.
#### Daytona
```bash
pip install langchain-daytona
```
```python
from daytona import Daytona
from langchain_daytona import DaytonaSandbox
sandbox = Daytona().create()
sandbox_backend = DaytonaSandbox(sandbox=sandbox)
```
#### Runloop
```bash
pip install langchain-runloop
```
```python
import os
from runloop_api_client import RunloopSDK
from langchain_runloop import RunloopSandbox
client = RunloopSDK(bearer_token=os.environ["RUNLOOP_API_KEY"])
devbox = client.devbox.create()
sandbox_backend = RunloopSandbox(devbox=devbox)
```
#### Local shell (no isolation — development only)
```python
from deepagents.backends import LocalShellBackend
sandbox_backend = LocalShellBackend(
root_dir="/path/to/repo",
inherit_env=True,
)
```
> **Warning**: `LocalShellBackend` runs commands directly on your host machine with no sandboxing. Only use for local development with human-in-the-loop enabled.
#### Wiring it up
All providers implement `SandboxBackendProtocol` and are interchangeable. Replace the sandbox creation in `agent/server.py`:
```python
# Before (LangSmith)
sandbox_backend = await asyncio.to_thread(create_langsmith_sandbox)
# After (any provider)
sandbox_backend = await asyncio.to_thread(create_my_sandbox)
```
### Building a custom sandbox provider
If none of the built-in providers fit, you can build your own. The agent accepts any backend that implements `SandboxBackendProtocol` from `deepagents`. The protocol requires:
- **File operations**: `ls_info()`, `read()`, `write()`, `edit()`, `glob_info()`, `grep_raw()`
- **Shell execution**: `execute(command, timeout=None) -> ExecuteResponse`
- **Identity**: `id` property returning a unique sandbox identifier
The easiest approach is to extend `BaseSandbox` from `deepagents.backends.sandbox` — it implements all file operations by delegating to `execute()`, so you only need to implement the shell execution layer:
```python
from deepagents.backends.sandbox import BaseSandbox
from deepagents.backends.protocol import ExecuteResponse
class MySandbox(BaseSandbox):
def __init__(self, connection):
self._conn = connection
@property
def id(self) -> str:
return self._conn.id
def execute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse:
result = self._conn.run(command, timeout=timeout or 300)
return ExecuteResponse(
output=result.stdout + result.stderr,
exit_code=result.exit_code,
truncated=False,
)
```
See `agent/integrations/langsmith.py` (`LangSmithBackend` class) for a full reference implementation.
---
## 2. Model
The model is configured in the `get_agent()` function in `agent/server.py`:
```python
model=make_model("anthropic:claude-opus-4-6", temperature=0, max_tokens=20_000)
```
### Switching models
Use the `provider:model` format:
```python
# Anthropic
model=make_model("anthropic:claude-sonnet-4-6", temperature=0, max_tokens=16_000)
# OpenAI (uses Responses API by default)
model=make_model("openai:gpt-4o", temperature=0, max_tokens=16_000)
# Google
model=make_model("google_genai:gemini-2.5-pro", temperature=0, max_tokens=16_000)
```
The `make_model()` helper in `agent/utils/model.py` wraps `langchain.chat_models.init_chat_model`. For OpenAI models, it automatically enables the Responses API. For full control, pass a pre-configured model instance directly:
```python
from langchain_anthropic import ChatAnthropic
model = ChatAnthropic(model_name="claude-sonnet-4-6", temperature=0, max_tokens=16_000)
return create_deep_agent(
model=model,
...
)
```
### Using different models per context
You can route to different models based on task complexity, repo, or trigger source:
```python
async def get_agent(config: RunnableConfig) -> Pregel:
source = config["configurable"].get("source")
if source == "slack":
# Faster model for Slack Q&A
model = make_model("anthropic:claude-sonnet-4-6", temperature=0, max_tokens=16_000)
else:
# Full model for code changes from Linear
model = make_model("anthropic:claude-opus-4-6", temperature=0, max_tokens=20_000)
return create_deep_agent(model=model, ...)
```
---
## 3. Tools
Open SWE ships with five custom tools on top of the built-in Deep Agents tools (file operations, shell execution, subagents, todos):
| Tool | File | Purpose |
|---|---|---|
| `commit_and_open_pr` | `agent/tools/commit_and_open_pr.py` | Git commit + GitHub draft PR |
| `fetch_url` | `agent/tools/fetch_url.py` | Fetch web pages as markdown |
| `http_request` | `agent/tools/http_request.py` | HTTP API calls |
| `linear_comment` | `agent/tools/linear_comment.py` | Post comments on Linear tickets |
| `slack_thread_reply` | `agent/tools/slack_thread_reply.py` | Reply in Slack threads |
### Adding a tool
Create a new file in `agent/tools/`, define a function, and add it to the tools list.
**Example — adding a Datadog search tool:**
```python
# agent/tools/datadog_search.py
import requests
from typing import Any
def datadog_search(query: str, time_range: str = "1h") -> dict[str, Any]:
"""Search Datadog logs for debugging context.
Args:
query: Datadog log query string
time_range: Time range to search (e.g. "1h", "24h", "7d")
Returns:
Dictionary with matching log entries
"""
# Your Datadog API integration here
...
```
Then register it in `agent/server.py`:
```python
from .tools import commit_and_open_pr, fetch_url, http_request, linear_comment, slack_thread_reply
from .tools.datadog_search import datadog_search
return create_deep_agent(
...
tools=[
http_request, fetch_url, commit_and_open_pr,
linear_comment, slack_thread_reply,
datadog_search, # new tool
],
...
)
```
The agent will automatically see the tool's name, docstring, and parameter types — the docstring serves as the tool description, so write it clearly.
### Removing tools
If you only use Linear (not Slack), remove `slack_thread_reply` from the tools list and vice versa. If you don't need web fetching, remove `fetch_url`. The only tool that's essential to the core workflow is `commit_and_open_pr`.
### Conditional tools
You can vary the toolset based on the trigger source:
```python
base_tools = [http_request, fetch_url, commit_and_open_pr]
source = config["configurable"].get("source")
if source == "linear":
tools = [*base_tools, linear_comment]
elif source == "slack":
tools = [*base_tools, slack_thread_reply]
else:
tools = [*base_tools, linear_comment, slack_thread_reply]
return create_deep_agent(tools=tools, ...)
```
---
## 4. Triggers
Open SWE supports three invocation surfaces: Linear, Slack, and GitHub. Each is implemented as a webhook endpoint in `agent/webapp.py`. You can add, remove, or modify triggers independently.
### Removing a trigger
If you don't use Linear, simply don't configure the Linear webhook and remove the env vars. Same for Slack. The webhook endpoints still exist but won't receive events.
To fully remove a trigger's code, delete the corresponding endpoint from `agent/webapp.py`:
- **Linear**: `linear_webhook()` and `process_linear_issue()`
- **Slack**: `slack_webhook()` and `process_slack_mention()`
### Customizing Linear routing
The `LINEAR_TEAM_TO_REPO` dict in `agent/webapp.py` maps Linear teams and projects to GitHub repos:
```python
LINEAR_TEAM_TO_REPO = {
"Engineering": {
"projects": {
"backend": {"owner": "my-org", "name": "backend"},
"frontend": {"owner": "my-org", "name": "frontend"},
},
"default": {"owner": "my-org", "name": "monorepo"},
},
}
```
### Customizing Slack routing
Slack uses env vars for default routing:
```bash
SLACK_REPO_OWNER="my-org"
SLACK_REPO_NAME="my-repo"
```
Users can override per-message with `repo:owner/name` syntax in their Slack message.
### Adding a new trigger
To add a new invocation surface (e.g. Jira, Discord, a custom API):
1. **Add a webhook endpoint** in `agent/webapp.py`:
```python
@app.post("/webhooks/my-trigger")
async def my_trigger_webhook(request: Request, background_tasks: BackgroundTasks):
# Parse the incoming event
payload = await request.json()
# Extract task description and repo info
task_description = payload["description"]
repo_config = {"owner": "my-org", "name": "my-repo"}
# Create a LangGraph run
background_tasks.add_task(process_my_trigger, task_description, repo_config)
return {"status": "accepted"}
```
2. **Create a processing function** that builds the prompt and starts an agent run:
```python
async def process_my_trigger(task_description: str, repo_config: dict):
thread_id = generate_deterministic_id(task_description)
langgraph_client = get_client(url=LANGGRAPH_URL)
await langgraph_client.runs.create(
thread_id,
"agent",
input={"messages": [{"role": "user", "content": task_description}]},
config={"configurable": {
"repo": repo_config,
"source": "my-trigger",
"user_email": "user@example.com",
}},
if_not_exists="create",
)
```
3. **Add a communication tool** (optional) so the agent can report back:
```python
# agent/tools/my_trigger_reply.py
def my_trigger_reply(message: str) -> dict:
"""Post a reply to the triggering service."""
# Your API call here
...
```
The key fields in `config.configurable` are:
- `repo`: `{"owner": "...", "name": "..."}` — which GitHub repo to work on
- `source`: string identifying the trigger (used for auth routing and communication)
- `user_email`: the triggering user's email (for GitHub OAuth resolution)
---
## 5. System prompt
The system prompt is assembled in `agent/prompt.py` from modular sections. You can customize behavior by editing individual sections:
| Section | What it controls |
|---|---|
| `WORKING_ENV_SECTION` | Sandbox paths and execution constraints |
| `TASK_EXECUTION_SECTION` | Workflow steps (understand → implement → verify → submit) |
| `CODING_STANDARDS_SECTION` | Code style, testing, and quality rules |
| `COMMIT_PR_SECTION` | PR title/body format and commit conventions |
| `CODE_REVIEW_GUIDELINES_SECTION` | How the agent reviews code changes |
| `COMMUNICATION_SECTION` | Formatting and messaging guidelines |
### Using AGENTS.md
Drop an `AGENTS.md` file in the root of any repository to add repo-specific instructions. The agent reads it from the sandbox at startup and appends it to the system prompt. This is the easiest way to encode conventions per-repo without modifying Open SWE's code.
---
## 6. Middleware
Middleware hooks run around the agent loop. Open SWE includes four:
| Middleware | Type | Purpose |
|---|---|---|
| `ToolErrorMiddleware` | Tool error handler | Catches and formats tool errors |
| `check_message_queue_before_model` | Before model | Injects follow-up messages that arrived mid-run |
| `ensure_no_empty_msg` | Before model | Prevents empty messages from reaching the model |
| `open_pr_if_needed` | After agent | Safety net — opens a PR if the agent didn't |
Add custom middleware by appending to the middleware list in `get_agent()`. See the [LangChain middleware docs](https://python.langchain.com/docs/concepts/agents/#middleware) for the `@before_model` and `@after_agent` decorators.
**Example — adding a CI check after agent completion:**
```python
from langchain.agents.middleware import AgentState, after_agent
from langgraph.runtime import Runtime
@after_agent
async def run_ci_check(state: AgentState, runtime: Runtime):
"""Run CI checks after the agent finishes."""
# Trigger your CI pipeline here
...
```
Then add it to the middleware list:
```python
middleware=[
ToolErrorMiddleware(),
check_message_queue_before_model,
ensure_no_empty_msg,
open_pr_if_needed,
run_ci_check, # new middleware
],
```

316
INSTALLATION.md Normal file
View file

@ -0,0 +1,316 @@
# Installation Guide
This guide walks you through setting up Open SWE end-to-end: local development, GitHub App creation, Linear and Slack webhooks, and production deployment.
## Prerequisites
- Python 3.11+
- [uv](https://docs.astral.sh/uv/) package manager
- [LangGraph CLI](https://langchain-ai.github.io/langgraph/cloud/reference/cli/)
- [ngrok](https://ngrok.com/) (for local development — exposes webhook endpoints to the internet)
## 1. Clone and install
```bash
git clone https://github.com/langchain-ai/open-swe.git
cd open-swe
uv sync
```
## 2. Create a GitHub App
Open SWE authenticates as a [GitHub App](https://docs.github.com/en/apps/creating-github-apps) to clone repos, push branches, and open PRs.
1. Go to **GitHub Settings** → **Developer settings** → **GitHub Apps** → **New GitHub App**
2. Fill in:
- **App name**: `open-swe` (or your preferred name)
- **Homepage URL**: any valid URL
- **Webhook URL**: `https://<your-ngrok-url>/webhooks/github` (you'll set this up in step 4)
- **Webhook secret**: generate with `openssl rand -hex 32` — save this for `GITHUB_WEBHOOK_SECRET`
3. Set permissions:
- **Repository permissions**:
- Contents: Read & write
- Pull requests: Read & write
- Issues: Read
- Metadata: Read-only
4. Under **Subscribe to events**, enable:
- Pull request review comment
- Issue comment
5. Click **Create GitHub App**
6. Note the **App ID** from the app settings page
7. Generate a **private key** (scroll down on the app page → **Generate a private key**). Save the `.pem` file contents.
8. **Install the app** on the repositories you want Open SWE to access:
- Go to your app's page → **Install App** → select your org/account → choose repositories
- Note the **Installation ID** from the URL after installation (e.g. `https://github.com/settings/installations/12345678` → `12345678`)
## 3. Set up LangSmith
Open SWE uses [LangSmith](https://smith.langchain.com/) for two things:
- **Tracing**: all agent runs are logged for debugging and observability
- **Sandboxes**: each task runs in an isolated LangSmith cloud sandbox
1. Create a [LangSmith account](https://smith.langchain.com/) if you don't have one
2. Go to **Settings** → **API Keys** → create a new API key
3. Save it as `LANGSMITH_API_KEY_PROD`
### GitHub OAuth (for user authentication)
Open SWE resolves GitHub tokens per-user via LangSmith's OAuth integration. This lets each user authenticate with their own GitHub account rather than sharing a single bot token.
You'll need these from your LangSmith workspace settings:
- `GITHUB_OAUTH_PROVIDER_ID` — the OAuth provider ID configured in LangSmith
- `X_SERVICE_AUTH_JWT_SECRET` — the service JWT secret for user token resolution
> **Note**: If these aren't configured, the agent will fall back to the GitHub App's installation token for all operations.
### Sandbox templates (optional)
You can configure a custom sandbox template for the agent's execution environment:
- `DEFAULT_SANDBOX_TEMPLATE_NAME` — name of a LangSmith sandbox template
- `DEFAULT_SANDBOX_TEMPLATE_IMAGE` — Docker image for the sandbox
If not set, the default LangSmith sandbox image is used.
## 4. Set up triggers
Open SWE can be triggered from Linear, Slack, or GitHub. Configure whichever invocation surfaces your team uses — you don't need all of them.
### Linear
Open SWE listens for Linear comments that mention `@openswe`.
**Create a webhook:**
1. In Linear, go to **Settings** → **API** → **Webhooks** → **New webhook**
2. Fill in:
- **Label**: `open-swe`
- **URL**: `https://<your-ngrok-url>/webhooks/linear`
- **Secret**: generate with `openssl rand -hex 32` — save this for `LINEAR_WEBHOOK_SECRET`
3. Under **Data change events**, enable **Comments** → `Create` only
4. Click **Create webhook**
**Get your API key:**
1. Go to **Settings** → **API** → **Personal API keys** → **New API key**
2. Name it `open-swe`, select **All access**, and copy the key
3. Save it as `LINEAR_API_KEY`
**Configure team-to-repo mapping:**
Open SWE routes Linear issues to GitHub repos based on the Linear team and project. The mapping is defined in `agent/webapp.py` in the `LINEAR_TEAM_TO_REPO` dict:
```python
LINEAR_TEAM_TO_REPO = {
"My Team": {"owner": "my-org", "name": "my-repo"},
"Engineering": {
"projects": {
"backend": {"owner": "my-org", "name": "backend"},
"frontend": {"owner": "my-org", "name": "frontend"},
},
"default": {"owner": "my-org", "name": "monorepo"},
},
}
```
- **Flat mapping**: team name → single repo
- **Nested mapping**: team name → project name → repo, with an optional `default` fallback
Update this to match your Linear workspace structure.
### Slack
**Create a Slack App:**
1. Go to [api.slack.com/apps](https://api.slack.com/apps) → **Create New App** → **From scratch**
2. Name it `open-swe` and select your workspace
**Configure OAuth & permissions:**
Under **OAuth & Permissions**, add these Bot Token Scopes:
- `app_mentions:read`
- `channels:history`
- `channels:read`
- `chat:write`
- `reactions:write`
- `users:read`
- `users:read.email`
Install the app to your workspace and copy the **Bot User OAuth Token** (`xoxb-...`).
**Configure event subscriptions:**
1. Under **Event Subscriptions**, enable events
2. Set the **Request URL** to `https://<your-ngrok-url>/webhooks/slack`
3. Subscribe to bot events:
- `app_mention`
- `message.channels` (if you want non-@ mentions to work with username matching)
4. Save changes
**Credentials you'll need:**
- `SLACK_BOT_TOKEN`: the Bot User OAuth Token (`xoxb-...`)
- `SLACK_SIGNING_SECRET`: found under **Basic Information** → **App Credentials**
- `SLACK_BOT_USER_ID`: the bot's user ID (find it in Slack by clicking the bot's profile)
- `SLACK_BOT_USERNAME`: the bot's display name (e.g. `open-swe`)
**Configure default repo:**
Slack messages are routed to a default repo unless the user specifies one with `repo:owner/name`:
```bash
SLACK_REPO_OWNER="my-org" # Default GitHub org
SLACK_REPO_NAME="my-repo" # Default GitHub repo
```
### GitHub
GitHub triggering works automatically once your GitHub App is set up (step 2). Tag `@openswe` in PR comments on agent-created PRs to have it address review feedback and push fixes to the same branch.
## 5. Environment variables
Create a `.env` file in the project root:
```bash
# === LangSmith ===
LANGSMITH_API_KEY_PROD="" # LangSmith API key
LANGCHAIN_TRACING_V2="true"
LANGCHAIN_PROJECT="" # LangSmith project name for traces
# === LLM ===
ANTHROPIC_API_KEY="" # Anthropic API key (default provider)
# === GitHub App ===
GITHUB_APP_ID="" # From step 2
GITHUB_APP_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
"
GITHUB_APP_INSTALLATION_ID="" # From step 2
# === GitHub Webhook ===
GITHUB_WEBHOOK_SECRET="" # openssl rand -hex 32
# === GitHub OAuth (via LangSmith) ===
GITHUB_OAUTH_PROVIDER_ID="" # Optional — LangSmith OAuth provider
X_SERVICE_AUTH_JWT_SECRET="" # Optional — service JWT secret
# === Linear ===
LINEAR_API_KEY="" # From step 4
LINEAR_WEBHOOK_SECRET="" # From step 4
# === Slack (optional) ===
SLACK_BOT_TOKEN="" # From step 4
SLACK_BOT_USER_ID=""
SLACK_BOT_USERNAME=""
SLACK_SIGNING_SECRET=""
SLACK_REPO_OWNER="" # Default org for Slack-triggered tasks
SLACK_REPO_NAME="" # Default repo for Slack-triggered tasks
# === Sandbox ===
DEFAULT_SANDBOX_TEMPLATE_NAME="" # Optional — custom sandbox template
DEFAULT_SANDBOX_TEMPLATE_IMAGE="" # Optional — custom Docker image
# === Token Encryption ===
TOKEN_ENCRYPTION_KEY="" # openssl rand -base64 32
```
## 6. Start the server (local development)
Start ngrok in one terminal to expose your local server:
In one terminal, expose your local server:
```bash
ngrok http 2024
```
Copy the HTTPS URL (e.g. `https://xxxx.ngrok.io`) and update your webhook URLs from step 4.
Then start the LangGraph server in another terminal:
```bash
uv run langgraph dev --no-browser
```
The server runs on `http://localhost:2024` with these endpoints:
| Endpoint | Purpose |
|---|---|
| `POST /webhooks/linear` | Linear comment webhooks |
| `GET /webhooks/linear` | Linear webhook verification |
| `POST /webhooks/slack` | Slack event webhooks |
| `GET /webhooks/slack` | Slack webhook verification |
| `GET /health` | Health check |
## 7. Verify it works
### Linear
1. Go to any Linear issue in a team you configured in `LINEAR_TEAM_TO_REPO`
2. Add a comment: `@openswe what files are in this repo?`
3. You should see:
- A 👀 reaction on your comment within a few seconds
- A new run in your LangSmith project
- The agent replies with a comment on the issue
### Slack
1. In any channel where the bot is invited, start a thread
2. Mention the bot: `@open-swe what's in the repo?`
3. You should see:
- An 👀 reaction on your message
- A reply in the thread with the agent's response
## 8. Production deployment
For production, deploy the agent on [LangGraph Cloud](https://langchain-ai.github.io/langgraph/cloud/) instead of running locally:
1. Push your code to a GitHub repository
2. Connect the repo to LangGraph Cloud
3. Set all environment variables from step 5 in the deployment config
4. Update your Linear and Slack webhook URLs to point to your production URL (replace the ngrok URL)
The `langgraph.json` at the project root already defines the graph entry point and HTTP app:
```json
{
"graphs": {
"agent": "agent.server:get_agent"
},
"http": {
"app": "agent.webapp:app"
}
}
```
## Troubleshooting
### Webhook not receiving events
- Verify ngrok is running and the URL matches what's configured in Linear/Slack
- Check the ngrok web inspector at `http://localhost:4040` for incoming requests
- Ensure you enabled the correct event types (Comments → Create for Linear, `app_mention` for Slack)
### GitHub authentication errors
- Verify `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`, and `GITHUB_APP_INSTALLATION_ID` are set correctly
- Ensure the GitHub App is installed on the target repositories
- Check that the private key includes the full `-----BEGIN RSA PRIVATE KEY-----` and `-----END RSA PRIVATE KEY-----` lines
### Sandbox creation failures
- Verify `LANGSMITH_API_KEY_PROD` is set and valid
- Check LangSmith sandbox quotas in your workspace settings
- If using a custom template, verify `DEFAULT_SANDBOX_TEMPLATE_NAME` matches an existing template
### Agent not responding to comments
- For Linear: ensure the comment contains `@openswe` (case-insensitive)
- For Slack: ensure the bot is invited to the channel and the message is an `@mention`
- Check server logs for webhook processing errors
### Token encryption errors
- Ensure `TOKEN_ENCRYPTION_KEY` is set (generate with `openssl rand -base64 32`)
- The key must be a valid 32-byte Fernet-compatible base64 string