Dax Raad пре 8 месеци
родитељ
комит
b4f809559e

+ 1 - 0
bun.lock

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

+ 1 - 0
packages/opencode/package.json

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

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

@@ -14,7 +14,7 @@ export namespace Config {
 
 
   export const Info = z
   export const Info = z
     .object({
     .object({
-      provider: Provider.Info.array().optional(),
+      provider: z.lazy(() => Provider.Info.array().optional()),
       tool: z
       tool: z
         .object({
         .object({
           provider: z.record(z.string(), z.string().array()).optional(),
           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 path from "path"
 import { Global } from "../global"
 import { Global } from "../global"
 import { BunProc } from "../bun"
 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 {
 export namespace Provider {
   const log = Log.create({ service: "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 {
   class ModelNotFoundError extends Error {
     constructor(public readonly model: string) {
     constructor(public readonly model: string) {
       super()
       super()

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

@@ -8,10 +8,11 @@ import {
   generateText,
   generateText,
   stepCountIs,
   stepCountIs,
   streamText,
   streamText,
+  tool,
+  type Tool as AITool,
   type LanguageModelUsage,
   type LanguageModelUsage,
 } from "ai"
 } from "ai"
-import { z } from "zod"
-import * as tools from "../tool"
+import { z, ZodSchema } from "zod"
 import { Decimal } from "decimal.js"
 import { Decimal } from "decimal.js"
 
 
 import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
 import PROMPT_ANTHROPIC from "./prompt/anthropic.txt"
@@ -290,6 +291,38 @@ export namespace Session {
       },
       },
     }
     }
     await updateMessage(next)
     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({
     const result = streamText({
       onStepFinish: async (step) => {
       onStepFinish: async (step) => {
         const assistant = next.metadata!.assistant!
         const assistant = next.metadata!.assistant!
@@ -358,12 +391,12 @@ export namespace Session {
               p.toolInvocation.toolCallId === value.toolCallId,
               p.toolInvocation.toolCallId === value.toolCallId,
           )
           )
           if (match && match.type === "tool-invocation") {
           if (match && match.type === "tool-invocation") {
-            const { output, metadata } = value.result as any
-            next.metadata!.tool[value.toolCallId] = metadata
             match.toolInvocation = {
             match.toolInvocation = {
-              ...match.toolInvocation,
+              args: match.toolInvocation.args,
+              toolCallId: match.toolInvocation.toolCallId,
+              toolName: match.toolInvocation.toolName,
               state: "result",
               state: "result",
-              result: output,
+              result: value.result as string,
             }
             }
           }
           }
           break
           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
 - Return an empty response - the user will see the gh output directly
 - Never update git config`
 - Never update git config`
 
 
-export const bash = Tool.define({
-  name: "opencode.bash",
+export const BashTool = Tool.define({
+  id: "opencode.bash",
   description: DESCRIPTION,
   description: DESCRIPTION,
   parameters: z.object({
   parameters: z.object({
     command: z.string(),
     command: z.string(),
@@ -193,6 +193,7 @@ export const bash = Tool.define({
       timeout: timeout,
       timeout: timeout,
     })
     })
     return {
     return {
+      metadata: {},
       output: process.stdout.toString("utf-8"),
       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.`
 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,
   description: DESCRIPTION,
   parameters: z.object({
   parameters: z.object({
     filePath: z.string().describe("The absolute path to the file to modify"),
     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
 - Use html format when you need the raw HTML structure
 - Set appropriate timeouts for potentially slow websites`
 - 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,
   description: DESCRIPTION,
   parameters: z.object({
   parameters: z.object({
     url: z.string().describe("The URL to fetch content from"),
     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)")
       .describe("Optional timeout in seconds (max 120)")
       .optional(),
       .optional(),
   }),
   }),
-  async execute(params, opts) {
+  async execute(param) {
     // Validate URL
     // Validate URL
     if (
     if (
       !params.url.startsWith("http://") &&
       !params.url.startsWith("http://") &&
@@ -69,9 +69,6 @@ export const Fetch = Tool.define({
 
 
     const controller = new AbortController()
     const controller = new AbortController()
     const timeoutId = setTimeout(() => controller.abort(), timeout)
     const timeoutId = setTimeout(() => controller.abort(), timeout)
-    if (opts?.abortSignal) {
-      opts.abortSignal.addEventListener("abort", () => controller.abort())
-    }
 
 
     const response = await fetch(params.url, {
     const response = await fetch(params.url, {
       signal: controller.signal,
       signal: controller.signal,
@@ -104,22 +101,22 @@ export const Fetch = Tool.define({
       case "text":
       case "text":
         if (contentType.includes("text/html")) {
         if (contentType.includes("text/html")) {
           const text = extractTextFromHTML(content)
           const text = extractTextFromHTML(content)
-          return { output: text }
+          return { output: text, metadata: {} }
         }
         }
-        return { output: content }
+        return { output: content, metadata: {} }
 
 
       case "markdown":
       case "markdown":
         if (contentType.includes("text/html")) {
         if (contentType.includes("text/html")) {
           const markdown = convertHTMLToMarkdown(content)
           const markdown = convertHTMLToMarkdown(content)
-          return { output: markdown }
+          return { output: markdown, metadata: {} }
         }
         }
         return { output: "```\n" + content + "\n```" }
         return { output: "```\n" + content + "\n```" }
 
 
       case "html":
       case "html":
-        return { output: content }
+        return { output: content, metadata: {} }
 
 
       default:
       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
 - 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`
 - 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,
   description: DESCRIPTION,
   parameters: z.object({
   parameters: z.object({
     pattern: z.string().describe("The glob pattern to match files against"),
     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 }
   return { matches, truncated }
 }
 }
 
 
-export const grep = Tool.define({
-  name: "opencode.grep",
+export const GrepTool = Tool.define({
+  id: "opencode.grep",
   description: DESCRIPTION,
   description: DESCRIPTION,
   parameters: z.object({
   parameters: z.object({
     pattern: z
     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/",
   ".vscode/",
 ]
 ]
 
 
-export const ls = Tool.define({
-  name: "opencode.ls",
+export const ListTool = Tool.define({
+  id: "opencode.list",
   description: "List directory contents",
   description: "List directory contents",
   parameters: z.object({
   parameters: z.object({
     path: z.string().optional(),
     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"
 import { App } from "../app/app"
 
 
 export const LspDiagnosticTool = Tool.define({
 export const LspDiagnosticTool = Tool.define({
-  name: "opencode.lsp_diagnostic",
+  id: "opencode.lsp_diagnostics",
   description: `Get diagnostics for a file and/or project.
   description: `Get diagnostics for a file and/or project.
 
 
 WHEN TO USE THIS TOOL:
 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"
 import { App } from "../app/app"
 
 
 export const LspHoverTool = Tool.define({
 export const LspHoverTool = Tool.define({
-  name: "opencode.lsp_hover",
+  id: "opencode.lsp_hover",
   description: `
   description: `
   Looks up hover information for a given position in a source file using the Language Server Protocol (LSP). 
   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. 
   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,
   description: DESCRIPTION,
   parameters: PatchParams,
   parameters: PatchParams,
   execute: async (params) => {
   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 {
 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<
   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
 - 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`
 - 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,
   description: DESCRIPTION,
   parameters: z.object({
   parameters: z.object({
     filePath: z.string().describe("The path to the file to read"),
     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 { describe, expect, test } from "bun:test"
 import { App } from "../../src/app/app"
 import { App } from "../../src/app/app"
-import { glob } from "../../src/tool/glob"
+import { GlobTool } from "../../src/tool/glob"
 import { ls } from "../../src/tool/ls"
 import { ls } from "../../src/tool/ls"
 
 
 describe("tool.glob", () => {
 describe("tool.glob", () => {
   test("truncate", async () => {
   test("truncate", async () => {
     await App.provide({ directory: process.cwd() }, async () => {
     await App.provide({ directory: process.cwd() }, async () => {
-      let result = await glob.execute(
+      let result = await GlobTool.execute(
         {
         {
           pattern: "./node_modules/**/*",
           pattern: "./node_modules/**/*",
         },
         },
@@ -20,7 +20,7 @@ describe("tool.glob", () => {
   })
   })
   test("basic", async () => {
   test("basic", async () => {
     await App.provide({ directory: process.cwd() }, async () => {
     await App.provide({ directory: process.cwd() }, async () => {
-      let result = await glob.execute(
+      let result = await GlobTool.execute(
         {
         {
           pattern: "*.json",
           pattern: "*.json",
         },
         },