fix(plugin): reload late SDK plugins (#35576)

This commit is contained in:
Kit Langton 2026-07-06 13:36:19 -04:00 committed by GitHub
commit 2830176972
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1187 additions and 922 deletions

View file

@ -61,6 +61,12 @@ const GlobalMessage = EventV2.ephemeral({
text: Schema.String,
},
})
const CountMessage = EventV2.ephemeral({
type: "test.count",
schema: {
count: Schema.Number,
},
})
const VersionedMessage = EventV2.durable({
type: "test.versioned",
@ -90,6 +96,26 @@ const it = testEffect(
const itWithoutLocation = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, EventV2.node])))
describe("EventV2", () => {
it.effect("subscribes to multiple event definitions with a discriminated payload union", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service
// @ts-expect-error multi-definition subscriptions require at least one definition
events.subscribe([])
const fiber = yield* events
.subscribe([Message, CountMessage])
.pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow
yield* events.publish(Message, { text: "hello" })
yield* events.publish(CountMessage, { count: 2 })
const received = Array.from(yield* Fiber.join(fiber)).map((event) =>
event.type === "test.message" ? event.data.text : event.data.count,
)
expect(received).toEqual(["hello", 2])
}),
)
it.effect("publishes events with the current location", () =>
Effect.gen(function* () {
const events = yield* EventV2.Service

View file

@ -2,6 +2,7 @@ import { AgentV2 } from "@opencode-ai/core/agent"
import type { PermissionV2 } from "@opencode-ai/core/permission"
import { SessionMessage } from "@opencode-ai/core/session/message"
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
import { Tool } from "@opencode-ai/core/tool/tool"
import { Tools } from "@opencode-ai/core/tool/tools"
import type { PluginContext } from "@opencode-ai/plugin/v2/effect"
import { Effect, type Scope } from "effect"
@ -49,7 +50,24 @@ export const registerToolPlugin = <R>(plugin: {
const tools = yield* Tools.Service
const context: Pick<PluginContext, "tool"> = {
tool: {
register: tools.register,
transform: (callback) =>
Effect.gen(function* () {
const registrations: Array<{
readonly name: string
readonly tool: Tool.AnyTool
readonly options?: Tool.RegisterOptions
}> = []
callback({
add: (name, tool, options) => {
registrations.push({ name, tool, ...(options ? { options } : {}) })
},
})
yield* Effect.forEach(
registrations,
(registration) => tools.register({ [registration.name]: registration.tool }, registration.options),
{ discard: true },
)
}),
execute: {
before: () => Effect.die("registerToolPlugin does not support tool hooks"),
after: () => Effect.die("registerToolPlugin does not support tool hooks"),

View file

@ -165,14 +165,17 @@ describe("PluginV2", () => {
id: "tool-plugin",
effect: (ctx) =>
ctx.tool
.register({
plugin_tool: Tool.make({
description: "Plugin tool",
input: Schema.Struct({}),
output: Schema.Struct({ ok: Schema.Boolean }),
execute: () => Effect.succeed({ ok: true }),
}),
})
.transform((draft) =>
draft.add(
"plugin_tool",
Tool.make({
description: "Plugin tool",
input: Schema.Struct({}),
output: Schema.Struct({ ok: Schema.Boolean }),
execute: () => Effect.succeed({ ok: true }),
}),
),
)
.pipe(Effect.orDie),
})
@ -202,13 +205,13 @@ describe("PluginV2", () => {
const plugin = define({
id: "grouped-tools",
effect: (ctx) =>
Effect.gen(function* () {
yield* ctx.tool.register({ plain: tool("Plain") }).pipe(Effect.orDie)
yield* ctx.tool.register({ "look/up": tool("Lookup") }, { group: "context 7" }).pipe(Effect.orDie)
yield* ctx.tool
.register({ search: tool("Search") }, { group: "context 7", deferred: true })
.pipe(Effect.orDie)
}),
ctx.tool
.transform((draft) => {
draft.add("plain", tool("Plain"))
draft.add("look/up", tool("Lookup"), { group: "context 7" })
draft.add("search", tool("Search"), { group: "context 7", deferred: true })
})
.pipe(Effect.orDie),
})
yield* plugins.activate([{ plugin }])
@ -236,14 +239,17 @@ describe("PluginV2", () => {
effect: (ctx) =>
Effect.gen(function* () {
yield* ctx.tool
.register({
echo: Tool.make({
description: "Echo",
input: Schema.Struct({ text: Schema.String }),
output: Schema.Struct({ text: Schema.String }),
execute: ({ text }) => Effect.sync(() => executed.push({ text })).pipe(Effect.as({ text })),
}),
})
.transform((draft) =>
draft.add(
"echo",
Tool.make({
description: "Echo",
input: Schema.Struct({ text: Schema.String }),
output: Schema.Struct({ text: Schema.String }),
execute: ({ text }) => Effect.sync(() => executed.push({ text })).pipe(Effect.as({ text })),
}),
),
)
.pipe(Effect.orDie)
yield* ctx.tool.execute

View file

@ -71,7 +71,7 @@ export function host(overrides: Overrides = {}): PluginContext {
reload: () => Effect.die("unused skill.reload"),
},
tool: overrides.tool ?? {
register: () => Effect.die("unused tool.register"),
transform: () => Effect.die("unused tool.transform"),
execute: {
before: () => Effect.die("unused tool.execute.before"),
after: () => Effect.die("unused tool.execute.after"),