diff --git a/apps/docs/setup/monorepo.mdx b/apps/docs/setup/monorepo.mdx index 6933bd35..785a321f 100644 --- a/apps/docs/setup/monorepo.mdx +++ b/apps/docs/setup/monorepo.mdx @@ -89,7 +89,7 @@ Run these commands from the repository root: ```json { "resolutions": { - "@langchain/langgraph-sdk": "^0.0.92", + "@langchain/langgraph-sdk": "^0.0.95", "@langchain/core": "^0.3.58" } } diff --git a/apps/open-swe/package.json b/apps/open-swe/package.json index 7df339cd..1d0b5f83 100644 --- a/apps/open-swe/package.json +++ b/apps/open-swe/package.json @@ -29,7 +29,7 @@ "@langchain/core": "^0.3.56", "@langchain/google-genai": "^0.2.9", "@langchain/langgraph": "^0.3.8", - "@langchain/langgraph-sdk": "^0.0.92", + "@langchain/langgraph-sdk": "^0.0.95", "@langchain/mcp-adapters": "^0.5.2", "@langchain/openai": "^0.5.10", "@mendable/firecrawl-js": "^1.29.1", @@ -49,7 +49,7 @@ "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.19.0", "@jest/globals": "^29.7.0", - "@langchain/langgraph-cli": "latest", + "@langchain/langgraph-cli": "^0.0.47", "@tsconfig/recommended": "^1.0.8", "@types/jest": "^29.5.0", "@types/node": "^22.13.5", diff --git a/apps/open-swe/src/__tests__/tokens.test.ts b/apps/open-swe/src/__tests__/tokens.test.ts new file mode 100644 index 00000000..5532a393 --- /dev/null +++ b/apps/open-swe/src/__tests__/tokens.test.ts @@ -0,0 +1,257 @@ +import { describe, it, expect } from "@jest/globals"; +import { AIMessage, HumanMessage, ToolMessage } from "@langchain/core/messages"; +import { + calculateConversationHistoryTokenCount, + getMessagesSinceLastSummary, + MAX_INTERNAL_TOKENS, +} from "../utils/tokens.js"; + +describe("calculateConversationHistoryTokenCount", () => { + it("should return 0 for empty messages array", () => { + const result = calculateConversationHistoryTokenCount([]); + expect(result).toBe(0); + }); + + it("should calculate token count for human messages", () => { + const messages = [ + new HumanMessage({ + content: "This is a test message with exactly 10 words in it.", + }), + ]; + + // 10 words, approximately 13 tokens, ~52 characters + // Since we estimate 1 token per 4 characters, this should be around 13 tokens + const result = calculateConversationHistoryTokenCount(messages); + expect(result).toBe(13); + }); + + it("should calculate token count for AI messages with usage metadata", () => { + const messages = [ + new AIMessage({ + content: "AI response", + usage_metadata: { + input_tokens: 10, + output_tokens: 10, + total_tokens: 20, + }, + }), + ]; + + const result = calculateConversationHistoryTokenCount(messages); + expect(result).toBe(20); + }); + + it("should calculate token count for AI messages without usage metadata", () => { + const messages = [ + new AIMessage({ + content: "This is an AI response with no usage metadata.", + }), + ]; + + // ~12 words, approximately 12 tokens, ~48 characters + // Since we estimate 1 token per 4 characters, this should be around 12 tokens + const result = calculateConversationHistoryTokenCount(messages); + expect(result).toBe(12); + }); + + it("should calculate token count for AI messages with tool calls", () => { + const messages = [ + new AIMessage({ + content: "Using a tool", + tool_calls: [ + { + name: "calculator", + args: { a: 1, b: 2 }, + }, + ], + }), + ]; + + // Content: "Using a tool" (~3 tokens) + // Tool name: "calculator" (~2 tokens) + // Args: JSON.stringify({a:1,b:2}) (~3 tokens) + // Total: ~8 tokens + const result = calculateConversationHistoryTokenCount(messages); + expect(result).toBeGreaterThan(0); + }); + + it("should calculate token count for tool messages", () => { + const messages = [ + new ToolMessage({ + content: "Result of tool execution with some data.", + tool_call_id: "tool-1", + name: "tool", + }), + ]; + + // ~8 words, approximately 10 tokens, ~40 characters + const result = calculateConversationHistoryTokenCount(messages); + expect(result).toBe(10); + }); + + it("should exclude hidden messages when option is provided", () => { + const messages = [ + new HumanMessage({ + content: "Visible message", + }), + new HumanMessage({ + content: "Hidden message", + additional_kwargs: { hidden: true }, + }), + ]; + + const resultWithoutOption = + calculateConversationHistoryTokenCount(messages); + const resultWithOption = calculateConversationHistoryTokenCount(messages, { + excludeHiddenMessages: true, + }); + + expect(resultWithoutOption).toBeGreaterThan(resultWithOption); + expect(resultWithOption).toBe(4); // "Visible message" is ~4 tokens + }); + + it("should exclude messages from the end when option is provided", () => { + const messages = [ + new HumanMessage({ content: "First message" }), + new HumanMessage({ content: "Second message" }), + new HumanMessage({ content: "Third message" }), + ]; + + const resultWithoutOption = + calculateConversationHistoryTokenCount(messages); + const resultWithOption = calculateConversationHistoryTokenCount(messages, { + excludeCountFromEnd: 1, + }); + + expect(resultWithoutOption).toBeGreaterThan(resultWithOption); + // First two messages should be ~7 tokens + expect(resultWithOption).toBe(7); + }); +}); + +describe("getMessagesSinceLastSummary", () => { + it("should return all messages when there is no summary message", () => { + const messages = [ + new HumanMessage({ content: "Message 1" }), + new AIMessage({ content: "Message 2" }), + new HumanMessage({ content: "Message 3" }), + ]; + + const result = getMessagesSinceLastSummary(messages); + expect(result).toHaveLength(3); + expect(result).toEqual(messages); + }); + + it("should return messages after the last summary message", () => { + const summaryMessage = new AIMessage({ + content: "Summary of conversation", + additional_kwargs: { summary_message: true }, + }); + + const messages = [ + new HumanMessage({ content: "Message 1" }), + summaryMessage, + new HumanMessage({ content: "Message 3" }), + new AIMessage({ content: "Message 4" }), + ]; + + const result = getMessagesSinceLastSummary(messages); + expect(result).toHaveLength(2); + expect(result[0].content).toBe("Message 3"); + expect(result[1].content).toBe("Message 4"); + }); + + it("should exclude hidden messages when option is provided", () => { + const summaryMessage = new AIMessage({ + content: "Summary of conversation", + additional_kwargs: { summary_message: true }, + }); + + const messages = [ + summaryMessage, + new HumanMessage({ content: "Visible message" }), + new HumanMessage({ + content: "Hidden message", + additional_kwargs: { hidden: true }, + }), + new AIMessage({ content: "Another visible message" }), + ]; + + const result = getMessagesSinceLastSummary(messages, { + excludeHiddenMessages: true, + }); + + expect(result).toHaveLength(2); + expect(result[0].content).toBe("Visible message"); + expect(result[1].content).toBe("Another visible message"); + }); + + it("should exclude messages from the end when option is provided", () => { + const summaryMessage = new AIMessage({ + content: "Summary of conversation", + additional_kwargs: { summary_message: true }, + }); + + const messages = [ + summaryMessage, + new HumanMessage({ content: "Message 1" }), + new AIMessage({ content: "Message 2" }), + new HumanMessage({ content: "Message 3" }), + ]; + + const result = getMessagesSinceLastSummary(messages, { + excludeCountFromEnd: 1, + }); + + expect(result).toHaveLength(2); + expect(result[0].content).toBe("Message 1"); + expect(result[1].content).toBe("Message 2"); + }); + + it("should handle both excludeHiddenMessages and excludeCountFromEnd options", () => { + const summaryMessage = new AIMessage({ + content: "Summary of conversation", + additional_kwargs: { summary_message: true }, + }); + + const messages = [ + summaryMessage, + new HumanMessage({ content: "Message 1" }), + new HumanMessage({ + content: "Hidden message", + additional_kwargs: { hidden: true }, + }), + new AIMessage({ content: "Message 3" }), + new HumanMessage({ content: "Message 4" }), + ]; + + const result = getMessagesSinceLastSummary(messages, { + excludeHiddenMessages: true, + excludeCountFromEnd: 1, + }); + + expect(result).toHaveLength(2); + expect(result[0].content).toBe("Message 1"); + expect(result[1].content).toBe("Message 3"); + }); + + it("should return empty array if all messages are before the summary", () => { + const messages = [ + new HumanMessage({ content: "Message 1" }), + new AIMessage({ content: "Message 2" }), + new AIMessage({ + content: "Summary of conversation", + additional_kwargs: { summary_message: true }, + }), + ]; + + const result = getMessagesSinceLastSummary(messages); + expect(result).toHaveLength(0); + }); +}); + +describe("MAX_INTERNAL_TOKENS constant", () => { + it("should be defined as 60,000", () => { + expect(MAX_INTERNAL_TOKENS).toBe(60_000); + }); +}); 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 2a183ec2..55d3b77b 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 @@ -23,7 +23,7 @@ Your sole objective in this phase is to gather comprehensive context about the c - Use \`search\` tool for all file searches. The \`search\` tool allows for efficient simple and complex searches, and it respect .gitignore patterns. - It's significantly faster results than alternatives like grep or ls -R. - When searching for specific file types, use glob patterns - - The pattern field supports both basic strings, and regex + - The query field supports both basic strings, and regex - Always use the \`search\` tools instead calling \`grep\` via the \`shell\` tool. You should NEVER call \`grep\` as the same functionality is better provided by \`search\`. - If the user passes a URL, you should use the \`get_url_content\` tool to fetch the contents of the URL. - You should only use this tool to fetch the contents of a URL the user has provided, or that you've discovered during your context searching, which you believe is vital to gathering context for the user's request. 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 f4458c4f..d2b972ec 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 @@ -59,7 +59,7 @@ You are currently executing a specific task from a pre-generated plan. You have * **Search**: Use \`search\` tool for all file searches. The \`search\` tool allows for efficient simple and complex searches, and it respect .gitignore patterns. * It's significantly faster results than alternatives like grep or ls -R. * When searching for specific file types, use glob patterns - * The pattern field supports both basic strings, and regex + * The query field supports both basic strings, and regex * **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/graphs/programmer/nodes/progress-plan-step.ts b/apps/open-swe/src/graphs/programmer/nodes/progress-plan-step.ts index ca8b575c..9a5f371f 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/progress-plan-step.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/progress-plan-step.ts @@ -117,6 +117,11 @@ Once you've determined the status of the current task, call either the \`mark_ta const totalInternalTokenCount = calculateConversationHistoryTokenCount( state.internalMessages, + { + // Retain the last 20 messages from state + excludeHiddenMessages: true, + excludeCountFromEnd: 20, + }, ); if (!isCompleted) { diff --git a/apps/open-swe/src/graphs/programmer/nodes/summarize-history.ts b/apps/open-swe/src/graphs/programmer/nodes/summarize-history.ts index f5bf2387..ac94c9bf 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/summarize-history.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/summarize-history.ts @@ -178,6 +178,10 @@ export async function summarizeHistory( const plan = getActivePlanItems(state.taskPlan); const conversationHistoryToSummarize = getMessagesSinceLastSummary( state.internalMessages, + { + excludeHiddenMessages: true, + excludeCountFromEnd: 20, + }, ); logger.info( diff --git a/apps/open-swe/src/graphs/reviewer/nodes/generate-review-actions/prompt.ts b/apps/open-swe/src/graphs/reviewer/nodes/generate-review-actions/prompt.ts index 0ae08847..c8c13600 100644 --- a/apps/open-swe/src/graphs/reviewer/nodes/generate-review-actions/prompt.ts +++ b/apps/open-swe/src/graphs/reviewer/nodes/generate-review-actions/prompt.ts @@ -33,12 +33,14 @@ By reviewing these actions, and comparing them to the plan and original user req 3. **Use git commands to gather context**: Below you're provided with a section '', which lists all of the files that were modified/created/deleted in the current branch. - Ensure you use this, paired with commands such as 'git diff {BASE_BRANCH_NAME} ' to inspect a diff of a file to gather context about the changes made by the Programmer Assistant. -3. **Gather all of the context necessary**: Ensure you gather all of the context necessary to provide a review of the changes made by the Programmer Assistant. +3. **Only search for what is necessary**: Ensure you gather all of the context necessary to provide a review of the changes made by the Programmer Assistant. + - Ensure that the actions you perform in this review phase are only the most necessary and targeted actions to gather context. + - Avoid rabbit holes for gathering context. You should always first consider whether or not the action you're about to take is necessary to generate a review for the user's request. If it is not, do not take it. 4. **Leverage \`search\` tool**: Use \`search\` tool for all file searches. The \`search\` tool allows for efficient simple and complex searches, and it respect .gitignore patterns. - It's significantly faster results than alternatives like grep or ls -R. - When searching for specific file types, use glob patterns - - The pattern field supports both basic strings, and regex + - The query field supports both basic strings, and regex 5. **Format shell commands precisely**: Ensure all shell commands include proper quoting and escaping. Well-formatted commands prevent errors and provide reliable results. diff --git a/apps/open-swe/src/graphs/reviewer/nodes/initialize-state.ts b/apps/open-swe/src/graphs/reviewer/nodes/initialize-state.ts index 1583f94c..29d7276d 100644 --- a/apps/open-swe/src/graphs/reviewer/nodes/initialize-state.ts +++ b/apps/open-swe/src/graphs/reviewer/nodes/initialize-state.ts @@ -6,9 +6,43 @@ import { getSandboxWithErrorHandling } from "../../../utils/sandbox.js"; import { getRepoAbsolutePath } from "@open-swe/shared/git"; import { createLogger, LogLevel } from "../../../utils/logger.js"; import { GraphConfig } from "@open-swe/shared/open-swe/types"; +import { AIMessage, ToolMessage } from "@langchain/core/messages"; +import { v4 as uuidv4 } from "uuid"; +import { createReviewStartedToolFields } from "@open-swe/shared/open-swe/tools"; const logger = createLogger(LogLevel.INFO, "InitializeStateNode"); +function createReviewStartedMessage() { + const reviewStartedTool = createReviewStartedToolFields(); + const toolCallId = uuidv4(); + const reviewStartedToolCall = { + id: toolCallId, + name: reviewStartedTool.name, + args: { + review_started: true, + }, + }; + + return [ + new AIMessage({ + id: uuidv4(), + content: "", + additional_kwargs: { + hidden: true, + }, + tool_calls: [reviewStartedToolCall], + }), + new ToolMessage({ + id: uuidv4(), + tool_call_id: toolCallId, + content: "", + additional_kwargs: { + hidden: true, + }, + }), + ]; +} + export async function initializeState( state: ReviewerGraphState, config: GraphConfig, @@ -54,6 +88,7 @@ export async function initializeState( return { baseBranchName, changedFiles, + messages: createReviewStartedMessage(), ...(codebaseTree ? { codebaseTree } : {}), ...(dependenciesInstalled !== null ? { dependenciesInstalled } : {}), }; diff --git a/apps/open-swe/src/utils/tokens.ts b/apps/open-swe/src/utils/tokens.ts index 81893fbb..38994f04 100644 --- a/apps/open-swe/src/utils/tokens.ts +++ b/apps/open-swe/src/utils/tokens.ts @@ -6,26 +6,43 @@ import { } from "@langchain/core/messages"; import { getMessageContentString } from "@open-swe/shared/messages"; -// After 100k tokens, summarize the conversation history. -export const MAX_INTERNAL_TOKENS = 100_000; +// After 60k tokens, summarize the conversation history. +export const MAX_INTERNAL_TOKENS = 60_000; export function calculateConversationHistoryTokenCount( messages: BaseMessage[], + options?: { + excludeHiddenMessages?: boolean; + excludeCountFromEnd?: number; + }, ) { let totalChars = 0; - messages.forEach((m) => { - if (isAIMessage(m)) { - const contentString = getMessageContentString(m.content); - totalChars += contentString.length; - m.tool_calls?.forEach((tc) => { - totalChars += tc.name.length; - totalChars += JSON.stringify(tc.args).length; - }); + const messagesToCount = options?.excludeCountFromEnd + ? messages.slice(0, -options.excludeCountFromEnd) + : messages; + messagesToCount.forEach((m) => { + if (options?.excludeHiddenMessages && m.additional_kwargs?.hidden) { + return; } if (isHumanMessage(m) || isToolMessage(m)) { const contentString = getMessageContentString(m.content); totalChars += contentString.length; } + + if (isAIMessage(m)) { + const usageMetadata = m.usage_metadata; + if (usageMetadata) { + // multiply by 4 here since we divide by 4 to estimate tokens. + totalChars += usageMetadata.total_tokens * 4; + } else { + const contentString = getMessageContentString(m.content); + totalChars += contentString.length; + m.tool_calls?.forEach((tc) => { + totalChars += tc.name.length; + totalChars += JSON.stringify(tc.args).length; + }); + } + } }); // Estimate 1 token for every 4 characters. @@ -34,9 +51,39 @@ export function calculateConversationHistoryTokenCount( export function getMessagesSinceLastSummary( messages: BaseMessage[], + options?: { + excludeHiddenMessages?: boolean; + excludeCountFromEnd?: number; + }, ): BaseMessage[] { - const allMessagesAfterLastSummary = messages.slice( - messages.findIndex((m) => m.additional_kwargs?.summary_message), + // Find the index of the last summary message + const lastSummaryIndex = messages.findIndex( + (m) => m.additional_kwargs?.summary_message, ); - return allMessagesAfterLastSummary; + + // Get all messages after the last summary message + let messagesAfterLastSummary = + lastSummaryIndex >= 0 + ? messages.slice(lastSummaryIndex + 1) + : [...messages]; + + // Apply excludeHiddenMessages option if provided + if (options?.excludeHiddenMessages) { + messagesAfterLastSummary = messagesAfterLastSummary.filter( + (m) => !m.additional_kwargs?.hidden, + ); + } + + // Apply excludeCountFromEnd option if provided + if (options?.excludeCountFromEnd && options.excludeCountFromEnd > 0) { + messagesAfterLastSummary = messagesAfterLastSummary.slice( + 0, + Math.max( + 0, + messagesAfterLastSummary.length - options.excludeCountFromEnd, + ), + ); + } + + return messagesAfterLastSummary; } diff --git a/apps/web/package.json b/apps/web/package.json index a2450d75..fa5a1e50 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,7 +21,7 @@ "dependencies": { "@langchain/core": "^0.3.57", "@langchain/langgraph": "^0.3.8", - "@langchain/langgraph-sdk": "^0.0.92", + "@langchain/langgraph-sdk": "^0.0.95", "@octokit/app": "^16.0.1", "@open-swe/shared": "*", "@radix-ui/react-alert-dialog": "^1.1.14", diff --git a/apps/web/src/app/(v2)/chat/threads/page.tsx b/apps/web/src/app/(v2)/chat/threads/page.tsx index 685e5897..d907b112 100644 --- a/apps/web/src/app/(v2)/chat/threads/page.tsx +++ b/apps/web/src/app/(v2)/chat/threads/page.tsx @@ -15,6 +15,7 @@ import { ThemeToggle } from "@/components/theme-toggle"; import { InstallationSelector } from "@/components/github/installation-selector"; import { GitHubAppProvider } from "@/providers/GitHubApp"; import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants"; +import { cn } from "@/lib/utils"; type FilterStatus = "all" | "running" | "completed" | "failed" | "pending"; @@ -120,11 +121,12 @@ function AllThreadsPageContent() { key={status} variant={statusFilter === status ? "secondary" : "ghost"} size="sm" - className={`h-7 text-xs ${ + className={cn( + "h-7 text-xs", statusFilter === status ? "bg-muted text-foreground dark:bg-gray-700" - : "text-muted-foreground hover:bg-muted hover:text-foreground" - }`} + : "text-muted-foreground hover:bg-muted hover:text-foreground", + )} onClick={() => setStatusFilter(status)} > {status === "all" diff --git a/apps/web/src/components/gen-ui/action-step.tsx b/apps/web/src/components/gen-ui/action-step.tsx index d05777d8..40d41f12 100644 --- a/apps/web/src/components/gen-ui/action-step.tsx +++ b/apps/web/src/components/gen-ui/action-step.tsx @@ -352,11 +352,11 @@ function ActionItem(props: ActionItemProps) {
- {castProps.pattern} + {castProps.query}
- {castProps.regex && ( + {!castProps.match_string && ( regex diff --git a/apps/web/src/components/gen-ui/code-review-started.tsx b/apps/web/src/components/gen-ui/code-review-started.tsx new file mode 100644 index 00000000..7ea91f5b --- /dev/null +++ b/apps/web/src/components/gen-ui/code-review-started.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { Sparkles } from "lucide-react"; +import { Badge } from "../ui/badge"; +import { Check } from "lucide-react"; +import { cn } from "@/lib/utils"; + +type CodeReviewStartedProps = { + status?: "generating" | "done"; +}; + +export function CodeReviewStarted({ status = "done" }: CodeReviewStartedProps) { + return ( +
+ {/* Header */} +
+
+ +
+ +
+
+

+ Code review started +

+ + + In Progress + +
+

+ Analyzing code for best practices and potential improvements +

+
+
+
+ ); +} diff --git a/apps/web/src/components/gen-ui/task-review.tsx b/apps/web/src/components/gen-ui/task-review.tsx index 2f44a97b..84df9505 100644 --- a/apps/web/src/components/gen-ui/task-review.tsx +++ b/apps/web/src/components/gen-ui/task-review.tsx @@ -11,6 +11,7 @@ import { FileText, } from "lucide-react"; import { BasicMarkdownText } from "../thread/markdown-text"; +import { cn } from "@/lib/utils"; type MarkTaskCompletedProps = { status: "loading" | "generating" | "done"; @@ -77,9 +78,10 @@ export function MarkTaskCompleted({ )}
setExpanded((prev) => !prev) @@ -200,11 +202,12 @@ export function MarkTaskIncomplete({ )}
setExpanded((prev) => !prev) diff --git a/apps/web/src/components/gen-ui/task-summary.tsx b/apps/web/src/components/gen-ui/task-summary.tsx index 2aa02857..4eefe168 100644 --- a/apps/web/src/components/gen-ui/task-summary.tsx +++ b/apps/web/src/components/gen-ui/task-summary.tsx @@ -74,19 +74,21 @@ export function TaskSummary({ {summaryText && status === "done" && (
{displayThread.title} diff --git a/package.json b/package.json index ad58a49c..c55d7350 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "typescript": "^5" }, "resolutions": { - "@langchain/langgraph-sdk": "^0.0.92", + "@langchain/langgraph-sdk": "^0.0.95", "@langchain/core": "^0.3.58" }, "packageManager": "yarn@3.5.1" diff --git a/packages/shared/package.json b/packages/shared/package.json index 74959ebd..e5557919 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -19,7 +19,7 @@ "dependencies": { "@langchain/core": "^0.3.56", "@langchain/langgraph": "^0.3.8", - "@langchain/langgraph-sdk": "^0.0.92", + "@langchain/langgraph-sdk": "^0.0.95", "@octokit/rest": "^22.0.0", "zod": "^3.25.32" }, diff --git a/packages/shared/src/open-swe/tools.ts b/packages/shared/src/open-swe/tools.ts index b9ae1da2..98dc3b06 100644 --- a/packages/shared/src/open-swe/tools.ts +++ b/packages/shared/src/open-swe/tools.ts @@ -109,15 +109,18 @@ export function createUpdatePlanToolFields() { export function createSearchToolFields(targetRepository: TargetRepository) { const repoRoot = getRepoAbsolutePath(targetRepository); const searchSchema = z.object({ - pattern: z + query: z .string() - .describe("The string or regex to search the codebase for."), - regex: z + .describe( + "The string or regex to search the codebase for. If passing a plain string, ensure to also set the 'match_string' field to true. If passing a regex, ensure to also set the 'match_string' field to false.", + ), + + match_string: z .boolean() .optional() .default(false) .describe( - "Whether or not to treat the pattern as a regex. Defaults to false.", + "Whether or not to treat the query as a fixed string to search for. If true, it will search for results which match the query exactly. If false, the query will be treated as a regex. Defaults to false.", ), case_sensitive: z @@ -165,7 +168,7 @@ export function createSearchToolFields(targetRepository: TargetRepository) { return { name: "search", schema: searchSchema, - description: `Execute a search in the repository. The working directory this command will be executed in is \`${repoRoot}\`.`, + description: `Execute a search in the repository. Should be used to search for content via string matching or regex in the codebase. The working directory this command will be executed in is \`${repoRoot}\`.`, }; } @@ -203,7 +206,7 @@ export function formatSearchCommand( } // Regex vs fixed string - if (!cmd.regex) { + if (cmd.match_string) { args.push("--fixed-strings"); } @@ -212,19 +215,25 @@ export function formatSearchCommand( args.push(`-C`, String(cmd.context_lines)); } - // File globs + // File globs - use ripgrep's glob handling instead of shell expansion if (cmd.include_files) { - args.push("--glob", cmd.include_files); + // Quote the glob pattern to prevent shell interpretation + args.push("--glob", escapeShellArg(cmd.include_files)); } if (cmd.exclude_files) { - args.push("--glob", `!${cmd.exclude_files}`); + // Quote the exclude pattern to prevent shell interpretation + args.push("--glob", escapeShellArg(`!${cmd.exclude_files}`)); } // File types if (cmd.file_types && cmd.file_types.length > 0) { + // Process each file type individually to avoid glob expansion issues for (const ext of cmd.file_types) { - args.push("--glob", `**/*${ext}`); + // Normalize extension format (ensure it has a leading dot) + const normalizedExt = ext.startsWith(".") ? ext : `.${ext}`; + // Quote the glob pattern to prevent shell interpretation + args.push("--glob", escapeShellArg(`**/*${normalizedExt}`)); } } @@ -238,9 +247,10 @@ export function formatSearchCommand( args.push("--max-count", String(cmd.max_results)); } - // The pattern (must come after flags) - if (cmd.pattern) { - args.push(escapeShellArg(cmd.pattern)); + // The query (must come after path for ripgrep to interpret it correctly) + if (cmd.query) { + // Double-quote the pattern to ensure it's treated as a pattern and not a path + args.push(escapeShellArg(cmd.query)); } return args; @@ -449,3 +459,16 @@ export function createCodeReviewMarkTaskNotCompleteFields() { "Use this tool to mark a task as not complete. This should be called if you determine that the task has not been successfully completed, and you have additional tasks the programmer should take to successfully complete the task.", }; } + +export function createReviewStartedToolFields() { + const reviewStartedSchema = z.object({ + review_started: z.boolean(), + }); + + return { + name: "review_started", + description: + "", + schema: reviewStartedSchema, + }; +} diff --git a/yarn.lock b/yarn.lock index 41af88f7..db636de2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2483,14 +2483,14 @@ __metadata: languageName: node linkType: hard -"@langchain/langgraph-api@npm:0.0.45": - version: 0.0.45 - resolution: "@langchain/langgraph-api@npm:0.0.45" +"@langchain/langgraph-api@npm:0.0.47": + version: 0.0.47 + resolution: "@langchain/langgraph-api@npm:0.0.47" dependencies: "@babel/code-frame": ^7.26.2 "@hono/node-server": ^1.12.0 "@hono/zod-validator": ^0.2.2 - "@langchain/langgraph-ui": 0.0.45 + "@langchain/langgraph-ui": 0.0.47 "@types/json-schema": ^7.0.15 "@typescript/vfs": ^1.6.0 dedent: ^1.5.3 @@ -2516,7 +2516,7 @@ __metadata: peerDependenciesMeta: "@langchain/langgraph-sdk": optional: true - checksum: ceddae262fe3e9064820dfb0faf53af771c29eb9b7e0a1862e2982666dadcc5e329ce0c286af171315135c963c5c56c4bd49c519edb745c8a1c40ab1187081c5 + checksum: 9d2ff14cc9fc360e200ea9ad134701593f2f717f49409155d7b3e42f8dd3cdda4736a75a31ffe4be4d18cd4b5cce1673098778d338a50dcf75d44ddc88a7e802 languageName: node linkType: hard @@ -2531,13 +2531,13 @@ __metadata: languageName: node linkType: hard -"@langchain/langgraph-cli@npm:latest": - version: 0.0.45 - resolution: "@langchain/langgraph-cli@npm:0.0.45" +"@langchain/langgraph-cli@npm:^0.0.47": + version: 0.0.47 + resolution: "@langchain/langgraph-cli@npm:0.0.47" dependencies: "@babel/code-frame": ^7.26.2 "@commander-js/extra-typings": ^13.0.0 - "@langchain/langgraph-api": 0.0.45 + "@langchain/langgraph-api": 0.0.47 chokidar: ^4.0.3 commander: ^13.0.0 dedent: ^1.5.3 @@ -2555,13 +2555,13 @@ __metadata: zod: ^3.23.8 bin: langgraphjs: dist/cli/cli.mjs - checksum: 5136009dbf8b2ba078be7dfe421f3a9d9b1c86ac52503e26d2d1b667ab8fea677f9596da5f5fced4191c1188861390ed11d2329c907cdb917fb86dc274d361ec + checksum: 159bac73345e98821e84c415faabb3e936b304ca51967017db4acd29090b9a631ca5bf3129a50caf21e19a57e8886144a16571f7e7049864414abaacb625d563 languageName: node linkType: hard -"@langchain/langgraph-sdk@npm:^0.0.92": - version: 0.0.92 - resolution: "@langchain/langgraph-sdk@npm:0.0.92" +"@langchain/langgraph-sdk@npm:^0.0.95": + version: 0.0.95 + resolution: "@langchain/langgraph-sdk@npm:0.0.95" dependencies: "@types/json-schema": ^7.0.15 p-queue: ^6.6.2 @@ -2578,13 +2578,13 @@ __metadata: optional: true react-dom: optional: true - checksum: 905380f0785da27dd5638b37e6215f3f2655cd0cb9111f6178e4b72085e930f1e250dfbfa6bb3b0ce4b7dc7f129f988095fac2240b420c09f63d65c3a8d5b8d6 + checksum: a8eaf39a3f69bf7fd3645bdaec4869ced7916427669cf1c8480dc8ee4efbc42b2c6431deedd41d02f977de3305832713d3bb894307246f15ba06562fb8384bd3 languageName: node linkType: hard -"@langchain/langgraph-ui@npm:0.0.45": - version: 0.0.45 - resolution: "@langchain/langgraph-ui@npm:0.0.45" +"@langchain/langgraph-ui@npm:0.0.47": + version: 0.0.47 + resolution: "@langchain/langgraph-ui@npm:0.0.47" dependencies: "@commander-js/extra-typings": ^13.0.0 commander: ^13.0.0 @@ -2593,7 +2593,7 @@ __metadata: zod: ^3.23.8 bin: langgraphjs-ui: ./dist/cli.mjs - checksum: e48c0aa52f898765a240f185318fe18668594a67eb0c059b99adf22e46c9f4a3a217c6696533df6c755863ff2c79925753bd17e5c98d73a865e84f933db81939 + checksum: cdacbba249ac6ce6ea07104b57c0c73bea7332be3d7a2f63b63b386bf5d1a4112acf7a3adc53b64fe7f10999756879b5aac90c6c8ad91bb05e0cd57e36710afc languageName: node linkType: hard @@ -3462,8 +3462,8 @@ __metadata: "@langchain/core": ^0.3.56 "@langchain/google-genai": ^0.2.9 "@langchain/langgraph": ^0.3.8 - "@langchain/langgraph-cli": latest - "@langchain/langgraph-sdk": ^0.0.92 + "@langchain/langgraph-cli": ^0.0.47 + "@langchain/langgraph-sdk": ^0.0.95 "@langchain/mcp-adapters": ^0.5.2 "@langchain/openai": ^0.5.10 "@mendable/firecrawl-js": ^1.29.1 @@ -3513,7 +3513,7 @@ __metadata: "@eslint/js": ^9.19.0 "@langchain/core": ^0.3.56 "@langchain/langgraph": ^0.3.8 - "@langchain/langgraph-sdk": ^0.0.92 + "@langchain/langgraph-sdk": ^0.0.95 "@octokit/rest": ^22.0.0 "@octokit/types": ^12.0.0 "@tsconfig/recommended": ^1.0.8 @@ -3538,7 +3538,7 @@ __metadata: "@eslint/js": ^9.19.0 "@langchain/core": ^0.3.57 "@langchain/langgraph": ^0.3.8 - "@langchain/langgraph-sdk": ^0.0.92 + "@langchain/langgraph-sdk": ^0.0.95 "@octokit/app": ^16.0.1 "@octokit/types": ^14.1.0 "@open-swe/shared": "*"