Track: build the agent. Proof: npx vitest run api/src/agent/manual-loop.test.ts.
Write the agent loop yourself, and prove it returns the same result as the SDK's tool runner.
A model cannot run your code. It can only ask for a tool to run. Something must run it, add the result to the chat, and call again. If that something has no limit, one bad worksheet can loop forever and spend your budget.
Call once and take the first reply. If the model submits a worksheet with 60 items for a kindergarten band, you have nothing to send back. The page gets a bad sheet.
runManualAgent in api/src/agent/manual-loop.ts is about 60 lines:
- Call the model with the history.
- Add up the usage.
- If
stop_reasonis nottool_use, stop. - Otherwise, add the assistant reply to the history, run each requested tool, and add a
tool_resultfor it. - Repeat, at most
MAX_ITERATIONStimes.
The tool is submit_worksheet. It checks the sheet with zod and then checkBand. A bad sheet returns Problems: ... so the model can fix it. A good one returns ok.
Give the loop a script: first an overfull sheet, then a good one, then a closing line. It sends the problems back, takes the fixed sheet, and returns it as a worksheet.
- Open
api/src/agent/manual-loop.test.ts. Findscripted: a real SDK client whose network is a list of replies. - Type
npx vitest run api/src/agent/manual-loop.test.ts. Expect 3 passed. - In
manual-loop.ts, changeMAX_ITERATIONSfrom 4 to 9 and run it again. Expect the cap test to fail. Change it back.
The test runs the same scripted turns through runAgent (the tool runner) and runManualAgent, and asserts the results are equal and the number of calls is equal. A second case sends an overfull sheet every turn and asserts both loops give up with failed after 4 calls.
Track A: make the loop stop with failed when the same problems come back twice in a row. Track B: stream the reply and show each tool call in the chat as it happens.
Where is the limit on your own agents: turns, tokens, money, or time? Which one stops a runaway first?