# Les 14 — Lesstof
## Agents — LLMs die tools gebruiken in een loop

**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Vorige les:** Les 12 — Tool Calling
**Volgende les:** Les 15 — RAG + Embeddings

---

## Inhoud

1. [Wat is een agent](#1-wat-is-een-agent)
2. [De agent-loop in detail](#2-de-agent-loop-in-detail)
3. [ToolLoopAgent — de v6 abstractie](#3-toolloopagent--de-v6-abstractie)
4. [Stop-condities](#4-stop-condities)
5. [prepareStep — control per stap](#5-preparestep--control-per-stap)
6. [Done-tool pattern](#6-done-tool-pattern)
7. [Planning + reflectie patronen](#7-planning--reflectie-patronen)
8. [Agent vs tool-call vs workflow](#8-agent-vs-tool-call-vs-workflow)
9. [Productie-overwegingen](#9-productie-overwegingen)

---

## 1. Wat is een agent

De definitie van Vercel AI SDK is kort en goed bruikbaar:

> Agents are LLMs that use tools in a loop to accomplish tasks.

Drie ingrediënten:

1. **LLM** — beslist iedere stap wat de volgende actie is. Tool aanroepen of antwoorden in text.
2. **Tools** — uitbreidingen van wat het model kan: zoeken, lezen, schrijven, rekenen, anything.
3. **Loop** — de runtime die LLM en tools afwisselt. Beheert berichten (context) en stop-condities.

Het verschil met Les 12 zit niet in *of* je een loop hebt, maar in **hoe lang die loop draait** en **hoeveel autonomie** het model krijgt. In Les 12 deden we maximaal 5 stappen — kort en gecontroleerd. In Les 14 gaan we naar 20-50 stappen, en het model bepaalt zelf welke tools het in welke volgorde gebruikt.

### Voorbeelden van agent-taken

- **Research agent** — gegeven een vraag, zoek op het web, lees relevante pagina's, schrijf een rapport
- **Coding agent** — gegeven een bugreport, lees code, draai tests, voer fix door
- **Planner** — gegeven beschikbaarheid + voorkeuren, plan een vakantie
- **Customer support triage** — gegeven een ticket, classify, gather context, draft response

In al deze gevallen is de *volgorde* van stappen niet vooraf bekend. Daarom heeft een agent meer ruimte nodig dan een simpele tool-call.

---

## 2. De agent-loop in detail

Wat gebeurt er per iteratie?

```
┌────────────────────────────────────────┐
│   STAP N:                              │
│                                        │
│   1. Stuur huidige messages naar LLM   │
│   2. LLM antwoordt met:                │
│      a) text-only?    → loop stopt     │
│      b) tool-call(s)? → ga door        │
│   3. Execute(s) de tools (parallel)    │
│   4. Voeg tool-result(s) toe aan msgs  │
│   5. Check stopWhen-conditie           │
│   6. Indien niet gestopt → STAP N+1    │
└────────────────────────────────────────┘
```

De loop stopt in vier gevallen:

- **Natural finish** — model genereert text in plaats van tool-call
- **stopWhen voldaan** — bijv. `stepCountIs(20)`
- **Tool zonder execute aangeroepen** — done-pattern
- **Tool needs approval** — agent vraagt user-confirmation (advanced)

> **Default:** `stepCountIs(20)`. Dit is een veiligheidsgrens om runaway loops te voorkomen.

---

## 3. ToolLoopAgent — de v6 abstractie

In AI SDK v3-v5 schreef je de loop vaak zelf met `streamText` + `stopWhen`. In v6 is er een dedicated class: `ToolLoopAgent`.

### Het basis-pattern

```typescript
import { ToolLoopAgent, tool, stepCountIs } from "ai";
import { z } from "zod";

const researchAgent = new ToolLoopAgent({
  model: "openai/gpt-4o",
  system: "Je bent een research-assistent. Plan je werk, zoek, lees, vat samen.",
  tools: {
    webSearch,
    readPage,
    saveReport,
  },
  stopWhen: stepCountIs(30),
});

const result = await researchAgent.generate({
  prompt: "Schrijf een rapport over AI in de Nederlandse bouwsector",
});

console.log(result.text);    // finale antwoord
console.log(result.steps);   // array van alle stappen
console.log(result.steps[0].toolCalls);   // welke tools werden aangeroepen
console.log(result.steps[0].toolResults); // resultaten daarvan
```

### Waarom ToolLoopAgent?

- **Minder boilerplate** — geen handmatige while-loop, geen message-array management
- **Herbruikbaar** — definieer agent 1x, gebruik in API-routes / scripts / queues
- **Single source of config** — system prompt, tools, model, stop-conditie op één plek

### streaming vs generate

```typescript
// Eenmalig antwoord
const result = await agent.generate({ prompt });

// Voor chat-UI: streaming
const result = agent.stream({ prompt });
return result.toUIMessageStreamResponse(); // streamt steps naar client
```

---

## 4. Stop-condities

De `stopWhen` parameter bepaalt wanneer de agent klaar is. Vier opties:

### 4.1 `stepCountIs(N)` — maximum aantal stappen

```typescript
stopWhen: stepCountIs(50)  // stop na 50 stappen
```

Bruikbaar als veiligheidsgrens. Default is `stepCountIs(20)`.

### 4.2 `hasToolCall(toolName)` — stop bij specifieke tool

```typescript
stopWhen: hasToolCall("saveReport")
```

Stop zodra een bepaalde tool is aangeroepen. Handig voor "doe het werk en sla op, dan klaar".

### 4.3 `isLoopFinished()` — onbeperkt

```typescript
stopWhen: isLoopFinished()  // GEEN limit — agent stopt alleen natuurlijk
```

> **Waarschuwing:** zonder limiet kan agent oneindig doorgaan. Alleen gebruiken met andere safeguards (cost limit, timeout).

### 4.4 Combineren — array van condities

```typescript
stopWhen: [
  stepCountIs(50),         // max 50 stappen, of
  hasToolCall("submit"),   // submit aangeroepen, of
]
```

Stop zodra één conditie waar is.

### 4.5 Custom stop-condition

```typescript
import { StopCondition, ToolSet } from "ai";

const tools = { webSearch, readPage, saveReport } satisfies ToolSet;

const budgetExceeded: StopCondition<typeof tools> = ({ steps }) => {
  const tokens = steps.reduce(
    (sum, s) => sum + (s.usage?.totalTokens ?? 0),
    0
  );
  return tokens > 50_000; // stop bij 50k tokens
};

new ToolLoopAgent({
  tools,
  stopWhen: [stepCountIs(30), budgetExceeded],
});
```

De callback krijgt alle `steps` tot nu toe. Je kunt op alles stoppen wat je kunt afleiden uit de step-historie.

---

## 5. prepareStep — control per stap

`prepareStep` is een async callback die VOOR elke stap draait. Je kunt dynamisch het model, de tools, de messages of de toolChoice aanpassen.

### 5.1 Signature

```typescript
prepareStep: async ({
  model,        // huidig model
  stepNumber,   // 0-indexed
  steps,        // alle vorige steps
  messages,     // berichten die naar model gaan
}) => {
  // return iets — of {} voor "geen wijziging"
  return {
    model?: ...,
    messages?: ...,
    activeTools?: [...],
    toolChoice?: ...,
  };
}
```

### 5.2 Dynamic model selection

Begin goedkoop, switch naar duur model voor complex reasoning:

```typescript
prepareStep: async ({ stepNumber, messages }) => {
  if (stepNumber > 2 && messages.length > 10) {
    return { model: "anthropic/claude-sonnet-4.5" };
  }
  return {};
}
```

### 5.3 Tool-filtering per fase

```typescript
prepareStep: async ({ stepNumber }) => {
  if (stepNumber <= 3) {
    return { activeTools: ["webSearch"], toolChoice: "required" };
  }
  if (stepNumber <= 6) {
    return { activeTools: ["readPage"] };
  }
  return { activeTools: ["saveReport", "done"], toolChoice: "required" };
}
```

Forceer een research → read → save flow zonder dat agent kan 'cheaten'.

### 5.4 Context-trimming

Lange agents genereren veel berichten. Token-budget bewaken:

```typescript
prepareStep: async ({ messages }) => {
  if (messages.length > 20) {
    return {
      messages: [
        messages[0],           // system
        ...messages.slice(-10),// laatste 10
      ],
    };
  }
  return {};
}
```

---

## 6. Done-tool pattern

Soms wil je dat agent ALTIJD via een tool stopt — bijvoorbeeld omdat je het uiteindelijke antwoord gestructureerd wilt.

```typescript
const tools = {
  webSearch,
  readPage,
  saveReport,
  done: tool({
    description: "Roep aan wanneer onderzoek klaar is en rapport is opgeslagen.",
    inputSchema: z.object({
      finalReportId: z.number(),
      summary: z.string(),
    }),
    // GEEN execute — dit signaleert: stop de loop
  }),
};

const agent = new ToolLoopAgent({
  model: "openai/gpt-4o",
  tools,
  toolChoice: "required",  // model MOET altijd een tool aanroepen
});

const result = await agent.generate({ prompt });

// Het finale antwoord zit in de done-tool call:
const doneCall = result.staticToolCalls[0];
if (doneCall?.toolName === "done") {
  console.log(doneCall.input.summary);
}
```

**Wanneer dit pattern gebruiken:**
- Je wilt een gestructureerd eindantwoord (geen vrije text)
- Je wilt voorkomen dat agent tussendoor stopt zonder iets op te slaan
- Combineren met `toolChoice: "required"` om text-generatie uit te sluiten

---

## 7. Planning + reflectie patronen

### 7.1 Plan-Act-Reflect (ReAct)

Een populair pattern uit de literatuur:

1. **Plan** — agent schrijft eerst een plan in text (stap 0)
2. **Act** — agent voert plan uit via tools (stap 1-N)
3. **Reflect** — agent kijkt terug, vat samen, evalueert (laatste stap)

In de praktijk komt dit door simpelweg in de system prompt te zetten:

```
1. Begin met een plan in 3-5 bullets.
2. Voer het plan uit met tools.
3. Aan het eind: schrijf een korte reflectie — wat ging goed, wat zou anders?
```

Het model doet dit dan vanzelf binnen de loop — geen aparte code nodig.

### 7.2 Sub-agents

Een agent kan een andere agent aanroepen als tool:

```typescript
const summarizer = new ToolLoopAgent({
  model: "openai/gpt-4o-mini",
  system: "Vat één webpagina samen in 5 bullets.",
  tools: { readPage },
  stopWhen: stepCountIs(3),
});

const summarizePageTool = tool({
  description: "Vat een URL samen in bullets",
  inputSchema: z.object({ url: z.string().url() }),
  execute: async ({ url }) => {
    const result = await summarizer.generate({
      prompt: `Vat samen: ${url}`,
    });
    return { summary: result.text };
  },
});

const researchAgent = new ToolLoopAgent({
  model: "openai/gpt-4o",
  tools: { webSearch, summarizePage: summarizePageTool, saveReport },
});
```

Voordelen:
- Token-budget: sub-agent ziet kleinere context
- Specialisatie: sub-agent kan kleiner/goedkoper model gebruiken
- Modulair: sub-agent te testen los van hoofdagent

### 7.3 Self-correction

Geef de agent een tool om eigen tussenresultaat te valideren:

```typescript
const validate = tool({
  description: "Check of een rapport feitelijk juist is. Returnt issues.",
  inputSchema: z.object({ reportText: z.string() }),
  execute: async ({ reportText }) => {
    // bv. tweede LLM-call als fact-checker
    return { issues: [...] };
  },
});
```

Combineer met system prompt: "Als validate issues teruggeeft, herschrijf en check opnieuw."

---

## 8. Agent vs tool-call vs workflow

Niet elke taak hoort bij een agent. Drie niveaus:

| Niveau | Wanneer | Voorbeeld |
|--------|---------|-----------|
| **Plain tool-call** | 1 tool nodig, 1 antwoord | "Wat is het weer in Utrecht?" |
| **Multi-step (Les 12)** | 2-5 tools, vraag heeft 1 doel | "Hoeveel jazz acts vs rock?" |
| **Agent (Les 14)** | 10-50 stappen, open-ended | "Onderzoek X en schrijf rapport" |
| **Workflow (code)** | Reproduceerbaar, audit-trail | Order intake pipeline |

### Wanneer GEEN agent

- **Determinisme nodig** — finance, juridisch, medische data
- **Latency** — agent gebruikt al snel 30s+
- **Voorspelbare kosten** — agent met 50 stappen kan duur uitlopen
- **Eenvoudige flow** — een if-statement is genoeg

### Wanneer WEL agent

- **Open-ended** — research, planning, debugging
- **Volgorde onbekend** — model bepaalt zelf
- **Lange ketens** — meerdere bronnen, meerdere stappen
- **Latency + kosten acceptabel** — async use cases vooral

Quote uit AI SDK docs:

> Agents are flexible and powerful, but non-deterministic. When you need reliable, repeatable outcomes with explicit control flow, use core functions with structured workflow patterns.

---

## 9. Productie-overwegingen

### 9.1 Kosten bewaken

Eén onbedoelde loop kan tientallen euro's kosten. Altijd:

- `stepCountIs(N)` als safety cap
- Custom `StopCondition` op tokens of dollar-amount
- Logging per step (welke tool, hoeveel tokens)

```typescript
const costCap: StopCondition<typeof tools> = ({ steps }) => {
  const usd = steps.reduce((s, step) =>
    s + estimateCost(step.usage), 0);
  return usd > 0.50;
};
```

### 9.2 Latency

Een 20-step agent op gpt-4o duurt 30-60 seconden. Voor user-facing:

- Stream steps naar client (`agent.stream()` + `toUIMessageStreamResponse()`)
- Toon progress (welke stap, welke tool)
- Loading state met geschatte tijd

### 9.3 Observability

Log per step:
- Step number
- Tool calls + input
- Tool result (truncated)
- Token usage
- Latency

Dit is essentieel voor debugging — agents zijn niet-deterministisch, dus een specifieke run is vaak niet exact reproduceerbaar.

### 9.4 Veiligheid

- Write-tools (DB inserts, mails, API calls) altijd met explicit user-intent
- Confirmation UI voor irreversible acties
- Read-only sandbox voor development (Supabase RLS policies open)
- Production: tighter RLS + write-tools authenticated

### 9.5 Testing

Agents zijn lastig te testen — non-determinisme. Strategieën:

- **Mock LLM** — test je tools los met fake LLM
- **Snapshot tests** — log volledige step-trace, vergelijk met goedgekeurde versie
- **Eval suites** — set van vragen + verwachte tool-sequences

---

## Bronnen

- **AI SDK Agents — Overview:** https://ai-sdk.dev/docs/agents/overview
- **AI SDK Agents — Loop Control:** https://ai-sdk.dev/docs/agents/loop-control
- **AI SDK Agents — Building Agents:** https://ai-sdk.dev/docs/agents/building-agents
- **AI SDK Agents — Subagents:** https://ai-sdk.dev/docs/agents/subagents
- **AI SDK Workflow patterns:** https://ai-sdk.dev/docs/agents/workflows
- **Tavily Web Search API:** https://tavily.com/
- **Anthropic — Building effective agents:** https://www.anthropic.com/research/building-effective-agents
- **ReAct paper:** https://arxiv.org/abs/2210.03629
