# Guide Formateur — Session 3 (Niveau Avancé)
## Claude Agent SDK : construire des systèmes agentiques en production

> **Programme :** Applied AI — Yann Isola
> **Niveau :** Avancé — architectes solutions préparant la certification *Claude Certified Architect*
> **Durée :** 2 h 00
> **Prérequis :** Sessions 1–2 (architectures agentiques, orchestration multi-agents), Python intermédiaire, notions d'API (Application Programming Interface — interface de programmation applicative) LLM (Large Language Model — grand modèle de langage).

---

## 1. Objectifs pédagogiques

À la fin de la session, les participants sauront :

1. **Décrire** l'architecture du Claude Agent SDK (Software Development Kit — kit de développement logiciel) : `Agent`, `Runner`, outils, handoffs, guardrails, hooks, variables de contexte.
2. **Implémenter** un agent avec outils typés via le décorateur `@tool`.
3. **Concevoir** des handoffs entre agents et justifier ce choix face à l'appel d'outil classique.
4. **Poser** des guardrails d'entrée et de sortie avec gestion des rejets.
5. **Orchestrer** des patterns multi-agents : coordinateur + sous-agents, pipeline, exécution parallèle.
6. **Gérer** les erreurs : échec d'outil, échec d'agent, timeout, stratégies de repli (fallback).

**Lien certification :** ces six objectifs couvrent les domaines « Agent Design » et « SDK Implementation » du référentiel *Claude Certified Architect* ⚠ (référentiel susceptible d'évoluer — vérifier la version en vigueur sur le site officiel Anthropic).

---

## 2. Déroulé minuté (120 min)

| # | Séquence | Durée | Format |
|---|----------|-------|--------|
| 0 | Accueil + rappel session 2 | 5 min | Plénière |
| 1 | Anatomie du SDK : Agent, Runner, boucle agentique | 20 min | Exposé + démo live |
| 2 | Outils : `@tool`, schémas, docstrings | 15 min | Live coding |
| 3 | Handoffs : transfert de contrôle entre agents | 15 min | Exposé + démo |
| — | **Pause** | 10 min | — |
| 4 | Guardrails et hooks | 15 min | Exposé + live coding |
| 5 | Patterns multi-agents : coordinateur, pipeline, parallèle | 20 min | Exposé + page interactive |
| 6 | Gestion d'erreurs + anti-patterns | 10 min | Exposé + discussion |
| 7 | Lancement des exercices (à finir en autonomie) | 8 min | Ateliers |
| 8 | Synthèse + quiz d'ancrage | 2 min | Plénière |

---

## 3. Contenu détaillé

### Séquence 1 — Anatomie du SDK (20 min)

#### 3.1.1 Pourquoi un SDK dédié ?

Point de départ : rappeler qu'appeler une API LLM « à la main » impose de réécrire soi-même la boucle agentique (envoi du message → détection d'appel d'outil → exécution → renvoi du résultat → itération). Le Claude Agent SDK est le framework Python officiel qui **industrialise cette boucle** et y ajoute les briques de production : validation, observabilité, délégation multi-agents.

**Message clé à marteler :** *le SDK n'est pas une abstraction magique — c'est la boucle agentique de la session 1, packagée, testée et outillée.*

#### 3.1.2 La classe `Agent`

Un agent se définit par quatre attributs :

```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` : sert au routage, aux logs et aux handoffs.
- `model` : le modèle cible. ⚠ Les identifiants de modèles changent régulièrement (versions, snapshots) — toujours vérifier la documentation.
- `instructions` : le **system prompt**. Insister : c'est le contrat comportemental de l'agent. Tout ce qui n'y figure pas est laissé à l'interprétation du modèle.
- `tools` : la liste des capacités. Un agent sans outil n'est qu'un chatbot.

**Question à poser à la salle :** « Où mettriez-vous la règle "ne jamais divulguer de données personnelles" : dans `instructions` ou dans un guardrail ? » — Réponse attendue en séquence 4 : *les deux* ; les instructions orientent, le guardrail garantit.

#### 3.1.3 Le `Runner` : la boucle

```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)
```

Dérouler au tableau ce que `Runner.run()` fait réellement :

1. Envoie le message utilisateur + `instructions` + schémas d'outils au modèle.
2. Le modèle répond : soit un texte final, soit un ou plusieurs **appels d'outils**.
3. Le Runner exécute les outils, renvoie leurs résultats au modèle.
4. **Boucle** jusqu'à obtention d'une réponse finale (ou déclenchement d'un handoff, ou dépassement du plafond `max_turns`).

Schéma à dessiner (repris dans la page interactive) :

```
Utilisateur → [Guardrail entrée] → Agent (modèle)
                                      │
                    ┌─── appel outil ─┤─── handoff ───→ Autre agent
                    ▼                 │
                Exécution outil       ▼
                    │           Réponse finale
                    └── résultat ──→ (boucle)
                                      │
                              [Guardrail sortie] → Utilisateur
```

**Piège de certification :** `Runner.run()` est synchrone ; `Runner.run_async()` (asyncio) est requis pour l'exécution parallèle (séquence 5). Une question type demande de choisir la bonne variante selon le scénario.

---

### Séquence 2 — Outils : `@tool` (15 min)

#### 3.2.1 Le décorateur

```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)
```

Trois mécanismes à expliciter :

1. **La docstring devient la description de l'outil** envoyée au modèle. C'est un artefact de *prompt engineering*, pas un commentaire : elle doit dire *quand* utiliser l'outil, pas seulement *ce qu'il fait*.
2. **Les annotations de type génèrent le schéma JSON** (JSON — JavaScript Object Notation, format d'échange de données) : `str` → `"type": "string"`, `int` → `"type": "integer"`, valeurs par défaut → paramètres optionnels. Types complexes : utiliser Pydantic ou `TypedDict`.
3. **La valeur de retour est renvoyée au modèle** telle quelle (convertie en texte). Retourner du contenu structuré et concis — pas un dump JSON de 50 Ko.

#### 3.2.2 Bonnes pratiques (à dicter)

- Un outil = une responsabilité. Pas de `faire_tout(action: str)`.
- Nommer les paramètres du point de vue du modèle (`requete`, pas `q`).
- Toujours borner : `max_resultats`, timeouts, pagination.
- Les erreurs métier se **retournent en texte** (« Aucun résultat pour… ») ; les erreurs techniques se **lèvent en exception** (gérées en séquence 6).

**Exercice éclair (3 min) :** faire critiquer cette docstring : `"""Cherche des trucs."""` — attendre : pas de cas d'usage, pas de description des paramètres, pas de limite.

---

### Séquence 3 — Handoffs (15 min)

#### 3.3.1 Concept

Un **handoff** est un **transfert de contrôle** : l'agent A décide que l'agent B est mieux placé et lui passe la conversation. Différence fondamentale avec l'appel d'outil :

| | Appel d'outil | Handoff |
|---|---|---|
| Qui garde la main ? | L'agent appelant | L'agent cible |
| Retour au premier agent ? | Oui, automatique | Non (sauf handoff retour explicite) |
| Contexte transmis | Arguments de l'outil | Historique de conversation |
| Cas d'usage | Capacité ponctuelle | Changement de spécialité |

#### 3.3.2 Implémentation

Le SDK exprime le handoff via la **liste `handoffs`** de l'agent et, dans les signatures d'outils de routage, via l'**annotation de type de retour** pointant vers un 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 à souligner :

- Le modèle « voit » chaque handoff comme un pseudo-outil (`transfer_to_facturation`). C'est le modèle qui **décide** de router — d'où l'importance des `instructions` du triage.
- L'agent cible **hérite de l'historique** : pas besoin de re-poser les questions au client.
- `handoff()` accepte des options : `on_handoff=` (callback), filtre d'historique, message d'accueil.

#### 3.3.3 Anti-pattern : le sous-agent amnésique

**Mauvais** (à projeter) :

```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
```

**Bon :**

```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.
""")
```

Règle à faire noter : **un sous-agent ne partage pas votre mémoire de travail. Tout ce qu'il doit savoir doit être dans son prompt ou dans le contexte transmis.** C'est la source n° 1 d'échec des systèmes multi-agents en production.

---

### Séquence 4 — Guardrails et hooks (15 min)

#### 3.4.1 Guardrails

Validateurs exécutés **avant** (input guardrail) ou **après** (output guardrail) le passage par le modèle.

```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")
```

À expliciter :

- Un guardrail déclenché (**tripwire**) interrompt le run et lève une exception dédiée (`InputGuardrailTripwireTriggered` / `OutputGuardrailTripwireTriggered`) que l'application intercepte.
- Les input guardrails peuvent tourner **en parallèle** du premier appel modèle (optimisation latence) : si le tripwire se déclenche, l'appel est annulé.
- Un guardrail peut lui-même appeler un LLM (pattern « LLM-as-judge ») — utiliser un modèle **rapide et peu coûteux** pour ne pas doubler la latence.

**Défense en profondeur** (schéma à dessiner) : instructions (souple) → guardrails (dur) → permissions d'outils (dur) → audit via hooks (a posteriori).

#### 3.4.2 Hooks

Callbacks de cycle de vie pour l'observabilité et le contrôle :

```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())
```

Hooks principaux : `on_agent_start`, `on_agent_end`, `on_tool_start`, `on_tool_end`, `on_handoff`. Cas d'usage : logs d'audit (conformité), métriques (latence, coût), injection de contexte, kill-switch.

**Distinction certification :** guardrail = *contrôle bloquant sur le contenu* ; hook = *observation/instrumentation du cycle de vie*. Un hook ne devrait pas porter la logique de sécurité principale.

#### 3.4.3 Variables de contexte

État typé partagé entre agents, outils, guardrails et hooks d'un même run — **jamais envoyé au modèle** (contrairement au 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"))
```

**Message sécurité :** l'identité du client vient du contexte applicatif (session authentifiée), **jamais** d'un paramètre que le modèle remplit — sinon un prompt injection peut lire les factures d'autrui. C'est un grand classique d'examen.

---

### Séquence 5 — Patterns multi-agents (20 min)

Projeter la page interactive (`webpage/index.html`) et dérouler le simulateur de flux.

#### 3.5.1 Coordinateur + sous-agents

Le coordinateur décompose, délègue, agrège. Dans l'environnement d'exécution Claude, la délégation passe par l'outil **`Task`** : le coordinateur doit donc l'avoir dans ses outils autorisés.

```python
options_coordinateur = {
    "allowedTools": ["Read", "Grep", "Task"],   # ⬅ "Task" = droit de déléguer
    "maxTurns": 40,
}
```

**Point d'examen :** un coordinateur dont `allowedTools` n'inclut pas `"Task"` ne peut **pas** créer de sous-agents — il tentera de tout faire lui-même, silencieusement. Symptôme typique : « mon architecture multi-agents n'utilise qu'un agent ». Cause : permission manquante, pas bug du modèle.

#### 3.5.2 Pipeline

Chaîne séquentielle : sortie de l'agent N = entrée de l'agent N+1.

```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
```

Avantages : chaque étape testable isolément, modèles dimensionnés par étape (extracteur léger, analyste puissant). Inconvénient : latence cumulée, erreur amont propagée — d'où l'intérêt d'un guardrail de sortie **entre les étapes**.

#### 3.5.3 Parallèle

Tâches indépendantes → exécution concurrente avec `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
```

À souligner : `return_exceptions=True` — un échec ne doit pas annuler les N−1 succès. Puis un agent agrégateur fusionne les `ok` et signale les `echecs`.

**Arbitrage coût/latence :** le parallèle divise la latence perçue mais multiplie les tokens consommés simultanément (attention aux limites de débit — *rate limits* ⚠, variables selon le palier de compte).

#### 3.5.4 Grille de choix (à faire recopier)

| Besoin | Pattern |
|---|---|
| Spécialités disjointes, routage à l'entrée | Triage + handoffs |
| Étapes dépendantes, transformation progressive | Pipeline |
| Sous-tâches indépendantes, volume | Parallèle + agrégateur |
| Décomposition dynamique décidée à l'exécution | Coordinateur + `Task` |

---

### Séquence 6 — Gestion d'erreurs (10 min)

Trois familles :

1. **Erreur d'outil.** Exception dans le code de l'outil. Par défaut le SDK renvoie l'erreur au modèle, qui peut retenter ou contourner. Pour maîtriser le message : décorer d'un try/except et retourner un texte actionnable (« Le service factures est indisponible, réessaie dans 30 s ou informe l'utilisateur »).
2. **Échec d'agent.** Boucle infinie ou dérive → borner avec `max_turns` ; sortie invalide → output guardrail + une relance contrôlée, puis fallback (réponse dégradée, escalade humaine).
3. **Timeout.** Toujours envelopper : `asyncio.wait_for(Runner.run_async(...), timeout=120)`. Prévoir l'idempotence des outils à effet de bord (un remboursement retenté ne doit pas partir deux fois → clé d'idempotence).

```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)
```

**Phrase de synthèse :** *en production, la question n'est pas « si » un agent échoue, mais « quoi ensuite ». Une architecture certifiable définit le comportement de chaque échec.*

---

### Séquence 7 — Exercices (8 min)

Présenter les trois exercices (`exercises/exercises.md`) :
1. Agent avec outils (`@tool`, schémas, docstrings) — 45 min estimées.
2. Handoffs triage → spécialistes — 60 min.
3. Guardrails entrée/sortie + hooks d'audit — 60 min.

Barème indicatif et solutions commentées inclus dans le document exercices.

---

## 4. Matériel et logistique

- Python ≥ 3.10, `pip install claude-agent-sdk` ⚠ (nom de paquet et version : vérifier la doc officielle au jour J).
- Clé API par participant (ou proxy mutualisé de la salle) — prévoir un budget tokens ; la session consomme peu (agents courts).
- Projecteur + la page `webpage/index.html` (fonctionne hors-ligne).
- Slides : `slides/slides.md` (25+ slides, format Marp/reveal compatible).

## 5. Pièges fréquents des participants

| Piège | Correction à apporter |
|---|---|
| Docstrings d'outils vides ou vagues | Rappeler : la docstring EST le prompt de l'outil |
| Confusion handoff / appel d'outil | Revenir au tableau comparatif (qui garde la main ?) |
| Identité utilisateur passée en paramètre d'outil | Variables de contexte + démonstration d'injection |
| Coordinateur sans `"Task"` dans `allowedTools` | Faire reproduire le symptôme, puis corriger |
| `asyncio.gather` sans `return_exceptions=True` | Simuler un échec sur 1 tâche parmi 5 |
| Sous-agent délégué sans contexte | Projeter le Bad vs Good de la séquence 3 |

## 6. Questions probables (FAQ formateur)

**« Quelle différence entre guardrail et instructions système ? »** Les instructions influencent le modèle (probabiliste) ; le guardrail est du code déterministe qui bloque. La conformité exige les deux.

**« Peut-on faire un handoff retour ? »** Oui — l'agent cible peut lister l'agent source dans ses propres `handoffs`. Attention aux boucles : borner avec `max_turns` et journaliser via `on_handoff`.

**« Handoff ou agent-as-tool ? »** Handoff = transfert définitif de la conversation. Agent-as-tool = le coordinateur consulte un agent et garde la main. Si l'utilisateur doit continuer à dialoguer avec le spécialiste → handoff.

**« Les variables de contexte sont-elles visibles du modèle ? »** Non, jamais sérialisées dans le prompt. C'est précisément leur intérêt (secrets, identifiants). Seul ce que les outils *retournent* atteint le modèle.

---

*Fin du guide formateur — Session 3, niveau avancé.*
