Skip to content

Commit 9c51cf4

Browse files
committed
refactor(effect): effectify task tool execution
Move the task tool body onto named Effect helpers and keep the Promise bridge at the outer init/execute boundaries. This matches the read tool migration shape while preserving the existing SessionPrompt runtime flow.
1 parent c6cd64a commit 9c51cf4

1 file changed

Lines changed: 142 additions & 123 deletions

File tree

packages/opencode/src/tool/task.ts

Lines changed: 142 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ import { Effect } from "effect"
55
import { Session } from "../session"
66
import { SessionID, MessageID } from "../session/schema"
77
import { MessageV2 } from "../session/message-v2"
8-
import { Identifier } from "../id/id"
98
import { Agent } from "../agent/agent"
109
import { SessionPrompt } from "../session/prompt"
1110
import { iife } from "@/util/iife"
12-
import { defer } from "@/util/defer"
1311
import { Config } from "../config/config"
1412
import { Permission } from "@/permission"
1513

@@ -32,148 +30,169 @@ export const TaskTool = Tool.defineEffect(
3230
const agent = yield* Agent.Service
3331
const config = yield* Config.Service
3432

35-
return async (ctx) => {
36-
const agents = await agent.list().pipe(
37-
Effect.map((x) => x.filter((a) => a.mode !== "primary")),
38-
Effect.runPromise,
39-
)
40-
41-
const caller = ctx?.agent
42-
const accessibleAgents = caller
43-
? agents.filter((a) => Permission.evaluate("task", a.name, caller.permission).action !== "deny")
44-
: agents
45-
const list = accessibleAgents.toSorted((a, b) => a.name.localeCompare(b.name))
46-
47-
const description = DESCRIPTION.replace(
33+
const list = Effect.fn("TaskTool.list")(function* (caller?: Tool.InitContext["agent"]) {
34+
const items = yield* agent.list().pipe(Effect.map((items) => items.filter((item) => item.mode !== "primary")))
35+
const filtered = caller
36+
? items.filter((item) => Permission.evaluate("task", item.name, caller.permission).action !== "deny")
37+
: items
38+
return filtered.toSorted((a, b) => a.name.localeCompare(b.name))
39+
})
40+
41+
const desc = Effect.fn("TaskTool.desc")(function* (caller?: Tool.InitContext["agent"]) {
42+
const items = yield* list(caller)
43+
return DESCRIPTION.replace(
4844
"{agents}",
49-
list
50-
.map((a) => `- ${a.name}: ${a.description ?? "This subagent should only be called manually by the user."}`)
45+
items
46+
.map(
47+
(item) =>
48+
`- ${item.name}: ${item.description ?? "This subagent should only be called manually by the user."}`,
49+
)
5150
.join("\n"),
5251
)
52+
})
5353

54-
return {
55-
description,
56-
parameters,
57-
async execute(params: z.infer<typeof parameters>, ctx) {
58-
const cfg = await config.get().pipe(Effect.runPromise)
59-
60-
// Skip permission check when user explicitly invoked via @ or command subtask
61-
if (!ctx.extra?.bypassAgentCheck) {
62-
await ctx.ask({
63-
permission: "task",
64-
patterns: [params.subagent_type],
65-
always: ["*"],
66-
metadata: {
67-
description: params.description,
68-
subagent_type: params.subagent_type,
69-
},
70-
})
71-
}
54+
const run = Effect.fn("TaskTool.execute")(function* (params: z.infer<typeof parameters>, ctx: Tool.Context) {
55+
const cfg = yield* config.get()
7256

73-
const next = await agent
74-
.get(params.subagent_type)
75-
.pipe(Effect.runPromise)
76-
.catch(() => undefined)
77-
if (!next) throw new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`)
78-
79-
const hasTaskPermission = next.permission.some((rule) => rule.permission === "task")
80-
const hasTodoWritePermission = next.permission.some((rule) => rule.permission === "todowrite")
81-
82-
const session = await iife(async () => {
83-
if (params.task_id) {
84-
const found = await Session.get(SessionID.make(params.task_id)).catch(() => {})
85-
if (found) return found
86-
}
87-
88-
return await Session.create({
89-
parentID: ctx.sessionID,
90-
title: params.description + ` (@${next.name} subagent)`,
91-
permission: [
92-
...(hasTodoWritePermission
93-
? []
94-
: [
95-
{
96-
permission: "todowrite" as const,
97-
pattern: "*" as const,
98-
action: "deny" as const,
99-
},
100-
]),
101-
...(hasTaskPermission
102-
? []
103-
: [
104-
{
105-
permission: "task" as const,
106-
pattern: "*" as const,
107-
action: "deny" as const,
108-
},
109-
]),
110-
...(cfg.experimental?.primary_tools?.map((t) => ({
111-
pattern: "*",
112-
action: "allow" as const,
113-
permission: t,
114-
})) ?? []),
115-
],
116-
})
117-
})
118-
const msg = await MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID })
119-
if (msg.info.role !== "assistant") throw new Error("Not an assistant message")
120-
121-
const model = next.model ?? {
122-
modelID: msg.info.modelID,
123-
providerID: msg.info.providerID,
124-
}
125-
126-
ctx.metadata({
127-
title: params.description,
57+
if (!ctx.extra?.bypassAgentCheck) {
58+
yield* Effect.promise(() =>
59+
ctx.ask({
60+
permission: "task",
61+
patterns: [params.subagent_type],
62+
always: ["*"],
12863
metadata: {
129-
sessionId: session.id,
130-
model,
64+
description: params.description,
65+
subagent_type: params.subagent_type,
13166
},
132-
})
67+
}),
68+
)
69+
}
70+
71+
const next = yield* agent.get(params.subagent_type).pipe(Effect.catch(() => Effect.succeed(undefined)))
72+
if (!next) {
73+
return yield* Effect.fail(new Error(`Unknown agent type: ${params.subagent_type} is not a valid agent type`))
74+
}
13375

134-
const messageID = MessageID.ascending()
76+
const hasTask = next.permission.some((rule) => rule.permission === "task")
77+
const hasTodo = next.permission.some((rule) => rule.permission === "todowrite")
13578

136-
function cancel() {
137-
SessionPrompt.cancel(session.id)
79+
const session = yield* Effect.promise(() =>
80+
iife(async () => {
81+
if (params.task_id) {
82+
const found = await Session.get(SessionID.make(params.task_id)).catch(() => {})
83+
if (found) return found
13884
}
139-
ctx.abort.addEventListener("abort", cancel)
140-
using _ = defer(() => ctx.abort.removeEventListener("abort", cancel))
141-
const promptParts = await SessionPrompt.resolvePromptParts(params.prompt)
142-
143-
const result = await SessionPrompt.prompt({
144-
messageID,
145-
sessionID: session.id,
146-
model: {
147-
modelID: model.modelID,
148-
providerID: model.providerID,
149-
},
150-
agent: next.name,
151-
tools: {
152-
...(hasTodoWritePermission ? {} : { todowrite: false }),
153-
...(hasTaskPermission ? {} : { task: false }),
154-
...Object.fromEntries((cfg.experimental?.primary_tools ?? []).map((t) => [t, false])),
155-
},
156-
parts: promptParts,
85+
86+
return Session.create({
87+
parentID: ctx.sessionID,
88+
title: params.description + ` (@${next.name} subagent)`,
89+
permission: [
90+
...(hasTodo
91+
? []
92+
: [
93+
{
94+
permission: "todowrite" as const,
95+
pattern: "*" as const,
96+
action: "deny" as const,
97+
},
98+
]),
99+
...(hasTask
100+
? []
101+
: [
102+
{
103+
permission: "task" as const,
104+
pattern: "*" as const,
105+
action: "deny" as const,
106+
},
107+
]),
108+
...(cfg.experimental?.primary_tools?.map((item) => ({
109+
pattern: "*",
110+
action: "allow" as const,
111+
permission: item,
112+
})) ?? []),
113+
],
157114
})
115+
}),
116+
)
158117

159-
const text = result.parts.findLast((x) => x.type === "text")?.text ?? ""
118+
const msg = yield* Effect.sync(() => MessageV2.get({ sessionID: ctx.sessionID, messageID: ctx.messageID }))
119+
if (msg.info.role !== "assistant") return yield* Effect.fail(new Error("Not an assistant message"))
160120

161-
const output = [
162-
`task_id: ${session.id} (for resuming to continue this task if needed)`,
163-
"",
164-
"<task_result>",
165-
text,
166-
"</task_result>",
167-
].join("\n")
121+
const model = next.model ?? {
122+
modelID: msg.info.modelID,
123+
providerID: msg.info.providerID,
124+
}
168125

126+
ctx.metadata({
127+
title: params.description,
128+
metadata: {
129+
sessionId: session.id,
130+
model,
131+
},
132+
})
133+
134+
const messageID = MessageID.ascending()
135+
136+
function cancel() {
137+
SessionPrompt.cancel(session.id)
138+
}
139+
return yield* Effect.acquireUseRelease(
140+
Effect.sync(() => {
141+
ctx.abort.addEventListener("abort", cancel)
142+
}),
143+
() => Effect.promise(() => SessionPrompt.resolvePromptParts(params.prompt)),
144+
() =>
145+
Effect.sync(() => {
146+
ctx.abort.removeEventListener("abort", cancel)
147+
}),
148+
).pipe(
149+
Effect.flatMap((parts) =>
150+
Effect.promise(() =>
151+
SessionPrompt.prompt({
152+
messageID,
153+
sessionID: session.id,
154+
model: {
155+
modelID: model.modelID,
156+
providerID: model.providerID,
157+
},
158+
agent: next.name,
159+
tools: {
160+
...(hasTodo ? {} : { todowrite: false }),
161+
...(hasTask ? {} : { task: false }),
162+
...Object.fromEntries((cfg.experimental?.primary_tools ?? []).map((item) => [item, false])),
163+
},
164+
parts,
165+
}),
166+
),
167+
),
168+
Effect.map((result) => {
169+
const text = result.parts.findLast((item) => item.type === "text")?.text ?? ""
169170
return {
170171
title: params.description,
171172
metadata: {
172173
sessionId: session.id,
173174
model,
174175
},
175-
output,
176+
output: [
177+
`task_id: ${session.id} (for resuming to continue this task if needed)`,
178+
"",
179+
"<task_result>",
180+
text,
181+
"</task_result>",
182+
].join("\n"),
176183
}
184+
}),
185+
)
186+
})
187+
188+
return async (ctx) => {
189+
const description = await Effect.runPromise(desc(ctx?.agent))
190+
191+
return {
192+
description,
193+
parameters,
194+
async execute(params: z.infer<typeof parameters>, ctx) {
195+
return Effect.runPromise(run(params, ctx))
177196
},
178197
}
179198
}

0 commit comments

Comments
 (0)