mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 19:43:15 +00:00
feat: stop auto-cloning and let agent manage repo setup [closes OPE-21] (#1159)
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* feat: authenticate git operations via sandbox proxy instead of credential files
* removing logger.info
* formatting and linting
* fix: resolve lint errors in server.py (imports, unused vars, undefined names)
* feat: use opaque proxy headers for GitHub auth in sandbox
* linting formatting and test changes
* linting
* Delete .claude directory
* Delete tests/evals directory
* fix: address PR review — guard missing tokens, quote shell paths, add proxy auth tests
* fix: restore authorship, branch_name support, and installation token for PR creation
* linitng
* fix: move installation token fetch before commit, clean up dead proxy validation code
* feat: stop auto-cloning and let agent manage repo setup [closes OPE-21]
* feat: stop auto-cloning and let agent manage repo setup [closes OPE-21]
* fix: address review feedback — restore agents_md, add git user config, lint fixes
* fix: drop github_token arg from sandbox creation, use generic create_sandbox factory with langsmith-only proxy config
* fix: use _get_langsmith_api_key() for prod key fallback, warn when API key missing for proxy config
* linting
* linting
* feat: add installation token auth to list_repos GitHub API call
* agents.md update
* linting
* fix: address PR review feedback — shell precedence bug in prompt, remove dead code
* linting
* Apply suggestion from @bracesproul
Co-authored-by: Brace Sproul <braceasproul@gmail.com>
* Apply suggestion from @bracesproul
Co-authored-by: Brace Sproul <braceasproul@gmail.com>
* fix: address PR review feedback — restore {working_dir} in prompt, remove clone code block
* fix:Extract check_or_recreate_sandbox utility from inline sandbox health check
* fix: address PR review feedback — async list_repos, restore template name, fix prompt colon
* fix: resolve merge conflicts with main, adopt deepagents v0.5.0a4 LangSmithSandbox
* linting
* yogesh/ope-21-stop-auto-cloning
* Update agent/tools/list_repos.py
Co-authored-by: Brace Sproul <braceasproul@gmail.com>
* Update agent/prompt.py
Co-authored-by: Brace Sproul <braceasproul@gmail.com>
* feat: address PR review — list_repos uses GitHub API only, PR trigger includes org/repo
* linting
* feat: address PR review feedback — list_repos pagination, simpler return, sandbox health check
* feat: support listing repos for personal user accounts via is_organization flag
---------
Co-authored-by: Brace Sproul <braceasproul@gmail.com>
This commit is contained in:
parent
91f63de361
commit
2039fe6660
14 changed files with 184 additions and 284 deletions
|
|
@ -6,8 +6,8 @@ Open SWE is designed to be forked and customized for your org. The core agent is
|
||||||
# agent/server.py — the key lines
|
# agent/server.py — the key lines
|
||||||
return create_deep_agent(
|
return create_deep_agent(
|
||||||
model=make_model(os.environ.get("LLM_MODEL_ID", DEFAULT_LLM_MODEL_ID), temperature=0, max_tokens=20_000),
|
model=make_model(os.environ.get("LLM_MODEL_ID", DEFAULT_LLM_MODEL_ID), temperature=0, max_tokens=20_000),
|
||||||
system_prompt=construct_system_prompt(repo_dir, ...),
|
system_prompt=construct_system_prompt(...),
|
||||||
tools=[http_request, fetch_url, commit_and_open_pr, linear_comment, slack_thread_reply],
|
tools=[http_request, fetch_url, list_repos, get_branch_name, commit_and_open_pr, linear_comment, slack_thread_reply],
|
||||||
backend=sandbox_backend,
|
backend=sandbox_backend,
|
||||||
middleware=[
|
middleware=[
|
||||||
ToolErrorMiddleware(),
|
ToolErrorMiddleware(),
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@ Rather than forking an existing agent or building from scratch, Open SWE **compo
|
||||||
```python
|
```python
|
||||||
create_deep_agent(
|
create_deep_agent(
|
||||||
model="anthropic:claude-opus-4-6",
|
model="anthropic:claude-opus-4-6",
|
||||||
system_prompt=construct_system_prompt(repo_dir, ...),
|
system_prompt=construct_system_prompt(...),
|
||||||
tools=[http_request, fetch_url, commit_and_open_pr, linear_comment, slack_thread_reply],
|
tools=[http_request, fetch_url, list_repos, get_branch_name, commit_and_open_pr, linear_comment, slack_thread_reply],
|
||||||
backend=sandbox_backend,
|
backend=sandbox_backend,
|
||||||
middleware=[ToolErrorMiddleware(), check_message_queue_before_model, ...],
|
middleware=[ToolErrorMiddleware(), check_message_queue_before_model, ...],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,10 @@ def create_langsmith_sandbox(
|
||||||
) -> SandboxBackendProtocol:
|
) -> SandboxBackendProtocol:
|
||||||
"""Create or connect to a LangSmith sandbox without automatic cleanup.
|
"""Create or connect to a LangSmith sandbox without automatic cleanup.
|
||||||
|
|
||||||
|
This function directly uses the LangSmithProvider to create/connect to sandboxes
|
||||||
|
without the context manager cleanup, allowing sandboxes to persist across
|
||||||
|
multiple agent invocations.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
sandbox_id: Optional existing sandbox ID to connect to.
|
sandbox_id: Optional existing sandbox ID to connect to.
|
||||||
If None, creates a new sandbox.
|
If None, creates a new sandbox.
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,35 @@ You are currently executing a software engineering task. You have access to:
|
||||||
- Project context and files
|
- Project context and files
|
||||||
- Shell commands and code editing tools
|
- Shell commands and code editing tools
|
||||||
- A sandboxed, git-backed workspace
|
- A sandboxed, git-backed workspace
|
||||||
- Project-specific rules and conventions from the repository's `AGENTS.md` file (if present)"""
|
- Project-specific rules and conventions from the repository's `AGENTS.md` file (read after cloning — see Repository Setup)"""
|
||||||
|
|
||||||
|
|
||||||
|
REPO_SETUP_SECTION = """---
|
||||||
|
|
||||||
|
### Repository Setup
|
||||||
|
|
||||||
|
Before starting any task, you must set up the repository in your sandbox. Follow these steps in order:
|
||||||
|
|
||||||
|
1. **Find the repo** — Call `list_repos(organization_name="<org>")` to list repositories for a GitHub organization, or `list_repos(organization_name="<username>", is_organization=False)` for a personal user account. Match the repo to your task context (e.g. the Linear team/project or issue description). If you are unsure which repo to use, ask the user for confirmation before proceeding.
|
||||||
|
|
||||||
|
2. **Clone the repo** — Clone it into `{working_dir}`.
|
||||||
|
|
||||||
|
3. **Get your branch** — Always call the `get_branch_name` tool to get the branch name for this thread.
|
||||||
|
|
||||||
|
4. **Checkout your branch** — Always fetch and checkout your branch before making any changes.
|
||||||
|
|
||||||
|
5. **Read and follow AGENTS.md** — After cloning, check if `AGENTS.md` exists at the repository root (`{working_dir}/<repo>/AGENTS.md`). If it exists, you MUST read it immediately and treat its contents as **mandatory rules** for all work in that repository. AGENTS.md contains project-specific conventions, coding standards, and constraints that override your default behavior. Violating AGENTS.md rules is equivalent to violating the system prompt. If AGENTS.md does not exist, skip this step.
|
||||||
|
|
||||||
|
You MUST complete ALL of these steps before doing any other work. The sandbox starts clean — no repo is pre-cloned."""
|
||||||
|
|
||||||
|
|
||||||
FILE_MANAGEMENT_SECTION = """---
|
FILE_MANAGEMENT_SECTION = """---
|
||||||
|
|
||||||
### File & Code Management
|
### File & Code Management
|
||||||
|
|
||||||
- **Repository location:** `{working_dir}`
|
- **Repository location:** `{working_dir}/<repo_name>` (clone the repo here first — see Repository Setup)
|
||||||
- Never create backup files.
|
- Never create backup files.
|
||||||
- Work only within the existing Git repository.
|
- Work only within the cloned Git repository.
|
||||||
- Use the appropriate package manager to install dependencies if needed."""
|
- Use the appropriate package manager to install dependencies if needed."""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -68,6 +87,12 @@ TOOL_USAGE_SECTION = """---
|
||||||
|
|
||||||
### Tool Usage
|
### Tool Usage
|
||||||
|
|
||||||
|
#### `list_repos`
|
||||||
|
Lists GitHub repositories for a given organization or user via the GitHub API. Pass `organization_name` to specify which org or user to query. Set `is_organization=False` for personal user accounts (defaults to True). Call this first to find the right repo for your task.
|
||||||
|
|
||||||
|
#### `get_branch_name`
|
||||||
|
Returns the git branch name for this thread. Always call this tool to get the correct branch before making any changes.
|
||||||
|
|
||||||
#### `execute`
|
#### `execute`
|
||||||
Run shell commands in the sandbox. Pass `timeout=<seconds>` for long-running commands (default: 300s).
|
Run shell commands in the sandbox. Pass `timeout=<seconds>` for long-running commands (default: 300s).
|
||||||
|
|
||||||
|
|
@ -259,8 +284,9 @@ Always call `commit_and_open_pr` followed by the appropriate reply tool once imp
|
||||||
|
|
||||||
SYSTEM_PROMPT = (
|
SYSTEM_PROMPT = (
|
||||||
WORKING_ENV_SECTION
|
WORKING_ENV_SECTION
|
||||||
+ FILE_MANAGEMENT_SECTION
|
|
||||||
+ TASK_OVERVIEW_SECTION
|
+ TASK_OVERVIEW_SECTION
|
||||||
|
+ REPO_SETUP_SECTION
|
||||||
|
+ FILE_MANAGEMENT_SECTION
|
||||||
+ TASK_EXECUTION_SECTION
|
+ TASK_EXECUTION_SECTION
|
||||||
+ TOOL_USAGE_SECTION
|
+ TOOL_USAGE_SECTION
|
||||||
+ TOOL_BEST_PRACTICES_SECTION
|
+ TOOL_BEST_PRACTICES_SECTION
|
||||||
|
|
@ -271,10 +297,6 @@ SYSTEM_PROMPT = (
|
||||||
+ COMMUNICATION_SECTION
|
+ COMMUNICATION_SECTION
|
||||||
+ EXTERNAL_UNTRUSTED_COMMENTS_SECTION
|
+ EXTERNAL_UNTRUSTED_COMMENTS_SECTION
|
||||||
+ COMMIT_PR_SECTION
|
+ COMMIT_PR_SECTION
|
||||||
+ """
|
|
||||||
|
|
||||||
{agents_md_section}
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -282,20 +304,9 @@ def construct_system_prompt(
|
||||||
working_dir: str,
|
working_dir: str,
|
||||||
linear_project_id: str = "",
|
linear_project_id: str = "",
|
||||||
linear_issue_number: str = "",
|
linear_issue_number: str = "",
|
||||||
agents_md: str = "",
|
|
||||||
) -> str:
|
) -> str:
|
||||||
agents_md_section = ""
|
|
||||||
if agents_md:
|
|
||||||
agents_md_section = (
|
|
||||||
"\nThe following text is pulled from the repository's AGENTS.md file. "
|
|
||||||
"It may contain specific instructions and guidelines for the agent.\n"
|
|
||||||
"<agents_md>\n"
|
|
||||||
f"{agents_md}\n"
|
|
||||||
"</agents_md>\n"
|
|
||||||
)
|
|
||||||
return SYSTEM_PROMPT.format(
|
return SYSTEM_PROMPT.format(
|
||||||
working_dir=working_dir,
|
working_dir=working_dir,
|
||||||
linear_project_id=linear_project_id or "<PROJECT_ID>",
|
linear_project_id=linear_project_id or "<PROJECT_ID>",
|
||||||
linear_issue_number=linear_issue_number or "<ISSUE_NUMBER>",
|
linear_issue_number=linear_issue_number or "<ISSUE_NUMBER>",
|
||||||
agents_md_section=agents_md_section,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
244
agent/server.py
244
agent/server.py
|
|
@ -5,12 +5,10 @@
|
||||||
# ruff: noqa: E402
|
# ruff: noqa: E402
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shlex
|
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from langgraph.config import get_config
|
|
||||||
from langgraph.graph.state import RunnableConfig
|
from langgraph.graph.state import RunnableConfig
|
||||||
from langgraph.pregel import Pregel
|
from langgraph.pregel import Pregel
|
||||||
from langgraph_sdk import get_client
|
from langgraph_sdk import get_client
|
||||||
|
|
@ -40,6 +38,7 @@ from .tools import (
|
||||||
create_pr_review,
|
create_pr_review,
|
||||||
dismiss_pr_review,
|
dismiss_pr_review,
|
||||||
fetch_url,
|
fetch_url,
|
||||||
|
get_branch_name,
|
||||||
get_pr_review,
|
get_pr_review,
|
||||||
github_comment,
|
github_comment,
|
||||||
http_request,
|
http_request,
|
||||||
|
|
@ -52,6 +51,7 @@ from .tools import (
|
||||||
linear_update_issue,
|
linear_update_issue,
|
||||||
list_pr_review_comments,
|
list_pr_review_comments,
|
||||||
list_pr_reviews,
|
list_pr_reviews,
|
||||||
|
list_repos,
|
||||||
slack_thread_reply,
|
slack_thread_reply,
|
||||||
submit_pr_review,
|
submit_pr_review,
|
||||||
update_pr_review,
|
update_pr_review,
|
||||||
|
|
@ -61,6 +61,7 @@ from .utils.auth import resolve_github_token
|
||||||
from .utils.github_app import get_github_app_installation_token
|
from .utils.github_app import get_github_app_installation_token
|
||||||
from .utils.model import make_model
|
from .utils.model import make_model
|
||||||
from .utils.sandbox import create_sandbox
|
from .utils.sandbox import create_sandbox
|
||||||
|
from .utils.sandbox_paths import aresolve_sandbox_work_dir
|
||||||
|
|
||||||
client = get_client()
|
client = get_client()
|
||||||
|
|
||||||
|
|
@ -68,110 +69,9 @@ SANDBOX_CREATING = "__creating__"
|
||||||
SANDBOX_CREATION_TIMEOUT = 180
|
SANDBOX_CREATION_TIMEOUT = 180
|
||||||
SANDBOX_POLL_INTERVAL = 1.0
|
SANDBOX_POLL_INTERVAL = 1.0
|
||||||
|
|
||||||
from .utils.agents_md import read_agents_md_in_sandbox
|
|
||||||
from .utils.github import (
|
|
||||||
git_has_uncommitted_changes,
|
|
||||||
is_valid_git_repo,
|
|
||||||
remove_directory,
|
|
||||||
)
|
|
||||||
from .utils.sandbox_paths import aresolve_repo_dir, aresolve_sandbox_work_dir
|
|
||||||
from .utils.sandbox_state import SANDBOX_BACKENDS, get_sandbox_id_from_metadata
|
from .utils.sandbox_state import SANDBOX_BACKENDS, get_sandbox_id_from_metadata
|
||||||
|
|
||||||
|
|
||||||
async def _clone_or_pull_repo_in_sandbox(
|
|
||||||
sandbox_backend: SandboxBackendProtocol,
|
|
||||||
owner: str,
|
|
||||||
repo: str,
|
|
||||||
) -> str:
|
|
||||||
"""Clone a GitHub repo into the sandbox, or pull if it already exists.
|
|
||||||
|
|
||||||
Authentication is handled by the sandbox proxy (configured at sandbox creation
|
|
||||||
time), so no token is needed here.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sandbox_backend: The sandbox backend to execute commands in
|
|
||||||
owner: GitHub repo owner
|
|
||||||
repo: GitHub repo name
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Path to the cloned/updated repo directory
|
|
||||||
"""
|
|
||||||
logger.info("_clone_or_pull_repo_in_sandbox called for %s/%s", owner, repo)
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
|
|
||||||
work_dir = await aresolve_sandbox_work_dir(sandbox_backend)
|
|
||||||
repo_dir = await aresolve_repo_dir(sandbox_backend, repo)
|
|
||||||
clean_url = f"https://github.com/{owner}/{repo}.git"
|
|
||||||
|
|
||||||
logger.info("Resolved sandbox work dir to %s", work_dir)
|
|
||||||
|
|
||||||
is_git_repo = await loop.run_in_executor(None, is_valid_git_repo, sandbox_backend, repo_dir)
|
|
||||||
|
|
||||||
if not is_git_repo:
|
|
||||||
logger.warning("Repo directory missing or not a valid git repo at %s, removing", repo_dir)
|
|
||||||
try:
|
|
||||||
removed = await loop.run_in_executor(None, remove_directory, sandbox_backend, repo_dir)
|
|
||||||
if not removed:
|
|
||||||
msg = f"Failed to remove invalid directory at {repo_dir}"
|
|
||||||
logger.error(msg)
|
|
||||||
raise RuntimeError(msg)
|
|
||||||
logger.info("Removed invalid directory, will clone fresh repo")
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to remove invalid directory")
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
logger.info("Repo exists at %s, checking for uncommitted changes", repo_dir)
|
|
||||||
has_changes = await loop.run_in_executor(
|
|
||||||
None, git_has_uncommitted_changes, sandbox_backend, repo_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
if has_changes:
|
|
||||||
logger.warning("Repo has uncommitted changes at %s, skipping pull", repo_dir)
|
|
||||||
return repo_dir
|
|
||||||
|
|
||||||
logger.info("Repo is clean, pulling latest changes from %s/%s", owner, repo)
|
|
||||||
|
|
||||||
try:
|
|
||||||
pull_result = await loop.run_in_executor(
|
|
||||||
None,
|
|
||||||
sandbox_backend.execute,
|
|
||||||
f"cd {shlex.quote(repo_dir)} && git pull origin $(git rev-parse --abbrev-ref HEAD)",
|
|
||||||
)
|
|
||||||
logger.debug("Git pull result: exit_code=%s", pull_result.exit_code)
|
|
||||||
if pull_result.exit_code != 0:
|
|
||||||
logger.warning(
|
|
||||||
"Git pull failed with exit code %s: %s",
|
|
||||||
pull_result.exit_code,
|
|
||||||
pull_result.output[:200] if pull_result.output else "",
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to execute git pull")
|
|
||||||
raise
|
|
||||||
|
|
||||||
logger.info("Repo updated at %s", repo_dir)
|
|
||||||
return repo_dir
|
|
||||||
|
|
||||||
logger.info("Cloning repo %s/%s to %s", owner, repo, repo_dir)
|
|
||||||
try:
|
|
||||||
result = await loop.run_in_executor(
|
|
||||||
None,
|
|
||||||
sandbox_backend.execute,
|
|
||||||
f"git clone {shlex.quote(clean_url)} {shlex.quote(repo_dir)}",
|
|
||||||
)
|
|
||||||
logger.debug("Git clone result: exit_code=%s", result.exit_code)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to execute git clone")
|
|
||||||
raise
|
|
||||||
|
|
||||||
if result.exit_code != 0:
|
|
||||||
msg = f"Failed to clone repo {owner}/{repo}: {result.output}"
|
|
||||||
logger.error(msg)
|
|
||||||
raise RuntimeError(msg)
|
|
||||||
|
|
||||||
logger.info("Repo cloned successfully at %s", repo_dir)
|
|
||||||
return repo_dir
|
|
||||||
|
|
||||||
|
|
||||||
async def _create_sandbox_with_proxy() -> SandboxBackendProtocol:
|
async def _create_sandbox_with_proxy() -> SandboxBackendProtocol:
|
||||||
"""Create a new sandbox with GitHub proxy auth configured.
|
"""Create a new sandbox with GitHub proxy auth configured.
|
||||||
|
|
||||||
|
|
@ -210,15 +110,12 @@ async def _refresh_github_proxy(
|
||||||
await asyncio.to_thread(_configure_github_proxy, sandbox_backend.id, installation_token)
|
await asyncio.to_thread(_configure_github_proxy, sandbox_backend.id, installation_token)
|
||||||
|
|
||||||
|
|
||||||
async def _recreate_sandbox(
|
async def _recreate_sandbox(thread_id: str) -> SandboxBackendProtocol:
|
||||||
thread_id: str,
|
"""Recreate a sandbox after a connection failure.
|
||||||
repo_owner: str,
|
|
||||||
repo_name: str,
|
|
||||||
) -> tuple[SandboxBackendProtocol, str]:
|
|
||||||
"""Recreate a sandbox and clone the repo after a connection failure.
|
|
||||||
|
|
||||||
Clears the stale cache entry, sets the SANDBOX_CREATING sentinel,
|
Clears the stale cache entry, sets the SANDBOX_CREATING sentinel,
|
||||||
creates a fresh sandbox (with proxy auth configured), and clones the repo.
|
and creates a fresh sandbox (with proxy auth configured).
|
||||||
|
The agent is responsible for cloning repos via tools.
|
||||||
"""
|
"""
|
||||||
SANDBOX_BACKENDS.pop(thread_id, None)
|
SANDBOX_BACKENDS.pop(thread_id, None)
|
||||||
await client.threads.update(
|
await client.threads.update(
|
||||||
|
|
@ -227,12 +124,33 @@ async def _recreate_sandbox(
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
sandbox_backend = await _create_sandbox_with_proxy()
|
sandbox_backend = await _create_sandbox_with_proxy()
|
||||||
repo_dir = await _clone_or_pull_repo_in_sandbox(sandbox_backend, repo_owner, repo_name)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to recreate sandbox after connection failure")
|
logger.exception("Failed to recreate sandbox after connection failure")
|
||||||
await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": None})
|
await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": None})
|
||||||
raise
|
raise
|
||||||
return sandbox_backend, repo_dir
|
return sandbox_backend
|
||||||
|
|
||||||
|
|
||||||
|
async def check_or_recreate_sandbox(
|
||||||
|
sandbox_backend: SandboxBackendProtocol, thread_id: str
|
||||||
|
) -> SandboxBackendProtocol:
|
||||||
|
"""Check if a cached sandbox is reachable; recreate it if not.
|
||||||
|
|
||||||
|
Pings the sandbox with a lightweight command. If the sandbox is
|
||||||
|
unreachable (SandboxClientError), it is torn down and a fresh one
|
||||||
|
is created via _recreate_sandbox.
|
||||||
|
|
||||||
|
Returns the original backend if healthy, or a new one if recreated.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
await asyncio.to_thread(sandbox_backend.execute, "echo ok")
|
||||||
|
except SandboxClientError:
|
||||||
|
logger.warning(
|
||||||
|
"Cached sandbox is no longer reachable for thread %s, recreating",
|
||||||
|
thread_id,
|
||||||
|
)
|
||||||
|
sandbox_backend = await _recreate_sandbox(thread_id)
|
||||||
|
return sandbox_backend
|
||||||
|
|
||||||
|
|
||||||
async def _wait_for_sandbox_id(thread_id: str) -> str:
|
async def _wait_for_sandbox_id(thread_id: str) -> str:
|
||||||
|
|
@ -269,16 +187,12 @@ DEFAULT_LLM_MODEL_ID = "anthropic:claude-opus-4-6"
|
||||||
DEFAULT_RECURSION_LIMIT = 1_000
|
DEFAULT_RECURSION_LIMIT = 1_000
|
||||||
|
|
||||||
|
|
||||||
async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
async def get_agent(config: RunnableConfig) -> Pregel:
|
||||||
"""Get or create an agent with a sandbox for the given thread."""
|
"""Get or create an agent with a sandbox for the given thread."""
|
||||||
thread_id = config["configurable"].get("thread_id", None)
|
thread_id = config["configurable"].get("thread_id", None)
|
||||||
|
|
||||||
config["recursion_limit"] = DEFAULT_RECURSION_LIMIT
|
config["recursion_limit"] = DEFAULT_RECURSION_LIMIT
|
||||||
|
|
||||||
repo_config = config["configurable"].get("repo", {})
|
|
||||||
repo_owner = repo_config.get("owner")
|
|
||||||
repo_name = repo_config.get("name")
|
|
||||||
|
|
||||||
if thread_id is None or not graph_loaded_for_execution(config):
|
if thread_id is None or not graph_loaded_for_execution(config):
|
||||||
logger.info("No thread_id or not for execution, returning agent without sandbox")
|
logger.info("No thread_id or not for execution, returning agent without sandbox")
|
||||||
return create_deep_agent(
|
return create_deep_agent(
|
||||||
|
|
@ -298,28 +212,8 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
||||||
|
|
||||||
if sandbox_backend:
|
if sandbox_backend:
|
||||||
logger.info("Using cached sandbox backend for thread %s", thread_id)
|
logger.info("Using cached sandbox backend for thread %s", thread_id)
|
||||||
metadata = get_config().get("metadata", {})
|
|
||||||
repo_dir = metadata.get("repo_dir")
|
|
||||||
|
|
||||||
await _refresh_github_proxy(sandbox_backend)
|
await _refresh_github_proxy(sandbox_backend)
|
||||||
|
sandbox_backend = await check_or_recreate_sandbox(sandbox_backend, thread_id)
|
||||||
if repo_owner and repo_name:
|
|
||||||
logger.info("Pulling latest changes for repo %s/%s", repo_owner, repo_name)
|
|
||||||
try:
|
|
||||||
repo_dir = await _clone_or_pull_repo_in_sandbox(
|
|
||||||
sandbox_backend, repo_owner, repo_name
|
|
||||||
)
|
|
||||||
except SandboxClientError:
|
|
||||||
logger.warning(
|
|
||||||
"Cached sandbox is no longer reachable for thread %s, recreating sandbox",
|
|
||||||
thread_id,
|
|
||||||
)
|
|
||||||
sandbox_backend, repo_dir = await _recreate_sandbox(
|
|
||||||
thread_id, repo_owner, repo_name
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to pull repo in cached sandbox")
|
|
||||||
raise
|
|
||||||
|
|
||||||
elif sandbox_id is None:
|
elif sandbox_id is None:
|
||||||
logger.info("Creating new sandbox for thread %s", thread_id)
|
logger.info("Creating new sandbox for thread %s", thread_id)
|
||||||
|
|
@ -328,21 +222,8 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
||||||
try:
|
try:
|
||||||
sandbox_backend = await _create_sandbox_with_proxy()
|
sandbox_backend = await _create_sandbox_with_proxy()
|
||||||
logger.info("Sandbox created: %s", sandbox_backend.id)
|
logger.info("Sandbox created: %s", sandbox_backend.id)
|
||||||
|
|
||||||
repo_dir = None
|
|
||||||
if repo_owner and repo_name:
|
|
||||||
logger.info("Cloning repo %s/%s into sandbox", repo_owner, repo_name)
|
|
||||||
repo_dir = await _clone_or_pull_repo_in_sandbox(
|
|
||||||
sandbox_backend, repo_owner, repo_name
|
|
||||||
)
|
|
||||||
logger.info("Repo cloned to %s", repo_dir)
|
|
||||||
|
|
||||||
await client.threads.update(
|
|
||||||
thread_id=thread_id,
|
|
||||||
metadata={"repo_dir": repo_dir},
|
|
||||||
)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.exception("Failed to create sandbox or clone repo")
|
logger.exception("Failed to create sandbox")
|
||||||
try:
|
try:
|
||||||
await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": None})
|
await client.threads.update(thread_id=thread_id, metadata={"sandbox_id": None})
|
||||||
logger.info("Reset sandbox_id to None for thread %s", thread_id)
|
logger.info("Reset sandbox_id to None for thread %s", thread_id)
|
||||||
|
|
@ -371,56 +252,26 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
||||||
raise
|
raise
|
||||||
|
|
||||||
await _refresh_github_proxy(sandbox_backend)
|
await _refresh_github_proxy(sandbox_backend)
|
||||||
|
sandbox_backend = await check_or_recreate_sandbox(sandbox_backend, thread_id)
|
||||||
metadata = get_config().get("metadata", {})
|
|
||||||
repo_dir = metadata.get("repo_dir")
|
|
||||||
|
|
||||||
if repo_owner and repo_name:
|
|
||||||
logger.info("Pulling latest changes for repo %s/%s", repo_owner, repo_name)
|
|
||||||
try:
|
|
||||||
repo_dir = await _clone_or_pull_repo_in_sandbox(
|
|
||||||
sandbox_backend, repo_owner, repo_name
|
|
||||||
)
|
|
||||||
except SandboxClientError:
|
|
||||||
logger.warning(
|
|
||||||
"Existing sandbox is no longer reachable for thread %s, recreating sandbox",
|
|
||||||
thread_id,
|
|
||||||
)
|
|
||||||
sandbox_backend, repo_dir = await _recreate_sandbox(
|
|
||||||
thread_id, repo_owner, repo_name
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to pull repo in existing sandbox")
|
|
||||||
raise
|
|
||||||
|
|
||||||
SANDBOX_BACKENDS[thread_id] = sandbox_backend
|
SANDBOX_BACKENDS[thread_id] = sandbox_backend
|
||||||
|
|
||||||
if not repo_dir:
|
if sandbox_id != sandbox_backend.id:
|
||||||
msg = "Cannot proceed: no repo was cloned. Set 'repo.owner' and 'repo.name' in the configurable config"
|
await client.threads.update(
|
||||||
raise RuntimeError(msg)
|
thread_id=thread_id,
|
||||||
|
metadata={"sandbox_id": sandbox_backend.id},
|
||||||
branch_name = get_config().get("metadata", {}).get("branch_name")
|
)
|
||||||
if branch_name:
|
|
||||||
logger.info("Checking out branch '%s' in sandbox for thread %s", branch_name, thread_id)
|
await asyncio.to_thread(
|
||||||
loop = asyncio.get_event_loop()
|
sandbox_backend.execute,
|
||||||
safe_repo_dir = shlex.quote(repo_dir)
|
"git config --global user.name 'open-swe[bot]' && git config --global user.email 'open-swe@users.noreply.github.com'",
|
||||||
safe_branch = shlex.quote(branch_name)
|
|
||||||
checkout_result = await loop.run_in_executor(
|
|
||||||
None,
|
|
||||||
sandbox_backend.execute,
|
|
||||||
f"cd {safe_repo_dir} && git fetch origin && git checkout {safe_branch}",
|
|
||||||
)
|
)
|
||||||
if checkout_result.exit_code != 0:
|
|
||||||
logger.warning(
|
|
||||||
"Failed to checkout branch '%s': %s",
|
|
||||||
branch_name,
|
|
||||||
checkout_result.output[:200] if checkout_result.output else "",
|
|
||||||
)
|
|
||||||
|
|
||||||
linear_issue = config["configurable"].get("linear_issue", {})
|
linear_issue = config["configurable"].get("linear_issue", {})
|
||||||
linear_project_id = linear_issue.get("linear_project_id", "")
|
linear_project_id = linear_issue.get("linear_project_id", "")
|
||||||
linear_issue_number = linear_issue.get("linear_issue_number", "")
|
linear_issue_number = linear_issue.get("linear_issue_number", "")
|
||||||
agents_md = await read_agents_md_in_sandbox(sandbox_backend, repo_dir)
|
|
||||||
|
work_dir = await aresolve_sandbox_work_dir(sandbox_backend)
|
||||||
|
|
||||||
logger.info("Returning agent with sandbox for thread %s", thread_id)
|
logger.info("Returning agent with sandbox for thread %s", thread_id)
|
||||||
return create_deep_agent(
|
return create_deep_agent(
|
||||||
|
|
@ -430,15 +281,16 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
|
||||||
max_tokens=20_000,
|
max_tokens=20_000,
|
||||||
),
|
),
|
||||||
system_prompt=construct_system_prompt(
|
system_prompt=construct_system_prompt(
|
||||||
repo_dir,
|
working_dir=work_dir,
|
||||||
linear_project_id=linear_project_id,
|
linear_project_id=linear_project_id,
|
||||||
linear_issue_number=linear_issue_number,
|
linear_issue_number=linear_issue_number,
|
||||||
agents_md=agents_md,
|
|
||||||
),
|
),
|
||||||
tools=[
|
tools=[
|
||||||
http_request,
|
http_request,
|
||||||
fetch_url,
|
fetch_url,
|
||||||
web_search,
|
web_search,
|
||||||
|
list_repos,
|
||||||
|
get_branch_name,
|
||||||
commit_and_open_pr,
|
commit_and_open_pr,
|
||||||
linear_comment,
|
linear_comment,
|
||||||
linear_create_issue,
|
linear_create_issue,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
from .commit_and_open_pr import commit_and_open_pr
|
from .commit_and_open_pr import commit_and_open_pr
|
||||||
from .fetch_url import fetch_url
|
from .fetch_url import fetch_url
|
||||||
|
from .get_branch_name import get_branch_name
|
||||||
from .github_comment import github_comment
|
from .github_comment import github_comment
|
||||||
from .github_review import (
|
from .github_review import (
|
||||||
create_pr_review,
|
create_pr_review,
|
||||||
|
|
@ -18,6 +19,7 @@ from .linear_get_issue import linear_get_issue
|
||||||
from .linear_get_issue_comments import linear_get_issue_comments
|
from .linear_get_issue_comments import linear_get_issue_comments
|
||||||
from .linear_list_teams import linear_list_teams
|
from .linear_list_teams import linear_list_teams
|
||||||
from .linear_update_issue import linear_update_issue
|
from .linear_update_issue import linear_update_issue
|
||||||
|
from .list_repos import list_repos
|
||||||
from .slack_thread_reply import slack_thread_reply
|
from .slack_thread_reply import slack_thread_reply
|
||||||
from .web_search import web_search
|
from .web_search import web_search
|
||||||
|
|
||||||
|
|
@ -26,6 +28,7 @@ __all__ = [
|
||||||
"create_pr_review",
|
"create_pr_review",
|
||||||
"dismiss_pr_review",
|
"dismiss_pr_review",
|
||||||
"fetch_url",
|
"fetch_url",
|
||||||
|
"get_branch_name",
|
||||||
"get_pr_review",
|
"get_pr_review",
|
||||||
"github_comment",
|
"github_comment",
|
||||||
"http_request",
|
"http_request",
|
||||||
|
|
@ -38,6 +41,7 @@ __all__ = [
|
||||||
"linear_get_issue_comments",
|
"linear_get_issue_comments",
|
||||||
"linear_list_teams",
|
"linear_list_teams",
|
||||||
"linear_update_issue",
|
"linear_update_issue",
|
||||||
|
"list_repos",
|
||||||
"slack_thread_reply",
|
"slack_thread_reply",
|
||||||
"submit_pr_review",
|
"submit_pr_review",
|
||||||
"update_pr_review",
|
"update_pr_review",
|
||||||
|
|
|
||||||
18
agent/tools/get_branch_name.py
Normal file
18
agent/tools/get_branch_name.py
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from langgraph.config import get_config
|
||||||
|
|
||||||
|
|
||||||
|
def get_branch_name() -> dict[str, Any]:
|
||||||
|
"""Return the git branch name for this thread.
|
||||||
|
|
||||||
|
Returns the branch_name from thread metadata if set,
|
||||||
|
otherwise falls back to open-swe/{thread_id}.
|
||||||
|
"""
|
||||||
|
config = get_config()
|
||||||
|
metadata = config.get("metadata", {})
|
||||||
|
branch_name = metadata.get("branch_name")
|
||||||
|
if not branch_name:
|
||||||
|
thread_id = config.get("configurable", {}).get("thread_id", "unknown")
|
||||||
|
branch_name = f"open-swe/{thread_id}"
|
||||||
|
return {"branch_name": branch_name}
|
||||||
59
agent/tools/list_repos.py
Normal file
59
agent/tools/list_repos.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from ..utils.github_app import get_github_app_installation_token
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def list_repos(
|
||||||
|
organization_name: str,
|
||||||
|
is_organization: bool = True,
|
||||||
|
page: int = 1,
|
||||||
|
per_page: int = 100,
|
||||||
|
sort: str = "updated",
|
||||||
|
name_filter: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""List GitHub repositories for an organization or user via the GitHub API.
|
||||||
|
|
||||||
|
Uses /orgs/{name}/repos for organizations and /users/{name}/repos for
|
||||||
|
personal user accounts, based on the is_organization flag.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
organization_name: The GitHub organization or username to list repos for.
|
||||||
|
is_organization: If True, uses the /orgs/ endpoint. If False, uses the
|
||||||
|
/users/ endpoint for personal accounts. Default: True.
|
||||||
|
page: Page number to fetch (default: 1).
|
||||||
|
per_page: Number of repos per page, max 100 (default: 100).
|
||||||
|
sort: Sort field — "updated", "created", "pushed", or "full_name" (default: "updated").
|
||||||
|
name_filter: Optional substring to filter repo names by (case-insensitive).
|
||||||
|
|
||||||
|
If unsure which repo to use, ask the user for confirmation.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
headers = {"Accept": "application/vnd.github+json"}
|
||||||
|
token = await get_github_app_installation_token()
|
||||||
|
if token:
|
||||||
|
headers["Authorization"] = f"Bearer {token}"
|
||||||
|
path_prefix = "orgs" if is_organization else "users"
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
response = await client.get(
|
||||||
|
f"https://api.github.com/{path_prefix}/{organization_name}/repos",
|
||||||
|
headers=headers,
|
||||||
|
params={"per_page": min(per_page, 100), "sort": sort, "page": page},
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
if response.status_code == 200:
|
||||||
|
repos: list[str] = [r["name"] for r in response.json()]
|
||||||
|
if name_filter:
|
||||||
|
repos = [r for r in repos if name_filter.lower() in r.lower()]
|
||||||
|
|
||||||
|
has_next = 'rel="next"' in response.headers.get("link", "")
|
||||||
|
result: dict[str, Any] = {"repos": repos, "page": page, "has_next_page": has_next}
|
||||||
|
return result
|
||||||
|
return {"error": f"GitHub API returned status {response.status_code}"}
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Failed to fetch repos for %s", organization_name)
|
||||||
|
return {"error": f"Failed to fetch repos for {organization_name}"}
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
"""Helpers for reading agent instructions from AGENTS.md."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
import shlex
|
|
||||||
|
|
||||||
from deepagents.backends.protocol import SandboxBackendProtocol
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
async def read_agents_md_in_sandbox(
|
|
||||||
sandbox_backend: SandboxBackendProtocol,
|
|
||||||
repo_dir: str | None,
|
|
||||||
) -> str | None:
|
|
||||||
"""Read AGENTS.md from the repo root if it exists."""
|
|
||||||
if not repo_dir:
|
|
||||||
return None
|
|
||||||
|
|
||||||
safe_agents_path = shlex.quote(f"{repo_dir}/AGENTS.md")
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
result = await loop.run_in_executor(
|
|
||||||
None,
|
|
||||||
sandbox_backend.execute,
|
|
||||||
f"test -f {safe_agents_path} && cat {safe_agents_path}",
|
|
||||||
)
|
|
||||||
if result.exit_code != 0:
|
|
||||||
logger.debug("AGENTS.md not found at %s", safe_agents_path)
|
|
||||||
return None
|
|
||||||
content = result.output or ""
|
|
||||||
content = content.strip()
|
|
||||||
return content or None
|
|
||||||
|
|
@ -23,21 +23,6 @@ def _run_git(
|
||||||
return sandbox_backend.execute(f"cd {safe_repo_dir} && {command}")
|
return sandbox_backend.execute(f"cd {safe_repo_dir} && {command}")
|
||||||
|
|
||||||
|
|
||||||
def is_valid_git_repo(sandbox_backend: SandboxBackendProtocol, repo_dir: str) -> bool:
|
|
||||||
"""Check if directory is a valid git repository."""
|
|
||||||
git_dir = f"{repo_dir}/.git"
|
|
||||||
safe_git_dir = shlex.quote(git_dir)
|
|
||||||
result = sandbox_backend.execute(f"test -d {safe_git_dir} && echo exists")
|
|
||||||
return result.exit_code == 0 and "exists" in result.output
|
|
||||||
|
|
||||||
|
|
||||||
def remove_directory(sandbox_backend: SandboxBackendProtocol, repo_dir: str) -> bool:
|
|
||||||
"""Remove a directory and all its contents."""
|
|
||||||
safe_repo_dir = shlex.quote(repo_dir)
|
|
||||||
result = sandbox_backend.execute(f"rm -rf {safe_repo_dir}")
|
|
||||||
return result.exit_code == 0
|
|
||||||
|
|
||||||
|
|
||||||
def git_has_uncommitted_changes(sandbox_backend: SandboxBackendProtocol, repo_dir: str) -> bool:
|
def git_has_uncommitted_changes(sandbox_backend: SandboxBackendProtocol, repo_dir: str) -> bool:
|
||||||
"""Check whether the repo has uncommitted changes."""
|
"""Check whether the repo has uncommitted changes."""
|
||||||
result = _run_git(sandbox_backend, repo_dir, "git status --porcelain")
|
result = _run_git(sandbox_backend, repo_dir, "git status --porcelain")
|
||||||
|
|
|
||||||
|
|
@ -383,7 +383,11 @@ async def extract_pr_context(
|
||||||
return repo_config, pr_number, branch_name, github_login, pr_url, comment_id, node_id
|
return repo_config, pr_number, branch_name, github_login, pr_url, comment_id, node_id
|
||||||
|
|
||||||
|
|
||||||
def build_pr_prompt(comments: list[dict[str, Any]], pr_url: str) -> str:
|
def build_pr_prompt(
|
||||||
|
comments: list[dict[str, Any]],
|
||||||
|
pr_url: str,
|
||||||
|
repo_config: dict[str, str] | None = None,
|
||||||
|
) -> str:
|
||||||
"""Format PR comments into a human message for the agent."""
|
"""Format PR comments into a human message for the agent."""
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
for c in comments:
|
for c in comments:
|
||||||
|
|
@ -398,8 +402,12 @@ def build_pr_prompt(comments: list[dict[str, Any]], pr_url: str) -> str:
|
||||||
lines.append(f"\n**{author}**:\n{body}\n")
|
lines.append(f"\n**{author}**:\n{body}\n")
|
||||||
|
|
||||||
comments_text = "".join(lines)
|
comments_text = "".join(lines)
|
||||||
|
repo_line = ""
|
||||||
|
if repo_config:
|
||||||
|
repo_line = f"## Repository: {repo_config.get('owner')}/{repo_config.get('name')}\n\n"
|
||||||
return (
|
return (
|
||||||
"You've been tagged in GitHub PR comments. Please resolve them.\n\n"
|
"You've been tagged in GitHub PR comments. Please resolve them.\n\n"
|
||||||
|
f"{repo_line}"
|
||||||
f"PR: {pr_url}\n\n"
|
f"PR: {pr_url}\n\n"
|
||||||
f"## Comments:\n{comments_text}\n\n"
|
f"## Comments:\n{comments_text}\n\n"
|
||||||
"If code changes are needed:\n"
|
"If code changes are needed:\n"
|
||||||
|
|
|
||||||
|
|
@ -1317,7 +1317,7 @@ async def process_github_pr_comment(payload: dict[str, Any], event_type: str) ->
|
||||||
logger.info("No comments found since last @open-swe tag for PR %s", pr_number)
|
logger.info("No comments found since last @open-swe tag for PR %s", pr_number)
|
||||||
return
|
return
|
||||||
|
|
||||||
prompt = build_pr_prompt(comments, pr_url)
|
prompt = build_pr_prompt(comments, pr_url, repo_config=repo_config)
|
||||||
await _trigger_or_queue_run(
|
await _trigger_or_queue_run(
|
||||||
thread_id,
|
thread_id,
|
||||||
prompt,
|
prompt,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ def test_build_pr_prompt_wraps_external_comments_without_trust_section() -> None
|
||||||
|
|
||||||
|
|
||||||
def test_construct_system_prompt_includes_untrusted_comment_guidance() -> None:
|
def test_construct_system_prompt_includes_untrusted_comment_guidance() -> None:
|
||||||
prompt = construct_system_prompt("/workspace/open-swe")
|
prompt = construct_system_prompt(working_dir="/workspace")
|
||||||
|
|
||||||
assert "External Untrusted Comments" in prompt
|
assert "External Untrusted Comments" in prompt
|
||||||
assert github_comments.UNTRUSTED_GITHUB_COMMENT_OPEN_TAG in prompt
|
assert github_comments.UNTRUSTED_GITHUB_COMMENT_OPEN_TAG in prompt
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,6 @@ class TestRefreshProxyOnSandboxReuse:
|
||||||
mock_sandbox = MagicMock(id="sandbox-cached")
|
mock_sandbox = MagicMock(id="sandbox-cached")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("agent.server.get_config", return_value=config),
|
|
||||||
patch(
|
patch(
|
||||||
"agent.server.resolve_github_token",
|
"agent.server.resolve_github_token",
|
||||||
new_callable=AsyncMock,
|
new_callable=AsyncMock,
|
||||||
|
|
@ -216,14 +215,14 @@ class TestRefreshProxyOnSandboxReuse:
|
||||||
),
|
),
|
||||||
patch("agent.server._configure_github_proxy") as mock_proxy,
|
patch("agent.server._configure_github_proxy") as mock_proxy,
|
||||||
patch(
|
patch(
|
||||||
"agent.server._clone_or_pull_repo_in_sandbox",
|
"agent.server.aresolve_sandbox_work_dir",
|
||||||
new_callable=AsyncMock,
|
new_callable=AsyncMock,
|
||||||
return_value="/workspace/open-swe",
|
return_value="/workspace",
|
||||||
),
|
),
|
||||||
patch(
|
patch(
|
||||||
"agent.server.read_agents_md_in_sandbox",
|
"agent.server.check_or_recreate_sandbox",
|
||||||
new_callable=AsyncMock,
|
new_callable=AsyncMock,
|
||||||
return_value="",
|
return_value=mock_sandbox,
|
||||||
),
|
),
|
||||||
patch("agent.server.make_model", return_value=MagicMock()),
|
patch("agent.server.make_model", return_value=MagicMock()),
|
||||||
patch("agent.server.construct_system_prompt", return_value="prompt"),
|
patch("agent.server.construct_system_prompt", return_value="prompt"),
|
||||||
|
|
@ -248,7 +247,6 @@ class TestRefreshProxyOnSandboxReuse:
|
||||||
mock_sandbox = MagicMock(id="sandbox-existing")
|
mock_sandbox = MagicMock(id="sandbox-existing")
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch("agent.server.get_config", return_value=config),
|
|
||||||
patch(
|
patch(
|
||||||
"agent.server.resolve_github_token",
|
"agent.server.resolve_github_token",
|
||||||
new_callable=AsyncMock,
|
new_callable=AsyncMock,
|
||||||
|
|
@ -267,14 +265,9 @@ class TestRefreshProxyOnSandboxReuse:
|
||||||
),
|
),
|
||||||
patch("agent.server._configure_github_proxy") as mock_proxy,
|
patch("agent.server._configure_github_proxy") as mock_proxy,
|
||||||
patch(
|
patch(
|
||||||
"agent.server._clone_or_pull_repo_in_sandbox",
|
"agent.server.aresolve_sandbox_work_dir",
|
||||||
new_callable=AsyncMock,
|
new_callable=AsyncMock,
|
||||||
return_value="/workspace/open-swe",
|
return_value="/workspace",
|
||||||
),
|
|
||||||
patch(
|
|
||||||
"agent.server.read_agents_md_in_sandbox",
|
|
||||||
new_callable=AsyncMock,
|
|
||||||
return_value="",
|
|
||||||
),
|
),
|
||||||
patch("agent.server.make_model", return_value=MagicMock()),
|
patch("agent.server.make_model", return_value=MagicMock()),
|
||||||
patch("agent.server.construct_system_prompt", return_value="prompt"),
|
patch("agent.server.construct_system_prompt", return_value="prompt"),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue