From 5ab7b94be97bac12cf029279b15c4eddedabfa4a Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Sun, 22 Jun 2025 14:04:58 -0700 Subject: [PATCH] feat: Implement rg tool (#278) * feat: Implement rg tool * cr * cr * test running rg commands * wrap commands in script * cr * cr --- .../planner/nodes/generate-message/index.ts | 3 +- .../planner/nodes/generate-message/prompt.ts | 27 ---- .../src/graphs/planner/nodes/take-action.ts | 3 + .../nodes/generate-message/index.ts | 2 + .../nodes/generate-message/prompt.ts | 95 +------------- apps/open-swe/src/tests/sandbox.int.test.ts | 43 ++++++ apps/open-swe/src/tools/rg.ts | 124 ++++++++++++++++++ .../web/src/components/gen-ui/action-step.tsx | 40 +++++- .../web/src/components/thread/messages/ai.tsx | 46 +++++-- packages/shared/src/open-swe/tools.ts | 55 ++++++++ 10 files changed, 305 insertions(+), 133 deletions(-) create mode 100644 apps/open-swe/src/tests/sandbox.int.test.ts create mode 100644 apps/open-swe/src/tools/rg.ts diff --git a/apps/open-swe/src/graphs/planner/nodes/generate-message/index.ts b/apps/open-swe/src/graphs/planner/nodes/generate-message/index.ts index b3d57031..8395386e 100644 --- a/apps/open-swe/src/graphs/planner/nodes/generate-message/index.ts +++ b/apps/open-swe/src/graphs/planner/nodes/generate-message/index.ts @@ -16,6 +16,7 @@ import { getRepoAbsolutePath } from "@open-swe/shared/git"; import { getMissingMessages } from "../../../../utils/github/issue-messages.js"; import { filterHiddenMessages } from "../../../../utils/message/filter-hidden.js"; import { getTaskPlanFromIssue } from "../../../../utils/github/issue-task.js"; +import { createRgTool } from "../../../../tools/rg.js"; const logger = createLogger(LogLevel.INFO, "GeneratePlanningMessageNode"); @@ -43,7 +44,7 @@ export async function generateAction( config: GraphConfig, ): Promise { const model = await loadModel(config, Task.ACTION_GENERATOR); - const tools = [createShellTool(state)]; + const tools = [createRgTool(state), createShellTool(state)]; const modelWithTools = model.bindTools(tools, { tool_choice: "auto", parallel_tool_calls: true, diff --git a/apps/open-swe/src/graphs/planner/nodes/generate-message/prompt.ts b/apps/open-swe/src/graphs/planner/nodes/generate-message/prompt.ts index 35c7cda0..c8f2d8df 100644 --- a/apps/open-swe/src/graphs/planner/nodes/generate-message/prompt.ts +++ b/apps/open-swe/src/graphs/planner/nodes/generate-message/prompt.ts @@ -1,30 +1,3 @@ -export const ORIGINAL_SYSTEM_PROMPT = `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. -{FOLLOWUP_MESSAGE_PROMPT} - -You MUST adhere to the following criteria when gathering context for the plan: -- Your ONLY job is to gather context for the plan. - - You are NOT allowed to take any write/update actions, instead you must only take read actions to gather context. -- Ensure each tool call you generate is of an extremely high quality, and targeted to aid in generating a plan. -- 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. -- When calling the \`shell\` tool it is incredibly important your commands are properly formatted. You should ALWAYS remember to include proper quotes, and ensure the command is formatted correctly. -- If you determine you've gathered enough context to generate a plan, simply reply with 'done' and do NOT call any tools. -- Not generating a tool call will be interpreted as an indication that you've gathered enough context to generate a plan. -- The repo is already cloned, and located inside your current working directory: {CURRENT_WORKING_DIRECTORY} - -Below is an up to date tree of the codebase (going 3 levels deep). You should assume this is always up to date. -It was generated by using the \`tree\` command, passing in the gitignore file to ignore files and directories you should not have access to (\`git ls-files | tree --fromfile -L 3\`). -It is always executed inside the repo directory (also your current working directory): {CURRENT_WORKING_DIRECTORY} -{CODEBASE_TREE} - -Your current working directory is: {CURRENT_WORKING_DIRECTORY} - -The user's request is the first user message in the conversation below. Ensure you generate your plan in accordance with the user's request. -`; - -// The original system prompt, but refactored by Claude. -// Additional prompting & context from Anthropic's prompt -// engineering guide. export const SYSTEM_PROMPT = `You are a terminal-based agentic coding assistant built by LangChain that enables natural language interaction with local codebases. You excel at being precise, safe, and helpful in your analysis. diff --git a/apps/open-swe/src/graphs/planner/nodes/take-action.ts b/apps/open-swe/src/graphs/planner/nodes/take-action.ts index 15c83c70..587b35c0 100644 --- a/apps/open-swe/src/graphs/planner/nodes/take-action.ts +++ b/apps/open-swe/src/graphs/planner/nodes/take-action.ts @@ -9,6 +9,7 @@ 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"; +import { createRgTool } from "../../../tools/rg.js"; const logger = createLogger(LogLevel.INFO, "TakeAction"); @@ -24,8 +25,10 @@ export async function takeActions( } const shellTool = createShellTool(state); + const rgTool = createRgTool(state); const toolsMap = { [shellTool.name]: shellTool, + [rgTool.name]: rgTool, }; const toolCalls = lastMessage.tool_calls; diff --git a/apps/open-swe/src/graphs/programmer/nodes/generate-message/index.ts b/apps/open-swe/src/graphs/programmer/nodes/generate-message/index.ts index 5e108d2e..4edfb3a4 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/generate-message/index.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/generate-message/index.ts @@ -20,6 +20,7 @@ import { SYSTEM_PROMPT } from "./prompt.js"; import { getRepoAbsolutePath } from "@open-swe/shared/git"; import { getMissingMessages } from "../../../../utils/github/issue-messages.js"; import { getTaskPlanFromIssue } from "../../../../utils/github/issue-task.js"; +import { createRgTool } from "../../../../tools/rg.js"; const logger = createLogger(LogLevel.INFO, "GenerateMessageNode"); @@ -58,6 +59,7 @@ export async function generateAction( ): Promise { const model = await loadModel(config, Task.ACTION_GENERATOR); const tools = [ + createRgTool(state), createShellTool(state), createApplyPatchTool(state), createRequestHumanHelpToolFields(), diff --git a/apps/open-swe/src/graphs/programmer/nodes/generate-message/prompt.ts b/apps/open-swe/src/graphs/programmer/nodes/generate-message/prompt.ts index ebbe515c..5e48b25e 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/generate-message/prompt.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/generate-message/prompt.ts @@ -1,96 +1,3 @@ -export const ORIGINAL_SYSTEM_PROMPT = `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. - -You can: -- Receive user prompts, project context, and files. -- Stream responses and emit function calls (e.g., shell commands, code edits). -- Apply patches, run commands, and manage user approvals based on policy. -- Work inside a sandboxed, git-backed workspace with rollback support. - -You work based on a plan which was generated in a previous step. After each task in a plan is completed, a summary of the task is generated, and included in the plan list below. These messages are then removed from the conversation history, so ensure you always weigh the task summaries highly when making decisions. - -The plan tasks and summaries are as follows: -{PLAN_PROMPT_WITH_SUMMARIES} - -When you were generating this plan, you also generated a summary of the actions you took in order to come up with this plan. Ensure you use this as context about the codebase, and plan generation process. -{PLAN_GENERATION_SUMMARY} - -You are an agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user. -Only terminate your turn when you are sure that the problem is solved. - -If you are not sure about file content or codebase structure pertaining to the user's request: -First, read through the conversation history to see if you have already searched for the file or information you need. Pay extra close attention to the condensed context tool call messages in the conversation history. These contain summarized/condensed context from previously completed steps. Ensure you always read these messages to avoid duplicate work (e.g.: searching for file paths). -If you are still not sure, use your tools to read files and gather the relevant information: do NOT guess or make up an answer. - -Please resolve the user's task by editing and testing the code files in your current code execution session. You are a deployed coding agent. Your session allows for you to modify and run code. - -The repo is already cloned, and located inside {REPO_DIRECTORY} - -You must fully solve the problem for your answer to be considered correct. You are permitted to take as long as you need to complete the current task. - -You MUST adhere to the following criteria when executing the task: -- Working on the repo(s) in the current environment is allowed, even if they are proprietary. -- Analyzing code for vulnerabilities is allowed. -- Showing user code and tool call details is allowed. -- Remember to always properly format and quote your shell commands. -- Take advantage of the task summaries from completed tasks in the prompt above. Ensure you always read these summaries to avoid duplicate work, and so you always have up to date context on the codebase, and tasks you've completed. - - Each summary message will include a short description of the task it completed, how it did so, and every change it made to the codebase during this task. This section will be titled 'Repository modifications summary'. - - The summary messages may also include a section called 'Key repository insights and learnings'. This contains key insights, learnings, and facts the model discovered while completing a task. -- Additionally, you're also provided with a section titled 'Codebase tree' which contains an up to date list of files three levels deep, ignoring gitignore. -- All changes are automatically committed, so you should not worry about creating backups, or committing changes. -- Use \`apply_patch\` to edit files. This tool accepts diffs and file paths. It will then apply the given diff to the file. -- You should NOT try to create empty files with \`apply_patch\`. If you need to create a file, use the \`shell\` tool, and pass \`touch \` to create the file. -- When using the \`shell\` tool, always take advantage of the \`workdir\` parameter to run commands inside the repo directory. You should not try to generate a command with \`cd \` as passing that path to \`workdir\` is much more efficient. -- Always use the correct package manager to install dependencies. If the package manager is not already installed in the sandbox, use the \`shell\` tool to install it. - - If the package manager fails to install, or you have issues installing dependencies, do not try to use a different package manager. Instead, skip installing dependencies. -- If you are lacking enough context to complete the user's task, you may call the \`request_human_help\` tool to request help from the human. - - This tool should only be used if you have already tried to gather all the context you need, and are still unable to complete the user's task. -- If you determine your current plan is not appropriate, or you need to update/remove/add steps to your plan, you may call the \`update_plan\` tool. - - This tool should only be called to make major changes, such as removing a task, or adding new tasks. For small changes, you do not necessarily need to call this tool, and can instead just act on those small updates. - - The \`update_plan\` tool can only update/remove/add plans to the list of tasks which are not yet completed (this includes the current task). -- If completing the user's task requires writing or modifying files: - - Your code and final answer should follow these *CODING GUIDELINES*: - - Avoid writing to files which you have not already read. - - If a call to \`apply_patch\` fails, it can be helpful to re-read the file to ensure you are up to date on its content. - - Fix the problem at the root cause rather than applying surface-level patches, when possible. - - Avoid unneeded complexity in your solution. - - Ignore unrelated bugs or broken tests; it is not your responsibility to fix them. - - Update documentation as necessary. - - Keep changes consistent with the style of the existing codebase. Changes should be minimal and focused on the task. - - Use \`git log\` and \`git blame\` to search the history of the codebase if additional context is required; internet access is disabled. - - NEVER add copyright or license headers unless specifically requested. - - If creating a new file or directory plus file, always remember to create both before trying to read/write the file. Keep in mind you can not write to files which don't exist. - - You do not need to \`git commit\` your changes; this will be done automatically for you. - - If there is a .pre-commit-config.yaml, use \`pre-commit run --files ...\` to check that your changes pass the pre-commit checks. However, do not fix pre-existing errors on lines you didn't touch. - - If pre-commit doesn't work after a few retries, politely inform the user that the pre-commit setup is broken. - - Once you finish coding, you must - - Remove all inline comments you added as much as possible, even if they look normal. Check using \`git diff\`. Inline comments must be generally avoided, unless active maintainers of the repo, after long careful study of the code and the issue, will still misinterpret the code without the comments. - - Check if you accidentally add copyright or license headers. If so, remove them. - - Try to run pre-commit if it is available. - - For smaller tasks, describe in brief bullet points - - For more complex tasks, include brief high-level description, use bullet points, and include details that would be relevant to a code reviewer. -- If completing the user's task DOES NOT require writing or modifying files (e.g., the user asks a question about the code base): - - Respond in a friendly tone as a remote teammate, who is knowledgeable, capable and eager to help with coding. -- When your task involves writing or modifying files: - - Do NOT tell the user to "save the file" or "copy the code into a file" if you already created or modified the file using \`apply_patch\`. Instead, reference the file as already saved. - - Do NOT show the full contents of large files you have already written, unless the user explicitly asks for them. -- 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 an up to date tree of the codebase (going 3 levels deep). This is up to date, and is updated after every action you take. Always assume this is the most up to date context about the codebase. -It was generated by using the \`tree\` command, passing in the gitignore file to ignore files and directories you should not have access to (\`git ls-files | tree --fromfile -L 3\`). It is always executed inside the repo directory: {REPO_DIRECTORY} -{CODEBASE_TREE} - -Your current working directory is: {CURRENT_WORKING_DIRECTORY} - -Once again, here are the completed tasks, remaining tasks, and the current task you're working on: -{PLAN_PROMPT} -`; - -// The original system prompt, but refactored by Claude. -// Additional prompting & context from OpenAI's prompt -// engineering guide. export const SYSTEM_PROMPT = `# Identity You are a terminal-based agentic coding assistant built by LangChain. You wrap LLM models to enable natural language interaction with local codebases. You are precise, safe, and helpful. @@ -129,7 +36,7 @@ You are currently executing a specific task from a pre-generated plan. You have ### Tool Usage Best Practices -* **Search**: Use \`rg\` (not grep/ls -R) with glob patterns (e.g., \`rg -i pattern -g **/*.tsx\`) +* **Search**: Use the \`rg\` tool (ripgrep) (not grep/ls -R) with glob patterns (e.g., \`rg -i pattern -g **/*.tsx\`) * **Dependencies**: Use the correct package manager; skip if installation fails * **Pre-commit**: Run \`pre-commit run --files ...\` if .pre-commit-config.yaml exists * **History**: Use \`git log\` and \`git blame\` for additional context when needed diff --git a/apps/open-swe/src/tests/sandbox.int.test.ts b/apps/open-swe/src/tests/sandbox.int.test.ts new file mode 100644 index 00000000..a42c2523 --- /dev/null +++ b/apps/open-swe/src/tests/sandbox.int.test.ts @@ -0,0 +1,43 @@ +/* eslint-disable no-console */ +import { test, expect } from "@jest/globals"; +import { daytonaClient } from "../utils/sandbox.js"; +import { SNAPSHOT_NAME } from "@open-swe/shared/constants"; + +test("Can execute rg commands", async () => { + const githubToken = process.env.GITHUB_PAT; + if (!githubToken) { + throw new Error("GITHUB_PAT environment variable is not set"); + } + + const client = daytonaClient(); + + console.log("Setting up sandbox..."); + const sandbox = await client.create({ + image: SNAPSHOT_NAME, + user: "daytona", + }); + console.log("Setup sandbox:", sandbox.id); + + const repoUrlWithToken = `https://x-access-token:${githubToken}@github.com/langchain-ai/open-swe.git`; + const cloneCommand = `git clone ${repoUrlWithToken}`; + + console.log("Cloning repo..."); + const cloneRes = await sandbox.process.executeCommand( + cloneCommand, + "/home/daytona", + ); + expect(cloneRes.exitCode).toBe(0); + + const testRes = await sandbox.process.executeCommand( + `script --return --quiet -c "$(cat <<'OPEN_SWE_X' +rg -i logger +OPEN_SWE_X +)" /dev/null`, + "/home/daytona/open-swe", + ); + console.log( + `test res status: ${testRes.exitCode}\ntest res output: ${testRes.result}`, + ); + + expect(testRes.exitCode).toBe(0); +}); diff --git a/apps/open-swe/src/tools/rg.ts b/apps/open-swe/src/tools/rg.ts new file mode 100644 index 00000000..610180ca --- /dev/null +++ b/apps/open-swe/src/tools/rg.ts @@ -0,0 +1,124 @@ +import { tool } from "@langchain/core/tools"; +import { Sandbox } from "@daytonaio/sdk"; +import { GraphState } from "@open-swe/shared/open-swe/types"; +import { getCurrentTaskInput } from "@langchain/langgraph"; +import { getSandboxErrorFields } from "../utils/sandbox-error-fields.js"; +import { createLogger, LogLevel } from "../utils/logger.js"; +import { daytonaClient } from "../utils/sandbox.js"; +import { TIMEOUT_SEC } from "@open-swe/shared/constants"; +import { + createRgToolFields, + formatRgCommand, +} from "@open-swe/shared/open-swe/tools"; +import { getRepoAbsolutePath } from "@open-swe/shared/git"; + +const wrapScript = (command: string): string => { + return `script --return --quiet -c "$(cat <<'OPEN_SWE_X' +${command} +OPEN_SWE_X +)" /dev/null`; +}; + +const logger = createLogger(LogLevel.INFO, "RgTool"); + +const DEFAULT_ENV = { + // Prevents corepack from showing a y/n download prompt which causes the command to hang + COREPACK_ENABLE_DOWNLOAD_PROMPT: "0", +}; + +export function createRgTool( + state: Pick, +) { + const rgTool = tool( + async (input): Promise<{ result: string; status: "success" | "error" }> => { + let sandbox: Sandbox | undefined; + try { + const state = getCurrentTaskInput(); + const { sandboxSessionId } = state; + if (!sandboxSessionId) { + logger.error( + "FAILED TO RUN COMMAND: No sandbox session ID provided", + { + input, + }, + ); + throw new Error( + "FAILED TO RUN COMMAND: No sandbox session ID provided", + ); + } + + const repoRoot = getRepoAbsolutePath(state.targetRepository); + + sandbox = await daytonaClient().get(sandboxSessionId); + const command = formatRgCommand({ + pattern: input.pattern, + paths: input.paths, + flags: input.flags, + }); + logger.info("Running rg command", { + command: command.join(" "), + repoRoot, + }); + const response = await sandbox.process.executeCommand( + wrapScript(command.join(" ")), + repoRoot, + DEFAULT_ENV, + TIMEOUT_SEC, + ); + + let successResult = response.result; + + if ( + response.exitCode === 1 || + (response.exitCode === 127 && response.result.startsWith("sh: 1: ")) + ) { + logger.info("Exit code 1. no results found", { + ...response, + }); + successResult = `Exit code 1. No results found.\n\n${response.result}`; + } else if (response.exitCode > 1) { + logger.error("Failed to run rg command", { + error: response.result, + error_result: response, + input, + }); + throw new Error( + `Command failed. Exit code: ${response.exitCode}\nResult: ${response.result}\nStdout:\n${response.artifacts?.stdout}`, + ); + } + + return { + result: successResult, + status: "success", + }; + } catch (e) { + const errorFields = getSandboxErrorFields(e); + if (errorFields) { + logger.error("Failed to run rg command", { + input, + error: errorFields, + }); + throw new Error( + `Command failed. Exit code: ${errorFields.exitCode}\nError: ${errorFields.result ?? errorFields.artifacts?.stdout}`, + ); + } + + logger.error( + "Failed to run rg command: " + + (e instanceof Error ? e.message : "Unknown error"), + { + error: e, + input, + }, + ); + throw new Error( + "FAILED TO RUN RG COMMAND: " + + (e instanceof Error ? e.message : "Unknown error"), + ); + } + }, + createRgToolFields(state.targetRepository), + ); + + return rgTool; +} diff --git a/apps/web/src/components/gen-ui/action-step.tsx b/apps/web/src/components/gen-ui/action-step.tsx index 5b3100ae..be1c0dd4 100644 --- a/apps/web/src/components/gen-ui/action-step.tsx +++ b/apps/web/src/components/gen-ui/action-step.tsx @@ -15,6 +15,8 @@ import { import { createApplyPatchToolFields, createShellToolFields, + formatRgCommand, + RipgrepCommand, } from "@open-swe/shared/open-swe/tools"; import { z } from "zod"; @@ -50,10 +52,18 @@ type PatchActionProps = BaseActionProps & fixedDiff?: string; }; +type RgActionProps = BaseActionProps & + Partial & { + actionType: "rg"; + output?: string; + errorCode?: number; + }; + export type ActionItemProps = | (BaseActionProps & { status: "loading" }) | ShellActionProps - | PatchActionProps; + | PatchActionProps + | RgActionProps; export type ActionStepProps = { actions: ActionItemProps[]; @@ -97,6 +107,8 @@ function ActionItem(props: ActionItemProps) { return props.success ? "Command completed" : "Command failed"; } else if (props.actionType === "apply-patch") { return props.success ? "Patch applied" : "Patch failed"; + } else if (props.actionType === "rg") { + return props.success ? "Search completed" : "Search failed"; } } @@ -107,7 +119,7 @@ function ActionItem(props: ActionItemProps) { const shouldShowToggle = () => { if (props.status !== "done") return false; - if (props.actionType === "shell") { + if (props.actionType === "shell" || props.actionType === "rg") { return !!props.output; } else if (props.actionType === "apply-patch") { return !!props.diff; @@ -168,6 +180,25 @@ function ActionItem(props: ActionItemProps) { ); + } else if (props.actionType === "rg") { + let formattedRgCommand = ""; + try { + formattedRgCommand = + formatRgCommand({ + pattern: props.pattern, + paths: props.paths, + flags: props.flags, + })?.join(" ") ?? ""; + } catch { + // no-op + } + return ( +
+ + {formattedRgCommand} + +
+ ); } else { return ( @@ -183,7 +214,10 @@ function ActionItem(props: ActionItemProps) { if (!expanded) return null; - if (props.actionType === "shell" && props.output) { + if ( + (props.actionType === "shell" || props.actionType === "rg") && + props.output + ) { return (
diff --git a/apps/web/src/components/thread/messages/ai.tsx b/apps/web/src/components/thread/messages/ai.tsx
index 49d20d2f..1e398e2a 100644
--- a/apps/web/src/components/thread/messages/ai.tsx
+++ b/apps/web/src/components/thread/messages/ai.tsx
@@ -25,6 +25,7 @@ import {
   createApplyPatchToolFields,
   createShellToolFields,
   createSetTaskStatusToolFields,
+  createRgToolFields,
 } from "@open-swe/shared/open-swe/tools";
 import { z } from "zod";
 import { isAIMessageSDK, isToolMessageSDK } from "@/lib/langchain-messages";
@@ -38,6 +39,8 @@ const applyPatchTool = createApplyPatchToolFields(dummyRepo);
 type ApplyPatchToolArgs = z.infer;
 const setTaskStatusTool = createSetTaskStatusToolFields();
 type SetTaskStatusToolArgs = z.infer;
+const rgTool = createRgToolFields(dummyRepo);
+type RgToolArgs = z.infer;
 
 function CustomComponent({
   message,
@@ -134,6 +137,17 @@ export function mapToolMessageToActionStepProps(
       reasoningText,
       errorMessage: !success ? getContentString(message.content) : undefined,
     };
+  } else if (toolCall?.name === rgTool.name) {
+    const args = toolCall.args as RgToolArgs;
+    return {
+      actionType: "rg",
+      status,
+      success,
+      pattern: args.pattern || "",
+      paths: args.paths || [],
+      output: getContentString(message.content),
+      reasoningText,
+    };
   }
   return {
     status: "loading",
@@ -191,9 +205,12 @@ export function AssistantMessage({
     })
     .filter((m): m is ToolMessage => !!m);
 
-  const shellOrPatchToolCalls = message
+  const actionableToolCalls = message
     ? aiToolCalls.filter(
-        (tc) => tc.name === shellTool.name || tc.name === applyPatchTool.name,
+        (tc) =>
+          tc.name === shellTool.name ||
+          tc.name === applyPatchTool.name ||
+          tc.name === rgTool.name,
       )
     : [];
 
@@ -223,17 +240,27 @@ export function AssistantMessage({
     );
   }
 
-  if (shellOrPatchToolCalls.length > 0) {
-    const actionItems = shellOrPatchToolCalls.map((toolCall) => {
+  if (actionableToolCalls.length > 0) {
+    const actionItems = actionableToolCalls.map((toolCall): ActionItemProps => {
       const correspondingToolResult = toolResults.find(
         (tr) => tr && tr.tool_call_id === toolCall.id,
       );
 
       const isShellTool = toolCall.name === shellTool.name;
+      const isRgTool = toolCall.name === rgTool.name;
 
       if (correspondingToolResult) {
         // If we have a tool result, map it to action props
         return mapToolMessageToActionStepProps(correspondingToolResult, thread);
+      } else if (isRgTool) {
+        const args = toolCall.args as RgToolArgs;
+        return {
+          actionType: "rg",
+          status: "generating",
+          pattern: args?.pattern || "",
+          paths: args?.paths || [],
+          output: "",
+        } as ActionItemProps;
       } else {
         if (isShellTool) {
           const args = toolCall.args as ShellToolArgs;
@@ -245,12 +272,13 @@ export function AssistantMessage({
             timeout: args?.timeout,
           } as ActionItemProps;
         } else {
-          const args = toolCall.args as ApplyPatchToolArgs;
+          // Must be apply_patch tool
+          const patchArgs = toolCall.args as ApplyPatchToolArgs;
           return {
             actionType: "apply-patch",
             status: "generating",
-            file_path: args?.file_path || "",
-            diff: args?.diff || "",
+            file_path: patchArgs?.file_path || "",
+            diff: patchArgs?.diff || "",
           } as ActionItemProps;
         }
       }
@@ -259,7 +287,9 @@ export function AssistantMessage({
     return (
       
item !== undefined, + )} reasoningText={contentString} />
diff --git a/packages/shared/src/open-swe/tools.ts b/packages/shared/src/open-swe/tools.ts index 9cf04ddd..17bc96b9 100644 --- a/packages/shared/src/open-swe/tools.ts +++ b/packages/shared/src/open-swe/tools.ts @@ -101,6 +101,61 @@ export function createUpdatePlanToolFields() { }; } +export function createRgToolFields(targetRepository: TargetRepository) { + const repoRoot = getRepoAbsolutePath(targetRepository); + // Main ripgrep command schema + const ripgrepCommandSchema = z.object({ + pattern: z + .string() + .optional() + .describe( + "The search pattern (regex). Leave empty when using flags like --files or --type-list", + ), + + paths: z + .array(z.string()) + .optional() + .describe( + "Files or directories to search. If empty, searches current directory", + ), + + flags: z + .array(z.string()) + .optional() + .describe( + 'Array of flags with their values. Examples: ["-i", "--type=rust", "-A", "3", "--files"]. Short flags like -i can be standalone, flags with values can be separate strings or use = for long flags', + ), + }); + + return { + name: "rg", + schema: ripgrepCommandSchema, + description: `Call this tool to run the rg command (ripgrep). This should ONLY be called if you want to search for files in the repository. The working directory this command will be executed in is \`${repoRoot}\`.`, + }; +} + +// Only used for type inference +const _tmpRgToolSchema = createRgToolFields({ owner: "x", repo: "x" }).schema; +export type RipgrepCommand = z.infer; + +export function formatRgCommand(cmd: RipgrepCommand): string[] { + const args = ["rg"]; + + if (cmd.flags) { + args.push(...cmd.flags); + } + + if (cmd.pattern) { + args.push(cmd.pattern); + } + + if (cmd.paths) { + args.push(...cmd.paths); + } + + return args; +} + export function createSetTaskStatusToolFields() { const setTaskStatusToolSchema = z.object({ reasoning: z