Skip to content

Commit f2d4ced

Browse files
kitlangtonjpcarranza94claude
authored
refactor(effect): build todowrite tool from Todo service (anomalyco#20789)
Co-authored-by: Juan Pablo Carranza Hurtado <52012198+jpcarranza94@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ae7e2eb commit f2d4ced

6 files changed

Lines changed: 219 additions & 196 deletions

File tree

packages/opencode/src/session/todo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ export namespace Todo {
8282
}),
8383
)
8484

85-
const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
85+
export const defaultLayer = layer.pipe(Layer.provide(Bus.layer))
8686
const { runPromise } = makeRuntime(Service, defaultLayer)
8787

8888
export async function update(input: { sessionID: SessionID; todos: Info[] }) {

packages/opencode/src/tool/registry.ts

Lines changed: 154 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { InstanceState } from "@/effect/instance-state"
3434
import { makeRuntime } from "@/effect/run-service"
3535
import { Env } from "../env"
3636
import { Question } from "../question"
37+
import { Todo } from "../session/todo"
3738

3839
export namespace ToolRegistry {
3940
const log = Log.create({ service: "tool.registry" })
@@ -56,172 +57,175 @@ export namespace ToolRegistry {
5657

5758
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/ToolRegistry") {}
5859

59-
export const layer: Layer.Layer<Service, never, Config.Service | Plugin.Service | Question.Service> = Layer.effect(
60-
Service,
61-
Effect.gen(function* () {
62-
const config = yield* Config.Service
63-
const plugin = yield* Plugin.Service
64-
65-
const build = <T extends Tool.Info>(tool: T | Effect.Effect<T, never, any>) =>
66-
Effect.isEffect(tool) ? tool : Effect.succeed(tool)
67-
68-
const state = yield* InstanceState.make<State>(
69-
Effect.fn("ToolRegistry.state")(function* (ctx) {
70-
const custom: Tool.Info[] = []
71-
72-
function fromPlugin(id: string, def: ToolDefinition): Tool.Info {
73-
return {
74-
id,
75-
init: async (initCtx) => ({
76-
parameters: z.object(def.args),
77-
description: def.description,
78-
execute: async (args, toolCtx) => {
79-
const pluginCtx = {
80-
...toolCtx,
81-
directory: ctx.directory,
82-
worktree: ctx.worktree,
83-
} as unknown as PluginToolContext
84-
const result = await def.execute(args as any, pluginCtx)
85-
const out = await Truncate.output(result, {}, initCtx?.agent)
86-
return {
87-
title: "",
88-
output: out.truncated ? out.content : result,
89-
metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined },
90-
}
91-
},
92-
}),
60+
export const layer: Layer.Layer<Service, never, Config.Service | Plugin.Service | Question.Service | Todo.Service> =
61+
Layer.effect(
62+
Service,
63+
Effect.gen(function* () {
64+
const config = yield* Config.Service
65+
const plugin = yield* Plugin.Service
66+
67+
const build = <T extends Tool.Info>(tool: T | Effect.Effect<T, never, any>) =>
68+
Effect.isEffect(tool) ? tool : Effect.succeed(tool)
69+
70+
const state = yield* InstanceState.make<State>(
71+
Effect.fn("ToolRegistry.state")(function* (ctx) {
72+
const custom: Tool.Info[] = []
73+
74+
function fromPlugin(id: string, def: ToolDefinition): Tool.Info {
75+
return {
76+
id,
77+
init: async (initCtx) => ({
78+
parameters: z.object(def.args),
79+
description: def.description,
80+
execute: async (args, toolCtx) => {
81+
const pluginCtx = {
82+
...toolCtx,
83+
directory: ctx.directory,
84+
worktree: ctx.worktree,
85+
} as unknown as PluginToolContext
86+
const result = await def.execute(args as any, pluginCtx)
87+
const out = await Truncate.output(result, {}, initCtx?.agent)
88+
return {
89+
title: "",
90+
output: out.truncated ? out.content : result,
91+
metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined },
92+
}
93+
},
94+
}),
95+
}
9396
}
94-
}
9597

96-
const dirs = yield* config.directories()
97-
const matches = dirs.flatMap((dir) =>
98-
Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true, symlink: true }),
99-
)
100-
if (matches.length) yield* config.waitForDependencies()
101-
for (const match of matches) {
102-
const namespace = path.basename(match, path.extname(match))
103-
const mod = yield* Effect.promise(
104-
() => import(process.platform === "win32" ? match : pathToFileURL(match).href),
98+
const dirs = yield* config.directories()
99+
const matches = dirs.flatMap((dir) =>
100+
Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true, symlink: true }),
105101
)
106-
for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
107-
custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
102+
if (matches.length) yield* config.waitForDependencies()
103+
for (const match of matches) {
104+
const namespace = path.basename(match, path.extname(match))
105+
const mod = yield* Effect.promise(
106+
() => import(process.platform === "win32" ? match : pathToFileURL(match).href),
107+
)
108+
for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
109+
custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
110+
}
108111
}
109-
}
110112

111-
const plugins = yield* plugin.list()
112-
for (const p of plugins) {
113-
for (const [id, def] of Object.entries(p.tool ?? {})) {
114-
custom.push(fromPlugin(id, def))
115-
}
116-
}
117-
118-
return { custom }
119-
}),
120-
)
121-
122-
const invalid = yield* build(InvalidTool)
123-
const ask = yield* build(QuestionTool)
124-
const bash = yield* build(BashTool)
125-
const read = yield* build(ReadTool)
126-
const glob = yield* build(GlobTool)
127-
const grep = yield* build(GrepTool)
128-
const edit = yield* build(EditTool)
129-
const write = yield* build(WriteTool)
130-
const task = yield* build(TaskTool)
131-
const fetch = yield* build(WebFetchTool)
132-
const todo = yield* build(TodoWriteTool)
133-
const search = yield* build(WebSearchTool)
134-
const code = yield* build(CodeSearchTool)
135-
const skill = yield* build(SkillTool)
136-
const patch = yield* build(ApplyPatchTool)
137-
const lsp = yield* build(LspTool)
138-
const batch = yield* build(BatchTool)
139-
const plan = yield* build(PlanExitTool)
140-
141-
const all = Effect.fn("ToolRegistry.all")(function* (custom: Tool.Info[]) {
142-
const cfg = yield* config.get()
143-
const question = ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL
144-
145-
return [
146-
invalid,
147-
...(question ? [ask] : []),
148-
bash,
149-
read,
150-
glob,
151-
grep,
152-
edit,
153-
write,
154-
task,
155-
fetch,
156-
todo,
157-
search,
158-
code,
159-
skill,
160-
patch,
161-
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [lsp] : []),
162-
...(cfg.experimental?.batch_tool === true ? [batch] : []),
163-
...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [plan] : []),
164-
...custom,
165-
]
166-
})
167-
168-
const ids = Effect.fn("ToolRegistry.ids")(function* () {
169-
const s = yield* InstanceState.get(state)
170-
const tools = yield* all(s.custom)
171-
return tools.map((t) => t.id)
172-
})
173-
174-
const tools = Effect.fn("ToolRegistry.tools")(function* (
175-
model: { providerID: ProviderID; modelID: ModelID },
176-
agent?: Agent.Info,
177-
) {
178-
const s = yield* InstanceState.get(state)
179-
const allTools = yield* all(s.custom)
180-
const filtered = allTools.filter((tool) => {
181-
if (tool.id === "codesearch" || tool.id === "websearch") {
182-
return model.providerID === ProviderID.opencode || Flag.OPENCODE_ENABLE_EXA
183-
}
184-
185-
const usePatch =
186-
!!Env.get("OPENCODE_E2E_LLM_URL") ||
187-
(model.modelID.includes("gpt-") && !model.modelID.includes("oss") && !model.modelID.includes("gpt-4"))
188-
if (tool.id === "apply_patch") return usePatch
189-
if (tool.id === "edit" || tool.id === "write") return !usePatch
190-
191-
return true
192-
})
193-
return yield* Effect.forEach(
194-
filtered,
195-
Effect.fnUntraced(function* (tool: Tool.Info) {
196-
using _ = log.time(tool.id)
197-
const next = yield* Effect.promise(() => tool.init({ agent }))
198-
const output = {
199-
description: next.description,
200-
parameters: next.parameters,
201-
}
202-
yield* plugin.trigger("tool.definition", { toolID: tool.id }, output)
203-
return {
204-
id: tool.id,
205-
description: output.description,
206-
parameters: output.parameters,
207-
execute: next.execute,
208-
formatValidationError: next.formatValidationError,
113+
const plugins = yield* plugin.list()
114+
for (const p of plugins) {
115+
for (const [id, def] of Object.entries(p.tool ?? {})) {
116+
custom.push(fromPlugin(id, def))
117+
}
209118
}
119+
120+
return { custom }
210121
}),
211-
{ concurrency: "unbounded" },
212122
)
213-
})
214123

215-
return Service.of({ ids, named: { task, read }, tools })
216-
}),
217-
)
124+
const invalid = yield* build(InvalidTool)
125+
const ask = yield* build(QuestionTool)
126+
const bash = yield* build(BashTool)
127+
const read = yield* build(ReadTool)
128+
const glob = yield* build(GlobTool)
129+
const grep = yield* build(GrepTool)
130+
const edit = yield* build(EditTool)
131+
const write = yield* build(WriteTool)
132+
const task = yield* build(TaskTool)
133+
const fetch = yield* build(WebFetchTool)
134+
const todo = yield* build(TodoWriteTool)
135+
const search = yield* build(WebSearchTool)
136+
const code = yield* build(CodeSearchTool)
137+
const skill = yield* build(SkillTool)
138+
const patch = yield* build(ApplyPatchTool)
139+
const lsp = yield* build(LspTool)
140+
const batch = yield* build(BatchTool)
141+
const plan = yield* build(PlanExitTool)
142+
143+
const all = Effect.fn("ToolRegistry.all")(function* (custom: Tool.Info[]) {
144+
const cfg = yield* config.get()
145+
const question =
146+
["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL
147+
148+
return [
149+
invalid,
150+
...(question ? [ask] : []),
151+
bash,
152+
read,
153+
glob,
154+
grep,
155+
edit,
156+
write,
157+
task,
158+
fetch,
159+
todo,
160+
search,
161+
code,
162+
skill,
163+
patch,
164+
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [lsp] : []),
165+
...(cfg.experimental?.batch_tool === true ? [batch] : []),
166+
...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [plan] : []),
167+
...custom,
168+
]
169+
})
170+
171+
const ids = Effect.fn("ToolRegistry.ids")(function* () {
172+
const s = yield* InstanceState.get(state)
173+
const tools = yield* all(s.custom)
174+
return tools.map((t) => t.id)
175+
})
176+
177+
const tools = Effect.fn("ToolRegistry.tools")(function* (
178+
model: { providerID: ProviderID; modelID: ModelID },
179+
agent?: Agent.Info,
180+
) {
181+
const s = yield* InstanceState.get(state)
182+
const allTools = yield* all(s.custom)
183+
const filtered = allTools.filter((tool) => {
184+
if (tool.id === "codesearch" || tool.id === "websearch") {
185+
return model.providerID === ProviderID.opencode || Flag.OPENCODE_ENABLE_EXA
186+
}
187+
188+
const usePatch =
189+
!!Env.get("OPENCODE_E2E_LLM_URL") ||
190+
(model.modelID.includes("gpt-") && !model.modelID.includes("oss") && !model.modelID.includes("gpt-4"))
191+
if (tool.id === "apply_patch") return usePatch
192+
if (tool.id === "edit" || tool.id === "write") return !usePatch
193+
194+
return true
195+
})
196+
return yield* Effect.forEach(
197+
filtered,
198+
Effect.fnUntraced(function* (tool: Tool.Info) {
199+
using _ = log.time(tool.id)
200+
const next = yield* Effect.promise(() => tool.init({ agent }))
201+
const output = {
202+
description: next.description,
203+
parameters: next.parameters,
204+
}
205+
yield* plugin.trigger("tool.definition", { toolID: tool.id }, output)
206+
return {
207+
id: tool.id,
208+
description: output.description,
209+
parameters: output.parameters,
210+
execute: next.execute,
211+
formatValidationError: next.formatValidationError,
212+
}
213+
}),
214+
{ concurrency: "unbounded" },
215+
)
216+
})
217+
218+
return Service.of({ ids, named: { task, read }, tools })
219+
}),
220+
)
218221

219222
export const defaultLayer = Layer.unwrap(
220223
Effect.sync(() =>
221224
layer.pipe(
222225
Layer.provide(Config.defaultLayer),
223226
Layer.provide(Plugin.defaultLayer),
224227
Layer.provide(Question.defaultLayer),
228+
Layer.provide(Todo.defaultLayer),
225229
),
226230
),
227231
)

0 commit comments

Comments
 (0)