Status: Stable
Authors: ACP Community
Date: 2026-04-04 (v2.47: RFC 8615 well-known headers; capabilities.groups; tasks pagination; auth evaluation)
License: Apache 2.0
Supersedes: core-v0.8.md
See also: transports.md · error-codes.md · identity-v2.0.md · auth-evaluation.md
Stability Promise (v1.0+): Endpoints and fields marked
stablewill not change in a backwards-incompatible way within the v1.x series. Endpoints markedexperimentalmay change with a minor version bump and advance notice. New optional fields may be added at any time.
ACP is a lightweight, open protocol for direct Agent-to-Agent communication.
| Principle | Meaning |
|---|---|
| Zero-server | No central relay, registry, or broker required |
| Zero-config | The acp:// link is the connection — no setup beyond starting the process |
| Curl-compatible | Every endpoint is reachable with plain curl. No SDK required. |
| Single required dep | websockets only. All other features are truly optional. |
| Forward-compatible | Unknown fields MUST be ignored by receivers |
| Warn-not-drop | Signature mismatches produce warnings, never message loss |
Design motto: MCP standardizes Agent↔Tool. ACP standardizes Agent↔Agent.
Every ACP message shares a common JSON envelope regardless of transport:
{
"type": "acp.message",
"message_id": "msg_7a3f9c2b",
"server_seq": 42,
"ts": "2026-03-21T07:00:00Z",
"from": "AgentA",
"role": "user",
"parts": [ ... ],
"task_id": "task_abc123", // optional — associate with a task
"context_id": "ctx_xyz456", // optional — multi-turn context group (v0.7)
"sig": "a3f9...", // optional — HMAC-SHA256 hex (v0.7)
"identity": { ... } // optional — Ed25519 identity block (v0.8)
}| Field | Type | Stability | Description |
|---|---|---|---|
type |
string | stable | Always "acp.message" for user/agent messages |
message_id |
string | stable | Client-generated unique ID (format: msg_<random>). Auto-generated by server if omitted. |
ts |
string | stable | ISO 8601 UTC timestamp |
from |
string | stable | Sender agent name |
role |
string | stable | "user" or "agent" — MUST be present; MUST be one of these two values |
parts |
array | stable | One or more Part objects (see §2). MUST contain at least one Part. |
v0.9 change:
roleis now server-enforced. A missing or invalidrolevalue MUST return400 ERR_INVALID_REQUEST. Prior versions silently defaulted to"user".
| Field | Type | Since | Stability | Description |
|---|---|---|---|---|
server_seq |
integer | v0.5 | stable | Server-assigned monotonic sequence number (ordering guarantee) |
task_id |
string | v0.5 | stable | Associates message with a task |
context_id |
string | v0.7 | stable | Groups messages into a named multi-turn context |
sig |
string | v0.7 | stable | HMAC-SHA256 signature (see §7.1) |
identity |
object | v0.8 | stable | Ed25519 identity block (see §7.2) |
message_id is intentionally optional on input:
- If the client provides
message_id: used as-is, enables client-side idempotency - If omitted: server auto-generates
msg_<16 hex chars>
This differs from A2A where messageId is REQUIRED (and not enforced in v1.0 SDK). ACP's approach reduces friction for quick integrations while supporting full idempotency when needed.
Idempotency: A server MAY deduplicate messages with the same message_id within a session window.
A Part is the atomic unit of message content. Every message carries one or more Parts.
{
"type": "text",
"content": "string content here"
}contentMUST be a string.- Use for natural language, instructions, and plain-text data.
{
"type": "file",
"url": "https://example.com/report.pdf",
"media_type": "application/pdf",
"filename": "report.pdf"
}urlREQUIRED. Must be an accessible HTTP/HTTPS URL.media_typeRECOMMENDED. Standard MIME type.filenameOPTIONAL. Display name hint.- ACP does not inline raw bytes. Use URL references only (keeps messages relay-friendly).
{
"type": "data",
"content": { "any": "json", "value": true }
}contentMUST be a JSON-serializable value (object, array, string, number, boolean, or null).- Use for structured results, function call outputs, and machine-readable payloads.
For convenience, POST /message:send accepts "text": "..." as shorthand for
"parts": [{"type": "text", "content": "..."}]. The server normalizes to full Part form.
Tasks are units of delegated work. A task is created by the requesting agent and progresses through a 5-state machine.
┌─────────────────────────────────────────────────────┐
│ │
──► submitted ──► working ──► completed (terminal) │
│ │
├──► failed (terminal) │
│ │
├──► input_required ──► working ──► ... │
│ │ │
│ └──► cancelling ──► canceled ◄───┘
│ │
└──► cancelling ──► canceled (terminal) ◄──┘
v2.6 —
cancellingintermediate state (ACP-unique): When a client callsPOST /tasks/{id}:cancel, the task transitions tocancellingfirst (an observable SSE event is pushed), then asynchronously completes tocanceled(terminal). This fills the semantic gap in A2A issues #1684 and #1680, where A2A lacks both a "being cancelled" intermediate state and a formalCancelTaskRequestdefinition.
| State | Terminal? | Stability | Description |
|---|---|---|---|
submitted |
No | stable | Task created, not yet picked up by the peer |
working |
No | stable | Peer is actively processing |
completed |
✅ Yes | stable | Peer finished successfully; artifact may be attached |
failed |
✅ Yes | stable | Peer encountered an unrecoverable error |
input_required |
No | stable | Peer needs additional input to continue |
cancelling |
No | stable (v2.6+) | Cancel requested; completion is in progress. Observers can detect cancellation before it finalises. |
canceled |
✅ Yes | stable | Task has been fully canceled |
- Terminal states (
completed,failed,canceled) cannot transition to any other state. input_required→workingresumes when the client sends a/tasks/{id}/continuemessage.- Any non-terminal, non-cancelling state may transition to
cancellingviaPOST /tasks/{id}:cancel. cancelling→canceledis completed asynchronously by the server after the in-flight work is stopped.- Calling
:cancelon a task already incancellingorcanceledis idempotent — returns200with the current status.
Client Server
| |
|-- POST /tasks/{id}:cancel --> |
| | phase-1: state → cancelling
| | SSE: {"type":"status","state":"cancelling"}
|<-- 200 {status:cancelling} -- |
| | phase-2 (async): stop work, state → canceled
| | SSE: {"type":"status","state":"canceled"}
This two-phase approach lets SSE stream consumers observe the cancellation in progress, which is especially useful when the underlying work cannot be stopped instantaneously (e.g., long-running LLM calls, external API requests).
{
"id": "task_abc123",
"status": "working",
"created_at": "2026-03-21T07:00:00Z",
"updated_at": "2026-03-21T07:00:05Z",
"input": { "parts": [...] },
"artifact": { "parts": [...] }, // present when status=completed
"error": "string", // present when status=failed
"message_id": "msg_xyz" // message_id that created this task
}| Endpoint | Method | Stability | Description |
|---|---|---|---|
/message:send |
POST | stable | Send a message to the connected peer |
/message:recv |
GET | stable | Poll pending received messages |
/stream |
GET | stable | SSE event stream |
/.well-known/acp.json |
GET | stable | AgentCard capability declaration |
/tasks |
GET | stable | List tasks; supports pagination via page_size/after/status (v2.40+, requires capabilities.tasks_pagination=true) |
/tasks |
POST | stable | Create a new task |
/tasks/{id} |
GET | stable | Get task by ID |
/tasks/{id} |
PUT | stable | Update task state/artifact |
/tasks/{id}:cancel |
POST | stable | Cancel a task |
/tasks/{id}:continue |
POST | stable | Resume input_required task |
/skills/query |
POST | stable | Query peer capability skills |
/peers |
GET | stable | List connected peers |
/peer/{id} |
GET | stable | Get single peer info |
/peer/{id}/send |
POST | stable | Send to a specific peer |
/peers/connect |
POST | stable | Connect to a new peer via acp:// link |
/discover |
GET | experimental | List mDNS-discovered LAN peers |
/status |
GET | stable | Relay status and uptime |
POST /message:send body (JSON):
| Field | Required? | Validation |
|---|---|---|
role |
MUST | Must be "user" or "agent". Missing or other value → 400 ERR_INVALID_REQUEST |
parts / text / content |
MUST | At least one of these must be present. Missing → 400 ERR_INVALID_REQUEST |
message_id |
Optional | Auto-generated if absent |
task_id |
Optional | Must reference an existing task if provided |
context_id |
Optional | Free-form string grouping identifier |
Implementation note:
rolevalidation was added in v0.9. Implementations upgrading from v0.8 MUST add explicitrolevalidation and MUST NOT silently default to"user".
Every ACP agent exposes its capabilities via a well-known endpoint:
GET /.well-known/acp.json [stable]
{
"name": "MyAgent",
"acp_version": "2.8.0",
"timestamp": "2026-03-28T07:00:00Z",
"skills": [{"id": "summarize", "name": "summarize"}],
"transport_modes": ["p2p", "relay"],
"extensions": [
{"uri": "acp:ext:hmac-v1", "required": false, "params": {"scheme": "hmac-sha256"}},
{"uri": "acp:ext:mdns-v1", "required": false, "params": {}},
{"uri": "acp:ext:h2c-v1", "required": false, "params": {}}
],
"capabilities": {
"streaming": true,
"push_notifications": true,
"input_required": true,
"part_types": ["text", "file", "data"],
"max_msg_bytes": 1048576,
"query_skill": true,
"server_seq": true,
"multi_session": true,
"error_codes": true,
"hmac_signing": false,
"lan_discovery": false,
"context_id": true,
"identity": "none",
"supported_transports": ["http", "ws"]
},
"identity": null,
"trust": {
"scheme": "none",
"enabled": false
},
"auth": {
"schemes": ["none"]
},
"endpoints": {
"send": "/message:send",
"stream": "/stream",
"tasks": "/tasks",
"agent_card": "/.well-known/acp.json",
"skills_query": "/skills/query",
"peers": "/peers",
"peer_send": "/peer/{id}/send",
"peers_connect": "/peers/connect"
}
}| Field | Type | Stability | Description |
|---|---|---|---|
name |
string | stable | Human-readable agent name |
acp_version |
string | stable | ACP protocol version implemented |
timestamp |
string | stable | ISO-8601 UTC timestamp of card generation |
skills |
object[] | stable | List of {id, name} skill descriptors |
transport_modes |
string[] | stable | v2.4+ Routing modes supported by this node. Values: "p2p" (direct peer-to-peer), "relay" (HTTP relay fallback). Default: ["p2p", "relay"]. Absent means ["p2p", "relay"]. See §5.4. |
capabilities |
object | stable | Protocol capability flags (see §5.3) |
identity |
object|null | stable | Ed25519 public key block, or null |
trust |
object | stable | HMAC signing configuration |
auth |
object | stable | Supported auth schemes |
endpoints |
object | stable | Endpoint path map |
availability |
object | experimental | v1.2+ heartbeat/cron availability metadata |
extensions |
object[] | stable | v2.8+ URI-identified extension declarations. Always present (empty [] when none). See §5.5. |
limitations |
string[] | experimental | v2.7+ declared limitations (what the agent CANNOT do) |
supported_interfaces |
string[] | experimental | v2.5+ interface groups implemented |
| Flag | Type | Stability | Description |
|---|---|---|---|
streaming |
bool | stable | SSE /stream endpoint available |
push_notifications |
bool | stable | Agent can push unsolicited events |
input_required |
bool | stable | Agent supports input_required task state |
part_types |
string[] | stable | Supported Part types |
max_msg_bytes |
int | stable | Maximum message size in bytes (default: 1,048,576) |
query_skill |
bool | stable | /skills/query endpoint available |
server_seq |
bool | stable | Outbound messages include server_seq |
multi_session |
bool | stable | Multiple simultaneous peer connections supported |
error_codes |
bool | stable | Error responses include error_code field |
hmac_signing |
bool | stable | HMAC-SHA256 signing active |
lan_discovery |
bool | experimental | mDNS LAN discovery active |
context_id |
bool | stable | context_id field supported |
identity |
string | stable | "ed25519" or "none" |
supported_transports |
string[] | stable | Protocol bindings active on this node (v2.2+). Values: "http" (HTTP/1.1), "ws" (WebSocket), "h2c" (HTTP/2 cleartext). Absent means ["http"]. Note: This declares protocol bindings; for routing topology, see top-level transport_modes (v2.4+). |
well_known_rfc8615 |
bool | stable | /.well-known/* endpoints include RFC 8615 headers (Cache-Control/Vary/X-Content-Type-Options) — v2.47+ |
tasks_pagination |
bool | stable | GET /tasks supports page_size/after/status pagination — v2.40+ |
message_priority |
bool | stable | message.priority field (critical/high/normal/low) supported — v2.43+ |
delivery_ack |
bool | stable | message.delivery_ack=true triggers explicit delivery acknowledgement — v2.43+ |
Since v2.46, capability flags are also available as a structured groups object for semantic discovery. Flat flags remain for backward compatibility; consumers SHOULD prefer groups for negotiation, producers MUST keep flat flags populated.
{
"capabilities": {
"streaming": true,
"multi_session": true,
"groups": {
"messaging": {
"streaming": true,
"push": false,
"input_required": true,
"message_priority": true,
"delivery_ack": false
},
"tasks": {
"cancelling": true,
"pagination": true,
"context_id": true
},
"identity": {
"ed25519": true,
"hmac": true,
"jwks": true,
"did": false
},
"transport": {
"sse": true,
"http2": false,
"p2p_direct": true,
"dcutr": true,
"relay_fallback": true
},
"discovery": {
"lan_mdns": false,
"skills_list": true,
"query_skill": true
}
}
}
}Unknown group keys MUST be ignored by consumers (forward compatibility).
transport_modes is a top-level AgentCard field that declares which routing topologies this node supports.
Distinction:
capabilities.supported_transportsdeclares protocol bindings (e.g. HTTP/1.1, WebSocket).transport_modesdeclares routing topology (e.g. direct P2P, relay-mediated). They are orthogonal dimensions.
Valid values:
| Value | Meaning |
|---|---|
"p2p" |
Agent supports direct peer-to-peer connections (WebSocket direct connect) |
"relay" |
Agent supports relay-mediated message delivery (HTTP relay fallback) |
Semantics:
- Default (absent or
["p2p", "relay"]): agent supports both topologies; peer may choose ["p2p"]: agent prefers/requires direct connection; relay not available (e.g. exposes public IP)["relay"]: agent is behind NAT/firewall and relay-only; P2P not possible (e.g. sandbox environment)
CLI flag: --transport-modes p2p,relay (comma-separated subset)
Example — relay-only sandbox agent:
{
"name": "SandboxAgent",
"transport_modes": ["relay"],
...
}Example — P2P-only edge agent:
{
"name": "EdgeAgent",
"transport_modes": ["p2p"],
...
}Receivers MUST treat transport_modes as advisory. Unknown values in the list MUST be ignored.
ACP supports a lightweight, declarative extension system inspired by A2A's extension model but designed to remain minimal and registry-free.
Each entry in the top-level extensions array is an Extension Object:
{
"uri": "acp:ext:hmac-v1",
"required": false,
"params": {"scheme": "hmac-sha256"}
}| Field | Type | Required | Description |
|---|---|---|---|
uri |
string | yes | Unique URI identifying the extension |
required |
bool | no | If true, clients that don't support this extension SHOULD abort. Default: false. |
params |
object | no | Arbitrary key-value parameters for the extension. Default: {}. |
| Namespace | Format | Example |
|---|---|---|
| ACP built-in | acp:ext:<name>-v<version> |
acp:ext:hmac-v1 |
| External / vendor | Full HTTPS URL | https://corp.example.com/ext/billing |
Custom URIs SHOULD use a full HTTPS URL to ensure global uniqueness and documentation discoverability.
Short acp:ext: URIs are reserved for officially defined ACP extensions.
| URI | Description | Auto-registered when |
|---|---|---|
acp:ext:hmac-v1 |
HMAC-SHA256 message signing | --secret flag is set |
acp:ext:mdns-v1 |
mDNS LAN peer discovery | --advertise-mdns flag is set |
acp:ext:h2c-v1 |
HTTP/2 cleartext transport | --http2 flag is set |
Built-in extensions are automatically registered in the relay based on runtime configuration.
No explicit declaration is needed; they appear in the extensions array when the corresponding
feature is active.
-
Always present: The
extensionsarray MUST always be present in the AgentCard response, even when empty ([]). This makes extension discovery unambiguous. -
Non-required default:
required: falseis the default. Receivers that do not recognise an extension MUST ignore it. This preserves full backward compatibility. -
Required extensions: When
required: true, a receiver that does not understand the extension SHOULD treat the connection as incompatible (e.g. abort task submission). This is opt-in and not enforced by the relay itself. -
No registry: ACP does not maintain a central extension registry. URI uniqueness is the responsibility of the extension definer.
-
Deduplication: If the same URI appears multiple times (e.g. from
--extensionand auto-registration), implementations MUST deduplicate by URI, keeping the first occurrence.
Extensions are visible via:
GET /.well-known/acp.json— AgentCard'sextensionsarrayGET /extensions— dedicated extensions list endpoint (same data)
curl http://localhost:7901/extensions
# {"extensions": [{"uri": "acp:ext:hmac-v1", "required": false, "params": {"scheme": "hmac-sha256"}}]}| Flag | Description |
|---|---|
--extension URI[,required=true][,key=val...] |
Declare a single extension (repeatable). Supports per-extension params. |
--extensions URI[,URI,...] |
Shorthand: declare multiple extensions by URI (comma-separated, no per-extension params). |
Example:
# Declare a single custom extension with params
python3 acp_relay.py --name Agent --extension "https://corp.example.com/ext/billing,required=true,tier=pro"
# Declare multiple extensions shorthand
python3 acp_relay.py --name Agent --extensions "acp:ext:custom-v1,https://corp.example.com/ext/audit"Receivers MUST ignore unknown capability fields. A flag value of false or absent is equivalent.
All error responses follow a consistent envelope:
{
"ok": false,
"error_code": "ERR_NOT_CONNECTED",
"error": "No P2P connection",
"failed_message_id": "msg_abc123"
}| Code | HTTP | Stability | Trigger |
|---|---|---|---|
ERR_NOT_CONNECTED |
503 | stable | No peer WebSocket connection |
ERR_MSG_TOO_LARGE |
413 | stable | Message exceeds max_msg_bytes |
ERR_NOT_FOUND |
404 | stable | Task, peer, or resource does not exist |
ERR_INVALID_REQUEST |
400 | stable | Missing required fields (role, parts), malformed body, invalid state transition |
ERR_TIMEOUT |
408 | stable | Sync wait timed out |
ERR_INTERNAL |
500 | stable | Unexpected server-side exception |
failed_message_id is present for ERR_TIMEOUT and ERR_MSG_TOO_LARGE only.
See error-codes.md for retry guidance and full examples.
Extensions are opt-in fields that can be combined freely. Absence of any extension has no effect on core protocol operation.
For closed deployments where both peers share a secret out-of-band:
Outbound: server appends sig to every message:
sig = HMAC-SHA256(secret, "{message_id}:{ts}").hexdigest()
Inbound: if sig present and secret configured, verify with hmac.compare_digest.
Mismatch → log warning + set _sig_invalid=true on message object. Message is not dropped.
AgentCard: capabilities.hmac_signing = true, trust.scheme = "hmac-sha256".
CLI: --secret <shared_key> (both peers must use the same key).
For open scenarios where peer identity must be publicly verifiable:
Wire format:
"identity": {
"scheme": "ed25519",
"public_key": "<base64url 32-byte public key>",
"sig": "<base64url 64-byte Ed25519 signature>"
}Signing input: canonical JSON of full message envelope, excluding identity.sig:
canonical = {k: v for k, v in msg.items() if k != "identity"}
payload = json.dumps(canonical, sort_keys=True, separators=(",",":")).encode()Keypair storage: ~/.acp/identity.json (auto-generated on first --identity run, chmod 0600).
Verification: warn-only on mismatch; accept if cryptography not installed.
AgentCard: capabilities.identity = "ed25519", identity.{scheme, public_key} block.
CLI: --identity [path]. Requires: pip install cryptography.
HMAC and Ed25519 may be active simultaneously — they serve different use cases.
See identity-v0.8.md for full spec including coexistence table and security properties.
Groups messages across multiple turns into a named conversation context:
{ "context_id": "ctx_xyz456", ... }Receivers SHOULD use context_id to correlate related messages in multi-turn workflows.
No server-side enforcement is required — it is a hint field.
When --advertise-mdns is set, the agent broadcasts its presence on the LAN via UDP multicast
(224.0.0.251:5354). Nearby ACP agents appear at GET /discover.
No external library required (raw UDP socket). AgentCard: capabilities.lan_discovery = true.
Experimental: LAN discovery behavior may change in v1.1. The
/discoverendpoint response format is considered stable; the underlying mDNS broadcast mechanism is not.
This section defines the normative ordering of SSE events over the Task lifecycle, the mandatory fields each event MUST carry, and complete wire-format examples.
Why this matters: Clients that consume
GET /streamorGET /tasks/{id}:subscribeneed a deterministic event sequence to drive state machines without polling. Implementations that omit required fields force clients to fall back to HTTP polling — defeating the purpose of SSE. This section formalises what was previously only implied by the implementation.
Every SSE event delivered via GET /stream is a JSON object with the following mandatory fields:
| Field | Type | Constraint | Description |
|---|---|---|---|
type |
string | MUST | Event category: "status", "artifact", "message", "peer", "mdns" |
ts |
string | MUST | ISO 8601 UTC timestamp of event emission |
seq |
integer | MUST | Monotonically-increasing SSE sequence counter (global, per-server). Enables gap detection. |
task_id |
string | MUST (when event is Task-related) | Associates the event with a specific Task. MUST be present for type=status and type=artifact. |
seqvsserver_seq: The message-levelserver_seqfield (§1.2) is assigned to messages and increments per outbound message. The SSE-levelseqfield is a separate counter that increments for every SSE event (across all types). Both use monotonically increasing integers; they are independent counters.
ACP uses named events for task-related SSE types:
event: acp.task.status
data: {"type":"status","ts":"2026-03-27T07:00:00Z","seq":1,"task_id":"task_abc123","state":"submitted"}
event: acp.task.artifact
data: {"type":"artifact","ts":"2026-03-27T07:00:05Z","seq":3,"task_id":"task_abc123","artifact":{...}}
All other types (message, peer, mdns) use plain data-only events:
data: {"type":"message","ts":"2026-03-27T07:00:01Z","seq":2,"message_id":"msg_xyz","role":"user","parts":[...]}
For a Task progressing from submitted to completed, the server MUST emit SSE events
in the following order:
1. type=status state=submitted ← Task accepted by server
2. type=status state=working ← Server/peer starts processing
3. type=artifact ← (optional) intermediate artifact(s)
4. type=status state=completed ← Task finished; final artifact attached if any
For other terminal outcomes, the sequence diverges after step 2:
submitted → working → failed (unrecoverable error)
submitted → working → input_required (peer needs more info)
submitted → working → cancelling → canceled (explicit cancel via :cancel, v2.6+)
v2.6+ cancel path: The server MUST emit a
cancellingSSE event before emitting the finalcanceledevent. Clients that observecancellingknow the cancel is in-progress and MUST NOT re-send the cancel request. See §3.3.1 for the two-phase cancel protocol.
For input_required, once the client calls /tasks/{id}:continue, the sequence resumes:
input_required → working → completed | failed | canceled
type=status events MUST include:
| Field | Type | Constraint | Description |
|---|---|---|---|
type |
string | MUST | "status" |
ts |
string | MUST | ISO 8601 UTC timestamp |
seq |
integer | MUST | Global SSE sequence counter |
task_id |
string | MUST | Associated Task ID |
state |
string | MUST | One of: submitted, working, completed, failed, input_required, cancelling, canceled |
error |
string | SHOULD (if state=failed) |
Human-readable error description |
context_id |
string | MAY | Multi-turn context group (if task has context) |
Complete status event examples:
// submitted
{
"type": "status",
"ts": "2026-03-27T07:00:00Z",
"seq": 1,
"task_id": "task_abc123",
"state": "submitted"
}
// working
{
"type": "status",
"ts": "2026-03-27T07:00:01Z",
"seq": 2,
"task_id": "task_abc123",
"state": "working"
}
// completed
{
"type": "status",
"ts": "2026-03-27T07:00:05Z",
"seq": 4,
"task_id": "task_abc123",
"state": "completed"
}
// failed
{
"type": "status",
"ts": "2026-03-27T07:00:03Z",
"seq": 3,
"task_id": "task_abc123",
"state": "failed",
"error": "Upstream service unavailable"
}
// input_required
{
"type": "status",
"ts": "2026-03-27T07:00:04Z",
"seq": 3,
"task_id": "task_abc123",
"state": "input_required"
}
// cancelling (v2.6+) — cancel in progress, will transition to canceled
{
"type": "status",
"ts": "2026-03-27T07:00:05Z",
"seq": 4,
"task_id": "task_abc123",
"state": "cancelling"
}
// canceled — cancel complete
{
"type": "status",
"ts": "2026-03-27T07:00:05Z",
"seq": 5,
"task_id": "task_abc123",
"state": "canceled"
}type=artifact events MUST include:
| Field | Type | Constraint | Description |
|---|---|---|---|
type |
string | MUST | "artifact" |
ts |
string | MUST | ISO 8601 UTC timestamp |
seq |
integer | MUST | Global SSE sequence counter |
task_id |
string | MUST | Associated Task ID |
artifact |
object | MUST | Artifact payload; MUST contain "parts" array |
context_id |
string | MAY | Multi-turn context group (if task has context) |
Complete artifact event example:
{
"type": "artifact",
"ts": "2026-03-27T07:00:04Z",
"seq": 3,
"task_id": "task_abc123",
"artifact": {
"parts": [
{ "type": "text", "content": "Here is your summary: ..." }
]
}
}type=message events carry a message delivered to or from this agent:
| Field | Type | Constraint | Description |
|---|---|---|---|
type |
string | MUST | "message" |
ts |
string | MUST | ISO 8601 UTC timestamp |
seq |
integer | MUST | Global SSE sequence counter |
message_id |
string | MUST | Unique message identifier |
role |
string | MUST | "user" or "agent" |
parts |
array | MUST | One or more Part objects (see §2) |
task_id |
string | MAY | Present if message is associated with a Task |
context_id |
string | MAY | Multi-turn context group |
Complete message event example:
{
"type": "message",
"ts": "2026-03-27T07:00:01Z",
"seq": 2,
"message_id": "msg_7a3f9c2b",
"role": "agent",
"parts": [{ "type": "text", "content": "Processing your request..." }],
"task_id": "task_abc123"
}A complete Task lifecycle for a summarization request — as it appears on the GET /stream wire:
event: acp.task.status
data: {"type":"status","ts":"2026-03-27T07:00:00Z","seq":1,"task_id":"task_abc123","state":"submitted"}
data: {"type":"message","ts":"2026-03-27T07:00:01Z","seq":2,"message_id":"msg_in01","role":"user","parts":[{"type":"text","content":"Summarize this document."}],"task_id":"task_abc123"}
event: acp.task.status
data: {"type":"status","ts":"2026-03-27T07:00:02Z","seq":3,"task_id":"task_abc123","state":"working"}
data: {"type":"message","ts":"2026-03-27T07:00:04Z","seq":4,"message_id":"msg_out01","role":"agent","parts":[{"type":"text","content":"Working on summary..."}],"task_id":"task_abc123"}
event: acp.task.artifact
data: {"type":"artifact","ts":"2026-03-27T07:00:06Z","seq":5,"task_id":"task_abc123","artifact":{"parts":[{"type":"text","content":"Summary: The document discusses..."}]}}
event: acp.task.status
data: {"type":"status","ts":"2026-03-27T07:00:06Z","seq":6,"task_id":"task_abc123","state":"completed"}
Implementations MUST:
- Include
type,ts,seqin every SSE event. - Include
task_idin everystatusandartifactevent. - Emit
state=submittedbeforestate=working. - Not emit
state=working(or any later state) beforestate=submitted. - Not transition a terminal task (
completed,failed,canceled) to any other state. - Emit exactly one
state=submittedevent per Task lifetime. - Use a strictly monotonically increasing
seqcounter (no resets, no gaps). - Validate
rolefield in/message:sendrequests — reject missing or invalid values with400 ERR_INVALID_REQUEST. (v0.9+) - Use timezone-aware datetime objects for all
tsfields — never naive UTC. (v2.44+) - Return RFC 8615 headers (
Cache-Control: no-cache, no-store/Vary: Accept/X-Content-Type-Options: nosniff) on all/.well-known/*endpoints whencapabilities.well_known_rfc8615=true. (v2.47+)
Implementations SHOULD:
- Emit a
state=completedorstate=failedevent as the final event in a Task's SSE stream. - Include
errorinstate=failedevents. - Populate
capabilities.groupsin AgentCard for structured capability discovery (v2.46+). - Support
GET /taskspagination parameters (page_size,after,status) whencapabilities.tasks_pagination=true(v2.40+).
Implementations MAY:
- Emit intermediate
type=messageevents betweenworkingand terminal states. - Emit multiple
type=artifactevents for streaming/chunked results. - Support
message.priorityfield for delivery ordering hints (v2.43+). - Declare
delivery_ack=trueto request explicit message acknowledgement (v2.43+).
ACP separates the message envelope (this document) from the transport layer. The same envelope is used across all bindings.
| Binding | Link Scheme | Stability | Description |
|---|---|---|---|
| A — WebSocket P2P | acp:// |
stable | Direct WebSocket (preferred) |
| B — stdio | n/a | stable | Subprocess pipe |
| C — HTTP Relay | acp+wss:// |
stable | Cloudflare Worker relay fallback |
| D — HTTP/SSE | http(s):// |
stable | Standard HTTP with SSE streaming |
| E — TCP | tcp:// |
experimental | Raw TCP (LAN) |
Binding A is the default and preferred transport. Use Binding C only when direct IP connectivity is blocked (firewalls, K8s NAT, etc.).
See transports.md for full binding specifications including HMAC header handling.
An ACP agent can maintain simultaneous connections to multiple peers.
| Endpoint | Method | Stability | Description |
|---|---|---|---|
/peers |
GET | stable | List all connected peers with metadata |
/peer/{id} |
GET | stable | Get single peer info (agent_card, link, stats) |
/peer/{id}/send |
POST | stable | Send message to a specific peer (same body as /message:send) |
/peers/connect |
POST | stable | Establish a new peer connection {"link": "acp://..."} |
{
"id": "peer_001",
"name": "AgentB",
"link": "acp://1.2.3.4:7801/tok_xxx",
"connected": true,
"connected_at": "2026-03-21T07:00:00Z",
"messages_sent": 12,
"messages_received": 8,
"agent_card": { ... }
}Enables runtime capability discovery:
POST /skills/query
{ "query": "summarize", "limit": 5 }
Response:
{
"ok": true,
"skills": [
{ "id": "summarize", "name": "summarize", "match_score": 0.95 }
]
}AgentCard declares available skills in skills[]. query_skill: true capability flag indicates
this endpoint is available.
The reference implementation CLI flags (all stable unless noted):
| Flag | Default | Description |
|---|---|---|
--name |
ACP-Agent |
Agent display name (appears in AgentCard) |
--port |
7801 |
WebSocket listener port |
--http-port |
7901 |
HTTP API port |
--skills |
"" |
Comma-separated skill IDs to advertise |
--secret |
"" |
HMAC-SHA256 shared secret |
--identity |
disabled | Ed25519 identity; optional path to key file |
--advertise-mdns |
off | Broadcast via mDNS (experimental) |
--relay |
"" |
acp+wss:// relay URL for Binding C |
--max-msg-bytes |
1048576 |
Maximum inbound message size |
--version |
— | Print version and exit |
--verbose / -v |
off | Set log level to DEBUG |
--config |
"" |
Load flag defaults from JSON/YAML file (CLI > config > defaults) |
Port layout:
| Port | Protocol | Purpose |
|---|---|---|
7801 (default) |
WebSocket | P2P peer connections (Binding A) |
7901 (default) |
HTTP | REST API + SSE stream |
pip install acp-relay # installs acp-relay CLI + relay module
pip install "acp-relay[identity]" # + Ed25519 support
pip install "acp-relay[dev]" # + pytest/httpx for testingnpm install acp-relay-client # zero-dep client SDKgit clone https://github.com/Kickflip73/agent-communication-protocol
cd agent-communication-protocol
pip install -e ".[dev]"ACP uses semantic versioning. The acp_version field in AgentCard declares the implemented version.
v1.0 stability promise: endpoints and fields marked stable will not change incompatibly in v1.x.
Compatibility guarantee: An ACP v1.0 implementation MUST:
- Accept connections from v0.5+ peers (unknown extension fields are silently ignored)
- Include
acp_version: "1.0"in its AgentCard - Return
error_codein all error responses - Reject
/message:sendrequests with missing or invalidrolewith400 ERR_INVALID_REQUEST
Breaking changes (requiring major version bump): changes to required envelope fields, removal of stable endpoints, changes to HTTP status code semantics.
Non-breaking changes (minor/patch): new optional fields, new endpoints, new capability flags.
The reference implementation is relay/acp_relay.py — a single Python file with one required
dependency (websockets).
# Minimal start
pip install acp-relay
acp-relay --name "MyAgent"
# Full feature set
pip install "acp-relay[identity]"
acp-relay --name "MyAgent" \
--secret "shared-key" \ # HMAC signing
--identity \ # Ed25519 identity
--advertise-mdns # LAN discovery (experimental)
# Config file
acp-relay --config relay/examples/config.jsonCompliance can be verified against any implementation using the compat test suite:
ACP_BASE_URL=http://localhost:7901 python3 tests/compat/run.py| Version | Date | Key Features |
|---|---|---|
| v0.1 | 2026-03-05 | P2P WebSocket, AgentCard, basic send/recv, auto-reconnect |
| v0.2 | 2026-03-10 | JSONL persistence, SSE streaming, relay fallback |
| v0.3 | 2026-03-12 | Task lifecycle (3 states), multi-session, Cloudflare Worker |
| v0.4 | 2026-03-14 | Multimodal parts (text/file/data), relay v2 |
| v0.5 | 2026-03-19 | QuerySkill API, server_seq, message idempotency, 5-state task machine |
| v0.6 | 2026-03-20 | Peer registry, standardized error codes (ERR_*), minimal agent spec |
| v0.7 | 2026-03-20 | HMAC-SHA256 signing, mDNS LAN discovery, context_id |
| v0.8 | 2026-03-21 | Ed25519 identity, Node.js SDK, compat test suite |
| v0.9 | 2026-03-21 | CLI flags (--version/--verbose/--config), async SDK stdlib-only, 63 unit tests, role server validation, pip install acp-relay, acp-relay-client npm |
| v1.0 | 2026-03-21 | Stability annotations, API surface freeze, v1.0 compatibility guarantee |
| v2.5 | 2026-03-27 | §8 Task Event Sequence spec, SSE mandatory field completeness (type/ts/seq/task_id), full lifecycle examples, conformance requirements |
| v2.6 | 2026-03-27 | cancelling intermediate state (§3.2), two-phase cancel protocol (§3.3.1), A2A #1684/#1680 gap analysis |
| v2.7 | 2026-03-28 | supported_transports field, §5.4 transport_modes section, Appendix B A2A comparison table |
| v2.8 | 2026-03-28 | Extension mechanism (§5.5): URI-identified extensions, acp:ext:* built-ins, required/optional semantics |
| v2.18 | 2026-03-30 | Ed25519 identity + JWKS endpoint (/.well-known/jwks.json, RFC 7517), trust.signals[] |
| v2.20 | 2026-03-31 | limitations[] structured format (LimitationObject), permanent/kind fields |
| v2.21 | 2026-03-31 | PATCH /.well-known/acp.json for runtime limitations update; ?filter_limitations= query param |
| v2.24 | 2026-04-01 | Peer card cache (GET /peers/{id}/card); AgentCard availability field |
| v2.29 | 2026-04-01 | PATCH /skills/{id}/limitations — per-skill runtime limitations update |
| v2.40 | 2026-04-03 | GET /tasks pagination (page_size/after/status filter); has_more/next_cursor response |
| v2.43 | 2026-04-03 | message_priority extension (critical/high/normal/low); delivery_ack flag |
| v2.44 | 2026-04-04 | datetime.utcnow() → timezone-aware (datetime.now(tz=UTC)) migration (Python 3.12) |
| v2.46 | 2026-04-04 | capabilities.groups structured grouping (messaging/tasks/identity/transport/discovery) |
| v2.47 | 2026-04-04 | RFC 8615 well-known headers (Cache-Control: no-cache/Vary: Accept/X-Content-Type-Options: nosniff) on all /.well-known/* endpoints; capabilities.well_known_rfc8615=true |
ACP and A2A target different deployment contexts:
| A2A v1.0 | ACP v1.0 | |
|---|---|---|
| Target | Enterprise orchestration | Personal / small teams |
| Server required | Yes (agent infrastructure) | No (pure P2P) |
| Auth | OAuth 2.0 (mandatory) | HMAC or Ed25519 (optional) |
| Required SDK deps | Many (incl. implicit SQLAlchemy — #883) | websockets only |
| Identity | Agent Passport System (full PKI) | Self-sovereign Ed25519 (no PKI) |
role validation |
Not enforced in SDK v1.0 | Server-enforced (400 on missing/invalid) |
| message_id | REQUIRED by spec, not enforced | Optional, auto-generated if absent |
| Test suite | ✅ ITK | ✅ tests/compat/ (MUST-level assertions) |
| Streaming | SSE | SSE |
| Task states | 6 (no cancelling intermediate) |
6 (cancelling intermediate + no unknown, v2.6+) |
| Cancel semantics | CancelTaskRequest undefined (#1684), no intermediate state (#1680) |
Two-phase cancel: cancelling → canceled; idempotent; fully specified (§3.3.1) |
| Install | pip install a2a-sdk (heavy) |
pip install acp-relay (websockets only) |
| npm | @google-a2a/a2a |
acp-relay-client |
Spec version: v1.0 (updated v2.47) | Supersedes: core-v0.8.md | Reference impl: relay/acp_relay.py (v2.47.0)