Protocol and DSL for communication among AI Agents
An AI agentic framework consists of multiple autonomous agents—AI models, software components, or potentially human participants—that collaborate to achieve specific goals. For these agents to interoperate effectively, especially when developed by different parties, we need an open standard protocol for communication. This protocol must be: Well-defined and public: Allowing any agent adhering to it to participate.
Transport-agnostic: Focusing on message format and semantics, not the underlying transmission mechanism (e.g., HTTP, WebSockets, MQTT).
Simple yet extensible: Covering core communication needs while allowing future enhancements.
The protocol will be specified in a DSL, a language tailored to the domain of agent communication. The DSL will use abstract constructs (e.g., types, enums) that map naturally to programming language features, ensuring translatability. Below, I’ll define the protocol’s components—message structure, message types, and semantics—followed by a Scala example.
DSL Specification for the Agent Communication Protocol The DSL defines the structure and behavior of messages exchanged between agents. Here’s the complete specification:
Basic Types
type AgentID = String // Unique identifier for an agent, e.g., "agentA" or a URI
type MessageID = String // Unique identifier for a message, e.g., "msg123"
type Timestamp = String // ISO 8601 formatted timestamp, e.g., "2023-10-25T10:00:00Z"
type Content = String // Message payload, e.g., JSON string, XML, or plain text
- AgentID: Identifies agents uniquely. While a simple string suffices for this example, it could be extended to a URI (e.g., "agent://example.com/myagent").
- Content: Kept as a string for flexibility; the format is specified by a content_type field.
Message Types
enum MessageType {
REQUEST, // Sender asks receiver to perform an action
RESPONSE, // Sender replies to a request or query
INFORM, // Sender shares information with receiver
QUERY, // Sender requests information from receiver
ACKNOWLEDGE, // Sender confirms receipt of a message
ERROR // Sender reports an error
}
These types, inspired by standards like FIPA ACL, define the intent of each message and guide the expected interaction patterns.
Message Structure
type Message = {
header: {
version: String, // Protocol version, e.g., "1.0"
sender: AgentID, // Who sent the message
receiver: AgentID, // Intended recipient
type: MessageType, // Type of message (from enum)
id: MessageID, // Unique message identifier
timestamp: Timestamp, // When the message was sent
content_type: String, // Format of body, e.g., "application/json"
reply_to: MessageID?, // Links to a prior message (optional)
conversation_id: String? // Groups related messages (optional)
},
body: Content // The actual payload
}
- Header: Contains metadata essential for routing and processing.
- version: Allows protocol evolution (e.g., "1.0").
- content_type: Specifies how to interpret the body (e.g., "application/json", "text/plain").
- reply_to and conversation_id: Optional fields for tracking message threads.
- Body: The payload, kept abstract as a string to support various formats.
Semantics of Message Types The DSL includes informal descriptions of each message type’s purpose and expected behavior:
-
REQUEST:
- Purpose: The sender requests the receiver to perform an action specified in the body.
- Expected Response: A RESPONSE message with reply_to set to this message’s id and matching conversation_id.
-
RESPONSE:
- Purpose: The sender provides a response to a prior REQUEST or QUERY, typically including results or status.
- Expected Response: None
-
INFORM:
- Purpose: The sender shares information with the receiver, not expecting a reply.
- Expected Response: None
-
QUERY:
- Purpose: The sender asks the receiver for information.
- Expected Response: An INFORM message with the requested data
-
ACKNOWLEDGE:
- Purpose: The sender confirms receipt of a message, useful for reliability.
- Expected Response: None
-
ERROR:
- Purpose: The sender reports an error, possibly linked to a prior message via reply_to.
- Expected Response: None
Design Notes
-
Transport-Agnostic: The DSL defines message format and semantics, not how messages are sent (e.g., direct, via broker).
-
Simplicity: Security (e.g., authentication, encryption) and advanced features (e.g., capability negotiation) are omitted for this core version but can be added via optional header fields or implementation-specific logic.
-
Extensibility: The version field and optional fields like reply_to support future enhancements.
This DSL uses a syntax resembling type definitions in programming languages, making it straightforward to translate into languages like Scala, Python, or Java.
Example of Using the DSL in Scala Below, I’ll demonstrate how to implement and use this protocol in Scala by translating the DSL into code and showing a simple interaction: an agent sending a REQUEST and another responding with a RESPONSE. Scala Implementation First, define the message structure using Scala’s case classes and sealed traits:
import java.time.Instant
// Basic types
case class AgentID(id: String)
case class MessageID(id: String)
case class Timestamp(time: String) // Using String for simplicity; could use Instant
// Message types as a sealed trait hierarchy
sealed trait MessageType
object MessageType {
case object REQUEST extends MessageType
case object RESPONSE extends MessageType
case object INFORM extends MessageType
case object QUERY extends MessageType
case object ACKNOWLEDGE extends MessageType
case object ERROR extends MessageType
}
// Header and Message case classes
case class Header(
version: String,
sender: AgentID,
receiver: AgentID,
`type`: MessageType, // Backticks needed as 'type' is a Scala keyword
id: MessageID,
timestamp: Timestamp,
contentType: String,
replyTo: Option[MessageID] = None,
conversationId: Option[String] = None
)
case class Message(header: Header, body: String)
Creating and Sending a REQUEST Agent A sends a REQUEST to Agent B to perform an action:
// Define agents and message details
val sender = AgentID("agentA")
val receiver = AgentID("agentB")
val messageId = MessageID("msg123")
val timestamp = Timestamp(Instant.now().toString)
val conversationId = "conv456"
// Create the REQUEST header
val requestHeader = Header(
version = "1.0",
sender = sender,
receiver = receiver,
`type` = MessageType.REQUEST,
id = messageId,
timestamp = timestamp,
contentType = "application/json",
conversationId = Some(conversationId)
)
// Define the body as a JSON string
val requestBody = """{"action": "doSomething", "params": {"param1": "value1"}}"""
val requestMessage = Message(requestHeader, requestBody)
// Simulated send function (transport-specific implementation omitted)
def sendMessage(msg: Message): Unit = {
println(s"Sending: $msg") // Placeholder for actual transport logic
}
sendMessage(requestMessage)
Receiving and Responding Agent B receives the REQUEST and sends a RESPONSE:
// Simulated receive function (returns the request message for this example)
def receiveMessage(): Message = requestMessage // In practice, this would listen on a transport
val receivedMsg = receiveMessage()
// Process based on message type
receivedMsg.header.`type` match {
case MessageType.REQUEST =>
// Create a RESPONSE
val responseHeader = Header(
version = "1.0",
sender = AgentID("agentB"),
receiver = receivedMsg.header.sender,
`type` = MessageType.RESPONSE,
id = MessageID("msg124"),
timestamp = Timestamp(Instant.now().toString),
contentType = "application/json",
replyTo = Some(receivedMsg.header.id),
conversationId = receivedMsg.header.conversationId
)
val responseBody = """{"status": "success", "result": "done"}"""
val responseMessage = Message(responseHeader, responseBody)
sendMessage(responseMessage)
case _ =>
println("Unhandled message type")
}
Output Explanation
-
REQUEST: Agent A sends a message asking Agent B to "doSomething" with a parameter "value1".
-
RESPONSE: Agent B replies with a success status and result, linking it to the original request via replyTo and conversationId.
Notes on the Example
-
Transport: The sendMessage and receiveMessage functions are placeholders. In a real system, they’d use a transport like HTTP or a message queue, serializing the Message (e.g., to JSON).
-
Content: The body is a JSON string, as indicated by contentType. Agents must parse it according to this type.
-
Simplicity: Error handling and serialization are omitted for brevity but would be critical in practice.