Skip to content

Defining and Consuming an Agent

Machine translation of the French original.

Three objects to place, in this order. None are optional: skipping the second one results in an agent whose expenses are not attributable to anyone.

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. The Team

It holds the budget for a project and groups its keys. A single agent does not need a team; two agents from the same project, if — otherwise, their budgets do not overlap.

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. The Virtual Key — One per Agent

This is the entire mechanism for attribution. Without it, everyone spends under the same name, and the dashboard displays numbers that don't answer any questions.

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>"}}'

The key goes into the vault, immediately, never in a file from the repository: see Reading a secret from the vault.

A budget cuts

When reached, the key is rejected. For a core agent that no one looks at, this provides a standing service whose work is dead. Calibrate above the peak measured, and wire the budget alert (Wiring a notification).

3. The 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
  }'
Field What it decides
agent_name The stable identifier. This is what we pair it with, never the agent_id: an agent recreated from the UI changes its ID and keeps its name
agent_card_params The A2A card served to callers — name, description, skills, formats
agent_card_params.url Where the proxy will type. If there is nothing behind it, the card lies
object_permission.models The whitelist of models. A second safeguard, distinct from that of the key
rpm_limit, tpm_limit The rate. session_* limits a conversation, not the entire agent

Consuming the Agent

Four paths, from the most universal to the most specific.

A. As a regular model — the path that serves 90% of cases

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>"}]}'

The proxy recognizes the prefix a2a/, resolves the agent in its registry, translates it to message/send, and translates the response back. Anything that is already talking to a model now talks to the agent: the chat interface sees it in its list, and the workflow engine also, and a three-line script doesn't need to do anything special.

With a caveat that is not theoretical: this resolution reads the registry in memory of the proxy. After a restart, it fails for everyone until the agents have been reapplied – see trap #4.

B. In A2A, for a client who knows the protocol

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>"}}}'

To be used when you want the task cycle – a task that takes time, with status tracking and artifacts delivered along the way.

C. From a script in the park, without going through A2A

This is the case of an agent that is a command-line program on a machine, not a server. The script reads its definition from the registry, and then launches the executable itself:

agent, provenance, contrat = charge(AGENT_DE_SECOURS)   # GET /v1/agents
print(f"définition : {provenance}")                      # litellm | cache | code

Three things to keep in mind, and they are all costly to rediscover:

  1. The reading is done with the agent's own key. The registry responds to a regular key: no administrative key needs to be present on the machine that is running.
  2. A local cache, and provenance displayed. An agent that runs on its backup makes the same output, at the same time, for the same cost: this line is the only visible difference between "up-to-date" and "proxy silent for three days".
  3. An attribution tag on the call x-litellm-tags, otherwise the cost is attributed to the key but not to the work.

D. Between agents

An agent that is registered can call another agent using the same method, with its key. This is where object_permission.agents comes in: to define who can call whom.

The Traps, All Measured

1. The administration interface overwrites litellm_params

The form overwrites this field with only its own fields. Any data that a script stores in it – an execution contract, custom metadata – disappears when the first human opens the agent and saves. There is no message, no trace other than in updated_by.

Measured: 21 fields set at 07:52, deleted at 08:13 on the same day. The map, the boundaries, and the list of templates, however, remained intact.

To remember: What needs to persist goes in the map. Anything stored in litellm_params needs an external source and an action to restore it.

2. The map discards what it doesn't know, resulting in a 200 error

capabilities.extensions – the standard location for custom metadata – is filtered during saving. Only streaming, pushNotifications, and stateTransitionHistory survive. The creation succeeds, the map returns incomplete, and nothing indicates this.

3. A read operation is not a valid write

Reading a map and then returning it unchanged fails with a 422 error, on fields that the proxy itself wrote. Simply removing these fields from the body is enough; they are regenerated. All read / modify / rewrite operations must remove them.

4. The registry lives in memory: restarting the proxy clears it

The list of agents is not re-read from the base at startup: it only contains the agents that have been created or modified since the process started. After a restart, they still exist – they can still be accessed using their identifier – but the list is empty, and most importantly a2a/<nom> stops resolving: the resolver reads the same registry. Anything that consumed the agent as a template stops without warning, while the proxy remains healthy and the templates respond.

Measured twice in the same hour on 2026-09-13, on six agents.

Three actions, in this order: keep the identifier of each agent in its definition (when the list is amnesic, this is the only handle that remains); make the condition named by the control rather than shouting "absent", which would trigger a creation and make "already exists" work; and re-apply – a simple write operation puts it back in the registry.

The re-arming process is automated without risk because the discrepancy is false: the base and the source say the same thing, there is nothing to arbitrate. But only in this case – as soon as a discrepancy of another nature is introduced, we anticipate and do nothing, otherwise we overwrite someone else's work.

5. A partial modification replaces

A PATCH does not merge: the missing fields are lost. Always return the complete object. And a contract that is missing parts behaves exactly like a healthy contract, since what is missing falls back on the default values of the code – which is why it is important for a consumer to announce what it has actually read.

Controls

# 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