Skip to content
19 changes: 15 additions & 4 deletions packages/opencode/src/tool/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,22 @@ export namespace ToolRegistry {
description: def.description,
execute: async (args, ctx) => {
const result = await def.execute(args as any, ctx)
const out = await Truncate.output(result, {}, initCtx?.agent)

const isString = typeof result === 'string'
const output = isString ? result : result.output

const truncatedOut = await Truncate.output(output, {}, initCtx?.agent)
const title = isString ? "" : result.title ?? ""
const metadata = {
...(isString ? {} : (result.metadata ?? {})),
truncated: truncatedOut.truncated,
outputPath: truncatedOut.truncated ? truncatedOut.outputPath : undefined,
}

return {
title: "",
output: out.truncated ? out.content : result,
metadata: { truncated: out.truncated, outputPath: out.truncated ? out.outputPath : undefined },
title,
metadata,
output: truncatedOut.truncated ? truncatedOut.content : output,
}
},
}),
Expand Down
53 changes: 51 additions & 2 deletions packages/opencode/test/tool/registry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import fs from "fs/promises"
import { tmpdir } from "../fixture/fixture"
import { Instance } from "../../src/project/instance"
import { ToolRegistry } from "../../src/tool/registry"
import { Tool } from "../../src/tool/tool"
import { Provider } from "../../src/provider/provider"

describe("tool.registry", () => {
test("loads tools from .opencode/tool (singular)", async () => {
Expand All @@ -28,14 +30,61 @@ describe("tool.registry", () => {
"",
].join("\n"),
)

await Bun.write(
path.join(toolDir, "goodbye.ts"),
[
"export default {",
" description: 'goodbye tool',",
" args: {},",
" execute: async () => {",
" return {",
" title: 'goodbye title',",
" output: 'goodbye world',",
" metadata: { nihilism: true },",
" }",
" },",
"}",
"",
].join("\n"),
)
},
})

await Instance.provide({
directory: tmp.path,
fn: async () => {
const ids = await ToolRegistry.ids()
expect(ids).toContain("hello")
const tools = await ToolRegistry.tools(await Provider.defaultModel())
const hello = tools.find((t) => t.id === "hello")
const goodbye = tools.find((t) => t.id === "goodbye")

expect(hello).toBeDefined()
expect(goodbye).toBeDefined()

const helloResult = await hello?.execute({}, {} as Tool.Context)
const goodbyeResult = await goodbye?.execute({}, {} as Tool.Context)

expect(helloResult).toMatchInlineSnapshot(`
{
"metadata": {
"outputPath": undefined,
"truncated": false,
},
"output": "hello world",
"title": "",
}
`)
expect(goodbyeResult).toMatchInlineSnapshot(`
{
"metadata": {
"nihilism": true,
"outputPath": undefined,
"truncated": false,
},
"output": "goodbye world",
"title": "goodbye title",
}
`)
},
})
})
Expand Down
29 changes: 22 additions & 7 deletions packages/plugin/src/tool.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
import { z } from "zod"
import type { FilePart } from "@opencode-ai/sdk"

export type ToolContext = {
export type Metadata = {
[key: string]: any
}

export type ToolContext<M extends Metadata = Metadata> = {
sessionID: string
messageID: string
agent: string
abort: AbortSignal
metadata(input: { title?: string; metadata?: { [key: string]: any } }): void
ask(input: AskInput): Promise<void>
callID?: string
extra?: M
metadata(input: { title?: string; metadata?: M }): void
ask(input: AskInput<M>): Promise<void>
}

type AskInput = {
export type AskInput<M extends Metadata = Metadata> = {
permission: string
patterns: string[]
always: string[]
metadata: { [key: string]: any }
metadata: M
}

export type ExecuteResult<M extends Metadata = Metadata> = {
title: string
metadata: M
output: string
attachments?: FilePart[]
}

export function tool<Args extends z.ZodRawShape>(input: {
export function tool<Args extends z.ZodRawShape, M extends Metadata = Metadata>(input: {
description: string
args: Args
execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext): Promise<string>
execute(args: z.infer<z.ZodObject<Args>>, context: ToolContext<M>): Promise<string | ExecuteResult<M>>
formatValidationError?(error: z.ZodError): string
}) {
return input
}
Expand Down