refactor(session): move prompt reminders out of core loop (#28082)
This commit is contained in:
@@ -16,8 +16,6 @@ import { ProviderTransform } from "@/provider/transform"
|
|||||||
import { SystemPrompt } from "./system"
|
import { SystemPrompt } from "./system"
|
||||||
import { Instruction } from "./instruction"
|
import { Instruction } from "./instruction"
|
||||||
import { Plugin } from "../plugin"
|
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 MAX_STEPS from "../session/prompt/max-steps.txt"
|
||||||
import { ToolRegistry } from "@/tool/registry"
|
import { ToolRegistry } from "@/tool/registry"
|
||||||
import { ToolJsonSchema } from "@/tool/json-schema"
|
import { ToolJsonSchema } from "@/tool/json-schema"
|
||||||
@@ -63,6 +61,7 @@ import * as DateTime from "effect/DateTime"
|
|||||||
import { eq } from "@/storage/db"
|
import { eq } from "@/storage/db"
|
||||||
import * as Database from "@/storage/db"
|
import * as Database from "@/storage/db"
|
||||||
import { SessionTable } from "./session.sql"
|
import { SessionTable } from "./session.sql"
|
||||||
|
import { SessionReminders } from "./reminders"
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
globalThis.AI_SDK_LOG_WARNINGS = false
|
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) })))
|
.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: `<system-reminder>
|
|
||||||
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.
|
|
||||||
</system-reminder>`,
|
|
||||||
synthetic: true,
|
|
||||||
})
|
|
||||||
userMessage.parts.push(part)
|
|
||||||
return input.messages
|
|
||||||
})
|
|
||||||
|
|
||||||
const resolveTools = Effect.fn("SessionPrompt.resolveTools")(function* (input: {
|
const resolveTools = Effect.fn("SessionPrompt.resolveTools")(function* (input: {
|
||||||
agent: Agent.Info
|
agent: Agent.Info
|
||||||
model: Provider.Model
|
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 maxSteps = agent.steps ?? Infinity
|
||||||
const isLastStep = step >= maxSteps
|
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 = {
|
const msg: MessageV2.Assistant = {
|
||||||
id: MessageID.ascending(),
|
id: MessageID.ascending(),
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<system-reminder>
|
||||||
|
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.
|
||||||
|
</system-reminder>
|
||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user