One turn
# grade this stage (from your course directory)
npx theinference test --stage one-turn --watchEvery agent is a loop, and every loop starts with one turn: hand the model the transcript, get back a message, read what it wants to do next. Before you can loop, you have to take that single step cleanly — call the model, append its reply to the running messages, and surface the two things the loop will branch on: the stop_reason and any tool_use blocks the model emitted.
No real API yet. The model is injected — at grade time it's a cassette that replays a recorded /v1/messages response — so this step is pure plumbing: in goes the transcript, out comes a well-formed assistant message.
messages → model(messages) → { content: [...blocks], stop_reason }
│
append to messages, then read:
stop_reason ("tool_use" | "end_turn" | ...)
tool_use blocks ({ id, name, input })
What to build
Take a single model step
In src/loop.ts, implement one model step. Call the injected model with the current messages, append the assistant message it returns to that array, and report back its stop_reason together with the names of any tool_use blocks in the response. That stop_reason is the signal the loop will key on next step; the tool_use blocks are the work it will dispatch.
Graded by program.ts one-turn, which builds a cassette model from the fixture, runs your step once, and prints { stopReason, toolNames }.
import type { Message, Model, AssistantMessage } from './types.ts'
// Take a single step: call the model, append its reply to `messages`,
// and return the stop_reason plus the tool_use blocks it emitted.
export async function step(messages: Message[], model: Model) {
// const resp = await model(messages)
// messages.push({ role: 'assistant', content: resp.content })
// -> return { stopReason: resp.stop_reason, toolUses: [...] }
}
The injected model has the same shape as the real /v1/messages endpoint — a request of messages in, an assistant message (content blocks + stop_reason) out. In your own runs you can point it at the SDK or plain fetch against ANTHROPIC_BASE_URL; the grader swaps in a cassette that replays a recorded response. Either way your step code is identical: it never inspects what the model is, only the message it gets back.
It's tempting to read stop_reason and bail without touching messages. Don't. The transcript has to carry the model's own turn — including its tool_use blocks — or the next request will be missing the context it's about to respond to. Append first, then return.
Process mode: your_program.sh one-turn runs program.ts one-turn on the case fixture. It feeds your step a one-turn cassette, then prints { stopReason, toolNames } as JSON. The grader compares that line to fixtures/expected.json — {"stopReason":"tool_use","toolNames":["Read"]} — as an exact match. If you forgot to read the tool_use block, toolNames comes back empty and the diff is obvious.