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
5 changes: 5 additions & 0 deletions .changeset/server-tool-executor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@openuidev/server": minor
---

Add provider-independent tool registration through `openUIClient.tools.create`, exposing generation definitions and an executor from one registry. Execute direct server tools or scripts from complete OpenUI responses using request-scoped context, cancellation, a total deadline, and bounded sandbox continuations without replaying tools after snapshot failures. Keep `openUIClient.tools.execute` for existing handler maps.
2 changes: 1 addition & 1 deletion docs/content/docs/api-reference/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The OpenUI SDK is split into packages that build on each other:

- **`@openuidev/langchain`** — LangChain and LangGraph integration primitives for transforming protocol-v2 agent streams into AG-UI events and relaying them to an OpenUI frontend over Web-standard Server-Sent Events.

- **`@openuidev/server`** — Server client for OpenUI Gateway. Fix invalid OpenUI Lang and persist Chat Completions turns. Import `createClient` from the package root.
- **`@openuidev/server`** — Server client for OpenUI Gateway. Fix invalid OpenUI Lang, persist Chat Completions turns, and execute customer tools or sandbox scripts. Import `createClient` from the package root.

- **`@openuidev/react-email`** — Pre-built email component library and prompt options for model-generated emails that can be rendered to HTML with React Email.

Expand Down
115 changes: 113 additions & 2 deletions docs/content/docs/api-reference/server.mdx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
title: "@openuidev/server"
description: A shared server client for auto-fix and conversation history persistence on the OpenUI gateway.
description: A shared server client for auto-fix, tool and script execution, and conversation history persistence on the OpenUI gateway.
---

Use `createClient` to fix invalid OpenUI Lang and save completed Chat Completions turns to conversation history on the OpenUI Gateway.
Use `createClient` to fix invalid OpenUI Lang, execute tools and generated scripts, and save completed Chat Completions turns to conversation history on the OpenUI Gateway.

## Install

Expand Down Expand Up @@ -116,3 +116,114 @@ await openUIClient.openai.completions.conversations.appendMessages({
```

Accepts Chat Completions messages only and returns a `ConversationItemList`. It converts user/assistant messages and function calls/results into Conversations API items; system/developer instructions are skipped. An empty converted turn makes no request. The conversation must already exist. Append only the new turn to avoid duplicate items; writes are not retried automatically.

## Execute tools and scripts

Register server tools once. The same registry provides definitions for generation and handlers for execution:

```ts
type AppContext = { userId: string };

const appTools = openUIClient.tools.create<AppContext>({
get_orders: {
description: "Read orders for a selected year.",
parameters: {
type: "object",
properties: { year: { type: "integer" } },
required: ["year"],
},
output: { orders: [{ region: "West", revenue: 120 }] },
execute: (args, { context, signal }) => readOrders(args, { userId: context.userId, signal }),
},
});
```

`readOrders` is your application's data function. Each registration requires JSON Schema `parameters` and an `execute` handler. `description` and illustrative `output` are optional. Handlers receive arguments as `Record<string, unknown>`; they validate inputs and authorize access to actual data. The example output describes a possible result for generation, not the data returned at runtime.

### Use the definitions for generation

```ts
import { generateSystemPrompt } from "@openuidev/lang-core";
import library from "./openui.spec.json";

const systemPrompt = generateSystemPrompt({
cloud: true,
library,
script: { tools: appTools.definitions },
});
```

`definitions` contains only `{ name, description?, parameters, output? }`. Handlers, request context and client credentials are excluded. Metadata must be JSON serializable. You can create separate registries for different MiniApp libraries using the same client.

### Execute a direct tool or a generated script

```ts
// A registered name runs directly on your server; no response bundle is needed.
const orders = await appTools.execute(
{ name: "get_orders", arguments: { year: 2026 } },
{ context: { userId: authenticatedUser.id }, signal: request.signal },
);

// A generated script can call those same registered tools.
const summary = await appTools.execute(
{ name: "sales_summary", arguments: { year: 2026 }, response: generatedOpenUIBundle },
{ context: { userId: authenticatedUser.id }, signal: request.signal, timeoutMs: 60_000 },
);
```

`execute` returns the final value. Registered names take precedence over scripts with the same name. Other names require the complete generated `response`, including its scripts; no parsing or splitting is needed. Scripts execute through `/v1/app/execute` in the Gateway sandbox. The helper runs any requested registered tools on your server and resumes the script until it finishes. Unknown tools or scripts produce errors.

Context, cancellation and the deadline belong to each call. A typed context is required when your registry declares one. For tools that need no context, omit the generic and call `appTools.execute({ name, arguments, response })` without a second argument. Client creation still requires a Gateway API key.

### Connect an execution route

Keep your registry in a server module. Derive context from authenticated server state and select only execution input from the body:

```ts
import { ServerClientError } from "@openuidev/server";
import { requireAppContext } from "./auth";
import { appTools } from "./app-tools";

export async function POST(request: Request) {
const context = await requireAppContext(request);
const { name, arguments: args, response } = await request.json();

try {
const result = await appTools.execute(
{ name, arguments: args, response },
{ context, signal: request.signal },
);
return Response.json({ result });
} catch (error) {
if (request.signal.aborted) throw error;
const code = error instanceof ServerClientError ? error.code : "tool_exec_error";
const status = error instanceof ServerClientError ? (error.status ?? 500) : 500;
return Response.json({ error: { code, message: "Tool execution failed." } }, { status });
}
}
```

Export `appTools` from the registration module. `requireAppContext` is your authentication helper; retain your application's normal authentication and error handling. The request body cannot supply the registry, trusted context or execution settings. If you store MiniApps by ID, authorize and load the selected response on your server, then pass that full response to `execute`.

Your Renderer's `toolProvider.callTool` can handle browser-only actions explicitly and forward other names to this route. Browser-only handlers are not available to generated scripts; register script-accessible tools on the server.

### Use an existing handler map

If you already manage tool metadata elsewhere, keep the one-shot form:

```ts
const result = await openUIClient.tools.execute({
name: "sales_summary",
arguments: { year: 2026 },
tools: {
get_orders: (args, { context, signal }) => readOrders(args, { userId: context.userId, signal }),
},
response: generatedOpenUIBundle,
context: { userId: authenticatedUser.id },
signal: request.signal,
});
```

Both forms use the same execution loop. Tools requested together run concurrently; dependent operations belong in successive script calls. Runs are limited to eight tool-call rounds and sixteen customer-tool calls. The total deadline defaults to 60 seconds, including tools. Cancellation propagates to requests and handlers; work that ignores it is no longer awaited. Tool failures are returned to the script, and snapshot failures never automatically replay previously executed tools.

The root exports `ToolDefinition`, `ToolRegistration`, `RegisteredTools`, `ToolExecutionRequest`, `ToolExecutionOptions`, `ExecuteToolInput`, and the handler type `ToolExecutor`.
2 changes: 1 addition & 1 deletion docs/content/docs/autofix.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Use either `chunks` or `toResponse()` once. `result` settles after consumption.
| `toResponse()` | SSE `Response` for the matching frontend. |
| `result` | Settled Autofix result. Persist `content` when present, rather than joined text. |

`result` is `null` when Autofix validation did not run. A failed streamed fix throws with `code: "fix_failed"`. See the [server client reference](/docs/api-reference/server) for configuration and conversation persistence.
`result` is `null` when Autofix validation did not run. A failed streamed fix throws with `code: "fix_failed"`. See the [server client reference](/docs/api-reference/server) for configuration, persistence, and tool execution.

## Call the API directly

Expand Down
113 changes: 112 additions & 1 deletion packages/server/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# `@openuidev/server`

Use `createClient` to fix invalid OpenUI Lang and save completed Chat Completions turns to conversation history on the OpenUI Gateway.
Use `createClient` to fix invalid OpenUI Lang, execute tools and generated scripts, and save completed Chat Completions turns to conversation history on the OpenUI Gateway.

## Install

Expand Down Expand Up @@ -113,3 +113,114 @@ await openUIClient.openai.completions.conversations.appendMessages({
```

Accepts Chat Completions messages only and returns a `ConversationItemList`. It converts user/assistant messages and function calls/results into Conversations API items; system/developer instructions are skipped. An empty converted turn makes no request. The conversation must already exist. Append only the new turn to avoid duplicate items; writes are not retried automatically.

## Execute tools and scripts

Register server tools once. The same registry provides definitions for generation and handlers for execution:

```ts
type AppContext = { userId: string };

const appTools = openUIClient.tools.create<AppContext>({
get_orders: {
description: "Read orders for a selected year.",
parameters: {
type: "object",
properties: { year: { type: "integer" } },
required: ["year"],
},
output: { orders: [{ region: "West", revenue: 120 }] },
execute: (args, { context, signal }) => readOrders(args, { userId: context.userId, signal }),
},
});
```

`readOrders` is your application's data function. Each registration requires JSON Schema `parameters` and an `execute` handler. `description` and illustrative `output` are optional. Handlers receive arguments as `Record<string, unknown>`; they validate inputs and authorize access to actual data. The example output describes a possible result for generation, not the data returned at runtime.

### Use the definitions for generation

```ts
import { generateSystemPrompt } from "@openuidev/lang-core";
import library from "./openui.spec.json";

const systemPrompt = generateSystemPrompt({
cloud: true,
library,
script: { tools: appTools.definitions },
});
```

`definitions` contains only `{ name, description?, parameters, output? }`. Handlers, request context and client credentials are excluded. Metadata must be JSON serializable. You can create separate registries for different MiniApp libraries using the same client.

### Execute a direct tool or a generated script

```ts
// A registered name runs directly on your server; no response bundle is needed.
const orders = await appTools.execute(
{ name: "get_orders", arguments: { year: 2026 } },
{ context: { userId: authenticatedUser.id }, signal: request.signal },
);

// A generated script can call those same registered tools.
const summary = await appTools.execute(
{ name: "sales_summary", arguments: { year: 2026 }, response: generatedOpenUIBundle },
{ context: { userId: authenticatedUser.id }, signal: request.signal, timeoutMs: 60_000 },
);
```

`execute` returns the final value. Registered names take precedence over scripts with the same name. Other names require the complete generated `response`, including its scripts; no parsing or splitting is needed. Scripts execute through `/v1/app/execute` in the Gateway sandbox. The helper runs any requested registered tools on your server and resumes the script until it finishes. Unknown tools or scripts produce errors.

Context, cancellation and the deadline belong to each call. A typed context is required when your registry declares one. For tools that need no context, omit the generic and call `appTools.execute({ name, arguments, response })` without a second argument. Client creation still requires a Gateway API key.

### Connect an execution route

Keep your registry in a server module. Derive context from authenticated server state and select only execution input from the body:

```ts
import { ServerClientError } from "@openuidev/server";
import { requireAppContext } from "./auth";
import { appTools } from "./app-tools";

export async function POST(request: Request) {
const context = await requireAppContext(request);
const { name, arguments: args, response } = await request.json();

try {
const result = await appTools.execute(
{ name, arguments: args, response },
{ context, signal: request.signal },
);
return Response.json({ result });
} catch (error) {
if (request.signal.aborted) throw error;
const code = error instanceof ServerClientError ? error.code : "tool_exec_error";
const status = error instanceof ServerClientError ? (error.status ?? 500) : 500;
return Response.json({ error: { code, message: "Tool execution failed." } }, { status });
}
}
```

Export `appTools` from the registration module. `requireAppContext` is your authentication helper; retain your application's normal authentication and error handling. The request body cannot supply the registry, trusted context or execution settings. If you store MiniApps by ID, authorize and load the selected response on your server, then pass that full response to `execute`.

Your Renderer's `toolProvider.callTool` can handle browser-only actions explicitly and forward other names to this route. Browser-only handlers are not available to generated scripts; register script-accessible tools on the server.

### Use an existing handler map

If you already manage tool metadata elsewhere, keep the one-shot form:

```ts
const result = await openUIClient.tools.execute({
name: "sales_summary",
arguments: { year: 2026 },
tools: {
get_orders: (args, { context, signal }) => readOrders(args, { userId: context.userId, signal }),
},
response: generatedOpenUIBundle,
context: { userId: authenticatedUser.id },
signal: request.signal,
});
```

Both forms use the same execution loop. Tools requested together run concurrently; dependent operations belong in successive script calls. Runs are limited to eight tool-call rounds and sixteen customer-tool calls. The total deadline defaults to 60 seconds, including tools. Cancellation propagates to requests and handlers; work that ignores it is no longer awaited. Tool failures are returned to the script, and snapshot failures never automatically replay previously executed tools.

The root exports `ToolDefinition`, `ToolRegistration`, `RegisteredTools`, `ToolExecutionRequest`, `ToolExecutionOptions`, `ExecuteToolInput`, and the handler type `ToolExecutor`.
12 changes: 12 additions & 0 deletions packages/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { createCompletionsConversations } from "./openai/client";
import { openAIResponsesAdapter } from "./openai/responses-adapter";
import { resolveClientOptions, type ClientOptions } from "./shared/client";
import { createClientAutofix, createClientFix } from "./shared/client-autofix";
import { createTools } from "./tools/execute";
import { vercelAIAdapter } from "./vercel/adapter";
import { eveStreamAdapter } from "./vercel/eve-adapter";

Expand All @@ -18,6 +19,7 @@ export type { AutofixResult, AutofixStream } from "./shared/types";
export function createClient(options: ClientOptions = {}) {
const config = resolveClientOptions(options);
return {
tools: createTools(config),
autofix: createClientFix(config),
openai: {
completions: {
Expand All @@ -42,3 +44,13 @@ export type Client = ReturnType<typeof createClient>;

export type { MessageStreamEvent as EveStreamEvent } from "eve/client";
export type { LangGraphStreamEvent } from "./langchain/types";

export type {
ExecuteToolInput,
RegisteredTools,
ToolDefinition,
ToolExecutionOptions,
ToolExecutionRequest,
ToolExecutor,
ToolRegistration,
} from "./tools/types";
Loading