# Les 14 — Agents
## Docenttekst (Klas A — 3 uur, fysiek, demo-driven)

**Les:** 13 van 18
**Onderwerp:** Agents — LLM in een loop met tools
**Duur:** 180 minuten
**Format:** Tim demonstreert klassikaal. Studenten kijken mee. Zelf bouwen = huiswerk.
**Demo-app:** Nieuwe research-agent (from scratch, los van Polderfest)

---

## Hoe deze tekst werkt

Dit document is een **lopend script**. Lees mee tijdens de les op je laptop.

- `[SLIDE X]` — Klik naar slide X
- `[SCHERM: slides | terminal | editor | browser | supabase]` — Welk scherm op de beamer
- **Vertel:** "..." — Letterlijk wat je zegt (mag in eigen woorden)
- `*[stage direction]*` — Korte instructie voor jezelf, niet uitspreken
- Code blocks = wat je typt
- 💬 = verwachte studentenvraag

---

## VÓÓR DE LES — Setup (45 min)

### 1. Tavily account

- Maak account op https://tavily.com/
- Kopieer API key (begint met `tvly-...`)
- Gratis tier = 1000 calls/maand, ruim genoeg voor deze les

### 2. Nieuwe lege Next.js demo-folder

`*[Doe dit thuis vooraf, niet in de klas. Klas kijkt naar de setup-stap als je 'm doet.]*`

```bash
cd ~/novi/novi-lessons/Les14-Agents
pnpm create next-app@latest research-agent \
  --typescript --tailwind --app --no-src-dir --import-alias "@/*"
cd research-agent
pnpm add ai @ai-sdk/openai zod @supabase/supabase-js
```

### 3. Environment vars

`.env.local`:
```
OPENAI_API_KEY=sk-...
TAVILY_API_KEY=tvly-...
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
```

### 4. Supabase tabel

```sql
create table research_reports (
  id          bigserial primary key,
  query       text not null,
  summary     text not null,
  sources     jsonb default '[]'::jsonb,
  created_at  timestamp default now()
);
alter table research_reports enable row level security;
create policy "demo open" on research_reports
  for all to anon using (true) with check (true);
```

### 5. Backup eindstaat

- Werkende `lib/tools.ts`, `lib/agent.ts`, `app/api/research/route.ts` ergens als backup
- Verwacht: 1-2 typos tijdens live coding — geen ramp

### 6. Browser tabs

- `localhost:3000` (dev server, maar gaan we voornamelijk via terminal testen)
- Supabase dashboard
- https://ai-sdk.dev/docs/agents/overview (referentie)
- https://tavily.com/ (dashboard voor usage)

---

# HET SCRIPT — Lees mee tijdens de les

## BLOK 1 — Welkom + Terugblik (10 min)

`[SLIDE 1 — Title]` `[SCHERM: slides]`

**Vertel:** "Welkom bij les 13. Vandaag stappen we van tool-calling naar agents. Eén stap verder qua autonomie van AI."

`[SLIDE 2 — Terugblik]`

**Vertel:** "Vorige les bouwden we Tool Calling in onze Polderfest-app. Zes tools. `stopWhen: stepCountIs(5)`. AI roept zelf functies aan om aan data te komen. Goed werk.

Maar er waren dingen die een tool-call bot **nog niet kon**. Lange ketens — 30, 40, 50 stappen — daar is `stepCountIs(5)` te krap voor. Een eigen plan maken en bijstellen. Tools die andere tools triggeren. Per stap een ander model kiezen.

Daarvoor heb je een **agent** nodig. Een LLM die tools gebruikt in een loop, en zelf bepaalt hoe lang die loop duurt en welke kant het op gaat."

`[SLIDE 3 — Planning]`

**Vertel:** "Vandaag drie uur. Eerst een half uur theorie — wat is een agent precies, hoe ziet de loop eruit, wat is een `ToolLoopAgent`. Daarna vier live demos waarin we from scratch een research-agent bouwen. Geen polderfest vandaag — losse repo, omdat agents een nieuwe context verdienen.

Aan het eind: wanneer wel/niet een agent, en wat doe je voor huiswerk."

---

## BLOK 2 — Theorie (30 min)

`[SLIDE 4 — Wat is een agent]`

**Vertel:** "De definitie uit de AI SDK docs is kort en bruikbaar:

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

Drie ingrediënten. Eén — een LLM die besluit wat de volgende actie is. Twee — tools die het LLM uitbreiden. Drie — een loop die deze twee orchestreert.

In Les 12 hadden we deze drie ook al — `stopWhen: stepCountIs(5)` is in feite een mini-agent. Het verschil zit in **schaal**. Vandaag praten we over 20-50 stappen, en het model bepaalt zelf welke tools het wanneer gebruikt."

💬 *Mogelijke vraag: "Wat is dan het verschil met Les 12 echt?"*

**Antwoord:** "Schaal en autonomie. Les 12 was: vraag → 2-5 tool-calls → antwoord. Vandaag: vraag → agent maakt plan → voert plan uit (10-50 stappen) → schrijft rapport. Het model beslist veel meer zelf."

`[SLIDE 5 — Anatomie van de loop]`

**Vertel:** "Wat gebeurt er per loop-iteratie, één stap? Vijf dingen.

Eén: we sturen de huidige messages naar het LLM. Twee: LLM antwoordt — óf met text, óf met een tool-call. Als het text is, stopt de loop. Dat is een natural finish — model heeft niks meer te doen. Als het een tool-call is, gaan we door. Drie: we voeren de tool execute uit. Vier: we voegen het resultaat toe aan de berichten. Vijf: we checken stop-conditie. Niet voldaan? Volgende iteratie.

De loop stopt in vier gevallen: text-only antwoord, stopWhen geactiveerd, tool zonder execute aangeroepen — dat is het 'done-pattern', komen we straks op terug, en tool die approval nodig heeft. Die laatste skippen we vandaag.

Default in v6: `stepCountIs(20)`. Dat is een veiligheidsgrens — voorkomt dat een rare bug in je systeem prompt resulteert in 200 API-calls en een rekening van 30 euro."

`[SLIDE 6 — ToolLoopAgent]`

**Vertel:** "In oudere AI SDK versies moest je de loop zelf schrijven — een while-loop rond `streamText` en `generateText`. In v6 is er een dedicated class: `ToolLoopAgent`.

`*[Wijs naar code op slide]*`

Je geeft model, system prompt, tools, en een stop-conditie. Eén keer definiëren. Dan `agent.generate({ prompt })` aanroepen, of `agent.stream` voor streaming.

Wat je terugkrijgt: `result.text` is het eindantwoord, `result.steps` is een array met alle stappen die de agent zette — welke tool aangeroepen, met welke input, welk resultaat. Dat is je debug-goud."

💬 *Vraag: "Kan dat nog steeds met streamText?"*

**Antwoord:** "Ja, voor backwards compatibility. Maar voor nieuwe code is `ToolLoopAgent` de aanrader. Minder boilerplate, herbruikbaar."

`[SLIDE 7 — Stop-condities]`

**Vertel:** "Vier soorten stop-condities.

Eén: `stepCountIs(N)` — stop na N stappen. Standaard veiligheidsmaatregel.

Twee: `hasToolCall('saveReport')` — stop zodra een specifieke tool is aangeroepen. Logisch voor 'doe het werk en sla op, dan klaar'.

Drie: `isLoopFinished()` — onbeperkt. Agent stopt alleen natuurlijk wanneer model klaar is. Dit is gevaarlijk zonder andere safeguards — agent kan in theorie oneindig doorgaan. Alleen gebruiken met cost limit erbij.

Vier: combineer ze in een array. Loop stopt zodra één conditie waar is.

En vijf — niet built-in maar zelf te schrijven: `StopCondition` callback. Daarmee kun je stoppen op kosten, op aantal tools, op wat dan ook dat je uit de step-historie kunt halen."

`[SLIDE 8 — prepareStep]`

**Vertel:** "`prepareStep` is misschien de krachtigste feature van `ToolLoopAgent`. Het is een async callback die VOOR elke stap draait. Je kunt het model dynamisch wisselen, tools beperken, messages trimmen.

Voorbeeld: begin met `gpt-4o-mini` — snel en goedkoop. Maar als de stap-historie complex wordt, switch naar Claude Sonnet voor beter reasoning. Dat scheelt geld zonder kwaliteit te verliezen.

Of: dwing een research-fase af. Stap 0-3 mag alleen `webSearch`. Stap 4-6 alleen `readPage`. Stap 7+ alleen `saveReport`. Agent kan niet 'cheaten' door te vroeg op te slaan.

Of: trim oude tool-results uit de berichten. Een lange agent kan 30k tokens aan tool-output verzamelen. Niet meer relevant voor de huidige stap — eruit, scheelt budget."

💬 *Vraag: "Hoe weet ik wanneer ik prepareStep nodig heb?"*

**Antwoord:** "Niet bij elke agent. Begin zonder. Als je merkt dat je agent veel geld kost, of te vaak in een verkeerde fase blijft hangen, dan kom je terug om `prepareStep` toe te voegen. Premature optimization is ook in agents reëel."

---

## BLOK 3 — Live Demo 1: Setup research-agent (25 min)

`[SLIDE 9 — Wat we bouwen]`

**Vertel:** "Genoeg theorie. We gaan een **research-agent** bouwen. Doel: gebruiker stelt een vraag — bijvoorbeeld 'wat zijn AI-trends in 2026?' — en de agent zoekt zelfstandig op het web, leest de relevante pagina's, schrijft een rapport, slaat op in Supabase.

Vier tools: `webSearch` met Tavily, `readPage` om URLs te lezen, `saveReport` om in DB te zetten, en eventueel een `done`-tool.

Stack: Next.js 16, AI SDK v6, Tavily, Supabase, gpt-4o-mini. Losse repo — niet de Polderfest-demo. Agents verdienen een schone context."

`[SLIDE 10 — LIVE DEMO 1]` `[SCHERM: terminal + editor]`

**Vertel:** "Klas, leun achterover. Vragen tussendoor mogen. Ik laat zien hoe je een agent from-scratch bouwt."

`*[Open terminal in /Les14-Agents]*`

```bash
pnpm create next-app@latest research-agent \
  --typescript --tailwind --app --no-src-dir --import-alias "@/*"
cd research-agent
```

`*[Wacht op next-app, ondertussen praat door over Tavily]*`

**Vertel:** "Voor web search heb je een API nodig. OpenAI heeft sinds kort een eigen web-search tool, maar die werkt alleen met gpt-4o en is duur. Voor onafhankelijke search gebruiken we Tavily — gemaakt voor AI agents, gratis tier van 1000 calls per maand. Gewoon registreren met email, API key gepakt, klaar.

Andere opties: Brave Search API, Serper, Google Custom Search. Tavily is het simpelst voor onze case omdat het naast search ook content-extractie biedt."

```bash
pnpm add ai @ai-sdk/openai zod @supabase/supabase-js
```

`*[Open editor]*`

**Vertel:** "Eerst environment vars."

`*[Maak .env.local]*`

```
OPENAI_API_KEY=sk-...
TAVILY_API_KEY=tvly-...
NEXT_PUBLIC_SUPABASE_URL=...
NEXT_PUBLIC_SUPABASE_ANON_KEY=...
```

**Vertel:** "Belangrijk: `OPENAI_API_KEY` is GEEN `NEXT_PUBLIC_*`. Anders zit-ie in je client-bundle en kan iedereen 'm zien in DevTools. Server-only. Hetzelfde voor `TAVILY_API_KEY`."

`*[Check .gitignore — moet .env.local in staan]*`

**Vertel:** "Check je `.gitignore`. `.env.local` moet erin. Standaard create-next-app doet dit goed maar — altijd checken."

`*[Supabase Dashboard openen]*` `[SCHERM: supabase]`

```sql
create table research_reports (
  id          bigserial primary key,
  query       text not null,
  summary     text not null,
  sources     jsonb default '[]'::jsonb,
  created_at  timestamp default now()
);
```

**Vertel:** "Tabel voor onze rapporten. `query` is de vraag, `summary` is het rapport zelf, `sources` is een JSON array van URLs die werden gebruikt."

```sql
alter table research_reports enable row level security;
create policy "demo open" on research_reports
  for all to anon using (true) with check (true);
```

**Vertel:** "RLS aan, en een open policy voor demo. Productie: koppelen aan auth.uid()."

`*[Terug naar editor]*` `[SCHERM: editor]`

**Vertel:** "Nu de tools. Ik maak een `lib/tools.ts` met onze drie tools."

`*[Maak lib/tools.ts]*`

```typescript
import { tool } from "ai";
import { z } from "zod";
import { createClient } from "@supabase/supabase-js";

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);

export const webSearch = tool({
  description: "Zoek op het web. Returnt top 5 resultaten met url + snippet.",
  inputSchema: z.object({
    query: z.string().describe("Zoekterm"),
  }),
  execute: async ({ query }) => {
    const res = await fetch("https://api.tavily.com/search", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        api_key: process.env.TAVILY_API_KEY,
        query,
        max_results: 5,
      }),
    });
    const data = await res.json();
    return data.results?.map((r: any) => ({
      url: r.url,
      title: r.title,
      snippet: r.content,
    })) ?? [];
  },
});
```

**Vertel:** "Beschrijving van `webSearch` is kort en specifiek. AI leest description om te beslissen welke tool nodig is — vaag = verkeerde keuze. `max_results: 5` houdt het beheersbaar — Tavily kan ook 10 of 20, maar 5 is goed startpunt."

`*[readPage tool toevoegen]*`

```typescript
export const readPage = tool({
  description: "Haal de volledige tekst van een URL op.",
  inputSchema: z.object({ url: z.string().url() }),
  execute: async ({ url }) => {
    const res = await fetch("https://api.tavily.com/extract", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        api_key: process.env.TAVILY_API_KEY,
        urls: [url],
      }),
    });
    const data = await res.json();
    const content = data.results?.[0]?.raw_content ?? "";
    return { url, content: content.slice(0, 5000) };
  },
});
```

**Vertel:** "`readPage` haalt de fulltext van een URL — Tavily heeft een extract-endpoint dat ruis verwijdert. Ik knip op 5000 chars om token-budget niet te exploderen. Voor productie: chunken + samenvatten."

`*[saveReport tool]*`

```typescript
export const saveReport = tool({
  description:
    "Sla een onderzoeksrapport op in de database. " +
    "Alleen aanroepen als het rapport compleet is.",
  inputSchema: z.object({
    query: z.string(),
    summary: z.string().min(100),
    sources: z.array(z.object({
      url: z.string().url(),
      title: z.string(),
    })),
  }),
  execute: async ({ query, summary, sources }) => {
    const { data, error } = await supabase
      .from("research_reports")
      .insert({ query, summary, sources })
      .select()
      .single();
    if (error) return { error: error.message };
    return { reportId: data.id, success: true };
  },
});
```

**Vertel:** "`saveReport` is onze enige write-tool. Description benadrukt: alleen aanroepen als compleet. `summary.min(100)` — zod schema dwingt minimum lengte af, AI mag geen 'TODO' insturen."

`*[lib/agent.ts maken]*`

**Vertel:** "Nu de agent zelf."

```typescript
import { ToolLoopAgent, stepCountIs } from "ai";
import { openai } from "@ai-sdk/openai";
import { webSearch, readPage, saveReport } from "./tools";

export const researchAgent = new ToolLoopAgent({
  model: openai("gpt-4o-mini"),
  system: `Je bent een research-assistent. Werk als volgt:
1. Begin met een kort plan (3-5 bullets).
2. Gebruik webSearch voor oriëntatie.
3. Gebruik readPage voor de 2-3 meest relevante URLs.
4. Schrijf een samenvatting van min 200 woorden.
5. Sla op met saveReport. Inclusief alle gebruikte sources.

Verzin niets — gebruik alleen wat je via tools hebt opgehaald.`,
  tools: { webSearch, readPage, saveReport },
  stopWhen: stepCountIs(10),
});
```

**Vertel:** "System prompt is **het** kritieke onderdeel. Ik geef expliciete fasen — plan, search, read, samenvat, save. Hoe duidelijker, hoe minder de agent drifft. 'Verzin niets' is belangrijk — anders maakt AI bronnen op."

`*[app/api/research/route.ts]*`

```typescript
import { researchAgent } from "@/lib/agent";

export async function POST(req: Request) {
  const { query } = await req.json();
  const result = await researchAgent.generate({ prompt: query });

  return Response.json({
    text: result.text,
    steps: result.steps.map((s, i) => ({
      step: i,
      toolCalls: s.toolCalls?.map((tc) => ({
        tool: tc.toolName,
        input: tc.input,
      })),
      hasText: !!s.text,
    })),
  });
}
```

**Vertel:** "Endpoint stuurt query naar de agent, returnt text + step-samenvatting. Voor productie zou je dit streamen, voor demo houden we het simpel — ��én POST, JSON response."

`*[Run]*` `[SCHERM: terminal]`

```bash
pnpm dev
```

`*[In tweede terminal:]*`

```bash
curl -X POST http://localhost:3000/api/research \
  -H "Content-Type: application/json" \
  -d '{"query": "Wat zijn de belangrijkste AI-trends in 2026?"}' \
  | jq
```

**Vertel:** "We zien... `*[wachten 20-40s]*` ...de agent draait. JSON komt terug. Kijk — `steps` array. webSearch, dan readPage, readPage, saveReport. Vier stappen. `text` veld is een samenvatting."

`*[Open Supabase, check tabel]*` `[SCHERM: supabase]`

**Vertel:** "Rapport staat in Supabase. Werkend agent. From scratch in 20 minuten."

💬 *Vraag: "Hoeveel tokens kost zo'n call?"*

**Antwoord:** "Goeie vraag. `result.usage` heeft `inputTokens` en `outputTokens`. Voor zo'n research van 4 stappen: ongeveer 8.000 input + 1.500 output tokens. Op gpt-4o-mini: ~0.001 dollar. Bijna gratis. Op gpt-4o: ~0.05 dollar. Houdbaar."

---

## BLOK 4 — Live Demo 2: stopWhen variants (25 min)

`[SLIDE 11 — DEMO 2]` `[SCHERM: editor]`

**Vertel:** "We hebben nu een agent met `stepCountIs(10)`. Laten we de andere stop-condities uitproberen en zien wat het verschil maakt."

`*[Wijzig stopWhen]*`

```typescript
import { ToolLoopAgent, stepCountIs, hasToolCall } from "ai";

// ...
stopWhen: hasToolCall("saveReport"),
```

**Vertel:** "Nu stopt agent **zodra** saveReport is aangeroepen. Geen veiligheidscap. Laten we draaien."

`*[Curl draaien]*`

**Vertel:** "Werkt. Agent doet zijn werk, roept saveReport, stopt. Voordeel: geen onnodige extra stappen. Nadeel: als saveReport faalt en agent retried, kan-ie alsnog lang doorgaan."

`*[Beter: combineren]*`

```typescript
stopWhen: [
  stepCountIs(20),
  hasToolCall("saveReport"),
],
```

**Vertel:** "Best practice: array. Veiligheidscap én task-completion. Loop stopt op de eerste van de twee."

`*[Probeer isLoopFinished]*`

```typescript
import { isLoopFinished } from "ai";
// ...
stopWhen: isLoopFinished(),
```

**Vertel:** "`isLoopFinished` is letterlijk: geen limit. Alleen natuurlijke finish stopt het. Dit is **gevaarlijk** zonder andere safeguards. Een misverstand in je system prompt en agent draait 50 stappen door."

`*[Curl draaien — agent doet nu 8-10 stappen]*`

**Vertel:** "Zie je? Agent gaat door tot het zelf besluit te stoppen. Eindigt met text — geen tool-call. Werkt, maar — wat als prompt slecht is, of als API een rare fout gooit en de agent retried?"

`*[Custom StopCondition]*`

**Vertel:** "Eigen stop. Bijvoorbeeld stoppen als we genoeg pagina's hebben gelezen."

```typescript
import type { StopCondition } from "ai";
import type { researchAgent } from "./agent";

type Tools = typeof researchAgent extends ToolLoopAgent<infer T> ? T : never;

const enoughReading: StopCondition<any> = ({ steps }) => {
  const readCount = steps.flatMap((s) =>
    s.toolCalls?.filter((tc) => tc.toolName === "readPage") ?? []
  ).length;
  return readCount >= 3;
};

stopWhen: [
  stepCountIs(20),
  enoughReading,
],
```

**Vertel:** "Drie pagina's lezen is genoeg, stoppen. Custom logic die uit step-historie wordt gehaald."

`*[Demo run]*`

**Vertel:** "Zie: agent stopt op stap waar readPage voor de derde keer wordt aangeroepen. Conditie werkt. Voor productie gebruik je dit vaak voor kostenlimieten — daar komen we morgen op terug, eh, in het huiswerk."

💬 *Vraag: "Waarom `: StopCondition<any>` en niet typed?"*

**Antwoord:** "Excuus, dat is een shortcut. In productie zou je een `ToolSet` type extracten zodat je `StopCondition<typeof tools>` krijgt — dan kun je tool-names typesafe checken. Voor demo houd ik 't simpel."

---

## BLOK 5 — Pauze (15 min)

`[SLIDE 12 — Pauze]`

**Vertel:** "Pauze. Vijftien minuten. Loop een ronde."

`*[Reset agent.ts naar stepCountIs(15) + hasToolCall("saveReport") combo voor volgende demo]*`

---

## BLOK 6 — Live Demo 3: prepareStep (25 min)

`[SLIDE 13 — DEMO 3]` `[SCHERM: editor]`

**Vertel:** "Nu het krachtigste stuk: `prepareStep`. Voor elke stap kunnen we model, tools, messages aanpassen. Drie use cases vandaag."

### Use case 1 — Dynamic model

```typescript
prepareStep: async ({ stepNumber, messages }) => {
  console.log(`Stap ${stepNumber}, ${messages.length} messages`);
  if (stepNumber > 3) {
    console.log("→ switch naar gpt-4o");
    return { model: openai("gpt-4o") };
  }
  return {};
},
```

**Vertel:** "Stap 0-3 doen we met gpt-4o-mini — goedkoop en snel. Vanaf stap 4 switchen we naar de grote gpt-4o. Reden: vroege stappen zijn 'zoek even', late stappen zijn 'vat samen en redeneer'. Dat laatste verdient een sterker model."

`*[Curl draaien — kijk in console naar logs]*`

**Vertel:** "Zien jullie: 'Stap 0 — mini'. 'Stap 1 — mini'. 'Stap 4 — switch naar gpt-4o'. Dynamic. Effect: lagere kosten, betere eindkwaliteit."

### Use case 2 — Fase-gebaseerde tools

`*[Pas aan]*`

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

**Vertel:** "Nu dwingen we een fase af. Stap 0-2 mag alleen searchen. Stap 3-5 alleen lezen. Stap 6+ alleen opslaan, en moet die ook gebruiken want `toolChoice: required`."

`*[Curl draaien]*`

**Vertel:** "Agent kan niet 'cheaten'. Geen `saveReport` op stap 2 met een vage samenvatting. Eerst werken, dan opslaan."

💬 *Vraag: "Is dat niet een beetje vechten tegen het model?"*

**Antwoord:** "Het is een trade-off. Meer controle = minder flexibel. Voor research is een gefaseerde aanpak vaak prima. Voor open-ended problem solving — bijv. een coding agent — zou ik 't niet doen. Daar moet het model vrij kunnen kiezen."

### Use case 3 — Context-trimming

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

**Vertel:** "Bij lange agents groeit `messages` snel — 30+ items, elk met tool-results van duizenden tokens. Hier zeggen we: na 12 messages, gooi de oudste weg, hou alleen system + laatste 8.

Voor research specifiek niet zo nodig — meestal eindigt agent na 8-10 stappen. Maar voor coding agents of debugging-agents die 30+ stappen doen — essentieel."

`*[Test]*`

**Vertel:** "Voor onze research-agent niet zichtbaar effect. Maar nu je het weet: bij langere agents — gebruik je dit."

---

## BLOK 7 — Live Demo 4: Done-tool + custom stop (20 min)

`[SLIDE 14 — DEMO 4]` `[SCHERM: editor]`

**Vertel:** "Nog twee patronen. Eerst de **done-tool**. Een tool **zonder execute**."

```typescript
export const done = tool({
  description:
    "Roep aan wanneer onderzoek klaar is en rapport is opgeslagen. " +
    "Geef het reportId en een korte samenvatting.",
  inputSchema: z.object({
    reportId: z.number(),
    summary: z.string(),
  }),
  // GEEN execute — dat stopt de loop
});
```

**Vertel:** "Geen execute betekent: als agent deze tool aanroept, stopt de loop automatisch. Geen tool-result toegevoegd, geen volgende stap.

Combineren met `toolChoice: 'required'` — dwingen dat agent altijd via een tool stopt, nooit met losse text."

```typescript
export const researchAgent = new ToolLoopAgent({
  model: openai("gpt-4o-mini"),
  system: `... [zelfde] ...
6. Wanneer alles opgeslagen is, roep dan 'done' aan met het reportId.`,
  tools: { webSearch, readPage, saveReport, done },
  toolChoice: "required",
  stopWhen: stepCountIs(15),
});
```

`*[Run]*`

**Vertel:** "Agent doet zijn werk, eindigt met `done({ reportId: 42, summary: '...' })`. We pakken het eindresultaat uit `result.staticToolCalls`:"

```typescript
const result = await researchAgent.generate({ prompt: query });
const doneCall = result.staticToolCalls[0];
if (doneCall?.toolName === "done") {
  console.log("Final:", doneCall.input.summary);
}
```

**Vertel:** "Voordeel: gestructureerd eindantwoord. Geen vage text. Voor pipelines waar je het resultaat ergens anders moet injecten — heel handig."

### Custom stop op kosten

`*[Voeg toe]*`

```typescript
const costLimit: StopCondition<any> = ({ steps }) => {
  const tokens = steps.reduce(
    (sum, s) => sum + (s.usage?.totalTokens ?? 0),
    0
  );
  const usd = (tokens * 0.00015) / 1000; // gpt-4o-mini rate
  console.log(`Total tokens: ${tokens}, ~$${usd.toFixed(4)}`);
  return usd > 0.05;
};

stopWhen: [stepCountIs(30), costLimit],
```

**Vertel:** "Stop als kosten boven 5 dollarcent uitkomen. Voor productie: kritiek. Eén bug in je prompt en je hebt 100 calls — zonder cost cap is dat 20 euro. Mét cap: maximaal 5 cent per run, klaar."

### Plan-Act-Reflect

`[SLIDE — terug naar slide 14 visual]`

**Vertel:** "Laatste pattern — alleen kort toelichten. ReAct: Reason + Act. Idee: in de system prompt vraag je expliciet om eerst een plan, dan acties, dan reflectie.

System prompt:
```
1. Begin met een plan in 3-5 bullets.
2. Voer plan uit met tools.
3. Eindig met korte reflectie — wat zou je anders doen?
```

Geen extra code nodig — het model doet dit vanzelf binnen de loop. Effect: tracebaarder gedrag, en zelf-correctie in latere stappen.

Voor sub-agents — een agent die een andere agent als tool gebruikt — schuiven we naar Les 14, want we hebben tijdsdruk."

---

## BLOK 8 — Wanneer wel, wanneer niet (10 min)

`[SLIDE 15 — Agent vs tool-call vs workflow]`

**Vertel:** "Drie niveaus van AI-autonomie. Plain tool-call: 1 tool, 1 antwoord. Multi-step uit Les 12: 2-5 tools, 1 doel. Agent uit Les 14: 10-50 stappen, open-ended. En het hoogste niveau van **controle** — een explicit workflow. Code die exact bepaalt welke tool wanneer.

`*[Wijs tabel op slide]*`

Wanneer GEEN agent. Determinisme nodig — finance, juridisch, medische data. Geen agent. Doe het in code. Latency belangrijk — agent kost al snel 30 seconden. Voor user-facing chat: te traag. Voorspelbare kosten — agent met 50 stappen kan duur zijn.

Wanneer WEL agent. Open-ended — research, planning, debugging. Volgorde onbekend — model moet zelf bepalen. Async gebruik — backend job, niet realtime UI.

Quote uit de docs: *Agents are flexible and powerful, but non-deterministic.*"

💬 *Vraag: "Wat is een workflow dan precies in code?"*

**Antwoord:** "Gewone JavaScript. If-statements, switches, expliciete functies. Bijvoorbeeld:

```typescript
async function processOrder(order) {
  const validated = await validate(order);
  if (!validated.ok) return reject(validated.errors);
  const enriched = await enrichWithAI(validated.data);
  await save(enriched);
  await notify(enriched);
}
```

Dat is een workflow. Reproduceerbaar, debuggable, testbaar. Een agent zou dit ook kunnen, maar je weet niet exact wat-ie doet — non-deterministisch."

---

## BLOK 9 — Lesopdracht + Huiswerk (15 min)

`[SLIDE 16 — Lesopdracht + Huiswerk]`

**Vertel:** "Lesopdracht. Vandaag in de les: setup van de research-agent zoals ik 'm vandaag liet zien. Tavily account, Supabase tabel, 3 tools, agent, één test. Half uur — kun je halen voor we klaar zijn.

Voor huiswerk: uitbreiden. Vier dingen.

Eén: 4e tool `listReports` — om eerder opgeslagen rapporten op te halen. System prompt aanpassen zodat agent eerst checkt of er al onderzoek is.

Twee: `prepareStep` toevoegen. Kies één use case — dynamic model, fase-tools, of context-trimming. Documenteer wat je koos en waarom.

Drie: custom `StopCondition`. Token-budget, of aantal pagina's gelezen, of iets eigens.

Vier: schrijf een `AGENT.md` in repo-root. Tool-lijst, stop-strategie, prepareStep-keuze, één voorbeeld-run met logs, één observatie.

Bonus voor wie wil: UI met live step-rendering, of een sub-agent voor samenvatting.

Beoordeling: 10 punten totaal, voldoende is 6+. Inleveren via Brightspace met repo URL voor Les 14."

---

## BLOK 10 — Afsluiting (5 min)

`[SLIDE 17 — Afsluiting]`

**Vertel:** "Wat hebben we vandaag gedaan. Een agent is: LLM in een loop met tools. We gebruikten `ToolLoopAgent` om de loop te managen. Vier stop-condities — stepCountIs, hasToolCall, isLoopFinished, custom. `prepareStep` voor dynamic model, tools, context. Done-tool pattern voor gestructureerde eindantwoorden. En we wisten wanneer een agent wel/niet de juiste tool is.

Volgende les: RAG. Embeddings. Vector search in pgvector. Combo met agents wordt heel sterk — een 'searchKnowledgeBase' tool die semantic search doet, in een research-agent.

Daarna: testing, deployment, performance, eindopdracht.

Vragen?"

`*[Vragenronde — minstens 5 minuten over laten]*`

---

## NA DE LES — Wrap-up

- Push working `research-agent` repo naar GitHub als referentie
- Brightspace: link naar deze repo + huiswerk-instructie
- Verzamel vragen die niet beantwoord — neem mee in Les 14 opening
- Note voor Klas B (later): Klas B doet dit waarschijnlijk in week na RAG, niet ervoor — orde aanpassen indien nodig

---

## Veelvoorkomende fouten tijdens live coding

| Fout | Oplossing |
|------|-----------|
| `TAVILY_API_KEY undefined` | `pnpm dev` opnieuw starten — env wordt bij start ingelezen |
| Tavily 401 | Verkeerde key — check dashboard tavily.com |
| `result.steps` is undefined | Je gebruikt oude v5 API — check je `ai` versie is `^6` |
| `ToolLoopAgent` import faalt | `pnpm update ai` — moet 6.0+ zijn |
| Agent stopt na 1 stap | `stopWhen` te restrictief, of system prompt zegt niet duidelijk wat te doen |
| Tool returnt `[Object]` | `JSON.stringify` voor logs — Tavily resultaat is genest |
| Supabase RLS error | Open policy nog niet aangemaakt, of `enable rls` ontbreekt |
| Cost-cap stop werkt niet | Check of `step.usage` echt populated is — sommige providers geven het niet altijd terug |

---

## Mentale model voor de klas

Als studenten verward zijn over "wanneer is iets een agent vs tool-call":

> **Tool-calling** (Les 12) = "AI mag tot 5 keer een tool aanroepen om je vraag te beantwoorden."
>
> **Agent** (Les 14) = "AI krijgt een DOEL, mag autonoom plannen en 20-50 stappen doen, en levert pas op als-ie zelf vindt dat het klaar is."

Het verschil is dus **doelgericht autonoom werken**, niet zomaar 'meer stappen'.
