agentleFS
khalilbenazkhalilbenaz/claude-skills-collection/skills/agent-message-protocol/SKILL.md

agent-message-protocol

Skill28 starsChanged 14 days agoMITView on GitHub

Design de protocoles de communication entre agents et sous-agents — formats de messages, routing, delivery guarantees, ACK/NACK, versioning et middleware. Se déclenche avec "protocole agent", "message protocol", "communication agent", "agent messaging", "inter-agent communication", "message format agent", "agent API interne". Also triggers on "agent message format", "inter-agent messaging", "agent protocol design".

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 4 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Never say "happy to help", "sure!", "great question", "let me", or similar.

    line 321
  2. Distinctive:

    Stop when done. No summary, no recap, no trailing commentary.

    line 324
  3. Distinctive:

    No politeness wrappers. Direct and blunt.

    line 325
  4. Distinctive:

    No emoji unless asked.

    line 328

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Agent Message Protocol

Quand utiliser ce skill

Utilise ce skill dès que deux agents ou plus doivent s'échanger des tâches, des résultats ou des signaux de contrôle de façon fiable et traçable — qu'il s'agisse d'une architecture mono-processus (event bus local) ou distribuée (Redis Streams, RabbitMQ, Kafka).


Workflow en 10 étapes

1. Définir le format de message standard

Tout message doit contenir ces champs minimaux :

ChampTypeDescription
message_idUUID v4Identifiant unique du message
senderstringID de l'agent émetteur
recipientstringID de l'agent cible ou "broadcast"
typeenumtask_request / task_result / status_update / error / heartbeat / control
payloaddictDonnées utiles sérialisées
timestampISO 8601 UTCHeure d'émission
correlation_idUUID v4Relie requête et réponse
schema_versionstringEx. "1.2" — pour la compatibilité
import uuid
from datetime import datetime, timezone
from dataclasses import dataclass, field
from typing import Any

@dataclass
class AgentMessage:
    message_id: str = field(default_factory=lambda: str(uuid.uuid4()))
    sender: str = ""
    recipient: str = ""
    type: str = ""  # task_request | task_result | status_update | error | heartbeat | control
    payload: dict[str, Any] = field(default_factory=dict)
    timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
    correlation_id: str | None = None
    schema_version: str = "1.0"

2. Typer les messages — un type = un schéma

Chaque type possède un schéma de payload documenté et validé via Pydantic :

from pydantic import BaseModel, Field

class TaskRequestPayload(BaseModel):
    task_id: str
    task_type: str
    input_data: dict
    priority: int = Field(default=5, ge=0, le=9)
    deadline_seconds: int | None = None

class TaskResultPayload(BaseModel):
    task_id: str
    status: str  # "success" | "partial" | "failed"
    output_data: dict
    duration_ms: int

class ErrorPayload(BaseModel):
    error_code: str          # "TIMEOUT" | "VALIDATION_FAILED" | "AGENT_UNAVAILABLE"
    error_message: str
    retry_hint: bool
    retry_after_seconds: int | None = None
    fallback_suggestion: str | None = None
    stack_trace: str | None = None  # debug only, masquer en prod

Ne jamais réutiliser un type pour deux sémantiques différentes. Si le payload diverge, crée un nouveau type.

3. Choisir le routing pattern

PatternQuand l'utiliserCoût
DirectDestinataire connu statiquementTrès faible
BroadcastSignal à tous les agents (ex. shutdown)Faible
Topic-basedAbonnement par catégorie (results.summarizer)Moyen
Content-basedLe routeur inspecte le payload pour déciderÉlevé (CPU)
class MessageRouter:
    def __init__(self):
        self._handlers: dict[str, list] = {}

    def subscribe(self, topic: str, handler):
        self._handlers.setdefault(topic, []).append(handler)

    def route(self, message: AgentMessage):
        targets = (
            [h for hs in self._handlers.values() for h in hs]
            if message.recipient == "broadcast"
            else self._handlers.get(message.recipient, [])
        )
        for h in targets:
            h(message)

Critère de décision : préfère topic-based si les agents changent fréquemment ; content-based uniquement si la destination dépend de données dans le payload.

4. Garantir la delivery

GarantieUsage typiqueContrainte côté récepteur
At-most-onceHeartbeats, métriquesAucune
At-least-onceRésultats de tâchesHandler idempotent obligatoire
Exactly-onceMutations financières, critiquesIdempotency key + dedup store

Implémentation de la déduplication (at-least-once → exactly-once) :

import redis.asyncio as aioredis

async def is_duplicate(r: aioredis.Redis, message_id: str, ttl: int = 3600) -> bool:
    key = f"processed:{message_id}"
    was_set = await r.set(key, "1", nx=True, ex=ttl)
    return was_set is None  # None = clé déjà existante = doublon

5. Ordonner les messages

  • FIFO : asyncio.Queue ou deque — couvre 80 % des cas.
  • Priority queue : asyncio.PriorityQueue avec priorité 0–9 (0 = urgent).
  • Causal ordering : vector clocks (chaque agent maintient un vecteur logique). À réserver aux systèmes fortement distribués.
  • Timestamp-based : éviter en distribué (dérives d'horloge NTP).
import asyncio

# Priority queue : tuple (priorité, AgentMessage)
pq: asyncio.PriorityQueue = asyncio.PriorityQueue()
await pq.put((2, message_normal))
await pq.put((0, message_urgent))  # traité en premier

6. Sérialiser et versionner

FormatAvantageInconvénient
JSONLisible, debug facileVerbeux
MessagePack~2× plus compact que JSONMoins lisible
ProtobufContrat strict, multi-languesTooling plus lourd

Règles de compatibilité :

  • Ajouter des champs optionnels → backward compatible.
  • Supprimer un champ → version bump obligatoire (schema_version).
  • Renommer un champ → jamais sans version bump.
# Validation à la réception
def parse_message(raw: dict) -> AgentMessage:
    version = raw.get("schema_version", "1.0")
    if version != "1.0":
        raise ValueError(f"Unsupported schema version: {version}")
    return AgentMessage(**raw)

7. Implémenter ACK / NACK

Tout task_request doit recevoir un accusé explicite. L'émetteur maintient un dictionnaire pending_acks :

import asyncio
from typing import Callable

class AckTracker:
    def __init__(self, timeout: float = 5.0):
        self._pending: dict[str, asyncio.Future] = {}
        self._timeout = timeout

    def expect(self, correlation_id: str) -> asyncio.Future:
        fut = asyncio.get_event_loop().create_future()
        self._pending[correlation_id] = fut
        return fut

    def acknowledge(self, correlation_id: str, result: dict):
        fut = self._pending.pop(correlation_id, None)
        if fut and not fut.done():
            fut.set_result(result)

    async def wait(self, correlation_id: str) -> dict:
        fut = self.expect(correlation_id)
        try:
            return await asyncio.wait_for(fut, timeout=self._timeout)
        except asyncio.TimeoutError:
            self._pending.pop(correlation_id, None)
            raise TimeoutError(f"No ACK received for {correlation_id}")

NACK → l'agent récepteur renvoie un message error avec error_code="NACK" et retry_hint=True/False.

8. Choisir le middleware

ContexteSolution recommandéeSnippet clé
In-process, asyncasyncio.Queuequeue = asyncio.Queue(maxsize=1000)
Multi-process localRedis StreamsXADD / XREADGROUP
Distribué modéréRabbitMQ (topics + DLQ)Exchange type topic
Volume élevéKafka / RedpandaConsumer groups, partitions
# Redis Streams — publish / consume
async def publish(r: aioredis.Redis, stream: str, msg: AgentMessage):
    await r.xadd(stream, {"data": msg.model_dump_json()}, maxlen=10_000)

async def consume(r: aioredis.Redis, stream: str, group: str, consumer: str):
    try:
        await r.xgroup_create(stream, group, id="0", mkstream=True)
    except Exception:
        pass  # groupe déjà existant
    while True:
        results = await r.xreadgroup(group, consumer, {stream: ">"}, count=10, block=1000)
        for _, messages in results:
            for msg_id, fields in messages:
                yield msg_id, AgentMessage(**json.loads(fields[b"data"]))
                await r.xack(stream, group, msg_id)

Dead-Letter Queue (DLQ) : tout message non traité après N retries (max 3) doit atterrir dans une DLQ séparée pour analyse post-mortem.

MAX_RETRIES = 3
DLQ_STREAM = "agents:dlq"

async def process_with_retry(r, stream, group, consumer, handler):
    async for msg_id, message in consume(r, stream, group, consumer):
        retries = int(message.payload.get("_retries", 0))
        try:
            await handler(message)
        except Exception as e:
            if retries >= MAX_RETRIES:
                message.payload["_error"] = str(e)
                await publish(r, DLQ_STREAM, message)
            else:
                message.payload["_retries"] = retries + 1
                await publish(r, stream, message)

9. Structurer les messages d'erreur

# Exemple complet d'un message d'erreur bien formé
error_msg = AgentMessage(
    sender="agent-ocr",
    recipient="agent-orchestrator",
    type="error",
    correlation_id="<id-du-task_request-original>",
    payload=ErrorPayload(
        error_code="VALIDATION_FAILED",
        error_message="Champ 'document_type' manquant dans input_data",
        retry_hint=False,
        fallback_suggestion="Utiliser agent-fallback-ocr avec paramètres par défaut",
    ).model_dump(),
)

10. Monitorer le système de messagerie

Métriques minimales à exposer (Prometheus ou logs JSON structurés) :

MétriqueDescription
msg_throughput{type}Messages/seconde par type
msg_latency_ms{p50,p95,p99}Délai émission → traitement
msg_dlq_countMessages en DLQ (alarme si > 0)
msg_pending_acksMessages en attente d'ACK
consumer_lag{agent}Retard d'un agent sur le flux
from prometheus_client import Counter, Histogram

messages_processed = Counter("agent_messages_total", "Total messages", ["type", "status"])
processing_latency = Histogram("agent_message_latency_seconds", "Latency", ["type"])

# Dans le handler
with processing_latency.labels(type=message.type).time():
    await handler(message)
messages_processed.labels(type=message.type, status="success").inc()

Anti-patterns et pièges

Anti-patternConséquenceCorrection
Messages sans correlation_idImpossible de relier réponse à requête en asyncToujours copier le message_id de la requête dans correlation_id de la réponse
Fire-and-forget pour tâches critiquesMutations perdues sans traceAttendre ACK ; déclencher retry ou alerte si timeout
Absence de schema_versionErreurs silencieuses lors d'un déploiement mixteInclure dès le premier message ; valider côté récepteur
Payloads trop volumineux (contexte LLM complet)Dégradation des performances, explosion mémoireStocker le contexte dans un state store ; passer uniquement une context_ref
Handler non idempotent en at-least-onceEffets de bord doublés (double paiement, double écriture)Déduplication via message_id dans Redis/DB avant traitement
Pas de DLQMessages perdus sans visibilitéConfigurer DLQ dès le départ, monitorer son contenu
Types de messages ambigusLogique conditionnelle complexe côté récepteurUn type = un schéma = un handler ; jamais de if payload.get("mode") pour bifurquer

Bonnes pratiques 2026

  • Contract-first : définis et versionne les schémas Pydantic/Protobuf dans un repo partagé avant de coder les agents. Les agents consomment le package de schémas, pas des dicts libres.
  • Observability as code : les spans OpenTelemetry doivent propager trace_id dans chaque message (champ telemetry.trace_id). Permet le tracing distribué de bout en bout entre agents.
  • Graceful degradation : si le middleware est indisponible, l'agent doit basculer sur une queue locale en mémoire (buffer temporaire), pas tomber en erreur fatale.
  • Backpressure : limite la taille des queues (maxsize ou maxlen Redis). Un agent lent ne doit pas faire exploser la mémoire de l'émetteur.
  • Adapte au framework :
    • LangGraph → Command objects + state graph nativement.
    • CrewAI → retours de tool comme messages structurés.
    • AutoGen → GroupChat + ConversableAgent avec reply_func personnalisés.
    • Custom → MessageBus + asyncio.Queue + dispatcher centralisé.

Communication Rules — MANDATORY

  • Ultra-concise. No filler, no preamble, no pleasantries.
  • Never say "happy to help", "sure!", "great question", "let me", or similar.
  • Tool first, talk second. Act before explaining.
  • Result first. Lead with outcome, not process.
  • Stop when done. No summary, no recap, no trailing commentary.
  • No politeness wrappers. Direct and blunt.
  • Minimum words. If one word works, do not use ten.
  • No unsolicited explanations.
  • No emoji unless asked.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.