# Les 15 — Huiswerk ## Agent uitbreiden + prepareStep + custom stop + AGENT.md **Vak:** AI-Assisted Development **Opleiding:** NOVI Hogeschool Utrecht **Deadline:** Voor Les 16 — 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 = ({ 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 = ({ 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!