From d079c634246b633d2a29939775ddf7ef02bcd4ea Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Wed, 28 May 2025 15:18:58 -0700 Subject: [PATCH] fix: Agent and web fixes (#40) * fix: Agent and web fixes * cr * cr * cr * cr * cr --- apps/open-swe/src/nodes/generate-message.ts | 14 ++++++-- apps/open-swe/src/nodes/take-action.ts | 3 +- .../planner/nodes/generate-message.ts | 18 ++++++++-- .../subgraphs/planner/nodes/take-action.ts | 13 ++++--- apps/open-swe/src/utils/current-task.ts | 12 +++++++ apps/open-swe/src/utils/truncate-outputs.ts | 36 +++++++++++++++++++ apps/web/src/components/thread/index.tsx | 10 ++++++ .../web/src/components/thread/messages/ai.tsx | 16 ++++----- .../src/components/thread/messages/human.tsx | 2 +- .../components/thread/messages/tool-calls.tsx | 2 +- 10 files changed, 106 insertions(+), 20 deletions(-) create mode 100644 apps/open-swe/src/utils/current-task.ts create mode 100644 apps/open-swe/src/utils/truncate-outputs.ts diff --git a/apps/open-swe/src/nodes/generate-message.ts b/apps/open-swe/src/nodes/generate-message.ts index d666981b..73edf47f 100644 --- a/apps/open-swe/src/nodes/generate-message.ts +++ b/apps/open-swe/src/nodes/generate-message.ts @@ -5,6 +5,8 @@ import { getRepoAbsolutePath } from "../utils/git/index.js"; import { formatPlanPrompt } from "../utils/plan-prompt.js"; import { pauseSandbox } from "../utils/sandbox.js"; import { createLogger, LogLevel } from "../utils/logger.js"; +import { getCurrentTask } from "../utils/current-task.js"; +import { getMessageContentString } from "../utils/message/content.js"; const logger = createLogger(LogLevel.INFO, "GenerateMessageNode"); @@ -81,6 +83,7 @@ You MUST adhere to the following criteria when executing the task: - Always use \`rg\` instead of \`grep/ls -R\` because it is much faster and respects gitignore. - Always use glob patterns when searching with \`rg\` for specific file types. For example, to search for all TSX files, use \`rg -i star -g **/*.tsx project-directory/\`. This is because \`rg\` does not have built in file types for every language. - Only make changes to the existing Git repo ({REPO_DIRECTORY}). Any changes outside this repo will not be detected, so do not attempt to create new files or directories outside of this repo. +- You do NOT have access to the \`set_task_status\` or \`diagnose_error\` tools. NEVER attempt to call them. Below, is a collection of useful context about the codebase. It is updated after each completed task, and is provided to you to help you make decisions, and avoid duplicate work: {CODEBASE_CONTEXT} @@ -133,9 +136,16 @@ export async function generateAction( } logger.info("Generated action", { - name: response.tool_calls?.[0].name, - args: response.tool_calls?.[0].args, + currentTask: getCurrentTask(state.plan).plan, + ...(response.tool_calls?.[0] && { + name: response.tool_calls?.[0].name, + args: response.tool_calls?.[0].args, + }), + ...(getMessageContentString(response.content) && { + content: getMessageContentString(response.content), + }), }); + return { messages: [response], ...(newSandboxSessionId && { sandboxSessionId: newSandboxSessionId }), diff --git a/apps/open-swe/src/nodes/take-action.ts b/apps/open-swe/src/nodes/take-action.ts index 6025eba4..7e6e1b3a 100644 --- a/apps/open-swe/src/nodes/take-action.ts +++ b/apps/open-swe/src/nodes/take-action.ts @@ -17,6 +17,7 @@ import { zodSchemaToString, } from "../utils/zod-to-string.js"; import { Command } from "@langchain/langgraph"; +import { truncateOutput } from "../utils/truncate-outputs.js"; const logger = createLogger(LogLevel.INFO, "TakeAction"); @@ -105,7 +106,7 @@ export async function takeAction( const toolMessage = new ToolMessage({ tool_call_id: toolCall.id ?? "", - content: result, + content: truncateOutput(result), name: toolCall.name, status: toolCallStatus, }); diff --git a/apps/open-swe/src/subgraphs/planner/nodes/generate-message.ts b/apps/open-swe/src/subgraphs/planner/nodes/generate-message.ts index d7536504..1ee06ebd 100644 --- a/apps/open-swe/src/subgraphs/planner/nodes/generate-message.ts +++ b/apps/open-swe/src/subgraphs/planner/nodes/generate-message.ts @@ -1,8 +1,12 @@ import { loadModel, Task } from "../../../utils/load-model.js"; -import { shellTool, applyPatchTool } from "../../../tools/index.js"; +import { shellTool } from "../../../tools/index.js"; import { PlannerGraphState, PlannerGraphUpdate } from "../types.js"; import { GraphConfig } from "../../../types.js"; import { isHumanMessage } from "@langchain/core/messages"; +import { createLogger, LogLevel } from "../../../utils/logger.js"; +import { getMessageContentString } from "../../../utils/message/content.js"; + +const logger = createLogger(LogLevel.INFO, "GeneratePlanningMessageNode"); const systemPrompt = `You are operating as a terminal-based agentic coding assistant built by LangChain. It wraps LLM models to enable natural language interaction with a local codebase. You are expected to be precise, safe, and helpful. @@ -23,7 +27,7 @@ export async function generateAction( config: GraphConfig, ): Promise { const model = await loadModel(config, Task.ACTION_GENERATOR); - const tools = [shellTool, applyPatchTool]; + const tools = [shellTool]; const modelWithTools = model.bindTools(tools, { tool_choice: "auto" }); const firstUserMessage = state.messages.find(isHumanMessage); @@ -39,6 +43,16 @@ export async function generateAction( ...state.plannerMessages, ]); + logger.info("Generated planning message", { + ...(response.tool_calls?.[0] && { + name: response.tool_calls?.[0].name, + args: response.tool_calls?.[0].args, + }), + ...(getMessageContentString(response.content) && { + content: getMessageContentString(response.content), + }), + }); + return { plannerMessages: [response], }; diff --git a/apps/open-swe/src/subgraphs/planner/nodes/take-action.ts b/apps/open-swe/src/subgraphs/planner/nodes/take-action.ts index 5503051c..0c0b8f5d 100644 --- a/apps/open-swe/src/subgraphs/planner/nodes/take-action.ts +++ b/apps/open-swe/src/subgraphs/planner/nodes/take-action.ts @@ -1,10 +1,11 @@ import { isAIMessage, ToolMessage } from "@langchain/core/messages"; -import { applyPatchTool, shellTool } from "../../../tools/index.js"; +import { shellTool } from "../../../tools/index.js"; import { GraphConfig } from "../../../types.js"; import { PlannerGraphState, PlannerGraphUpdate } from "../types.js"; import { createLogger, LogLevel } from "../../../utils/logger.js"; import { zodSchemaToString } from "../../../utils/zod-to-string.js"; import { formatBadArgsError } from "../../../utils/zod-to-string.js"; +import { truncateOutput } from "../../../utils/truncate-outputs.js"; const logger = createLogger(LogLevel.INFO, "TakeAction"); @@ -20,7 +21,6 @@ export async function takeAction( } const toolsMap = { - [applyPatchTool.name]: applyPatchTool, [shellTool.name]: shellTool, }; @@ -39,9 +39,12 @@ export async function takeAction( let result = ""; let toolCallStatus: "success" | "error" = "success"; try { - const toolResult: { result: string; status: "success" | "error" } = + const toolResult = // @ts-expect-error tool.invoke types are weird here... - await tool.invoke(toolCall.args); + (await tool.invoke(toolCall.args)) as { + result: string; + status: "success" | "error"; + }; result = toolResult.result; toolCallStatus = toolResult.status; } catch (e) { @@ -68,7 +71,7 @@ export async function takeAction( const toolMessage = new ToolMessage({ tool_call_id: toolCall.id ?? "", - content: result, + content: truncateOutput(result), name: toolCall.name, status: toolCallStatus, }); diff --git a/apps/open-swe/src/utils/current-task.ts b/apps/open-swe/src/utils/current-task.ts new file mode 100644 index 00000000..0c2f1670 --- /dev/null +++ b/apps/open-swe/src/utils/current-task.ts @@ -0,0 +1,12 @@ +import { PlanItem } from "../types.js"; + +export function getCurrentTask(plan: PlanItem[]) { + return ( + plan.filter((p) => !p.completed).sort((a, b) => a.index - b.index)?.[0] || { + plan: "No current task found.", + index: -1, + completed: true, + summary: "", + } + ); +} diff --git a/apps/open-swe/src/utils/truncate-outputs.ts b/apps/open-swe/src/utils/truncate-outputs.ts new file mode 100644 index 00000000..b2c62680 --- /dev/null +++ b/apps/open-swe/src/utils/truncate-outputs.ts @@ -0,0 +1,36 @@ +export function truncateOutput( + output: string, + options?: { + /** + * @default 10000 + */ + numStartCharacters?: number; + + /** + * @default 10000 + */ + numEndCharacters?: number; + }, +) { + const { numStartCharacters = 10000, numEndCharacters = 10000 } = + options ?? {}; + + if (numStartCharacters < 0 || numEndCharacters < 0) { + throw new Error("numStartCharacters and numEndCharacters must be >= 0"); + } + if (!numStartCharacters && !numEndCharacters) { + throw new Error( + "At least one of numStartCharacters or numEndCharacters must be > 0", + ); + } + + if (output.length <= numStartCharacters + numEndCharacters) { + return output; + } + + return ( + output.slice(0, numStartCharacters) + + `\n... Output too long. Truncated the middle ${output.length - numStartCharacters - numEndCharacters} characters of the output ...\n` + + output.slice(-numEndCharacters) + ); +} diff --git a/apps/web/src/components/thread/index.tsx b/apps/web/src/components/thread/index.tsx index 1baeaaba..18818401 100644 --- a/apps/web/src/components/thread/index.tsx +++ b/apps/web/src/components/thread/index.tsx @@ -254,6 +254,16 @@ export function Thread() { stream.submit(undefined, { checkpoint: parentCheckpoint, streamMode: ["values"], + config: { + // TODO: Make configurable (just that target_repository object) & recursion limit in the UI. + configurable: { + target_repository: { + owner: "langchain-ai", + repo: "open-swe", + }, + }, + recursion_limit: 400, + }, }); }; diff --git a/apps/web/src/components/thread/messages/ai.tsx b/apps/web/src/components/thread/messages/ai.tsx index af5eac2c..7c23e974 100644 --- a/apps/web/src/components/thread/messages/ai.tsx +++ b/apps/web/src/components/thread/messages/ai.tsx @@ -141,19 +141,19 @@ export function AssistantMessage({ } return ( -
-
+
+
{isToolResult ? ( - <> + - + ) : ( - <> + {contentString.length > 0 && (
{contentString} @@ -161,7 +161,7 @@ export function AssistantMessage({ )} {!hideToolCalls && ( - <> + {(hasToolCalls && toolCallsHaveContents && ( )) || @@ -171,7 +171,7 @@ export function AssistantMessage({ (hasToolCalls && ( ))} - + )} {message && ( @@ -204,7 +204,7 @@ export function AssistantMessage({ handleRegenerate={() => handleRegenerate(parentCheckpoint)} />
- +
)}
diff --git a/apps/web/src/components/thread/messages/human.tsx b/apps/web/src/components/thread/messages/human.tsx index e422f92a..877608bb 100644 --- a/apps/web/src/components/thread/messages/human.tsx +++ b/apps/web/src/components/thread/messages/human.tsx @@ -119,7 +119,7 @@ export function HumanMessage({ )} {/* Render text if present, otherwise fallback to file/image name */} {contentString ? ( -

+

{contentString}

) : null} diff --git a/apps/web/src/components/thread/messages/tool-calls.tsx b/apps/web/src/components/thread/messages/tool-calls.tsx index b66e8a28..2db9c5ce 100644 --- a/apps/web/src/components/thread/messages/tool-calls.tsx +++ b/apps/web/src/components/thread/messages/tool-calls.tsx @@ -15,7 +15,7 @@ export function ToolCalls({ if (!toolCalls || toolCalls.length === 0) return null; return ( -
+
{toolCalls.map((tc, idx) => { const args = tc.args as Record; const hasArgs = Object.keys(args).length > 0;