Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions packages/opencode/src/server/experimental.ts
Original file line number Diff line number Diff line change
@@ -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<string, any> } = {}

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,
})
},
)
2 changes: 2 additions & 0 deletions packages/opencode/src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions packages/sdk/js/src/gen/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ import type {
ToolListData,
ToolListResponses,
ToolListErrors,
ToolExecuteData,
ToolExecuteResponses,
ToolExecuteErrors,
InstanceDisposeData,
InstanceDisposeResponses,
PathGetData,
Expand Down Expand Up @@ -390,6 +393,17 @@ class Tool extends _HeyApiClient {
...options,
})
}

public execute<ThrowOnError extends boolean = false>(options?: Options<ToolExecuteData, ThrowOnError>) {
return (options?.client ?? this._client).post<ToolExecuteResponses, ToolExecuteErrors, ThrowOnError>({
url: "/experimental/tool/execute",
...options,
headers: {
"Content-Type": "application/json",
...options?.headers,
},
})
}
}

class Instance extends _HeyApiClient {
Expand Down
46 changes: 46 additions & 0 deletions packages/sdk/js/src/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions packages/sdk/js/src/v2/gen/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ import type {
SessionUpdateResponses,
SubtaskPartInput,
TextPartInput,
ToolExecuteErrors,
ToolExecuteResponses,
ToolIdsErrors,
ToolIdsResponses,
ToolListErrors,
Expand Down Expand Up @@ -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<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
sessionID?: string
messageID?: string
providerID?: string
toolID?: string
args?: {
[key: string]: unknown
}
agent?: string
callID?: string
},
options?: Options<never, ThrowOnError>,
) {
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<ToolExecuteResponses, ToolExecuteErrors, ThrowOnError>({
url: "/experimental/tool/execute",
...options,
...params,
headers: {
"Content-Type": "application/json",
...options?.headers,
...params.headers,
},
})
}
}

export class Instance extends HeyApiClient {
Expand Down
70 changes: 70 additions & 0 deletions packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1767,6 +1767,14 @@ export type ToolListItem = {

export type ToolList = Array<ToolListItem>

export type ToolExecuteResult = {
title: string
output: string
metadata?: {
[key: string]: unknown
}
}

export type Path = {
home: string
state: string
Expand Down Expand Up @@ -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
Expand Down