feat: GPT-5 support (#712)

* feat: GPT-5 support

* cr

* standardize sandbox usage, fix wrapping commands

* cr

* cr

* cr

* cr

* cr

* use uuid instead of math.random
This commit is contained in:
Brace Sproul 2025-08-07 16:31:43 -07:00 • committed by GitHub
parent 076219352a
commit ce3dbe0bff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 252 additions and 163 deletions

View file

@ -145,6 +145,7 @@ export async function runPytestOnFiles(
let parsed: Omit<TestResults, "success" | "error">; let parsed: Omit<TestResults, "success" | "error">;
try { try {
const jsonReportResult = await readFile({ const jsonReportResult = await readFile({
config: {},
sandbox, sandbox,
filePath: "/tmp/pytest_report.json", filePath: "/tmp/pytest_report.json",
workDir: repoDir, workDir: repoDir,

View file

@ -173,7 +173,7 @@ export async function openPullRequest(
} }
if (process.env.SKIP_CI_UNTIL_LAST_COMMIT === "true") { if (process.env.SKIP_CI_UNTIL_LAST_COMMIT === "true") {
await pushEmptyCommit(state.targetRepository, sandbox, { await pushEmptyCommit(state.targetRepository, sandbox, config, {
githubInstallationToken, githubInstallationToken,
}); });
} }

View file

@ -263,7 +263,7 @@ export async function takeAction(
...toolCallResults, ...toolCallResults,
]); ]);
const codebaseTree = await getCodebaseTree(undefined, undefined, config); const codebaseTree = await getCodebaseTree(config);
// If the codebase tree failed to generate, fallback to the previous codebase tree, or if that's not defined, use the failed to generate message. // If the codebase tree failed to generate, fallback to the previous codebase tree, or if that's not defined, use the failed to generate message.
const codebaseTreeToReturn = const codebaseTreeToReturn =
codebaseTree === FAILED_TO_GENERATE_TREE_MESSAGE codebaseTree === FAILED_TO_GENERATE_TREE_MESSAGE

View file

@ -189,11 +189,7 @@ export async function initializeSandbox(
}; };
emitStepEvent(baseGenerateCodebaseTreeAction, "pending"); emitStepEvent(baseGenerateCodebaseTreeAction, "pending");
try { try {
const codebaseTree = await getCodebaseTree( const codebaseTree = await getCodebaseTree(config, existingSandbox.id);
existingSandbox.id,
undefined,
config,
);
if (codebaseTree === FAILED_TO_GENERATE_TREE_MESSAGE) { if (codebaseTree === FAILED_TO_GENERATE_TREE_MESSAGE) {
emitStepEvent( emitStepEvent(
baseGenerateCodebaseTreeAction, baseGenerateCodebaseTreeAction,
@ -208,7 +204,11 @@ export async function initializeSandbox(
sandboxSessionId: existingSandbox.id, sandboxSessionId: existingSandbox.id,
codebaseTree, codebaseTree,
messages: createEventsMessage(), messages: createEventsMessage(),
customRules: await getCustomRules(existingSandbox, absoluteRepoDir), customRules: await getCustomRules(
existingSandbox,
absoluteRepoDir,
config,
),
}; };
} catch { } catch {
emitStepEvent( emitStepEvent(
@ -220,7 +220,11 @@ export async function initializeSandbox(
sandboxSessionId: existingSandbox.id, sandboxSessionId: existingSandbox.id,
codebaseTree: FAILED_TO_GENERATE_TREE_MESSAGE, codebaseTree: FAILED_TO_GENERATE_TREE_MESSAGE,
messages: createEventsMessage(), messages: createEventsMessage(),
customRules: await getCustomRules(existingSandbox, absoluteRepoDir), customRules: await getCustomRules(
existingSandbox,
absoluteRepoDir,
config,
),
}; };
} }
} catch { } catch {
@ -347,7 +351,7 @@ export async function initializeSandbox(
emitStepEvent(baseGenerateCodebaseTreeAction, "pending"); emitStepEvent(baseGenerateCodebaseTreeAction, "pending");
let codebaseTree: string | undefined; let codebaseTree: string | undefined;
try { try {
codebaseTree = await getCodebaseTree(sandbox.id, undefined, config); codebaseTree = await getCodebaseTree(config, sandbox.id);
emitStepEvent(baseGenerateCodebaseTreeAction, "success"); emitStepEvent(baseGenerateCodebaseTreeAction, "success");
} catch (_) { } catch (_) {
emitStepEvent( emitStepEvent(
@ -363,7 +367,7 @@ export async function initializeSandbox(
codebaseTree, codebaseTree,
messages: createEventsMessage(), messages: createEventsMessage(),
dependenciesInstalled: false, dependenciesInstalled: false,
customRules: await getCustomRules(sandbox, absoluteRepoDir), customRules: await getCustomRules(sandbox, absoluteRepoDir, config),
branchName: newBranchName, branchName: newBranchName,
}; };
} }
@ -455,7 +459,7 @@ async function initializeSandboxLocal(
let codebaseTree = undefined; let codebaseTree = undefined;
try { try {
codebaseTree = await getCodebaseTree(undefined, targetRepository, config); codebaseTree = await getCodebaseTree(config, undefined, targetRepository);
emitStepEvent(baseGenerateCodebaseTreeAction, "success"); emitStepEvent(baseGenerateCodebaseTreeAction, "success");
} catch (_) { } catch (_) {
emitStepEvent( emitStepEvent(

View file

@ -14,6 +14,7 @@ import {
} from "@open-swe/shared/open-swe/local-mode"; } from "@open-swe/shared/open-swe/local-mode";
import { createShellExecutor } from "../utils/shell-executor/shell-executor.js"; import { createShellExecutor } from "../utils/shell-executor/shell-executor.js";
import { join } from "path"; import { join } from "path";
import { v4 as uuidv4 } from "uuid";
type FileOperationResult = { type FileOperationResult = {
success: boolean; success: boolean;
@ -38,11 +39,8 @@ async function applyPatchWithGit(
): Promise<FileOperationResult> { ): Promise<FileOperationResult> {
// Generate temp patch file path // Generate temp patch file path
const tempPatchFile = isLocalMode(config) const tempPatchFile = isLocalMode(config)
? join( ? join(workDir, `patch_${uuidv4()}.diff`)
workDir, : `/tmp/patch_${uuidv4()}.diff`;
`patch_${Date.now()}_${Math.random().toString(36).substring(2)}.diff`,
)
: `/tmp/patch_${Date.now()}_${Math.random().toString(36).substring(2)}.diff`;
try { try {
// Create the patch file using unified shell executor // Create the patch file using unified shell executor

View file

@ -2,53 +2,36 @@ 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 { GraphConfig } from "@open-swe/shared/open-swe/types";
import { import { createShellExecutor } from "../../utils/shell-executor/index.js";
createShellExecutor,
LocalExecuteResponse, interface ViewCommandInputs {
} from "../../utils/shell-executor/index.js"; path: string;
workDir: string;
viewRange?: [number, number];
}
export async function handleViewCommand( export async function handleViewCommand(
sandbox: Sandbox, sandbox: Sandbox,
path: string, config: GraphConfig,
workDir: string, inputs: ViewCommandInputs,
viewRange?: [number, number],
config?: GraphConfig,
): Promise<string> { ): Promise<string> {
const { path, workDir, viewRange } = inputs;
try { try {
// Check if path is a directory // Check if path is a directory
let statOutput: LocalExecuteResponse; const executor = createShellExecutor(config);
if (!config) { const statOutput = await executor.executeCommand({
// Fallback to direct sandbox execution if no config provided command: `stat -c %F "${path}"`,
statOutput = await sandbox.process.executeCommand( workdir: workDir,
`stat -c %F "${path}"`, sandbox,
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
let lsOutput: LocalExecuteResponse; const lsOutput = await executor.executeCommand({
if (!config) { command: `ls -la "${path}"`,
// Fallback to direct sandbox execution if no config provided workdir: workDir,
lsOutput = await sandbox.process.executeCommand( sandbox,
`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}`);
@ -99,17 +82,24 @@ export async function handleViewCommand(
} }
} }
interface StrReplaceCommandInputs {
path: string;
workDir: string;
oldStr: string;
newStr: string;
}
export async function handleStrReplaceCommand( export async function handleStrReplaceCommand(
sandbox: Sandbox, sandbox: Sandbox,
path: string, config: GraphConfig,
workDir: string, inputs: StrReplaceCommandInputs,
oldStr: string,
newStr: string,
): Promise<string> { ): Promise<string> {
const { path, workDir, oldStr, newStr } = inputs;
const { success: readSuccess, output: fileContent } = await readFile({ const { success: readSuccess, output: fileContent } = await readFile({
sandbox, sandbox,
filePath: path, filePath: path,
workDir, workDir,
config,
}); });
if (!readSuccess) { if (!readSuccess) {
@ -152,17 +142,24 @@ export async function handleStrReplaceCommand(
return `Successfully replaced text in ${path} at exactly one location.`; return `Successfully replaced text in ${path} at exactly one location.`;
} }
interface CreateCommandInputs {
path: string;
workDir: string;
fileText: string;
}
export async function handleCreateCommand( export async function handleCreateCommand(
sandbox: Sandbox, sandbox: Sandbox,
path: string, config: GraphConfig,
workDir: string, inputs: CreateCommandInputs,
fileText: string,
): Promise<string> { ): Promise<string> {
const { path, workDir, fileText } = inputs;
// Check if file already exists // Check if file already exists
const { success: readSuccess } = await readFile({ const { success: readSuccess } = await readFile({
sandbox, sandbox,
filePath: path, filePath: path,
workDir, workDir,
config,
}); });
if (readSuccess) { if (readSuccess) {
@ -185,17 +182,24 @@ export async function handleCreateCommand(
return `Successfully created file ${path}.`; return `Successfully created file ${path}.`;
} }
interface InsertCommandInputs {
path: string;
workDir: string;
insertLine: number;
newStr: string;
}
export async function handleInsertCommand( export async function handleInsertCommand(
sandbox: Sandbox, sandbox: Sandbox,
path: string, config: GraphConfig,
workDir: string, inputs: InsertCommandInputs,
insertLine: number,
newStr: string,
): Promise<string> { ): Promise<string> {
const { path, workDir, insertLine, newStr } = inputs;
const { success: readSuccess, output: fileContent } = await readFile({ const { success: readSuccess, output: fileContent } = await readFile({
sandbox, sandbox,
filePath: path, filePath: path,
workDir, workDir,
config,
}); });
if (!readSuccess) { if (!readSuccess) {

View file

@ -169,12 +169,11 @@ export function createTextEditorTool(
switch (command) { switch (command) {
case "view": case "view":
result = await handleViewCommand( result = await handleViewCommand(sandbox, config, {
sandbox,
path, path,
workDir, workDir,
view_range, viewRange: view_range,
); });
break; break;
case "str_replace": case "str_replace":
if (!old_str || new_str === undefined) { if (!old_str || new_str === undefined) {
@ -182,24 +181,22 @@ export function createTextEditorTool(
"str_replace command requires both old_str and new_str parameters", "str_replace command requires both old_str and new_str parameters",
); );
} }
result = await handleStrReplaceCommand( result = await handleStrReplaceCommand(sandbox, config, {
sandbox,
path, path,
workDir, workDir,
old_str, oldStr: old_str,
new_str, newStr: new_str,
); });
break; break;
case "create": case "create":
if (!file_text) { if (!file_text) {
throw new Error("create command requires file_text parameter"); throw new Error("create command requires file_text parameter");
} }
result = await handleCreateCommand( result = await handleCreateCommand(sandbox, config, {
sandbox,
path, path,
workDir, workDir,
file_text, fileText: file_text,
); });
break; break;
case "insert": case "insert":
if (insert_line === undefined || new_str === undefined) { if (insert_line === undefined || new_str === undefined) {
@ -207,13 +204,12 @@ export function createTextEditorTool(
"insert command requires both insert_line and new_str parameters", "insert command requires both insert_line and new_str parameters",
); );
} }
result = await handleInsertCommand( result = await handleInsertCommand(sandbox, config, {
sandbox,
path, path,
workDir, workDir,
insert_line, insertLine: insert_line,
new_str, newStr: new_str,
); });
break; break;
default: default:
throw new Error(`Unknown command: ${command}`); throw new Error(`Unknown command: ${command}`);

View file

@ -59,12 +59,11 @@ export function createViewTool(
} else { } else {
// Sandbox mode: use existing handler // Sandbox mode: use existing handler
const sandbox = await getSandboxSessionOrThrow(input); const sandbox = await getSandboxSessionOrThrow(input);
result = await handleViewCommand( result = await handleViewCommand(sandbox, config, {
sandbox,
path, path,
workDir, workDir,
view_range as [number, number] | undefined, viewRange: view_range as [number, number] | undefined,
); });
} }
logger.info(`View command executed successfully on ${path}`); logger.info(`View command executed successfully on ${path}`);

View file

@ -13,6 +13,7 @@ import {
formatGrepCommand, formatGrepCommand,
} from "@open-swe/shared/open-swe/tools"; } from "@open-swe/shared/open-swe/tools";
import { createShellExecutor } from "../utils/shell-executor/index.js"; import { createShellExecutor } from "../utils/shell-executor/index.js";
import { wrapScript } from "../utils/wrap-script.js";
const logger = createLogger(LogLevel.INFO, "GrepTool"); const logger = createLogger(LogLevel.INFO, "GrepTool");
@ -23,7 +24,7 @@ export function createGrepTool(
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 command = formatGrepCommand(input as any); const command = formatGrepCommand(input);
const localMode = isLocalMode(config); const localMode = isLocalMode(config);
const localAbsolutePath = getLocalWorkingDirectory(); const localAbsolutePath = getLocalWorkingDirectory();
const sandboxAbsolutePath = getRepoAbsolutePath(state.targetRepository); const sandboxAbsolutePath = getRepoAbsolutePath(state.targetRepository);
@ -36,7 +37,7 @@ export function createGrepTool(
const executor = createShellExecutor(config); const executor = createShellExecutor(config);
const response = await executor.executeCommand({ const response = await executor.executeCommand({
command, command: wrapScript(command.join(" ")),
workdir: workDir, workdir: workDir,
timeout: TIMEOUT_SEC, timeout: TIMEOUT_SEC,
}); });

View file

@ -11,7 +11,7 @@ export async function getSandboxSessionOrThrow(
): Promise<Sandbox> { ): Promise<Sandbox> {
let sandboxSessionId = ""; let sandboxSessionId = "";
// Attempt to extract from input. // Attempt to extract from input.
if ("xSandboxSessionId" in input) { if ("xSandboxSessionId" in input && input.xSandboxSessionId) {
sandboxSessionId = input.xSandboxSessionId as string; sandboxSessionId = input.xSandboxSessionId as string;
} else { } else {
const state = getCurrentTaskInput<GraphState>(); const state = getCurrentTaskInput<GraphState>();

View file

@ -9,6 +9,7 @@ import {
import { promises as fs } from "fs"; import { promises as fs } from "fs";
import { join } from "path"; import { join } from "path";
import { GraphConfig } from "@open-swe/shared/open-swe/types"; import { GraphConfig } from "@open-swe/shared/open-swe/types";
import { createShellExecutor } from "./shell-executor/shell-executor.js";
const logger = createLogger(LogLevel.INFO, "CustomRules"); const logger = createLogger(LogLevel.INFO, "CustomRules");
@ -124,18 +125,21 @@ export function parseCustomRulesFromString(
export async function getCustomRules( export async function getCustomRules(
sandbox: Sandbox, sandbox: Sandbox,
rootDir: string, rootDir: string,
config?: GraphConfig, config: GraphConfig,
): Promise<CustomRules | undefined> { ): Promise<CustomRules | undefined> {
try { try {
if (config && isLocalMode(config)) { if (isLocalMode(config)) {
return getCustomRulesLocal(rootDir); return getCustomRulesLocal(rootDir);
} }
const executor = createShellExecutor(config);
const catAgentsMdFileCommand = ["cat", "AGENTS.md"]; const catAgentsMdFileCommand = ["cat", "AGENTS.md"];
const agentsMdRes = await sandbox.process.executeCommand( const agentsMdRes = await executor.executeCommand({
catAgentsMdFileCommand.join(" "), command: catAgentsMdFileCommand.join(" "),
rootDir, workdir: rootDir,
); sandbox,
});
if (agentsMdRes.exitCode === 0 && agentsMdRes.result?.length > 0) { if (agentsMdRes.exitCode === 0 && agentsMdRes.result?.length > 0) {
return parseCustomRulesFromString(agentsMdRes.result); return parseCustomRulesFromString(agentsMdRes.result);
} }
@ -144,9 +148,21 @@ export async function getCustomRules(
const catClaudeMdFileCommand = ["cat", "CLAUDE.md"]; const catClaudeMdFileCommand = ["cat", "CLAUDE.md"];
const catCursorMdFileCommand = ["cat", "CURSOR.md"]; const catCursorMdFileCommand = ["cat", "CURSOR.md"];
const [agentMdRes, claudeMdRes, cursorMdRes] = await Promise.all([ const [agentMdRes, claudeMdRes, cursorMdRes] = await Promise.all([
sandbox.process.executeCommand(catAgentMdFileCommand.join(" "), rootDir), executor.executeCommand({
sandbox.process.executeCommand(catClaudeMdFileCommand.join(" "), rootDir), command: catAgentMdFileCommand.join(" "),
sandbox.process.executeCommand(catCursorMdFileCommand.join(" "), rootDir), workdir: rootDir,
sandbox,
}),
executor.executeCommand({
command: catClaudeMdFileCommand.join(" "),
workdir: rootDir,
sandbox,
}),
executor.executeCommand({
command: catCursorMdFileCommand.join(" "),
workdir: rootDir,
sandbox,
}),
]); ]);
if (agentMdRes.exitCode === 0 && agentMdRes.result?.length > 0) { if (agentMdRes.exitCode === 0 && agentMdRes.result?.length > 0) {
return parseCustomRulesFromString(agentMdRes.result); return parseCustomRulesFromString(agentMdRes.result);

View file

@ -376,6 +376,7 @@ export async function checkoutBranchAndCommit(
export async function pushEmptyCommit( export async function pushEmptyCommit(
targetRepository: TargetRepository, targetRepository: TargetRepository,
sandbox: Sandbox, sandbox: Sandbox,
config: GraphConfig,
options: { options: {
githubInstallationToken: string; githubInstallationToken: string;
}, },
@ -390,12 +391,12 @@ export async function pushEmptyCommit(
try { try {
const absoluteRepoDir = getRepoAbsolutePath(targetRepository); const absoluteRepoDir = getRepoAbsolutePath(targetRepository);
const setGitConfigRes = await sandbox.process.executeCommand( const executor = createShellExecutor(config);
`git config user.name "${userName}" && git config user.email "${userEmail}"`, const setGitConfigRes = await executor.executeCommand({
absoluteRepoDir, command: `git config user.name "${userName}" && git config user.email "${userEmail}"`,
undefined, workdir: absoluteRepoDir,
TIMEOUT_SEC, timeout: TIMEOUT_SEC,
); });
if (setGitConfigRes.exitCode !== 0) { if (setGitConfigRes.exitCode !== 0) {
logger.error(`Failed to set git config`, { logger.error(`Failed to set git config`, {
exitCode: setGitConfigRes.exitCode, exitCode: setGitConfigRes.exitCode,
@ -404,12 +405,11 @@ export async function pushEmptyCommit(
return; return;
} }
const emptyCommitRes = await sandbox.process.executeCommand( const emptyCommitRes = await executor.executeCommand({
"git commit --allow-empty -m 'Empty commit to trigger CI'", command: "git commit --allow-empty -m 'Empty commit to trigger CI'",
absoluteRepoDir, workdir: absoluteRepoDir,
undefined, timeout: TIMEOUT_SEC,
TIMEOUT_SEC, });
);
if (emptyCommitRes.exitCode !== 0) { if (emptyCommitRes.exitCode !== 0) {
logger.error(`Failed to push empty commit`, { logger.error(`Failed to push empty commit`, {
exitCode: emptyCommitRes.exitCode, exitCode: emptyCommitRes.exitCode,

View file

@ -180,7 +180,6 @@ export class ModelManager {
const modelOptions: InitChatModelArgs = { const modelOptions: InitChatModelArgs = {
modelProvider: provider, modelProvider: provider,
temperature: thinkingModel ? undefined : temperature,
max_retries: MAX_RETRIES, max_retries: MAX_RETRIES,
...(apiKey ? { apiKey } : {}), ...(apiKey ? { apiKey } : {}),
...(thinkingModel && provider === "anthropic" ...(thinkingModel && provider === "anthropic"
@ -188,7 +187,15 @@ export class ModelManager {
thinking: { budget_tokens: thinkingBudgetTokens, type: "enabled" }, thinking: { budget_tokens: thinkingBudgetTokens, type: "enabled" },
maxTokens: thinkingMaxTokens, maxTokens: thinkingMaxTokens,
} }
: { maxTokens: finalMaxTokens }), : modelName.includes("gpt-5")
? {
max_completion_tokens: finalMaxTokens,
temperature: 1,
}
: {
maxTokens: finalMaxTokens,
temperature: thinkingModel ? undefined : temperature,
}),
}; };
logger.debug("Initializing model", { logger.debug("Initializing model", {
@ -219,8 +226,17 @@ export class ModelManager {
selectedModelConfig = { selectedModelConfig = {
provider, provider,
modelName, modelName,
temperature: defaultConfig.temperature ?? baseConfig.temperature, ...(modelName.includes("gpt-5")
maxTokens: defaultConfig.maxTokens ?? baseConfig.maxTokens, ? {
max_completion_tokens:
defaultConfig.maxTokens ?? baseConfig.maxTokens,
temperature: 1,
}
: {
maxTokens: defaultConfig.maxTokens ?? baseConfig.maxTokens,
temperature:
defaultConfig.temperature ?? baseConfig.temperature,
}),
...(isThinkingModel ...(isThinkingModel
? { ? {
thinkingModel: true, thinkingModel: true,
@ -247,8 +263,17 @@ export class ModelManager {
const fallbackConfig = { const fallbackConfig = {
...fallbackModel, ...fallbackModel,
temperature: isThinkingModel ? undefined : baseConfig.temperature, ...(fallbackModel.modelName.includes("gpt-5")
maxTokens: baseConfig.maxTokens, ? {
max_completion_tokens: baseConfig.maxTokens,
temperature: 1,
}
: {
maxTokens: baseConfig.maxTokens,
temperature: isThinkingModel
? undefined
: baseConfig.temperature,
}),
...(isThinkingModel ...(isThinkingModel
? { ? {
thinkingModel: true, thinkingModel: true,
@ -331,8 +356,15 @@ export class ModelManager {
return { return {
modelName, modelName,
provider: modelProvider as Provider, provider: modelProvider as Provider,
temperature: taskConfig.temperature, ...(modelName.includes("gpt-5")
maxTokens: config.configurable?.maxTokens ?? 10_000, ? {
max_completion_tokens: config.configurable?.maxTokens ?? 10_000,
temperature: 1,
}
: {
maxTokens: config.configurable?.maxTokens ?? 10_000,
temperature: taskConfig.temperature,
}),
thinkingModel, thinkingModel,
thinkingBudgetTokens, thinkingBudgetTokens,
}; };
@ -361,11 +393,11 @@ export class ModelManager {
[LLMTask.SUMMARIZER]: "gemini-2.5-pro", [LLMTask.SUMMARIZER]: "gemini-2.5-pro",
}, },
openai: { openai: {
[LLMTask.PLANNER]: "o3", [LLMTask.PLANNER]: "gpt-5",
[LLMTask.PROGRAMMER]: "gpt-4.1", [LLMTask.PROGRAMMER]: "gpt-5",
[LLMTask.REVIEWER]: "o3", [LLMTask.REVIEWER]: "gpt-5",
[LLMTask.ROUTER]: "gpt-4o-mini", [LLMTask.ROUTER]: "gpt-5-nano",
[LLMTask.SUMMARIZER]: "gpt-4.1-mini", [LLMTask.SUMMARIZER]: "gpt-5-mini",
}, },
}; };

View file

@ -9,27 +9,30 @@ import {
import { promises as fs } from "fs"; import { promises as fs } from "fs";
import { join, isAbsolute } from "path"; import { join, isAbsolute } from "path";
import { GraphConfig } from "@open-swe/shared/open-swe/types"; import { GraphConfig } from "@open-swe/shared/open-swe/types";
import { createShellExecutor } from "./shell-executor/shell-executor.js";
import { v4 as uuidv4 } from "uuid";
const logger = createLogger(LogLevel.INFO, "ReadWriteUtil"); const logger = createLogger(LogLevel.INFO, "ReadWriteUtil");
async function handleCreateFile( async function handleCreateFile(
sandbox: Sandbox, sandbox: Sandbox | null,
filePath: string, filePath: string,
config: GraphConfig,
args?: { args?: {
workDir?: string; workDir?: string;
}, },
config?: GraphConfig,
) { ) {
if (config && isLocalMode(config)) { if (isLocalMode(config)) {
return handleCreateFileLocal(filePath, args?.workDir); return handleCreateFileLocal(filePath, args?.workDir);
} }
try { try {
const touchCommand = `touch "${filePath}"`; const executor = createShellExecutor(config);
const touchOutput = await sandbox.process.executeCommand( const touchOutput = await executor.executeCommand({
touchCommand, command: `touch "${filePath}"`,
args?.workDir, workdir: args?.workDir,
); sandbox: sandbox ?? undefined,
});
return touchOutput; return touchOutput;
} catch (e) { } catch (e) {
const errorFields = getSandboxErrorFields(e); const errorFields = getSandboxErrorFields(e);
@ -49,26 +52,25 @@ async function readFileFunc(inputs: {
sandbox: Sandbox | null; sandbox: Sandbox | null;
filePath: string; filePath: string;
workDir?: string; workDir?: string;
config?: GraphConfig; config: GraphConfig;
}): Promise<{ }): Promise<{
success: boolean; success: boolean;
output: string; output: string;
}> { }> {
const { sandbox, filePath, workDir, config } = inputs; const { sandbox, filePath, workDir, config } = inputs;
if (config && isLocalMode(config)) { if (isLocalMode(config)) {
return readFileLocal(filePath, workDir); return readFileLocal(filePath, workDir);
} }
if (!sandbox) { const executor = createShellExecutor(config);
throw new Error("Sandbox is required when not in local mode");
}
try { try {
const readOutput = await sandbox.process.executeCommand( const readOutput = await executor.executeCommand({
`cat "${filePath}"`, command: `cat "${filePath}"`,
workDir, workdir: workDir,
); sandbox: sandbox ?? undefined,
});
if (readOutput.exitCode !== 0) { if (readOutput.exitCode !== 0) {
const errorResult = readOutput.result ?? readOutput.artifacts?.stdout; const errorResult = readOutput.result ?? readOutput.artifacts?.stdout;
@ -90,7 +92,7 @@ async function readFileFunc(inputs: {
createOutput = await handleCreateFileLocal(filePath, workDir); createOutput = await handleCreateFileLocal(filePath, workDir);
} else { } else {
// Sandbox mode: use handleCreateFile // Sandbox mode: use handleCreateFile
createOutput = await handleCreateFile(sandbox, filePath, { createOutput = await handleCreateFile(sandbox, filePath, config, {
workDir, workDir,
}); });
} }
@ -164,14 +166,16 @@ async function writeFileFunc(inputs: {
} }
try { try {
const delimiter = "EOF_" + Date.now() + "_" + Math.random().toString(36); const delimiter = `EOF_${uuidv4()}`;
const writeCommand = `cat > "${filePath}" << '${delimiter}' const writeCommand = `cat > "${filePath}" << '${delimiter}'
${content} ${content}
${delimiter}`; ${delimiter}`;
const writeOutput = await sandbox.process.executeCommand( const executor = createShellExecutor(config);
writeCommand, const writeOutput = await executor.executeCommand({
workDir, command: writeCommand,
); workdir: workDir,
sandbox: sandbox ?? undefined,
});
if (writeOutput.exitCode !== 0) { if (writeOutput.exitCode !== 0) {
const errorResult = writeOutput.result ?? writeOutput.artifacts?.stdout; const errorResult = writeOutput.result ?? writeOutput.artifacts?.stdout;

View file

@ -169,9 +169,9 @@ export async function getSandboxWithErrorHandling(
// Get codebase tree // Get codebase tree
const codebaseTree = await getCodebaseTree( const codebaseTree = await getCodebaseTree(
config,
sandbox.id, sandbox.id,
targetRepository, targetRepository,
config,
); );
const codebaseTreeToReturn = const codebaseTreeToReturn =
codebaseTree === FAILED_TO_GENERATE_TREE_MESSAGE ? null : codebaseTree; codebaseTree === FAILED_TO_GENERATE_TREE_MESSAGE ? null : codebaseTree;

View file

@ -40,6 +40,7 @@ export class ShellExecutor {
env = {}, env = {},
timeout = TIMEOUT_SEC, timeout = TIMEOUT_SEC,
sandbox, sandbox,
sandboxSessionId,
} = options; } = options;
const commandString = Array.isArray(command) ? command.join(" ") : command; const commandString = Array.isArray(command) ? command.join(" ") : command;
@ -60,6 +61,7 @@ export class ShellExecutor {
environment, environment,
timeout, timeout,
sandbox, sandbox,
sandboxSessionId,
); );
} }
} }
@ -93,8 +95,13 @@ export class ShellExecutor {
env?: Record<string, string>, env?: Record<string, string>,
timeout?: number, timeout?: number,
sandbox?: Sandbox, sandbox?: Sandbox,
sandboxSessionId?: string,
): Promise<LocalExecuteResponse> { ): Promise<LocalExecuteResponse> {
const sandbox_ = sandbox ?? (await getSandboxSessionOrThrow({})); const sandbox_ =
sandbox ??
(await getSandboxSessionOrThrow({
xSandboxSessionId: sandboxSessionId,
}));
return await sandbox_.process.executeCommand( return await sandbox_.process.executeCommand(
command, command,

View file

@ -15,4 +15,5 @@ export interface ExecuteCommandOptions {
env?: Record<string, string>; env?: Record<string, string>;
timeout?: number; timeout?: number;
sandbox?: Sandbox; sandbox?: Sandbox;
sandboxSessionId?: string;
} }

View file

@ -4,7 +4,6 @@ import {
TargetRepository, TargetRepository,
GraphConfig, GraphConfig,
} from "@open-swe/shared/open-swe/types"; } from "@open-swe/shared/open-swe/types";
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";
@ -18,9 +17,9 @@ export const FAILED_TO_GENERATE_TREE_MESSAGE =
"Failed to generate tree. Please try again."; "Failed to generate tree. Please try again.";
export async function getCodebaseTree( export async function getCodebaseTree(
config: GraphConfig,
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`;
@ -28,7 +27,7 @@ export async function getCodebaseTree(
let targetRepository = targetRepository_; let targetRepository = targetRepository_;
// Check if we're in local mode // Check if we're in local mode
if (config && isLocalMode(config)) { if (isLocalMode(config)) {
return getCodebaseTreeLocal(config); return getCodebaseTreeLocal(config);
} }
@ -53,14 +52,14 @@ export async function getCodebaseTree(
throw new Error("Failed generate tree: No target repository provided"); throw new Error("Failed generate tree: No target repository provided");
} }
const sandbox = await daytonaClient().get(sandboxSessionId); const executor = createShellExecutor(config);
const repoDir = path.join(SANDBOX_ROOT_DIR, targetRepository.repo); const repoDir = path.join(SANDBOX_ROOT_DIR, targetRepository.repo);
const response = await sandbox.process.executeCommand( const response = await executor.executeCommand({
command, command,
repoDir, workdir: repoDir,
undefined, timeout: TIMEOUT_SEC,
TIMEOUT_SEC, sandboxSessionId,
); });
if (response.exitCode !== 0) { if (response.exitCode !== 0) {
logger.error("Failed to generate tree", { logger.error("Failed to generate tree", {

View file

@ -1,7 +1,22 @@
import { v4 as uuidv4 } from "uuid";
export function wrapScript(command: string): string { export function wrapScript(command: string): string {
// Use bash directly to avoid script command compatibility issues const makeDelim = () => `OPEN_SWE_${uuidv4()}`;
return `bash -c "$(cat <<'OPEN_SWE_X'
// Ensure the delimiter does not appear as a standalone line in the command
let delim = makeDelim();
const containsStandalone = (d: string) =>
command === d ||
command.startsWith(`${d}\n`) ||
command.endsWith(`\n${d}`) ||
command.includes(`\n${d}\n`);
while (containsStandalone(delim)) {
delim = makeDelim();
}
return `script --return --quiet -c "$(cat <<'${delim}'
${command} ${command}
OPEN_SWE_X ${delim}
)"`; )" /dev/null`;
} }

View file

@ -32,6 +32,18 @@ export const MODEL_OPTIONS = [
label: "Claude 3.5 Haiku", label: "Claude 3.5 Haiku",
value: "anthropic:claude-3-5-haiku-latest", value: "anthropic:claude-3-5-haiku-latest",
}, },
{
label: "GPT 5",
value: "openai:gpt-5",
},
{
label: "GPT 5 mini",
value: "openai:gpt-5-mini",
},
{
label: "GPT 5 nano",
value: "openai:gpt-5-nano",
},
{ {
label: "o4", label: "o4",
value: "openai:o4", value: "openai:o4",