diff --git a/apps/docs/faq.mdx b/apps/docs/faq.mdx index f8d9900b..04c522a9 100644 --- a/apps/docs/faq.mdx +++ b/apps/docs/faq.mdx @@ -16,6 +16,15 @@ description: Frequently Asked Questions Yes. When using Anthropic models, all input tokens are cached on Anthropic's servers. + + Yes. There's two ways to disable Open SWE from creating an issue when you submit a request: + + 1. Toggle the 'eye' icon in the main chat area when submitting a request. + 2. In the configuration tab in settings, toggle the 'Should Create Issue' switch. + + By default, it's set to `true`. By modifying this setting in the configuration tab, all runs will default to that setting. You may override this setting on a per-run basis by toggling the 'eye' icon in the main chat area. + + We're sorry you're experiencing this! Open SWE will automatically commit any changes it makes to a draft pull request. Please check the draft pull request and make any necessary changes. diff --git a/apps/open-swe/src/graphs/manager/nodes/classify-message/index.ts b/apps/open-swe/src/graphs/manager/nodes/classify-message/index.ts index 0489ef33..339ee486 100644 --- a/apps/open-swe/src/graphs/manager/nodes/classify-message/index.ts +++ b/apps/open-swe/src/graphs/manager/nodes/classify-message/index.ts @@ -45,6 +45,7 @@ import { isLocalMode } from "@open-swe/shared/open-swe/local-mode"; import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types"; import { GraphState } from "@open-swe/shared/open-swe/types"; import { Client } from "@langchain/langgraph-sdk"; +import { shouldCreateIssue } from "../../../../utils/should-create-issue.js"; const logger = createLogger(LogLevel.INFO, "ClassifyMessage"); /** @@ -186,6 +187,39 @@ export async function classifyMessage( ); } + if (!shouldCreateIssue(config)) { + const commandUpdate: ManagerGraphUpdate = { + messages: [response], + }; + if ( + toolCallArgs.route === "start_planner" || + toolCallArgs.route === "start_planner_for_followup" + ) { + return new Command({ + update: commandUpdate, + goto: "start-planner", + }); + } + + if (toolCallArgs.route === "create_new_issue") { + return new Command({ + update: commandUpdate, + goto: "create-new-session", + }); + } + + if (toolCallArgs.route === "no_op") { + return new Command({ + update: commandUpdate, + goto: END, + }); + } + + throw new Error( + `Unsupported route received: ${toolCallArgs.route}\nUnable to route message there when not creating GitHub issues for request.`, + ); + } + const { githubAccessToken } = getGitHubTokensFromConfig(config); let githubIssueId = state.githubIssueId; 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 3cdbff1d..42920b20 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 @@ -31,6 +31,7 @@ import { StreamMode } from "@langchain/langgraph-sdk"; import { isLocalMode } from "@open-swe/shared/open-swe/local-mode"; import { regenerateInstallationToken } from "../../../utils/github/regenerate-token.js"; import { createLogger, LogLevel } from "../../../utils/logger.js"; +import { shouldCreateIssue } from "../../../utils/should-create-issue.js"; const logger = createLogger(LogLevel.INFO, "CreateNewSession"); @@ -48,16 +49,21 @@ export async function createNewSession( state.messages, config.configurable, ); - const { githubAccessToken } = getGitHubTokensFromConfig(config); - const newIssue = await createIssue({ - owner: state.targetRepository.owner, - repo: state.targetRepository.repo, - title: titleAndContent.title, - body: formatContentForIssueBody(titleAndContent.body), - githubAccessToken, - }); - if (!newIssue) { - throw new Error("Failed to create new issue"); + + let newIssueNumber: number | undefined; + if (shouldCreateIssue(config)) { + const { githubAccessToken } = getGitHubTokensFromConfig(config); + const newIssue = await createIssue({ + owner: state.targetRepository.owner, + repo: state.targetRepository.repo, + title: titleAndContent.title, + body: formatContentForIssueBody(titleAndContent.body), + githubAccessToken, + }); + if (!newIssue) { + throw new Error("Failed to create new issue"); + } + newIssueNumber = newIssue.number; } const inputMessages: BaseMessage[] = [ @@ -71,7 +77,7 @@ ${ISSUE_CONTENT_OPEN_TAG} ${titleAndContent.body} ${ISSUE_CONTENT_CLOSE_TAG}`, additional_kwargs: { - githubIssueId: newIssue.number, + githubIssueId: newIssueNumber, isOriginalIssue: true, }, }), @@ -102,7 +108,7 @@ ${ISSUE_CONTENT_CLOSE_TAG}`, const newManagerThreadId = uuidv4(); const commandUpdate: ManagerGraphUpdate = { - githubIssueId: newIssue.number, + githubIssueId: newIssueNumber, targetRepository: state.targetRepository, messages: inputMessages, branchName: state.branchName ?? getBranchName(config), 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 d84f4d8f..4b52174d 100644 --- a/apps/open-swe/src/graphs/manager/nodes/start-planner.ts +++ b/apps/open-swe/src/graphs/manager/nodes/start-planner.ts @@ -22,6 +22,7 @@ import { getCustomConfigurableFields } from "../../../utils/config.js"; import { getRecentUserRequest } from "../../../utils/user-request.js"; import { StreamMode } from "@langchain/langgraph-sdk"; import { regenerateInstallationToken } from "../../../utils/github/regenerate-token.js"; +import { shouldCreateIssue } from "../../../utils/should-create-issue.js"; const logger = createLogger(LogLevel.INFO, "StartPlanner"); @@ -68,6 +69,9 @@ export async function startPlanner( branchName: state.branchName ?? getBranchName(config), autoAcceptPlan: state.autoAcceptPlan, ...(followupMessage || localMode ? { messages: [followupMessage] } : {}), + ...(!shouldCreateIssue(config) && followupMessage + ? { internalMessages: [followupMessage] } + : {}), }; const run = await langGraphClient.runs.create( diff --git a/apps/open-swe/src/graphs/planner/nodes/determine-needs-context.ts b/apps/open-swe/src/graphs/planner/nodes/determine-needs-context.ts index 00a6bc8c..20e4b541 100644 --- a/apps/open-swe/src/graphs/planner/nodes/determine-needs-context.ts +++ b/apps/open-swe/src/graphs/planner/nodes/determine-needs-context.ts @@ -18,6 +18,7 @@ import { filterHiddenMessages } from "../../../utils/message/filter-hidden.js"; import { createLogger, LogLevel } from "../../../utils/logger.js"; import { trackCachePerformance } from "../../../utils/caching.js"; import { getModelManager } from "../../../utils/llms/model-manager.js"; +import { shouldCreateIssue } from "../../../utils/should-create-issue.js"; const logger = createLogger(LogLevel.INFO, "DetermineNeedsContext"); @@ -118,7 +119,7 @@ export async function determineNeedsContext( config: GraphConfig, ): Promise { const [missingMessages, model] = await Promise.all([ - getMissingMessages(state, config), + shouldCreateIssue(config) ? getMissingMessages(state, config) : [], loadModel(config, LLMTask.ROUTER), ]); const modelManager = getModelManager(); 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 11cab739..c3ddb750 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 @@ -40,6 +40,7 @@ import { trackCachePerformance, } from "../../../../utils/caching.js"; import { createViewTool } from "../../../../tools/builtin-tools/view.js"; +import { shouldCreateIssue } from "../../../../utils/should-create-issue.js"; const logger = createLogger(LogLevel.INFO, "GeneratePlanningMessageNode"); @@ -124,10 +125,14 @@ export async function generateAction( : {}), }); - const [missingMessages, { taskPlan: latestTaskPlan }] = await Promise.all([ - getMissingMessages(state, config), - getPlansFromIssue(state, config), - ]); + const [missingMessages, { taskPlan: latestTaskPlan }] = shouldCreateIssue( + config, + ) + ? await Promise.all([ + getMissingMessages(state, config), + getPlansFromIssue(state, config), + ]) + : [[], { taskPlan: null }]; const inputMessages = filterMessagesWithoutContent([ ...state.messages, diff --git a/apps/open-swe/src/graphs/planner/nodes/prepare-state.ts b/apps/open-swe/src/graphs/planner/nodes/prepare-state.ts index c6144193..5190cbde 100644 --- a/apps/open-swe/src/graphs/planner/nodes/prepare-state.ts +++ b/apps/open-swe/src/graphs/planner/nodes/prepare-state.ts @@ -21,24 +21,27 @@ import { import { filterHiddenMessages } from "../../../utils/message/filter-hidden.js"; import { DO_NOT_RENDER_ID_PREFIX } from "@open-swe/shared/constants"; import { isLocalMode } from "@open-swe/shared/open-swe/local-mode"; +import { shouldCreateIssue } from "../../../utils/should-create-issue.js"; export async function prepareGraphState( state: PlannerGraphState, config: GraphConfig, ): Promise { - if (isLocalMode(config)) { - // In local mode, just proceed to initialize-sandbox with existing messages + if (isLocalMode(config) || !shouldCreateIssue(config)) { return new Command({ update: {}, goto: "initialize-sandbox", }); } + if (!state.githubIssueId) { throw new Error("No github issue id provided"); } + if (!state.targetRepository) { throw new Error("No target repository provided"); } + const { githubInstallationToken } = getGitHubTokensFromConfig(config); const baseGetIssueInputs = { owner: state.targetRepository.owner, @@ -57,8 +60,6 @@ export async function prepareGraphState( throw new Error(`Issue not found. Issue ID: ${state.githubIssueId}`); } - // Ensure the main issue & all comments are included in the state; - // If the messages state is empty, we can just include all comments as human messages. if (!state.messages?.length) { const commandUpdate: PlannerGraphUpdate = { 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 54ea25ab..64911ac1 100644 --- a/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts +++ b/apps/open-swe/src/graphs/planner/nodes/proposed-plan.ts @@ -49,6 +49,7 @@ import { cleanTaskItems, } from "../../../utils/github/plan.js"; import { regenerateInstallationToken } from "../../../utils/github/regenerate-token.js"; +import { shouldCreateIssue } from "../../../utils/should-create-issue.js"; const logger = createLogger(LogLevel.INFO, "ProposedPlan"); @@ -149,7 +150,7 @@ async function startProgrammerRun(input: { ); // Skip GitHub operations in local mode - if (!isLocalMode(config)) { + if (!isLocalMode(config) && shouldCreateIssue(config)) { await addTaskPlanToIssue( { githubIssueId: state.githubIssueId, diff --git a/apps/open-swe/src/graphs/planner/nodes/rewrite-plan.ts b/apps/open-swe/src/graphs/planner/nodes/rewrite-plan.ts deleted file mode 100644 index 38d24e99..00000000 --- a/apps/open-swe/src/graphs/planner/nodes/rewrite-plan.ts +++ /dev/null @@ -1,288 +0,0 @@ -// TODO: NOT HOOKED UP TO THE GRAPH YET -// TODO: WILL NEED TO REFACTOR TO ALLOW FOR CHATTING WITH PLANNING SUBGRAPH - -import { GraphConfig, PlanItem } from "@open-swe/shared/open-swe/types"; -import { z } from "zod"; -import { tool } from "@langchain/core/tools"; -import { traceable } from "langsmith/traceable"; -import { - PlannerGraphState, - PlannerGraphUpdate, -} from "@open-swe/shared/open-swe/planner/types"; -import { - getInitialUserRequest, - getRecentUserRequest, -} from "../../../utils/user-request.js"; -import { - loadModel, - supportsParallelToolCallsParam, -} from "../../../utils/llms/index.js"; -import { LLMTask } from "@open-swe/shared/open-swe/llm-task"; -import { FallbackRunnable } from "../../../utils/runtime-fallback.js"; - -const systemPromptIdentifyChanges = `You are operating as an agentic coding assistant built by LangChain. You've previously been given a task to generate a plan of action for, to address the user's initial request. - -Here is the user's initial request: -{USER_INITIAL_REQUEST} - -After generating that plan, the user has submitted some feedback/change requests. You should now identify exactly which tasks in the plan should be modified based on their request. - -Here is their request: -{USER_REQUEST} - -The plan you generated originally, which they submitted the above request for is as follows: -{PLAN} - -Please read over the generated plan, and the user's request, and identify exactly which tasks in the plan should be modified/removed. Call the 'identify_plan_changes' tool and use the indices of the tasks listed above when calling the tool.`; - -const systemPrompt = `You are operating as an agentic coding assistant built by LangChain. You've previously been given a task to generate a plan of action for, to address the user's initial request. - -In this step, the user has requested you rewrite/modify parts of a high-level plan. You have already identified the specific tasks in the plan that should be modified/removed based on the user's request. - -Here is the user's initial request which you used to generate the initial plan: -{USER_INITIAL_REQUEST} - -Here is the full plan you generated: -{PLAN} - -Here is the request the user has just made which you should use to rewrite/modify the plan: -{USER_REQUEST} - -And here are the specific tasks in the plan which were identified as tasks the user wants to modify/remove: -{TASKS_TO_MODIFY} - -Given this context, please address the user's request to modify/remove/add the tasks in the plan. - -You MUST adhere to the following criteria when generating the plan: -- Make as few changes as possible to the tasks, while still following the users request. -- You should NOT make ANY changes to the tasks in the plan that are NOT listed as tasks to modify/remove. -- Do NOT modify tasks in the plan not listed as tasks to modify/remove. -- When responding, ensure you include the unmodified tasks in the plan, as well as the modified/new tasks. -- To remove a specific task, simply do NOT include it in the response. -- To add a new task, simply include it in the response. -`; - -const formatSysPromptIdentifyTasks = ( - userInitialRequest: string, - userRequest: string, - previousPlan: string[], -) => { - return systemPromptIdentifyChanges - .replace("{USER_INITIAL_REQUEST}", userInitialRequest) - .replace("{USER_REQUEST}", userRequest) - .replace( - "{PLAN}", - previousPlan.map((plan, index) => `${index}: ${plan}`).join("\n"), - ); -}; - -const formatSysPromptRewritePlan = ( - userInitialRequest: string, - userRequest: string, - previousPlan: string[], - tasksToModify: PlanItem[], -) => { - return systemPrompt - .replace("{USER_INITIAL_REQUEST}", userInitialRequest) - .replace("{USER_REQUEST}", userRequest) - .replace( - "{PLAN}", - previousPlan.map((plan, index) => `${index}: ${plan}`).join("\n"), - ) - .replace( - "{TASKS_TO_MODIFY}", - tasksToModify.map((p) => `${p.index}: ${p.plan}`).join("\n"), - ); -}; - -async function identifyTasksToModifyFunc( - state: PlannerGraphState, - model: FallbackRunnable, - supportsParallelToolCallsParam: boolean, -): Promise { - if (!state.planChangeRequest) { - throw new Error("No plan change request found."); - } - - const identifyPlanChangesSchema = z.object({ - task_change_indices: z - .array(z.number()) - .describe( - "The indices of the tasks in the plan that should be modified/removed.", - ), - }); - - const identifyPlanChangesTool = tool( - (input): PlanItem[] => { - const { task_change_indices } = input; - const tasksToModify = state.proposedPlan.flatMap((plan, planIndex) => { - const planItem = task_change_indices.some( - (changeIndex) => changeIndex === planIndex, - ); - if (!planItem) { - return []; - } - return { - index: planIndex, - plan: plan, - completed: false, - }; - }); - - return tasksToModify; - }, - { - name: "identify_plan_changes", - schema: identifyPlanChangesSchema, - description: - "Identify which tasks in the plan should be modified/removed based on the user's request.", - }, - ); - - const modelWithIdentifyChangesTool = model.bindTools( - [identifyPlanChangesTool], - { - // The model should always call the tool when identifying plan changes. - tool_choice: identifyPlanChangesTool.name, - ...(supportsParallelToolCallsParam - ? { - parallel_tool_calls: false, - } - : {}), - }, - ); - - const userInitialRequest = getInitialUserRequest(state.messages); - const userFollowupRequest = getRecentUserRequest(state.messages); - const userRequest = - userFollowupRequest ?? userInitialRequest ?? "No user message found"; - - const response = await modelWithIdentifyChangesTool.invoke([ - { - role: "user", - content: formatSysPromptIdentifyTasks( - userRequest, - state.planChangeRequest, - state.proposedPlan, - ), - }, - ]); - - const toolCall = response.tool_calls?.[0]; - if (!toolCall) { - throw new Error( - "Tool call not returned when attempting to identify plan changes.", - ); - } - - const tasksToModify = await identifyPlanChangesTool.invoke( - toolCall.args as z.infer, - ); - return tasksToModify; -} - -const identifyTasksToModify = traceable(identifyTasksToModifyFunc, { - name: "identify_tasks_to_modify", -}); - -async function updatePlanTasksFunc( - state: PlannerGraphState, - tasksToModify: PlanItem[], - model: FallbackRunnable, - supportsParallelToolCallsParam: boolean, -): Promise { - if (!state.planChangeRequest) { - throw new Error("No plan change request found."); - } - - const updatePlanTasksSchema = z.object({ - updated_plan_tasks: z - .array( - z - .string() - .describe( - "The updated or unmodified plan for the task. Do NOT include the task index.", - ), - ) - .describe( - "The updated plan tasks. Must be in the order of which they should be executed in.", - ), - }); - const updatePlanTasksTool = { - name: "update_plan_tasks", - description: "Call this tool to respond with the updated plan.", - schema: updatePlanTasksSchema, - }; - - const modelWithUpdatePlanTasksTool = model.bindTools([updatePlanTasksTool], { - // The model should always call the tool when identifying plan changes. - tool_choice: updatePlanTasksTool.name, - ...(supportsParallelToolCallsParam - ? { - parallel_tool_calls: false, - } - : {}), - }); - - const userInitialRequest = getInitialUserRequest(state.messages); - const userFollowupRequest = getRecentUserRequest(state.messages); - const userRequest = - userFollowupRequest ?? userInitialRequest ?? "No user message found"; - - const response = await modelWithUpdatePlanTasksTool.invoke([ - { - role: "user", - content: formatSysPromptRewritePlan( - userRequest, - state.planChangeRequest, - state.proposedPlan, - tasksToModify, - ), - }, - ]); - - const toolCall = response.tool_calls?.[0]; - if (!toolCall) { - throw new Error( - "Tool call not returned when attempting to update plan tasks.", - ); - } - - return ( - toolCall.args as z.infer - ).updated_plan_tasks.map((p) => p); -} - -const updatePlanTasks = traceable(updatePlanTasksFunc, { - name: "update_plan_tasks", -}); - -export async function rewritePlan( - state: PlannerGraphState, - config: GraphConfig, -): Promise { - if (!state.planChangeRequest) { - throw new Error("No plan change request found."); - } - - const model = await loadModel(config, LLMTask.PLANNER); - const modelSupportsParallelToolCallsParam = supportsParallelToolCallsParam( - config, - LLMTask.PLANNER, - ); - const tasksToModify = await identifyTasksToModify( - state, - model, - modelSupportsParallelToolCallsParam, - ); - const updatedPlanTasks = await updatePlanTasks( - state, - tasksToModify, - model, - modelSupportsParallelToolCallsParam, - ); - - return { - proposedPlan: updatedPlanTasks, - }; -} 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 1e5623a8..97267d21 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 @@ -59,6 +59,7 @@ import { HumanMessage, } from "@langchain/core/messages"; import { BindToolsInput } from "@langchain/core/language_models/chat_models"; +import { shouldCreateIssue } from "../../../../utils/should-create-issue.js"; const logger = createLogger(LogLevel.INFO, "GenerateMessageNode"); @@ -280,10 +281,14 @@ export async function generateAction( const markTaskCompletedTool = createMarkTaskCompletedToolFields(); const isAnthropicModel = modelName.includes("claude-"); - const [missingMessages, { taskPlan: latestTaskPlan }] = await Promise.all([ - getMissingMessages(state, config), - getPlansFromIssue(state, config), - ]); + const [missingMessages, { taskPlan: latestTaskPlan }] = shouldCreateIssue( + config, + ) + ? await Promise.all([ + getMissingMessages(state, config), + getPlansFromIssue(state, config), + ]) + : [[], { taskPlan: null }]; const { providerTools, providerMessages } = await createToolsAndPrompt( state, diff --git a/apps/open-swe/src/graphs/programmer/nodes/handle-completed-task.ts b/apps/open-swe/src/graphs/programmer/nodes/handle-completed-task.ts index c321eabd..5fb80de6 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/handle-completed-task.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/handle-completed-task.ts @@ -25,6 +25,7 @@ import { MAX_INTERNAL_TOKENS, } from "../../../utils/tokens.js"; import { z } from "zod"; +import { shouldCreateIssue } from "../../../utils/should-create-issue.js"; const logger = createLogger(LogLevel.INFO, "HandleCompletedTask"); @@ -88,7 +89,7 @@ export async function handleCompletedTask( summary, ); // Update the github issue to reflect this task as completed. - if (!isLocalMode(config)) { + if (!isLocalMode(config) && shouldCreateIssue(config)) { await addTaskPlanToIssue( { githubIssueId: state.githubIssueId, diff --git a/apps/open-swe/src/graphs/programmer/nodes/open-pr.ts b/apps/open-swe/src/graphs/programmer/nodes/open-pr.ts index f2672e7d..6c3c7343 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/open-pr.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/open-pr.ts @@ -45,6 +45,7 @@ import { } from "../../../utils/github/types.js"; import { getRepoAbsolutePath } from "@open-swe/shared/git"; import { GITHUB_USER_LOGIN_HEADER } from "@open-swe/shared/constants"; +import { shouldCreateIssue } from "../../../utils/should-create-issue.js"; const logger = createLogger(LogLevel.INFO, "Open PR"); @@ -191,6 +192,9 @@ export async function openPullRequest( | GitHubPullRequestList[number] | GitHubPullRequestUpdate | null = null; + + const prBody = `${shouldCreateIssue(config) ? `Fixes #${state.githubIssueId}` : ""}${userLogin ? `\n\nOwner: @${userLogin}` : ""}\n\n${body}`; + if (!prForTask) { // No PR created yet. Shouldn't be possible, but we have a condition here anyway pullRequest = await createPullRequest({ @@ -198,7 +202,7 @@ export async function openPullRequest( repo, headBranch: branchName, title, - body: `Fixes #${state.githubIssueId}${userLogin ? `\n\nOwner: @${userLogin}` : ""}\n\n${body}`, + body: prBody, githubInstallationToken, baseBranch: state.targetRepository.branch, }); @@ -208,7 +212,7 @@ export async function openPullRequest( owner, repo, title, - body: `Fixes #${state.githubIssueId}${userLogin ? `\n\nOwner: @${userLogin}` : ""}\n\n${body}`, + body: prBody, pullNumber: prForTask, githubInstallationToken, }); diff --git a/apps/open-swe/src/graphs/programmer/nodes/request-help.ts b/apps/open-swe/src/graphs/programmer/nodes/request-help.ts index c27e8bb5..19cef211 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/request-help.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/request-help.ts @@ -21,6 +21,8 @@ import { REQUEST_HELP_NODE_ID, } from "@open-swe/shared/open-swe/custom-node-events"; import { postGitHubIssueComment } from "../../../utils/github/plan.js"; +import { shouldCreateIssue } from "../../../utils/should-create-issue.js"; +import { isLocalMode } from "@open-swe/shared/open-swe/local-mode"; const constructDescription = (helpRequest: string): string => { return `The agent has requested help. Here is the help request: @@ -60,12 +62,13 @@ export async function requestHelp( throw new Error("Thread ID not found in config"); } - const userLogin = config.configurable?.[GITHUB_USER_LOGIN_HEADER]; - const userTag = userLogin ? `@${userLogin} ` : ""; + if (!isLocalMode(config) && shouldCreateIssue(config)) { + const userLogin = config.configurable?.[GITHUB_USER_LOGIN_HEADER]; + const userTag = userLogin ? `@${userLogin} ` : ""; + const runUrl = getOpenSweAppUrl(threadId); - const runUrl = getOpenSweAppUrl(threadId); - const commentBody = runUrl - ? `### 🤖 Open SWE Needs Help + const commentBody = runUrl + ? `### 🤖 Open SWE Needs Help ${userTag}I've encountered a situation where I need human assistance to continue. @@ -75,7 +78,7 @@ ${toolCall.args.help_request} You can view and respond to this request in the [Open SWE interface](${runUrl}). Please provide guidance so I can continue working on this issue.` - : `### 🤖 Open SWE Needs Help + : `### 🤖 Open SWE Needs Help ${userTag}I've encountered a situation where I need human assistance to continue. @@ -84,12 +87,13 @@ ${toolCall.args.help_request} Please check the Open SWE interface to respond to this request.`; - await postGitHubIssueComment({ - githubIssueId: state.githubIssueId, - targetRepository: state.targetRepository, - commentBody, - config, - }); + await postGitHubIssueComment({ + githubIssueId: state.githubIssueId, + targetRepository: state.targetRepository, + commentBody, + config, + }); + } const interruptInput: HumanInterrupt = { action_request: { diff --git a/apps/open-swe/src/graphs/programmer/nodes/update-plan.ts b/apps/open-swe/src/graphs/programmer/nodes/update-plan.ts index d2167c0b..36e9bc6c 100644 --- a/apps/open-swe/src/graphs/programmer/nodes/update-plan.ts +++ b/apps/open-swe/src/graphs/programmer/nodes/update-plan.ts @@ -30,6 +30,8 @@ import { formatCustomRulesPrompt } from "../../../utils/custom-rules.js"; import { trackCachePerformance } from "../../../utils/caching.js"; import { getModelManager } from "../../../utils/llms/model-manager.js"; import { addTaskPlanToIssue } from "../../../utils/github/issue-task.js"; +import { shouldCreateIssue } from "../../../utils/should-create-issue.js"; +import { isLocalMode } from "@open-swe/shared/open-swe/local-mode"; const logger = createLogger(LogLevel.INFO, "UpdatePlanNode"); @@ -217,15 +219,17 @@ export async function updatePlan( newPlanItems, "agent", ); - // Update the github issue to reflect the changes in the plan - await addTaskPlanToIssue( - { - githubIssueId: state.githubIssueId, - targetRepository: state.targetRepository, - }, - config, - newTaskPlan, - ); + if (!isLocalMode(config) && shouldCreateIssue(config)) { + // Update the github issue to reflect the changes in the plan + await addTaskPlanToIssue( + { + githubIssueId: state.githubIssueId, + targetRepository: state.targetRepository, + }, + config, + newTaskPlan, + ); + } const toolMessage = new ToolMessage({ id: uuidv4(), diff --git a/apps/open-swe/src/graphs/reviewer/nodes/final-review.ts b/apps/open-swe/src/graphs/reviewer/nodes/final-review.ts index bb0e8753..16c13818 100644 --- a/apps/open-swe/src/graphs/reviewer/nodes/final-review.ts +++ b/apps/open-swe/src/graphs/reviewer/nodes/final-review.ts @@ -35,6 +35,7 @@ import { import { trackCachePerformance } from "../../../utils/caching.js"; import { getModelManager } from "../../../utils/llms/model-manager.js"; import { createScratchpadTool } from "../../../tools/scratchpad.js"; +import { shouldCreateIssue } from "../../../utils/should-create-issue.js"; const logger = createLogger(LogLevel.INFO, "FinalReview"); @@ -192,7 +193,7 @@ export async function finalReview( "agent", ); - if (!isLocalMode(config)) { + if (!isLocalMode(config) && shouldCreateIssue(config)) { await addTaskPlanToIssue( { githubIssueId: state.githubIssueId, diff --git a/apps/open-swe/src/utils/github/git.ts b/apps/open-swe/src/utils/github/git.ts index 2e9c78a7..7ba97160 100644 --- a/apps/open-swe/src/utils/github/git.ts +++ b/apps/open-swe/src/utils/github/git.ts @@ -21,6 +21,7 @@ import { DEFAULT_EXCLUDED_PATTERNS } from "./constants.js"; import { escapeRegExp } from "../string-utils.js"; import { isLocalMode } from "@open-swe/shared/open-swe/local-mode"; import { createShellExecutor } from "../shell-executor/index.js"; +import { shouldCreateIssue } from "../should-create-issue.js"; const logger = createLogger(LogLevel.INFO, "GitHub-Git"); @@ -336,33 +337,39 @@ export async function checkoutBranchAndCommit( let updatedTaskPlan: TaskPlan | undefined; const activeTask = getActiveTask(options.taskPlan); const prForTask = getPullRequestNumberFromActiveTask(options.taskPlan); + if (!prForTask) { logger.info("First commit detected, creating a draft pull request."); + const hasIssue = shouldCreateIssue(config); + const pullRequest = await createPullRequest({ owner: targetRepository.owner, repo: targetRepository.repo, headBranch: branchName, title: `[WIP]: ${activeTask?.title ?? "Open SWE task"}`, - body: `**WORK IN PROGRESS OPEN SWE PR**\n\nFixes: #${options.githubIssueId}`, + body: `**WORK IN PROGRESS OPEN SWE PR**${hasIssue ? `\n\nFixes: #${options.githubIssueId}` : ""}`, githubInstallationToken: options.githubInstallationToken, draft: true, baseBranch: targetRepository.branch, nullOnError: true, }); + if (pullRequest) { updatedTaskPlan = addPullRequestNumberToActiveTask( options.taskPlan, pullRequest.number, ); - await addTaskPlanToIssue( - { - githubIssueId: options.githubIssueId, - targetRepository, - }, - config, - updatedTaskPlan, - ); - logger.info(`Draft pull request created: #${pullRequest.number}`); + if (hasIssue) { + await addTaskPlanToIssue( + { + githubIssueId: options.githubIssueId, + targetRepository, + }, + config, + updatedTaskPlan, + ); + logger.info(`Draft pull request created: #${pullRequest.number}`); + } } } diff --git a/apps/open-swe/src/utils/should-create-issue.ts b/apps/open-swe/src/utils/should-create-issue.ts new file mode 100644 index 00000000..1febed51 --- /dev/null +++ b/apps/open-swe/src/utils/should-create-issue.ts @@ -0,0 +1,5 @@ +import { GraphConfig } from "@open-swe/shared/open-swe/types"; + +export function shouldCreateIssue(config: GraphConfig): boolean { + return !!config.configurable?.shouldCreateIssue; +} diff --git a/apps/open-swe/src/utils/user-request.ts b/apps/open-swe/src/utils/user-request.ts index cc2760c9..7f519da4 100644 --- a/apps/open-swe/src/utils/user-request.ts +++ b/apps/open-swe/src/utils/user-request.ts @@ -7,6 +7,7 @@ import { getMessageContentString } from "@open-swe/shared/messages"; import { extractContentWithoutDetailsFromIssueBody } from "./github/issue-messages.js"; import { isLocalMode } from "@open-swe/shared/open-swe/local-mode"; import { GraphConfig } from "@open-swe/shared/open-swe/types"; +import { shouldCreateIssue } from "./should-create-issue.js"; // TODO: Might want a better way of doing this. // maybe add a new kwarg `isRequest` and have this return the last human message with that field? @@ -55,7 +56,10 @@ export function getRecentUserRequest( ): string | HumanMessage { let recentUserMessage: HumanMessage | undefined; - if (options?.config && isLocalMode(options.config)) { + if ( + options?.config && + (isLocalMode(options.config) || !shouldCreateIssue(options.config)) + ) { // In local mode, get the last human message regardless of flags recentUserMessage = messages.findLast(isHumanMessage); } else { diff --git a/apps/web/src/components/icons/github-issue.tsx b/apps/web/src/components/icons/github-issue.tsx new file mode 100644 index 00000000..4b01413d --- /dev/null +++ b/apps/web/src/components/icons/github-issue.tsx @@ -0,0 +1,22 @@ +interface GitHubIssueSVGProps { + width?: string; + height?: string; + className?: string; +} + +export const GitHubIssueSVG = ({ + width = "100%", + height = "100%", + className, +}: GitHubIssueSVGProps) => ( + + + + +); diff --git a/apps/web/src/components/v2/default-view.tsx b/apps/web/src/components/v2/default-view.tsx index 2cc03b87..5543fa81 100644 --- a/apps/web/src/components/v2/default-view.tsx +++ b/apps/web/src/components/v2/default-view.tsx @@ -1,7 +1,7 @@ "use client"; import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; -import { Archive, ListChecks } from "lucide-react"; +import { Archive, Eye, EyeOff, List, ListChecks } from "lucide-react"; import { useRouter } from "next/navigation"; import { TerminalInput } from "./terminal-input"; import { useFileUpload } from "@/hooks/useFileUpload"; @@ -32,6 +32,7 @@ import { threadsToMetadata } from "@/lib/thread-utils"; import { Settings, BookOpen } from "lucide-react"; import NextLink from "next/link"; import { OpenSWELogoSVG } from "../icons/openswe"; +import { DEFAULT_CONFIG_KEY, useConfigStore } from "@/hooks/useConfigStore"; function OpenSettingsButton() { return ( @@ -86,6 +87,8 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) { const apiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL ?? ""; const [draftToLoad, setDraftToLoad] = useState(""); const assistantId: string | undefined = MANAGER_GRAPH_ID; + const { getConfig } = useConfigStore(); + const config = getConfig(DEFAULT_CONFIG_KEY); const { contentBlocks, setContentBlocks, @@ -95,6 +98,9 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) { handlePaste, } = useFileUpload(); const [autoAccept, setAutoAccept] = useState(false); + const [shouldCreateIssue, setShouldCreateIssue] = useState( + config?.shouldCreateIssue != null ? !!config.shouldCreateIssue : true, + ); const threadsMetadata = useMemo(() => threadsToMetadata(threads), [threads]); const displayThreads = threadsMetadata.slice(0, 4); @@ -172,11 +178,13 @@ export function DefaultView({ threads, threadsLoading }: DefaultViewProps) { draftToLoad={draftToLoad} autoAcceptPlan={autoAccept} setAutoAcceptPlan={setAutoAccept} + shouldCreateIssue={shouldCreateIssue} + setShouldCreateIssue={setShouldCreateIssue} />
setAutoAccept((prev) => !prev)} side="bottom" > - + {autoAccept ? ( + + ) : ( + + )} + + setShouldCreateIssue((prev) => !prev)} + side="bottom" + > + {shouldCreateIssue ? ( + + ) : ( + + )}
diff --git a/apps/web/src/components/v2/manager-chat.tsx b/apps/web/src/components/v2/manager-chat.tsx index 4f66c724..4da68c47 100644 --- a/apps/web/src/components/v2/manager-chat.tsx +++ b/apps/web/src/components/v2/manager-chat.tsx @@ -77,6 +77,7 @@ interface ManagerChatProps { name: string | null; email: string | null; }; + disableSubmit?: boolean; } function extractResponseFromMessage(message: Message): string { @@ -162,6 +163,7 @@ export function ManagerChat({ cancelRun, errorState, githubUser, + disableSubmit, }: ManagerChatProps) { return (
@@ -252,7 +254,13 @@ export function ManagerChat({ placeholder="Type your message..." className="border-border bg-background text-foreground placeholder:text-muted-foreground min-h-[60px] flex-1 resize-none text-sm" onKeyDown={(e) => { - if (e.key === "Enter" && (e.metaKey || e.ctrlKey) && !isLoading) { + if ( + e.key === "Enter" && + (e.metaKey || e.ctrlKey) && + !isLoading && + !disableSubmit && + !!chatInput.trim() + ) { e.preventDefault(); handleSendMessage(); } @@ -270,7 +278,7 @@ export function ManagerChat({ ) : (