Minimalna pętla agenta
Polecenie: sztuczny model zwraca tekst końcowy albo listę wywołań narzędzi. Obsłuż wywołania, sprawdź argumenty i odeślij wyniki. Ogranicz czas, kroki i liczbę narzędzi. Na początek narzędzia tylko odczytują dane.
Sprawdza: sterowanie procesem i granice zaufania. Model proponuje, aplikacja weryfikuje i wykonuje.
Kontrakt: agentLoop(input, model, tools, options). model(messages, signal) zwraca {kind: 'final', text} albo {kind: 'tools', calls: [{id, name, args}]}. tools to obiekt { [name]: { validate(args), run(args, signal) } }. Wynik narzędzia wraca do historii jako {role: 'tool', callId, content: JSON.stringify(wynik)}, a funkcja zwraca {text, history}.
Opcje: maxTurns (6), maxCalls (8), timeoutMs (5000), maxChars (50 000), signal. Błędy mają komunikaty: unknown tool, duplicate call id, tool budget, turn budget, deadline, context budget, output budget. Deadline ma działać także wtedy, gdy model ignoruje sygnał. parseLookupArgs jest dostępne obok.
Limit znaków jest uproszczeniem limitu kontekstu, nie dokładnym liczeniem tokenów. Brak trwałego stanu i rzeczywistego rozliczania kosztów.
Dopytanie: co zmieni się po dodaniu zapisu?
Przećwiczysz
- Zbudować pętlę model → narzędzia → model
- Walidować nazwę narzędzia i argumenty przed wykonaniem
- Wymusić limity kroków, wywołań, czasu i kontekstu
- Powiązać wynik z ID wywołania
Testy (7)
Testy przeniesione z reference.test.ts materiału źródłowego (node:test). W przeglądarce działają z tym samym zestawem asercji.
agent: tool result z prawidłowym callId trafia do modelu
let n = 0;
const result = await agentLoop('question', async messages => {
if (n++ === 0) return { kind: 'tools', calls: [{ id: 'c1', name: 'lookup', args: { id: 'A' } }] };
assert.deepEqual(messages.at(-1), { role: 'tool', callId: 'c1', content: '{"id":"A"}' });
return { kind: 'final', text: 'done' };
}, { lookup: { validate: parseLookupArgs, run: async x => parseLookupArgs(x) } });
assert.equal(result.text, 'done');agent: zła nazwa, także odziedziczona, nie wykonuje narzędzia
for (const name of ['deleteEverything', 'toString']) {
await assert.rejects(agentLoop('q', async () => ({ kind: 'tools', calls: [{ id: '1', name, args: {} }] }), {}), /unknown tool/);
}agent: zły schemat nie wywoła run
let ran = false;
await assert.rejects(agentLoop('q', async () => ({ kind: 'tools', calls: [{ id: '1', name: 'lookup', args: { id: 7 } }] }),
{ lookup: { validate: parseLookupArgs, run: async () => { ran = true; } } }), /args/);
assert.equal(ran, false);agent: limit tool calls i limit tur
const model = async () => ({ kind: 'tools', calls: [{ id: String(Math.random()), name: 'lookup', args: { id: 'A' } }] });
const tools = { lookup: { validate: parseLookupArgs, run: async () => ({}) } };
await assert.rejects(agentLoop('q', model, tools, { maxCalls: 0 }), /tool budget/);
await assert.rejects(agentLoop('q', model, tools, { maxTurns: 1 }), /turn budget/);agent: ponowne call ID jest odrzucane
await assert.rejects(agentLoop('q', async () => ({ kind: 'tools', calls: [{ id: 'same', name: 'lookup', args: { id: 'A' } }] }),
{ lookup: { validate: parseLookupArgs, run: async () => ({}) } }), /duplicate call id/);agent: deadline działa także z adapterem ignorującym signal
await assert.rejects(agentLoop('q', async () => new Promise(() => {}), {}, { timeoutMs: 15 }), /deadline/);agent: kontekst, output i anulowanie mają twarde ograniczenia
await assert.rejects(agentLoop('too long', async () => ({ kind: 'final', text: 'a' }), {}, { maxChars: 2 }), /context budget/);
await assert.rejects(agentLoop('q', async () => ({ kind: 'final', text: 'too long' }), {}, { maxChars: 2 }), /output budget/);
const c = new AbortController(); c.abort(new Error('stop'));
await assert.rejects(agentLoop('q', async () => ({ kind: 'final', text: 'ok' }), {}, { signal: c.signal }), /stop/);Powiązane pytania
Źródło: AI & Agents — ćwiczenia praktyczne — Autorskie ćwiczenie z programu nauki, nie potwierdzone zadanie rekrutacyjne.