refactor(effect): simplify task tool registry bridge

This commit is contained in:
Kit Langton 2026-04-08 13:59:58 -04:00
commit 0f3a2a7b67
4 changed files with 62 additions and 62 deletions

View file

@ -47,6 +47,7 @@ import { Process } from "@/util/process"
import { Cause, Effect, Exit, Layer, Option, Scope, ServiceMap } from "effect" import { Cause, Effect, Exit, Layer, Option, Scope, ServiceMap } from "effect"
import { InstanceState } from "@/effect/instance-state" import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service" import { makeRuntime } from "@/effect/run-service"
import { TaskTool } from "@/tool/task"
// @ts-ignore // @ts-ignore
globalThis.AI_SDK_LOG_WARNINGS = false globalThis.AI_SDK_LOG_WARNINGS = false
@ -558,7 +559,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
}) { }) {
const { task, model, lastUser, sessionID, session, msgs } = input const { task, model, lastUser, sessionID, session, msgs } = input
const ctx = yield* InstanceState.context const ctx = yield* InstanceState.context
const taskTool = yield* registry.fromID("task") const taskTool = yield* registry.fromID(TaskTool.id)
const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model
const assistantMessage: MessageV2.Assistant = yield* sessions.updateMessage({ const assistantMessage: MessageV2.Assistant = yield* sessions.updateMessage({
id: MessageID.ascending(), id: MessageID.ascending(),
@ -581,7 +582,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
sessionID: assistantMessage.sessionID, sessionID: assistantMessage.sessionID,
type: "tool", type: "tool",
callID: ulid(), callID: ulid(),
tool: "task", tool: TaskTool.id,
state: { state: {
status: "running", status: "running",
input: { input: {

View file

@ -82,18 +82,10 @@ export namespace ToolRegistry {
const config = yield* Config.Service const config = yield* Config.Service
const plugin = yield* Plugin.Service const plugin = yield* Plugin.Service
const info = <T extends Tool.Info, R = never>( const task = yield* TaskTool
tool: T | Effect.Effect<T, never, R>, const read = yield* ReadTool
): Effect.Effect<T, never, R> => (Effect.isEffect(tool) ? tool : Effect.succeed(tool)) const question = yield* QuestionTool
const todo = yield* TodoWriteTool
const build = <T extends Tool.Info, R = never>(
tool: T | Effect.Effect<T, never, R>,
): Effect.Effect<Tool.Def, never, R> => info(tool).pipe(Effect.flatMap(Tool.init))
const task = yield* info(TaskTool)
const read = yield* info(ReadTool)
const askInfo = yield* info(QuestionTool)
const todoInfo = yield* info(TodoWriteTool)
const state = yield* InstanceState.make<State>( const state = yield* InstanceState.make<State>(
Effect.fn("ToolRegistry.state")(function* (ctx) { Effect.fn("ToolRegistry.state")(function* (ctx) {
@ -147,47 +139,49 @@ export namespace ToolRegistry {
} }
const cfg = yield* config.get() const cfg = yield* config.get()
const question = const questionEnabled =
["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL ["app", "cli", "desktop"].includes(Flag.OPENCODE_CLIENT) || Flag.OPENCODE_ENABLE_QUESTION_TOOL
const invalid = yield* build(InvalidTool) const tool = yield* Effect.all({
const bash = yield* build(BashTool) invalid: Tool.init(InvalidTool),
const readDef = yield* build(read) bash: Tool.init(BashTool),
const glob = yield* build(GlobTool) read: Tool.init(read),
const grep = yield* build(GrepTool) glob: Tool.init(GlobTool),
const edit = yield* build(EditTool) grep: Tool.init(GrepTool),
const write = yield* build(WriteTool) edit: Tool.init(EditTool),
const taskDef = yield* build(task) write: Tool.init(WriteTool),
const fetch = yield* build(WebFetchTool) task: Tool.init(task),
const todo = yield* build(todoInfo) fetch: Tool.init(WebFetchTool),
const search = yield* build(WebSearchTool) todo: Tool.init(todo),
const code = yield* build(CodeSearchTool) search: Tool.init(WebSearchTool),
const skill = yield* build(SkillTool) code: Tool.init(CodeSearchTool),
const patch = yield* build(ApplyPatchTool) skill: Tool.init(SkillTool),
const ask = yield* build(askInfo) patch: Tool.init(ApplyPatchTool),
const lsp = yield* build(LspTool) question: Tool.init(question),
const plan = yield* build(PlanExitTool) lsp: Tool.init(LspTool),
plan: Tool.init(PlanExitTool),
})
return { return {
custom, custom,
builtin: [ builtin: [
invalid, tool.invalid,
...(question ? [ask] : []), ...(questionEnabled ? [tool.question] : []),
bash, tool.bash,
readDef, tool.read,
glob, tool.glob,
grep, tool.grep,
edit, tool.edit,
write, tool.write,
taskDef, tool.task,
fetch, tool.fetch,
todo, tool.todo,
search, tool.search,
code, tool.code,
skill, tool.skill,
patch, tool.patch,
...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [lsp] : []), ...(Flag.OPENCODE_EXPERIMENTAL_LSP_TOOL ? [tool.lsp] : []),
...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [plan] : []), ...(Flag.OPENCODE_EXPERIMENTAL_PLAN_MODE && Flag.OPENCODE_CLIENT === "cli" ? [tool.plan] : []),
], ],
} }
}), }),
@ -237,7 +231,7 @@ export namespace ToolRegistry {
id: tool.id, id: tool.id,
description: [ description: [
output.description, output.description,
tool.id === "task" ? yield* TaskDescription(input.agent) : undefined, tool.id === TaskTool.id ? yield* TaskDescription(input.agent) : undefined,
tool.id === SkillTool.id ? yield* SkillDescription(input.agent) : undefined, tool.id === SkillTool.id ? yield* SkillDescription(input.agent) : undefined,
] ]
.filter(Boolean) .filter(Boolean)

View file

@ -10,6 +10,8 @@ import { Config } from "../config/config"
import { Permission } from "@/permission" import { Permission } from "@/permission"
import { Effect } from "effect" import { Effect } from "effect"
const id = "task"
const parameters = z.object({ const parameters = z.object({
description: z.string().describe("A short (3-5 words) description of the task"), description: z.string().describe("A short (3-5 words) description of the task"),
prompt: z.string().describe("The task for the agent to perform"), prompt: z.string().describe("The task for the agent to perform"),
@ -24,7 +26,7 @@ const parameters = z.object({
}) })
export const TaskTool = Tool.defineEffect( export const TaskTool = Tool.defineEffect(
"task", id,
Effect.gen(function* () { Effect.gen(function* () {
const agent = yield* Agent.Service const agent = yield* Agent.Service
const config = yield* Config.Service const config = yield* Config.Service
@ -35,7 +37,7 @@ export const TaskTool = Tool.defineEffect(
if (!ctx.extra?.bypassAgentCheck) { if (!ctx.extra?.bypassAgentCheck) {
yield* Effect.promise(() => yield* Effect.promise(() =>
ctx.ask({ ctx.ask({
permission: "task", permission: id,
patterns: [params.subagent_type], patterns: [params.subagent_type],
always: ["*"], always: ["*"],
metadata: { metadata: {
@ -51,7 +53,7 @@ export const TaskTool = Tool.defineEffect(
return yield* Effect.fail(new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)) return yield* Effect.fail(new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`))
} }
const canTask = next.permission.some((rule) => rule.permission === "task") const canTask = next.permission.some((rule) => rule.permission === id)
const canTodo = next.permission.some((rule) => rule.permission === "todowrite") const canTodo = next.permission.some((rule) => rule.permission === "todowrite")
const taskID = params.task_id const taskID = params.task_id
@ -81,7 +83,7 @@ export const TaskTool = Tool.defineEffect(
? [] ? []
: [ : [
{ {
permission: "task" as const, permission: id,
pattern: "*" as const, pattern: "*" as const,
action: "deny" as const, action: "deny" as const,
}, },

View file

@ -98,24 +98,27 @@ export namespace Tool {
} }
} }
export function define<Parameters extends z.ZodType, Result extends Metadata>( export function define<Parameters extends z.ZodType, Result extends Metadata, ID extends string = string>(
id: string, id: ID,
init: (() => Promise<DefWithoutID<Parameters, Result>>) | DefWithoutID<Parameters, Result>, init: (() => Promise<DefWithoutID<Parameters, Result>>) | DefWithoutID<Parameters, Result>,
): Info<Parameters, Result> { ): Info<Parameters, Result> & { id: ID } {
return { return {
id, id,
init: wrap(id, init), init: wrap(id, init),
} }
} }
export function defineEffect<Parameters extends z.ZodType, Result extends Metadata, R>( export function defineEffect<Parameters extends z.ZodType, Result extends Metadata, R, ID extends string = string>(
id: string, id: ID,
init: Effect.Effect<(() => Promise<DefWithoutID<Parameters, Result>>) | DefWithoutID<Parameters, Result>, never, R>, init: Effect.Effect<(() => Promise<DefWithoutID<Parameters, Result>>) | DefWithoutID<Parameters, Result>, never, R>,
): Effect.Effect<Info<Parameters, Result>, never, R> { ): Effect.Effect<Info<Parameters, Result>, never, R> & { id: ID } {
return Effect.map(init, (next) => ({ id, init: wrap(id, next) })) return Object.assign(
Effect.map(init, (next) => ({ id, init: wrap(id, next) })),
{ id },
)
} }
export function init(info: Info): Effect.Effect<Def, never, any> { export function init(info: Info): Effect.Effect<Def> {
return Effect.gen(function* () { return Effect.gen(function* () {
const init = yield* Effect.promise(() => info.init()) const init = yield* Effect.promise(() => info.init())
return { return {