Le Claude Agent SDK : construire ton propre agent
Quand Claude Code en interactif ne suffit plus et que tu veux mettre un agent dans ton pipeline, ton CI ou ton API interne.
Temps de lecture : 18 min | Niveau : Avancé
Ce que tu sauras faire après
- Choisir entre le SDK, la CLI, le Client SDK et les agents hébergés.
- Installer le SDK et faire tourner un agent minimal en Python ou TypeScript.
- Lire le flux de messages et savoir si la tâche a réussi, coûté trop cher ou tourné trop longtemps.
- Écrire un outil personnalisé qui interroge ton entrepôt de données en lecture seule.
- Poser des garde-fous : outils autorisés, mode de permission, plafond de tours et de budget.
1. À quoi sert le SDK, et quand il ne sert à rien
Le Claude Agent SDK te permet d'embarquer la boucle d'agent de Claude Code dans ton propre programme. La documentation le formule ainsi :
« An agent is an application that completes a task by planning its own steps and calling tools that read files, run commands, or edit code. The Agent SDK gives you the same tools, agent loop, and context management that power Claude Code, programmable in Python and TypeScript. »
Il existe quatre façons de construire avec Claude. La documentation officielle les compare :
| Tu veux… | Utilise | Ce que tu obtiens |
|---|---|---|
| Embarquer l'agent de Claude Code dans ton application Python ou TypeScript, dans un process que tu exploites | Agent SDK | Une bibliothèque qui lance le binaire Claude Code, avec ses outils intégrés, ses permissions, ses sessions et ses hooks |
| Travailler en interactif ou lancer des tâches ponctuelles depuis un terminal | CLI Claude Code | L'interface terminal, pensée pour l'usage quotidien |
| Appeler l'API Claude directement depuis ton code | Client SDK | Accès direct à l'API. Tu écris la boucle d'outils toi-même |
| Faire héberger l'agent par Anthropic, configuré via l'API | Managed Agents | Un harnais hébergé qui exécute la boucle |
La question à te poser
« Est-ce que j'ai besoin de la boucle et des outils fichiers/bash, dans un programme à moi ? »
- Oui → Agent SDK.
- Non, je veux juste une réponse structurée d'un modèle → Client SDK, ou même un simple appel API. Beaucoup moins de complexité.
- Non, je travaille à la main dans mon terminal → reste sur la CLI.
Et si tu n'écris ni Python ni TypeScript
La documentation donne la solution officielle : lancer la CLI comme un sous-processus avec l'option -p et --output-format json.
claude -p "Audite models/marts/mart_ca_mensuel.sql" --output-format jsonC'est parfaitement viable depuis du Go, du Java, du Scala ou un opérateur Airflow qui appelle un shell.
2. Installation
Prérequis documentés : Node.js 18+ ou Python 3.10+, et un compte Anthropic.
Python
# Avec uv (le gestionnaire recommande par la doc)
uv init
uv add claude-agent-sdk# Avec pip, sous Windows
py -m venv .venv
.venv\Scripts\Activate.ps1
pip install claude-agent-sdkSi PowerShell bloque
Activate.ps1avec une erreur de politique d'exécution, la documentation indique de lancer d'abordSet-ExecutionPolicy -Scope Process RemoteSigned.
TypeScript
npm init -y
npm pkg set type=module
npm install @anthropic-ai/claude-agent-sdk
npm install --save-dev tsxLa clé d'API
# macOS / Linux
export ANTHROPIC_API_KEY=ta-cle-api# Windows PowerShell
$env:ANTHROPIC_API_KEY = "ta-cle-api"⚠️ Piège numéro un. La documentation est explicite : « The SDK reads the key from the environment of the process that runs your agent; it doesn't load
.envfiles automatically. » Si tu gardes ta clé dans un.env, charge-le toi-même, par exemple avecdotenv, avant d'appeler le SDK.
Le SDK supporte aussi Amazon Bedrock (CLAUDE_CODE_USE_BEDROCK=1), Google Cloud's Agent Platform (CLAUDE_CODE_USE_VERTEX=1) et Microsoft Foundry (CLAUDE_CODE_USE_FOUNDRY=1).
3. L'agent minimal
Trois éléments seulement : query, prompt, options.
Python
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ResultMessage
async def main():
# query() est le point d'entree : il cree la boucle d'agent
# et renvoie un iterateur asynchrone de messages.
async for message in query(
prompt="Relis utils.py et corrige les bugs qui provoqueraient un crash.",
options=ClaudeAgentOptions(
allowed_tools=["Read", "Edit", "Glob"], # ces outils sont pre-approuves
permission_mode="acceptEdits", # les modifications de fichier passent sans demander
),
):
if isinstance(message, AssistantMessage):
for block in message.content:
if hasattr(block, "text"):
print(block.text) # le raisonnement de Claude
elif hasattr(block, "name"):
print(f"Outil : {block.name}") # l'outil appele
elif isinstance(message, ResultMessage):
print(f"Termine : {message.subtype}")
asyncio.run(main())TypeScript
import { query } from "@anthropic-ai/claude-agent-sdk";
// La boucle d'agent : chaque message est emis au fur et a mesure.
for await (const message of query({
prompt: "Relis utils.py et corrige les bugs qui provoqueraient un crash.",
options: {
allowedTools: ["Read", "Edit", "Glob"], // ces outils sont pre-approuves
permissionMode: "acceptEdits" // les modifications passent sans demander
}
})) {
if (message.type === "assistant" && message.message?.content) {
for (const block of message.message.content) {
if ("text" in block) {
console.log(block.text);
} else if ("name" in block) {
console.log(`Outil : ${block.name}`);
}
}
} else if (message.type === "result") {
console.log(`Termine : ${message.subtype}`);
}
}Lancement : uv run agent.py (ou python agent.py) en Python, npx tsx agent.ts en TypeScript.
La différence de nommage à retenir
| Concept | Python | TypeScript |
|---|---|---|
| Outils pré-approuvés | allowed_tools | allowedTools |
| Mode de permission | permission_mode | permissionMode |
| Plafond de tours | max_turns | maxTurns |
| Plafond de budget | max_budget_usd | maxBudgetUsd |
| Prompt système | system_prompt | systemPrompt |
| Serveurs MCP | mcp_servers | mcpServers |
Python utilise le snake_case, TypeScript le camelCase. Une exception documentée : dans AgentDefinition côté Python, les noms multi-mots comme disallowedTools et mcpServers gardent le camelCase pour coller au format de transport.
4. Lire le flux de messages
Le SDK émet cinq types de messages. Les connaître t'évite de coder à l'aveugle.
| Type | Quand | À quoi ça te sert |
|---|---|---|
SystemMessage | Événements de cycle de vie. Sous-type "init" au démarrage, "compact_boundary" après compaction | Récupérer l'identifiant de session, savoir qu'une compaction a eu lieu |
AssistantMessage | Un par bloc de contenu produit par Claude (texte ou appel d'outil) | Afficher la progression |
UserMessage | Après chaque exécution d'outil, avec le résultat renvoyé à Claude | Journaliser ce que les outils ont retourné |
StreamEvent | Seulement si les messages partiels sont activés | Affichage en direct, caractère par caractère |
ResultMessage | Fin de la boucle | Le message qui compte : succès ou échec, coût, tokens, identifiant de session |
Les sous-types de résultat
C'est la table à garder sous la main. Le champ result n'existe que sur success.
| Sous-type | Ce qui s'est passé | result disponible ? |
|---|---|---|
success | Tâche terminée normalement | Oui |
error_max_turns | Plafond de tours atteint | Non |
error_max_budget_usd | Plafond de budget atteint | Non |
error_during_execution | Une erreur a interrompu la boucle | Non |
error_max_structured_output_retries | Aucune sortie structurée valide après les tentatives | Non |
⚠️ Piège numéro deux. Un
query()en un coup lève une exception après avoir émis un résultat d'erreur. La documentation précise que ce comportement est volontaire. Entoure toujours ta boucle d'untry.
Autre point documenté : quelques événements système peuvent arriver après le ResultMessage. Parcours le flux jusqu'au bout plutôt que de sortir de la boucle dès le résultat.
5. Les garde-fous : la partie qu'on oublie
Les outils
options = ClaudeAgentOptions(
allowed_tools=["Read", "Grep", "Glob"], # analyse en lecture seule
)Combinaisons documentées :
| Outils | Ce que l'agent peut faire |
|---|---|
Read, Glob, Grep | Analyse en lecture seule |
Read, Edit, Glob | Analyser et modifier du code |
Read, Edit, Bash, Glob, Grep | Automatisation complète |
Attention à la nuance documentée entre disponibilité et permission :
- L'option
toolscontrôle ce qui apparaît dans le contexte de Claude. allowedToolscontrôle ce qui s'exécute sans demander.- Un outil non listé dans
allowedToolsreste disponible : son appel passe par le flux de permission.
Pour retirer complètement un outil intégré, la documentation donne deux moyens équivalents : l'omettre de tools, ou lister son nom nu dans disallowedTools.
Le mode de permission
| Mode | Comportement | Quand l'utiliser |
|---|---|---|
"default" | Les appels qui demandent une approbation passent par ton callback canUseTool ; sans callback, c'est refusé | Application interactive |
"acceptEdits" | Approuve automatiquement les modifications de fichier et les commandes de système de fichiers courantes (mkdir, touch, mv, cp) ; les autres commandes Bash suivent les règles par défaut | Prototypage, répertoire isolé |
"plan" | Claude explore et planifie sans modifier tes sources | Revue de code, tu veux valider avant |
"dontAsk" | Ne demande jamais. Ce qui est pré-approuvé tourne, tout le reste est refusé | Agent sans interface, surface d'outils figée |
"auto" | Un classificateur modèle approuve ou refuse | Agent autonome avec garde-fous |
"bypassPermissions" | Tout passe. En TypeScript, exige aussi allowDangerouslySkipPermissions: true | Uniquement en CI ou conteneur isolé |
La documentation est nette sur
bypassPermissions: « Use only in isolated environments where the agent's actions can't affect systems you care about ». Et c'est interdit en root sous Unix.
Les plafonds
Deux options qui t'évitent une mauvaise surprise sur la facture :
| Option | Ce qu'elle contrôle | Défaut |
|---|---|---|
max_turns / maxTurns | Nombre maximum d'allers-retours avec outils | Aucune limite |
max_budget_usd / maxBudgetUsd | Dépense maximale avant arrêt | Aucune limite |
Le plafond de budget couvre aussi les sous-agents : leurs dépenses comptent dans le total. Une fois le plafond atteint, lancer un nouveau sous-agent échoue avec Budget limit reached.
« Setting a budget is a good default for production agents. » Suis ce conseil.
Deux plafonds de plus se règlent par variables d'environnement, passées au SDK via l'option env :
| Variable | Ce qu'elle limite | Défaut |
|---|---|---|
CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH | Profondeur d'imbrication des sous-agents (1 = tes sous-agents n'en lancent aucun) | 3 |
CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS | Sous-agents lancés en même temps | 20 |
⚠️ Les deux SDK ne traitent pas
envpareil. En TypeScript, il remplace l'environnement du sous-processus : étale...process.envdedans pour garderPATH. En Python, il est fusionné avec l'environnement hérité.
Le niveau d'effort
L'option effort règle la profondeur de raisonnement : "low", "medium", "high", "xhigh", "max". Pour un agent qui ne fait que lister des fichiers, "low" réduit le coût et la latence. Tous les modèles ne supportent pas ce paramètre.
6. Un outil personnalisé : interroger ton entrepôt en lecture seule
C'est le vrai intérêt du SDK pour un data engineer : donner à Claude un accès contrôlé à ta base.
Le mécanisme documenté tient en trois gestes :
- Tu définis une fonction avec le décorateur
@tool(Python) ou l'assistanttool()(TypeScript). - Tu l'emballes dans un serveur MCP en-process avec
create_sdk_mcp_server/createSdkMcpServer. - Tu passes ce serveur à
queryvia l'optionmcpServers.
Un outil se définit par quatre parties : nom, description, schéma d'entrée, handler.
⚠️ Le schéma d'entrée en Python. Le dict simple (
{"sql": str}) est pratique, mais la documentation prévient qu'il rend toutes les clés obligatoires et ne gère ni les énumérations ni les plages de valeurs. Pour un paramètre optionnel : laisse-le hors du schéma, décris-le dans le texte de la description, et lis-le avecargs.get("limite", 50). Pour une énumération, passe un vrai JSON Schema à la place du dict.
Le code, commenté ligne par ligne
import asyncio
from typing import Any
import psycopg
from claude_agent_sdk import (
query,
tool,
create_sdk_mcp_server,
ClaudeAgentOptions,
ResultMessage,
)
DSN = "postgresql://lecteur_seul@entrepot:5432/analytics"
# 1) DEFINITION DE L'OUTIL
# Les 3 premiers arguments : nom, description, schema d'entree.
# La description est lue par Claude pour decider QUAND appeler l'outil :
# ecris-la avec soin, elle vaut autant que le code.
@tool(
"executer_select",
"Execute une requete SELECT en lecture seule sur l'entrepot analytics "
"et renvoie au maximum 50 lignes. Refuse toute requete qui n'est pas un SELECT.",
{"sql": str}, # schema simple : un dict nom -> type, converti en JSON Schema par le SDK
)
async def executer_select(args: dict[str, Any]) -> dict[str, Any]:
sql = args["sql"].strip()
# 2) GARDE-FOU COTE CODE, pas cote prompt.
# Un prompt peut etre contourne ; ce test, non.
if not sql.lower().startswith("select"):
return {
"content": [
{"type": "text", "text": "Refuse : seules les requetes SELECT sont autorisees."}
],
"is_error": True, # marque l'appel comme un echec pour que Claude reagisse
}
try:
with psycopg.connect(DSN) as conn:
with conn.cursor() as cur:
cur.execute(sql)
colonnes = [d[0] for d in cur.description]
lignes = cur.fetchmany(50) # plafond dur : protege ton contexte
except Exception as e:
# 3) ON COMPOSE LE MESSAGE D'ERREUR QUE CLAUDE VA LIRE.
# Une exception non rattrapee arriverait a Claude sans aucun contexte.
return {
"content": [{"type": "text", "text": f"Erreur SQL : {e}"}],
"is_error": True,
}
entete = " | ".join(colonnes)
corps = "\n".join(" | ".join(str(v) for v in l) for l in lignes)
return {"content": [{"type": "text", "text": f"{entete}\n{corps}\n({len(lignes)} lignes)"}]}
# 4) ON EMBALLE L'OUTIL DANS UN SERVEUR MCP EN-PROCESS.
# "en-process" = il tourne dans ton programme, pas dans un process separe.
serveur_entrepot = create_sdk_mcp_server(
name="entrepot",
version="1.0.0",
tools=[executer_select],
)
async def main():
options = ClaudeAgentOptions(
mcp_servers={"entrepot": serveur_entrepot},
# 5) NOM COMPLET DE L'OUTIL : mcp__{nom_du_serveur}__{nom_de_l_outil}
# La cle de mcp_servers ("entrepot") devient le segment du milieu.
allowed_tools=["mcp__entrepot__executer_select"],
system_prompt=(
"Tu es analyste de donnees. Tu reponds en citant la requete SQL "
"que tu as executee, puis le resultat, puis ton interpretation. "
"Tu ne devines jamais un chiffre : tu l'obtiens par une requete."
),
max_turns=12, # l'agent ne peut pas boucler indefiniment
max_budget_usd=0.50, # plafond de depense pour cette execution
)
try:
async for message in query(
prompt="Quel est le chiffre d'affaires par mois sur les 6 derniers mois ?",
options=options,
):
if isinstance(message, ResultMessage):
if message.subtype == "success":
print(message.result)
else:
print(f"Arret : {message.subtype}")
if message.total_cost_usd is not None:
print(f"Cout : ${message.total_cost_usd:.4f}")
except Exception as e:
# Un query() en un coup leve apres un resultat d'erreur : c'est voulu.
print(f"Session terminee en erreur : {e}")
asyncio.run(main())Les quatre décisions de conception à retenir
| Décision | Pourquoi |
|---|---|
| Le filtre « SELECT uniquement » est dans le code, pas dans le prompt | Un garde-fou en dur ne se contourne pas par une formulation habile |
fetchmany(50) | Les grosses sorties d'outils consomment énormément de contexte |
is_error: True avec un message composé | Claude lit ton message au lieu d'une exception brute et peut réagir |
max_turns et max_budget_usd | Deux filets différents : l'un contre la boucle infinie, l'autre contre la facture |
À adapter chez toi. Le squelette est celui de la documentation officielle :
@tool,create_sdk_mcp_server, le format de retourcontent/is_error, et le nom completmcp__entrepot__executer_select. En revanche, le client PostgreSQL utilisé dedans (psycopg) et la chaîne de connexion sont un choix d'illustration : remplace-les par le connecteur de ton entrepôt.
L'annotation qui accélère
Si ton outil ne modifie rien, déclare-le. La documentation indique que readOnlyHint contrôle si l'outil peut être appelé en parallèle d'autres outils en lecture seule.
from claude_agent_sdk import tool, ToolAnnotations
@tool(
"executer_select",
"Execute une requete SELECT en lecture seule.",
{"sql": str},
annotations=ToolAnnotations(readOnlyHint=True),
)
async def executer_select(args):
...Attention à la mise en garde de la documentation : « Annotations are metadata, not enforcement. A tool marked readOnlyHint: true can still write to disk if that's what the handler does. » Garde l'annotation honnête.
7. Template de démarrage
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
async def main():
options = ClaudeAgentOptions(
allowed_tools=["[OUTIL_1]", "[OUTIL_2]"], # ex. "Read", "Grep", "Glob"
permission_mode="[default | acceptEdits | plan | dontAsk | auto]",
system_prompt="[ROLE_DE_L_AGENT + CE_QU_IL_NE_DOIT_JAMAIS_FAIRE + FORMAT_DE_SORTIE]",
setting_sources=["project"], # charge CLAUDE.md, skills et hooks du projet
max_turns=[NOMBRE],
max_budget_usd=[MONTANT],
effort="[low | medium | high]",
)
try:
async for message in query(prompt="[TON_OBJECTIF]", options=options):
if isinstance(message, ResultMessage):
if message.subtype == "success":
print(message.result)
else:
print(f"Arret : {message.subtype}")
except Exception as e:
print(f"Erreur : {e}")
asyncio.run(main())8. Ce qu'il faut savoir avant de se lancer
Le contexte s'accumule
La documentation le dit clairement : la fenêtre de contexte « does not reset between turns within a session. Everything accumulates ». Quand elle approche de sa limite, le SDK compacte automatiquement : il résume l'historique ancien.
Conséquence pratique et souvent douloureuse :
« Compaction replaces older messages with a summary, so specific instructions from early in the conversation may not be preserved. Persistent rules belong in CLAUDE.md [...] rather than in the initial prompt, because CLAUDE.md content is re-injected on every request. »
Traduction : tes règles importantes vont dans CLAUDE.md, pas dans le prompt initial.
Les sous-agents sont ton meilleur outil d'économie de contexte
Chaque sous-agent démarre avec une conversation neuve, et seule sa réponse finale revient au parent. Le contexte du parent grandit du résumé, pas de tout le transcript.
Les autres écosystèmes
Si ton équipe est déjà engagée ailleurs, voici ce que disent leurs documentations officielles :
| SDK | Éditeur | Langages | Installation | Primitives |
|---|---|---|---|---|
| Claude Agent SDK | Anthropic | Python, TypeScript | pip install claude-agent-sdk / npm install @anthropic-ai/claude-agent-sdk | Boucle d'agent, outils intégrés, sous-agents, hooks, MCP |
| OpenAI Agents SDK | OpenAI | Python, TypeScript | pip install openai-agents / npm install @openai/agents | Agents (« LLMs equipped with instructions and tools »), Handoffs (délégation d'un agent à un autre), Guardrails (validation des entrées et sorties), Sessions (historique de conversation) |
| Microsoft Agent Framework | Microsoft | .NET, Python, Go (préversion) | pip install agent-framework | Agents, Harness Agent (agent outillé pour les tâches longues), Workflows (graphes d'exécution explicites), intégrations. Microsoft le présente comme « the direct successor » de Semantic Kernel et d'AutoGen |
| Agent Development Kit (ADK) | Python, TypeScript, Go, Java, Kotlin | pip install google-adk | LlmAgent, agents de workflow : SequentialAgent, ParallelAgent, LoopAgent, systèmes multi-agents |
Le « hello world » d'OpenAI, tel que publié dans leur documentation :
from agents import Agent, Runner
agent = Agent(name="Assistant", instructions="You are a helpful assistant")
result = Runner.run_sync(agent, "Write a haiku about recursion in programming.")
print(result.final_output)Et celui de Google ADK :
from google.adk import Agent
from google.adk.tools import google_search
agent = Agent(
name="researcher",
model="gemini-flash-latest",
instruction="You help users research topics thoroughly.",
tools=[google_search],
)Tu remarqueras la convergence : partout, un agent = des instructions + des outils + un exécuteur de boucle. Ce que tu apprends ici se transpose.
Un mot sur Microsoft : si tu tombes sur du Semantic Kernel, ce n'est pas faux, c'est l'ancêtre. Microsoft écrit noir sur blanc qu'Agent Framework est « the next generation of both Semantic Kernel and AutoGen ». Pour un nouveau projet .NET ou Python, pars d'Agent Framework.
Note d'honnêteté. Ces écosystèmes évoluent vite et leurs API changent plus souvent que celles d'un langage. Les noms de paquets et de classes ci-dessus ont été relus sur les documentations officielles en septembre 2026 ; vérifie la page de démarrage avant de t'engager sur une version.
À retenir
- Le SDK te donne la boucle d'agent de Claude Code dans ton propre programme, en Python ou TypeScript. Pour les autres langages : la CLI en sous-processus avec
-p --output-format json. - Trois éléments suffisent :
query, unprompt, desoptions. - Le message qui compte est le
ResultMessage: vérifie toujours sonsubtypeavant de lireresult. - Mets toujours
max_turnsetmax_budget_usdsur un agent de production. - Un outil personnalisé =
@tool+create_sdk_mcp_server+ le nom completmcp__serveur__outildansallowed_tools. - Les garde-fous critiques vont dans le code du handler, pas dans le prompt.
- Tes règles permanentes vont dans CLAUDE.md, parce que la compaction peut effacer les instructions du prompt initial.
Sources
- Agent SDK overview — Claude Code Docs
- Agent SDK quickstart — Claude Code Docs
- How the agent loop works — Claude Code Docs
- Give Claude custom tools — Claude Code Docs
- Subagents in the SDK — Claude Code Docs
- OpenAI Agents SDK — Python
- OpenAI Agents SDK — TypeScript
- Agents — OpenAI API Docs
- Microsoft Agent Framework Overview — Microsoft Learn
- Agent Development Kit (ADK) — Google
- anthropics/claude-agent-sdk-python — le code source du SDK et ses exemples exécutables : outil MCP en-process, hooks, plafond de budget (
examples/mcp_calculator.py,examples/hooks.py,examples/max_budget_usd.py) - openai/openai-agents-python — le SDK concurrent, avec des exemples de garde-fous en entrée et en sortie (
examples/agent_patterns/input_guardrails.py)