feat(plugin): restore ai request hook
This commit is contained in:
parent
b95a5dc259
commit
f2f5eb6f16
19 changed files with 261 additions and 10 deletions
|
|
@ -44,6 +44,9 @@ export const registerToolPlugin = <R>(plugin: {
|
|||
Effect.gen(function* () {
|
||||
const tools = yield* Tools.Service
|
||||
const context = host({
|
||||
ai: {
|
||||
hook: () => Effect.succeed({ dispose: Effect.void }),
|
||||
},
|
||||
tool: {
|
||||
transform: (callback) =>
|
||||
Effect.gen(function* () {
|
||||
|
|
|
|||
45
packages/core/test/plugin-hooks.test.ts
Normal file
45
packages/core/test/plugin-hooks.test.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Message, SystemPart } from "@opencode-ai/ai"
|
||||
import { Agent } from "@opencode-ai/schema/agent"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import { Session } from "@opencode-ai/schema/session"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { PluginHooks } from "../src/plugin/hooks"
|
||||
import { testEffect } from "./lib/effect"
|
||||
|
||||
const layer = PluginHooks.node.implementation as Layer.Layer<PluginHooks.Service>
|
||||
const it = testEffect(layer)
|
||||
|
||||
describe("PluginHooks", () => {
|
||||
it.effect("registers scoped AI hooks and triggers them sequentially", () =>
|
||||
Effect.gen(function* () {
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const seen: string[] = []
|
||||
yield* hooks.register("ai", "request", (event) =>
|
||||
Effect.sync(() => {
|
||||
seen.push("first")
|
||||
event.system.push(SystemPart.make("second"))
|
||||
}),
|
||||
)
|
||||
yield* hooks.register("ai", "request", (event) =>
|
||||
Effect.sync(() => {
|
||||
seen.push(event.system[1]?.text ?? "missing")
|
||||
event.messages = [Message.user("changed")]
|
||||
}),
|
||||
)
|
||||
const event = {
|
||||
sessionID: Session.ID.make("ses_hooks"),
|
||||
agent: Agent.ID.make("build"),
|
||||
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
|
||||
system: [SystemPart.make("first")],
|
||||
messages: [Message.user("original")],
|
||||
tools: {},
|
||||
}
|
||||
|
||||
expect(yield* hooks.trigger("ai", "request", event)).toBe(event)
|
||||
expect(seen).toEqual(["first", "second"])
|
||||
expect(event.messages).toEqual([Message.user("changed")])
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
@ -18,6 +18,9 @@ export function host(overrides: Overrides = {}): PluginContext {
|
|||
transform: () => Effect.die("unused agent.transform"),
|
||||
reload: () => Effect.die("unused agent.reload"),
|
||||
},
|
||||
ai: overrides.ai ?? {
|
||||
hook: () => Effect.die("unused ai.hook"),
|
||||
},
|
||||
aisdk: overrides.aisdk ?? {
|
||||
hook: () => Effect.die("unused aisdk.hook"),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Message, SystemPart } from "@opencode-ai/ai"
|
||||
import { Effect, Schema } from "effect"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { PluginPromise } from "@opencode-ai/core/plugin/promise"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||
import { Plugin } from "@opencode-ai/plugin/v2"
|
||||
import type { AIHooks } from "@opencode-ai/plugin/v2/effect/ai"
|
||||
import { Model } from "@opencode-ai/schema/model"
|
||||
import { Provider } from "@opencode-ai/schema/provider"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { PluginTestLayer } from "./fixture"
|
||||
|
||||
|
|
@ -72,6 +77,38 @@ describe("fromPromise", () => {
|
|||
}),
|
||||
)
|
||||
|
||||
it.effect("forwards AI request hooks", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugin = yield* PluginV2.Service
|
||||
const hooks = yield* PluginHooks.Service
|
||||
const host = yield* PluginHost.make(plugin)
|
||||
yield* PluginPromise.fromPromise(
|
||||
Plugin.define({
|
||||
id: "promise-ai-request",
|
||||
setup: async (ctx) => {
|
||||
await ctx.ai.hook("request", (event) => {
|
||||
event.system.push(SystemPart.make("Promise hook"))
|
||||
delete event.tools.echo
|
||||
})
|
||||
},
|
||||
}),
|
||||
).effect(host)
|
||||
const event: AIHooks["request"] = {
|
||||
sessionID: SessionV2.ID.make("ses_promise_ai_request"),
|
||||
agent: AgentV2.ID.make("build"),
|
||||
model: Model.Ref.make({ providerID: Provider.ID.make("test"), id: Model.ID.make("model") }),
|
||||
system: [SystemPart.make("Initial")],
|
||||
messages: [Message.user("Hello")],
|
||||
tools: { echo: { description: "Echo", input: { type: "object" } } },
|
||||
}
|
||||
|
||||
yield* hooks.trigger("ai", "request", event)
|
||||
|
||||
expect(event.system.map((part) => part.text)).toEqual(["Initial", "Promise hook"])
|
||||
expect(event.tools).toEqual({})
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("disposes a hook registration on request", () =>
|
||||
Effect.gen(function* () {
|
||||
const agents = yield* AgentV2.Service
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ import {
|
|||
LLMClient,
|
||||
LLMError,
|
||||
LLMEvent,
|
||||
Message,
|
||||
Model,
|
||||
SystemPart,
|
||||
ToolFailure,
|
||||
TransportReason,
|
||||
InvalidProviderOutputReason,
|
||||
|
|
@ -42,6 +44,7 @@ import { SessionRunnerModel } from "@opencode-ai/core/session/runner/model"
|
|||
import { SessionRunnerSystemPrompt } from "@opencode-ai/core/session/runner/system-prompt"
|
||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||
import { PluginSupervisor } from "@opencode-ai/core/plugin/supervisor"
|
||||
import { PluginHooks } from "@opencode-ai/core/plugin/hooks"
|
||||
import { QuestionTool } from "@opencode-ai/core/tool/question"
|
||||
import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
|
|
@ -397,6 +400,7 @@ const it = testEffect(
|
|||
AgentV2.node,
|
||||
ToolRegistry.node,
|
||||
ToolRegistry.toolsNode,
|
||||
PluginHooks.node,
|
||||
echoNode,
|
||||
SessionRunnerModel.node,
|
||||
InstructionBuiltIns.node,
|
||||
|
|
@ -773,6 +777,32 @@ const verifyPartialFlushOnInterruption = (kind: FragmentKind) =>
|
|||
})
|
||||
|
||||
describe("SessionRunnerLLM", () => {
|
||||
it.effect("applies AI request hooks without exposing unavailable tools", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setup
|
||||
const hooks = yield* PluginHooks.Service
|
||||
yield* hooks.register("ai", "request", (event) =>
|
||||
Effect.sync(() => {
|
||||
event.system = [SystemPart.make("Hooked system")]
|
||||
event.messages = [Message.user("Hooked message")]
|
||||
delete event.tools.echo
|
||||
event.tools.unregistered = { description: "Unavailable", input: { type: "object" } }
|
||||
}),
|
||||
)
|
||||
yield* admit(session, "Original message")
|
||||
responses = [reply.tool("call-removed", "echo", { text: "blocked" })]
|
||||
|
||||
yield* session.resume(sessionID)
|
||||
|
||||
expect(requests).toHaveLength(1)
|
||||
expect(requests[0]?.system.map((part) => part.text)).toEqual(["Hooked system"])
|
||||
expect(requests[0]?.messages).toEqual([Message.user("Hooked message")])
|
||||
expect(requests[0]?.tools.map((tool) => tool.name)).not.toContain("echo")
|
||||
expect(requests[0]?.tools.map((tool) => tool.name)).not.toContain("unregistered")
|
||||
expect(executions).toEqual([])
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("advertises and executes a location registered tool", () =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* setup
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue