Concept
An "agent," stripped of marketing language, is nothing more than a loop: call the model, let it decide whether it needs a tool, execute whatever it asks for, feed the result back, and repeat, until the model decides it has enough information to give a final answer, with the number of iterations not fixed in advance. This distinguishes an agent from a single-turn tool call (covered in the tool-calling topic), where the loop runs at most once. The defining property of an agent is that the model itself controls how many steps the task takes, not your application code.
while (response.stop_reason === 'tool_use') {// execute every requested tool, collect results, loop}
An 'agent' is really just this loop: keep calling the API, executing whatever tools it asks for, and feeding results back, until it stops asking for tools.
The loop, restated for genuinely multi-step tasks
let messages: Anthropic.MessageParam[] = [
{ role: "user", content: "Book me a flight to Tokyo and find a hotel near the airport." },
];
while (true) {
const response = await client.messages.create({
model: "claude-opus-4-8",
max_tokens: 4096,
tools: [searchFlightsTool, checkHotelAvailabilityTool],
messages,
});
if (response.stop_reason !== "tool_use") break; // model decided it's done
messages.push({ role: "assistant", content: response.content });
const toolUseBlocks = response.content.filter(
(b): b is Anthropic.ToolUseBlock => b.type === "tool_use",
);
const results = await Promise.all(
toolUseBlocks.map(async (block) => ({
type: "tool_result" as const,
tool_use_id: block.id,
content: JSON.stringify(await executeTool(block.name, block.input)),
})),
);
messages.push({ role: "user", content: results });
}In a genuinely multi-step task like "book a flight and find a hotel," turn 1 might call search_flights; turn 2, having seen the flight results, calls check_hotel_availability using dates derived from the flight results, a real sequential dependency, which is exactly why this needs a loop rather than one batch of parallel calls. The loop terminates the moment stop_reason stops being "tool_use", a production loop always needs a max_iterations guard, since an unbounded loop paired with a misbehaving tool (or a model stuck oscillating between two tool calls) can otherwise run forever.
Three ways to build this, at increasing levels of managed infrastructure
1. The manual loop (above), you own everything, no framework, no beta dependency. Best when you need control that no helper exposes.
2. The SDK's beta Tool Runner, automates exactly the loop above via client.beta.messages.toolRunner(), with per-turn hooks for approval gating, error interception, and result modification, covered in depth in the tool-calling topic. This is the right default for most custom-tool agents that you're hosting and running yourself.
3. Managed Agents, a fundamentally different tier: Anthropic hosts both the agent loop and a per-session sandboxed container where the agent's tools (bash, file operations, code execution) actually execute. You define a persisted, versioned Agent config once, then start Sessions against it:
// ONE-TIME SETUP, create the agent, store its id, never re-create per request
const agent = await client.beta.agents.create({
name: "Research Agent",
model: "claude-opus-4-8",
system: "You are a research assistant. Cite sources for every claim.",
tools: [{ type: "agent_toolset_20260401" }], // bash, read, write, edit, glob, grep, web_search, web_fetch
});
// EVERY RUN, reference the stored agent id, start a session
const session = await client.beta.sessions.create({
agent: agent.id,
environment_id: environmentId,
});
const stream = client.beta.sessions.events.stream(session.id);
await client.beta.sessions.events.send(session.id, {
events: [{ type: "user.message"
The key architectural distinction that trips people up: agents.create() is a one-time setup step, not something you call per request, an agent is a persisted, versioned resource you create once and reuse across every subsequent session, the same way you'd create a database schema once rather than on every query. Calling agents.create() inside a request handler is a real anti-pattern that silently accumulates orphaned agent objects and pays creation latency on every call for no benefit.
Where a framework like LangChain fits
Frameworks like LangChain provide higher-level abstractions on top of raw API calls, pre-built agent loop implementations, memory management helpers, and a large ecosystem of pre-built tool integrations. They can genuinely save time when you want a common pattern (a RAG-backed agent, a multi-tool research assistant) without hand-assembling every piece, and their tool-integration ecosystem can mean less bespoke glue code for common third-party services. The tradeoff is an added abstraction layer between your code and the raw API, when something misbehaves, understanding why often requires understanding both the framework's internals and the underlying Messages API concepts covered throughout this domain (tool use, streaming, stop reasons). For learning the actual mechanics, the loop, the message shapes, the stop reasons, working directly against the SDK first, as this course does, makes debugging a framework-based agent later far more tractable, because the framework's abstractions map onto a mental model you already have rather than replacing it.