refactor: Prompt improvements (#148)

* refactor: Prompt improvements

* format n lint

* cleanup
This commit is contained in:
Brace Sproul 2025-06-13 10:14:33 -07:00 • committed by GitHub
parent 713686a250
commit 0bfbf99db9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 539 additions and 380 deletions

View file

@ -0,0 +1,101 @@
import {
GraphState,
GraphConfig,
GraphUpdate,
} from "@open-swe/shared/open-swe/types";
import { loadModel, Task } from "../../utils/load-model.js";
import {
createShellTool,
createApplyPatchTool,
requestHumanHelpTool,
updatePlanTool,
} from "../../tools/index.js";
import { getRepoAbsolutePath } from "../../utils/git.js";
import { formatPlanPrompt } from "../../utils/plan-prompt.js";
import { stopSandbox } from "../../utils/sandbox.js";
import { createLogger, LogLevel } from "../../utils/logger.js";
import { getCurrentPlanItem } from "../../utils/current-task.js";
import { getMessageContentString } from "@open-swe/shared/messages";
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
import { SYSTEM_PROMPT } from "./prompt.js";
const logger = createLogger(LogLevel.INFO, "GenerateMessageNode");
const formatPrompt = (state: GraphState): string => {
const repoDirectory = getRepoAbsolutePath(state.targetRepository);
const activePlanItems = getActivePlanItems(state.plan);
const currentPlanItem = activePlanItems
.filter((p) => !p.completed)
.sort((a, b) => a.index - b.index)[0];
return SYSTEM_PROMPT.replaceAll(
"{PLAN_PROMPT_WITH_SUMMARIES}",
formatPlanPrompt(getActivePlanItems(state.plan), {
includeSummaries: true,
}),
)
.replaceAll(
"{PLAN_PROMPT}",
formatPlanPrompt(getActivePlanItems(state.plan)),
)
.replaceAll("{REPO_DIRECTORY}", repoDirectory)
.replaceAll(
"{PLAN_GENERATION_SUMMARY}",
`<plan-generation-summary>\n${state.planContextSummary}\n</plan-generation-summary>`,
)
.replaceAll(
"{CODEBASE_TREE}",
state.codebaseTree || "No codebase tree generated yet.",
)
.replaceAll("{CURRENT_WORKING_DIRECTORY}", repoDirectory)
.replaceAll("{CURRENT_TASK_NUMBER}", currentPlanItem.index.toString());
};
export async function generateAction(
state: GraphState,
config: GraphConfig,
): Promise<GraphUpdate> {
const model = await loadModel(config, Task.ACTION_GENERATOR);
const tools = [
createShellTool(state),
createApplyPatchTool(state),
requestHumanHelpTool,
updatePlanTool,
];
const modelWithTools = model.bindTools(tools, {
tool_choice: "auto",
parallel_tool_calls: false,
});
const response = await modelWithTools.invoke([
{
role: "system",
content: formatPrompt(state),
},
...state.internalMessages,
]);
const hasToolCalls = !!response.tool_calls?.length;
// No tool calls means the graph is going to end. Stop the sandbox.
let newSandboxSessionId: string | undefined;
if (!hasToolCalls && state.sandboxSessionId) {
logger.info("No tool calls found. Stopping sandbox...");
newSandboxSessionId = await stopSandbox(state.sandboxSessionId);
}
logger.info("Generated action", {
currentTask: getCurrentPlanItem(getActivePlanItems(state.plan)).plan,
...(getMessageContentString(response.content) && {
content: getMessageContentString(response.content),
}),
...(response.tool_calls?.[0] && {
name: response.tool_calls?.[0].name,
args: response.tool_calls?.[0].args,
}),
});
return {
messages: [response],
internalMessages: [response],
...(newSandboxSessionId && { sandboxSessionId: newSandboxSessionId }),
};
}

View file

@ -1,27 +1,4 @@
import {
GraphState,
GraphConfig,
GraphUpdate,
} from "@open-swe/shared/open-swe/types";
import { loadModel, Task } from "../utils/load-model.js";
import {
shellTool,
applyPatchTool,
requestHumanHelpTool,
updatePlanTool,
} from "../tools/index.js";
import { getRepoAbsolutePath } from "../utils/git.js";
import { formatPlanPrompt } from "../utils/plan-prompt.js";
import { stopSandbox } from "../utils/sandbox.js";
import { createLogger, LogLevel } from "../utils/logger.js";
import { getCurrentPlanItem } from "../utils/current-task.js";
import { getMessageContentString } from "@open-swe/shared/messages";
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
import { SANDBOX_ROOT_DIR } from "@open-swe/shared/constants";
const logger = createLogger(LogLevel.INFO, "GenerateMessageNode");
const systemPrompt = `You are operating as a terminal-based agentic coding assistant built by LangChain. It wraps LLM models to enable natural language interaction with a local codebase. You are expected to be precise, safe, and helpful.
export const ORIGINAL_SYSTEM_PROMPT = `You are operating as a terminal-based agentic coding assistant built by LangChain. It wraps LLM models to enable natural language interaction with a local codebase. You are expected to be precise, safe, and helpful.
You can:
- Receive user prompts, project context, and files.
@ -111,77 +88,90 @@ Once again, here are the completed tasks, remaining tasks, and the current task
{PLAN_PROMPT}
`;
const formatPrompt = (state: GraphState): string => {
const repoDirectory = getRepoAbsolutePath(state.targetRepository);
return systemPrompt
.replaceAll(
"{PLAN_PROMPT_WITH_SUMMARIES}",
formatPlanPrompt(getActivePlanItems(state.plan), {
includeSummaries: true,
}),
)
.replaceAll(
"{PLAN_PROMPT}",
formatPlanPrompt(getActivePlanItems(state.plan)),
)
.replaceAll("{REPO_DIRECTORY}", repoDirectory)
.replaceAll(
"{PLAN_GENERATION_SUMMARY}",
`<plan-generation-summary>\n${state.planContextSummary}\n</plan-generation-summary>`,
)
.replaceAll(
"{CODEBASE_TREE}",
`<codebase-tree>\n${state.codebaseTree || "No codebase tree generated yet."}\n</codebase-tree>`,
)
.replaceAll("{CURRENT_WORKING_DIRECTORY}", SANDBOX_ROOT_DIR);
};
// The original system prompt, but refactored by Claude.
// Additional prompting & context from OpenAI's prompt
// engineering guide.
export const SYSTEM_PROMPT = `# Identity
export async function generateAction(
state: GraphState,
config: GraphConfig,
): Promise<GraphUpdate> {
const model = await loadModel(config, Task.ACTION_GENERATOR);
const tools = [
shellTool,
applyPatchTool,
requestHumanHelpTool,
updatePlanTool,
];
const modelWithTools = model.bindTools(tools, {
tool_choice: "auto",
parallel_tool_calls: false,
});
You are a terminal-based agentic coding assistant built by LangChain. You wrap LLM models to enable natural language interaction with local codebases. You are precise, safe, and helpful.
const response = await modelWithTools.invoke([
{
role: "system",
content: formatPrompt(state),
},
...state.internalMessages,
]);
You are currently executing a specific task from a pre-generated plan. You have access to:
- Project context and files
- Shell commands and code editing tools
- A sandboxed, git-backed workspace with rollback support
const hasToolCalls = !!response.tool_calls?.length;
// No tool calls means the graph is going to end. Stop the sandbox.
let newSandboxSessionId: string | undefined;
if (!hasToolCalls && state.sandboxSessionId) {
logger.info("No tool calls found. Stopping sandbox...");
newSandboxSessionId = await stopSandbox(state.sandboxSessionId);
}
# Instructions
logger.info("Generated action", {
currentTask: getCurrentPlanItem(getActivePlanItems(state.plan)).plan,
...(getMessageContentString(response.content) && {
content: getMessageContentString(response.content),
}),
...(response.tool_calls?.[0] && {
name: response.tool_calls?.[0].name,
args: response.tool_calls?.[0].args,
}),
});
## Core Behavior
return {
messages: [response],
internalMessages: [response],
...(newSandboxSessionId && { sandboxSessionId: newSandboxSessionId }),
};
}
* **Persistence**: Keep working until the current task is completely resolved. Only terminate when you are certain the task is complete.
* **Accuracy**: Never guess or make up information. Always use tools to gather accurate data about files and codebase structure.
* **Planning**: Leverage the plan context and task summaries heavily - they contain critical information about completed work and the overall strategy.
## Task Execution Guidelines
### Working with the Plan
* You are executing task #{CURRENT_TASK_NUMBER} from the following plan:
- Previous completed tasks and their summaries contain crucial context - always review them first
- Condensed context messages in conversation history summarize previous work - read these to avoid duplication
- The plan generation summary provides important codebase insights
### File and Code Management
* **Repository location**: {REPO_DIRECTORY}
* **Current directory**: {CURRENT_WORKING_DIRECTORY}
* All changes are auto-committed - no manual commits needed
* Work only within the existing Git repository
* Use \`apply_patch\` for file edits (accepts diffs and file paths)
* Use \`shell\` with \`touch\` to create new files (not \`apply_patch\`)
* Always use \`workdir\` parameter instead of \`cd\` when running commands via the \`shell\` tool
### Tool Usage Best Practices
* **Search**: Use \`rg\` (not grep/ls -R) with glob patterns (e.g., \`rg -i pattern -g **/*.tsx\`)
* **Dependencies**: Use the correct package manager; skip if installation fails
* **Pre-commit**: Run \`pre-commit run --files ...\` if .pre-commit-config.yaml exists
* **History**: Use \`git log\` and \`git blame\` for additional context when needed
### Coding Standards
When modifying files:
* Read files before modifying them
* Fix root causes, not symptoms
* Maintain existing code style
* Update documentation as needed
* Remove unnecessary inline comments after completion
* Never add copyright/license headers unless requested
* Ignore unrelated bugs or broken tests
* Write concise and clear code. Do not write overly verbose code.
### Communication Guidelines
* For coding tasks: Focus on implementation and provide brief summaries
## Special Tools
* **request_human_help**: Use only after exhausting all attempts to gather context
* **update_plan**: Use for major plan changes (adding/removing tasks)
# Context
<plan_information>
## Generated Plan with Summaries
{PLAN_PROMPT_WITH_SUMMARIES}
## Plan Generation Summary
{PLAN_GENERATION_SUMMARY}
## Current Task Status
{PLAN_PROMPT}
</plan_information>
<codebase_structure>
## Codebase Tree (3 levels deep, respecting .gitignore)
Generated via: \`git ls-files | tree --fromfile -L 3\`
Location: {REPO_DIRECTORY}
{CODEBASE_TREE}
</codebase_structure>`;

View file

@ -1,5 +1,5 @@
export * from "./initialize.js";
export * from "./generate-message.js";
export * from "./generate-message/index.js";
export * from "./take-action.js";
export * from "./rewrite-plan.js";
export * from "./interrupt-plan.js";

View file

@ -4,7 +4,7 @@ import {
ToolMessage,
} from "@langchain/core/messages";
import { createLogger, LogLevel } from "../utils/logger.js";
import { applyPatchTool, shellTool } from "../tools/index.js";
import { createApplyPatchTool, createShellTool } from "../tools/index.js";
import { GraphState, GraphConfig } from "@open-swe/shared/open-swe/types";
import {
checkoutBranchAndCommit,
@ -53,6 +53,8 @@ export async function takeAction(
throw new Error("Last message is not an AI message with tool calls.");
}
const applyPatchTool = createApplyPatchTool(state);
const shellTool = createShellTool(state);
const toolsMap = {
[applyPatchTool.name]: applyPatchTool,
[shellTool.name]: shellTool,

View file

@ -1,95 +0,0 @@
import { loadModel, Task } from "../../../utils/load-model.js";
import { shellTool } from "../../../tools/index.js";
import { PlannerGraphState, PlannerGraphUpdate } from "../types.js";
import { GraphConfig } from "@open-swe/shared/open-swe/types";
import { createLogger, LogLevel } from "../../../utils/logger.js";
import { getMessageContentString } from "@open-swe/shared/messages";
import { getUserRequest } from "../../../utils/user-request.js";
import { isHumanMessage } from "@langchain/core/messages";
import { formatFollowupMessagePrompt } from "../utils/followup-prompt.js";
import { getRepoAbsolutePath } from "../../../utils/git.js";
import { SANDBOX_ROOT_DIR } from "@open-swe/shared/constants";
const logger = createLogger(LogLevel.INFO, "GeneratePlanningMessageNode");
const systemPrompt = `You are operating as a terminal-based agentic coding assistant built by LangChain. It wraps LLM models to enable natural language interaction with a local codebase. You are expected to be precise, safe, and helpful.
{FOLLOWUP_MESSAGE_PROMPT}
You MUST adhere to the following criteria when gathering context for the plan:
- Your ONLY job is to gather context for the plan.
- You are NOT allowed to take any write/update actions, instead you must only take read actions to gather context.
- All write/update actions will be taken in a later step, only after you've gathered all the necessary context.
- Keep in mind you are only permitted to make a maximum of 6 tool calls to gather all your context. Ensure each action is of high quality, and targeted to aid in generating a plan.
- Always use \`rg\` instead of \`grep/ls -R\` because it is much faster and respects gitignore.
- Always use glob patterns when searching with \`rg\` for specific file types. For example, to search for all TSX files, use \`rg -i star -g **/*.tsx project-directory/\`. This is because \`rg\` does not have built in file types for every language.
- If you determine you've gathered enough context to generate a plan, simply reply with 'done' and do NOT call any tools.
- Not generating a tool call will be interpreted as an indication that you've gathered enough context to generate a plan.
- The repo is already cloned, and located inside {REPO_DIRECTORY}
Below is an up to date tree of the codebase (going 3 levels deep). This is up to date, and is updated after every action you take. Always assume this is the most up to date context about the codebase.
It was generated by using the \`tree\` command, passing in the gitignore file to ignore files and directories you should not have access to (\`git ls-files | tree --fromfile -L 3\`). It is always executed inside the repo directory: {REPO_DIRECTORY}
{CODEBASE_TREE}
Your current working directory is: {CURRENT_WORKING_DIRECTORY}
The user's request is the first user message in the conversation below. Ensure you generate your plan in accordance with the user's request.
`;
function formatSystemPrompt(state: PlannerGraphState): string {
// It's a followup if there's more than one human message.
const isFollowup = state.internalMessages.filter(isHumanMessage).length > 1;
return systemPrompt
.replace(
"{FOLLOWUP_MESSAGE_PROMPT}",
isFollowup ? formatFollowupMessagePrompt(state.plan) : "",
)
.replaceAll("{REPO_DIRECTORY}", getRepoAbsolutePath(state.targetRepository))
.replaceAll(
"{CODEBASE_TREE}",
`<codebase-tree>\n${state.codebaseTree || "No codebase tree generated yet."}\n</codebase-tree>`,
)
.replaceAll("{CURRENT_WORKING_DIRECTORY}", SANDBOX_ROOT_DIR);
}
export async function generateAction(
state: PlannerGraphState,
config: GraphConfig,
): Promise<PlannerGraphUpdate> {
const model = await loadModel(config, Task.ACTION_GENERATOR);
const tools = [shellTool];
const modelWithTools = model.bindTools(tools, {
tool_choice: "auto",
parallel_tool_calls: false,
});
const userRequest = getUserRequest(state.internalMessages, {
returnFullMessage: true,
});
const response = await modelWithTools
.withConfig({ tags: ["nostream"] })
.invoke([
{
role: "system",
content: formatSystemPrompt(state),
},
userRequest,
...state.plannerMessages,
]);
logger.info("Generated planning message", {
...(getMessageContentString(response.content) && {
content: getMessageContentString(response.content),
}),
...(response.tool_calls?.[0] && {
name: response.tool_calls?.[0].name,
args: response.tool_calls?.[0].args,
}),
});
return {
messages: [response],
plannerMessages: [response],
};
}

View file

@ -0,0 +1,72 @@
import { loadModel, Task } from "../../../../utils/load-model.js";
import { createShellTool } from "../../../../tools/index.js";
import { PlannerGraphState, PlannerGraphUpdate } from "../../types.js";
import { GraphConfig } from "@open-swe/shared/open-swe/types";
import { createLogger, LogLevel } from "../../../../utils/logger.js";
import { getMessageContentString } from "@open-swe/shared/messages";
import { getUserRequest } from "../../../../utils/user-request.js";
import { isHumanMessage } from "@langchain/core/messages";
import { formatFollowupMessagePrompt } from "../../utils/followup-prompt.js";
import { getRepoAbsolutePath } from "../../../../utils/git.js";
import { SYSTEM_PROMPT } from "./prompt.js";
const logger = createLogger(LogLevel.INFO, "GeneratePlanningMessageNode");
function formatSystemPrompt(state: PlannerGraphState): string {
// It's a followup if there's more than one human message.
const isFollowup = state.internalMessages.filter(isHumanMessage).length > 1;
return SYSTEM_PROMPT.replace(
"{FOLLOWUP_MESSAGE_PROMPT}",
isFollowup ? formatFollowupMessagePrompt(state.plan) : "",
)
.replaceAll(
"{CODEBASE_TREE}",
state.codebaseTree || "No codebase tree generated yet.",
)
.replaceAll(
"{CURRENT_WORKING_DIRECTORY}",
getRepoAbsolutePath(state.targetRepository),
);
}
export async function generateAction(
state: PlannerGraphState,
config: GraphConfig,
): Promise<PlannerGraphUpdate> {
const model = await loadModel(config, Task.ACTION_GENERATOR);
const tools = [createShellTool(state)];
const modelWithTools = model.bindTools(tools, {
tool_choice: "auto",
parallel_tool_calls: false,
});
const userRequest = getUserRequest(state.internalMessages, {
returnFullMessage: true,
});
const response = await modelWithTools
.withConfig({ tags: ["nostream"] })
.invoke([
{
role: "system",
content: formatSystemPrompt(state),
},
userRequest,
...state.plannerMessages,
]);
logger.info("Generated planning message", {
...(getMessageContentString(response.content) && {
content: getMessageContentString(response.content),
}),
...(response.tool_calls?.[0] && {
name: response.tool_calls?.[0].name,
args: response.tool_calls?.[0].args,
}),
});
return {
messages: [response],
plannerMessages: [response],
};
}

View file

@ -0,0 +1,67 @@
export const ORIGINAL_SYSTEM_PROMPT = `You are operating as a terminal-based agentic coding assistant built by LangChain. It wraps LLM models to enable natural language interaction with a local codebase. You are expected to be precise, safe, and helpful.
{FOLLOWUP_MESSAGE_PROMPT}
You MUST adhere to the following criteria when gathering context for the plan:
- Your ONLY job is to gather context for the plan.
- You are NOT allowed to take any write/update actions, instead you must only take read actions to gather context.
- Ensure each tool call you generate is of an extremely high quality, and targeted to aid in generating a plan.
- Always use \`rg\` instead of \`grep/ls -R\` because it is much faster and respects gitignore.
- Always use glob patterns when searching with \`rg\` for specific file types. For example, to search for all TSX files, use \`rg -i star -g **/*.tsx project-directory/\`. This is because \`rg\` does not have built in file types for every language.
- When calling the \`shell\` tool it is incredibly important your commands are properly formatted. You should ALWAYS remember to include proper quotes, and ensure the command is formatted correctly.
- If you determine you've gathered enough context to generate a plan, simply reply with 'done' and do NOT call any tools.
- Not generating a tool call will be interpreted as an indication that you've gathered enough context to generate a plan.
- The repo is already cloned, and located inside your current working directory: {CURRENT_WORKING_DIRECTORY}
Below is an up to date tree of the codebase (going 3 levels deep). You should assume this is always up to date.
It was generated by using the \`tree\` command, passing in the gitignore file to ignore files and directories you should not have access to (\`git ls-files | tree --fromfile -L 3\`).
It is always executed inside the repo directory (also your current working directory): {CURRENT_WORKING_DIRECTORY}
{CODEBASE_TREE}
Your current working directory is: {CURRENT_WORKING_DIRECTORY}
The user's request is the first user message in the conversation below. Ensure you generate your plan in accordance with the user's request.
`;
// The original system prompt, but refactored by Claude.
// Additional prompting & context from Anthropic's prompt
// engineering guide.
export const SYSTEM_PROMPT = `You are a terminal-based agentic coding assistant built by LangChain that enables natural language interaction with local codebases. You excel at being precise, safe, and helpful in your analysis.
<role>
Context Gathering Assistant - Read-Only Phase
</role>
<primary_objective>
Your sole objective in this phase is to gather comprehensive context about the codebase to inform plan generation. Focus on understanding the code structure, dependencies, and relevant implementation details through targeted read operations.
</primary_objective>
{FOLLOWUP_MESSAGE_PROMPT}
<context_gathering_guidelines>
1. **Use only read operations**: Execute commands that inspect and analyze the codebase without modifying any files. This ensures we understand the current state before making changes.
2. **Make high-quality, targeted tool calls**: Each command should have a clear purpose in building your understanding of the codebase. Think strategically about what information you need.
3. **Leverage efficient search tools**: Use \`rg\` (ripgrep) for all file searches because it respects .gitignore patterns and provides significantly faster results than alternatives like grep or ls -R.
- When searching for specific file types, use glob patterns: \`rg -i pattern -g **/*.tsx project-directory/\`
- This explicit pattern matching ensures accurate results across all file extensions
4. **Format shell commands precisely**: Ensure all shell commands include proper quoting and escaping. Well-formatted commands prevent errors and provide reliable results.
5. **Signal completion clearly**: When you have gathered sufficient context, respond with exactly 'done' without any tool calls. This indicates readiness to proceed to the planning phase.
</context_gathering_guidelines>
<workspace_information>
**Current Working Directory**: {CURRENT_WORKING_DIRECTORY}
**Repository Status**: Already cloned and accessible in the current directory
**Codebase Structure** (3 levels deep, respecting .gitignore):
Generated via: \`git ls-files | tree --fromfile -L 3\`
<codebase_tree>
{CODEBASE_TREE}
</codebase_tree>
</workspace_information>
<task_context>
The user's request appears as the first message in the conversation below. Your context gathering should specifically target information needed to address this request effectively.
</task_context>`;

View file

@ -1,4 +1,4 @@
export * from "./generate-message.js";
export * from "./generate-message/index.js";
export * from "./take-action.js";
export * from "./generate-plan.js";
export * from "./summarizer.js";

View file

@ -1,5 +1,5 @@
import { isAIMessage, ToolMessage } from "@langchain/core/messages";
import { shellTool } from "../../../tools/index.js";
import { createShellTool } from "../../../tools/index.js";
import { GraphConfig } from "@open-swe/shared/open-swe/types";
import { PlannerGraphState, PlannerGraphUpdate } from "../types.js";
import { createLogger, LogLevel } from "../../../utils/logger.js";
@ -20,6 +20,7 @@ export async function takeAction(
throw new Error("Last message is not an AI message with tool calls.");
}
const shellTool = createShellTool(state);
const toolsMap = {
[shellTool.name]: shellTool,
};

View file

@ -7,124 +7,128 @@ import { getCurrentTaskInput } from "@langchain/langgraph";
import { fixGitPatch } from "../utils/diff.js";
import { createLogger, LogLevel } from "../utils/logger.js";
import { daytonaClient } from "../utils/sandbox.js";
import { SANDBOX_ROOT_DIR } from "@open-swe/shared/constants";
import { getRepoAbsolutePath } from "../utils/git.js";
const logger = createLogger(LogLevel.INFO, "ApplyPatchTool");
const createApplyPatchToolDescription = (state: GraphState) => {
const repoRoot = getRepoAbsolutePath(state.targetRepository);
return (
"Applies a diff to a file given a file path and diff content." +
`The working directory this diff will be applied to is \`${repoRoot}\`. Ensure the file paths you provide are relative to this directory.`
);
};
const applyPatchToolSchema = z.object({
diff: z
.string()
.describe(
"The diff to apply. Use a standard diff format. Ensure this field is ALWAYS provided.",
`The diff to apply. Use a standard diff format. Ensure this field is ALWAYS provided.`,
),
file_path: z.string().describe("The file path to apply the diff to."),
workdir: z
.string()
.default(SANDBOX_ROOT_DIR)
.describe(
`The working directory for the command. Ensure this path is NOT included in any command arguments, as it will be added automatically. Defaults to '${SANDBOX_ROOT_DIR}' as this is the root directory of the sandbox.`,
),
});
export const applyPatchTool = tool(
async (input): Promise<{ result: string; status: "success" | "error" }> => {
const state = getCurrentTaskInput<GraphState>();
const { sandboxSessionId } = state;
if (!sandboxSessionId) {
logger.error("FAILED TO RUN COMMAND: No sandbox session ID provided", {
input,
});
throw new Error("FAILED TO RUN COMMAND: No sandbox session ID provided");
}
const { diff, file_path, workdir } = input;
const sandbox = await daytonaClient().get(sandboxSessionId);
const { success: readFileSuccess, output: readFileOutput } = await readFile(
sandbox,
file_path,
{
workDir: workdir,
},
);
if (!readFileSuccess) {
logger.error(readFileOutput);
throw new Error(readFileOutput);
}
let patchedContent: string | false;
let fixedDiff: string | false = false;
let errorApplyingPatchMessage: string | undefined;
try {
logger.info(`Applying patch to file ${file_path}`);
patchedContent = applyPatch(readFileOutput, diff);
} catch (e) {
errorApplyingPatchMessage = e instanceof Error ? e.message : undefined;
try {
logger.warn("Failed to apply patch, trying to fix diff", {
error: e,
export function createApplyPatchTool(state: GraphState) {
const applyPatchTool = tool(
async (input): Promise<{ result: string; status: "success" | "error" }> => {
const state = getCurrentTaskInput<GraphState>();
const { sandboxSessionId } = state;
if (!sandboxSessionId) {
logger.error("FAILED TO RUN COMMAND: No sandbox session ID provided", {
input,
});
const fixedDiff_ = fixGitPatch(diff, {
[file_path]: readFileOutput,
});
patchedContent = applyPatch(readFileOutput, fixedDiff_);
logger.info("Successfully fixed diff and applied patch to file", {
file_path,
});
if (patchedContent) {
fixedDiff = fixedDiff_;
}
} catch (_) {
logger.error("Failed to apply patch", {
...(e instanceof Error
? { name: e.name, message: e.message, stack: e.stack }
: { error: e }),
});
const errMessage = e instanceof Error ? e.message : "Unknown error";
throw new Error(
`FAILED TO APPLY PATCH: The diff could not be applied to file '${file_path}'.\n\nError: ${errMessage}`,
"FAILED TO RUN COMMAND: No sandbox session ID provided",
);
}
}
if (patchedContent === false) {
logger.error(
`FAILED TO APPLY PATCH: The diff could not be applied to file '${file_path}'. This may be due to an invalid diff format or conflicting changes with the file's current content. Original content length: ${readFileOutput.length}, Diff: ${diff.substring(0, 100)}...`,
);
throw new Error(
`FAILED TO APPLY PATCH: The diff could not be applied to file '${file_path}'. This may be due to an invalid diff format or conflicting changes with the file's current content. Original content length: ${readFileOutput.length}, Diff: ${diff.substring(0, 100)}...`,
);
}
const { diff, file_path } = input;
const workDir = getRepoAbsolutePath(state.targetRepository);
const { success: writeFileSuccess, output: writeFileOutput } =
await writeFile(sandbox, file_path, patchedContent, {
workDir: workdir,
});
if (!writeFileSuccess) {
logger.error("Failed to write file", {
writeFileOutput,
});
throw new Error(writeFileOutput);
}
const sandbox = await daytonaClient().get(sandboxSessionId);
let resultMessage = `Successfully applied diff to \`${file_path}\` and saved changes.`;
logger.info(resultMessage);
if (fixedDiff) {
resultMessage +=
"\n\nNOTE: The generated diff was NOT formatted properly, and had to be fixed." +
`\nHere is the error that was thrown when your generated diff was applied:\n<apply-diff-error>\n${errorApplyingPatchMessage}\n</apply-diff-error>` +
`\nThe diff which was applied is:\n<fixed-diff>\n${fixedDiff}\n</fixed-diff>`;
}
return {
result: resultMessage,
status: "success",
};
},
{
name: "apply_patch",
description:
"Applies a diff to a file given a file path and diff content. Ensure you ALWAYS pass a valid file path to this tool. The combination of `workdir` and `file_path` should point to a valid file in the sandbox. Ensure you do not omit parts of the path between `workdir` and `file_path`.",
schema: applyPatchToolSchema,
},
);
const { success: readFileSuccess, output: readFileOutput } =
await readFile(sandbox, file_path, {
workDir,
});
if (!readFileSuccess) {
logger.error(readFileOutput);
throw new Error(readFileOutput);
}
let patchedContent: string | false;
let fixedDiff: string | false = false;
let errorApplyingPatchMessage: string | undefined;
try {
logger.info(`Applying patch to file ${file_path}`);
patchedContent = applyPatch(readFileOutput, diff);
} catch (e) {
errorApplyingPatchMessage = e instanceof Error ? e.message : undefined;
try {
logger.warn("Failed to apply patch, trying to fix diff", {
error: e,
});
const fixedDiff_ = fixGitPatch(diff, {
[file_path]: readFileOutput,
});
patchedContent = applyPatch(readFileOutput, fixedDiff_);
logger.info("Successfully fixed diff and applied patch to file", {
file_path,
});
if (patchedContent) {
fixedDiff = fixedDiff_;
}
} catch (_) {
logger.error("Failed to apply patch", {
...(e instanceof Error
? { name: e.name, message: e.message, stack: e.stack }
: { error: e }),
});
const errMessage = e instanceof Error ? e.message : "Unknown error";
throw new Error(
`FAILED TO APPLY PATCH: The diff could not be applied to file '${file_path}'.\n\nError: ${errMessage}`,
);
}
}
if (patchedContent === false) {
logger.error(
`FAILED TO APPLY PATCH: The diff could not be applied to file '${file_path}'. This may be due to an invalid diff format or conflicting changes with the file's current content. Original content length: ${readFileOutput.length}, Diff: ${diff.substring(0, 100)}...`,
);
throw new Error(
`FAILED TO APPLY PATCH: The diff could not be applied to file '${file_path}'. This may be due to an invalid diff format or conflicting changes with the file's current content. Original content length: ${readFileOutput.length}, Diff: ${diff.substring(0, 100)}...`,
);
}
const { success: writeFileSuccess, output: writeFileOutput } =
await writeFile(sandbox, file_path, patchedContent, {
workDir,
});
if (!writeFileSuccess) {
logger.error("Failed to write file", {
writeFileOutput,
});
throw new Error(writeFileOutput);
}
let resultMessage = `Successfully applied diff to \`${file_path}\` and saved changes.`;
logger.info(resultMessage);
if (fixedDiff) {
resultMessage +=
"\n\nNOTE: The generated diff was NOT formatted properly, and had to be fixed." +
`\nHere is the error that was thrown when your generated diff was applied:\n<apply-diff-error>\n${errorApplyingPatchMessage}\n</apply-diff-error>` +
`\nThe diff which was applied is:\n<fixed-diff>\n${fixedDiff}\n</fixed-diff>`;
}
return {
result: resultMessage,
status: "success",
};
},
{
name: "apply_patch",
description: createApplyPatchToolDescription(state),
schema: applyPatchToolSchema,
},
);
return applyPatchTool;
}

View file

@ -6,7 +6,8 @@ import { getCurrentTaskInput } from "@langchain/langgraph";
import { getSandboxErrorFields } from "../utils/sandbox-error-fields.js";
import { createLogger, LogLevel } from "../utils/logger.js";
import { daytonaClient } from "../utils/sandbox.js";
import { SANDBOX_ROOT_DIR, TIMEOUT_SEC } from "@open-swe/shared/constants";
import { TIMEOUT_SEC } from "@open-swe/shared/constants";
import { getRepoAbsolutePath } from "../utils/git.js";
const logger = createLogger(LogLevel.INFO, "ShellTool");
@ -15,91 +16,106 @@ const DEFAULT_ENV = {
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
};
const shellToolSchema = z.object({
command: z.array(z.string()).describe("The command to run"),
workdir: z
.string()
.default(SANDBOX_ROOT_DIR)
.describe(
`The working directory for the command. Ensure this path is NOT included in any command arguments, as it will be added automatically. Defaults to '${SANDBOX_ROOT_DIR}' as this is the root directory of the sandbox.`,
),
timeout: z
.number()
.optional()
.default(TIMEOUT_SEC)
.describe(
"The maximum time to wait for the command to complete in seconds.",
),
});
const createShellToolSchema = (state: GraphState) => {
const repoRoot = getRepoAbsolutePath(state.targetRepository);
const shellToolSchema = z.object({
command: z
.array(z.string())
.describe(
"The command to run. Ensure the command is properly formatted, with arguments in the correct order, and including any wrapping strings, quotes, etc. By default, this command will be executed in the root of the repository, unless a custom workdir is specified.",
),
workdir: z
.string()
.default(repoRoot)
.describe(
`The working directory for the command. Defaults to the root of the repository (${repoRoot}). You should only specify this if the command you're running can not be executed from the root of the repository.`,
),
timeout: z
.number()
.optional()
.default(TIMEOUT_SEC)
.describe(
"The maximum time to wait for the command to complete in seconds.",
),
});
return shellToolSchema;
};
export const shellTool = tool(
async (input): Promise<{ result: string; status: "success" | "error" }> => {
let sandbox: Sandbox | undefined;
try {
const state = getCurrentTaskInput<GraphState>();
const { sandboxSessionId } = state;
if (!sandboxSessionId) {
logger.error("FAILED TO RUN COMMAND: No sandbox session ID provided", {
input,
});
export function createShellTool(state: GraphState) {
const shellTool = tool(
async (input): Promise<{ result: string; status: "success" | "error" }> => {
let sandbox: Sandbox | undefined;
try {
const state = getCurrentTaskInput<GraphState>();
const { sandboxSessionId } = state;
if (!sandboxSessionId) {
logger.error(
"FAILED TO RUN COMMAND: No sandbox session ID provided",
{
input,
},
);
throw new Error(
"FAILED TO RUN COMMAND: No sandbox session ID provided",
);
}
sandbox = await daytonaClient().get(sandboxSessionId);
const { command, workdir, timeout } = input;
const response = await sandbox.process.executeCommand(
command.join(" "),
workdir,
DEFAULT_ENV,
timeout ?? TIMEOUT_SEC,
);
if (response.exitCode !== 0) {
logger.error("Failed to run command", {
error: response.result,
error_result: response,
input,
});
throw new Error(
`Command failed. Exit code: ${response.exitCode}\nResult: ${response.result}\nStdout:\n${response.artifacts?.stdout}`,
);
}
return {
result: response.result,
status: "success",
};
} catch (e) {
const errorFields = getSandboxErrorFields(e);
if (errorFields) {
logger.error("Failed to run command", {
input,
error: errorFields,
});
throw new Error(
`Command failed. Exit code: ${errorFields.exitCode}\nError: ${errorFields.result}\nStdout:\n${errorFields.artifacts?.stdout}`,
);
}
logger.error(
"Failed to run command: " +
(e instanceof Error ? e.message : "Unknown error"),
{
error: e,
input,
},
);
throw new Error(
"FAILED TO RUN COMMAND: No sandbox session ID provided",
"FAILED TO RUN COMMAND: " +
(e instanceof Error ? e.message : "Unknown error"),
);
}
},
{
name: "shell",
description: "Runs a shell command, and returns its output.",
schema: createShellToolSchema(state),
},
);
sandbox = await daytonaClient().get(sandboxSessionId);
const { command, workdir, timeout } = input;
const response = await sandbox.process.executeCommand(
command.join(" "),
workdir,
DEFAULT_ENV,
timeout ?? TIMEOUT_SEC,
);
if (response.exitCode !== 0) {
logger.error("Failed to run command", {
error: response.result,
error_result: response,
input,
});
throw new Error(
`Command failed. Exit code: ${response.exitCode}\nResult: ${response.result}\nStdout:\n${response.artifacts?.stdout}`,
);
}
return {
result: response.result,
status: "success",
};
} catch (e) {
const errorFields = getSandboxErrorFields(e);
if (errorFields) {
logger.error("Failed to run command", {
input,
error: errorFields,
});
throw new Error(
`Command failed. Exit code: ${errorFields.exitCode}\nError: ${errorFields.result}\nStdout:\n${errorFields.artifacts?.stdout}`,
);
}
logger.error(
"Failed to run command: " +
(e instanceof Error ? e.message : "Unknown error"),
{
error: e,
input,
},
);
throw new Error(
"FAILED TO RUN COMMAND: " +
(e instanceof Error ? e.message : "Unknown error"),
);
}
},
{
name: "shell",
description: "Runs a shell command, and returns its output.",
schema: shellToolSchema,
},
);
return shellTool;
}

View file

@ -5,6 +5,7 @@ import { GraphConfig, TargetRepository } from "@open-swe/shared/open-swe/types";
import { TIMEOUT_SEC, SANDBOX_ROOT_DIR } from "@open-swe/shared/constants";
import { getSandboxErrorFields } from "./sandbox-error-fields.js";
import { ExecuteResponse } from "@daytonaio/sdk/dist/types/ExecuteResponse.js";
import path from "node:path";
const logger = createLogger(LogLevel.INFO, "GitUtil");
@ -16,7 +17,7 @@ export function getRepoAbsolutePath(
throw new Error("No repository name provided");
}
return `${SANDBOX_ROOT_DIR}/${repoName}`;
return path.join(SANDBOX_ROOT_DIR, repoName);
}
export function getBranchName(config: GraphConfig): string {