Lesson 2 — Your first agent loop
You'll build: a working agent with
generateText+stepCountIs, and learn to readresult.stepsto see the loop happen. Concepts:generateText,stopWhen/stepCountIs, system prompts, inspecting steps · Run:yarn dev 1
The idea
In Lesson 1 you learned what an agent is. Now you'll learn the exact API that turns a normal LLM call into one. In the Vercel AI SDK, the workhorse function is generateText. By itself it does one round-trip. Add one option — stopWhen: stepCountIs(N) — and it becomes a loop that can call tools and keep going until it has a final answer.
This lesson uses the same file as Lesson 1 (src/agents/01-oneshot.ts), but now we focus on the mechanics of each option rather than the big-picture idea.
Mental model
generateText runs the loop for you. Each pass through the loop is a step. A step is either a tool call (the model asked to run a tool) or the final answer (the model is done).
The SDK is the orchestrator in the middle: it relays tool calls to your code and results back to the model, counting steps against your stopWhen limit.
The problem
If you call generateText with tools but without stopWhen, the loop never runs to completion:
// ❌ Stops after the FIRST tool call — no final answer
const result = await generateText({
model: openai("gpt-4o-mini"),
prompt: "Weather in Paris and 23 * 19?",
tools,
});The model asks to call a tool, the SDK runs it… and then stops, because nothing told it the loop may continue. You get a tool result but no synthesized answer. This is the #1 beginner gotcha.
Walkthrough
Here is the complete first agent. Each numbered comment maps to one option you pass to generateText:
/**
* agents/01-oneshot.ts
* ----------------------------------------------------------------------------
* TUTORIAL 1: A "one-shot" agent
*
* Goal: understand the core *agent loop* in its simplest form.
*
* What makes an LLM call an "agent"? The ability to call tools in a LOOP:
* 1. We send the model a task + the list of tools it may use.
* 2. The model may respond with a "tool call" instead of a final answer.
* 3. The SDK runs our tool's `execute()` and feeds the result back.
* 4. The model sees the result and either calls another tool or answers.
* 5. Repeat until the model gives a final text answer (or we hit a step limit).
*
* The single line that turns one request into this loop is:
* stopWhen: stepCountIs(10)
* It lets the model take up to 10 steps. Without it, the SDK stops after the
* first tool call and won't continue to a final answer.
* ----------------------------------------------------------------------------
*/
import { generateText, stepCountIs } from "ai";
import { openai } from "@ai-sdk/openai";
import { tools } from "../tools.js";
import { marked } from "../markdown.js";
export async function runOneShotAgent(
task = "What's the weather in Paris right now, and what is 23 * 19? " +
"Summarize both in one short paragraph.",
) {
console.log("\n🤖 TUTORIAL 1 — One-shot agent\n");
console.log(`📋 Task: ${task}\n`);
// #region loop
const result = await generateText({
// 1) Pick the model. `openai(...)` reads OPENAI_API_KEY from the env.
model: openai("gpt-4o-mini"),
// 2) A system prompt shapes the agent's behavior/persona.
system:
"You are a concise, helpful assistant. " +
"Always use the provided tools for weather and math instead of guessing.",
// 3) The task we want done.
prompt: task,
// 4) The tools the model is allowed to call (defined in tools.ts).
tools,
// 5) THE KEY LINE: allow multiple steps so tool calls + the final answer
// can all happen in one `generateText` call. This is the agent loop.
stopWhen: stepCountIs(10),
});
// #endregion loop
// ---- Observe the loop --------------------------------------------------
// `result.steps` lets us SEE what the agent did at each step: which tools it
// called, with what arguments, and what came back. Great for learning/debug.
console.log("🔎 Agent steps (the loop in action):\n");
result.steps.forEach((step, i) => {
for (const call of step.toolCalls) {
console.log(` Step ${i + 1}: 🛠 called \`${call.toolName}\``);
console.log(` input: ${JSON.stringify(call.input)}`);
}
for (const toolResult of step.toolResults) {
console.log(` result: ${JSON.stringify(toolResult.output)}`);
}
});
// ---- Final answer ------------------------------------------------------
// `result.text` is the model's final text answer after the loop finished.
// We render it through the terminal markdown renderer for nice formatting.
console.log("\n💬 Final answer:\n");
console.log(await marked.parse(result.text));
// `result.usage` reports token counts — handy for cost awareness.
console.log(
`\n📊 Tokens — input: ${result.usage.inputTokens}, output: ${result.usage.outputTokens}\n`,
);
return result.text;
}Walking through the key options:
model: openai("gpt-4o-mini")— picks the model. Theopenai()helper reads yourOPENAI_API_KEYfrom the environment, so no key is hard-coded.system: "..."— the system prompt. It shapes persona and rules. Here we tell the model to always use the tools for weather and math instead of guessing. Pass this via thesystemoption — never as a fakemessagesentry (that invites prompt-injection problems; more in Lesson 10).prompt: task— the actual user request.tools— the set of tools the model may call (defined insrc/tools.ts, covered in Lesson 3).stopWhen: stepCountIs(10)— the line that makes it a loop. Up to 10 steps.
Reading the loop with result.steps
The most useful learning tool here is result.steps. After the loop finishes, it contains one entry per step, each with the toolCalls the model made and the toolResults that came back:
result.steps.forEach((step, i) => {
for (const call of step.toolCalls) {
console.log(`Step ${i + 1}: called ${call.toolName}`, call.input);
}
for (const toolResult of step.toolResults) {
console.log(" result:", toolResult.output);
}
});This is how you debug an agent: you can see exactly what it decided to do and with what arguments — invaluable when behavior surprises you.
Run it
yarn dev 1Expected shape of the output (values vary, weather is mock):
🤖 TUTORIAL 1 — One-shot agent
📋 Task: What's the weather in Paris right now, and what is 23 * 19? ...
🔎 Agent steps (the loop in action):
Step 1: 🛠 called `getWeather`
input: {"city":"Paris"}
result: {"city":"Paris","condition":"sunny","temperatureC":...}
Step 2: 🛠 called `calculator`
input: {"expression":"23 * 19"}
result: {"expression":"23 * 19","result":437}
💬 Final answer:
<one short paragraph using the real weather + 437>
📊 Tokens — input: ..., output: ...Notice the final 📊 Tokens line — every step consumed tokens. Hold that thought for Lesson 11 (observability & cost).
Production caveats
Pick stepCountIs(N) deliberately
N is your circuit breaker. Too low and complex tasks get cut off mid-loop; too high and a confused agent can rack up cost. Start small (5–10) and raise it only when a real task needs more.
System prompt = behavior contract
Most "the agent won't use my tool" issues are really prompt issues. Be explicit in system: tell the model when to use each tool and that guessing is not allowed. The model reads both your system prompt and each tool's description (Lesson 3) to decide.
Exercises
Watch the step count change. Add a third sub-question to the
task(e.g., "…and the weather in Tokyo too"). Re-run and count the steps inresult.steps. Did the agent add a step?Solution
Yes — asking about a second city usually adds another
getWeatherstep, so you'll see three tool-call steps before the final answer. The model calls the tool once per city because each call needs different input.Tighten the limit. Set
stopWhen: stepCountIs(1)and run the default task. What happens to the final answer?Solution
With only one step allowed, the agent can make a tool call but cannot loop back to synthesize a complete answer — you'll get a truncated or missing final answer. It proves that the limit directly bounds how much the agent can do.
Strengthen the system prompt. Rewrite the
systemstring to forbid guessing even more firmly, then ask a math question the model might "know". Does it still call the calculator?Solution
A firm system prompt ("you MUST use the calculator for any arithmetic, never compute in your head") makes the model reliably route math through the tool. This is how you make tool usage dependable rather than occasional.
Key takeaways
generateTextis the core call;stopWhen: stepCountIs(N)turns it into an agent loop.- Each pass is a step;
result.stepsexposes everytoolCallandtoolResultso you can see and debug the loop. - The system prompt is your behavior contract — use it to make tool usage reliable.
- The step limit is a safety bound: choose it deliberately.