Skip to content

Commit 3f88048

Browse files
committed
refactor(effect): build task tool from agent services
1 parent 220541c commit 3f88048

4 files changed

Lines changed: 353 additions & 326 deletions

File tree

packages/opencode/src/tool/registry.ts

Lines changed: 156 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { Effect, Layer, ServiceMap } from "effect"
3333
import { InstanceState } from "@/effect/instance-state"
3434
import { makeRuntime } from "@/effect/run-service"
3535
import { Env } from "../env"
36+
import { Agent as AgentSvc } from "../agent/agent"
3637
import { Question } from "../question"
3738
import { Todo } from "../session/todo"
3839

@@ -57,173 +58,176 @@ export namespace ToolRegistry {
5758

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

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

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 }),
102+
const dirs = yield* config.directories()
103+
const matches = dirs.flatMap((dir) =>
104+
Glob.scanSync("{tool,tools}/*.{js,ts}", { cwd: dir, absolute: true, dot: true, symlink: true }),
105+
)
106+
if (matches.length) yield* config.waitForDependencies()
107+
for (const match of matches) {
108+
const namespace = path.basename(match, path.extname(match))
109+
const mod = yield* Effect.promise(
110+
() => import(process.platform === "win32" ? match : pathToFileURL(match).href),
101111
)
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-
}
112+
for (const [id, def] of Object.entries<ToolDefinition>(mod)) {
113+
custom.push(fromPlugin(id === "default" ? namespace : `${namespace}_${id}`, def))
111114
}
115+
}
112116

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

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-
)
221+
return Service.of({ ids, named: { task, read }, tools })
222+
}),
223+
)
221224

222225
export const defaultLayer = Layer.unwrap(
223226
Effect.sync(() =>
224227
layer.pipe(
225228
Layer.provide(Config.defaultLayer),
226229
Layer.provide(Plugin.defaultLayer),
230+
Layer.provide(AgentSvc.defaultLayer),
227231
Layer.provide(Question.defaultLayer),
228232
Layer.provide(Todo.defaultLayer),
229233
),

0 commit comments

Comments
 (0)