refactor(core): consolidate tool architecture
This commit is contained in:
parent
0fd73a2976
commit
8db7487c89
466 changed files with 9405 additions and 11071 deletions
|
|
@ -1,16 +1,14 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Context, Effect, Exit, Fiber, Schema, Stream } from "effect"
|
||||
import { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect"
|
||||
import { Plugin as EffectPlugin } from "@opencode-ai/plugin/effect"
|
||||
import { Config as ConfigSchema } from "@opencode-ai/schema/config"
|
||||
import { Plugin } from "@opencode-ai/schema/plugin"
|
||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||
import { EventV2 } from "@opencode-ai/core/event"
|
||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||
import { Agent } from "@opencode-ai/core/agent"
|
||||
import { Bus } from "@opencode-ai/core/bus"
|
||||
import { Plugin } from "@opencode-ai/core/plugin"
|
||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||
import { SessionV2 } from "@opencode-ai/core/session"
|
||||
import { Session } from "@opencode-ai/core/session"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { Tool } from "@opencode-ai/core/tool/tool"
|
||||
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
|
||||
import { Tool } from "@opencode-ai/core/tool"
|
||||
import { testEffect } from "./lib/effect"
|
||||
import { PluginTestLayer } from "./plugin/fixture"
|
||||
|
||||
|
|
@ -20,11 +18,11 @@ class Secret extends Context.Service<Secret, string>()("@opencode/test/PluginSec
|
|||
|
||||
const versioned = <R>(plugin: EffectPlugin.Plugin<R>, version = "1") => ({ ...plugin, version })
|
||||
|
||||
describe("PluginV2", () => {
|
||||
describe("Plugin", () => {
|
||||
it.live("exposes public events through the plugin context", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const plugins = yield* Plugin.Service
|
||||
const bus = yield* Bus.Service
|
||||
const host = yield* PluginHost.make(plugins)
|
||||
const received = yield* host.event.subscribe().pipe(
|
||||
Stream.filter((event) => event.type === "config.updated"),
|
||||
|
|
@ -33,7 +31,7 @@ describe("PluginV2", () => {
|
|||
)
|
||||
yield* Effect.sleep("10 millis")
|
||||
|
||||
yield* events.publish(ConfigSchema.Event.Updated, {})
|
||||
yield* bus.publish(ConfigSchema.Event.Updated, {})
|
||||
|
||||
expect((yield* Fiber.join(received)).valueOrUndefined?.type).toBe("config.updated")
|
||||
}),
|
||||
|
|
@ -41,12 +39,12 @@ describe("PluginV2", () => {
|
|||
|
||||
it.effect("replaces plugins by ID and version", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const events = yield* EventV2.Service
|
||||
const plugins = yield* Plugin.Service
|
||||
const agents = yield* Agent.Service
|
||||
const bus = yield* Bus.Service
|
||||
let description = "first"
|
||||
let updates = 0
|
||||
const unsubscribe = yield* events.listen((event) =>
|
||||
const unsubscribe = yield* bus.listen((event) =>
|
||||
Effect.sync(() => {
|
||||
if (event.type === Plugin.Event.Updated.type) updates++
|
||||
}),
|
||||
|
|
@ -67,19 +65,19 @@ describe("PluginV2", () => {
|
|||
|
||||
yield* plugins.activate([versioned(managed(), "1")])
|
||||
|
||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("first")
|
||||
expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("first")
|
||||
|
||||
description = "second"
|
||||
yield* plugins.activate([versioned(managed(), "2")])
|
||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second")
|
||||
expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("second")
|
||||
|
||||
description = "third"
|
||||
yield* plugins.activate([versioned(managed(), "2")])
|
||||
expect(updates).toBe(2)
|
||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("second")
|
||||
expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("second")
|
||||
|
||||
yield* plugins.activate([])
|
||||
expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined()
|
||||
expect(yield* agents.get(Agent.ID.make("configured"))).toBeUndefined()
|
||||
expect(updates).toBe(3)
|
||||
yield* unsubscribe
|
||||
}),
|
||||
|
|
@ -87,7 +85,7 @@ describe("PluginV2", () => {
|
|||
|
||||
it.effect("rejects duplicate IDs before replacing active plugins", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const plugins = yield* Plugin.Service
|
||||
const active = Plugin.ID.make("active")
|
||||
const duplicate = "duplicate"
|
||||
yield* plugins.activate([{ id: active, version: "1", effect: () => Effect.void }])
|
||||
|
|
@ -106,8 +104,8 @@ describe("PluginV2", () => {
|
|||
|
||||
it.effect("skips failed plugins and loads the rest", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const plugins = yield* Plugin.Service
|
||||
const agents = yield* Agent.Service
|
||||
let fail = true
|
||||
const good = EffectPlugin.define({
|
||||
id: "good",
|
||||
|
|
@ -130,7 +128,7 @@ describe("PluginV2", () => {
|
|||
|
||||
yield* plugins.activate([versioned(good), versioned(bad)])
|
||||
expect(yield* plugins.list()).toEqual([{ id: Plugin.ID.make("good") }])
|
||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("loaded")
|
||||
expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("loaded")
|
||||
|
||||
fail = false
|
||||
yield* plugins.activate([versioned(good), versioned(bad, "2")])
|
||||
|
|
@ -140,8 +138,8 @@ describe("PluginV2", () => {
|
|||
|
||||
it.effect("restores the previous plugin when its replacement fails", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const plugins = yield* Plugin.Service
|
||||
const agents = yield* Agent.Service
|
||||
const previous = EffectPlugin.define({
|
||||
id: "managed",
|
||||
effect: (ctx) =>
|
||||
|
|
@ -170,14 +168,14 @@ describe("PluginV2", () => {
|
|||
yield* plugins.activate([versioned(replacement, "2")])
|
||||
|
||||
expect(yield* plugins.list()).toEqual([{ id: Plugin.ID.make("managed") }])
|
||||
expect((yield* agents.get(AgentV2.ID.make("configured")))?.description).toBe("previous")
|
||||
expect((yield* agents.get(Agent.ID.make("configured")))?.description).toBe("previous")
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("deactivates a plugin when replacement and restoration fail", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const agents = yield* AgentV2.Service
|
||||
const plugins = yield* Plugin.Service
|
||||
const agents = yield* Agent.Service
|
||||
let loads = 0
|
||||
const previous = EffectPlugin.define({
|
||||
id: "managed",
|
||||
|
|
@ -202,13 +200,13 @@ describe("PluginV2", () => {
|
|||
yield* plugins.activate([versioned(replacement, "2")])
|
||||
|
||||
expect(yield* plugins.list()).toEqual([])
|
||||
expect(yield* agents.get(AgentV2.ID.make("configured"))).toBeUndefined()
|
||||
expect(yield* agents.get(Agent.ID.make("configured"))).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
|
||||
it.effect("closes the previous generation in reverse order", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const plugins = yield* Plugin.Service
|
||||
const closed: string[] = []
|
||||
yield* plugins.activate(
|
||||
["first", "second"].map((id) => ({
|
||||
|
|
@ -226,7 +224,7 @@ describe("PluginV2", () => {
|
|||
|
||||
it.effect("isolates plugins from ambient services", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const plugins = yield* Plugin.Service
|
||||
let visible = true
|
||||
const plugin = EffectPlugin.define({
|
||||
id: "isolated",
|
||||
|
|
@ -245,22 +243,22 @@ describe("PluginV2", () => {
|
|||
|
||||
it.effect("registers location tools through the plugin context", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const plugins = yield* Plugin.Service
|
||||
const registry = yield* Tool.Service
|
||||
const plugin = EffectPlugin.define({
|
||||
id: "tool-plugin",
|
||||
effect: (ctx) =>
|
||||
ctx.tool
|
||||
.transform((draft) =>
|
||||
draft.add(
|
||||
"plugin_tool",
|
||||
Tool.make({
|
||||
({
|
||||
name: "plugin_tool",
|
||||
options: { codemode: false },
|
||||
description: "Plugin tool",
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Struct({ ok: Schema.Boolean }),
|
||||
execute: () => Effect.succeed({ output: { ok: true } }),
|
||||
}),
|
||||
{ codemode: false },
|
||||
),
|
||||
)
|
||||
.pipe(Effect.orDie),
|
||||
|
|
@ -276,10 +274,12 @@ describe("PluginV2", () => {
|
|||
|
||||
it.effect("namespaces tool names and routes codemode registrations through execute", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const tool = (description: string) =>
|
||||
Tool.make({
|
||||
const plugins = yield* Plugin.Service
|
||||
const registry = yield* Tool.Service
|
||||
const tool = (name: string, description: string, options?: Tool.Options) =>
|
||||
({
|
||||
name,
|
||||
options,
|
||||
description,
|
||||
input: Schema.Struct({}),
|
||||
output: Schema.Struct({ ok: Schema.Boolean }),
|
||||
|
|
@ -290,9 +290,9 @@ describe("PluginV2", () => {
|
|||
effect: (ctx) =>
|
||||
ctx.tool
|
||||
.transform((draft) => {
|
||||
draft.add("plain", tool("Plain"), { codemode: false })
|
||||
draft.add("look/up", tool("Lookup"), { namespace: "context7", codemode: false })
|
||||
draft.add("search", tool("Search"), { namespace: "context7" })
|
||||
draft.add(tool("plain", "Plain", { codemode: false }))
|
||||
draft.add(tool("look/up", "Lookup", { namespace: "context7", codemode: false }))
|
||||
draft.add(tool("search", "Search", { namespace: "context7" }))
|
||||
})
|
||||
.pipe(Effect.orDie),
|
||||
})
|
||||
|
|
@ -309,8 +309,8 @@ describe("PluginV2", () => {
|
|||
|
||||
it.effect("fires before/after tool hooks with mutable events around execution", () =>
|
||||
Effect.gen(function* () {
|
||||
const plugins = yield* PluginV2.Service
|
||||
const registry = yield* ToolRegistry.Service
|
||||
const plugins = yield* Plugin.Service
|
||||
const registry = yield* Tool.Service
|
||||
const executed: unknown[] = []
|
||||
const seen: {
|
||||
before?: unknown
|
||||
|
|
@ -324,15 +324,15 @@ describe("PluginV2", () => {
|
|||
yield* ctx.tool
|
||||
.transform((draft) =>
|
||||
draft.add(
|
||||
"echo",
|
||||
Tool.make({
|
||||
({
|
||||
name: "echo",
|
||||
options: { codemode: false },
|
||||
description: "Echo",
|
||||
input: Schema.Struct({ text: Schema.String }),
|
||||
output: Schema.Struct({ text: Schema.String }),
|
||||
execute: ({ text }) =>
|
||||
Effect.sync(() => executed.push({ text })).pipe(Effect.as({ output: { text } })),
|
||||
}),
|
||||
{ codemode: false },
|
||||
),
|
||||
)
|
||||
.pipe(Effect.orDie)
|
||||
|
|
@ -352,12 +352,15 @@ describe("PluginV2", () => {
|
|||
seen.after = {
|
||||
input: event.input,
|
||||
status: event.status,
|
||||
content: event.content,
|
||||
metadata: event.metadata,
|
||||
content: event.status === "completed" ? event.result.content : undefined,
|
||||
metadata: event.status === "completed" ? event.result.metadata : event.error.metadata,
|
||||
}
|
||||
if (event.status !== "completed") return
|
||||
event.content = [{ type: "text", text: "after-mutated" }]
|
||||
event.metadata = { rewritten: true }
|
||||
event.result = {
|
||||
...event.result,
|
||||
content: [{ type: "text", text: "after-mutated" }],
|
||||
metadata: { rewritten: true },
|
||||
}
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.asVoid)
|
||||
|
|
@ -365,7 +368,7 @@ describe("PluginV2", () => {
|
|||
yield* ctx.tool
|
||||
.hook("execute.after", (event) =>
|
||||
Effect.sync(() => {
|
||||
if (event.status === "completed") (event.content as unknown as unknown[]).splice(0)
|
||||
if (event.status === "completed" && Array.isArray(event.result.content)) event.result.content.splice(0)
|
||||
}),
|
||||
)
|
||||
.pipe(Effect.asVoid)
|
||||
|
|
@ -376,8 +379,8 @@ describe("PluginV2", () => {
|
|||
|
||||
const toolSet = yield* registry.snapshot()
|
||||
const execution = yield* toolSet.execute({
|
||||
sessionID: SessionV2.ID.make("ses_hooks"),
|
||||
agent: AgentV2.ID.make("build"),
|
||||
sessionID: Session.ID.make("ses_hooks"),
|
||||
agent: Agent.ID.make("build"),
|
||||
messageID: SessionMessage.ID.make("msg_hooks"),
|
||||
call: { type: "tool-call", id: "call-hooks", name: "echo", input: { text: "original" } },
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue