Compare commits

...

1 Commits

Author SHA1 Message Date
Kit Langton 7832a677fb feat(plugin): support dynamic Effect tools 2026-07-15 18:47:10 -04:00
4 changed files with 49 additions and 1 deletions
+7
View File
@@ -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(
+3
View File
@@ -59,6 +59,9 @@ export const registerToolPlugin = <R>(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,
+36
View File
@@ -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
+3 -1
View File
@@ -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 {