Skip to content

Commit c3d3221

Browse files
committed
refactor: break SessionPrompt → ToolRegistry → TaskTool cycle
TaskTool no longer imports SessionPrompt at runtime. Instead, prompt operations (cancel, resolvePromptParts, prompt) are injected via ctx.extra.promptOps by the caller. This inverts the dependency so the service graph is acyclic, unblocking future single-runtime composition.
1 parent fb26308 commit c3d3221

3 files changed

Lines changed: 58 additions & 60 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.runPromise(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: 36 additions & 53 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"
@@ -180,21 +180,16 @@ describe("tool.task", () => {
180180
const child = yield* sessions.create({ parentID: chat.id, title: "Existing child" })
181181
const tool = yield* TaskTool
182182
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
183+
let seen: SessionPrompt.PromptInput | undefined
186184

187-
SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }]
188-
SessionPrompt.prompt = async (input) => {
189-
seen = input
190-
return reply(input, "resumed")
185+
const promptOps: TaskPromptOps = {
186+
cancel() {},
187+
resolvePromptParts: async (template) => [{ type: "text", text: template }],
188+
prompt: async (input) => {
189+
seen = input
190+
return reply(input, "resumed")
191+
},
191192
}
192-
yield* Effect.addFinalizer(() =>
193-
Effect.sync(() => {
194-
SessionPrompt.resolvePromptParts = resolve
195-
SessionPrompt.prompt = prompt
196-
}),
197-
)
198193

199194
const result = yield* Effect.promise(() =>
200195
def.execute(
@@ -209,6 +204,7 @@ describe("tool.task", () => {
209204
messageID: assistant.id,
210205
agent: "build",
211206
abort: new AbortController().signal,
207+
extra: { promptOps },
212208
messages: [],
213209
metadata() {},
214210
ask: async () => {},
@@ -232,20 +228,15 @@ describe("tool.task", () => {
232228
const { chat, assistant } = yield* seed()
233229
const tool = yield* TaskTool
234230
const def = yield* Effect.promise(() => tool.init())
235-
const resolve = SessionPrompt.resolvePromptParts
236-
const prompt = SessionPrompt.prompt
237231
const calls: unknown[] = []
238232

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-
)
233+
const promptOps: TaskPromptOps = {
234+
cancel() {},
235+
resolvePromptParts: async (template) => [{ type: "text", text: template }],
236+
prompt: async (input) => reply(input, "done"),
237+
}
247238

248-
const exec = (extra?: { bypassAgentCheck?: boolean }) =>
239+
const exec = (extra?: Record<string, any>) =>
249240
Effect.promise(() =>
250241
def.execute(
251242
{
@@ -258,7 +249,7 @@ describe("tool.task", () => {
258249
messageID: assistant.id,
259250
agent: "build",
260251
abort: new AbortController().signal,
261-
extra,
252+
extra: { promptOps, ...extra },
262253
messages: [],
263254
metadata() {},
264255
ask: async (input) => {
@@ -292,21 +283,16 @@ describe("tool.task", () => {
292283
const { chat, assistant } = yield* seed()
293284
const tool = yield* TaskTool
294285
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
286+
let seen: SessionPrompt.PromptInput | undefined
298287

299-
SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }]
300-
SessionPrompt.prompt = async (input) => {
301-
seen = input
302-
return reply(input, "created")
288+
const promptOps: TaskPromptOps = {
289+
cancel() {},
290+
resolvePromptParts: async (template) => [{ type: "text", text: template }],
291+
prompt: async (input) => {
292+
seen = input
293+
return reply(input, "created")
294+
},
303295
}
304-
yield* Effect.addFinalizer(() =>
305-
Effect.sync(() => {
306-
SessionPrompt.resolvePromptParts = resolve
307-
SessionPrompt.prompt = prompt
308-
}),
309-
)
310296

311297
const result = yield* Effect.promise(() =>
312298
def.execute(
@@ -321,6 +307,7 @@ describe("tool.task", () => {
321307
messageID: assistant.id,
322308
agent: "build",
323309
abort: new AbortController().signal,
310+
extra: { promptOps },
324311
messages: [],
325312
metadata() {},
326313
ask: async () => {},
@@ -346,21 +333,16 @@ describe("tool.task", () => {
346333
const { chat, assistant } = yield* seed()
347334
const tool = yield* TaskTool
348335
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
336+
let seen: SessionPrompt.PromptInput | undefined
352337

353-
SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }]
354-
SessionPrompt.prompt = async (input) => {
355-
seen = input
356-
return reply(input, "done")
338+
const promptOps: TaskPromptOps = {
339+
cancel() {},
340+
resolvePromptParts: async (template) => [{ type: "text", text: template }],
341+
prompt: async (input) => {
342+
seen = input
343+
return reply(input, "done")
344+
},
357345
}
358-
yield* Effect.addFinalizer(() =>
359-
Effect.sync(() => {
360-
SessionPrompt.resolvePromptParts = resolve
361-
SessionPrompt.prompt = prompt
362-
}),
363-
)
364346

365347
const result = yield* Effect.promise(() =>
366348
def.execute(
@@ -374,6 +356,7 @@ describe("tool.task", () => {
374356
messageID: assistant.id,
375357
agent: "build",
376358
abort: new AbortController().signal,
359+
extra: { promptOps },
377360
messages: [],
378361
metadata() {},
379362
ask: async () => {},

0 commit comments

Comments
 (0)