Просмотр исходного кода

refactor(tool): convert plan tool internals to Effect (#21807)

Kit Langton 5 месяцев назад
Родитель
Сommit
44f38193c0
2 измененных файлов с 66 добавлено и 110 удалено
  1. 58 109
      packages/opencode/src/tool/plan.ts
  2. 8 1
      packages/opencode/src/tool/registry.ts

+ 58 - 109
packages/opencode/src/tool/plan.ts

@@ -1,5 +1,6 @@
 import z from "zod"
 import z from "zod"
 import path from "path"
 import path from "path"
+import { Effect } from "effect"
 import { Tool } from "./tool"
 import { Tool } from "./tool"
 import { Question } from "../question"
 import { Question } from "../question"
 import { Session } from "../session"
 import { Session } from "../session"
@@ -9,123 +10,71 @@ import { Instance } from "../project/instance"
 import { type SessionID, MessageID, PartID } from "../session/schema"
 import { type SessionID, MessageID, PartID } from "../session/schema"
 import EXIT_DESCRIPTION from "./plan-exit.txt"
 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
     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 {
     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,
+          })
 
 
-/*
-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))
+          if (answers[0]?.[0] === "No") yield* new Question.RejectedError()
 
 
-    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 model = getLastModel(ctx.sessionID) ?? (yield* provider.defaultModel())
 
 
-    const answer = answers[0]?.[0]
+          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)
 
 
-    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: {},
+          return {
+            title: "Switching to build agent",
+            output: "User approved switching to build agent. Wait for further instructions.",
+            metadata: {},
+          }
+        }).pipe(Effect.runPromise),
     }
     }
-  },
-})
-*/
+  }),
+)

+ 8 - 1
packages/opencode/src/tool/registry.ts

@@ -1,4 +1,5 @@
 import { PlanExitTool } from "./plan"
 import { PlanExitTool } from "./plan"
+import { Session } from "../session"
 import { QuestionTool } from "./question"
 import { QuestionTool } from "./question"
 import { BashTool } from "./bash"
 import { BashTool } from "./bash"
 import { EditTool } from "./edit"
 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 { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
 import z from "zod"
 import z from "zod"
 import { Plugin } from "../plugin"
 import { Plugin } from "../plugin"
+import { Provider } from "../provider/provider"
 import { ProviderID, type ModelID } from "../provider/schema"
 import { ProviderID, type ModelID } from "../provider/schema"
 import { WebSearchTool } from "./websearch"
 import { WebSearchTool } from "./websearch"
 import { CodeSearchTool } from "./codesearch"
 import { CodeSearchTool } from "./codesearch"
@@ -76,6 +78,8 @@ export namespace ToolRegistry {
     | Todo.Service
     | Todo.Service
     | Agent.Service
     | Agent.Service
     | Skill.Service
     | Skill.Service
+    | Session.Service
+    | Provider.Service
     | LSP.Service
     | LSP.Service
     | FileTime.Service
     | FileTime.Service
     | Instruction.Service
     | Instruction.Service
@@ -93,6 +97,7 @@ export namespace ToolRegistry {
       const question = yield* QuestionTool
       const question = yield* QuestionTool
       const todo = yield* TodoWriteTool
       const todo = yield* TodoWriteTool
       const lsptool = yield* LspTool
       const lsptool = yield* LspTool
+      const plan = yield* PlanExitTool
 
 
       const state = yield* InstanceState.make<State>(
       const state = yield* InstanceState.make<State>(
         Effect.fn("ToolRegistry.state")(function* (ctx) {
         Effect.fn("ToolRegistry.state")(function* (ctx) {
@@ -166,7 +171,7 @@ export namespace ToolRegistry {
             patch: Tool.init(ApplyPatchTool),
             patch: Tool.init(ApplyPatchTool),
             question: Tool.init(question),
             question: Tool.init(question),
             lsp: Tool.init(lsptool),
             lsp: Tool.init(lsptool),
-            plan: Tool.init(PlanExitTool),
+            plan: Tool.init(plan),
           })
           })
 
 
           return {
           return {
@@ -298,6 +303,8 @@ export namespace ToolRegistry {
       Layer.provide(Todo.defaultLayer),
       Layer.provide(Todo.defaultLayer),
       Layer.provide(Skill.defaultLayer),
       Layer.provide(Skill.defaultLayer),
       Layer.provide(Agent.defaultLayer),
       Layer.provide(Agent.defaultLayer),
+      Layer.provide(Session.defaultLayer),
+      Layer.provide(Provider.defaultLayer),
       Layer.provide(LSP.defaultLayer),
       Layer.provide(LSP.defaultLayer),
       Layer.provide(FileTime.defaultLayer),
       Layer.provide(FileTime.defaultLayer),
       Layer.provide(Instruction.defaultLayer),
       Layer.provide(Instruction.defaultLayer),