« L'agent propose, le code dispose » & pas de DSL
La loi qui gouverne toute l'usine, et le choix qui la rend durable : du Python et du YAML standard, aucun framework. Vous posez l'embryon du runner : hello_factory.py.
Hier, vous avez regardé un agent construire Plume en un shot. Qui a décidé que c’était fini ?
L’agent. Qui a décidé de l’ordre des opérations ? L’agent. Qui déciderait de réessayer en cas
d’échec ? Encore l’agent, s’il y pense. Rien de ce qui compte dans ce run ne vous appartenait. Ce
chapitre énonce la loi qui inverse ce rapport, l’agent propose, le code dispose, puis le choix
technique qui la rend durable : tout écrire en Python et en YAML standard, sans framework à
apprendre. Vous posez ensuite la pièce la plus importante du module, hello_factory.py :
l’embryon du runner, quelques dizaines de lignes qui contiennent déjà toute la loi.
L’agent propose, le code dispose
L’idée en une phrase
Le code déterministe possède le graphe, c’est-à-dire le séquencement, les reprises et l’acceptation. L’agent n’est qu’un nœud borné à l’intérieur d’une phase nommée : il propose un travail, et c’est une vérification écrite en code, la gate, qui décide si ce travail est accepté. Tout ce que vous construirez dans ce livre, du runner du chapitre 8 au best-of-N du chapitre 25, est une déclinaison de cette seule loi.
Points clés
- La phase est l’unité de tout : de la trace (« que s’est-il passé ? »), du coût (« combien cette étape ? ») et de la reprise (« on rejoue quoi ? »). Sans phases, un run est un bloc opaque.
- Le contexte ne traverse une frontière que dans une enveloppe JSON typée : la sortie d’une phase est un contrat vérifiable, pas de la prose qu’il faudrait interpréter.
- Les gates définissent « fini » : une commande, un code retour. Tant qu’une gate n’a pas parlé, aucun travail d’agent n’est accepté, quelle que soit son assurance.
- Une correction coûte moins cher qu’un redémarrage : un échec de gate revient à l’agent par la même porte qu’un rapport, dans une session encore vivante. Repartir de zéro jette tout ce que l’agent venait d’apprendre.
- La loi vaut dans les deux sens : le code ne fait pas le travail de jugement. Décider si un plan est implémentable reste une affaire d’agent, le code n’en fait que l’orchestration.
Exemple concret
Comparez deux stratégies face au même incident : le builder livre, la suite sort rouge.
- Redémarrage à froid : vous relancez tout. Le nouvel agent relit le projet, refait le plan, réécrit, et rien ne dit qu’il ne refera pas la même erreur. Comptez le prix d’un run entier, à nouveau, en tokens comme en minutes.
- Correction en session vivante : le runner renvoie à l’agent l’échec en enveloppe, quels tests et quels messages, dans la session qui vient d’écrire le code. L’agent sait déjà tout du contexte, il ne paie que la lecture de l’échec et le correctif. En pratique la boucle de correction coûte une fraction du run initial, souvent moins du quart, et converge en une à deux passes.
- La différence n’est pas une astuce d’optimisation : c’est une conséquence directe de la loi. Seul un code qui possède le graphe peut offrir une reprise chirurgicale. Un agent qui possède sa propre boucle ne peut que recommencer.
Qui possède quoi
| Décision | Propriétaire | Forme |
|---|---|---|
| L’ordre des phases | le code | un script Python |
| Le travail dans une phase | l’agent | une session bornée |
| Le verdict d’acceptation | le code | une gate : commande + code retour |
| La réparation après échec | l’agent | l’échec reçu en enveloppe, session vivante |
| L’arrêt (succès ou abandon) | le code | boucle de correction bornée |
Script — la loi, à l’œuvre en quatre lignes
Le TP du jour tient tout entier dans cette structure : un subprocess vers le harnais, une validation déterministe de ce qui en sort.
# L'agent propose : le harnais rend du texte, librement.
result = subprocess.run(argv, capture_output=True, text=True, timeout=300)
# Le code dispose : soit la sortie est le JSON attendu, soit elle est refusée.
payload = json.loads(extract_json(result.stdout)) # ValueError = refus
missing = REQUIRED_KEYS - payload.keys() # contrat explicite
L’agent peut être éloquent, créatif, convaincant : si la sortie ne satisfait pas le contrat, elle ne passe pas. C’est une gate, la plus petite possible, mais une vraie.
Piège courant : « mon harnais fait déjà tout ça ». Non. Un harnais gère très bien une session : les outils, le contexte, la conversation. Ce qu’il ne possède pas, c’est ce qui se passe entre les sessions : l’ordre des phases, le critère d’acceptation, la décision de rejouer. C’est la couche que l’usine ajoute, et elle tient en quelques centaines de lignes de Python que vous allez écrire.
Pas de DSL : Python et YAML, distribution standard
L’idée en une phrase
Toute l’usine s’écrit avec des technologies que vous connaissez déjà : des scripts Python, de
la configuration YAML, des prompts en Markdown. Rester dans la distribution de ce que les
modèles ont vu à l’entraînement est une fonctionnalité, pas une paresse. Les agents lisent,
déboguent et étendent une usine en Python standard bien mieux qu’un framework propriétaire. La
pièce du jour, hello_factory.py, n’utilise que la bibliothèque standard.
Points clés
- Aucun framework d’orchestration à apprendre : le « runtime » de l’usine, c’est Python.
Séquencer des phases est une boucle
for, réessayer est unforavec un compteur, une gate est unsubprocess.runet un code retour. - Les scripts sont autonomes grâce aux dépendances inline (PEP 723) lancées par
uv run: pas de virtualenv à préparer, pas de requirements à synchroniser. Le chapitre 4 y est consacré. - Tout est greppable, diffable, versionnable : un bug d’usine se debugge avec
printetgit diff, pas dans les entrailles d’une bibliothèque tierce. - L’argument décisif est agentique : vos agents devront maintenir l’usine elle-même (module 7, l’usine s’installe et s’étend par agents). Un modèle a vu des millions de scripts Python et de fichiers YAML. Il n’a jamais vu votre DSL.
Exemple concret
Mesurez le coût d’entrée des deux voies. Adopter un framework d’orchestration d’agents, c’est
apprendre ses abstractions, graphes, nœuds, callbacks et état, avant la première ligne utile :
comptez des heures de documentation, et chaque bug vous renvoie dans du code que vous ne possédez
pas. L’usine de ce livre, complète, tiendra en quelques centaines de lignes de Python
réparties en modules d’une page. hello_factory.py tient sur une page et se lit en deux minutes.
Quand un agent devra l’étendre au chapitre 8, il recevra du Python standard dans son contexte, le
langage le mieux représenté de son entraînement, et le travail coûtera quelques centimes. La
simplicité n’est pas ici une préférence esthétique : c’est ce qui rend l’usine réparable par ses
propres ouvriers.
Framework d’orchestration vs Python + YAML
| Critère | Framework / DSL | Python + YAML standard |
|---|---|---|
| Coût d’entrée | apprendre ses abstractions | vous savez déjà |
| Débogage | dans la bibliothèque tierce | print, pdb, git diff |
| Ce que le modèle connaît | rien, ou une version datée | des millions d’exemples |
| Évolution | attendre le mainteneur | éditer le fichier |
| Dépendance | le framework peut mourir | la stdlib vous enterrera |
Commande — lancer l’embryon
hello_factory.py accepte les deux harnais du livre. Le port commun qui les unifiera proprement
arrive au chapitre 7. D’ici là, ce script est le seul endroit autorisé à les appeler.
# version pi
uv run adws/hello_factory.py --harness pi
# version Claude Code
uv run adws/hello_factory.py --harness claude
Piège courant : « sans framework, ce n’est pas sérieux, il faudra bien un orchestrateur ». Il y en a un : c’est le script Python que vous possédez. La question n’est pas « orchestrateur ou pas » mais « orchestrateur de qui ? ». Quelques centaines de lignes lisibles que vous pouvez modifier en une minute battent, pour ce problème, n’importe quelle abstraction générique : votre usine n’a pas besoin d’être générique, elle a besoin d’être à vous.
Fil rouge — la pièce posée aujourd’hui
La pièce du jour est adws/hello_factory.py, l’embryon du runner et le premier fichier de la
zone adws/, le cœur de l’usine sur le plan. Elle s’appuie sur le dépôt du chapitre 1 et
interroge le harnais qui a généré Plume hier. La loi y est déjà entière : l’agent propose (du
texte, librement), le code dispose (le JSON est conforme, ou il est refusé, code retour à
l’appui). Il n’y a encore ni phases, ni reprises, ni traces. Chaque chapitre du module 3 fera
grandir cet embryon d’un organe. Coût à l’usage : de l’ordre du centime et de la demi-minute
par exécution. C’est la première fois que votre code, et non vous, juge la sortie d’un agent.
Travaux pratiques — la pièce du jour
Une pièce complète à poser dans le repo compagnon plume-factory, qui devient, chapitre après
chapitre, votre usine logicielle agentique. Aujourd’hui : l’embryon du runner.
Pièce — adws/hello_factory.py
Un subprocess vers le harnais, une sortie JSON validée : la loi du chapitre, exécutable.
Bibliothèque standard uniquement, l’en-tête PEP 723 (expliqué au chapitre 4) déclare juste la
version de Python. Le script vit du côté déterministe, l’agent n’y est qu’un nœud.
Si uv n’est pas encore sur votre poste, une commande suffit :
instructions d’installation officielles.
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.11"
# ///
"""hello_factory — l'embryon du runner.
L'agent propose : le harnais répond librement, en texte.
Le code dispose : la sortie est le JSON attendu, ou elle est refusée.
"""
import argparse
import json
import subprocess
import sys
PROMPT = (
"Reponds UNIQUEMENT avec un objet JSON, sans texte autour ni bloc markdown, "
"de la forme suivante : un champ 'tool' (la commande qui lance les tests du "
"projet apps/plume) et un champ 'purpose' (son role, en une phrase)."
)
# Les deux harnais du livre. Appel direct encore permis ici :
# a partir du chapitre 7, seul adw_modules/harness.py aura ce droit.
HARNESSES = {
"pi": ["pi", "-p", PROMPT],
"claude": ["claude", "-p", PROMPT],
}
REQUIRED_KEYS = {"tool", "purpose"}
def extract_json(text: str) -> str:
"""Isole le premier objet JSON de la sortie — l'agent ajoute parfois du texte autour."""
start, end = text.find("{"), text.rfind("}")
if start == -1 or end <= start:
raise ValueError("aucun objet JSON dans la sortie de l'agent")
return text[start:end + 1]
def main() -> int:
parser = argparse.ArgumentParser(description="Un subprocess, une sortie JSON validee.")
parser.add_argument("--harness", choices=HARNESSES, default="pi")
args = parser.parse_args()
# L'agent propose.
result = subprocess.run(HARNESSES[args.harness], capture_output=True,
text=True, timeout=300)
if result.returncode != 0:
print(f"harnais en echec ({result.returncode}) : {result.stderr.strip()[:200]}",
file=sys.stderr)
return 1
# Le code dispose : JSON parsable, contrat respecte — sinon, refus motive.
try:
payload = json.loads(extract_json(result.stdout))
except ValueError as error:
print(f"REFUSE : {error}", file=sys.stderr)
return 2
missing = REQUIRED_KEYS - payload.keys()
if missing:
print(f"REFUSE : champs manquants {sorted(missing)}", file=sys.stderr)
return 3
print(json.dumps(payload, indent=2, ensure_ascii=False))
return 0
if __name__ == "__main__":
sys.exit(main())
La gate du TP
uv run adws/hello_factory.py --harness pi && echo "gate : OK"
# ou : uv run adws/hello_factory.py --harness claude && echo "gate : OK"
Attendu : l’objet JSON validé, joliment imprimé (le champ tool devrait mentionner bun test),
puis gate : OK. En cas de refus, le code retour distingue la cause : sortie non-JSON (2) ou
contrat incomplet (3). Relancez, et observez que le refus est motivé et reproductible, là où
un « ça n’a pas marché » de session libre ne vous apprenait rien. Coût : ~1 centime, ~15 à
40 s. Quand la gate passe, commitez et cochez le jalon « ch. 3 » dans PLAN.md.