diff --git a/packages/opencode/src/tool/plan.ts b/packages/opencode/src/tool/plan.ts index e91bc3faa2..aa9c698842 100644 --- a/packages/opencode/src/tool/plan.ts +++ b/packages/opencode/src/tool/plan.ts @@ -1,5 +1,6 @@ import z from "zod" import path from "path" +import { Effect } from "effect" import { Tool } from "./tool" import { Question } from "../question" import { Session } from "../session" @@ -9,123 +10,71 @@ import { Instance } from "../project/instance" import { type SessionID, MessageID, PartID } from "../session/schema" import EXIT_DESCRIPTION from "./plan-exit.txt" -async function getLastModel(sessionID: SessionID) { - for await (const item of MessageV2.stream(sessionID)) { +function getLastModel(sessionID: SessionID) { + for (const item of MessageV2.stream(sessionID)) { if (item.info.role === "user" && item.info.model) return item.info.model } - return Provider.defaultModel() + return undefined } -export const PlanExitTool = Tool.define("plan_exit", { - description: EXIT_DESCRIPTION, - parameters: z.object({}), - async execute(_params, ctx) { - const session = await Session.get(ctx.sessionID) - const plan = path.relative(Instance.worktree, Session.plan(session)) - const answers = await Question.ask({ - sessionID: ctx.sessionID, - questions: [ - { - question: `Plan at ${plan} is complete. Would you like to switch to the build agent and start implementing?`, - header: "Build Agent", - custom: false, - options: [ - { label: "Yes", description: "Switch to build agent and start implementing the plan" }, - { label: "No", description: "Stay with plan agent to continue refining the plan" }, - ], - }, - ], - tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined, - }) - - const answer = answers[0]?.[0] - if (answer === "No") throw new Question.RejectedError() - - const model = await getLastModel(ctx.sessionID) - - const userMsg: MessageV2.User = { - id: MessageID.ascending(), - sessionID: ctx.sessionID, - role: "user", - time: { - created: Date.now(), - }, - agent: "build", - model, - } - await Session.updateMessage(userMsg) - await Session.updatePart({ - id: PartID.ascending(), - messageID: userMsg.id, - sessionID: ctx.sessionID, - type: "text", - text: `The plan at ${plan} has been approved, you can now edit files. Execute the plan`, - synthetic: true, - } satisfies MessageV2.TextPart) +export const PlanExitTool = Tool.defineEffect( + "plan_exit", + Effect.gen(function* () { + const session = yield* Session.Service + const question = yield* Question.Service + const provider = yield* Provider.Service return { - title: "Switching to build agent", - output: "User approved switching to build agent. Wait for further instructions.", - metadata: {}, + description: EXIT_DESCRIPTION, + parameters: z.object({}), + execute: (_params: {}, ctx: Tool.Context) => + Effect.gen(function* () { + const info = yield* session.get(ctx.sessionID) + const plan = path.relative(Instance.worktree, Session.plan(info)) + const answers = yield* question.ask({ + sessionID: ctx.sessionID, + questions: [ + { + question: `Plan at ${plan} is complete. Would you like to switch to the build agent and start implementing?`, + header: "Build Agent", + custom: false, + options: [ + { label: "Yes", description: "Switch to build agent and start implementing the plan" }, + { label: "No", description: "Stay with plan agent to continue refining the plan" }, + ], + }, + ], + tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined, + }) + + if (answers[0]?.[0] === "No") yield* new Question.RejectedError() + + const model = getLastModel(ctx.sessionID) ?? (yield* provider.defaultModel()) + + const msg: MessageV2.User = { + id: MessageID.ascending(), + sessionID: ctx.sessionID, + role: "user", + time: { created: Date.now() }, + agent: "build", + model, + } + yield* session.updateMessage(msg) + yield* session.updatePart({ + id: PartID.ascending(), + messageID: msg.id, + sessionID: ctx.sessionID, + type: "text", + text: `The plan at ${plan} has been approved, you can now edit files. Execute the plan`, + synthetic: true, + } satisfies MessageV2.TextPart) + + return { + title: "Switching to build agent", + output: "User approved switching to build agent. Wait for further instructions.", + metadata: {}, + } + }).pipe(Effect.runPromise), } - }, -}) - -/* -export const PlanEnterTool = Tool.define("plan_enter", { - description: ENTER_DESCRIPTION, - parameters: z.object({}), - async execute(_params, ctx) { - const session = await Session.get(ctx.sessionID) - const plan = path.relative(Instance.worktree, Session.plan(session)) - - const answers = await Question.ask({ - sessionID: ctx.sessionID, - questions: [ - { - question: `Would you like to switch to the plan agent and create a plan saved to ${plan}?`, - header: "Plan Mode", - custom: false, - options: [ - { label: "Yes", description: "Switch to plan agent for research and planning" }, - { label: "No", description: "Stay with build agent to continue making changes" }, - ], - }, - ], - tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined, - }) - - const answer = answers[0]?.[0] - - if (answer === "No") throw new Question.RejectedError() - - const model = await getLastModel(ctx.sessionID) - - const userMsg: MessageV2.User = { - id: MessageID.ascending(), - sessionID: ctx.sessionID, - role: "user", - time: { - created: Date.now(), - }, - agent: "plan", - model, - } - await Session.updateMessage(userMsg) - await Session.updatePart({ - id: PartID.ascending(), - messageID: userMsg.id, - sessionID: ctx.sessionID, - type: "text", - text: "User has requested to enter plan mode. Switch to plan mode and begin planning.", - synthetic: true, - } satisfies MessageV2.TextPart) - - return { - title: "Switching to plan agent", - output: `User confirmed to switch to plan mode. A new message has been created to switch you to plan mode. The plan file will be at ${plan}. Begin planning.`, - metadata: {}, - } - }, -}) -*/ + }), +) diff --git a/packages/opencode/src/tool/registry.ts b/packages/opencode/src/tool/registry.ts index b653c336ef..716a8ca4e3 100644 --- a/packages/opencode/src/tool/registry.ts +++ b/packages/opencode/src/tool/registry.ts @@ -1,4 +1,5 @@ import { PlanExitTool } from "./plan" +import { Session } from "../session" import { QuestionTool } from "./question" import { BashTool } from "./bash" import { EditTool } from "./edit" @@ -16,6 +17,7 @@ import { Config } from "../config/config" import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin" import z from "zod" import { Plugin } from "../plugin" +import { Provider } from "../provider/provider" import { ProviderID, type ModelID } from "../provider/schema" import { WebSearchTool } from "./websearch" import { CodeSearchTool } from "./codesearch" @@ -76,6 +78,8 @@ export namespace ToolRegistry { | Todo.Service | Agent.Service | Skill.Service + | Session.Service + | Provider.Service | LSP.Service | FileTime.Service | Instruction.Service @@ -93,6 +97,7 @@ export namespace ToolRegistry { const question = yield* QuestionTool const todo = yield* TodoWriteTool const lsptool = yield* LspTool + const plan = yield* PlanExitTool const state = yield* InstanceState.make( Effect.fn("ToolRegistry.state")(function* (ctx) { @@ -166,7 +171,7 @@ export namespace ToolRegistry { patch: Tool.init(ApplyPatchTool), question: Tool.init(question), lsp: Tool.init(lsptool), - plan: Tool.init(PlanExitTool), + plan: Tool.init(plan), }) return { @@ -298,6 +303,8 @@ export namespace ToolRegistry { Layer.provide(Todo.defaultLayer), Layer.provide(Skill.defaultLayer), Layer.provide(Agent.defaultLayer), + Layer.provide(Session.defaultLayer), + Layer.provide(Provider.defaultLayer), Layer.provide(LSP.defaultLayer), Layer.provide(FileTime.defaultLayer), Layer.provide(Instruction.defaultLayer),