Learn how to create your own agentic loop with full control over tool execution
When you need complete control over the agentic loop and tool execution, you can manage the agent flow yourself rather than using prepareStep and stopWhen. This approach gives you full flexibility over when and how tools are executed, message history management, and loop termination conditions.
This pattern is useful when you want to:
Implement custom logic between tool calls
Handle tool execution errors in specific ways
Add custom logging or monitoring
Integrate with external systems during the loop
Have complete control over the conversation history
import { ModelMessage, streamText, tool } from 'ai';import 'dotenv/config';import z from 'zod';const getWeather = async ({ location }: { location: string }) => { return `The weather in ${location} is ${Math.floor(Math.random() * 100)} degrees.`;};const messages: ModelMessage[] = [ { role: 'user', content: 'Get the weather in New York and San Francisco', },];async function main() { while (true) { const result = streamText({ model: 'openai/gpt-6-astra', messages, tools: { getWeather: tool({ description: 'Get the current weather in a given location', inputSchema: z.object({ location: z.string(), }), }), // add more tools here, omitting the execute function so you handle it yourself }, }); // Stream the response (only necessary for providing updates to the user) for await (const chunk of result.stream) { if (chunk.type === 'text-delta') { process.stdout.write(chunk.text); } if (chunk.type === 'tool-call') { console.log('\\nCalling tool:', chunk.toolName); } } // Add LLM generated messages to the message history const responseMessages = await result.responseMessages; messages.push(...responseMessages); const finishReason = await result.finishReason; if (finishReason === 'tool-calls') { const toolCalls = await result.toolCalls; // Handle all tool call execution here for (const toolCall of toolCalls) { if (toolCall.toolName === 'getWeather') { const toolOutput = await getWeather(toolCall.input); messages.push({ role: 'tool', content: [ { toolName: toolCall.toolName, toolCallId: toolCall.toolCallId, type: 'tool-result', output: { type: 'text', value: toolOutput }, // update depending on the tool's output format }, ], }); } // Handle other tool calls } } else { // Exit the loop when the model doesn't request to use any more tools console.log('\\n\\nFinal message history:'); console.dir(messages, { depth: null }); break; } }}main().catch(console.error);
The example maintains a messages array that tracks the entire conversation history. After each model response, the generated messages are added to this history:
This manual approach gives you complete control over the agentic loop while still leveraging the AI SDK's powerful streaming and tool calling capabilities.