Lesson 6 — The reusable ToolLoopAgent
You'll build: a named, reusable agent you configure once and call anywhere — no more re-wiring
model + tools + system + stopWhenon every request. Concepts:ToolLoopAgent,.generate()/.stream(),instructions, baked-inoutput· Run:yarn dev 4
The idea
In Lessons 1–5 we passed model, tools, system, and stopWhen by hand on every generateText / streamText call. Fine for a one-off, tedious for a real app. The SDK ships the ToolLoopAgent class: configure everything once in the constructor, then call .generate(...) or .stream(...) as often as you like.
Think of it as packaging a configured agent into an object you can pass around.
Mental model
The agent bundles five things; callers just supply a prompt.
.generate() returns the same shape as generateText (.text, .steps, .output, .usage); .stream() matches streamText.
It's instructions, not system
The ToolLoopAgent constructor names the system prompt field instructions. Passing system here won't set the persona. (ToolLoopAgent is also exported as Experimental_Agent; Agent is the interface it implements.)
The problem
Re-wiring the same config everywhere invites drift:
// ❌ Repeated on every call — easy to let configs diverge
await generateText({ model, tools, system, stopWhen: stepCountIs(10), prompt: q1 });
await generateText({ model, tools, system, stopWhen: stepCountIs(10), prompt: q2 });Change the persona in one place and forget another, and your agent behaves inconsistently. Configure it once instead.
Walkthrough
Configure once
Everything the agent needs is captured in the constructor. Note instructions (the persona) and stopWhen (the same agent loop from Lesson 2).
const assistant = new ToolLoopAgent({
model: openai("gpt-4o-mini"),
instructions:
"You are a concise travel assistant. Use the weather and calculator " +
"tools when relevant instead of guessing.",
tools,
stopWhen: stepCountIs(10), // the agent loop, just like Tutorials 1 & 2
});Now any caller just provides a prompt — no model, tools, or persona repeated:
const result = await assistant.generate({ prompt: "What's the weather in Lisbon?" });
console.log(result.text);The same object also streams via assistant.stream({ prompt }), yielding a textStream exactly like Lesson 4.
A specialized agent with baked-in JSON output
Because output can live in the constructor too, you can build a specialist whose every call returns typed JSON — combining Lesson 5 + this lesson:
const tripPlanner = new ToolLoopAgent({
model: openai("gpt-4o-mini"),
instructions:
"You plan day trips. Check the weather with the tool before deciding.",
tools,
stopWhen: stepCountIs(10),
output: Output.object({
schema: z.object({
city: z.string(),
weather: z.string().describe("Short weather summary from the tool"),
recommendation: z
.enum(["beach", "museum", "hike", "cafe"])
.describe("Best activity given the weather"),
reason: z.string(),
}),
}),
});This tripPlanner checks the weather via its tool, then returns a validated object (recommendation is constrained to beach | museum | hike | cafe). The structured contract is part of the agent's identity, not something callers remember to add.
Run it
yarn dev 4Three demos run: reusing one agent for multiple .generate() calls, .stream()ing from the same object, and the structured tripPlanner returning typed JSON. Notice you never re-specify the model or tools after construction.
Production caveats
One agent, many call sites
Define each agent once in a module and import it where needed. This keeps persona, tools, and the loop budget consistent across your app — and gives you a single place to tune them.
stopWhen is still required for the loop
Just like raw generateText, an agent without stopWhen stops after the first tool call and never reaches a final answer. Always set stopWhen: stepCountIs(N) when the agent uses tools.
Exercises
Add a second specialist. Create a
mathTutoragent with a differentinstructionsstring but the sametools. Call both from one script.Solution
const mathTutor = new ToolLoopAgent({ model, instructions: "You are a patient math tutor…", tools, stopWhen: stepCountIs(10) });— construct it just likeassistant. Two configured objects, each with its own persona, sharing the same tool set.Stream the structured agent. Call
tripPlanner.stream({ prompt })and readpartialOutputStream. What do you observe?Solution
You get the structured object building up field-by-field (Lesson 5's streaming behavior) from an agent object — proof that
.stream()mirrorsstreamTextincluding itsoutputhandling.Inspect the steps. Log
result.stepsafter a.generate()call that uses a tool. Which fields tell you what the agent did?Solution
Each step exposes
toolCalls,toolResults,finishReason, andusage. A tool-using turn shows a step withfinishReason: "tool-calls"followed by a final step withfinishReason: "stop"— the loop in action.
Key takeaways
ToolLoopAgentpackagesmodel + instructions + tools + stopWhen + outputinto one reusable object.- Call it with
.generate()(likegenerateText) or.stream()(likestreamText); results have the same shape. - The persona field is
instructions, notsystem. - Bake
outputinto the constructor for a specialist that always returns typed JSON. - You still need
stopWhenfor the tool-calling loop.