From 9995fe3fc8b9ed89d0a03624e54eb651b538f13e Mon Sep 17 00:00:00 2001 From: Duncan Casteleyn <10881109+DuncanCasteleyn@users.noreply.github.com> Date: Fri, 12 Jun 2026 16:43:13 +0200 Subject: [PATCH 1/3] feat(core): add configurable plan reminders --- packages/core/src/agent.ts | 2 + packages/core/src/config/agent.ts | 2 + packages/core/src/config/plugin/agent.ts | 4 + packages/core/src/v1/config/agent.ts | 4 + packages/core/src/v1/config/migrate.ts | 2 + packages/core/test/config/agent.test.ts | 8 + packages/core/test/config/config.test.ts | 24 +++ packages/opencode/src/agent/agent.ts | 4 + packages/opencode/src/session/reminders.ts | 59 ++++-- packages/opencode/test/agent/agent.test.ts | 19 ++ .../opencode/test/session/reminders.test.ts | 179 ++++++++++++++++++ 11 files changed, 289 insertions(+), 18 deletions(-) create mode 100644 packages/opencode/test/session/reminders.test.ts diff --git a/packages/core/src/agent.ts b/packages/core/src/agent.ts index fabf7477d681..4088751ccb2b 100644 --- a/packages/core/src/agent.ts +++ b/packages/core/src/agent.ts @@ -22,6 +22,8 @@ export class Info extends Schema.Class("AgentV2.Info")({ model: ModelV2.Ref.pipe(Schema.optional), request: ProviderV2.Request, system: Schema.String.pipe(Schema.optional), + planReminder: Schema.String.pipe(Schema.optional), + buildSwitchReminder: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional), mode: Schema.Literals(["subagent", "primary", "all"]), hidden: Schema.Boolean, diff --git a/packages/core/src/config/agent.ts b/packages/core/src/config/agent.ts index 1dea6044bce5..8c6a5d009289 100644 --- a/packages/core/src/config/agent.ts +++ b/packages/core/src/config/agent.ts @@ -15,6 +15,8 @@ export class Info extends Schema.Class("ConfigV2.Agent")({ variant: Schema.String.pipe(Schema.optional), request: ConfigProvider.Request.pipe(Schema.optional), system: Schema.String.pipe(Schema.optional), + plan_reminder: Schema.String.pipe(Schema.optional), + build_switch_reminder: Schema.String.pipe(Schema.optional), description: Schema.String.pipe(Schema.optional), mode: Schema.Literals(["subagent", "primary", "all"]).pipe(Schema.optional), hidden: Schema.Boolean.pipe(Schema.optional), diff --git a/packages/core/src/config/plugin/agent.ts b/packages/core/src/config/plugin/agent.ts index 36534b0d3827..c54ed096414d 100644 --- a/packages/core/src/config/plugin/agent.ts +++ b/packages/core/src/config/plugin/agent.ts @@ -24,6 +24,8 @@ const agentKeys = new Set([ "variant", "request", "system", + "plan_reminder", + "build_switch_reminder", "description", "mode", "hidden", @@ -87,6 +89,8 @@ export const Plugin = PluginV2.define({ Object.assign(agent.request.body, item.request.body ?? {}) } if (item.system !== undefined) agent.system = item.system + if (item.plan_reminder !== undefined) agent.planReminder = item.plan_reminder + if (item.build_switch_reminder !== undefined) agent.buildSwitchReminder = item.build_switch_reminder if (item.description !== undefined) agent.description = item.description if (item.mode !== undefined) agent.mode = item.mode if (item.hidden !== undefined) agent.hidden = item.hidden diff --git a/packages/core/src/v1/config/agent.ts b/packages/core/src/v1/config/agent.ts index b220bd7ef87d..beb40f7d26f4 100644 --- a/packages/core/src/v1/config/agent.ts +++ b/packages/core/src/v1/config/agent.ts @@ -18,6 +18,8 @@ const AgentSchema = Schema.StructWithRest( temperature: Schema.optional(Schema.Finite), top_p: Schema.optional(Schema.Finite), prompt: Schema.optional(Schema.String), + plan_reminder: Schema.optional(Schema.String), + build_switch_reminder: Schema.optional(Schema.String), tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({ description: "@deprecated Use 'permission' field instead", }), @@ -45,6 +47,8 @@ const KNOWN_KEYS = new Set([ "model", "variant", "prompt", + "plan_reminder", + "build_switch_reminder", "description", "temperature", "top_p", diff --git a/packages/core/src/v1/config/migrate.ts b/packages/core/src/v1/config/migrate.ts index c474cac51a75..4317a8a819ae 100644 --- a/packages/core/src/v1/config/migrate.ts +++ b/packages/core/src/v1/config/migrate.ts @@ -115,6 +115,8 @@ export function migrateAgent(info: ConfigAgentV1.Info) { variant: info.variant, request: Object.keys(body).length ? { body } : undefined, system: info.prompt, + plan_reminder: info.plan_reminder, + build_switch_reminder: info.build_switch_reminder, description: info.description, mode: info.mode, hidden: info.hidden, diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts index 79e872f74e29..8e22a5ae3765 100644 --- a/packages/core/test/config/agent.test.ts +++ b/packages/core/test/config/agent.test.ts @@ -121,6 +121,8 @@ describe("ConfigAgentPlugin.Plugin", () => { reviewer: { model: "anthropic/claude-sonnet", system: "Review carefully.", + plan_reminder: "Plan carefully ${planInfo}", + build_switch_reminder: "Build carefully ${planInfo}", description: "Reviews changes", mode: "subagent", hidden: true, @@ -159,6 +161,8 @@ describe("ConfigAgentPlugin.Plugin", () => { if (!reviewer) throw new Error("expected configured reviewer agent") expect(reviewer).toMatchObject({ system: "Review carefully.", + planReminder: "Plan carefully ${planInfo}", + buildSwitchReminder: "Build carefully ${planInfo}", description: "Reviews changes", mode: "subagent", hidden: true, @@ -215,6 +219,8 @@ describe("ConfigAgentPlugin.Plugin", () => { model: openrouter/openai/gpt-5 description: Markdown description temperature: 0.5 +plan_reminder: Markdown plan reminder \${planInfo} +build_switch_reminder: Markdown build reminder \${planInfo} tools: write: false --- @@ -259,6 +265,8 @@ Use native v2 fields.`, expect(yield* agents.get(AgentV2.ID.make("reviewer"))).toMatchObject({ model: { providerID: "openrouter", id: "openai/gpt-5" }, system: "Review carefully.", + planReminder: "Markdown plan reminder ${planInfo}", + buildSwitchReminder: "Markdown build reminder ${planInfo}", description: "Markdown description", request: { body: { temperature: 0.5 } }, permissions: [{ action: "edit", resource: "*", effect: "deny" }], diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 6275d8fed350..936cd133bc95 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -144,6 +144,30 @@ describe("Config", () => { }), ) + it.effect("migrates v1 agent reminder overrides", () => + Effect.sync(() => { + const migrated = ConfigMigrateV1.migrate({ + agent: { + plan: { + prompt: "Plan system", + plan_reminder: "Plan reminder ${planInfo}", + }, + build: { + build_switch_reminder: "Build reminder ${planInfo}", + }, + }, + }) + + expect(migrated.agents?.plan).toMatchObject({ + system: "Plan system", + plan_reminder: "Plan reminder ${planInfo}", + }) + expect(migrated.agents?.build).toMatchObject({ + build_switch_reminder: "Build reminder ${planInfo}", + }) + }), + ) + it.live("returns an empty configuration when directory files do not exist", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index b1430314fffe..511344f4a7fb 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -50,6 +50,8 @@ export const Info = Schema.Struct({ ), variant: Schema.optional(Schema.String), prompt: Schema.optional(Schema.String), + planReminder: Schema.optional(Schema.String), + buildSwitchReminder: Schema.optional(Schema.String), options: Schema.Record(Schema.String, Schema.Unknown), steps: Schema.optional(Schema.Finite), }).annotate({ identifier: "Agent" }) @@ -279,6 +281,8 @@ export const layer = Layer.effect( if (value.model) item.model = Provider.parseModel(value.model) item.variant = value.variant ?? item.variant item.prompt = value.prompt ?? item.prompt + item.planReminder = value.plan_reminder ?? item.planReminder + item.buildSwitchReminder = value.build_switch_reminder ?? item.buildSwitchReminder item.description = value.description ?? item.description item.temperature = value.temperature ?? item.temperature item.topP = value.top_p ?? item.topP diff --git a/packages/opencode/src/session/reminders.ts b/packages/opencode/src/session/reminders.ts index f5484b8e9ba4..e057c7765ec2 100644 --- a/packages/opencode/src/session/reminders.ts +++ b/packages/opencode/src/session/reminders.ts @@ -6,7 +6,6 @@ import { FSUtil } from "@opencode-ai/core/fs-util" import { InstanceState } from "@/effect/instance-state" import { RuntimeFlags } from "@/effect/runtime-flags" import { PartID } from "./schema" -import { MessageV2 } from "./message-v2" import { Session } from "./session" import PROMPT_PLAN from "./prompt/plan.txt" import BUILD_SWITCH from "./prompt/build-switch.txt" @@ -25,23 +24,33 @@ export const apply = Effect.fn("SessionReminders.apply")(function* (input: { if (!flags.experimentalPlanMode) { if (input.agent.name === "plan") { + const text = yield* Effect.gen(function* () { + if (!input.agent.planReminder) return PROMPT_PLAN + const state = yield* planState(input.session, fsys) + return render(input.agent.planReminder, state.planReminder) + }) userMessage.parts.push({ id: PartID.ascending(), messageID: userMessage.info.id, sessionID: userMessage.info.sessionID, type: "text", - text: PROMPT_PLAN, + text, synthetic: true, }) } const wasPlan = input.messages.some((msg) => msg.info.role === "assistant" && msg.info.agent === "plan") if (wasPlan && input.agent.name === "build") { + const text = yield* Effect.gen(function* () { + if (!input.agent.buildSwitchReminder) return BUILD_SWITCH + const state = yield* planState(input.session, fsys) + return render(input.agent.buildSwitchReminder, state.buildSwitch) + }) userMessage.parts.push({ id: PartID.ascending(), messageID: userMessage.info.id, sessionID: userMessage.info.sessionID, type: "text", - text: BUILD_SWITCH, + text, synthetic: true, }) } @@ -50,17 +59,17 @@ export const apply = Effect.fn("SessionReminders.apply")(function* (input: { 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 state = yield* planState(input.session, fsys) 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, + text: input.agent.buildSwitchReminder + ? render(input.agent.buildSwitchReminder, state.buildSwitch) + : state.exists + ? `${BUILD_SWITCH}\n\n${state.buildSwitch}` + : BUILD_SWITCH, synthetic: true, }) userMessage.parts.push(part) @@ -69,24 +78,38 @@ export const apply = Effect.fn("SessionReminders.apply")(function* (input: { 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 state = yield* planState(input.session, fsys) + if (!state.exists) yield* fsys.ensureDir(path.dirname(state.filepath)).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.`, - ), + text: input.agent.planReminder ? render(input.agent.planReminder, state.planReminder) : render(PLAN_MODE, state.planReminder), synthetic: true, }) userMessage.parts.push(part) return input.messages }) +function render(template: string, planInfo: string) { + return template.replaceAll("${planInfo}", planInfo) +} + +function planState(session: Session.Info, fsys: FSUtil.Interface) { + return Effect.gen(function* () { + const ctx = yield* InstanceState.context + const filepath = Session.plan(session, ctx) + const exists = yield* fsys.existsSafe(filepath) + return { + exists, + filepath, + planReminder: exists + ? `A plan file already exists at ${filepath}. You can read it and make incremental edits using the edit tool.` + : `No plan file exists yet. You should create your plan at ${filepath} using the write tool.`, + buildSwitch: exists ? `A plan file exists at ${filepath}. You should execute on the plan defined within it` : "", + } + }) +} + export * as SessionReminders from "./reminders" diff --git a/packages/opencode/test/agent/agent.test.ts b/packages/opencode/test/agent/agent.test.ts index 1df95b5c0f87..6d61df42a715 100644 --- a/packages/opencode/test/agent/agent.test.ts +++ b/packages/opencode/test/agent/agent.test.ts @@ -370,6 +370,25 @@ it.instance( }, ) +it.instance( + "agent reminder overrides can be set from config", + () => + Effect.gen(function* () { + const build = yield* load((svc) => svc.get("build")) + const plan = yield* load((svc) => svc.get("plan")) + expect(build?.buildSwitchReminder).toBe("Custom build reminder") + expect(plan?.planReminder).toBe("Custom plan reminder") + }), + { + config: { + agent: { + build: { build_switch_reminder: "Custom build reminder" }, + plan: { plan_reminder: "Custom plan reminder" }, + }, + }, + }, +) + it.instance( "unknown agent properties are placed into options", () => diff --git a/packages/opencode/test/session/reminders.test.ts b/packages/opencode/test/session/reminders.test.ts new file mode 100644 index 000000000000..820782ffad06 --- /dev/null +++ b/packages/opencode/test/session/reminders.test.ts @@ -0,0 +1,179 @@ +import { describe, expect } from "bun:test" +import { SessionV1 } from "@opencode-ai/core/v1/session" +import { FSUtil } from "@opencode-ai/core/fs-util" +import { ModelV2 } from "@opencode-ai/core/model" +import { ProjectV2 } from "@opencode-ai/core/project" +import { ProviderV2 } from "@opencode-ai/core/provider" +import { Effect, Layer } from "effect" +import { Agent } from "../../src/agent/agent" +import { RuntimeFlags } from "../../src/effect/runtime-flags" +import { InstanceState } from "../../src/effect/instance-state" +import { SessionReminders } from "../../src/session/reminders" +import { Session } from "../../src/session/session" +import { MessageID, SessionID } from "../../src/session/schema" +import { TestInstance } from "../fixture/fixture" +import { testEffect } from "../lib/effect" + +const it = testEffect( + Layer.mergeAll( + FSUtil.defaultLayer, + Layer.mock(Session.Service)({ + updatePart: (part) => Effect.succeed(part), + }), + ), +) + +describe("SessionReminders", () => { + it.instance("uses custom plan reminders outside experimental plan mode", () => + Effect.gen(function* () { + const test = yield* TestInstance + const session = sessionInfo(test.directory) + const ctx = yield* InstanceState.context + const result = yield* SessionReminders.apply({ + messages: [userMessage(session.id)], + agent: planAgent({ planReminder: "Custom plan reminder\n${planInfo}" }), + session, + }).pipe(Effect.provide(RuntimeFlags.layer())) + + expect(textPart(result)).toBe( + [ + "Custom plan reminder", + `No plan file exists yet. You should create your plan at ${Session.plan(session, ctx)} using the write tool.`, + ].join("\n"), + ) + }), + { git: true }, + ) + + it.instance("falls back to the built-in experimental plan reminder", () => + Effect.gen(function* () { + const test = yield* TestInstance + const session = sessionInfo(test.directory) + const ctx = yield* InstanceState.context + const result = yield* SessionReminders.apply({ + messages: [userMessage(session.id)], + agent: planAgent(), + session, + }).pipe(Effect.provide(RuntimeFlags.layer({ experimentalPlanMode: true }))) + + expect(textPart(result)).toContain("Plan mode is active.") + expect(textPart(result)).toContain(`No plan file exists yet. You should create your plan at ${Session.plan(session, ctx)} using the write tool.`) + }), + { git: true }, + ) + + it.instance("uses custom build switch reminders in experimental plan mode", () => + Effect.gen(function* () { + const test = yield* TestInstance + const session = sessionInfo(test.directory) + const ctx = yield* InstanceState.context + const plan = Session.plan(session, ctx) + + yield* FSUtil.use.writeWithDirs(plan, "# existing plan") + + const result = yield* SessionReminders.apply({ + messages: [userMessage(session.id), assistantMessage(session.id, "plan")], + agent: buildAgent({ buildSwitchReminder: "Custom build reminder\n${planInfo}" }), + session, + }).pipe(Effect.provide(RuntimeFlags.layer({ experimentalPlanMode: true }))) + + expect(textPart(result)).toBe( + [ + "Custom build reminder", + `A plan file exists at ${plan}. You should execute on the plan defined within it`, + ].join("\n"), + ) + }), + { git: true }, + ) +}) + +function sessionInfo(directory: string): Session.Info { + return { + id: SessionID.make("ses_reminders"), + slug: "reminders", + projectID: ProjectV2.ID.make("project_reminders"), + directory, + title: "Reminders", + version: "1", + time: { + created: 123, + updated: 123, + }, + } +} + +function userMessage(sessionID: SessionID): SessionV1.WithParts { + return { + info: { + id: MessageID.make("msg_user_reminders"), + role: "user", + sessionID, + time: { created: 123 }, + agent: "build", + model: { + providerID: ProviderV2.ID.make("openrouter"), + modelID: ModelV2.ID.make("openai/gpt-5"), + }, + }, + parts: [], + } +} + +function assistantMessage(sessionID: SessionID, agent: string): SessionV1.WithParts { + return { + info: { + id: MessageID.make("msg_assistant_reminders"), + role: "assistant", + sessionID, + time: { created: 124 }, + parentID: MessageID.make("msg_user_reminders"), + modelID: ModelV2.ID.make("openai/gpt-5"), + providerID: ProviderV2.ID.make("openrouter"), + mode: agent, + agent, + path: { + cwd: ".", + root: ".", + }, + cost: 0, + tokens: { + input: 0, + output: 0, + reasoning: 0, + cache: { + read: 0, + write: 0, + }, + }, + }, + parts: [], + } +} + +function planAgent(overrides: Partial = {}): Agent.Info { + return { + name: "plan", + mode: "primary", + permission: [], + options: {}, + ...overrides, + } +} + +function buildAgent(overrides: Partial = {}): Agent.Info { + return { + name: "build", + mode: "primary", + permission: [], + options: {}, + ...overrides, + } +} + +function textPart(messages: SessionV1.WithParts[]) { + const user = messages.find((message) => message.info.role === "user") + const part = user?.parts.findLast((item) => item.type === "text") + if (!part || part.type !== "text") throw new Error("expected injected text reminder") + return part.text +} From d772cc3a649a26b105bc7921c08b98a19889e466 Mon Sep 17 00:00:00 2001 From: Duncan Casteleyn <10881109+DuncanCasteleyn@users.noreply.github.com> Date: Fri, 12 Jun 2026 17:11:15 +0200 Subject: [PATCH 2/3] docs: document configurable plan reminders --- packages/core/src/config/agent.ts | 10 ++++++++-- packages/core/src/v1/config/agent.ts | 10 ++++++++-- packages/web/src/content/docs/agents.mdx | 24 ++++++++++++++++++++++++ packages/web/src/content/docs/config.mdx | 23 +++++++++++++++++++++++ 4 files changed, 63 insertions(+), 4 deletions(-) diff --git a/packages/core/src/config/agent.ts b/packages/core/src/config/agent.ts index 8c6a5d009289..cf44f8a52917 100644 --- a/packages/core/src/config/agent.ts +++ b/packages/core/src/config/agent.ts @@ -15,8 +15,14 @@ export class Info extends Schema.Class("ConfigV2.Agent")({ variant: Schema.String.pipe(Schema.optional), request: ConfigProvider.Request.pipe(Schema.optional), system: Schema.String.pipe(Schema.optional), - plan_reminder: Schema.String.pipe(Schema.optional), - build_switch_reminder: Schema.String.pipe(Schema.optional), + plan_reminder: Schema.String.pipe(Schema.optional).annotate({ + description: + "Override the reminder injected when the plan agent enters plan mode. Supports ${planInfo} interpolation.", + }), + build_switch_reminder: Schema.String.pipe(Schema.optional).annotate({ + description: + "Override the reminder injected when switching from plan mode back to the build agent. Supports ${planInfo} interpolation.", + }), description: Schema.String.pipe(Schema.optional), mode: Schema.Literals(["subagent", "primary", "all"]).pipe(Schema.optional), hidden: Schema.Boolean.pipe(Schema.optional), diff --git a/packages/core/src/v1/config/agent.ts b/packages/core/src/v1/config/agent.ts index beb40f7d26f4..23a6bb8879cf 100644 --- a/packages/core/src/v1/config/agent.ts +++ b/packages/core/src/v1/config/agent.ts @@ -18,8 +18,14 @@ const AgentSchema = Schema.StructWithRest( temperature: Schema.optional(Schema.Finite), top_p: Schema.optional(Schema.Finite), prompt: Schema.optional(Schema.String), - plan_reminder: Schema.optional(Schema.String), - build_switch_reminder: Schema.optional(Schema.String), + plan_reminder: Schema.optional(Schema.String).annotate({ + description: + "Override the reminder injected when the plan agent enters plan mode. Supports ${planInfo} interpolation.", + }), + build_switch_reminder: Schema.optional(Schema.String).annotate({ + description: + "Override the reminder injected when switching from plan mode back to the build agent. Supports ${planInfo} interpolation.", + }), tools: Schema.optional(Schema.Record(Schema.String, Schema.Boolean)).annotate({ description: "@deprecated Use 'permission' field instead", }), diff --git a/packages/web/src/content/docs/agents.mdx b/packages/web/src/content/docs/agents.mdx index 53048b7927b9..987f85784e06 100644 --- a/packages/web/src/content/docs/agents.mdx +++ b/packages/web/src/content/docs/agents.mdx @@ -346,6 +346,30 @@ Specify a custom system prompt file for this agent with the `prompt` config. The This path is relative to where the config file is located. So this works for both the global OpenCode config and the project specific config. +### Plan reminders + +The built-in `plan` and `build` agents also support reminder-specific overrides that are separate from the main `prompt`. + +- `plan_reminder` customizes the reminder injected when the `plan` agent enters plan mode. +- `build_switch_reminder` customizes the reminder injected when control returns from `plan` to `build`. + +Both fields support `${planInfo}`, which expands to the current plan-file guidance. + +```json title="opencode.json" +{ + "agent": { + "plan": { + "plan_reminder": "You are in planning mode.\n\n${planInfo}" + }, + "build": { + "build_switch_reminder": "Implementation can begin.\n\n${planInfo}" + } + } +} +``` + +Use these when you want to customize plan-mode guidance without replacing the agent's full system prompt. + --- ### Model diff --git a/packages/web/src/content/docs/config.mdx b/packages/web/src/content/docs/config.mdx index c1a69f5a866d..d7a9000b5685 100644 --- a/packages/web/src/content/docs/config.mdx +++ b/packages/web/src/content/docs/config.mdx @@ -519,6 +519,29 @@ You can configure specialized agents for specific tasks through the `agent` opti You can also define agents using markdown files in `~/.config/opencode/agents/` or `.opencode/agents/`. [Learn more here](/docs/agents). +Agent configs also support reminder overrides for the built-in plan workflow: + +- `plan_reminder` overrides the reminder injected when the `plan` agent enters plan mode. +- `build_switch_reminder` overrides the reminder injected when control switches back to the `build` agent. + +Both fields support `${planInfo}`, which expands to the current plan-file guidance. + +```jsonc title="opencode.jsonc" +{ + "$schema": "https://opencode.ai/config.json", + "agent": { + "plan": { + "plan_reminder": "Plan mode is active.\n\n${planInfo}\n\nKeep the plan concise and execution-ready." + }, + "build": { + "build_switch_reminder": "Plan approved. Resume implementation.\n\n${planInfo}" + } + } +} +``` + +These fields only affect the injected reminder text. They do not change the agent's main `prompt`, and they are unrelated to model catalog settings like `OPENCODE_MODELS_PATH`. + --- ### Default agent From 6481c9a8c2d5510f1a23d1bb911d765b737f2c48 Mon Sep 17 00:00:00 2001 From: Duncan Casteleyn <10881109+DuncanCasteleyn@users.noreply.github.com> Date: Fri, 12 Jun 2026 17:30:40 +0200 Subject: [PATCH 3/3] fix(opencode): keep build reminder plan info --- packages/opencode/src/session/reminders.ts | 4 +++- .../opencode/test/session/reminders.test.ts | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/session/reminders.ts b/packages/opencode/src/session/reminders.ts index e057c7765ec2..318817baa4f8 100644 --- a/packages/opencode/src/session/reminders.ts +++ b/packages/opencode/src/session/reminders.ts @@ -107,7 +107,9 @@ function planState(session: Session.Info, fsys: FSUtil.Interface) { planReminder: exists ? `A plan file already exists at ${filepath}. You can read it and make incremental edits using the edit tool.` : `No plan file exists yet. You should create your plan at ${filepath} using the write tool.`, - buildSwitch: exists ? `A plan file exists at ${filepath}. You should execute on the plan defined within it` : "", + buildSwitch: exists + ? `A plan file exists at ${filepath}. You should execute on the plan defined within it` + : `No plan file exists yet. There is no saved plan to execute from.`, } }) } diff --git a/packages/opencode/test/session/reminders.test.ts b/packages/opencode/test/session/reminders.test.ts index 820782ffad06..915808813b16 100644 --- a/packages/opencode/test/session/reminders.test.ts +++ b/packages/opencode/test/session/reminders.test.ts @@ -86,6 +86,27 @@ describe("SessionReminders", () => { }), { git: true }, ) + + it.instance("includes no-plan guidance in custom build switch reminders", () => + Effect.gen(function* () { + const test = yield* TestInstance + const session = sessionInfo(test.directory) + + const result = yield* SessionReminders.apply({ + messages: [userMessage(session.id), assistantMessage(session.id, "plan")], + agent: buildAgent({ buildSwitchReminder: "Custom build reminder\n${planInfo}" }), + session, + }).pipe(Effect.provide(RuntimeFlags.layer({ experimentalPlanMode: true }))) + + expect(textPart(result)).toBe( + [ + "Custom build reminder", + "No plan file exists yet. There is no saved plan to execute from.", + ].join("\n"), + ) + }), + { git: true }, + ) }) function sessionInfo(directory: string): Session.Info {