mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 17:12:11 +00:00
feat: add planner and manager changes (#602)
* openswe cli: add planner and manager changes * openswe cli: fix programmer config * openswe cli: formatting * openswe cli: add config to reviewer * Potential fix for code scanning alert no. 17: Incomplete string escaping or encoding Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * openswe cli: update code security * feat: use commander * fix: debug * fix: comment * fix: remove config * fix: updates * fix: local mode * fix: repeated islocalmodes * fix: update imports * fix: revert wrap script and commands * fix: adjust types * fix: update function signatures * fix: final shell exec fix * fix: update tsconfig for test * fix: add run input type * fix: update types * fix: reent user message * fixes from code review --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: bracesproul <braceasproul@gmail.com>
This commit is contained in:
parent
0b23ca97c4
commit
46b435a543
46 changed files with 1709 additions and 377 deletions
|
|
@ -19,6 +19,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@langchain/langgraph-sdk": "^0.0.95",
|
"@langchain/langgraph-sdk": "^0.0.95",
|
||||||
"@open-swe/shared": "*",
|
"@open-swe/shared": "*",
|
||||||
|
"commander": "^12.0.0",
|
||||||
"dotenv": "^16.6.1",
|
"dotenv": "^16.6.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"ink": "^6.0.1",
|
"ink": "^6.0.1",
|
||||||
|
|
|
||||||
1
apps/cli/src/constants.ts
Normal file
1
apps/cli/src/constants.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export const OPEN_SWE_CLI_VERSION = "0.0.0";
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { render, Box, Text, useInput } from "ink";
|
import { render, Box, Text, useInput } from "ink";
|
||||||
|
import { Command } from "commander";
|
||||||
import {
|
import {
|
||||||
startAuthServer,
|
startAuthServer,
|
||||||
getAccessToken,
|
getAccessToken,
|
||||||
|
|
@ -12,15 +13,31 @@ import {
|
||||||
MANAGER_GRAPH_ID,
|
MANAGER_GRAPH_ID,
|
||||||
OPEN_SWE_STREAM_MODE,
|
OPEN_SWE_STREAM_MODE,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
import { Client } from "@langchain/langgraph-sdk";
|
import { Client, StreamMode } from "@langchain/langgraph-sdk";
|
||||||
import { submitFeedback } from "./utils.js";
|
import { submitFeedback } from "./utils.js";
|
||||||
import { StreamingService } from "./streaming.js";
|
import { StreamingService } from "./streaming.js";
|
||||||
|
import { OPEN_SWE_CLI_VERSION } from "./constants.js";
|
||||||
type StreamMode = "values" | "updates" | "messages";
|
|
||||||
|
|
||||||
const GITHUB_LOGIN_URL =
|
const GITHUB_LOGIN_URL =
|
||||||
process.env.GITHUB_LOGIN_URL || "http://localhost:3000/api/auth/github/login";
|
process.env.GITHUB_LOGIN_URL || "http://localhost:3000/api/auth/github/login";
|
||||||
|
|
||||||
|
// Set up Commander.js
|
||||||
|
const program = new Command();
|
||||||
|
|
||||||
|
program
|
||||||
|
.name("open-swe")
|
||||||
|
.description("Open SWE CLI")
|
||||||
|
.version(OPEN_SWE_CLI_VERSION)
|
||||||
|
.option(
|
||||||
|
"--local",
|
||||||
|
"Work directly on local codebase without GitHub authentication",
|
||||||
|
)
|
||||||
|
.helpOption("-h, --help", "Display help for command")
|
||||||
|
.parse();
|
||||||
|
|
||||||
|
const options = program.opts();
|
||||||
|
const isLocalMode = options.local;
|
||||||
|
|
||||||
startAuthServer();
|
startAuthServer();
|
||||||
|
|
||||||
const LoadingSpinner: React.FC<{ text: string }> = ({ text }) => {
|
const LoadingSpinner: React.FC<{ text: string }> = ({ text }) => {
|
||||||
|
|
@ -168,7 +185,7 @@ const App: React.FC = () => {
|
||||||
const [authInput, setAuthInput] = useState("");
|
const [authInput, setAuthInput] = useState("");
|
||||||
const [exit, setExit] = useState(false);
|
const [exit, setExit] = useState(false);
|
||||||
const [authStarted, setAuthStarted] = useState(false);
|
const [authStarted, setAuthStarted] = useState(false);
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(isLocalMode);
|
||||||
const [repos, setRepos] = useState<any[]>([]);
|
const [repos, setRepos] = useState<any[]>([]);
|
||||||
const [selectedRepo, setSelectedRepo] = useState<any | null>(null);
|
const [selectedRepo, setSelectedRepo] = useState<any | null>(null);
|
||||||
const [selectingRepo, setSelectingRepo] = useState(false);
|
const [selectingRepo, setSelectingRepo] = useState(false);
|
||||||
|
|
@ -235,6 +252,18 @@ const App: React.FC = () => {
|
||||||
|
|
||||||
// On mount, check for existing token
|
// On mount, check for existing token
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isLocalMode) {
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
setLoadingRepos(false); // Ensure no loading state in local mode
|
||||||
|
// Set up local mode defaults
|
||||||
|
setSelectedRepo({
|
||||||
|
full_name: process.env.OPEN_SWE_LOCAL_PROJECT_PATH || "",
|
||||||
|
clone_url: process.env.OPEN_SWE_LOCAL_PROJECT_PATH || "",
|
||||||
|
default_branch: "main",
|
||||||
|
});
|
||||||
|
setInstallChecked(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const token = getAccessToken();
|
const token = getAccessToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
|
|
@ -243,6 +272,7 @@ const App: React.FC = () => {
|
||||||
|
|
||||||
// After login, fetch and store user repos
|
// After login, fetch and store user repos
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isLocalMode) return;
|
||||||
if (isLoggedIn && repos.length === 0 && !loadingRepos) {
|
if (isLoggedIn && repos.length === 0 && !loadingRepos) {
|
||||||
const token = getAccessToken();
|
const token = getAccessToken();
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|
@ -263,6 +293,7 @@ const App: React.FC = () => {
|
||||||
|
|
||||||
// Poll for installation_id after opening install page
|
// Poll for installation_id after opening install page
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isLocalMode) return;
|
||||||
let interval: ReturnType<typeof setInterval>;
|
let interval: ReturnType<typeof setInterval>;
|
||||||
if (waitingForInstall) {
|
if (waitingForInstall) {
|
||||||
interval = setInterval(() => {
|
interval = setInterval(() => {
|
||||||
|
|
@ -324,6 +355,20 @@ const App: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [authPrompt, authStarted]);
|
}, [authPrompt, authStarted]);
|
||||||
|
|
||||||
|
// Poll for token after auth flow starts
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLocalMode || !pollingForToken || isLoggedIn) return;
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const token = getAccessToken();
|
||||||
|
if (token) {
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
setPollingForToken(false);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [pollingForToken, isLoggedIn]);
|
||||||
|
|
||||||
// Poll for token after auth flow starts
|
// Poll for token after auth flow starts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pollingForToken && !isLoggedIn) {
|
if (pollingForToken && !isLoggedIn) {
|
||||||
|
|
@ -338,47 +383,52 @@ const App: React.FC = () => {
|
||||||
}
|
}
|
||||||
}, [pollingForToken, isLoggedIn]);
|
}, [pollingForToken, isLoggedIn]);
|
||||||
|
|
||||||
// Custom input for planner feedback (must be inside App)
|
const PlannerFeedbackInput: React.FC = () => {
|
||||||
const PlanFeedbackSelect: React.FC = () => {
|
const [selectedOption, setSelectedOption] = useState<
|
||||||
const [highlighted, setHighlighted] = useState(0);
|
"approve" | "deny" | null
|
||||||
const [isMessage, setIsMessage] = useState(false);
|
>(null);
|
||||||
|
|
||||||
const options = [
|
useInput((inputChar: string, key: { [key: string]: any }) => {
|
||||||
{ value: "approve", label: "Approve" },
|
|
||||||
{ value: "deny", label: "Deny" },
|
|
||||||
];
|
|
||||||
|
|
||||||
useInput((input: string, key: { [key: string]: any }) => {
|
|
||||||
if (streamingPhase !== "awaitingFeedback") return;
|
if (streamingPhase !== "awaitingFeedback") return;
|
||||||
if (isMessage) return;
|
|
||||||
|
|
||||||
if (key.return) {
|
if (key.return && selectedOption) {
|
||||||
setIsMessage(true);
|
setPlannerFeedback(selectedOption);
|
||||||
setPlannerFeedback(options[highlighted].value);
|
setSelectedOption(null);
|
||||||
} else if (key.leftArrow) {
|
} else if (key.leftArrow) {
|
||||||
setHighlighted((h) => (h - 1 + options.length) % options.length);
|
setSelectedOption("approve");
|
||||||
} else if (key.rightArrow) {
|
} else if (key.rightArrow) {
|
||||||
setHighlighted((h) => (h + 1) % options.length);
|
setSelectedOption("deny");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (streamingPhase !== "awaitingFeedback") return null;
|
if (streamingPhase !== "awaitingFeedback") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="row" alignItems="center" gap={2}>
|
<Box flexDirection="row" alignItems="center" gap={2}>
|
||||||
<Text>Plan feedback:</Text>
|
<Text>Plan feedback: </Text>
|
||||||
{options.map((option, idx) => (
|
<Box
|
||||||
<Text
|
borderStyle="round"
|
||||||
key={option.value}
|
borderColor={selectedOption === "approve" ? "green" : "white"}
|
||||||
dimColor={idx !== highlighted}
|
paddingX={2}
|
||||||
bold={idx === highlighted}
|
paddingY={0}
|
||||||
>
|
>
|
||||||
{idx === highlighted ? "[" : " "}
|
<Text color={selectedOption === "approve" ? "green" : "white"}>
|
||||||
{option.label}
|
{selectedOption === "approve" ? "▶ " : " "}Approve
|
||||||
{idx === highlighted ? "]" : " "}
|
|
||||||
</Text>
|
</Text>
|
||||||
))}
|
</Box>
|
||||||
<Text dimColor>Use ←/→ to navigate, Enter to select</Text>
|
<Box
|
||||||
|
borderStyle="round"
|
||||||
|
borderColor={selectedOption === "deny" ? "red" : "white"}
|
||||||
|
paddingX={2}
|
||||||
|
paddingY={0}
|
||||||
|
>
|
||||||
|
<Text color={selectedOption === "deny" ? "red" : "white"}>
|
||||||
|
{selectedOption === "deny" ? "▶ " : " "}Deny
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Text dimColor>(Use ←/→ to select, Enter to confirm)</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -390,9 +440,6 @@ const App: React.FC = () => {
|
||||||
plannerFeedback &&
|
plannerFeedback &&
|
||||||
plannerThreadId
|
plannerThreadId
|
||||||
) {
|
) {
|
||||||
// Immediately switch to streaming mode to hide the feedback prompt
|
|
||||||
setStreamingPhase("streaming");
|
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await submitFeedback({
|
await submitFeedback({
|
||||||
plannerFeedback,
|
plannerFeedback,
|
||||||
|
|
@ -400,6 +447,7 @@ const App: React.FC = () => {
|
||||||
selectedRepo,
|
selectedRepo,
|
||||||
setLogs,
|
setLogs,
|
||||||
setPlannerFeedback: () => setPlannerFeedback(null),
|
setPlannerFeedback: () => setPlannerFeedback(null),
|
||||||
|
setStreamingPhase,
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
|
|
@ -415,7 +463,12 @@ const App: React.FC = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repo selection UI
|
// Repo selection UI
|
||||||
if (isLoggedIn && repos.length > 0 && (selectingRepo || !selectedRepo)) {
|
if (
|
||||||
|
isLoggedIn &&
|
||||||
|
repos.length > 0 &&
|
||||||
|
(selectingRepo || !selectedRepo) &&
|
||||||
|
!isLocalMode
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" padding={1}>
|
<Box flexDirection="column" padding={1}>
|
||||||
<Box justifyContent="center" marginBottom={1}>
|
<Box justifyContent="center" marginBottom={1}>
|
||||||
|
|
@ -494,6 +547,13 @@ const App: React.FC = () => {
|
||||||
|
|
||||||
// Main UI: logs area + input prompt
|
// Main UI: logs area + input prompt
|
||||||
if (isLoggedIn && selectedRepo) {
|
if (isLoggedIn && selectedRepo) {
|
||||||
|
const modeIndicator = isLocalMode ? (
|
||||||
|
<Box paddingX={2} paddingY={1}>
|
||||||
|
<Text dimColor>
|
||||||
|
🏠 Local Mode - Working on {process.env.OPEN_SWE_LOCAL_PROJECT_PATH}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
) : null;
|
||||||
// Calculate available space for logs based on whether welcome message is shown
|
// Calculate available space for logs based on whether welcome message is shown
|
||||||
const headerHeight = 0; // Welcome message is now above input bar, not at top
|
const headerHeight = 0; // Welcome message is now above input bar, not at top
|
||||||
const inputHeight = 4; // Fixed input area height (increased due to padding)
|
const inputHeight = 4; // Fixed input area height (increased due to padding)
|
||||||
|
|
@ -514,6 +574,7 @@ const App: React.FC = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" height={process.stdout.rows}>
|
<Box flexDirection="column" height={process.stdout.rows}>
|
||||||
|
{modeIndicator}
|
||||||
{/* Auto-scrolling logs area - strict boundary container */}
|
{/* Auto-scrolling logs area - strict boundary container */}
|
||||||
<Box
|
<Box
|
||||||
height={availableLogHeight}
|
height={availableLogHeight}
|
||||||
|
|
@ -581,7 +642,9 @@ const App: React.FC = () => {
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
{!hasStartedChat ? (
|
{streamingPhase === "awaitingFeedback" ? (
|
||||||
|
<PlannerFeedbackInput />
|
||||||
|
) : !hasStartedChat ? (
|
||||||
<CustomInput
|
<CustomInput
|
||||||
onSubmit={(value) => {
|
onSubmit={(value) => {
|
||||||
setHasStartedChat(true);
|
setHasStartedChat(true);
|
||||||
|
|
@ -608,19 +671,12 @@ const App: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Plan feedback below the input bar */}
|
|
||||||
{streamingPhase === "awaitingFeedback" && (
|
|
||||||
<Box flexDirection="column" paddingX={2} marginTop={1}>
|
|
||||||
<PlanFeedbackSelect />
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth prompt UI
|
// Auth prompt UI
|
||||||
if (!isLoggedIn && authPrompt === null) {
|
if (!isLoggedIn && authPrompt === null && !isLocalMode) {
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" padding={1}>
|
<Box flexDirection="column" padding={1}>
|
||||||
<Box
|
<Box
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
GITHUB_INSTALLATION_NAME,
|
GITHUB_INSTALLATION_NAME,
|
||||||
GITHUB_INSTALLATION_ID,
|
GITHUB_INSTALLATION_ID,
|
||||||
OPEN_SWE_STREAM_MODE,
|
OPEN_SWE_STREAM_MODE,
|
||||||
|
LOCAL_MODE_HEADER,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
import {
|
import {
|
||||||
getAccessToken,
|
getAccessToken,
|
||||||
|
|
@ -16,9 +17,13 @@ import {
|
||||||
} from "./auth-server.js";
|
} from "./auth-server.js";
|
||||||
import { formatDisplayLog } from "./logger.js";
|
import { formatDisplayLog } from "./logger.js";
|
||||||
import { isAgentInboxInterruptSchema } from "@open-swe/shared/agent-inbox-interrupt";
|
import { isAgentInboxInterruptSchema } from "@open-swe/shared/agent-inbox-interrupt";
|
||||||
|
import { ManagerGraphUpdate } from "@open-swe/shared/open-swe/manager/types";
|
||||||
|
import { HumanMessage } from "@langchain/core/messages";
|
||||||
|
|
||||||
const LANGGRAPH_URL = process.env.LANGGRAPH_URL || "http://localhost:2024";
|
const LANGGRAPH_URL = process.env.LANGGRAPH_URL || "http://localhost:2024";
|
||||||
|
|
||||||
|
type RunInput = ManagerGraphUpdate;
|
||||||
|
|
||||||
interface StreamingCallbacks {
|
interface StreamingCallbacks {
|
||||||
setLogs: (updater: (prev: string[]) => string[]) => void; // eslint-disable-line no-unused-vars
|
setLogs: (updater: (prev: string[]) => string[]) => void; // eslint-disable-line no-unused-vars
|
||||||
setPlannerThreadId: (id: string) => void; // eslint-disable-line no-unused-vars
|
setPlannerThreadId: (id: string) => void; // eslint-disable-line no-unused-vars
|
||||||
|
|
@ -105,6 +110,7 @@ export class StreamingService {
|
||||||
interruptArr && interruptArr[0] && interruptArr[0].value
|
interruptArr && interruptArr[0] && interruptArr[0].value
|
||||||
? interruptArr[0].value
|
? interruptArr[0].value
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (isAgentInboxInterruptSchema(firstInterruptValue)) {
|
if (isAgentInboxInterruptSchema(firstInterruptValue)) {
|
||||||
return { needsFeedback: true };
|
return { needsFeedback: true };
|
||||||
}
|
}
|
||||||
|
|
@ -165,49 +171,81 @@ export class StreamingService {
|
||||||
this.callbacks.setLoadingLogs(true);
|
this.callbacks.setLoadingLogs(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const userAccessToken = getAccessToken();
|
const isLocalMode = process.env.OPEN_SWE_LOCAL_MODE === "true";
|
||||||
const installationAccessToken = await getInstallationAccessToken();
|
|
||||||
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
|
||||||
|
|
||||||
if (!userAccessToken || !installationAccessToken || !encryptionKey) {
|
let headers: Record<string, string> = {};
|
||||||
this.callbacks.setLogs(() => [
|
let runInput: RunInput;
|
||||||
`Missing secrets: ${userAccessToken ? "" : "userAccessToken, "}${installationAccessToken ? "" : "installationAccessToken, "}${encryptionKey ? "" : "encryptionKey"}`,
|
|
||||||
]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const encryptedUserToken = encryptSecret(userAccessToken, encryptionKey);
|
if (isLocalMode) {
|
||||||
const encryptedInstallationToken = encryptSecret(
|
// Local mode: no GitHub authentication required
|
||||||
installationAccessToken,
|
runInput = {
|
||||||
encryptionKey,
|
messages: [
|
||||||
);
|
new HumanMessage({
|
||||||
const [owner, repoName] = selectedRepo.full_name.split("/");
|
id: uuidv4(),
|
||||||
|
content: prompt,
|
||||||
const runInput = {
|
}),
|
||||||
messages: [
|
],
|
||||||
{
|
targetRepository: {
|
||||||
id: uuidv4(),
|
owner: "local",
|
||||||
type: "human",
|
repo: "local",
|
||||||
content: [{ type: "text", text: prompt }],
|
branch: "main",
|
||||||
},
|
},
|
||||||
],
|
autoAcceptPlan: false,
|
||||||
targetRepository: {
|
};
|
||||||
owner,
|
|
||||||
repo: repoName,
|
|
||||||
branch: selectedRepo.default_branch || "main",
|
|
||||||
},
|
|
||||||
autoAcceptPlan: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const installationId = getInstallationId();
|
headers = {
|
||||||
const newClient = new Client({
|
[LOCAL_MODE_HEADER]: "true",
|
||||||
apiUrl: LANGGRAPH_URL,
|
};
|
||||||
defaultHeaders: {
|
} else {
|
||||||
|
// Normal mode: require GitHub authentication
|
||||||
|
const userAccessToken = getAccessToken();
|
||||||
|
const installationAccessToken = await getInstallationAccessToken();
|
||||||
|
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
||||||
|
|
||||||
|
if (!userAccessToken || !installationAccessToken || !encryptionKey) {
|
||||||
|
this.callbacks.setLogs(() => [
|
||||||
|
`Missing secrets: ${userAccessToken ? "" : "userAccessToken, "}${installationAccessToken ? "" : "installationAccessToken, "}${encryptionKey ? "" : "encryptionKey"}`,
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedUserToken = encryptSecret(
|
||||||
|
userAccessToken,
|
||||||
|
encryptionKey,
|
||||||
|
);
|
||||||
|
const encryptedInstallationToken = encryptSecret(
|
||||||
|
installationAccessToken,
|
||||||
|
encryptionKey,
|
||||||
|
);
|
||||||
|
const [owner, repoName] = selectedRepo.full_name.split("/");
|
||||||
|
|
||||||
|
runInput = {
|
||||||
|
messages: [
|
||||||
|
new HumanMessage({
|
||||||
|
id: uuidv4(),
|
||||||
|
content: prompt,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
targetRepository: {
|
||||||
|
owner,
|
||||||
|
repo: repoName,
|
||||||
|
branch: selectedRepo.default_branch || "main",
|
||||||
|
},
|
||||||
|
autoAcceptPlan: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const installationId = getInstallationId();
|
||||||
|
headers = {
|
||||||
[GITHUB_TOKEN_COOKIE]: encryptedUserToken,
|
[GITHUB_TOKEN_COOKIE]: encryptedUserToken,
|
||||||
[GITHUB_INSTALLATION_TOKEN_COOKIE]: encryptedInstallationToken,
|
[GITHUB_INSTALLATION_TOKEN_COOKIE]: encryptedInstallationToken,
|
||||||
[GITHUB_INSTALLATION_NAME]: owner,
|
[GITHUB_INSTALLATION_NAME]: owner,
|
||||||
[GITHUB_INSTALLATION_ID]: installationId,
|
[GITHUB_INSTALLATION_ID]: installationId,
|
||||||
},
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const newClient = new Client({
|
||||||
|
apiUrl: LANGGRAPH_URL,
|
||||||
|
defaultHeaders: headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.callbacks.setClient(newClient);
|
this.callbacks.setClient(newClient);
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { encryptSecret } from "@open-swe/shared/crypto";
|
||||||
import {
|
import {
|
||||||
OPEN_SWE_STREAM_MODE,
|
OPEN_SWE_STREAM_MODE,
|
||||||
PLANNER_GRAPH_ID,
|
PLANNER_GRAPH_ID,
|
||||||
|
LOCAL_MODE_HEADER,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
import {
|
import {
|
||||||
getAccessToken,
|
getAccessToken,
|
||||||
|
|
@ -25,44 +26,64 @@ export async function submitFeedback({
|
||||||
selectedRepo,
|
selectedRepo,
|
||||||
setLogs,
|
setLogs,
|
||||||
setPlannerFeedback,
|
setPlannerFeedback,
|
||||||
|
setStreamingPhase,
|
||||||
}: {
|
}: {
|
||||||
plannerFeedback: string;
|
plannerFeedback: string;
|
||||||
plannerThreadId: string;
|
plannerThreadId: string;
|
||||||
selectedRepo: any;
|
selectedRepo: any;
|
||||||
setLogs: (updater: (prev: string[]) => string[]) => void; // eslint-disable-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
setLogs: (updater: (prev: string[]) => string[]) => void;
|
||||||
setPlannerFeedback: () => void;
|
setPlannerFeedback: () => void;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
setStreamingPhase: (phase: "streaming" | "awaitingFeedback" | "done") => void;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const userAccessToken = getAccessToken();
|
// Set streaming phase back to streaming when feedback submission starts
|
||||||
const installationAccessToken = await getInstallationAccessToken();
|
setStreamingPhase("streaming");
|
||||||
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
|
||||||
|
|
||||||
if (!userAccessToken || !installationAccessToken || !encryptionKey) {
|
const isLocalMode = process.env.OPEN_SWE_LOCAL_MODE === "true";
|
||||||
setLogs((prev) => [
|
let client: Client;
|
||||||
...prev,
|
|
||||||
"Missing access tokens for feedback submission",
|
if (isLocalMode) {
|
||||||
]);
|
// In local mode, create client without GitHub authentication
|
||||||
return;
|
client = new Client({
|
||||||
|
apiUrl: LANGGRAPH_URL,
|
||||||
|
defaultHeaders: {
|
||||||
|
[LOCAL_MODE_HEADER]: "true", // Signal to server this is local mode
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const userAccessToken = getAccessToken();
|
||||||
|
const installationAccessToken = await getInstallationAccessToken();
|
||||||
|
const encryptionKey = process.env.SECRETS_ENCRYPTION_KEY;
|
||||||
|
|
||||||
|
if (!userAccessToken || !installationAccessToken || !encryptionKey) {
|
||||||
|
setLogs((prev) => [
|
||||||
|
...prev,
|
||||||
|
"Missing access tokens for feedback submission",
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedUserToken = encryptSecret(userAccessToken, encryptionKey);
|
||||||
|
const encryptedInstallationToken = encryptSecret(
|
||||||
|
installationAccessToken,
|
||||||
|
encryptionKey,
|
||||||
|
);
|
||||||
|
const [owner] = selectedRepo?.full_name.split("/") || [];
|
||||||
|
|
||||||
|
const installationId = getInstallationId();
|
||||||
|
client = new Client({
|
||||||
|
apiUrl: LANGGRAPH_URL,
|
||||||
|
defaultHeaders: {
|
||||||
|
"x-github-access-token": encryptedUserToken,
|
||||||
|
"x-github-installation-token": encryptedInstallationToken,
|
||||||
|
"x-github-installation-name": owner,
|
||||||
|
"x-github-installation-id": installationId,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const encryptedUserToken = encryptSecret(userAccessToken, encryptionKey);
|
|
||||||
const encryptedInstallationToken = encryptSecret(
|
|
||||||
installationAccessToken,
|
|
||||||
encryptionKey,
|
|
||||||
);
|
|
||||||
const [owner] = selectedRepo?.full_name.split("/") || [];
|
|
||||||
|
|
||||||
const installationId = getInstallationId();
|
|
||||||
const client = new Client({
|
|
||||||
apiUrl: LANGGRAPH_URL,
|
|
||||||
defaultHeaders: {
|
|
||||||
"x-github-access-token": encryptedUserToken,
|
|
||||||
"x-github-installation-token": encryptedInstallationToken,
|
|
||||||
"x-github-installation-name": owner,
|
|
||||||
"x-github-installation-id": installationId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const formatted = formatDisplayLog(`Human feedback: ${plannerFeedback}`);
|
const formatted = formatDisplayLog(`Human feedback: ${plannerFeedback}`);
|
||||||
if (formatted.length > 0) {
|
if (formatted.length > 0) {
|
||||||
setLogs((prev) => [...prev, ...formatted]);
|
setLogs((prev) => [...prev, ...formatted]);
|
||||||
|
|
@ -110,10 +131,15 @@ export async function submitFeedback({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set streaming phase to done when complete
|
||||||
|
setStreamingPhase("done");
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : "Unknown error";
|
error instanceof Error ? error.message : "Unknown error";
|
||||||
setLogs((prev) => [...prev, `Error submitting feedback: ${errorMessage}`]);
|
setLogs((prev) => [...prev, `Error submitting feedback: ${errorMessage}`]);
|
||||||
|
// Set streaming phase to done even on error
|
||||||
|
setStreamingPhase("done");
|
||||||
} finally {
|
} finally {
|
||||||
// Clear feedback state
|
// Clear feedback state
|
||||||
setPlannerFeedback();
|
setPlannerFeedback();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { GraphConfig, GraphState } from "@open-swe/shared/open-swe/types";
|
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
import {
|
import {
|
||||||
ManagerGraphState,
|
ManagerGraphState,
|
||||||
ManagerGraphUpdate,
|
ManagerGraphUpdate,
|
||||||
|
|
@ -38,11 +38,13 @@ import {
|
||||||
PLANNER_GRAPH_ID,
|
PLANNER_GRAPH_ID,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
import { createLogger, LogLevel } from "../../../../utils/logger.js";
|
import { createLogger, LogLevel } from "../../../../utils/logger.js";
|
||||||
import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
|
|
||||||
import { createClassificationPromptAndToolSchema } from "./utils.js";
|
import { createClassificationPromptAndToolSchema } from "./utils.js";
|
||||||
import { RequestSource } from "../../../../constants.js";
|
import { RequestSource } from "../../../../constants.js";
|
||||||
import { StreamMode } from "@langchain/langgraph-sdk";
|
import { StreamMode } from "@langchain/langgraph-sdk";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
import { Thread } from "@langchain/langgraph-sdk";
|
||||||
|
import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
|
||||||
|
import { GraphState } from "@open-swe/shared/open-swe/types";
|
||||||
const logger = createLogger(LogLevel.INFO, "ClassifyMessage");
|
const logger = createLogger(LogLevel.INFO, "ClassifyMessage");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -60,21 +62,23 @@ export async function classifyMessage(
|
||||||
throw new Error("No human message found.");
|
throw new Error("No human message found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let plannerThread: Thread<PlannerGraphState> | undefined;
|
||||||
|
let programmerThread: Thread<GraphState> | undefined;
|
||||||
const langGraphClient = createLangGraphClient({
|
const langGraphClient = createLangGraphClient({
|
||||||
defaultHeaders: getDefaultHeaders(config),
|
defaultHeaders: getDefaultHeaders(config),
|
||||||
});
|
});
|
||||||
|
|
||||||
const plannerThread = state.plannerSession?.threadId
|
if (!isLocalMode(config)) {
|
||||||
? await langGraphClient.threads.get<PlannerGraphState>(
|
plannerThread = state.plannerSession?.threadId
|
||||||
state.plannerSession.threadId,
|
? await langGraphClient.threads.get(state.plannerSession.threadId)
|
||||||
)
|
: undefined;
|
||||||
: undefined;
|
const plannerThreadValues = plannerThread?.values;
|
||||||
const plannerThreadValues = plannerThread?.values;
|
programmerThread = plannerThreadValues?.programmerSession?.threadId
|
||||||
const programmerThread = plannerThreadValues?.programmerSession?.threadId
|
? await langGraphClient.threads.get(
|
||||||
? await langGraphClient.threads.get<GraphState>(
|
plannerThreadValues.programmerSession.threadId,
|
||||||
plannerThreadValues.programmerSession.threadId,
|
)
|
||||||
)
|
: undefined;
|
||||||
: undefined;
|
}
|
||||||
|
|
||||||
const programmerStatus = programmerThread?.status ?? "not_started";
|
const programmerStatus = programmerThread?.status ?? "not_started";
|
||||||
const plannerStatus = plannerThread?.status ?? "not_started";
|
const plannerStatus = plannerThread?.status ?? "not_started";
|
||||||
|
|
@ -157,6 +161,28 @@ export async function classifyMessage(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLocalMode(config)) {
|
||||||
|
// In local mode, just route to planner without GitHub issue creation
|
||||||
|
const newMessages: BaseMessage[] = [response];
|
||||||
|
const commandUpdate: ManagerGraphUpdate = {
|
||||||
|
messages: newMessages,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
toolCallArgs.route === "start_planner" ||
|
||||||
|
toolCallArgs.route === "start_planner_for_followup"
|
||||||
|
) {
|
||||||
|
return new Command({
|
||||||
|
update: commandUpdate,
|
||||||
|
goto: "start-planner",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported route for local mode received: ${toolCallArgs.route}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const { githubAccessToken } = getGitHubTokensFromConfig(config);
|
const { githubAccessToken } = getGitHubTokensFromConfig(config);
|
||||||
let githubIssueId = state.githubIssueId;
|
let githubIssueId = state.githubIssueId;
|
||||||
|
|
||||||
|
|
@ -260,6 +286,9 @@ export async function classifyMessage(
|
||||||
args: "resume planner",
|
args: "resume planner",
|
||||||
};
|
};
|
||||||
logger.info("Resuming planner session");
|
logger.info("Resuming planner session");
|
||||||
|
if (!langGraphClient) {
|
||||||
|
throw new Error("LangGraph client not initialized");
|
||||||
|
}
|
||||||
const newPlannerRun = await langGraphClient.runs.create(
|
const newPlannerRun = await langGraphClient.runs.create(
|
||||||
state.plannerSession?.threadId,
|
state.plannerSession?.threadId,
|
||||||
PLANNER_GRAPH_ID,
|
PLANNER_GRAPH_ID,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { HumanMessage, isHumanMessage } from "@langchain/core/messages";
|
||||||
import { getIssue } from "../../../utils/github/api.js";
|
import { getIssue } from "../../../utils/github/api.js";
|
||||||
import { extractTasksFromIssueContent } from "../../../utils/github/issue-task.js";
|
import { extractTasksFromIssueContent } from "../../../utils/github/issue-task.js";
|
||||||
import { getMessageContentFromIssue } from "../../../utils/github/issue-messages.js";
|
import { getMessageContentFromIssue } from "../../../utils/github/issue-messages.js";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The initialize function will do nothing if there's already a human message
|
* The initialize function will do nothing if there's already a human message
|
||||||
|
|
@ -18,6 +19,11 @@ export async function initializeGithubIssue(
|
||||||
state: ManagerGraphState,
|
state: ManagerGraphState,
|
||||||
config: GraphConfig,
|
config: GraphConfig,
|
||||||
): Promise<ManagerGraphUpdate> {
|
): Promise<ManagerGraphUpdate> {
|
||||||
|
if (isLocalMode(config)) {
|
||||||
|
// In local mode, we don't need GitHub issues
|
||||||
|
// The human message should already be in the state from the CLI input
|
||||||
|
return {};
|
||||||
|
}
|
||||||
const { githubInstallationToken } = getGitHubTokensFromConfig(config);
|
const { githubInstallationToken } = getGitHubTokensFromConfig(config);
|
||||||
let taskPlan = state.taskPlan;
|
let taskPlan = state.taskPlan;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
import {
|
import {
|
||||||
ManagerGraphState,
|
ManagerGraphState,
|
||||||
ManagerGraphUpdate,
|
ManagerGraphUpdate,
|
||||||
|
|
@ -8,6 +9,7 @@ import { createLangGraphClient } from "../../../utils/langgraph-client.js";
|
||||||
import {
|
import {
|
||||||
OPEN_SWE_STREAM_MODE,
|
OPEN_SWE_STREAM_MODE,
|
||||||
PLANNER_GRAPH_ID,
|
PLANNER_GRAPH_ID,
|
||||||
|
LOCAL_MODE_HEADER,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
import { createLogger, LogLevel } from "../../../utils/logger.js";
|
import { createLogger, LogLevel } from "../../../utils/logger.js";
|
||||||
import { getBranchName } from "../../../utils/github/git.js";
|
import { getBranchName } from "../../../utils/github/git.js";
|
||||||
|
|
@ -22,20 +24,29 @@ const logger = createLogger(LogLevel.INFO, "StartPlanner");
|
||||||
/**
|
/**
|
||||||
* Start planner node.
|
* Start planner node.
|
||||||
* This node will kickoff a new planner session using the LangGraph SDK.
|
* This node will kickoff a new planner session using the LangGraph SDK.
|
||||||
|
* In local mode, creates a planner session with local mode headers.
|
||||||
*/
|
*/
|
||||||
export async function startPlanner(
|
export async function startPlanner(
|
||||||
state: ManagerGraphState,
|
state: ManagerGraphState,
|
||||||
config: GraphConfig,
|
config: GraphConfig,
|
||||||
): Promise<ManagerGraphUpdate> {
|
): Promise<ManagerGraphUpdate> {
|
||||||
const langGraphClient = createLangGraphClient({
|
|
||||||
defaultHeaders: getDefaultHeaders(config),
|
|
||||||
});
|
|
||||||
|
|
||||||
const plannerThreadId = state.plannerSession?.threadId ?? uuidv4();
|
const plannerThreadId = state.plannerSession?.threadId ?? uuidv4();
|
||||||
const followupMessage = getRecentUserRequest(state.messages, {
|
const followupMessage = getRecentUserRequest(state.messages, {
|
||||||
returnFullMessage: true,
|
returnFullMessage: true,
|
||||||
|
config,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const localMode = isLocalMode(config);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const langGraphClient = createLangGraphClient({
|
||||||
|
defaultHeaders: localMode
|
||||||
|
? {
|
||||||
|
[LOCAL_MODE_HEADER]: "true",
|
||||||
|
}
|
||||||
|
: getDefaultHeaders(config),
|
||||||
|
});
|
||||||
|
|
||||||
const runInput: PlannerGraphUpdate = {
|
const runInput: PlannerGraphUpdate = {
|
||||||
// github issue ID & target repo so the planning agent can fetch the user's request, and clone the repo.
|
// github issue ID & target repo so the planning agent can fetch the user's request, and clone the repo.
|
||||||
githubIssueId: state.githubIssueId,
|
githubIssueId: state.githubIssueId,
|
||||||
|
|
@ -44,8 +55,9 @@ export async function startPlanner(
|
||||||
taskPlan: state.taskPlan,
|
taskPlan: state.taskPlan,
|
||||||
branchName: state.branchName ?? getBranchName(config),
|
branchName: state.branchName ?? getBranchName(config),
|
||||||
autoAcceptPlan: state.autoAcceptPlan,
|
autoAcceptPlan: state.autoAcceptPlan,
|
||||||
...(followupMessage && { messages: [followupMessage] }),
|
...(followupMessage || localMode ? { messages: [followupMessage] } : {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const run = await langGraphClient.runs.create(
|
const run = await langGraphClient.runs.create(
|
||||||
plannerThreadId,
|
plannerThreadId,
|
||||||
PLANNER_GRAPH_ID,
|
PLANNER_GRAPH_ID,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ import {
|
||||||
} from "../../utils/followup.js";
|
} from "../../utils/followup.js";
|
||||||
import { SYSTEM_PROMPT } from "./prompt.js";
|
import { SYSTEM_PROMPT } from "./prompt.js";
|
||||||
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
||||||
|
import {
|
||||||
|
isLocalMode,
|
||||||
|
getLocalWorkingDirectory,
|
||||||
|
} from "@open-swe/shared/open-swe/local-mode";
|
||||||
import { getMissingMessages } from "../../../../utils/github/issue-messages.js";
|
import { getMissingMessages } from "../../../../utils/github/issue-messages.js";
|
||||||
import { getPlansFromIssue } from "../../../../utils/github/issue-task.js";
|
import { getPlansFromIssue } from "../../../../utils/github/issue-task.js";
|
||||||
import { createGrepTool } from "../../../../tools/grep.js";
|
import { createGrepTool } from "../../../../tools/grep.js";
|
||||||
|
|
@ -39,7 +43,10 @@ import { createViewTool } from "../../../../tools/builtin-tools/view.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GeneratePlanningMessageNode");
|
const logger = createLogger(LogLevel.INFO, "GeneratePlanningMessageNode");
|
||||||
|
|
||||||
function formatSystemPrompt(state: PlannerGraphState): string {
|
function formatSystemPrompt(
|
||||||
|
state: PlannerGraphState,
|
||||||
|
config: GraphConfig,
|
||||||
|
): string {
|
||||||
// It's a followup if there's more than one human message.
|
// It's a followup if there's more than one human message.
|
||||||
const isFollowup = isFollowupRequest(state.taskPlan, state.proposedPlan);
|
const isFollowup = isFollowupRequest(state.taskPlan, state.proposedPlan);
|
||||||
const scratchpad = getScratchpad(state.messages)
|
const scratchpad = getScratchpad(state.messages)
|
||||||
|
|
@ -57,7 +64,15 @@ function formatSystemPrompt(state: PlannerGraphState): string {
|
||||||
)
|
)
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
"{CURRENT_WORKING_DIRECTORY}",
|
"{CURRENT_WORKING_DIRECTORY}",
|
||||||
getRepoAbsolutePath(state.targetRepository),
|
isLocalMode(config)
|
||||||
|
? getLocalWorkingDirectory()
|
||||||
|
: getRepoAbsolutePath(state.targetRepository),
|
||||||
|
)
|
||||||
|
.replaceAll(
|
||||||
|
"{LOCAL_MODE_NOTE}",
|
||||||
|
isLocalMode(config)
|
||||||
|
? "<local_mode_note>IMPORTANT: You are running in local mode. When specifying file paths, use relative paths from the current working directory or absolute paths that start with the current working directory. Do NOT use sandbox paths like '/home/daytona/project/'.</local_mode_note>"
|
||||||
|
: "",
|
||||||
)
|
)
|
||||||
.replaceAll(
|
.replaceAll(
|
||||||
"{CODEBASE_TREE}",
|
"{CODEBASE_TREE}",
|
||||||
|
|
@ -81,9 +96,9 @@ export async function generateAction(
|
||||||
const mcpTools = await getMcpTools(config);
|
const mcpTools = await getMcpTools(config);
|
||||||
|
|
||||||
const tools = [
|
const tools = [
|
||||||
createGrepTool(state),
|
createGrepTool(state, config),
|
||||||
createShellTool(state),
|
createShellTool(state, config),
|
||||||
createViewTool(state),
|
createViewTool(state, config),
|
||||||
createScratchpadTool(
|
createScratchpadTool(
|
||||||
"when generating a final plan, after all context gathering is complete",
|
"when generating a final plan, after all context gathering is complete",
|
||||||
),
|
),
|
||||||
|
|
@ -129,10 +144,13 @@ export async function generateAction(
|
||||||
.invoke([
|
.invoke([
|
||||||
{
|
{
|
||||||
role: "system",
|
role: "system",
|
||||||
content: formatSystemPrompt({
|
content: formatSystemPrompt(
|
||||||
...state,
|
{
|
||||||
taskPlan: latestTaskPlan ?? state.taskPlan,
|
...state,
|
||||||
}),
|
taskPlan: latestTaskPlan ?? state.taskPlan,
|
||||||
|
},
|
||||||
|
config,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
...inputMessagesWithCache,
|
...inputMessagesWithCache,
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ Your sole objective in this phase is to gather comprehensive context about the c
|
||||||
<workspace_information>
|
<workspace_information>
|
||||||
<current_working_directory>{CURRENT_WORKING_DIRECTORY}</current_working_directory>
|
<current_working_directory>{CURRENT_WORKING_DIRECTORY}</current_working_directory>
|
||||||
<repository_status>Already cloned and accessible in the current directory</repository_status>
|
<repository_status>Already cloned and accessible in the current directory</repository_status>
|
||||||
|
{LOCAL_MODE_NOTE}
|
||||||
|
|
||||||
<codebase_tree>
|
<codebase_tree>
|
||||||
Generated via: \`git ls-files | tree --fromfile -L 3\`:
|
Generated via: \`git ls-files | tree --fromfile -L 3\`:
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
|
||||||
import { filterMessagesWithoutContent } from "../../../../utils/message/content.js";
|
import { filterMessagesWithoutContent } from "../../../../utils/message/content.js";
|
||||||
import { getModelManager } from "../../../../utils/llms/model-manager.js";
|
import { getModelManager } from "../../../../utils/llms/model-manager.js";
|
||||||
import { trackCachePerformance } from "../../../../utils/caching.js";
|
import { trackCachePerformance } from "../../../../utils/caching.js";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
|
||||||
function formatSystemPrompt(state: PlannerGraphState): string {
|
function formatSystemPrompt(state: PlannerGraphState): string {
|
||||||
// It's a followup if there's more than one human message.
|
// It's a followup if there's more than one human message.
|
||||||
|
|
@ -107,7 +108,7 @@ export async function generatePlan(
|
||||||
}
|
}
|
||||||
|
|
||||||
let newSessionId: string | undefined;
|
let newSessionId: string | undefined;
|
||||||
if (state.sandboxSessionId) {
|
if (state.sandboxSessionId && !isLocalMode(config)) {
|
||||||
// Stop before returning, as the next step will be to interrupt the graph.
|
// Stop before returning, as the next step will be to interrupt the graph.
|
||||||
newSessionId = await stopSandbox(state.sandboxSessionId);
|
newSessionId = await stopSandbox(state.sandboxSessionId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,11 +20,19 @@ import {
|
||||||
} from "../../../utils/github/issue-messages.js";
|
} from "../../../utils/github/issue-messages.js";
|
||||||
import { filterHiddenMessages } from "../../../utils/message/filter-hidden.js";
|
import { filterHiddenMessages } from "../../../utils/message/filter-hidden.js";
|
||||||
import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
|
import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
|
||||||
export async function prepareGraphState(
|
export async function prepareGraphState(
|
||||||
state: PlannerGraphState,
|
state: PlannerGraphState,
|
||||||
config: GraphConfig,
|
config: GraphConfig,
|
||||||
): Promise<Command> {
|
): Promise<Command> {
|
||||||
|
if (isLocalMode(config)) {
|
||||||
|
// In local mode, just proceed to initialize-sandbox with existing messages
|
||||||
|
return new Command({
|
||||||
|
update: {},
|
||||||
|
goto: "initialize-sandbox",
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!state.githubIssueId) {
|
if (!state.githubIssueId) {
|
||||||
throw new Error("No github issue id provided");
|
throw new Error("No github issue id provided");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import {
|
||||||
DO_NOT_RENDER_ID_PREFIX,
|
DO_NOT_RENDER_ID_PREFIX,
|
||||||
PROGRAMMER_GRAPH_ID,
|
PROGRAMMER_GRAPH_ID,
|
||||||
OPEN_SWE_STREAM_MODE,
|
OPEN_SWE_STREAM_MODE,
|
||||||
|
LOCAL_MODE_HEADER,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
|
import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
|
||||||
import { createLangGraphClient } from "../../../utils/langgraph-client.js";
|
import { createLangGraphClient } from "../../../utils/langgraph-client.js";
|
||||||
|
|
@ -43,6 +44,7 @@ import {
|
||||||
postGitHubIssueComment,
|
postGitHubIssueComment,
|
||||||
cleanTaskItems,
|
cleanTaskItems,
|
||||||
} from "../../../utils/github/plan.js";
|
} from "../../../utils/github/plan.js";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "ProposedPlan");
|
const logger = createLogger(LogLevel.INFO, "ProposedPlan");
|
||||||
|
|
||||||
|
|
@ -86,7 +88,9 @@ async function startProgrammerRun(input: {
|
||||||
}) {
|
}) {
|
||||||
const { runInput, state, config, newMessages } = input;
|
const { runInput, state, config, newMessages } = input;
|
||||||
const langGraphClient = createLangGraphClient({
|
const langGraphClient = createLangGraphClient({
|
||||||
defaultHeaders: getDefaultHeaders(config),
|
defaultHeaders: isLocalMode(config)
|
||||||
|
? { [LOCAL_MODE_HEADER]: "true" }
|
||||||
|
: getDefaultHeaders(config),
|
||||||
});
|
});
|
||||||
|
|
||||||
const programmerThreadId = uuidv4();
|
const programmerThreadId = uuidv4();
|
||||||
|
|
@ -121,14 +125,17 @@ async function startProgrammerRun(input: {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
await addTaskPlanToIssue(
|
// Skip GitHub operations in local mode
|
||||||
{
|
if (!isLocalMode(config)) {
|
||||||
githubIssueId: state.githubIssueId,
|
await addTaskPlanToIssue(
|
||||||
targetRepository: state.targetRepository,
|
{
|
||||||
},
|
githubIssueId: state.githubIssueId,
|
||||||
config,
|
targetRepository: state.targetRepository,
|
||||||
runInput.taskPlan,
|
},
|
||||||
);
|
config,
|
||||||
|
runInput.taskPlan,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return new Command({
|
return new Command({
|
||||||
goto: END,
|
goto: END,
|
||||||
|
|
@ -153,6 +160,13 @@ export async function interruptProposedPlan(
|
||||||
throw new Error("No proposed plan found.");
|
throw new Error("No proposed plan found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.info("Interrupting proposed plan", {
|
||||||
|
autoAcceptPlan: state.autoAcceptPlan,
|
||||||
|
isLocalMode: isLocalMode(config),
|
||||||
|
proposedPlanLength: proposedPlan.length,
|
||||||
|
proposedPlanTitle: state.proposedPlanTitle,
|
||||||
|
});
|
||||||
|
|
||||||
let planItems: PlanItem[];
|
let planItems: PlanItem[];
|
||||||
const userRequest = getInitialUserRequest(state.messages);
|
const userRequest = getInitialUserRequest(state.messages);
|
||||||
const userFollowupRequest = getRecentUserRequest(state.messages);
|
const userFollowupRequest = getRecentUserRequest(state.messages);
|
||||||
|
|
@ -167,7 +181,10 @@ export async function interruptProposedPlan(
|
||||||
};
|
};
|
||||||
|
|
||||||
if (state.autoAcceptPlan) {
|
if (state.autoAcceptPlan) {
|
||||||
logger.info("Auto accepting plan.");
|
logger.info("Auto accepting plan.", {
|
||||||
|
autoAcceptPlan: state.autoAcceptPlan,
|
||||||
|
isLocalMode: isLocalMode(config),
|
||||||
|
});
|
||||||
|
|
||||||
// Post comment to GitHub issue about auto-accepting the plan
|
// Post comment to GitHub issue about auto-accepting the plan
|
||||||
await postGitHubIssueComment({
|
await postGitHubIssueComment({
|
||||||
|
|
@ -206,22 +223,24 @@ export async function interruptProposedPlan(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await addProposedPlanToIssue(
|
if (!isLocalMode(config)) {
|
||||||
{
|
await addProposedPlanToIssue(
|
||||||
|
{
|
||||||
|
githubIssueId: state.githubIssueId,
|
||||||
|
targetRepository: state.targetRepository,
|
||||||
|
},
|
||||||
|
config,
|
||||||
|
proposedPlan,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Post comment to GitHub issue about plan being ready for approval
|
||||||
|
await postGitHubIssueComment({
|
||||||
githubIssueId: state.githubIssueId,
|
githubIssueId: state.githubIssueId,
|
||||||
targetRepository: state.targetRepository,
|
targetRepository: state.targetRepository,
|
||||||
},
|
commentBody: `### 🟠 Plan Ready for Approval 🟠\n\nI've generated a plan for this issue and it's ready for your review.\n\n**Plan: ${state.proposedPlanTitle}**\n\n${proposedPlan.map((step, index) => `- Task ${index + 1}:\n${cleanTaskItems(step)}`).join("\n")}\n\nPlease review the plan and let me know if you'd like me to proceed, make changes, or if you have any feedback.`,
|
||||||
config,
|
config,
|
||||||
proposedPlan,
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
// Post comment to GitHub issue about plan being ready for approval
|
|
||||||
await postGitHubIssueComment({
|
|
||||||
githubIssueId: state.githubIssueId,
|
|
||||||
targetRepository: state.targetRepository,
|
|
||||||
commentBody: `### 🟠 Plan Ready for Approval 🟠\n\nI've generated a plan for this issue and it's ready for your review.\n\n**Plan: ${state.proposedPlanTitle}**\n\n${proposedPlan.map((step, index) => `- Task ${index + 1}:\n${cleanTaskItems(step)}`).join("\n")}\n\nPlease review the plan and let me know if you'd like me to proceed, make changes, or if you have any feedback.`,
|
|
||||||
config,
|
|
||||||
});
|
|
||||||
|
|
||||||
const interruptResponse = interrupt<
|
const interruptResponse = interrupt<
|
||||||
HumanInterrupt,
|
HumanInterrupt,
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ import {
|
||||||
isToolMessage,
|
isToolMessage,
|
||||||
ToolMessage,
|
ToolMessage,
|
||||||
} from "@langchain/core/messages";
|
} from "@langchain/core/messages";
|
||||||
|
import {
|
||||||
|
isLocalMode,
|
||||||
|
getLocalWorkingDirectory,
|
||||||
|
} from "@open-swe/shared/open-swe/local-mode";
|
||||||
import {
|
import {
|
||||||
createGetURLContentTool,
|
createGetURLContentTool,
|
||||||
createShellTool,
|
createShellTool,
|
||||||
|
|
@ -49,9 +53,9 @@ export async function takeActions(
|
||||||
throw new Error("Last message is not an AI message with tool calls.");
|
throw new Error("Last message is not an AI message with tool calls.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewTool = createViewTool(state);
|
const viewTool = createViewTool(state, config);
|
||||||
const shellTool = createShellTool(state);
|
const shellTool = createShellTool(state, config);
|
||||||
const searchTool = createGrepTool(state);
|
const searchTool = createGrepTool(state, config);
|
||||||
const scratchpadTool = createScratchpadTool("");
|
const scratchpadTool = createScratchpadTool("");
|
||||||
const getURLContentTool = createGetURLContentTool(state);
|
const getURLContentTool = createGetURLContentTool(state);
|
||||||
const searchDocumentForTool = createSearchDocumentForTool(state, config);
|
const searchDocumentForTool = createSearchDocumentForTool(state, config);
|
||||||
|
|
@ -198,31 +202,35 @@ export async function takeActions(
|
||||||
{ documentCache: {} } as { documentCache: Record<string, string> },
|
{ documentCache: {} } as { documentCache: Record<string, string> },
|
||||||
);
|
);
|
||||||
|
|
||||||
const repoPath = getRepoAbsolutePath(state.targetRepository);
|
if (!isLocalMode(config)) {
|
||||||
const changedFiles = await getChangedFilesStatus(repoPath, sandbox);
|
const repoPath = isLocalMode(config)
|
||||||
if (changedFiles?.length > 0) {
|
? getLocalWorkingDirectory()
|
||||||
logger.warn(
|
: getRepoAbsolutePath(state.targetRepository);
|
||||||
"Changes found in the codebase after taking action. Reverting.",
|
const changedFiles = await getChangedFilesStatus(repoPath, sandbox);
|
||||||
{
|
if (changedFiles?.length > 0) {
|
||||||
changedFiles,
|
logger.warn(
|
||||||
},
|
"Changes found in the codebase after taking action. Reverting.",
|
||||||
);
|
{
|
||||||
await stashAndClearChanges(repoPath, sandbox);
|
changedFiles,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await stashAndClearChanges(repoPath, sandbox);
|
||||||
|
|
||||||
// Rewrite the tool call contents to include a changed files warning.
|
// Rewrite the tool call contents to include a changed files warning.
|
||||||
toolCallResults = toolCallResults.map(
|
toolCallResults = toolCallResults.map(
|
||||||
(tc) =>
|
(tc) =>
|
||||||
new ToolMessage({
|
new ToolMessage({
|
||||||
...tc,
|
...tc,
|
||||||
content: `**WARNING**: THIS TOOL, OR A PREVIOUS TOOL HAS CHANGED FILES IN THE REPO.
|
content: `**WARNING**: THIS TOOL, OR A PREVIOUS TOOL HAS CHANGED FILES IN THE REPO.
|
||||||
Remember that you are only permitted to take **READ** actions during the planning step. The changes have been reverted.
|
Remember that you are only permitted to take **READ** actions during the planning step. The changes have been reverted.
|
||||||
|
|
||||||
Please ensure you only take read actions during the planning step to gather context. You may also call the \`take_notes\` tool at any time to record important information for the programmer step.
|
Please ensure you only take read actions during the planning step to gather context. You may also call the \`take_notes\` tool at any time to record important information for the programmer step.
|
||||||
|
|
||||||
Command Output:\n
|
Command Output:\n
|
||||||
${tc.content}`,
|
${tc.content}`,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Completed planner tool action", {
|
logger.info("Completed planner tool action", {
|
||||||
|
|
|
||||||
|
|
@ -173,12 +173,12 @@ async function createToolsAndPrompt(
|
||||||
}> {
|
}> {
|
||||||
const mcpTools = await getMcpTools(config);
|
const mcpTools = await getMcpTools(config);
|
||||||
const sharedTools = [
|
const sharedTools = [
|
||||||
createGrepTool(state),
|
createGrepTool(state, config),
|
||||||
createShellTool(state),
|
createShellTool(state, config),
|
||||||
createRequestHumanHelpToolFields(),
|
createRequestHumanHelpToolFields(),
|
||||||
createUpdatePlanToolFields(),
|
createUpdatePlanToolFields(),
|
||||||
createGetURLContentTool(state),
|
createGetURLContentTool(state),
|
||||||
createInstallDependenciesTool(state),
|
createInstallDependenciesTool(state, config),
|
||||||
createMarkTaskCompletedToolFields(),
|
createMarkTaskCompletedToolFields(),
|
||||||
createSearchDocumentForTool(state, config),
|
createSearchDocumentForTool(state, config),
|
||||||
...mcpTools,
|
...mcpTools,
|
||||||
|
|
|
||||||
|
|
@ -52,10 +52,10 @@ export async function takeAction(
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyPatchTool = createApplyPatchTool(state);
|
const applyPatchTool = createApplyPatchTool(state);
|
||||||
const shellTool = createShellTool(state);
|
const shellTool = createShellTool(state, config);
|
||||||
const searchTool = createGrepTool(state);
|
const searchTool = createGrepTool(state, config);
|
||||||
const textEditorTool = createTextEditorTool(state);
|
const textEditorTool = createTextEditorTool(state, config);
|
||||||
const installDependenciesTool = createInstallDependenciesTool(state);
|
const installDependenciesTool = createInstallDependenciesTool(state, config);
|
||||||
const getURLContentTool = createGetURLContentTool(state);
|
const getURLContentTool = createGetURLContentTool(state);
|
||||||
const searchDocumentForTool = createSearchDocumentForTool(state, config);
|
const searchDocumentForTool = createSearchDocumentForTool(state, config);
|
||||||
const mcpTools = await getMcpTools(config);
|
const mcpTools = await getMcpTools(config);
|
||||||
|
|
|
||||||
|
|
@ -120,15 +120,18 @@ ${messages.map(getMessageString).join("\n")}
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function createToolsAndPrompt(state: ReviewerGraphState): {
|
function createToolsAndPrompt(
|
||||||
|
state: ReviewerGraphState,
|
||||||
|
config: GraphConfig,
|
||||||
|
): {
|
||||||
providerTools: Record<Provider, BindToolsInput[]>;
|
providerTools: Record<Provider, BindToolsInput[]>;
|
||||||
providerMessages: Record<Provider, BaseMessageLike[]>;
|
providerMessages: Record<Provider, BaseMessageLike[]>;
|
||||||
} {
|
} {
|
||||||
const tools = [
|
const tools = [
|
||||||
createGrepTool(state),
|
createGrepTool(state, config),
|
||||||
createShellTool(state),
|
createShellTool(state, config),
|
||||||
createViewTool(state),
|
createViewTool(state, config),
|
||||||
createInstallDependenciesTool(state),
|
createInstallDependenciesTool(state, config),
|
||||||
createScratchpadTool(
|
createScratchpadTool(
|
||||||
"when generating a final review, after all context gathering and reviewing is complete",
|
"when generating a final review, after all context gathering and reviewing is complete",
|
||||||
),
|
),
|
||||||
|
|
@ -193,7 +196,10 @@ export async function generateReviewActions(
|
||||||
);
|
);
|
||||||
const isAnthropicModel = modelName.includes("claude-");
|
const isAnthropicModel = modelName.includes("claude-");
|
||||||
|
|
||||||
const { providerTools, providerMessages } = createToolsAndPrompt(state);
|
const { providerTools, providerMessages } = createToolsAndPrompt(
|
||||||
|
state,
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
|
||||||
const model = await loadModel(config, Task.REVIEWER, {
|
const model = await loadModel(config, Task.REVIEWER, {
|
||||||
providerTools,
|
providerTools,
|
||||||
|
|
|
||||||
|
|
@ -46,10 +46,10 @@ export async function takeReviewerActions(
|
||||||
throw new Error("Last message is not an AI message with tool calls.");
|
throw new Error("Last message is not an AI message with tool calls.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const shellTool = createShellTool(state);
|
const shellTool = createShellTool(state, config);
|
||||||
const searchTool = createGrepTool(state);
|
const searchTool = createGrepTool(state, config);
|
||||||
const viewTool = createViewTool(state);
|
const viewTool = createViewTool(state, config);
|
||||||
const installDependenciesTool = createInstallDependenciesTool(state);
|
const installDependenciesTool = createInstallDependenciesTool(state, config);
|
||||||
const scratchpadTool = createScratchpadTool("");
|
const scratchpadTool = createScratchpadTool("");
|
||||||
const allTools = [
|
const allTools = [
|
||||||
shellTool,
|
shellTool,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import * as crypto from "crypto";
|
||||||
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
||||||
import { getGitHubTokensFromConfig } from "../../utils/github-tokens.js";
|
import { getGitHubTokensFromConfig } from "../../utils/github-tokens.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -23,6 +24,10 @@ import { AIMessage, BaseMessage } from "@langchain/core/messages";
|
||||||
import { DEFAULT_SANDBOX_CREATE_PARAMS } from "../../constants.js";
|
import { DEFAULT_SANDBOX_CREATE_PARAMS } from "../../constants.js";
|
||||||
import { getCustomRules } from "../../utils/custom-rules.js";
|
import { getCustomRules } from "../../utils/custom-rules.js";
|
||||||
import { withRetry } from "../../utils/retry.js";
|
import { withRetry } from "../../utils/retry.js";
|
||||||
|
import {
|
||||||
|
isLocalMode,
|
||||||
|
getLocalWorkingDirectory,
|
||||||
|
} from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "InitializeSandbox");
|
const logger = createLogger(LogLevel.INFO, "InitializeSandbox");
|
||||||
|
|
||||||
|
|
@ -40,7 +45,6 @@ export async function initializeSandbox(
|
||||||
state: InitializeSandboxState,
|
state: InitializeSandboxState,
|
||||||
config: GraphConfig,
|
config: GraphConfig,
|
||||||
): Promise<Partial<InitializeSandboxState>> {
|
): Promise<Partial<InitializeSandboxState>> {
|
||||||
const { githubInstallationToken } = getGitHubTokensFromConfig(config);
|
|
||||||
const { sandboxSessionId, targetRepository, branchName } = state;
|
const { sandboxSessionId, targetRepository, branchName } = state;
|
||||||
const absoluteRepoDir = getRepoAbsolutePath(targetRepository);
|
const absoluteRepoDir = getRepoAbsolutePath(targetRepository);
|
||||||
const repoName = `${targetRepository.owner}/${targetRepository.repo}`;
|
const repoName = `${targetRepository.owner}/${targetRepository.repo}`;
|
||||||
|
|
@ -79,6 +83,18 @@ export async function initializeSandbox(
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Check if we're in local mode before trying to get GitHub tokens
|
||||||
|
if (isLocalMode(config)) {
|
||||||
|
return initializeSandboxLocal(
|
||||||
|
state,
|
||||||
|
config,
|
||||||
|
emitStepEvent,
|
||||||
|
createEventsMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { githubInstallationToken } = getGitHubTokensFromConfig(config);
|
||||||
|
|
||||||
if (!sandboxSessionId) {
|
if (!sandboxSessionId) {
|
||||||
emitStepEvent(
|
emitStepEvent(
|
||||||
{
|
{
|
||||||
|
|
@ -322,7 +338,7 @@ export async function initializeSandbox(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
emitStepEvent(baseGenerateCodebaseTreeAction, "pending");
|
emitStepEvent(baseGenerateCodebaseTreeAction, "pending");
|
||||||
let codebaseTree = undefined;
|
let codebaseTree: string | undefined;
|
||||||
try {
|
try {
|
||||||
codebaseTree = await getCodebaseTree(sandbox.id);
|
codebaseTree = await getCodebaseTree(sandbox.id);
|
||||||
emitStepEvent(baseGenerateCodebaseTreeAction, "success");
|
emitStepEvent(baseGenerateCodebaseTreeAction, "success");
|
||||||
|
|
@ -344,3 +360,114 @@ export async function initializeSandbox(
|
||||||
branchName: newBranchName,
|
branchName: newBranchName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local mode version of initializeSandbox
|
||||||
|
* Skips sandbox creation and repository cloning, works directly with local filesystem
|
||||||
|
*/
|
||||||
|
async function initializeSandboxLocal(
|
||||||
|
state: InitializeSandboxState,
|
||||||
|
config: GraphConfig,
|
||||||
|
emitStepEvent: (
|
||||||
|
base: CustomNodeEvent,
|
||||||
|
status: "pending" | "success" | "error" | "skipped",
|
||||||
|
error?: string,
|
||||||
|
) => void,
|
||||||
|
createEventsMessage: () => BaseMessage[],
|
||||||
|
): Promise<Partial<InitializeSandboxState>> {
|
||||||
|
const { targetRepository, branchName } = state;
|
||||||
|
const absoluteRepoDir = getLocalWorkingDirectory(); // Use local working directory in local mode
|
||||||
|
const repoName = `${targetRepository.owner}/${targetRepository.repo}`;
|
||||||
|
|
||||||
|
// Skip sandbox creation in local mode
|
||||||
|
emitStepEvent(
|
||||||
|
{
|
||||||
|
nodeId: INITIALIZE_NODE_ID,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
actionId: uuidv4(),
|
||||||
|
action: "Creating sandbox",
|
||||||
|
data: {
|
||||||
|
status: "skipped",
|
||||||
|
sandboxSessionId: null,
|
||||||
|
branch: branchName,
|
||||||
|
repo: repoName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"skipped",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skip repository cloning in local mode
|
||||||
|
emitStepEvent(
|
||||||
|
{
|
||||||
|
nodeId: INITIALIZE_NODE_ID,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
actionId: uuidv4(),
|
||||||
|
action: "Cloning repository",
|
||||||
|
data: {
|
||||||
|
status: "skipped",
|
||||||
|
sandboxSessionId: null,
|
||||||
|
branch: branchName,
|
||||||
|
repo: repoName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"skipped",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Skip branch checkout in local mode
|
||||||
|
emitStepEvent(
|
||||||
|
{
|
||||||
|
nodeId: INITIALIZE_NODE_ID,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
actionId: uuidv4(),
|
||||||
|
action: "Checking out branch",
|
||||||
|
data: {
|
||||||
|
status: "skipped",
|
||||||
|
sandboxSessionId: null,
|
||||||
|
branch: branchName,
|
||||||
|
repo: repoName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"skipped",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Generate codebase tree locally
|
||||||
|
const generateCodebaseTreeActionId = uuidv4();
|
||||||
|
const baseGenerateCodebaseTreeAction: CustomNodeEvent = {
|
||||||
|
nodeId: INITIALIZE_NODE_ID,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
actionId: generateCodebaseTreeActionId,
|
||||||
|
action: "Generating codebase tree",
|
||||||
|
data: {
|
||||||
|
status: "pending",
|
||||||
|
sandboxSessionId: null,
|
||||||
|
branch: branchName,
|
||||||
|
repo: repoName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
emitStepEvent(baseGenerateCodebaseTreeAction, "pending");
|
||||||
|
|
||||||
|
let codebaseTree = undefined;
|
||||||
|
try {
|
||||||
|
codebaseTree = await getCodebaseTree(undefined, targetRepository, config);
|
||||||
|
emitStepEvent(baseGenerateCodebaseTreeAction, "success");
|
||||||
|
} catch (_) {
|
||||||
|
emitStepEvent(
|
||||||
|
baseGenerateCodebaseTreeAction,
|
||||||
|
"error",
|
||||||
|
"Failed to generate codebase tree.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a mock sandbox ID for consistency
|
||||||
|
const mockSandboxId = `local-${Date.now()}-${crypto.randomBytes(16).toString("hex")}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sandboxSessionId: mockSandboxId,
|
||||||
|
targetRepository,
|
||||||
|
codebaseTree,
|
||||||
|
messages: [...(state.messages || []), ...createEventsMessage()],
|
||||||
|
dependenciesInstalled: false,
|
||||||
|
customRules: await getCustomRules(null as any, absoluteRepoDir, config),
|
||||||
|
branchName: branchName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
GITHUB_TOKEN_COOKIE,
|
GITHUB_TOKEN_COOKIE,
|
||||||
GITHUB_USER_ID_HEADER,
|
GITHUB_USER_ID_HEADER,
|
||||||
GITHUB_USER_LOGIN_HEADER,
|
GITHUB_USER_LOGIN_HEADER,
|
||||||
|
LOCAL_MODE_HEADER,
|
||||||
} from "@open-swe/shared/constants";
|
} from "@open-swe/shared/constants";
|
||||||
import { decryptSecret } from "@open-swe/shared/crypto";
|
import { decryptSecret } from "@open-swe/shared/crypto";
|
||||||
import { verifyGitHubWebhookOrThrow } from "./github.js";
|
import { verifyGitHubWebhookOrThrow } from "./github.js";
|
||||||
|
|
@ -48,6 +49,21 @@ export const auth = new Auth()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for local mode first
|
||||||
|
const localModeHeader = request.headers.get(LOCAL_MODE_HEADER);
|
||||||
|
const isRunningLocalModeEnv = process.env.OPEN_SWE_LOCAL_MODE === "true";
|
||||||
|
if (localModeHeader === "true" && isRunningLocalModeEnv) {
|
||||||
|
return {
|
||||||
|
identity: "local-user",
|
||||||
|
is_authenticated: true,
|
||||||
|
display_name: "Local User",
|
||||||
|
metadata: {
|
||||||
|
installation_name: "local-mode",
|
||||||
|
},
|
||||||
|
permissions: LANGGRAPH_USER_PERMISSIONS,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const ghSecretHashHeader = request.headers.get("X-Hub-Signature-256");
|
const ghSecretHashHeader = request.headers.get("X-Hub-Signature-256");
|
||||||
if (ghSecretHashHeader) {
|
if (ghSecretHashHeader) {
|
||||||
// This will either return a valid user, or throw an error
|
// This will either return a valid user, or throw an error
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,54 @@
|
||||||
import { Sandbox } from "@daytonaio/sdk";
|
import { Sandbox } from "@daytonaio/sdk";
|
||||||
import { readFile, writeFile } from "../../utils/read-write.js";
|
import { readFile, writeFile } from "../../utils/read-write.js";
|
||||||
import { getSandboxErrorFields } from "../../utils/sandbox-error-fields.js";
|
import { getSandboxErrorFields } from "../../utils/sandbox-error-fields.js";
|
||||||
|
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
|
import {
|
||||||
|
createShellExecutor,
|
||||||
|
LocalExecuteResponse,
|
||||||
|
} from "../../utils/shell-executor/index.js";
|
||||||
|
|
||||||
export async function handleViewCommand(
|
export async function handleViewCommand(
|
||||||
sandbox: Sandbox,
|
sandbox: Sandbox,
|
||||||
path: string,
|
path: string,
|
||||||
workDir: string,
|
workDir: string,
|
||||||
viewRange?: [number, number],
|
viewRange?: [number, number],
|
||||||
|
config?: GraphConfig,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
// Check if path is a directory
|
// Check if path is a directory
|
||||||
const statOutput = await sandbox.process.executeCommand(
|
let statOutput: LocalExecuteResponse;
|
||||||
`stat -c %F "${path}"`,
|
if (!config) {
|
||||||
workDir,
|
// Fallback to direct sandbox execution if no config provided
|
||||||
);
|
statOutput = await sandbox.process.executeCommand(
|
||||||
|
`stat -c %F "${path}"`,
|
||||||
|
workDir,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const executor = createShellExecutor(config);
|
||||||
|
statOutput = await executor.executeCommand({
|
||||||
|
command: `stat -c %F "${path}"`,
|
||||||
|
workdir: workDir,
|
||||||
|
sandbox,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (statOutput.exitCode === 0 && statOutput.result?.includes("directory")) {
|
if (statOutput.exitCode === 0 && statOutput.result?.includes("directory")) {
|
||||||
// List directory contents
|
// List directory contents
|
||||||
const lsOutput = await sandbox.process.executeCommand(
|
let lsOutput: LocalExecuteResponse;
|
||||||
`ls -la "${path}"`,
|
if (!config) {
|
||||||
workDir,
|
// Fallback to direct sandbox execution if no config provided
|
||||||
);
|
lsOutput = await sandbox.process.executeCommand(
|
||||||
|
`ls -la "${path}"`,
|
||||||
|
workDir,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const executor = createShellExecutor(config);
|
||||||
|
lsOutput = await executor.executeCommand({
|
||||||
|
command: `ls -la "${path}"`,
|
||||||
|
workdir: workDir,
|
||||||
|
sandbox,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (lsOutput.exitCode !== 0) {
|
if (lsOutput.exitCode !== 0) {
|
||||||
throw new Error(`Failed to list directory: ${lsOutput.result}`);
|
throw new Error(`Failed to list directory: ${lsOutput.result}`);
|
||||||
|
|
@ -34,6 +62,7 @@ export async function handleViewCommand(
|
||||||
sandbox,
|
sandbox,
|
||||||
filePath: path,
|
filePath: path,
|
||||||
workDir,
|
workDir,
|
||||||
|
config,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
import { join } from "path";
|
||||||
import { tool } from "@langchain/core/tools";
|
import { tool } from "@langchain/core/tools";
|
||||||
import { GraphState } from "@open-swe/shared/open-swe/types";
|
import { GraphState, GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
import { createLogger, LogLevel } from "../../utils/logger.js";
|
import { createLogger, LogLevel } from "../../utils/logger.js";
|
||||||
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
||||||
import { getSandboxSessionOrThrow } from "../utils/get-sandbox-id.js";
|
import { getSandboxSessionOrThrow } from "../utils/get-sandbox-id.js";
|
||||||
|
|
@ -10,18 +11,22 @@ import {
|
||||||
handleCreateCommand,
|
handleCreateCommand,
|
||||||
handleInsertCommand,
|
handleInsertCommand,
|
||||||
} from "./handlers.js";
|
} from "./handlers.js";
|
||||||
|
import {
|
||||||
|
isLocalMode,
|
||||||
|
getLocalWorkingDirectory,
|
||||||
|
} from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
import { TIMEOUT_SEC } from "@open-swe/shared/constants";
|
||||||
|
import { getLocalShellExecutor } from "../../utils/shell-executor/index.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "TextEditorTool");
|
const logger = createLogger(LogLevel.INFO, "TextEditorTool");
|
||||||
|
|
||||||
export function createTextEditorTool(
|
export function createTextEditorTool(
|
||||||
state: Pick<GraphState, "sandboxSessionId" | "targetRepository">,
|
state: Pick<GraphState, "sandboxSessionId" | "targetRepository">,
|
||||||
|
config: GraphConfig,
|
||||||
) {
|
) {
|
||||||
const textEditorTool = tool(
|
const textEditorTool = tool(
|
||||||
async (input): Promise<{ result: string; status: "success" | "error" }> => {
|
async (input): Promise<{ result: string; status: "success" | "error" }> => {
|
||||||
try {
|
try {
|
||||||
const sandbox = await getSandboxSessionOrThrow(input);
|
|
||||||
const workDir = getRepoAbsolutePath(state.targetRepository);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
command,
|
command,
|
||||||
path,
|
path,
|
||||||
|
|
@ -32,58 +37,184 @@ export function createTextEditorTool(
|
||||||
insert_line,
|
insert_line,
|
||||||
} = input;
|
} = input;
|
||||||
|
|
||||||
|
const localMode = isLocalMode(config);
|
||||||
|
const localAbsolutePath = getLocalWorkingDirectory();
|
||||||
|
const sandboxAbsolutePath = getRepoAbsolutePath(state.targetRepository);
|
||||||
|
const workDir = localMode ? localAbsolutePath : sandboxAbsolutePath;
|
||||||
let result: string;
|
let result: string;
|
||||||
|
|
||||||
switch (command) {
|
if (localMode) {
|
||||||
case "view":
|
// Local mode: use LocalShellExecutor for file operations
|
||||||
result = await handleViewCommand(
|
const executor = getLocalShellExecutor(localAbsolutePath);
|
||||||
sandbox,
|
|
||||||
path,
|
// Convert sandbox path to local path
|
||||||
workDir,
|
let localPath = path;
|
||||||
view_range,
|
if (path.startsWith("/home/daytona/project/")) {
|
||||||
);
|
// Remove the sandbox prefix to get the relative path
|
||||||
break;
|
localPath = path.replace("/home/daytona/project/", "");
|
||||||
case "str_replace":
|
}
|
||||||
if (!old_str || new_str === undefined) {
|
const filePath = join(workDir, localPath);
|
||||||
throw new Error(
|
|
||||||
"str_replace command requires both old_str and new_str parameters",
|
switch (command) {
|
||||||
|
case "view": {
|
||||||
|
// Use cat command to view file content
|
||||||
|
const viewResponse = await executor.executeCommand(
|
||||||
|
`cat "${filePath}"`,
|
||||||
|
{
|
||||||
|
workdir: workDir,
|
||||||
|
timeout: TIMEOUT_SEC,
|
||||||
|
localMode: true,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
if (viewResponse.exitCode !== 0) {
|
||||||
|
throw new Error(`Failed to read file: ${viewResponse.result}`);
|
||||||
|
}
|
||||||
|
result = viewResponse.result;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
result = await handleStrReplaceCommand(
|
case "str_replace": {
|
||||||
sandbox,
|
if (!old_str || new_str === undefined) {
|
||||||
path,
|
throw new Error(
|
||||||
workDir,
|
"str_replace command requires both old_str and new_str parameters",
|
||||||
old_str,
|
);
|
||||||
new_str,
|
}
|
||||||
);
|
// Use sed command for string replacement with proper escaping
|
||||||
break;
|
const escapedOldStr = old_str
|
||||||
case "create":
|
.replace(/\\/g, "\\\\")
|
||||||
if (!file_text) {
|
.replace(/\//g, "\\/")
|
||||||
throw new Error("create command requires file_text parameter");
|
.replace(/'/g, "'\"'\"'");
|
||||||
}
|
const escapedNewStr = new_str
|
||||||
result = await handleCreateCommand(
|
.replace(/\\/g, "\\\\")
|
||||||
sandbox,
|
.replace(/\//g, "\\/")
|
||||||
path,
|
.replace(/'/g, "'\"'\"'");
|
||||||
workDir,
|
|
||||||
file_text,
|
const sedResponse = await executor.executeCommand(
|
||||||
);
|
`sed -i 's/${escapedOldStr}/${escapedNewStr}/g' "${filePath}"`,
|
||||||
break;
|
{
|
||||||
case "insert":
|
workdir: workDir,
|
||||||
if (insert_line === undefined || new_str === undefined) {
|
timeout: TIMEOUT_SEC,
|
||||||
throw new Error(
|
localMode: true,
|
||||||
"insert command requires both insert_line and new_str parameters",
|
},
|
||||||
);
|
);
|
||||||
|
if (sedResponse.exitCode !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to replace string: ${sedResponse.result}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result = `Successfully replaced '${old_str}' with '${new_str}' in ${path}`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
result = await handleInsertCommand(
|
case "create": {
|
||||||
sandbox,
|
if (!file_text) {
|
||||||
path,
|
throw new Error("create command requires file_text parameter");
|
||||||
workDir,
|
}
|
||||||
insert_line,
|
// Create file with content using proper escaping
|
||||||
new_str,
|
const escapedFileText = file_text
|
||||||
);
|
.replace(/\\/g, "\\\\")
|
||||||
break;
|
.replace(/'/g, "'\"'\"'");
|
||||||
default:
|
|
||||||
throw new Error(`Unknown command: ${command}`);
|
const createResponse = await executor.executeCommand(
|
||||||
|
`echo '${escapedFileText}' > "${filePath}"`,
|
||||||
|
{
|
||||||
|
workdir: workDir,
|
||||||
|
timeout: TIMEOUT_SEC,
|
||||||
|
localMode: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (createResponse.exitCode !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to create file: ${createResponse.result}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result = `Successfully created file ${path}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "insert": {
|
||||||
|
if (insert_line === undefined || new_str === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
"insert command requires both insert_line and new_str parameters",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Insert line at specific position with proper escaping
|
||||||
|
const escapedNewStr = new_str
|
||||||
|
.replace(/\\/g, "\\\\")
|
||||||
|
.replace(/\//g, "\\/")
|
||||||
|
.replace(/'/g, "'\"'\"'");
|
||||||
|
|
||||||
|
const insertResponse = await executor.executeCommand(
|
||||||
|
`sed -i '${insert_line}i\\${escapedNewStr}' "${filePath}"`,
|
||||||
|
{
|
||||||
|
workdir: workDir,
|
||||||
|
timeout: TIMEOUT_SEC,
|
||||||
|
localMode: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (insertResponse.exitCode !== 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to insert line: ${insertResponse.result}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result = `Successfully inserted line at position ${insert_line} in ${path}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown command: ${command}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Sandbox mode: use existing handler
|
||||||
|
const sandbox = await getSandboxSessionOrThrow(input);
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "view":
|
||||||
|
result = await handleViewCommand(
|
||||||
|
sandbox,
|
||||||
|
path,
|
||||||
|
workDir,
|
||||||
|
view_range,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "str_replace":
|
||||||
|
if (!old_str || new_str === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
"str_replace command requires both old_str and new_str parameters",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result = await handleStrReplaceCommand(
|
||||||
|
sandbox,
|
||||||
|
path,
|
||||||
|
workDir,
|
||||||
|
old_str,
|
||||||
|
new_str,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "create":
|
||||||
|
if (!file_text) {
|
||||||
|
throw new Error("create command requires file_text parameter");
|
||||||
|
}
|
||||||
|
result = await handleCreateCommand(
|
||||||
|
sandbox,
|
||||||
|
path,
|
||||||
|
workDir,
|
||||||
|
file_text,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "insert":
|
||||||
|
if (insert_line === undefined || new_str === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
"insert command requires both insert_line and new_str parameters",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
result = await handleInsertCommand(
|
||||||
|
sandbox,
|
||||||
|
path,
|
||||||
|
workDir,
|
||||||
|
insert_line,
|
||||||
|
new_str,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown command: ${command}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,72 @@
|
||||||
|
import { join } from "path";
|
||||||
import { tool } from "@langchain/core/tools";
|
import { tool } from "@langchain/core/tools";
|
||||||
import { GraphState } from "@open-swe/shared/open-swe/types";
|
import { GraphState, GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
import { createLogger, LogLevel } from "../../utils/logger.js";
|
import { createLogger, LogLevel } from "../../utils/logger.js";
|
||||||
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
||||||
import { getSandboxSessionOrThrow } from "../utils/get-sandbox-id.js";
|
import { getSandboxSessionOrThrow } from "../utils/get-sandbox-id.js";
|
||||||
import { createViewToolFields } from "@open-swe/shared/open-swe/tools";
|
import { createViewToolFields } from "@open-swe/shared/open-swe/tools";
|
||||||
import { handleViewCommand } from "./handlers.js";
|
import { handleViewCommand } from "./handlers.js";
|
||||||
|
import {
|
||||||
|
isLocalMode,
|
||||||
|
getLocalWorkingDirectory,
|
||||||
|
} from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
import { TIMEOUT_SEC } from "@open-swe/shared/constants";
|
||||||
|
import { getLocalShellExecutor } from "../../utils/shell-executor/index.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "ViewTool");
|
const logger = createLogger(LogLevel.INFO, "ViewTool");
|
||||||
|
|
||||||
export function createViewTool(
|
export function createViewTool(
|
||||||
state: Pick<GraphState, "sandboxSessionId" | "targetRepository">,
|
state: Pick<GraphState, "sandboxSessionId" | "targetRepository">,
|
||||||
|
config: GraphConfig,
|
||||||
) {
|
) {
|
||||||
const viewTool = tool(
|
const viewTool = tool(
|
||||||
async (input): Promise<{ result: string; status: "success" | "error" }> => {
|
async (input): Promise<{ result: string; status: "success" | "error" }> => {
|
||||||
try {
|
try {
|
||||||
const sandbox = await getSandboxSessionOrThrow(input);
|
const { command, path, view_range } = input as any;
|
||||||
const workDir = getRepoAbsolutePath(state.targetRepository);
|
|
||||||
|
|
||||||
const { command, path, view_range } = input;
|
|
||||||
if (command !== "view") {
|
if (command !== "view") {
|
||||||
throw new Error(`Unknown command: ${command}`);
|
throw new Error(`Unknown command: ${command}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await handleViewCommand(
|
const localMode = isLocalMode(config);
|
||||||
sandbox,
|
const localAbsolutePath = getLocalWorkingDirectory();
|
||||||
path,
|
const sandboxAbsolutePath = getRepoAbsolutePath(state.targetRepository);
|
||||||
workDir,
|
const workDir = localMode ? localAbsolutePath : sandboxAbsolutePath;
|
||||||
view_range as [number, number] | undefined,
|
let result: string;
|
||||||
);
|
|
||||||
|
if (localMode) {
|
||||||
|
// Local mode: use LocalShellExecutor for file viewing
|
||||||
|
const executor = getLocalShellExecutor(localAbsolutePath);
|
||||||
|
|
||||||
|
// Convert sandbox path to local path
|
||||||
|
let localPath = path;
|
||||||
|
if (path.startsWith("/home/daytona/project/")) {
|
||||||
|
// Remove the sandbox prefix to get the relative path
|
||||||
|
localPath = path.replace("/home/daytona/project/", "");
|
||||||
|
}
|
||||||
|
const filePath = join(workDir, localPath);
|
||||||
|
|
||||||
|
// Use cat command to view file content
|
||||||
|
const response = await executor.executeCommand(`cat "${filePath}"`, {
|
||||||
|
workdir: workDir,
|
||||||
|
timeout: TIMEOUT_SEC,
|
||||||
|
localMode: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.exitCode !== 0) {
|
||||||
|
throw new Error(`Failed to read file: ${response.result}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = response.result;
|
||||||
|
} else {
|
||||||
|
// Sandbox mode: use existing handler
|
||||||
|
const sandbox = await getSandboxSessionOrThrow(input);
|
||||||
|
result = await handleViewCommand(
|
||||||
|
sandbox,
|
||||||
|
path,
|
||||||
|
workDir,
|
||||||
|
view_range as [number, number] | undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(`View command executed successfully on ${path}`);
|
logger.info(`View command executed successfully on ${path}`);
|
||||||
return { result, status: "success" };
|
return { result, status: "success" };
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,54 @@
|
||||||
import { tool } from "@langchain/core/tools";
|
import { tool } from "@langchain/core/tools";
|
||||||
import { GraphState } from "@open-swe/shared/open-swe/types";
|
import { GraphState, GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
import { getSandboxErrorFields } from "../utils/sandbox-error-fields.js";
|
import { getSandboxErrorFields } from "../utils/sandbox-error-fields.js";
|
||||||
import { createLogger, LogLevel } from "../utils/logger.js";
|
import { createLogger, LogLevel } from "../utils/logger.js";
|
||||||
import { TIMEOUT_SEC } from "@open-swe/shared/constants";
|
import { TIMEOUT_SEC } from "@open-swe/shared/constants";
|
||||||
|
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
||||||
|
import { getSandboxSessionOrThrow } from "./utils/get-sandbox-id.js";
|
||||||
|
import {
|
||||||
|
isLocalMode,
|
||||||
|
getLocalWorkingDirectory,
|
||||||
|
} from "@open-swe/shared/open-swe/local-mode";
|
||||||
import {
|
import {
|
||||||
createGrepToolFields,
|
createGrepToolFields,
|
||||||
formatGrepCommand,
|
formatGrepCommand,
|
||||||
} from "@open-swe/shared/open-swe/tools";
|
} from "@open-swe/shared/open-swe/tools";
|
||||||
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
import { Sandbox } from "@daytonaio/sdk";
|
||||||
import { wrapScript } from "../utils/wrap-script.js";
|
import { createShellExecutor } from "../utils/shell-executor/index.js";
|
||||||
import { getSandboxSessionOrThrow } from "./utils/get-sandbox-id.js";
|
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GrepTool");
|
const logger = createLogger(LogLevel.INFO, "GrepTool");
|
||||||
|
|
||||||
const DEFAULT_ENV = {
|
|
||||||
// Prevents corepack from showing a y/n download prompt which causes the command to hang
|
|
||||||
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createGrepTool(
|
export function createGrepTool(
|
||||||
state: Pick<GraphState, "sandboxSessionId" | "targetRepository">,
|
state: Pick<GraphState, "sandboxSessionId" | "targetRepository">,
|
||||||
|
config: GraphConfig,
|
||||||
) {
|
) {
|
||||||
const grepTool = tool(
|
const grepTool = tool(
|
||||||
async (input): Promise<{ result: string; status: "success" | "error" }> => {
|
async (input): Promise<{ result: string; status: "success" | "error" }> => {
|
||||||
try {
|
try {
|
||||||
const sandbox = await getSandboxSessionOrThrow(input);
|
const command = formatGrepCommand(input as any);
|
||||||
|
const localMode = isLocalMode(config);
|
||||||
|
const localAbsolutePath = getLocalWorkingDirectory();
|
||||||
|
const sandboxAbsolutePath = getRepoAbsolutePath(state.targetRepository);
|
||||||
|
const workDir = localMode ? localAbsolutePath : sandboxAbsolutePath;
|
||||||
|
|
||||||
const repoRoot = getRepoAbsolutePath(state.targetRepository);
|
|
||||||
const command = formatGrepCommand(input);
|
|
||||||
logger.info("Running grep search command", {
|
logger.info("Running grep search command", {
|
||||||
command: command.join(" "),
|
command: command.join(" "),
|
||||||
repoRoot,
|
workDir,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get sandbox if needed for sandbox mode
|
||||||
|
let sandbox: Sandbox | undefined;
|
||||||
|
if (!isLocalMode(config)) {
|
||||||
|
sandbox = await getSandboxSessionOrThrow(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
const executor = createShellExecutor(config);
|
||||||
|
const response = await executor.executeCommand({
|
||||||
|
command,
|
||||||
|
workdir: workDir,
|
||||||
|
timeout: TIMEOUT_SEC,
|
||||||
|
sandbox,
|
||||||
});
|
});
|
||||||
const response = await sandbox.process.executeCommand(
|
|
||||||
wrapScript(command.join(" ")),
|
|
||||||
repoRoot,
|
|
||||||
DEFAULT_ENV,
|
|
||||||
TIMEOUT_SEC,
|
|
||||||
);
|
|
||||||
|
|
||||||
let successResult = response.result;
|
let successResult = response.result;
|
||||||
|
|
||||||
|
|
@ -58,17 +69,19 @@ export function createGrepTool(
|
||||||
result: successResult,
|
result: successResult,
|
||||||
status: "success",
|
status: "success",
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (error: any) {
|
||||||
const errorFields = getSandboxErrorFields(e);
|
const errorFields = getSandboxErrorFields(error);
|
||||||
if (errorFields) {
|
if (errorFields) {
|
||||||
const errorResult =
|
return {
|
||||||
errorFields.result ?? errorFields.artifacts?.stdout;
|
result: `Error: ${errorFields.result ?? errorFields.artifacts?.stdout}`,
|
||||||
throw new Error(
|
status: "error",
|
||||||
`Failed to run grep search command. Exit code: ${errorFields.exitCode}\nError: ${errorResult}`,
|
};
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e;
|
return {
|
||||||
|
result: `Error: ${error.message || String(error)}`,
|
||||||
|
status: "error",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createGrepToolFields(state.targetRepository),
|
createGrepToolFields(state.targetRepository),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
import { tool } from "@langchain/core/tools";
|
import { tool } from "@langchain/core/tools";
|
||||||
import { GraphState } from "@open-swe/shared/open-swe/types";
|
import { GraphState, GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
import { getSandboxErrorFields } from "../utils/sandbox-error-fields.js";
|
import { getSandboxErrorFields } from "../utils/sandbox-error-fields.js";
|
||||||
import { createLogger, LogLevel } from "../utils/logger.js";
|
import { createLogger, LogLevel } from "../utils/logger.js";
|
||||||
import { TIMEOUT_SEC } from "@open-swe/shared/constants";
|
import { TIMEOUT_SEC } from "@open-swe/shared/constants";
|
||||||
import { createInstallDependenciesToolFields } from "@open-swe/shared/open-swe/tools";
|
import { createInstallDependenciesToolFields } from "@open-swe/shared/open-swe/tools";
|
||||||
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
import { getRepoAbsolutePath } from "@open-swe/shared/git";
|
||||||
import { getSandboxSessionOrThrow } from "./utils/get-sandbox-id.js";
|
import { getSandboxSessionOrThrow } from "./utils/get-sandbox-id.js";
|
||||||
|
import {
|
||||||
|
isLocalMode,
|
||||||
|
getLocalWorkingDirectory,
|
||||||
|
} from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
import { LocalExecuteResponse } from "../utils/shell-executor/types.js";
|
||||||
|
import { getLocalShellExecutor } from "../utils/shell-executor/index.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "InstallDependenciesTool");
|
const logger = createLogger(LogLevel.INFO, "InstallDependenciesTool");
|
||||||
|
|
||||||
|
|
@ -16,12 +22,11 @@ const DEFAULT_ENV = {
|
||||||
|
|
||||||
export function createInstallDependenciesTool(
|
export function createInstallDependenciesTool(
|
||||||
state: Pick<GraphState, "sandboxSessionId" | "targetRepository">,
|
state: Pick<GraphState, "sandboxSessionId" | "targetRepository">,
|
||||||
|
config: GraphConfig,
|
||||||
) {
|
) {
|
||||||
const installDependenciesTool = tool(
|
const installDependenciesTool = tool(
|
||||||
async (input): Promise<{ result: string; status: "success" | "error" }> => {
|
async (input): Promise<{ result: string; status: "success" | "error" }> => {
|
||||||
try {
|
try {
|
||||||
const sandbox = await getSandboxSessionOrThrow(input);
|
|
||||||
|
|
||||||
const repoRoot = getRepoAbsolutePath(state.targetRepository);
|
const repoRoot = getRepoAbsolutePath(state.targetRepository);
|
||||||
const command = input.command.join(" ");
|
const command = input.command.join(" ");
|
||||||
const workdir = input.workdir || repoRoot;
|
const workdir = input.workdir || repoRoot;
|
||||||
|
|
@ -29,12 +34,28 @@ export function createInstallDependenciesTool(
|
||||||
command,
|
command,
|
||||||
workdir,
|
workdir,
|
||||||
});
|
});
|
||||||
const response = await sandbox.process.executeCommand(
|
|
||||||
command,
|
let response: LocalExecuteResponse;
|
||||||
workdir,
|
|
||||||
DEFAULT_ENV,
|
if (isLocalMode(config)) {
|
||||||
TIMEOUT_SEC * 2.5, // add a 2.5 min timeout
|
// Local mode: use LocalShellExecutor
|
||||||
);
|
const executor = getLocalShellExecutor(getLocalWorkingDirectory());
|
||||||
|
response = await executor.executeCommand(command, {
|
||||||
|
workdir: workdir,
|
||||||
|
env: DEFAULT_ENV,
|
||||||
|
timeout: TIMEOUT_SEC * 2.5, // add a 2.5 min timeout
|
||||||
|
localMode: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Sandbox mode: use existing sandbox logic
|
||||||
|
const sandbox = await getSandboxSessionOrThrow(input);
|
||||||
|
response = await sandbox.process.executeCommand(
|
||||||
|
command,
|
||||||
|
workdir,
|
||||||
|
DEFAULT_ENV,
|
||||||
|
TIMEOUT_SEC * 2.5, // add a 2.5 min timeout
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (response.exitCode !== 0) {
|
if (response.exitCode !== 0) {
|
||||||
const errorResult = response.result ?? response.artifacts?.stdout;
|
const errorResult = response.result ?? response.artifacts?.stdout;
|
||||||
|
|
@ -48,16 +69,22 @@ export function createInstallDependenciesTool(
|
||||||
status: "success",
|
status: "success",
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const errorFields = getSandboxErrorFields(e);
|
if (isLocalMode(config)) {
|
||||||
if (errorFields) {
|
// Local mode error handling
|
||||||
const errorResult =
|
throw e;
|
||||||
errorFields.result ?? errorFields.artifacts?.stdout;
|
} else {
|
||||||
throw new Error(
|
// Sandbox mode error handling
|
||||||
`Failed to install dependencies. Exit code: ${errorFields.exitCode}\nError: ${errorResult}`,
|
const errorFields = getSandboxErrorFields(e);
|
||||||
);
|
if (errorFields) {
|
||||||
}
|
const errorResult =
|
||||||
|
errorFields.result ?? errorFields.artifacts?.stdout;
|
||||||
|
throw new Error(
|
||||||
|
`Failed to install dependencies. Exit code: ${errorFields.exitCode}\nError: ${errorResult}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createInstallDependenciesToolFields(state.targetRepository),
|
createInstallDependenciesToolFields(state.targetRepository),
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,35 @@
|
||||||
import { tool } from "@langchain/core/tools";
|
import { tool } from "@langchain/core/tools";
|
||||||
import { GraphState } from "@open-swe/shared/open-swe/types";
|
import { GraphState, GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
import { getSandboxErrorFields } from "../utils/sandbox-error-fields.js";
|
import { getSandboxErrorFields } from "../utils/sandbox-error-fields.js";
|
||||||
import { TIMEOUT_SEC } from "@open-swe/shared/constants";
|
import { TIMEOUT_SEC } from "@open-swe/shared/constants";
|
||||||
import { createShellToolFields } from "@open-swe/shared/open-swe/tools";
|
import { createShellToolFields } from "@open-swe/shared/open-swe/tools";
|
||||||
import { getSandboxSessionOrThrow } from "./utils/get-sandbox-id.js";
|
import { getSandboxSessionOrThrow } from "./utils/get-sandbox-id.js";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
const DEFAULT_ENV = {
|
import { Sandbox } from "@daytonaio/sdk";
|
||||||
// Prevents corepack from showing a y/n download prompt which causes the command to hang
|
import { createShellExecutor } from "../utils/shell-executor/index.js";
|
||||||
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function createShellTool(
|
export function createShellTool(
|
||||||
state: Pick<GraphState, "sandboxSessionId" | "targetRepository">,
|
state: Pick<GraphState, "sandboxSessionId" | "targetRepository">,
|
||||||
|
config: GraphConfig,
|
||||||
) {
|
) {
|
||||||
const shellTool = tool(
|
const shellTool = tool(
|
||||||
async (input): Promise<{ result: string; status: "success" | "error" }> => {
|
async (input): Promise<{ result: string; status: "success" | "error" }> => {
|
||||||
try {
|
try {
|
||||||
const sandbox = await getSandboxSessionOrThrow(input);
|
|
||||||
|
|
||||||
const { command, workdir, timeout } = input;
|
const { command, workdir, timeout } = input;
|
||||||
const response = await sandbox.process.executeCommand(
|
|
||||||
command.join(" "),
|
// Get sandbox if needed for sandbox mode
|
||||||
|
let sandbox: Sandbox | undefined;
|
||||||
|
if (!isLocalMode(config)) {
|
||||||
|
sandbox = await getSandboxSessionOrThrow(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
const executor = createShellExecutor(config);
|
||||||
|
const response = await executor.executeCommand({
|
||||||
|
command,
|
||||||
workdir,
|
workdir,
|
||||||
DEFAULT_ENV,
|
timeout: timeout ?? TIMEOUT_SEC,
|
||||||
timeout ?? TIMEOUT_SEC,
|
sandbox,
|
||||||
);
|
});
|
||||||
|
|
||||||
if (response.exitCode !== 0) {
|
if (response.exitCode !== 0) {
|
||||||
const errorResult = response.result ?? response.artifacts?.stdout;
|
const errorResult = response.result ?? response.artifacts?.stdout;
|
||||||
|
|
@ -37,17 +42,19 @@ export function createShellTool(
|
||||||
result: response.result ?? `exit code: ${response.exitCode}`,
|
result: response.result ?? `exit code: ${response.exitCode}`,
|
||||||
status: "success",
|
status: "success",
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (error: any) {
|
||||||
const errorFields = getSandboxErrorFields(e);
|
const errorFields = getSandboxErrorFields(error);
|
||||||
if (errorFields) {
|
if (errorFields) {
|
||||||
const errorResult =
|
return {
|
||||||
errorFields.result ?? errorFields.artifacts?.stdout;
|
result: `Error: ${errorFields.result ?? errorFields.artifacts?.stdout}`,
|
||||||
throw new Error(
|
status: "error",
|
||||||
`Command failed. Exit code: ${errorFields.exitCode}\nError: ${errorResult}`,
|
};
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw e;
|
return {
|
||||||
|
result: `Error: ${error.message || String(error)}`,
|
||||||
|
status: "error",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createShellToolFields(state.targetRepository),
|
createShellToolFields(state.targetRepository),
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@ import { CustomRules } from "@open-swe/shared/open-swe/types";
|
||||||
import { Sandbox } from "@daytonaio/sdk";
|
import { Sandbox } from "@daytonaio/sdk";
|
||||||
import { createLogger, LogLevel } from "./logger.js";
|
import { createLogger, LogLevel } from "./logger.js";
|
||||||
import { getSandboxErrorFields } from "./sandbox-error-fields.js";
|
import { getSandboxErrorFields } from "./sandbox-error-fields.js";
|
||||||
|
import {
|
||||||
|
isLocalMode,
|
||||||
|
getLocalWorkingDirectory,
|
||||||
|
} from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { join } from "path";
|
||||||
|
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "CustomRules");
|
const logger = createLogger(LogLevel.INFO, "CustomRules");
|
||||||
|
|
||||||
|
|
@ -117,8 +124,13 @@ export function parseCustomRulesFromString(
|
||||||
export async function getCustomRules(
|
export async function getCustomRules(
|
||||||
sandbox: Sandbox,
|
sandbox: Sandbox,
|
||||||
rootDir: string,
|
rootDir: string,
|
||||||
|
config?: GraphConfig,
|
||||||
): Promise<CustomRules | undefined> {
|
): Promise<CustomRules | undefined> {
|
||||||
try {
|
try {
|
||||||
|
if (config && isLocalMode(config)) {
|
||||||
|
return getCustomRulesLocal(rootDir);
|
||||||
|
}
|
||||||
|
|
||||||
const catAgentsMdFileCommand = ["cat", "AGENTS.md"];
|
const catAgentsMdFileCommand = ["cat", "AGENTS.md"];
|
||||||
const agentsMdRes = await sandbox.process.executeCommand(
|
const agentsMdRes = await sandbox.process.executeCommand(
|
||||||
catAgentsMdFileCommand.join(" "),
|
catAgentsMdFileCommand.join(" "),
|
||||||
|
|
@ -155,6 +167,48 @@ export async function getCustomRules(
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local version of getCustomRules using Node.js fs
|
||||||
|
*/
|
||||||
|
async function getCustomRulesLocal(
|
||||||
|
rootDir: string,
|
||||||
|
): Promise<CustomRules | undefined> {
|
||||||
|
try {
|
||||||
|
const workingDirectory = rootDir || getLocalWorkingDirectory();
|
||||||
|
|
||||||
|
// Try to read AGENTS.md first
|
||||||
|
try {
|
||||||
|
const agentsMdPath = join(workingDirectory, "AGENTS.md");
|
||||||
|
const agentsMdContent = await fs.readFile(agentsMdPath, "utf-8");
|
||||||
|
if (agentsMdContent && agentsMdContent.length > 0) {
|
||||||
|
return parseCustomRulesFromString(agentsMdContent);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to read AGENTS.md", { error });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to read AGENT.md, CLAUDE.md, CURSOR.md
|
||||||
|
const filesToTry = ["AGENT.md", "CLAUDE.md", "CURSOR.md"];
|
||||||
|
|
||||||
|
for (const fileName of filesToTry) {
|
||||||
|
try {
|
||||||
|
const filePath = join(workingDirectory, fileName);
|
||||||
|
const content = await fs.readFile(filePath, "utf-8");
|
||||||
|
if (content && content.length > 0) {
|
||||||
|
return parseCustomRulesFromString(content);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// File doesn't exist, continue to next file
|
||||||
|
logger.error(`Failed to read ${fileName}`, { error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("Failed to get custom rules in local mode", { error });
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export const CUSTOM_RULES_PROMPT = `<custom_rules>
|
export const CUSTOM_RULES_PROMPT = `<custom_rules>
|
||||||
The following are custom rules provided by the user.
|
The following are custom rules provided by the user.
|
||||||
{EXTRA_CONTEXT}
|
{EXTRA_CONTEXT}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,14 @@ import { createPullRequest } from "./api.js";
|
||||||
import { addTaskPlanToIssue } from "./issue-task.js";
|
import { addTaskPlanToIssue } from "./issue-task.js";
|
||||||
import { DEFAULT_EXCLUDED_PATTERNS } from "./constants.js";
|
import { DEFAULT_EXCLUDED_PATTERNS } from "./constants.js";
|
||||||
import { escapeRegExp } from "../string-utils.js";
|
import { escapeRegExp } from "../string-utils.js";
|
||||||
|
import {
|
||||||
|
getLocalWorkingDirectory,
|
||||||
|
isLocalMode,
|
||||||
|
} from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
import {
|
||||||
|
getLocalShellExecutor,
|
||||||
|
LocalExecuteResponse,
|
||||||
|
} from "../shell-executor/index.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GitHub-Git");
|
const logger = createLogger(LogLevel.INFO, "GitHub-Git");
|
||||||
|
|
||||||
|
|
@ -43,12 +51,26 @@ async function getValidFilesToCommit(
|
||||||
sandbox: Sandbox,
|
sandbox: Sandbox,
|
||||||
excludePatterns: string[] = DEFAULT_EXCLUDED_PATTERNS,
|
excludePatterns: string[] = DEFAULT_EXCLUDED_PATTERNS,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const gitStatusOutput = await sandbox.process.executeCommand(
|
let gitStatusOutput: LocalExecuteResponse;
|
||||||
"git status --porcelain",
|
|
||||||
absoluteRepoDir,
|
// Check if we're in local mode (sandbox doesn't have process)
|
||||||
undefined,
|
if (!sandbox.process) {
|
||||||
TIMEOUT_SEC,
|
// Local mode: use LocalShellExecutor
|
||||||
);
|
const executor = getLocalShellExecutor(getLocalWorkingDirectory());
|
||||||
|
gitStatusOutput = await executor.executeCommand("git status --porcelain", {
|
||||||
|
workdir: absoluteRepoDir,
|
||||||
|
timeout: TIMEOUT_SEC,
|
||||||
|
localMode: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Sandbox mode: use sandbox.process
|
||||||
|
gitStatusOutput = await sandbox.process.executeCommand(
|
||||||
|
"git status --porcelain",
|
||||||
|
absoluteRepoDir,
|
||||||
|
undefined,
|
||||||
|
TIMEOUT_SEC,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (gitStatusOutput.exitCode !== 0) {
|
if (gitStatusOutput.exitCode !== 0) {
|
||||||
logger.error(`Failed to get git status for file validation`, {
|
logger.error(`Failed to get git status for file validation`, {
|
||||||
|
|
@ -120,12 +142,26 @@ export async function getChangedFilesStatus(
|
||||||
absoluteRepoDir: string,
|
absoluteRepoDir: string,
|
||||||
sandbox: Sandbox,
|
sandbox: Sandbox,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const gitStatusOutput = await sandbox.process.executeCommand(
|
let gitStatusOutput: LocalExecuteResponse;
|
||||||
"git status --porcelain",
|
|
||||||
absoluteRepoDir,
|
// Check if we're in local mode (sandbox doesn't have process)
|
||||||
undefined,
|
if (!sandbox.process) {
|
||||||
TIMEOUT_SEC,
|
// Local mode: use LocalShellExecutor
|
||||||
);
|
const executor = getLocalShellExecutor(getLocalWorkingDirectory());
|
||||||
|
gitStatusOutput = await executor.executeCommand("git status --porcelain", {
|
||||||
|
workdir: absoluteRepoDir,
|
||||||
|
timeout: TIMEOUT_SEC,
|
||||||
|
localMode: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Sandbox mode: use sandbox.process
|
||||||
|
gitStatusOutput = await sandbox.process.executeCommand(
|
||||||
|
"git status --porcelain",
|
||||||
|
absoluteRepoDir,
|
||||||
|
undefined,
|
||||||
|
TIMEOUT_SEC,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (gitStatusOutput.exitCode !== 0) {
|
if (gitStatusOutput.exitCode !== 0) {
|
||||||
logger.error(`Failed to get changed files status`, {
|
logger.error(`Failed to get changed files status`, {
|
||||||
|
|
@ -139,9 +175,23 @@ export async function getChangedFilesStatus(
|
||||||
|
|
||||||
export async function stashAndClearChanges(
|
export async function stashAndClearChanges(
|
||||||
absoluteRepoDir: string,
|
absoluteRepoDir: string,
|
||||||
sandbox: Sandbox,
|
sandbox: Sandbox | null,
|
||||||
|
config?: GraphConfig,
|
||||||
): Promise<ExecuteResponse | false> {
|
): Promise<ExecuteResponse | false> {
|
||||||
|
// In local mode, we don't want to stash and clear changes
|
||||||
|
if (config && isLocalMode(config)) {
|
||||||
|
logger.info("Skipping stash and clear changes in local mode");
|
||||||
|
return {
|
||||||
|
exitCode: 0,
|
||||||
|
result: "Skipped stash and clear in local mode",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Sandbox mode: use existing sandbox logic
|
||||||
|
if (!sandbox) {
|
||||||
|
throw new Error("Sandbox is required in non-local mode");
|
||||||
|
}
|
||||||
const gitStashOutput = await sandbox.process.executeCommand(
|
const gitStashOutput = await sandbox.process.executeCommand(
|
||||||
"git add -A && git stash && git reset --hard",
|
"git add -A && git stash && git reset --hard",
|
||||||
absoluteRepoDir,
|
absoluteRepoDir,
|
||||||
|
|
@ -156,6 +206,7 @@ export async function stashAndClearChanges(
|
||||||
}
|
}
|
||||||
return gitStashOutput;
|
return gitStashOutput;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// Sandbox mode error handling
|
||||||
const errorFields = getSandboxErrorFields(e);
|
const errorFields = getSandboxErrorFields(e);
|
||||||
logger.error(`Failed to stash and clear changes`, {
|
logger.error(`Failed to stash and clear changes`, {
|
||||||
...(errorFields && { errorFields }),
|
...(errorFields && { errorFields }),
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import { getIssue, getIssueComments } from "./api.js";
|
||||||
import { GraphConfig, TargetRepository } from "@open-swe/shared/open-swe/types";
|
import { GraphConfig, TargetRepository } from "@open-swe/shared/open-swe/types";
|
||||||
import { getGitHubTokensFromConfig } from "../github-tokens.js";
|
import { getGitHubTokensFromConfig } from "../github-tokens.js";
|
||||||
import { DETAILS_OPEN_TAG } from "./issue-task.js";
|
import { DETAILS_OPEN_TAG } from "./issue-task.js";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
|
||||||
export function getUntrackedComments(
|
export function getUntrackedComments(
|
||||||
existingMessages: BaseMessage[],
|
existingMessages: BaseMessage[],
|
||||||
|
|
@ -53,6 +54,10 @@ export async function getMissingMessages(
|
||||||
input: GetMissingMessagesInput,
|
input: GetMissingMessagesInput,
|
||||||
config: GraphConfig,
|
config: GraphConfig,
|
||||||
): Promise<BaseMessage[]> {
|
): Promise<BaseMessage[]> {
|
||||||
|
if (isLocalMode(config)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const { githubInstallationToken } = getGitHubTokensFromConfig(config);
|
const { githubInstallationToken } = getGitHubTokensFromConfig(config);
|
||||||
const [issue, comments] = await Promise.all([
|
const [issue, comments] = await Promise.all([
|
||||||
getIssue({
|
getIssue({
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
import { getIssue, updateIssue } from "./api.js";
|
import { getIssue, updateIssue } from "./api.js";
|
||||||
import { getGitHubTokensFromConfig } from "../github-tokens.js";
|
import { getGitHubTokensFromConfig } from "../github-tokens.js";
|
||||||
import { createLogger, LogLevel } from "../logger.js";
|
import { createLogger, LogLevel } from "../logger.js";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
const logger = createLogger(LogLevel.INFO, "IssueTaskString");
|
const logger = createLogger(LogLevel.INFO, "IssueTaskString");
|
||||||
|
|
||||||
export const TASK_OPEN_TAG = "<open-swe-do-not-edit-task-plan>";
|
export const TASK_OPEN_TAG = "<open-swe-do-not-edit-task-plan>";
|
||||||
|
|
@ -95,6 +95,12 @@ export async function getPlansFromIssue(
|
||||||
taskPlan: TaskPlan | null;
|
taskPlan: TaskPlan | null;
|
||||||
proposedPlan: string[] | null;
|
proposedPlan: string[] | null;
|
||||||
}> {
|
}> {
|
||||||
|
if (isLocalMode(config)) {
|
||||||
|
return {
|
||||||
|
taskPlan: null,
|
||||||
|
proposedPlan: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
const issue = await getIssue({
|
const issue = await getIssue({
|
||||||
owner: input.targetRepository.owner,
|
owner: input.targetRepository.owner,
|
||||||
repo: input.targetRepository.repo,
|
repo: input.targetRepository.repo,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import {
|
||||||
updateIssueComment,
|
updateIssueComment,
|
||||||
} from "./api.js";
|
} from "./api.js";
|
||||||
import { createLogger, LogLevel } from "../logger.js";
|
import { createLogger, LogLevel } from "../logger.js";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "GitHubPlan");
|
const logger = createLogger(LogLevel.INFO, "GitHubPlan");
|
||||||
|
|
||||||
|
|
@ -40,6 +41,13 @@ export async function postGitHubIssueComment(input: {
|
||||||
config: GraphConfig;
|
config: GraphConfig;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const { githubIssueId, targetRepository, commentBody, config } = input;
|
const { githubIssueId, targetRepository, commentBody, config } = input;
|
||||||
|
|
||||||
|
if (isLocalMode(config)) {
|
||||||
|
// In local mode, we don't post GitHub comments
|
||||||
|
logger.info("Skipping GitHub comment posting in local mode");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const githubAppName = process.env.GITHUB_APP_NAME;
|
const githubAppName = process.env.GITHUB_APP_NAME;
|
||||||
if (!githubAppName) {
|
if (!githubAppName) {
|
||||||
throw new Error("GITHUB_APP_NAME not set");
|
throw new Error("GITHUB_APP_NAME not set");
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@ import { Sandbox } from "@daytonaio/sdk";
|
||||||
import { createLogger, LogLevel } from "./logger.js";
|
import { createLogger, LogLevel } from "./logger.js";
|
||||||
import { getSandboxErrorFields } from "./sandbox-error-fields.js";
|
import { getSandboxErrorFields } from "./sandbox-error-fields.js";
|
||||||
import { traceable } from "langsmith/traceable";
|
import { traceable } from "langsmith/traceable";
|
||||||
|
import {
|
||||||
|
isLocalMode,
|
||||||
|
getLocalWorkingDirectory,
|
||||||
|
} from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
import { promises as fs } from "fs";
|
||||||
|
import { join, isAbsolute } from "path";
|
||||||
|
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "ReadWriteUtil");
|
const logger = createLogger(LogLevel.INFO, "ReadWriteUtil");
|
||||||
|
|
||||||
|
|
@ -11,7 +18,12 @@ async function handleCreateFile(
|
||||||
args?: {
|
args?: {
|
||||||
workDir?: string;
|
workDir?: string;
|
||||||
},
|
},
|
||||||
|
config?: GraphConfig,
|
||||||
) {
|
) {
|
||||||
|
if (config && isLocalMode(config)) {
|
||||||
|
return handleCreateFileLocal(filePath, args?.workDir);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const touchCommand = `touch "${filePath}"`;
|
const touchCommand = `touch "${filePath}"`;
|
||||||
const touchOutput = await sandbox.process.executeCommand(
|
const touchOutput = await sandbox.process.executeCommand(
|
||||||
|
|
@ -37,11 +49,17 @@ async function readFileFunc(inputs: {
|
||||||
sandbox: Sandbox;
|
sandbox: Sandbox;
|
||||||
filePath: string;
|
filePath: string;
|
||||||
workDir?: string;
|
workDir?: string;
|
||||||
|
config?: GraphConfig;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
output: string;
|
output: string;
|
||||||
}> {
|
}> {
|
||||||
const { sandbox, filePath, workDir } = inputs;
|
const { sandbox, filePath, workDir, config } = inputs;
|
||||||
|
|
||||||
|
if (config && isLocalMode(config)) {
|
||||||
|
return readFileLocal(filePath, workDir);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const readOutput = await sandbox.process.executeCommand(
|
const readOutput = await sandbox.process.executeCommand(
|
||||||
`cat "${filePath}"`,
|
`cat "${filePath}"`,
|
||||||
|
|
@ -62,13 +80,20 @@ async function readFileFunc(inputs: {
|
||||||
};
|
};
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e instanceof Error && e.message.includes("No such file or directory")) {
|
if (e instanceof Error && e.message.includes("No such file or directory")) {
|
||||||
const createOutput = await handleCreateFile(sandbox, filePath, {
|
let createOutput;
|
||||||
workDir,
|
if (config && isLocalMode(config)) {
|
||||||
});
|
// Local mode: use handleCreateFileLocal
|
||||||
|
createOutput = await handleCreateFileLocal(filePath, workDir);
|
||||||
|
} else {
|
||||||
|
// Sandbox mode: use handleCreateFile
|
||||||
|
createOutput = await handleCreateFile(sandbox, filePath, {
|
||||||
|
workDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (createOutput.exitCode !== 0) {
|
if (createOutput.exitCode !== 0) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
output: `FAILED TO EXECUTE READ COMMAND for sandbox '${filePath}'. Error: ${(e as Error).message || String(e)}`,
|
output: `FAILED TO EXECUTE READ COMMAND for ${config && isLocalMode(config) ? "local" : "sandbox"} '${filePath}'. Error: ${(e as Error).message || String(e)}`,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// If the file was created successfully, try reading it again.
|
// If the file was created successfully, try reading it again.
|
||||||
|
|
@ -108,7 +133,7 @@ async function readFileFunc(inputs: {
|
||||||
export const readFile = traceable(readFileFunc, {
|
export const readFile = traceable(readFileFunc, {
|
||||||
name: "read_file",
|
name: "read_file",
|
||||||
processInputs: (inputs) => {
|
processInputs: (inputs) => {
|
||||||
const { sandbox: _sandbox, ...rest } = inputs;
|
const { sandbox: _sandbox, config: _config, ...rest } = inputs;
|
||||||
return rest;
|
return rest;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -118,11 +143,17 @@ async function writeFileFunc(inputs: {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
content: string;
|
content: string;
|
||||||
workDir?: string;
|
workDir?: string;
|
||||||
|
config?: GraphConfig;
|
||||||
}): Promise<{
|
}): Promise<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
output: string;
|
output: string;
|
||||||
}> {
|
}> {
|
||||||
const { sandbox, filePath, content, workDir } = inputs;
|
const { sandbox, filePath, content, workDir, config } = inputs;
|
||||||
|
|
||||||
|
// Check if we're in local mode
|
||||||
|
if (config && isLocalMode(config)) {
|
||||||
|
return writeFileLocal(filePath, content, workDir);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const delimiter = "EOF_" + Date.now() + "_" + Math.random().toString(36);
|
const delimiter = "EOF_" + Date.now() + "_" + Math.random().toString(36);
|
||||||
const writeCommand = `cat > "${filePath}" << '${delimiter}'
|
const writeCommand = `cat > "${filePath}" << '${delimiter}'
|
||||||
|
|
@ -173,7 +204,116 @@ ${delimiter}`;
|
||||||
export const writeFile = traceable(writeFileFunc, {
|
export const writeFile = traceable(writeFileFunc, {
|
||||||
name: "write_file",
|
name: "write_file",
|
||||||
processInputs: (inputs) => {
|
processInputs: (inputs) => {
|
||||||
const { sandbox: _sandbox, ...rest } = inputs;
|
const { sandbox: _sandbox, config: _config, ...rest } = inputs;
|
||||||
return rest;
|
return rest;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local version of readFile using Node.js fs
|
||||||
|
*/
|
||||||
|
async function readFileLocal(
|
||||||
|
filePath: string,
|
||||||
|
workDir?: string,
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
output: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const workingDirectory = workDir || getLocalWorkingDirectory();
|
||||||
|
const fullPath = isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: join(workingDirectory, filePath);
|
||||||
|
const content = await fs.readFile(fullPath, "utf-8");
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: content,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.code === "ENOENT") {
|
||||||
|
// File doesn't exist, create it
|
||||||
|
try {
|
||||||
|
const workingDirectory = workDir || getLocalWorkingDirectory();
|
||||||
|
const fullPath = isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: join(workingDirectory, filePath);
|
||||||
|
await fs.writeFile(fullPath, "", "utf-8");
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: "",
|
||||||
|
};
|
||||||
|
} catch (createError: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: `FAILED TO AUTOMATICALLY CREATE FILE '${filePath}' AFTER READING FILE ERRORED WITH CODE: ${error.code}. Error: ${createError.message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: `FAILED TO READ FILE '${filePath}'. Error: ${error.message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local version of writeFile using Node.js fs
|
||||||
|
*/
|
||||||
|
async function writeFileLocal(
|
||||||
|
filePath: string,
|
||||||
|
content: string,
|
||||||
|
workDir?: string,
|
||||||
|
): Promise<{
|
||||||
|
success: boolean;
|
||||||
|
output: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const workingDirectory = workDir || getLocalWorkingDirectory();
|
||||||
|
const fullPath = isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: join(workingDirectory, filePath);
|
||||||
|
await fs.writeFile(fullPath, content, "utf-8");
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
output: `Successfully wrote file '${filePath}' to local filesystem.`,
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
output: `FAILED TO WRITE FILE '${filePath}'. Error: ${error.message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local version of handleCreateFile using Node.js fs
|
||||||
|
*/
|
||||||
|
async function handleCreateFileLocal(
|
||||||
|
filePath: string,
|
||||||
|
workDir?: string,
|
||||||
|
): Promise<{
|
||||||
|
exitCode: number;
|
||||||
|
error?: string;
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const workingDirectory = workDir || getLocalWorkingDirectory();
|
||||||
|
const fullPath = isAbsolute(filePath)
|
||||||
|
? filePath
|
||||||
|
: join(workingDirectory, filePath);
|
||||||
|
await fs.writeFile(fullPath, "", "utf-8");
|
||||||
|
return {
|
||||||
|
exitCode: 0,
|
||||||
|
stdout: `Created file '${filePath}'`,
|
||||||
|
stderr: "",
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
return {
|
||||||
|
exitCode: 1,
|
||||||
|
error: error.message,
|
||||||
|
stdout: "",
|
||||||
|
stderr: error.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { DEFAULT_SANDBOX_CREATE_PARAMS } from "../constants.js";
|
||||||
import { getGitHubTokensFromConfig } from "./github-tokens.js";
|
import { getGitHubTokensFromConfig } from "./github-tokens.js";
|
||||||
import { cloneRepo } from "./github/git.js";
|
import { cloneRepo } from "./github/git.js";
|
||||||
import { FAILED_TO_GENERATE_TREE_MESSAGE, getCodebaseTree } from "./tree.js";
|
import { FAILED_TO_GENERATE_TREE_MESSAGE, getCodebaseTree } from "./tree.js";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "Sandbox");
|
const logger = createLogger(LogLevel.INFO, "Sandbox");
|
||||||
|
|
||||||
|
|
@ -96,6 +97,18 @@ export async function getSandboxWithErrorHandling(
|
||||||
codebaseTree: string | null;
|
codebaseTree: string | null;
|
||||||
dependenciesInstalled: boolean | null;
|
dependenciesInstalled: boolean | null;
|
||||||
}> {
|
}> {
|
||||||
|
if (isLocalMode(config)) {
|
||||||
|
const mockSandbox = {
|
||||||
|
id: sandboxSessionId || "local-mock-sandbox",
|
||||||
|
state: "started",
|
||||||
|
} as Sandbox;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sandbox: mockSandbox,
|
||||||
|
codebaseTree: null,
|
||||||
|
dependenciesInstalled: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (!sandboxSessionId) {
|
if (!sandboxSessionId) {
|
||||||
throw new Error("No sandbox ID provided.");
|
throw new Error("No sandbox ID provided.");
|
||||||
|
|
|
||||||
3
apps/open-swe/src/utils/shell-executor/index.ts
Normal file
3
apps/open-swe/src/utils/shell-executor/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from "./shell-executor.js";
|
||||||
|
export * from "./local-shell-executor.js";
|
||||||
|
export * from "./types.js";
|
||||||
149
apps/open-swe/src/utils/shell-executor/local-shell-executor.ts
Normal file
149
apps/open-swe/src/utils/shell-executor/local-shell-executor.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
import { LocalExecuteResponse } from "./types.js";
|
||||||
|
import { createLogger, LogLevel } from "../logger.js";
|
||||||
|
|
||||||
|
const logger = createLogger(LogLevel.INFO, "LocalShellExecutor");
|
||||||
|
|
||||||
|
export class LocalShellExecutor {
|
||||||
|
private workingDirectory: string;
|
||||||
|
|
||||||
|
constructor(workingDirectory: string = process.cwd()) {
|
||||||
|
this.workingDirectory = workingDirectory;
|
||||||
|
logger.info("LocalShellExecutor created", { workingDirectory });
|
||||||
|
}
|
||||||
|
|
||||||
|
async executeCommand(
|
||||||
|
command: string,
|
||||||
|
args?: {
|
||||||
|
workdir?: string;
|
||||||
|
env?: Record<string, string>;
|
||||||
|
timeout?: number;
|
||||||
|
localMode?: boolean;
|
||||||
|
},
|
||||||
|
): Promise<LocalExecuteResponse> {
|
||||||
|
const { workdir, env, timeout = 30, localMode = false } = args || {};
|
||||||
|
const cwd = workdir || this.workingDirectory;
|
||||||
|
const environment = { ...process.env, ...(env || {}) };
|
||||||
|
|
||||||
|
logger.info("Executing command locally", { command, cwd, localMode });
|
||||||
|
|
||||||
|
// In local mode, use spawn directly for better reliability
|
||||||
|
if (localMode) {
|
||||||
|
try {
|
||||||
|
const cleanEnv = Object.fromEntries(
|
||||||
|
Object.entries(environment).filter(([_, v]) => v !== undefined),
|
||||||
|
) as Record<string, string>;
|
||||||
|
const result = await this.executeWithSpawn(
|
||||||
|
command,
|
||||||
|
cwd,
|
||||||
|
cleanEnv,
|
||||||
|
timeout,
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
} catch (spawnError: any) {
|
||||||
|
logger.error("Spawn execution failed in local mode", {
|
||||||
|
command,
|
||||||
|
error: spawnError.message,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
exitCode: 1,
|
||||||
|
result: spawnError.message,
|
||||||
|
artifacts: {
|
||||||
|
stdout: "",
|
||||||
|
stderr: spawnError.message,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-local mode: throw error as this executor is for local mode only
|
||||||
|
throw new Error("LocalShellExecutor is only for local mode operations");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async executeWithSpawn(
|
||||||
|
command: string,
|
||||||
|
cwd: string,
|
||||||
|
env: Record<string, string>,
|
||||||
|
timeout: number,
|
||||||
|
): Promise<LocalExecuteResponse> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Try different shell paths
|
||||||
|
const shellPaths = [
|
||||||
|
"/bin/bash",
|
||||||
|
"/usr/bin/bash",
|
||||||
|
"/bin/sh",
|
||||||
|
"/usr/bin/sh",
|
||||||
|
];
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
|
||||||
|
const tryShell = (shellPath: string) => {
|
||||||
|
const child = spawn(shellPath, ["-c", command], {
|
||||||
|
cwd,
|
||||||
|
env: { ...process.env, ...env },
|
||||||
|
timeout: timeout * 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
|
||||||
|
child.stdout?.on("data", (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr?.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("close", (code) => {
|
||||||
|
resolve({
|
||||||
|
exitCode: code || 0,
|
||||||
|
result: stdout,
|
||||||
|
artifacts: {
|
||||||
|
stdout,
|
||||||
|
stderr: stderr || undefined,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on("error", (error) => {
|
||||||
|
lastError = error;
|
||||||
|
// Try next shell path
|
||||||
|
const nextIndex = shellPaths.indexOf(shellPath) + 1;
|
||||||
|
if (nextIndex < shellPaths.length) {
|
||||||
|
tryShell(shellPaths[nextIndex]);
|
||||||
|
} else {
|
||||||
|
reject(lastError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start with the first shell path
|
||||||
|
tryShell(shellPaths[0]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getWorkingDirectory(): string {
|
||||||
|
return this.workingDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
setWorkingDirectory(directory: string): void {
|
||||||
|
this.workingDirectory = directory;
|
||||||
|
logger.info("Working directory changed", { workingDirectory: directory });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let sharedExecutor: LocalShellExecutor | null = null;
|
||||||
|
|
||||||
|
export function getLocalShellExecutor(
|
||||||
|
workingDirectory?: string,
|
||||||
|
): LocalShellExecutor {
|
||||||
|
if (
|
||||||
|
!sharedExecutor ||
|
||||||
|
(workingDirectory &&
|
||||||
|
sharedExecutor.getWorkingDirectory() !== workingDirectory)
|
||||||
|
) {
|
||||||
|
sharedExecutor = new LocalShellExecutor(workingDirectory);
|
||||||
|
}
|
||||||
|
return sharedExecutor;
|
||||||
|
}
|
||||||
141
apps/open-swe/src/utils/shell-executor/shell-executor.ts
Normal file
141
apps/open-swe/src/utils/shell-executor/shell-executor.ts
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
import { Sandbox } from "@daytonaio/sdk";
|
||||||
|
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
|
import { TIMEOUT_SEC } from "@open-swe/shared/constants";
|
||||||
|
import {
|
||||||
|
isLocalMode,
|
||||||
|
getLocalWorkingDirectory,
|
||||||
|
} from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
import { getLocalShellExecutor } from "./local-shell-executor.js";
|
||||||
|
import { createLogger, LogLevel } from "../logger.js";
|
||||||
|
import { ExecuteCommandOptions, LocalExecuteResponse } from "./types.js";
|
||||||
|
|
||||||
|
const logger = createLogger(LogLevel.INFO, "ShellExecutor");
|
||||||
|
|
||||||
|
const DEFAULT_ENV = {
|
||||||
|
// Prevents corepack from showing a y/n download prompt which causes the command to hang
|
||||||
|
COREPACK_ENABLE_DOWNLOAD_PROMPT: "0",
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified shell executor that handles both local and sandbox command execution
|
||||||
|
* This eliminates the need for if/else blocks in every tool that runs shell commands
|
||||||
|
*/
|
||||||
|
export class ShellExecutor {
|
||||||
|
private config: GraphConfig;
|
||||||
|
|
||||||
|
constructor(config: GraphConfig) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a command either locally or in the sandbox based on the current mode
|
||||||
|
*/
|
||||||
|
async executeCommand(
|
||||||
|
options: ExecuteCommandOptions,
|
||||||
|
): Promise<LocalExecuteResponse> {
|
||||||
|
const {
|
||||||
|
command,
|
||||||
|
workdir,
|
||||||
|
env = {},
|
||||||
|
timeout = TIMEOUT_SEC,
|
||||||
|
sandbox,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const commandString = Array.isArray(command) ? command.join(" ") : command;
|
||||||
|
const environment = { ...DEFAULT_ENV, ...env };
|
||||||
|
|
||||||
|
logger.info("Executing command", {
|
||||||
|
command: commandString,
|
||||||
|
workdir,
|
||||||
|
localMode: isLocalMode(this.config),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isLocalMode(this.config)) {
|
||||||
|
return this.executeLocal(commandString, workdir, environment, timeout);
|
||||||
|
} else {
|
||||||
|
return this.executeSandbox(
|
||||||
|
commandString,
|
||||||
|
workdir,
|
||||||
|
environment,
|
||||||
|
timeout,
|
||||||
|
sandbox,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute command locally using LocalShellExecutor
|
||||||
|
*/
|
||||||
|
private async executeLocal(
|
||||||
|
command: string,
|
||||||
|
workdir?: string,
|
||||||
|
env?: Record<string, string>,
|
||||||
|
timeout?: number,
|
||||||
|
): Promise<LocalExecuteResponse> {
|
||||||
|
const executor = getLocalShellExecutor(getLocalWorkingDirectory());
|
||||||
|
const localWorkdir = workdir || getLocalWorkingDirectory();
|
||||||
|
|
||||||
|
return await executor.executeCommand(command, {
|
||||||
|
workdir: localWorkdir,
|
||||||
|
env,
|
||||||
|
timeout,
|
||||||
|
localMode: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute command in sandbox
|
||||||
|
*/
|
||||||
|
private async executeSandbox(
|
||||||
|
command: string,
|
||||||
|
workdir?: string,
|
||||||
|
env?: Record<string, string>,
|
||||||
|
timeout?: number,
|
||||||
|
sandbox?: Sandbox,
|
||||||
|
): Promise<LocalExecuteResponse> {
|
||||||
|
if (!sandbox) {
|
||||||
|
throw new Error("Sandbox is required for sandbox mode execution");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sandbox.process.executeCommand(command, workdir, env, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if we're in local mode
|
||||||
|
*/
|
||||||
|
isLocalMode(): boolean {
|
||||||
|
return isLocalMode(this.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the appropriate working directory for the current mode
|
||||||
|
*/
|
||||||
|
getWorkingDirectory(): string {
|
||||||
|
if (isLocalMode(this.config)) {
|
||||||
|
return getLocalWorkingDirectory();
|
||||||
|
}
|
||||||
|
// For sandbox mode, this would need to be provided by the caller
|
||||||
|
// since it depends on the specific sandbox context
|
||||||
|
throw new Error(
|
||||||
|
"Working directory for sandbox mode must be provided explicitly",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory function to create a ShellExecutor instance
|
||||||
|
*/
|
||||||
|
export function createShellExecutor(config: GraphConfig): ShellExecutor {
|
||||||
|
return new ShellExecutor(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function for one-off command execution
|
||||||
|
*/
|
||||||
|
export async function executeCommand(
|
||||||
|
config: GraphConfig,
|
||||||
|
options: ExecuteCommandOptions,
|
||||||
|
): Promise<LocalExecuteResponse> {
|
||||||
|
const executor = createShellExecutor(config);
|
||||||
|
return await executor.executeCommand(options);
|
||||||
|
}
|
||||||
20
apps/open-swe/src/utils/shell-executor/types.ts
Normal file
20
apps/open-swe/src/utils/shell-executor/types.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { Sandbox } from "@daytonaio/sdk";
|
||||||
|
|
||||||
|
export interface LocalExecutionArtifacts {
|
||||||
|
stdout?: string;
|
||||||
|
stderr?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocalExecuteResponse {
|
||||||
|
exitCode: number;
|
||||||
|
result: string;
|
||||||
|
artifacts?: LocalExecutionArtifacts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecuteCommandOptions {
|
||||||
|
command: string | string[];
|
||||||
|
workdir?: string;
|
||||||
|
env?: Record<string, string>;
|
||||||
|
timeout?: number;
|
||||||
|
sandbox?: Sandbox;
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
import { getCurrentTaskInput } from "@langchain/langgraph";
|
import { getCurrentTaskInput } from "@langchain/langgraph";
|
||||||
import { GraphState, TargetRepository } from "@open-swe/shared/open-swe/types";
|
import {
|
||||||
|
GraphState,
|
||||||
|
TargetRepository,
|
||||||
|
GraphConfig,
|
||||||
|
} from "@open-swe/shared/open-swe/types";
|
||||||
import { daytonaClient } from "./sandbox.js";
|
import { daytonaClient } from "./sandbox.js";
|
||||||
import { createLogger, LogLevel } from "./logger.js";
|
import { createLogger, LogLevel } from "./logger.js";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { SANDBOX_ROOT_DIR, TIMEOUT_SEC } from "@open-swe/shared/constants";
|
import { SANDBOX_ROOT_DIR, TIMEOUT_SEC } from "@open-swe/shared/constants";
|
||||||
import { getSandboxErrorFields } from "./sandbox-error-fields.js";
|
import { getSandboxErrorFields } from "./sandbox-error-fields.js";
|
||||||
|
import {
|
||||||
|
isLocalMode,
|
||||||
|
getLocalWorkingDirectory,
|
||||||
|
} from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
import { getLocalShellExecutor } from "./shell-executor/index.js";
|
||||||
|
|
||||||
const logger = createLogger(LogLevel.INFO, "Tree");
|
const logger = createLogger(LogLevel.INFO, "Tree");
|
||||||
|
|
||||||
|
|
@ -14,12 +23,18 @@ export const FAILED_TO_GENERATE_TREE_MESSAGE =
|
||||||
export async function getCodebaseTree(
|
export async function getCodebaseTree(
|
||||||
sandboxSessionId_?: string,
|
sandboxSessionId_?: string,
|
||||||
targetRepository_?: TargetRepository,
|
targetRepository_?: TargetRepository,
|
||||||
|
config?: GraphConfig,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const command = `git ls-files | tree --fromfile -L 3`;
|
const command = `git ls-files | tree --fromfile -L 3`;
|
||||||
let sandboxSessionId = sandboxSessionId_;
|
let sandboxSessionId = sandboxSessionId_;
|
||||||
let targetRepository = targetRepository_;
|
let targetRepository = targetRepository_;
|
||||||
|
|
||||||
|
// Check if we're in local mode
|
||||||
|
if (config && isLocalMode(config)) {
|
||||||
|
return getCodebaseTreeLocal();
|
||||||
|
}
|
||||||
|
|
||||||
// If sandbox session ID is not provided, try to get it from the current state.
|
// If sandbox session ID is not provided, try to get it from the current state.
|
||||||
if (!sandboxSessionId || !targetRepository) {
|
if (!sandboxSessionId || !targetRepository) {
|
||||||
try {
|
try {
|
||||||
|
|
@ -76,3 +91,45 @@ export async function getCodebaseTree(
|
||||||
return FAILED_TO_GENERATE_TREE_MESSAGE;
|
return FAILED_TO_GENERATE_TREE_MESSAGE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Local version of getCodebaseTree using LocalShellExecutor
|
||||||
|
*/
|
||||||
|
async function getCodebaseTreeLocal(): Promise<string> {
|
||||||
|
try {
|
||||||
|
// In local mode, always use the current working directory
|
||||||
|
const workingDirectory = getLocalWorkingDirectory();
|
||||||
|
|
||||||
|
const executor = getLocalShellExecutor(workingDirectory);
|
||||||
|
const command = `git ls-files | tree --fromfile -L 3`;
|
||||||
|
|
||||||
|
const response = await executor.executeCommand(command, {
|
||||||
|
workdir: workingDirectory,
|
||||||
|
timeout: TIMEOUT_SEC,
|
||||||
|
localMode: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.exitCode !== 0) {
|
||||||
|
logger.error("Failed to generate tree in local mode", {
|
||||||
|
exitCode: response.exitCode,
|
||||||
|
result: response.result,
|
||||||
|
});
|
||||||
|
throw new Error(
|
||||||
|
`Failed to generate tree in local mode: ${response.result}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.result;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error("Failed to generate tree in local mode", {
|
||||||
|
...(e instanceof Error
|
||||||
|
? {
|
||||||
|
name: e.name,
|
||||||
|
message: e.message,
|
||||||
|
stack: e.stack,
|
||||||
|
}
|
||||||
|
: { error: e }),
|
||||||
|
});
|
||||||
|
return FAILED_TO_GENERATE_TREE_MESSAGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import {
|
||||||
} from "@langchain/core/messages";
|
} from "@langchain/core/messages";
|
||||||
import { getMessageContentString } from "@open-swe/shared/messages";
|
import { getMessageContentString } from "@open-swe/shared/messages";
|
||||||
import { extractContentWithoutDetailsFromIssueBody } from "./github/issue-messages.js";
|
import { extractContentWithoutDetailsFromIssueBody } from "./github/issue-messages.js";
|
||||||
|
import { isLocalMode } from "@open-swe/shared/open-swe/local-mode";
|
||||||
|
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
|
|
||||||
// TODO: Might want a better way of doing this.
|
// TODO: Might want a better way of doing this.
|
||||||
// maybe add a new kwarg `isRequest` and have this return the last human message with that field?
|
// maybe add a new kwarg `isRequest` and have this return the last human message with that field?
|
||||||
|
|
@ -41,19 +43,27 @@ export function getInitialUserRequest(
|
||||||
|
|
||||||
export function getRecentUserRequest(
|
export function getRecentUserRequest(
|
||||||
messages: BaseMessage[],
|
messages: BaseMessage[],
|
||||||
options?: { returnFullMessage?: never | false },
|
options?: { returnFullMessage?: never | false; config?: GraphConfig },
|
||||||
): string;
|
): string;
|
||||||
export function getRecentUserRequest(
|
export function getRecentUserRequest(
|
||||||
messages: BaseMessage[],
|
messages: BaseMessage[],
|
||||||
options?: { returnFullMessage?: true },
|
options?: { returnFullMessage?: true; config?: GraphConfig },
|
||||||
): HumanMessage;
|
): HumanMessage;
|
||||||
export function getRecentUserRequest(
|
export function getRecentUserRequest(
|
||||||
messages: BaseMessage[],
|
messages: BaseMessage[],
|
||||||
options?: { returnFullMessage?: boolean },
|
options?: { returnFullMessage?: boolean; config?: GraphConfig },
|
||||||
): string | HumanMessage {
|
): string | HumanMessage {
|
||||||
const recentUserMessage = messages.findLast(
|
let recentUserMessage: HumanMessage | undefined;
|
||||||
(m) => isHumanMessage(m) && m.additional_kwargs?.isFollowup,
|
|
||||||
);
|
if (options?.config && isLocalMode(options.config)) {
|
||||||
|
// In local mode, get the last human message regardless of flags
|
||||||
|
recentUserMessage = messages.findLast(isHumanMessage);
|
||||||
|
} else {
|
||||||
|
// In normal mode, look for messages with isFollowup flag
|
||||||
|
recentUserMessage = messages.findLast(
|
||||||
|
(m) => isHumanMessage(m) && m.additional_kwargs?.isFollowup,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (!recentUserMessage) {
|
if (!recentUserMessage) {
|
||||||
return "";
|
return "";
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"strictFunctionTypes": false,
|
"strictFunctionTypes": false,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
"rootDir": ".",
|
||||||
"types": ["jest", "node"],
|
"types": ["jest", "node"],
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true
|
"isolatedModules": true
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ export const GITHUB_INSTALLATION_TOKEN_COOKIE = "x-github-installation-token";
|
||||||
export const GITHUB_INSTALLATION_NAME = "x-github-installation-name";
|
export const GITHUB_INSTALLATION_NAME = "x-github-installation-name";
|
||||||
export const GITHUB_PAT = "x-github-pat";
|
export const GITHUB_PAT = "x-github-pat";
|
||||||
export const GITHUB_INSTALLATION_ID = "x-github-installation-id";
|
export const GITHUB_INSTALLATION_ID = "x-github-installation-id";
|
||||||
|
export const LOCAL_MODE_HEADER = "x-local-mode";
|
||||||
export const DO_NOT_RENDER_ID_PREFIX = "do-not-render-";
|
export const DO_NOT_RENDER_ID_PREFIX = "do-not-render-";
|
||||||
export const GITHUB_AUTH_STATE_COOKIE = "github_auth_state";
|
export const GITHUB_AUTH_STATE_COOKIE = "github_auth_state";
|
||||||
export const GITHUB_INSTALLATION_ID_COOKIE = "github_installation_id";
|
export const GITHUB_INSTALLATION_ID_COOKIE = "github_installation_id";
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,19 @@
|
||||||
import { SANDBOX_ROOT_DIR } from "./constants.js";
|
import { SANDBOX_ROOT_DIR } from "./constants.js";
|
||||||
import { TargetRepository } from "./open-swe/types.js";
|
import { TargetRepository, GraphConfig } from "./open-swe/types.js";
|
||||||
|
import {
|
||||||
|
isLocalMode,
|
||||||
|
getLocalWorkingDirectory,
|
||||||
|
} from "./open-swe/local-mode.js";
|
||||||
|
|
||||||
export function getRepoAbsolutePath(
|
export function getRepoAbsolutePath(
|
||||||
targetRepository: TargetRepository,
|
targetRepository: TargetRepository,
|
||||||
|
config?: GraphConfig,
|
||||||
): string {
|
): string {
|
||||||
|
// Check for local mode first
|
||||||
|
if (config && isLocalMode(config)) {
|
||||||
|
return getLocalWorkingDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
const repoName = targetRepository.repo;
|
const repoName = targetRepository.repo;
|
||||||
if (!repoName) {
|
if (!repoName) {
|
||||||
throw new Error("No repository name provided");
|
throw new Error("No repository name provided");
|
||||||
|
|
|
||||||
29
packages/shared/src/open-swe/local-mode.ts
Normal file
29
packages/shared/src/open-swe/local-mode.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { GraphConfig } from "@open-swe/shared/open-swe/types";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the current execution context is in local mode
|
||||||
|
* (working on local files instead of sandbox/Daytona)
|
||||||
|
*/
|
||||||
|
export function isLocalMode(config: GraphConfig): boolean {
|
||||||
|
return (config.configurable as any)?.["x-local-mode"] === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the local working directory for local mode operations
|
||||||
|
* Defaults to a test folder on the desktop if not specified
|
||||||
|
*/
|
||||||
|
export function getLocalWorkingDirectory(): string {
|
||||||
|
return (
|
||||||
|
process.env.OPEN_SWE_PROJECT_PATH ||
|
||||||
|
path.join(process.env.HOME || "", "Desktop", "test")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if we're in local mode based on environment variables
|
||||||
|
* (useful for contexts where GraphConfig is not available)
|
||||||
|
*/
|
||||||
|
export function isLocalModeFromEnv(): boolean {
|
||||||
|
return process.env.OPEN_SWE_LOCAL_MODE === "true";
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TargetRepository } from "./types.js";
|
import { TargetRepository, GraphConfig } from "./types.js";
|
||||||
import { getRepoAbsolutePath } from "../git.js";
|
import { getRepoAbsolutePath } from "../git.js";
|
||||||
import { TIMEOUT_SEC } from "../constants.js";
|
import { TIMEOUT_SEC } from "../constants.js";
|
||||||
|
import { isLocalMode, getLocalWorkingDirectory } from "./local-mode.js";
|
||||||
|
|
||||||
export function createApplyPatchToolFields(targetRepository: TargetRepository) {
|
export function createApplyPatchToolFields(targetRepository: TargetRepository) {
|
||||||
const repoRoot = getRepoAbsolutePath(targetRepository);
|
const repoRoot = getRepoAbsolutePath(targetRepository);
|
||||||
|
|
@ -546,8 +547,14 @@ export function createTextEditorToolFields(targetRepository: TargetRepository) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createViewToolFields(targetRepository: TargetRepository) {
|
export function createViewToolFields(
|
||||||
const repoRoot = getRepoAbsolutePath(targetRepository);
|
targetRepository: TargetRepository,
|
||||||
|
config?: GraphConfig,
|
||||||
|
) {
|
||||||
|
const repoRoot =
|
||||||
|
config && isLocalMode(config)
|
||||||
|
? getLocalWorkingDirectory()
|
||||||
|
: getRepoAbsolutePath(targetRepository);
|
||||||
const viewSchema = z.object({
|
const viewSchema = z.object({
|
||||||
command: z.enum(["view"]).describe("The command to execute: view"),
|
command: z.enum(["view"]).describe("The command to execute: view"),
|
||||||
path: z
|
path: z
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"strictFunctionTypes": false,
|
"strictFunctionTypes": false,
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
"types": ["jest", "node"],
|
"types": ["jest", "node"],
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4153,6 +4153,7 @@ __metadata:
|
||||||
"@types/uuid": ^10.0.0
|
"@types/uuid": ^10.0.0
|
||||||
"@typescript-eslint/eslint-plugin": ^8.38.0
|
"@typescript-eslint/eslint-plugin": ^8.38.0
|
||||||
"@typescript-eslint/parser": ^8.38.0
|
"@typescript-eslint/parser": ^8.38.0
|
||||||
|
commander: ^12.0.0
|
||||||
dotenv: ^16.6.1
|
dotenv: ^16.6.1
|
||||||
eslint: ^9.19.0
|
eslint: ^9.19.0
|
||||||
eslint-config-prettier: ^8.8.0
|
eslint-config-prettier: ^8.8.0
|
||||||
|
|
@ -9203,6 +9204,13 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"commander@npm:^12.0.0":
|
||||||
|
version: 12.1.0
|
||||||
|
resolution: "commander@npm:12.1.0"
|
||||||
|
checksum: 68e9818b00fc1ed9cdab9eb16905551c2b768a317ae69a5e3c43924c2b20ac9bb65b27e1cab36aeda7b6496376d4da908996ba2c0b5d79463e0fb1e77935d514
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"commander@npm:^13.0.0":
|
"commander@npm:^13.0.0":
|
||||||
version: 13.1.0
|
version: 13.1.0
|
||||||
resolution: "commander@npm:13.1.0"
|
resolution: "commander@npm:13.1.0"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue