refactor: break SessionPrompt/TaskTool cycle via ctx injection (#21948)
This commit is contained in:
@@ -46,7 +46,7 @@ import { Process } from "@/util/process"
|
|||||||
import { Cause, Effect, Exit, Layer, Option, Scope, ServiceMap } from "effect"
|
import { Cause, Effect, Exit, Layer, Option, Scope, ServiceMap } from "effect"
|
||||||
import { InstanceState } from "@/effect/instance-state"
|
import { InstanceState } from "@/effect/instance-state"
|
||||||
import { makeRuntime } from "@/effect/run-service"
|
import { makeRuntime } from "@/effect/run-service"
|
||||||
import { TaskTool } from "@/tool/task"
|
import { TaskTool, type TaskPromptOps } from "@/tool/task"
|
||||||
import { SessionRunState } from "./run-state"
|
import { SessionRunState } from "./run-state"
|
||||||
|
|
||||||
// @ts-ignore
|
// @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!,
|
abort: options.abortSignal!,
|
||||||
messageID: input.processor.message.id,
|
messageID: input.processor.message.id,
|
||||||
callID: options.toolCallId,
|
callID: options.toolCallId,
|
||||||
extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck },
|
extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck, promptOps },
|
||||||
agent: input.agent.name,
|
agent: input.agent.name,
|
||||||
messages: input.messages,
|
messages: input.messages,
|
||||||
metadata: (val) =>
|
metadata: (val) =>
|
||||||
@@ -586,7 +586,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
|
|||||||
sessionID,
|
sessionID,
|
||||||
abort: signal,
|
abort: signal,
|
||||||
callID: part.callID,
|
callID: part.callID,
|
||||||
extra: { bypassAgentCheck: true },
|
extra: { bypassAgentCheck: true, promptOps },
|
||||||
messages: msgs,
|
messages: msgs,
|
||||||
metadata(val: { title?: string; metadata?: Record<string, any> }) {
|
metadata(val: { title?: string; metadata?: Record<string, any> }) {
|
||||||
return Effect.runPromise(
|
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
|
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({
|
return Service.of({
|
||||||
cancel,
|
cancel,
|
||||||
prompt,
|
prompt,
|
||||||
|
|||||||
@@ -5,11 +5,17 @@ import { Session } from "../session"
|
|||||||
import { SessionID, MessageID } from "../session/schema"
|
import { SessionID, MessageID } from "../session/schema"
|
||||||
import { MessageV2 } from "../session/message-v2"
|
import { MessageV2 } from "../session/message-v2"
|
||||||
import { Agent } from "../agent/agent"
|
import { Agent } from "../agent/agent"
|
||||||
import { SessionPrompt } from "../session/prompt"
|
import type { SessionPrompt } from "../session/prompt"
|
||||||
import { Config } from "../config/config"
|
import { Config } from "../config/config"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Log } from "@/util/log"
|
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 id = "task"
|
||||||
|
|
||||||
const parameters = z.object({
|
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()
|
const messageID = MessageID.ascending()
|
||||||
|
|
||||||
function cancel() {
|
function cancel() {
|
||||||
SessionPrompt.cancel(nextSession.id)
|
ops.cancel(nextSession.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
return yield* Effect.acquireUseRelease(
|
return yield* Effect.acquireUseRelease(
|
||||||
@@ -125,9 +134,9 @@ export const TaskTool = Tool.defineEffect(
|
|||||||
}),
|
}),
|
||||||
() =>
|
() =>
|
||||||
Effect.gen(function* () {
|
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(() =>
|
const result = yield* Effect.promise(() =>
|
||||||
SessionPrompt.prompt({
|
ops.prompt({
|
||||||
messageID,
|
messageID,
|
||||||
sessionID: nextSession.id,
|
sessionID: nextSession.id,
|
||||||
model: {
|
model: {
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
|||||||
import { Instance } from "../../src/project/instance"
|
import { Instance } from "../../src/project/instance"
|
||||||
import { Session } from "../../src/session"
|
import { Session } from "../../src/session"
|
||||||
import { MessageV2 } from "../../src/session/message-v2"
|
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 { MessageID, PartID } from "../../src/session/schema"
|
||||||
import { ModelID, ProviderID } from "../../src/provider/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 { ToolRegistry } from "../../src/tool/registry"
|
||||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
@@ -62,6 +62,17 @@ const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") {
|
|||||||
return { chat, assistant }
|
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 {
|
function reply(input: Parameters<typeof SessionPrompt.prompt>[0], text: string): MessageV2.WithParts {
|
||||||
const id = MessageID.ascending()
|
const id = MessageID.ascending()
|
||||||
return {
|
return {
|
||||||
@@ -180,21 +191,8 @@ describe("tool.task", () => {
|
|||||||
const child = yield* sessions.create({ parentID: chat.id, title: "Existing child" })
|
const child = yield* sessions.create({ parentID: chat.id, title: "Existing child" })
|
||||||
const tool = yield* TaskTool
|
const tool = yield* TaskTool
|
||||||
const def = yield* Effect.promise(() => tool.init())
|
const def = yield* Effect.promise(() => tool.init())
|
||||||
const resolve = SessionPrompt.resolvePromptParts
|
let seen: SessionPrompt.PromptInput | undefined
|
||||||
const prompt = SessionPrompt.prompt
|
const promptOps = stubOps({ text: "resumed", onPrompt: (input) => (seen = input) })
|
||||||
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
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = yield* Effect.promise(() =>
|
const result = yield* Effect.promise(() =>
|
||||||
def.execute(
|
def.execute(
|
||||||
@@ -209,6 +207,7 @@ describe("tool.task", () => {
|
|||||||
messageID: assistant.id,
|
messageID: assistant.id,
|
||||||
agent: "build",
|
agent: "build",
|
||||||
abort: new AbortController().signal,
|
abort: new AbortController().signal,
|
||||||
|
extra: { promptOps },
|
||||||
messages: [],
|
messages: [],
|
||||||
metadata() {},
|
metadata() {},
|
||||||
ask: async () => {},
|
ask: async () => {},
|
||||||
@@ -232,20 +231,10 @@ describe("tool.task", () => {
|
|||||||
const { chat, assistant } = yield* seed()
|
const { chat, assistant } = yield* seed()
|
||||||
const tool = yield* TaskTool
|
const tool = yield* TaskTool
|
||||||
const def = yield* Effect.promise(() => tool.init())
|
const def = yield* Effect.promise(() => tool.init())
|
||||||
const resolve = SessionPrompt.resolvePromptParts
|
|
||||||
const prompt = SessionPrompt.prompt
|
|
||||||
const calls: unknown[] = []
|
const calls: unknown[] = []
|
||||||
|
const promptOps = stubOps()
|
||||||
|
|
||||||
SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }]
|
const exec = (extra?: Record<string, any>) =>
|
||||||
SessionPrompt.prompt = async (input) => reply(input, "done")
|
|
||||||
yield* Effect.addFinalizer(() =>
|
|
||||||
Effect.sync(() => {
|
|
||||||
SessionPrompt.resolvePromptParts = resolve
|
|
||||||
SessionPrompt.prompt = prompt
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const exec = (extra?: { bypassAgentCheck?: boolean }) =>
|
|
||||||
Effect.promise(() =>
|
Effect.promise(() =>
|
||||||
def.execute(
|
def.execute(
|
||||||
{
|
{
|
||||||
@@ -258,7 +247,7 @@ describe("tool.task", () => {
|
|||||||
messageID: assistant.id,
|
messageID: assistant.id,
|
||||||
agent: "build",
|
agent: "build",
|
||||||
abort: new AbortController().signal,
|
abort: new AbortController().signal,
|
||||||
extra,
|
extra: { promptOps, ...extra },
|
||||||
messages: [],
|
messages: [],
|
||||||
metadata() {},
|
metadata() {},
|
||||||
ask: async (input) => {
|
ask: async (input) => {
|
||||||
@@ -292,21 +281,8 @@ describe("tool.task", () => {
|
|||||||
const { chat, assistant } = yield* seed()
|
const { chat, assistant } = yield* seed()
|
||||||
const tool = yield* TaskTool
|
const tool = yield* TaskTool
|
||||||
const def = yield* Effect.promise(() => tool.init())
|
const def = yield* Effect.promise(() => tool.init())
|
||||||
const resolve = SessionPrompt.resolvePromptParts
|
let seen: SessionPrompt.PromptInput | undefined
|
||||||
const prompt = SessionPrompt.prompt
|
const promptOps = stubOps({ text: "created", onPrompt: (input) => (seen = input) })
|
||||||
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
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = yield* Effect.promise(() =>
|
const result = yield* Effect.promise(() =>
|
||||||
def.execute(
|
def.execute(
|
||||||
@@ -321,6 +297,7 @@ describe("tool.task", () => {
|
|||||||
messageID: assistant.id,
|
messageID: assistant.id,
|
||||||
agent: "build",
|
agent: "build",
|
||||||
abort: new AbortController().signal,
|
abort: new AbortController().signal,
|
||||||
|
extra: { promptOps },
|
||||||
messages: [],
|
messages: [],
|
||||||
metadata() {},
|
metadata() {},
|
||||||
ask: async () => {},
|
ask: async () => {},
|
||||||
@@ -346,21 +323,8 @@ describe("tool.task", () => {
|
|||||||
const { chat, assistant } = yield* seed()
|
const { chat, assistant } = yield* seed()
|
||||||
const tool = yield* TaskTool
|
const tool = yield* TaskTool
|
||||||
const def = yield* Effect.promise(() => tool.init())
|
const def = yield* Effect.promise(() => tool.init())
|
||||||
const resolve = SessionPrompt.resolvePromptParts
|
let seen: SessionPrompt.PromptInput | undefined
|
||||||
const prompt = SessionPrompt.prompt
|
const promptOps = stubOps({ onPrompt: (input) => (seen = input) })
|
||||||
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
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = yield* Effect.promise(() =>
|
const result = yield* Effect.promise(() =>
|
||||||
def.execute(
|
def.execute(
|
||||||
@@ -374,6 +338,7 @@ describe("tool.task", () => {
|
|||||||
messageID: assistant.id,
|
messageID: assistant.id,
|
||||||
agent: "build",
|
agent: "build",
|
||||||
abort: new AbortController().signal,
|
abort: new AbortController().signal,
|
||||||
|
extra: { promptOps },
|
||||||
messages: [],
|
messages: [],
|
||||||
metadata() {},
|
metadata() {},
|
||||||
ask: async () => {},
|
ask: async () => {},
|
||||||
|
|||||||
Reference in New Issue
Block a user