Merge pull request #902 from langchain-ai/yogesh/generated_PR_and_custom_prompting

feat: add commit_and_open_pr tool and custom agent prompting [closes #892, closes #887]
This commit is contained in:
Aran Yogesh 2026-02-07 17:28:33 -08:00 • committed by GitHub
commit 1c847d56ab
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 230 additions and 17 deletions

View file

@ -4,11 +4,80 @@ You are operating in a **remote Linux sandbox** at `{working_dir}`.
All code execution and file operations happen in this sandbox environment.
**Important:**
- Use `{working_dir}` as your working directory for all operations
### Code Style
- NEVER add inline comments to code
- Any docstrings on functions you add or modify must be VERY concise (1 line preferred)
### Installing Dependencies
If the repository needs dependency installation, run:
```
uv run poe install-deps
```
### Committing Changes and Opening Pull Requests
When you have completed your implementation, follow these steps in order:
1. **Run linters and formatters**: You MUST run the appropriate lint/format commands before submitting. Determine which languages are in the repo and run the corresponding commands:
**Python** (if repo contains `.py` files):
- `make format` then `make lint`
**Frontend / TypeScript / JavaScript** (if repo contains `package.json`):
- `yarn format` then `yarn lint`
**Go** (if repo contains `.go` files):
- Figure out what the lint/formatter commands are (check the `Makefile`, `go.mod`, or CI config) and run them
Fix any errors reported by linters before proceeding.
2. **Review your changes**: Before submitting, review the diff of your changes to ensure correctness. Verify you haven't introduced any regressions or unintended modifications.
3. **Submit via `commit_and_open_pr` tool**: Call this tool as the final step. It will commit all changes, push to a branch, and create a pull request.
**PR Title** (keep under 70 characters):
```
<type>: <concise description> [closes {linear_project_id}-{linear_issue_number}]
```
Where type is one of: `fix` (bug fix), `feat` (new feature), `chore` (maintenance), `ci` (CI/CD)
**PR Body**:
```
## Description
<Explain WHY this PR is needed, list the changes, and reference the Linear issue>
## Test Plan
- [ ] <specific verification step>
```
**Commit message**: Should be concise and focus on the "why" rather than the "what". If not provided, the PR title is used.
Always call `commit_and_open_pr` as the final step once your implementation is complete and code quality checks pass.
"""
def construct_system_prompt(working_dir: str) -> str:
return SYSTEM_PROMPT.format(working_dir=working_dir)
def construct_system_prompt(
working_dir: str,
linear_project_id: str = "",
linear_issue_number: str = "",
) -> str:
return SYSTEM_PROMPT.format(
working_dir=working_dir,
linear_project_id=linear_project_id or "<PROJECT_ID>",
linear_issue_number=linear_issue_number or "<ISSUE_NUMBER>",
)

View file

@ -30,9 +30,10 @@ from deepagents.backends.sandbox import SandboxBackendProtocol
# Local import for encryption
from langchain_anthropic import ChatAnthropic
from .encryption import decrypt_token
from .prompt import construct_system_prompt
from .tools import fetch_url, http_request
from .tools import commit_and_open_pr, fetch_url, http_request
def _get_langsmith_api_key() -> str | None:
@ -456,6 +457,28 @@ async def post_to_linear_after_model( # noqa: PLR0911, PLR0912
return None
def _extract_pr_params_from_messages(messages: list) -> dict[str, str] | None:
"""Extract PR title/body/commit_message from the last commit_and_open_pr tool result."""
for msg in reversed(messages):
if isinstance(msg, dict):
content = msg.get("content", "")
name = msg.get("name", "")
else:
content = getattr(msg, "content", "")
name = getattr(msg, "name", "")
if name == "commit_and_open_pr" and content:
import json as _json
try:
parsed = _json.loads(content) if isinstance(content, str) else content
if isinstance(parsed, dict) and "title" in parsed:
return parsed
except (ValueError, TypeError):
pass
return None
@after_agent
async def open_pr_if_needed( # noqa: PLR0912, PLR0915
state: AgentState,
@ -465,7 +488,6 @@ async def open_pr_if_needed( # noqa: PLR0912, PLR0915
logger.info("After-agent middleware started")
pr_url = None
pr_number = None
pr_title = "feat: Open SWE PR"
try:
config = get_config()
@ -485,6 +507,21 @@ async def open_pr_if_needed( # noqa: PLR0912, PLR0915
linear_issue = configurable.get("linear_issue", {})
linear_issue_id = linear_issue.get("id")
pr_params = _extract_pr_params_from_messages(messages)
if not pr_params:
logger.info("No commit_and_open_pr tool call found, skipping PR creation")
if linear_issue_id and last_message_content:
comment = f""" **Agent Response**
{last_message_content}"""
await comment_on_linear_issue(linear_issue_id, comment)
return None
pr_title = pr_params.get("title", "feat: Open SWE PR")
pr_body = pr_params.get("body", "Automated PR created by Open SWE agent.")
commit_message = pr_params.get("commit_message", pr_title)
if not thread_id:
if linear_issue_id and last_message_content:
comment = f"""🤖 **Agent Response**
@ -564,8 +601,9 @@ async def open_pr_if_needed( # noqa: PLR0912, PLR0915
await asyncio.to_thread(sandbox_backend.execute, f"cd {repo_dir} && git add -A")
safe_commit_msg = commit_message.replace("'", "'\\''")
await asyncio.to_thread(
sandbox_backend.execute, f'cd {repo_dir} && git commit -m "feat: Open SWE PR"'
sandbox_backend.execute, f"cd {repo_dir} && git commit -m '{safe_commit_msg}'"
)
encrypted_token = configurable.get("github_token_encrypted")
@ -579,7 +617,6 @@ async def open_pr_if_needed( # noqa: PLR0912, PLR0915
if remote_result.exit_code == 0:
remote_url = remote_result.output.strip()
if "github.com" in remote_url and "@" not in remote_url:
# Convert https://github.com/owner/repo.git to https://git:token@github.com/owner/repo.git
auth_url = remote_url.replace("https://", f"https://git:{github_token}@")
await asyncio.to_thread(
sandbox_backend.execute,
@ -590,13 +627,9 @@ async def open_pr_if_needed( # noqa: PLR0912, PLR0915
sandbox_backend.execute, f"cd {repo_dir} && git push origin {target_branch}"
)
# Get default branch from GitHub API (most reliable method)
base_branch = await get_github_default_branch(repo_owner, repo_name, github_token)
logger.info("Using base branch: %s", base_branch)
pr_title = "feat: Open SWE PR"
pr_body = "Automated PR created by Open SWE agent."
pr_url, pr_number = await create_github_pr(
repo_owner=repo_owner,
repo_name=repo_name,
@ -911,13 +944,19 @@ async def get_agent(config: RunnableConfig) -> Pregel: # noqa: PLR0915
_SANDBOX_BACKENDS[thread_id] = sandbox_backend
linear_issue = config["configurable"].get("linear_issue", {})
linear_project_id = linear_issue.get("linear_project_id", "")
linear_issue_number = linear_issue.get("linear_issue_number", "")
logger.info("Returning agent with sandbox for thread %s", thread_id)
return create_deep_agent(
model=ChatAnthropic(
model="claude-opus-4-6", max_tokens=20_000
), # TODO: Actually pass a model here
system_prompt=construct_system_prompt(repo_dir),
tools=[http_request, fetch_url],
model=ChatAnthropic(model="claude-opus-4-6", max_tokens=20_000),
system_prompt=construct_system_prompt(
repo_dir,
linear_project_id=linear_project_id,
linear_issue_number=linear_issue_number,
),
tools=[http_request, fetch_url, commit_and_open_pr],
backend=sandbox_backend,
middleware=[
check_message_queue_before_model,

View file

@ -1,4 +1,5 @@
from .commit_and_open_pr import commit_and_open_pr
from .fetch_url import fetch_url
from .http_request import http_request
__all__ = ["fetch_url", "http_request"]
__all__ = ["commit_and_open_pr", "fetch_url", "http_request"]

View file

@ -0,0 +1,94 @@
import logging
from typing import Any
logger = logging.getLogger(__name__)
def commit_and_open_pr(
title: str,
body: str,
commit_message: str | None = None,
) -> dict[str, Any]:
"""Commit all current changes and open a GitHub Pull Request.
You MUST call this tool when you have completed your work and want to
submit your changes for review. This is the final step in your workflow.
Before calling this tool, ensure you have:
1. Reviewed your changes for correctness
2. Run `make format` and `make lint` if a Makefile exists in the repo root
## Title Format (REQUIRED — keep under 70 characters)
The PR title MUST follow this exact format:
<type>: <short lowercase description> [closes <PROJECT_ID>-<ISSUE_NUMBER>]
The description MUST be entirely lowercase (no capital letters).
Where <type> is one of:
- fix: for bug fixes
- feat: for new features
- chore: for maintenance tasks (deps, configs, cleanup)
- ci: for CI/CD changes
The [closes ...] suffix links and auto-closes the Linear ticket.
Use the linear_project_id and linear_issue_number from your context.
Examples:
- "fix: resolve null pointer in user auth [closes AA-123]"
- "feat: add dark mode toggle to settings [closes ENG-456]"
- "chore: upgrade dependencies to latest versions [closes OPS-789]"
## Body Format (REQUIRED)
The PR body MUST follow this exact template:
## Description
<Explain WHY this PR is needed. Include:
- List of changes made
- Reference to the Linear issue or design docs
- Any context on the approach taken>
## Test Plan
- [ ] <specific test step 1>
- [ ] <specific test step 2>
Example body:
## Description
Fixes the null pointer exception that occurs when a user without
a profile attempts to authenticate. The root cause was a missing
null check in the `getProfile` method.
Changes:
- Added null check in `auth/getProfile.ts`
- Added fallback default profile object
- Updated related unit tests
Resolves AA-123
## Test Plan
- [ ] Verify login works for users without profiles
- [ ] Verify existing users are unaffected
- [ ] Run `yarn test` and confirm all tests pass
## Commit Message
The commit message should be concise (1-2 sentences) and focus on the "why"
rather than the "what". Summarize the nature of the changes: new feature,
bug fix, refactoring, etc. If not provided, the PR title is used.
Args:
title: PR title following the format above (e.g. "fix: resolve auth bug [closes AA-123]")
body: PR description following the template above with ## Description and ## Test Plan
commit_message: Optional git commit message. If not provided, the PR title is used.
Returns:
Dictionary with the result of the operation including PR URL if successful.
"""
return {
"title": title,
"body": body,
"commit_message": commit_message or title,
}

View file

@ -552,13 +552,23 @@ async def process_linear_issue( # noqa: PLR0912, PLR0915
"When you're done, commit and push your changes."
)
identifier = full_issue.get("identifier", "") or issue_data.get("identifier", "")
linear_project_id = ""
linear_issue_number = ""
if identifier and "-" in identifier:
parts = identifier.split("-", 1)
linear_project_id = parts[0]
linear_issue_number = parts[1]
configurable: dict[str, Any] = {
"repo": repo_config,
"linear_issue": {
"id": issue_id,
"title": title,
"url": full_issue.get("url", "") or issue_data.get("url", ""),
"identifier": full_issue.get("identifier", "") or issue_data.get("identifier", ""),
"identifier": identifier,
"linear_project_id": linear_project_id,
"linear_issue_number": linear_issue_number,
},
}
if github_token: