Przejdź do treści
Jobless Busters
Z testamiZaawansowana25–35 min

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?

  • 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

JavaScript
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

JavaScript
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

JavaScript
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

JavaScript
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

JavaScript
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

JavaScript
await assert.rejects(agentLoop('q', async () => new Promise(() => {}), {}, { timeoutMs: 15 }), /deadline/);

agent: kontekst, output i anulowanie mają twarde ograniczenia

JavaScript
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/);

Źródło: AI & Agents — ćwiczenia praktyczne — Autorskie ćwiczenie z programu nauki, nie potwierdzone zadanie rekrutacyjne.

JavaScript · uruchamiany w Web Workerze w przeglądarce

Gotowe funkcje pomocnicze (już zdefiniowane, tylko do odczytu)

Tab wcina · Esc, a potem Tab, opuszcza edytor · ⌘/Ctrl + Enter uruchamia testy · Twój kod jest zapisywany w tej przeglądarce.

Uruchom testy, żeby sprawdzić rozwiązanie. Gotowych testów: 7.

Utknąłeś? Podpowiedzi idą od ogólnych do konkretnych.

Najpierw spróbuj naprawdę — rozwiązanie najwięcej daje, gdy już trochę się namęczysz.