Skip to content

Commit 05ac345

Browse files
authored
refactor(session): move prompt reminders out of core loop (#28082)
1 parent e3feca0 commit 05ac345

3 files changed

Lines changed: 167 additions & 140 deletions

File tree

packages/opencode/src/session/prompt.ts

Lines changed: 6 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ import { ProviderTransform } from "@/provider/transform"
1616
import { SystemPrompt } from "./system"
1717
import { Instruction } from "./instruction"
1818
import { Plugin } from "../plugin"
19-
import PROMPT_PLAN from "../session/prompt/plan.txt"
20-
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
2119
import MAX_STEPS from "../session/prompt/max-steps.txt"
2220
import { ToolRegistry } from "@/tool/registry"
2321
import { ToolJsonSchema } from "@/tool/json-schema"
@@ -63,6 +61,7 @@ import * as DateTime from "effect/DateTime"
6361
import { eq } from "@/storage/db"
6462
import * as Database from "@/storage/db"
6563
import { SessionTable } from "./session.sql"
64+
import { SessionReminders } from "./reminders"
6665

6766
// @ts-ignore
6867
globalThis.AI_SDK_LOG_WARNINGS = false
@@ -382,143 +381,6 @@ export const layer = Layer.effect(
382381
.pipe(Effect.catchCause((cause) => elog.error("failed to generate title", { error: Cause.squash(cause) })))
383382
})
384383

385-
const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: {
386-
messages: MessageV2.WithParts[]
387-
agent: Agent.Info
388-
session: Session.Info
389-
}) {
390-
const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
391-
if (!userMessage) return input.messages
392-
393-
if (!flags.experimentalPlanMode) {
394-
if (input.agent.name === "plan") {
395-
userMessage.parts.push({
396-
id: PartID.ascending(),
397-
messageID: userMessage.info.id,
398-
sessionID: userMessage.info.sessionID,
399-
type: "text",
400-
text: PROMPT_PLAN,
401-
synthetic: true,
402-
})
403-
}
404-
const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
405-
if (wasPlan && input.agent.name === "build") {
406-
userMessage.parts.push({
407-
id: PartID.ascending(),
408-
messageID: userMessage.info.id,
409-
sessionID: userMessage.info.sessionID,
410-
type: "text",
411-
text: BUILD_SWITCH,
412-
synthetic: true,
413-
})
414-
}
415-
return input.messages
416-
}
417-
418-
const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant")
419-
if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") {
420-
const ctx = yield* InstanceState.context
421-
const plan = Session.plan(input.session, ctx)
422-
if (!(yield* fsys.existsSafe(plan))) return input.messages
423-
const part = yield* sessions.updatePart({
424-
id: PartID.ascending(),
425-
messageID: userMessage.info.id,
426-
sessionID: userMessage.info.sessionID,
427-
type: "text",
428-
text: `${BUILD_SWITCH}\n\nA plan file exists at ${plan}. You should execute on the plan defined within it`,
429-
synthetic: true,
430-
})
431-
userMessage.parts.push(part)
432-
return input.messages
433-
}
434-
435-
if (input.agent.name !== "plan" || assistantMessage?.info.agent === "plan") return input.messages
436-
437-
const ctx = yield* InstanceState.context
438-
const plan = Session.plan(input.session, ctx)
439-
const exists = yield* fsys.existsSafe(plan)
440-
if (!exists) yield* fsys.ensureDir(path.dirname(plan)).pipe(Effect.catch(Effect.die))
441-
const part = yield* sessions.updatePart({
442-
id: PartID.ascending(),
443-
messageID: userMessage.info.id,
444-
sessionID: userMessage.info.sessionID,
445-
type: "text",
446-
text: `<system-reminder>
447-
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.
448-
449-
## Plan File Info:
450-
${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.`}
451-
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.
452-
453-
## Plan Workflow
454-
455-
### Phase 1: Initial Understanding
456-
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.
457-
458-
1. Focus on understanding the user's request and the code associated with their request
459-
460-
2. **Launch up to 3 explore agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase.
461-
- 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.
462-
- Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning.
463-
- Quality over quantity - 3 agents maximum, but you should try to use the minimum number of agents necessary (usually just 1)
464-
- 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
465-
466-
3. After exploring the code, use the question tool to clarify ambiguities in the user request up front.
467-
468-
### Phase 2: Design
469-
Goal: Design an implementation approach.
470-
471-
Launch general agent(s) to design the implementation based on the user's intent and your exploration results from Phase 1.
472-
473-
You can launch up to 1 agent(s) in parallel.
474-
475-
**Guidelines:**
476-
- **Default**: Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives
477-
- **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames)
478-
479-
Examples of when to use multiple agents:
480-
- The task touches multiple parts of the codebase
481-
- It's a large refactor or architectural change
482-
- There are many edge cases to consider
483-
- You'd benefit from exploring different approaches
484-
485-
Example perspectives by task type:
486-
- New feature: simplicity vs performance vs maintainability
487-
- Bug fix: root cause vs workaround vs prevention
488-
- Refactoring: minimal change vs clean architecture
489-
490-
In the agent prompt:
491-
- Provide comprehensive background context from Phase 1 exploration including filenames and code path traces
492-
- Describe requirements and constraints
493-
- Request a detailed implementation plan
494-
495-
### Phase 3: Review
496-
Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions.
497-
1. Read the critical files identified by agents to deepen your understanding
498-
2. Ensure that the plans align with the user's original request
499-
3. Use question tool to clarify any remaining questions with the user
500-
501-
### Phase 4: Final Plan
502-
Goal: Write your final plan to the plan file (the only file you can edit).
503-
- Include only your recommended approach, not all alternatives
504-
- Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively
505-
- Include the paths of critical files to be modified
506-
- Include a verification section describing how to test the changes end-to-end (run the code, use MCP tools, run tests)
507-
508-
### Phase 5: Call plan_exit tool
509-
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.
510-
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.
511-
512-
**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.
513-
514-
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.
515-
</system-reminder>`,
516-
synthetic: true,
517-
})
518-
userMessage.parts.push(part)
519-
return input.messages
520-
})
521-
522384
const resolveTools = Effect.fn("SessionPrompt.resolveTools")(function* (input: {
523385
agent: Agent.Info
524386
model: Provider.Model
@@ -1726,7 +1588,11 @@ NOTE: At any point in time through this workflow you should feel free to ask the
17261588
}
17271589
const maxSteps = agent.steps ?? Infinity
17281590
const isLastStep = step >= maxSteps
1729-
msgs = yield* insertReminders({ messages: msgs, agent, session })
1591+
msgs = yield* SessionReminders.apply({ messages: msgs, agent, session }).pipe(
1592+
Effect.provideService(RuntimeFlags.Service, flags),
1593+
Effect.provideService(AppFileSystem.Service, fsys),
1594+
Effect.provideService(Session.Service, sessions),
1595+
)
17301596

17311597
const msg: MessageV2.Assistant = {
17321598
id: MessageID.ascending(),
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<system-reminder>
2+
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.
3+
4+
## Plan File Info:
5+
${planInfo}
6+
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.
7+
8+
## Plan Workflow
9+
10+
### Phase 1: Initial Understanding
11+
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.
12+
13+
1. Focus on understanding the user's request and the code associated with their request
14+
15+
2. **Launch up to 3 explore agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase.
16+
- 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.
17+
- Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning.
18+
- Quality over quantity - 3 agents maximum, but you should try to use the minimum number of agents necessary (usually just 1)
19+
- 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
20+
21+
3. After exploring the code, use the question tool to clarify ambiguities in the user request up front.
22+
23+
### Phase 2: Design
24+
Goal: Design an implementation approach.
25+
26+
Launch general agent(s) to design the implementation based on the user's intent and your exploration results from Phase 1.
27+
28+
You can launch up to 1 agent(s) in parallel.
29+
30+
**Guidelines:**
31+
- **Default**: Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives
32+
- **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames)
33+
34+
Examples of when to use multiple agents:
35+
- The task touches multiple parts of the codebase
36+
- It's a large refactor or architectural change
37+
- There are many edge cases to consider
38+
- You'd benefit from exploring different approaches
39+
40+
Example perspectives by task type:
41+
- New feature: simplicity vs performance vs maintainability
42+
- Bug fix: root cause vs workaround vs prevention
43+
- Refactoring: minimal change vs clean architecture
44+
45+
In the agent prompt:
46+
- Provide comprehensive background context from Phase 1 exploration including filenames and code path traces
47+
- Describe requirements and constraints
48+
- Request a detailed implementation plan
49+
50+
### Phase 3: Review
51+
Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions.
52+
1. Read the critical files identified by agents to deepen your understanding
53+
2. Ensure that the plans align with the user's original request
54+
3. Use question tool to clarify any remaining questions with the user
55+
56+
### Phase 4: Final Plan
57+
Goal: Write your final plan to the plan file (the only file you can edit).
58+
- Include only your recommended approach, not all alternatives
59+
- Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively
60+
- Include the paths of critical files to be modified
61+
- Include a verification section describing how to test the changes end-to-end (run the code, use MCP tools, run tests)
62+
63+
### Phase 5: Call plan_exit tool
64+
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.
65+
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.
66+
67+
**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.
68+
69+
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.
70+
</system-reminder>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import path from "path"
2+
import { Effect } from "effect"
3+
import { Agent } from "@/agent/agent"
4+
import { AppFileSystem } from "@opencode-ai/core/filesystem"
5+
import { InstanceState } from "@/effect/instance-state"
6+
import { RuntimeFlags } from "@/effect/runtime-flags"
7+
import { PartID } from "./schema"
8+
import { MessageV2 } from "./message-v2"
9+
import * as Session from "./session"
10+
import PROMPT_PLAN from "./prompt/plan.txt"
11+
import BUILD_SWITCH from "./prompt/build-switch.txt"
12+
import PLAN_MODE from "./prompt/plan-mode.txt"
13+
14+
export const apply = Effect.fn("SessionReminders.apply")(function* (input: {
15+
messages: MessageV2.WithParts[]
16+
agent: Agent.Info
17+
session: Session.Info
18+
}) {
19+
const flags = yield* RuntimeFlags.Service
20+
const fsys = yield* AppFileSystem.Service
21+
const sessions = yield* Session.Service
22+
const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
23+
if (!userMessage) return input.messages
24+
25+
if (!flags.experimentalPlanMode) {
26+
if (input.agent.name === "plan") {
27+
userMessage.parts.push({
28+
id: PartID.ascending(),
29+
messageID: userMessage.info.id,
30+
sessionID: userMessage.info.sessionID,
31+
type: "text",
32+
text: PROMPT_PLAN,
33+
synthetic: true,
34+
})
35+
}
36+
const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan")
37+
if (wasPlan && input.agent.name === "build") {
38+
userMessage.parts.push({
39+
id: PartID.ascending(),
40+
messageID: userMessage.info.id,
41+
sessionID: userMessage.info.sessionID,
42+
type: "text",
43+
text: BUILD_SWITCH,
44+
synthetic: true,
45+
})
46+
}
47+
return input.messages
48+
}
49+
50+
const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant")
51+
if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") {
52+
const ctx = yield* InstanceState.context
53+
const plan = Session.plan(input.session, ctx)
54+
const exists = yield* fsys.existsSafe(plan)
55+
const part = yield* sessions.updatePart({
56+
id: PartID.ascending(),
57+
messageID: userMessage.info.id,
58+
sessionID: userMessage.info.sessionID,
59+
type: "text",
60+
text: exists
61+
? `${BUILD_SWITCH}\n\nA plan file exists at ${plan}. You should execute on the plan defined within it`
62+
: BUILD_SWITCH,
63+
synthetic: true,
64+
})
65+
userMessage.parts.push(part)
66+
return input.messages
67+
}
68+
69+
if (input.agent.name !== "plan" || assistantMessage?.info.agent === "plan") return input.messages
70+
71+
const ctx = yield* InstanceState.context
72+
const plan = Session.plan(input.session, ctx)
73+
const exists = yield* fsys.existsSafe(plan)
74+
if (!exists) yield* fsys.ensureDir(path.dirname(plan)).pipe(Effect.catch(Effect.die))
75+
const part = yield* sessions.updatePart({
76+
id: PartID.ascending(),
77+
messageID: userMessage.info.id,
78+
sessionID: userMessage.info.sessionID,
79+
type: "text",
80+
text: PLAN_MODE.replace("${planInfo}", () =>
81+
exists
82+
? `A plan file already exists at ${plan}. You can read it and make incremental edits using the edit tool.`
83+
: `No plan file exists yet. You should create your plan at ${plan} using the write tool.`,
84+
),
85+
synthetic: true,
86+
})
87+
userMessage.parts.push(part)
88+
return input.messages
89+
})
90+
91+
export * as SessionReminders from "./reminders"

0 commit comments

Comments
 (0)