Skip to content
182 changes: 63 additions & 119 deletions apps/open-swe/src/graphs/planner/nodes/take-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export async function takeActions(
const shellTool = createShellTool(state, config);
const searchTool = createGrepTool(state, config);
const scratchpadTool = createScratchpadTool("");
const getURLContentTool = createGetURLContentTool(state);
const getURLContentTool = createGetURLContentTool(config.run_id);
const searchDocumentForTool = createSearchDocumentForTool(state, config);
const mcpTools = await getMcpTools(config);

Expand All @@ -76,130 +76,78 @@ export async function takeActions(
searchDocumentForTool,
...mcpTools,
];
const toolsMap = Object.fromEntries(
allTools.map((tool) => [tool.name, tool]),
);

const toolCalls = lastMessage.tool_calls;
if (!toolCalls?.length) {
throw new Error("No tool calls found.");
}

const { sandbox, codebaseTree, dependenciesInstalled } =
await getSandboxWithErrorHandling(
state.sandboxSessionId,
state.targetRepository,
state.branchName,
config,
);

const toolCallResultsPromise = toolCalls.map(async (toolCall) => {
const tool = toolsMap[toolCall.name];
if (!tool) {
logger.error(`Unknown tool: ${toolCall.name}`);
const toolMessage = new ToolMessage({
id: `${DO_NOT_RENDER_ID_PREFIX}${uuidv4()}`,
tool_call_id: toolCall.id ?? "",
content: `Unknown tool: ${toolCall.name}`,
name: toolCall.name,
status: "error",
});

return { toolMessage, stateUpdates: undefined };
}

logger.info("Executing planner tool action", {
...toolCall,
});

let result = "";
let toolCallStatus: "success" | "error" = "success";
try {
const toolResult =
// @ts-expect-error tool.invoke types are weird here...
(await tool.invoke({
...toolCall.args,
// Only pass sandbox session ID in sandbox mode, not local mode
...(isLocalMode(config) ? {} : { xSandboxSessionId: sandbox.id }),
})) as {
result: string;
status: "success" | "error";
};
if (typeof toolResult === "string") {
result = toolResult;
toolCallStatus = "success";
} else {
result = toolResult.result;
toolCallStatus = toolResult.status;
const sandbox = await getSandboxWithErrorHandling(config);
const toolCallResultsWithUpdates = await Promise.all(
lastMessage.tool_calls.map(async (toolCall) => {
const tool = allTools.find((t) => t.name === toolCall.name);
if (!tool) {
return new ToolMessage({
tool_call_id: toolCall.id,
content: `Tool ${toolCall.name} not found.`,
});
}
try {
const toolOutput = await tool.invoke(toolCall.args, {
...config,
runId: uuidv4(),
runId: config.run_id,
...(sandbox && {
sandbox,
}),
});
Comment on lines +91 to +98

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Duplicate runId property in object literal.

Lines 93-94 both define runId. The second assignment (runId: config.run_id) overwrites the first (runId: uuidv4()), making line 93 dead code. This appears to be a merge conflict or copy-paste error.

Based on the context (the tool needs a consistent run identifier for caching), config.run_id is likely the intended value.

🐛 Proposed fix
         const toolOutput = await tool.invoke(toolCall.args, {
           ...config,
-          runId: uuidv4(),
           runId: config.run_id,
           ...(sandbox && {
             sandbox,
           }),
         });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const toolOutput = await tool.invoke(toolCall.args, {
...config,
runId: uuidv4(),
runId: config.run_id,
...(sandbox && {
sandbox,
}),
});
const toolOutput = await tool.invoke(toolCall.args, {
...config,
runId: config.run_id,
...(sandbox && {
sandbox,
}),
});
🧰 Tools
🪛 Biome (2.3.14)

[error] 93-93: This property is later overwritten by an object member with the same name.

Overwritten with this property.

If an object property with the same name is defined multiple times (except when combining a getter with a setter), only the last definition makes it into the object and previous definitions are ignored.
Unsafe fix: Remove this property.

(lint/suspicious/noDuplicateObjectKeys)

🤖 Prompt for AI Agents
In `@apps/open-swe/src/graphs/planner/nodes/take-action.ts` around lines 91 - 98,
The object passed to tool.invoke contains a duplicate runId property (uuidv4()
and config.run_id) so remove the dead/duplicate assignment and ensure the tool
receives the intended consistent run identifier; update the call site in the
tool.invoke invocation (the object built around toolCall.args, config, sandbox)
to only set runId using config.run_id (or, if you prefer to generate when
missing, set runId: config.run_id ?? uuidv4()), leaving sandbox spread and other
config intact.


if (!result) {
result =
toolCallStatus === "success"
? "Tool call returned no result"
: "Tool call failed";
}
} catch (e) {
toolCallStatus = "error";
if (
e instanceof Error &&
e.message === "Received tool input did not match expected schema"
) {
logger.error("Received tool input did not match expected schema", {
toolCall,
expectedSchema: safeSchemaToString(tool.schema),
const processedContent = processToolCallContent(
toolOutput,
higherContextLimitToolNames.includes(toolCall.name),
);

return new ToolMessage({
tool_call_id: toolCall.id,
content: processedContent.result,
...("stateUpdates" in processedContent && {
stateUpdates: processedContent.stateUpdates,
}),
});
result = safeBadArgsError(tool.schema, toolCall.args, toolCall.name);
} else {
logger.error("Failed to call tool", {
...(e instanceof Error
? { name: e.name, message: e.message, stack: e.stack }
: { error: e }),
} catch (e) {
const error = e as Error;
logger.error("Error invoking tool", {
toolName: toolCall.name,
toolArgs: toolCall.args,
error: error.message,
});
return new ToolMessage({
tool_call_id: toolCall.id,
content: safeBadArgsError(error, tool.schema, toolCall.args),
name: toolCall.name,
});
const errMessage = e instanceof Error ? e.message : "Unknown error";
result = `FAILED TO CALL TOOL: "${toolCall.name}"\n\n${errMessage}`;
}
}

const { content, stateUpdates } = await processToolCallContent(
toolCall,
result,
{
higherContextLimitToolNames,
state,
config,
},
);

const toolMessage = new ToolMessage({
id: uuidv4(),
tool_call_id: toolCall.id ?? "",
content,
name: toolCall.name,
status: toolCallStatus,
});

return { toolMessage, stateUpdates };
});
}),
);

const toolCallResultsWithUpdates = await Promise.all(toolCallResultsPromise);
let toolCallResults = toolCallResultsWithUpdates.map(
(item) => item.toolMessage,
({ stateUpdates, ...rest }) => rest,
);

// merging document cache updates from tool calls
const allStateUpdates = toolCallResultsWithUpdates
.map((item) => item.stateUpdates)
.filter(Boolean)
.reduce(
(acc: { documentCache: Record<string, string> }, update) => {
if (update?.documentCache) {
acc.documentCache = { ...acc.documentCache, ...update.documentCache };
}
return acc;
},
{ documentCache: {} } as { documentCache: Record<string, string> },
);
let codebaseTree: string | undefined;
let dependenciesInstalled: boolean | null = null;

const shellToolCall = lastMessage.tool_calls.find(
(tc) => tc.name === "shell",
);
if (shellToolCall) {
const { command } = shellToolCall.args;
if (command.includes("ls -R")) {
const toolCallResult = toolCallResults.find(
(tc) => tc.tool_call_id === shellToolCall.id,
);
if (toolCallResult) {
codebaseTree = toolCallResult.content;
}
} else if (command.includes("yarn install")) {
dependenciesInstalled = true;
}
}

if (!isLocalMode(config)) {
const repoPath = isLocalMode(config)
Expand All @@ -215,7 +163,6 @@ export async function takeActions(
);
await stashAndClearChanges(repoPath, sandbox);

// Rewrite the tool call contents to include a changed files warning.
toolCallResults = toolCallResults.map(
(tc) =>
new ToolMessage({
Expand Down Expand Up @@ -244,18 +191,15 @@ export async function takeActions(
sandboxSessionId: sandbox.id,
...(codebaseTree && { codebaseTree }),
...(dependenciesInstalled !== null && { dependenciesInstalled }),
...allStateUpdates,
};

const maxContextActions = config.configurable?.maxContextActions ?? 75;
const maxActionsCount = maxContextActions * 2;
// Exclude hidden messages, and messages that are not AI messages or tool messages.
const filteredMessages = filterHiddenMessages([
...state.messages,
...(commandUpdate.messages ?? []),
]).filter((m) => isAIMessage(m) || isToolMessage(m));
if (filteredMessages.length >= maxActionsCount) {
// If we've exceeded the max actions count, we should generate a plan.
logger.info("Exceeded max actions count, generating plan.", {
maxActionsCount,
filteredMessages,
Expand Down
8 changes: 4 additions & 4 deletions apps/open-swe/src/graphs/programmer/nodes/request-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ export async function requestHelp(

const toolCall = lastMessage.tool_calls[0];

const threadId = config.configurable?.thread_id;
if (!threadId) {
throw new Error("Thread ID not found in config");
const runId = config.configurable?.run_id;
if (!runId) {
throw new Error("Run ID not found in config");
}

const userLogin = config.configurable?.[GITHUB_USER_LOGIN_HEADER];
const userTag = userLogin ? `@${userLogin} ` : "";

const runUrl = getOpenSweAppUrl(threadId);
const runUrl = getOpenSweAppUrl(runId);
const commentBody = runUrl
? `### 🤖 Open SWE Needs Help

Expand Down
22 changes: 7 additions & 15 deletions apps/open-swe/src/tools/url-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@ import { tool } from "@langchain/core/tools";
import { createLogger, LogLevel } from "../utils/logger.js";
import { createGetURLContentToolFields } from "@open-swe/shared/open-swe/tools";
import { FireCrawlLoader } from "@langchain/community/document_loaders/web/firecrawl";
import { GraphState } from "@open-swe/shared/open-swe/types";
import { parseUrl } from "../utils/url-parser.js";
import {
getDocumentFromCache,
saveDocumentToCache,
} from "../utils/http-db-client.js";

const logger = createLogger(LogLevel.INFO, "GetURLContentTool");

export function createGetURLContentTool(
state: Pick<GraphState, "documentCache">,
) {
export function createGetURLContentTool(runId: string) {
const getURLContentTool = tool(
async (
input,
): Promise<{
result: string;
status: "success" | "error";
stateUpdates?: Partial<Pick<GraphState, "documentCache">>;
}> => {
const { url } = input;

Expand All @@ -27,7 +27,7 @@ export function createGetURLContentTool(
const parsedUrl = urlParseResult.url?.href;

try {
let documentContent = state.documentCache[parsedUrl];
let documentContent = await getDocumentFromCache(runId, parsedUrl);
Comment on lines 27 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

parsedUrl could be undefined when passed to cache functions.

When urlParseResult.success is true, urlParseResult.url should exist, but the optional chaining on line 27 (urlParseResult.url?.href) can still result in parsedUrl being undefined. This undefined value is then passed to getDocumentFromCache and saveDocumentToCache, which expect a string.

Consider adding a type guard or asserting the value after the success check.

🛡️ Proposed fix
       const parsedUrl = urlParseResult.url?.href;
+      if (!parsedUrl) {
+        return { result: "Failed to parse URL", status: "error" };
+      }

       try {
         let documentContent = await getDocumentFromCache(runId, parsedUrl);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const parsedUrl = urlParseResult.url?.href;
try {
let documentContent = state.documentCache[parsedUrl];
let documentContent = await getDocumentFromCache(runId, parsedUrl);
const parsedUrl = urlParseResult.url?.href;
if (!parsedUrl) {
return { result: "Failed to parse URL", status: "error" };
}
try {
let documentContent = await getDocumentFromCache(runId, parsedUrl);
🤖 Prompt for AI Agents
In `@apps/open-swe/src/tools/url-content.ts` around lines 27 - 30, The code uses
parsedUrl = urlParseResult.url?.href which can be undefined even when
urlParseResult.success is true; ensure parsedUrl is a string before calling
getDocumentFromCache and saveDocumentToCache by adding a type guard or assertion
immediately after verifying urlParseResult.success (e.g., check that
urlParseResult.url and urlParseResult.url.href are defined), and if not present
return or throw a descriptive error; update places that call
getDocumentFromCache(runId, parsedUrl) and saveDocumentToCache(runId, parsedUrl,
...) to only run when parsedUrl is guaranteed to be a string.


if (!documentContent) {
logger.info("Document not cached, fetching via FireCrawl", {
Expand All @@ -44,15 +44,7 @@ export function createGetURLContentTool(
const docs = await loader.load();
documentContent = docs.map((doc) => doc.pageContent).join("\n\n");

if (state.documentCache) {
const stateUpdates = {
documentCache: {
...state.documentCache,
[parsedUrl]: documentContent,
},
};
return { result: documentContent, status: "success", stateUpdates };
}
await saveDocumentToCache(runId, parsedUrl, documentContent);
} else {
logger.info("Using cached document content", {
url: parsedUrl,
Expand Down
Loading