Définir un agent et le consommer¶
Trois objets à poser, dans cet ordre. Aucun n'est facultatif : sauter le deuxième donne un agent dont la dépense n'est imputable à personne.
flowchart TD
e["1 · Équipe<br/>budget commun, un projet"] --> c["2 · Clé virtuelle<br/>une par agent = imputation"]
c --> a["3 · Agent<br/>carte + limites + contrat"]
a --> u["Consommation<br/>script · workflow · chat · autre agent"]
1. L'équipe¶
Elle porte le budget d'un projet et regroupe ses clés. Un agent seul n'a pas besoin d'équipe ; deux agents du même projet, si — sinon leurs budgets ne s'additionnent nulle part.
curl -sX POST "$PROXY/team/new" -H "Authorization: Bearer $CLE_ADMIN" \
-H 'Content-Type: application/json' \
-d '{"team_alias":"<projet>","max_budget":0.5,"budget_duration":"1d"}'
2. La clé virtuelle — une par agent¶
C'est tout le mécanisme d'imputation. Sans elle, tout le monde dépense sous le même nom et le tableau de bord affiche des nombres qui ne répondent à aucune question.
curl -sX POST "$PROXY/key/generate" -H "Authorization: Bearer $CLE_ADMIN" \
-H 'Content-Type: application/json' \
-d '{"key_alias":"<projet>-<agent>","team_id":"<id>",
"models":["<alias1>","<alias2>"],
"max_budget":0.5,"budget_duration":"1d",
"metadata":{"role":"agent-projet","agent":"<agent>"}}'
La clé part dans le coffre, immédiatement, jamais dans un fichier du dépôt : voir Lire un secret du coffre.
Un budget coupe
Atteint, la clé est refusée. Pour un agent de fond dont personne ne regarde la sortie, ça donne un service debout dont le travail est mort. Calibrer au-dessus du pic mesuré, et câbler l'alerte de budget (Câbler une notification).
3. L'agent¶
curl -sX POST "$PROXY/v1/agents" -H "Authorization: Bearer $CLE_ADMIN" \
-H 'Content-Type: application/json' -d '{
"agent_name": "<nom>",
"agent_card_params": {
"protocolVersion": "0.3",
"name": "<Nom affiché>",
"description": "<ce qu'"'"'il fait, et ce qu'"'"'il ne fera jamais>",
"url": "http://<hôte>:<port>/a2a/<nom>",
"version": "1.0.0",
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/markdown"],
"skills": [{
"id": "<verbe>", "name": "<verbe — ce qu'"'"'il établit>",
"description": "<ce que cette compétence rend, et ce qu'"'"'elle ne rend pas>",
"tags": ["<axe>"], "examples": ["<appel typique>"]
}]
},
"object_permission": { "models": ["<alias1>","<alias2>"] },
"rpm_limit": 6
}'
| Champ | Ce qu'il décide |
|---|---|
agent_name |
l'identifiant stable. C'est lui qu'on apparie, jamais l'agent_id : un agent recréé depuis l'UI change d'id et garde son nom |
agent_card_params |
la carte A2A servie aux appelants — nom, description, compétences, formats |
agent_card_params.url |
où le proxy va taper. S'il n'y a rien derrière, la carte ment |
object_permission.models |
la liste blanche des modèles. Deuxième garde-fou, distinct de celui de la clé |
rpm_limit, tpm_limit |
le débit. session_* borne une conversation, pas l'agent entier |
Consommer l'agent¶
Quatre voies, de la plus universelle à la plus spécifique.
A. Comme un modèle ordinaire — la voie qui sert 90 % des cas¶
curl -sX POST "$PROXY/v1/chat/completions" -H "Authorization: Bearer $CLE" \
-H 'Content-Type: application/json' \
-d '{"model":"a2a/<nom>","messages":[{"role":"user","content":"<consigne>"}]}'
Le proxy reconnaît le préfixe a2a/, résout l'agent dans son registre, traduit
en message/send et retraduit la réponse. Tout ce qui parle déjà à un modèle
parle alors à l'agent : l'interface de chat le voit dans sa liste, le moteur
de workflows aussi, et un script de trois lignes n'a rien de spécial à faire.
Avec une réserve qui n'est pas théorique : cette résolution lit le registre en mémoire du proxy. Après un redémarrage, elle échoue pour tout le monde tant que les agents n'ont pas été réappliqués — voir le piège n°4.
B. En A2A, pour un client qui connaît le protocole¶
curl -s "$PROXY/a2a/<id>/.well-known/agent-card.json" -H "Authorization: Bearer $CLE"
curl -sX POST "$PROXY/a2a/<id>/message/send" -H "Authorization: Bearer $CLE" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"1","method":"message/send","params":{"message":{
"role":"user","parts":[{"kind":"text","text":"<consigne>"}],"messageId":"<uuid>"}}}'
À utiliser quand on veut le cycle de tâche — un travail qui dure, un suivi d'état, des artefacts livrés au fil de l'eau.
C. Depuis un script du parc, sans passer par A2A¶
C'est le cas de figure d'un agent qui est un programme en ligne de commande sur une machine, pas un serveur. Le script lit sa définition dans le registre, puis lance l'exécutant lui-même :
agent, provenance, contrat = charge(AGENT_DE_SECOURS) # GET /v1/agents
print(f"définition : {provenance}") # litellm | cache | code
Trois choses à respecter, et elles sont toutes coûteuses à redécouvrir :
- La lecture se fait avec la clé de l'agent lui-même. Le registre répond à une clé ordinaire : aucune clé d'administration n'a à traîner sur la machine qui exécute.
- Un cache local, et la provenance affichée. Un agent qui tourne sur son plan de secours rend la même sortie, dans le même temps, pour le même coût : cette ligne est la seule différence visible entre « à jour » et « proxy muet depuis trois jours ».
- Une étiquette d'imputation sur l'appel (
x-litellm-tags), sinon la dépense est attribuable à la clé mais pas au travail.
D. Entre agents¶
Un agent enregistré peut en appeler un autre par les mêmes voies, avec sa
clé. C'est là que object_permission.agents sert : borner qui peut appeler qui.
Les pièges, tous mesurés¶
1. L'interface d'administration écrase litellm_params
Le formulaire réécrit ce champ avec ses seuls champs. Toute donnée qu'un
script y range — un contrat d'exécution, des métadonnées maison — disparaît
au premier humain qui ouvre l'agent et enregistre. Sans message, sans trace
ailleurs que dans updated_by.
Mesuré : 21 champs posés à 07:52, détruits à 08:13 le même jour. La carte, les limites et la liste de modèles, elles, étaient intactes.
À retenir : ce qui doit durer va dans la carte. Ce qu'on range dans
litellm_params a besoin d'une source ailleurs et d'un geste qui le repose.
2. La carte jette ce qu'elle ne connaît pas, en rendant 200
capabilities.extensions — l'endroit normatif du protocole pour des
métadonnées maison — est filtré à l'enregistrement. Seuls streaming,
pushNotifications et stateTransitionHistory survivent. La création
réussit, la carte revient amputée, rien ne le signale.
3. La sortie n'est pas une entrée valide
Relire une carte puis la renvoyer telle quelle échoue en 422, sur des champs de sécurité que le proxy a lui-même écrits. Les retirer du corps suffit : il les régénère. Tout aller-retour lire / modifier / réécrire doit les enlever.
4. Le registre vit en mémoire : un redémarrage du proxy le vide
La liste des agents n'est pas relue depuis la base au démarrage : elle ne
contient que les agents créés ou modifiés depuis que le processus tourne.
Après un redémarrage, ils existent toujours — on les atteint encore par leur
identifiant — mais la liste est vide, et surtout a2a/<nom> cesse de
résoudre : la résolution lit ce même registre. Tout ce qui consommait
l'agent comme un modèle s'arrête, sans alarme, pendant que le proxy reste
healthy et que les modèles répondent.
Mesuré deux fois dans la même heure le 2026-09-13, sur six agents.
Trois gestes, dans cet ordre : garder l'identifiant de chaque agent dans sa définition (quand la liste est amnésique, c'est le seul handle qui reste) ; faire nommer la condition par le contrôle plutôt que crier « absent », ce qui ferait tenter une création et rendrait « existe déjà » ; et réappliquer — un simple aller-retour d'écriture le remet dans le registre.
Le réarmement s'automatise sans risque parce que le désaccord est faux : la base et la source disent la même chose, il n'y a rien à arbitrer. Mais seulement pour ce cas-là — dès qu'un écart d'une autre nature s'y mêle, on prévient et on ne touche à rien, sinon on écrase le travail en cours de quelqu'un d'autre.
5. Une modification partielle remplace
Un PATCH ne fusionne pas : les champs absents du corps sont perdus.
Toujours renvoyer l'objet complet. Et un contrat amputé se comporte
exactement comme un contrat sain, puisque ce qui manque retombe sur les
valeurs par défaut du code — d'où l'intérêt d'un consommateur qui annonce
ce qu'il a réellement lu.
Contrôles¶
# La définition telle que le registre la sert, et rien d'autre
curl -s "$PROXY/v1/agents" -H "Authorization: Bearer $CLE" | jq '.[] | {agent_name, rpm_limit}'
# Ce que l'exécutant a réellement chargé, à son dernier run
<commande de l'agent> 2>&1 >/dev/null | head -1
# Qui a modifié l'agent en dernier — un humain dans l'UI s'y voit
curl -s "$PROXY/v1/agents/<id>" -H "Authorization: Bearer $CLE_ADMIN" \
| jq '{updated_at, updated_by}'
Dernière vérification de cette page : 2026-09-13