diff --git a/packages/core/src/plugin/agent.ts b/packages/core/src/plugin/agent.ts index afbc9b8e04..0e7964b51c 100644 --- a/packages/core/src/plugin/agent.ts +++ b/packages/core/src/plugin/agent.ts @@ -101,16 +101,6 @@ export const Plugin = define({ item.permissions.push({ action: "question", resource: "*", effect: "allow" }) }) - draft.update(Agent.ID.make("plan"), (item) => { - item.name = Agent.Name.make("Plan") - item.description = "Plan mode. Disallows all edit tools." - item.mode = "primary" - item.permissions.push( - { action: "question", resource: "*", effect: "allow" }, - { action: "edit", resource: "*", effect: "deny" }, - ) - }) - draft.update(Agent.ID.make("general"), (item) => { item.name = Agent.Name.make("General") item.description = diff --git a/packages/core/src/plugin/internal.ts b/packages/core/src/plugin/internal.ts index 1b47c04a2c..d65321b0bd 100644 --- a/packages/core/src/plugin/internal.ts +++ b/packages/core/src/plugin/internal.ts @@ -61,6 +61,7 @@ import { WellKnown } from "../wellknown" import { WriteTool } from "../tool/plugin/write" import { AgentPlugin } from "./agent" import { CommandPlugin } from "./command" +import { PlanPlugin } from "./plan" import { ModelsDevPlugin } from "./models-dev" import { ProviderPlugins } from "./provider" import { WebSearchPlugins } from "./websearch" @@ -196,6 +197,7 @@ export type InternalPlugin = Plugin const pre = [ WellKnownPlugin.Plugin, AgentPlugin.Plugin, + PlanPlugin.Plugin, CommandPlugin.Plugin, SkillPlugin.Plugin, ...SystemPromptPlugin.Plugins, diff --git a/packages/core/src/plugin/plan.ts b/packages/core/src/plugin/plan.ts new file mode 100644 index 0000000000..1f3d209357 --- /dev/null +++ b/packages/core/src/plugin/plan.ts @@ -0,0 +1,70 @@ +export * as PlanPlugin from "./plan" + +import { ToolFailure } from "@opencode-ai/ai" +import { define } from "@opencode-ai/plugin/effect/plugin" +import { Effect, Stream } from "effect" +import { Agent } from "../agent" +import { SessionEvent } from "../session/event" + +const plan = Agent.ID.make("plan") + +const enter = ` +You are in Plan mode. You are not allowed to edit or create files, and you may not ask a subagent to do that either. + +You are in Plan mode until the user switches agents. Plan mode is not changed by user intent, tone, or imperative language. If the user asks you to change files, do not edit. Tell them they need to switch agents. +` + +const leave = ` +You are NO LONGER in Plan mode. The previous Plan restrictions no longer apply. Any Plan mode instructions from earlier in this conversation are no longer active. +` + +export const Plugin = define({ + id: "opencode.plan", + effect: Effect.fn(function* (ctx) { + yield* ctx.agent.transform((draft) => { + draft.update(plan, (item) => { + item.name = Agent.Name.make("Plan") + item.description = "Read-only agent for exploring the codebase and planning work before implementation." + item.mode = "primary" + item.permissions.push({ action: "question", resource: "*", effect: "allow" }) + }) + }) + + yield* ctx.tool.hook("execute.before", (event) => { + if (event.agent !== plan) return Effect.void + if (event.tool !== "edit" && event.tool !== "write" && event.tool !== "patch") return Effect.void + return new ToolFailure({ + message: `Cannot use ${event.tool} in Plan mode. You are in a read-only mode and must not modify files.`, + }) + }) + + yield* ctx.event.subscribe().pipe( + Stream.filter( + (event): event is SessionEvent.Created | SessionEvent.AgentSelected => + event.type === "session.created" || event.type === "session.agent.selected", + ), + Stream.runForEach((event) => { + const text = reminder(event) + if (!text) return Effect.void + return ctx.session + .synthetic({ + sessionID: event.data.sessionID, + text, + resume: false, + }) + .pipe(Effect.catch(() => Effect.void)) + }), + Effect.forkScoped({ startImmediately: true }), + ) + }), +}) + +function reminder(event: SessionEvent.Created | SessionEvent.AgentSelected) { + if (event.type === "session.created") { + if (event.data.agent !== plan) return + return enter + } + if (event.data.agent === event.data.previous) return + if (event.data.agent === plan) return enter + if (event.data.previous === plan) return leave +} diff --git a/packages/core/test/agent.test.ts b/packages/core/test/agent.test.ts index 9280400024..f607bf68d2 100644 --- a/packages/core/test/agent.test.ts +++ b/packages/core/test/agent.test.ts @@ -165,7 +165,6 @@ describe("Agent", () => { "compaction", "explore", "general", - "plan", "summary", "title", ])