diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index ba9a4d6f1a..1dd972a36c 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -16,8 +16,6 @@ import { ProviderTransform } from "@/provider/transform" import { SystemPrompt } from "./system" import { Instruction } from "./instruction" import { Plugin } from "../plugin" -import PROMPT_PLAN from "../session/prompt/plan.txt" -import BUILD_SWITCH from "../session/prompt/build-switch.txt" import MAX_STEPS from "../session/prompt/max-steps.txt" import { ToolRegistry } from "@/tool/registry" import { ToolJsonSchema } from "@/tool/json-schema" @@ -63,6 +61,7 @@ import * as DateTime from "effect/DateTime" import { eq } from "@/storage/db" import * as Database from "@/storage/db" import { SessionTable } from "./session.sql" +import { SessionReminders } from "./reminders" // @ts-ignore globalThis.AI_SDK_LOG_WARNINGS = false @@ -382,143 +381,6 @@ export const layer = Layer.effect( .pipe(Effect.catchCause((cause) => elog.error("failed to generate title", { error: Cause.squash(cause) }))) }) - const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: { - messages: MessageV2.WithParts[] - agent: Agent.Info - session: Session.Info - }) { - const userMessage = input.messages.findLast((msg) => msg.info.role === "user") - if (!userMessage) return input.messages - - if (!flags.experimentalPlanMode) { - if (input.agent.name === "plan") { - userMessage.parts.push({ - id: PartID.ascending(), - messageID: userMessage.info.id, - sessionID: userMessage.info.sessionID, - type: "text", - text: PROMPT_PLAN, - synthetic: true, - }) - } - const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") - if (wasPlan && input.agent.name === "build") { - userMessage.parts.push({ - id: PartID.ascending(), - messageID: userMessage.info.id, - sessionID: userMessage.info.sessionID, - type: "text", - text: BUILD_SWITCH, - synthetic: true, - }) - } - return input.messages - } - - const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant") - if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") { - const ctx = yield* InstanceState.context - const plan = Session.plan(input.session, ctx) - if (!(yield* fsys.existsSafe(plan))) return input.messages - const part = yield* sessions.updatePart({ - id: PartID.ascending(), - messageID: userMessage.info.id, - sessionID: userMessage.info.sessionID, - type: "text", - text: `${BUILD_SWITCH}\n\nA plan file exists at ${plan}. You should execute on the plan defined within it`, - synthetic: true, - }) - userMessage.parts.push(part) - return input.messages - } - - if (input.agent.name !== "plan" || assistantMessage?.info.agent === "plan") return input.messages - - const ctx = yield* InstanceState.context - const plan = Session.plan(input.session, ctx) - const exists = yield* fsys.existsSafe(plan) - if (!exists) yield* fsys.ensureDir(path.dirname(plan)).pipe(Effect.catch(Effect.die)) - const part = yield* sessions.updatePart({ - id: PartID.ascending(), - messageID: userMessage.info.id, - sessionID: userMessage.info.sessionID, - type: "text", - text: ` -Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received. - -## Plan File Info: -${exists ? `A plan file already exists at ${plan}. You can read it and make incremental edits using the edit tool.` : `No plan file exists yet. You should create your plan at ${plan} using the write tool.`} -You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions. - -## Plan Workflow - -### Phase 1: Initial Understanding -Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions. Critical: In this phase you should only use the explore subagent type. - -1. Focus on understanding the user's request and the code associated with their request - -2. **Launch up to 3 explore agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase. - - Use 1 agent when the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change. - - Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning. - - Quality over quantity - 3 agents maximum, but you should try to use the minimum number of agents necessary (usually just 1) - - If using multiple agents: Provide each agent with a specific search focus or area to explore. Example: One agent searches for existing implementations, another explores related components, a third investigates testing patterns - -3. After exploring the code, use the question tool to clarify ambiguities in the user request up front. - -### Phase 2: Design -Goal: Design an implementation approach. - -Launch general agent(s) to design the implementation based on the user's intent and your exploration results from Phase 1. - -You can launch up to 1 agent(s) in parallel. - -**Guidelines:** -- **Default**: Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives -- **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames) - -Examples of when to use multiple agents: -- The task touches multiple parts of the codebase -- It's a large refactor or architectural change -- There are many edge cases to consider -- You'd benefit from exploring different approaches - -Example perspectives by task type: -- New feature: simplicity vs performance vs maintainability -- Bug fix: root cause vs workaround vs prevention -- Refactoring: minimal change vs clean architecture - -In the agent prompt: -- Provide comprehensive background context from Phase 1 exploration including filenames and code path traces -- Describe requirements and constraints -- Request a detailed implementation plan - -### Phase 3: Review -Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions. -1. Read the critical files identified by agents to deepen your understanding -2. Ensure that the plans align with the user's original request -3. Use question tool to clarify any remaining questions with the user - -### Phase 4: Final Plan -Goal: Write your final plan to the plan file (the only file you can edit). -- Include only your recommended approach, not all alternatives -- Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively -- Include the paths of critical files to be modified -- Include a verification section describing how to test the changes end-to-end (run the code, use MCP tools, run tests) - -### Phase 5: Call plan_exit tool -At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call plan_exit to indicate to the user that you are done planning. -This is critical - your turn should only end with either asking the user a question or calling plan_exit. Do not stop unless it's for these 2 reasons. - -**Important:** Use question tool to clarify requirements/approach, use plan_exit to request plan approval. Do NOT use question tool to ask "Is this plan okay?" - that's what plan_exit does. - -NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins. -`, - synthetic: true, - }) - userMessage.parts.push(part) - return input.messages - }) - const resolveTools = Effect.fn("SessionPrompt.resolveTools")(function* (input: { agent: Agent.Info model: Provider.Model @@ -1726,7 +1588,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the } const maxSteps = agent.steps ?? Infinity const isLastStep = step >= maxSteps - msgs = yield* insertReminders({ messages: msgs, agent, session }) + msgs = yield* SessionReminders.apply({ messages: msgs, agent, session }).pipe( + Effect.provideService(RuntimeFlags.Service, flags), + Effect.provideService(AppFileSystem.Service, fsys), + Effect.provideService(Session.Service, sessions), + ) const msg: MessageV2.Assistant = { id: MessageID.ascending(), diff --git a/packages/opencode/src/session/prompt/plan-mode.txt b/packages/opencode/src/session/prompt/plan-mode.txt new file mode 100644 index 0000000000..2057f36d7d --- /dev/null +++ b/packages/opencode/src/session/prompt/plan-mode.txt @@ -0,0 +1,70 @@ + +Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received. + +## Plan File Info: +${planInfo} +You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions. + +## Plan Workflow + +### Phase 1: Initial Understanding +Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions. Critical: In this phase you should only use the explore subagent type. + +1. Focus on understanding the user's request and the code associated with their request + +2. **Launch up to 3 explore agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase. + - Use 1 agent when the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change. + - Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning. + - Quality over quantity - 3 agents maximum, but you should try to use the minimum number of agents necessary (usually just 1) + - If using multiple agents: Provide each agent with a specific search focus or area to explore. Example: One agent searches for existing implementations, another explores related components, a third investigates testing patterns + +3. After exploring the code, use the question tool to clarify ambiguities in the user request up front. + +### Phase 2: Design +Goal: Design an implementation approach. + +Launch general agent(s) to design the implementation based on the user's intent and your exploration results from Phase 1. + +You can launch up to 1 agent(s) in parallel. + +**Guidelines:** +- **Default**: Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives +- **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames) + +Examples of when to use multiple agents: +- The task touches multiple parts of the codebase +- It's a large refactor or architectural change +- There are many edge cases to consider +- You'd benefit from exploring different approaches + +Example perspectives by task type: +- New feature: simplicity vs performance vs maintainability +- Bug fix: root cause vs workaround vs prevention +- Refactoring: minimal change vs clean architecture + +In the agent prompt: +- Provide comprehensive background context from Phase 1 exploration including filenames and code path traces +- Describe requirements and constraints +- Request a detailed implementation plan + +### Phase 3: Review +Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions. +1. Read the critical files identified by agents to deepen your understanding +2. Ensure that the plans align with the user's original request +3. Use question tool to clarify any remaining questions with the user + +### Phase 4: Final Plan +Goal: Write your final plan to the plan file (the only file you can edit). +- Include only your recommended approach, not all alternatives +- Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively +- Include the paths of critical files to be modified +- Include a verification section describing how to test the changes end-to-end (run the code, use MCP tools, run tests) + +### Phase 5: Call plan_exit tool +At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call plan_exit to indicate to the user that you are done planning. +This is critical - your turn should only end with either asking the user a question or calling plan_exit. Do not stop unless it's for these 2 reasons. + +**Important:** Use question tool to clarify requirements/approach, use plan_exit to request plan approval. Do NOT use question tool to ask "Is this plan okay?" - that's what plan_exit does. + +NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins. + diff --git a/packages/opencode/src/session/reminders.ts b/packages/opencode/src/session/reminders.ts new file mode 100644 index 0000000000..a11bd5e67b --- /dev/null +++ b/packages/opencode/src/session/reminders.ts @@ -0,0 +1,91 @@ +import path from "path" +import { Effect } from "effect" +import { Agent } from "@/agent/agent" +import { AppFileSystem } from "@opencode-ai/core/filesystem" +import { InstanceState } from "@/effect/instance-state" +import { RuntimeFlags } from "@/effect/runtime-flags" +import { PartID } from "./schema" +import { MessageV2 } from "./message-v2" +import * as Session from "./session" +import PROMPT_PLAN from "./prompt/plan.txt" +import BUILD_SWITCH from "./prompt/build-switch.txt" +import PLAN_MODE from "./prompt/plan-mode.txt" + +export const apply = Effect.fn("SessionReminders.apply")(function* (input: { + messages: MessageV2.WithParts[] + agent: Agent.Info + session: Session.Info +}) { + const flags = yield* RuntimeFlags.Service + const fsys = yield* AppFileSystem.Service + const sessions = yield* Session.Service + const userMessage = input.messages.findLast((msg) => msg.info.role === "user") + if (!userMessage) return input.messages + + if (!flags.experimentalPlanMode) { + if (input.agent.name === "plan") { + userMessage.parts.push({ + id: PartID.ascending(), + messageID: userMessage.info.id, + sessionID: userMessage.info.sessionID, + type: "text", + text: PROMPT_PLAN, + synthetic: true, + }) + } + const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") + if (wasPlan && input.agent.name === "build") { + userMessage.parts.push({ + id: PartID.ascending(), + messageID: userMessage.info.id, + sessionID: userMessage.info.sessionID, + type: "text", + text: BUILD_SWITCH, + synthetic: true, + }) + } + return input.messages + } + + const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant") + if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") { + const ctx = yield* InstanceState.context + const plan = Session.plan(input.session, ctx) + const exists = yield* fsys.existsSafe(plan) + const part = yield* sessions.updatePart({ + id: PartID.ascending(), + messageID: userMessage.info.id, + sessionID: userMessage.info.sessionID, + type: "text", + text: exists + ? `${BUILD_SWITCH}\n\nA plan file exists at ${plan}. You should execute on the plan defined within it` + : BUILD_SWITCH, + synthetic: true, + }) + userMessage.parts.push(part) + return input.messages + } + + if (input.agent.name !== "plan" || assistantMessage?.info.agent === "plan") return input.messages + + const ctx = yield* InstanceState.context + const plan = Session.plan(input.session, ctx) + const exists = yield* fsys.existsSafe(plan) + if (!exists) yield* fsys.ensureDir(path.dirname(plan)).pipe(Effect.catch(Effect.die)) + const part = yield* sessions.updatePart({ + id: PartID.ascending(), + messageID: userMessage.info.id, + sessionID: userMessage.info.sessionID, + type: "text", + text: PLAN_MODE.replace("${planInfo}", () => + exists + ? `A plan file already exists at ${plan}. You can read it and make incremental edits using the edit tool.` + : `No plan file exists yet. You should create your plan at ${plan} using the write tool.`, + ), + synthetic: true, + }) + userMessage.parts.push(part) + return input.messages +}) + +export * as SessionReminders from "./reminders"