streamText Multi-Step Agent
You may want to have different steps in your stream where each step has different settings, e.g. models, tools, or system prompts.
With createUIMessageStream
and sendFinish
/ sendStart
options when merging
into the UIMessageStream
, you can control when the finish and start events are sent to the client,
allowing you to have different steps in a single assistant UI message.
Server
app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';import { convertToModelMessages, createUIMessageStream, createUIMessageStreamResponse, streamText, tool,} from 'ai';import { z } from 'zod';
export async function POST(req: Request) { const { messages } = await req.json();
const stream = createUIMessageStream({ execute: async ({ writer }) => { // step 1 example: forced tool call const result1 = streamText({ model: openai('gpt-4o-mini'), system: 'Extract the user goal from the conversation.', messages, toolChoice: 'required', // force the model to call a tool tools: { extractGoal: tool({ inputSchema: z.object({ goal: z.string() }), execute: async ({ goal }) => goal, // no-op extract tool }), }, });
// forward the initial result to the client without the finish event: writer.merge(result1.toUIMessageStream({ sendFinish: false }));
// note: you can use any programming construct here, e.g. if-else, loops, etc. // workflow programming is normal programming with this approach.
// example: continue stream with forced tool call from previous step const result2 = streamText({ // different system prompt, different model, no tools: model: openai('gpt-4o'), system: 'You are a helpful assistant with a different system prompt. Repeat the extract user goal in your answer.', // continue the workflow stream with the messages from the previous step: messages: [ ...convertToModelMessages(messages), ...(await result1.response).messages, ], });
// forward the 2nd result to the client (incl. the finish event): writer.merge(result2.toUIMessageStream({ sendStart: false })); }, });
return createUIMessageStreamResponse({ stream });}
Client
app/page.tsx
'use client';
import { useChat } from '@ai-sdk/react';import { useState } from 'react';
export default function Chat() { const [input, setInput] = useState(''); const { messages, sendMessage } = useChat();
return ( <div> {messages?.map(message => ( <div key={message.id}> <strong>{`${message.role}: `}</strong> {message.parts.map((part, index) => { switch (part.type) { case 'text': return <span key={index}>{part.text}</span>; case 'tool-extractGoal': { return <pre key={index}>{JSON.stringify(part, null, 2)}</pre>; } } })} </div> ))} <form onSubmit={e => { e.preventDefault(); sendMessage({ text: input }); setInput(''); }} > <input value={input} onChange={e => setInput(e.currentTarget.value)} /> </form> </div> );}