From 7832a677fb06908a21efe67c688cba8c9987fb79 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 15 Jul 2026 18:47:10 -0400 Subject: [PATCH] feat(plugin): support dynamic Effect tools --- packages/core/src/plugin/host.ts | 7 ++++++ packages/core/test/lib/tool.ts | 3 +++ packages/core/test/plugin.test.ts | 36 +++++++++++++++++++++++++++ packages/plugin/src/v2/effect/tool.ts | 4 ++- 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/core/src/plugin/host.ts b/packages/core/src/plugin/host.ts index 5831991df1..9e501beaa8 100644 --- a/packages/core/src/plugin/host.ts +++ b/packages/core/src/plugin/host.ts @@ -317,6 +317,13 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int add: (name, tool, options) => { registrations.push({ name, tool, ...(options ? { options } : {}) }) }, + addDynamic: (name, config, options) => { + registrations.push({ + name, + tool: Tool.make(config), + ...(options ? { options } : {}), + }) + }, }), ) yield* Effect.forEach( diff --git a/packages/core/test/lib/tool.ts b/packages/core/test/lib/tool.ts index e3fdccb13a..d2c4854004 100644 --- a/packages/core/test/lib/tool.ts +++ b/packages/core/test/lib/tool.ts @@ -59,6 +59,9 @@ export const registerToolPlugin = (plugin: { add: (name, tool, options) => { registrations.push({ name, tool, ...(options ? { options } : {}) }) }, + addDynamic: (name, config, options) => { + registrations.push({ name, tool: Tool.make(config), ...(options ? { options } : {}) }) + }, }) yield* Effect.forEach( registrations, diff --git a/packages/core/test/plugin.test.ts b/packages/core/test/plugin.test.ts index 9ff717d665..b14bef64ec 100644 --- a/packages/core/test/plugin.test.ts +++ b/packages/core/test/plugin.test.ts @@ -274,6 +274,42 @@ describe("PluginV2", () => { }), ) + it.effect("registers dynamic Effect tools through the host context", () => + Effect.gen(function* () { + const plugins = yield* PluginV2.Service + const registry = yield* ToolRegistry.Service + const plugin = EffectPlugin.define({ + id: "dynamic-tool-plugin", + effect: (ctx) => + ctx.tool + .transform((draft) => + draft.addDynamic( + "dynamic_tool", + { + description: "Dynamic plugin tool", + jsonSchema: { + type: "object", + properties: { value: { type: "string" } }, + required: ["value"], + additionalProperties: false, + }, + execute: (input) => + Effect.succeed({ + structured: input, + content: [{ type: "text", text: "dynamic output" }], + }), + }, + { codemode: false }, + ), + ) + .pipe(Effect.orDie), + }) + + yield* plugins.activate([versioned(plugin)]) + expect((yield* registry.materialize()).definitions.map((tool) => tool.name)).toContain("dynamic_tool") + }), + ) + it.effect("groups tool names and routes codemode registrations through execute", () => Effect.gen(function* () { const plugins = yield* PluginV2.Service diff --git a/packages/plugin/src/v2/effect/tool.ts b/packages/plugin/src/v2/effect/tool.ts index 8668570f8f..9e825a767d 100644 --- a/packages/plugin/src/v2/effect/tool.ts +++ b/packages/plugin/src/v2/effect/tool.ts @@ -103,7 +103,7 @@ export type DynamicOutput = { * time (MCP servers, plugin manifests). Input is passed through as `unknown`; * `execute` returns the already-projected structured value and model content. */ -type DynamicConfig = { +export type DynamicConfig = { readonly description: string readonly jsonSchema: JsonSchema.JsonSchema readonly outputSchema?: JsonSchema.JsonSchema @@ -284,6 +284,8 @@ export interface RegisterOptions { export interface ToolDraft { add(name: string, tool: AnyTool, options?: RegisterOptions): void + /** Registers a dynamic Effect tool without importing the host's Tool module. */ + addDynamic(name: string, config: DynamicConfig, options?: RegisterOptions): void } export interface ToolHooks {