diff --git a/packages/opencode/src/server/experimental.ts b/packages/opencode/src/server/experimental.ts new file mode 100644 index 00000000000..5be0753d834 --- /dev/null +++ b/packages/opencode/src/server/experimental.ts @@ -0,0 +1,94 @@ +import { Hono } from "hono" +import { describeRoute, validator, resolver } from "hono-openapi" +import { errors } from "./error" +import z from "zod" +import { Session } from "../session" +import { Agent } from "../agent/agent" +import { Storage } from "../storage/storage" +import { ToolRegistry } from "../tool/registry" +import { Tool } from "../tool/tool" +import { PermissionNext } from "@/permission/next" + +export const ExperimentalRoute = new Hono().post( + "/tool/execute", + describeRoute({ + summary: "Execute tool", + description: "Execute a specific tool with the provided arguments. Returns the tool output.", + operationId: "tool.execute", + responses: { + 200: { + description: "Tool execution result", + content: { + "application/json": { + schema: resolver( + z + .object({ + title: z.string(), + output: z.string(), + metadata: z.record(z.string(), z.any()).optional(), + }) + .meta({ ref: "ToolExecuteResult" }), + ), + }, + }, + }, + ...errors(400, 404), + }, + }), + validator( + "json", + z.object({ + sessionID: z.string().meta({ description: "Session ID for context" }), + messageID: z.string().meta({ description: "Message ID for context" }), + providerID: z.string().meta({ description: "Provider ID for tool filtering" }), + toolID: z.string().meta({ description: "Tool ID to execute" }), + args: z.record(z.string(), z.any()).meta({ description: "Tool arguments" }), + agent: z.string().optional().meta({ description: "Agent name (optional)" }), + callID: z.string().optional().meta({ description: "Tool call ID (optional)" }), + }), + ), + async (c) => { + const body = c.req.valid("json") + const session = await Session.get(body.sessionID) + const agentName = body.agent ?? (await Agent.defaultAgent()) + const agent = await Agent.get(agentName) + if (!agent) { + throw new Storage.NotFoundError({ message: `Agent not found: ${agentName}` }) + } + + const tools = await ToolRegistry.tools(body.providerID, agent) + const tool = tools.find((t) => t.id === body.toolID) + if (!tool) { + throw new Storage.NotFoundError({ message: `Tool not found: ${body.toolID}` }) + } + + const abortController = new AbortController() + let currentMetadata: { title?: string; metadata?: Record } = {} + + const ctx: Tool.Context = { + sessionID: body.sessionID, + messageID: body.messageID, + agent: agentName, + abort: abortController.signal, + callID: body.callID, + metadata: (input) => { + currentMetadata = input + }, + ask: async (req) => { + await PermissionNext.ask({ + ...req, + sessionID: session.id, + tool: body.callID ? { messageID: body.messageID, callID: body.callID } : undefined, + ruleset: PermissionNext.merge(agent.permission, session.permission ?? []), + }) + }, + } + + const result = await tool.execute(body.args, ctx) + return c.json({ + title: result.title || currentMetadata.title || "", + output: result.output, + metadata: result.metadata, + }) + }, +) diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 52457515b8e..794215cc974 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -31,6 +31,7 @@ import { Command } from "../command" import { ProviderAuth } from "../provider/auth" import { Global } from "../global" import { ProjectRoute } from "./project" +import { ExperimentalRoute } from "./experimental" import { ToolRegistry } from "../tool/registry" import { zodToJsonSchema } from "zod-to-json-schema" import { SessionPrompt } from "../session/prompt" @@ -75,6 +76,7 @@ export namespace Server { } const app = new Hono() + app.route("/experimental", ExperimentalRoute) export const App: () => Hono = lazy( () => // TODO: Break server.ts into smaller route files to fix type inference diff --git a/packages/sdk/js/src/gen/sdk.gen.ts b/packages/sdk/js/src/gen/sdk.gen.ts index 5e3e67e1c03..b5c94e8e576 100644 --- a/packages/sdk/js/src/gen/sdk.gen.ts +++ b/packages/sdk/js/src/gen/sdk.gen.ts @@ -36,6 +36,9 @@ import type { ToolListData, ToolListResponses, ToolListErrors, + ToolExecuteData, + ToolExecuteResponses, + ToolExecuteErrors, InstanceDisposeData, InstanceDisposeResponses, PathGetData, @@ -390,6 +393,17 @@ class Tool extends _HeyApiClient { ...options, }) } + + public execute(options?: Options) { + return (options?.client ?? this._client).post({ + url: "/experimental/tool/execute", + ...options, + headers: { + "Content-Type": "application/json", + ...options?.headers, + }, + }) + } } class Instance extends _HeyApiClient { diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 32f33f66219..3c535a1c0bc 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -1996,6 +1996,52 @@ export type ToolListResponses = { export type ToolListResponse = ToolListResponses[keyof ToolListResponses] +export type ToolExecuteData = { + body?: { + agent?: string + args?: { + [key: string]: unknown + } + messageID?: string + providerID?: string + sessionID?: string + toolID?: string + } + path?: never + query?: { + directory?: string + } + url: "/experimental/tool/execute" +} + +export type ToolExecuteErrors = { + /** + * Bad request + */ + 400: string + /** + * Tool not found + */ + 404: string + /** + * Tool execution failed + */ + 500: string +} + +export type ToolExecuteError = ToolExecuteErrors[keyof ToolExecuteErrors] + +export type ToolExecuteResponses = { + /** + * Tool execution result + */ + 200: { + output?: string + } +} + +export type ToolExecuteResponse = ToolExecuteResponses[keyof ToolExecuteResponses] + export type InstanceDisposeData = { body?: never path?: never diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index f83913ea5e1..2f9c69c9058 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -136,6 +136,8 @@ import type { SessionUpdateResponses, SubtaskPartInput, TextPartInput, + ToolExecuteErrors, + ToolExecuteResponses, ToolIdsErrors, ToolIdsResponses, ToolListErrors, @@ -651,6 +653,55 @@ export class Tool extends HeyApiClient { ...params, }) } + + /** + * Execute tool + * + * Execute a specific tool with the provided arguments. Returns the tool output. + */ + public execute( + parameters?: { + directory?: string + sessionID?: string + messageID?: string + providerID?: string + toolID?: string + args?: { + [key: string]: unknown + } + agent?: string + callID?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "body", key: "sessionID" }, + { in: "body", key: "messageID" }, + { in: "body", key: "providerID" }, + { in: "body", key: "toolID" }, + { in: "body", key: "args" }, + { in: "body", key: "agent" }, + { in: "body", key: "callID" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post({ + url: "/experimental/tool/execute", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }) + } } export class Instance extends HeyApiClient { diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index e423fecea42..f5976a27fac 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1767,6 +1767,14 @@ export type ToolListItem = { export type ToolList = Array +export type ToolExecuteResult = { + title: string + output: string + metadata?: { + [key: string]: unknown + } +} + export type Path = { home: string state: string @@ -2481,6 +2489,68 @@ export type ToolListResponses = { export type ToolListResponse = ToolListResponses[keyof ToolListResponses] +export type ToolExecuteData = { + body?: { + /** + * Session ID for context + */ + sessionID: string + /** + * Message ID for context + */ + messageID: string + /** + * Provider ID for tool filtering + */ + providerID: string + /** + * Tool ID to execute + */ + toolID: string + /** + * Tool arguments + */ + args: { + [key: string]: unknown + } + /** + * Agent name (optional) + */ + agent?: string + /** + * Tool call ID (optional) + */ + callID?: string + } + path?: never + query?: { + directory?: string + } + url: "/experimental/tool/execute" +} + +export type ToolExecuteErrors = { + /** + * Bad request + */ + 400: BadRequestError + /** + * Not found + */ + 404: NotFoundError +} + +export type ToolExecuteError = ToolExecuteErrors[keyof ToolExecuteErrors] + +export type ToolExecuteResponses = { + /** + * Tool execution result + */ + 200: ToolExecuteResult +} + +export type ToolExecuteResponse = ToolExecuteResponses[keyof ToolExecuteResponses] + export type InstanceDisposeData = { body?: never path?: never