mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 06:53:14 +00:00
docs: refresh AGENTS.md and CLAUDE.md for current architecture (#1327)
Bring the agent-facing docs back in sync with the codebase: the reviewer and review_style_analyzer graphs, the dashboard router and Agents UI, auto-review on PR opened/ready_for_review, the current middleware order in get_agent, model/profile/team-default resolution, and the leaner reviewer middleware stack. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
parent
44807bf9f9
commit
d02f4961d6
2 changed files with 130 additions and 44 deletions
87
AGENTS.md
87
AGENTS.md
|
|
@ -4,16 +4,18 @@ This file provides guidance to Coding Agents when working with code in this repo
|
||||||
|
|
||||||
## Project
|
## Project
|
||||||
|
|
||||||
Open SWE is an open-source coding-agent framework built on **LangGraph** + **Deep Agents** (`deepagents.create_deep_agent`). It runs as a LangGraph app: each thread spawns its own isolated cloud sandbox, and the agent is invoked from Slack, Linear, or GitHub PR comments.
|
Open SWE is an open-source coding-agent framework built on **LangGraph** + **Deep Agents** (`deepagents.create_deep_agent`). It runs as a LangGraph app: each thread spawns its own isolated cloud sandbox, and the agent is invoked from Slack, Linear, or GitHub (PR comments, plus auto-review on opened / ready-for-review).
|
||||||
|
|
||||||
|
A separate **reviewer** graph runs read-only code reviews on PRs, and a **review-style analyzer** graph learns per-repo review style from historical PRs.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
Dependencies are managed with **uv**. Tests use pytest (`asyncio_mode = "auto"`). Lint/format is **ruff** (line-length 100, target py311).
|
Dependencies are managed with **uv**. Tests use pytest (`asyncio_mode = "auto"`). Lint/format is **ruff** (line-length 100, target py311). `requires-python = ">=3.11"`; `langgraph.json` pins the runtime to 3.12.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make install # uv pip install -e .
|
make install # uv pip install -e .
|
||||||
make dev # langgraph dev — run the LangGraph dev server (graph defined in langgraph.json)
|
make dev # uv run langgraph dev — serves all three graphs + the FastAPI app from langgraph.json
|
||||||
make run # uvicorn agent.webapp:app --reload --port 8000 (webhook server only)
|
make run # uvicorn agent.webapp:app --reload --port 8000 (FastAPI only, no LangGraph runtime)
|
||||||
make test # uv run pytest -vvv tests/
|
make test # uv run pytest -vvv tests/
|
||||||
make test TEST_FILE=tests/test_open_pr_middleware.py # single test file
|
make test TEST_FILE=tests/test_open_pr_middleware.py # single test file
|
||||||
uv run pytest -vvv tests/test_open_pr_middleware.py::test_name # single test
|
uv run pytest -vvv tests/test_open_pr_middleware.py::test_name # single test
|
||||||
|
|
@ -21,53 +23,94 @@ make lint # ruff check + ruff format --diff
|
||||||
make format # ruff format + ruff check --fix
|
make format # ruff format + ruff check --fix
|
||||||
```
|
```
|
||||||
|
|
||||||
`langgraph.json` declares the graph entrypoint as `agent.server:get_agent` and the FastAPI app as `agent.webapp:app`. Both are served together by `langgraph dev`.
|
`langgraph.json` declares three graph entrypoints and the FastAPI app, all served together by `langgraph dev`:
|
||||||
|
|
||||||
|
| Graph | Entrypoint | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `agent` | `agent.server:get_agent` | Main coding agent (Slack/Linear/GitHub-triggered). |
|
||||||
|
| `reviewer` | `agent.reviewer:get_reviewer_agent` | Read-only PR reviewer. Findings model + `publish_review`. |
|
||||||
|
| `review_style_analyzer` | `agent.review_style_analyzer:get_review_style_analyzer` | Learns per-repo reviewer style from historical PRs. |
|
||||||
|
|
||||||
|
The FastAPI app is `agent.webapp:app`.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Two entrypoints, one process
|
### Entrypoints
|
||||||
|
|
||||||
- **`agent/server.py` → `get_agent(config)`** — the LangGraph graph factory. Called per-thread. Resolves the GitHub token, gets-or-creates the sandbox for the thread, then constructs a fresh `create_deep_agent(...)` with the full tool list and middleware stack. The agent itself is stateless — all per-thread state lives in the sandbox + thread metadata.
|
- **`agent/server.py` → `get_agent(config)`** — main graph factory. Called per-thread. Resolves the GitHub token, gets-or-creates the sandbox for the thread, resolves the team/profile/per-thread model + effort, then constructs a fresh `create_deep_agent(...)` with the curated tool list and middleware stack. The agent itself is stateless — all per-thread state lives in the sandbox + thread metadata.
|
||||||
- **`agent/webapp.py`** — custom FastAPI routes mounted alongside the LangGraph server. This is where webhooks land (GitHub, Linear, Slack). Each webhook resolves a deterministic `thread_id` (so follow-up messages route to the same agent run), then triggers/streams a run via the `langgraph_sdk` client.
|
- **`agent/reviewer.py` → `get_reviewer_agent(config)`** — reviewer graph factory. Shares `ensure_sandbox_for_thread` with the main agent but wires a reviewer-only toolset (`add_finding`, `update_finding`, `list_findings`, `publish_review`, `web_search`, `fetch_url`, `http_request`) and a different system prompt that pins the single-evolving-findings model and the diff-anchored bar for filing a finding. Read-only: no commit/push/PR-opening tools.
|
||||||
|
- **`agent/review_style_analyzer.py` → `get_review_style_analyzer(config)`** — small graph that reads historical PR reviews from a repo and emits a per-repo style prompt via the `save_review_style_prompt` tool. Output is consumed by the reviewer as a "repository-specific review style" appendix.
|
||||||
|
- **`agent/webapp.py`** — custom FastAPI routes mounted alongside the LangGraph server. Webhooks land here (GitHub, Linear, Slack). Each webhook resolves a deterministic `thread_id` (so follow-up messages route to the same agent run) and triggers/streams a run via the `langgraph_sdk` client. Also auto-reviews PRs on `opened` / `ready_for_review` events when the repo+author opt in.
|
||||||
|
- **`agent/dashboard/`** — `router` mounted under the FastAPI app at startup (`app.include_router(dashboard_router)`). Owns GitHub OAuth, per-user profiles, admin endpoints, team defaults, enabled-repo lists, review-style management, and the Agents chat thread API used by the UI in `ui/`.
|
||||||
|
|
||||||
### Sandbox lifecycle (the tricky part)
|
### Sandbox lifecycle (the tricky part)
|
||||||
|
|
||||||
`SANDBOX_BACKENDS` is an in-process dict keyed by `thread_id`. Thread metadata persists `sandbox_id` across processes. `get_agent` handles four cases:
|
`SANDBOX_BACKENDS` (in `agent/utils/sandbox_state.py`) is an in-process dict keyed by `thread_id`. Thread metadata persists `sandbox_id` across processes. `ensure_sandbox_for_thread` handles four cases:
|
||||||
|
|
||||||
1. Sandbox cached in memory → ping it (`echo ok`); recreate on `SandboxClientError`.
|
1. Sandbox cached in memory → ping it (`echo ok`); recreate on `SandboxClientError`. Healthy reused sandboxes also get a GitHub-proxy refresh (recreate on failure).
|
||||||
2. Metadata says `__creating__` and no cache → poll until ready (`_wait_for_sandbox_id`).
|
2. Metadata says `__creating__` and no cache → poll until ready (`_wait_for_sandbox_id`).
|
||||||
3. No sandbox at all → create one, set `__creating__` sentinel, then real id.
|
3. No sandbox at all → set `__creating__` sentinel, create one, persist the real id.
|
||||||
4. Metadata has an id but no cache → reconnect; fall back to recreate on failure.
|
4. Metadata has an id but no cache → reconnect; fall back to recreate on failure.
|
||||||
|
|
||||||
For `SANDBOX_TYPE=langsmith` (default), every sandbox creation/refresh also calls `_configure_github_proxy` with a fresh GitHub App installation token (`get_github_app_installation_token`). The proxy injects Basic auth for `github.com` git traffic and Bearer auth for `api.github.com` so sandbox commands can use `GH_TOKEN=dummy gh ...` without storing real tokens in the sandbox. Other providers (modal, daytona, runloop, local) skip the proxy step. Provider is selected via `SANDBOX_TYPE` env var; factory is `agent/utils/sandbox.py:create_sandbox`.
|
For `SANDBOX_TYPE=langsmith` (default), every sandbox creation/refresh also calls `_configure_github_proxy` with a fresh GitHub App installation token (`get_github_app_installation_token`). The proxy injects Basic auth for `github.com` git traffic and Bearer auth for `api.github.com` so sandbox commands can use `GH_TOKEN=dummy gh ...` without storing real tokens in the sandbox. Other providers (modal, daytona, runloop, local) skip the proxy step. Provider is selected via `SANDBOX_TYPE`; factory is `agent/utils/sandbox.py:create_sandbox` (`SANDBOX_FACTORIES` maps each provider name to a creator in `agent/integrations/`).
|
||||||
|
|
||||||
|
Every run re-applies `git config --global user.name/email` for the bot identity, because reused/reconnected sandboxes can lose `--global` config and Vercel preview deploys reject commits whose author email doesn't resolve to a GitHub account.
|
||||||
|
|
||||||
### Middleware stack (order matters)
|
### Middleware stack (order matters)
|
||||||
|
|
||||||
Configured in `get_agent`, runs around every model call:
|
Configured in `agent/server.py:get_agent`, runs around every model call (in this order):
|
||||||
|
|
||||||
1. `ToolErrorMiddleware` — catches tool exceptions.
|
1. `SanitizeToolInputsMiddleware` — strips/normalizes tool inputs before they reach tools.
|
||||||
2. `check_message_queue_before_model` — pulls Linear comments / Slack messages that arrived mid-run from the thread queue and injects them as user messages before the next LLM call. This is what makes "message the agent while it's working" work.
|
2. `ModelCallLimitMiddleware` (from `langchain.agents.middleware`) — caps model calls at `MODEL_CALL_RECURSION_LIMIT` (~half of `DEFAULT_RECURSION_LIMIT`); `exit_behavior="end"`.
|
||||||
3. `ensure_no_empty_msg` — guards against empty assistant messages that some providers reject.
|
3. `ToolErrorMiddleware` — catches tool exceptions and surfaces them as tool messages.
|
||||||
4. `notify_step_limit_reached` — after-agent hook that posts a Slack reply when the agent hits the step limit, so the user gets a clear signal instead of silence.
|
4. `check_message_queue_before_model` — pulls Linear comments / Slack messages that arrived mid-run from the thread queue and injects them as user messages before the next LLM call. This is what makes "message the agent while it's working" work.
|
||||||
|
5. `SlackAssistantStatusMiddleware` — keeps the Slack "assistant is typing"-style status up to date around model calls.
|
||||||
|
6. `ensure_no_empty_msg` — guards against empty assistant messages that some providers reject.
|
||||||
|
7. `notify_step_limit_reached` — after-agent hook that posts a Slack reply when the agent hits the step limit, so the user gets a clear signal instead of silence.
|
||||||
|
8. `SandboxCircuitBreakerMiddleware` — trips the agent out of repeated sandbox failures instead of looping.
|
||||||
|
9. `ModelFallbackMiddleware` (optional, last) — added only when `LLM_FALLBACK_MODEL_ID` or the per-model default fallback differs from the primary model.
|
||||||
|
|
||||||
|
Other middleware exists in `agent/middleware/` (`ExcludeToolsMiddleware`) but isn't wired into the default agent. The reviewer uses a leaner stack: `SanitizeToolInputsMiddleware`, `ModelCallLimitMiddleware`, `ToolErrorMiddleware`, `SlackAssistantStatusMiddleware`.
|
||||||
|
|
||||||
There is intentionally no after-agent safety net that opens a PR for the agent. The agent itself is responsible for committing, pushing, opening/updating the draft PR, and replying in the source channel — all via `GH_TOKEN=dummy gh` and `slack_thread_reply` / `linear_comment`.
|
There is intentionally no after-agent safety net that opens a PR for the agent. The agent itself is responsible for committing, pushing, opening/updating the draft PR, and replying in the source channel — all via `GH_TOKEN=dummy gh` and `slack_thread_reply` / `linear_comment`.
|
||||||
|
|
||||||
### Tools
|
### Tools
|
||||||
|
|
||||||
All tools live in `agent/tools/` and are flat-imported via `agent/tools/__init__.py`. The set is intentionally small and curated — see README "Tools — Curated, Not Accumulated". Built-in deepagents tools (`read_file`, `execute`, `glob`, `grep`, `task` for subagent spawning, …) are added by `create_deep_agent` itself; don't duplicate them.
|
All tools live in `agent/tools/` and are flat-imported via `agent/tools/__init__.py`. The set is intentionally small and curated — see README "Tools — Curated, Not Accumulated".
|
||||||
|
|
||||||
|
Wired into `get_agent`:
|
||||||
|
`http_request`, `fetch_url`, `web_search`, `linear_comment`, `linear_create_issue`, `linear_delete_issue`, `linear_get_issue`, `linear_get_issue_comments`, `linear_list_teams`, `linear_update_issue`, `request_pr_review`, `slack_read_thread_messages`, `slack_thread_reply`.
|
||||||
|
|
||||||
|
Reviewer-only tools (in `agent/reviewer.py`): `add_finding`, `update_finding`, `list_findings`, `publish_review`. The review-style analyzer uses `save_review_style` (exported as `save_review_style_prompt`).
|
||||||
|
|
||||||
|
Built-in deepagents tools (`read_file`, `write_file`, `edit_file`, `ls`, `glob`, `grep`, `execute`, `write_todos`, `task` for subagent spawning, …) are added by `create_deep_agent` itself; don't duplicate them.
|
||||||
|
|
||||||
|
### Models, profiles, and team defaults
|
||||||
|
|
||||||
|
Model + reasoning effort are resolved per run in this precedence (highest wins):
|
||||||
|
|
||||||
|
1. Per-thread config (`agent_model_id` + `agent_effort` in `configurable`) — set by webhooks/UI.
|
||||||
|
2. Per-user dashboard profile override (`agent/dashboard/agent_overrides.py:load_profile`), keyed by resolved GitHub login.
|
||||||
|
3. Team default model (`agent/dashboard/team_settings.py:get_team_default_model("agent")`).
|
||||||
|
|
||||||
|
Supported model IDs and per-model effort/reasoning rules live in `agent/dashboard/options.py`. Profile flags also drive run behavior — e.g. `profile_create_prs` disables PR creation for users who've opted out. Model construction goes through `agent/utils/model.py` (`make_model`, `provider_model_kwargs`, `fallback_model_id_for`).
|
||||||
|
|
||||||
### Auth
|
### Auth
|
||||||
|
|
||||||
- **GitHub**: dual-mode. User OAuth tokens are encrypted-at-rest in thread metadata (`agent/encryption.py`, `utils/auth.py:resolve_github_token`). When no user token is available, falls back to a GitHub App installation token (`utils/github_app.py`). The installation token is also what configures the LangSmith sandbox's GitHub proxy.
|
- **GitHub**: dual-mode. User OAuth tokens are encrypted-at-rest in thread metadata (`agent/encryption.py`, `utils/auth.py:resolve_github_token`). When no user token is available, falls back to a GitHub App installation token (`utils/github_app.py`). The installation token is also what configures the LangSmith sandbox's GitHub proxy.
|
||||||
- **Webhooks**: GitHub signatures verified in `utils/github_comments.py:verify_github_signature`; Slack/Linear handled in their respective utils.
|
- **Webhooks**: GitHub signatures verified in `utils/github_comments.py:verify_github_signature`; Slack/Linear handled in their respective utils.
|
||||||
|
- **Dashboard / UI**: GitHub OAuth login lives in `agent/dashboard/oauth.py` and `routes.py` (`/auth/login`, `/auth/callback`, `/auth/logout`, `/me`).
|
||||||
|
|
||||||
### Thread-id derivation
|
### Thread-id derivation
|
||||||
|
|
||||||
Webhooks compute deterministic thread ids so the same Linear issue / Slack thread / PR routes back to the same running agent. See `utils/github_comments.py:get_thread_id_from_branch` and the equivalents in `utils/linear.py` / `utils/slack.py`.
|
Webhooks compute deterministic thread ids so the same Linear issue / Slack thread / PR routes back to the same running agent. See `utils/github_comments.py:get_thread_id_from_branch` and the equivalents in `utils/linear.py` / `utils/slack.py`. Reviewer threads have their own deterministic ids and are tagged with `REVIEWER_THREAD_KIND` metadata so the FastAPI side can find them.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- Tests are unit-only by default (`tests/`). Integration tests would go under `tests/integration_tests/` (currently empty — `make integration_tests` no-ops if missing).
|
- Tests are unit-only by default (`tests/`). Integration tests would go under `tests/integration_tests/` (currently empty — `make integration_tests` no-ops if missing).
|
||||||
- New sandbox providers: add a module under `agent/integrations/` and wire it into `utils/sandbox.py:create_sandbox`. See `CUSTOMIZATION.md`.
|
- New sandbox providers: add a module under `agent/integrations/` and wire it into `SANDBOX_FACTORIES` in `agent/utils/sandbox.py`. See `CUSTOMIZATION.md`.
|
||||||
- New tools: add to `agent/tools/`, export from `agent/tools/__init__.py`, add to the `tools=[...]` list in `server.py:get_agent`.
|
- New tools: add to `agent/tools/`, export from `agent/tools/__init__.py`, add to the `tools=[...]` list in `server.py:get_agent` (or `reviewer.py` for reviewer-only tools).
|
||||||
- New middleware: add to `agent/middleware/`, export from `agent/middleware/__init__.py`, add to the `middleware=[...]` list in `server.py:get_agent` — order is significant.
|
- New middleware: add to `agent/middleware/`, export from `agent/middleware/__init__.py`, add to the `middleware=[...]` list in `server.py:get_agent` — order is significant (see the stack above).
|
||||||
|
- New dashboard endpoints: add to `agent/dashboard/routes.py`. The router is auto-mounted on the FastAPI app.
|
||||||
|
- New graphs: register the entrypoint in `langgraph.json` under `graphs`.
|
||||||
|
- Minimal-to-no code comments — only when the *why* isn't obvious from the code.
|
||||||
|
|
|
||||||
87
CLAUDE.md
87
CLAUDE.md
|
|
@ -4,16 +4,18 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
|
|
||||||
## Project
|
## Project
|
||||||
|
|
||||||
Open SWE is an open-source coding-agent framework built on **LangGraph** + **Deep Agents** (`deepagents.create_deep_agent`). It runs as a LangGraph app: each thread spawns its own isolated cloud sandbox, and the agent is invoked from Slack, Linear, or GitHub PR comments.
|
Open SWE is an open-source coding-agent framework built on **LangGraph** + **Deep Agents** (`deepagents.create_deep_agent`). It runs as a LangGraph app: each thread spawns its own isolated cloud sandbox, and the agent is invoked from Slack, Linear, or GitHub (PR comments, plus auto-review on opened / ready-for-review).
|
||||||
|
|
||||||
|
A separate **reviewer** graph runs read-only code reviews on PRs, and a **review-style analyzer** graph learns per-repo review style from historical PRs.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
Dependencies are managed with **uv**. Tests use pytest (`asyncio_mode = "auto"`). Lint/format is **ruff** (line-length 100, target py311).
|
Dependencies are managed with **uv**. Tests use pytest (`asyncio_mode = "auto"`). Lint/format is **ruff** (line-length 100, target py311). `requires-python = ">=3.11"`; `langgraph.json` pins the runtime to 3.12.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make install # uv pip install -e .
|
make install # uv pip install -e .
|
||||||
make dev # langgraph dev — run the LangGraph dev server (graph defined in langgraph.json)
|
make dev # uv run langgraph dev — serves all three graphs + the FastAPI app from langgraph.json
|
||||||
make run # uvicorn agent.webapp:app --reload --port 8000 (webhook server only)
|
make run # uvicorn agent.webapp:app --reload --port 8000 (FastAPI only, no LangGraph runtime)
|
||||||
make test # uv run pytest -vvv tests/
|
make test # uv run pytest -vvv tests/
|
||||||
make test TEST_FILE=tests/test_open_pr_middleware.py # single test file
|
make test TEST_FILE=tests/test_open_pr_middleware.py # single test file
|
||||||
uv run pytest -vvv tests/test_open_pr_middleware.py::test_name # single test
|
uv run pytest -vvv tests/test_open_pr_middleware.py::test_name # single test
|
||||||
|
|
@ -21,53 +23,94 @@ make lint # ruff check + ruff format --diff
|
||||||
make format # ruff format + ruff check --fix
|
make format # ruff format + ruff check --fix
|
||||||
```
|
```
|
||||||
|
|
||||||
`langgraph.json` declares the graph entrypoint as `agent.server:get_agent` and the FastAPI app as `agent.webapp:app`. Both are served together by `langgraph dev`.
|
`langgraph.json` declares three graph entrypoints and the FastAPI app, all served together by `langgraph dev`:
|
||||||
|
|
||||||
|
| Graph | Entrypoint | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `agent` | `agent.server:get_agent` | Main coding agent (Slack/Linear/GitHub-triggered). |
|
||||||
|
| `reviewer` | `agent.reviewer:get_reviewer_agent` | Read-only PR reviewer. Findings model + `publish_review`. |
|
||||||
|
| `review_style_analyzer` | `agent.review_style_analyzer:get_review_style_analyzer` | Learns per-repo reviewer style from historical PRs. |
|
||||||
|
|
||||||
|
The FastAPI app is `agent.webapp:app`.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Two entrypoints, one process
|
### Entrypoints
|
||||||
|
|
||||||
- **`agent/server.py` → `get_agent(config)`** — the LangGraph graph factory. Called per-thread. Resolves the GitHub token, gets-or-creates the sandbox for the thread, then constructs a fresh `create_deep_agent(...)` with the full tool list and middleware stack. The agent itself is stateless — all per-thread state lives in the sandbox + thread metadata.
|
- **`agent/server.py` → `get_agent(config)`** — main graph factory. Called per-thread. Resolves the GitHub token, gets-or-creates the sandbox for the thread, resolves the team/profile/per-thread model + effort, then constructs a fresh `create_deep_agent(...)` with the curated tool list and middleware stack. The agent itself is stateless — all per-thread state lives in the sandbox + thread metadata.
|
||||||
- **`agent/webapp.py`** — custom FastAPI routes mounted alongside the LangGraph server. This is where webhooks land (GitHub, Linear, Slack). Each webhook resolves a deterministic `thread_id` (so follow-up messages route to the same agent run), then triggers/streams a run via the `langgraph_sdk` client.
|
- **`agent/reviewer.py` → `get_reviewer_agent(config)`** — reviewer graph factory. Shares `ensure_sandbox_for_thread` with the main agent but wires a reviewer-only toolset (`add_finding`, `update_finding`, `list_findings`, `publish_review`, `web_search`, `fetch_url`, `http_request`) and a different system prompt that pins the single-evolving-findings model and the diff-anchored bar for filing a finding. Read-only: no commit/push/PR-opening tools.
|
||||||
|
- **`agent/review_style_analyzer.py` → `get_review_style_analyzer(config)`** — small graph that reads historical PR reviews from a repo and emits a per-repo style prompt via the `save_review_style_prompt` tool. Output is consumed by the reviewer as a "repository-specific review style" appendix.
|
||||||
|
- **`agent/webapp.py`** — custom FastAPI routes mounted alongside the LangGraph server. Webhooks land here (GitHub, Linear, Slack). Each webhook resolves a deterministic `thread_id` (so follow-up messages route to the same agent run) and triggers/streams a run via the `langgraph_sdk` client. Also auto-reviews PRs on `opened` / `ready_for_review` events when the repo+author opt in.
|
||||||
|
- **`agent/dashboard/`** — `router` mounted under the FastAPI app at startup (`app.include_router(dashboard_router)`). Owns GitHub OAuth, per-user profiles, admin endpoints, team defaults, enabled-repo lists, review-style management, and the Agents chat thread API used by the UI in `ui/`.
|
||||||
|
|
||||||
### Sandbox lifecycle (the tricky part)
|
### Sandbox lifecycle (the tricky part)
|
||||||
|
|
||||||
`SANDBOX_BACKENDS` is an in-process dict keyed by `thread_id`. Thread metadata persists `sandbox_id` across processes. `get_agent` handles four cases:
|
`SANDBOX_BACKENDS` (in `agent/utils/sandbox_state.py`) is an in-process dict keyed by `thread_id`. Thread metadata persists `sandbox_id` across processes. `ensure_sandbox_for_thread` handles four cases:
|
||||||
|
|
||||||
1. Sandbox cached in memory → ping it (`echo ok`); recreate on `SandboxClientError`.
|
1. Sandbox cached in memory → ping it (`echo ok`); recreate on `SandboxClientError`. Healthy reused sandboxes also get a GitHub-proxy refresh (recreate on failure).
|
||||||
2. Metadata says `__creating__` and no cache → poll until ready (`_wait_for_sandbox_id`).
|
2. Metadata says `__creating__` and no cache → poll until ready (`_wait_for_sandbox_id`).
|
||||||
3. No sandbox at all → create one, set `__creating__` sentinel, then real id.
|
3. No sandbox at all → set `__creating__` sentinel, create one, persist the real id.
|
||||||
4. Metadata has an id but no cache → reconnect; fall back to recreate on failure.
|
4. Metadata has an id but no cache → reconnect; fall back to recreate on failure.
|
||||||
|
|
||||||
For `SANDBOX_TYPE=langsmith` (default), every sandbox creation/refresh also calls `_configure_github_proxy` with a fresh GitHub App installation token (`get_github_app_installation_token`). The proxy injects Basic auth for `github.com` git traffic and Bearer auth for `api.github.com` so sandbox commands can use `GH_TOKEN=dummy gh ...` without storing real tokens in the sandbox. Other providers (modal, daytona, runloop, local) skip the proxy step. Provider is selected via `SANDBOX_TYPE` env var; factory is `agent/utils/sandbox.py:create_sandbox`.
|
For `SANDBOX_TYPE=langsmith` (default), every sandbox creation/refresh also calls `_configure_github_proxy` with a fresh GitHub App installation token (`get_github_app_installation_token`). The proxy injects Basic auth for `github.com` git traffic and Bearer auth for `api.github.com` so sandbox commands can use `GH_TOKEN=dummy gh ...` without storing real tokens in the sandbox. Other providers (modal, daytona, runloop, local) skip the proxy step. Provider is selected via `SANDBOX_TYPE`; factory is `agent/utils/sandbox.py:create_sandbox` (`SANDBOX_FACTORIES` maps each provider name to a creator in `agent/integrations/`).
|
||||||
|
|
||||||
|
Every run re-applies `git config --global user.name/email` for the bot identity, because reused/reconnected sandboxes can lose `--global` config and Vercel preview deploys reject commits whose author email doesn't resolve to a GitHub account.
|
||||||
|
|
||||||
### Middleware stack (order matters)
|
### Middleware stack (order matters)
|
||||||
|
|
||||||
Configured in `get_agent`, runs around every model call:
|
Configured in `agent/server.py:get_agent`, runs around every model call (in this order):
|
||||||
|
|
||||||
1. `ToolErrorMiddleware` — catches tool exceptions.
|
1. `SanitizeToolInputsMiddleware` — strips/normalizes tool inputs before they reach tools.
|
||||||
2. `check_message_queue_before_model` — pulls Linear comments / Slack messages that arrived mid-run from the thread queue and injects them as user messages before the next LLM call. This is what makes "message the agent while it's working" work.
|
2. `ModelCallLimitMiddleware` (from `langchain.agents.middleware`) — caps model calls at `MODEL_CALL_RECURSION_LIMIT` (~half of `DEFAULT_RECURSION_LIMIT`); `exit_behavior="end"`.
|
||||||
3. `ensure_no_empty_msg` — guards against empty assistant messages that some providers reject.
|
3. `ToolErrorMiddleware` — catches tool exceptions and surfaces them as tool messages.
|
||||||
4. `notify_step_limit_reached` — after-agent hook that posts a Slack reply when the agent hits the step limit, so the user gets a clear signal instead of silence.
|
4. `check_message_queue_before_model` — pulls Linear comments / Slack messages that arrived mid-run from the thread queue and injects them as user messages before the next LLM call. This is what makes "message the agent while it's working" work.
|
||||||
|
5. `SlackAssistantStatusMiddleware` — keeps the Slack "assistant is typing"-style status up to date around model calls.
|
||||||
|
6. `ensure_no_empty_msg` — guards against empty assistant messages that some providers reject.
|
||||||
|
7. `notify_step_limit_reached` — after-agent hook that posts a Slack reply when the agent hits the step limit, so the user gets a clear signal instead of silence.
|
||||||
|
8. `SandboxCircuitBreakerMiddleware` — trips the agent out of repeated sandbox failures instead of looping.
|
||||||
|
9. `ModelFallbackMiddleware` (optional, last) — added only when `LLM_FALLBACK_MODEL_ID` or the per-model default fallback differs from the primary model.
|
||||||
|
|
||||||
|
Other middleware exists in `agent/middleware/` (`ExcludeToolsMiddleware`) but isn't wired into the default agent. The reviewer uses a leaner stack: `SanitizeToolInputsMiddleware`, `ModelCallLimitMiddleware`, `ToolErrorMiddleware`, `SlackAssistantStatusMiddleware`.
|
||||||
|
|
||||||
There is intentionally no after-agent safety net that opens a PR for the agent. The agent itself is responsible for committing, pushing, opening/updating the draft PR, and replying in the source channel — all via `GH_TOKEN=dummy gh` and `slack_thread_reply` / `linear_comment`.
|
There is intentionally no after-agent safety net that opens a PR for the agent. The agent itself is responsible for committing, pushing, opening/updating the draft PR, and replying in the source channel — all via `GH_TOKEN=dummy gh` and `slack_thread_reply` / `linear_comment`.
|
||||||
|
|
||||||
### Tools
|
### Tools
|
||||||
|
|
||||||
All tools live in `agent/tools/` and are flat-imported via `agent/tools/__init__.py`. The set is intentionally small and curated — see README "Tools — Curated, Not Accumulated". Built-in deepagents tools (`read_file`, `execute`, `glob`, `grep`, `task` for subagent spawning, …) are added by `create_deep_agent` itself; don't duplicate them.
|
All tools live in `agent/tools/` and are flat-imported via `agent/tools/__init__.py`. The set is intentionally small and curated — see README "Tools — Curated, Not Accumulated".
|
||||||
|
|
||||||
|
Wired into `get_agent`:
|
||||||
|
`http_request`, `fetch_url`, `web_search`, `linear_comment`, `linear_create_issue`, `linear_delete_issue`, `linear_get_issue`, `linear_get_issue_comments`, `linear_list_teams`, `linear_update_issue`, `request_pr_review`, `slack_read_thread_messages`, `slack_thread_reply`.
|
||||||
|
|
||||||
|
Reviewer-only tools (in `agent/reviewer.py`): `add_finding`, `update_finding`, `list_findings`, `publish_review`. The review-style analyzer uses `save_review_style` (exported as `save_review_style_prompt`).
|
||||||
|
|
||||||
|
Built-in deepagents tools (`read_file`, `write_file`, `edit_file`, `ls`, `glob`, `grep`, `execute`, `write_todos`, `task` for subagent spawning, …) are added by `create_deep_agent` itself; don't duplicate them.
|
||||||
|
|
||||||
|
### Models, profiles, and team defaults
|
||||||
|
|
||||||
|
Model + reasoning effort are resolved per run in this precedence (highest wins):
|
||||||
|
|
||||||
|
1. Per-thread config (`agent_model_id` + `agent_effort` in `configurable`) — set by webhooks/UI.
|
||||||
|
2. Per-user dashboard profile override (`agent/dashboard/agent_overrides.py:load_profile`), keyed by resolved GitHub login.
|
||||||
|
3. Team default model (`agent/dashboard/team_settings.py:get_team_default_model("agent")`).
|
||||||
|
|
||||||
|
Supported model IDs and per-model effort/reasoning rules live in `agent/dashboard/options.py`. Profile flags also drive run behavior — e.g. `profile_create_prs` disables PR creation for users who've opted out. Model construction goes through `agent/utils/model.py` (`make_model`, `provider_model_kwargs`, `fallback_model_id_for`).
|
||||||
|
|
||||||
### Auth
|
### Auth
|
||||||
|
|
||||||
- **GitHub**: dual-mode. User OAuth tokens are encrypted-at-rest in thread metadata (`agent/encryption.py`, `utils/auth.py:resolve_github_token`). When no user token is available, falls back to a GitHub App installation token (`utils/github_app.py`). The installation token is also what configures the LangSmith sandbox's GitHub proxy.
|
- **GitHub**: dual-mode. User OAuth tokens are encrypted-at-rest in thread metadata (`agent/encryption.py`, `utils/auth.py:resolve_github_token`). When no user token is available, falls back to a GitHub App installation token (`utils/github_app.py`). The installation token is also what configures the LangSmith sandbox's GitHub proxy.
|
||||||
- **Webhooks**: GitHub signatures verified in `utils/github_comments.py:verify_github_signature`; Slack/Linear handled in their respective utils.
|
- **Webhooks**: GitHub signatures verified in `utils/github_comments.py:verify_github_signature`; Slack/Linear handled in their respective utils.
|
||||||
|
- **Dashboard / UI**: GitHub OAuth login lives in `agent/dashboard/oauth.py` and `routes.py` (`/auth/login`, `/auth/callback`, `/auth/logout`, `/me`).
|
||||||
|
|
||||||
### Thread-id derivation
|
### Thread-id derivation
|
||||||
|
|
||||||
Webhooks compute deterministic thread ids so the same Linear issue / Slack thread / PR routes back to the same running agent. See `utils/github_comments.py:get_thread_id_from_branch` and the equivalents in `utils/linear.py` / `utils/slack.py`.
|
Webhooks compute deterministic thread ids so the same Linear issue / Slack thread / PR routes back to the same running agent. See `utils/github_comments.py:get_thread_id_from_branch` and the equivalents in `utils/linear.py` / `utils/slack.py`. Reviewer threads have their own deterministic ids and are tagged with `REVIEWER_THREAD_KIND` metadata so the FastAPI side can find them.
|
||||||
|
|
||||||
## Conventions
|
## Conventions
|
||||||
|
|
||||||
- Tests are unit-only by default (`tests/`). Integration tests would go under `tests/integration_tests/` (currently empty — `make integration_tests` no-ops if missing).
|
- Tests are unit-only by default (`tests/`). Integration tests would go under `tests/integration_tests/` (currently empty — `make integration_tests` no-ops if missing).
|
||||||
- New sandbox providers: add a module under `agent/integrations/` and wire it into `utils/sandbox.py:create_sandbox`. See `CUSTOMIZATION.md`.
|
- New sandbox providers: add a module under `agent/integrations/` and wire it into `SANDBOX_FACTORIES` in `agent/utils/sandbox.py`. See `CUSTOMIZATION.md`.
|
||||||
- New tools: add to `agent/tools/`, export from `agent/tools/__init__.py`, add to the `tools=[...]` list in `server.py:get_agent`.
|
- New tools: add to `agent/tools/`, export from `agent/tools/__init__.py`, add to the `tools=[...]` list in `server.py:get_agent` (or `reviewer.py` for reviewer-only tools).
|
||||||
- New middleware: add to `agent/middleware/`, export from `agent/middleware/__init__.py`, add to the `middleware=[...]` list in `server.py:get_agent` — order is significant.
|
- New middleware: add to `agent/middleware/`, export from `agent/middleware/__init__.py`, add to the `middleware=[...]` list in `server.py:get_agent` — order is significant (see the stack above).
|
||||||
|
- New dashboard endpoints: add to `agent/dashboard/routes.py`. The router is auto-mounted on the FastAPI app.
|
||||||
|
- New graphs: register the entrypoint in `langgraph.json` under `graphs`.
|
||||||
|
- Minimal-to-no code comments — only when the *why* isn't obvious from the code.
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue