refactor(plugin): simplify promise tool declarations
This commit is contained in:
parent
d54038b9d2
commit
efeff63749
7 changed files with 91 additions and 60 deletions
|
|
@ -1,7 +1,9 @@
|
||||||
export * as PluginPromise from "./promise"
|
export * as PluginPromise from "./promise"
|
||||||
|
|
||||||
import { Plugin } from "@opencode-ai/plugin/v2/effect"
|
import { Plugin } from "@opencode-ai/plugin/v2/effect"
|
||||||
|
import type { AnyTool, RegisterOptions } from "@opencode-ai/plugin/v2/tool"
|
||||||
import { Effect, Scope, Stream } from "effect"
|
import { Effect, Scope, Stream } from "effect"
|
||||||
|
import { Tool } from "../tool/tool"
|
||||||
|
|
||||||
type HostRegistration = { readonly dispose: Effect.Effect<void> }
|
type HostRegistration = { readonly dispose: Effect.Effect<void> }
|
||||||
type Registration = { readonly dispose: () => Promise<void> }
|
type Registration = { readonly dispose: () => Promise<void> }
|
||||||
|
|
@ -108,7 +110,15 @@ export function fromPromise(plugin: PromisePlugin) {
|
||||||
reload: () => run(host.skill.reload()),
|
reload: () => run(host.skill.reload()),
|
||||||
},
|
},
|
||||||
tool: {
|
tool: {
|
||||||
transform: transform(host.tool),
|
transform: (callback) =>
|
||||||
|
register(
|
||||||
|
host.tool.transform((draft) =>
|
||||||
|
callback({
|
||||||
|
add: (name: string, tool: AnyTool, options?: RegisterOptions) =>
|
||||||
|
draft.add(name, fromPromiseTool(tool), options),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
hook: (name, callback) =>
|
hook: (name, callback) =>
|
||||||
register(host.tool.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
register(host.tool.hook(name, (event) => Effect.promise(() => Promise.resolve(callback(event))))),
|
||||||
},
|
},
|
||||||
|
|
@ -125,3 +135,15 @@ export function fromPromise(plugin: PromisePlugin) {
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fromPromiseTool(tool: AnyTool) {
|
||||||
|
if ("jsonSchema" in tool)
|
||||||
|
return Tool.make({
|
||||||
|
...tool,
|
||||||
|
execute: (input, context) => Effect.promise(() => tool.execute(input, context)),
|
||||||
|
})
|
||||||
|
return Tool.make({
|
||||||
|
...tool,
|
||||||
|
execute: (input, context) => Effect.promise(() => tool.execute(input, context)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import { describe, expect } from "bun:test"
|
import { describe, expect } from "bun:test"
|
||||||
import { Effect } from "effect"
|
import { Effect, Schema } from "effect"
|
||||||
import { AgentV2 } from "@opencode-ai/core/agent"
|
import { AgentV2 } from "@opencode-ai/core/agent"
|
||||||
import { PluginV2 } from "@opencode-ai/core/plugin"
|
import { PluginV2 } from "@opencode-ai/core/plugin"
|
||||||
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
import { PluginHost } from "@opencode-ai/core/plugin/host"
|
||||||
import { PluginPromise } from "@opencode-ai/core/plugin/promise"
|
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 { Plugin } from "@opencode-ai/plugin/v2"
|
||||||
import { testEffect } from "../lib/effect"
|
import { testEffect } from "../lib/effect"
|
||||||
import { PluginTestLayer } from "./fixture"
|
import { PluginTestLayer } from "./fixture"
|
||||||
|
|
@ -93,4 +96,38 @@ describe("fromPromise", () => {
|
||||||
expect(yield* agents.get(AgentV2.ID.make("temp"))).toBeUndefined()
|
expect(yield* agents.get(AgentV2.ID.make("temp"))).toBeUndefined()
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
it.effect("constructs plain Promise tool declarations in the host", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
const plugins = yield* PluginV2.Service
|
||||||
|
const registry = yield* ToolRegistry.Service
|
||||||
|
const host = yield* PluginHost.make(plugins)
|
||||||
|
const promisePlugin = Plugin.define({
|
||||||
|
id: "promise-tool",
|
||||||
|
setup: async (ctx) => {
|
||||||
|
await ctx.tool.transform((tools) => {
|
||||||
|
tools.add("hello", {
|
||||||
|
description: "Hello",
|
||||||
|
input: Schema.Struct({ name: Schema.String }),
|
||||||
|
output: Schema.String,
|
||||||
|
execute: async ({ name }) => `Hello, ${name}!`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
yield* PluginPromise.fromPromise(promisePlugin).effect(host)
|
||||||
|
|
||||||
|
const materialized = yield* registry.materialize()
|
||||||
|
expect(materialized.definitions).toContainEqual(expect.objectContaining({ name: "hello", description: "Hello" }))
|
||||||
|
expect(
|
||||||
|
yield* materialized.settle({
|
||||||
|
sessionID: SessionV2.ID.make("ses_promise_tool"),
|
||||||
|
agent: AgentV2.ID.make("build"),
|
||||||
|
assistantMessageID: SessionMessage.ID.make("msg_promise_tool"),
|
||||||
|
call: { type: "tool-call", id: "call_promise_tool", name: "hello", input: { name: "world" } },
|
||||||
|
}),
|
||||||
|
).toMatchObject({ result: { type: "text", value: "Hello, world!" } })
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -271,25 +271,23 @@ handle expected errors inside the callback.
|
||||||
|
|
||||||
## Add a tool
|
## Add a tool
|
||||||
|
|
||||||
Use `Tool.make` with Effect schemas. Promise tools use async executors:
|
Pass a plain object with Effect schemas to `tools.add`. Promise tools use async
|
||||||
|
executors:
|
||||||
|
|
||||||
```ts title=".opencode/plugins/greeting.ts"
|
```ts title=".opencode/plugins/greeting.ts"
|
||||||
import { Plugin } from "@opencode-ai/plugin/v2"
|
import { Plugin } from "@opencode-ai/plugin/v2"
|
||||||
import { Tool } from "@opencode-ai/plugin/v2/tool"
|
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
|
|
||||||
const greeting = Tool.make({
|
|
||||||
description: "Create a greeting",
|
|
||||||
input: Schema.Struct({ name: Schema.String }),
|
|
||||||
output: Schema.String,
|
|
||||||
execute: async ({ name }) => `Hello, ${name}!`,
|
|
||||||
})
|
|
||||||
|
|
||||||
export default Plugin.define({
|
export default Plugin.define({
|
||||||
id: "acme.greeting",
|
id: "acme.greeting",
|
||||||
setup: async (ctx) => {
|
setup: async (ctx) => {
|
||||||
await ctx.tool.transform((tools) => {
|
await ctx.tool.transform((tools) => {
|
||||||
tools.add("greeting", greeting)
|
tools.add("greeting", {
|
||||||
|
description: "Create a greeting",
|
||||||
|
input: Schema.Struct({ name: Schema.String }),
|
||||||
|
output: Schema.String,
|
||||||
|
execute: async ({ name }) => `Hello, ${name}!`,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -305,10 +303,9 @@ letters, digits, underscores, or hyphens. `tools.add` also accepts
|
||||||
tool instead of exposing it directly.
|
tool instead of exposing it directly.
|
||||||
|
|
||||||
The executor receives a second context argument containing `sessionID`,
|
The executor receives a second context argument containing `sessionID`,
|
||||||
`agent`, `assistantMessageID`, and `toolCallID`. Use
|
`agent`, `assistantMessageID`, and `toolCallID`. Effect plugins import their
|
||||||
`Tool.withPermission(tool, "permission-name")` to assign a permission key.
|
tool contracts from `@opencode-ai/plugin/v2/effect/tool` and return an `Effect`
|
||||||
Effect plugins import the helper from `@opencode-ai/plugin/v2/effect/tool` and
|
from `execute`.
|
||||||
return an `Effect` from `execute`.
|
|
||||||
|
|
||||||
## Types
|
## Types
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,20 +85,19 @@ await ctx.session.hook("request", (event) => {
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
Promise tools use the same schemas and registration model as Effect tools, with async executors:
|
Promise tools use plain object declarations with async executors:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { Schema } from "effect"
|
import { Schema } from "effect"
|
||||||
import { Tool } from "@opencode-ai/plugin/v2/tool"
|
|
||||||
|
|
||||||
const echo = Tool.make({
|
await ctx.tool.transform((tools) => {
|
||||||
description: "Echo text",
|
tools.add("echo", {
|
||||||
input: Schema.Struct({ text: Schema.String }),
|
description: "Echo text",
|
||||||
output: Schema.Struct({ text: Schema.String }),
|
input: Schema.Struct({ text: Schema.String }),
|
||||||
execute: async ({ text }) => ({ text }),
|
output: Schema.Struct({ text: Schema.String }),
|
||||||
|
execute: async ({ text }) => ({ text }),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
await ctx.tool.transform((tools) => tools.add("echo", echo))
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Reloading A Domain
|
## Reloading A Domain
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
export type { PluginOptions } from "../options.js"
|
export type { PluginOptions } from "../options.js"
|
||||||
export * as Plugin from "./plugin.js"
|
export * as Plugin from "./plugin.js"
|
||||||
|
|
||||||
export { Tool } from "./tool.js"
|
|
||||||
|
|
||||||
export { Agent } from "@opencode-ai/schema/agent"
|
export { Agent } from "@opencode-ai/schema/agent"
|
||||||
export { Command } from "@opencode-ai/schema/command"
|
export { Command } from "@opencode-ai/schema/command"
|
||||||
export { Connection } from "@opencode-ai/schema/connection"
|
export { Connection } from "@opencode-ai/schema/connection"
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,16 @@
|
||||||
export * as Tool from "./tool.js"
|
import type { Tool } from "../effect/tool.js"
|
||||||
|
|
||||||
import { Tool } from "../effect/tool.js"
|
|
||||||
import type { Agent } from "@opencode-ai/schema/agent"
|
import type { Agent } from "@opencode-ai/schema/agent"
|
||||||
import type { Session } from "@opencode-ai/schema/session"
|
import type { Session } from "@opencode-ai/schema/session"
|
||||||
import type { SessionMessage } from "@opencode-ai/schema/session-message"
|
import type { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||||
import { Effect, type JsonSchema, type Schema } from "effect"
|
import type { JsonSchema, Schema } from "effect"
|
||||||
import type { Hooks, Transform } from "./registration.js"
|
import type { Hooks, Transform } from "./registration.js"
|
||||||
|
|
||||||
export type Context = Tool.Context
|
export type Context = Tool.Context
|
||||||
export type SchemaType<A> = Tool.SchemaType<A>
|
export type SchemaType<A> = Tool.SchemaType<A>
|
||||||
export type Definition<Input extends SchemaType<any>, Output extends SchemaType<any>> = Tool.Definition<Input, Output>
|
|
||||||
export type AnyTool = Tool.AnyTool
|
|
||||||
export const Failure = Tool.Failure
|
|
||||||
export type Failure = Tool.Failure
|
|
||||||
export const RegistrationError = Tool.RegistrationError
|
|
||||||
export type RegistrationError = Tool.RegistrationError
|
|
||||||
export type Content = Tool.Content
|
export type Content = Tool.Content
|
||||||
export type DynamicOutput = Tool.DynamicOutput
|
export type DynamicOutput = Tool.DynamicOutput
|
||||||
|
|
||||||
type Config<
|
export type Definition<
|
||||||
Input extends SchemaType<any>,
|
Input extends SchemaType<any>,
|
||||||
Output extends SchemaType<any>,
|
Output extends SchemaType<any>,
|
||||||
Structured extends SchemaType<any> = Output,
|
Structured extends SchemaType<any> = Output,
|
||||||
|
|
@ -41,32 +33,14 @@ type Config<
|
||||||
}) => ReadonlyArray<Content>
|
}) => ReadonlyArray<Content>
|
||||||
}
|
}
|
||||||
|
|
||||||
type DynamicConfig = {
|
export type DynamicDefinition = {
|
||||||
readonly description: string
|
readonly description: string
|
||||||
readonly jsonSchema: JsonSchema.JsonSchema
|
readonly jsonSchema: JsonSchema.JsonSchema
|
||||||
readonly outputSchema?: JsonSchema.JsonSchema
|
readonly outputSchema?: JsonSchema.JsonSchema
|
||||||
readonly execute: (input: unknown, context: Context) => Promise<DynamicOutput>
|
readonly execute: (input: unknown, context: Context) => Promise<DynamicOutput>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function make<
|
export type AnyTool = Definition<any, any, any> | DynamicDefinition
|
||||||
Input extends SchemaType<any>,
|
|
||||||
Output extends SchemaType<any>,
|
|
||||||
Structured extends SchemaType<any> = Output,
|
|
||||||
>(config: Config<Input, Output, Structured>): Definition<Input, Structured>
|
|
||||||
export function make(config: DynamicConfig): AnyTool
|
|
||||||
export function make(config: Config<any, any, any> | DynamicConfig): AnyTool {
|
|
||||||
if ("jsonSchema" in config)
|
|
||||||
return Tool.make({
|
|
||||||
...config,
|
|
||||||
execute: (input, context) => Effect.promise(() => config.execute(input, context)),
|
|
||||||
})
|
|
||||||
return Tool.make({
|
|
||||||
...config,
|
|
||||||
execute: (input, context) => Effect.promise(() => config.execute(input, context)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const withPermission = Tool.withPermission
|
|
||||||
|
|
||||||
export interface ToolExecuteBeforeEvent {
|
export interface ToolExecuteBeforeEvent {
|
||||||
readonly tool: string
|
readonly tool: string
|
||||||
|
|
@ -95,7 +69,12 @@ export interface RegisterOptions {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolDraft {
|
export interface ToolDraft {
|
||||||
add(name: string, tool: AnyTool, options?: RegisterOptions): void
|
add<
|
||||||
|
Input extends SchemaType<any>,
|
||||||
|
Output extends SchemaType<any>,
|
||||||
|
Structured extends SchemaType<any> = Output,
|
||||||
|
>(name: string, tool: Definition<Input, Output, Structured>, options?: RegisterOptions): void
|
||||||
|
add(name: string, tool: DynamicDefinition, options?: RegisterOptions): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolHooks {
|
export interface ToolHooks {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,5 @@ test.each([
|
||||||
"Provider",
|
"Provider",
|
||||||
"Reference",
|
"Reference",
|
||||||
"Skill",
|
"Skill",
|
||||||
...(name === "promise" ? ["Tool"] : []),
|
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue