mirror of
https://gitee.com/zhangsken/opencode.git
synced 2026-10-16 10:30:01 +08:00
feat(plugin): add list to the tool domain (#49838)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
This commit is contained in:
@@ -453,6 +453,7 @@ export const make = Effect.fn("PluginHost.make")(function* (
|
||||
tool: {
|
||||
transform: tools.transform,
|
||||
reload: tools.reload,
|
||||
list: tools.list,
|
||||
hook: (name, callback) => hooks.register("tool", name, callback),
|
||||
},
|
||||
vcs: {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
export * as ToolInputRepairPlugin from "./tool-input-repair.js"
|
||||
|
||||
import { define } from "@opencode/plugin/effect/plugin"
|
||||
import type { ToolEditor } from "@opencode/plugin/effect/tool"
|
||||
import { Effect, JsonSchema, Option, Predicate, Schema } from "effect"
|
||||
import { definition } from "../tool/runtime.js"
|
||||
|
||||
@@ -21,25 +20,19 @@ const maxDepth = 6
|
||||
|
||||
export const Plugin = define({
|
||||
id: "opencode.tool.input.repair",
|
||||
effect: Effect.fn(function* (ctx) {
|
||||
let get: ToolEditor["get"] = () => undefined
|
||||
yield* ctx.tool.transform((draft) => {
|
||||
// The draft sees later tool transforms too; reload replaces this lookup.
|
||||
get = draft.get
|
||||
})
|
||||
yield* ctx.tool.hook("execute.before", (event) =>
|
||||
Effect.sync(() => {
|
||||
effect: (ctx) =>
|
||||
ctx.tool.hook("execute.before", (event) =>
|
||||
Effect.gen(function* () {
|
||||
// The outer Code Mode tool is built per snapshot rather than registered, so it cannot be
|
||||
// looked up here. Its `{ code }` input is trivial; the tools it calls are repaired normally.
|
||||
if (event.tool === "execute") return
|
||||
const tool = get(event.tool)
|
||||
const tool = (yield* ctx.tool.list()).find((tool) => tool.id === event.tool)
|
||||
if (!tool) return
|
||||
const schema = definition(tool).inputSchema
|
||||
if (schema.type !== "object") return
|
||||
event.input = repair(event.input, schema, schema, 0)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
function repair(value: unknown, schema: JsonSchema.JsonSchema, root: JsonSchema.JsonSchema, depth: number): unknown {
|
||||
|
||||
@@ -39,6 +39,7 @@ type Data = {
|
||||
}
|
||||
|
||||
export interface Interface extends State.Transformable<Editor> {
|
||||
readonly list: () => Effect.Effect<ReadonlyArray<Tool.Info & { readonly id: string }>>
|
||||
readonly snapshot: (permissions?: Permission.Ruleset) => Effect.Effect<Snapshot>
|
||||
}
|
||||
|
||||
@@ -220,6 +221,7 @@ const layer = Layer.effect(
|
||||
return Service.of({
|
||||
transform: state.transform,
|
||||
reload: state.reload,
|
||||
list: () => Effect.sync(() => Array.from(state.get().tools.values())),
|
||||
snapshot: Effect.fn("Tool.snapshot")((permissions) =>
|
||||
Effect.sync(() => {
|
||||
const data = state.get()
|
||||
|
||||
@@ -72,6 +72,7 @@ export const registerToolPlugin = <R>(
|
||||
tool: {
|
||||
transform: tools.transform,
|
||||
reload: tools.reload,
|
||||
list: tools.list,
|
||||
hook: () => Effect.die("registerToolPlugin does not support tool hooks"),
|
||||
},
|
||||
})
|
||||
|
||||
@@ -133,6 +133,7 @@ export function host(overrides: Overrides = {}): Plugin.Context {
|
||||
tool: overrides.tool ?? {
|
||||
transform: () => Effect.die("unused tool.transform"),
|
||||
reload: () => Effect.die("unused tool.reload"),
|
||||
list: () => Effect.die("unused tool.list"),
|
||||
hook: () => Effect.die("unused tool.hook"),
|
||||
},
|
||||
vcs: overrides.vcs ?? {
|
||||
|
||||
@@ -73,6 +73,7 @@ const run = Effect.fnUntraced(function* (events: ReadonlyArray<SessionEvent.Agen
|
||||
tool: {
|
||||
transform: () => Effect.die("unused tool.transform"),
|
||||
reload: () => Effect.die("unused tool.reload"),
|
||||
list: () => Effect.die("unused tool.list"),
|
||||
hook: (name, callback) => {
|
||||
if (name === "execute.after") {
|
||||
// Hook names and callbacks are correlated, but TypeScript does not narrow this generic registration API.
|
||||
|
||||
@@ -22,30 +22,19 @@ function run(input: unknown, inputSchema: JsonSchema.JsonSchema) {
|
||||
"execute.before": event,
|
||||
"execute.after": { ...event, status: "error", error: new Tool.Error({ message: "unused" }) },
|
||||
}
|
||||
const tool = {
|
||||
id: "test",
|
||||
name: "test",
|
||||
description: "Test repair",
|
||||
input: inputSchema,
|
||||
execute: () => Effect.succeed({ content: "unused" }),
|
||||
}
|
||||
const base = host()
|
||||
return ToolInputRepairPlugin.Plugin.effect(
|
||||
host({
|
||||
tool: {
|
||||
...base.tool,
|
||||
transform: (callback) =>
|
||||
Effect.sync(() => {
|
||||
const tool = {
|
||||
id: "test",
|
||||
name: "test",
|
||||
description: "Test repair",
|
||||
input: inputSchema,
|
||||
execute: () => Effect.succeed({ content: "unused" }),
|
||||
}
|
||||
callback({
|
||||
list: () => [tool],
|
||||
get: (id) => (id === tool.id ? tool : undefined),
|
||||
add: () => {},
|
||||
namespace: () => {},
|
||||
update: () => {},
|
||||
remove: () => {},
|
||||
})
|
||||
return { dispose: Effect.void }
|
||||
}),
|
||||
list: () => Effect.succeed([tool]),
|
||||
hook: (name, callback) => callback(events[name]).pipe(Effect.orDie, Effect.as({ dispose: Effect.void })),
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -855,6 +855,21 @@ describe("Tool", () => {
|
||||
])
|
||||
}),
|
||||
)
|
||||
it.effect("lists registered tools by effective name", () =>
|
||||
Effect.gen(function* () {
|
||||
const service = yield* Tool.Service
|
||||
yield* transform(service, { echo: make() }, { codemode: false })
|
||||
yield* transform(service, { count: { ...constant("1"), name: "count" } }, { namespace: "acme" })
|
||||
|
||||
expect((yield* service.list()).map((tool) => [tool.id, tool.name])).toEqual([
|
||||
["echo", "echo"],
|
||||
["acme_count", "count"],
|
||||
])
|
||||
|
||||
yield* service.transform((editor) => editor.remove("echo"))
|
||||
expect((yield* service.list()).map((tool) => tool.id)).toEqual(["acme_count"])
|
||||
}),
|
||||
)
|
||||
;[
|
||||
{ name: "string", content: "hooked", text: "hooked" },
|
||||
{ name: "empty string", content: "", text: "" },
|
||||
|
||||
@@ -54,5 +54,7 @@ export interface ToolFailures extends Record<keyof ToolHooks, unknown> {
|
||||
export interface ToolDomain {
|
||||
readonly transform: Transform<ToolEditor>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
/** Currently registered tools, after every transform, keyed by effective name. */
|
||||
readonly list: () => Effect.Effect<readonly (Tool.Info & { readonly id: string })[]>
|
||||
readonly hook: Hooks<ToolHooks, ToolFailures>
|
||||
}
|
||||
|
||||
@@ -462,6 +462,10 @@ export function fromPromise(plugin: Plugin) {
|
||||
},
|
||||
tool: {
|
||||
reload: () => run(host.tool.reload()),
|
||||
list: () =>
|
||||
run(host.tool.list()).then((tools) =>
|
||||
tools.map((tool) => ({ ...tool, execute: promiseExecutor(tool.execute) })),
|
||||
),
|
||||
transform: (callback) =>
|
||||
register(
|
||||
host.tool.transform((editor) =>
|
||||
|
||||
@@ -65,5 +65,7 @@ interface ToolHooks {
|
||||
export interface ToolDomain {
|
||||
readonly transform: Transform<ToolEditor>
|
||||
readonly reload: () => Promise<void>
|
||||
/** Currently registered tools, after every transform, keyed by effective name. */
|
||||
readonly list: () => Promise<readonly (Info & { readonly id: string })[]>
|
||||
readonly hook: Hooks<ToolHooks>
|
||||
}
|
||||
|
||||
@@ -1014,6 +1014,7 @@ interface ToolEditor {
|
||||
}
|
||||
|
||||
interface ToolDomain {
|
||||
readonly list: () => Effect.Effect<readonly (Tool.Info & { readonly id: string })[]>
|
||||
readonly transform: Transform<ToolEditor>
|
||||
readonly reload: () => Effect.Effect<void>
|
||||
}
|
||||
|
||||
@@ -892,6 +892,14 @@ interface StorageScanResult {
|
||||
|
||||
### Tools
|
||||
|
||||
Read the tools currently registered, after every transform, outside of a transform callback. Each tool has an `id`
|
||||
containing its effective name.
|
||||
|
||||
```ts
|
||||
const tools = await ctx.tool.list()
|
||||
const read = tools.find((tool) => tool.id === "read")
|
||||
```
|
||||
|
||||
Register, update, and remove tools with a synchronous transform, including in Promise plugins. Load external data
|
||||
before registering or reloading. A later valid registration overrides the same effective tool name.
|
||||
|
||||
@@ -964,6 +972,7 @@ Schemas: [`Tool.Content`](/api#schema-Tool.Content), [`Tool.TextContent`](/api#s
|
||||
|
||||
```ts
|
||||
interface ToolContext {
|
||||
list(): Promise<readonly (ToolInfo & { readonly id: string })[]>
|
||||
transform(callback: (editor: ToolEditor) => void): Promise<Registration>
|
||||
reload(): Promise<void>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user