8.4 KiB
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.
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
listReportswerkt- System prompt aangepast — agent weet wanneer te checken
- Minstens 1 test-query gedraaid die
listReportstriggert
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.
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
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
prepareStep: async ({ messages }) => {
if (messages.length > 15) {
return {
messages: [messages[0], ...messages.slice(-10)],
};
}
return {};
}
Eisen
- Minimaal 1
prepareStepuse-case geïmplementeerd - Effect zichtbaar in console-logs (welk model / welke tools per stap)
- In
AGENT.mduitleggen 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
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
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:
stopWhen: [
stepCountIs(30),
tokenBudget,
hasToolCall("saveReport"),
]
Eisen
- Custom
StopConditionwerkt - Gecombineerd met
stepCountIs(N)voor veiligheid - In
AGENT.mduitleggen 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
prepareStepzichtbaar?
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():
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:
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
- GitHub repo URL in Brightspace
AGENT.mdin repo-root- Updated
lib/tools.tsmetlistReports(+ optionele extra tool) - Updated
lib/agent.tsmetprepareStep+ custom stop - 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
prepareStepreturnt niks → je moet{}returnen voor "geen wijziging", nietundefined- Custom stop returnt nooit true → log
steps.lengthen je conditie-waarde per stap - Agent stopt te vroeg → check of je
stopWhenarray 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
StopConditionmet fake steps eerst - Houd
AGENT.mdbij 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!