diff --git a/apps/open-swe/langbench/utils.ts b/apps/open-swe/langbench/utils.ts index 28643dbc..40079d08 100644 --- a/apps/open-swe/langbench/utils.ts +++ b/apps/open-swe/langbench/utils.ts @@ -145,6 +145,7 @@ export async function runPytestOnFiles( let parsed: Omit; try { const jsonReportResult = await readFile({ + config: {}, sandbox, filePath: "/tmp/pytest_report.json", workDir: repoDir, diff --git a/apps/open-swe/src/graphs/programmer/nodes/open-pr.ts b/apps/open-swe/src/graphs/programmer/nodes/open-pr.ts index c0a9eda3..2d989912 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/open-pr.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/open-pr.ts @@ -173,7 +173,7 @@ export async function openPullRequest( } if (process.env.SKIP_CI_UNTIL_LAST_COMMIT === "true") { - await pushEmptyCommit(state.targetRepository, sandbox, { + await pushEmptyCommit(state.targetRepository, sandbox, config, { githubInstallationToken, }); } diff --git a/apps/open-swe/src/graphs/programmer/nodes/take-action.ts b/apps/open-swe/src/graphs/programmer/nodes/take-action.ts index bff9df82..ece57e39 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/take-action.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/take-action.ts @@ -263,7 +263,7 @@ export async function takeAction( ...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. const codebaseTreeToReturn = codebaseTree === FAILED_TO_GENERATE_TREE_MESSAGE diff --git a/apps/open-swe/src/graphs/shared/initialize-sandbox.ts b/apps/open-swe/src/graphs/shared/initialize-sandbox.ts index 31e0cd2c..86544cf5 100644 --- a/apps/open-swe/src/graphs/shared/initialize-sandbox.ts +++ b/apps/open-swe/src/graphs/shared/initialize-sandbox.ts @@ -189,11 +189,7 @@ export async function initializeSandbox( }; emitStepEvent(baseGenerateCodebaseTreeAction, "pending"); try { - const codebaseTree = await getCodebaseTree( - existingSandbox.id, - undefined, - config, - ); + const codebaseTree = await getCodebaseTree(config, existingSandbox.id); if (codebaseTree === FAILED_TO_GENERATE_TREE_MESSAGE) { emitStepEvent( baseGenerateCodebaseTreeAction, @@ -208,7 +204,11 @@ export async function initializeSandbox( sandboxSessionId: existingSandbox.id, codebaseTree, messages: createEventsMessage(), - customRules: await getCustomRules(existingSandbox, absoluteRepoDir), + customRules: await getCustomRules( + existingSandbox, + absoluteRepoDir, + config, + ), }; } catch { emitStepEvent( @@ -220,7 +220,11 @@ export async function initializeSandbox( sandboxSessionId: existingSandbox.id, codebaseTree: FAILED_TO_GENERATE_TREE_MESSAGE, messages: createEventsMessage(), - customRules: await getCustomRules(existingSandbox, absoluteRepoDir), + customRules: await getCustomRules( + existingSandbox, + absoluteRepoDir, + config, + ), }; } } catch { @@ -347,7 +351,7 @@ export async function initializeSandbox( emitStepEvent(baseGenerateCodebaseTreeAction, "pending"); let codebaseTree: string | undefined; try { - codebaseTree = await getCodebaseTree(sandbox.id, undefined, config); + codebaseTree = await getCodebaseTree(config, sandbox.id); emitStepEvent(baseGenerateCodebaseTreeAction, "success"); } catch (_) { emitStepEvent( @@ -363,7 +367,7 @@ export async function initializeSandbox( codebaseTree, messages: createEventsMessage(), dependenciesInstalled: false, - customRules: await getCustomRules(sandbox, absoluteRepoDir), + customRules: await getCustomRules(sandbox, absoluteRepoDir, config), branchName: newBranchName, }; } @@ -455,7 +459,7 @@ async function initializeSandboxLocal( let codebaseTree = undefined; try { - codebaseTree = await getCodebaseTree(undefined, targetRepository, config); + codebaseTree = await getCodebaseTree(config, undefined, targetRepository); emitStepEvent(baseGenerateCodebaseTreeAction, "success"); } catch (_) { emitStepEvent( diff --git a/apps/open-swe/src/tools/apply-patch.ts b/apps/open-swe/src/tools/apply-patch.ts index ca0022b4..5ba90b21 100644 --- a/apps/open-swe/src/tools/apply-patch.ts +++ b/apps/open-swe/src/tools/apply-patch.ts @@ -14,6 +14,7 @@ import { } from "@open-swe/shared/open-swe/local-mode"; import { createShellExecutor } from "../utils/shell-executor/shell-executor.js"; import { join } from "path"; +import { v4 as uuidv4 } from "uuid"; type FileOperationResult = { success: boolean; @@ -38,11 +39,8 @@ async function applyPatchWithGit( ): Promise { // Generate temp patch file path const tempPatchFile = isLocalMode(config) - ? join( - workDir, - `patch_${Date.now()}_${Math.random().toString(36).substring(2)}.diff`, - ) - : `/tmp/patch_${Date.now()}_${Math.random().toString(36).substring(2)}.diff`; + ? join(workDir, `patch_${uuidv4()}.diff`) + : `/tmp/patch_${uuidv4()}.diff`; try { // Create the patch file using unified shell executor diff --git a/apps/open-swe/src/tools/builtin-tools/handlers.ts b/apps/open-swe/src/tools/builtin-tools/handlers.ts index 296cb20b..3fe6e2b5 100644 --- a/apps/open-swe/src/tools/builtin-tools/handlers.ts +++ b/apps/open-swe/src/tools/builtin-tools/handlers.ts @@ -2,53 +2,36 @@ import { Sandbox } from "@daytonaio/sdk"; import { readFile, writeFile } from "../../utils/read-write.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"; +import { createShellExecutor } from "../../utils/shell-executor/index.js"; + +interface ViewCommandInputs { + path: string; + workDir: string; + viewRange?: [number, number]; +} export async function handleViewCommand( sandbox: Sandbox, - path: string, - workDir: string, - viewRange?: [number, number], - config?: GraphConfig, + config: GraphConfig, + inputs: ViewCommandInputs, ): Promise { + const { path, workDir, viewRange } = inputs; try { // Check if path is a directory - let statOutput: LocalExecuteResponse; - if (!config) { - // 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, - }); - } + const executor = createShellExecutor(config); + const statOutput = await executor.executeCommand({ + command: `stat -c %F "${path}"`, + workdir: workDir, + sandbox, + }); if (statOutput.exitCode === 0 && statOutput.result?.includes("directory")) { // List directory contents - let lsOutput: LocalExecuteResponse; - if (!config) { - // 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, - }); - } + const lsOutput = await executor.executeCommand({ + command: `ls -la "${path}"`, + workdir: workDir, + sandbox, + }); if (lsOutput.exitCode !== 0) { 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( sandbox: Sandbox, - path: string, - workDir: string, - oldStr: string, - newStr: string, + config: GraphConfig, + inputs: StrReplaceCommandInputs, ): Promise { + const { path, workDir, oldStr, newStr } = inputs; const { success: readSuccess, output: fileContent } = await readFile({ sandbox, filePath: path, workDir, + config, }); if (!readSuccess) { @@ -152,17 +142,24 @@ export async function handleStrReplaceCommand( return `Successfully replaced text in ${path} at exactly one location.`; } +interface CreateCommandInputs { + path: string; + workDir: string; + fileText: string; +} + export async function handleCreateCommand( sandbox: Sandbox, - path: string, - workDir: string, - fileText: string, + config: GraphConfig, + inputs: CreateCommandInputs, ): Promise { + const { path, workDir, fileText } = inputs; // Check if file already exists const { success: readSuccess } = await readFile({ sandbox, filePath: path, workDir, + config, }); if (readSuccess) { @@ -185,17 +182,24 @@ export async function handleCreateCommand( return `Successfully created file ${path}.`; } +interface InsertCommandInputs { + path: string; + workDir: string; + insertLine: number; + newStr: string; +} + export async function handleInsertCommand( sandbox: Sandbox, - path: string, - workDir: string, - insertLine: number, - newStr: string, + config: GraphConfig, + inputs: InsertCommandInputs, ): Promise { + const { path, workDir, insertLine, newStr } = inputs; const { success: readSuccess, output: fileContent } = await readFile({ sandbox, filePath: path, workDir, + config, }); if (!readSuccess) { diff --git a/apps/open-swe/src/tools/builtin-tools/text-editor.ts b/apps/open-swe/src/tools/builtin-tools/text-editor.ts index 85b2b11a..03e137a6 100644 --- a/apps/open-swe/src/tools/builtin-tools/text-editor.ts +++ b/apps/open-swe/src/tools/builtin-tools/text-editor.ts @@ -169,12 +169,11 @@ export function createTextEditorTool( switch (command) { case "view": - result = await handleViewCommand( - sandbox, + result = await handleViewCommand(sandbox, config, { path, workDir, - view_range, - ); + viewRange: view_range, + }); break; case "str_replace": if (!old_str || new_str === undefined) { @@ -182,24 +181,22 @@ export function createTextEditorTool( "str_replace command requires both old_str and new_str parameters", ); } - result = await handleStrReplaceCommand( - sandbox, + result = await handleStrReplaceCommand(sandbox, config, { path, workDir, - old_str, - new_str, - ); + oldStr: old_str, + newStr: new_str, + }); break; case "create": if (!file_text) { throw new Error("create command requires file_text parameter"); } - result = await handleCreateCommand( - sandbox, + result = await handleCreateCommand(sandbox, config, { path, workDir, - file_text, - ); + fileText: file_text, + }); break; case "insert": if (insert_line === undefined || new_str === undefined) { @@ -207,13 +204,12 @@ export function createTextEditorTool( "insert command requires both insert_line and new_str parameters", ); } - result = await handleInsertCommand( - sandbox, + result = await handleInsertCommand(sandbox, config, { path, workDir, - insert_line, - new_str, - ); + insertLine: insert_line, + newStr: new_str, + }); break; default: throw new Error(`Unknown command: ${command}`); diff --git a/apps/open-swe/src/tools/builtin-tools/view.ts b/apps/open-swe/src/tools/builtin-tools/view.ts index 27996fd4..683d874b 100644 --- a/apps/open-swe/src/tools/builtin-tools/view.ts +++ b/apps/open-swe/src/tools/builtin-tools/view.ts @@ -59,12 +59,11 @@ export function createViewTool( } else { // Sandbox mode: use existing handler const sandbox = await getSandboxSessionOrThrow(input); - result = await handleViewCommand( - sandbox, + result = await handleViewCommand(sandbox, config, { path, workDir, - view_range as [number, number] | undefined, - ); + viewRange: view_range as [number, number] | undefined, + }); } logger.info(`View command executed successfully on ${path}`); diff --git a/apps/open-swe/src/tools/grep.ts b/apps/open-swe/src/tools/grep.ts index 23172ff4..4bc95bc9 100644 --- a/apps/open-swe/src/tools/grep.ts +++ b/apps/open-swe/src/tools/grep.ts @@ -13,6 +13,7 @@ import { formatGrepCommand, } from "@open-swe/shared/open-swe/tools"; import { createShellExecutor } from "../utils/shell-executor/index.js"; +import { wrapScript } from "../utils/wrap-script.js"; const logger = createLogger(LogLevel.INFO, "GrepTool"); @@ -23,7 +24,7 @@ export function createGrepTool( const grepTool = tool( async (input): Promise<{ result: string; status: "success" | "error" }> => { try { - const command = formatGrepCommand(input as any); + const command = formatGrepCommand(input); const localMode = isLocalMode(config); const localAbsolutePath = getLocalWorkingDirectory(); const sandboxAbsolutePath = getRepoAbsolutePath(state.targetRepository); @@ -36,7 +37,7 @@ export function createGrepTool( const executor = createShellExecutor(config); const response = await executor.executeCommand({ - command, + command: wrapScript(command.join(" ")), workdir: workDir, timeout: TIMEOUT_SEC, }); diff --git a/apps/open-swe/src/tools/utils/get-sandbox-id.ts b/apps/open-swe/src/tools/utils/get-sandbox-id.ts index f3c208fb..89cfcf65 100644 --- a/apps/open-swe/src/tools/utils/get-sandbox-id.ts +++ b/apps/open-swe/src/tools/utils/get-sandbox-id.ts @@ -11,7 +11,7 @@ export async function getSandboxSessionOrThrow( ): Promise { let sandboxSessionId = ""; // Attempt to extract from input. - if ("xSandboxSessionId" in input) { + if ("xSandboxSessionId" in input && input.xSandboxSessionId) { sandboxSessionId = input.xSandboxSessionId as string; } else { const state = getCurrentTaskInput(); diff --git a/apps/open-swe/src/utils/custom-rules.ts b/apps/open-swe/src/utils/custom-rules.ts index a97499b5..f44ebf0d 100644 --- a/apps/open-swe/src/utils/custom-rules.ts +++ b/apps/open-swe/src/utils/custom-rules.ts @@ -9,6 +9,7 @@ import { import { promises as fs } from "fs"; import { join } from "path"; import { GraphConfig } from "@open-swe/shared/open-swe/types"; +import { createShellExecutor } from "./shell-executor/shell-executor.js"; const logger = createLogger(LogLevel.INFO, "CustomRules"); @@ -124,18 +125,21 @@ export function parseCustomRulesFromString( export async function getCustomRules( sandbox: Sandbox, rootDir: string, - config?: GraphConfig, + config: GraphConfig, ): Promise { try { - if (config && isLocalMode(config)) { + if (isLocalMode(config)) { return getCustomRulesLocal(rootDir); } + const executor = createShellExecutor(config); + const catAgentsMdFileCommand = ["cat", "AGENTS.md"]; - const agentsMdRes = await sandbox.process.executeCommand( - catAgentsMdFileCommand.join(" "), - rootDir, - ); + const agentsMdRes = await executor.executeCommand({ + command: catAgentsMdFileCommand.join(" "), + workdir: rootDir, + sandbox, + }); if (agentsMdRes.exitCode === 0 && agentsMdRes.result?.length > 0) { return parseCustomRulesFromString(agentsMdRes.result); } @@ -144,9 +148,21 @@ export async function getCustomRules( const catClaudeMdFileCommand = ["cat", "CLAUDE.md"]; const catCursorMdFileCommand = ["cat", "CURSOR.md"]; const [agentMdRes, claudeMdRes, cursorMdRes] = await Promise.all([ - sandbox.process.executeCommand(catAgentMdFileCommand.join(" "), rootDir), - sandbox.process.executeCommand(catClaudeMdFileCommand.join(" "), rootDir), - sandbox.process.executeCommand(catCursorMdFileCommand.join(" "), rootDir), + executor.executeCommand({ + command: catAgentMdFileCommand.join(" "), + 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) { return parseCustomRulesFromString(agentMdRes.result); diff --git a/apps/open-swe/src/utils/github/git.ts b/apps/open-swe/src/utils/github/git.ts index 7e013e5d..2e9c78a7 100644 --- a/apps/open-swe/src/utils/github/git.ts +++ b/apps/open-swe/src/utils/github/git.ts @@ -376,6 +376,7 @@ export async function checkoutBranchAndCommit( export async function pushEmptyCommit( targetRepository: TargetRepository, sandbox: Sandbox, + config: GraphConfig, options: { githubInstallationToken: string; }, @@ -390,12 +391,12 @@ export async function pushEmptyCommit( try { const absoluteRepoDir = getRepoAbsolutePath(targetRepository); - const setGitConfigRes = await sandbox.process.executeCommand( - `git config user.name "${userName}" && git config user.email "${userEmail}"`, - absoluteRepoDir, - undefined, - TIMEOUT_SEC, - ); + const executor = createShellExecutor(config); + const setGitConfigRes = await executor.executeCommand({ + command: `git config user.name "${userName}" && git config user.email "${userEmail}"`, + workdir: absoluteRepoDir, + timeout: TIMEOUT_SEC, + }); if (setGitConfigRes.exitCode !== 0) { logger.error(`Failed to set git config`, { exitCode: setGitConfigRes.exitCode, @@ -404,12 +405,11 @@ export async function pushEmptyCommit( return; } - const emptyCommitRes = await sandbox.process.executeCommand( - "git commit --allow-empty -m 'Empty commit to trigger CI'", - absoluteRepoDir, - undefined, - TIMEOUT_SEC, - ); + const emptyCommitRes = await executor.executeCommand({ + command: "git commit --allow-empty -m 'Empty commit to trigger CI'", + workdir: absoluteRepoDir, + timeout: TIMEOUT_SEC, + }); if (emptyCommitRes.exitCode !== 0) { logger.error(`Failed to push empty commit`, { exitCode: emptyCommitRes.exitCode, diff --git a/apps/open-swe/src/utils/llms/model-manager.ts b/apps/open-swe/src/utils/llms/model-manager.ts index 28d28232..b8503d8f 100644 --- a/apps/open-swe/src/utils/llms/model-manager.ts +++ b/apps/open-swe/src/utils/llms/model-manager.ts @@ -180,7 +180,6 @@ export class ModelManager { const modelOptions: InitChatModelArgs = { modelProvider: provider, - temperature: thinkingModel ? undefined : temperature, max_retries: MAX_RETRIES, ...(apiKey ? { apiKey } : {}), ...(thinkingModel && provider === "anthropic" @@ -188,7 +187,15 @@ export class ModelManager { thinking: { budget_tokens: thinkingBudgetTokens, type: "enabled" }, maxTokens: thinkingMaxTokens, } - : { maxTokens: finalMaxTokens }), + : modelName.includes("gpt-5") + ? { + max_completion_tokens: finalMaxTokens, + temperature: 1, + } + : { + maxTokens: finalMaxTokens, + temperature: thinkingModel ? undefined : temperature, + }), }; logger.debug("Initializing model", { @@ -219,8 +226,17 @@ export class ModelManager { selectedModelConfig = { provider, modelName, - temperature: defaultConfig.temperature ?? baseConfig.temperature, - maxTokens: defaultConfig.maxTokens ?? baseConfig.maxTokens, + ...(modelName.includes("gpt-5") + ? { + max_completion_tokens: + defaultConfig.maxTokens ?? baseConfig.maxTokens, + temperature: 1, + } + : { + maxTokens: defaultConfig.maxTokens ?? baseConfig.maxTokens, + temperature: + defaultConfig.temperature ?? baseConfig.temperature, + }), ...(isThinkingModel ? { thinkingModel: true, @@ -247,8 +263,17 @@ export class ModelManager { const fallbackConfig = { ...fallbackModel, - temperature: isThinkingModel ? undefined : baseConfig.temperature, - maxTokens: baseConfig.maxTokens, + ...(fallbackModel.modelName.includes("gpt-5") + ? { + max_completion_tokens: baseConfig.maxTokens, + temperature: 1, + } + : { + maxTokens: baseConfig.maxTokens, + temperature: isThinkingModel + ? undefined + : baseConfig.temperature, + }), ...(isThinkingModel ? { thinkingModel: true, @@ -331,8 +356,15 @@ export class ModelManager { return { modelName, provider: modelProvider as Provider, - temperature: taskConfig.temperature, - maxTokens: config.configurable?.maxTokens ?? 10_000, + ...(modelName.includes("gpt-5") + ? { + max_completion_tokens: config.configurable?.maxTokens ?? 10_000, + temperature: 1, + } + : { + maxTokens: config.configurable?.maxTokens ?? 10_000, + temperature: taskConfig.temperature, + }), thinkingModel, thinkingBudgetTokens, }; @@ -361,11 +393,11 @@ export class ModelManager { [LLMTask.SUMMARIZER]: "gemini-2.5-pro", }, openai: { - [LLMTask.PLANNER]: "o3", - [LLMTask.PROGRAMMER]: "gpt-4.1", - [LLMTask.REVIEWER]: "o3", - [LLMTask.ROUTER]: "gpt-4o-mini", - [LLMTask.SUMMARIZER]: "gpt-4.1-mini", + [LLMTask.PLANNER]: "gpt-5", + [LLMTask.PROGRAMMER]: "gpt-5", + [LLMTask.REVIEWER]: "gpt-5", + [LLMTask.ROUTER]: "gpt-5-nano", + [LLMTask.SUMMARIZER]: "gpt-5-mini", }, }; diff --git a/apps/open-swe/src/utils/read-write.ts b/apps/open-swe/src/utils/read-write.ts index 1437a8a2..bc9c8225 100644 --- a/apps/open-swe/src/utils/read-write.ts +++ b/apps/open-swe/src/utils/read-write.ts @@ -9,27 +9,30 @@ import { import { promises as fs } from "fs"; import { join, isAbsolute } from "path"; 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"); async function handleCreateFile( - sandbox: Sandbox, + sandbox: Sandbox | null, filePath: string, + config: GraphConfig, args?: { workDir?: string; }, - config?: GraphConfig, ) { - if (config && isLocalMode(config)) { + if (isLocalMode(config)) { return handleCreateFileLocal(filePath, args?.workDir); } try { - const touchCommand = `touch "${filePath}"`; - const touchOutput = await sandbox.process.executeCommand( - touchCommand, - args?.workDir, - ); + const executor = createShellExecutor(config); + const touchOutput = await executor.executeCommand({ + command: `touch "${filePath}"`, + workdir: args?.workDir, + sandbox: sandbox ?? undefined, + }); return touchOutput; } catch (e) { const errorFields = getSandboxErrorFields(e); @@ -49,26 +52,25 @@ async function readFileFunc(inputs: { sandbox: Sandbox | null; filePath: string; workDir?: string; - config?: GraphConfig; + config: GraphConfig; }): Promise<{ success: boolean; output: string; }> { const { sandbox, filePath, workDir, config } = inputs; - if (config && isLocalMode(config)) { + if (isLocalMode(config)) { return readFileLocal(filePath, workDir); } - if (!sandbox) { - throw new Error("Sandbox is required when not in local mode"); - } + const executor = createShellExecutor(config); try { - const readOutput = await sandbox.process.executeCommand( - `cat "${filePath}"`, - workDir, - ); + const readOutput = await executor.executeCommand({ + command: `cat "${filePath}"`, + workdir: workDir, + sandbox: sandbox ?? undefined, + }); if (readOutput.exitCode !== 0) { const errorResult = readOutput.result ?? readOutput.artifacts?.stdout; @@ -90,7 +92,7 @@ async function readFileFunc(inputs: { createOutput = await handleCreateFileLocal(filePath, workDir); } else { // Sandbox mode: use handleCreateFile - createOutput = await handleCreateFile(sandbox, filePath, { + createOutput = await handleCreateFile(sandbox, filePath, config, { workDir, }); } @@ -164,14 +166,16 @@ async function writeFileFunc(inputs: { } try { - const delimiter = "EOF_" + Date.now() + "_" + Math.random().toString(36); + const delimiter = `EOF_${uuidv4()}`; const writeCommand = `cat > "${filePath}" << '${delimiter}' ${content} ${delimiter}`; - const writeOutput = await sandbox.process.executeCommand( - writeCommand, - workDir, - ); + const executor = createShellExecutor(config); + const writeOutput = await executor.executeCommand({ + command: writeCommand, + workdir: workDir, + sandbox: sandbox ?? undefined, + }); if (writeOutput.exitCode !== 0) { const errorResult = writeOutput.result ?? writeOutput.artifacts?.stdout; diff --git a/apps/open-swe/src/utils/sandbox.ts b/apps/open-swe/src/utils/sandbox.ts index 5d125f0f..f57711ef 100644 --- a/apps/open-swe/src/utils/sandbox.ts +++ b/apps/open-swe/src/utils/sandbox.ts @@ -169,9 +169,9 @@ export async function getSandboxWithErrorHandling( // Get codebase tree const codebaseTree = await getCodebaseTree( + config, sandbox.id, targetRepository, - config, ); const codebaseTreeToReturn = codebaseTree === FAILED_TO_GENERATE_TREE_MESSAGE ? null : codebaseTree; diff --git a/apps/open-swe/src/utils/shell-executor/shell-executor.ts b/apps/open-swe/src/utils/shell-executor/shell-executor.ts index a0e025f3..d2e3bee1 100644 --- a/apps/open-swe/src/utils/shell-executor/shell-executor.ts +++ b/apps/open-swe/src/utils/shell-executor/shell-executor.ts @@ -40,6 +40,7 @@ export class ShellExecutor { env = {}, timeout = TIMEOUT_SEC, sandbox, + sandboxSessionId, } = options; const commandString = Array.isArray(command) ? command.join(" ") : command; @@ -60,6 +61,7 @@ export class ShellExecutor { environment, timeout, sandbox, + sandboxSessionId, ); } } @@ -93,8 +95,13 @@ export class ShellExecutor { env?: Record, timeout?: number, sandbox?: Sandbox, + sandboxSessionId?: string, ): Promise { - const sandbox_ = sandbox ?? (await getSandboxSessionOrThrow({})); + const sandbox_ = + sandbox ?? + (await getSandboxSessionOrThrow({ + xSandboxSessionId: sandboxSessionId, + })); return await sandbox_.process.executeCommand( command, diff --git a/apps/open-swe/src/utils/shell-executor/types.ts b/apps/open-swe/src/utils/shell-executor/types.ts index 345300b9..4575d13c 100644 --- a/apps/open-swe/src/utils/shell-executor/types.ts +++ b/apps/open-swe/src/utils/shell-executor/types.ts @@ -15,4 +15,5 @@ export interface ExecuteCommandOptions { env?: Record; timeout?: number; sandbox?: Sandbox; + sandboxSessionId?: string; } diff --git a/apps/open-swe/src/utils/tree.ts b/apps/open-swe/src/utils/tree.ts index 75770a58..64ab4ca1 100644 --- a/apps/open-swe/src/utils/tree.ts +++ b/apps/open-swe/src/utils/tree.ts @@ -4,7 +4,6 @@ import { TargetRepository, GraphConfig, } from "@open-swe/shared/open-swe/types"; -import { daytonaClient } from "./sandbox.js"; import { createLogger, LogLevel } from "./logger.js"; import path from "node:path"; 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."; export async function getCodebaseTree( + config: GraphConfig, sandboxSessionId_?: string, targetRepository_?: TargetRepository, - config?: GraphConfig, ): Promise { try { const command = `git ls-files | tree --fromfile -L 3`; @@ -28,7 +27,7 @@ export async function getCodebaseTree( let targetRepository = targetRepository_; // Check if we're in local mode - if (config && isLocalMode(config)) { + if (isLocalMode(config)) { return getCodebaseTreeLocal(config); } @@ -53,14 +52,14 @@ export async function getCodebaseTree( 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 response = await sandbox.process.executeCommand( + const response = await executor.executeCommand({ command, - repoDir, - undefined, - TIMEOUT_SEC, - ); + workdir: repoDir, + timeout: TIMEOUT_SEC, + sandboxSessionId, + }); if (response.exitCode !== 0) { logger.error("Failed to generate tree", { diff --git a/apps/open-swe/src/utils/wrap-script.ts b/apps/open-swe/src/utils/wrap-script.ts index a866a71e..14f7ac54 100644 --- a/apps/open-swe/src/utils/wrap-script.ts +++ b/apps/open-swe/src/utils/wrap-script.ts @@ -1,7 +1,22 @@ +import { v4 as uuidv4 } from "uuid"; + export function wrapScript(command: string): string { - // Use bash directly to avoid script command compatibility issues - return `bash -c "$(cat <<'OPEN_SWE_X' + const makeDelim = () => `OPEN_SWE_${uuidv4()}`; + + // 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} -OPEN_SWE_X -)"`; +${delim} +)" /dev/null`; } diff --git a/packages/shared/src/open-swe/models.ts b/packages/shared/src/open-swe/models.ts index 965c5760..d0ec8043 100644 --- a/packages/shared/src/open-swe/models.ts +++ b/packages/shared/src/open-swe/models.ts @@ -32,6 +32,18 @@ export const MODEL_OPTIONS = [ label: "Claude 3.5 Haiku", 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", value: "openai:o4",