📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-05-29 08:33:53 +00:00
parent fdb52f1e96
commit 06e0d13d57
1615 changed files with 232858 additions and 0 deletions
@@ -0,0 +1,184 @@
# ToolLoopAgent Patterns
Deep dive on v6 agent workflows.
## When to Use ToolLoopAgent
| Use Case | Approach |
|----------|----------|
| Single tool call | `streamText` with tools |
| Multi-step reasoning | `ToolLoopAgent` |
| Autonomous workflows | `ToolLoopAgent` with `stopWhen` |
| Complex orchestration | `ToolLoopAgent` with custom stop conditions |
## Agent Configuration
```typescript
import { ToolLoopAgent, stepCountIs } from 'ai';
const agent = new ToolLoopAgent({
// Required
model: 'anthropic/claude-sonnet-4.5',
// Optional
instructions: 'You are a research assistant.',
tools: { search, calculate, summarize },
// Stop conditions
stopWhen: stepCountIs(10),
// Or custom: async ({ steps }) => steps.length >= 10
// Tool selection
toolChoice: 'auto', // 'auto' | 'required' | 'none'
// Token limits
maxOutputTokens: 4096,
});
```
## Execution Patterns
### Non-Streaming (Simple)
```typescript
const result = await agent.generate({
prompt: 'Research quantum computing breakthroughs.',
});
console.log(result.text);
console.log(result.steps); // Array of all steps
console.log(result.steps.length, 'steps executed');
```
### Streaming (Real-time)
```typescript
const stream = agent.stream({
prompt: 'Analyze this data and provide insights.',
});
for await (const chunk of stream.textStream) {
process.stdout.write(chunk);
}
```
### UI Streaming (React/Next.js)
```typescript
import { createAgentUIStream } from 'ai';
const stream = await createAgentUIStream({
agent,
messages: [{ role: 'user', content: 'What is the weather?' }],
abortSignal: controller.signal,
});
for await (const chunk of stream) {
// Yield to client
}
```
## Stop Conditions
### Built-in: Step Count
```typescript
import { stepCountIs } from 'ai';
stopWhen: stepCountIs(5) // Stop after 5 steps
```
### Custom: Finish Reason
```typescript
stopWhen: async ({ steps }) =>
steps.at(-1)?.finishReason === 'stop'
```
### Custom: Combined
```typescript
stopWhen: async ({ steps }) =>
steps.length >= 10 || steps.at(-1)?.finishReason === 'stop'
```
## Tool Choice Control
```typescript
const agent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-4.5',
tools: { search, calculate },
// Force tool use every step
toolChoice: 'required',
// Disable tools (text only)
toolChoice: 'none',
// Let model decide (default)
toolChoice: 'auto',
});
```
## Agent with Constraints
```typescript
const customerSupportAgent = new ToolLoopAgent({
model: 'anthropic/claude-sonnet-4.5',
instructions: `You are a customer support specialist.
Rules:
- Never promise refunds without checking policy
- Always be empathetic and professional
- If unsure, offer to escalate
- Keep responses concise
- Never share internal company information`,
tools: {
checkOrderStatus,
lookupPolicy,
createTicket,
},
});
```
## Accessing Step History
```typescript
const result = await agent.generate({ prompt: '...' });
for (const step of result.steps) {
if (step.type === 'tool-call') {
console.log(`Called ${step.tool} with`, step.input);
} else if (step.type === 'text-generation') {
console.log('Generated:', step.output);
}
}
```
## Error Handling
```typescript
try {
const result = await agent.generate({ prompt: '...' });
} catch (error) {
if (error.name === 'AbortError') {
console.log('Agent execution aborted');
} else {
console.error('Agent error:', error);
}
}
```
## Best Practices
**DO:**
- Set reasonable `stopWhen` limits
- Use typed tool schemas with Zod
- Handle abort signals for long-running agents
- Log step history for debugging
**DON'T:**
- Leave agents unbounded (no stop condition)
- Use synchronous blocking operations in tools
- Ignore error states
- Skip tool validation
@@ -0,0 +1,259 @@
# Chat Persistence
Strategies for persisting chat messages with AI SDK v6.
## Core Principle
**Persist `UIMessage[]`, convert to `ModelMessage[]` only at call sites.**
```typescript
// Database stores UIMessage format
const chat = await loadChat(chatId); // Returns UIMessage[]
// Convert only when calling the model
const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(chat.messages),
});
```
## Server-Side Persistence
### onFinish Callback
```typescript
import { streamText, createIdGenerator } from 'ai';
export async function POST(req: Request) {
const { messages, chatId } = await req.json();
const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse({
originalMessages: messages,
// Generate stable, server-side message IDs
generateMessageId: createIdGenerator({ prefix: 'msg', size: 16 }),
// Persist after stream completes
onFinish: async ({ messages: completeMessages }) => {
await db.chat.upsert({
where: { id: chatId },
update: { messages: completeMessages, updatedAt: new Date() },
create: { id: chatId, messages: completeMessages },
});
},
});
}
```
### Survive Client Disconnects
Call `consumeStream()` to ensure the stream completes even if the client disconnects:
```typescript
const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(messages),
});
// Start consuming immediately (runs in background)
result.consumeStream();
return result.toUIMessageStreamResponse({
originalMessages: messages,
onFinish: async ({ messages }) => {
await saveChat(chatId, messages);
},
});
```
## Database Schema
### Drizzle Example
```typescript
import { pgTable, text, jsonb, timestamp, uuid } from 'drizzle-orm/pg-core';
export const chats = pgTable('chats', {
id: uuid('id').primaryKey().defaultRandom(),
userId: text('user_id').notNull(),
title: text('title'),
messages: jsonb('messages').$type<UIMessage[]>().notNull().default([]),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
```
### Prisma Example
```prisma
model Chat {
id String @id @default(cuid())
userId String
title String?
messages Json @default("[]")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId])
}
```
## Loading Chats
```typescript
// API route to load chat
export async function GET(req: Request, { params }: { params: { id: string } }) {
const chat = await db.chat.findUnique({
where: { id: params.id },
});
if (!chat) {
return new Response('Not found', { status: 404 });
}
return Response.json({
id: chat.id,
messages: chat.messages as UIMessage[],
});
}
```
```tsx
// Client-side loading
'use client';
import { useChat } from '@ai-sdk/react';
import { useEffect, useState } from 'react';
function Chat({ chatId }: { chatId: string }) {
const [initialMessages, setInitialMessages] = useState<UIMessage[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`/api/chats/${chatId}`)
.then((res) => res.json())
.then((data) => {
setInitialMessages(data.messages);
setLoading(false);
});
}, [chatId]);
const { messages, sendMessage } = useChat({
id: chatId,
messages: initialMessages,
transport: new DefaultChatTransport({ api: '/api/chat' }),
});
if (loading) return <div>Loading...</div>;
return <ChatUI messages={messages} onSend={sendMessage} />;
}
```
## Bandwidth Optimization
Send only the last message, load history server-side:
```typescript
// Client: Send only new message
const { sendMessage } = useChat({
transport: new DefaultChatTransport({
api: '/api/chat',
prepareSendMessagesRequest: ({ id, messages }) => ({
body: {
chatId: id,
message: messages.at(-1) // Only send last message
},
}),
}),
});
// Server: Load history from database
export async function POST(req: Request) {
const { chatId, message } = await req.json();
// Load existing messages from database
const chat = await db.chat.findUnique({ where: { id: chatId } });
const messages = [...(chat?.messages ?? []), message];
const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse({
originalMessages: messages,
onFinish: async ({ messages }) => {
await db.chat.update({
where: { id: chatId },
data: { messages },
});
},
});
}
```
## Resumable Streams
Resume interrupted streams on page reload:
```tsx
function Chat({ chatId }: { chatId: string }) {
const { messages, resumeStream, status } = useChat({
id: chatId,
messages: initialMessages,
transport: new DefaultChatTransport({ api: '/api/chat' }),
});
// Resume on mount if there's an incomplete stream
useEffect(() => {
const lastMessage = messages.at(-1);
if (lastMessage?.role === 'assistant' && status === 'ready') {
// Check if message seems incomplete
resumeStream();
}
}, []);
return (
<div>
{/* ... */}
{status === 'streaming' && <div>AI is typing...</div>}
<button onClick={() => resumeStream()}>Resume</button>
</div>
);
}
```
## Migration from v4/v5
If migrating existing data, use the dual-write pattern:
1. Create new `messages_v6` table
2. Dual-write to both tables
3. Run background migration
4. Switch reads to v6 schema
5. Remove dual-write
6. Drop old table
See [AI SDK Migration Guide](https://sdk.vercel.ai/docs/migration-guides) for detailed steps.
## Best Practices
**Persistence:**
- Always use `onFinish` for reliable persistence
- Generate server-side message IDs for consistency
- Use `consumeStream()` to complete streams even on disconnect
**Performance:**
- Index by userId for user-specific queries
- Consider pagination for long conversations
- Use bandwidth optimization for mobile clients
**Reliability:**
- Handle concurrent updates with optimistic locking
- Implement retry logic for database failures
- Log persistence errors for debugging
@@ -0,0 +1,250 @@
# Tool Approval (Human-in-the-Loop)
v6 patterns for requiring user approval before tool execution.
## When to Use
| Scenario | Approval Type |
|----------|---------------|
| Always dangerous (delete, payment) | Static: `needsApproval: true` |
| Conditionally risky (large amounts) | Dynamic: `needsApproval: async (args) => boolean` |
| User preference | Dynamic based on user settings |
## Static Approval
Tool always requires approval:
```typescript
import { tool } from 'ai';
import { z } from 'zod';
const deleteUserTool = tool({
description: 'Permanently delete a user account',
inputSchema: z.object({
userId: z.string(),
reason: z.string(),
}),
needsApproval: true, // Always require
execute: async ({ userId, reason }) => {
await deleteUser(userId, reason);
return { success: true };
},
});
```
## Dynamic Approval
Approval based on input:
```typescript
const paymentTool = tool({
description: 'Process a payment',
inputSchema: z.object({
amount: z.number(),
recipient: z.string(),
currency: z.string().default('USD'),
}),
needsApproval: async ({ amount }) => amount > 1000,
execute: async ({ amount, recipient, currency }) => {
return await processPayment(amount, recipient, currency);
},
});
```
## Complex Approval Logic
```typescript
const externalApiTool = tool({
description: 'Call external API',
inputSchema: z.object({
endpoint: z.string(),
method: z.enum(['GET', 'POST', 'DELETE']),
body: z.any().optional(),
}),
needsApproval: async ({ method, endpoint }) => {
// Approve all non-GET requests
if (method !== 'GET') return true;
// Approve requests to sensitive endpoints
if (endpoint.includes('/admin')) return true;
// No approval needed for safe reads
return false;
},
execute: async ({ endpoint, method, body }) => {
return await fetch(endpoint, { method, body: JSON.stringify(body) });
},
});
```
## Client-Side Handling
### useChat with Approval
```tsx
'use client';
import { useChat } from '@ai-sdk/react';
function Chat() {
const { messages, sendMessage, addToolApprovalResponse } = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
});
return (
<div>
{messages.map((message) => (
<Message
key={message.id}
message={message}
onApprove={(id) => addToolApprovalResponse({ id, approved: true })}
onDeny={(id) => addToolApprovalResponse({ id, approved: false })}
/>
))}
</div>
);
}
```
### Approval UI Component
```tsx
function ToolInvocation({ invocation, onApprove, onDeny }) {
switch (invocation.state) {
case 'approval-requested':
return (
<div className="border rounded p-4 bg-yellow-50">
<h4 className="font-bold">Approval Required</h4>
<p>Tool: {invocation.toolName}</p>
<pre className="text-sm bg-gray-100 p-2 rounded">
{JSON.stringify(invocation.input, null, 2)}
</pre>
<div className="flex gap-2 mt-2">
<button
onClick={() => onApprove(invocation.approval.id)}
className="bg-green-500 text-white px-4 py-2 rounded"
>
Approve
</button>
<button
onClick={() => onDeny(invocation.approval.id)}
className="bg-red-500 text-white px-4 py-2 rounded"
>
Deny
</button>
</div>
</div>
);
case 'pending':
return <div className="text-gray-500">Waiting for tool execution...</div>;
case 'output-available':
return (
<div className="border rounded p-4 bg-green-50">
<h4 className="font-bold">Tool Result</h4>
<pre className="text-sm">{JSON.stringify(invocation.output, null, 2)}</pre>
</div>
);
default:
return null;
}
}
```
### Rendering Tool Parts in Messages
```tsx
function Message({ message, onApprove, onDeny }) {
return (
<div>
{message.parts.map((part, i) => {
if (part.type === 'text') {
return <p key={i}>{part.text}</p>;
}
if (part.type === 'tool-invocation') {
return (
<ToolInvocation
key={i}
invocation={part}
onApprove={onApprove}
onDeny={onDeny}
/>
);
}
return null;
})}
</div>
);
}
```
## Server-Side Processing
For complex approval workflows with server-side tool execution:
```typescript
import { processToolCalls } from './tool-processor';
export async function POST(req: Request) {
const { messages } = await req.json();
// Check for pending approvals
const lastMessage = messages.at(-1);
const hasPendingApprovals = lastMessage?.parts?.some(
(p) => p.type === 'tool-invocation' && p.state === 'output-available'
);
if (hasPendingApprovals) {
// Process approved tools
const stream = createUIMessageStream({
execute: async ({ writer }) => {
const processed = await processToolCalls({
writer,
messages,
tools: myTools,
}, toolExecuteFunctions);
// Continue conversation with tool results
const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(processed),
});
writer.merge(result.toUIMessageStream());
},
originalMessages: messages,
});
return createUIMessageStreamResponse({ stream });
}
// Normal flow
const result = streamText({
model: openai('gpt-4o'),
messages: convertToModelMessages(messages),
tools: myTools,
});
return result.toUIMessageStreamResponse({ originalMessages: messages });
}
```
## Best Practices
**Security:**
- Always require approval for destructive operations
- Use dynamic approval for operations with varying risk levels
- Log all approval decisions for audit trails
**UX:**
- Show clear context for what the tool will do
- Display input parameters so users can make informed decisions
- Provide cancel/timeout options for pending approvals
**Error Handling:**
- Handle denied approvals gracefully
- Provide alternative actions when tools are denied
- Don't retry denied tools automatically