summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorKit Langton <[email protected]>2026-04-10 19:36:13 -0400
committerGitHub <[email protected]>2026-04-10 19:36:13 -0400
commitd9d5a0615e2fbfc702bbb8eca2557868b053a3aa (patch)
treee837103eb6cf7be0d7e94543969d8e910f3e4ab9
parentd72ddd71faf144864c985cd6372eeea7f2d4ba6f (diff)
downloadopencode-d9d5a0615e2fbfc702bbb8eca2557868b053a3aa.tar.gz
opencode-d9d5a0615e2fbfc702bbb8eca2557868b053a3aa.zip
refactor: break SessionPrompt/TaskTool cycle via ctx injection (#21948)
-rw-r--r--packages/opencode/src/session/prompt.ts12
-rw-r--r--packages/opencode/src/tool/task.ts17
-rw-r--r--packages/opencode/test/tool/task.test.ts85
3 files changed, 47 insertions, 67 deletions
diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts
index 33be6b9c5..66740cd40 100644
--- a/packages/opencode/src/session/prompt.ts
+++ b/packages/opencode/src/session/prompt.ts
@@ -46,7 +46,7 @@ import { Process } from "@/util/process"
import { Cause, Effect, Exit, Layer, Option, Scope, ServiceMap } from "effect"
import { InstanceState } from "@/effect/instance-state"
import { makeRuntime } from "@/effect/run-service"
-import { TaskTool } from "@/tool/task"
+import { TaskTool, type TaskPromptOps } from "@/tool/task"
import { SessionRunState } from "./run-state"
// @ts-ignore
@@ -356,7 +356,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
abort: options.abortSignal!,
messageID: input.processor.message.id,
callID: options.toolCallId,
- extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck },
+ extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck, promptOps },
agent: input.agent.name,
messages: input.messages,
metadata: (val) =>
@@ -586,7 +586,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
sessionID,
abort: signal,
callID: part.callID,
- extra: { bypassAgentCheck: true },
+ extra: { bypassAgentCheck: true, promptOps },
messages: msgs,
metadata(val: { title?: string; metadata?: Record<string, any> }) {
return Effect.runPromise(
@@ -1655,6 +1655,12 @@ NOTE: At any point in time through this workflow you should feel free to ask the
return result
})
+ const promptOps: TaskPromptOps = {
+ cancel: (sessionID) => Effect.runFork(cancel(sessionID)),
+ resolvePromptParts: (template) => Effect.runPromise(resolvePromptParts(template)),
+ prompt: (input) => Effect.runPromise(prompt(input)),
+ }
+
return Service.of({
cancel,
prompt,
diff --git a/packages/opencode/src/tool/task.ts b/packages/opencode/src/tool/task.ts
index 900938f0d..440691e46 100644
--- a/packages/opencode/src/tool/task.ts
+++ b/packages/opencode/src/tool/task.ts
@@ -5,11 +5,17 @@ import { Session } from "../session"
import { SessionID, MessageID } from "../session/schema"
import { MessageV2 } from "../session/message-v2"
import { Agent } from "../agent/agent"
-import { SessionPrompt } from "../session/prompt"
+import type { SessionPrompt } from "../session/prompt"
import { Config } from "../config/config"
import { Effect } from "effect"
import { Log } from "@/util/log"
+export interface TaskPromptOps {
+ cancel(sessionID: SessionID): void
+ resolvePromptParts(template: string): Promise<SessionPrompt.PromptInput["parts"]>
+ prompt(input: SessionPrompt.PromptInput): Promise<MessageV2.WithParts>
+}
+
const id = "task"
const parameters = z.object({
@@ -113,10 +119,13 @@ export const TaskTool = Tool.defineEffect(
},
})
+ const ops = ctx.extra?.promptOps as TaskPromptOps
+ if (!ops) return yield* Effect.fail(new Error("TaskTool requires promptOps in ctx.extra"))
+
const messageID = MessageID.ascending()
function cancel() {
- SessionPrompt.cancel(nextSession.id)
+ ops.cancel(nextSession.id)
}
return yield* Effect.acquireUseRelease(
@@ -125,9 +134,9 @@ export const TaskTool = Tool.defineEffect(
}),
() =>
Effect.gen(function* () {
- const parts = yield* Effect.promise(() => SessionPrompt.resolvePromptParts(params.prompt))
+ const parts = yield* Effect.promise(() => ops.resolvePromptParts(params.prompt))
const result = yield* Effect.promise(() =>
- SessionPrompt.prompt({
+ ops.prompt({
messageID,
sessionID: nextSession.id,
model: {
diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts
index e3e6d58d3..c019052a5 100644
--- a/packages/opencode/test/tool/task.test.ts
+++ b/packages/opencode/test/tool/task.test.ts
@@ -6,10 +6,10 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
import { Instance } from "../../src/project/instance"
import { Session } from "../../src/session"
import { MessageV2 } from "../../src/session/message-v2"
-import { SessionPrompt } from "../../src/session/prompt"
+import type { SessionPrompt } from "../../src/session/prompt"
import { MessageID, PartID } from "../../src/session/schema"
import { ModelID, ProviderID } from "../../src/provider/schema"
-import { TaskTool } from "../../src/tool/task"
+import { TaskTool, type TaskPromptOps } from "../../src/tool/task"
import { ToolRegistry } from "../../src/tool/registry"
import { provideTmpdirInstance } from "../fixture/fixture"
import { testEffect } from "../lib/effect"
@@ -62,6 +62,17 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") {
return { chat, assistant }
})
+function stubOps(opts?: { onPrompt?: (input: SessionPrompt.PromptInput) => void; text?: string }): TaskPromptOps {
+ return {
+ cancel() {},
+ resolvePromptParts: async (template) => [{ type: "text", text: template }],
+ prompt: async (input) => {
+ opts?.onPrompt?.(input)
+ return reply(input, opts?.text ?? "done")
+ },
+ }
+}
+
function reply(input: Parameters<typeof SessionPrompt.prompt>[0], text: string): MessageV2.WithParts {
const id = MessageID.ascending()
return {
@@ -180,21 +191,8 @@ describe("tool.task", () => {
const child = yield* sessions.create({ parentID: chat.id, title: "Existing child" })
const tool = yield* TaskTool
const def = yield* Effect.promise(() => tool.init())
- const resolve = SessionPrompt.resolvePromptParts
- const prompt = SessionPrompt.prompt
- let seen: Parameters<typeof SessionPrompt.prompt>[0] | undefined
-
- SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }]
- SessionPrompt.prompt = async (input) => {
- seen = input
- return reply(input, "resumed")
- }
- yield* Effect.addFinalizer(() =>
- Effect.sync(() => {
- SessionPrompt.resolvePromptParts = resolve
- SessionPrompt.prompt = prompt
- }),
- )
+ let seen: SessionPrompt.PromptInput | undefined
+ const promptOps = stubOps({ text: "resumed", onPrompt: (input) => (seen = input) })
const result = yield* Effect.promise(() =>
def.execute(
@@ -209,6 +207,7 @@ describe("tool.task", () => {
messageID: assistant.id,
agent: "build",
abort: new AbortController().signal,
+ extra: { promptOps },
messages: [],
metadata() {},
ask: async () => {},
@@ -232,20 +231,10 @@ describe("tool.task", () => {
const { chat, assistant } = yield* seed()
const tool = yield* TaskTool
const def = yield* Effect.promise(() => tool.init())
- const resolve = SessionPrompt.resolvePromptParts
- const prompt = SessionPrompt.prompt
const calls: unknown[] = []
+ const promptOps = stubOps()
- SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }]
- SessionPrompt.prompt = async (input) => reply(input, "done")
- yield* Effect.addFinalizer(() =>
- Effect.sync(() => {
- SessionPrompt.resolvePromptParts = resolve
- SessionPrompt.prompt = prompt
- }),
- )
-
- const exec = (extra?: { bypassAgentCheck?: boolean }) =>
+ const exec = (extra?: Record<string, any>) =>
Effect.promise(() =>
def.execute(
{
@@ -258,7 +247,7 @@ describe("tool.task", () => {
messageID: assistant.id,
agent: "build",
abort: new AbortController().signal,
- extra,
+ extra: { promptOps, ...extra },
messages: [],
metadata() {},
ask: async (input) => {
@@ -292,21 +281,8 @@ describe("tool.task", () => {
const { chat, assistant } = yield* seed()
const tool = yield* TaskTool
const def = yield* Effect.promise(() => tool.init())
- const resolve = SessionPrompt.resolvePromptParts
- const prompt = SessionPrompt.prompt
- let seen: Parameters<typeof SessionPrompt.prompt>[0] | undefined
-
- SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }]
- SessionPrompt.prompt = async (input) => {
- seen = input
- return reply(input, "created")
- }
- yield* Effect.addFinalizer(() =>
- Effect.sync(() => {
- SessionPrompt.resolvePromptParts = resolve
- SessionPrompt.prompt = prompt
- }),
- )
+ let seen: SessionPrompt.PromptInput | undefined
+ const promptOps = stubOps({ text: "created", onPrompt: (input) => (seen = input) })
const result = yield* Effect.promise(() =>
def.execute(
@@ -321,6 +297,7 @@ describe("tool.task", () => {
messageID: assistant.id,
agent: "build",
abort: new AbortController().signal,
+ extra: { promptOps },
messages: [],
metadata() {},
ask: async () => {},
@@ -346,21 +323,8 @@ describe("tool.task", () => {
const { chat, assistant } = yield* seed()
const tool = yield* TaskTool
const def = yield* Effect.promise(() => tool.init())
- const resolve = SessionPrompt.resolvePromptParts
- const prompt = SessionPrompt.prompt
- let seen: Parameters<typeof SessionPrompt.prompt>[0] | undefined
-
- SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }]
- SessionPrompt.prompt = async (input) => {
- seen = input
- return reply(input, "done")
- }
- yield* Effect.addFinalizer(() =>
- Effect.sync(() => {
- SessionPrompt.resolvePromptParts = resolve
- SessionPrompt.prompt = prompt
- }),
- )
+ let seen: SessionPrompt.PromptInput | undefined
+ const promptOps = stubOps({ onPrompt: (input) => (seen = input) })
const result = yield* Effect.promise(() =>
def.execute(
@@ -374,6 +338,7 @@ describe("tool.task", () => {
messageID: assistant.id,
agent: "build",
abort: new AbortController().signal,
+ extra: { promptOps },
messages: [],
metadata() {},
ask: async () => {},