Просмотр исходного кода

feat(cli): Support debug tool calling directly in CLI. (#6564)

Eric Guo 1 месяц назад
Родитель
Сommit
f4f8f2d151
1 измененных файлов с 143 добавлено и 6 удалено
  1. 143 6
      packages/opencode/src/cli/cmd/debug/agent.ts

+ 143 - 6
packages/opencode/src/cli/cmd/debug/agent.ts

@@ -1,6 +1,14 @@
 import { EOL } from "os"
 import { basename } from "path"
 import { Agent } from "../../../agent/agent"
+import { Provider } from "../../../provider/provider"
+import { Session } from "../../../session"
+import type { MessageV2 } from "../../../session/message-v2"
+import { Identifier } from "../../../id/id"
+import { ToolRegistry } from "../../../tool/registry"
+import { Instance } from "../../../project/instance"
+import { PermissionNext } from "../../../permission/next"
+import { iife } from "../../../util/iife"
 import { bootstrap } from "../../bootstrap"
 import { cmd } from "../cmd"
 
@@ -8,11 +16,20 @@ export const AgentCommand = cmd({
   command: "agent <name>",
   describe: "show agent configuration details",
   builder: (yargs) =>
-    yargs.positional("name", {
-      type: "string",
-      demandOption: true,
-      description: "Agent name",
-    }),
+    yargs
+      .positional("name", {
+        type: "string",
+        demandOption: true,
+        description: "Agent name",
+      })
+      .option("tool", {
+        type: "string",
+        description: "Tool id to execute",
+      })
+      .option("params", {
+        type: "string",
+        description: "Tool params as JSON or a JS object literal",
+      }),
   async handler(args) {
     await bootstrap(process.cwd(), async () => {
       const agentName = args.name as string
@@ -23,7 +40,127 @@ export const AgentCommand = cmd({
         )
         process.exit(1)
       }
-      process.stdout.write(JSON.stringify(agent, null, 2) + EOL)
+      const availableTools = await getAvailableTools(agent)
+      const resolvedTools = await resolveTools(agent, availableTools)
+      const toolID = args.tool as string | undefined
+      if (toolID) {
+        const tool = availableTools.find((item) => item.id === toolID)
+        if (!tool) {
+          process.stderr.write(`Tool ${toolID} not found for agent ${agentName}` + EOL)
+          process.exit(1)
+        }
+        if (resolvedTools[toolID] === false) {
+          process.stderr.write(`Tool ${toolID} is disabled for agent ${agentName}` + EOL)
+          process.exit(1)
+        }
+        const params = parseToolParams(args.params as string | undefined)
+        const ctx = await createToolContext(agent)
+        const result = await tool.execute(params, ctx)
+        process.stdout.write(JSON.stringify({ tool: toolID, input: params, result }, null, 2) + EOL)
+        return
+      }
+
+      const output = {
+        ...agent,
+        tools: resolvedTools,
+      }
+      process.stdout.write(JSON.stringify(output, null, 2) + EOL)
     })
   },
 })
+
+async function getAvailableTools(agent: Agent.Info) {
+  const providerID = agent.model?.providerID ?? (await Provider.defaultModel()).providerID
+  return ToolRegistry.tools(providerID, agent)
+}
+
+async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
+  const disabled = PermissionNext.disabled(
+    availableTools.map((tool) => tool.id),
+    agent.permission,
+  )
+  const resolved: Record<string, boolean> = {}
+  for (const tool of availableTools) {
+    resolved[tool.id] = !disabled.has(tool.id)
+  }
+  return resolved
+}
+
+function parseToolParams(input?: string) {
+  if (!input) return {}
+  const trimmed = input.trim()
+  if (trimmed.length === 0) return {}
+
+  const parsed = iife(() => {
+    try {
+      return JSON.parse(trimmed)
+    } catch (jsonError) {
+      try {
+        return new Function(`return (${trimmed})`)()
+      } catch (evalError) {
+        throw new Error(
+          `Failed to parse --params. Use JSON or a JS object literal. JSON error: ${jsonError}. Eval error: ${evalError}.`,
+        )
+      }
+    }
+  })
+
+  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
+    throw new Error("Tool params must be an object.")
+  }
+  return parsed as Record<string, unknown>
+}
+
+async function createToolContext(agent: Agent.Info) {
+  const session = await Session.create({ title: `Debug tool run (${agent.name})` })
+  const messageID = Identifier.ascending("message")
+  const model = agent.model ?? (await Provider.defaultModel())
+  const now = Date.now()
+  const message: MessageV2.Assistant = {
+    id: messageID,
+    sessionID: session.id,
+    role: "assistant",
+    time: {
+      created: now,
+    },
+    parentID: messageID,
+    modelID: model.modelID,
+    providerID: model.providerID,
+    mode: "debug",
+    agent: agent.name,
+    path: {
+      cwd: Instance.directory,
+      root: Instance.worktree,
+    },
+    cost: 0,
+    tokens: {
+      input: 0,
+      output: 0,
+      reasoning: 0,
+      cache: {
+        read: 0,
+        write: 0,
+      },
+    },
+  }
+  await Session.updateMessage(message)
+
+  const ruleset = PermissionNext.merge(agent.permission, session.permission ?? [])
+
+  return {
+    sessionID: session.id,
+    messageID,
+    callID: Identifier.ascending("part"),
+    agent: agent.name,
+    abort: new AbortController().signal,
+    metadata: () => {},
+    async ask(req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) {
+      for (const pattern of req.patterns) {
+        const rule = PermissionNext.evaluate(req.permission, pattern, ruleset)
+        if (rule.action === "deny") {
+          throw new PermissionNext.DeniedError(ruleset)
+        }
+      }
+    },
+  }
+}