Agent loops: what happens after the model call
Follow a research assistant through model decisions, tool execution and observations, with explicit stopping rules and a distinction between a final answer and a verified outcome.
Exploration · Principles and possible approaches inspired by a project context.
- AI learning
- Agent loop
- Tool calling
- TypeScript
Ask an assistant to research agent execution and prepare a note. Its first response might contain an explanation or a request to search. Who performs that search, returns its results and stops the assistant from searching indefinitely?
A loop means taking a step, looking at the result and deciding what to do next. When researching something yourself, you read one page before deciding which page to open next. An agent loop divides that work between a model and application code.
This article uses a fictional research assistant, rather than claiming experience with a deployed implementation.
One model call is not necessarily one task
Prepare input → Call model → Inspect output
├─ Tool request → Execute → Record ─┐
└─ Final answer → Stop │
↑─────────────────────────────────────────┘
The model requests a tool; application code executes it and supplies the observation to a later call. The LangChain agents documentation illustrates this iteration between model and tool nodes. It describes application behavior, not a claim about the model’s internal reasoning.
A question might finish immediately. Research can require several searches and readings. Extra rounds are useful only if they add evidence or narrow uncertainty; their count is not a quality measure.
A small loop with explicit responsibilities
Walk through a task first: find a source about tool retries and summarize it in three sentences.
- The model requests a search; application code performs it.
- The search tool returns a passage; the application gives it to the model.
- The model writes three sentences; the application recognizes a final answer and stops.
If the search found nothing useful, the third step might request different keywords instead. That begins another round. Application code still performs every actual search.
This is a custom teaching interface, not an SDK request format. It supports one read-only search per round. A real adapter must validate model output before converting it into Decision. The supplied model function already has the user’s task bound to it; history contains only the execution turns, so it can be empty on the first call.
type Decision =
| { kind: 'answer'; text: string }
| { kind: 'search'; callId: string; query: string }
type Turn = { decision: Decision; observation?: string }
type Model = (history: readonly Turn[]) => Promise<Decision>
export async function runLoop(
model: Model,
search: (query: string) => Promise<string>,
maxCalls = 4,
) {
if (!Number.isInteger(maxCalls) || maxCalls < 1) {
throw new RangeError('maxCalls must be a positive integer')
}
const history: Turn[] = []
for (let calls = 0; calls < maxCalls; calls++) {
const decision = await model(history)
if (decision.kind === 'answer') {
return { status: 'answered' as const, text: decision.text, history }
}
if (!decision.query.trim() || !decision.callId.trim()) {
throw new Error('Search requires a query and call ID')
}
const observation = await search(decision.query)
history.push({ decision, observation })
}
return { status: 'limit_reached' as const, text: '', history }
}
Injected dependencies let us test with fixed responses before paying for model calls. A search followed by an answer should execute the search once. A model that always requests another search should exhaust its allowance after four calls.
The history preserves each request with its observation. An API adapter must preserve the provider’s request/result association. A model-written sentence saying “search succeeded” is not a tool observation.
A round limit is only one bound
maxCalls does not bound elapsed time. A hanging search still blocks an await, while a large response can consume the context window in one round.
Networking, persistence and automatic retries are deliberately outside this example. Tool failures propagate to the caller. A real application must address request deadlines, cancellation, response size, costs and error records separately. Timing out with Promise.race does not, by itself, cancel the underlying request.
When the allowance runs out, report an incomplete task and retain useful intermediate evidence. The returned history exposes observations to the caller; keeping them across process restarts requires the caller to persist them. Returning normally from the function is not a reason to present an empty result as completed work.
An answer is different from a verified result
Suppose the assistant says it saved a note:
| Evidence | What it establishes |
|---|---|
| A generated completion message | The model expressed completion |
| A successful save-tool response | The tool reported success |
| The expected file and correct contents | The inspected artifact exists and matches requirements |
| Sources support its conclusions | Additional evidence of research quality |
answered therefore does not mean verified_success. The task defines whether files, citations or human review are required. See evaluating actual agent outcomes.
When a loop earns its place
If the steps are always read, extract and format, an ordinary sequence may suffice. Model-directed execution helps when new observations determine the next action. Anthropic’s Building effective agents distinguishes predefined workflows from dynamically directed agents.
Try four cases: immediate answer, search then answer, endless search, and a failed search. Verify how each exits before adding tools or parallel branches.
Continue with graphs and state transitions, then the harness around execution.