refactor(plugin): simplify promise tool declarations · argszero/opencode@efeff63 · GitHub
Skip to content

Commit efeff63

Browse files
committed
refactor(plugin): simplify promise tool declarations
1 parent d54038b commit efeff63

7 files changed

Lines changed: 91 additions & 60 deletions

File tree

packages/core/src/plugin/promise.ts

Lines changed: 23 additions & 1 deletion

packages/core/test/plugin/promise.test.ts

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { describe, expect } from "bun:test"
2-
import { Effect } from "effect"
2+
import { Effect, Schema } from "effect"
33
import { AgentV2 } from "@opencode-ai/core/agent"
44
import { PluginV2 } from "@opencode-ai/core/plugin"
55
import { PluginHost } from "@opencode-ai/core/plugin/host"
66
import { PluginPromise } from "@opencode-ai/core/plugin/promise"
7+
import { SessionV2 } from "@opencode-ai/core/session"
8+
import { SessionMessage } from "@opencode-ai/core/session/message"
9+
import { ToolRegistry } from "@opencode-ai/core/tool/registry"
710
import { Plugin } from "@opencode-ai/plugin/v2"
811
import { testEffect } from "../lib/effect"
912
import { PluginTestLayer } from "./fixture"
@@ -93,4 +96,38 @@ describe("fromPromise", () => {
9396
expect(yield* agents.get(AgentV2.ID.make("temp"))).toBeUndefined()
9497
}),
9598
)
99+
100+
it.effect("constructs plain Promise tool declarations in the host", () =>
101+
Effect.gen(function* () {
102+
const plugins = yield* PluginV2.Service
103+
const registry = yield* ToolRegistry.Service
104+
const host = yield* PluginHost.make(plugins)
105+
const promisePlugin = Plugin.define({
106+
id: "promise-tool",
107+
setup: async (ctx) => {
108+
await ctx.tool.transform((tools) => {
109+
tools.add("hello", {
110+
description: "Hello",
111+
input: Schema.Struct({ name: Schema.String }),
112+
output: Schema.String,
113+
execute: async ({ name }) => `Hello, ${name}!`,
114+
})
115+
})
116+
},
117+
})
118+
119+
yield* PluginPromise.fromPromise(promisePlugin).effect(host)
120+
121+
const materialized = yield* registry.materialize()
122+
expect(materialized.definitions).toContainEqual(expect.objectContaining({ name: "hello", description: "Hello" }))
123+
expect(
124+
yield* materialized.settle({
125+
sessionID: SessionV2.ID.make("ses_promise_tool"),
126+
agent: AgentV2.ID.make("build"),
127+
assistantMessageID: SessionMessage.ID.make("msg_promise_tool"),
128+
call: { type: "tool-call", id: "call_promise_tool", name: "hello", input: { name: "world" } },
129+
}),
130+
).toMatchObject({ result: { type: "text", value: "Hello, world!" } })
131+
}),
132+
)
96133
})

packages/docs/plugins.mdx

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -271,25 +271,23 @@ handle expected errors inside the callback.
271271

272272
## Add a tool
273273

274-
Use `Tool.make` with Effect schemas. Promise tools use async executors:
274+
Pass a plain object with Effect schemas to `tools.add`. Promise tools use async
275+
executors:
275276

276277
```ts title=".opencode/plugins/greeting.ts"
277278
import { Plugin } from "@opencode-ai/plugin/v2"
278-
import { Tool } from "@opencode-ai/plugin/v2/tool"
279279
import { Schema } from "effect"
280280

281-
const greeting = Tool.make({
282-
description: "Create a greeting",
283-
input: Schema.Struct({ name: Schema.String }),
284-
output: Schema.String,
285-
execute: async ({ name }) => `Hello, ${name}!`,
286-
})
287-
288281
export default Plugin.define({
289282
id: "acme.greeting",
290283
setup: async (ctx) => {
291284
await ctx.tool.transform((tools) => {
292-
tools.add("greeting", greeting)
285+
tools.add("greeting", {
286+
description: "Create a greeting",
287+
input: Schema.Struct({ name: Schema.String }),
288+
output: Schema.String,
289+
execute: async ({ name }) => `Hello, ${name}!`,
290+
})
293291
})
294292
},
295293
})
@@ -305,10 +303,9 @@ letters, digits, underscores, or hyphens. `tools.add` also accepts
305303
tool instead of exposing it directly.
306304

307305
The executor receives a second context argument containing `sessionID`,
308-
`agent`, `assistantMessageID`, and `toolCallID`. Use
309-
`Tool.withPermission(tool, "permission-name")` to assign a permission key.
310-
Effect plugins import the helper from `@opencode-ai/plugin/v2/effect/tool` and
311-
return an `Effect` from `execute`.
306+
`agent`, `assistantMessageID`, and `toolCallID`. Effect plugins import their
307+
tool contracts from `@opencode-ai/plugin/v2/effect/tool` and return an `Effect`
308+
from `execute`.
312309

313310
## Types
314311

packages/plugin/src/v2/promise/README.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,20 +85,19 @@ await ctx.session.hook("request", (event) => {
8585
})
8686
```
8787

88-
Promise tools use the same schemas and registration model as Effect tools, with async executors:
88+
Promise tools use plain object declarations with async executors:
8989

9090
```ts
9191
import { Schema } from "effect"
92-
import { Tool } from "@opencode-ai/plugin/v2/tool"
9392

94-
const echo = Tool.make({
95-
description: "Echo text",
96-
input: Schema.Struct({ text: Schema.String }),
97-
output: Schema.Struct({ text: Schema.String }),
98-
execute: async ({ text }) => ({ text }),
93+
await ctx.tool.transform((tools) => {
94+
tools.add("echo", {
95+
description: "Echo text",
96+
input: Schema.Struct({ text: Schema.String }),
97+
output: Schema.Struct({ text: Schema.String }),
98+
execute: async ({ text }) => ({ text }),
99+
})
99100
})
100-
101-
await ctx.tool.transform((tools) => tools.add("echo", echo))
102101
```
103102

104103
## Reloading A Domain

packages/plugin/src/v2/promise/index.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
export type { PluginOptions } from "../options.js"
22
export * as Plugin from "./plugin.js"
33

4-
export { Tool } from "./tool.js"
5-
64
export { Agent } from "@opencode-ai/schema/agent"
75
export { Command } from "@opencode-ai/schema/command"
86
export { Connection } from "@opencode-ai/schema/connection"

packages/plugin/src/v2/promise/tool.ts

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,16 @@
1-
export * as Tool from "./tool.js"
2-
3-
import { Tool } from "../effect/tool.js"
1+
import type { Tool } from "../effect/tool.js"
42
import type { Agent } from "@opencode-ai/schema/agent"
53
import type { Session } from "@opencode-ai/schema/session"
64
import type { SessionMessage } from "@opencode-ai/schema/session-message"
7-
import { Effect, type JsonSchema, type Schema } from "effect"
5+
import type { JsonSchema, Schema } from "effect"
86
import type { Hooks, Transform } from "./registration.js"
97

108
export type Context = Tool.Context
119
export type SchemaType<A> = Tool.SchemaType<A>
12-
export type Definition<Input extends SchemaType<any>, Output extends SchemaType<any>> = Tool.Definition<Input, Output>
13-
export type AnyTool = Tool.AnyTool
14-
export const Failure = Tool.Failure
15-
export type Failure = Tool.Failure
16-
export const RegistrationError = Tool.RegistrationError
17-
export type RegistrationError = Tool.RegistrationError
1810
export type Content = Tool.Content
1911
export type DynamicOutput = Tool.DynamicOutput
2012

21-
type Config<
13+
export type Definition<
2214
Input extends SchemaType<any>,
2315
Output extends SchemaType<any>,
2416
Structured extends SchemaType<any> = Output,
@@ -41,32 +33,14 @@ type Config<
4133
}) => ReadonlyArray<Content>
4234
}
4335

44-
type DynamicConfig = {
36+
export type DynamicDefinition = {
4537
readonly description: string
4638
readonly jsonSchema: JsonSchema.JsonSchema
4739
readonly outputSchema?: JsonSchema.JsonSchema
4840
readonly execute: (input: unknown, context: Context) => Promise<DynamicOutput>
4941
}
5042

51-
export function make<
52-
Input extends SchemaType<any>,
53-
Output extends SchemaType<any>,
54-
Structured extends SchemaType<any> = Output,
55-
>(config: Config<Input, Output, Structured>): Definition<Input, Structured>
56-
export function make(config: DynamicConfig): AnyTool
57-
export function make(config: Config<any, any, any> | DynamicConfig): AnyTool {
58-
if ("jsonSchema" in config)
59-
return Tool.make({
60-
...config,
61-
execute: (input, context) => Effect.promise(() => config.execute(input, context)),
62-
})
63-
return Tool.make({
64-
...config,
65-
execute: (input, context) => Effect.promise(() => config.execute(input, context)),
66-
})
67-
}
68-
69-
export const withPermission = Tool.withPermission
43+
export type AnyTool = Definition<any, any, any> | DynamicDefinition
7044

7145
export interface ToolExecuteBeforeEvent {
7246
readonly tool: string
@@ -95,7 +69,12 @@ export interface RegisterOptions {
9569
}
9670

9771
export interface ToolDraft {
98-
add(name: string, tool: AnyTool, options?: RegisterOptions): void
72+
add<
73+
Input extends SchemaType<any>,
74+
Output extends SchemaType<any>,
75+
Structured extends SchemaType<any> = Output,
76+
>(name: string, tool: Definition<Input, Output, Structured>, options?: RegisterOptions): void
77+
add(name: string, tool: DynamicDefinition, options?: RegisterOptions): void
9978
}
10079

10180
export interface ToolHooks {

packages/plugin/test/contract-identity.test.ts

Lines changed: 0 additions & 1 deletion

0 commit comments

Comments
 (0)