Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export default {
text: '6. Building Tool Chains and Complex Workflows',
link: '/chapters/06-tool-chains',
},
{
text: '7. Advanced Agent Patterns',
link: '/chapters/07-advanced-patterns',
},
],
},
],
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Published with VitePress at https://yagop.github.io/coding-agents-tutorial/. Eac
4. [Context and Conversation Management](chapters/04-context.md)
5. [Implementing Tools and Function Calling](chapters/05-tools.md)
6. [Building Tool Chains and Complex Workflows](chapters/06-tool-chains.md)
7. [Advanced Agent Patterns](chapters/07-advanced-patterns.md)

More chapters are tracked as issues and land here as they are written.

Expand Down
69 changes: 69 additions & 0 deletions chapters/07-advanced-patterns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Advanced Agent Patterns

🧠 Chapters 5 and 6 gave you a loop that calls tools until the model is done. That loop is the skeleton; this chapter adds the muscles - a step budget so it can never run away, extended thinking so it reasons before it acts, self-critique so it catches its own mistakes, structured output you can parse, error recovery, and a coordinator that hands work to helper agents. Each pattern is a small, independent addition to the loop you already know.

You already have the tool round-trip and the agent loop from Chapters 5 and 6, so here you only add patterns on top. As always the key comes from the environment - never hardcode it - and Bun auto-loads `.env`.

## The autonomous loop

An autonomous agent is the loop with a leash. You accumulate `messages`, branch on `stop_reason`, and - the part beginners skip - cap the number of steps so a confused model can never spin forever. Track `usage` as you go, so you can see what each run costs.

<<< @/examples/07-advanced-patterns/autonomous-loop.ts

Run it and watch the step count and running token total print each iteration:

```sh
bun run examples/07-advanced-patterns/autonomous-loop.ts
```

The loop exits two ways: cleanly on `end_turn`, or defensively when `maxSteps` is hit - never trusting the model to stop on its own.

::: details Going deeper: token guardrails
That running `totalTokens` is the hook for a hard budget. Read `usage.input_tokens` and `usage.output_tokens` after each step and `break` once a threshold is crossed - a runaway agent costs money one step at a time, and inside the loop is the only place to catch it.
:::

## Thinking and reflecting

Sometimes you want the model to reason before it answers. Extended thinking turns that reasoning into its own `thinking` content block you can read, separate from the final text - and those thinking tokens count toward `usage`.

<<< @/examples/07-advanced-patterns/extended-thinking.ts

You enable it with `thinking: { type: 'adaptive' }`, then narrow on `block.type === 'thinking'` to read `block.thinking`.

::: info Older models
`adaptive` is the current shape. On models 4.6 and earlier, use `thinking: { type: 'enabled', budget_tokens: N }` instead (at least 1024, and below `max_tokens`).
:::

A cheaper kind of reasoning is to let the model grade its own work. After it writes a draft, you ask it to critique and revise - but the critique must go back as a plain `user` turn, never as a fabricated `tool_result`.

<<< @/examples/07-advanced-patterns/reflection.ts

The draft is appended verbatim as the assistant turn, and the critique request is an ordinary `user` message - so the model treats it as feedback to act on, not as a tool's output.

## Structured output and recovering from errors

When you need machine-readable output instead of prose, give the model one tool and force it with `tool_choice` - it must call the tool, so its `input` becomes your typed result.

<<< @/examples/07-advanced-patterns/structured-output.ts

The `enum` and `required` fields pin the shape; you read `block.input` as your own type and skip parsing free text entirely.

Real agents also hit failures - a rate limit, a tool that throws. Retry transient API errors with backoff, and turn tool failures into `tool_result` blocks with `is_error: true` so the model can adapt instead of crashing.

<<< @/examples/07-advanced-patterns/error-recovery.ts

`Anthropic.RateLimitError` gets exponential backoff; a thrown tool becomes an error result the model reads and works around - here it reports the missing file instead of stalling.

## Orchestrating subagents

For bigger jobs, one agent becomes a coordinator: it holds the high-level tools, and each tool call is dispatched to a helper that runs its own `client.messages.create` with a narrow job and its own context.

<<< @/examples/07-advanced-patterns/orchestrator.ts

The coordinator never sees the subagent's internal turns - only its final result comes back as a `tool_result`, which keeps each context small and focused.

::: details Going deeper: a planning tool
A common first move for a coordinator is a dedicated `make_plan` tool that returns an explicit task list. The loop then iterates those subtasks on following turns, dispatching each to a helper - so planning and doing become separate, inspectable steps.
:::

What's next: Chapter 8 - Production and Deployment.
46 changes: 46 additions & 0 deletions examples/07-advanced-patterns/autonomous-loop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// bun run examples/07-advanced-patterns/autonomous-loop.ts

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();
const model = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? 'claude-sonnet-4-6';

const tools: Anthropic.Tool[] = [
{
name: 'word_length',
description: 'Return the number of letters in a single word.',
input_schema: {
type: 'object',
properties: { word: { type: 'string', description: 'One word' } },
required: ['word'],
},
},
];

const messages: Anthropic.MessageParam[] = [
{ role: 'user', content: 'Use the tool to compare the lengths of "agent" and "orchestration", then say which is longer.' },
];

const maxSteps = 6;
let totalTokens = 0;

for (let step = 0; step < maxSteps; step++) {
const response = await client.messages.create({ model, max_tokens: 1024, tools, messages });
totalTokens += response.usage.input_tokens + response.usage.output_tokens;
messages.push({ role: 'assistant', content: response.content });
console.log(`step ${step + 1}: stop_reason=${response.stop_reason} total_tokens=${totalTokens}`);

if (response.stop_reason !== 'tool_use') {
const text = response.content.find((b) => b.type === 'text');
console.log(text?.text ?? '');
break;
}

const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type !== 'tool_use') continue;
const { word } = block.input as { word: string };
results.push({ type: 'tool_result', tool_use_id: block.id, content: String(word.length) });
}
messages.push({ role: 'user', content: results });
}
66 changes: 66 additions & 0 deletions examples/07-advanced-patterns/error-recovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// bun run examples/07-advanced-patterns/error-recovery.ts

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();
const model = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? 'claude-sonnet-4-6';

const tools: Anthropic.Tool[] = [
{
name: 'read_file',
description: 'Read a text file by name.',
input_schema: {
type: 'object',
properties: { path: { type: 'string', description: 'File name' } },
required: ['path'],
},
},
];

function readFile(path: string): string {
if (path !== 'notes.txt') throw new Error(`no such file: ${path}`);
return 'remember to water the plants';
}

async function createWithBackoff(messages: Anthropic.MessageParam[]): Promise<Anthropic.Message> {
for (let attempt = 0; attempt < 3; attempt++) {
try {
return await client.messages.create({ model, max_tokens: 1024, tools, messages });
} catch (error) {
if (error instanceof Anthropic.RateLimitError && attempt < 2) {
await new Promise((resolve) => setTimeout(resolve, 2 ** attempt * 1000));
continue;
}
throw error;
}
}
throw new Error('unreachable');
}

const messages: Anthropic.MessageParam[] = [
{ role: 'user', content: 'Read notes.txt and config.txt, then tell me what each says or whether it is missing.' },
];

while (true) {
const response = await createWithBackoff(messages);
messages.push({ role: 'assistant', content: response.content });

if (response.stop_reason !== 'tool_use') {
const text = response.content.find((b) => b.type === 'text');
console.log(text?.text ?? '');
break;
}

const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type !== 'tool_use') continue;
const { path } = block.input as { path: string };
try {
results.push({ type: 'tool_result', tool_use_id: block.id, content: readFile(path) });
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
results.push({ type: 'tool_result', tool_use_id: block.id, content: reason, is_error: true });
}
}
messages.push({ role: 'user', content: results });
}
26 changes: 26 additions & 0 deletions examples/07-advanced-patterns/extended-thinking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// bun run examples/07-advanced-patterns/extended-thinking.ts

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();
const model = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? 'claude-sonnet-4-6';

const response = await client.messages.create({
model,
max_tokens: 2048,
thinking: { type: 'adaptive' },
messages: [
{ role: 'user', content: 'A bat and a ball cost 1.10 in total. The bat costs 1.00 more than the ball. How much is the ball?' },
],
});

console.log('stop_reason:', response.stop_reason);

for (const block of response.content) {
if (block.type === 'thinking') {
console.log('\n[thinking]\n' + block.thinking);
}
if (block.type === 'text') {
console.log('\n[answer]\n' + block.text);
}
}
52 changes: 52 additions & 0 deletions examples/07-advanced-patterns/orchestrator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// bun run examples/07-advanced-patterns/orchestrator.ts

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();
const model = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? 'claude-sonnet-4-6';

async function summarize(text: string): Promise<string> {
const response = await client.messages.create({
model,
max_tokens: 256,
system: 'You summarize text in one short sentence.',
messages: [{ role: 'user', content: text }],
});
const block = response.content.find((b) => b.type === 'text');
return block?.text ?? '';
}

const tools: Anthropic.Tool[] = [
{
name: 'summarize_text',
description: 'Summarize a block of text into one sentence.',
input_schema: {
type: 'object',
properties: { text: { type: 'string', description: 'Text to summarize' } },
required: ['text'],
},
},
];

const messages: Anthropic.MessageParam[] = [
{ role: 'user', content: 'Summarize this for me: "The agent loop calls the model, runs any tools it asks for, and repeats until the model is done."' },
];

while (true) {
const response = await client.messages.create({ model, max_tokens: 1024, tools, messages });
messages.push({ role: 'assistant', content: response.content });

if (response.stop_reason !== 'tool_use') {
const text = response.content.find((b) => b.type === 'text');
console.log(text?.text ?? '');
break;
}

const results: Anthropic.ToolResultBlockParam[] = [];
for (const block of response.content) {
if (block.type !== 'tool_use') continue;
const { text } = block.input as { text: string };
results.push({ type: 'tool_result', tool_use_id: block.id, content: await summarize(text) });
}
messages.push({ role: 'user', content: results });
}
24 changes: 24 additions & 0 deletions examples/07-advanced-patterns/reflection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// bun run examples/07-advanced-patterns/reflection.ts

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();
const model = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? 'claude-sonnet-4-6';

const messages: Anthropic.MessageParam[] = [
{ role: 'user', content: 'Write a one-sentence tagline for a tool that turns shell commands into plain English.' },
];

const draft = await client.messages.create({ model, max_tokens: 512, messages });
const draftText = draft.content.find((b) => b.type === 'text');
console.log('draft:', draftText?.text ?? '');

messages.push({ role: 'assistant', content: draft.content });
messages.push({
role: 'user',
content: 'Critique your tagline for clarity and length, then write one improved version.',
});

const revised = await client.messages.create({ model, max_tokens: 512, messages });
const revisedText = revised.content.find((b) => b.type === 'text');
console.log('\nrevised:', revisedText?.text ?? '');
38 changes: 38 additions & 0 deletions examples/07-advanced-patterns/structured-output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// bun run examples/07-advanced-patterns/structured-output.ts

import Anthropic from '@anthropic-ai/sdk';

const client = new Anthropic();
const model = process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? 'claude-sonnet-4-6';

type Sentiment = { label: 'positive' | 'negative' | 'neutral'; confidence: number };

const emitResult: Anthropic.Tool = {
name: 'emit_result',
description: 'Return the sentiment classification as structured data.',
input_schema: {
type: 'object',
properties: {
label: { type: 'string', enum: ['positive', 'negative', 'neutral'] },
confidence: { type: 'number', description: 'A value from 0 to 1' },
},
required: ['label', 'confidence'],
},
};

const response = await client.messages.create({
model,
max_tokens: 512,
tools: [emitResult],
tool_choice: { type: 'tool', name: 'emit_result' },
messages: [
{ role: 'user', content: 'Classify the sentiment of: "This refactor saved me hours, fantastic work."' },
],
});

const block = response.content.find((b) => b.type === 'tool_use');
if (block && block.type === 'tool_use') {
const result = block.input as Sentiment;
console.log('label:', result.label);
console.log('confidence:', result.confidence);
}