mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-02 12:03:15 +00:00
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:
parent
513cf6556d
commit
4ec7c31733
19 changed files with 776 additions and 121 deletions
8
e2b.Dockerfile
Normal file
8
e2b.Dockerfile
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
2
e2b.toml
2
e2b.toml
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
94
scripts/run-e2e.ts
Normal 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
150
scripts/run-from-plan.ts
Normal 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);
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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] }),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
30
src/types.ts
30
src/types.ts
|
|
@ -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
346
src/utils/git/index.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
20
src/utils/sandbox-error-fields.ts
Normal file
20
src/utils/sandbox-error-fields.ts
Normal 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
15
src/utils/shell.ts
Normal 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]);
|
||||
}
|
||||
10
yarn.lock
10
yarn.lock
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue