Skillsai-agents-ui-skills
A

ai-agents-ui-skills

Comprehensive guide for building AI agents using ToolLoopAgent, workflow patterns, and AI SDK UI components (useChat, generative UIs, tool calling)

gocallum
9 stars
1.2k downloads
Updated 1w ago

Readme

ai-agents-ui-skills follows the SKILL.md standard. Use the install command to add it to your agent stack.

---
title: "AI Agents & UI - Building Agentic Applications with AI SDK 6"
description: "Comprehensive guide for building AI agents using ToolLoopAgent, workflow patterns, and AI SDK UI components (useChat, generative UIs, tool calling)"
keywords: ["ToolLoopAgent", "agents", "workflows", "AI SDK UI", "useChat", "generative-ui", "tool-calling", "chatbot"]
---

# AI Agents & UI Skills

**Status:** AI SDK 6 Beta
**Package Manager:** pnpm
**Key Version:** @ai-sdk/core, @ai-sdk/react (for UI)
**Official Docs:** 
- [Agents Overview](https://v6.ai-sdk.dev/docs/agents/overview)
- [Building Agents](https://v6.ai-sdk.dev/docs/agents/building-agents)
- [Workflow Patterns](https://v6.ai-sdk.dev/docs/agents/workflows)
- [AI SDK UI](https://v6.ai-sdk.dev/docs/ai-sdk-ui/overview)
- [Chatbot Guide](https://v6.ai-sdk.dev/docs/ai-sdk-ui/chatbot)
- [Chatbot Tool Usage](https://v6.ai-sdk.dev/docs/ai-sdk-ui/chatbot-tool-usage)
- [Generative User Interfaces](https://v6.ai-sdk.dev/docs/ai-sdk-ui/generative-user-interfaces)

---

## Table of Contents

1. [Installation & Setup](#installation--setup)
2. [Understanding Agents](#understanding-agents)
3. [Building ToolLoopAgent](#building-toolloopagent)
4. [Agent Configuration Options](#agent-configuration-options)
5. [System Instructions & Behavior](#system-instructions--behavior)
6. [Workflow Patterns](#workflow-patterns)
7. [AI SDK UI - useChat Hook](#ai-sdk-ui---usechat-hook)
8. [Building Chatbot Applications](#building-chatbot-applications)
9. [Tool Calling in UI](#tool-calling-in-ui)
10. [Generative User Interfaces](#generative-user-interfaces)
11. [Advanced Patterns](#advanced-patterns)
12. [Best Practices](#best-practices)

---

## Installation & Setup

### Install Dependencies

```bash
# Core packages
pnpm add ai @ai-sdk/core @ai-sdk/anthropic

# For UI (React)
pnpm add @ai-sdk/react

# Optional: specific providers
pnpm add @ai-sdk/openai @ai-sdk/google
```

### TypeScript Configuration

Ensure your `tsconfig.json` has proper settings:

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM"],
    "moduleResolution": "bundler",
    "strict": true
  }
}
```

---

## Understanding Agents

### What Are Agents?

Agents are LLMs that use tools in a loop to accomplish tasks. Three core components work together:

1. **LLMs** - Process input, decide next action
2. **Tools** - Extend capabilities (APIs, databases, files)
3. **Loop** - Orchestrates execution through context management and stopping conditions

### ToolLoopAgent Class

The `ToolLoopAgent` class is the recommended approach because it:

- **Reduces boilerplate** - Manages loops and message arrays automatically
- **Improves reusability** - Define once, use throughout application
- **Simplifies maintenance** - Single place to update configuration
- **Provides type safety** - Full TypeScript support for tools and outputs

### vs. Core Functions

For most use cases, use `ToolLoopAgent`. Use core functions (`generateText`, `streamText`) when you need explicit control for complex structured workflows.

---

## Building ToolLoopAgent

### Basic Agent

```typescript
import { ToolLoopAgent, tool } from 'ai';
import { z } from 'zod';

const weatherAgent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  tools: {
    weather: tool({
      description: 'Get weather in a location (Fahrenheit)',
      inputSchema: z.object({
        location: z.string().describe('The location'),
      }),
      execute: async ({ location }) => ({
        location,
        temperature: 72 + Math.floor(Math.random() * 21) - 10,
      }),
    }),
    convertFahrenheitToCelsius: tool({
      description: 'Convert Fahrenheit to Celsius',
      inputSchema: z.object({
        temperature: z.number(),
      }),
      execute: async ({ temperature }) => ({
        celsius: Math.round((temperature - 32) * (5 / 9)),
      }),
    }),
  },
});

// Use the agent
const result = await weatherAgent.generate({
  prompt: 'What is the weather in San Francisco in celsius?',
});

console.log(result.text); // Agent's final answer
console.log(result.steps); // Steps taken by agent
```

### Multi-Tool Execution

The agent automatically:
1. Calls `weather` tool to get temperature in Fahrenheit
2. Calls `convertFahrenheitToCelsius` to convert it
3. Generates final text response with result

---

## Agent Configuration Options

### Model and System Instructions

```typescript
const agent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  instructions: 'You are an expert data analyst. Provide clear insights.',
});
```

### Tools

```typescript
const codeAgent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  tools: {
    runCode: tool({
      description: 'Execute Python code',
      inputSchema: z.object({
        code: z.string(),
      }),
      execute: async ({ code }) => {
        // Execute code
        return { output: 'Result' };
      },
    }),
  },
});
```

### Loop Control (stopWhen)

By default, agents run for 20 steps (`stopWhen: stepCountIs(20)`). Each step is one generation (text or tool call).

```typescript
import { ToolLoopAgent, stepCountIs } from 'ai';

const agent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  stopWhen: stepCountIs(20), // Allow up to 20 steps
});

// Combine multiple stop conditions
const agent2 = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  stopWhen: [
    stepCountIs(20),
    yourCustomCondition(), // Custom logic
  ],
});
```

The loop stops when:
- Finish reasoning (non-tool-call) is returned
- Tool without execute function is invoked
- Tool call needs approval
- Stop condition is met

### Tool Choice

Control how agent uses tools:

```typescript
const agent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  tools: { /* ... */ },
  toolChoice: 'required', // Force tool use
  // or 'none' to disable tools
  // or 'auto' (default) to let model decide
});

// Force specific tool
const agent2 = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  tools: { weather: weatherTool, attractions: attractionsTool },
  toolChoice: {
    type: 'tool',
    toolName: 'weather', // Force weather tool
  },
});
```

### Structured Output

Define structured output schemas:

```typescript
import { ToolLoopAgent, Output, stepCountIs } from 'ai';

const analysisAgent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  output: Output.object({
    schema: z.object({
      sentiment: z.enum(['positive', 'neutral', 'negative']),
      summary: z.string(),
      keyPoints: z.array(z.string()),
    }),
  }),
  stopWhen: stepCountIs(10),
});

const { output } = await analysisAgent.generate({
  prompt: 'Analyze customer feedback',
});
```

---

## System Instructions & Behavior

### Basic System Instructions

```typescript
const agent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  instructions: 'You are an expert software engineer.',
});
```

### Detailed Behavioral Instructions

```typescript
const codeReviewAgent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  instructions: `You are a senior software engineer conducting code reviews.

Your approach:
- Focus on security vulnerabilities first
- Identify performance bottlenecks
- Suggest improvements for readability and maintainability
- Be constructive and educational in your feedback
- Always explain why something is an issue and how to fix it`,
});
```

### Constrain Agent Behavior

```typescript
const supportAgent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  instructions: `You are a customer support specialist.

Rules:
- Never make promises about refunds without checking policy
- Always be empathetic and professional
- If you don't know something, say so and offer to escalate
- Keep responses concise and actionable
- Never share internal company information`,
  tools: {
    checkOrderStatus,
    lookupPolicy,
    createTicket,
  },
});
```

### Tool Usage Instructions

```typescript
const researchAgent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  instructions: `You are a research assistant with access to search and document tools.

When researching:
1. Always start with a broad search to understand the topic
2. Use document analysis for detailed information
3. Cross-reference multiple sources before drawing conclusions
4. Cite your sources when presenting information
5. If information conflicts, present both viewpoints`,
  tools: {
    webSearch,
    analyzeDocument,
    extractQuotes,
  },
});
```

### Format and Style Instructions

```typescript
const technicalWriterAgent = new ToolLoopAgent({
  model: 'anthropic/claude-sonnet-4.5',
  instructions: `You are a technical documentation writer.

Writing style:
- Use clear, simple language
- Avoid jargon unless necessary
- Structure information with headers and bullet points
- Include code examples where relevant
- Write in second person ("you" instead of "the user")

Always format responses in Markdown.`,
});
```

---

## Workflow Patterns

Combine patterns thoughtfully to build reliable solutions for complex problems.

### 1. Sequential Processing (Chains)

Execute steps in predefined order. Each step's output becomes next step's input.

**Use case:** Content generation pipelines, data transformation processes

```typescript
import { generateText, generateObject } from 'ai';
import { z } from 'zod';

async function generateMarketingCopy(input: string) {
  const model = 'anthropic/claude-sonnet-4.5';

  // Step 1: Generate marketing copy
  const { text: copy } = await generateText({
    model,
    prompt: `Write persuasive marketing copy for: ${input}. Focus on benefits and emotional appeal.`,
  });

  // Step 2: Quality check
  const { object: qualityMetrics } = await generateObject({
    model,
    schema: z.object({
      hasCallToAction: z.boolean(),
      emotionalAppeal: z.number().min(1).max(10),
      clarity: z.number().min(1).max(10),
    }),
    prompt: `Evaluate this marketing copy:
1. Presence of call to action (true/false)
2. Emotional appeal (1-10)
3. Clarity (1-10)

Copy to evaluate: ${copy}`,
  });

  // Step 3: Regenerate if needed
  if (
    !qualityMetrics.hasCallToAction ||
    qualityMetrics.emotionalAppeal < 7 ||
    qualityMetrics.clarity < 7
  ) {
    const { text: improvedCopy } = await generateText({
      model,
      prompt: `Rewrite this marketing copy with:
${!qualityMetrics.hasCallToAction ? '- A clear call to action' : ''}
${qualityMetrics.emotionalAppeal < 7 ? '- Stronger emotional appeal' : ''}
${qualityMetrics.clarity < 7 ? '- Improved clarity and directness' : ''}

Original copy: ${copy}`,
    });
    return { copy: improvedCopy, qualityMetrics };
  }

  return { copy, qualityMetrics };
}
```

### 2. Routing

Let the model decide which path to take based on context and intermediate results.

**Use case:** Handling varied inputs that require different processing approaches

```typescript
import { generateObject, generateText } from 'ai';
import { z } from 'zod';

async function handleCustomerQuery(query: string) {
  const model = 'anthropic/claude-sonnet-4.5';

  // Step 1: Classify the query
  const { object: classification } = await generateObject({
    model,
    schema: z.object({
      reasoning: z.string(),
      type: z.enum(['general', 'refund', 'technical']),
      complexity: z.enum(['simple', 'complex']),
    }),
    prompt: `Classify this customer query: ${query}

Determine:
1. Query type (general, refund, or technical)
2. Complexity (simple or complex)
3. Brief reasoning`,
  });

  // Step 2: Route based on classification
  const { text: response } = await generateText({
    model:
      classification.complexity === 'simple'
        ? 'openai/gpt-4o-mini'
        : 'openai/o4-mini',
    system: {
      general: 'You are an expert customer service agent.',
      refund: 'You are a refund specialist. Follow company policy strictly.',
      technical: 'You are a technical support specialist.',
    }[classification.type],
    prompt: query,
  });

  return { response, classification };
}
```

### 3. Parallel Processing

Break tasks into independent subtasks that execute simultaneously.

**Use case:** Analyzing multiple documents, different aspects of input (like code review)

```typescript
import { generateText, generateObject } from 'ai';
import { z } from 'zod';

async function parallelCodeReview(code: string) {
  const model = 'anthropic/claude-sonnet-4.5';

  // Run parallel reviews
  const [securityReview, performanceReview, maintainabilityReview] =
    await Promise.all([
      generateObject({
        model,
        system: 'You are an expert in code security.',
        schema: z.object({
          vulnerabilities: z.array(z.string()),
          riskLevel: z.enum(['low', 'medium', 'high']),
          suggestions: z.array(z.string()),
        }),
        prompt: `Review this code: ${code}`,
      }),
      generateObject({
        model,
        system: 'You are an expert in code performance.',
        schema: z.object({
          issues: z.array(z.string()),
          impact: z.enum(['low', 'medium', 'high']),
          optimizations: z.array(z.string()),
        }),
        prompt: `Review this code: ${code}`,
      }),
      generateObject({
        model,
        system: 'You are an expert in code quality.',
        schema: z.object({
          concerns: z.array(z.string()),
          qualityScore: z.number().min(1).max(10),
          recommendations: z.array(z.string()),
        }),
        prompt: `Review this code: ${code}`,
      }),
    ]);

  // Aggregate results
  const { text: summary } = await generateText({
    model,
    system: 'You are a technical lead summarizing multiple code reviews.',
    prompt: `Synthesize these code review results into a concise summary:
${JSON.stringify(
  [
    { ...securityReview.object, type: 'security' },
    { ...performanceReview.object, type: 'performance' },
    { ...maintainabilityReview.object, type: 'maintainability' },
  ],
  null,
  2,
)}`,
  });

  return { reviews: [securityReview.object, performanceReview.object, maintainabilityReview.object], summary };
}
```

### 4. Orchestrator-Worker

Primary model coordinates specialized workers. Each worker optimizes for specific subtask.

**Use case:** Complex tasks requiring different types of expertise

```typescript
import { generateObject } from 'ai';
import { z } from 'zod';

async function implementFeature(featureRequest: string) {
  // Orchestrator: Plan the implementation
  const { object: implementationPlan } = await generateObject({
    model: 'anthropic/claude-sonnet-4.5',
    schema: z.object({
      files: z.array(
        z.object({
          purpose: z.string(),
          filePath: z.string(),
          changeType: z.enum(['create', 'modify', 'delete']),
        }),
      ),
      estimatedComplexity: z.enum(['low', 'medium', 'high']),
    }),
    system:
      'You are a senior software architect planning feature implementations.',
    prompt: `Analyze this feature request and create an implementation plan: ${featureRequest}`,
  });

  // Workers: Execute the planned changes
  const fileChanges = await Promise.all(
    implementationPlan.files.map(async (file) => {
      const workerSystemPrompt = {
        create:
          'You are an expert at implementing new files following best practices.',
        modify:
          'You are an expert at modifying existing code while maintaining consistency.',
        delete:
          'You are an expert at safely removing code while ensuring no breaking changes.',
      }[file.changeType];

      const { object: change } = await generateObject({
        model: 'anthropic/claude-sonnet-4.5',
        schema: z.object({
          explanation: z.string(),
          code: z.string(),
        }),
        system: workerSystemPrompt,
        prompt: `Implement changes for ${file.filePath} to support: ${file.purpose}`,
      });

      return {
        file,
        implementation: change,
      };
    }),
  );

  return {
    plan: implementationPlan,
    changes: fileChanges,
  };
}
```

### 5. Evaluator-Optimizer

Add quality control with dedicated evaluation steps that assess intermediate results.

**Use case:** Robust workflows with self-improvement and error recovery

```typescript
import { generateText, generateObject } from 'ai';
import { z } from 'zod';

async function translateWithFeedback(text: string, targetLanguage: string) {
  let currentTranslation = '';
  let iterations = 0;
  const MAX_ITERATIONS = 3;

  // Initial translation
  const { text: translation } = await generateText({
    model: 'anthropic/claude-sonnet-4.5',
    system: 'You are an expert literary translator.',
    prompt: `Translate this text to ${targetLanguage}, preserving tone and cultural nuances: ${text}`,
  });

  currentTranslation = translation;

  // Evaluation-optimization loop
  while (iterations < MAX_ITERATIONS) {
    // Evaluate current translation
    const { object: evaluation } = await generateObject({
      model: 'anthropic/claude-sonnet-4.5',
      schema: z.object({
        qualityScore: z.number().min(1).max(10),
        preservesTone: z.boolean(),
        preservesNuance: z.boolean(),
        culturallyAccurate: z.boolean(),
        specificIssues: z.array(z.string()),
        improvementSuggestions: z.array(z.string()),
      }),
      system: 'You are an expert in evaluating literary translations.',
      prompt: `Evaluate this translation:
Original: ${text}
Translation: ${currentTranslation}

Consider: tone, nuance, cultural accuracy`,
    });

    // Check if quality meets threshold
    if (
      evaluation.qualityScore >= 8 &&
      evaluation.preservesTone &&
      evaluation.preservesNuance &&
      evaluation.culturallyAccurate
    ) {
      break;
    }

    // Generate improved translation based on feedback
    const { text: improvedTranslation } = await generateText({
      model: 'anthropic/claude-sonnet-4.5',
      system: 'You are an expert literary translator.',
      prompt: `Improve this translation based on feedback:
${evaluation.specificIssues.join('\n')}
${evaluation.improvementSuggestions.join('\n')}

Original: ${text}
Current Translation: ${currentTranslation}`,
    });

    currentTranslation = improvedTranslation;
    iterations++;
  }

  return {
    finalTranslation: currentTranslation,
    iterationsRequired: iterations,
  };
}
```

---

## AI SDK UI - useChat Hook

The `useChat` hook makes it effortless to create a conversational UI for chatbot applications.

### Features

- **Message Streaming** - Real-time streaming from AI provider
- **Managed States** - Input, messages, status, error states
- **Seamless Integration** - Easy integration into any design

### Basic Example

**Client:**

```typescript
'use client';

import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';

export default function Page() {
  const { messages, sendMessage, status } = useChat({
    transport: new DefaultChatTransport({
      api: '/api/chat',
    }),
  });
  const [input, setInput] = useState('');

  return (
    <>
      {messages.map((message) => (
        <div key={message.id}>
          {message.role === 'user' ? 'User: ' : 'AI: '}
          {message.parts.map((part, index) =>
            part.type === 'text' ? <span key={index}>{part.text}</span> : null,
          )}
        </div>
      ))}

      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (input.trim()) {
            sendMessage({ text: input });
            setInput('');
          }
        }}
      >
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          disabled={status !== 'ready'}
          placeholder="Say something..."
        />
        <button type="submit" disabled={status !== 'ready'}>
          Submit
        </button>
      </form>
    </>
  );
}
```

**Server:**

```typescript
import { convertToModelMessages, streamText, UIMessage } from 'ai';

export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: 'anthropic/claude-sonnet-4.5',
    system: 'You are a helpful assistant.',
    messages: await convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse();
}
```

### Message Parts

Messages use a `parts` property that contains message parts (text, tool invocation, tool result):

```typescript
{messages.map((message) => (
  <div key={message.id}>
    {message.parts.map((part, index) => {
      if (part.type === 'text') {
        return <span key={index}>{part.text}</span>;
      }
      // Handle other part types
    })}
  </div>
))}
```

### Status States

```typescript
const { messages, sendMessage, status, stop } = useChat({
  transport: new DefaultChatTransport({
    api: '/api/chat',
  }),
});

// Status values:
// - 'submitted': Message sent, awaiting response start
// - 'streaming': Response actively streaming
// - 'ready': Full response received
// - 'error': Error occurred

{(status === 'submitted' || status === 'streaming') && (
  <div>
    {status === 'submitted' && <Spinner />}
    <button type="button" onClick={() => stop()}>
      Stop
    </button>
  </div>
)}
```

### Error Handling

```typescript
const { messages, sendMessage, error, reload } = useChat({
  transport: new DefaultChatTransport({
    api: '/api/chat',
  }),
});

{error && (
  <>
    <div>An error occurred.</div>
    <button type="button" onClick={() => reload()}>
      Retry
    </button>
  </>
)}
```

### Modify Messages

```typescript
const { messages, setMessages } = useChat();

const handleDelete = (id: string) => {
  setMessages(messages.filter((message) => message.id !== id));
};

{messages.map((message) => (
  <div key={message.id}>
    {/* message content */}
    <button onClick={() => handleDelete(message.id)}>Delete</button>
  </div>
))}
```

### Cancellation and Regeneration

```typescript
const { stop, status, regenerate } = useChat();

<button onClick={stop} disabled={!(status === 'streaming' || status === 'submitted')}>
  Stop
</button>

<button onClick={regenerate} disabled={!(status === 'ready' || status === 'error')}>
  Regenerate
</button>
```

### Event Callbacks

```typescript
import { UIMessage } from 'ai';

const { messages } = useChat({
  onFinish: ({ message, messages, isAbort, isDisconnect, isError }) => {
    // Use information to update other UI states
  },
  onError: (error) => {
    console.error('An error occurred:', error);
  },
  onData: (data) => {
    console.log('Received data part from server:', data);
  },
});
```

### Request Configuration

```typescript
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';

const { messages, sendMessage } = useChat({
  transport: new DefaultChatTransport({
    api: '/api/custom-chat',
    headers: {
      Authorization: 'your_token',
    },
    body: {
      user_id: '123',
    },
  }),
});

// Dynamic configuration
const { messages: messages2 } = useChat({
  transport: new DefaultChatTransport({
    api: '/api/chat',
    headers: () => ({
      Authorization: `Bearer ${getAuthToken()}`,
      'X-User-ID': getCurrentUserId(),
    }),
    body: () => ({
      sessionId: getCurrentSessionId(),
      preferences: getUserPreferences(),
    }),
  }),
});

// Request-level configuration (recommended)
sendMessage(
  { text: input },
  {
    headers: {
      Authorization: 'Bearer token123',
      'X-Custom-Header': 'custom-value',
    },
    body: {
      temperature: 0.7,
      max_tokens: 100,
      user_id: '123',
    },
  },
);
```

---

## Building Chatbot Applications

### Full Chatbot Example with Status and Error Handling

```typescript
'use client';

import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';

export default function Chat() {
  const { messages, sendMessage, error, reload, status, stop } = useChat({
    transport: new DefaultChatTransport({
      api: '/api/chat',
    }),
  });
  const [input, setInput] = useState('');

  return (
    <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch">
      {messages.map((m) => (
        <div key={m.id} className="whitespace-pre-wrap">
          {m.role === 'user' ? 'User: ' : 'AI: '}
          {m.parts.map((part, i) =>
            part.type === 'text' ? <span key={i}>{part.text}</span> : null,
          )}
        </div>
      ))}

      {status === 'error' && (
        <div className="text-red-500">
          <p>An error occurred. Please try again.</p>
          <button onClick={() => reload()}>Retry</button>
        </div>
      )}

      {(status === 'submitted' || status === 'streaming') && (
        <div>
          <button onClick={() => stop()}>Stop</button>
        </div>
      )}

      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (input.trim() && status === 'ready') {
            sendMessage({ text: input });
            setInput('');
          }
        }}
      >
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          disabled={status !== 'ready'}
          placeholder="Send message..."
          className="w-full p-2 border rounded"
        />
        <button type="submit" disabled={status !== 'ready'}>
          Send
        </button>
      </form>
    </div>
  );
}
```

### Message Metadata

Track token consumption and usage information:

```typescript
import { LanguageModelUsage, UIMessage, streamText, convertToModelMessages } from 'ai';

// Create custom metadata type
type MyMetadata = {
  totalUsage: LanguageModelUsage;
};

export type MyUIMessage = UIMessage<MyMetadata>;

// Server
export async function POST(req: Request) {
  const { messages }: { messages: MyUIMessage[] } = await req.json();

  const result = streamText({
    model: 'anthropic/claude-sonnet-4.5',
    messages: await convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse({
    originalMessages: messages,
    messageMetadata: ({ part }) => {
      if (part.type === 'finish') {
        return { totalUsage: part.totalUsage };
      }
    },
  });
}

// Client
export default function Chat() {
  const { messages } = useChat<MyUIMessage>({
    transport: new DefaultChatTransport({
      api: '/api/chat',
    }),
  });

  return (
    <div>
      {messages.map((m) => (
        <div key={m.id}>
          {m.role === 'user' ? 'User: ' : 'AI: '}
          {m.parts.map((part) => {
            if (part.type === 'text') {
              return part.text;
            }
          })}
          {m.metadata?.totalUsage && (
            <div>Total usage: {m.metadata?.totalUsage.totalTokens} tokens</div>
          )}
        </div>
      ))}
    </div>
  );
}
```

---

## Tool Calling in UI

### Three Types of Tools in Chatbot

1. **Automatically executed server-side tools** - Execute on server, use `execute` method
2. **Automatically executed client-side tools** - Execute in browser, handle with `onToolCall`
3. **Tools requiring user interaction** - Confirmation dialogs, displayed in UI

### Tool Call Flow

1. User enters message
2. Message sent to API route
3. Language model generates tool calls during `streamText`
4. Tool calls forwarded to client
5. Server-side tools executed, results forwarded
6. Client-side tools handled with `onToolCall` callback
7. User-interaction tools displayed in UI
8. `addToolOutput` provides tool result
9. Chat configured to auto-submit when all results available

### Full Tool Usage Example

**Server:**

```typescript
import { convertToModelMessages, streamText, UIMessage } from 'ai';
import { z } from 'zod';

export const maxDuration = 30;

export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: 'anthropic/claude-sonnet-4.5',
    messages: await convertToModelMessages(messages),
    tools: {
      // Server-side tool with execute
      getWeatherInformation: {
        description: 'Show the weather in a given city to the user',
        inputSchema: z.object({ city: z.string() }),
        execute: async ({ city }: { city: string }) => {
          const weatherOptions = ['sunny', 'cloudy', 'rainy', 'snowy', 'windy'];
          return weatherOptions[Math.floor(Math.random() * weatherOptions.length)];
        },
      },
      // Client-side tool for user interaction
      askForConfirmation: {
        description: 'Ask the user for confirmation.',
        inputSchema: z.object({
          message: z.string().describe('The message to ask for confirmation.'),
        }),
      },
      // Automatically executed client-side tool
      getLocation: {
        description: 'Get the user\'s location',
        inputSchema: z.object({
          query: z.string(),
        }),
      },
    },
  });

  return result.toUIMessageStreamResponse();
}
```

**Client:**

```typescript
'use client';

import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport, lastAssistantMessageIsCompleteWithToolCalls } from 'ai';
import { useState } from 'react';

export default function Chat() {
  const { messages, sendMessage, addToolOutput } = useChat({
    transport: new DefaultChatTransport({
      api: '/api/chat',
    }),

    sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,

    // Run client-side tools automatically
    async onToolCall({ toolCall }) {
      // Always check if dynamic tool first
      if (toolCall.dynamic) {
        return;
      }

      if (toolCall.toolName === 'getLocation') {
        const cities = ['New York', 'Los Angeles', 'Chicago'];
        addToolOutput({
          tool: 'getLocation',
          toolCallId: toolCall.toolCallId,
          output: cities[Math.floor(Math.random() * cities.length)],
        });
      }
    },
  });
  const [input, setInput] = useState('');

  return (
    <>
      {messages?.map((message) => (
        <div key={message.id}>
          <strong>{`${message.role}: `}</strong>
          {message.parts.map((part) => {
            switch (part.type) {
              case 'text':
                return part.text;

              case 'tool-askForConfirmation': {
                const callId = part.toolCallId;
                switch (part.state) {
                  case 'input-available':
                    return (
                      <div key={callId}>
                        {part.input.message}
                        <button
                          onClick={() =>
                            addToolOutput({
                              tool: 'askForConfirmation',
                              toolCallId: callId,
                              output: 'Yes, confirmed.',
                            })
                          }
                        >
                          Yes
                        </button>
                        <button
                          onClick={() =>
                            addToolOutput({
                              tool: 'askForConfirmation',
                              toolCallId: callId,
                              output: 'No, denied',
                            })
                          }
                        >
                          No
                        </button>
                      </div>
                    );
                  case 'output-available':
                    return <div key={callId}>Result: {part.output}</div>;
                }
                break;
              }

              case 'tool-getWeatherInformation': {
                const callId = part.toolCallId;
                switch (part.state) {
                  case 'input-available':
                    return <div key={callId}>Getting weather for {part.input.city}...</div>;
                  case 'output-available':
                    return (
                      <div key={callId}>
                        Weather in {part.input.city}: {part.output}
                      </div>
                    );
                }
                break;
              }
            }
          })}
          <br />
        </div>
      ))}

      <form
        onSubmit={(e) => {
          e.preventDefault();
          if (input.trim()) {
            sendMessage({ text: input });
            setInput('');
          }
        }}
      >
        <input value={input} onChange={(e) => setInput(e.target.value)} />
      </form>
    </>
  );
}
```

### Tool Execution Approval

Require user confirmation before server-side tool runs:

**Server:**

```typescript
import { streamText, tool } from 'ai';
import { z } from 'zod';

export async function POST(req: Request) {
  const { messages } = await req.json();

  const result = streamText({
    model: 'anthropic/claude-sonnet-4.5',
    messages,
    tools: {
      getWeather: tool({
        description: 'Get the weather in a location',
        inputSchema: z.object({
          city: z.string(),
        }),
        needsApproval: true,
        execute: async ({ city }) => {
          const weather = await fetchWeather(city);
          return weather;
        },
      }),
    },
  });

  return result.toUIMessageStreamResponse();
}
```

**Client:**

```typescript
'use client';

import { useChat } from '@ai-sdk/react';

export default function Chat() {
  const { messages, addToolApprovalResponse } = useChat();

  return (
    <>
      {messages.map((message) => (
        <div key={message.id}>
          {message.parts.map((part) => {
            if (part.type === 'tool-getWeather') {
              switch (part.state) {
                case 'approval-requested':
                  return (
                    <div key={part.toolCallId}>
                      <p>Get weather for {part.input.city}?</p>
                      <button
                        onClick={() =>
                          addToolApprovalResponse({
                            id: part.approval.id,
                            approved: true,
                          })
                        }
                      >
                        Approve
                      </button>
                      <button
                        onClick={() =>
                          addToolApprovalResponse({
                            id: part.approval.id,
                            approved: false,
                          })
                        }
                      >
                        Deny
                      </button>
                    </div>
                  );
                case 'output-available':
                  return (
                    <div key={part.toolCallId}>
                      Weather in {part.input.city}: {part.output}
                    </div>
                  );
              }
            }
          })}
        </div>
      ))}
    </>
  );
}
```

---

## Generative User Interfaces

Generative UI allows LLMs to generate UI components, not just text.

### Core Concept

1. You provide model with tools
2. Model decides when/how to use tools based on context
3. Tool executes and returns data
4. Data passed to React component for rendering

### Basic Generative UI Example

**Tools:**

```typescript
import { tool as createTool } from 'ai';
import { z } from 'zod';

export const weatherTool = createTool({
  description: 'Display the weather for a location',
  inputSchema: z.object({
    location: z.string().describe('The location'),
  }),
  execute: async function ({ location }) {
    await new Promise((resolve) => setTimeout(resolve, 2000));
    return { weather: 'Sunny', temperature: 75, location };
  },
});

export const tools = {
  displayWeather: weatherTool,
};
```

**UI Component:**

```typescript
type WeatherProps = {
  temperature: number;
  weather: string;
  location: string;
};

export const Weather = ({ temperature, weather, location }: WeatherProps) => {
  return (
    <div>
      <h2>Current Weather for {location}</h2>
      <p>Condition: {weather}</p>
      <p>Temperature: {temperature}°C</p>
    </div>
  );
};
```

**Chatbot:**

```typescript
'use client';

import { useChat } from '@ai-sdk/react';
import { useState } from 'react';
import { Weather } from '@/components/weather';

export default function Page() {
  const [input, setInput] = useState('');
  const { messages, sendMessage } = useChat();

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    sendMessage({ text: input });
    setInput('');
  };

  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>
          <div>{message.role === 'user' ? 'User: ' : 'AI: '}</div>
          <div>
            {message.parts.map((part, index) => {
              if (part.type === 'text') {
                return <span key={index}>{part.text}</span>;
              }

              if (part.type === 'tool-displayWeather') {
                switch (part.state) {
                  case 'input-available':
                    return <div key={index}>Loading weather...</div>;
                  case 'output-available':
                    return (
                      <div key={index}>
                        <Weather {...part.output} />
                      </div>
                    );
                  case 'output-error':
                    return <div key={index}>Error: {part.errorText}</div>;
                }
              }

              return null;
            })}
          </div>
        </div>
      ))}

      <form onSubmit={handleSubmit}>
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Type a message..."
        />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}
```

**Server:**

```typescript
import { streamText, convertToModelMessages, UIMessage, stepCountIs } from 'ai';
import { tools } from '@/ai/tools';

export async function POST(request: Request) {
  const { messages }: { messages: UIMessage[] } = await request.json();

  const result = streamText({
    model: 'anthropic/claude-sonnet-4.5',
    system: 'You are a friendly assistant!',
    messages: await convertToModelMessages(messages),
    stopWhen: stepCountIs(5),
    tools,
  });

  return result.toUIMessageStreamResponse();
}
```

### Adding Multiple Tools

```typescript
// Define more tools
export const stockTool = createTool({
  description: 'Get price for a stock',
  inputSchema: z.object({
    symbol: z.string(),
  }),
  execute: async ({ symbol }) => {
    await new Promise((resolve) => setTimeout(resolve, 2000));
    return { symbol, price: 150 };
  },
});

export const tools = {
  displayWeather: weatherTool,
  getStockPrice: stockTool,
};
```

Then render multiple components based on tool types:

```typescript
{message.parts.map((part, index) => {
  if (part.type === 'tool-displayWeather') {
    return <Weather {...part.output} />;
  }
  if (part.type === 'tool-getStockPrice') {
    return <Stock {...part.output} />;
  }
})}
```

---

## Advanced Patterns

### Type Safety for Tools

```typescript
import { InferUITool, InferUITools, UIMessage, UIDataTypes } from 'ai';

// Infer single tool type
type WeatherUITool = InferUITool<typeof weatherTool>;
// { input: { location: string }; output: string }

// Infer tool set types
type MyUITools = InferUITools<typeof tools>;

// Use in custom message type
type MyUIMessage = UIMessage<never, UIDataTypes, MyUITools>;

// Pass to useChat
const { messages } = useChat<MyUIMessage>();
```

### Reasoning and Sources

Some models support reasoning tokens and sources:

**Server:**

```typescript
export async function POST(req: Request) {
  const { messages }: { messages: UIMessage[] } = await req.json();

  const result = streamText({
    model: 'deepseek/deepseek-r1',
    messages: await convertToModelMessages(messages),
  });

  return result.toUIMessageStreamResponse({
    sendReasoning: true, // Include reasoning tokens
    sendSources: true,   // Include sources
  });
}
```

**Client:**

```typescript
{message.parts.map((part, index) => {
  if (part.type === 'text') {
    return <div key={index}>{part.text}</div>;
  }

  if (part.type === 'reasoning') {
    return <pre key={index}>{part.text}</pre>;
  }

  if (part.type === 'source-url') {
    return (
      <a key={`source-${part.id}`} href={part.url} target="_blank">
        {part.title ?? new URL(part.url).hostname}
      </a>
    );
  }

  if (part.type === 'source-document') {
    return <span key={`source-${part.id}`}>{part.title ?? `Document`}</span>;
  }
})}
```

### File Attachments

```typescript
'use client';

import { useChat } from '@ai-sdk/react';
import { useRef, useState } from 'react';

export default function Page() {
  const { messages, sendMessage, status } = useChat();
  const [input, setInput] = useState('');
  const [files, setFiles] = useState<FileList | undefined>();
  const fileInputRef = useRef<HTMLInputElement>(null);

  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>
          {message.parts.map((part, index) => {
            if (part.type === 'text') {
              return <span key={index}>{part.text}</span>;
            }

            if (part.type === 'file' && part.mediaType?.startsWith('image/')) {
              return <img key={index} src={part.url} alt={part.filename} />;
            }

            return null;
          })}
        </div>
      ))}

      <form
        onSubmit={(event) => {
          event.preventDefault();
          if (input.trim()) {
            sendMessage({
              text: input,
              files,
            });
            setInput('');
            setFiles(undefined);
            if (fileInputRef.current) {
              fileInputRef.current.value = '';
            }
          }
        }}
      >
        <input
          type="file"
          onChange={(event) => {
            if (event.target.files) {
              setFiles(event.target.files);
            }
          }}
          multiple
          ref={fileInputRef}
        />
        <input
          value={input}
          placeholder="Send message..."
          onChange={(e) => setInput(e.target.value)}
          disabled={status !== 'ready'}
        />
      </form>
    </div>
  );
}
```

---

## Best Practices

### Agents

1. **Start with ToolLoopAgent** - Use for most cases; only use core functions when you need explicit control
2. **Clear system instructions** - Define agent's role, expertise, and constraints clearly
3. **Appropriate tool choice** - Use `'required'` when tools must be used, `'auto'` for flexibility
4. **Set reasonable step limits** - Balance thorough execution with resource consumption
5. **Tool descriptions matter** - Provide clear, specific descriptions so model uses tools correctly

### Workflows

1. **Choose appropriate pattern** - Use simplest pattern that meets requirements
2. **Plan before implementation** - Break down complex tasks into clear steps
3. **Add error handling** - Gracefully handle tool failures and edge cases
4. **Monitor costs** - Complex workflows mean more LLM calls
5. **Test extensively** - Non-deterministic nature requires thorough testing

### Chat UI

1. **Handle all status states** - Show spinners, error states, and stop buttons
2. **Use typed message parts** - Leverage `tool-${toolName}` typed parts for type safety
3. **Display tool execution** - Show loading states and results to users
4. **Manage history** - Consider message persistence for long conversations
5. **Implement retry logic** - Provide easy way to retry failed requests

### Generative UI

1. **Separate concerns** - Keep tools, components, and chat logic separate
2. **Type tool outputs** - Ensure component props match tool output schema
3. **Handle loading states** - Show `input-streaming` and `input-available` states
4. **Error boundaries** - Handle tool execution errors gracefully
5. **Progressive enhancement** - Start simple, add complexity incrementally

### General

1. **Use pnpm** - Default package manager for all AI SDK projects
2. **Pin versions** - AI SDK 6 is beta; consider pinning versions for stability
3. **Test edge cases** - Test with various inputs, tool combinations, error scenarios
4. **Monitor performance** - Track token usage, execution time, and error rates
5. **Iterate on prompts** - System instructions have significant impact on results

---

## Framework Support

**AI SDK UI supports:**
- React (@ai-sdk/react) - Full support: useChat, useCompletion, useObject
- Vue.js (@ai-sdk/vue) - useChat, useCompletion, useObject
- Svelte (@ai-sdk/svelte) - useChat, useCompletion, useObject
- Angular (@ai-sdk/angular) - useChat, useCompletion, useObject
- SolidJS (community) - Contributions welcome

---

## Resources

- [AI SDK 6 Docs](https://v6.ai-sdk.dev/docs)
- [Cookbook Examples](https://v6.ai-sdk.dev/cookbook)
- [Showcase](https://v6.ai-sdk.dev/showcase)
- [GitHub](https://github.com/vercel/ai)
- [Discussions](https://github.com/vercel/ai/discussions)
- [Playground](https://v6.ai-sdk.dev/playground)

Install

Requires askill CLI v1.0+

Metadata

LicenseUnknown
Version-
Updated1w ago
Publishergocallum

Tags

apici-cdgithubgithub-actionsllmpromptingsecuritytesting