Skip to content

Commit d9d5a06

Browse files
authored
refactor: break SessionPrompt/TaskTool cycle via ctx injection (#21948)
1 parent d72ddd7 commit d9d5a06

3 files changed

Lines changed: 47 additions & 67 deletions

File tree

packages/opencode/src/session/prompt.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import { Process } from "@/util/process"
4646
import { Cause, Effect, Exit, Layer, Option, Scope, ServiceMap } from "effect"
4747
import { InstanceState } from "@/effect/instance-state"
4848
import { makeRuntime } from "@/effect/run-service"
49-
import { TaskTool } from "@/tool/task"
49+
import { TaskTool, type TaskPromptOps } from "@/tool/task"
5050
import { SessionRunState } from "./run-state"
5151

5252
// @ts-ignore
@@ -356,7 +356,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
356356
abort: options.abortSignal!,
357357
messageID: input.processor.message.id,
358358
callID: options.toolCallId,
359-
extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck },
359+
extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck, promptOps },
360360
agent: input.agent.name,
361361
messages: input.messages,
362362
metadata: (val) =>
@@ -586,7 +586,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
586586
sessionID,
587587
abort: signal,
588588
callID: part.callID,
589-
extra: { bypassAgentCheck: true },
589+
extra: { bypassAgentCheck: true, promptOps },
590590
messages: msgs,
591591
metadata(val: { title?: string; metadata?: Record<string, any> }) {
592592
return Effect.runPromise(
@@ -1655,6 +1655,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the
16551655
return result
16561656
})
16571657

1658+
const promptOps: TaskPromptOps = {
1659+
cancel: (sessionID) => Effect.runFork(cancel(sessionID)),
1660+
resolvePromptParts: (template) => Effect.runPromise(resolvePromptParts(template)),
1661+
prompt: (input) => Effect.runPromise(prompt(input)),
1662+
}
1663+
16581664
return Service.of({
16591665
cancel,
16601666
prompt,

packages/opencode/src/tool/task.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ import { Session } from "../session"
55
import { SessionID, MessageID } from "../session/schema"
66
import { MessageV2 } from "../session/message-v2"
77
import { Agent } from "../agent/agent"
8-
import { SessionPrompt } from "../session/prompt"
8+
import type { SessionPrompt } from "../session/prompt"
99
import { Config } from "../config/config"
1010
import { Effect } from "effect"
1111
import { Log } from "@/util/log"
1212

13+
export interface TaskPromptOps {
14+
cancel(sessionID: SessionID): void
15+
resolvePromptParts(template: string): Promise<SessionPrompt.PromptInput["parts"]>
16+
prompt(input: SessionPrompt.PromptInput): Promise<MessageV2.WithParts>
17+
}
18+
1319
const id = "task"
1420

1521
const parameters = z.object({
@@ -113,10 +119,13 @@ export const TaskTool = Tool.defineEffect(
113119
},
114120
})
115121

122+
const ops = ctx.extra?.promptOps as TaskPromptOps
123+
if (!ops) return yield* Effect.fail(new Error("TaskTool requires promptOps in ctx.extra"))
124+
116125
const messageID = MessageID.ascending()
117126

118127
function cancel() {
119-
SessionPrompt.cancel(nextSession.id)
128+
ops.cancel(nextSession.id)
120129
}
121130

122131
return yield* Effect.acquireUseRelease(
@@ -125,9 +134,9 @@ export const TaskTool = Tool.defineEffect(
125134
}),
126135
() =>
127136
Effect.gen(function* () {
128-
const parts = yield* Effect.promise(() => SessionPrompt.resolvePromptParts(params.prompt))
137+
const parts = yield* Effect.promise(() => ops.resolvePromptParts(params.prompt))
129138
const result = yield* Effect.promise(() =>
130-
SessionPrompt.prompt({
139+
ops.prompt({
131140
messageID,
132141
sessionID: nextSession.id,
133142
model: {

packages/opencode/test/tool/task.test.ts

Lines changed: 25 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
66
import { Instance } from "../../src/project/instance"
77
import { Session } from "../../src/session"
88
import { MessageV2 } from "../../src/session/message-v2"
9-
import { SessionPrompt } from "../../src/session/prompt"
9+
import type { SessionPrompt } from "../../src/session/prompt"
1010
import { MessageID, PartID } from "../../src/session/schema"
1111
import { ModelID, ProviderID } from "../../src/provider/schema"
12-
import { TaskTool } from "../../src/tool/task"
12+
import { TaskTool, type TaskPromptOps } from "../../src/tool/task"
1313
import { ToolRegistry } from "../../src/tool/registry"
1414
import { provideTmpdirInstance } from "../fixture/fixture"
1515
import { testEffect } from "../lib/effect"
@@ -62,6 +62,17 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") {
6262
return { chat, assistant }
6363
})
6464

65+
function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void; text?: string }): TaskPromptOps {
66+
return {
67+
cancel() {},
68+
resolvePromptParts: async (template) => [{ type: "text", text: template }],
69+
prompt: async (input) => {
70+
opts?.onPrompt?.(input)
71+
return reply(input, opts?.text ?? "done")
72+
},
73+
}
74+
}
75+
6576
function reply(input: Parameters<typeof SessionPrompt.prompt>[0], text: string): MessageV2.WithParts {
6677
const id = MessageID.ascending()
6778
return {
@@ -180,21 +191,8 @@ describe("tool.task", () => {
180191
const child = yield* sessions.create({ parentID: chat.id, title: "Existing child" })
181192
const tool = yield* TaskTool
182193
const def = yield* Effect.promise(() => tool.init())
183-
const resolve = SessionPrompt.resolvePromptParts
184-
const prompt = SessionPrompt.prompt
185-
let seen: Parameters<typeof SessionPrompt.prompt>[0] | undefined
186-
187-
SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }]
188-
SessionPrompt.prompt = async (input) => {
189-
seen = input
190-
return reply(input, "resumed")
191-
}
192-
yield* Effect.addFinalizer(() =>
193-
Effect.sync(() => {
194-
SessionPrompt.resolvePromptParts = resolve
195-
SessionPrompt.prompt = prompt
196-
}),
197-
)
194+
let seen: SessionPrompt.PromptInput | undefined
195+
const promptOps = stubOps({ text: "resumed", onPrompt: (input) => (seen = input) })
198196

199197
const result = yield* Effect.promise(() =>
200198
def.execute(
@@ -209,6 +207,7 @@ describe("tool.task", () => {
209207
messageID: assistant.id,
210208
agent: "build",
211209
abort: new AbortController().signal,
210+
extra: { promptOps },
212211
messages: [],
213212
metadata() {},
214213
ask: async () => {},
@@ -232,20 +231,10 @@ describe("tool.task", () => {
232231
const { chat, assistant } = yield* seed()
233232
const tool = yield* TaskTool
234233
const def = yield* Effect.promise(() => tool.init())
235-
const resolve = SessionPrompt.resolvePromptParts
236-
const prompt = SessionPrompt.prompt
237234
const calls: unknown[] = []
235+
const promptOps = stubOps()
238236

239-
SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }]
240-
SessionPrompt.prompt = async (input) => reply(input, "done")
241-
yield* Effect.addFinalizer(() =>
242-
Effect.sync(() => {
243-
SessionPrompt.resolvePromptParts = resolve
244-
SessionPrompt.prompt = prompt
245-
}),
246-
)
247-
248-
const exec = (extra?: { bypassAgentCheck?: boolean }) =>
237+
const exec = (extra?: Record<string, any>) =>
249238
Effect.promise(() =>
250239
def.execute(
251240
{
@@ -258,7 +247,7 @@ describe("tool.task", () => {
258247
messageID: assistant.id,
259248
agent: "build",
260249
abort: new AbortController().signal,
261-
extra,
250+
extra: { promptOps, ...extra },
262251
messages: [],
263252
metadata() {},
264253
ask: async (input) => {
@@ -292,21 +281,8 @@ describe("tool.task", () => {
292281
const { chat, assistant } = yield* seed()
293282
const tool = yield* TaskTool
294283
const def = yield* Effect.promise(() => tool.init())
295-
const resolve = SessionPrompt.resolvePromptParts
296-
const prompt = SessionPrompt.prompt
297-
let seen: Parameters<typeof SessionPrompt.prompt>[0] | undefined
298-
299-
SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }]
300-
SessionPrompt.prompt = async (input) => {
301-
seen = input
302-
return reply(input, "created")
303-
}
304-
yield* Effect.addFinalizer(() =>
305-
Effect.sync(() => {
306-
SessionPrompt.resolvePromptParts = resolve
307-
SessionPrompt.prompt = prompt
308-
}),
309-
)
284+
let seen: SessionPrompt.PromptInput | undefined
285+
const promptOps = stubOps({ text: "created", onPrompt: (input) => (seen = input) })
310286

311287
const result = yield* Effect.promise(() =>
312288
def.execute(
@@ -321,6 +297,7 @@ describe("tool.task", () => {
321297
messageID: assistant.id,
322298
agent: "build",
323299
abort: new AbortController().signal,
300+
extra: { promptOps },
324301
messages: [],
325302
metadata() {},
326303
ask: async () => {},
@@ -346,21 +323,8 @@ describe("tool.task", () => {
346323
const { chat, assistant } = yield* seed()
347324
const tool = yield* TaskTool
348325
const def = yield* Effect.promise(() => tool.init())
349-
const resolve = SessionPrompt.resolvePromptParts
350-
const prompt = SessionPrompt.prompt
351-
let seen: Parameters<typeof SessionPrompt.prompt>[0] | undefined
352-
353-
SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }]
354-
SessionPrompt.prompt = async (input) => {
355-
seen = input
356-
return reply(input, "done")
357-
}
358-
yield* Effect.addFinalizer(() =>
359-
Effect.sync(() => {
360-
SessionPrompt.resolvePromptParts = resolve
361-
SessionPrompt.prompt = prompt
362-
}),
363-
)
326+
let seen: SessionPrompt.PromptInput | undefined
327+
const promptOps = stubOps({ onPrompt: (input) => (seen = input) })
364328

365329
const result = yield* Effect.promise(() =>
366330
def.execute(
@@ -374,6 +338,7 @@ describe("tool.task", () => {
374338
messageID: assistant.id,
375339
agent: "build",
376340
abort: new AbortController().signal,
341+
extra: { promptOps },
377342
messages: [],
378343
metadata() {},
379344
ask: async () => {},

0 commit comments

Comments
 (0)