feat: Commit changes after writing (#14)

* feat: Add a planning summary step in planner subgraph

* feat: Commit changes after writing

* cr

* always check and commit after taking an action

* cr
This commit is contained in:
Brace Sproul 2025-05-23 15:56:06 -07:00 • committed by GitHub
parent 513cf6556d
commit 4ec7c31733
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 776 additions and 121 deletions

8
e2b.Dockerfile Normal file
View file

@ -0,0 +1,8 @@
# Always start from the base E2B image
FROM e2bdev/code-interpreter:latest
# Ensure git is installed
RUN apt-get update && apt-get install -y git curl ripgrep
# Set the working directory
WORKDIR /app

View file

@ -1,21 +0,0 @@
# Always start from the base E2B image
FROM e2bdev/code-interpreter:latest
# Ensure git is installed
RUN apt-get update && apt-get install -y git curl ripgrep
# Set the working directory
WORKDIR /app
# Download and install nvm, then Node.js, then Yarn
ENV NVM_DIR /root/.nvm
ENV NODE_VERSION 22
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash \
&& . "$NVM_DIR/nvm.sh" \
&& nvm install "$NODE_VERSION" \
&& nvm alias default "$NODE_VERSION" \
&& nvm use default \
&& corepack enable yarn
# Add NVM's bin to the PATH for subsequent commands
ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH

View file

@ -1,20 +0,0 @@
# Always start from the base E2B image
FROM e2bdev/code-interpreter:latest
# Ensure git is installed
RUN apt-get update && apt-get install -y git curl
# Set the working directory
WORKDIR /app
# Ensure Python and pip are installed
RUN apt-get install -y python3 python3-pip python3-venv
# Upgrade pip
RUN python3 -m pip install --upgrade pip
# Install common Python development tools
RUN python3 -m pip install pytest black isort mypy
# Set Python 3 as the default python
RUN ln -sf /usr/bin/python3 /usr/bin/python

View file

@ -12,5 +12,5 @@
team_id = "af4eaf9c-3d40-415c-bb98-4831c47745cf"
start_cmd = "/root/.jupyter/start-up.sh"
dockerfile = "e2b.js.Dockerfile"
dockerfile = "e2b.Dockerfile"
template_id = "eh0860emqx28qyxmbctu"

View file

@ -17,7 +17,9 @@
"format:check": "prettier --check .",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.test\\.ts$ --testPathIgnorePatterns=\\.int\\.test\\.ts$",
"test:int": "node --experimental-vm-modules node_modules/jest/bin/jest.js --testPathPattern=\\.int\\.test\\.ts$",
"test:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.js --testTimeout 100000"
"test:single": "NODE_OPTIONS=--experimental-vm-modules yarn run jest --config jest.config.js --testTimeout 100000",
"run:e2e": "npx tsx scripts/run-e2e.ts",
"run:from-plan": "npx tsx scripts/run-from-plan.ts"
},
"dependencies": {
"@e2b/code-interpreter": "^1.2.0-beta.4",
@ -25,6 +27,7 @@
"@langchain/core": "^0.3.56",
"@langchain/google-genai": "^0.2.9",
"@langchain/langgraph": "^0.2.73",
"@langchain/langgraph-sdk": "^0.0.78",
"@langchain/openai": "^0.5.10",
"diff": "^8.0.1",
"langchain": "^0.3.26",

94
scripts/run-e2e.ts Normal file
View file

@ -0,0 +1,94 @@
import "dotenv/config";
import { Client } from "@langchain/langgraph-sdk";
import { v4 as uuidv4 } from "uuid";
import { GraphConfig } from "../src/types.js";
import { HumanResponse } from "@langchain/langgraph/prebuilt";
async function runE2E() {
const client = new Client({
apiKey: process.env.LANGCHAIN_API_KEY,
apiUrl: process.env.LANGGRAPH_API_URL ?? "http://localhost:2024",
});
const threadId = uuidv4();
const userRequest = "ADD YOUR REQUEST HERE";
const configurable: Omit<
GraphConfig["configurable"],
"thread_id" | "assistant_id"
> = {
target_repository: {
owner: "YOUR_USERNAME",
repo: "YOUR_REPO",
branch: "OPTIONAL BRANCH NAME",
},
};
const stream = client.runs.stream(threadId, "open-codex", {
input: {
messages: [{ role: "user", content: userRequest }],
},
config: {
configurable,
recursion_limit: 200,
},
ifNotExists: "create",
streamSubgraphs: true,
streamMode: "updates",
});
for await (const chunk of stream) {
console.dir(chunk.data, { depth: null });
}
}
async function resumeGraph(threadId: string) {
const client = new Client({
apiKey: process.env.LANGCHAIN_API_KEY,
apiUrl: process.env.LANGGRAPH_API_URL ?? "http://localhost:2024",
});
// EDIT THIS IF YOU DO NOT WANT TO ACCEPT
const resumeValue: HumanResponse[] = [
{
type: "accept",
args: null,
},
];
const stream = client.runs.stream(threadId, "open-codex", {
command: {
resume: resumeValue,
},
streamSubgraphs: true,
streamMode: "updates",
});
for await (const chunk of stream) {
console.dir(chunk.data, { depth: null });
}
}
const args = process.argv.slice(2); // Skip node executable and script path
if (args.length === 0) {
runE2E().catch((error) => {
console.error("Error running E2E test:", error);
process.exit(1);
});
} else if (args.length === 2 && args[0] === "--threadId") {
const threadId = args[1];
resumeGraph(threadId).catch((error) => {
console.error(`Error resuming graph for thread ID ${threadId}:`, error);
process.exit(1);
});
} else {
console.log("Usage:");
console.log(" To run a new E2E test:");
console.log(" yarn run:e2e");
console.log("");
console.log(" To resume a graph with a thread ID:");
console.log(" yarn run:e2e --threadId <thread_id>");
process.exit(1);
}

150
scripts/run-from-plan.ts Normal file
View file

@ -0,0 +1,150 @@
import "dotenv/config";
import { Client } from "@langchain/langgraph-sdk";
import { v4 as uuidv4 } from "uuid";
import { GraphConfig } from "../src/types.js";
import { graph } from "../src/index.js";
async function runFromPlan() {
const client = new Client({
apiKey: process.env.LANGCHAIN_API_KEY,
apiUrl: process.env.LANGGRAPH_API_URL ?? "http://localhost:2024",
});
const threadId = uuidv4();
const inputs = {
messages: [
{
role: "user",
content:
"This repo contains the react/next.js code for my persona/portfolio site. It currently has static values set for the number of stars on the repositories I highlight. I want this to be accurate, but I do NOT want it to make requests to GitHub every time a user visits. Instead, please implement a solution which will run once a day, fetch the number of stars from a list of repos, then write them to vercel's KV store. Finally, update the UI to make a request to the KV store when the user visits my page and render the accurate star counts.",
},
{
id: "toolu_01BDCDYr7oGxJnfgY6MjXGnx",
role: "assistant",
content: [
{
index: 0,
type: "tool_use",
id: "toolu_01BDCDYr7oGxJnfgY6MjXGnx",
name: "condense_planning_context",
input:
'{"context": "The user has a React/Next.js portfolio site with static star counts for GitHub repositories. The project structure shows:\\n\\n1. **Current Implementation**: \\n - Main page at `./personal-site/src/app/page.tsx` contains hardcoded star values for 6 repositories:\\n - langchain-ai/langchainjs (14.3k stars)\\n - langchain-ai/langgraphjs (1.3k stars) \\n - langchain-ai/open-canvas (4.4k stars)\\n - langchain-ai/agent-inbox (496 stars)\\n - langchain-ai/agent-chat-ui (583 stars)\\n - langchain-ai/social-media-agent (1.2k stars)\\n - Stars are displayed in ProjectCard components with a stars prop\\n\\n2. **Dependencies**: \\n - Project already has `@vercel/kv` dependency installed\\n - Uses Next.js 15.2.3 with TypeScript\\n - Has existing API routes in `./personal-site/src/app/api/` including gh-viewer route that already uses Vercel KV\\n\\n3. **Requirements**:\\n - Implement daily GitHub API fetching for star counts\\n - Store results in Vercel KV store \\n - Update UI to fetch from KV store instead of static values\\n - Avoid GitHub API calls on every user visit\\n\\n4. **Repository URLs to track**:\\n - https://github.com/langchain-ai/langchainjs\\n - https://github.com/langchain-ai/langgraphjs \\n - https://github.com/langchain-ai/open-canvas\\n - https://github.com/langchain-ai/agent-inbox\\n - https://github.com/langchain-ai/agent-chat-ui\\n - https://github.com/langchain-ai/social-media-agent"}',
},
],
tool_calls: [
{
name: "condense_planning_context",
args: {
context:
"The user has a React/Next.js portfolio site with static star counts for GitHub repositories. The project structure shows:\n\n1. **Current Implementation**: \n - Main page at `./personal-site/src/app/page.tsx` contains hardcoded star values for 6 repositories:\n - langchain-ai/langchainjs (14.3k stars)\n - langchain-ai/langgraphjs (1.3k stars) \n - langchain-ai/open-canvas (4.4k stars)\n - langchain-ai/agent-inbox (496 stars)\n - langchain-ai/agent-chat-ui (583 stars)\n - langchain-ai/social-media-agent (1.2k stars)\n - Stars are displayed in ProjectCard components with a stars prop\n\n2. **Dependencies**: \n - Project already has `@vercel/kv` dependency installed\n - Uses Next.js 15.2.3 with TypeScript\n - Has existing API routes in `./personal-site/src/app/api/` including gh-viewer route that already uses Vercel KV\n\n3. **Requirements**:\n - Implement daily GitHub API fetching for star counts\n - Store results in Vercel KV store \n - Update UI to fetch from KV store instead of static values\n - Avoid GitHub API calls on every user visit\n\n4. **Repository URLs to track**:\n - https://github.com/langchain-ai/langchainjs\n - https://github.com/langchain-ai/langgraphjs \n - https://github.com/langchain-ai/open-canvas\n - https://github.com/langchain-ai/agent-inbox\n - https://github.com/langchain-ai/agent-chat-ui\n - https://github.com/langchain-ai/social-media-agent",
},
id: "toolu_01BDCDYr7oGxJnfgY6MjXGnx",
type: "tool_call",
},
],
},
{
role: "tool",
tool_call_id: "toolu_01BDCDYr7oGxJnfgY6MjXGnx",
name: "condense_planning_context",
content: "Successfully summarized planning context.",
},
],
plan: [
{
index: 0,
plan: "Create a list of GitHub repositories with their URLs extracted from the current static star values in page.tsx",
completed: false,
},
{
index: 1,
plan: "Create a new API route `/api/github-stars` that fetches star counts from GitHub API for the repository list",
completed: false,
},
{
index: 2,
plan: "Create a new API route `/api/stars` that retrieves cached star counts from Vercel KV store",
completed: false,
},
{
index: 3,
plan: "Implement a daily cron job using Vercel Cron Jobs (vercel.json) that calls the GitHub stars API and updates the KV store",
completed: false,
},
{
index: 4,
plan: "Update the ProjectCard component to fetch star counts from the KV store API instead of using static values",
completed: false,
},
{
index: 5,
plan: "Add error handling and fallback to static values if KV store is unavailable",
completed: false,
},
{
index: 6,
plan: "Update the page.tsx to remove static star values and implement dynamic fetching",
completed: false,
},
],
proposedPlan: [
"Create a list of GitHub repositories with their URLs extracted from the current static star values in page.tsx",
"Create a new API route `/api/github-stars` that fetches star counts from GitHub API for the repository list",
"Create a new API route `/api/stars` that retrieves cached star counts from Vercel KV store",
"Implement a daily cron job using Vercel Cron Jobs (vercel.json) that calls the GitHub stars API and updates the KV store",
"Update the ProjectCard component to fetch star counts from the KV store API instead of using static values",
"Add error handling and fallback to static values if KV store is unavailable",
"Update the page.tsx to remove static star values and implement dynamic fetching",
],
planChangeRequest: undefined,
sandboxSessionId: undefined,
branchName: `open-swe/${threadId}`,
};
const configurable: Omit<
GraphConfig["configurable"],
"thread_id" | "assistant_id"
> = {
target_repository: {
owner: "bracesproul",
repo: "personal-site",
},
};
console.log("\nInitializing sandbox...");
const initResult = await graph.nodes.initialize.invoke(inputs as any, {
configurable,
});
if (!initResult.sandboxSessionId) {
throw new Error("Failed to initialize sandbox.");
}
console.log(
`Sandbox initialized successfully. ID: ${initResult.sandboxSessionId}\n\n`,
);
const stream = client.runs.stream(threadId, "open-codex", {
command: {
goto: "generate-action",
update: {
...inputs,
sandboxSessionId: initResult.sandboxSessionId,
},
},
config: {
configurable,
recursion_limit: 200,
},
ifNotExists: "create",
streamSubgraphs: true,
streamMode: "updates",
});
for await (const chunk of stream) {
console.dir(chunk.data, { depth: null });
}
}
runFromPlan().catch(console.error);

View file

@ -28,6 +28,7 @@ You MUST adhere to the following criteria when executing the task:
- Use \`apply_patch\` to edit files. This tool accepts diffs and file paths. It will then apply the given diff to the file.
- If completing the user's task requires writing or modifying files:
- Your code and final answer should follow these *CODING GUIDELINES*:
- Avoid writing to files which you have not already read.
- Fix the problem at the root cause rather than applying surface-level patches, when possible.
- Avoid unneeded complexity in your solution.
- Ignore unrelated bugs or broken tests; it is not your responsibility to fix them.
@ -35,6 +36,7 @@ You MUST adhere to the following criteria when executing the task:
- Keep changes consistent with the style of the existing codebase. Changes should be minimal and focused on the task.
- Use \`git log\` and \`git blame\` to search the history of the codebase if additional context is required; internet access is disabled.
- NEVER add copyright or license headers unless specifically requested.
- If creating a new file or directory plus file, always remember to create both before trying to read/write the file. Keep in mind you can not write to files which don't exist.
- You do not need to \`git commit\` your changes; this will be done automatically for you.
- If there is a .pre-commit-config.yaml, use \`pre-commit run --files ...\` to check that your changes pass the pre-commit checks. However, do not fix pre-existing errors on lines you didn't touch.
- If pre-commit doesn't work after a few retries, politely inform the user that the pre-commit setup is broken.

View file

@ -6,8 +6,14 @@ import {
TargetRepository,
} from "../types.js";
import { TIMEOUT_EXTENSION_OPT } from "../constants.js";
import {
checkoutBranch,
configureGitUserInRepo,
getBranchName,
getRepoAbsolutePath,
} from "../utils/git/index.js";
const JS_SANDBOX_TEMPLATE_ID = "eh0860emqx28qyxmbctu";
const SANDBOX_TEMPLATE_ID = "eh0860emqx28qyxmbctu";
async function cloneRepo(sandbox: Sandbox, targetRepository: TargetRepository) {
if (!process.env.GITHUB_PAT) {
@ -67,36 +73,47 @@ export async function initialize(
}
}
const { target_repository, sandbox_language } = config.configurable;
const { target_repository } = config.configurable;
if (!sandbox_language || !target_repository) {
if (!target_repository) {
throw new Error(
"Missing required configuration. Please provide a sandbox language and git repository URL.",
"Missing required configuration. Please provide a git repository URL.",
);
}
if (sandbox_language === "js") {
console.log("Creating JS sandbox...");
const sandbox = await Sandbox.create(
JS_SANDBOX_TEMPLATE_ID,
TIMEOUT_EXTENSION_OPT,
);
console.log("Creating sandbox...");
const sandbox = await Sandbox.create(
SANDBOX_TEMPLATE_ID,
TIMEOUT_EXTENSION_OPT,
);
const res = await cloneRepo(sandbox, target_repository);
if (res.error) {
// TODO: This should probably be an interrupt.
console.error("Failed to clone repository.", res.error);
throw new Error(`Failed to clone repository.\n${res.error}`);
}
console.log("Repository cloned successfully.");
return {
sandboxSessionId: sandbox.sandboxId,
};
const res = await cloneRepo(sandbox, target_repository);
if (res.error) {
// TODO: This should probably be an interrupt.
console.error("Failed to clone repository.", res.error);
throw new Error(`Failed to clone repository.\n${res.error}`);
}
console.log("Repository cloned successfully.");
const absoluteRepoDir = getRepoAbsolutePath(config);
console.log(`Configuring git user for repository at "${absoluteRepoDir}"...`);
await configureGitUserInRepo(absoluteRepoDir, sandbox);
console.log("Git user configured successfully.");
const checkoutBranchRes = await checkoutBranch(
absoluteRepoDir,
state.branchName ?? getBranchName(config),
sandbox,
);
if (!checkoutBranchRes) {
// TODO: This should probably be an interrupt.
console.error("\nFailed to checkout branch.");
throw new Error("Failed to checkout branch");
}
if (sandbox_language === "python") {
throw new Error("Python sandbox not implemented yet.");
}
throw new Error("Unsupported sandbox language: " + sandbox_language);
return {
sandboxSessionId: sandbox.sandboxId,
};
}

View file

@ -1,10 +1,16 @@
import { isAIMessage, ToolMessage } from "@langchain/core/messages";
import { applyPatchTool, shellTool } from "../tools/index.js";
import { GraphState, GraphConfig, GraphUpdate } from "../types.js";
import {
checkoutBranchAndCommit,
getChangedFilesStatus,
getRepoAbsolutePath,
} from "../utils/git/index.js";
import { Sandbox } from "@e2b/code-interpreter";
export async function takeAction(
state: GraphState,
_config: GraphConfig,
config: GraphConfig,
): Promise<GraphUpdate> {
const lastMessage = state.messages[state.messages.length - 1];
@ -42,7 +48,26 @@ export async function takeAction(
name: toolCall.name,
});
// Always check if there are changed files after running a tool.
// If there are, commit them.
const sandbox = await Sandbox.connect(state.sandboxSessionId);
const changedFiles = await getChangedFilesStatus(
getRepoAbsolutePath(config),
sandbox,
);
let branchName: string | undefined = state.branchName;
if (changedFiles.length > 0) {
console.log(`\nHas ${changedFiles.length} changed files. Committing...`, {
changedFiles,
});
branchName = await checkoutBranchAndCommit(config, sandbox, {
branchName,
});
}
return {
messages: [toolMessage],
...(branchName && { branchName }),
};
}

View file

@ -5,7 +5,7 @@
* Steps:
*/
import { END, Send, START, StateGraph } from "@langchain/langgraph";
import { END, START, StateGraph } from "@langchain/langgraph";
import { PlannerGraphState, PlannerGraphStateObj } from "./types.js";
import { GraphConfiguration } from "../../types.js";
import {
@ -14,11 +14,11 @@ import {
summarizer,
takeAction,
} from "./nodes/index.js";
import { isAIMessage, ToolMessage } from "@langchain/core/messages";
import { isAIMessage } from "@langchain/core/messages";
function takeActionOrGeneratePlan(
state: PlannerGraphState,
): "take-plan-action" | "generate-plan" | Send {
): "take-plan-action" | "generate-plan" {
const { plannerMessages } = state;
const lastMessage = plannerMessages[plannerMessages.length - 1];
// If the last message is a tool call, and we have executed less than 6 actions, take action.
@ -32,23 +32,6 @@ function takeActionOrGeneratePlan(
return "take-plan-action";
}
if (isAIMessage(lastMessage) && lastMessage.tool_calls?.length) {
// If this is true, we need to return a `Command` adding a ToolMessage to the state
// so that the last AI message has a tool message pair
const lastMessageToolCall = lastMessage.tool_calls[0];
return new Send("generate-plan", {
...state,
plannerMessages: [
...state.plannerMessages,
new ToolMessage({
tool_call_id: lastMessageToolCall.id ?? "",
name: lastMessageToolCall.name,
content: "Tool call not executed. Max actions reached.",
}),
],
});
}
// If the last message does not have tool calls, continue to generate plan without modifications.
return "generate-plan";
}

View file

@ -1,4 +1,8 @@
import { isHumanMessage } from "@langchain/core/messages";
import {
isAIMessage,
isHumanMessage,
ToolMessage,
} from "@langchain/core/messages";
import { sessionPlanTool } from "../../../tools/index.js";
import { GraphConfig } from "../../../types.js";
import { loadModel, Task } from "../../../utils/load-model.js";
@ -29,6 +33,17 @@ export async function generatePlan(
const firstUserMessage = state.messages.find(isHumanMessage);
let optionalToolMessage: ToolMessage | undefined;
const lastMessage = state.plannerMessages[state.plannerMessages.length - 1];
if (isAIMessage(lastMessage) && lastMessage.tool_calls?.[0]) {
const lastMessageToolCall = lastMessage.tool_calls?.[0];
optionalToolMessage = new ToolMessage({
tool_call_id: lastMessageToolCall.id ?? "",
name: lastMessageToolCall.name,
content: "Tool call not executed. Max actions reached.",
});
}
const response = await modelWithTools
.bind({ tags: ["langsmith:nostream"] })
.invoke([
@ -38,6 +53,7 @@ export async function generatePlan(
},
...(firstUserMessage ? [firstUserMessage] : []),
...state.plannerMessages,
...(optionalToolMessage ? [optionalToolMessage] : []),
]);
if (!response.tool_calls?.length) {
@ -54,5 +70,7 @@ export async function generatePlan(
proposedPlan: response.tool_calls[0].args.plan,
plan: [],
...(newSessionId && { sandboxSessionId: newSessionId }),
// Do this so that the planner state is up to date with the tool call.
...(optionalToolMessage && { plannerMessages: [optionalToolMessage] }),
};
}

View file

@ -34,6 +34,9 @@ export const applyPatchTool = tool(
return readFileOutput;
}
console.log(`\nApplying patch to file ${file_path}\n`);
console.log("\nreadFileOutput\n", readFileOutput);
console.log("\ndiff\n", diff);
const patchedContent = applyPatch(readFileOutput, diff);
if (patchedContent === false) {

View file

@ -54,6 +54,13 @@ export const GraphAnnotation = z.object({
.string()
.optional()
.langgraph.reducer((_state, update) => update),
/**
* The name of the branch changes in this thread will be pushed to
*/
branchName: z
.string()
.optional()
.langgraph.reducer((_state, update) => update),
});
export type GraphState = z.infer<typeof GraphAnnotation>;
@ -143,29 +150,6 @@ export const GraphConfiguration = z.object({
}`,
},
}),
/**
* The language of the sandbox to use.
*/
sandbox_language: z
.enum(["js", "python"])
.optional()
.langgraph.metadata({
x_oap_ui_config: {
type: "select",
default: "js",
description: "The primary language of the sandbox to use.",
options: [
{
label: "JavaScript/TypeScript",
value: "js",
},
{
label: "Python",
value: "python",
},
],
},
}),
/**
* The model ID to use for the planning step.
* This includes initial planning, and rewriting.

346
src/utils/git/index.ts Normal file
View file

@ -0,0 +1,346 @@
import { CommandResult, Sandbox } from "@e2b/code-interpreter";
import { GraphConfig } from "../../types.js";
import { TIMEOUT_MS } from "../../constants.js";
import { getSandboxErrorFields } from "../sandbox-error-fields.js";
export function getRepoAbsolutePath(config: GraphConfig): string {
const repoName = config.configurable?.target_repository.repo;
if (!repoName) {
throw new Error("No repository name provided");
}
return `/home/user/${repoName}`;
}
export function getBranchName(config: GraphConfig): string {
const threadId = config.configurable?.thread_id;
if (!threadId) {
throw new Error("No thread ID provided");
}
return `open-swe/${threadId}`;
}
export async function checkoutBranch(
absoluteRepoDir: string,
branchName: string,
sandbox: Sandbox,
): Promise<CommandResult | false> {
console.log("\nChecking out branch...", {
branchName,
});
try {
const getCurrentBranchOutput = await sandbox.commands.run(
"git branch --show-current",
{ cwd: absoluteRepoDir },
);
await sandbox.setTimeout(TIMEOUT_MS);
if (getCurrentBranchOutput.exitCode !== 0) {
console.error("Failed to get current branch", getCurrentBranchOutput);
} else {
const currentBranch = getCurrentBranchOutput.stdout.trim();
if (currentBranch === branchName) {
console.log(`\nAlready on branch '${branchName}'. No checkout needed.`);
return {
stdout: `Already on branch ${branchName}`,
stderr: "",
exitCode: 0,
};
}
}
} catch (e) {
const errorFields = getSandboxErrorFields(e);
console.error("Failed to get current branch", errorFields ?? e);
return false;
}
let checkoutCommand: string;
try {
console.log("\nChecking if branch exists...", {
command: `git rev-parse --verify --quiet "refs/heads/${branchName}"`,
});
// Check if branch exists using git rev-parse for robustness
const checkBranchExistsOutput = await sandbox.commands.run(
`git rev-parse --verify --quiet "refs/heads/${branchName}"`,
{ cwd: absoluteRepoDir },
);
await sandbox.setTimeout(TIMEOUT_MS);
if (checkBranchExistsOutput.exitCode === 0) {
// Branch exists (rev-parse exit code 0 means success)
checkoutCommand = `git checkout "${branchName}"`;
} else {
// Branch does not exist (rev-parse non-zero exit code) or other error.
// Attempt to create it.
checkoutCommand = `git checkout -b "${branchName}"`;
}
} catch (e: unknown) {
const errorFields = getSandboxErrorFields(e);
if (
errorFields &&
errorFields.exitCode === 1 &&
errorFields.stderr === ""
) {
checkoutCommand = `git checkout -b "${branchName}"`;
} else {
console.error("\nError checking if branch exists", e);
return false;
}
}
try {
const gitCheckoutOutput = await sandbox.commands.run(checkoutCommand, {
cwd: absoluteRepoDir,
});
if (gitCheckoutOutput.exitCode !== 0) {
console.error("\nFailed to checkout branch", gitCheckoutOutput);
return false;
}
console.log("\nChecked out branch successfully.", {
branchName,
gitCheckoutOutput: gitCheckoutOutput.stdout,
});
return gitCheckoutOutput;
} catch (e) {
const errorFields = getSandboxErrorFields(e);
console.error("Error checking out branch", errorFields ?? e);
return false;
}
}
interface GitHubUserResponse {
login: string;
id: number;
name: string | null;
email: string | null;
}
async function getGitUserDetailsFromGitHub(): Promise<{
userName?: string;
userEmail?: string;
}> {
const githubToken = process.env.GITHUB_PAT;
if (!githubToken) {
console.warn(
"GITHUB_PAT environment variable is not set. Cannot fetch user details from GitHub.",
);
return {};
}
try {
const response = await fetch("https://api.github.com/user", {
headers: {
Authorization: `token ${githubToken}`,
Accept: "application/vnd.github.v3+json",
},
});
if (!response.ok) {
console.error(
`Failed to fetch GitHub user info: ${response.status} ${response.statusText}. Response: ${await response.text()}`,
);
return {};
}
const userData = (await response.json()) as GitHubUserResponse;
const fetchedUserName = userData.name || userData.login;
let fetchedUserEmail = userData.email; // This can be string | null
if (!fetchedUserEmail && userData.id && userData.login) {
fetchedUserEmail = `${userData.id}+${userData.login}@users.noreply.github.com`;
} else if (!fetchedUserEmail && userData.login) {
fetchedUserEmail = `${userData.login}@users.noreply.github.com`;
}
const finalUserName = fetchedUserName || undefined;
const finalUserEmail = fetchedUserEmail || undefined;
if (!finalUserName) {
console.warn("Could not determine GitHub username from API response.");
}
if (!finalUserEmail) {
console.warn("Could not determine GitHub user email from API response.");
}
return { userName: finalUserName, userEmail: finalUserEmail };
} catch (e) {
console.error("Error fetching GitHub user info:", e);
return {};
}
}
export async function configureGitUserInRepo(
absoluteRepoDir: string,
sandbox: Sandbox,
): Promise<void> {
let needsGitConfig = false;
try {
const nameCheck = await sandbox.commands.run("git config user.name", {
cwd: absoluteRepoDir,
});
await sandbox.setTimeout(TIMEOUT_MS);
const emailCheck = await sandbox.commands.run("git config user.email", {
cwd: absoluteRepoDir,
});
await sandbox.setTimeout(TIMEOUT_MS);
if (
nameCheck.exitCode !== 0 ||
nameCheck.stdout.trim() === "" ||
emailCheck.exitCode !== 0 ||
emailCheck.stdout.trim() === ""
) {
needsGitConfig = true;
}
} catch (checkError) {
console.warn(
"Could not check existing git config, will attempt to set it:",
checkError,
);
needsGitConfig = true;
}
if (needsGitConfig) {
const { userName, userEmail } = await getGitUserDetailsFromGitHub();
if (userName) {
const configUserNameOutput = await sandbox.commands.run(
`git config user.name "${userName}"`,
{ cwd: absoluteRepoDir },
);
await sandbox.setTimeout(TIMEOUT_MS);
if (configUserNameOutput.exitCode !== 0) {
console.error(
"Failed to set git user.name:",
configUserNameOutput.stderr || configUserNameOutput.stdout,
);
} else {
console.log("\nSet git user.name successfully.", {
userName,
});
}
}
if (userEmail) {
const configUserEmailOutput = await sandbox.commands.run(
`git config user.email "${userEmail}"`,
{ cwd: absoluteRepoDir },
);
await sandbox.setTimeout(TIMEOUT_MS);
if (configUserEmailOutput.exitCode !== 0) {
console.error(
"Failed to set git user.email:",
configUserEmailOutput.stderr || configUserEmailOutput.stdout,
);
} else {
console.log("\nSet git user.email successfully.", {
userEmail,
});
}
}
} else {
console.log(
"Git user.name and user.email are already configured in this repository.",
);
}
}
export async function commitAll(
absoluteRepoDir: string,
message: string,
sandbox: Sandbox,
): Promise<CommandResult | false> {
try {
const gitAddOutput = await sandbox.commands.run(
`git add -A && git commit -m "${message}"`,
{ cwd: absoluteRepoDir },
);
await sandbox.setTimeout(TIMEOUT_MS);
if (gitAddOutput.exitCode !== 0) {
console.error(
"Failed to commit all changes to git repository",
gitAddOutput,
);
}
return gitAddOutput;
} catch (e) {
console.error("Failed to commit all changes to git repository", e);
return false;
}
}
export async function commitAllAndPush(
absoluteRepoDir: string,
message: string,
sandbox: Sandbox,
): Promise<CommandResult | false> {
try {
const commitOutput = await commitAll(absoluteRepoDir, message, sandbox);
const pushCurrentBranchCmd =
"git push -u origin $(git rev-parse --abbrev-ref HEAD)";
if (!commitOutput || commitOutput.exitCode !== 0) {
return false;
}
const gitPushOutput = await sandbox.commands.run(pushCurrentBranchCmd, {
cwd: absoluteRepoDir,
});
await sandbox.setTimeout(TIMEOUT_MS);
if (gitPushOutput.exitCode !== 0) {
console.error("Failed to push changes to git repository", gitPushOutput);
return false;
}
return gitPushOutput;
} catch (e) {
console.error("Failed to commit all and push changes to git repository", e);
return false;
}
}
export async function getChangedFilesStatus(
absoluteRepoDir: string,
sandbox: Sandbox,
): Promise<string[]> {
const gitStatusOutput = await sandbox.commands.run("git status --porcelain", {
cwd: absoluteRepoDir,
});
if (gitStatusOutput.exitCode !== 0) {
console.error("Failed to get changed files status", gitStatusOutput);
return [];
}
return gitStatusOutput.stdout
.split("\n")
.map((line) => line.trim())
.filter((line) => line !== "");
}
export async function checkoutBranchAndCommit(
config: GraphConfig,
sandbox: Sandbox,
options?: {
branchName?: string;
},
): Promise<string> {
console.log("\nChecking out branch and committing changes...");
const absoluteRepoDir = getRepoAbsolutePath(config);
const branchName = options?.branchName || getBranchName(config);
await checkoutBranch(absoluteRepoDir, branchName, sandbox);
console.log(`Committing changes to branch ${branchName}`);
await commitAllAndPush(absoluteRepoDir, "Apply patch", sandbox);
console.log("Successfully checked out & committed changes.\n");
return branchName;
}

View file

@ -1,5 +1,6 @@
import { Sandbox } from "@e2b/code-interpreter";
import { TIMEOUT_MS } from "../constants.js";
import { getSandboxErrorFields } from "./sandbox-error-fields.js";
export async function readFile(
sandbox: Sandbox,
@ -15,7 +16,7 @@ export async function readFile(
if (readOutput.exitCode !== 0) {
console.error(
`Error reading file '${filePath}' from sandbox via cat:`,
`\nError reading file '${filePath}' from sandbox via cat:`,
readOutput,
);
return {
@ -25,7 +26,7 @@ export async function readFile(
}
if (readOutput.stderr) {
console.warn(
`Stderr while reading file '${filePath}' from sandbox via cat: ${readOutput.stderr}`,
`\nStderr while reading file '${filePath}' from sandbox via cat: ${readOutput.stderr}`,
);
}
return {
@ -34,12 +35,20 @@ export async function readFile(
};
} catch (e: any) {
console.error(
`Exception while trying to read file '${filePath}' from sandbox via cat:`,
`\nException while trying to read file '${filePath}' from sandbox via cat:`,
e,
);
let outputMessage = `FAILED TO EXECUTE READ COMMAND for sandbox '${filePath}'.`;
const errorFields = getSandboxErrorFields(e);
if (errorFields) {
outputMessage += `\nExit code: ${errorFields.exitCode}.\nStderr: ${errorFields.stderr}.\nStdout: ${errorFields.stdout}`;
} else {
outputMessage += ` Error: ${(e as Error).message || String(e)}`;
}
return {
success: false,
output: `FAILED TO EXECUTE READ COMMAND for sandbox '${filePath}'. Error: ${(e as Error).message || String(e)}`,
output: outputMessage,
};
}
}
@ -60,7 +69,7 @@ export async function writeFile(
if (writeOutput.exitCode !== 0) {
console.error(
`Error writing file '${filePath}' to sandbox via printf:`,
`\nError writing file '${filePath}' to sandbox via printf:`,
writeOutput,
);
return {
@ -70,7 +79,7 @@ export async function writeFile(
}
if (writeOutput.stderr) {
console.warn(
`Stderr while writing file '${filePath}' to sandbox via printf: ${writeOutput.stderr}`,
`\nStderr while writing file '${filePath}' to sandbox via printf: ${writeOutput.stderr}`,
);
}
return {
@ -79,12 +88,21 @@ export async function writeFile(
};
} catch (e: any) {
console.error(
`Exception while trying to write file '${filePath}' to sandbox via printf:`,
`\nException while trying to write file '${filePath}' to sandbox via printf:`,
e,
);
let outputMessage = `FAILED TO EXECUTE WRITE COMMAND for sandbox '${filePath}'.`;
const errorFields = getSandboxErrorFields(e);
if (errorFields) {
outputMessage += `\nExit code: ${errorFields.exitCode}.\nStderr: ${errorFields.stderr}.\nStdout: ${errorFields.stdout}`;
} else {
outputMessage += ` Error: ${(e as Error).message || String(e)}`;
}
return {
success: false,
output: `FAILED TO EXECUTE WRITE COMMAND for sandbox '${filePath}'. Error: ${(e as Error).message || String(e)}`,
output: outputMessage,
};
}
}

View file

@ -0,0 +1,20 @@
import { CommandResult } from "@e2b/code-interpreter";
export function getSandboxErrorFields(
error: unknown,
): CommandResult | undefined {
if (
!error ||
typeof error !== "object" ||
!("result" in error) ||
!error.result ||
typeof error.result !== "object" ||
!("exitCode" in error.result) ||
!("stderr" in error.result) ||
!("stdout" in error.result)
) {
return undefined;
}
return error.result as CommandResult;
}

15
src/utils/shell.ts Normal file
View file

@ -0,0 +1,15 @@
export function isWriteCommand(command: string[]): boolean {
const writeCommands = [
"cat",
"echo",
"printf",
"tee",
"cp",
"mv",
"ln",
"install",
"rsync",
];
return writeCommands.includes(command[0]);
}

View file

@ -934,6 +934,16 @@
yaml "^2.7.0"
zod "^3.23.8"
"@langchain/langgraph-sdk@^0.0.78":
version "0.0.78"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.78.tgz#898fbd5d844e3b8a74341df89af008d878956766"
integrity sha512-skkUDmEhClWzlsr8jRaS1VpXVBISm5OFd0MUtS1jKRL5pn08K+IJRvHnlzgum9x7Dste9KXGcIGVoR7cNKJQrw==
dependencies:
"@types/json-schema" "^7.0.15"
p-queue "^6.6.2"
p-retry "4"
uuid "^9.0.0"
"@langchain/langgraph-sdk@~0.0.32":
version "0.0.74"
resolved "https://registry.yarnpkg.com/@langchain/langgraph-sdk/-/langgraph-sdk-0.0.74.tgz#ef74d34d3c36437cb3d5249ea79091b8192c9b0c"