fix: A handful of small fixes (#408)

This commit is contained in:
Brace Sproul 2025-07-14 20:01:27 -07:00 • committed by GitHub
parent 78f38503e0
commit a1d9b88d69
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 556 additions and 92 deletions

View file

@ -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"
}
}

View file

@ -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",

View file

@ -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);
});
});

View file

@ -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.

View file

@ -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

View file

@ -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) {

View file

@ -178,6 +178,10 @@ export async function summarizeHistory(
const plan = getActivePlanItems(state.taskPlan);
const conversationHistoryToSummarize = getMessagesSinceLastSummary(
state.internalMessages,
{
excludeHiddenMessages: true,
excludeCountFromEnd: 20,
},
);
logger.info(

View file

@ -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 '<changed_files>', 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} <file_path>' 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.

View file

@ -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 } : {}),
};

View file

@ -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;
}

View file

@ -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",

View file

@ -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"

View file

@ -352,11 +352,11 @@ function ActionItem(props: ActionItemProps) {
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<code className="text-foreground/80 text-xs font-normal">
{castProps.pattern}
{castProps.query}
</code>
<div className="bg-border h-4 w-[1px] dark:bg-white"></div>
<MatchCaseIcon matchCase={!!castProps.case_sensitive} />
{castProps.regex && (
{!castProps.match_string && (
<span className="text-muted-foreground bg-muted/50 rounded px-1 text-xs font-normal">
regex
</span>

View file

@ -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 (
<div
className={cn(
"group via-background to-background dark:via-background dark:to-background rounded-xl border bg-gradient-to-br from-blue-50/50 transition-shadow dark:from-blue-950/20",
"shadow-sm hover:shadow-md",
)}
>
{/* Header */}
<div
className={cn(
"relative flex items-center bg-gradient-to-r from-blue-50 to-blue-50/50 p-4 backdrop-blur-sm dark:from-blue-950/30 dark:to-blue-950/10",
"rounded-xl",
)}
>
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-500 shadow-md dark:bg-blue-600">
<Sparkles className="h-4 w-4 text-white" />
</div>
<div className="ml-3 flex-1">
<div className="flex items-center gap-2">
<h3 className="text-foreground text-sm font-semibold">
Code review started
</h3>
<Badge
variant="secondary"
className="border-blue-200 bg-blue-100 text-blue-700 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-300"
>
<Check className="h-3 w-3" />
In Progress
</Badge>
</div>
<p className="text-muted-foreground mt-1 text-xs">
Analyzing code for best practices and potential improvements
</p>
</div>
</div>
</div>
);
}

View file

@ -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({
)}
<div
className={`flex items-center border-b border-green-200 bg-green-50 p-2 dark:border-green-800 dark:bg-green-900/50 ${
status === "done" && review ? "cursor-pointer" : ""
}`}
className={cn(
"flex items-center border-b border-green-200 bg-green-50 p-2 dark:border-green-800 dark:bg-green-900/50",
status === "done" && review ? "cursor-pointer" : "",
)}
onClick={
status === "done" && review
? () => setExpanded((prev) => !prev)
@ -200,11 +202,12 @@ export function MarkTaskIncomplete({
)}
<div
className={`flex items-center border-b border-red-200 bg-red-50 p-2 dark:border-red-800 dark:bg-red-900/50 ${
className={cn(
"flex items-center border-b border-red-200 bg-red-50 p-2 dark:border-red-800 dark:bg-red-900/50",
status === "done" && (review || additionalActions)
? "cursor-pointer"
: ""
}`}
: "",
)}
onClick={
status === "done" && (review || additionalActions)
? () => setExpanded((prev) => !prev)

View file

@ -74,19 +74,21 @@ export function TaskSummary({
{summaryText && status === "done" && (
<div
className={`border-t p-2 ${
className={cn(
"border-t p-2",
completed === false
? "border-amber-300 bg-amber-100/50 dark:border-amber-800 dark:bg-amber-900/50"
: "border-green-300 bg-green-100/50 dark:border-green-800 dark:bg-green-900/50"
}`}
: "border-green-300 bg-green-100/50 dark:border-green-800 dark:bg-green-900/50",
)}
>
<button
onClick={() => setShowSummary(!showSummary)}
className={`flex cursor-pointer items-center gap-1 text-xs font-normal ${
className={cn(
"flex cursor-pointer items-center gap-1 text-xs font-normal",
completed === false
? "text-amber-600 hover:text-amber-700 dark:text-amber-400 dark:hover:text-amber-300"
: "text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300"
}`}
: "text-green-600 hover:text-green-700 dark:text-green-400 dark:hover:text-green-300",
)}
>
<FileText className="h-3 w-3" />
{showSummary ? "Hide summary" : "Show summary"}

View file

@ -3,6 +3,7 @@ import { useGitHubAppProvider } from "@/providers/GitHubApp";
import { InstallationPrompt } from "./installation-prompt";
import { Button } from "@/components/ui/button";
import { RefreshCw } from "lucide-react";
import { cn } from "@/lib/utils";
interface RepositoryListProps {
className?: string;
@ -14,7 +15,7 @@ export function RepositoryList({ className = "" }: RepositoryListProps) {
if (isLoading) {
return (
<div className={`p-4 ${className}`}>
<div className={cn("p-4", className)}>
<div className="mb-4 h-6 w-24 animate-pulse rounded bg-gray-200"></div>
{[1, 2, 3].map((i) => (
<div
@ -31,7 +32,7 @@ export function RepositoryList({ className = "" }: RepositoryListProps) {
if (error) {
return (
<div className={`p-4 ${className}`}>
<div className={cn("p-4", className)}>
<div className="mb-4 rounded-md border border-red-200 bg-red-50 p-4">
<p className="text-sm text-red-800">{error}</p>
</div>
@ -47,14 +48,14 @@ export function RepositoryList({ className = "" }: RepositoryListProps) {
if (!isInstalled) {
return (
<div className={`p-4 ${className}`}>
<div className={cn("p-4", className)}>
<InstallationPrompt />
</div>
);
}
return (
<div className={`p-4 ${className}`}>
<div className={cn("p-4", className)}>
<div className="mb-4 flex items-center justify-between">
<h2 className="text-lg font-medium">Your GitHub Repositories</h2>
<Button
@ -107,7 +108,12 @@ export function RepositoryList({ className = "" }: RepositoryListProps) {
</p>
<div className="mt-2 flex items-center text-xs">
<span
className={`rounded-full px-2 py-0.5 ${repo.private ? "bg-gray-100" : "bg-green-100 text-green-800"} mr-2`}
className={cn(
"rounded-full px-2 py-0.5",
repo.private
? "bg-gray-100"
: "bg-green-100 text-green-800",
)}
>
{repo.private ? "Private" : "Public"}
</span>

View file

@ -27,6 +27,7 @@ import {
} from "@/components/gen-ui/task-review";
import { DiagnoseErrorAction } from "@/components/v2/diagnose-error-action";
import { WriteTechnicalNotes } from "@/components/gen-ui/write-technical-notes";
import { CodeReviewStarted } from "@/components/gen-ui/code-review-started";
import { ToolCall } from "@langchain/core/messages/tool";
import {
createApplyPatchToolFields,
@ -43,6 +44,7 @@ import {
createGetURLContentToolFields,
createWriteTechnicalNotesToolFields,
createConversationHistorySummaryToolFields,
createReviewStartedToolFields,
} from "@open-swe/shared/open-swe/tools";
import { z } from "zod";
import { isAIMessageSDK, isToolMessageSDK } from "@/lib/langchain-messages";
@ -61,6 +63,8 @@ const markTaskNotCompletedTool = createMarkTaskNotCompletedToolFields();
type MarkTaskNotCompletedToolArgs = z.infer<
typeof markTaskNotCompletedTool.schema
>;
const reviewStartedTool = createReviewStartedToolFields();
type ReviewStartedToolArgs = z.infer<typeof reviewStartedTool.schema>;
const searchTool = createSearchToolFields(dummyRepo);
type SearchToolArgs = z.infer<typeof searchTool.schema>;
const openPrTool = createOpenPrToolFields();
@ -200,8 +204,8 @@ export function mapToolMessageToActionStepProps(
actionType: "search",
status,
success,
pattern: args.pattern || "",
regex: args.regex || false,
query: args.query || "",
match_string: args.match_string || false,
case_sensitive: args.case_sensitive || false,
context_lines: args.context_lines || 0,
max_results: args.max_results || 0,
@ -349,6 +353,10 @@ export function AssistantMessage({
? aiToolCalls.find((tc) => tc.name === conversationHistorySummaryTool.name)
: undefined;
const reviewStartedToolCall = message
? aiToolCalls.find((tc) => tc.name === reviewStartedTool.name)
: undefined;
// Check if this is a conversation history summary message
if (conversationHistorySummaryToolCall && aiToolCalls.length === 1) {
const args =
@ -363,6 +371,21 @@ export function AssistantMessage({
);
}
// Check if this is a review started message
if (reviewStartedToolCall && aiToolCalls.length === 1) {
const correspondingToolResult = toolResults.find(
(tr) => tr && tr.tool_call_id === reviewStartedToolCall.id,
);
return (
<div className="flex flex-col gap-4">
<CodeReviewStarted
status={correspondingToolResult ? "done" : "generating"}
/>
</div>
);
}
// We can be sure that if either task status tool call is present, it will be the
// only tool call/result we need to render for this message.
if (markTaskCompletedToolCall || markTaskNotCompletedToolCall) {
@ -550,8 +573,8 @@ export function AssistantMessage({
return {
actionType: "search",
status: "generating",
pattern: args?.pattern || "",
regex: args?.regex || false,
query: args?.query || "",
match_string: args?.match_string || false,
case_sensitive: args?.case_sensitive || false,
context_lines: args?.context_lines || 0,
max_results: args?.max_results || 0,

View file

@ -17,7 +17,6 @@ import {
import {
DO_NOT_RENDER_ID_PREFIX,
PLANNER_GRAPH_ID,
PROGRAMMER_GRAPH_ID,
} from "@open-swe/shared/constants";
import { Message } from "@langchain/langgraph-sdk";
import { InitializeStep } from "../gen-ui/initialize-step";
@ -158,6 +157,7 @@ function isNodeEndCommandUpdate(data: unknown): data is {
}
const REVIEWER_NODE_IDS = [
"initialize-state",
"generate-review-actions",
"take-review-actions",
"diagnose-reviewer-error",
@ -205,11 +205,9 @@ export function ActionsRenderer<State extends PlannerGraphState | GraphState>({
if (isNodeEndCommandUpdate(data.data)) {
const outputMessages = data.data.output.update
.messages as unknown as Message[];
console.log("outputMessages", outputMessages);
debouncedSetMessages(outputMessages);
} else if (isNodeEndMessagesUpdate(data.data)) {
const outputMessages = data.data.output.messages;
console.log("outputMessages", outputMessages);
debouncedSetMessages(outputMessages);
}
}

View file

@ -12,6 +12,7 @@ import { useRouter } from "next/navigation";
import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Skeleton } from "../ui/skeleton";
import { cn } from "@/lib/utils";
export function ThreadCard({ thread }: { thread: ThreadDisplayInfo }) {
const router = useRouter();
@ -82,7 +83,7 @@ export function ThreadCard({ thread }: { thread: ThreadDisplayInfo }) {
</div>
<Badge
variant="secondary"
className={`${getStatusColor(thread.status)} text-xs`}
className={cn(getStatusColor(thread.status), "text-xs")}
>
<div className="flex items-center gap-1">
{getStatusIcon(thread.status)}
@ -122,7 +123,10 @@ export function ThreadCard({ thread }: { thread: ThreadDisplayInfo }) {
<Button
variant="ghost"
size="sm"
className={`h-5 w-5 p-0 hover:text-gray-300 ${getPRStatusColor(thread.pullRequest.status)}`}
className={cn(
"h-5 w-5 p-0 hover:text-gray-300",
getPRStatusColor(thread.pullRequest.status),
)}
onClick={(e) => {
e.stopPropagation();
window.open(thread.pullRequest!.url, "_blank");

View file

@ -27,6 +27,7 @@ import {
} from "../../utils/scroll-utils";
import { ManagerChat } from "./manager-chat";
import { CancelStreamButton } from "./cancel-stream-button";
import { cn } from "@/lib/utils";
interface ThreadViewProps {
stream: ReturnType<typeof useStream<ManagerGraphState>>;
@ -99,13 +100,14 @@ export function ThreadView({
</Button>
<div className="flex min-w-0 flex-1 items-center gap-2">
<div
className={`size-2 flex-shrink-0 rounded-full ${
className={cn(
"size-2 flex-shrink-0 rounded-full",
displayThread.status === "running"
? "bg-blue-500"
: displayThread.status === "completed"
? "bg-green-500"
: "bg-red-500"
}`}
: "bg-red-500",
)}
></div>
<span className="text-muted-foreground max-w-[500px] truncate font-mono text-sm">
{displayThread.title}

View file

@ -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"

View file

@ -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"
},

View file

@ -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:
"<not used as an actual tool call. only used as shared types between the client and agent>",
schema: reviewStartedSchema,
};
}

View file

@ -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": "*"