# Trainer Guide — Session 3 (Advanced Level)
## Claude Agent SDK: building agentic systems in production

> **Program:** Applied AI — Yann Isola
> **Level:** Advanced — solutions architects preparing for certification *Claude Certified Architect*
> **Duration:** 2 hours
> **Prerequisites:** Sessions 1–2 (agentic architectures, multi-agent orchestration), intermediate Python, notions of API (Application Programming Interface) LLM (Large Language Model).

---

## 1. Educational objectives

At the end of the session, participants will know:

1. **Describe** the architecture of the Claude Agent SDK (Software Development Kit): `Agent`, `Runner`, tools, handoffs, guardrails, hooks, context variables.
2. **Implement** an agent with typed tools via the `@tool` decorator.
3. **Design** handoffs between agents and justify this choice in the face of the classic tool call.
4. **Install** entry and exit guardrails with waste management.
5. **Orchestrate** multi-agent patterns: coordinator + sub-agents, pipeline, parallel execution.
6. **Manage** errors: tool failure, agent failure, timeout, fallback strategies.

**Certification link:** these six objectives cover the “Agent Design” and “SDK Implementation” areas of the *Claude Certified Architect* ⚠ framework (repository subject to change — check the current version on the official Anthropic website).

---

## 2. Timed course (120 min)

| # | Sequence | Duration | Format |
|---|----------|-------|--------|
| 0 | Home + session 2 reminder | 5 mins | Plenary |
| 1 | Anatomy of the SDK: Agent, Runner, agentic loop | 20 mins | Presentation + live demo |
| 2 | Tools: `@tool`, schematics, docstrings | 15 mins | Live coding |
| 3 | Handoffs: transfer of control between agents | 15 mins | Presentation + demo |
| — | **Pause** | 10 mins | — |
| 4 | Guardrails and hooks | 15 mins | Presentation + live coding |
| 5 | Multi-agent patterns: coordinator, pipeline, parallel | 20 mins | Presentation + interactive page |
| 6 | Error handling + anti-patterns | 10 mins | Presentation + discussion |
| 7 | Launch of exercises (to be completed independently) | 8 mins | Workshops |
| 8 | Summary + anchor quiz | 2 mins | Plenary |

---

## 3. Detailed content

### Sequence 1 — Anatomy of the SDK (20 min)

#### 3.1.1 Why a dedicated SDK?

Starting point: remember that calling an LLM API “by hand” requires rewriting the agentic loop yourself (message sending → tool call detection → execution → returning the result → iteration). The Claude Agent SDK is the official Python framework which **industrializes this loop** and adds the production building blocks: validation, observability, multi-agent delegation.

**Key message to hammer home:** *the SDK is not a magical abstraction — it is the agentic loop of session 1, packaged, tested and equipped.*

#### 3.1.2 The `Agent` class

An agent is defined by four attributes:```python
from claude_agent_sdk import Agent

agent_support = Agent(
    name="support-client",                      # identifiant unique
    model="claude-sonnet-4-5",                  # ⚠ nom de modèle volatile
    instructions=(
        "Tu es un agent de support de la société Acme. "
        "Réponds en français, cite toujours la source interne utilisée. "
        "Si la demande concerne un remboursement, transfère à l'agent facturation."
    ),
    tools=[chercher_kb, creer_ticket],           # liste de fonctions décorées @tool
)
```- `name`: used for routing, logs and handoffs.
- `model`: the target model. ⚠ Model IDs change regularly (versions, snapshots) — always check the documentation.
- `instructions`: the **system prompt**. Insist: this is the agent's behavioral contract. Anything not there is left to interpretation of the model.
- `tools`: the list of abilities. An agent without tools is just a chatbot.

**Question to ask the room:** “Where would you put the “never disclose personal data” rule: in `instructions` or in a guardrail? » — Expected response in sequence 4: *both*; the instructions guide, the guardrail guarantees.

#### 3.1.3 The `Runner`: the loop```python
from claude_agent_sdk import Runner

resultat = Runner.run(
    agent_support,
    "Mon abonnement a été facturé deux fois ce mois-ci.",
)
print(resultat.final_output)
```Draw out on the board what `Runner.run()` actually does:

1. Sends user message + `instructions` + tool drawings to the model.
2. The model responds: either a final text, or one or more **tool calls**.
3. The Runner runs the tools, returns their results to the model.
4. **Loop** until a final response is obtained (or a handoff is triggered, or the ceiling `max_turns` is exceeded).

Diagram to draw (included in the interactive page):```
Utilisateur → [Guardrail entrée] → Agent (modèle)
                                      │
                    ┌─── appel outil ─┤─── handoff ───→ Autre agent
                    ▼                 │
                Exécution outil       ▼
                    │           Réponse finale
                    └── résultat ──→ (boucle)
                                      │
                              [Guardrail sortie] → Utilisateur
```**Certification pitfall:** `Runner.run()` is synchronous; `Runner.run_async()` (asyncio) is required for parallel execution (sequence 5). A typical question asks you to choose the correct variation depending on the scenario.

---

### Sequence 2 — Tools: `@tool` (15 min)

#### 3.2.1 The decorator```python
from claude_agent_sdk import tool

@tool
def chercher_kb(requete: str, max_resultats: int = 5) -> str:
    """Recherche dans la base de connaissances interne d'Acme.

    Args:
        requete: termes de recherche en langage naturel.
        max_resultats: nombre maximal de documents retournés.
    """
    docs = kb_client.search(requete, limit=max_resultats)
    return "\n---\n".join(d.snippet for d in docs)
```Three mechanisms to explain:

1. **The docstring becomes the tool description** sent to the model. This is an artifact of *prompt engineering*, not a comment: it should say *when* to use the tool, not just *what it does*.
2. **Type annotations generate the JSON schema** (JSON — JavaScript Object Notation, data exchange format): `str` → `"type": "string"`, `int` → `"type": "integer"`, default values ​​→ optional parameters. Complex types: use Pydantic or `TypedDict`.
3. **The return value is returned to the model** as is (converted to text). Return structured, concise content — not a 50 KB JSON dump.

#### 3.2.2 Good practices (to be dictated)

- A tool = a responsibility. No `faire_tout(action: str)`.
- Name the parameters from the model perspective (`requete`, not `q`).
- Always bound: `max_resultats`, timeouts, pagination.
- Business errors **return to text** (“No results for…”); technical errors are **raised as exceptions** (managed in sequence 6).

**Quick exercise (3 min):** have this docstring critiqued: `"""Cherche des trucs."""` — wait: no use cases, no description of parameters, no limit.

---

### Sequence 3 — Handoffs (15 min)

#### 3.3.1 Concept

A **handoff** is a **transfer of control**: agent A decides that agent B is in a better position and passes the conversation to him. Fundamental difference with tool call:

| | Tool call | Handoff |
|---|---|---|
| Who keeps the hand? | The Calling Agent | The target agent |
| Back to the first agent? | Yes, automatic | No (except explicit return handoff) |
| Context transmitted | Tool arguments | Chat History |
| Use cases | One-time capacity | Change of specialty |

#### 3.3.2 Implementation

The SDK expresses the handoff via the agent's **`handoffs`** list and, in routing tool signatures, via the **return type annotation** pointing to an agent:```python
from claude_agent_sdk import Agent, handoff

agent_facturation = Agent(
    name="facturation",
    model="claude-sonnet-4-5",  # ⚠ volatile
    instructions="Tu traites remboursements et litiges de facturation. "
                 "Tu as accès à l'historique complet de la conversation.",
    tools=[consulter_factures, initier_remboursement],
)

agent_triage = Agent(
    name="triage",
    model="claude-haiku-4-5",   # ⚠ volatile — modèle léger pour router
    instructions="Analyse la demande et route vers le bon spécialiste. "
                 "Ne tente JAMAIS de résoudre toi-même.",
    handoffs=[handoff(agent_facturation), handoff(agent_support)],
)
```Points to highlight:

- The model “sees” each handoff as a pseudo-tool (`transfer_to_facturation`). This is the model that **decides** to route — hence the importance of the `instructions` of the triage.
- The target agent **inherits the history**: no need to re-ask the customer the questions.
- `handoff()` accepts options: `on_handoff=` (callback), history filter, welcome message.

#### 3.3.3 Anti-pattern: the amnesic subagent

**Bad** (to be projected):```python
# ❌ Le coordinateur délègue sans contexte
Runner.run(agent_redacteur, "Rédige la section 2.")
# → l'agent ne sait ni de quel document il s'agit, ni le ton, ni le plan
```**Good :**```python
# ✅ Contexte complet dans le prompt de délégation
Runner.run(agent_redacteur, f"""
Mission : rédiger la section 2 du rapport « {titre} ».
Plan global : {plan}
Sections déjà rédigées (résumé) : {resume_sections}
Ton : formel, public : direction financière. Longueur : 400–600 mots.
Livrable : Markdown uniquement, sans préambule.
""")
```Rule to note: **a subagent does not share your working memory. Everything it needs to know must be in its prompt or in the context transmitted.** This is the No. 1 source of failure of multi-agent systems in production.

---

### Sequence 4 — Guardrails and hooks (15 min)

#### 3.4.1 Guardrails

Validators executed **before** (input guardrail) or **after** (output guardrail) passing through the model.```python
from claude_agent_sdk import input_guardrail, output_guardrail, GuardrailTripwire

@input_guardrail
def bloquer_donnees_carte(ctx, agent, message: str):
    """Rejette tout message contenant un numéro de carte bancaire."""
    if re.search(r"\b\d{4}[ -]?\d{4}[ -]?\d{4}[ -]?\d{4}\b", message):
        return GuardrailTripwire(
            triggered=True,
            message="Ne transmettez jamais de numéro de carte. "
                    "Utilisez le portail sécurisé.",
        )
    return GuardrailTripwire(triggered=False)

@output_guardrail
def verifier_pas_de_promesse(ctx, agent, sortie: str):
    """Empêche l'agent de promettre un remboursement non validé."""
    verdict = petit_modele_classifieur(sortie)   # LLM léger en juge
    return GuardrailTripwire(triggered=verdict == "promesse_engageante")
```To explain:

- A triggered guardrail (**tripwire**) interrupts the run and throws a dedicated exception (`InputGuardrailTripwireTriggered` / `OutputGuardrailTripwireTriggered`) which the application intercepts.
- The input guardrails can run **in parallel** to the first model call (latency optimization): if the tripwire triggers, the call is canceled.
- A guardrail can itself call an LLM (“LLM-as-judge” pattern) — use a **fast and inexpensive** model to avoid doubling latency.

**Defense in depth** (diagram to draw): instructions (soft) → guardrails (hard) → tool permissions (hard) → audit via hooks (a posteriori).

#### 3.4.2 Hooks

Lifecycle callbacks for observability and control:```python
from claude_agent_sdk import RunHooks

class HooksAudit(RunHooks):
    async def on_tool_start(self, ctx, agent, tool):
        logger.info("agent=%s outil=%s args=%s", agent.name, tool.name, ctx.tool_args)

    async def on_tool_end(self, ctx, agent, tool, result):
        metrics.timing(f"tool.{tool.name}.latency", ctx.elapsed_ms)

    async def on_handoff(self, ctx, source, cible):
        logger.info("handoff %s → %s", source.name, cible.name)

resultat = Runner.run(agent_triage, message, hooks=HooksAudit())
```Main hooks: `on_agent_start`, `on_agent_end`, `on_tool_start`, `on_tool_end`, `on_handoff`. Use cases: audit logs (compliance), metrics (latency, cost), context injection, kill-switch.

**Certification distinction:** guardrail = *blocking control over content*; hook = *life cycle observation/instrumentation*. A hook should not carry core security logic.

#### 3.4.3 Context variables

Typed state shared between agents, tools, guardrails and hooks of the same run — **never sent to the model** (unlike the prompt):```python
from dataclasses import dataclass
from claude_agent_sdk import Agent, Runner, RunContextWrapper

@dataclass
class ContexteClient:
    client_id: str
    tier: str            # "standard" | "premium"
    langue: str

@tool
def consulter_factures(ctx: RunContextWrapper[ContexteClient]) -> str:
    """Liste les factures du client authentifié."""
    return facturation_api.factures(ctx.context.client_id)  # jamais demandé au modèle !

agent = Agent[ContexteClient](name="support", ...)
resultat = Runner.run(agent, message, context=ContexteClient("C-4812", "premium", "fr"))
```**Security message:** the identity of the client comes from the application context (authenticated session), **never** from a parameter that the model fills in — otherwise a prompt injection can read other people's invoices. It's a great exam classic.

---

### Sequence 5 — Multi-agent patterns (20 min)

Project the interactive page (`webpage/index.html`) and unfold the flow simulator.

#### 3.5.1 Coordinator + sub-agents

The coordinator breaks down, delegates, aggregates. In the Claude execution environment, delegation goes through the **`Task`** tool: the coordinator must therefore have it in his authorized tools.```python
options_coordinateur = {
    "allowedTools": ["Read", "Grep", "Task"],   # ⬅ "Task" = droit de déléguer
    "maxTurns": 40,
}
```**Point of review:** a coordinator whose `allowedTools` does not include `"Task"` cannot **not** create subagents — it will attempt to do everything itself, silently. Typical symptom: “my multi-agent architecture only uses one agent”. Cause: missing permission, not model bug.

#### 3.5.2 Pipeline

Sequential chain: agent N output = agent N+1 input.```python
brut     = Runner.run(agent_extracteur, document).final_output
analyse  = Runner.run(agent_analyste,  f"Données extraites :\n{brut}").final_output
rapport  = Runner.run(agent_redacteur, f"Analyse :\n{analyse}\nRédige le rapport.").final_output
```Advantages: each step can be tested in isolation, models sized by step (light extractor, powerful analyst). Disadvantage: cumulative latency, propagated upstream error — hence the interest of an output guardrail **between steps**.

#### 3.5.3 Parallel

Independent tasks → concurrent execution with `asyncio`:```python
import asyncio
from claude_agent_sdk import Runner

async def analyser_dossier(chunks: list[str]):
    taches = [Runner.run_async(agent_analyste, c) for c in chunks]
    resultats = await asyncio.gather(*taches, return_exceptions=True)
    ok      = [r.final_output for r in resultats if not isinstance(r, Exception)]
    echecs  = [r for r in resultats if isinstance(r, Exception)]
    return ok, echecs
```Please note: `return_exceptions=True` — a failure must not cancel the N−1 successes. Then an aggregator agent merges the `ok` and reports the `echecs`.

**Cost/latency arbitration:** the parallel divides the perceived latency but multiplies the tokens consumed simultaneously (be careful of the flow limits — *rate limits* ⚠, variable depending on the account level).

#### 3.5.4 Choice grid (to be copied)

| Need | Pattern |
|---|---|
| Disjoint specialties, routing to entry | Triage + handoffs |
| Dependent stages, progressive transformation | Pipeline |
| Independent subtasks, volume | Parallel + aggregator |
| Dynamic decomposition decided at runtime | Coordinator + `Task` |

---

### Sequence 6 — Error handling (10 min)

Three families:

1. **Tool error.** Exception in tool code. By default the SDK returns the error to the model, which can retry or work around. To control the message: decorate with a try/except and return an actionable text (“The invoice service is unavailable, try again in 30 seconds or inform the user”).
2. **Agent failure.** Infinite loop or drift → bound with `max_turns`; invalid output → output guardrail + a controlled restart, then fallback (degraded response, human escalation).
3. **Timeout.** Always wrap: `asyncio.wait_for(Runner.run_async(...), timeout=120)`. Provide for the idempotence of tools with side effects (a re-attempted reimbursement must not be issued twice → idempotence key).```python
try:
    res = await asyncio.wait_for(Runner.run_async(agent, msg), timeout=120)
except asyncio.TimeoutError:
    res = reponse_degradee("Analyse trop longue, version abrégée fournie.")
except OutputGuardrailTripwireTriggered:
    res = escalade_humaine(msg)
```**Summary sentence:** *in production, the question is not “if” an agent fails, but “what next”. A certifiable architecture defines the behavior of each failure.*

---

### Sequence 7 — Exercises (8 min)

Present the three exercises (`exercises/exercises.md`):
1. Agent with tools (`@tool`, schematics, docstrings) — 45 min estimated.
2. Triage handoffs → specialists — 60 min.
3. Input/output guardrails + audit hooks — 60 min.

Indicative scale and commented solutions included in the exercise document.

---

## 4. Material and logistics

- Python ≥ 3.10, `pip install claude-agent-sdk` ⚠ (package name and version: check the official doc on the day).
- API key per participant (or shared room proxy) — provide a token budget; the session consumes little (short agents).
- Projector + page `webpage/index.html` (works offline).
- Slides: `slides/slides.md` (25+ slides, Marp/reveal format compatible).

## 5. Common participant pitfalls

| Trap | Correction to be made |
|---|---|
| Empty or vague tool docstrings | Remember: the docstring IS the tool prompt |
| Confusion handoff / tool call | Return to the comparison table (who keeps control?) |
| User identity passed as tool parameter | Context variables + injection demonstration |
| Coordinator without `"Task"` in `allowedTools` | Reproduce the symptom, then correct |
| `asyncio.gather` without `return_exceptions=True` | Simulate failure on 1 task out of 5 |
| Context-free delegated subagent | Projecting the Bad vs Good of sequence 3 |

## 6. Likely Questions (Trainer FAQ)

**“What is the difference between guardrail and system instructions? »** Instructions influence the (probabilistic) model; guardrail is deterministic code that blocks. Compliance requires both.

**“Can we do a return handoff? »** Yes — the target agent can list the source agent in its own `handoffs`. Beware of loops: bound with `max_turns` and log via `on_handoff`.

**“Handoff or agent-as-tool? »** Handoff = definitive transfer of the conversation. Agent-as-tool = the coordinator consults an agent and keeps control. If the user must continue to communicate with the specialist → handoff.

**“Are context variables visible from the model? »** No, never serialized in the prompt. This is precisely their interest (secrets, identifiers). Only what the tools *return* reaches the model.

---

*End of the trainer guide — Session 3, advanced level.*