Dax Raad 8 месяцев назад
Родитель
Сommit
b4f809559e

+ 1 - 0
bun.lock

@@ -24,6 +24,7 @@
         "@flystorage/file-storage": "1.1.0",
         "@flystorage/local-fs": "1.1.0",
         "@hono/zod-validator": "0.5.0",
+        "@standard-schema/spec": "1.0.0",
         "ai": "catalog:",
         "cac": "6.7.14",
         "decimal.js": "10.5.0",

+ 1 - 0
packages/opencode/package.json

@@ -21,6 +21,7 @@
     "@flystorage/file-storage": "1.1.0",
     "@flystorage/local-fs": "1.1.0",
     "@hono/zod-validator": "0.5.0",
+    "@standard-schema/spec": "1.0.0",
     "ai": "catalog:",
     "cac": "6.7.14",
     "decimal.js": "10.5.0",

+ 1 - 1
packages/opencode/src/config/config.ts

@@ -14,7 +14,7 @@ export namespace Config {
 
   export const Info = z
     .object({
-      provider: Provider.Info.array().optional(),
+      provider: z.lazy(() => Provider.Info.array().optional()),
       tool: z
         .object({
           provider: z.record(z.string(), z.string().array()).optional(),

+ 38 - 0
packages/opencode/src/provider/provider.ts

@@ -7,6 +7,17 @@ import { Log } from "../util/log"
 import path from "path"
 import { Global } from "../global"
 import { BunProc } from "../bun"
+import { BashTool } from "../tool/bash"
+import { EditTool } from "../tool/edit"
+import { FetchTool } from "../tool/fetch"
+import { GlobTool } from "../tool/glob"
+import { GrepTool } from "../tool/grep"
+import { ListTool } from "../tool/ls"
+import { LspDiagnosticTool } from "../tool/lsp-diagnostics"
+import { LspHoverTool } from "../tool/lsp-hover"
+import { PatchTool } from "../tool/patch"
+import { ViewTool } from "../tool/view"
+import type { Tool } from "../tool/tool"
 
 export namespace Provider {
   const log = Log.create({ service: "provider" })
@@ -130,6 +141,33 @@ export namespace Provider {
     }
   }
 
+  const TOOLS = [
+    BashTool,
+    EditTool,
+    FetchTool,
+    GlobTool,
+    GrepTool,
+    ListTool,
+    LspDiagnosticTool,
+    LspHoverTool,
+    PatchTool,
+    ViewTool,
+    EditTool,
+  ]
+  const TOOL_MAPPING: Record<string, Tool.Info[]> = {
+    anthropic: TOOLS,
+    openai: TOOLS,
+    google: TOOLS,
+  }
+  export async function tools(providerID: string) {
+    const cfg = await Config.get()
+    if (cfg.tool?.provider?.[providerID])
+      return cfg.tool.provider[providerID].map(
+        (id) => TOOLS.find((t) => t.id === id)!,
+      )
+    return TOOL_MAPPING[providerID] ?? TOOLS
+  }
+
   class ModelNotFoundError extends Error {
     constructor(public readonly model: string) {
       super()

+ 39 - 6
packages/opencode/src/session/session.ts

@@ -8,10 +8,11 @@ import {
   generateText,
   stepCountIs,
   streamText,
+  tool,
+  type Tool as AITool,
   type LanguageModelUsage,
 } from "ai"
-import { z } from "zod"
-import * as tools from "../tool"
+import { z, ZodSchema } from "zod"
 import { Decimal } from "decimal.js"
 
 import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
@@ -290,6 +291,38 @@ export namespace Session {
       },
     }
     await updateMessage(next)
+    const tools: Record<string, AITool> = {}
+    for (const item of await Provider.tools(input.providerID)) {
+      tools[item.id.replaceAll(".", "_")] = tool({
+        id: item.id as any,
+        description: item.description,
+        parameters: item.parameters as ZodSchema,
+        async execute(args, opts) {
+          const start = Date.now()
+          try {
+            const result = await item.execute(args)
+            next.metadata!.tool![opts.toolCallId] = {
+              ...result.metadata,
+              time: {
+                start,
+                end: Date.now(),
+              },
+            }
+            return result.output
+          } catch (e: any) {
+            next.metadata!.tool![opts.toolCallId] = {
+              error: true,
+              message: e.toString(),
+              time: {
+                start,
+                end: Date.now(),
+              },
+            }
+            return e.toString()
+          }
+        },
+      })
+    }
     const result = streamText({
       onStepFinish: async (step) => {
         const assistant = next.metadata!.assistant!
@@ -358,12 +391,12 @@ export namespace Session {
               p.toolInvocation.toolCallId === value.toolCallId,
           )
           if (match && match.type === "tool-invocation") {
-            const { output, metadata } = value.result as any
-            next.metadata!.tool[value.toolCallId] = metadata
             match.toolInvocation = {
-              ...match.toolInvocation,
+              args: match.toolInvocation.args,
+              toolCallId: match.toolInvocation.toolCallId,
+              toolName: match.toolInvocation.toolName,
               state: "result",
-              result: output,
+              result: value.result as string,
             }
           }
           break

+ 3 - 2
packages/opencode/src/tool/bash.ts

@@ -170,8 +170,8 @@ Important:
 - Return an empty response - the user will see the gh output directly
 - Never update git config`
 
-export const bash = Tool.define({
-  name: "opencode.bash",
+export const BashTool = Tool.define({
+  id: "opencode.bash",
   description: DESCRIPTION,
   parameters: z.object({
     command: z.string(),
@@ -193,6 +193,7 @@ export const bash = Tool.define({
       timeout: timeout,
     })
     return {
+      metadata: {},
       output: process.stdout.toString("utf-8"),
     }
   },

+ 2 - 2
packages/opencode/src/tool/edit.ts

@@ -52,8 +52,8 @@ When making edits:
 
 Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.`
 
-export const edit = Tool.define({
-  name: "opencode.edit",
+export const EditTool = Tool.define({
+  id: "opencode.edit",
   description: DESCRIPTION,
   parameters: z.object({
     filePath: z.string().describe("The absolute path to the file to modify"),

+ 19 - 0
packages/opencode/src/tool/example.ts

@@ -0,0 +1,19 @@
+import { z } from "zod"
+import { Tool } from "./tool"
+
+export const ExampleTool = Tool.define({
+  id: "opencode.example",
+  description: "Example tool",
+  parameters: z.object({
+    foo: z.string().describe("The foo parameter"),
+    bar: z.number().describe("The bar parameter"),
+  }),
+  async execute(params) {
+    return {
+      metadata: {
+        lol: "hey",
+      },
+      output: "Hello, world!",
+    }
+  },
+})

+ 8 - 11
packages/opencode/src/tool/fetch.ts

@@ -36,8 +36,8 @@ TIPS:
 - Use html format when you need the raw HTML structure
 - Set appropriate timeouts for potentially slow websites`
 
-export const Fetch = Tool.define({
-  name: "opencode.fetch",
+export const FetchTool = Tool.define({
+  id: "opencode.fetch",
   description: DESCRIPTION,
   parameters: z.object({
     url: z.string().describe("The URL to fetch content from"),
@@ -53,7 +53,7 @@ export const Fetch = Tool.define({
       .describe("Optional timeout in seconds (max 120)")
       .optional(),
   }),
-  async execute(params, opts) {
+  async execute(param) {
     // Validate URL
     if (
       !params.url.startsWith("http://") &&
@@ -69,9 +69,6 @@ export const Fetch = Tool.define({
 
     const controller = new AbortController()
     const timeoutId = setTimeout(() => controller.abort(), timeout)
-    if (opts?.abortSignal) {
-      opts.abortSignal.addEventListener("abort", () => controller.abort())
-    }
 
     const response = await fetch(params.url, {
       signal: controller.signal,
@@ -104,22 +101,22 @@ export const Fetch = Tool.define({
       case "text":
         if (contentType.includes("text/html")) {
           const text = extractTextFromHTML(content)
-          return { output: text }
+          return { output: text, metadata: {} }
         }
-        return { output: content }
+        return { output: content, metadata: {} }
 
       case "markdown":
         if (contentType.includes("text/html")) {
           const markdown = convertHTMLToMarkdown(content)
-          return { output: markdown }
+          return { output: markdown, metadata: {} }
         }
         return { output: "```\n" + content + "\n```" }
 
       case "html":
-        return { output: content }
+        return { output: content, metadata: {} }
 
       default:
-        return { output: content }
+        return { output: content, metadata: {} }
     }
   },
 })

+ 2 - 2
packages/opencode/src/tool/glob.ts

@@ -37,8 +37,8 @@ TIPS:
 - When doing iterative exploration that may require multiple rounds of searching, consider using the Agent tool instead
 - Always check if results are truncated and refine your search pattern if needed`
 
-export const glob = Tool.define({
-  name: "opencode.glob",
+export const GlobTool = Tool.define({
+  id: "opencode.glob",
   description: DESCRIPTION,
   parameters: z.object({
     pattern: z.string().describe("The glob pattern to match files against"),

+ 2 - 2
packages/opencode/src/tool/grep.ts

@@ -255,8 +255,8 @@ async function searchFiles(
   return { matches, truncated }
 }
 
-export const grep = Tool.define({
-  name: "opencode.grep",
+export const GrepTool = Tool.define({
+  id: "opencode.grep",
   description: DESCRIPTION,
   parameters: z.object({
     pattern: z

+ 0 - 9
packages/opencode/src/tool/index.ts

@@ -1,9 +0,0 @@
-export * from "./bash"
-export * from "./edit"
-export * from "./fetch"
-export * from "./glob"
-export * from "./grep"
-export * from "./view"
-export * from "./ls"
-export * from "./lsp-diagnostics"
-export * from "./lsp-hover"

+ 2 - 2
packages/opencode/src/tool/ls.ts

@@ -17,8 +17,8 @@ const IGNORE_PATTERNS = [
   ".vscode/",
 ]
 
-export const ls = Tool.define({
-  name: "opencode.ls",
+export const ListTool = Tool.define({
+  id: "opencode.list",
   description: "List directory contents",
   parameters: z.object({
     path: z.string().optional(),

+ 1 - 1
packages/opencode/src/tool/lsp-diagnostics.ts

@@ -5,7 +5,7 @@ import { LSP } from "../lsp"
 import { App } from "../app/app"
 
 export const LspDiagnosticTool = Tool.define({
-  name: "opencode.lsp_diagnostic",
+  id: "opencode.lsp_diagnostics",
   description: `Get diagnostics for a file and/or project.
 
 WHEN TO USE THIS TOOL:

+ 1 - 1
packages/opencode/src/tool/lsp-hover.ts

@@ -5,7 +5,7 @@ import { LSP } from "../lsp"
 import { App } from "../app/app"
 
 export const LspHoverTool = Tool.define({
-  name: "opencode.lsp_hover",
+  id: "opencode.lsp_hover",
   description: `
   Looks up hover information for a given position in a source file using the Language Server Protocol (LSP). 
   This includes type information, documentation, or symbol details at the specified line and character. 

+ 2 - 2
packages/opencode/src/tool/patch.ts

@@ -265,8 +265,8 @@ async function applyCommit(
   }
 }
 
-export const patch = Tool.define({
-  name: "opencode.patch",
+export const PatchTool = Tool.define({
+  id: "opencode.patch",
   description: DESCRIPTION,
   parameters: PatchParams,
   execute: async (params) => {

+ 15 - 64
packages/opencode/src/tool/tool.ts

@@ -1,72 +1,23 @@
-import { tool, type Tool as AITool } from "ai"
-import { Log } from "../util/log"
-import { Config } from "../config/config"
+import type { StandardSchemaV1 } from "@standard-schema/spec"
 
 export namespace Tool {
-  const log = Log.create({ service: "tool" })
-
-  export interface Metadata<
-    Properties extends Record<string, any> = Record<string, any>,
+  export interface Info<
+    Parameters extends StandardSchemaV1 = StandardSchemaV1,
+    Metadata extends Record<string, any> = Record<string, any>,
   > {
-    properties: Properties
-    time: {
-      start: number
-      end: number
-    }
-  }
-
-  const TOOL_MAPPING: Record<string, string[]> = {
-    anthropic: [],
-  }
-  export async function forProvider(providerID: string) {
-    const config = await Config.get()
-    const match = config.tool?.provider?.[providerID] ?? []
+    id: string
+    description: string
+    parameters: Parameters
+    execute(args: StandardSchemaV1.InferOutput<Parameters>): Promise<{
+      metadata: Metadata
+      output: string
+    }>
   }
 
   export function define<
-    Params,
-    Output extends { metadata?: any; output: any },
-    Name extends string,
-  >(
-    input: AITool<Params, Output> & {
-      name: Name
-    },
-  ) {
-    return tool({
-      ...input,
-      execute: async (params, opts) => {
-        log.info("invoking", {
-          id: opts.toolCallId,
-          name: input.name,
-          ...params,
-        })
-        try {
-          const start = Date.now()
-          const result = await input.execute!(params, opts)
-          const metadata: Metadata<Output["metadata"]> = {
-            ...result.metadata,
-            time: {
-              start,
-              end: Date.now(),
-            },
-          }
-          return {
-            metadata,
-            output: result.output,
-          }
-        } catch (e: any) {
-          log.error("error", {
-            msg: e.toString(),
-          })
-          return {
-            metadata: {
-              error: true,
-              message: e.toString(),
-            },
-            output: "An error occurred: " + e.toString(),
-          }
-        }
-      },
-    })
+    Parameters extends StandardSchemaV1,
+    Result extends Record<string, any>,
+  >(input: Info<Parameters, Result>): Info<Parameters, Result> {
+    return input
   }
 }

+ 2 - 2
packages/opencode/src/tool/view.ts

@@ -40,8 +40,8 @@ TIPS:
 - For code exploration, first use Grep to find relevant files, then View to examine them
 - When viewing large files, use the offset parameter to read specific sections`
 
-export const view = Tool.define({
-  name: "opencode.view",
+export const ViewTool = Tool.define({
+  id: "opencode.view",
   description: DESCRIPTION,
   parameters: z.object({
     filePath: z.string().describe("The path to the file to read"),

+ 3 - 3
packages/opencode/test/tool/tool.test.ts

@@ -1,12 +1,12 @@
 import { describe, expect, test } from "bun:test"
 import { App } from "../../src/app/app"
-import { glob } from "../../src/tool/glob"
+import { GlobTool } from "../../src/tool/glob"
 import { ls } from "../../src/tool/ls"
 
 describe("tool.glob", () => {
   test("truncate", async () => {
     await App.provide({ directory: process.cwd() }, async () => {
-      let result = await glob.execute(
+      let result = await GlobTool.execute(
         {
           pattern: "./node_modules/**/*",
         },
@@ -20,7 +20,7 @@ describe("tool.glob", () => {
   })
   test("basic", async () => {
     await App.provide({ directory: process.cwd() }, async () => {
-      let result = await glob.execute(
+      let result = await GlobTool.execute(
         {
           pattern: "*.json",
         },