Utiliser AI SDK UI
AI SDK UI est une bibliothèque d'utilitaires et de composants React permettant de créer des interfaces alimentées par l'IA. Dans ce guide, vous apprendrez à utiliser @mastra/ai-sdk pour convertir les sorties de Mastra dans des formats compatibles avec AI SDK, afin d'employer ses hooks et ses composants dans votre frontend.
Vous migrez d'AI SDK v4 vers v5 ? Consultez le guide de migration.
Vous souhaitez voir davantage d'exemples ? Consultez le UI Dojo de Mastra ou le guide de démarrage rapide Next.js.
Bien démarrerLien direct vers Bien démarrer
Utilisez Mastra et AI SDK UI ensemble en installant le package @mastra/ai-sdk. @mastra/ai-sdk fournit des routes d'API personnalisées et des utilitaires permettant de diffuser les agents Mastra dans des formats compatibles avec AI SDK. Il comprend des gestionnaires de routes pour le chat, les workflows et les réseaux, ainsi que des utilitaires et des types exportés destinés aux intégrations d'interface utilisateur.
@mastra/ai-sdk s'intègre aux trois principaux hooks d'AI SDK UI : useChat(), useCompletion() et useObject().
Pour commencer, installez les packages requis :
- npm
- pnpm
- Yarn
- Bun
npm install @mastra/ai-sdk@latest @ai-sdk/react ai
pnpm add @mastra/ai-sdk@latest @ai-sdk/react ai
yarn add @mastra/ai-sdk@latest @ai-sdk/react ai
bun add @mastra/ai-sdk@latest @ai-sdk/react ai
Vous pouvez maintenant suivre les guides d'intégration et les recettes ci-dessous !
Guides d'intégrationLien direct vers Guides d'intégration
En général, vous configurez des routes d'API qui diffusent le contenu Mastra dans un format compatible avec AI SDK, puis utilisez ces routes dans des hooks AI SDK UI tels que useChat(). Choisissez l'une des approches suivantes :
Une fois vos routes d'API configurées, vous pouvez les utiliser dans le hook useChat().
Serveur MastraLien direct vers Serveur Mastra
Exécutez Mastra comme serveur autonome et connectez votre frontend (par exemple avec Vite + React) à ses points de terminaison d'API. Pour cela, vous utiliserez la fonctionnalité de routes d'API personnalisées de Mastra.
Le UI Dojo de Mastra illustre cette configuration.
Vous pouvez utiliser chatRoute(), workflowRoute() et networkRoute() pour créer des routes d'API qui diffusent le contenu Mastra dans un format compatible avec AI SDK. Une fois mises en œuvre, ces routes d'API peuvent être utilisées dans useChat().
- chatRoute()
- workflowRoute()
- networkRoute()
Cet exemple montre comment configurer, sur le point de terminaison /chat, une route de chat qui utilise un agent portant l'ID weatherAgent.
import { Mastra } from '@mastra/core'
import { chatRoute } from '@mastra/ai-sdk'
export const mastra = new Mastra({
server: {
apiRoutes: [
chatRoute({
path: '/chat',
agent: 'weatherAgent',
}),
],
},
})
Vous pouvez également utiliser le routage dynamique des agents. Consultez la documentation de référence de chatRoute() pour en savoir plus.
Cet exemple montre comment configurer, sur le point de terminaison /workflow, une route de workflow qui utilise un workflow portant l'ID weatherWorkflow.
import { Mastra } from '@mastra/core'
import { workflowRoute } from '@mastra/ai-sdk'
export const mastra = new Mastra({
server: {
apiRoutes: [
workflowRoute({
path: '/workflow',
workflow: 'weatherWorkflow',
}),
],
},
})
Vous pouvez également utiliser le routage dynamique des workflows. Consultez la documentation de référence de workflowRoute() pour en savoir plus.
Lorsqu'une étape de workflow redirige le stream d'un agent vers le writer du workflow (par exemple, await response.fullStream.pipeTo(writer)), les segments de texte et les appels de tools de l'agent sont transmis en temps réel au stream de l'interface utilisateur, même si l'agent s'exécute au sein d'étapes de workflow.
Consultez la section Streaming des workflows pour en savoir plus.
Cet exemple montre comment configurer, sur le point de terminaison /network, une route de réseau qui utilise un agent portant l'ID weatherAgent.
import { Mastra } from '@mastra/core'
import { networkRoute } from '@mastra/ai-sdk'
export const mastra = new Mastra({
server: {
apiRoutes: [
networkRoute({
path: '/network',
agent: 'weatherAgent',
}),
],
},
})
Vous pouvez également utiliser le routage dynamique des réseaux. Consultez la documentation de référence de networkRoute() pour en savoir plus.
Indépendante du frameworkLien direct vers Indépendante du framework
Si vous ne souhaitez pas exécuter le serveur Mastra et préférez utiliser des frameworks comme Next.js ou Express, vous pouvez employer les fonctions handleChatStream(), handleWorkflowStream() et handleNetworkStream() dans vos propres gestionnaires de routes d'API.
Elles renvoient un ReadableStream que vous pouvez encapsuler avec createUIMessageStreamResponse().
Les gestionnaires indépendants du framework conservent le comportement existant d'AI SDK v5, utilisé par défaut. Si votre application est typée avec AI SDK v6, transmettez version: 'v6'. Pour bénéficier de la meilleure inférence TypeScript avec handleChatStream() et handleNetworkStream(), transmettez messages sous la forme UIMessage[] provenant de la version de ai installée.
Les exemples ci-dessous montrent comment les utiliser avec Next.js App Router.
- handleChatStream()
- handleWorkflowStream()
- handleNetworkStream()
Cet exemple montre comment configurer, sur le point de terminaison /chat, une route de chat qui utilise un agent portant l'ID weatherAgent.
import { handleChatStream } from '@mastra/ai-sdk'
import { createUIMessageStreamResponse } from 'ai'
import { mastra } from '@/src/mastra'
export async function POST(req: Request) {
const params = await req.json()
const stream = await handleChatStream({
mastra,
agentId: 'weatherAgent',
params,
})
return createUIMessageStreamResponse({ stream })
}
Cet exemple montre comment configurer, sur le point de terminaison /workflow, une route de workflow qui utilise un workflow portant l'ID weatherWorkflow.
import { handleWorkflowStream } from '@mastra/ai-sdk'
import { createUIMessageStreamResponse } from 'ai'
import { mastra } from '@/src/mastra'
export async function POST(req: Request) {
const params = await req.json()
const stream = await handleWorkflowStream({
mastra,
workflowId: 'weatherWorkflow',
params,
})
return createUIMessageStreamResponse({ stream })
}
Cet exemple montre comment configurer, sur le point de terminaison /network, une route de réseau qui utilise un agent portant l'ID routingAgent.
import { handleNetworkStream } from '@mastra/ai-sdk'
import { createUIMessageStreamResponse } from 'ai'
import { mastra } from '@/src/mastra'
export async function POST(req: Request) {
const params = await req.json()
const stream = await handleNetworkStream({
mastra,
agentId: 'routingAgent',
params,
})
return createUIMessageStreamResponse({ stream })
}
useChat()Lien direct vers usechat
Que vous ayez créé des routes d'API avec le serveur Mastra ou utilisé le framework de votre choix, vous pouvez désormais employer les points de terminaison de l'API dans le hook useChat().
En supposant que vous ayez configuré sur /chat une route utilisant un agent météo, vous pouvez lui poser des questions comme dans l'exemple ci-dessous. Veillez à définir correctement l'URL api.
import { useChat } from '@ai-sdk/react'
import { useState } from 'react'
import { DefaultChatTransport } from 'ai'
export default function Chat() {
const [inputValue, setInputValue] = useState('')
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({
api: 'http://localhost:4111/chat',
}),
})
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault()
sendMessage({ text: inputValue })
}
return (
<div>
<pre>{JSON.stringify(messages, null, 2)}</pre>
<form onSubmit={handleFormSubmit}>
<input
value={inputValue}
onChange={e => setInputValue(e.target.value)}
placeholder="Name of the city"
/>
</form>
</div>
)
}
Utilisez prepareSendMessagesRequest pour personnaliser la requête envoyée à la route de chat, par exemple afin de transmettre une configuration supplémentaire à l'agent.
Utiliser la Memory de MastraLien direct vers Utiliser la Memory de Mastra
Lorsque la memory de votre agent est configurée, Mastra charge l'historique des conversations depuis le stockage sur le serveur. Depuis le client, envoyez uniquement le nouveau message plutôt que l'intégralité de l'historique.
L'envoi de l'historique complet est redondant et peut provoquer des erreurs d'ordre des messages, car les horodatages côté client peuvent entrer en conflit avec ceux stockés dans votre base de données.
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({
api: 'http://localhost:4111/chat/weatherAgent',
prepareSendMessagesRequest({ messages }) {
return {
body: {
messages: [messages[messages.length - 1]],
memory: {
thread: 'user-thread-123',
resource: 'user-123',
},
},
}
},
}),
})
Définissez memory.thread et memory.resource à partir de l'état propre à votre application, par exemple les paramètres d'URL, le contexte d'authentification ou votre base de données.
Consultez la section Historique des messages pour en savoir plus sur la façon dont la Memory de Mastra charge et stocke les messages.
chatRoute() et handleChatStream() fonctionnent déjà avec la Memory. Configurez le client pour qu'il envoie uniquement le nouveau message et inclue les identifiants de thread et de ressource.
useCompletion()Lien direct vers usecompletion
Le hook useCompletion() gère les complétions à un seul tour entre votre frontend et un agent Mastra, ce qui vous permet d'envoyer un prompt et de recevoir une réponse diffusée via HTTP.
Votre frontend pourrait ressembler à ceci :
import { useCompletion } from '@ai-sdk/react'
export default function Page() {
const { completion, input, handleInputChange, handleSubmit } = useCompletion({
api: '/api/completion',
})
return (
<form onSubmit={handleSubmit}>
<input name="prompt" value={input} onChange={handleInputChange} id="input" />
<button type="submit">Submit</button>
<div>{completion}</div>
</form>
)
}
Choisissez une implémentation backend :
- Mastra Server
- Next.js
import { Mastra } from '@mastra/core/mastra'
import { registerApiRoute } from '@mastra/core/server'
import { handleChatStream } from '@mastra/ai-sdk'
import { createUIMessageStreamResponse } from 'ai'
export const mastra = new Mastra({
server: {
apiRoutes: [
registerApiRoute('/completion', {
method: 'POST',
handler: async c => {
const { prompt } = await c.req.json()
const mastra = c.get('mastra')
const stream = await handleChatStream({
mastra,
agentId: 'weatherAgent',
params: {
messages: [
{
id: '1',
role: 'user',
parts: [
{
type: 'text',
text: prompt,
},
],
},
],
},
})
return createUIMessageStreamResponse({ stream })
},
}),
],
},
})
import { handleChatStream } from '@mastra/ai-sdk'
import { createUIMessageStreamResponse } from 'ai'
import { mastra } from '@/src/mastra'
// Allow streaming responses up to 30 seconds
export const maxDuration = 30
export async function POST(req: Request) {
const { prompt }: { prompt: string } = await req.json()
const stream = await handleChatStream({
mastra,
agentId: 'weatherAgent',
params: {
messages: [
{
id: '1',
role: 'user',
parts: [
{
type: 'text',
text: prompt,
},
],
},
],
},
})
return createUIMessageStreamResponse({ stream })
}
Interface utilisateur personnaliséeLien direct vers Interface utilisateur personnalisée
Custom UI (également appelée Generative UI) permet d'afficher des composants React personnalisés à partir de données diffusées par Mastra. Au lieu d'afficher du texte brut ou du JSON, vous pouvez créer des composants visuels pour les sorties de tools et la progression des workflows, notamment l'exécution des réseaux d'agents et les événements personnalisés.
Utilisez Custom UI lorsque vous souhaitez :
- Afficher les sorties de tools sous forme de composants visuels (par exemple, une carte météo plutôt que du JSON)
- Afficher la progression des étapes d'un workflow à l'aide d'indicateurs d'état
- Visualiser l'exécution d'un réseau d'agents avec des mises à jour étape par étape
- Afficher des indicateurs de progression ou des mises à jour d'état pendant les opérations de longue durée
Types de parties de donnéesLien direct vers Types de parties de données
Mastra diffuse les données vers le frontend sous forme de « parties » au sein des messages. Chaque partie possède un type qui détermine son rendu. Le package @mastra/ai-sdk transforme les streams Mastra en UI Message DataParts compatibles avec AI SDK.
| Type de partie de données | Source | Description |
|---|---|---|
tool-{toolKey} | Fonctionnalité intégrée d'AI SDK | Appel de Tool avec les états : input-available, output-available, output-error |
data-workflow | workflowRoute() | Instantanés de l'état d'exécution du workflow avec l'état des étapes et les sorties finales |
data-workflow-step | workflowRoute() | Delta d'une étape de workflow avec le payload complet de l'étape modifiée |
data-network | networkRoute() | Exécution du réseau d'agents avec les étapes et sorties ordonnées |
data-tool-agent | Agent imbriqué dans un Tool | Instantané compact de l'agent imbriqué pendant que l'étape courante est encore en cours |
data-tool-agent-step | Agent imbriqué dans un Tool | Payload complet de l'étape de l'agent imbriqué émis à la fin d'une étape imbriquée |
data-tool-workflow | Workflow imbriqué dans un Tool | Sortie du workflow diffusée depuis la fonction execute() d'un Tool |
data-tool-network | Réseau imbriqué dans un Tool | Sortie du réseau diffusée depuis la fonction execute() d'un Tool |
data-{custom} | writer.custom() | Événements personnalisés pour les indicateurs de progression, les mises à jour d'état, etc. |
Afficher les sorties des ToolsLien direct vers Afficher les sorties des Tools
AI SDK crée automatiquement des parties tool-{toolKey} lorsqu'un agent appelle un Tool. Ces parties comprennent l'état et la sortie du Tool, que vous pouvez utiliser pour afficher des composants personnalisés.
La partie du Tool passe successivement par les états suivants :
input-streaming: l'entrée du Tool est en cours de diffusion (lorsque le streaming des appels de Tools est activé)input-available: le Tool a été appelé avec une entrée complète et attend son exécutionoutput-available: l'exécution du Tool est terminée et a produit une sortieoutput-error: l'exécution du Tool a échoué
Voici un exemple de rendu de la sortie d'un Tool météo sous forme de composant WeatherCard personnalisé.
- Backend
- Frontend
Définissez un Tool avec un outputSchema afin que le frontend connaisse la structure des données à afficher.
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
export const weatherTool = createTool({
id: 'get-weather',
description: 'Get current weather for a location',
inputSchema: z.object({
location: z.string().describe('The location to get the weather for'),
}),
outputSchema: z.object({
temperature: z.number(),
feelsLike: z.number(),
humidity: z.number(),
windSpeed: z.number(),
conditions: z.string(),
location: z.string(),
}),
execute: async inputData => {
const response = await fetch(
`https://api.weatherapi.com/v1/current.json?key=${process.env.WEATHER_API_KEY}&q=${inputData.location}`,
)
const data = await response.json()
return {
temperature: data.current.temp_c,
feelsLike: data.current.feelslike_c,
humidity: data.current.humidity,
windSpeed: data.current.wind_kph,
conditions: data.current.condition.text,
location: data.location.name,
}
},
})
Recherchez les parties tool-{toolKey} dans le message et affichez un composant personnalisé en fonction de l'état et de la sortie du Tool.
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import { WeatherCard } from './weather-card'
import { Loader } from './loader'
export function Chat() {
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({
api: 'http://localhost:4111/chat/weatherAgent',
}),
})
return (
<div>
{messages.map(message => (
<div key={message.id}>
{message.parts.map((part, index) => {
// Handle user text messages
if (part.type === 'text' && message.role === 'user') {
return <p key={index}>{part.text}</p>
}
// Handle weather tool output
if (part.type === 'tool-weatherTool') {
switch (part.state) {
case 'input-available':
return <Loader key={index} />
case 'output-available':
return <WeatherCard key={index} {...part.output} />
case 'output-error':
return <div key={index}>Error: {part.errorText}</div>
default:
return null
}
}
return null
})}
</div>
))}
</div>
)
}
Le type de partie du Tool suit le modèle tool-{toolKey}, où toolKey est la clé utilisée lors de l'enregistrement du Tool auprès de l'agent. Par exemple, si vous enregistrez des Tools sous la forme tools: { weatherTool }, le type de la partie sera tool-weatherTool.
Afficher les données de workflowLien direct vers Afficher les données de workflow
Lorsque vous utilisez workflowRoute() ou handleWorkflowStream(), Mastra émet des parties data-workflow pour les instantanés de l'état du workflow et des parties data-workflow-step pour le payload complet de l'étape modifiée. Les workflows de longue durée évitent ainsi de répéter la sortie de chaque étape terminée dans tous les instantanés intermédiaires.
- Backend
- Frontend
Définissez un workflow en plusieurs étapes qui émettra des parties data-workflow et data-workflow-step au fil de son exécution.
import { createStep, createWorkflow } from '@mastra/core/workflows'
import { z } from 'zod'
const fetchWeather = createStep({
id: 'fetch-weather',
inputSchema: z.object({
location: z.string(),
}),
outputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
}),
execute: async ({ inputData }) => {
// Fetch weather data...
return { temperature: 22, conditions: 'Sunny' }
},
})
const planActivities = createStep({
id: 'plan-activities',
inputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
}),
outputSchema: z.object({
activities: z.string(),
}),
execute: async ({ inputData, mastra }) => {
const agent = mastra?.getAgent('activityAgent')
const response = await agent?.generate(
`Suggest activities for ${inputData.conditions} weather at ${inputData.temperature}°C`,
)
return { activities: response?.text || '' }
},
})
export const activitiesWorkflow = createWorkflow({
id: 'activities-workflow',
inputSchema: z.object({
location: z.string(),
}),
outputSchema: z.object({
activities: z.string(),
}),
})
.then(fetchWeather)
.then(planActivities)
activitiesWorkflow.commit()
Enregistrez le workflow auprès de Mastra et exposez-le avec workflowRoute() afin de diffuser les événements du workflow vers le frontend.
import { Mastra } from '@mastra/core'
import { workflowRoute } from '@mastra/ai-sdk'
export const mastra = new Mastra({
workflows: { activitiesWorkflow },
server: {
apiRoutes: [
workflowRoute({
path: '/workflow/activitiesWorkflow',
workflow: 'activitiesWorkflow',
}),
],
},
})
Recherchez les parties data-workflow afin d'afficher les instantanés de l'état du workflow. Si vous avez besoin du payload complet de l'étape qui vient d'être modifiée, lisez également les parties data-workflow-step.
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import type { WorkflowDataPart, WorkflowStepDataPart } from '@mastra/ai-sdk'
type WorkflowData = WorkflowDataPart['data']
type WorkflowStepData = WorkflowStepDataPart['data']
type StepStatus = 'running' | 'success' | 'failed' | 'suspended' | 'waiting'
function StepIndicator({
name,
status,
output,
}: {
name: string
status: StepStatus
output: unknown
}) {
return (
<div className="step">
<div className="step-header">
<span>{name}</span>
<span className={`status status-${status}`}>{status}</span>
</div>
{status === 'success' && output && <pre>{JSON.stringify(output, null, 2)}</pre>}
</div>
)
}
export function WorkflowChat() {
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({
api: 'http://localhost:4111/workflow/activitiesWorkflow',
prepareSendMessagesRequest: ({ messages }) => ({
body: {
inputData: {
location: messages[messages.length - 1]?.parts[0]?.text,
},
},
}),
}),
})
return (
<div>
{messages.map(message => (
<div key={message.id}>
{message.parts.map((part, index) => {
if (part.type === 'data-workflow') {
const workflowData = part.data as WorkflowData
const steps = Object.values(workflowData.steps)
return (
<div key={index} className="workflow-progress">
<h3>Workflow: {workflowData.name}</h3>
<p>Status: {workflowData.status}</p>
{steps.map(step => (
<StepIndicator
key={step.name}
name={step.name}
status={step.status}
output={step.output}
/>
))}
</div>
)
}
if (part.type === 'data-workflow-step') {
const stepData = part.data as WorkflowStepData
return (
<StepIndicator
key={index}
name={stepData.step.name}
status={stepData.step.status}
output={stepData.step.output}
/>
)
}
return null
})}
</div>
))}
</div>
)
}
Pour en savoir plus sur le streaming des workflows, consultez la section Streaming des workflows.
Afficher les données de réseauLien direct vers Afficher les données de réseau
Lorsque vous utilisez networkRoute() ou handleNetworkStream(), Mastra émet des parties data-network qui contiennent l'état d'exécution du réseau d'agents, notamment les agents appelés et leurs sorties.
- Backend
- Frontend
Enregistrez les agents auprès de Mastra et exposez l'agent de routage avec networkRoute() afin de diffuser les événements d'exécution du réseau vers le frontend.
import { Mastra } from '@mastra/core'
import { networkRoute } from '@mastra/ai-sdk'
export const mastra = new Mastra({
agents: { routingAgent, researchAgent, weatherAgent },
server: {
apiRoutes: [
networkRoute({
path: '/network',
agent: 'routingAgent',
}),
],
},
})
Recherchez les parties data-network et affichez l'étape d'exécution de chaque agent en utilisant le type NetworkDataPart pour garantir la sûreté des types.
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import type { NetworkDataPart } from '@mastra/ai-sdk'
type NetworkData = NetworkDataPart['data']
function AgentStep({ step }: { step: NetworkData['steps'][number] }) {
return (
<div className="agent-step">
<div className="step-header">
<span className="agent-name">{step.name}</span>
<span className={`status status-${step.status}`}>{step.status}</span>
</div>
{step.input && (
<div className="step-input">
<strong>Input:</strong>
<pre>{JSON.stringify(step.input, null, 2)}</pre>
</div>
)}
{step.output && (
<div className="step-output">
<strong>Output:</strong>
<pre>
{typeof step.output === 'string' ? step.output : JSON.stringify(step.output, null, 2)}
</pre>
</div>
)}
</div>
)
}
export function NetworkChat() {
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({
api: 'http://localhost:4111/network',
}),
})
return (
<div>
{messages.map(message => (
<div key={message.id}>
{message.parts.map((part, index) => {
if (part.type === 'data-network') {
const networkData = part.data as NetworkData
return (
<div key={index} className="network-execution">
<div className="network-header">
<h3>Agent Network: {networkData.name}</h3>
<span className={`status status-${networkData.status}`}>
{networkData.status}
</span>
</div>
<div className="network-steps">
{networkData.steps.map((step, stepIndex) => (
<AgentStep key={stepIndex} step={step} />
))}
</div>
</div>
)
}
return null
})}
</div>
))}
</div>
)
}
Pour en savoir plus sur les réseaux d'agents, consultez la section Réseaux d'agents.
Événements personnalisésLien direct vers Événements personnalisés
Utilisez writer.custom() dans la fonction execute() d'un Tool afin d'émettre des parties de données personnalisées. Cette méthode est utile pour les indicateurs de progression, les mises à jour d'état ou toute mise à jour personnalisée de l'interface utilisateur pendant l'exécution du Tool.
Les types d'événements personnalisés doivent commencer par data- pour être reconnus comme des parties de données.
Vous devez utiliser await lors de l'appel à writer.custom(), faute de quoi vous risquez de rencontrer une erreur WritableStream is locked.
- Backend
- Frontend
Utilisez writer.custom() dans la fonction execute() du Tool afin d'émettre, à différentes étapes de l'exécution, des événements personnalisés préfixés par data-.
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
export const taskTool = createTool({
id: 'process-task',
description: 'Process a task with progress updates',
inputSchema: z.object({
task: z.string().describe('The task to process'),
}),
outputSchema: z.object({
result: z.string(),
status: z.string(),
}),
execute: async (inputData, context) => {
const { task } = inputData
// Emit "in progress" custom event
await context?.writer?.custom({
type: 'data-tool-progress',
data: {
status: 'in-progress',
message: 'Gathering information...',
},
})
// Simulate work
await new Promise(resolve => setTimeout(resolve, 3000))
// Emit "done" custom event
await context?.writer?.custom({
type: 'data-tool-progress',
data: {
status: 'done',
message: `Successfully processed "${task}"`,
},
})
return {
result: `Task "${task}" has been completed successfully!`,
status: 'completed',
}
},
})
Filtrez les parties du message selon votre type d'événement personnalisé et affichez un indicateur de progression qui s'actualise à mesure que de nouveaux événements arrivent.
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import { useMemo } from 'react'
type ProgressData = {
status: 'in-progress' | 'done'
message: string
}
function ProgressIndicator({ progress }: { progress: ProgressData }) {
return (
<div className="progress-indicator">
{progress.status === 'in-progress' ? (
<span className="spinner" />
) : (
<span className="check-icon" />
)}
<span className={`status-${progress.status}`}>{progress.message}</span>
</div>
)
}
export function TaskChat() {
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({
api: 'http://localhost:4111/chat/taskAgent',
}),
})
// Extract the latest progress event from messages
const latestProgress = useMemo(() => {
const allProgressParts: ProgressData[] = []
messages.forEach(message => {
message.parts.forEach(part => {
if (part.type === 'data-tool-progress') {
allProgressParts.push(part.data as ProgressData)
}
})
})
return allProgressParts[allProgressParts.length - 1]
}, [messages])
return (
<div>
{latestProgress && <ProgressIndicator progress={latestProgress} />}
{messages.map(message => (
<div key={message.id}>
{message.parts.map((part, index) => {
if (part.type === 'text') {
return <p key={index}>{part.text}</p>
}
return null
})}
</div>
))}
</div>
)
}
Streaming des ToolsLien direct vers Streaming des Tools
Les Tools peuvent également diffuser des données à l'aide de context.writer.write() pour un contrôle de plus bas niveau, ou rediriger directement le stream d'un agent vers le writer du Tool. Pour en savoir plus, consultez la section Streaming des Tools.
ExemplesLien direct vers Exemples
Pour voir des exemples interactifs de modèles Custom UI, consultez le UI Dojo de Mastra. Le dépôt contient des implémentations pour :
- Interfaces utilisateur génératives : composants personnalisés pour les sorties de Tools
- Workflows : visualisation des étapes du workflow
- Réseaux d'agents : affichage de l'exécution du réseau
- Événements personnalisés : indicateurs de progression avec des événements personnalisés
RecettesLien direct vers Recettes
Transformer les streamsLien direct vers Transformer les streams
Pour transformer manuellement les streams Mastra dans un format compatible avec AI SDK, utilisez l'utilitaire toAISdkStream(). Consultez les exemples pour découvrir des modèles d'utilisation concrets.
toAISdkStream() conserve le comportement existant d'AI SDK v5, utilisé par défaut. Si votre application est typée avec AI SDK v6, transmettez version: 'v6'.
import { toAISdkStream } from '@mastra/ai-sdk'
const v5Stream = toAISdkStream(mastraStream, { from: 'agent' })
const v6Stream = toAISdkStream(mastraStream, { from: 'agent', version: 'v6' })
Charger les messages historiquesLien direct vers Charger les messages historiques
Lorsque vous chargez des messages depuis la Memory de Mastra pour les afficher dans une interface de chat, utilisez toAISdkV5Messages() ou toAISdkV4Messages() afin de les convertir au format AI SDK adapté à useChat(), dans sa propriété initialMessages.
Transmettre des données supplémentairesLien direct vers Transmettre des données supplémentaires
sendMessage() permet de transmettre des données supplémentaires du frontend à Mastra. Ces données peuvent ensuite être utilisées sur le serveur sous la forme d'un RequestContext.
Voici un exemple de code frontend :
import { useChat } from '@ai-sdk/react'
import { useState } from 'react'
import { DefaultChatTransport } from 'ai'
export function ChatAdditional() {
const [inputValue, setInputValue] = useState('')
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({
api: 'http://localhost:4111/chat-extra',
}),
})
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault()
sendMessage(
{ text: inputValue },
{
body: {
data: {
userId: 'user123',
preferences: {
language: 'en',
temperature: 'celsius',
},
},
},
},
)
}
return (
<div>
<pre>{JSON.stringify(messages, null, 2)}</pre>
<form onSubmit={handleFormSubmit}>
<input
value={inputValue}
onChange={e => setInputValue(e.target.value)}
placeholder="Name of the city"
/>
</form>
</div>
)
}
Implémentez le backend à l'aide de l'un des exemples suivants.
- Mastra Server
- Next.js
Ajoutez une chatRoute() à votre configuration Mastra comme illustré ci-dessus. Ajoutez ensuite un middleware au niveau du serveur :
import { Mastra } from '@mastra/core'
export const mastra = new Mastra({
server: {
middleware: [
async (c, next) => {
const requestContext = c.get('requestContext')
if (c.req.method === 'POST') {
const clonedReq = c.req.raw.clone()
const body = await clonedReq.json()
if (body?.data) {
for (const [key, value] of Object.entries(body.data)) {
requestContext.set(key, value)
}
}
}
await next()
},
],
},
})
Vous pouvez accéder à ces données dans vos Tools à l'aide du paramètre requestContext. Consultez la documentation sur Request Context pour en savoir plus.
import { handleChatStream } from '@mastra/ai-sdk'
import { RequestContext } from '@mastra/core/request-context'
import { createUIMessageStreamResponse } from 'ai'
import { mastra } from '@/src/mastra'
export async function POST(req: Request) {
const { messages, data } = await req.json()
const requestContext = new RequestContext()
if (data) {
for (const [key, value] of Object.entries(data)) {
requestContext.set(key, value)
}
}
const stream = await handleChatStream({
mastra,
agentId: 'weatherAgent',
params: {
messages,
requestContext,
},
})
return createUIMessageStreamResponse({ stream })
}
Suspendre et reprendre un workflow avec l'approbation de l'utilisateurLien direct vers Suspendre et reprendre un workflow avec l'approbation de l'utilisateur
Les workflows peuvent suspendre leur exécution et attendre une saisie de l'utilisateur avant de continuer. Cette fonctionnalité est utile pour les processus d'approbation, les confirmations ou tout scénario impliquant une intervention humaine.
Le workflow utilise les éléments suivants :
suspendSchema/resumeSchema: définissent la structure des données du payload de suspension et de l'entrée de reprisesuspend(): suspend le workflow et envoie le payload de suspension à l'interface utilisateurresumeData: contient la réponse de l'utilisateur lors de la reprise du workflowbail(): met fin au workflow de façon anticipée (par exemple, lorsque l'utilisateur refuse)
- Backend
- Frontend
Créez une étape de workflow qui se suspend dans l'attente d'une approbation. L'étape examine resumeData pour déterminer s'il s'agit d'une reprise et appelle suspend() lors de la première exécution.
import { createStep, createWorkflow } from '@mastra/core/workflows'
import { z } from 'zod'
const requestApproval = createStep({
id: 'request-approval',
inputSchema: z.object({ requestId: z.string(), summary: z.string() }),
outputSchema: z.object({
approved: z.boolean(),
requestId: z.string(),
approvedBy: z.string().optional(),
}),
resumeSchema: z.object({
approved: z.boolean(),
approverName: z.string().optional(),
}),
suspendSchema: z.object({
message: z.string(),
requestId: z.string(),
}),
execute: async ({ inputData, resumeData, suspend, bail }) => {
// User rejected - bail out
if (resumeData?.approved === false) {
return bail({ message: 'Request rejected' })
}
// User approved - continue
if (resumeData?.approved) {
return {
approved: true,
requestId: inputData.requestId,
approvedBy: resumeData.approverName || 'User',
}
}
// First execution - suspend and wait
return await suspend({
message: `Please approve: ${inputData.summary}`,
requestId: inputData.requestId,
})
},
})
export const approvalWorkflow = createWorkflow({
id: 'approval-workflow',
inputSchema: z.object({ requestId: z.string(), summary: z.string() }),
outputSchema: z.object({
approved: z.boolean(),
requestId: z.string(),
approvedBy: z.string().optional(),
}),
}).then(requestApproval)
approvalWorkflow.commit()
Enregistrez le workflow. Un stockage est nécessaire pour conserver l'état lors des opérations de suspension et de reprise.
import { Mastra } from '@mastra/core'
import { workflowRoute } from '@mastra/ai-sdk'
import { LibSQLStore } from '@mastra/libsql'
export const mastra = new Mastra({
workflows: { approvalWorkflow },
storage: new LibSQLStore({
id: 'mastra-storage',
url: 'file:../mastra.db',
}),
server: {
apiRoutes: [
workflowRoute({ path: '/workflow/approvalWorkflow', workflow: 'approvalWorkflow' }),
],
},
})
Détectez la suspension du workflow et envoyez les données de reprise avec runId, step et resumeData.
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import { useMemo, useState } from 'react'
import type { WorkflowDataPart } from '@mastra/ai-sdk'
type WorkflowData = WorkflowDataPart['data']
export function ApprovalWorkflow() {
const [requestId, setRequestId] = useState('')
const [summary, setSummary] = useState('')
const { messages, sendMessage, setMessages, status } = useChat({
transport: new DefaultChatTransport({
api: 'http://localhost:4111/workflow/approvalWorkflow',
prepareSendMessagesRequest: ({ messages }) => {
const lastMessage = messages[messages.length - 1]
const text = lastMessage.parts.find(p => p.type === 'text')?.text
const metadata = lastMessage.metadata as Record<string, string>
// Resuming: send runId, step, and resumeData
if (text === 'Approve' || text === 'Reject') {
return {
body: {
runId: metadata.runId,
step: 'request-approval',
resumeData: { approved: text === 'Approve' },
},
}
}
// Starting: send inputData
return {
body: { inputData: { requestId: metadata.requestId, summary: metadata.summary } },
}
},
}),
})
// Find suspended workflow
const suspended = useMemo(() => {
for (const m of messages) {
for (const p of m.parts) {
if (p.type === 'data-workflow' && (p.data as WorkflowData).status === 'suspended') {
return { data: p.data as WorkflowData, runId: p.id }
}
}
}
return null
}, [messages])
const handleApprove = () => {
setMessages([])
sendMessage({ text: 'Approve', metadata: { runId: suspended?.runId } })
}
const handleReject = () => {
setMessages([])
sendMessage({ text: 'Reject', metadata: { runId: suspended?.runId } })
}
return (
<div>
{!suspended ? (
<form
onSubmit={e => {
e.preventDefault()
setMessages([])
sendMessage({ text: 'Start', metadata: { requestId, summary } })
}}
>
<input
value={requestId}
onChange={e => setRequestId(e.target.value)}
placeholder="Request ID"
/>
<input value={summary} onChange={e => setSummary(e.target.value)} placeholder="Summary" />
<button type="submit" disabled={status !== 'ready'}>
Submit
</button>
</form>
) : (
<div>
<p>
{
(suspended.data.steps['request-approval']?.suspendPayload as { message: string })
?.message
}
</p>
<button onClick={handleApprove}>Approve</button>
<button onClick={handleReject}>Reject</button>
</div>
)}
</div>
)
}
Points essentiels :
- Le payload de suspension est accessible via
step.suspendPayload - Pour reprendre le workflow, envoyez
runId,step(l'ID de l'étape) etresumeDatadans le corps de la requête - Le stockage doit être configuré afin de conserver l'état du workflow lors des opérations de suspension et de reprise
Pour consulter une implémentation complète, reportez-vous à l'exemple workflow-suspend-resume dans UI Dojo.
Streams d'agents imbriqués dans des ToolsLien direct vers Streams d'agents imbriqués dans des Tools
Les Tools peuvent appeler des agents en interne et rediffuser la sortie de l'agent vers l'interface utilisateur. Cela crée des instantanés data-tool-agent compacts tant que l'étape imbriquée est encore en cours, des parties data-tool-agent-step lorsqu'une étape imbriquée se termine et un instantané data-tool-agent complet à la fin de l'exécution imbriquée.
Ce modèle utilise les éléments suivants :
context.mastra.getAgent(): récupère une instance d'agent depuis un Toolagent.stream(): diffuse la réponse de l'agentstream.fullStream.pipeTo(context.writer): redirige le stream de l'agent vers le writer du Tool
- Backend
- Frontend
Créez un Tool qui appelle un agent et redirige son stream vers le writer du Tool.
import { createTool } from '@mastra/core/tools'
import { z } from 'zod'
export const nestedAgentTool = createTool({
id: 'nested-agent-stream',
description: 'Analyze weather using a nested agent',
inputSchema: z.object({
city: z.string().describe('The city to analyze'),
}),
outputSchema: z.object({
summary: z.string(),
}),
execute: async (inputData, context) => {
const agent = context?.mastra?.getAgent('weatherAgent')
if (!agent) {
return { summary: 'Weather agent not available' }
}
const stream = await agent.stream(
`Analyze the weather in ${inputData.city} and provide a summary.`,
)
// Pipe the agent's stream to emit data-tool-agent parts
await stream.fullStream.pipeTo(context!.writer!)
return { summary: (await stream.text) ?? 'No summary available' }
},
})
Créez un agent qui utilise ce Tool.
import { Agent } from '@mastra/core/agent'
import { nestedAgentTool } from '../tools/nested-agent-tool'
export const forecastAgent = new Agent({
id: 'forecast-agent',
instructions: 'Use the nested-agent-stream tool when asked about weather.',
model: 'openai/gpt-5.6-sol',
tools: { nestedAgentTool },
})
Traitez les parties data-tool-agent pour l'instantané en direct et les parties data-tool-agent-step pour le payload de l'étape imbriquée terminée.
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import { useState } from 'react'
import type { AgentDataPart, AgentStepDataPart } from '@mastra/ai-sdk'
export function NestedAgentChat() {
const [input, setInput] = useState('')
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({
api: 'http://localhost:4111/chat/forecastAgent',
}),
})
return (
<div>
<form
onSubmit={e => {
e.preventDefault()
sendMessage({ text: input })
setInput('')
}}
>
<input value={input} onChange={e => setInput(e.target.value)} placeholder="Enter a city" />
<button type="submit" disabled={status !== 'ready'}>
Get Forecast
</button>
</form>
{messages.map(message => (
<div key={message.id}>
{message.parts.map((part, index) => {
if (part.type === 'text') {
return <p key={index}>{part.text}</p>
}
if (part.type === 'data-tool-agent') {
const { id, data } = part as AgentDataPart
return (
<div key={index} className="nested-agent">
<strong>Nested Agent: {id}</strong>
{data.text && <p>{data.text}</p>}
</div>
)
}
if (part.type === 'data-tool-agent-step') {
const { data } = part as AgentStepDataPart
return (
<div key={index} className="nested-agent-step">
<strong>Completed nested step {data.stepIndex + 1}</strong>
{data.step.text && <p>{data.step.text}</p>}
</div>
)
}
return null
})}
</div>
))}
</div>
)
}
Points essentiels :
- Rediriger
fullStreamverscontext.writercrée des partiesdata-tool-agent - Lisez
data-tool-agent-steplorsque vous avez besoin du payload complet de l'étape imbriquée qui vient de se terminer AgentDataPartpossèdeid(sur la partie) etdata.text(l'instantané de texte actuel de l'agent imbriqué)- Le Tool renvoie toujours sa propre sortie une fois le stream terminé
Pour consulter une implémentation complète, reportez-vous à l'exemple tool-nested-streams dans UI Dojo.
Diffuser le texte d'un agent depuis les étapes d'un workflowLien direct vers Diffuser le texte d'un agent depuis les étapes d'un workflow
Les étapes d'un workflow peuvent diffuser la sortie textuelle d'un agent en temps réel en redirigeant le stream de l'agent vers le writer de l'étape. Les utilisateurs peuvent ainsi voir l'agent « réfléchir » pendant l'exécution du workflow, au lieu d'attendre la fin de l'étape.
Ce modèle utilise les éléments suivants :
writerdans l'étape du workflow : redirige lefullStreamde l'agent vers le writer de l'étape- Parties
textetdata-workflow: le frontend reçoit le texte diffusé en même temps que la progression de l'étape
- Backend
- Frontend
Créez une étape de workflow qui diffuse la réponse d'un agent en la redirigeant vers le writer de l'étape.
import { createStep, createWorkflow } from '@mastra/core/workflows'
import { z } from 'zod'
import { weatherAgent } from '../agents/weather-agent'
const analyzeWeather = createStep({
id: 'analyze-weather',
inputSchema: z.object({ location: z.string() }),
outputSchema: z.object({ analysis: z.string(), location: z.string() }),
execute: async ({ inputData, writer }) => {
const response = await weatherAgent.stream(
`Analyze the weather in ${inputData.location} and provide insights.`,
)
// Pipe agent stream to step writer for real-time text streaming
await response.fullStream.pipeTo(writer)
return {
analysis: await response.text,
location: inputData.location,
}
},
})
const calculateScore = createStep({
id: 'calculate-score',
inputSchema: z.object({ analysis: z.string(), location: z.string() }),
outputSchema: z.object({ score: z.number(), summary: z.string() }),
execute: async ({ inputData }) => {
const score = inputData.analysis.includes('sunny') ? 85 : 50
return { score, summary: `Comfort score for ${inputData.location}: ${score}/100` }
},
})
export const weatherWorkflow = createWorkflow({
id: 'weather-workflow',
inputSchema: z.object({ location: z.string() }),
outputSchema: z.object({ score: z.number(), summary: z.string() }),
})
.then(analyzeWeather)
.then(calculateScore)
weatherWorkflow.commit()
Enregistrez le workflow avec une workflowRoute(). Le streaming du texte est activé par défaut.
import { Mastra } from '@mastra/core'
import { workflowRoute } from '@mastra/ai-sdk'
export const mastra = new Mastra({
agents: { weatherAgent },
workflows: { weatherWorkflow },
server: {
apiRoutes: [workflowRoute({ path: '/workflow/weather', workflow: 'weatherWorkflow' })],
},
})
Affichez à la fois les parties text (sortie diffusée de l'agent) et les parties data-workflow (progression de l'étape).
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
import { useState } from 'react'
import type { WorkflowDataPart } from '@mastra/ai-sdk'
type WorkflowData = WorkflowDataPart['data']
export function WeatherWorkflow() {
const [location, setLocation] = useState('')
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({
api: 'http://localhost:4111/workflow/weather',
prepareSendMessagesRequest: ({ messages }) => ({
body: {
inputData: {
location: messages[messages.length - 1].parts.find(p => p.type === 'text')?.text,
},
},
}),
}),
})
return (
<div>
<form
onSubmit={e => {
e.preventDefault()
sendMessage({ text: location })
setLocation('')
}}
>
<input
value={location}
onChange={e => setLocation(e.target.value)}
placeholder="Enter city"
/>
<button type="submit" disabled={status !== 'ready'}>
Analyze
</button>
</form>
{messages.map(message => (
<div key={message.id}>
{message.parts.map((part, index) => {
// Streaming agent text
if (part.type === 'text' && message.role === 'assistant') {
return (
<div key={index}>
{status === 'streaming' && (
<p>
<em>Agent analyzing...</em>
</p>
)}
<p>{part.text}</p>
</div>
)
}
// Workflow step progress
if (part.type === 'data-workflow') {
const workflow = part.data as WorkflowData
return (
<div key={index}>
{Object.entries(workflow.steps).map(([stepId, step]) => (
<div key={stepId}>
<strong>{stepId}</strong>: {step.status}
</div>
))}
</div>
)
}
return null
})}
</div>
))}
</div>
)
}
Points essentiels :
- Le
writerde l'étape est disponible dans la fonctionexecute(et non viacontext) includeTextStreamPartsvauttruepar défaut surworkflowRoute(); le texte est donc diffusé par défaut- Les parties textuelles sont diffusées en temps réel tandis que les parties
data-workflowsont mises à jour avec l'état de l'étape
Pour consulter une implémentation complète, reportez-vous à l'exemple workflow-agent-text-stream dans UI Dojo.
Progression en plusieurs étapes avec des workflows à branchementsLien direct vers Progression en plusieurs étapes avec des workflows à branchements
Pour les workflows comportant des branchements conditionnels (par exemple, livraison express ou standard), vous pouvez suivre la progression dans les différentes branches en incluant un identifiant dans vos événements personnalisés.
L'exemple UI Dojo utilise un champ stage dans les données de l'événement pour identifier la branche en cours d'exécution (par exemple, "validation", "standard-processing", "express-processing"). Le frontend regroupe les événements selon ce champ afin d'afficher une interface de progression sous forme de pipeline.
Consultez les fichiers branching-workflow.ts (backend) et workflow-custom-events.tsx (frontend) dans UI Dojo.
Indicateurs de progression dans les réseaux d'agentsLien direct vers Indicateurs de progression dans les réseaux d'agents
Lorsque vous utilisez des réseaux d'agents, vous pouvez émettre des événements de progression personnalisés depuis les Tools employés par les sous-agents afin d'indiquer l'agent actuellement actif.
L'exemple UI Dojo inclut un champ stage dans les données de l'événement afin d'identifier le sous-agent en cours d'exécution (par exemple, "report-generation", "report-review"). Le frontend regroupe les événements selon ce champ et affiche l'état le plus récent de chacun.
Consultez les fichiers report-generation-tool.ts (backend) et agent-network-custom-events.tsx (frontend) dans UI Dojo.