Skip to content

Commit 44f3819

Browse files
authored
refactor(tool): convert plan tool internals to Effect (#21807)
1 parent 9a6b455 commit 44f3819

2 files changed

Lines changed: 66 additions & 110 deletions

File tree

packages/opencode/src/tool/plan.ts

Lines changed: 58 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import z from "zod"
22
import path from "path"
3+
import { Effect } from "effect"
34
import { Tool } from "./tool"
45
import { Question } from "../question"
56
import { Session } from "../session"
@@ -9,123 +10,71 @@ import { Instance } from "../project/instance"
910
import { type SessionID, MessageID, PartID } from "../session/schema"
1011
import EXIT_DESCRIPTION from "./plan-exit.txt"
1112

12-
async function getLastModel(sessionID: SessionID) {
13-
for await (const item of MessageV2.stream(sessionID)) {
13+
function getLastModel(sessionID: SessionID) {
14+
for (const item of MessageV2.stream(sessionID)) {
1415
if (item.info.role === "user" && item.info.model) return item.info.model
1516
}
16-
return Provider.defaultModel()
17+
return undefined
1718
}
1819

19-
export const PlanExitTool = Tool.define("plan_exit", {
20-
description: EXIT_DESCRIPTION,
21-
parameters: z.object({}),
22-
async execute(_params, ctx) {
23-
const session = await Session.get(ctx.sessionID)
24-
const plan = path.relative(Instance.worktree, Session.plan(session))
25-
const answers = await Question.ask({
26-
sessionID: ctx.sessionID,
27-
questions: [
28-
{
29-
question: `Plan at ${plan} is complete. Would you like to switch to the build agent and start implementing?`,
30-
header: "Build Agent",
31-
custom: false,
32-
options: [
33-
{ label: "Yes", description: "Switch to build agent and start implementing the plan" },
34-
{ label: "No", description: "Stay with plan agent to continue refining the plan" },
35-
],
36-
},
37-
],
38-
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
39-
})
40-
41-
const answer = answers[0]?.[0]
42-
if (answer === "No") throw new Question.RejectedError()
43-
44-
const model = await getLastModel(ctx.sessionID)
45-
46-
const userMsg: MessageV2.User = {
47-
id: MessageID.ascending(),
48-
sessionID: ctx.sessionID,
49-
role: "user",
50-
time: {
51-
created: Date.now(),
52-
},
53-
agent: "build",
54-
model,
55-
}
56-
await Session.updateMessage(userMsg)
57-
await Session.updatePart({
58-
id: PartID.ascending(),
59-
messageID: userMsg.id,
60-
sessionID: ctx.sessionID,
61-
type: "text",
62-
text: `The plan at ${plan} has been approved, you can now edit files. Execute the plan`,
63-
synthetic: true,
64-
} satisfies MessageV2.TextPart)
20+
export const PlanExitTool = Tool.defineEffect(
21+
"plan_exit",
22+
Effect.gen(function* () {
23+
const session = yield* Session.Service
24+
const question = yield* Question.Service
25+
const provider = yield* Provider.Service
6526

6627
return {
67-
title: "Switching to build agent",
68-
output: "User approved switching to build agent. Wait for further instructions.",
69-
metadata: {},
70-
}
71-
},
72-
})
28+
description: EXIT_DESCRIPTION,
29+
parameters: z.object({}),
30+
execute: (_params: {}, ctx: Tool.Context) =>
31+
Effect.gen(function* () {
32+
const info = yield* session.get(ctx.sessionID)
33+
const plan = path.relative(Instance.worktree, Session.plan(info))
34+
const answers = yield* question.ask({
35+
sessionID: ctx.sessionID,
36+
questions: [
37+
{
38+
question: `Plan at ${plan} is complete. Would you like to switch to the build agent and start implementing?`,
39+
header: "Build Agent",
40+
custom: false,
41+
options: [
42+
{ label: "Yes", description: "Switch to build agent and start implementing the plan" },
43+
{ label: "No", description: "Stay with plan agent to continue refining the plan" },
44+
],
45+
},
46+
],
47+
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
48+
})
7349

74-
/*
75-
export const PlanEnterTool = Tool.define("plan_enter", {
76-
description: ENTER_DESCRIPTION,
77-
parameters: z.object({}),
78-
async execute(_params, ctx) {
79-
const session = await Session.get(ctx.sessionID)
80-
const plan = path.relative(Instance.worktree, Session.plan(session))
50+
if (answers[0]?.[0] === "No") yield* new Question.RejectedError()
8151

82-
const answers = await Question.ask({
83-
sessionID: ctx.sessionID,
84-
questions: [
85-
{
86-
question: `Would you like to switch to the plan agent and create a plan saved to ${plan}?`,
87-
header: "Plan Mode",
88-
custom: false,
89-
options: [
90-
{ label: "Yes", description: "Switch to plan agent for research and planning" },
91-
{ label: "No", description: "Stay with build agent to continue making changes" },
92-
],
93-
},
94-
],
95-
tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
96-
})
52+
const model = getLastModel(ctx.sessionID) ?? (yield* provider.defaultModel())
9753

98-
const answer = answers[0]?.[0]
54+
const msg: MessageV2.User = {
55+
id: MessageID.ascending(),
56+
sessionID: ctx.sessionID,
57+
role: "user",
58+
time: { created: Date.now() },
59+
agent: "build",
60+
model,
61+
}
62+
yield* session.updateMessage(msg)
63+
yield* session.updatePart({
64+
id: PartID.ascending(),
65+
messageID: msg.id,
66+
sessionID: ctx.sessionID,
67+
type: "text",
68+
text: `The plan at ${plan} has been approved, you can now edit files. Execute the plan`,
69+
synthetic: true,
70+
} satisfies MessageV2.TextPart)
9971

100-
if (answer === "No") throw new Question.RejectedError()
101-
102-
const model = await getLastModel(ctx.sessionID)
103-
104-
const userMsg: MessageV2.User = {
105-
id: MessageID.ascending(),
106-
sessionID: ctx.sessionID,
107-
role: "user",
108-
time: {
109-
created: Date.now(),
110-
},
111-
agent: "plan",
112-
model,
113-
}
114-
await Session.updateMessage(userMsg)
115-
await Session.updatePart({
116-
id: PartID.ascending(),
117-
messageID: userMsg.id,
118-
sessionID: ctx.sessionID,
119-
type: "text",
120-
text: "User has requested to enter plan mode. Switch to plan mode and begin planning.",
121-
synthetic: true,
122-
} satisfies MessageV2.TextPart)
123-
124-
return {
125-
title: "Switching to plan agent",
126-
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.`,
127-
metadata: {},
72+
return {
73+
title: "Switching to build agent",
74+
output: "User approved switching to build agent. Wait for further instructions.",
75+
metadata: {},
76+
}
77+
}).pipe(Effect.runPromise),
12878
}
129-
},
130-
})
131-
*/
79+
}),
80+
)

packages/opencode/src/tool/registry.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { PlanExitTool } from "./plan"
2+
import { Session } from "../session"
23
import { QuestionTool } from "./question"
34
import { BashTool } from "./bash"
45
import { EditTool } from "./edit"
@@ -16,6 +17,7 @@ import { Config } from "../config/config"
1617
import { type ToolContext as PluginToolContext, type ToolDefinition } from "@opencode-ai/plugin"
1718
import z from "zod"
1819
import { Plugin } from "../plugin"
20+
import { Provider } from "../provider/provider"
1921
import { ProviderID, type ModelID } from "../provider/schema"
2022
import { WebSearchTool } from "./websearch"
2123
import { CodeSearchTool } from "./codesearch"
@@ -76,6 +78,8 @@ export namespace ToolRegistry {
7678
| Todo.Service
7779
| Agent.Service
7880
| Skill.Service
81+
| Session.Service
82+
| Provider.Service
7983
| LSP.Service
8084
| FileTime.Service
8185
| Instruction.Service
@@ -93,6 +97,7 @@ export namespace ToolRegistry {
9397
const question = yield* QuestionTool
9498
const todo = yield* TodoWriteTool
9599
const lsptool = yield* LspTool
100+
const plan = yield* PlanExitTool
96101

97102
const state = yield* InstanceState.make<State>(
98103
Effect.fn("ToolRegistry.state")(function* (ctx) {
@@ -166,7 +171,7 @@ export namespace ToolRegistry {
166171
patch: Tool.init(ApplyPatchTool),
167172
question: Tool.init(question),
168173
lsp: Tool.init(lsptool),
169-
plan: Tool.init(PlanExitTool),
174+
plan: Tool.init(plan),
170175
})
171176

172177
return {
@@ -298,6 +303,8 @@ export namespace ToolRegistry {
298303
Layer.provide(Todo.defaultLayer),
299304
Layer.provide(Skill.defaultLayer),
300305
Layer.provide(Agent.defaultLayer),
306+
Layer.provide(Session.defaultLayer),
307+
Layer.provide(Provider.defaultLayer),
301308
Layer.provide(LSP.defaultLayer),
302309
Layer.provide(FileTime.defaultLayer),
303310
Layer.provide(Instruction.defaultLayer),

0 commit comments

Comments
 (0)