📦 deps(thirdparty): update snapshots
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
---
|
||||
name: ai-sdk
|
||||
description: This skill should be used when building AI features with Vercel AI SDK, using useChat, streamText, or generateObject, or when "AI SDK", "streaming chat", or "structured outputs" are mentioned.
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Vercel AI SDK v6
|
||||
|
||||
Patterns for building AI-powered applications with the Vercel AI SDK v6.
|
||||
|
||||
<when_to_use>
|
||||
|
||||
- Building streaming chat UIs
|
||||
- Structured JSON outputs with Zod schemas
|
||||
- Multi-step agent workflows with tools
|
||||
- Tool approval flows (human-in-the-loop)
|
||||
- Next.js App Router integrations
|
||||
|
||||
</when_to_use>
|
||||
|
||||
## Version Guard
|
||||
|
||||
Target **AI SDK 6.x** APIs. Default packages:
|
||||
|
||||
```
|
||||
ai@^6
|
||||
@ai-sdk/react@^2
|
||||
@ai-sdk/openai@^2 (or @ai-sdk/anthropic, etc.)
|
||||
zod@^3
|
||||
```
|
||||
|
||||
**Avoid v4/v5 holdovers:**
|
||||
- `StreamingTextResponse` → use `result.toUIMessageStreamResponse()`
|
||||
- Legacy `Message` shape → use `UIMessage`
|
||||
- Input-managed `useChat` → use transport-based pattern
|
||||
|
||||
## Core Concepts
|
||||
|
||||
### Message Types
|
||||
|
||||
| Type | Purpose | When to Use |
|
||||
|------|---------|-------------|
|
||||
| `UIMessage` | User-facing, persistence | Store in database, render in UI |
|
||||
| `ModelMessage` | LLM-compatible | Convert at call sites only |
|
||||
|
||||
**Rule:** Persist `UIMessage[]`. Convert to `ModelMessage[]` only when calling the model.
|
||||
|
||||
### Streaming Patterns
|
||||
|
||||
| Function | Use Case |
|
||||
|----------|----------|
|
||||
| `streamText` | Streaming text responses |
|
||||
| `generateText` | Non-streaming text |
|
||||
| `streamObject` | Streaming JSON with partial updates |
|
||||
| `generateObject` | Non-streaming JSON |
|
||||
| `ToolLoopAgent` | Multi-step agent with tools |
|
||||
|
||||
## Golden Path: Streaming Chat
|
||||
|
||||
### API Route (App Router)
|
||||
|
||||
```typescript
|
||||
// app/api/chat/route.ts
|
||||
import { openai } from '@ai-sdk/openai';
|
||||
import { streamText, convertToModelMessages, type UIMessage } from 'ai';
|
||||
|
||||
export const maxDuration = 30;
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { messages }: { messages: UIMessage[] } = await req.json();
|
||||
|
||||
const result = streamText({
|
||||
model: openai('gpt-4o-mini'),
|
||||
messages: convertToModelMessages(messages),
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse({
|
||||
originalMessages: messages,
|
||||
getErrorMessage: (e) =>
|
||||
e instanceof Error ? e.message : 'An error occurred',
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Client Hook
|
||||
|
||||
```tsx
|
||||
'use client';
|
||||
import { useState } from 'react';
|
||||
import { useChat, type UIMessage } from '@ai-sdk/react';
|
||||
import { DefaultChatTransport } from 'ai';
|
||||
|
||||
export function Chat({ initialMessages = [] }: { initialMessages?: UIMessage[] }) {
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
const { messages, sendMessage, status, error } = useChat({
|
||||
messages: initialMessages,
|
||||
transport: new DefaultChatTransport({ api: '/api/chat' }),
|
||||
});
|
||||
|
||||
const submit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (input.trim()) {
|
||||
sendMessage({ role: 'user', content: [{ type: 'text', text: input }] });
|
||||
setInput('');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{messages.map((m) => (
|
||||
<div key={m.id}>
|
||||
<b>{m.role === 'user' ? 'You' : 'AI'}:</b>
|
||||
{m.parts.map((p, i) => (p.type === 'text' ? <span key={i}>{p.text}</span> : null))}
|
||||
</div>
|
||||
))}
|
||||
{status === 'error' && <div className="text-red-600">{error?.message}</div>}
|
||||
<form onSubmit={submit}>
|
||||
<input value={input} onChange={(e) => setInput(e.target.value)} />
|
||||
<button type="submit">Send</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Structured Outputs
|
||||
|
||||
```typescript
|
||||
import { generateObject, streamObject } from 'ai';
|
||||
import { openai } from '@ai-sdk/openai';
|
||||
import { z } from 'zod';
|
||||
|
||||
const schema = z.object({
|
||||
recipe: z.object({
|
||||
name: z.string(),
|
||||
ingredients: z.array(z.string()),
|
||||
steps: z.array(z.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
// One-shot JSON
|
||||
const { object } = await generateObject({
|
||||
model: openai('gpt-4o'),
|
||||
schema,
|
||||
prompt: 'Generate a lasagna recipe.',
|
||||
});
|
||||
|
||||
// Streaming JSON (partial updates)
|
||||
const { partialObjectStream } = streamObject({
|
||||
model: openai('gpt-4o'),
|
||||
schema,
|
||||
prompt: 'Generate a lasagna recipe.',
|
||||
});
|
||||
|
||||
for await (const partial of partialObjectStream) {
|
||||
// Render progressively
|
||||
}
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
### Server-Side Tool Definition
|
||||
|
||||
```typescript
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
const searchTool = tool({
|
||||
description: 'Search product catalog',
|
||||
inputSchema: z.object({ query: z.string() }),
|
||||
execute: async ({ query }) => {
|
||||
// Implementation
|
||||
return [{ id: 'p1', name: 'Example Product' }];
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Multi-Step Tool Loops
|
||||
|
||||
```typescript
|
||||
import { streamText, stepCountIs } from 'ai';
|
||||
|
||||
const result = streamText({
|
||||
model: openai('gpt-4o'),
|
||||
messages: convertToModelMessages(messages),
|
||||
tools: { search: searchTool },
|
||||
stopWhen: stepCountIs(6), // Max 6 iterations
|
||||
prepareStep: async ({ stepNumber, messages }) =>
|
||||
messages.length > 10 ? { messages: messages.slice(-10) } : {},
|
||||
});
|
||||
|
||||
return result.toUIMessageStreamResponse();
|
||||
```
|
||||
|
||||
## v6: ToolLoopAgent
|
||||
|
||||
First-class agent abstraction for autonomous multi-step workflows.
|
||||
|
||||
### Basic Agent
|
||||
|
||||
```typescript
|
||||
import { ToolLoopAgent, stepCountIs } from 'ai';
|
||||
|
||||
const agent = new ToolLoopAgent({
|
||||
model: 'anthropic/claude-sonnet-4.5',
|
||||
instructions: 'You are a helpful research assistant.',
|
||||
tools: {
|
||||
search: searchTool,
|
||||
calculator: calculatorTool,
|
||||
},
|
||||
stopWhen: stepCountIs(5),
|
||||
});
|
||||
|
||||
// Non-streaming
|
||||
const result = await agent.generate({
|
||||
prompt: 'What is the weather in NYC?',
|
||||
});
|
||||
console.log(result.text);
|
||||
console.log(result.steps); // All steps taken
|
||||
|
||||
// Streaming
|
||||
const stream = agent.stream({ prompt: 'Research quantum computing.' });
|
||||
for await (const chunk of stream.textStream) {
|
||||
process.stdout.write(chunk);
|
||||
}
|
||||
```
|
||||
|
||||
### Agent with UI Streaming
|
||||
|
||||
```typescript
|
||||
import { ToolLoopAgent, createAgentUIStream } from 'ai';
|
||||
|
||||
const agent = new ToolLoopAgent({ model, instructions, tools });
|
||||
|
||||
const stream = await createAgentUIStream({
|
||||
agent,
|
||||
messages: [{ role: 'user', content: 'What is the weather?' }],
|
||||
});
|
||||
|
||||
for await (const chunk of stream) {
|
||||
// UI message chunks
|
||||
}
|
||||
```
|
||||
|
||||
## v6: Tool Approval (Human-in-the-Loop)
|
||||
|
||||
### Static Approval (Always Require)
|
||||
|
||||
```typescript
|
||||
const dangerousTool = tool({
|
||||
description: 'Delete user data',
|
||||
inputSchema: z.object({ userId: z.string() }),
|
||||
needsApproval: true, // Always require approval
|
||||
execute: async ({ userId }) => {
|
||||
return await deleteUserData(userId);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Dynamic Approval (Conditional)
|
||||
|
||||
```typescript
|
||||
const paymentTool = tool({
|
||||
description: 'Process payment',
|
||||
inputSchema: z.object({
|
||||
amount: z.number(),
|
||||
recipient: z.string(),
|
||||
}),
|
||||
needsApproval: async ({ amount }) => amount > 1000, // Only large transactions
|
||||
execute: async ({ amount, recipient }) => {
|
||||
return await processPayment(amount, recipient);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Client-Side Approval UI
|
||||
|
||||
```tsx
|
||||
function ToolApprovalView({ invocation, addToolApprovalResponse }) {
|
||||
if (invocation.state === 'approval-requested') {
|
||||
return (
|
||||
<div>
|
||||
<p>Approve action: {invocation.input.description}?</p>
|
||||
<button
|
||||
onClick={() =>
|
||||
addToolApprovalResponse({ id: invocation.approval.id, approved: true })
|
||||
}
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
addToolApprovalResponse({ id: invocation.approval.id, approved: false })
|
||||
}
|
||||
>
|
||||
Deny
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (invocation.state === 'output-available') {
|
||||
return <div>Result: {JSON.stringify(invocation.output)}</div>;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
## Persistence
|
||||
|
||||
```typescript
|
||||
return result.toUIMessageStreamResponse({
|
||||
originalMessages: messages,
|
||||
generateMessageId: createIdGenerator({ prefix: 'msg', size: 16 }),
|
||||
onFinish: async ({ messages: complete }) => {
|
||||
await saveChat({ chatId, messages: complete }); // Persist UIMessage[]
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
```typescript
|
||||
// Server: Surface errors to client
|
||||
return result.toUIMessageStreamResponse({
|
||||
getErrorMessage: (e) =>
|
||||
e instanceof Error ? e.message : typeof e === 'string' ? e : JSON.stringify(e),
|
||||
});
|
||||
|
||||
// Client: Handle error state
|
||||
const { status, error } = useChat({ ... });
|
||||
if (status === 'error') {
|
||||
return <div>Error: {error?.message}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Avoid | Use Instead |
|
||||
|-------|-------------|
|
||||
| `StreamingTextResponse` | `result.toUIMessageStreamResponse()` |
|
||||
| Persisting `ModelMessage` | Persist `UIMessage[]` |
|
||||
| Unbounded tool loops | `stopWhen: stepCountIs(N)` |
|
||||
| Client-only state for long sessions | Add persistence + resumable streams |
|
||||
| `any` types | Zod schemas + typed `UIMessage` |
|
||||
|
||||
<references>
|
||||
|
||||
- [agents.md](references/agents.md) - ToolLoopAgent patterns and workflows
|
||||
- [tool-approval.md](references/tool-approval.md) - Human-in-the-loop approval flows
|
||||
- [persistence.md](references/persistence.md) - Chat persistence strategies
|
||||
|
||||
</references>
|
||||
Reference in New Issue
Block a user