Skip to content
Draft
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
108 changes: 108 additions & 0 deletions .changeset/ai-gateway-tools.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
---
"@tailor-platform/app-shell": minor
---

Add tool support to `useAIChat()` and the AI Gateway transport layer.

You can now register **local tools** and **provider tools** via the `tools` option on `useAIChat()`. Tool-call and tool-result protocol messages stay internal to the hook — the public `messages` array remains user/assistant text only.

## Local tools

Define tools that run inside AppShell using `defineAIChatTool`. The model decides when to call them, AppShell validates arguments with the schema, executes the handler, and feeds results back into the next model turn automatically.

```ts
import { defineAIChatTool, aiToolSchema } from "@tailor-platform/app-shell";

const lookupCustomer = defineAIChatTool({
description: "Look up a customer by ID and return their profile",
schema: aiToolSchema.object({
customerId: aiToolSchema.string({ description: "Customer ID" }),
includeInactive: aiToolSchema.optional(aiToolSchema.boolean()),
}),
async execute({ customerId, includeInactive }, { signal }) {
const res = await fetch(`/api/customers/${customerId}?inactive=${includeInactive ?? false}`, {
signal,
});
return res.json();
},
});
```

### Schema primitives (`aiToolSchema`)

| Helper | Description |
| --------------------------------------- | --------------------------------------------------------------- |
| `aiToolSchema.string(opts?)` | String input (supports `minLength`, `maxLength`, `description`) |
| `aiToolSchema.number(opts?)` | Numeric input (supports `minimum`, `maximum`, `integer`) |
| `aiToolSchema.boolean(opts?)` | Boolean input |
| `aiToolSchema.enum(values, opts?)` | Fixed set of string literals |
| `aiToolSchema.array(itemSchema, opts?)` | Array of a given schema |
| `aiToolSchema.object(shape)` | Object with named fields |
| `aiToolSchema.optional(schema)` | Marks a field as optional |

### Tool context

The `execute` function receives a second argument with:

- `signal` — the `AbortSignal` for the in-flight chat request
- `messages` — the public user/assistant message history at the time of execution

## Provider tools

Provider tools are not executed locally — they are passed through to the AI Gateway and handled upstream by the model provider.

```ts
import { aiProviderTool } from "@tailor-platform/app-shell";

const webSearch = aiProviderTool.openai.webSearch({
searchContextSize: "high",
userLocation: {
type: "approximate",
country: "JP",
city: "Tokyo",
timezone: "Asia/Tokyo",
},
filters: {
allowedDomains: ["nikkei.com", "reuters.com"],
},
});
```

## Registering tools with `useAIChat`

Pass tools as a record to the `tools` option. The key becomes the tool name sent to the model.

```tsx
import {
useAIChat,
defineAIChatTool,
aiToolSchema,
aiProviderTool,
} from "@tailor-platform/app-shell";

const lookupCustomer = defineAIChatTool({
description: "Look up a customer by ID",
schema: aiToolSchema.object({
customerId: aiToolSchema.string(),
}),
async execute({ customerId }) {
return { customerId, name: "Acme Corp", plan: "enterprise" };
},
});

function ChatPanel({ client }) {
const { messages, status, sendMessage } = useAIChat({
client,
model: "gpt-5-mini",
tools: {
lookupCustomer,
web_search: aiProviderTool.openai.webSearch({ searchContextSize: "high" }),
},
});

// messages only contains user/assistant text — tool calls are handled internally
// status transitions: "submitted" → "streaming" → ("submitted" during tool rounds) → "ready"
}
```

The hook runs up to 8 tool rounds per user message. If the model keeps requesting tools beyond that limit, the request fails with an error.
50 changes: 47 additions & 3 deletions docs/api/create-ai-gateway-client.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Create a low-level AI Gateway client that reuses AppShell authentic

# createAIGatewayClient

Creates a small AI Gateway transport client for text-only chat completions.
Creates a small AI Gateway transport client for chat completions, tool calls, and optional sources.

## Signature

Expand Down Expand Up @@ -41,7 +41,8 @@ interface AIGatewayClient {
The iterable yields completion events:

- `text-delta` — append `event.text` to build the assistant response
- `done` — terminal event with an optional `finishReason`
- `tool-call` — a local function tool call requested by the model
- `done` — terminal event with an optional `finishReason` and optional `sources`

## Related Types

Expand All @@ -54,11 +55,40 @@ type AIGatewayChatMessage =
| {
role: "assistant";
content?: string;
toolCalls?: AIGatewayToolCall[];
}
| {
role: "tool";
toolCallId: string;
content: string;
};

interface AIGatewayToolCall {
id: string;
name: string;
argumentsText: string;
}

type AIGatewayTool =
| {
type: "function";
function: {
name: string;
description?: string;
parameters: Record<string, unknown>;
};
}
| {
type: "provider";
provider: "openai";
name: "web_search";
options?: unknown;
};

interface AIGatewayChatRequest {
model: string;
messages: AIGatewayChatMessage[];
tools?: AIGatewayTool[];
signal?: AbortSignal;
}

Expand All @@ -67,10 +97,23 @@ type AIChatCompletionEvent =
type: "text-delta";
text: string;
}
| {
type: "tool-call";
toolCallId: string;
toolName: string;
argumentsText: string;
}
| {
type: "done";
finishReason?: string;
sources?: AIChatSource[];
};

interface AIChatSource {
type: "url";
url: string;
title?: string;
}
```

## Usage
Expand Down Expand Up @@ -104,7 +147,8 @@ console.log(text);
## Notes

- AppShell chooses the appropriate AI Gateway transport automatically
- The low-level API is intentionally narrow: text deltas plus completion metadata
- Local tools are sent as normalized `type: "function"` definitions; provider tools are passed through as normalized `type: "provider"` definitions
- The low-level API stays event-based so hooks can layer streaming UI and tool loops on top
- `request.signal` is passed through so callers can abort in-flight work

## Related
Expand Down
50 changes: 45 additions & 5 deletions docs/api/use-ai-chat.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
---
title: useAIChat
description: Simple text-only chat hook for AI Gateway
description: AI Gateway chat hook with optional local and provider tools
---

# useAIChat

React hook for simple text-only chat on top of `createAIGatewayClient`.
React hook for AI Gateway chat on top of `createAIGatewayClient`, with optional local and provider tool support.

## Signature

```typescript
const useAIChat: (config: { client: AIGatewayClient; model: string }) => {
const useAIChat: (config: {
client: AIGatewayClient;
model: string;
tools?: Record<string, AIChatConfiguredTool>;
}) => {
messages: AIChatMessage[];
status: "ready" | "submitted" | "streaming" | "error";
error?: Error;
Expand All @@ -28,6 +32,13 @@ interface AIChatMessage {
id: string;
role: "user" | "assistant";
content: string;
sources?: AIChatSource[];
}

interface AIChatSource {
type: "url";
url: string;
title?: string;
}
```

Expand All @@ -52,10 +63,24 @@ interface AIChatMessage {
- **Type:** `() => void`
- **Description:** Aborts the current request if one is in progress

### `tools`

Register tools under a single object:

- local tools created with `defineAIChatTool(...)`
- provider tools such as `aiProviderTool.openai.webSearch(...)`

## Usage

```tsx
import { createAuthClient, createAIGatewayClient, useAIChat } from "@tailor-platform/app-shell";
import {
aiProviderTool,
aiToolSchema,
createAuthClient,
createAIGatewayClient,
defineAIChatTool,
useAIChat,
} from "@tailor-platform/app-shell";

const authClient = createAuthClient({
clientId: "your-client-id",
Expand All @@ -67,10 +92,24 @@ const aiClient = createAIGatewayClient({
authClient,
});

const lookupCustomer = defineAIChatTool({
description: "Look up a customer in the current workspace",
schema: aiToolSchema.object({
customerId: aiToolSchema.string(),
}),
async execute({ customerId }) {
return { customerId, name: "Acme Corp" };
},
});

export function ChatScreen() {
const { messages, sendMessage, status, stop, error } = useAIChat({
client: aiClient,
model: "gpt-5-mini",
tools: {
lookupCustomer,
web_search: aiProviderTool.openai.webSearch({ searchContextSize: "high" }),
},
});

return (
Expand Down Expand Up @@ -99,7 +138,8 @@ export function ChatScreen() {
## Notes

- AppShell chooses the appropriate AI Gateway transport automatically
- The hook is intentionally text-only
- Public messages stay user/assistant text-first; internal tool messages remain private to the hook
- Provider tools can attach optional `sources` to assistant messages
- System prompts and custom history shaping should use the low-level client directly
- `stop()` keeps any already-streamed assistant text and ignores late chunks from the stopped request

Expand Down
Loading
Loading