# Les 13 — 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/Les13-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 /Les13-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 ? T : never; const enoughReading: StopCondition = ({ 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` en niet typed?"* **Antwoord:** "Excuus, dat is een shortcut. In productie zou je een `ToolSet` type extracten zodat je `StopCondition` 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 = ({ 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 13: 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 13) = "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'.