diff --git a/apps/open-swe/src/graphs/planner/nodes/generate-plan.ts b/apps/open-swe/src/graphs/planner/nodes/generate-plan.ts deleted file mode 100644 index cb96a84f..00000000 --- a/apps/open-swe/src/graphs/planner/nodes/generate-plan.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { isAIMessage, ToolMessage } from "@langchain/core/messages"; -import { createSessionPlanToolFields } from "../../../tools/index.js"; -import { GraphConfig } from "@open-swe/shared/open-swe/types"; -import { loadModel, Task } from "../../../utils/load-model.js"; -import { - PlannerGraphState, - PlannerGraphUpdate, -} from "@open-swe/shared/open-swe/planner/types"; -import { getUserRequest } from "../../../utils/user-request.js"; -import { - formatFollowupMessagePrompt, - isFollowupRequest, -} from "../utils/followup.js"; -import { stopSandbox } from "../../../utils/sandbox.js"; -import { filterHiddenMessages } from "../../../utils/message/filter-hidden.js"; -import { z } from "zod"; -import { formatCustomRulesPrompt } from "../../../utils/custom-rules.js"; -import { getPlannerNotes } from "../utils/get-notes.js"; - -const PLANNER_NOTES_PROMPT = `Here is a collection of technical notes you took while gathering context for the plan. Ensure you take these into account when writing your plan. - - -{PLANNER_NOTES} -`; - -const systemPrompt = `You are a terminal-based agentic coding assistant built by LangChain, designed to enable natural language interaction with local codebases through wrapped LLM models. - -{FOLLOWUP_MESSAGE_PROMPT} -You have already gathered comprehensive context from the repository through the conversation history below. All previous messages will be deleted after this planning step, so your plan must be self-contained and actionable without referring back to this context. - - - -Generate a high-level execution plan to address the user's request. Your plan will guide the implementation phase, so each action must be specific and actionable. - - -{USER_REQUEST} - - - - -Create your plan following these guidelines: - -1. **Structure each action item to include:** - - The specific task to accomplish - - Key technical details needed for execution - - File paths, function names, or other concrete references from the context you've gathered - -2. **Write actionable items that:** - - Focus on implementation steps, not information gathering - - Can be executed independently without additional context discovery - - Build upon each other in logical sequence - - Are not open ended, and require additional context to execute - -3. **Optimize for efficiency by:** - - Completing the request in the minimum number of steps - - Reusing existing code and patterns wherever possible - - Writing reusable components when code will be used multiple times - -4. **Include only what's requested:** - - Add testing steps only if the user explicitly requested tests - - Add documentation steps only if the user explicitly requested documentation - - Focus solely on fulfilling the stated requirements - - - -When ready, call the 'session_plan' tool with your plan. Each plan item should be a complete, self-contained action that can be executed without referring back to this conversation. - -Structure your plan items as clear directives, for example: -- "Implement function X in file Y that performs Z using the existing pattern from file A" -- "Modify the authentication middleware in /src/auth.js to add rate limiting using the Express rate-limit package" - - -{CUSTOM_RULES} - -{PLANNER_NOTES} - -Remember: Your goal is to create a focused, executable plan that efficiently accomplishes the user's request using the context you've already gathered.`; - -function formatSystemPrompt(state: PlannerGraphState): string { - // It's a followup if there's more than one human message. - const isFollowup = isFollowupRequest(state.taskPlan, state.proposedPlan); - const userRequest = getUserRequest(state.messages); - const plannerNotes = getPlannerNotes(state.messages) - .map((n) => `- ${n}`) - .join("\n"); - return systemPrompt - .replace( - "{FOLLOWUP_MESSAGE_PROMPT}", - isFollowup - ? "\n" + - formatFollowupMessagePrompt(state.taskPlan, state.proposedPlan) + - "\n\n" - : "", - ) - .replace("{USER_REQUEST}", userRequest) - .replaceAll("{CUSTOM_RULES}", formatCustomRulesPrompt(state.customRules)) - .replaceAll( - "{PLANNER_NOTES}", - plannerNotes.length - ? PLANNER_NOTES_PROMPT.replace("{PLANNER_NOTES}", plannerNotes) - : "", - ); -} - -export async function generatePlan( - state: PlannerGraphState, - config: GraphConfig, -): Promise { - const model = await loadModel(config, Task.PLANNER); - const sessionPlanTool = createSessionPlanToolFields(); - const modelWithTools = model.bindTools([sessionPlanTool], { - tool_choice: sessionPlanTool.name, - parallel_tool_calls: false, - }); - - let optionalToolMessage: ToolMessage | undefined; - const lastMessage = state.messages[state.messages.length - 1]; - if (isAIMessage(lastMessage) && lastMessage.tool_calls?.[0]) { - const lastMessageToolCall = lastMessage.tool_calls?.[0]; - optionalToolMessage = new ToolMessage({ - tool_call_id: lastMessageToolCall.id ?? "", - name: lastMessageToolCall.name, - content: "Tool call not executed. Max actions reached.", - }); - } - - const response = await modelWithTools - .withConfig({ tags: ["nostream"] }) - .invoke([ - { - role: "system", - content: formatSystemPrompt(state), - }, - ...filterHiddenMessages(state.messages), - ...(optionalToolMessage ? [optionalToolMessage] : []), - ]); - - if (!response.tool_calls?.length) { - throw new Error("Failed to generate plan"); - } - - let newSessionId: string | undefined; - if (state.sandboxSessionId) { - // Stop before returning, as the next step will be to interrupt the graph. - newSessionId = await stopSandbox(state.sandboxSessionId); - } - - const proposedPlanArgs = response.tool_calls[0].args as z.infer< - typeof sessionPlanTool.schema - >; - - return { - messages: [response], - proposedPlanTitle: proposedPlanArgs.title, - proposedPlan: proposedPlanArgs.plan, - ...(newSessionId && { sandboxSessionId: newSessionId }), - }; -} diff --git a/apps/open-swe/src/graphs/planner/nodes/generate-plan/index.ts b/apps/open-swe/src/graphs/planner/nodes/generate-plan/index.ts new file mode 100644 index 00000000..90ee4fe1 --- /dev/null +++ b/apps/open-swe/src/graphs/planner/nodes/generate-plan/index.ts @@ -0,0 +1,99 @@ +import { isAIMessage, ToolMessage } from "@langchain/core/messages"; +import { createSessionPlanToolFields } from "../../../../tools/index.js"; +import { GraphConfig } from "@open-swe/shared/open-swe/types"; +import { loadModel, Task } from "../../../../utils/load-model.js"; +import { + PlannerGraphState, + PlannerGraphUpdate, +} from "@open-swe/shared/open-swe/planner/types"; +import { getUserRequest } from "../../../../utils/user-request.js"; +import { + formatFollowupMessagePrompt, + isFollowupRequest, +} from "../../utils/followup.js"; +import { stopSandbox } from "../../../../utils/sandbox.js"; +import { filterHiddenMessages } from "../../../../utils/message/filter-hidden.js"; +import { z } from "zod"; +import { formatCustomRulesPrompt } from "../../../../utils/custom-rules.js"; +import { getPlannerNotes } from "../../utils/get-notes.js"; +import { PLANNER_NOTES_PROMPT, SYSTEM_PROMPT } from "./prompt.js"; + +function formatSystemPrompt(state: PlannerGraphState): string { + // It's a followup if there's more than one human message. + const isFollowup = isFollowupRequest(state.taskPlan, state.proposedPlan); + const userRequest = getUserRequest(state.messages); + const plannerNotes = getPlannerNotes(state.messages) + .map((n) => `- ${n}`) + .join("\n"); + return SYSTEM_PROMPT.replace( + "{FOLLOWUP_MESSAGE_PROMPT}", + isFollowup + ? "\n" + + formatFollowupMessagePrompt(state.taskPlan, state.proposedPlan) + + "\n\n" + : "", + ) + .replace("{USER_REQUEST}", userRequest) + .replaceAll("{CUSTOM_RULES}", formatCustomRulesPrompt(state.customRules)) + .replaceAll( + "{PLANNER_NOTES}", + plannerNotes.length + ? PLANNER_NOTES_PROMPT.replace("{PLANNER_NOTES}", plannerNotes) + : "", + ); +} + +export async function generatePlan( + state: PlannerGraphState, + config: GraphConfig, +): Promise { + const model = await loadModel(config, Task.PLANNER); + const sessionPlanTool = createSessionPlanToolFields(); + const modelWithTools = model.bindTools([sessionPlanTool], { + tool_choice: sessionPlanTool.name, + parallel_tool_calls: false, + }); + + let optionalToolMessage: ToolMessage | undefined; + const lastMessage = state.messages[state.messages.length - 1]; + if (isAIMessage(lastMessage) && lastMessage.tool_calls?.[0]) { + const lastMessageToolCall = lastMessage.tool_calls?.[0]; + optionalToolMessage = new ToolMessage({ + tool_call_id: lastMessageToolCall.id ?? "", + name: lastMessageToolCall.name, + content: "Tool call not executed. Max actions reached.", + }); + } + + const response = await modelWithTools + .withConfig({ tags: ["nostream"] }) + .invoke([ + { + role: "system", + content: formatSystemPrompt(state), + }, + ...filterHiddenMessages(state.messages), + ...(optionalToolMessage ? [optionalToolMessage] : []), + ]); + + if (!response.tool_calls?.length) { + throw new Error("Failed to generate plan"); + } + + let newSessionId: string | undefined; + if (state.sandboxSessionId) { + // Stop before returning, as the next step will be to interrupt the graph. + newSessionId = await stopSandbox(state.sandboxSessionId); + } + + const proposedPlanArgs = response.tool_calls[0].args as z.infer< + typeof sessionPlanTool.schema + >; + + return { + messages: [response], + proposedPlanTitle: proposedPlanArgs.title, + proposedPlan: proposedPlanArgs.plan, + ...(newSessionId && { sandboxSessionId: newSessionId }), + }; +} diff --git a/apps/open-swe/src/graphs/planner/nodes/generate-plan/prompt.ts b/apps/open-swe/src/graphs/planner/nodes/generate-plan/prompt.ts new file mode 100644 index 00000000..3de9bf5b --- /dev/null +++ b/apps/open-swe/src/graphs/planner/nodes/generate-plan/prompt.ts @@ -0,0 +1,68 @@ +export const PLANNER_NOTES_PROMPT = `Here is a collection of technical notes you took while gathering context for the plan. Ensure you take these into account when writing your plan. + + +{PLANNER_NOTES} +`; + +export const SYSTEM_PROMPT = `You are a terminal-based agentic coding assistant built by LangChain, designed to enable natural language interaction with local codebases through wrapped LLM models. + +{FOLLOWUP_MESSAGE_PROMPT} +You have already gathered comprehensive context from the repository through the conversation history below. All previous messages will be deleted after this planning step, so your plan must be self-contained and actionable without referring back to this context. + + + +Generate an execution plan to address the user's request. Your plan will guide the implementation phase, so each action must be specific, actionable and detailed. +It should contain enough information to not require many additional context gathering steps to execute. + + +{USER_REQUEST} + + + + +Create your plan following these guidelines: + +1. **Structure each action item to include:** + - The specific task to accomplish + - Key technical details needed for execution + - File paths, function names, or other concrete references from the context you've gathered. + - If you're mentioning a file, or code within a file that already exists, you are required to include the file path in the plan item. + - This is incredibly important as we do not want to force the programmer to search for this information again, if you've already found it. + +2. **Write actionable items that:** + - Focus on implementation steps, not information gathering + - Can be executed independently without additional context discovery + - Build upon each other in logical sequence + - Are not open ended, and require additional context to execute + +3. **Optimize for efficiency by:** + - Completing the request in the minimum number of steps. This is absolutely vital to the success of the plan. You should generate as few plan items as possible. + - Reusing existing code and patterns wherever possible + - Writing reusable components when code will be used multiple times + +4. **Include only what's requested:** + - Add testing steps only if the user explicitly requested tests + - Add documentation steps only if the user explicitly requested documentation + - Focus solely on fulfilling the stated requirements + +5. **Follow the custom rules:** + - Carefully read, and follow any instructions provided in the 'custom_rules' section. E.g. if the rules state you must run a linter or formatter, etc., include a plan item to do so. + +6. **Combine simple, related steps:** + - If you have multiple simple steps that are related, and should be executed one after the other, combine them into a single step. + - For example, if you have multiple steps to run a linter, formatter, etc., combine them into a single step. The same goes for passing arguments, or editing files. + + + +When ready, call the 'session_plan' tool with your plan. Each plan item should be a complete, self-contained action that can be executed without referring back to this conversation. + +Structure your plan items as clear directives, for example: +- "Implement function X in file Y that performs Z using the existing pattern from file A" +- "Modify the authentication middleware in /src/auth.js to add rate limiting using the Express rate-limit package" + + +{CUSTOM_RULES} + +{PLANNER_NOTES} + +Remember: Your goal is to create a focused, executable plan that efficiently accomplishes the user's request using the context you've already gathered.`; diff --git a/apps/open-swe/src/graphs/planner/nodes/index.ts b/apps/open-swe/src/graphs/planner/nodes/index.ts index cae2ddfa..6fdfba0d 100644 --- a/apps/open-swe/src/graphs/planner/nodes/index.ts +++ b/apps/open-swe/src/graphs/planner/nodes/index.ts @@ -1,6 +1,6 @@ export * from "./generate-message/index.js"; export * from "./take-action.js"; -export * from "./generate-plan.js"; +export * from "./generate-plan/index.js"; export * from "./notetaker.js"; export * from "./proposed-plan.js"; export * from "./prepare-state.js"; diff --git a/apps/open-swe/src/graphs/planner/nodes/notetaker.ts b/apps/open-swe/src/graphs/planner/nodes/notetaker.ts index 4d99094f..d57b2751 100644 --- a/apps/open-swe/src/graphs/planner/nodes/notetaker.ts +++ b/apps/open-swe/src/graphs/planner/nodes/notetaker.ts @@ -35,6 +35,8 @@ You MUST adhere to the following criteria when generating your notes: - Do not retain any full code snippets. - Do not retain any full file contents. - Only take notes on the context provided below, and do not make up, or attempt to infer any information/context which is not explicitly provided. +- If mentioning specific code from the repo, ensure you also provide the path to the file the code is in. +- Carefully inspect the proposed plan. Your notes should be focused on context which will be most useful to you when you execute the plan. You may reference specific proposed plan items in your notes. {EXTRA_RULES} Here is the user's request diff --git a/apps/web/src/components/gen-ui/action-step.tsx b/apps/web/src/components/gen-ui/action-step.tsx index fc10c994..941f33b0 100644 --- a/apps/web/src/components/gen-ui/action-step.tsx +++ b/apps/web/src/components/gen-ui/action-step.tsx @@ -95,6 +95,14 @@ export type ActionStepProps = { summaryText?: string; }; +const ACTION_GENERATING_TEXT_MAP = { + [shellTool.name]: "Executing...", + [applyPatchTool.name]: "Applying patch...", + ["rg"]: "Searching...", + [installDependenciesTool.name]: "Installing dependencies...", + [plannerNotesTool.name]: "Saving notes...", +}; + function ActionItem(props: ActionItemProps) { const [expanded, setExpanded] = useState(false); @@ -121,9 +129,7 @@ function ActionItem(props: ActionItemProps) { } if (props.status === "generating") { - return props.actionType === "shell" - ? "Executing..." - : "Applying patch..."; + return ACTION_GENERATING_TEXT_MAP[props.actionType]; } if (props.status === "done") {