Skip to main content

COG-9: Agent Messaging

Status:      Draft (Work in Progress)
Version: 0.1
Created: 2026-01-29
Authors: Mike Anderson
Work in Progress

This specification is under active development. Structure and details may change significantly based on implementation experience and community feedback.

This standard specifies the messaging protocol for delivering messages to Jobs and Agents on the Covia Grid. It defines how clients, users, and other agents submit messages to a running process via a per-job message queue, and how those messages are processed into state transitions.

Purpose

Jobs on the Covia Grid represent running processes — from short-lived operation invocations to long-lived interactive agents. Many of these processes need to receive input after creation:

  • User interaction: A human sends messages to a conversational AI agent
  • Agent-to-agent communication: One agent sends data or instructions to another
  • Authorisation responses: A client provides credentials or approval requested by a paused job
  • External notifications: A system signals that an out-of-band action has completed

COG-8 defines the job lifecycle and state chain. This specification defines how messages are delivered to jobs and how they trigger state transitions. It also defines compatibility mappings to the A2A and MCP protocols.

Terminology

See COG-1: Architecture for definitions of Grid terminology. Additional terms used in this specification:

  • Message: An arbitrary JSON value submitted to a job's message queue for processing
  • Message Queue: An ordered, per-job buffer of unprocessed messages
  • Transition Function: The Operation asset that processes messages and produces new state records

Specification

Message Format

A message is an arbitrary JSON value submitted to a job for processing.

Messages SHOULD be JSON objects. The following top-level fields are RECOMMENDED for interoperability, but not required:

FieldTypeDescription
rolestringOrigin of the message: "user", "agent", or "system"
partsarrayContent parts, each with a type field (e.g. "text", "data", "file")
fromstringSender identity (DID or venue URL) for agent-to-agent messages
messageIdstringOptional client-assigned identifier for deduplication or correlation

Venues and transition functions MUST accept arbitrary JSON as message content. The interpretation of message content is the responsibility of the transition function, not the messaging layer.

Example: User Text Message

{
"role": "user",
"parts": [
{"type": "text", "text": "What is the capital of France?"}
]
}

Example: Agent-to-Agent Message

{
"role": "agent",
"from": "did:web:other-venue.covia.ai",
"parts": [
{"type": "text", "text": "Here are the results you requested."},
{"type": "data", "data": {"count": 42, "items": ["a", "b", "c"]}}
]
}

Example: Authorisation Response

{
"role": "system",
"type": "auth/complete",
"credentials": {"apiKey": "sk-..."}
}

Example: Simple Input

For simple interactions, a plain JSON value is sufficient:

{"prompt": "Hello, how are you?"}

Message Delivery Endpoint

Messages are delivered to a job via:

POST /api/v1/jobs/{id}
Content-Type: application/json

<message body>

Response

The venue MUST return one of the following:

StatusConditionResponse Body
202 AcceptedMessage enqueued for processing{"id": "<job-id>", "status": "<current-status>", "queued": true}
404 Not FoundJob ID does not existError object
409 ConflictJob is in a terminal state{"id": "<job-id>", "status": "<terminal-status>", "error": "Job has finished"}
429 Too Many RequestsMessage queue is full or rate limit exceededError object with Retry-After header

The 202 Accepted response indicates the message has been received and queued. It does NOT indicate that the message has been processed or that a state transition has occurred. Clients MUST use job observation (polling or SSE) to observe the resulting state changes.

Behaviour Requirements

A venue MUST:

  • Accept messages for jobs in any non-terminal state (PENDING, STARTED, PAUSED, INPUT_REQUIRED, AUTH_REQUIRED)
  • Enqueue the message and return 202 before processing begins
  • Preserve message ordering within a single connection

A venue SHOULD:

  • Accept messages while the job is actively processing (status STARTED), queuing them for later processing
  • Include the current job status in the 202 response for client convenience

A venue MAY:

  • Impose a maximum queue depth per job and return 429 when exceeded
  • Impose rate limits on message submission and return 429 when exceeded
  • Assign a server-side message ID if the client did not provide a messageId

Message Queue

Each job maintains an ordered message queue. Messages are processed sequentially by the job's transition function.

The queue is the key mechanism that decouples message submission from message processing. A job may be in any non-terminal state when a message arrives — including STARTED (actively processing a previous message). The queue holds the message until the job is ready to process it.

Ordering

Messages within a single job's queue MUST be processed in the order they were accepted (FIFO).

When messages arrive from multiple sources concurrently, the venue determines the interleaving order. Venues SHOULD use arrival time at the venue as the ordering criterion.

Processing

When a message is dequeued for processing:

  1. The job transitions to STARTED
  2. The transition function is invoked with the current state and the message as input
  3. The transition function produces a new state (which may itself be INPUT_REQUIRED, COMPLETE, FAILED, etc.)
  4. A new state record is appended to the job's chain (see COG-8)
  5. If the new state is non-terminal and there are remaining messages in the queue, the next message is dequeued and processing continues from step 1

The state record produced by processing a message SHOULD include a reference to the message that triggered it, enabling audit trail reconstruction.

When Messages Are Processed

Messages are dequeued and processed when the job is ready to accept input. This occurs:

  • When the job enters an interactive state (INPUT_REQUIRED, AUTH_REQUIRED, PAUSED) and messages are already waiting in the queue
  • When a new message is submitted and the job is already in an interactive state with an empty queue

If the job is actively processing (STARTED), newly submitted messages wait in the queue. The job will process them after the current transition completes and the job returns to an interactive state.

This means a client can submit a message to a job in any non-terminal state:

Job StatusMessage Behaviour
PENDINGQueued; processed when job starts and is ready for input
STARTEDQueued; processed after current transition completes
INPUT_REQUIRED / AUTH_REQUIRED / PAUSEDQueued; immediately dequeued and processed
TerminalRejected with 409 Conflict

Queue Draining

If a job reaches a terminal state while messages remain in the queue, those messages are discarded. The venue MAY log discarded messages for debugging.

If a job transitions to an interactive state (INPUT_REQUIRED, AUTH_REQUIRED, PAUSED) while messages remain in the queue, the next queued message is immediately dequeued and processing continues. This enables pipelining, where a client submits multiple messages without waiting for intermediate responses.

State Record Linkage

When a message triggers a state transition, the resulting state record SHOULD include a trigger field referencing the message:

{
"status": "INPUT_REQUIRED",
"prev": "0xabc...",
"output": {"response": "The capital of France is Paris."},
"trigger": {
"messageId": "msg-001",
"role": "user"
},
"updated": 1769683717710
}

This enables clients to correlate responses with the messages that produced them, which is essential for multi-source messaging where responses may arrive out of order relative to submissions.

Agent Interaction Patterns

Conversational Agent

The most common pattern: a user exchanges messages with an LLM-backed agent.

Client                              Venue
│ │
│ POST /api/v1/invoke │
│ {"operation": "llm-agent", │
│ "input": {"system": "..."}} │
│ ─────────────────────────────────▶ │
│ │ State(0): PENDING
│ 201 Created │ State(1): INPUT_REQUIRED
│ {"id": "0x456..."} │ (agent ready for first message)
│ ◀───────────────────────────────── │
│ │
│ POST /api/v1/jobs/0x456... │
│ {"role": "user", │
│ "parts": [{"text": "Hello"}]} │
│ ─────────────────────────────────▶ │
│ │
│ 202 Accepted │ Message queued
│ ◀───────────────────────────────── │ State(2): STARTED (LLM processing)
│ │ State(3): INPUT_REQUIRED
│ GET /api/v1/jobs/0x456... │ (response ready, awaiting next)
│ ─────────────────────────────────▶ │
│ │
│ 200 OK │
│ {"status": "INPUT_REQUIRED", │
│ "output": {"response": "Hi!"}} │
│ ◀───────────────────────────────── │
│ │
│ POST /api/v1/jobs/0x456... │
│ {"role": "user", │
│ "parts": [{"text": "Bye"}]} │
│ ─────────────────────────────────▶ │
│ │
│ 202 Accepted │ State(4): STARTED
│ ◀───────────────────────────────── │ State(5): COMPLETE
│ │ (agent chose to terminate)

Agent-to-Agent

Two agents on different venues communicate by posting messages to each other's job queues.

Venue A                             Venue B
│ │
│ Agent A's transition function │
│ decides to contact Agent B │
│ │
│ POST /api/v1/jobs/{agent-b-id} │
│ {"role": "agent", │
│ "from": "did:web:venue-a...", │
│ "parts": [{"text": "Request"}]} │
│ ─────────────────────────────────▶ │
│ │
│ 202 Accepted │ Agent B processes message
│ ◀───────────────────────────────── │ Agent B's transition function
│ │ posts reply to Agent A
│ │
│ POST /api/v1/jobs/{agent-a-id} │
│ ◀───────────────────────────────── │
│ │
│ 202 Accepted │
│ ─────────────────────────────────▶ │

Cross-venue agent messaging uses the same endpoint and format. The sending venue's adapter makes an HTTP POST to the target venue's API. No special federation protocol is needed — the messaging API is the federation protocol.

Pipelining

A client may submit multiple messages without waiting for responses:

Client                              Venue
│ │
│ POST /api/v1/jobs/{id} │
│ {"role": "user", ...msg1...} │
│ ─────────────────────────────────▶ │
│ 202 Accepted │
│ ◀───────────────────────────────── │
│ │
│ POST /api/v1/jobs/{id} │
│ {"role": "user", ...msg2...} │
│ ─────────────────────────────────▶ │
│ 202 Accepted │
│ ◀───────────────────────────────── │
│ │
│ POST /api/v1/jobs/{id} │
│ {"role": "user", ...msg3...} │
│ ─────────────────────────────────▶ │
│ 202 Accepted │
│ ◀───────────────────────────────── │
│ │ Messages processed in order:
│ │ msg1 → State(N+1)
│ │ msg2 → State(N+2)
│ │ msg3 → State(N+3)

This is useful for batch input, data streaming, or pre-loading context into an agent before it begins processing.

Synchronous and Asynchronous Interaction

The message queue supports both synchronous and asynchronous interaction patterns on the same job:

Asynchronous (Native)

The native Covia pattern: the client submits messages via POST /api/v1/jobs/{id} and observes state changes via polling or SSE. The client is in full control of the interaction rhythm and can submit messages at any time.

This is the pattern used by:

  • Conversational UIs
  • A2A agent-to-agent communication
  • Orchestrator workflows
  • Any client that wants real-time observation

Synchronous (Bridge)

Some protocols require a synchronous request/response cycle — the caller blocks until a final result is available. The most prominent example is MCP's tools/call, which expects a single response containing the tool output.

Synchronous bridges work by:

  1. Creating a job (or sending a message to an existing one)
  2. Waiting for the job to reach a terminal state (COMPLETE, FAILED, etc.)
  3. Returning the terminal state's output or error as the response

The bridge does not need to know whether the job is one-shot or multi-turn internally. A multi-turn job that requires intermediate input (e.g. via MCP elicitation) goes through its full interactive cycle — the bridge mediates each INPUT_REQUIRED → input → STARTED transition — and the synchronous caller only sees the final result.

This means the same job can be driven by a synchronous bridge (MCP tools/call) or an asynchronous client (A2A message/send + SSE) interchangeably. The job's behaviour is identical in both cases; only the client-side interaction pattern differs.

Protocol Compatibility

A2A (Agent-to-Agent Protocol)

The Covia messaging model is designed for compatibility with the A2A protocol. The mapping is:

A2ACoviaNotes
TaskJobBoth are stateful processes with lifecycle states
message/send (no taskId)POST /api/v1/invokeCreates a new job/task
message/send (with taskId)POST /api/v1/jobs/{id}Sends message to existing job
message/sendStreamingPOST /api/v1/jobs/{id} + GET /jobs/{id}/sseSend message, then subscribe to SSE for streaming response
Task states: submitted, workingPENDING, STARTED
Task state: input-requiredINPUT_REQUIRED
Task states: completed, failed, cancelled, rejectedCOMPLETE, FAILED, CANCELLED, REJECTED
Message with role + partsMessage with role + partsSame structure by design
Artifacts (in task output)Job output / asset references

A2A Message Mapping

An A2A message/send request:

{
"jsonrpc": "2.0",
"id": 1,
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"type": "text", "text": "Hello"}]
},
"configuration": {
"taskId": "0x456..."
}
}
}

Maps to a Covia message delivery:

POST /api/v1/jobs/0x456...
Content-Type: application/json

{
"role": "user",
"parts": [{"type": "text", "text": "Hello"}]
}

The A2A bridge layer on the venue (see A2A.java) translates between JSON-RPC and the REST endpoint, extracting the taskId from the configuration and the message body from params.message.

A2A Ordering Guarantee

A2A requires: "All implementations MUST deliver events in the order they were generated." The per-job FIFO queue satisfies this requirement.

MCP (Model Context Protocol)

MCP uses a different interaction model — servers expose tools to clients, and clients invoke them. However, several MCP features map onto the Covia messaging model:

MCPCoviaNotes
tools/callPOST /api/v1/invokeInvoke an operation, creating a job
elicitation/create (form mode)Job enters INPUT_REQUIRED; response = message to jobServer asks client for input; client responds with data
elicitation/create (URL mode)Job enters AUTH_REQUIREDServer directs user to external URL for auth
notifications/elicitation/completeMessage to job confirming auth completeFire-and-forget notification
notifications/cancelledPUT /api/v1/jobs/{id}/cancelCancellation
Progress notificationsSSE events on job state chainEach state record = progress update

MCP Elicitation as Job Messaging

When a Covia venue acts as an MCP server and a tool call requires user input:

  1. The venue creates a job for the tool call
  2. The job transitions to INPUT_REQUIRED with a message describing what's needed
  3. The MCP bridge sends an elicitation/create request to the MCP client
  4. The MCP client presents the form to the user
  5. The user's response is translated into a message posted to the job queue
  6. The job processes the message and continues
MCP Client              MCP Bridge              Job Queue
│ │ │
│ tools/call │ │
│ ──────────────────────▶│ │
│ │ invoke → Job created │
│ │ Job: INPUT_REQUIRED │
│ │ │
│ elicitation/create │ │
│ ◀──────────────────────│ │
│ │ │
│ User provides input │ │
│ ──────────────────────▶│ │
│ │ POST /jobs/{id} │
│ │ ─────────────────────▶│
│ │ 202 Accepted │
│ │ ◀─────────────────────│
│ │ │ Process → COMPLETE
│ tools/call result │ │
│ ◀──────────────────────│ │

MCP Notifications as Messages

MCP's notifications/elicitation/complete is a fire-and-forget notification (no response expected). This maps directly to the 202 Accepted semantics of the Covia messaging endpoint — the sender does not wait for processing.

MCP Tasks (November 2025)

The November 2025 MCP specification introduced Tasks as a server-side abstraction for tracking work. MCP Tasks align with Covia Jobs:

  • Both represent asynchronous work with lifecycle states
  • Both support progress tracking and cancellation
  • MCP's tasks/cancel maps to PUT /api/v1/jobs/{id}/cancel

As the MCP Tasks specification matures, deeper integration may be defined. The Covia messaging model is designed to be forward-compatible with task-based MCP interactions.

Security Considerations

Message Authentication

For agent-to-agent messaging across venues, the receiving venue SHOULD:

  • Verify the sender's identity (e.g. via DID resolution or mutual TLS)
  • Apply access control policies to determine whether the sender is authorised to message the target job
  • Log the sender's identity in the state record for audit purposes

For user messages, the venue SHOULD:

  • Apply the same authentication used for other API endpoints
  • Associate the message with the authenticated user identity

Message Size Limits

Venues SHOULD impose maximum message sizes to prevent resource exhaustion. A recommended default is 1 MB per message.

Venues MUST return 413 Payload Too Large if a message exceeds the configured limit.

Queue Depth Limits

Venues SHOULD impose maximum queue depths per job to prevent memory exhaustion from clients that submit messages faster than the transition function can process them.

When the queue is full, the venue MUST return 429 Too Many Requests with a Retry-After header.

Sensitive Content

Messages may contain sensitive data (credentials, personal information, proprietary data). Venues SHOULD:

  • Apply the same data protection policies as for job inputs and outputs (see COG-8)
  • Not log message content at debug level in production environments
  • Support encryption of message content at rest

For credential delivery (e.g. API keys in response to AUTH_REQUIRED), clients SHOULD use secure transport (HTTPS) and venues SHOULD NOT persist credentials in the state chain. The transition function SHOULD extract and store credentials separately, recording only a reference (e.g. "API key provided") in the state record.

Denial of Service

The messaging endpoint is a potential target for denial-of-service attacks. Venues SHOULD:

  • Rate-limit message submissions per client
  • Rate-limit message submissions per job
  • Impose connection limits
  • Reject messages from unauthenticated clients for non-public jobs

Implementation Notes

Relationship to Job Creation

The POST /api/v1/invoke endpoint (COG-7/COG-8) creates a new job with initial input. The POST /api/v1/jobs/{id} endpoint (this specification) sends subsequent messages to an existing job. Together they form the complete interaction lifecycle:

ActionEndpointCOG
Create job with initial inputPOST /api/v1/invokeCOG-7, COG-8
Send message to existing jobPOST /api/v1/jobs/{id}COG-9 (this spec)
Observe job stateGET /api/v1/jobs/{id}COG-8
Stream job eventsGET /api/v1/jobs/{id}/sseCOG-8
View job historyGET /api/v1/jobs/{id}/historyCOG-8
Pause jobPUT /api/v1/jobs/{id}/pauseCOG-8
Resume paused jobPUT /api/v1/jobs/{id}/resumeCOG-8
Cancel jobPUT /api/v1/jobs/{id}/cancelCOG-8
Delete jobPUT /api/v1/jobs/{id}/deleteCOG-8

Transition Function Contract

A transition function that supports messaging MUST:

  • Accept arbitrary JSON as the message input
  • Return a new state value (which becomes the next state record in the chain)
  • Handle unknown message formats gracefully (e.g. by returning an error state or ignoring unrecognised fields)

A transition function SHOULD:

  • Be idempotent where possible (processing the same message twice produces the same result)
  • Complete within a reasonable time to avoid blocking the queue
  • Produce meaningful state records that capture the essence of each interaction

SSE Integration

When a message is processed and produces a new state record, the venue MUST emit an SSE event to any active subscribers for that job (see COG-8: Job Observation). This provides real-time feedback to clients that submit a message via POST and observe via SSE concurrently.