final lessons

This commit is contained in:
2026-06-07 10:44:05 +02:00
parent ceea2f206a
commit 39ec1bac72
105 changed files with 7789 additions and 1515 deletions

View File

@@ -1 +0,0 @@
,sharp-zealous-planck,claude,21.05.2026 07:13,file:///sessions/sharp-zealous-planck/.config/libreoffice/4;

View File

@@ -1,785 +0,0 @@
# 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 — <20><>é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 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'.

View File

@@ -1,322 +0,0 @@
# Les 13 — Huiswerk
## Agent uitbreiden + prepareStep + custom stop + AGENT.md
**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Deadline:** Voor Les 14 — RAG + embeddings
**Inleveren:** GitHub repo + `AGENT.md` in root
---
## Doel
Bouwt voort op de **lesopdracht** (research-agent met 3 tools + `stepCountIs(10)`). In dit huiswerk maak je de agent slimmer: extra tools, dynamic step-control, een eigen stop-conditie, en documenteer je het geheel.
> Niet klaar met de lesopdracht? Eerst die afmaken — het huiswerk heeft die als startpunt nodig.
---
## Onderdeel A — Extra tool (verplicht)
Voeg een 4e tool toe: **`listReports`** om opgeslagen rapporten op te halen.
```typescript
export const listReports = tool({
description:
"Toon eerder opgeslagen onderzoeksrapporten. " +
"Gebruik dit als de gebruiker vraagt naar bestaand onderzoek " +
"voordat je nieuw onderzoek doet.",
inputSchema: z.object({
limit: z.number().int().min(1).max(20).default(5),
}),
execute: async ({ limit }) => {
const { data, error } = await supabase
.from("research_reports")
.select("id, query, created_at")
.order("created_at", { ascending: false })
.limit(limit);
if (error) return { error: error.message };
return data;
},
});
```
Voeg toe aan `agent.tools` en test:
> "Heb ik eerder iets over AI in de bouw onderzocht? Zo nee, doe het nu."
Verwacht: agent roept eerst `listReports`, dan eventueel `webSearch``readPage``saveReport`.
### Eisen
- [ ] `listReports` werkt
- [ ] System prompt aangepast — agent weet wanneer te checken
- [ ] Minstens 1 test-query gedraaid die `listReports` triggert
---
## Onderdeel B — prepareStep toevoegen (verplicht)
Voeg een `prepareStep` callback toe aan je `ToolLoopAgent`. Kies **minimaal één** use case:
### Optie B1 — Dynamic model
Begin met `gpt-4o-mini`, switch naar betere model bij complexe stappen.
```typescript
import { openai } from "@ai-sdk/openai";
new ToolLoopAgent({
model: openai("gpt-4o-mini"),
tools: { webSearch, readPage, saveReport, listReports },
prepareStep: async ({ stepNumber, messages }) => {
if (stepNumber > 4 || messages.length > 12) {
return { model: openai("gpt-4o") };
}
return {};
},
});
```
### Optie B2 — Fase-gebaseerde tools
```typescript
prepareStep: async ({ stepNumber }) => {
if (stepNumber <= 2) {
return { activeTools: ["listReports", "webSearch"] };
}
if (stepNumber <= 5) {
return { activeTools: ["readPage"] };
}
return { activeTools: ["saveReport"], toolChoice: "required" };
}
```
### Optie B3 — Context-trimming
```typescript
prepareStep: async ({ messages }) => {
if (messages.length > 15) {
return {
messages: [messages[0], ...messages.slice(-10)],
};
}
return {};
}
```
### Eisen
- [ ] Minimaal 1 `prepareStep` use-case geïmplementeerd
- [ ] Effect zichtbaar in console-logs (welk model / welke tools per stap)
- [ ] In `AGENT.md` uitleggen welke optie gekozen + waarom
---
## Onderdeel C — Custom stop-conditie (verplicht)
Schrijf een eigen `StopCondition`. Twee voorbeelden — kies één of bedenk eigen:
### Optie C1 — Token-budget
```typescript
import type { StopCondition } from "ai";
const tokenBudget: StopCondition<typeof tools> = ({ steps }) => {
const total = steps.reduce(
(sum, s) => sum + (s.usage?.totalTokens ?? 0),
0
);
return total > 30_000;
};
```
### Optie C2 — Genoeg sources verzameld
```typescript
const enoughSources: StopCondition<typeof tools> = ({ steps }) => {
const readPages = steps.flatMap((s) =>
s.toolCalls?.filter((tc) => tc.toolName === "readPage") ?? []
);
return readPages.length >= 5; // 5 pagina's gelezen = genoeg
};
```
Combineren met andere condities:
```typescript
stopWhen: [
stepCountIs(30),
tokenBudget,
hasToolCall("saveReport"),
]
```
### Eisen
- [ ] Custom `StopCondition` werkt
- [ ] Gecombineerd met `stepCountIs(N)` voor veiligheid
- [ ] In `AGENT.md` uitleggen wat de conditie doet
---
## Onderdeel D — `AGENT.md` documentatie (verplicht)
Schrijf in repo-root.
### Sectie 1 — Tools
| Tool | Wat doet 't | Read / Write |
|------|-------------|--------------|
| webSearch | Tavily query → 5 resultaten | Read |
| readPage | Tavily extract → fulltext | Read |
| saveReport | Insert in research_reports | Write |
| listReports | Bestaande rapporten ophalen | Read |
| (eigen tool?) | ... | ... |
### Sectie 2 — Stop-strategie
- `stepCountIs(N)` — safety cap, N = ...
- Custom: ... (uitleg)
- Combinatie waarom: ...
### Sectie 3 — prepareStep keuze
- Welke optie gekozen (B1/B2/B3)
- Waarom
- Wat zie je in de console-logs verschillen tussen "met" en "zonder" prepareStep
### Sectie 4 — Voorbeeld-run
Pak een echte query, draai 'm, en log alle steps. Plak in `AGENT.md`:
```
Query: "Wat zijn de belangrijkste AI-trends in 2026?"
Step 0: webSearch({ query: "AI trends 2026" })
→ 5 resultaten
Step 1: readPage({ url: "..." })
→ 4500 char tekst
Step 2: readPage({ url: "..." })
Step 3: webSearch({ query: "AI regulation EU 2026" })
Step 4: readPage({ url: "..." })
Step 5: saveReport({ ... })
→ reportId: 42
Finale rapport: (200-300 woorden)
```
### Sectie 5 — Eén observatie
Iets wat opviel:
- Onverwacht goed gedrag?
- Loop die te lang duurde?
- Tool die niet werd gekozen?
- Effect van `prepareStep` zichtbaar?
### Vorm
- Max 700 woorden totaal
- Concrete logs, geen abstracte tekst
- Mag wat informeel
---
## Bonus (optioneel)
### Bonus 1 — UI met live steps
`app/page.tsx` — een form + lijst die elke step toont terwijl agent draait.
Gebruik `agent.stream()` ipv `agent.generate()`:
```typescript
const result = researchAgent.stream({ prompt: query });
return result.toUIMessageStreamResponse();
```
Client: tonen welke tool aangeroepen wordt + resultaat collapsed.
### Bonus 2 — Sub-agent voor samenvatten
Maak een 2e `ToolLoopAgent` (kleiner model) die per URL samenvat. Hoofdagent gebruikt deze als tool:
```typescript
const summarizer = new ToolLoopAgent({
model: openai("gpt-4o-mini"),
system: "Vat één pagina samen in 5 bullets.",
tools: { readPage },
stopWhen: stepCountIs(3),
});
const summarizePage = tool({
description: "Vat een URL samen",
inputSchema: z.object({ url: z.string().url() }),
execute: async ({ url }) => {
const r = await summarizer.generate({ prompt: `Samenvat: ${url}` });
return { summary: r.text };
},
});
```
### Bonus 3 — Done-tool pattern
Vervang `hasToolCall("saveReport")` door een expliciete `done`-tool zonder execute, met `toolChoice: "required"`. Documenteer in `AGENT.md` wat het verschil was.
---
## Inleveren
1. **GitHub repo URL** in Brightspace
2. **`AGENT.md`** in repo-root
3. **Updated `lib/tools.ts`** met `listReports` (+ optionele extra tool)
4. **Updated `lib/agent.ts`** met `prepareStep` + custom stop
5. **Schema-update** in `schema.sql` (mocht je nieuwe tabel hebben)
---
## Beoordeling
| Criterium | Punten |
|-----------|--------|
| A — listReports werkt + agent gebruikt 'm correct | 2 |
| B — prepareStep geïmplementeerd + zichtbaar effect | 2 |
| C — Custom StopCondition werkt | 2 |
| D — AGENT.md compleet met 5 secties | 3 |
| Agent draait end-to-end (geen broken runs) | 1 |
| **Totaal** | **10** |
Voldoende = 6+. Bonus telt mee bij twijfelgevallen.
---
## Tijd-indicatie
| Onderdeel | Tijd |
|-----------|------|
| A — listReports + test | 20 min |
| B — prepareStep | 30 min |
| C — Custom StopCondition + tests | 30 min |
| D — AGENT.md schrijven met logs | 30 min |
| **Totaal** | **~2 uur** |
---
## Veelvoorkomende valkuilen
- **`prepareStep` returnt niks** → je moet `{}` returnen voor "geen wijziging", niet `undefined`
- **Custom stop returnt nooit true** → log `steps.length` en je conditie-waarde per stap
- **Agent stopt te vroeg** → check of je `stopWhen` array niet te restrictief is
- **Sub-agent recursie** → een sub-agent gebruikt zelf weer een sub-agent? Stack-overflow risico — limit op depth
- **Token-cost out of control** → log per stap usage en zet kostencap
---
## Tips
- **Log per stap** — `console.log({ step, toolCalls, usage })` is de beste debug-tool
- **Test isolated** — test je `StopCondition` met fake steps eerst
- **Houd `AGENT.md` bij tijdens werken** — niet aan eind
- **Eindrapport kwaliteit** — als agent rapport een 4 is van 10, ligt het meestal aan system prompt — niet aan tools
Volgende les: RAG + embeddings. Combo met agents: een RAG-tool in een agent. Tot dan!

View File

@@ -1,194 +0,0 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/PageMode /UseNone /Pages 14 0 R /Type /Catalog
>>
endobj
13 0 obj
<<
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521071303+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521071303+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 13 Huiswerk) /Trapped /False
>>
endobj
14 0 obj
<<
/Count 7 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Type /Pages
>>
endobj
15 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2436
>>
stream
Gatm<CN%rs(B)NcEFGLJ3H_4ji+(uF$b4s@0i2u$M)P6"5XA=YM_RZC,S_[9dbhcOL/6K[ja,N66uk8.m_*[KB-`;n,M[LS-Mh^Whu,)s8=EnJn<m)D?g-%q]ChO"A`O*7PiaH]`+!<XW4M[0$:5NJ1)?[2`,t((Y6EB9eg.LQMF9jtTrT!$+;TYndQTB;Jl%45eghM.$.1pE_D&^=^=,k^ojS\[g-O!W+(1HZ3hlmYjYJN@.%pC)B#"?:h$(JEY>(RjatflE2jHs9%5"B7kj7<j=^>Z_A!ZbNq-`c6ZSk6n9_8!8Ipln_n9l$*?h%eGrteUt;"T('eT[n%BlX0<>\?Jl@KQ0@2RQ$^AZ'7@"H28)_&8=%\//W1g@M)4h.BDB*b$IAB\uLl5L`;ccQT.An0!Bn4:$UG4-#"g73\T09o-<M2$b-j;K)+epGua##TqIh-nP$NeB%19c0m9.'LCrb4SY<NXegX:'!\09e7e3gs3Gkf!3%/*[9Z7\CED]eFr>M1#%>E*[=*9:C`c.ALrMqns0M)./7=#?>)\439El\1ZLg.h5A_ltpG6?52Hp_pZ:gNU#U@;WUnj^j.WQTLdh$\bgW/i%$Fh9'>`hjDil3O8_J0)'ZYQ?LD6rB11>1(ioIXo0ZUlY<[P8^`V;&R1f7YgSFE[ZKf/[%g&*=:R7CMD&B;Q>soFH=09Xsc*HeiTN<9;C63E3S=4U4<c";\HG]Cc?4B'lmNgjutU3I@+JSF.dgeLF%%faW3V[b/P_>bH8ZEDlX97f6e[AY'Y!YU-u5LS-6KBZ!4Alr$tej<Die,ONb/3P97\QY*ePMX1*NRj<KL9c$6Y/ZVQbfVk]?\EBITXEhLm6m?Nk8H_sm:fWgBGtC3CTR$6[q#jonA]gX.[bL'Jpi#n[$u$QO$VHDiFY=3E$)h,-KSM\JbrH!s>_UY6-9mIDieHGbDtiG&muN".hD^c*4dMPt1,j/;g/+5N)bVf"]0HN1LN_V`Hl+9FgFf9/a%)e_%qtA+j=,]OrWo7oLlV?HCUKGU,&40$[tIt]e5d?tk1t^aRl`c3>1%h+:uQh4MWFqE=Nk:"Z"L#7!U;h+>@u6)#!K6R,p(c`nDnm#^C.S*Q%Nd'bHC8_SXlEsPU:U*A**O\1Z*r9W6">GEdQEb#?6>1;"p<2E`8<oAX,5U2QKMgOt'bN9O.HsRt_V7?u@hW%!?O\O)eG](\M5apu6O<bikdoA_,5.B41$N\Y8bp&*4ZsZrhIBXk0F2-1eC@Or!\CQCj%FSYZhnAQfjD<TYZ>d'$,tAZe2LEQH30?9h\^DJ^+HVLfdkR3C$lf4JSfcCUUs^'\!G$M`..;R,W`Ys(\j'd?:$5P.=I-0/BZ4,e`sJ>$ph:WB3uB%ckub3;IDcUkfY$o]:QNqs2P86Z8b#Ail>2ePs[+%W)5J"M&hDoML7%mYN(@`_P&C<oBr]>]C?'?`I4Fe9<R(R8C+qB:'>)&TH>QKL$"1`2`-0,#-Wd;JRSlNH*b/eHu)g^%_lV/&0fYD*Ua^TEho^&"=@[.uZE,ZRbf&(%gEH%qhk*$8WX$Iu'cOhY/qhj*?X7D!JAFdg_i=BW2NOX[br6WLFs>2]qtgE>F<g_:9/r.//U:'FjC7>;,/9gBd;Y%''Tr/oH3U*&4O\X'RC]5S`.=[(k0O/=/5T<gobIAUHgmEg>n?H@Q,Z3?Re\!%"*L(Q\1%lS_YXXC=J#f]A;2LtI6"!;[QerSC@[sJ2o]\7XimuQ@lEoW',d2kp)8*_iqeZ,+0\GF&;GVT*.s+iaEER&qQ]82jeMKGCifqum6*@eI0+jMhoJ7H3F6q?jS6mX_@'#el(;lHUkqP,l+oakDfNp,4"n;)/h\j+'(S',Q9h7*rVTLY[-?aiCapRt3Bl\:9BWT_d[9p20m%[*G,0(`EtO5b!5=sf4\qaG,*G8%>pBc;/:EW@iXc6b,P<c#4hgN7#XaXR2)b00NmrWg+]Hj#$:UKZks=^qJi/a.;aR%Z*D7H:Q:X?`hn2p'+Z9VQ1*V7B4,3F2rKj$W[;)TS/d[%b8S?6IT$,2VWt5+6TJQr?WB:IU/Hp:42S,1-pNVaHd@J;GrsrS9s8B7^X,o\umfU#HHS3k7l`>&Q@AAQ##<6"B#VjTnJsY,ZNUQlYk\j7fRuQ_U,2]*A__6j5c#_$$es\b"fX[QL8<Xf4LT:g07iB=Qi_jrbq:YjZ?ZVi[`L+!=<d:9Y*tSUrf[n8q9(#_$9-ol9ID@THbh"a:GZbhC81pQ#E,K=da6qXrMVSEg"PLK:;*<Ws1Y"/kO?X\E(GkNU.jBVeh,Ca8AQ4#lQl"%sqT])CTl&(:VH3rP?'bHAta)]9GZ,J-*(#PO_I>5$oipZtm\5B\.i:-7Z*&1O?gpa_(?O+1NuMGH3G/jpD%<Ca:%p*Zj&SM[8Ca:e3%kN%p\PP8FsV`c+~>endstream
endobj
16 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 535
>>
stream
GauI1bu3:M'SZ:1MR-!E5tJ:4a\+ZH"#KQSl?:.dj'3Rj=>mH5_qS7tSMY&Rb?>g$jEp<=cBSBj98mN6!q-Me>mL^miZ:E@L#O!mMrZU$;G3c?Ygi5#QXG*Mq;CSW+k'-J1)I1M0P_Ae(!PCe6H2&rKRkN+T-OK^BPsmQZQdk!+4[8_9,UqaQ>)oi>sIr6%:7?P9G]F]*Q@"[@Q(Vm9bl\41Du&cMpGaZ>iDuhBrG'<\&S1#WEKJ\8SN-mFa?4[bn+QF&+ke2?/LeQmnipMEO+i:Vm"O-WK`l>rG*;o;HuCC___+D8b["'2m.hh'=s$o*g)nY\]qXA_hcol]UhfVA)ZHMjDHC#qNs(29)^QB4`V%?@6M5a>Kq07M`Y@k!TH:R.Te$N't[emJs4D#<$peqCFm35`)p79^2*UrZ?.tsjY-pHehLc!'?-_gNlWFVWYJMEjPE:^^AjtqD6'5*=1-#@p8>o!gFg3sBP7[LO&+hD:t*jZA[B=\+fs^N];<75@6j<V?'>dh3&fkanf'F+h?*~>endstream
endobj
17 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1696
>>
stream
Gb!;dgN)%,&:N_Cm-%#9/Hf,fFIUM8i\[(#8^jVdM=Np\YfrCLRAB5IM>t6s?9h]&/A'.d;orO$9Ao.<fQ2i`",^I:r/WXaVfR+XJcXmc56<0gK_]E9T6hq,iB@dJ![9H9+U8VeqgiePje."\LHK+?Bg?b_6#&lr%Q/2F(Bc:>d_O64Nr]Q7)JBFF1Z`EPb'64"cI'$Fb4igHnQgOrL)1K;M[mX@rt#TcY\m>K<CfHE$5C]?R(dkCq^Zno/56j$(I\\GeYS+`CKPNs^lqj+-WQF2[5ip'k00X?.Pl@`"7n`brkS@KiCfl#'K!=Ys4UGf"M?ikWcM,<g#DuD/X&9u0`lZVMqTEO]SeV>RKs9=B/d!'-ZEY(OM:?1o=,u]e(IW[#D?!S7lIN^?BVP.k@9G8qo#)4@=r:6/<hrEfIp4^YQ\h>\,^hBMuiY>e789<%1fE?U<ZEud34A]Dd8C[?oO(mb[^+oCQchhgV&+3_Br'B]fshLIZ3tO,`IGICLDrK[*m3j4WWnt@I5hR?Usm?K-(_T'jl!U=GVf\$%GsD<E,F][95e$R'XX\9b**GR423OK%[Wcfp3M)86ohtN'6/u=Wa`=C-u<B_o(Ed^N'Y0T+Xi?`^:Dhdp\[kH'1Tn$4EP0o5Hatnud0I2hf`'5TEd9mo#t5a,6u<A(>FtrDmcAEgXRHeVSjTXQ@k$:O/0N0kA7s8[er!:]ckJ27%C6'K64"aB`"R20;1(,.uaAb`b?>MO1ZGT%)7"*)',dHpT+g`YRD)M*5JZ),WLmIKS-T":9_kOH:K-W4G_V!bXftiPmW&d!G>O%UaDJAP&_R3RAr#DYu2XTm85h"GBLpe*B"/ElNN$;'MjfHp-Tc+K@We1$8@G5\4673R&%-\I4h)WX*_a/f$P<kV7J!?=nP/'bD[F?k#4`EeJrF*'D54I/2be8S[g-I>hdX&Uk+r<5]T(m[sU<2YCdP_rir*p7PNU4A$C<a`:D-A2-BgMskS='QujFd^5AX2tuI%'D>/Pp$VcsF4+F_W/GL*U/5ZQ2LHfSns5Rp:`!_:F+>'**ZEP<,-A<&'D%JcN9K$FPMn$O9?G3GAGDIR8dS!`L+fd.PDd4Z/3VgKUSkHF!K7N&m[9Z*.`-QZDb&,XOqM>]X&-tKV#90`gk'saikeUj.fH*O:<q)=VUHT6R&"s^qphd*ZE>Od0KD&smnHd$a-2Qgl$,di[aK/Rf&3pbIh7mQ=d:<p_ulaE`@S`\W3#m"U,19$lK$uC_qetG71u(Q^sGhOPL:FG[RPEf,n_\L`;`G%,[)L?<og[aX.IjQP2QFS\QsluAS-"Pr8o3APW^g/3[6=j1XZ'`70q519rC:1H:oJLrQQHO]nHr(FRXG!,'6C02Mf^#cNS+Ld;\cI;nssj];?/BA*tTXp=&n/HM,Mn'ZicLEM6BWni)KkN7#Via5+@!aoCi!_'sF^45!MBXf9.8VJ@r&\t;qKhctrH\t@*L:YIr!ZaDH&(jA2ao]%eM9Y)o:Qf&h"Cb_D_Cq="?YVA[2)CU?:YOcnWP4%MIH=JY30!S$<fd[Gs`g(X_&CYH\)Z+$!4ecd%9B/LthqSHDDJ%LPZch;(7n4o/iGVp\+%/s48Ifa&=%-k9BsHH@o#05;j5g3igg9+3jN%3XF-7Q\Bi:j@Bn%Tl2sHq=Wt>0.q[Unkc:)n&Jp5HU]RKqRG(%I~>endstream
endobj
18 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1699
>>
stream
Gau`SgN)%,&:N_Cm.a.I'*MAL9PSne2]'f9:=H0/6^6gh'I5Ep73O*mg._LZ$XQ*@[]I'(E@d/A/V155fKIC]",\2OI)d,?dj[D,c@Bo%/q?M;@JU>&9ta=DiB@dJ![9H9+U8VeqZl62b<bsB%sc5]dt$XIK%,`m*GXLl0*kY[V1HTKNr]Q7)JBFF1Z`EPb'ZL&cI'$Fb4ifso/hETdL1ohYsXuI5D%i"eh,%<$:uXEJV%S?POmD@T>A][BrZDX'Iir\QR6%h0]P9G-YEs]<)liHAe%%n$^2@lbiJF?9L$DdT(OtXQU295BX!Krb#H.Z]\Fd_B2ef[ir0QBP<8n,!d]Kgg*?F+@iA0<KS>Ak)gC'N\2.;Z?i#k0>`na2fi!k9KMnd>2QjYhP-Dab@uKWm`1F<o%F^8=-t`m,>c>GH$eC>X"F<Fo"T/)GA,)8t&#fZ*c:dNOJOPG\*6E*(>K1@<Z+':mUp?UNp(*eMjCtpO-b@K^;9VAD=NJ#WL:BQ#h8JZ%'GnX?D8(B<iVO(58GH24abaIM-LNekVQ,O_n8%tF0(O9c-O+1H$WF4W"I!bI\1]T7OLiB7@YFH)45k22XLgs<ZrO>A3-cIHb2hRaN6/m]9Dj6s9MSe978@7efbh4IMXlBSl.k?\[cgsl#k%sa=%TW.o7q;OhH$#V?JGE<AZ]"1`EssAn6n+[RX.1/`G(MGNFEaD]uM&OY,u^_`F)Oe]T#YJaWP9_PI%*WeC7559Wj3IKM!/A/\3#;Hd9W-(0jJ_5WN%>Uf%8?E/#QM"uun5Jl'>q/"/`hd:54fdD;1"N?6Y9#Hln\@5Ysi[CQq]2CV"sQVGO=7kmn1R'2qa:QfO"PVps?Y75m4:+>rFM\7iF+A*+b?XGsCaA="]:cFKJ8)]10,r&c.A!BfDIVkZ=D:7Lm[eCjqV.,!#MR5F!,F$mu'VAN0n9&m[8&t,\AkkO-[+(O'h>hkOFd+(P"%5a;ho'+b\J[pA`3Ntq&0R0OPMM"oV1JC1K=-h9'eRZ:Bm_`=W8Q_>kj7g<bmpNn^'M*`V`,,&JcrrV^NTPAE)1F4k-9=8GN2=pDp#`IM.7+h:J0lcpi]_snFc=c3[[;_jSY"[$?+,1$^2B=$!"5Z@sg^TmJSETYhJIt0gapt:+>o892*E_idp[@,pouBeUd0]9W5NgNe+Oh''"GN'&tNb!2G1M4"`6lg1ut/OkVEgDqi?]CTUs,B^;o[d2[Kkb`cB%M_qG`Nn1NCWjT[VSLuF%$(aV#>+LaDP1HM;:j(Q6XQ/I=3G'NV*?Ds^:Z)5F@j-*kr[r<[<_ZJU0C'>m++a9cneN(u?:q\E@nqsK6^!oOqj5OV&ERuGLVBC\r^5/O;%[ZWq7W%5K_Cp'eQBre.KigJa]`e=*!9iY8p3FhP+@::@!(m\a%G2dQL0,#=CGtOmr"m?/&'JAPLjHtCa/Sf%ke5te0F=JGY&LJW1%m]SP9Toked74Cq#%C]RR-jC+P59q\6kXkYh!#T#8S`J[i$]'8=Q7Uq3OLGNWD3a$+5/\<!`?*-XpQI_Nf\@5$E,8(/J8a#]fs+P96ugKhd[grS+7MF;g@g?qGt6%&E063F-0\$!R!Xk*0^`,5<+jh*$l>KSqGf7Z<%@9\qka`:sUh0f_bpC::%^D;0^_ikn_cDMqW_^;?]Za>M=cVY/CU#<i$\E8>:VYPc:0>G_$mJ~>endstream
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1837
>>
stream
Gb!;d>>O9K'Roe[380^R$AOu=gl0acJ7Z>`/+u<P^?!8c6SLKCeom]Zec4J^G%*NE_F8-B#U3k9o:>>TkF=6!pgWpRBEGCsK^JLB(l.&f0L5gM5Jk7`qsG'!\/#^5Dl=as+G'!1TGW"n84?Ln!sXKp@^H,D(bm?k_"!rG9LE7IlrT5Kd>DZ`J/BVu;0U=_6ue6]"d)C\\'NCr)+Q>&C<d<'2STNP44mf;(=a4S18uQ.-SROVFE.X/o!'dAo/oh-c`44sGm,G<2=)B%m?V<]<]5\Vp<'9.FCMRk",7UGr7/pSi<s!*iD[FKs3[.JI/#Bgb%k3d2RoBsEKZ"uHU(rbW!alik=d`j`-7<jA?Eg6+"^uMTnJ#Sa!GF@gU5PkRSYJ-DiL7^/jYk*]Y>X,HQoY!_ZscjF&;7%JGTBS/[iuo4Y;6(6A,IXc`+tS;q?@f>9MWlM*0Ls8F`=<jSWtt7SiWs488$5Ej*LWg[AG;eBt=QRE%hDB)uEli7n<">i/(Nqs7V708)2p^AZp+=#GFlpg&9`M.?VkmLoNe*sY;'15R1"UR;LgYY$I*#$87#V?o`LB]Gf-:]YDX_dt&L^WR4MAomP.'W+S34,S[.%.e,#D\[P(D8K:nE<`8!O@-Rb,^6Y,I-N!7Q,[PpGD<liJ1rscV;g/h.u'F@jWb$:rd<7OiMfO[@Oh^_\?JdiR:uH#2N5,['hMt,)rqSNYSb*Y&U7f=j!p[=PuSDsG+@33RC'q,b!$JBJ%UT^<d3EOmDX>Q^0)<BhR^#8Sp]4_,Gt.I,Vd1__gH?5XY>uQ%(2b^F!hC#mS<NJ=]QQ/UVY>kUTmW5T%WB67,`5$No2i=jF2mCNC/2eRL<7o<-*Oi<r&OgDO+8$*O!rT-`Zd>%,9)UVg,b_OM24B,+mX&o]<fgP$G\B<r%_XVc`E)_B*2P_53^Y4<h%`;QhIQT'2qf2SHJZ:Rc-4O)=@Um61L#Xn1H5d8RGV*H&&/G>;t*D.o)!P\4R!g9!1RQgV<9^<*_6(6;6;iL?0/-./O:8Y-`reu%t<1!:WP.0VNR^TB_UQXFY7Xs:FFI:eBZRh-BER(tA=9gJ%&24duj(WBm1(QSLRN2sO^Ae[<_S3Yg&-5u/u)FkR[S'Gm1$*B]]V-Q_W@Al9?/[Zg`,#hCp?SRI%Ok6`j/Vm8]ePPWO2UYe+YQ/=i1n.YQm0Z1&s1"@u)q4\OG$q\@jgo3Uks.>5oCJM2h;cY\g4u>E'4'En#IT\^j9Vf%f+,MSkO$'SDd`f.a[HE="`2/pCg85%Qfp[nCuO`G`?ccIQ2n)E^0*U4[J0gB)\tF227mh)#de>Ke/&,@JSTUYCr7GX*tA3eiX2[P*l=FtPPtdc=Xo6.TUE,@XU,C9[XJI<NHI6SZ(\Y[")"SY9Z\>-[Y'@Q(hZ5%=Nu^;3FR"##U4h\h?o5*L^6SWLn9A*<#2UJr\t+T0*Ps7bCr0QM[G7O`paJ^?a;Vb49QfbEHG:f+o6R-JKQ48^,@!^gqg_pb'q=7Vl6+X#kG2P^/)-)`l$B171F2Qn).lgr[c*kG$e:OO3Y_V'C['t-7iat3-@!"n7LgIDY`M;CTP(OboRo63VD,.pHiFu2J)nrj@be)ITkIbb&qn=G>O7A*gnBt:f@8)Mn5,FUmpUIL;Wk1ke8Jak;J("`G92cG"qSu$JN@XGs6p!I?)k1<G%&"1Q8qg>.oNrd!`#mCR$2*p0ZRkH0[6#Z.W="rpRP[_:]\Q-sVJZdWBQj^-=Zt:ACu4pGRqglLe.EZrQ/SoQlZ:PG\$<-k9VtrWTm6>YsN^(o@H0o.2AmM_FF52f[RhDu2>D^lJ!Z(_>K<]QFcn~>endstream
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2009
>>
stream
Gb"/'gMYb*&:N/3%"cI<BcBO;.#Dul"d66,!sIFsj)eZ+:g%CIV7K*iS6h;,hk@2j@Ri_R^24il1.<80+_T(/7DZnQXhKE)i+N/s-PRt]aX<aOE1!03X,1h-&EY[5gH[;u=rme*=o&["KopoQ&BA9%"_"u1\/`#O6jQQg_f;hP?-"=>H=B\nY%,%p!tj%+KQCe=73'<dC//TcnSXD+lkA)8O#W,ABP.4(@+^)iUcMJ5'9h.Q%?hVuDu]fU$?+@H/:qH%7W>&A)(ZLr)0=d+FT8Qn_Eu@)]TaoaV-,>_[0MmgQorc;Xl4?E-JUB4+7[Zf!Ru][AQW3\r,[kpO1tr6+.P3]GIXS'P/A22>uM;-6O*(o.rJJ!1;`#\G9Af2:$UK9/M`\d9TtCB2C=l!S5@gGlRd;7-SW+i]V0pT%Fm=]f[$mt_mX77V_au_#ggB\4u+NMV>Wcf"#@Jk!lOB,M"!oIA2C`jZ,Sm[Q:Bt8qGZk]_23?,b'P-Q)ZNpo1QU08B1r:o>YE_Dg9Xjrqa+u0bdXckpeHP3&U"t_1La8n*VTg:UGOD-R#]TgKkV`B1u0,A"@niM-X<e"J4lXlBgo2&]A;gsH<Rr?`@8`'5npIITV#l9&!h%?`>Pn6f='TqR$B8)q:ChFQF"445=7<Q_26_7)>OXK/5fmJq=/RV/4bgVJhVa,npONRb"i"abM6/sG9q2JKfBuMRc:f851`b!ao"34N1e%BH1(R'+qNUEYt&,9<j21A3SU`S=f#gBKo]]Kns,B[YYc5Z`:cm&OofBm;k\GZf3/e:.6t9eXEM2-i3$8lDq>aB**ijUp._)=42g3W]A=ac5(V*s3+IUL_()CdNME&9<r*`)0rOHh=jXQ!<<#=dJLe[mA$ZCrqmj'VLn97PgY.VTg%;`m"i18DMH=l**dEW!PS_doh"7W6*X-7U-n*4tp@IdCbi&^NnerjB0$_m%44H>Q*IZh8'j=!T"DWqX<JUWp3g\52dS)!:3$%B;1:>HWrA[KfSa#";!A)hg?P'ld.__Xda(*cti%+S$qE_+AoCe&=;^\'WgN4._0IrnhWe0RGc/\<9qjMda]V8jDh_Fs'F%g!TDMA.-L:cJhL8m*-G:A_q'qWH[RAeSD3jOJHF5/?5CI<!:Hd;r.@iFlBVjd=N?53!sCO2KD\753,@Q_<0VG4"![j:-=8`X<Vm*1he>ka0NBCu`CXgr)<p\&qQ-<[=nK;FP*0mk51lr"&Afq?P6h11(p-AtJ"Ysi$YAoKabC+YpM6Ig.u02fF))#WB/&jV)PET"fC0YlQ8#d5GZ`WE)?;?r&.NI&2P2l%8W!DqMsg-45A!b?QpB5.hscPtCS_]Rd`baXXqA@YDSb7u&IYkqN<E@kH1ZpSl5auhu1YIT$$6V`mDU>TV`M4m&sK!dl_$CCIt_94e#"NTnM_W)mkX%lVIYq"GYcB$t9kp_*QI_o+>@LEp'<P\p.@ocslW="r?Tk3rMO3k@[$\'J[b:LogmY1dOY&gV'2>JPV_B>(8@TuLd>FNc5\f5"+VMh%3eCo;]lgZ_>",quj>[E\(0'BjUV]rW96)dW]&g:+M%q!(t4!F,6Xte^u=AGu/Nou6Q_M7%N'aGD3'`DD%s/nCiGg;j@s5Z,jq56f;LW1<>ngPPMFAL-VW7F=ENV_OX_ps9G[j$9<*NE-'9c.gA_X:Z&D/3upZ[1ETDge7`mM.uK*ih]00m1+oDDAQ=XH[-][g*8;s8#.>cd[fl7VMWCo]o>e.;,U>=^#BG%,%C<>V&m;q17<^/QTfN_MgX.RIbLD3orY"RbiLbVWo3hr)!`&%j8f?NmL3Qp\!j<.%D;W"Xh,"?[Y6m<%\+(O/R;^GSGW)!W:TRo;g3M%DN=\cd1^`5.Ee1TYt;"Xt&R\fKKXFqZ15WNb_q0m["Z"m@cu!5`2TJX3<QRn(B<>,bZcm,q+6i,KuacR7qM3&CEDLgQjrNVP.>)I^'7F<>mGW10O2qE0Woa<+6.7cI;%7D(u4S5;>1f^A~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1570
>>
stream
Gb!ksbBDVu&DcY&As:e;/X-7mmE0EH-TsJ('!',D)i0.q=<t]_TRG7Yr-JYkorr74D)YbMH;GjkpC7B`(LmdQ^8V&a)M%!,q!f.-Peh/B1a3OUhS@=fjR\&/KV_NS,g:ioB0c%7rIM*u/]7reEgBYFJs9e["AqW[QK8u<S<p*0"YF-XCdN]K8W[NrOGq1NQ9:NVP(YQcN_#Zcq;M8!^Wa6TP@X:qh&P8WL)+l9e.sp#83l2lL04UkDh%ZZ)IBC8Qsumk%DQ+!MNrlU@jtZqSmE6YN>3bP&@\B7>pEPM'P99XHfhj3n=qZa;&H,'n_XTS1k\rD1j;9i#MaO9OtDH2&*Q.\oms#\`^gjM&T5UfR,bh)gBDoGU2>kFCL\#HO4fWM"[@[XlXgn?:*SlC^Aq$^8_iOW7@h5*O03M]ldH'E.e%/s`jFFRaolD%r:XE2Bn_k$F(EULEJ$1Cn+tUmG0`/;Q5`^/q-S*HggaU)G>63(l=[%Mofe1,BktLqO+e[+=."P8M1t@D55;jC6>=Zs@p-A8E("?"gECu!AF/n>IA]NS[CEZ>6fL<Uf#<5^%:ZmG)gimc;8.u!&`1rAp;X*:Y-$tA<Ei'G<BDc/"P&<MPUTB+pkp`Q`RW/@1X_L)\u)N^YgD;U#(t*^_OR8<iPK)H=H\+PrC3'@F(mqIjj^:Z?=84_$.6UVX#44*hjXAi'9".[4iTJJlqCR-lP!*I.1Q!pSFHH6T:6.\=iNd-(l"J!H?IM0F5XsdKZ%i=Q&oTZ?LG7VCXfQ^pG=F0)\#FhM(-]>L-7#,;%daU5#7b$"DT/`^8M4F["b<@SN@O)(@n?E^`+D3PbRB^@0XQ]'.2LuU)8bN?\?_Mg>0cY^h<s?K5='9D@2p&Y4W(dIL"VQJHmaq)P]_Tg'lkipU>YfI!Q"ajGDXd__kkMo9@3kNi/KWqbD'^IF3+[K7FtWaao!0@S:";&+SGu!Rt>G8(8X@?3.#-+ETS5%^3HqZ]@H&[n>nm_Lo$+E6IQi0C)KpS.h$?@,i3h\?+D0G;c@5H;@,>YeA'!B][qVa5N#.7UZ=HrAG.Cp)JkX8WTB7fo"5,3TL3ZPR@?B_c$mMc0AUDW3rXu',ljTk9u3R;oJ;<267JsLdSR25fCH\C*fk\Vc\dbm70@BY[G#mr6+#hFh;@hQ;0<'#CtG)@QJ0[mWbppgpeXLF;+U*7;#][=2Bgf<8@npXOWkS(V5_fjV[)"k0-E:Zqpr-HMf>MU9)ACjl))DKBt#@c?`[`mU!*U$LqfK7PQ,E%GB-d%C$r.)#UQeQK1?'Tnnl%@CqD>Rh)(BIoNmIqa!=PA7tmEM8%Y=Sg!W(CK)kkVUbV2.h7C'*%X"iQb9KGq3YUJk"e8]b[ssb/I;9Zkc:[=4%rs#G'/'0I!5haP-9Rp2^!cCX8//OAr^a&GVnhb?E(ZoS_jg#"4gPrclcbFHImRjEo)D0m@e^9`(-8?Y,d0I*Nu`a_h+t$Ab#e[-[qBWK&/>_>1anK"%9URErc#5eWlKA>*tr?_o3AASS/e5/7i$'!gfoi-pQVl"'@1Y"n;(JNYEg6rWJYYAB+~>endstream
endobj
xref
0 22
0000000000 65535 f
0000000061 00000 n
0000000112 00000 n
0000000219 00000 n
0000000331 00000 n
0000000436 00000 n
0000000641 00000 n
0000000846 00000 n
0000001051 00000 n
0000001256 00000 n
0000001461 00000 n
0000001667 00000 n
0000001873 00000 n
0000001943 00000 n
0000002236 00000 n
0000002334 00000 n
0000004862 00000 n
0000005488 00000 n
0000007276 00000 n
0000009067 00000 n
0000010996 00000 n
0000013097 00000 n
trailer
<<
/ID
[<464a00a4f31894d5e1fb188fac933ea2><464a00a4f31894d5e1fb188fac933ea2>]
% ReportLab generated PDF document -- digest (opensource)
/Info 13 0 R
/Root 12 0 R
/Size 22
>>
startxref
14759
%%EOF

View File

@@ -1,282 +0,0 @@
# Les 13 — Lesopdracht
## Setup research-agent + eerste run
**Vak:** AI-Assisted Development
**Opleiding:** NOVI Hogeschool Utrecht
**Duur:** 30 min in-class
**Status:** Tijdens de les afronden — daarna huiswerk uitbreiden
---
## Doel
Aan het einde van deze opdracht heb je een **werkende research-agent** in een eigen Next.js repo, draait er één test-query, en kun je in de console alle stappen zien die de agent zette.
---
## Vereisten
- Node 20+, pnpm, git
- Supabase account (mag dezelfde zijn als Les 11/12)
- OpenAI API key (zit waarschijnlijk al in je `.env`)
- **Tavily API key** — gratis tier op https://tavily.com/ (1000 calls/maand)
- VS Code / Cursor
---
## Stap 1 — Nieuwe Next.js app
```bash
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
pnpm add -D @types/node
```
Open in editor.
---
## Stap 2 — Environment variabelen
`.env.local` (in `.gitignore`!):
```
OPENAI_API_KEY=sk-...
TAVILY_API_KEY=tvly-...
NEXT_PUBLIC_SUPABASE_URL=https://...supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJ...
```
> **Let op:** `OPENAI_API_KEY` is GEEN `NEXT_PUBLIC_*` — moet server-side blijven.
---
## Stap 3 — Supabase tabel
In Supabase SQL editor:
```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);
```
---
## Stap 4 — Tools schrijven
`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,
})) ?? [];
},
});
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) };
},
});
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 };
},
});
```
---
## Stap 5 — Agent definiëren
`lib/agent.ts`:
```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),
});
```
---
## Stap 6 — API route
`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,
})),
});
}
```
---
## Stap 7 — Test in terminal
In één terminal:
```bash
pnpm dev
```
In een 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
```
Verwacht: JSON met `text` (eindrapport) en `steps` (welke tools werden aangeroepen in welke volgorde).
---
## Eisen
- [ ] Repo werkt — `pnpm dev` zonder errors
- [ ] `.env.local` met alle 4 keys + in `.gitignore`
- [ ] Supabase tabel aangemaakt
- [ ] Alle 3 tools werken
- [ ] Agent draait 1 succesvolle query
- [ ] Je kunt in steps zien welke tools werden aangeroepen
- [ ] Eindrapport staat in Supabase (`research_reports` tabel)
---
## Tijdsindeling (30 min)
| Stap | Tijd |
|------|------|
| 1 — Next.js setup | 5 min |
| 2-3 — Env + Supabase | 5 min |
| 4 — Tools | 10 min |
| 5-6 — Agent + route | 5 min |
| 7 — Test + debug | 5 min |
---
## Veelvoorkomende problemen
| Symptoom | Oplossing |
|----------|-----------|
| `TAVILY_API_KEY undefined` | Restart `pnpm dev` na env-edit |
| Tavily 401 | Key fout — kopieer opnieuw |
| Supabase insert faalt | RLS policy check (`alter table ... enable rls; create policy "demo open" ...`) |
| Agent stopt te vroeg | `stepCountIs(10)``stepCountIs(20)` |
| Steps array leeg | Check je `result.steps`-mapping — niet `result.text.steps` |
| `gpt-4o-mini` does not exist | Check OpenAI key heeft GPT-4 access — anders `gpt-4o` |
---
## Klaar? Verder met huiswerk
Het huiswerk bouwt voort op deze setup:
- Extra tools (listReports, eigen tool)
- `prepareStep` toevoegen
- Custom stop-conditie
- `AGENT.md` documentatie
Zie `Les13-Huiswerk.md`.

View File

@@ -1,194 +0,0 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 4 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
5 0 obj
<<
/Contents 15 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/Contents 16 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
7 0 obj
<<
/Contents 17 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 18 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 19 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 20 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 21 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 14 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/PageMode /UseNone /Pages 14 0 R /Type /Catalog
>>
endobj
13 0 obj
<<
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521071303+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521071303+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 13 Lesopdracht) /Trapped /False
>>
endobj
14 0 obj
<<
/Count 7 /Kids [ 5 0 R 6 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R ] /Type /Pages
>>
endobj
15 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2307
>>
stream
Gb"/'D/\/u')p0`0it#uP=DV/jkI(+JLIMPo@7fM>4Gl!!sVJ:Ct]T&OHa&JfAEn[[A;mXgr5mp)'\hBR@2m!B1=D-j!1#]A5;c79&ne3#t`Ul,Wt5:FI0G6c528X5_bk+bZ3GX,dGEJIoN"#3V`[`+rL\b4Cfr6aA[[>'e,;J(PX-0;Uag\NZ])A4B*@V-2^"3\(8/BO:n9Njp+2Z\\?Ng)>,0hFP9%*p)d\0+ZcALWp;Alr6VA+S@8,T*:D0<Mc3n-B17s1Vn1C"=\VCaL,@R$9r]\FC-A>!oqfrk`E_U!/"3'YoX0f=_"XYFAE8I1pq+*]i=[3N@TLJaH9XSfo9dGrlQc+G"W#0Sft\=RN-lH)n_g9+!S5L90D+"ik*2PO1!'MR1,shM\[$,YPW6)pKZgmo$a[;1$^_ljLb+u+;*2r.ZTX.$(s\XHR-iiq)XiA+%Ykr?^:X3s_t:eVSMLL@c%K!CL$]N@i\lglYT*U,gSG9K2cN0!A]VC@^?TR%;)cZ*ZTZ<OgT\-@*o<[^?J,9o])hMk08'tO@;nVA7#710R$g_h%HH=g,;9-#a-'`H%(mES/j`XN'LG76_eA8QNTUdVGs)]WWXoV>\.:nr/<[ju!TX>g0,N!AdW(\FNT*mT(]?a*Q\[[PQhdBW!nbk]$j's!_1oQDE'*$:_jfW8?9fsr-nHsVXk%Q6<P&CB[/]H>!lm&3QjrFJ)LC1"N%]Wd;gPBB"@"Z=58@D(@m<^Zlf-hBr2YeOFh'%F\)OCF)cjJ)ehgab3i=eV-!=JQ%pAC%"ru-4]r[f56Ap:3Z,PsM+3:@^BmMtlGssM`)+#SI>S]=\g&RP[E=$AE>Gfr9/h^n+egI:h0n)cbNuM8*YP*C?3e`sYnEPISkLp6Oe**F=U[tKo#=W_)JuBt"&/-)>*@>)0>/2f:O!bkAM<aj]6PSg]VBeeN-8umD`T4g4D*s>7n#CN1Q#=r-JNBH0@2@O^TgGpf!QeMe]I-g>8(JhAK\e<>"t@C+a"Zs/[,8OD1SHW$pM7pB+*BcZAIFE@UcA&VAE8@n=k%'6N`C^_qBIh#.Xa(#^jX.,^'4?S31&t;oDZso:b6_>30'-aD7ZH]Vd>Lf8Y"7tY">P\8%bD>\H\JaA3<_Y^(%*]+e)o2U.'G*n!]eKR'S3t&3)jK!EIR1laMSj<rWBecrE7*ZIJ"/S!@K%!T[^X)kd7d[%tdD!o"c/po<V(Z+u<f<9NW4Z/>eCKrBRjN<N)^89g8CZpo)p*m4-W00m'u[+W1I4sY!@B^VicT2aVtPIprWm.".u'$'_)"dum"I;B^/c\;'.)i;%l@hi*g,Z&IQSiHkIU*&G3Lg)=*HLQ:KH*:bffU=P!]IPHiMZV;9(<SFU8*8s,#(auMJEn\L0ICoQCKDk:D;KXMn=--SQ/E[JJL=T)YH6\)C9T$_h0gS7?dQF`q*-+A.p<Yk%lhfI;U(=0_gCt`Nit%[H<JS:Q$W\/8FiD0clui@hp?(`J$oU."tsUc0C)aF*@r#s8R-fZP,)33JW@2$nL;!-7VA?%lV2\sK.\WgC/-K!o'"U+(AEO4\$u,_Mma/S]ZCXXp35^e?c1_>Yj)s\"T7>6YuuC%!J:hiJi9%B0-<$\YIVB\_(/[_=%Di,l6sA?Zg3mm2Ks9mW\'B/bIC:tW`,^[@Y*TVQE[0P@t+XE&<)iNNTY;OcQ:r((Yk%S3r[#!H>eC[9^e<Z<Kp9THnn$^j)OlXYJ"UUPM\@'iMXd!K2!3LV0[E`D;g3Fkcs$W;M;-^!]*Rp38d[5gLjm13aR:F@.$M$NN3)$c,%ZA^8nHZ)>AA8lqsfDK,^!SMMT(BL,UH1q/%33=+2`G.(VuN"5<pV^1$p`<,+,2HSF="19R<s)gJQ%\%]>KJ%a;g>`+u6j*,dglAEIM=`[AKcHRVB8kL,URW9D##cI>RMbP)%=]HOah0hf'Joq[XY*:Ljd:cKd06EJDl(U(q36eU&^uH.Rn=:AjW_>mgZpZ.008^CZZS\iKLUZ<q5<LJiib6Q<Bd5-rD5d=#g>Pb#@t$P0;4Q(h3#u#*MfF<tL?;2bUUO6ifZ1%Zd<YDP1Q0W#)1<mBQR+Wc,m[8S"Ljq/VIbmZ>:R4Cb;;E1ftA,Kmh^G_nr%/D>>mhmX?c`7;?B9%6ckjik!c9LEV/Pq$R%%4]a+(Zmf`O9"T)VWHZ-32N4SQSO6s]2W@gZ!Z"FX4FE4mtqj1Sq?Y7oN,HT;QhY)Gra8;LcmI%VO55;KLGrIb955t3;mOr3q]r6B$Nrc_a2\`;g-dOl*-[Qs/)GTnt^Xi)Uk`GCCnUVR8~>endstream
endobj
16 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 627
>>
stream
Gar>C?#SFN'Rf.GS<lhg;I/&I=!$UlDJ4i(.IB$-&q]%.W^'Z!nb[O$KTDEYZ$g->j5Y3A!2h0t52-,L.IJ8]KLGY@TpS/-6';`W%@B!#.Ld.R*lSaCNl$E`4kN%>AmuVc3,Y.j$H`\;>jNR^cWXaSH+XRk!mDR#SH<k!)eZ>E,A*aIUD;7t`1QXJIP0!*RH`B:c0cl3g*-;cSPUQM\Bn8$h;WVSoK(!QEp`1/;jISnR`]94#(NJV(lduRFe.-D?f@J=pnHP^rN>sJoH!0MS99CdUgc)[p*9RTf+8O-./0cWEIsc6f(j1aH%GW^\mCLVZ"$OM7J3>8S#*#>1lGr(8V=*1<oj-#).dZYXQaih-n$30F;0="r?H@n26q9gb_qB-VUDD!LmD[qRV.jX6d[.,PTNhK5g!ce[*8CV?-c`H))h7I`,9PSGH7dVO=+Kf[>N'>:.Dhh'Q4j/g"fNP=b*u5Fd6jXb$2'lP>dO"$5NQK.UEgK4r`c4>&3"[P1N)%@&!8akHS3GOu=`_D;i?gWnf?*QSsM;SuLL/c'%=)4<Mhfo:O,Hj>FY!aV<4bG<2A04A<b%kJPRYgQIY6?QFN:=6D)pAWa_Lr/Q1Z8[-<XgG&C5!^GA0~>endstream
endobj
17 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2004
>>
stream
Gb"/&=``=U&:W67f]jc+lG*B*n5(K,SMZbGof-ikAD0Kj%8FYS*F9n_3(bOt]aKpR.-G"0P.m/E8?ID\Qb2,r?pO7;g&FVRn8J_i:Dbbqdn=)##ah4!j6>e#LiAsk63?[Dl9u[`jI$D@X&/oG-[ruY"sFFH(^LZr,@MFC^i<]mYQ/DAQZkq.8Dqr>'L4u3rQtRX^IN[s;7.OppS6L?48V?S?:=_:3]+2A75.u_\sIH9Sfj9>5f,@5^!in*O5ZGr8i=_r/2MOJCR$G.bW$-kkkO"3e"ht($i!l<;-@&`WmLVs]nYcWUT9/'\0*mH_(P_DC4LS[\$B2eZ%G5uYL4U!YD;d4qK2o&o?Q!=L;`[sJFWE/dmkH?-hN'FI)nf_P4m;FZ4RTQV'>9JGhB,2KEXR.9B9\Z13,-t?m0"ab)%<I(J/Su*c'@e"#b`6%GX'Y,DP*]m?c<';5Y]XdB+-$IW.c"S)%lPhE2Z-8![6=V[`[b'>knb@%W)m7=&BPPjjcS%iF;PTaYej6n^0G[;N^,n)=-Z59g#8I)1YM&TW(#s'3SZ)K)Nn^8g1N\5ApZ$^D/56AgRqmeN.[N$:)jOh8*@Y_9g!,J7$IO$jPd)<bVa8;,K/gko@*qLf1ooHiI.9ep`E-5iLT1(32S*jX;i?j"]MC_o)>S-5Oo*t0NK0^O3^[pX<YbKqHg^'!3Fg?me3c1^[k[.#3D;59am778?F3-#Jm(^7XX5tbr-CE'-E@0LCF>UR^?XfA\X%F"N0>o+Ip_^#P%A>'"O"h<kB]kUN;`Yh`uR`G(>?E*V6!$%8Wq,2<RY?)(?[?jp42&%A9Xg2aC-:/Nl.,;TpX5*"?=r<45-Nthc#?.ej"=p+TY+)`,J;(Rn=.+e)04*M'9WW%1dZ91q@E:OOf(k5LXO4=1=,i73b/"kTc"q=,s(+S9&7ZrkLpR%%^:[W?id65Nf?Z,@4;)n/[n.TQ>1K7?LFQJQGlsIK-.&T$<Mt.+?j=Zq1/4su^oGW,A.@RQb"F-5<K*V&5#OBPq!R!bpTgkDc8*`m`!'VPR^BRk9'^f"B)?P2l6R]KQ+V.C*sN)fongV-oRH'#VO^A/Mp<X&c8ERtW<&NZlLaq4.Y\dkg$BRG\uuEGA?_Jm0C?\LNEKpoqcdFD?C"n;C86iW>&1M&W/G^iGfZ/)C&B=&erq*n5%t2<q$a1$i4j3'8-W:[q@1pf=A^?@ap-Y=3b0$e$_5-l;"qd`:a_r@0Y5g-N>ngOgp.&AgduQcR,C,p5f*c9XNMU'EB5/>JN3ZB.3l8/o$/Dk.i1Pa4Ml+k4h1sJf$d,lMGq2Uql/faZGXr)^$B8A5=AS2JSr:+N3_BD&P6+^)E')'rf/^#_Z][LXJ%p]]rS`_DVG_&]HO26s.%B\Sio1rIPa9=G'<NIkksuKfcMU!%CB=l6:*g^s"3<Zl-n,WDiao:`:\M`kqdrTQ^]cFVfKS4N0B6tlE(M,lca37\8NfSgNu/XO&@08[$c@YGRCP$8uH5TJaqWoHA1/iQHCBb/9_h"dq3UZ^do3Km]>[^^5&RVX%I(]*h(e@-*S<iMe]*7Z*EH]"Q#:3i`[WFqtKJ;B1.`^YnNV[#!U9)LX6->\_COf]u:RNWsQ_h\k'Wq)C2im,7HPeq8/HA%LkHHj85Tuj-r$YE2drbSF)<&>$&u0n1_"CU2;&#8XU[/o>p]7:m9sBHNN._JT:(_>[S8kSt8^_]dg5jKe2NV?`J@El=PW>5(6HH(bL/S\usO"+lfT>q'e414uAJe(d8?WkT9o<RCuj5J:9Y2HJ6=d]j)*=mFE/J^!LJ7SG+Bm3B'*IIXL_<r93M",h^e?"kq.Sq>g*]mC]8H3^\APBu!ukXLg@egFWMQIVZCL:hp[/fPq:Y-71d[iNf^EfGh>(kuD"k4OUq@"-U\>@dV^5&&l-)JAHd/%KW+b0]+6S'>,@D3CIN&I'O7fRjqek8^"XFfBjP"3nL':%j&NEQX=edJ3aD6a8HOXVfs!V"12sOJH~>endstream
endobj
18 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1601
>>
stream
Gb!#ZD/\/e&H8h>EUY?RE#Fh5hE5mG[^BeWTZnCa5Z&<u.l4G6]p5>@Udb:[Nfk^ADE>bS0[)FKlca_DGgDK_),P^eqE)t&I[S%21BrfUlk5[.!bSA"FQRso#krTe4<SN4h1u/i>l+SNjlG@q`/f/]+J]#E!s'TV,RgFZ3;NdZ$U?5PI,lt(JPUnp!"#(VL]2Ms0(CsBd"6MV+78p.o*]D]&b4K+@@GFG_/KT_0<.B^"ej-J4IB\6Ie!!R#s9lD<#%'E,)+W<X*A>dc?TE1-]a=h.]JA1^i/LAV+$fOg`4,r,Kq4^=<%$e$c4,#Cu_.f$CD2\']h[)%f/k*-+O/`Jb:DA[0?0t3n_=A8a'$&I+2__k-M`X$Gm"J\Y9O7[MEATj8oE40Y"LS<iC@tH'Au^.(@"uUFbtuTl+\6[L>/e.H)?%,KLi:":_u6/TjHPX^c;7>/2LbcsY<8[W>oUD\UKR#X_ocFZ+er7^ej+E6eDo;O9[^6)ac+FVkG%E8tljK@)k`m*Iqa'HankfDb_`qOb%!FQuo8mtR.GB.cS0%?uP3VVGQ0+q\JC#>Q][1odMoX.-EA#3$i1%!9-FWL96:^YRdf/J8FYiZUB@'#4R<=XU%;ML5c+k>J0jK'OUTeKV9STKZeGCRKfQ3P9f`$:RNUPCXR%:V8(ilPN3"Y0;1(<108n=&OG@W+(C9_Fg`W:`5hb=7bt[J=slAE&TuuMW'K)'A"tgNJ^e,9gMghGmnJ1MZZb(L7t&\*(^OlI%`'-KiOCFA7FIpR<U_6O-[eMeb&?-Q3f;:3_=6]>\&WT`o)S/UWHmlJNS]=l9YM^at/GS<r_OJq/!:B4=!e^M[f\4G(A#k'Y_EIf-3.ul9(ZoQ>2)7Z7OHMQBpCFO&_='i,0qC$O@4*AqD+^`U+@g5C`K<9iZ4Z45=Ju,L#m?'*Z1O?$oNU<pCl0<CKl99kMFa:/9\Q*1rd,r:0FH5^JQNS#L>.k(QLho&&EqI_ZuM#M0V;5/$i4X8*>>oqlYud,h1?GOY`D_m#\l>"BmMQGW>>Q4r$B")u+%^)M_eBQ6Bt!a8i\GcT=n8)t\=Img[AEm[WA3eGam+GB'D^u`4iTp8dF*9qr/Bk'll+/Hcf;Y)W6TFt(9\*Q?[N^pEUjE6!H5&<_NH`,`plB)?@Val)l?R;o+.oi4A&!T<(n@IShhZ#k?4LA#!95MbCO)i(LW0]_B]UmWr?YcY&YC5WrYI2^uN:sOUC\bl;:"p(\lF0(7"80ut3Tno(8,n>?QP5^2h>S7^Vm7)W,0/a<COngOn[Q!+TL?XO.Ob9FdgB$u8pi=F]"+[.5M")m"<QLm9'P)IS_[)V!NK.ca^#I@-$XSf]D;3MC;_W')UEnk-af8=-4aJ?Y#`H$Y7X-9IBUU:0r`m1<kEsBLXiWX[`CYj[^3*5pY9q,Q7"7_PIAC?]E*F.X3LA7<\)>rPh(PC]2#r;<@L/R9'n4=jAHd3?DC>tPgo6o6J%^9)U/fde6p3enk*UPYkUA()IY.]er>K6<+-<5l5p:REf$UA<Q"BO8m312bs4%ss4D\qODb&"R%)g#i75*`s(#Og&DMQ^'T??-6iWX=%*i4>!Sq2~>endstream
endobj
19 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1809
>>
stream
Gb"/%bAu>q']&@2DS52S)%-L2&-S7igH:qU*D_j<9!Zhg0Ek+V.?Z#$4)$&AhTIk>GX?&,DIRrD&d$?ApPHe<i\.A3LKf;,$^eV8^c3mjJI*N.E<:V@S1BA6aaN+KE><Tu%O?#:+3<&/!hLYmhuNpKIA2)i68>36TRc&_he,.6#0n;O%#?R9Y65EU4K7CCk?T8l8(3'7;?jVV2uHWH0-IQ/LG\<]"N)7]EN2BK"ej034]lN/o((>CA,%Q[Rj-X`BaeosDShBe-0IHS3M&D`MQ!ak"jbH$M:69V&pjnB$WoMBD%5t<(PE%1ho@r9"Y^b^,L'9"m\!qQFd-S/#$Cn5>[&o()mVY-`-&&?q=nRK*>a@'>TnfSXi!;"5B^hI_gOkN:kLX.RU(M!?h305fBrs9D@I\T@]Y7JQ3,hXGgKm2XPVD<BGWu\fKR=c_f&]X-1-4rB-hY5cQ^AqNb!4@F(=-Rk\QchY0,W_rNd;OrDWqOM/#XnO>]Q7=,ac,&D'pRWmW4!Gp07!oBjj)^9$f+_S=),S9QU#g>0Y2[5nDnWNnD4'WfN(PXN9Rh)9_HRq/TQA3dc<.,,@!^\s>9TU"*.*i=&jQNS[UF>hUE<FUm^Su"Mc$-si&5\3rJN1r8`\D:e'McC3qV\<B`]6/eqf1k/)`K!d%4P2<0P<fg2W'=Nam@M\hk6L0<.)d^8DG;8"TU[bp39RU(e1p/f&?:4/bKs=*n29ULBQ'rQ7+M0"H`kWhhOFN7c:W1%W3S\W.`?i?S8kWG8r93,Q+9Jnc-kCaB3=)23_@f%.[D<pOd&"%GDSI8i!Z(<knXbNFR\M^dF:K=V20okg4rEZPN\rtq-?M>@Wa]3QU`3IM7'$pZ+Vl)Vafk.jp3?B?K&+EaB;l>2l><Vg*/>2IMJVIUo6nj2"HW3*li['2&5_f1)-TW@?EoSZ)KhP0dPmn5G63Ak7In"F/E%[3IktX1@`#6gi@G>cKnN+dk,b4_=I)/":I_)i(MI*'q[1JgGsZVgV?t6c^:M<4Os-'K<5LL9n!C00&SdMf60>eK],JqBhas,G\Z`^JZYp10?`!YYte[eEMjkJmGcdN@X1L6:oQsPoP3Z]^n9a?haNl[^6)Wg6^gNd=PLoUhutb,93XPsO4P.A%C\+.V_4,k<A%d&XV5L.m@0;K"<V90lNs*3-,(L@)K,-0ik1.7U%'bMf%*=4)A!K7HY)fb%GG'TA-?'?6r`3?K_>s<elWG_K_n(`E1DUOY)(:^AhH!GXrTq(7CpZ01Zj5NPEJ0-G3'*m8Ct%`Q?O\U(EH'kbU6(gK9^S$qF4]!^p$9HWm66V3O?eRD)PS</+R,EG$UI00'B76h$RY@T8"H(jZ(IWC"N'39saR5PK\f7DE2-I[(4o:''II/nBolO.[XH7a\R?X,P@?%,P77;o[W8;aO?\;A$pgfl>#:i23lImREhO041T!hanlAq-@7KA;dfPg!c)-fFCJq7C$"dq=813;NC_I9V^-re>nYj>:#P]Fe&??jS#\Q^0ipHLoTk7dVG@C'$0PqQHThP'c9J!YO$V[eW4Y6ra@24<bK9`s*)fe_$`REJ#B5<_&c?9^D%2@Oa=qq.S[SaAXUXpQ]p2N6ln2'f84A:"9]Of.UkZ_lnAguY8@8)LPV6P;D5`"XJF\RI3eZdXCitIeSeO[+@IbK>$DY9-U_#d!4GRU'c^r.U&!ki<1VE7?<C+3_hca&=]I0U?N<@O[cbY>Nn4OHmk[TeCCpd;,N=[.GRKaK7AFsIUMOaOm7Mgt4Gn1r1pW;2BnT$k#\!5!Z.IS^I^"EqAInc#i5l~>endstream
endobj
20 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 2065
>>
stream
Gb".=gJZcqoNbgDL51MZj(3GBP#c\%*Lc+W_?9omD.]0W..Ui#F[M<dS@8ISZaq)l<&,-]-.j@)B[?Xg$Nl8.<rVb:J7$@$pQ>R;0YRZ:Lk,"-]GMSH#2qIaKE];Ye9!\^f@3$H9oTu@S(K?;)+a$(km.i`&Lgul*:Y#(3KP]"HtG&TN_LNCJ4O;*d_:KT&t<rQ/VmummTo`o5,lMBM-Wc0@THh]L+?B.<(kaUKJL`U?l[#N]ArKMGb%N.mU%83#eq1DnuiD1i7rm9_pLcA#C3!in=*U[0M*K((JeIe%HD)i=-()RJoFdNX7VHP%a5'g,j?TG>1+eT1l+<!HaMY)>oR62YD%+t,c4-RoZ)SR2bZ&g1kdMtL^\0`lF)D##'U?L6M=eleuJp^Nm7G4X'+I%/IZQbA5&2%5q]<]/SJjfXBO'[ciAG-Ydk8RL2U5hP.$$WbJcDR-Uih%XcPD\X%#m+hmbGa\C."lrHg2?r0.4u'3U)]cY\UR/f?U!+^J#MZ?2T&q1Z&qb]g6RqK>Hf-2]rT_26-9N856FB8)\(;h`(jjTPcA'Io"p-GlI<Y[,t;K[)ua^8oYAp<noBoVGRaC>!dN2%sBp.t#oh&iRSuXFcL1<!+qb]iIF$"s9^%iK]jo+WRE&p(eD^6;ifqJ_H9;*!CE\&D=/TN>?-sP">1tA/J@DL8p(Rgb:U.'F9-7@HL*`JnU'02(]hRZBL;l;Ud+cSDESdi%i,#X\79bQ:kK.\D:K!kL2DM@%nEh?H,BHD#a07k5W9@QGA_pPlLbi[N4?]gCepAgY-mIfgI=L%]^DR'!r5;q"CD(Ig_qj1"u0OE\%KSfVVSI!Ip\p0_uGXNmG^a<>=]shHlTE??har`P+nmh=K'&02oFP1B.\60%RM'L:1e/a5jlW%PW2<43s,#^m6hr02o7h?CHV"mhJ,$mp5*J]JE*AUDn=$D+IF[a^7+gF-S>$p0m+pYCJ:BFc6>1T2b$ndk&##@DC<o'0Bnja`V1TPhWt>r:=H\><bsbreBa.Tes#Jg'7#jRae8KU.^SS,/N]Xb&2>phM4%&a6t-K"dJql/$%dDL&,'(Y_L(K>jSI5`bqBF8QMKV._^[n2G!EsLpnk(.4B/baPg02'1DN?Car(WMYCecC;hA'V[:)OGY3]H=@eWX0"&G)$!SD_gi#;#]EiNmDb,CrR^J[:pGW:.?T1[5Y/&f.]tp&VMDi.ZA&PHa212g^>VO4O(np.TMkR%0Ark0_;jA[j:M0:PHoK;N\nOCX^*N5:AY?=[amtG^8T=M!N6q*@<&5mO?BDHEPSM=K6\R%'C72u#\mUV6:^#6,aj^E=@aEetSfP")l$50_]h_spe^;9@;O,5Bo4GrDC0C9(eEV9A9!&3_0=*2)ZTC1\WfS[=VbkG"E=,E[TE&eX?;TRcI.$k]a*r_WQ@m!Ug@6b\S;u3r[!SQU2NQ#j=kU]4^/7U'hUJtUdWgZWJ!I`$\k*)@p:O;p-5Gu(ID9J!0q!aM($^5i6,(_P@\Yed4km0!#`O5m)<>&%E02F!/;\>7=-.%9Vbfh"EsWLT)\<,7KnsBbDJ(rgU-0U"m8L/"@bhs^l@VH'p<L(^_D3f5fsSSHYep_1MR?%cd,f&N]GE#tF'V"*J6BNG@p2>N<ig!5Q4&>P3aZQd+/`P1k<V5tXhQqm\drcADFnGfE.3'(iT%briQ&D\5/8.jjt_bCA4VRrrD[2;_G\klFqB#1]ipBE,hR)(MQCN7eiNL?6+@%`fatJ.iN6;#AMPpsLM5]/=$OMP8HXZVA!4i_Rbr!lLrHeZE&4^$ENW.#9@!u)#hcMd;,<7u+AHf.^'+I.q6NecXD6d$[G8DcdK4B/]th'4*FMiVl5oX]_1m-#?XU:Wc0FM*=93FFDISYE>+J^Z7d+EO[tIQeZECI(CW548@0l&fYTRL=*C\Ur<eG4B@hf0rZl"X1'ErIVP=4LD-O$Hp5sAgc\&\:]47g:^*gGX6a`o)Hdt:8B1SDqB)uRChdO?J$Ys*.t$FBZFKZ&`OVlpM<_/3D,U+SJ!@gH_:>9YjTI2lb^$M`HXj'RG%Y\!~>endstream
endobj
21 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 721
>>
stream
GarnSgMWN8'R\fAR$U@5.-oMn%.pAgeYJ$?V+jT]WQGU"P?h@0"`G%Hl['',-&i8Qk$ZLhShg_:D`*JV"h[KIEiu&t,S*^TBI$Y=94kqO_D>Ms!sh,SIdBA=^:V`DB/K`89^K_*OUOYmC?S3F.#K6oN!hufU*Dc?bnNQZA/K$:8-L=qYOK7[T#\Us+ttAC(I./BS@hsk8KGto+"$"gC-'k1e^m-f+:O?2QsY?S]u3@GUcr!?9o3esUMOL]kBS4G0,LO7?hBY)]Ve7LS$o'F*[:E:jN.:8O'd6;Gd.`cnm>6)jb'J.c+A'jrAe.!Fc(.!l\9h9-.YIf7"NkC%phH0-\fI7pEd'LR71-lXg<s`'m++YT-[)^fTO$o`P2Adfp)TD?"&s"D(D5_UN\!'m+D+s@k:F;`5o-&"-JTmj!int3GfU'3U"\k_Y5;)ZNI6!=(`,MN$CNiLcpbdbk%4++W5=IZ-g)>@LRi.C`E77,ZYD3osT/%po:^u3dA3e3XK0(DQ+f;-g.!ZjTtjXEm-b6C"g8>/Tj'rW5rGU0)?s\ILRlO.eLe:4Uc@jhT;6om"'-5XIh$bS#3N*>3,=2d$%bEDa;XW949iP4mqGYkOAqf]W](34@\(&Rs+GS;Ti;.Z(WJoa2liFD5p=Ak$59OXJtg.(3BtbEVeY\3Y>AP]ZJ_WeL@[-r$]X?<V_Q28XX8`#Os"8/uU_HNS.Z7Gm[^~>endstream
endobj
xref
0 22
0000000000 65535 f
0000000061 00000 n
0000000112 00000 n
0000000219 00000 n
0000000331 00000 n
0000000436 00000 n
0000000641 00000 n
0000000846 00000 n
0000001051 00000 n
0000001256 00000 n
0000001461 00000 n
0000001667 00000 n
0000001873 00000 n
0000001943 00000 n
0000002239 00000 n
0000002337 00000 n
0000004736 00000 n
0000005454 00000 n
0000007550 00000 n
0000009243 00000 n
0000011144 00000 n
0000013301 00000 n
trailer
<<
/ID
[<2116caca3db23abc7cc418256d9c507e><2116caca3db23abc7cc418256d9c507e>]
% ReportLab generated PDF document -- digest (opensource)
/Info 13 0 R
/Root 12 0 R
/Size 22
>>
startxref
14113
%%EOF

View File

@@ -1,474 +0,0 @@
# Les 13 — 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 14 — 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 13 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 13)** | 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

View File

@@ -1,315 +0,0 @@
%PDF-1.4
%<25><><EFBFBD><EFBFBD> ReportLab Generated PDF document (opensource)
1 0 obj
<<
/F1 2 0 R /F2 3 0 R /F3 6 0 R /F4 15 0 R
>>
endobj
2 0 obj
<<
/BaseFont /Helvetica /Encoding /WinAnsiEncoding /Name /F1 /Subtype /Type1 /Type /Font
>>
endobj
3 0 obj
<<
/BaseFont /Helvetica-Bold /Encoding /WinAnsiEncoding /Name /F2 /Subtype /Type1 /Type /Font
>>
endobj
4 0 obj
<<
/Contents 22 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
5 0 obj
<<
/Contents 23 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
6 0 obj
<<
/BaseFont /Courier /Encoding /WinAnsiEncoding /Name /F3 /Subtype /Type1 /Type /Font
>>
endobj
7 0 obj
<<
/Contents 24 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
8 0 obj
<<
/Contents 25 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
9 0 obj
<<
/Contents 26 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
10 0 obj
<<
/Contents 27 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
11 0 obj
<<
/Contents 28 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
12 0 obj
<<
/Contents 29 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
13 0 obj
<<
/Contents 30 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
14 0 obj
<<
/Contents 31 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
15 0 obj
<<
/BaseFont /Helvetica-Oblique /Encoding /WinAnsiEncoding /Name /F4 /Subtype /Type1 /Type /Font
>>
endobj
16 0 obj
<<
/Contents 32 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
17 0 obj
<<
/Contents 33 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
18 0 obj
<<
/Contents 34 0 R /MediaBox [ 0 0 595.2756 841.8898 ] /Parent 21 0 R /Resources <<
/Font 1 0 R /ProcSet [ /PDF /Text /ImageB /ImageC /ImageI ]
>> /Rotate 0 /Trans <<
>>
/Type /Page
>>
endobj
19 0 obj
<<
/PageMode /UseNone /Pages 21 0 R /Type /Catalog
>>
endobj
20 0 obj
<<
/Author (NOVI Hogeschool Utrecht) /CreationDate (D:20260521071303+00'00') /Creator (\(unspecified\)) /Keywords () /ModDate (D:20260521071303+00'00') /Producer (ReportLab PDF Library - \(opensource\))
/Subject (\(unspecified\)) /Title (Les 13 Lesstof) /Trapped /False
>>
endobj
21 0 obj
<<
/Count 13 /Kids [ 4 0 R 5 0 R 7 0 R 8 0 R 9 0 R 10 0 R 11 0 R 12 0 R 13 0 R 14 0 R
16 0 R 17 0 R 18 0 R ] /Type /Pages
>>
endobj
22 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1870
>>
stream
Gatm;?$Dbt&:N_CbY-8RgR(S7K]lY:\N-/8VQI=UaU8.&AcmhjG..;6G9ekM4!E,>36/I\FD%agk)N,di/=&u-j+]MLK>QG:^6Lp:tKl!IKf3'edor9SGB;m[NgSA0X(XLs$=3&Eq$T`!u=]E`4(HhJh*cO6m.$=52W8%PS+`b/./i3M(`,<"nfC`#Q#O21PVc4gZ(EKhr"8X[!RAK>OTq`jr_EY"`6hRW/bX,lM2LF1H>:'e$i!F9eBokXcuH:YI]tGWeMJ4I(6\V>uXg%eM;l'E[+L_A2:?\_u+G?^X0Cnpo_eV&)H!Z^QK=.#OnLgeY7W.%[Ta.!Zg3H`\ZEPERm1n*GYJ&k?oi1&)?0S0=+p@CJL9_MM&]VK^1"<e*"SVrKor_FTY5VRs6")Hi#g@4$F+H6_cJO<cuEnRE-:/:9E)L2a_mJdMn)qJo7CZhQCc]oIT-t[B_L12ALg(aIQb>3$r>BT#U$)1C#'D?OXJ$h^3r"U?BgRG#`1VS'g$'6-$[.+[$b.NW!e`)U,NN8d4U=\Mj%mJ@2[71&Yi:iRJ1?_0*"j@^3@F"rW@7:47mXi1l3`pq\Y:Rt=aOqH(<)D\RdAKlBLY"$9migIKWb15Dp259S'6in%//!]!nGNAXfq[_d0hEhtO]X'p_.ma<d?+fV%a)mngEftjs30'EGRA,V!$(,:lZ-\VI3_hE5:\(shf]*2n,s18Vjl))Se@r=Sk@%McSmp>HB4nL5>m*_de'q=W#5:oaEjp3$G;'\=?WD):s?Tq1qWeEScO%AdH'h964jnCU5Sn9\lS0!:""l,V.Y[3[#pfSp6mMo](WEEIQ)k#SG+/-WoE@E6XNCoP1MPl;*26fZqkQ#C5F0=Qp2G@&nLG:R)EBtd(O!UTB6C9+RG_h=5OSNqe&RL(5DIop@]/hYAH-^T11`gfs-;^9-Pbqf<(SL/I_op>lO#VE'FBh'8/e8j,`te_`+-nPGT,li#le/DYRfq7"D(8EOUDfj`.J+'A2b!(3]PED%NH*8V]1nDJ]H8>45G6YFdc"$3/[pM/HKWk0*^mMFJ#4g!r_C%Q_uN81IrGR*f/2s7p`%$V^;#3RHA97#i+*'<&-U;hH\dI5bmp1h:tl6>ON1%UReA[CgbB[[Rn>d,6OD9Le>/'q<R^4S^Q.R6P%C_*.W^eFg8s&qku&4;IeE]1h,*!!q-G=l;C<*],=X.5</sS2h`"um,t$L:iD/MpNrl*RkKC;D^,.FK<>H&#5jCDD_?ZrVj$AE""\X,FXqO4Nc*tK[>$&_\.a6p(XHTJ;=F3Tr&m59B@>e9anlU6A[@Y%NBj&)Klb's5c4Qr%<jaoWA$ab;59R(C-0''&ed.E*5o6@K_GYhO'QiSH=W>XYPn<u>_^pWe;mA`tL8m6R4I:Q_4e-_@?8K8!M@_L7/.@6R<OIOuc;L"pW*YfO6?p^G*f##hO3cfD=?YA\)Wj'&!?Q),(F^[T"pTWO*>Q5JP[,9CaGZu\LMD6!1D-Vs_XJS>ct"*29kB^9,!7&4\6%eJ,$"@Xq<7tAi&;a-,!7>?%ccAsd9g`P8^BJ(2F"<GUR@JE9!.,D"hE)1]T3P=:9Hi]&`NYVT+CZ,C-V!Y9A6"M"&lta2nk&)-?W[TO'4k.r$6+2'K5kK2Xb1o@V5&c-jR#Y_^$6Ijdq-i1PO?0e03[LNOO"ab'LLsH)AJhX`$1P[oiX'Y2_6glm)_i.0S=D;>[r"_IHil8[s"D\<,a&2:R'g.P:%aQ>k;j_IA2TC".`1@!ADK*&OH'18b]lIfc#_[:6KbNY":3@#,--guF2rN&Kc0AuAL`?imYZL#T[CnQNtIVDb`mf=r&dUTljSq]umm*Co<&-JLo@f%c>%q@@e`3r&~>endstream
endobj
23 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1565
>>
stream
Gb!;cfkq`f'RcUW\-NXF0l2U*m!Ws@"uYDK8Y[UcI\C@%M:$!++mGj!qHe1XNNFK7$3RPD%7u\;G<+iL@R0J&r+E8=pgjdc?SPq.VK6,WM-tW5^J0>V!T=>\UDQOhd4j`Ko$I>d?3E.(#t]0/1WX6%Z8@.]@jK7g$okC_;jN$$0L#j\O>NYF('2NZS7KaeqN)tmj44g!P9bXj^9]J6k2dlQX@"eu2hKusei>U\L(U+1:1KZo@IiSgmk<YoAg'mRCmk_1Z!%o9$eLoHE.3(UDN$>OQ>e!cA^tVJZF2Doq@TSPDQaY$'QFb\Gn9GKU]D545)a@@/aOF]:pg9e_Kh%om=>o,)4K!4KHX&E(InQKKG^(-N05T?FhhM82M\Oei$k?Vb!ol'lXiS=V$S6)`;Q2M(rier1<(<`0U1V14GEkuAF5o'J:=.V"8d6V0`75R^SCrC*Hik5]!\9Lc<t1lG]70RGnDS0cRJU';ucERj,PL*6F)UmUQdK*GfFt6$us09c@!de-ng7]@=J+oi?o[2-[Z`f0nSN.gB[KqKEYpPar84X+e?V0)#0P-X/Ee3i;$%jHbbMA,uFl#Kq_HaV]&gJL+#c"WDTUuK;s_pf,7489X`aoMJX_5h4gU0UH5/%8BeR,fIh+p5,o))S$ZNh*E%HL(r)s(K_CUU3t#0-LD-V=kHZ^aHm(Mp1Pugj3EChRT.T5SEM20KhSqA<['bG\Yk<9]\b17,6cg70*k>,ib>G;/5BYXf^;fue1'_c#G;-RhF&a`k,[=-j0B(a+CFG[h>efbGF[ebN7/+\Y0$dbf-I9sX!=T8+iTf\="*TO^\1nUI(*`pKBE;,^Cp4GE.%9h+;J(at1Fkha4^a<1cJ.ct;bE2Y.ksgTkP+r^:5.MW7tHN:ETp1>4#ORq(^!bkZn.KOBlY66@G@faXFK(]FGJpsb!u8XFYY`Y]-TrSjK:F*^Fc>WCN@\JoUq=KWa^^:C"V`jGkf#B?tKX5FGXoHJseH_:94Z*PfNi7`<Pal!2YA'VF#$*6PPPgarIbu,u(!a"T;Za(kS`^oDTlf-p,mUQ,k&KLps,P+]q>El_]m]X@.K\pEt(]qi[$*h=%kfWNB>s.$YT5T_GSR`V`IK,:iZaqMq=i,bO+pf"Ph)LSR-Y-h!nObE/`3NB-M&(Oe%t>6F**qSBHj(6H_NgBacc@N=e]XMqkV$W5l.iH2?@;'32GUTFpS65E:4F]1l29*.7C#R_e<5V7XY\t6mJ#<;,,R,Ifm_.I_?qcNVEX#Vf6<djX`;cMQ^3C,PA"hB^NIA9i/HoWVSR/4S'dPQ7sAQ<mtnQ1K%.ou5F4GN,>dI@%5CRdMX^A>OW;.+lH,D@b9UW_/Ie35g3KmB2Z!n,''`a?N218fXjfBVB91_KqJFemC^!9_S0N@UaDio%L'OS,MJC*=9nBe5"t[ZMKjl5oCINW-1YRQ$M1CT7(*^VMT%`VW]oZ/Q6o]NWP_rl"Ds\jc'hnjX^`e.6NX]Y!]=#-/J:@=KDW9&L67q7g<;Z'+epX/9/H#@'ugpX[f'DXhIY2GJ&LJV+5[rr@,3_[Z~>endstream
endobj
24 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1637
>>
stream
GauHK997gc&AII3bU%cuKc*'C.Ye4B*b^(@RNQKgnO4sJD'cLK9!jfSf6=.uV;8P'RiZ#X(.iM[H17:HaaiB;FoU,)i3sF`H-LkZ=Bu#m7)J4%?EKs:,JmrnV#hhL'aY,.#lfHf'31]7&goeM@&PDA99=l?@hJk`jg8V<)@P/X,6gQV<#Z.^0GU)PNk?JhkGVPs(!"F5q8#8TbtCSi=GLM^56SkACiCks$:uXE>/=qYMmn1bHiq.8)r"uZ\h;qpf@Xp*@*Q6:kAQ[4gU:ddc/GK^g9TT(Cum9Wobgek]ri6ePaj-YmjA%1D1N#/8*fI+*qimN.B4*#%-KdnF9IKs@&TrW#XL>N`Ou;5"9ujO)9Wk?&QbcHL9WMnRSYKYClFilQ6]/EHp4:E3E['9Ye^`X*\%RE3%n,?YQa@U"9ZYp$k432A]4'iRpOMC7"^dVU6?<<G(Pj/E,]=LGnt7lN`5r30E%*dSi[s<@q#Xgba+2b_@>N,(ubmt9XeP[[P!7+@D;]1iPusf6MOi6cNogZDf=RK[#f0e7a&*?pJ=hN>F-,e6YqPl)VEs]&Gk9YP%1H'N\5*]T=*OW`B2g>iG=^h_EPZ,l(fr.BG]6[H&SI.R&],u1U(@1ju223O1Rqth5>N8.jJc,(S$4H/Vj^u<6n(Y3%'X$(diUh.9Qlj`Y'J\&hnnn,Z?WN=:dq5C?g'7Bk(bYklaI'?(QOXnpsQE%e>fB;tD:7"=2eQcr9R08AZ67;H$"dXLa[$HrrO]#B-6+Xtc\>,#E&aH3lj;i.F*2)V?*g]\$?=@C\[a[8<]6".,@(g^f0k'hT[PHT4d5#c*a_>1ZL:&C*FH:lI])`pa#&qkWNcE;#c*elDPER3,T2$6A<7'^j:Hd1K2hi[nV\^afqO7=(AuA[u-Sl\fBsEg&hMG2A9-'o@W&".UM'lC6$.c?:n/JK8\;lbu&)&D`YV_bHgU5(?^BglEuhd=Us%=q^U4gJ)U"A(sB8H6'0.]MGT42YWtejLX77##n%L`R\X.]$PlZg1m&+<F0&k5<tIlfqZIghA4ZCCA07:K@eT>(\U\u;1aR\nAEc]/)IZ$^>^.@X@F4VoT?<%f;#Q]95I>VC%lpIZ(R#1e&a"V&7IEjm.P_IIX]cF2:ME'O]Y3i_egpf\dR0.9X(kmi<^cP#G`>E(KJk#4#N4#MuLZ!X)hesDAosg[W,SVq)Ifc1j(AiO]EV[c5LF_R%G1S;:c=JA)oQglS?M&3?=0]iq&9TSa?>EIAkqjG.li+7\\a_S^X4oqP`]b?!mF!k<G?bQlEK;:;1+PCfB*%4VN[bY3^2$3o&JjqqV4.hZ&)g.r4_ldf$nZYr%to\2uafYt<E(K0sDn?O<ooAqrmG]#"N?k*QXHbtl^FrRq$6jfH0Y]!?QG#FAb^(2k+ZK<-WtraLIW4$7&lr?6e[M&j6R.,d8Z>E4N;Y(CKT8F;*t0>S$XS3Rd\d'9.8/3_2W&Y!kVTcPZ#LVhYbR*bl^cHZj,nEDZ,d3DlWGC75:>b\cl$/W<'(M^IjdE[?k\g;OrMq3B\\@BQF'EOT8`;nIDi9]^^mhs_+m[\n3nht(Wb0JXZV\n>UCl>UOCi,<DS&SJ\Q;&V81l!RZRXX_6Um@Mo#t4dp$'TE`~>endstream
endobj
25 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1844
>>
stream
Gau`S968iG&AIa;m.eq)1,]Pk+94IOKA.Xb[7Qk+8p\m=VBC^%7EQleVoe#7G6'm=b@]2eUlm9k+RncF^GQ+NDs"do%DXVQ3.9)&EL8>/#pNdN&t[@\/!"YkPmDp8Ree04')M;*`QH@HO@4gcA>#nG=pYb/&ZnD0*7$(Q'GLb;O$*\88ZK,tU0/VH]Q;pWP&h<h80DJgqg>02[d&Q72VOjno`r<P#A]:`:jkVX$WI,4%8?croDKA0j==0KlT@u%>ud8:?,<%P"m=?uo<M'4at2JM\Me09h3b9i"LAUmHYd7&n:fdG5kXj@?^_Up&,9((q6Ga'*NbJU!\qK9K0F.',]=As<"n/e$=iV!_`](^UO=;>KT4"S;I=T_WQ,aI<9V.:f?Gp6R$)j,'"!!pY?SmPVAZ^:h,9(+_>lW5T@J9L"knYI'l:'PO5X$g5GT;2mA.ZAmBoLTVGjc\gmKn$4SI?tb:3=k6\"sfk)N)@8T^.;K9.f[\7XM$L:Fr-ZjW9dL^RY!RrA<:n>4#5$,;C:1P1=J[sRm+bd3X]^-,h30#:>Q79$F^^r!mpK]o_CMC>V?1R9"YaY#TVL-kFVa^M>U?2[[%+t?]EMA8g^)<AD`_(n'D_St!]iS"uWK"6bM:`-Nl&&r@KU71T)UeB@-$;`SnD._p/70!?bh4t0PJ2%pZ?6jN$$RJQCZ92pQG:mq,p[*.MHfZSbcea9iMng\aPg#BpUWd'<^NE\hEkmDsGjY9c-^:WX:UcX96;r68?g8Po,ZISt>g_r1f^%9/-;L5=P1/A.-S"cUaVp[Z_fVA/%D!:-l>LK-.'3`8Bb*[[=gf`#DZfBLD9hn,oTof@_M?M"/3QrG+UM",<gqSb5bGWP9fCb=W8J'M`UF]o]KO9I0\]t+>ujZmp^51ghnW*R*'H=;-T_?r^d3diIo!lC%cMKC(kn'gjbAJ6?rqm(Znu:s`HbKb80=[bMKiEni6RLQRsicJP:n/T"aWtNg-#G*HstGZ36J]WMiuQ>[Q5#rdI@JHp:F-md9r2gUE_]'nsb2'X-]rh<n!82-DIRcUTb.gZD+I92FcdG](X-K?9##r7#_u'63^p9?(8=C3WJC^/e&)M6)SUcDuX"1ltdO=q8B3&7jC9mO6P=erp0L08<gBrr'4Z=I5&f!,XQ1hpS5AoD0VhF.(L(7JmYCjL,Bn65S5N;2.gb]3O]_mklNqXnmPZk.G?g!k1E.?7nmeeKZeae;jt5=Mm8A#J4O&3Dfgg('BO;<+*Iq1`al!Dc67IJ8=HaEPM.[c_FBQk!+2Y%Yf)YL2%X?L9qE/ZOU*k[pHdPm-u3XPN`$!4/+etG`b8*)o[#MlWtG3q):,/-JF-WLS%Q4bR.Q]kU"Ff@PXD_*4`9"cb>F4Bj)99NK<H4gn"7qodBpo8,X%POV=%=r-`)@P>h$m,$eTJ_5K*5uOpma"ZS>$GYtZt9Dj,*&QEG$NYG5p=M5k)ti+@rR:@&7*YA-U3oZ,YBb5+m1?^TiYfHhUmUSp.gq8-P5-O--[4rpC?gico?kHCd27D>gA0KR2#W(=@^]t2pcSLGZOaq&U:^[fK8P-&Otk.;fqJH)!E'ebd$@JA9i<3q!u>]\`+li2L-\K<MLDUMm'YYEB_43G=KDm]+aoq,7_Kptuf5.E5=mIK;d,tF"GUppAR\dU=#>Qo.-?&#fLCF-UW1(9UGSHdo-SM%ODDS#18BFdRZ=[./Bggb,qLhA'h6trt'7fM*$jF^tbh'VrP\7(n6#A!aL*,H(g>QaLWW_VDFc^Olu0F&\cFGHTGAiKm-T^Pd.aN]F<G!uTeiTK[,Y=r(CFBp/19t<]lkam4P2]d5D!'+(NJH~>endstream
endobj
26 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1640
>>
stream
Gb"/&gQ(#H&:N/3$j$QOJqV=:8?+osdV[D8#e!In2F-Rlcna3R<u4Ai2f6=D6X;*cf?'lqd_<XX/;YR3%QS0^L(#JZ=neO(!8f?dlo>0f@<i/Q&dc`_n8&uUN<iTjb8?$h<$WGS,5_]2D,`j'Le;p/^edaaQ66V]`ZtaJNmjG2W#],,MAQo,W&>9E@BhTQep'IpkP/^^@\o6Wis1%Qdn>K8fHJN=LLAbe%?h-t65%FM*Q,15`PqSd]S%*Oehu]pWcImFX^/MR@E#d3V_=Z)D47P?h$1T9f.O9#Ke(o5Yl"ThO8Q=rpel,F,J@Ub=0;QN!"Ve.?1tKefPOYmLhA]NED8qG`<W-+)Bo=7(lJnWGU.6:N0$X]-mB^rr\V"D&_D\6BkD5;%)!op>4Hl24SD@Ad"Wpl0]o&q;hFt2(]q-RI@ic4Ce0]HGb.D6n$;1)%6ua4-&q-,,0,U,EYodn0q3G,m@m?SrnE5-m((_C-XK6c+HK[H(a[rk/q>+J0X9k/->.<AXC(+Z`u2VVn.WZWj*O5!"Ef'Es&3.hT;5JtakFrrGDSR&)FX(<%Z[ULiE/%BBr:?3$k!d?rk(:VmE9+L\6(o\j]L/"Zf4f3[:O,pBNi%*]CLd0BkC]8g@XrH'ISe6*0.^+4*dP$&Df'.&P9*O0*lE?,]Si(E,#`7Ta6o_cWeWG-]LH^i#Tm`KeNouCIAmco'rEjlU:T%(SWgbCSG5GoiPOLAn"7X(;l5jkB)#JIKG[-*V>Tm&s@4R<B,;ATLnM-ls0?$*5sD)(b@X."+ME*GL2On3Y`s*rYQsMgX,t\]lDCsBD/\kHd?p+9mt*cf%/D[o*2@'Q=nsCb;RVRG%F%f'JJhsDrL"l\fBst]*/lf1T0r_<4g++A9PGfMk+^NkhkP]cW?#?cRs%m<Y%EHKLFL4A\P,<F/\j^/GC'0m='0SWc%:]#gK?aE"N0CYD):*(B1Hp[0>_c=M)8Sq8Rte#%W_f\ui"`LC$B,KgdZO_KAl/n6f0TfXVdS\bZYPBTlZ!p5`Wl9=W,!'cqaUZ=OZp\:9r]B?dKWg-<p+XP(//C,+Fc<T:*AAGZs\A3s`W^M*POB`(8K]N'IUm+DR'=cp,\;>O7R)A0jX2S-YH=<9eEf=Ad[2f0__Zb8#MEE%JsV"t7hOQ-M1]s7&Co,?Aej*2G/3XZSlK>Nnhnlnj38knMoT&Nj7j9).IX$7#d*\#&qN48&uU&V#2cP1Qljq3j*I91<(blq+6BHG>"l[H`?4R=##a'`HP">BU,T6:T#7Ohq0&%ds#cUm#?46Hb4P"pb-@s96T)bbA/&KKY=jgGPhIr:mYP7`?Vl9o=U/(SgFmUD1\EeT6V\%#_Z9QaM$0H5,$/Qr[9Vg7]YC$X@L%?"j&Nj*uBK>q6/mD7AC#$-YhC>)u_O%1'>g8o8DTfr(/IJl>;&90h]fVcDuc`:NGPt6%j?.\eR]1n)sgrrVArdPSZRV8R-3O^3oF'Ej8eK.u%Ti5>1>.cMBMr7@(Khc#tHIdR^<AGmnBQM(kIQP5c*<unpDS^T_$>u3'O(%sZ^YuW[F;]B+R!1ERecYE$Wo1*NH/IrH&K&"!IFJ*GrF,W71&l](@DVor$psmQ<&igq(?!Aqo6s22h:t"VoFG?\m6:~>endstream
endobj
27 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 944
>>
stream
GasIe?#S1G'Re<2\CskCMJ##256fk!`[3$,^fZ*lMGTYND4jHmibkEgs.,X?-7tn!WFG&5pY=eMO3N47_l@pF!5&Ztk_]E$&5I`^+s=OLbL$.N&MCU,!>&U:hf'^X07T'Z]bCN>Uu@&ZVM]%55\NWo2B"R;.E*"^(++eX"/7&@=ocr$OBVXImh<f'Q51a-deoZnnC&[H6Y+(EC"@b:_\K9+</^#L6<Tr_#SZF8IJ;tEhIKPh7'eG6_ag'JB]`^D\UsG=kN>8G(XA'"[=,:NCm!B"oca5CiF,l)"()=/!i3;bIq&jo52]8ICf80b+&1d;"EFV-(,]A"8_t!fKMN#slmVB0UWc7CX;u*89>N_iR.(+9"qj][<K#\Qk+9r,9^G)_7jOd[Wm-?+YDKnM`f)5m4KkF/]qc>i]'O;Dc7,q5[1(ap6?OAkNtI)6?%dB/[&XpeZm.BIO-XRFhsI;17c)@iS5hb\!KO=10U79q7cTVj^6ql//qGHC!eV30[`-?4b8i^]S]_cb=ZVRKS>?;nd3Ekd_$VN7QGC<b5'ZUI41)4i<:)5FGH;-2mRbsJUF[u:3%S(F$$(9tpNLeF8$`6mIPaOkfDu\]#Q)></6DW*iaNOqf,B8<8;U5^[Z;-S`$i/fl@(ab>'KcmZ?C')a4p0tFMAV-9jTP=7I%eI'B%hP'X/MLiT(]Ep<[9+:dlm<4b0pM5AXL1;'D(WNgGkKp@]WVJ1qjs"ppA`YtS!%RKq-s=W&I-!nBCX/p!e5=B@OF"$5cZJF5$uI.Z4J03Fm^7rpdO5+ZH&=eAp-bgZ7Q:*H:f.&D8EU`i'<bq"b'T#H7)ANdK6aW_kU=1U^lm:0_#Rk.d6:[t="#4AYQqaC7MlXj'qk6JUt38Pb=/`*;][CGuoZfs2l.3!Ys/Q6k!%"!HRK5JAOh#k>3.s89>EpJ;N#P)c+NW~>endstream
endobj
28 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1615
>>
stream
Gau`SD0+/q&H8h>Z/Z?g@%>Erl.uPs%cq7oLES7ZHti@p8/GK!QAhW80HL.>=)TJ>A>1dBJF1;+lLqdEgO5YT0qe;RmJhD=nB2/6Shjao<p.7$&[6QfcW7m&J8iK8O?c-"_h\DO_WD$`3ljT:U*:X50N8^\"UN#%0p)KsITeN:V1\Ad!<]&2eOBNn9IcgOndgT:*2aQ?.MX!#p[OXu]SNLM3Xi@l+;g,HI8V+D'SJ6@-*4Ooo]l)9$i%,3].2YO70\7mV2I8iS8k^jnVcGJ<L$Um9Ale]FBPm']"G`bX1r(cYF00X0*EX8_qW4]kpg'ai)`npk8ipXG+Ib2oE==f>gi^EnNKcD%1,D%YnkqDGS&lOQr#3)?lO&V\t@iF"^D^5KpgT'ksl&s[,P#B*6,pJ\)@j0(i,eW.Dg(e"HOS/DN#*60Z4gdn2h!phIY_Al@Gn+,nB,U7>;UBaPPUJ1!)Nf2F6(rc+>M)an)BRg?)DRo^+@^)5Dj(`^=-i)]EGEP0$:$#j[fPn9]bj>5X3GYOS*B?kN,"*r`<a4'T9T[G/NgD.9Qj",MlJ0!Y@uY(Wg`]=HKh>5?`_fEg;G^Z4jfS[%91m5Wl(H1\WCSAu2q/:2<q&!3RshO^=Q#+'aWmIZL)ett<Uh].$RaD4=1kB<==7poOH/mCZ%A>tm!`P/cO*l-WM9M[Q8)S70h,AZnAXX][$]nEO`8XO3af.QugU$*-]F`+7-oHml,[_=0Gr3Se"knmJRaV`!;Tdom,Nbg;h:rWe0b..t7et&O\>8@K3)op@eAPWmfW1I2:0I@:BNG1\;;QPTC2Ir0*+Um:&Rr4=K^fq@b]P)PeNLAS!,8_HaC8baa*9\5TmltD?rk+D/+[g/NkZ(<78CT7IP8n#=3$c@hY\d1-*W:>m\p=@-pkD3h!"@]9^J7O3F-.g6]!K^*U"Vn<M&lslIeW,:qqdU]%Z)7)H/_#uA-RcGMb9`N9Jm7b8`ns:.+>TiDU-$p8XPZ.<!0n$"!#=Iir8$0QF492AX=pKO_`Q97j6G:1*-"RZb,_r&^7=7[UCQfd]SM*FNCG/c:LGm#\/,Je/.B4q`kEi?LK@$Br5C8CS1Wd]S.U\eAaT)\5"^5W_k8<rsJkabohZ_d37Zt`2IB/MLZpgRhJE_s5h'j7JcJ]+EN]!E5rrOlF-Qa[$C6mDk5$^%[X3:kLD?+c+P^U%p-r1iFtqX2-%hu.A,#1X(<RR5s>?gC"1R<Gd[/Z3eGb!Y4&HXHVD"S/baA1n&@:rpAs#H4J!URE:=,mgpuH28`oSd]g4HL#X<riaMgG+\sBr=$_,b;ZIl#D8Y"Il`#VoJT"Fh9:\ouPiZ.I[b;Z\H#j$PI-]bV^`bjD^.+[p6RanA0q78^&T%iN7B,N$B4r<E):JQWT"^Hl^UC<acKp7dr9I"5qT^aEd=PBOoE%rDF"q)HfkW%2g50?QCl<//aOHOs;o[f?*kok`OgcO/r-fgc`Lq"T\rc.Da3*XCl`Ok5BX$_&rmc-`>\n%Ci#J9pC'k\cso2E-FQ@ng%B+o[DG)LS%7o0]YYO&*4m<1p$0:&h5MVR#sS\IaU)G]okE*G&CJkr93;gho;l#qu*FJ(r3r!.]!/,]~>endstream
endobj
29 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 742
>>
stream
GasJPgMYJ*&;KZJ.HWCfd`J'k78tagMFnTg@CU#t@@:sFA1&NY86(q!BC,Cm5`$Lmj2q;XcMfV8!M"&THtNVuRfm=^!ec6f&VNU_aW8#efM@7M8uE,knlu>)(fYq"DSan3/D;5!!7)[3Va5Z\h?ORSFNt0t]*OJ\9E7,sS_&Bu)!gQRHA,Bl>$@]+2Z0[qoCI*N!H";HV0VhIN+Rlh;a",pjMEkY8HG"c+2?XKqop=NflFF@iG5iZfs64?PRaj70<WK=<EAWO]T'[9^=a9A?(F:P"-W8%XV"oUM2'H*ahN`[#R*&Le`QCPrKJp8lP08Qo9h`.5ZY=MRNTsjE]#?a$8-oV_trn2Hu0]FLJ\tI#:Ps_mU12[bF/(:((R%P+r7`ODB/3&S;d+q@0CU\7"ac7MQ1)"FDt[4@8`^`+!K/'O>DI5qpF;KJFR'MN:A'ZS0%N3>Q(dqiFg/rXQch_4uC(A7R:]KR%nSHA5p$g5mh>*b<6&BF]5a9/oK-Dr)1N$-)BYO3eR7h)%%j'9"b<'q&-iZK'30C6B;fj\`9AL!j&@"P7E.h>VE*'>_1r=4%8-5Kf-Ff!?H^Uf5?"/(<BJhr#gW@$^Yg^>YB3?:%o+N6%.'$VD,Sn1FSmZS.s#32PAXXb83i?lqJr#<>Ob]$UL@XCMKH<'lh?Gj(.2=UEW1gLZrf82JABI]jh$,pjApb.4I?prqL;$a/"/e[BmJkSQ_g.CXrF'$Nes&[DG.!~>endstream
endobj
30 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1781
>>
stream
GauHK9lo&I&A@[:m._i=C`!.<!d)K3E>,6m`_Dmi9b157'Eu[eq&tr`SYhVNh?E]=`%od_,0;#4@NXH9h_*Ig.)),H:3(lWk&BsU!TPlK"=AqTq$!;5]j$QeO@p/cnJ1^A%OCW4$`^U95S0)4iXl8],uIQj:lHe;.H"ke-'M/>\:!En+>T`j#p>*/TaJQ?pIWFd/hKU*L=)@]/O*h273l^&+jK/%?oWeH:A(R"+I!$bJ9_X2f5EjC__:+Tf?%#qB(3F3FE1r#_7>6Jo>B%g:2?V+n3Z$sXPDmJ0`*Q=LA<X;ed,Ug:'.6c&H>)K^jNpjD9C$+p:#:;(,83%S@?#+*Djg]U67"cqc%#UT%n;2a,Ns1#Xo$0`rjcFp1'N2-)";:d25sP/4#X3q041Kjs47tL,Vd\Atf'p#j4VWo[l?Ck4AefJHQ:&pR0(k.2s>V*f0Nj,T[t.AGDoIfc'U&N''nK/'hqP^73Ik]sQL5iXL`ZjY7)[UP(/;I:.lsX$F4WKql-henjrEf+EJqn0>fb`j^h5&aK&jren)/LW53g'-!^I@S-m!%!s#MNq)Yc5r+do/lel^K3Xj@[>*B319]Y#c/;dC.Q.jA;8R9u0gC0\!q+JRZnB/A6%s2[jY,*K]<\N(>%]?6F)-Qn=5(GJ2**<i3_QYp1rP*Ya*F`2Kc)5#WO@Z:<a&QT]<3:fr-Qp\?W,l]`[[NVgbUJjrO#M8;``JB_OE^54X"B8<r0)F'8fu2@Ap@m+eaM1nVHJm,i2@k,+h7C1H$25Kc#"T5R5RZfhCZKoHOW!r30Xs8rXn@!K7M(=hUlP#<Hg@m0@QagILr)6fd<!+-V%^=6#Z2&fhXu.8WT=eAfJBL(j@Fn_2Pu#'f<(B*`I[Qg]'%-!(=RO5gt@215III&Gld9j*m:#qVbUoi]XnXY;8c*KS0UMM7*qLtBWX9sJqEl,KN1E=@3_9"fd80&SoG$5=YJW7L64WGg]AMrP/m)Gos(]WiaLYB#8qc"p^J]d_ALpRfp-?TTht>Ot%hdWp3KT1HN*eqoggJ8m?SDI8`T,%Ss4SZaSBZ-AWH>0IcZ;j8[VJjs2P<c@RlY5Yj`R#PgbrWX4:'F?J@HC3%--<QUd8.@'k*Pp'7B<OUNeB-9T3.s=uUL+iAF6q$W`X9#Wge7^'_A.e@p`#S'*"tb!9KrJ10j.n-.-AmKR"^;m,XX:YP,<8k5b[XTN^CEE7o&I(A@ret6W2@XTtJ>?M,Goa\qnh1ou7u/E_:Y:#)%[r\(`NF-VUbYn54_;an)Pg9eaug.Xep'/%sXuE,fr`i7Bo#F,2W9?E]eB!V<8,I+$!_$Xon("+fX0g],7l6j1A_#b_r^3WajX(6a_%;!E:qfau*rES:VMi0UTUUG3+[WQH4,Y\=Fg+JCI=#hp,fkAce2HF"%B:2J@s7bWmSJ'u(Y5^f7qTCW7E`GTVFJc84,G.ruX%4<MNY\2#N(Aj<g((7P;GhW4%U1L2n<iu[@i^sbrF+2R-mckuReTprB_0"l2m6i`e1aPn8UYa077C,54EkK9)[>b]uos3;K2nIYp,4o/&=(<`04ml(YY4#f3rs,C,=5^-FrU(/A#Q"_SoiAC&PB&$Blt$9RSa0sk^&`QpS37")V-TkW3jC=V.,/N[9c!#.D5Z/h.5k[*Il*)^SXcqKT\n)c@BKRjp]kp,VT_PYWS,k1/ljsg[\KJMnI;HUp7OmY!KZMj[p[SOn*'KFo(mX"02nrT(s6*Q*G$,#DAV<M[_/Hj[bd#D[KlhY#N!$NGlIhVpr7g~>endstream
endobj
31 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1937
>>
stream
GauHK>Ar7S'RnZ;3%0Ntko,;P,`6Wi!SQ%u!OAgD2gb'^AXR_+aJ,\-dO*%bhkBI0m&68DEF?m&q47CCT,:E&Yj+=*!;nR(K3&HuLY3$%6QDI6_onJ@aWau/cZ+NgP1i))inNT+ZF,sJ.u<]*cTkBh8C%<,1*WPJJ`Ac:@if.f<_k:O`"UXa%M6`e+iV.d2A)ISBSX)apik%CqFRHR:ILruW`f?H)"(dk;&u\(&B\8(L"QYWs874a"L%K%jl*2i0mkB)'qQ0pkM1^-U]&(mO7GUhi@f>I@\%C9;XD6d$t`:pX9$%F97jTs\+,MZ%a5-ijFppjY]#,9PQ\ec!9c!q[7(_@dF=qSfJ`CV(_:rJZI(JVG(_)J@r[jb\(t5(EK<r]aMAd]bq=c*:[m>#g[5BMD@LNOAB[;]90Ve8?0JM0<q#8eBGX!7Ye?)LL/1tPOENmrP7h'+f-83VSW_7VcA[>9Tlk*M8S().ljmEjjJfFd&D?-b+D-QF/fD-L+g*D&=iJT(`.bGLc$->nHr8l"K\sgo3,t@IM0fX19cPEMPcWh&bi;^)_BMm>#">C&Q&OgXOL32f:F#/0Y+?XYGi4S((R63ALLIIfY#YYePBR@`chD<kZ!@l[9+O_;l@4Rsj6F44<.\JW"B(eVE=c^$nj6moPYhHS"`]41Jtag&ktWhA>'><Kd"5;ZgX($tH.TLYo+J+3<IAF^m;V"1%RtWAkg617[$'jpeK@$=^km2M.e);V8_lR^2>/0.I[*?7WreO.Eq>83`?Pp?:oM^Z:;*k<5Dn_'.no-`B0'Y_6^D(mPng^3-=ng(!"*Puq#_s.MT,9j=T!RO2s'dqdHAp1H%D+&b4RR##Vc5YNhU0Z/qUUO'Y#7S\%=-A(!`IaDQ1e>:fk)"f:u%Y.l.>2lrm>ODHO7@(S$U,82IqN0c*'PE>1mB!564H=#d75*:rbG1U[j>NDDHYWFAtE]Npu;a'`3Vad%`bXW`lsP[H_W>(Y=>m>SHpo,nLJ6:$D=OQ^8_hAr!?BNmT&gO*?\p8b.Y::X[`'0k%pS+*2&.asTX6LIc*#!nDdQ($eoYA.Hha6XMWGt2,3MMb=dMUMl7=Egn5U>ckXgBb!\f>_hnY@_b0>&62YM==ofeBpBEW'e8WMJW$?<_io[-]XG(K'Lo,eC-@@Ym0Are=/K.\eTn`Ii)AJ8ND#hHSF0s1<`)Y2UGd^k'V)AJ%LtKf\fi)U>(KX:OLGIne=]Q6iALIP>EXZWUk/:$*rbeAafa6FU9i4gbEF9TaC"3EkS#M[YRULiaS-%7&]Eb$;WLU5nG'<dph4^qjT!jSidqmncU:kF;gg`KG!GOS'.ub#g5OtDm'IPW^U+?euJ8-\p%R[`j%"61+GmU<VA;EWbstRCLfZG'$7A0RkXo0@s#@Yf0ZD8SZ64XG:NPiQUu1]cR8F6D[Zji[?RanjCZ.#Ht5>0;;W%PfEZRS,etJo\&I!%BB"iC2HGl/Qb%hV[l&8aY9d>9(6M@\aI"=DiSAJqKdI"6+n0>Bk!%KrP*X)an5O7V.9mIu35bQEg-M.GAg&f_,cL=VI#m3C\\cUiGHls@f>0@Kb>=gr8Y.@6q3N@[#2S4QMr.s]&],nj<Nn=r5j_ZP1VmW6%EjQ8h&+/17_S,6`9>6&:hW>L'L&+q1Na&>>bdlL+%NDT>U!u[G$*ehAD>&/3U`#>W[O#)?8olOR6j@OH%VRu,`_9?]EpMfA*)0G[q,I2)<'<*N;1e/H(oG9/II\!+!6smY@mi2K5]]@[X8L`2eS_k;b;@VFW%ojG-\iEmEP:69(j(_EijL]^<TYrAQ[H-XJ3XlIB7uSDGPbRq5p`:2#i!(_C158g^qO]q[N".bWf3F[62JiQSmiDXVP%WS_n^(^>k9Hk`5-&`nu)F]`jHW!r6`-E7uU/L3#_IiDf-nZ?j$5qLeH*`LX,H~>endstream
endobj
32 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1856
>>
stream
Gb!Sl=`Y6:&:U6Yd4\H&eeTLTV0U9#JVD>M!AA`?Ml`'.M8tdI,6<8l[cWU2GbMRE@Uj&rmT,re[nOWW&$:n5"7Z0rT"h'd)9`j]IH&02&h[eAB8<^Q7=G6Tj/s1S+L,SZ+C@/JJrkX?M^`U(,!>G4kEKp,F(!'*75TU$_;^kMJOH1NE?>Ia6--r,IZK/@iR0mr_D'j.DtW-k]pOJTG,T$E7:A_Q>GHMDSkiT06ikLiE;'"]Vq:En=fOfkN61#H108Kr_@9ueSA1Ye(I_Ml.Y/E$:1\%L;7_,^ZWie[ZYPQ]q]7+u9Cu'[V(."AJ>]+uMam&[YtVP*8k4K9d&ES3OL:NH/gm7R7Mq(S4mmRY)=I<o+2Z$+!GO1`B:6X]ijq;q4_).$nd1:B2LZ9\1t(W\i<A>->#G6n)!U#[HJ'?VBR0in#8.d<Xttjm><9E=HU2PaMKIW>M[XNM-!TNgN035M=ZLV&fhW:Yk:^]CeCbcRhWDj,B%/,c;s%L>d*Xu6Fj0V//qc-#hE?q\LUI&IY^Q=2,6<cYE?E7d?XaYa>lc6`gf0.^ZT%V*?S=0UIj?K>oF:SW'IO5"q3X6Chj!qbiO(kT1i[brKZ(;AdGUR:b7q4FEddohEgcs+gs5)#mILE$`sctdVR"Z4-*"kj9&5"Oap-j@Ts[eo@;6QeWa]<.e=V]Ak=T=-Lu,7j3u'3_a#-7V<m(;5UjC+5':Q@'Zt>]`)kkH%e0?uIF3Aceec@DTinIlg/H)NEri=2aFGdT;Kpr:`fZhkY$F[$P)Up5P<l`.W,BibV[;9d"[r8\^E[00Y9r6%mR#[Qi6P[#oGph-X\YqS!>UW4Lh'tPgK?'t>M)sb<QMlFcPfVqnEYMpPUOJ.@Vmdt3>"Y!WP<-_N)Va$mQ,JL'KubVaW['a5;QjHFqCYS/0UH7N7LU&$hVeDc_AVO\6b$OVFhPBdEWcsKnB)mTgin"I)2]!o1AHZ%FEP)`"VIIf$b:td_ZU#-#sOusduG<^_qHnumi&jN15/dG(N2Sj+84]p_g(E9hIulD1>P6ZJY/]jP7W(/j[NiF/Y1_E-p"@Rp;!?#Jp+8EJW6$_/@Lu/RkNaeKdp*:8Te3Q'PN]+bBg/(Yd7s$=Q$icV4,2:Q)@32Zd&\3WkM;kLf,5COFtT'Sf>8K^=*OXU/Y<F4`#J9-EpJ@]Lpd%L^XWm1@X)Y[n!N3ma&5N>O+AQ[U#iaWba;l8ZiMs[=6=%)ca`TIBXJ*P,cZu9$>Gqi+((-M8&a.='R/%B+QAU3%]4se=ITn>+I:MX'rHh0cnaEdm>"]gf*MegA-3eT7bhi`T:eJo>9^@Mdl,r2W;?bj_`J;^c"QPV0Y.Q7*VbYDPQo8pIc[]-^--%L/VLP>clBYLHVlXkok\ig'Wt-n4dJP?Q6R/KuR"rA,"Ej&W<nLE0a?TcOH1sSd\Cu"T+JSlFcnl,ufR".1149"`ki(7_l-oj"RT6`:$r>r6pWd$!7:I\Dui>a]pFbSjU^XKu.@,1/#+M-Ptj\Te?bBU!L]Gcc<jS$p8kmrr'hUm1oec_bEZR;h*uhB^$$o_JkTsf+N?r4'=HSn^?f,>UrD*.DXPQ9@+[7>#,GT\:+Y9H"+InCC?!TR,^Ai`/Fo1o-SGjr-!Q-N]4Rpe*Jaf24e8Vl(O2cF=64XM>=CZKX7`%PR>]C_oERCi]]pD8`9'7MiIB#Q[Y8Aou<m0g[uANqN>%Z"aXWnmT%!"E-""B8t6+Y2!N&u16i!F=4dPgRL6:d,\VIh>2N'DC9'b_7oP]$CfJ1D3kJ22'rWe?/<qZ1f]Y(L=l!U9:&Z'37\s[a)I5U&?Bf^]b::P$\fWe;.n\;[(Z\-PK(BL^o"sq~>endstream
endobj
33 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 1724
>>
stream
Gb!TW>Bed\&:i[:/*:\_)dLR1pT9eSCsLIEB_R2s@12*</D$%VZbLrZ1)QoSDVL@,%@ggb2)id-)Mrus04OpY97U)RGlF!/EFCs3inHYU7rUMt^0.l`\re(bmDY*P\6p\Ure!8@8dIaF9\b:dlpQ":q;B<3NoMAh(WSrVM4eH7HYO.sUX4j,+7,?t@3"]op[$7L;71<I@@6sPIViGnbtIDBD&o(u^/5_26.SeR;E`o\8.YqY?GbY!LTb@[VoY9"k<a._U2$">'hhjkJsU,fQVnSsYmfKn(#u#lZ;Ni:nHFlDC0a!u^),Pf96=J=Le/^1i.k9@er'Dso:i#tet/O1(t=P@cs9io3O.ss:5"gNA&Kr0,85EQk_HCLgq[e\]6i=R,MR59\9H8Yf2)3#M*e\$-eLPF>2;?S+>G$C<-Kk/kE5g+fcFk"kmZ5"8C>5t27Ekj9"$rFSKOiZP$6/^$BAIS,M.Q02gV'nf%`#PV-iWF5C]_ta(0+%a6WZebo(M;2^Y[4"QarL&=Y[QHLkU02U"C7p>guuIU]q$npT+"8:l\F:&`1:A50'22A@ZB?S]i[S<^Q06Pqmj146^Q0%(K?Y;Q$j@"L*-941M]0P-SmnUp!&]#28M>m80._6Cf(C+pFj]?Xh=nH^j!n1%OIPp"3ub66iC""ltF$X*UdUf<S"1lXB#pd;p8(i$r]Y)8/(`Bd//iXR!<B;oBihTXNi3oY<&gPh30Mq<MC>\=9CgWD>b(edI7*AeBb4s66MSZ]k]o_&G$6W(N49rZG!AFP'=`$j?N2#tcF8318G$;t%JZi`>T4J&l`?(EKmKp1i>?T/87-(HK:5*oEG$@tcIJJ+?T&s/rXi1/@c]$qn6ak92Vog,a@7'7+%'rsXH0h_e<N%Z%E.ej=o"YMrsY<?5gHf'cXLWVN2r1A_&&]I(1YARuA&6r.[^JLG0l+>6f7"HNrQ(IQZ;F=eVehjc9cP_HI8=!^_BJOrMU4u.CH8qZK7e$tIn<<?c@)QC(Sn%deQR7\;Pa'a\E_TQXWAQ:dBGm34g<pCs%!HWr-e"*5+Cfu@W+qIHfR.g!,o*OW?s?tsHhm!&K4Z#Bk`oV]A-u!X9,2p)/*>YjiJ6r!A&X3Pf&MW_$XTbn&B"8&LD'dKfn&`)[I7CrdlhFf[kWr0$X6p2$H9QQB%\`5XM,jUKCJCNg$6.ej`9]q34:WhTI?l'VWIdVJFZ)*5mfJSF'-seR:<^=`t)9g89!t8L#f0MHD%T)Le>u$+2/[TPnKG>68s!:h7@-5g9g7>3?'Z1Z@m^bM\=0aWF<d\X-Z7X86$ecmFuXG3BrSc?PtU]T<i!N-.tSblT#JClOa]mn%)>3W;?bS8\B>&<5KqVaV.e)Jn%`>HS<(hRO+>LYBZ0FWbkCe;R%28-V".jgk!_-H:c@.m)rP$\OBfa`h\p>PV]!/4@:/KcTX.R%7SCBE"cMh;t1l@)=)?t1G1.&h<30G<'jA4gU@VR,33T9TLASWgVeK"Lb[aLK?ISqdmhS>X3>$qG<EIgNaVi*J&Vo%=6@!0L\SX'\W=@@a`o2]"cPq>G0YHYMP2^lE7&Q8,]Fn9K4Yhl2k)P::.=$goCJ,GE\<Sr;XX:Ois&m;3+lBV1U@N-Wg]gO";"<gj2K!3h&$Haj_Vc%3b)R-okS\%pC,=2MK4df<pP=H8G%0r93B`UVnga3B?`9(X#<t4P'q,Mn(@g1[^*aG(DCWQD?~>endstream
endobj
34 0 obj
<<
/Filter [ /ASCII85Decode /FlateDecode ] /Length 934
>>
stream
GauHI9lldX&A@Zcp6]I366@>,UXM%u:GY_od,pK!`j/<kOTWO9s*^50PB]E+I'E,X=KV;1\%>g"TQma<2r4O>K]$/k?l[$00TUGi?Gi,VH#a1W>^8LETrA;WO?cbeaB)+@^*GJl!<Z%2dE2Pe1ST#bJ`AqjR"F,/o%[Y=Pb^ou5W&)/Q3[9jOdmZ'7slb<\,0BY\tTuPfs3u6bnl#+#AZd$;2%Lgo9BjF#0ss?r1:`+].ugf[PihHAT$LM#nNOVR*I&-U.C0i/E"tC9PV$UPEha_qubo'Kf+\T#u+6SH6*%>'Yjk0XXNSB$2YrN-!>o`6(hFUC)9J4QDpUI#XK1@@l35+^s$KN8Pd4CiBCrsHX#cS74@j$^rCN_jUn@8+'STf-['eT4tt5`'rj1rkSO$@`tkcHkm;Hd(^YB3WP`\?m2'IYM"W7%6h`-0I-F52RsQ"Tjfu.8lDCSaVY9FXeNA!K8tt)>(t;5g2?V5^0T7NhYNqW33[bDF9qL8!#6;%6#REWtn2Tr@J=ZQEi$,X5B!QX'h`%Pg]tXD?%]u'Z=O]lW=Pf[lDEk-e7J377VAYK6oN&>6/PXUgTV"A$V-\-]ZGc>0%#_Lu4,hh1fosA`2;`cbB@@aR?%.Fj$;XV%2`m5l'(aPAO=6M2Mat',4*P,n_Mr:Xf="HCP,\j\:FNmY4&5X[[9NEBZSKS;1T!6&a%>&GNbCqJ-^3Cq*N/hp3Ct#Y[d'kW-@XhJ:7CnhrV0001Ar6EEVf-qkL%>mMp?*+ZS,+P\\gI'o=`BjQ`5CaD,cfJcc3B-[,,<H`<iH43,Zfkd4S#j_7c8SSVco^%FZE*7X/&hOk=P@,k;UD1OeC=bK=sF[n5ArlArAV/koRt3s-Q_nHnC0EF\=4RDpO2magH9fB.RH*O5F_NTPr<3MV67ab'Zs\+KpMIgbq4T)~>endstream
endobj
xref
0 35
0000000000 65535 f
0000000061 00000 n
0000000123 00000 n
0000000230 00000 n
0000000342 00000 n
0000000547 00000 n
0000000752 00000 n
0000000857 00000 n
0000001062 00000 n
0000001267 00000 n
0000001472 00000 n
0000001678 00000 n
0000001884 00000 n
0000002090 00000 n
0000002296 00000 n
0000002502 00000 n
0000002618 00000 n
0000002824 00000 n
0000003030 00000 n
0000003236 00000 n
0000003306 00000 n
0000003598 00000 n
0000003742 00000 n
0000005704 00000 n
0000007361 00000 n
0000009090 00000 n
0000011026 00000 n
0000012758 00000 n
0000013793 00000 n
0000015500 00000 n
0000016333 00000 n
0000018206 00000 n
0000020235 00000 n
0000022183 00000 n
0000023999 00000 n
trailer
<<
/ID
[<11be394e8ad147cab02e0a1e4416f65a><11be394e8ad147cab02e0a1e4416f65a>]
% ReportLab generated PDF document -- digest (opensource)
/Info 20 0 R
/Root 19 0 R
/Size 35
>>
startxref
25024
%%EOF

View File

@@ -1,442 +0,0 @@
# Les 13 — Agents
## Slide Overzicht (Klas A — 3 uur fysiek, demo-driven)
**Lesvorm:** Tim demonstreert klassikaal. Studenten kijken. Zelf bouwen = huiswerk.
**Demo-app:** Nieuwe research-agent (from scratch, los van Polderfest)
**Vervolg op:** Les 12 — Tool Calling
**Aansluit op:** Les 14 — RAG + embeddings
---
## Slide 1: Title
### Les 13 — Agents
**Visual:**
- Background: CREAM
- "Les 13" in BLUE
- "Agents" in BLACK
- Subtitle: "Een LLM in een loop met tools — autonoom tot het klaar is"
---
## Slide 2: Terugblik
### Waar staan we?
**Vorige les (Les 12):**
- Tool Calling — AI roept zelf functies aan
- `tool({ description, inputSchema, execute })`
- `stopWhen: stepCountIs(5)` — multi-step
- Polderfest chat met 6 tools (search, stats, schedule, favorites)
**Wat een tool-call bot nog NIET kan:**
- Lange ketens (30+ stappen)
- Eigen plan opstellen + bijstellen
- Tools die andere tools triggeren
- Dynamisch het model wisselen per stap
**Vandaag: agents. Eén stap verder.**
**Visual:** Pijltje van "5-step tool calling" naar "20-50 step agent loop"
---
## Slide 3: Planning
### Vandaag — 180 minuten
| Onderwerp | Duur |
|-----------|------|
| Welkom + Terugblik | 10 min |
| Theorie: wat is een agent? | 30 min |
| **Live Demo 1** — Setup nieuwe research-agent | 25 min |
| **Live Demo 2** — ToolLoopAgent + stopWhen | 25 min |
| **Pauze** | 15 min |
| **Live Demo 3** — prepareStep + dynamic control | 25 min |
| **Live Demo 4** — Planning patterns + done-tool | 20 min |
| Wanneer agent vs tool-call vs workflow? | 10 min |
| Lesopdracht + Huiswerk uitleg | 15 min |
| Vragen + Afsluiting | 5 min |
**Format:** Demo-driven. Code-fragmenten in slides + live in editor.
---
## Slide 4: Wat is een agent?
### LLM + Tools + Loop
**De definitie van Vercel AI SDK:**
> Agents are LLMs that use tools in a loop to accomplish tasks.
**Drie componenten:**
1. **LLM** — beslist wat de volgende actie is
2. **Tools** — uitbreiding (search, read, write, anything)
3. **Loop** — runtime die deze twee orchestreert
- *Context management:* wat ziet model in elke stap
- *Stopping condition:* wanneer is de taak klaar
**Het verschil met Les 12:**
- Les 12: tot 5 stappen, korte taken
- Les 13: 20-50+ stappen, langere autonome workflows
**Visual:** Loop-diagram — LLM → kies tool → execute → resultaat → LLM → ... → klaar
---
## Slide 5: De anatomie van de loop
### Wat gebeurt er per stap?
```
┌─────────────────────────────────────┐
│ Loop iteratie (1 step): │
│ │
│ 1. Stuur messages naar model │
│ 2. Model genereert response: │
│ - text? → loop stopt │
│ - tool-call? → ga door │
│ 3. Voer tool execute() uit │
│ 4. Voeg tool-result toe aan msgs │
│ 5. Check stop-conditie │
│ 6. Volgende iteratie │
└─────────────────────────────────────┘
```
**De loop stopt wanneer:**
- Model geeft text in plaats van tool-call (natural finish), OF
- `stopWhen`-conditie is voldaan, OF
- Tool zonder execute wordt aangeroepen (done-pattern), OF
- Tool-call needs approval
**Default in v6:** `stepCountIs(20)` — veiligheidsgrens.
---
## Slide 6: ToolLoopAgent
### De nieuwe agent-abstractie in v6
**In plaats van zelf streamText loopen, gebruik je ToolLoopAgent:**
```typescript
import { ToolLoopAgent, tool, stepCountIs } from "ai";
const researchAgent = new ToolLoopAgent({
model: "openai/gpt-4o",
system: "You are a research assistant...",
tools: {
webSearch,
readPage,
saveReport,
},
stopWhen: stepCountIs(30),
});
const result = await researchAgent.generate({
prompt: "Onderzoek de impact van AI op de Nederlandse bouwsector",
});
console.log(result.text); // antwoord
console.log(result.steps); // alle stappen
```
**Wat krijg je:**
- Minder boilerplate (geen eigen loop schrijven)
- Herbruikbaar (definieer 1x, gebruik overal)
- Built-in loop, context, stopping
**Visual:** Side-by-side: handmatige while-loop vs ToolLoopAgent.
---
## Slide 7: Stop-condities
### Wanneer is een agent klaar?
**Built-in condities:**
| Conditie | Wanneer | Wanneer gebruiken |
|----------|---------|-------------------|
| `stepCountIs(N)` | Na N stappen | Default veiligheid |
| `hasToolCall("done")` | Specifieke tool gebruikt | Done-pattern |
| `isLoopFinished()` | Nooit — model bepaalt | Lange autonomie |
**Combineren met array:**
```typescript
stopWhen: [
stepCountIs(50), // max budget
hasToolCall("submit"), // OF submit-tool aangeroepen
]
```
**Custom conditie:**
```typescript
const budgetExceeded: StopCondition<typeof tools> = ({ steps }) => {
const cost = computeCost(steps);
return cost > 0.50; // stop bij $0.50
};
```
**Visual:** Drie traffic lights — rood (step limit), oranje (tool reached), groen (kostgrens).
---
## Slide 8: prepareStep
### Per stap iets anders doen
**`prepareStep` runt VOOR elke stap. Je kunt aanpassen:**
- Model wisselen (mini → sonnet voor complex)
- Tools beperken (alleen search in fase 1, alleen write in fase 3)
- Messages trimmen (context-budget bewaken)
- `toolChoice` forceren
**Voorbeeld — dynamic model:**
```typescript
new ToolLoopAgent({
model: "openai/gpt-4o-mini", // default
prepareStep: async ({ stepNumber, messages }) => {
if (stepNumber > 2 && messages.length > 10) {
return { model: "anthropic/claude-sonnet-4.5" };
}
return {}; // niks aanpassen
},
});
```
**Voorbeeld — fase-gebaseerde tools:**
```typescript
prepareStep: async ({ stepNumber }) => {
if (stepNumber <= 3) return { activeTools: ["webSearch"] };
if (stepNumber <= 6) return { activeTools: ["readPage"] };
return { activeTools: ["saveReport"], toolChoice: "required" };
};
```
---
## Slide 9: Wat we vandaag bouwen
### Research-agent from scratch
**Doel:** Een agent die zelfstandig een onderzoeksvraag beantwoordt door web te zoeken, pagina's te lezen, en een rapport in Supabase op te slaan.
**Stack:**
- Next.js 16 + TypeScript + Tailwind + Shadcn
- AI SDK v6 (ToolLoopAgent)
- Tavily API (web search)
- Supabase (rapport opslag)
- OpenAI gpt-4o / Anthropic Sonnet
**Tools die we bouwen:**
| Tool | Wat | Gebruik in flow |
|------|-----|-----------------|
| `webSearch` | Tavily query → 5 resultaten | Fase 1: oriëntatie |
| `readPage` | URL → fulltext extractie | Fase 2: diepgang |
| `saveReport` | Insert in `research_reports` | Fase 3: opslaan |
| `done` | No-execute, signaal klaar | Eind: stoppen |
**Visual:** Diagram van research-flow: vraag → search → read → save → done.
---
## Slide 10: LIVE DEMO 1 — Setup research-agent
### ~25 min
**Wat ik laat zien:**
1. `pnpm create next-app research-agent --typescript --tailwind --app`
2. `pnpm add ai @ai-sdk/openai @ai-sdk/anthropic zod @supabase/supabase-js`
3. `.env.local` — TAVILY_API_KEY, OPENAI_API_KEY, SUPABASE_*
4. Supabase schema: `research_reports` (id, query, summary, sources jsonb, created_at)
5. `lib/tools.ts` — eerste tool `webSearch` met Tavily fetch
6. `lib/agent.ts``ToolLoopAgent` initialiseren
7. `app/api/research/route.ts` — POST endpoint die agent.generate() doet
8. Eerste test in terminal: `curl /api/research -d '{"query": "..."}'`
**Belangrijk:** Tavily API key is gratis tier (1000 calls/maand). Studenten krijgen instructie om zelf account te maken.
**Visual:** Folder-tree van de research-agent repo.
---
## Slide 11: LIVE DEMO 2 — ToolLoopAgent + stopWhen
### ~25 min
**Wat ik laat zien:**
1. Tweede en derde tool toevoegen: `readPage`, `saveReport`
2. `ToolLoopAgent` configuren met alle drie + `stopWhen: stepCountIs(30)`
3. System prompt schrijven: "plan eerst, search dan, lees relevante pagina's, vat samen, sla op"
4. Test met `"Schrijf een rapport over de Polderfest 2027 lineup"`
5. `result.steps` loggen — zien dat agent 8-15 stappen doet
6. Wisselen naar `isLoopFinished()` — zien dat agent zelf stopt
7. `hasToolCall("saveReport")` toevoegen — stoppen zodra opgeslagen
**Visual:** Console-log met alle stappen (toolName, input, output) — laat zien hoe agent zelf plant.
---
## Slide 12: Pauze
### 15 minuten
---
## Slide 13: LIVE DEMO 3 — prepareStep
### ~25 min
**Wat ik laat zien:**
**Use case 1 — Dynamic model:**
- Default: `gpt-4o-mini` (snel + goedkoop)
- Vanaf stap 5: `claude-sonnet-4.5` (beter reasoning)
- Console-log per stap welk model
**Use case 2 — Fase-gebaseerde tools:**
- Stap 0-3: alleen `webSearch` (oriëntatie)
- Stap 4-7: alleen `readPage` (diepgang)
- Stap 8+: alleen `saveReport` + `done` (afronden)
- Zien: agent kan niet 'cheaten' door te vroeg op te slaan
**Use case 3 — Context-trimming:**
- Na 15 messages: trim oude tool-results
- System prompt + laatste 10 berichten
- Voorkomt token-explosie bij lange agents
**Visual:** Timeline-strip met de drie fasen + model-switch.
---
## Slide 14: LIVE DEMO 4 — Done-tool + custom stop
### ~20 min
**Wat ik laat zien:**
**Pattern 1 — Done-tool (no-execute):**
```typescript
done: tool({
description: "Signaleer dat onderzoek klaar is.",
inputSchema: z.object({
finalReportId: z.number(),
summary: z.string(),
}),
// GEEN execute — stopt de loop
})
```
Met `toolChoice: "required"` forceer je dat agent ALTIJD een tool aanroept — en `done` is hoe-ie stopt.
**Pattern 2 — Custom stop op kosten:**
```typescript
const costLimit: StopCondition<typeof tools> = ({ steps }) => {
const tokens = steps.reduce((sum, s) =>
sum + (s.usage?.totalTokens ?? 0), 0);
return tokens > 50_000;
};
```
**Pattern 3 — Plan-act-reflect loop:**
- Stap 0: agent maakt plan (text)
- Stap 1-N: voert plan uit (tools)
- Eind: agent schrijft reflectie (text)
**Visual:** Plan-Act-Reflect ReAct-diagram.
---
## Slide 15: Wanneer agent vs tool-call vs workflow?
### Niet alles hoeft autonoom
| Scenario | Aanpak |
|----------|--------|
| Vraag → 1 tool → antwoord | Plain tool-call |
| Vraag → 2-5 tools → antwoord | `stopWhen: stepCountIs(5)` (Les 12) |
| Onderzoek → 10-50 tools → rapport | **Agent** (Les 13) |
| Productie pipeline met validatie | **Workflow** (explicit code) |
| Wettelijk: must be reproducible | **Workflow** — geen agent |
**Wanneer GEEN agent gebruiken:**
- Deterministisch nodig (audit, finance, juridisch)
- Voorspelbare kosten
- Latency-sensitive (< 1s antwoord)
**Wanneer WEL agent:**
- Open-ended taken (research, planning, debugging)
- Tot stand komen via meerdere stappen
- Cost en latency mag wat hoger
**Quote uit AI SDK docs:** *"Agents are flexible and powerful, but non-deterministic. When you need reliable, repeatable outcomes, use workflow patterns."*
---
## Slide 16: Lesopdracht + Huiswerk
### Bouw je eigen agent
**Lesopdracht (in-class, 30 min):**
- Setup research-agent in eigen repo
- Tavily account aanmaken + key in .env
- Agent draaien met `stepCountIs(10)` en eerste eigen query stellen
- Logs bekijken: hoeveel stappen, welke tools
**Huiswerk (voor Les 14):**
- 4e tool toevoegen — `listReports` om opgeslagen rapporten op te halen
- `prepareStep` toevoegen — minimaal 1 use case (model OF tools OF context)
- Custom `StopCondition` schrijven (kosten OF specifieke conditie)
- `AGENT.md` schrijven: tool-lijst + stop-strategie + 1 voorbeeld-run met logs
**Bonus:**
- UI met live-step-rendering (elke stap als card)
- Sub-agent: 2e agent die per pagina samenvat, hoofdagent gebruikt sub-agent als tool
---
## Slide 17: Volgende les + Afsluiting
### Vragen?
**Vandaag gezien:**
- Agent = LLM + tools + loop
- `ToolLoopAgent` met `stopWhen` en `prepareStep`
- 4 stop-condities: stepCountIs, hasToolCall, isLoopFinished, custom
- prepareStep voor dynamic model / tools / context
- Done-tool pattern voor forced tool calling
- Wanneer wel/niet een agent
**Volgende les (Les 14): RAG + embeddings**
- Semantic search op grote datasets
- pgvector in Supabase
- Embedding models — wat zijn vectors
- RAG-pipeline: chunk → embed → retrieve → generate
- Combo: RAG-tool in een agent — best of both worlds
**Daarna in deze leerlijn:**
- Les 15-16: Testing + Deployment + Performance
- Les 17-18: Eindopdracht-werkdagen + Pitch
**Vragen? Feedback?**
---
## Slide Summary
| # | Title | Type |
|---|-------|------|
| 1 | Title | Opening |
| 2 | Terugblik | Recap |
| 3 | Planning | 180-min |
| 4 | Wat is een agent | Theorie |
| 5 | Anatomie van de loop | Theorie |
| 6 | ToolLoopAgent | Theorie |
| 7 | Stop-condities | Theorie |
| 8 | prepareStep | Theorie |
| 9 | Wat we vandaag bouwen | Intro demo |
| 10 | **LIVE DEMO 1** — Setup | Demo |
| 11 | **LIVE DEMO 2** — stopWhen | Demo |
| 12 | Pauze | Break |
| 13 | **LIVE DEMO 3** — prepareStep | Demo |
| 14 | **LIVE DEMO 4** — done + custom stop | Demo |
| 15 | Wanneer wel/niet | Reflectie |
| 16 | Lesopdracht + Huiswerk | Praktijk |
| 17 | Afsluiting + Les 14 preview | Closing |
---
## 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 — Workflows: https://ai-sdk.dev/docs/agents/workflows
- Tavily API: https://tavily.com/
- Anthropic — Building effective agents: https://www.anthropic.com/research/building-effective-agents

Binary file not shown.

Binary file not shown.

Binary file not shown.