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 6308ada9..2555b4af 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/take-action.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/take-action.ts @@ -22,7 +22,10 @@ import { import { Command } from "@langchain/langgraph"; import { truncateOutput } from "../../../utils/truncate-outputs.js"; import { getSandboxWithErrorHandling } from "../../../utils/sandbox.js"; -import { getCodebaseTree } from "../../../utils/tree.js"; +import { + FAILED_TO_GENERATE_TREE_MESSAGE, + getCodebaseTree, +} from "../../../utils/tree.js"; import { getRepoAbsolutePath } from "@open-swe/shared/git"; import { createInstallDependenciesTool } from "../../../tools/install-dependencies.js"; import { createSearchTool } from "../../../tools/search.js"; @@ -180,6 +183,11 @@ export async function takeAction( ]); const codebaseTree = await getCodebaseTree(); + // 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 + ? (state.codebaseTree ?? codebaseTree) + : codebaseTree; // Prioritize wereDependenciesInstalled over dependenciesInstalled const dependenciesInstalledUpdate = @@ -193,7 +201,7 @@ export async function takeAction( messages: toolCallResults, internalMessages: toolCallResults, ...(branchName && { branchName }), - codebaseTree, + codebaseTree: codebaseTreeToReturn, sandboxSessionId: sandbox.id, ...(dependenciesInstalledUpdate !== null && { dependenciesInstalled: dependenciesInstalledUpdate, diff --git a/apps/open-swe/src/graphs/shared/initialize-sandbox.ts b/apps/open-swe/src/graphs/shared/initialize-sandbox.ts index 424a9625..280c580b 100644 --- a/apps/open-swe/src/graphs/shared/initialize-sandbox.ts +++ b/apps/open-swe/src/graphs/shared/initialize-sandbox.ts @@ -14,7 +14,10 @@ import { configureGitUserInRepo, pullLatestChanges, } from "../../utils/github/git.js"; -import { getCodebaseTree } from "../../utils/tree.js"; +import { + FAILED_TO_GENERATE_TREE_MESSAGE, + getCodebaseTree, +} from "../../utils/tree.js"; import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants"; import { CustomNodeEvent, @@ -171,7 +174,15 @@ export async function initializeSandbox( emitStepEvent(baseGenerateCodebaseTreeAction, "pending"); try { const codebaseTree = await getCodebaseTree(existingSandbox.id); - emitStepEvent(baseGenerateCodebaseTreeAction, "success"); + if (codebaseTree === FAILED_TO_GENERATE_TREE_MESSAGE) { + emitStepEvent( + baseGenerateCodebaseTreeAction, + "error", + FAILED_TO_GENERATE_TREE_MESSAGE, + ); + } else { + emitStepEvent(baseGenerateCodebaseTreeAction, "success"); + } return { sandboxSessionId: existingSandbox.id, @@ -183,9 +194,14 @@ export async function initializeSandbox( emitStepEvent( baseGenerateCodebaseTreeAction, "error", - "Failed to generate codebase tree. Please try again later.", + FAILED_TO_GENERATE_TREE_MESSAGE, ); - throw new Error("Failed to generate codebase tree."); + return { + sandboxSessionId: existingSandbox.id, + codebaseTree: FAILED_TO_GENERATE_TREE_MESSAGE, + messages: createEventsMessage(), + customRules: await getCustomRules(existingSandbox, absoluteRepoDir), + }; } } catch { emitStepEvent( diff --git a/apps/open-swe/src/utils/sandbox.ts b/apps/open-swe/src/utils/sandbox.ts index 6bc0ba12..39a94848 100644 --- a/apps/open-swe/src/utils/sandbox.ts +++ b/apps/open-swe/src/utils/sandbox.ts @@ -5,7 +5,7 @@ import { DEFAULT_SANDBOX_CREATE_PARAMS } from "../constants.js"; import { getGitHubTokensFromConfig } from "./github-tokens.js"; import { cloneRepo, configureGitUserInRepo } from "./github/git.js"; import { getRepoAbsolutePath } from "@open-swe/shared/git"; -import { getCodebaseTree } from "./tree.js"; +import { FAILED_TO_GENERATE_TREE_MESSAGE, getCodebaseTree } from "./tree.js"; const logger = createLogger(LogLevel.INFO, "Sandbox"); @@ -65,6 +65,28 @@ export async function deleteSandbox( } } +async function createSandbox(attempt: number): Promise { + try { + return await daytonaClient().create(DEFAULT_SANDBOX_CREATE_PARAMS, { + timeout: 100, // 100s timeout on creation. + }); + } catch (e) { + logger.error("Failed to create sandbox", { + attempt, + ...(e instanceof Error + ? { + name: e.name, + message: e.message, + stack: e.stack, + } + : { + error: e, + }), + }); + return null; + } +} + export async function getSandboxWithErrorHandling( sandboxSessionId: string | undefined, targetRepository: TargetRepository, @@ -112,7 +134,19 @@ export async function getSandboxWithErrorHandling( error, }); - const sandbox = await daytonaClient().create(DEFAULT_SANDBOX_CREATE_PARAMS); + let sandbox: Sandbox | null = null; + let numSandboxCreateAttempts = 0; + while (!sandbox && numSandboxCreateAttempts < 3) { + sandbox = await createSandbox(numSandboxCreateAttempts); + if (!sandbox) { + numSandboxCreateAttempts++; + } + } + + if (!sandbox) { + throw new Error("Failed to create sandbox after 3 attempts"); + } + const { githubInstallationToken } = getGitHubTokensFromConfig(config); // Clone repository @@ -130,14 +164,16 @@ export async function getSandboxWithErrorHandling( }); // Get codebase tree - const codebaseTree = await getCodebaseTree(sandbox.id); + const codebaseTree = await getCodebaseTree(sandbox.id, targetRepository); + const codebaseTreeToReturn = + codebaseTree === FAILED_TO_GENERATE_TREE_MESSAGE ? null : codebaseTree; logger.info("Sandbox created successfully", { sandboxId: sandbox.id, }); return { sandbox, - codebaseTree, + codebaseTree: codebaseTreeToReturn, dependenciesInstalled: false, }; } diff --git a/apps/open-swe/src/utils/tree.ts b/apps/open-swe/src/utils/tree.ts index 3e96fe56..016e8bc9 100644 --- a/apps/open-swe/src/utils/tree.ts +++ b/apps/open-swe/src/utils/tree.ts @@ -1,36 +1,78 @@ import { getCurrentTaskInput } from "@langchain/langgraph"; -import { GraphState } from "@open-swe/shared/open-swe/types"; +import { GraphState, TargetRepository } 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"; +import { getSandboxErrorFields } from "./sandbox-error-fields.js"; const logger = createLogger(LogLevel.INFO, "Tree"); -export async function getCodebaseTree(sandboxSessionId_?: string) { - const command = `git ls-files | tree --fromfile -L 3`; - const state = getCurrentTaskInput(); - const sandboxSessionId = sandboxSessionId_ || state.sandboxSessionId; - if (!sandboxSessionId) { - logger.error("Failed to generate tree: No sandbox session ID provided"); - throw new Error("Failed generate tree: No sandbox session ID provided"); - } - const sandbox = await daytonaClient().get(sandboxSessionId); - const repoDir = path.join(SANDBOX_ROOT_DIR, state.targetRepository.repo); - const response = await sandbox.process.executeCommand( - command, - repoDir, - undefined, - TIMEOUT_SEC, - ); +export const FAILED_TO_GENERATE_TREE_MESSAGE = + "Failed to generate tree. Please try again."; - if (response.exitCode !== 0) { +export async function getCodebaseTree( + sandboxSessionId_?: string, + targetRepository_?: TargetRepository, +): Promise { + try { + const command = `git ls-files | tree --fromfile -L 3`; + let sandboxSessionId = sandboxSessionId_; + let targetRepository = targetRepository_; + + // If sandbox session ID is not provided, try to get it from the current state. + if (!sandboxSessionId || !targetRepository) { + try { + const state = getCurrentTaskInput(); + // Prefer the provided sandbox session ID and target repository. Fallback to state if defined. + sandboxSessionId = sandboxSessionId ?? state.sandboxSessionId; + targetRepository = targetRepository ?? state.targetRepository; + } catch { + // not executed in a LangGraph instance. continue. + } + } + + if (!sandboxSessionId) { + logger.error("Failed to generate tree: No sandbox session ID provided"); + throw new Error("Failed generate tree: No sandbox session ID provided"); + } + if (!targetRepository) { + logger.error("Failed to generate tree: No target repository provided"); + throw new Error("Failed generate tree: No target repository provided"); + } + + const sandbox = await daytonaClient().get(sandboxSessionId); + const repoDir = path.join(SANDBOX_ROOT_DIR, targetRepository.repo); + const response = await sandbox.process.executeCommand( + command, + repoDir, + undefined, + TIMEOUT_SEC, + ); + + if (response.exitCode !== 0) { + logger.error("Failed to generate tree", { + exitCode: response.exitCode, + result: response.result ?? response.artifacts?.stdout, + }); + throw new Error( + `Failed to generate tree: ${response.result ?? response.artifacts?.stdout}`, + ); + } + + return response.result; + } catch (e) { + const errorFields = getSandboxErrorFields(e); logger.error("Failed to generate tree", { - error: response.result, - error_result: response, + ...(errorFields ? { errorFields } : {}), + ...(e instanceof Error + ? { + name: e.name, + message: e.message, + stack: e.stack, + } + : {}), }); - throw new Error(`Failed to generate tree: ${response.result}`); + return FAILED_TO_GENERATE_TREE_MESSAGE; } - - return response.result; }