diff --git a/apps/open-swe/src/graphs/manager/nodes/create-new-session.ts b/apps/open-swe/src/graphs/manager/nodes/create-new-session.ts index 2e20efd1..2a39b01e 100644 --- a/apps/open-swe/src/graphs/manager/nodes/create-new-session.ts +++ b/apps/open-swe/src/graphs/manager/nodes/create-new-session.ts @@ -10,6 +10,7 @@ import { GITHUB_TOKEN_COOKIE, GITHUB_USER_ID_HEADER, GITHUB_USER_LOGIN_HEADER, + MANAGER_GRAPH_ID, } from "@open-swe/shared/constants"; import { createLangGraphClient } from "../../../utils/langgraph-client.js"; import { createIssue } from "../../../utils/github/api.js"; @@ -91,7 +92,7 @@ ${ISSUE_CONTENT_CLOSE_TAG}`, messages: inputMessages, branchName: state.branchName ?? getBranchName(config), }; - await langGraphClient.runs.create(newManagerThreadId, "manager", { + await langGraphClient.runs.create(newManagerThreadId, MANAGER_GRAPH_ID, { input: {}, command: { update: commandUpdate, diff --git a/apps/open-swe/src/graphs/manager/nodes/start-planner.ts b/apps/open-swe/src/graphs/manager/nodes/start-planner.ts index 05d8f1e5..dd91ce35 100644 --- a/apps/open-swe/src/graphs/manager/nodes/start-planner.ts +++ b/apps/open-swe/src/graphs/manager/nodes/start-planner.ts @@ -10,9 +10,11 @@ import { GITHUB_TOKEN_COOKIE, GITHUB_USER_ID_HEADER, GITHUB_USER_LOGIN_HEADER, + PLANNER_GRAPH_ID, } from "@open-swe/shared/constants"; import { createLogger, LogLevel } from "../../../utils/logger.js"; import { getBranchName } from "../../../utils/github/git.js"; +import { PlannerGraphUpdate } from "@open-swe/shared/open-swe/planner/types"; const logger = createLogger(LogLevel.INFO, "StartPlanner"); @@ -38,23 +40,29 @@ export async function startPlanner( const plannerThreadId = state.plannerSession?.threadId ?? uuidv4(); try { - const run = await langGraphClient.runs.create(plannerThreadId, "planner", { - input: { - // github issue ID & target repo so the planning agent can fetch the user's request, and clone the repo. - githubIssueId: state.githubIssueId, - targetRepository: state.targetRepository, - // Include the existing task plan, so the agent can use it as context when generating followup tasks. - taskPlan: state.taskPlan, - branchName: state.branchName ?? getBranchName(config), + const runInput: PlannerGraphUpdate = { + // github issue ID & target repo so the planning agent can fetch the user's request, and clone the repo. + githubIssueId: state.githubIssueId, + targetRepository: state.targetRepository, + // Include the existing task plan, so the agent can use it as context when generating followup tasks. + taskPlan: state.taskPlan, + branchName: state.branchName ?? getBranchName(config), + autoAcceptPlan: state.autoAcceptPlan, + }; + const run = await langGraphClient.runs.create( + plannerThreadId, + PLANNER_GRAPH_ID, + { + input: runInput, + config: { + recursion_limit: 400, + }, + ifNotExists: "create", + multitaskStrategy: "enqueue", + streamResumable: true, + streamMode: ["values", "messages", "custom"], }, - config: { - recursion_limit: 400, - }, - ifNotExists: "create", - multitaskStrategy: "enqueue", - streamResumable: true, - streamMode: ["values", "messages", "custom"], - }); + ); return { plannerSession: { diff --git a/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts b/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts index 668f04c8..3906dfa2 100644 --- a/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts +++ b/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts @@ -1,6 +1,10 @@ import { v4 as uuidv4 } from "uuid"; import { Command, END, interrupt } from "@langchain/langgraph"; -import { GraphUpdate, GraphConfig } from "@open-swe/shared/open-swe/types"; +import { + GraphUpdate, + GraphConfig, + TaskPlan, +} from "@open-swe/shared/open-swe/types"; import { ActionRequest, HumanInterrupt, @@ -16,6 +20,7 @@ import { GITHUB_USER_LOGIN_HEADER, PLAN_INTERRUPT_ACTION_TITLE, PLAN_INTERRUPT_DELIMITER, + PROGRAMMER_GRAPH_ID, } from "@open-swe/shared/constants"; import { PlannerGraphState, @@ -23,6 +28,63 @@ import { } from "@open-swe/shared/open-swe/planner/types"; import { createLangGraphClient } from "../../../utils/langgraph-client.js"; import { addTaskPlanToIssue } from "../../../utils/github/issue-task.js"; +import { createLogger, LogLevel } from "../../../utils/logger.js"; + +const logger = createLogger(LogLevel.INFO, "ProposedPlan"); + +async function startProgrammerRun(input: { + runInput: Exclude & { taskPlan: TaskPlan }; + state: PlannerGraphState; + config: GraphConfig; +}) { + const { runInput, state, config } = input; + const langGraphClient = createLangGraphClient({ + defaultHeaders: { + [GITHUB_TOKEN_COOKIE]: config.configurable?.[GITHUB_TOKEN_COOKIE] ?? "", + [GITHUB_INSTALLATION_TOKEN_COOKIE]: + config.configurable?.[GITHUB_INSTALLATION_TOKEN_COOKIE] ?? "", + [GITHUB_USER_ID_HEADER]: + config.configurable?.[GITHUB_USER_ID_HEADER] ?? "", + [GITHUB_USER_LOGIN_HEADER]: + config.configurable?.[GITHUB_USER_LOGIN_HEADER] ?? "", + }, + }); + + const programmerThreadId = uuidv4(); + // Restart the sandbox. + runInput.sandboxSessionId = (await startSandbox(state.sandboxSessionId)).id; + + const run = await langGraphClient.runs.create( + programmerThreadId, + PROGRAMMER_GRAPH_ID, + { + input: runInput, + config: { + recursion_limit: 400, + }, + ifNotExists: "create", + streamResumable: true, + streamMode: ["values", "messages", "custom"], + }, + ); + + await addTaskPlanToIssue( + { + githubIssueId: state.githubIssueId, + targetRepository: state.targetRepository, + }, + config, + runInput.taskPlan, + ); + return { + programmerSession: { + threadId: programmerThreadId, + runId: run.run_id, + }, + sandboxSessionId: runInput.sandboxSessionId, + taskPlan: runInput.taskPlan, + }; +} export async function interruptProposedPlan( state: PlannerGraphState, @@ -33,6 +95,37 @@ export async function interruptProposedPlan( throw new Error("No proposed plan found."); } + const userRequest = getUserRequest(state.messages); + const runInput: GraphUpdate = { + contextGatheringNotes: state.contextGatheringNotes, + branchName: state.branchName, + targetRepository: state.targetRepository, + githubIssueId: state.githubIssueId, + }; + + if (state.autoAcceptPlan) { + logger.info("Auto accepting plan."); + const planItems = proposedPlan.map((p, index) => ({ + index, + plan: p, + completed: false, + })); + runInput.taskPlan = createNewTask( + userRequest, + state.proposedPlanTitle, + planItems, + { existingTaskPlan: state.taskPlan }, + ); + + return await startProgrammerRun({ + runInput: runInput as Exclude & { + taskPlan: TaskPlan; + }, + state, + config, + }); + } + const interruptRes = interrupt({ action_request: { action: PLAN_INTERRUPT_ACTION_TITLE, @@ -67,30 +160,6 @@ export async function interruptProposedPlan( }); } - const langGraphClient = createLangGraphClient({ - defaultHeaders: { - [GITHUB_TOKEN_COOKIE]: config.configurable?.[GITHUB_TOKEN_COOKIE] ?? "", - [GITHUB_INSTALLATION_TOKEN_COOKIE]: - config.configurable?.[GITHUB_INSTALLATION_TOKEN_COOKIE] ?? "", - [GITHUB_USER_ID_HEADER]: - config.configurable?.[GITHUB_USER_ID_HEADER] ?? "", - [GITHUB_USER_LOGIN_HEADER]: - config.configurable?.[GITHUB_USER_LOGIN_HEADER] ?? "", - }, - }); - - const userRequest = getUserRequest(state.messages); - - const runInput: GraphUpdate = { - contextGatheringNotes: state.contextGatheringNotes, - branchName: state.branchName, - targetRepository: state.targetRepository, - githubIssueId: state.githubIssueId, - }; - // TODO: UPDATE ISSUE WITH PROGRAMMER THREAD ID. - // TODO: UPDATE ISSUE WITH TASK PLAN - const programmerThreadId = uuidv4(); - if (interruptRes.type === "accept") { const planItems = proposedPlan.map((p, index) => ({ index, @@ -125,38 +194,11 @@ export async function interruptProposedPlan( throw new Error("Unknown interrupt type." + interruptRes.type); } - // Restart the sandbox. - runInput.sandboxSessionId = (await startSandbox(state.sandboxSessionId)).id; - - const run = await langGraphClient.runs.create( - programmerThreadId, - "programmer", - { - input: runInput, - config: { - recursion_limit: 400, - }, - ifNotExists: "create", - streamResumable: true, - streamMode: ["values", "messages", "custom"], - }, - ); - - await addTaskPlanToIssue( - { - githubIssueId: state.githubIssueId, - targetRepository: state.targetRepository, + return await startProgrammerRun({ + runInput: runInput as Exclude & { + taskPlan: TaskPlan; }, + state, config, - runInput.taskPlan, - ); - - return { - programmerSession: { - threadId: programmerThreadId, - runId: run.run_id, - }, - sandboxSessionId: runInput.sandboxSessionId, - taskPlan: runInput.taskPlan, - }; + }); } diff --git a/apps/open-swe/src/routes/github/issue-webhook.ts b/apps/open-swe/src/routes/github/issue-webhook.ts index ad6ae190..0f06e02d 100644 --- a/apps/open-swe/src/routes/github/issue-webhook.ts +++ b/apps/open-swe/src/routes/github/issue-webhook.ts @@ -13,7 +13,11 @@ import { } from "@open-swe/shared/constants"; import { encryptGitHubToken } from "@open-swe/shared/crypto"; import { HumanMessage } from "@langchain/core/messages"; -import { getOpenSWELabel } from "../../utils/github/label.js"; +import { + getOpenSWEAutoAcceptLabel, + getOpenSWELabel, +} from "../../utils/github/label.js"; +import { ManagerGraphUpdate } from "@open-swe/shared/open-swe/manager/types"; const logger = createLogger(LogLevel.INFO, "GitHubIssueWebhook"); @@ -72,11 +76,21 @@ webhooks.on("issues.labeled", async ({ payload }) => { "GITHUB_TOKEN_ENCRYPTION_KEY environment variable is required", ); } - if (payload.label?.name !== getOpenSWELabel()) { + const validOpenSWELabels = [getOpenSWELabel(), getOpenSWEAutoAcceptLabel()]; + if ( + !payload.label?.name || + !validOpenSWELabels.some((l) => l === payload.label?.name) + ) { return; } + const isAutoAcceptLabel = payload.label.name === getOpenSWEAutoAcceptLabel(); - logger.info(`'open-swe' label added to issue #${payload.issue.number}`); + logger.info( + `'${payload.label.name}' label added to issue #${payload.issue.number}`, + { + isAutoAcceptLabel, + }, + ); try { // Get installation ID from the webhook payload @@ -113,24 +127,26 @@ webhooks.on("issues.labeled", async ({ payload }) => { }); const threadId = uuidv4(); - const run = await langGraphClient.runs.create(threadId, MANAGER_GRAPH_ID, { - input: { - messages: [ - new HumanMessage({ - id: uuidv4(), - content: `**${issueData.issueTitle}**\n\n${issueData.issueBody}`, - additional_kwargs: { - isOriginalIssue: true, - githubIssueId: issueData.issueNumber, - }, - }), - ], - githubIssueId: issueData.issueNumber, - targetRepository: { - owner: issueData.owner, - repo: issueData.repo, - }, + const runInput: ManagerGraphUpdate = { + messages: [ + new HumanMessage({ + id: uuidv4(), + content: `**${issueData.issueTitle}**\n\n${issueData.issueBody}`, + additional_kwargs: { + isOriginalIssue: true, + githubIssueId: issueData.issueNumber, + }, + }), + ], + githubIssueId: issueData.issueNumber, + targetRepository: { + owner: issueData.owner, + repo: issueData.repo, }, + autoAcceptPlan: isAutoAcceptLabel, + }; + const run = await langGraphClient.runs.create(threadId, MANAGER_GRAPH_ID, { + input: runInput, config: { recursion_limit: 400, }, @@ -140,13 +156,14 @@ webhooks.on("issues.labeled", async ({ payload }) => { }); logger.info("Created new run from GitHub issue.", { - thread_id: threadId, - run_id: run.run_id, - issue_number: issueData.issueNumber, + threadId, + runId: run.run_id, + issueNumber: issueData.issueNumber, owner: issueData.owner, repo: issueData.repo, - user_id: issueData.userId, - user_login: issueData.userLogin, + userId: issueData.userId, + userLogin: issueData.userLogin, + autoAcceptPlan: isAutoAcceptLabel, }); logger.info("Creating comment..."); diff --git a/apps/open-swe/src/utils/github/label.ts b/apps/open-swe/src/utils/github/label.ts index b39f90a6..6eb2afdd 100644 --- a/apps/open-swe/src/utils/github/label.ts +++ b/apps/open-swe/src/utils/github/label.ts @@ -1,6 +1,17 @@ /** * @returns "open-swe" or "open-swe-dev" based on the NODE_ENV. */ -export function getOpenSWELabel() { +export function getOpenSWELabel(): "open-swe" | "open-swe-dev" { return process.env.NODE_ENV === "production" ? "open-swe" : "open-swe-dev"; } + +/** + * @returns "open-swe-auto" or "open-swe-auto-dev" based on the NODE_ENV. + */ +export function getOpenSWEAutoAcceptLabel(): + | "open-swe-auto" + | "open-swe-auto-dev" { + return process.env.NODE_ENV === "production" + ? "open-swe-auto" + : "open-swe-auto-dev"; +} diff --git a/apps/web/src/components/github/repo-branch-selectors/index.tsx b/apps/web/src/components/github/repo-branch-selectors/index.tsx index 3177247c..4091ce49 100644 --- a/apps/web/src/components/github/repo-branch-selectors/index.tsx +++ b/apps/web/src/components/github/repo-branch-selectors/index.tsx @@ -6,14 +6,13 @@ export function RepositoryBranchSelectors() { const [threadId] = useQueryState("threadId"); const chatStarted = !!threadId; const defaultButtonStyles = - "bg-inherit border-none text-gray-500 hover:text-black dark:hover:text-gray-300 text-xs p-0 h-fit hover:bg-inherit"; + "bg-inherit border-none text-gray-500 hover:text-black dark:hover:text-gray-300 text-xs p-0 px-0 py-0 !p-0 !px-0 !py-0 h-fit hover:bg-inherit shadow-none"; const defaultStylesChatStarted = - "hover:bg-inherit cursor-default hover:cursor-default hover:text-black dark:hover:text-gray-300 hover:border-gray-300 hover:ring-inherit"; + "hover:bg-inherit cursor-default hover:cursor-default hover:text-black dark:hover:text-gray-300 hover:border-gray-300 hover:ring-inherit shadow-none p-0 px-0 py-0 !p-0 !px-0 !py-0"; return ( -
+
- ( - )
+ :
- ( - )
); diff --git a/apps/web/src/components/ui/tooltip-icon-button.tsx b/apps/web/src/components/ui/tooltip-icon-button.tsx index b11f0707..e73b8179 100644 --- a/apps/web/src/components/ui/tooltip-icon-button.tsx +++ b/apps/web/src/components/ui/tooltip-icon-button.tsx @@ -30,7 +30,8 @@ export type TooltipIconButtonProps = ButtonProps & { | "outline" | "secondary" | "ghost" - | "link"; + | "link" + | "brand"; }; export const TooltipIconButton = forwardRef< diff --git a/apps/web/src/components/v2/default-view.tsx b/apps/web/src/components/v2/default-view.tsx index 09eaeec2..d4021098 100644 --- a/apps/web/src/components/v2/default-view.tsx +++ b/apps/web/src/components/v2/default-view.tsx @@ -1,13 +1,7 @@ "use client"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { FilePlus2, Archive } from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; +import { FilePlus2, Archive, Zap } from "lucide-react"; import { useRouter } from "next/navigation"; import { ThreadDisplayInfo } from "./types"; import { TerminalInput } from "./terminal-input"; @@ -28,6 +22,7 @@ import { QuickActions } from "./quick-actions"; import { useState } from "react"; import { GitHubLogoutButton } from "../github/github-oauth-button"; import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants"; +import { TooltipIconButton } from "../ui/tooltip-icon-button"; interface DefaultViewProps { threads: ThreadDisplayInfo[]; @@ -48,6 +43,7 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) { dragOver, handlePaste, } = useFileUpload(); + const [autoAccept, setAutoAccept] = useState(false); if (!apiUrl) { return
Missing API URL environment variable
; @@ -112,8 +108,10 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) { onPaste={handlePaste} quickActionPrompt={quickActionPrompt} setQuickActionPrompt={setQuickActionPrompt} + autoAcceptPlan={autoAccept} + setAutoAcceptPlan={setAutoAccept} /> -
+
@@ -124,9 +122,24 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) { - Attach files + + Attach files + + setAutoAccept((prev) => !prev)} + side="bottom" + > + +
diff --git a/apps/web/src/components/v2/terminal-input.tsx b/apps/web/src/components/v2/terminal-input.tsx index 14591dd4..e2bb1246 100644 --- a/apps/web/src/components/v2/terminal-input.tsx +++ b/apps/web/src/components/v2/terminal-input.tsx @@ -15,6 +15,7 @@ import { Base64ContentBlock, HumanMessage } from "@langchain/core/messages"; import { toast } from "sonner"; import { DEFAULT_CONFIG_KEY, useConfigStore } from "@/hooks/useConfigStore"; import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants"; +import { ManagerGraphUpdate } from "@open-swe/shared/open-swe/manager/types"; interface TerminalInputProps { placeholder?: string; @@ -26,6 +27,8 @@ interface TerminalInputProps { onPaste?: (e: React.ClipboardEvent) => void; quickActionPrompt?: string; setQuickActionPrompt?: Dispatch>; + autoAcceptPlan: boolean; + setAutoAcceptPlan: Dispatch>; } export function TerminalInput({ @@ -38,6 +41,8 @@ export function TerminalInput({ onPaste, quickActionPrompt, setQuickActionPrompt, + autoAcceptPlan, + setAutoAcceptPlan, }: TerminalInputProps) { const { push } = useRouter(); const [message, setMessage] = useState(""); @@ -52,7 +57,6 @@ export function TerminalInput({ }); const handleSend = async () => { - const assistantId = MANAGER_GRAPH_ID; if (!selectedRepository) { toast.error("Please select a repository first", { richColors: true, @@ -75,27 +79,34 @@ export function TerminalInput({ try { const newThreadId = uuidv4(); - const run = await stream.client.runs.create(newThreadId, assistantId, { - input: { - messages: [newHumanMessage], - targetRepository: selectedRepository, - }, - config: { - recursion_limit: 400, - configurable: { - ...getConfig(DEFAULT_CONFIG_KEY), + const runInput: ManagerGraphUpdate = { + messages: [newHumanMessage], + targetRepository: selectedRepository, + autoAcceptPlan, + }; + const run = await stream.client.runs.create( + newThreadId, + MANAGER_GRAPH_ID, + { + input: runInput, + config: { + recursion_limit: 400, + configurable: { + ...getConfig(DEFAULT_CONFIG_KEY), + }, }, + ifNotExists: "create", + streamResumable: true, + streamMode: ["values", "messages", "custom"], }, - ifNotExists: "create", - streamResumable: true, - streamMode: ["values", "messages", "custom"], - }); + ); // set session storage so the stream can be resumed after redirect. sessionStorage.setItem(`lg:stream:${newThreadId}`, run.run_id); push(`/chat/${newThreadId}`); setMessage(""); setContentBlocks([]); + setAutoAcceptPlan(false); } catch (e) { console.error(e); } finally { @@ -121,36 +132,25 @@ export function TerminalInput({ return (
-
- open-swe - @ - github - : +
+
+ open-swe + @ + github +
{/* Repository & Branch Selectors */} {/* Prompt */} $ -
- {/* Multiline Input */} -
-