diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 33be6b9c58..66740cd401 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 }) { 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 900938f0d3..440691e46d 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 + prompt(input: SessionPrompt.PromptInput): Promise +} + 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 e3e6d58d3c..c019052a5e 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[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[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) => 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[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[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 () => {},