| Internet-Draft | ACP | November 2026 |
| Liu | Expires 6 May 2027 | [Page] |
An agentic dialog can run for hours or days, go quiet for long stretches, move between hosts, and survive the loss of whatever connection was carrying it. The Agent Communication Protocol (ACP) architecture names Agentic Dialog Management as a building block and requires an identifier that correlates every message of a dialog and outlives the transport, but leaves the wire format out of scope. This document specifies one such wire format.¶
ACP is an application-level session protocol in the style of SIP: every message is a start line, a block of text header fields, a blank line, and an optional body. A dialog is named by an Agentic Dialog Identifier (ADID) and its durable state is an append-only log of entries, each digesting the one before it into a hash chain. The chain lets two endpoints that have been apart establish in one exchange which entries they still share, replay only what is missing, and detect divergence. Periodic checkpoints bound the log, so aligning a dialog does not cost more as it ages.¶
ACP carries control and log state only. Real-time media flows beside it on its own channels and enters the log as a metadata entry or as a content-addressed reference, never as bytes the chain must digest. The semantics are transport-independent; this version binds them to Media over QUIC Transport (MoQ) and to WebTransport over HTTP/3, both of which multiplex the channels ACP needs and migrate across network paths without disturbing the dialog.¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 6 May 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
A task handed to an agent is rarely one request and one answer. It runs for hours, produces output long after the request that caused it, goes quiet while a human sleeps, and resumes from a different device. The connection carrying it will be replaced several times over that life. Nothing in the connection survives, so the dialog needs a name and a state of its own.¶
The ACP architecture [I-D.manyfolks-agentproto-arch] names that state Agentic Dialog Management and requires an identifier that correlates every message of a dialog and survives the loss or replacement of the transport. It does not say what the messages look like. This document supplies a wire format for that building block, and adds the one thing a dialog that spans days cannot do without: a way for two endpoints that have been apart to agree on what they both remember.¶
The problem is not reliability. QUIC already delivers a stream without gaps. The problem is the interval between connections. An agent may finish work while its principal is offline; a principal may accept an entry whose acknowledgement never arrived; a server may recover from a crash having lost its last few entries. Each side holds a plausible history and there is no shared clock or shared connection to arbitrate them. ACP answers this with a hash chain: every log entry digests its predecessor, so a history is a sequence of digests that can be compared from the tail backwards, and the first position where two histories differ is exactly the position where they diverged (Section 6.6).¶
The encoding follows SIP [RFC3261] rather than a structured data format, for two reasons. A text start line and text header fields can be read by a human in a log, generated by a shell script, and extended by adding a field without a schema change. And because the body is carried as raw octets with no field ordering to normalize, the digest of an entry is the digest of a byte string both endpoints already hold verbatim. A format whose in-memory representation is serialized differently by different implementations cannot offer that, and a chain that disagrees about its own digests is worse than no chain at all.¶
The rest of this document is organized as follows. Section 2 defines the terms used throughout; Section 3 gives the layering, the traffic classes, the participants and their identity, and the mapping onto transports; Section 4 gives the dialog and transport state machines; Section 5 defines the message format, the header fields, the methods, transactions, and status codes; Section 6 defines the log, its event boundaries, the entry digest, asset references, checkpoints, and the alignment procedure that lets a dialog survive its transport; and Section 7 maps ACP onto MoQ and WebTransport.¶
This is a first revision and is deliberately narrow. How a client learns
the https URI of the agent service it wants is not specified
here; ACP assumes the client already has one. Delegation between
dialogs, per-entry signatures, concurrent access from several devices,
and the media negotiation body are likewise identified but not
specified.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
The syntax uses the Augmented Backus-Naur Form of [RFC5234]. The terms "octet", "byte", and the character names CR, LF, and SP are used as in that specification.¶
The terms below follow the ACP architecture [I-D.manyfolks-agentproto-arch] where it defines them.¶
A software process that acts on a task over an extended period, holds state between exchanges, and produces output that is not a direct function of a single request. An agent is the server side of a dialog in this document, but an agent may also be the principal of another dialog.¶
The entity on whose behalf a dialog is conducted: a human user, or an agent acting for one.¶
A long-lived session between two participants, named by an ADID. A dialog is a protocol object with state of its own; it is not a connection, and it does not end when one is lost.¶
The Agentic Dialog Identifier: an opaque, unguessable identifier for a dialog (Section 3.3).¶
The participant that sends INITIATE is the client; the
participant that answers it is the server. The roles name who offered
the dialog, not which side runs the model.¶
The participant that assigns the authoritative Sequence
numbers of a dialog's log. In this version the server is always the
ordering authority (Section 6.1).¶
The ordered, append-only sequence of entries that constitutes a dialog's durable state (Section 6). Each participant holds a replica; the ordering authority's replica is authoritative.¶
One unit of the log, occupying exactly one Sequence number
and carrying one digest (Section 6.3).¶
A point at which an application's execution has completed a whole unit of work: a turn, a tool call and its result, an accepted utterance. Every entry closes exactly one event boundary (Section 6.2).¶
The protocol state of a dialog: its ADID, its participants, its capability and lifetime parameters, and its log.¶
The inference state an agent's runtime keeps for a dialog: token windows, scratch memory, tool bindings. Model context belongs to the application. ACP neither carries nor reconstructs it, and losing it is not an ACP failure.¶
The process by which two replicas of a log establish their last common entry and bring the lagging one up to date (Section 6.6).¶
An entry of Entry-Type system that commits to a
prefix of the log, after which entries in that prefix may be discarded
(Section 6.5).¶
The two planes on which ACP carries traffic that must be delivered
reliably. The control plane is requests and responses, matched by
CSeq. The log plane is committed entries. They are separated
because their durability requirements differ: the log must also be
recoverable after a connection is replaced. A third class, progress
traffic, rides neither plane and is never logged
(Section 3.2).¶
ACP sits between the application that owns the dialog and the transport that carries it. It is layered so that neither knows the other: the application issues methods and appends entries, the transport adapter decides which channel each byte takes, and the session layer owns the log that makes the two independent of any particular connection (Table 1).¶
| Layer | Responsibilities |
|---|---|
| ACP application layer | dialog management, identity and delegation, authorization |
| ACP session and synchronization layer | log, entry digest, alignment, checkpoints |
| ACP transport adapter | channel mapping, connection migration, resumption |
| underlying transport | WebTransport over HTTP/3 or MoQ; further bindings TBD |
The layering matters for one reason above the others. A dialog's lifetime is not a connection's lifetime, and the difference can be days. Everything ACP defines is therefore keyed to the ADID and to log position, never to a connection, a stream, or a socket. A transport connection may be established, migrated, and lost any number of times while the dialog stays in one state, and when a connection is replaced the dialog does not restart: it aligns (Section 6.6).¶
Two parties take part in a dialog: the client, which acts for a principal, and the server, which acts for an agent and is the ordering authority for the log. Figure 1 walks one dialog through establishment, steady state, an interruption, and recovery.¶
Client Server
(for a principal) (for an agent)
| |
(1) | INITIATE |
| ADID, From, To, Supported-Modalities |
|-------------------------------------------->|
(2) | 200 |
| ADID, Session-TTL, Tail-Seq: 0 |
|<--------------------------------------------|
| dialog ACTIVE |
| |
(3) | APPEND Sequence: 1, Entry-Type: input |
|-------------------------------------------->|
(4) | 200 Sequence: 1, Entry-Hash |
|<--------------------------------------------|
(5) | APPEND Sequence: 2, Entry-Type: output |
|<--------------------------------------------|
(6) | 200 Sequence: 2 |
|-------------------------------------------->|
| |
(7) | media and progress, on their own channels |
|<- - - - - - - - - - - - - - - - - - - - - ->|
| no ACP header, no Sequence, no digest |
| |
(8) | the connection dies |
| X |
| both go SUSPENDED |
| |
(9) | RESUME Tail-Seq: 2, Tail-Hash: H2 |
|-------------------------------------------->|
(10) | 206 Action: replay, Replay-From: 3 |
|<--------------------------------------------|
(11) | the entries the client missed |
|<============================================|
| both ACTIVE again |
Steps 1 and 2 establish the dialog. The client generates the ADID and offers what it supports; the server records the dialog durably, grants a lifetime, and answers with an empty log position. Three fields are the whole handshake, because everything else is either implied by the channel or negotiated later.¶
Steps 3 to 6 are the steady state, and they are the same exchange in
both directions. Each entry is proposed by its author with the next
Sequence and the digest of the entry before it, and is
committed when the peer accepts it. The response is not an
acknowledgement in the transport sense; it is the commit, and until it
arrives the entry is pending (Section 6.1.1).¶
Step 7 is the traffic ACP deliberately does not carry. Real-time media and progress indications flow on their own channels with no header, no position, and no digest, and are recorded in the log only as the event that opened or closed them (Section 6.2). This is what keeps the chain cheap enough for a dialog that runs for days.¶
Steps 8 to 11 are the reason the chain exists. The connection dies and both participants suspend without exchanging anything. When the client returns, possibly days later and on a different host, it names the last entry it holds and its digest; the server compares, and answers with either agreement or the position to replay from. One round trip establishes what the two have in common, and the entries that follow are verified as they arrive rather than trusted (Section 6.6).¶
ACP distinguishes traffic by what has to survive.¶
Requests and responses on the control plane: INITIATE,
SUSPEND, RESUME, PROBE,
CHECKPOINT, TERMINATE, and CANCEL. It
must be delivered reliably and in order within a connection, and it
is small. It is matched by CSeq
(Section 5.3).¶
Committed entries, each an indivisible octet string that has already been digested by its author. It must be delivered reliably and in order, it may be large, and it must be recoverable after a connection is replaced, because a peer that reconnects needs the entries it missed rather than the ones it has.¶
Ephemeral indications that something is happening: generation is underway, a tool call started, a partial transcript. It is time-bounded and superseded by the next indication. It MUST be carried unreliably, it is never part of the log, and no ACP state may depend on receiving it.¶
Keeping these apart is what allows ACP to be both light and durable. Real-time media takes the third path: it flows on its own channels, and the log records only that it flowed and how to find it (Section 6.4). A dialog can therefore carry hours of voice and video while its log stays small enough to align in one exchange.¶
Every dialog has an Agentic Dialog Identifier (ADID), a version 4 UUID
[RFC9562]: 122 bits from a cryptographically secure
random source [RFC4086], written in the canonical
textual form of 36 lowercase hexadecimal digits in five
hyphen-separated groups so that it can be compared verbatim. The
client generates it and sends it in the INITIATE; the server
echoes it in the final response and from then on it names the dialog
for both participants. It MUST NOT be chosen or influenced by a peer of
the generator, and MUST NOT be reused, including for an
INITIATE that went unanswered.¶
The ADID identifies the dialog, not the connection carrying it, so a dialog that moves to a new connection keeps its ADID and its log position. Because it carries 122 bits of randomness, collisions are negligible and no registration is needed. It is also not a credential: it is opaque, non-self-certifying, and reveals nothing about the dialog. Knowledge of an ADID is not authority to use it (Section 8.1).¶
A dialog has exactly two participants, both named when it is
initiated: the client, acting for a principal, and the server, acting
for an agent. Each is named by a SPIFFE ID [SPIFFE],
which is what the architecture uses for a participant and what a
workload identity carries
([I-D.ietf-wimse-workload-creds]), for example
spiffe://example.org/ns/agent/travel.¶
A SPIFFE ID names a participant, not a place to send a message. The
request target of INITIATE is an https URI
(Section 5.1) naming the agent service: it says where to
send the offer, while the From, To, and
Author fields say who is in the dialog. A client that does
not yet know which workload will serve it addresses the service and
learns the participant from the To of the server's final
response, which from then on is the name the log uses.¶
ACP is neither an identity mechanism nor an authorization mechanism, and it introduces no new cryptographic construction. It takes the model the architecture specifies [I-D.manyfolks-agentproto-arch] and states where it rides.¶
Each participant holds a WIMSE workload identity
[I-D.ietf-wimse-workload-creds], expressed as a SPIFFE
ID [SPIFFE] and carried as the sub claim of a
Workload Identity Token (WIT) that binds a public key in
cnf.jwk:¶
{ "iss": "https://aim.example.org",
"sub": "spiffe://example.org/ns/agent/travel",
"aud": "https://agent.example.com",
"cnf": { "jwk": { "kty": "EC", "crv": "P-256",
"x": "...", "y": "..." } },
"exp": 1793316000 }
The same identity serves twice. At the transport layer the participant
presents it as a workload identity credential over a channel that
mutually authenticates both ends, which is what makes the channel
requirement of Section 8 satisfiable. At the
application layer it presents a token derived from that credential, in
an Authorization field, in the messages where the peer has to
check authority rather than merely connectivity: INITIATE and
RESUME. The WIT is the input to that derivation and is not
itself what is sent: a workload identity says who a participant is,
while authorizing one request also needs the transaction context that
Section 8.2 describes.¶
Attribution then needs no signature of ACP's own, and two rules are
the whole of the mechanism. A recipient MUST check that the
Author of an APPEND, and the From of any
other request, equal the workload identity the channel authenticated,
and MUST reject the message with 403 if they do not. And a
participant MUST NOT accept dialog traffic on a channel that does not
authenticate its sender, because without an authenticated channel
there is nothing to attribute a message to and the ADID by itself
asserts nothing.¶
The private key corresponding to cnf.jwk is also the key with
which the participant signs the tokens it obtains, so one identity
serves for channel authentication, for the authority it presents, and
for the name that appears in the log. ACP adds no second identity and
no second key. Note what this does and does not buy: it proves who
sent a message on the connection that carried it, and it does not
survive the connection. An entry committed to the log is attributed by
an Author string inside a digest, which anyone able to write
to the authoritative replica can set.¶
The three traffic classes of Section 3.2 have different delivery requirements, and the adapter's only job is to satisfy each of them on whatever transport is available. Table 2 summarizes the mapping; Section 7 specifies it.¶
| Channel | Requires | MoQ | WebTransport |
|---|---|---|---|
control
|
reliable, ordered, bidirectional | one bidirectional stream | one bidirectional stream |
log
|
reliable, ordered, recoverable after a reconnect | published track, one group per entry | bidirectional stream, replayed by the server |
asset
|
reliable, content-addressed, must not block the log | a track per asset | unidirectional streams |
| progress, live media | low latency, unreliable is acceptable | datagrams or media objects | datagrams |
Two properties of the underlying transport do work that ACP would otherwise have to do itself. Connection migration lets a dialog change network path without changing state (Section 4.2), and per-stream flow control lets a large asset move without holding up an entry that is a few hundred octets. A binding that cannot provide either is still conformant, but the dialog will suspend more often and its log will stall behind its media.¶
A participant in a dialog tracks two independent variables. Its dialog state is a property of the ADID and survives any number of connections. Its transport state is a property of the connection the binding gives it and survives nothing. Conflating them is the mistake that makes a session protocol fragile, so ACP keeps them apart and defines exactly how the second drives the first (Section 4.2).¶
ACP keeps one dialog state per participant. The states are the same for
both, except that only the server enters ALIGNING: the client
stays in SUSPENDED until alignment completes, because it is
the server that arbitrates and only the server needs a state in which
it has accepted a resumption but not yet settled it.¶
IDLE | | INITIATE v INITIATING --4xx / 5xx / T1--> TERMINATED | | 200 v ACTIVE <--------------------------+ | | | transport loss, or SUSPEND | alignment complete v | (200, or 206 after replay) SUSPENDED ---+ | | | | | +--> ALIGNING -------+ | (server only) | | TTL expiry, or TERMINATE v TERMINATED
No dialog state exists for this ADID.¶
An INITIATE has been sent or received and no final response
has settled it. The client has chosen an ADID that is not yet in
use.¶
The dialog is established, a transport channel carries it, and the two replicas are aligned. Entries may be appended and work may be requested. On the server this is also the state in which it holds runtime resources for the dialog; it is not required to hold model context, and losing it is not a protocol failure.¶
The dialog exists and its log is durably held, but no channel
carries it. The server releases whatever runtime resources it was
holding. This state may last for the whole Session-TTL,
which is measured from the moment the dialog was suspended and not
from the moment it was created.¶
The server has accepted a RESUME and is comparing replicas.
It does not accept APPEND in this state, because an append
would move the tail it is trying to agree on.¶
The dialog is finished. State MAY be purged, and the ADID MUST NOT
be reused. A RESUME for a terminated dialog receives
404.¶
CANCEL does not change dialog state. It stops work associated
with an entry already in the log and adds an entry recording that it
did (Section 5.4.3), after which the dialog is
ACTIVE and accepts the next transaction.¶
Transport state is whatever the binding reports: connected, migrating, or gone. It is not an ACP state machine and ACP defines no polling for it, because QUIC reports path loss directly [RFC9000]. What ACP defines is the mapping from transport events to dialog transitions, in Table 3.¶
| Who | From | Event | To |
|---|---|---|---|
| client | IDLE | sends INITIATE
|
INITIATING |
| client | INITIATING | receives 200
|
ACTIVE |
| client | INITIATING |
4xx, 5xx, or T1 expiry |
TERMINATED |
| server | IDLE | receives INITIATE
|
INITIATING |
| server | INITIATING | log initialized, sends 200
|
ACTIVE |
| server | INITIATING | rejects, sends 4xx or 5xx
|
TERMINATED |
| both | ACTIVE | path changes; migration completes within T1 | ACTIVE |
| both | ACTIVE | transport gone, and no migration within T1 | SUSPENDED |
| both | ACTIVE |
SUSPEND settled by 200
|
SUSPENDED |
| server | SUSPENDED | receives RESUME
|
ALIGNING |
| server | ALIGNING | replicas agree; 200, or 206 and replay complete |
ACTIVE |
| server | ALIGNING | digests differ, sends 409; or tail below Min-Seq, sends 412
|
ALIGNING |
| server | ALIGNING | T2 expiry, or client abandons | SUSPENDED |
| client | SUSPENDED | alignment complete | ACTIVE |
| client | SUSPENDED | receives 404, or Session-TTL expires |
TERMINATED |
| both | any |
TERMINATE settled, or Session-TTL expires |
TERMINATED |
The row that matters most is the one that changes nothing. Where the
binding supports connection migration, as QUIC does, a participant
moving from one network path to another stays ACTIVE and sends
no ACP message at all: the dialog is keyed to the ADID and to log
position, never to a socket (Section 3). It moves
to SUSPENDED only when the connection is gone and no
migration has completed within T1. This is why a dialog can survive a
walk from a wireless network to a cellular one without either
participant noticing, and why the resumption path is reserved for
separations long enough that the connection really did die.¶
Everything on the control plane and the log plane has one shape: a start line naming a method or a status, a block of text header fields, a blank line, and an optional body. This section defines that shape, the fields, the methods, and the transactions that match a request to its response. Section 5.4 gives each method in full, and Section 5.5 the response codes.¶
An ACP message is a start line, zero or more header fields, a blank line, and an optional body:¶
ACP-message = request / response
request = Request-Line *( header-field CRLF ) CRLF [ body ]
response = Status-Line *( header-field CRLF ) CRLF [ body ]
Request-Line = method SP request-uri SP "ACP/1.0" CRLF
Status-Line = "ACP/1.0" SP status-code SP reason-phrase CRLF
method = "INITIATE" / "APPEND" / "CANCEL" / "SUSPEND"
/ "RESUME" / "PROBE" / "CHECKPOINT" / "TERMINATE"
/ token
status-code = 3DIGIT
reason-phrase = 1*( unreserved / SP )
header-field = field-name ":" OWS field-value OWS
field-name = token
field-value = *( HTAB / SP / VCHAR )
request-uri = "https:" "//" authority [ path-abempty ]
body = *OCTET
OWS = *( SP / HTAB )
token = 1*tchar
tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+"
/ "-" / "." / "^" / "_" / "`" / "|" / "~"
/ DIGIT / ALPHA
3DIGIT = DIGIT DIGIT DIGIT
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
authority = <authority, Section 3.2 of [RFC3986]>
path-abempty = <path-abempty, Section 3.3 of [RFC3986]>
OWS, token, and 3DIGIT match the definitions
of the same names in [RFC9110], and unreserved
matches [RFC3986], Section 2.3; all four are repeated
here so that the grammar is self-contained. authority and
path-abempty are used by reference to
[RFC3986], Sections 3.2 and 3.3.¶
The request-uri is an https URI naming the agent
service, and is the same in every request of a dialog. It is a
locator, not the name of a participant
(Section 3.4); its userinfo,
query, and fragment components MUST NOT be used.
That the scheme is https does not mean ACP runs inside HTTP:
how the URI is reached is a property of the binding
(Section 7).¶
Three properties of this syntax are normative because the entry digest
of Section 6.3 depends on them. First, a
field-value cannot contain CR or LF, since it is built from
VCHAR, SP, and HTAB only; that is what makes a sequence of
field values separable without length prefixes. Second, the blank line
that ends the header block is exactly CRLF following the CRLF of the
last header field, with no other form permitted. Third, the body is an
opaque octet string whose extent is given by Content-Length;
no transfer encoding, and in particular no chunked encoding, is
defined.¶
A sender MUST NOT send a header field it does not understand the
syntax of, and a recipient MUST ignore a header field it does not
recognize unless Section 5.2 marks it as required for the
method in question. This is what allows a field to be added without a
version change. A recipient that receives a message whose start line
it cannot parse MUST respond 400; one whose
method it does not support MUST respond 501.¶
Content-Length is REQUIRED on every message sent on a channel
that carries more than one message, and on every message with a body.
On a channel that carries exactly one entry body and is then closed by
the transport, the end of the stream delimits the body and
Content-Length MAY be omitted (Section 7).¶
Table 4 lists the fields this version defines. Field names are case-insensitive. A field MUST NOT appear more than once unless its definition says otherwise.¶
| Field | Value | Where it appears |
|---|---|---|
ADID
|
The dialog identifier (Section 3.3) | Every message, including an INITIATE request, where it is the client-generated value |
CSeq
|
seq SP method, per transaction |
Every request; echoed by its response |
From, To
|
Participant URIs (Section 3.4) |
INITIATE request and its final response |
Sequence
|
Log position of an entry |
APPEND; a response that assigns one |
Prev-Hash
|
Entry-Hash of Sequence minus one |
APPEND
|
Entry-Hash
|
Digest of this entry (Section 6.3) |
APPEND
|
Entry-Type
|
input, output, meta, abort, system
|
APPEND
|
Author
|
Participant URI of the entry's creator, which MUST equal the identity the channel authenticated (Section 3.5) |
APPEND
|
Timestamp
|
Seconds since the Unix epoch |
APPEND, CHECKPOINT
|
Event-Cursor
|
A token naming the runtime boundary the entry closes (Section 6.2); optional |
APPEND, CANCEL
|
Tail-Seq, Tail-Hash
|
Position and digest of the sender's last committed entry |
RESUME, PROBE, and responses that report position |
Min-Seq
|
Oldest Sequence still retained |
RESUME and its responses |
CP-Seq, CP-Hash
|
Latest checkpoint entry and its digest (Section 6.5) |
CHECKPOINT, RESUME responses |
Replay-From
|
First Sequence the responder will replay |
206 response to RESUME
|
Action
|
replay, resync, or probe
|
Responses to RESUME
|
Proof
|
One seq=hash-value pair; repeatable |
PROBE request and response |
Session-TTL
|
delta-seconds; requested in a request, granted in a response |
INITIATE, SUSPEND, RESUME
|
Supported-Modalities
|
Comma-separated media type ranges |
INITIATE request |
Target-Seq
|
Sequence whose processing is to stop |
CANCEL
|
Reason
|
Machine-readable token |
CANCEL, TERMINATE
|
Authorization
|
Credentials of the sender, syntax as in [RFC9110]; carries the token obtained as Section 8.2 describes |
INITIATE, RESUME
|
Content-Type
|
Media type of the body [RFC2046] | Any message with a body |
Content-Length
|
Body length in octets | See Section 5.1 |
The values of the fields that carry a digest or a position use this syntax:¶
seq = 1*15DIGIT hash-value = hash-alg "-" b64 hash-alg = "sha256" b64 = 43*unreserved ; RFC 4648, Section 5, unpadded Proof = seq "=" hash-value action = "replay" / "resync" / "probe"
Tail-Seq and Tail-Hash MUST appear together and
describe the same entry. Min-Seq defaults to 1 when absent.
Proof is the only field in this document that MAY appear more
than once; the set it denotes is the union of its occurrences, and a
seq MUST NOT be repeated across them.¶
Digest values are written as the algorithm name, a hyphen, and the
base64url encoding of the digest without padding
([RFC4648], Section 5), which for SHA-256 is 43
characters. Hexadecimal is not used because a 64-character digest
cannot share a line with a field name inside the formatting width this
document observes, and folding a digest across lines would put its
textual form at the mercy of each implementation's folder. The
algorithm prefix is not decorative: a recipient MUST check it and MUST
reject a value naming an algorithm it does not support with
400.¶
Header field order is not significant. A sender MAY emit fields in any order, and a recipient MUST NOT infer meaning from their arrangement. The digest of Section 6.3 uses a fixed order of values that has nothing to do with the order they appear in.¶
A transaction is one request and the responses to it. The
CSeq field carries a sequence number and the method name,
separated by a single space. Each participant keeps one
CSeq counter per dialog, starting at 1 and increasing by at
least one for each request it sends; the counter is independent of log
Sequence numbers, because a dialog may see many transactions
that add no entry. A response echoes the CSeq of its request
verbatim. A recipient that sees a CSeq it has already
answered on this dialog MUST discard the request, which makes requests
safe to repeat after a connection is replaced.¶
A participant MUST NOT have more than one outstanding
APPEND per dialog, so that its pending entries have a total
order. It MAY have one outstanding APPEND and one outstanding
control request at the same time. Responses may arrive in a different
order from requests only where this paragraph allows it, so a
recipient matches them by CSeq and never by arrival order.¶
Because every channel ACP uses is reliable and ordered within a connection (Section 7), ACP defines no retransmission timer and no hop-by-hop acknowledgement. It does define two timers, because a request can still go unanswered when a connection dies:¶
| Timer | Default | Meaning |
|---|---|---|
| T1 | 5 s | Bound on waiting for a final response. On expiry the transaction fails locally; the outcome is unknown and the participant MUST align before relying on it (Section 6.1.1). |
| T2 | 60 s | Bound on completing a replay announced by 206. On expiry the alignment attempt fails and MAY be retried. |
A dialog also has a lifetime, Session-TTL, expressed in
delta-seconds. A request carries the value the sender wants; the
response carries the value granted, which MAY be shorter. The default
is 259200 (72 hours). Time starts again on each successful
APPEND, RESUME, or CHECKPOINT. When it
expires the dialog moves to the terminated state and its state MAY be
purged; a later RESUME then fails with 404.¶
ACP defines eight methods, summarized in Table 6. The subsections that follow define each one in full: what it requires, what a successful response commits both participants to, and how it fails. The examples use one dialog throughout, with the digests that follow from it, so they can be checked against the construction of Section 6.3.¶
| Method | Sent by | Effect |
|---|---|---|
INITIATE
|
client | Create a dialog and fix its ADID and participants |
APPEND
|
either | Add one entry to the log |
CANCEL
|
either | Stop work caused by an entry already in the log |
SUSPEND
|
either | Release runtime resources, keep the dialog |
RESUME
|
client | Align the log and reactivate the dialog |
PROBE
|
client | Locate the common ancestor after a 409
|
CHECKPOINT
|
either | Commit to a log prefix and permit its truncation |
TERMINATE
|
either | End the dialog and purge its state |
INITIATE creates a dialog. The client generates the ADID
(Section 3.3), names both participants, states the modalities
it can handle, and asks for a lifetime. Required fields:
ADID, CSeq, From, To. Optional:
Supported-Modalities, Session-TTL.¶
INITIATE https://agent.example.com/travel ACP/1.0 CSeq: 1 INITIATE ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d From: spiffe://example.org/ns/agent/client-1 To: spiffe://example.com/ns/agent/travel Authorization: Bearer eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOi... Supported-Modalities: text/plain, image/*, audio/opus Session-TTL: 259200 Content-Length: 0 ACP/1.0 200 OK CSeq: 1 INITIATE ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d From: spiffe://example.org/ns/agent/client-1 To: spiffe://example.com/ns/agent/travel Session-TTL: 259200 Tail-Seq: 0 Tail-Hash: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Min-Seq: 1 Content-Length: 0
On 200 the server has durably recorded the dialog with an
empty log and both participants are ACTIVE. The ADID in the
response MUST equal the one requested; a server cannot substitute its
own, since the client has already begun using it.
Session-TTL in the response is the lifetime granted, which MAY
be shorter than requested.¶
A server that cannot serve the modalities offered responds
488; one that is out of capacity responds 486; one
that cannot durably record the dialog responds 500 and MUST
NOT leave partial state. After any failure, and after a T1 expiry, the
client MUST discard the ADID and generate a fresh one for any retry: a
dialog whose establishment did not settle has no state to resume.¶
ACP does not define the format of a media negotiation body in this
revision. Supported-Modalities is a list of media type ranges
as in [RFC6838], and 488 is the only
negotiation failure this version specifies. Offer and answer semantics,
and how a modality is bound to a data channel, are deferred to a later
revision.¶
APPEND adds one entry to the log. Required fields:
ADID, CSeq, Sequence,
Entry-Type, Prev-Hash, Author,
Timestamp, Entry-Hash. Optional:
Event-Cursor (Section 6.2). The body is
optional but its extent must be determinate, so Content-Length
is required unless the binding delimits the body by closing a stream.¶
APPEND https://agent.example.com/travel ACP/1.0 ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d CSeq: 502 APPEND Sequence: 90 Entry-Type: input Prev-Hash: sha256-_NrWKsIcH0PxTqjFxzYbg4w8ad2vogH1iWwLtcffWBQ Author: spiffe://example.org/ns/agent/client-1 Timestamp: 1793312400 Event-Cursor: turn-complete Content-Type: multipart/mixed; boundary=acp-9b1deb4d Content-Length: 113 Entry-Hash: sha256-2qyCqGMY8i6F-zXfLQRYMkzVSHBqre6g0CkYq9mmziU --acp-9b1deb4d Content-Type: text/plain; charset=utf-8 Please review the flight schedule. --acp-9b1deb4d-- ACP/1.0 200 OK CSeq: 502 APPEND ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d Sequence: 90 Entry-Hash: sha256-2qyCqGMY8i6F-zXfLQRYMkzVSHBqre6g0CkYq9mmziU Content-Length: 0
An entry authored by the server is identical in form, with
Entry-Type: output and the server's URI as Author,
and travels in the other direction on the log plane. The response
echoes Sequence and Entry-Hash and nothing else is
needed: the echo is the commit, and after it the entry is part of the
log and its author may use its digest as the next
Prev-Hash.¶
A server rejects an APPEND with 409 if it does not
extend its tail exactly (Section 6.1), with
422 if the received digest does not match the octets it got
(Section 6.3), with 413 if the body is larger than
it will accept, and with 415 if it cannot handle the media
type. A rejected entry is not written and its Sequence is not
consumed. Until a final response arrives the entry is pending and its
author is bound by Section 6.1.1.¶
CANCEL asks the peer to stop work caused by an entry that is
already in the log. It does not remove the entry and does not undo the
dialog: an entry, once committed, is part of the history whether or not
the work it caused was finished. Required fields: ADID,
CSeq, Target-Seq. Optional: Reason,
Event-Cursor.¶
CANCEL https://agent.example.com/travel ACP/1.0 ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d CSeq: 503 CANCEL Target-Seq: 90 Reason: user-interrupted Content-Length: 0 ACP/1.0 200 OK CSeq: 503 CANCEL ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d Content-Length: 0
CANCEL is a request to stop, not a guarantee that nothing
further was produced. The recipient SHOULD stop as soon as it can and
MUST then append an entry of Entry-Type: abort recording the
Target-Seq and the reason, so that both replicas agree on
where the abandoned work ended. That entry, not the 200, is
what settles the cancellation in the log; a partial output produced
before the stop MAY be appended as an output entry below it,
and an application that must know how much was produced reads the two
together. This is not an exception to Section 6.2: the
cancellation closes the event, so what was produced before it is
complete in the only sense that matters, namely that no more is
coming. Fragments still in flight when the stop arrives are not part of
any entry and are discarded.¶
Because stopping work is often urgent and generation is often metered,
a binding SHOULD give CANCEL the highest priority available
and SHOULD NOT queue it behind log traffic
(Section 7). A server that cannot identify the work
referred to, because the entry is not in its log or the work already
finished, responds 200 anyway: the requested end state holds.
A server that is not authorized to be told to stop responds
403.¶
SUSPEND tells the peer that the sender is going away and asks
it to release runtime resources while keeping the dialog. It is
advisory about resources and normative about nothing else: a dialog
that is never suspended behaves identically, since losing the transport
has the same effect (Section 4). Sending it is nonetheless
useful, because it lets the server release capacity immediately rather
than at a timeout, and it lets the sender ask for a specific retention
period. Required fields: ADID, CSeq. Optional:
Session-TTL.¶
SUSPEND https://agent.example.com/travel ACP/1.0 ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d CSeq: 601 SUSPEND Session-TTL: 86400 Content-Length: 0 ACP/1.0 200 OK CSeq: 601 SUSPEND ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d Session-TTL: 86400 Tail-Seq: 90 Tail-Hash: sha256-2qyCqGMY8i6F-zXfLQRYMkzVSHBqre6g0CkYq9mmziU Min-Seq: 1 Content-Length: 0
The response reports the lifetime granted and the responder's tail, so
the sender can tell before disconnecting whether it is about to strand
an unacknowledged append. Session-TTL in the response MAY be
shorter than requested; a server that will not retain the dialog at all
responds 404 and terminates it. After a 200 the
sender closes its channels and moves to SUSPENDED.¶
RESUME re-opens a suspended dialog. It is the entry point to
alignment, and its outcomes are specified in
Section 6.6.1. Required fields: ADID,
CSeq, Tail-Seq, Tail-Hash. Optional:
Min-Seq, Session-TTL.¶
The example below is the ordinary recovery case, and it exercises the
pending-entry rule. The client sent the APPEND of
Figure 7 and lost the connection before the response
arrived, so the entry stayed pending and its tail is still 89. The
server received and committed it.¶
RESUME https://agent.example.com/travel ACP/1.0 ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d CSeq: 701 RESUME Authorization: Bearer eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOi... Tail-Seq: 89 Tail-Hash: sha256-_NrWKsIcH0PxTqjFxzYbg4w8ad2vogH1iWwLtcffWBQ Min-Seq: 1 Session-TTL: 259200 Content-Length: 0 ACP/1.0 206 Partial Content CSeq: 701 RESUME ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d Tail-Seq: 90 Tail-Hash: sha256-2qyCqGMY8i6F-zXfLQRYMkzVSHBqre6g0CkYq9mmziU Min-Seq: 1 Action: replay Replay-From: 90 Content-Length: 0
The server now replays entry 90 on the log plane. It is the entry of
Figure 7, whose digest the client already holds as
its pending entry, so the replay settles that entry as committed rather
than duplicating it. Once the client has committed up to
Tail-Seq 90 both participants are ACTIVE with no
further exchange. Had the client instead sent new input while offline,
it would renumber and resubmit it now
(Section 6.1.1).¶
RESUME MUST be authenticated and authorized against the
dialog. A server MUST NOT reveal whether an ADID exists to a sender it
has not authenticated; to one it has authenticated but that is not a
participant of the dialog it responds 403 rather than
404, and to an unauthenticated sender it responds
401 (Section 8.1).¶
PROBE locates the last position at which two diverged replicas
agree, after a 409. The procedure is specified in
Section 6.6.2. Required fields: ADID,
CSeq, and at least one Proof.¶
In the example, the client wrongly treated its entry 89 as committed while the server holds a different entry 89, produced after the hold expired and was extended. Both agree at 88.¶
RESUME https://agent.example.com/travel ACP/1.0 ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d CSeq: 702 RESUME Authorization: Bearer eyJhbGciOiJFUzI1NiJ9.eyJzdWIiOi... Tail-Seq: 90 Tail-Hash: sha256-2qyCqGMY8i6F-zXfLQRYMkzVSHBqre6g0CkYq9mmziU Min-Seq: 1 Content-Length: 0 ACP/1.0 409 Conflict CSeq: 702 RESUME ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d Tail-Seq: 90 Tail-Hash: sha256-0pNGg9pstIfSFkwGd466m1ObAHFrwyy2cp7nGHgrTIU Min-Seq: 1 Action: probe Content-Length: 0 PROBE https://agent.example.com/travel ACP/1.0 ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d CSeq: 703 PROBE Proof: 89=sha256-_NrWKsIcH0PxTqjFxzYbg4w8ad2vogH1iWwLtcffWBQ Proof: 88=sha256-ej6bM05D6ILtYQ7s78oWlq87P_1UAdmlri_92oCZUcM Content-Length: 0 ACP/1.0 200 OK CSeq: 703 PROBE ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d Proof: 88=sha256-ej6bM05D6ILtYQ7s78oWlq87P_1UAdmlri_92oCZUcM Tail-Seq: 90 Tail-Hash: sha256-0pNGg9pstIfSFkwGd466m1ObAHFrwyy2cp7nGHgrTIU Min-Seq: 1 Content-Length: 0
The highest position the server confirmed is 88, so that is the common ancestor. The client moves its entries 89 and 90 back to pending, accepts a replay of the server's 89 and 90, and then renumbers its own input to 91 against the tail it has just been given. Only two rungs were needed here; a search that has to go further adds 86, 82, 74, and so on, widening the step each time, and Section 6.6.2 bounds the whole ladder at sixteen proofs. The digests at 88, 89, and 90 are the ones the construction of Section 6.3 produces for the bodies shown in Section 5.4.2, so those rungs can be recomputed.¶
CHECKPOINT commits a prefix of the log to one entry so that
the prefix can be discarded. The construction is in
Section 6.5. Required fields: ADID,
CSeq, and a body carrying Lower and Upper.
Either participant may propose one; the ordering authority appends the
entry.¶
CHECKPOINT https://agent.example.com/travel ACP/1.0 ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d CSeq: 801 CHECKPOINT Content-Type: text/plain; charset=utf-8 Content-Length: 78 acp-checkpoint/1 88 90 sha256-eMqjKss9gHwczrgROqwJKOhAc04c0G30WVgbvF9AJ48 ACP/1.0 200 OK CSeq: 801 CHECKPOINT ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d CP-Seq: 91 CP-Hash: sha256-4eYccg4zhfuEkqtJWcZwf91PJUsFTps8TFd-fZk6Js8 Tail-Seq: 91 Tail-Hash: sha256-4eYccg4zhfuEkqtJWcZwf91PJUsFTps8TFd-fZk6Js8 Min-Seq: 91 Content-Length: 0
The checkpoint occupies Sequence 91 and is an ordinary entry,
so CP-Hash and Tail-Hash are the same value and the
next entry above it links to it as to any predecessor. Both
participants may now discard entries 88 to 90, and
Min-Seq becomes 91. A recipient that cannot reproduce the
accumulator responds 409 and discards nothing. A server MUST
NOT accept a CHECKPOINT from a sender it has not authorized
for the dialog, and a client MAY refuse one it did not propose.¶
TERMINATE ends a dialog and permits both participants to purge
its state. Required fields: ADID, CSeq. Optional:
Reason.¶
TERMINATE https://agent.example.com/travel ACP/1.0 ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d CSeq: 901 TERMINATE Reason: task-completed Content-Length: 0 ACP/1.0 200 OK CSeq: 901 TERMINATE ADID: 9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d Content-Length: 0
After a 200 both participants are TERMINATED: the log
MAY be erased, the ADID MUST NOT be reused, and a later
RESUME receives 404. A participant MAY terminate
unilaterally, and a server MAY terminate on Session-TTL expiry
without asking; the other participant learns of it on its next
RESUME. Termination is therefore not a negotiation, and an
application that needs both sides to agree before discarding history
must arrange that above ACP.¶
Purging is OPTIONAL and is a matter of local policy. A participant subject to a retention obligation MAY keep its replica after termination; doing so does not keep the dialog alive.¶
A status code is three digits and a reason phrase. The reason phrase is
descriptive only and a recipient MUST NOT parse it. ACP status codes
occupy the same numeric space as HTTP [RFC9110] and SIP
[RFC3261] and several deliberately echo them, but ACP
defines its own code space and the meaning of a code is the one given
here. In particular 206 and 412 are not HTTP's
byte-range responses: they report the outcome of an alignment attempt.¶
Class 1xx is reserved for provisional responses. This version
defines none, but a recipient MUST accept and ignore one. A recipient
that receives a code it does not recognize MUST treat it as the default
for its class: 2xx as success, 3xx as a redirection it
cannot follow, 4xx as a permanent failure of the request, and
5xx as a failure of the recipient that MAY succeed later.¶
| Code | Meaning |
|---|---|
| 200 | The request succeeded. |
| 206 | Resumption accepted; the responder is ahead and will replay from Replay-From (Section 6.6.1). |
| 400 | The message could not be parsed, a required field is missing, or a digest names an unsupported algorithm. |
| 401 | The sender is not authenticated. |
| 403 | The sender is authenticated but is not a participant of this dialog, or is not permitted this operation. |
| 404 | No dialog with this ADID, or it has terminated or its Session-TTL expired. |
| 408 | The request was not completed in time. |
| 409 | The replicas disagree: an APPEND does not extend the authoritative tail, a digest did not verify on replay, or a checkpoint accumulator could not be reproduced. For RESUME, proceed with PROBE (Section 6.6.2). |
| 412 | The sender's position is below the responder's Min-Seq; align from the checkpoint instead (Section 6.6.3). |
| 413 | The body is larger than the recipient accepts. |
| 415 | The media type is not supported for this dialog. |
| 422 | The message is well formed but the received Entry-Hash does not match the octets received (Section 6.3). |
| 486 | The agent has no capacity for another dialog now; the request MAY succeed later. |
| 488 | The offered modalities cannot be served (Section 5.4.1). |
| 500 | The recipient failed internally. |
| 501 | The method is not implemented. |
| 503 | The recipient is unavailable; the request MAY succeed later. |
Class 3xx is undefined in this version. Redirecting a dialog is
not a simple matter of following a pointer, because the log lives where
the dialog lives; a mechanism for it is deferred to a later revision.¶
A dialog's durable state is its log: an ordered sequence of entries,
each occupying one Sequence number, each carrying a digest of
itself and of the entry before it. Both participants hold a replica.
The log is append-only: an entry that has been committed is never
edited, and a committed Sequence number is never reused. What
changes history is not editing but truncation from the front
(Section 6.5), which is itself a committed act.¶
The log holds protocol state, not conversation. An entry may carry a message body, but nothing in ACP interprets it: the body is opaque, and an application is free to put a transcript, a tool call, or a capability description in it. What ACP requires is that the body be an octet string both participants can digest identically. Media is handled by reference rather than by inclusion (Section 6.4).¶
Sequence is a positive integer. The first entry of a dialog is
1 and each subsequent entry is one greater, with no gaps in the
authoritative log. The ordering authority for a dialog is its server:
its replica defines what the log is, and a client replica that differs
from it is wrong and is repaired
(Section 6.6).¶
A participant that authors an entry proposes the next
Sequence and its Prev-Hash. The ordering authority
accepts the entry only if it extends its own tail exactly, that is,
only if the proposed Sequence is one greater than its current
tail and the proposed Prev-Hash equals that tail's
Entry-Hash. If it does, the entry is committed at that
position and the response echoes both. If it does not, the entry is
rejected with 409 and nothing is written; the author must
align, then renumber its entry against the tail it learns, recompute
the digest, and resubmit. Rejecting rather than silently renumbering
keeps the digest of a committed entry stable: an entry is committed
once, at one position, with one digest, and that digest is what the
next entry's Prev-Hash refers to.¶
+------+ +------+ +------+ +------+ | e.88 |===>| e.89 |===>| e.90 |===>| e.91 | +------+ +------+ +------+ +------+ Prev=H87 Prev=H88 Prev=H89 Prev=H90 Hash=H88 Hash=H89 Hash=H90 Hash=H91
An APPEND that has been sent but not finally answered leaves
its entry pending. A pending entry is not committed.
Its author:¶
MUST NOT report it as its tail in Tail-Seq and
Tail-Hash;¶
MUST NOT use its Entry-Hash as the Prev-Hash of a
later entry;¶
MUST retain it, with its digest, until alignment settles whether the ordering authority committed it; and¶
MUST NOT present it to its application as part of the dialog history without marking it unconfirmed.¶
Pending entries are the ordinary source of divergence. A client whose
APPEND timed out at T1 cannot know whether the entry was
committed before the connection died, and a server that crashed may
have committed it without answering. This is why alignment compares
digests rather than counts (Section 6.6.1): a count
says the two replicas are the same length, which is not the same
statement as that they are the same log.¶
Once alignment establishes the ordering authority's tail, each
pending entry is settled by comparison. If the authority holds an
entry at that Sequence with that Entry-Hash, the
pending entry was committed and becomes part of the replica. If the
authority's entry at that Sequence differs, or there is
none, the pending entry was not committed: its author renumbers it
against the tail, recomputes its digest, and resubmits it. Nothing is
lost and nothing is duplicated, which is the property that makes an
unacknowledged append safe to retry.¶
A log entry is the unit of commitment, and ACP requires that it also be a unit of meaning: an entry closes exactly one complete event of the application that produced it, and an incomplete event is never committed. One turn of conversation, one tool invocation together with its result, one accepted user utterance, one cancellation are each a complete event. Half of a turn is not an event and MUST NOT become an entry.¶
This rule is what makes the chain usable under streaming. An agent
generating a long answer emits it as fragments on a data channel; the
fragments carry no ACP header, no Sequence, and no digest
(Section 3.2). Nothing is hashed while they flow. When
generation ends, the author gathers the completed event into one body
and appends one entry, so the cost of the chain is one digest per event
rather than one per fragment. A connection that dies in the middle of
generation therefore loses fragments that were never part of any entry:
the chain is untouched, both tails still point at the last committed
entry, and alignment has nothing to reconcile
(Section 6.6). The alternative, digesting fragments as
they arrive, would put a hash computation in the per-token path and
would leave a truncated fragment committed in a chain that both peers
then have to agree to forget.¶
Two interruption cases look alike on the wire and are worth separating.
If the connection dies while the author is still gathering the event, no
APPEND was sent, no entry exists on either side, and the
fragments are simply dropped. If it dies after the author sent the
APPEND but before the response arrived, the entry is pending
(Section 6.1.1): the peer may or may not have committed it,
and alignment settles which by comparing digests. In neither case is a
partial event committed, and in neither case does a participant have to
guess how many octets of an unfinished stream the other one received.¶
An author MAY record which boundary an entry closes by including
Event-Cursor, whose value is a token
[RFC9110] meaningful to the application, such as
turn-complete or tool-result-3. ACP neither defines
the token space, nor requires the field, nor interprets it. Its use is
local and diagnostic: it tells an application which step of its own
execution a given Sequence corresponds to, which is what it
needs to re-run the right step after a rollback
(Section 6.6.2) or to continue at the right point after an
interruption.¶
Event-Cursor is deliberately not part of the digest input
(Section 6.3). Alignment compares digests and sequence
numbers; it never compares cursors. Two replicas holding identical
bytes hold the same log whatever their cursors say, and a cursor inside
the digest would let an application-level label cause a 422 on
an otherwise valid entry. A CANCEL MAY carry the cursor of the
boundary at which the sender wants work to stop; that is a hint about
where to cut, not a position in the log, and the normative target of a
cancellation remains Target-Seq (Section 5.4.3).¶
Each entry carries Entry-Hash, the digest of a fixed octet
string built from six field values and the body. The construction is
the whole basis of alignment, so it is stated once and precisely.¶
digest-input = ADID-value LF
Prev-Hash-value LF
Sequence-value LF
Entry-Type-value LF
Author-value LF
Timestamp-value LF
body-octets
Entry-Hash = "sha256-" base64url( SHA-256( digest-input ) )
Each -value is the corresponding header field value exactly
as it appears in the message, after stripping leading and trailing
OWS, and without the field name or the colon. body-octets is
the body, exactly Content-Length octets, or the octets
between the blank line and the end of the stream where
Content-Length is omitted as Section 5.1 allows.
An empty body contributes zero octets. The separator is LF (0x0A),
not CRLF: the digest input is deliberately not a substring of any
message, so that no part of a wire encoding can be mistaken for it.
SHA-256 is as specified in [RFC6234] and base64url in
[RFC4648], Section 5, without padding.¶
For the entry at Sequence 1 there is no predecessor. Its
Prev-Hash is the constant GENESIS, defined as
sha256- followed by 43 A characters, which is the
base64url encoding of 32 zero octets. GENESIS is a constant
and not a digest of anything; it MUST NOT be interpreted as an entry.
A log with no entries is described in the position fields as
Tail-Seq: 0 with Tail-Hash equal to
GENESIS, which keeps the invariant that the entry at
Sequence n carries the digest of n - 1 in its
Prev-Hash true for n = 1 without a special case.¶
Why these six fields and not the whole message. A digest over the complete header block would depend on field order, on OWS, and on fields that legitimately differ between the original transmission of an entry and its replay, so two participants could hold the same entry and compute different digests. That is the failure this construction exists to avoid. Why these six and not the body alone. A digest over the body would leave the entry's position, its predecessor, its type, and its author uncommitted, so an entry could be relabelled, reordered, or moved to another dialog without changing its digest. Binding the ADID in particular is what stops an entry from one dialog being replayed into another at the same position. The six fields are exactly those whose meaning a verifier must be able to trust.¶
Fields not in the digest input are protected only by the channel. That
is acceptable for CSeq, which is per transaction and differs
when an entry is resubmitted, for Event-Cursor, which labels
the boundary an entry closes rather than anything the entry asserts
(Section 6.2), and for Content-Length, which
the recipient can check against what it received. It is also acceptable
for the message-level Content-Type, because a body that needs
typed parts carries them as MIME parts [RFC2046] whose
own Content-Type headers are inside the body and therefore
covered. An implementation that needs the message-level media type to
be committed SHOULD repeat it as the first body part header.¶
A recipient of an APPEND MUST recompute the digest and compare
it with the received Entry-Hash before committing the entry.
On mismatch it MUST NOT commit, MUST respond 422, and SHOULD
treat the dialog as requiring alignment, since a mismatch means the two
participants disagree about the octet string and not merely about a
checksum.¶
The digest of an entry depends only on octets its author already held, so it can be computed once and verified anywhere, including by a participant that never saw the original transmission. It is not a signature. A digest proves that two replicas hold the same bytes; it does not prove who produced them, and a participant that can write to the authoritative log can rewrite history and produce consistent digests. Per-entry signatures are deferred to a later revision.¶
A dialog that ran for hours of voice and video would have an unusable log if every entry digested its media. ACP therefore keeps media out of the chain, in two ways according to whether the media is stored or live.¶
Stored media is carried by reference. A MIME part MAY omit its content
and carry instead an Asset-Ref parameter giving the
hash-value of the asset's octets and an Asset-Size
giving their length:¶
--acp-9b1deb4d Content-Type: video/mp4 Asset-Ref: sha256-GIgdzwklKPw4gUd4U32RfGAuSrr5MEpdNK5NP3bMj78 Asset-Size: 104857600 Content-Location: https://agent.example.com/acp-asset/9b1deb4d/a3
The reference is text inside the body, so the digest commits to it, and
the asset's octets are transferred separately on an asset channel
(Section 7) and never enter the digest input. A
recipient that retrieves the asset MUST verify it against
Asset-Ref. A consequence worth stating plainly: a log can be
fully aligned and every digest verified while an asset is still
missing, and an asset can be lost without invalidating the chain. That
is intended. The chain records what the dialog said about its media,
which is the part that has to survive days of separation; the media
itself has its own storage and its own lifetime.¶
Live media is not an asset and is not referenced either. It flows on
its own channels while the dialog is active, and the event that opens
or closes a media session is recorded as a meta entry
describing it (Section 6.2), for example:¶
Session-Type: realtime-voice Media-Status: active Active-Tracks: audio-01 (opus/48000/2) Transport-Mapping: wt-session 0xef4a
Such an entry is a few hundred octets, costs one digest, and is enough
to re-establish the media path after an interruption. ACP does not
define the fields of a meta body, does not transcribe speech,
and does not summarize a conversation: those are application concerns,
and an application that wants a transcript in the log appends an entry
containing one. Keeping them out is what lets the signaling path stay
short enough not to interfere with the media it is setting up.¶
A dialog may run for months. Its log must not grow without bound, so
ACP allows a prefix of it to be committed to a single entry and then
discarded. A checkpoint is itself an entry, of Entry-Type
system, appended at the next Sequence like any other.
Making it an entry rather than a side structure is deliberate: it keeps
the chain continuous across truncation, it means a checkpoint has an
Entry-Hash of exactly the same kind as any other entry, and it
means a participant that has lost everything below a checkpoint can
still resume by naming that one entry
(Section 6.6.3).¶
The body of a checkpoint entry is four lines:¶
body = "acp-checkpoint/1" CRLF Lower CRLF Upper CRLF
accumulator CRLF
Lower = seq ; first Sequence covered, inclusive
Upper = seq ; last Sequence covered, inclusive
accumulator = "sha256-" base64url( SHA-256( cp-input ) )
cp-input = for i = Lower to Upper, in increasing order:
Entry-Hash-value(i) LF
The body is ordinary message content, so its line endings are CRLF like
the rest of a message. cp-input is not a wire format: it is the
octet string the accumulator is computed over, and its separator is LF,
as in Section 6.3.¶
where Entry-Hash-value(i) is the complete textual
hash-value of the entry at Sequence i, and
Lower is the proposer's current Min-Seq, which is 1
for a dialog that has never been checkpointed. Checkpoints therefore
compose: a checkpoint whose Lower is an earlier checkpoint's
Sequence is well defined, and a participant that has already
truncated can still take part in a later one.¶
A CHECKPOINT transaction proposes Lower and
Upper. The recipient recomputes accumulator from the
entries it holds. If it matches, the ordering authority appends the
checkpoint entry and responds 200 with CP-Seq set to
that entry's Sequence and CP-Hash to its
Entry-Hash. Both participants record the entry durably, and
only then MAY either discard entries with Sequence below
CP-Seq; Min-Seq becomes CP-Seq. If the
recipient cannot recompute accumulator, because the digests
disagree or because it has already discarded part of the range without
holding a checkpoint that covers it, it MUST NOT discard anything and
MUST respond 409, or 412 where
Section 6.6.3 applies. Discarding before the peer has
confirmed is not recoverable and MUST NOT be done.¶
Sequence numbers are never reset by a checkpoint, and the
checkpoint entry is retained: the entry above it carries its
Entry-Hash as Prev-Hash, so the chain above a
checkpoint links to the chain below it through a single digest. A
dialog checkpointed a hundred times still has one continuous
numbering. A participant holding only the latest checkpoint and the
entries above it can still verify every entry it holds, and a forged
prefix would require inverting the checkpoint entry's digest.¶
Alignment is what makes a dialog survive its transport. A participant that reconnects after an hour or after a week runs the same procedure, and the cost depends on how much it missed rather than on how long it was away. The procedure has three outcomes, decided by one exchange: the replicas agree, the client is behind, or they diverged. A fourth case, where the client is behind by more than the server retains, is handled by Section 6.6.3.¶
The client sends RESUME with Tail-Seq and
Tail-Hash giving its last committed entry, and with
Min-Seq. It MUST NOT report a pending entry as its tail
(Section 6.1.1). The server then applies the first rule that
matches:¶
| # | Condition | Response |
|---|---|---|
| 1 | ADID unknown, terminated, or TTL expired |
404
|
| 2 | Sender not authenticated or not authorized for this dialog |
401 or 403
|
| 3 | Client Tail-Seq is below server Min-Seq
|
412 with Action: resync
|
| 4 | Server holds no entry at the client's Tail-Seq, or holds one with a different digest |
409 with Action: probe
|
| 5 | Tails equal |
200
|
| 6 | Server tail ahead of client tail |
206 with Action: replay and Replay-From
|
The order matters. Rule 3 precedes rule 4 because a client below
Min-Seq cannot be probed: the entries needed to find a common
ancestor are gone. Rules 5 and 6 are reached only when the digests
match, which is the point of comparing digests rather than lengths.¶
Every response to RESUME carries the server's
Tail-Seq, Tail-Hash, and Min-Seq, and
CP-Seq and CP-Hash where a checkpoint exists, so the
client learns the authoritative position and any truncation in the same
exchange.¶
On 206 the server replays entries from
Replay-From, which is the client's Tail-Seq plus one,
up to and including its own tail, in increasing Sequence order,
on the log plane. The recipient verifies each entry's digest and that
its Prev-Hash equals the Entry-Hash of the entry below
it, and commits each as it arrives; replay is driven by the transport's
ordering, not by per-entry acknowledgement. Alignment is complete when
the entry at the Tail-Seq announced in the 206 is
committed, so no further message is needed, the target having been
announced in advance. If T2 expires first, the attempt fails and MAY be
retried with a new RESUME.¶
On 200 or on completion of a replay, both participants are
ACTIVE. The client then settles any pending entries as
Section 6.1.1 describes, renumbering and resubmitting those
the server did not commit.¶
A 409 means the two replicas held the same dialog and now
disagree about it. The cause is usually benign: an APPEND whose
acknowledgement was lost, a server that recovered from a crash behind
its last flush, or a client that committed an entry it should have left
pending. Resolution is to find the last position at which the two agree
and rebuild above it.¶
The client sends PROBE carrying a ladder of Proof
fields: one for each of the positions Tail-Seq - 1,
Tail-Seq - 2, Tail-Seq - 4, and so
on by powers of two down to its Min-Seq, one for
Min-Seq itself, and at most 16 in total. The server responds
200 with Proof fields for exactly those positions
whose digest it also holds, together with its own Tail-Seq,
Tail-Hash, and Min-Seq. It MUST NOT assert a pair it
does not hold, and it need not explain the absence of one.¶
The highest matching position is the common ancestor. The client moves
every entry it holds above it back to pending, accepts a replay from
the position above it, and then renumbers and resubmits its pending
entries against the tail it has been given. Because a renumbered entry
has a different Sequence and a different Prev-Hash, it
has a different digest: the two versions of the same input are distinct
entries, and only the one the ordering authority committed is in the
log. No content is lost, and the chain above the ancestor is the
server's alone.¶
A 16-pair ladder reaches 215 entries below the tail, so one
PROBE settles almost every divergence. Where it does not, the
client continues from the ancestor it found with a further
PROBE. If no pair matches, the divergence is below everything
the ladder reaches and the server responds 412 instead;
Section 6.6.3 applies. A server MUST rate-limit
PROBE per dialog, since each one is a read against durable
storage and none of them makes forward progress.¶
A participant that has been away long enough to fall below the other's
Min-Seq cannot be aligned entry by entry, because the entries
are gone. A 412 carries Action: resync together with
Min-Seq, CP-Seq, CP-Hash,
Tail-Seq, and Tail-Hash. Because a checkpoint is an
ordinary entry (Section 6.5), CP-Hash is the
Entry-Hash of the entry at CP-Seq and can be adopted
as a tail directly, with no second kind of digest value anywhere in the
protocol.¶
The recipient first checks whether it holds the entry at
CP-Seq. If it does and the digest matches, its own history is a
prefix of the authoritative one and it simply discards what is below and
continues. If it does not hold that entry, or the digest differs, its
history below CP-Seq is unreconcilable: it MUST stop treating
those entries as part of the dialog, SHOULD retain them in local storage
if its own policy permits, since they may be the only record it has of
what it was told, and then adopts the checkpoint as its base by setting
Min-Seq and Tail-Seq to CP-Seq and
Tail-Hash to CP-Hash. A second RESUME then
proceeds normally and is answered 200 or 206.¶
A participant that adopts a checkpoint it could not verify is trusting the ordering authority. That is not a new trust: it already relies on that authority for every entry it was given. What the checkpoint changes is that the reliance can no longer be checked against the discarded prefix. A signed checkpoint would remove the reliance and is deferred to a later revision.¶
ACP is transport-independent, and a binding is the mapping of its channels onto one transport. A binding MUST provide:¶
a reliable, ordered, bidirectional channel for the control plane, on which many messages may follow one another;¶
a reliable, ordered channel for the log plane that does not share a
head-of-line queue with the control plane, so that a large entry
cannot delay a CANCEL;¶
a way to carry asset octets without blocking either of the above; and¶
a signal when the connection is lost, so that a participant can move
to SUSPENDED (Section 4).¶
A binding SHOULD also provide an unreliable low-latency channel for
progress traffic, and SHOULD support connection migration, which lets a
dialog stay ACTIVE across a change of network path. A binding
that provides neither is still conformant; progress is then carried on
the log channel and simply not logged, and migration is then no better
than a suspend and resume.¶
Every stream a binding opens for ACP begins with a preamble that says what the stream carries:¶
channel-preamble = "ACP/1.0" SP channel-name CRLF channel-name = "control" / "log" / "asset" / token
A recipient MUST read the preamble before treating a stream as ACP and
MUST reset a stream whose preamble it does not recognize. The preamble
is what identifies a channel, and no binding may identify one by a
stream number instead: transports assign stream identifiers, and an
application cannot choose one. Messages on the control channel
are delimited by Content-Length, which is REQUIRED there. On a
log channel exactly one entry message is sent per stream and
the end of the stream delimits it, so Content-Length MAY be
omitted; the octets that follow the preamble are the entry exactly as
its author digested it, and a recipient MUST NOT re-encode them before
verifying the digest.¶
This binding carries the log plane on Media over QUIC Transport (MoQ) [I-D.ietf-moq-transport]. It is the binding to use where a dialog should be relayable and cacheable by infrastructure that understands MoQ, and where recovery of missed entries should be a subscription rather than an application-level replay.¶
A dialog occupies one track namespace:¶
{ authority, "acp", adid }
where authority is the domain of the agent service that
publishes the dialog and adid is the ADID, which uses only
URI-unreserved characters and so needs no further encoding. The literal
"acp" keeps these namespaces distinct from other uses of the
same authority. Because the ADID is in the namespace, a relay forwards,
caches, and correlates every track of a dialog without decrypting or
interpreting a payload, and a peer that sees the same namespace on a
new session can correlate it with the dialog it already knows.¶
| ACP | MoQ |
|---|---|
| Dialog | Track namespace {authority, "acp", adid}
|
| Authoritative log | The track named log, published by the ordering authority |
| One committed entry | One object on log, with Group equal to Sequence and Object 0 |
| Control plane | A bidirectional stream in the same session, carrying the control preamble |
| One asset or media track | A track of its own in the namespace, named by the application |
| Replay after resumption |
SUBSCRIBE to log starting at Group Tail-Seq + 1 |
Two points in this mapping need stating, because both are places where an obvious alternative does not work.¶
A committed entry is carried as a single object rather than split across one object per MIME part. Splitting would require a recipient to reassemble parts before it could verify the digest, and any reassembly that is not byte-exact turns a verified entry into an unverified one. One object per entry means the object payload is the octet string the author digested, and verification is a single pass over what arrived. Bulk content is separated by the asset reference of Section 6.4, not by the object structure.¶
The control plane does not ride MoQ control messages. MoQ data tracks flow from publisher to subscribers, and putting ACP requests inside MoQ control messages would require an extension to MoQ that this document cannot define. Using a bidirectional stream in the same session needs no extension, keeps the preamble framing of Section 7.1, and lets both participants send requests. Whether a native MoQ control-message binding is worth defining is left open.¶
Only the ordering authority publishes log, and it publishes
every committed entry including those a client authored. A single
authoritative track is what makes recovery a subscription: the client
subscribes from the group above its tail and receives exactly the
entries it is missing, in order, from whatever cache can serve them.
Relay caching is an optimization and not a store. The ordering authority
MUST be able to serve the whole retained range itself, and a subscriber
MUST NOT treat an object as committed until its digest verifies.¶
This binding runs ACP over WebTransport [I-D.ietf-webtrans-http3], and therefore over HTTP/3 [RFC9114] and QUIC [RFC9000]. It is the binding to use where one endpoint is a browser or another environment that offers WebTransport but not a general QUIC stack.¶
The request target of Section 5.4.1 is already an
https origin and path, so the client opens a WebTransport
session to it directly with an extended CONNECT. The
session is the dialog's transport for as long as it lives, and it may
carry more than one dialog, since every stream is identified by its
preamble and every message by its ADID.¶
| ACP | WebTransport |
|---|---|
| Control plane | The first bidirectional stream the client opens in the session, carrying the control preamble |
| One log entry | One unidirectional stream opened by the entry's author, carrying the log preamble, closed when the entry is complete |
| One asset | One unidirectional stream carrying the asset preamble, at lower priority than log
|
| Progress traffic | QUIC datagrams [RFC9297] |
| Connection loss | Session closure or abort |
| Change of network path | QUIC connection migration; the dialog stays ACTIVE
|
Because a log entry occupies its own stream, an entry carrying a large
body cannot delay the control plane or another entry, and several
entries and assets may be in flight at once. A sender SHOULD set stream
priority so that control outranks log and
log outranks asset.¶
Progress datagrams are limited in size by the path MTU and are neither delivered nor ordered. A binding MUST NOT put anything on them that the log or the state machine depends on, and a recipient MUST tolerate losing any of them. Where datagrams are unavailable, progress traffic MAY be sent on the control channel, in which case it MUST still be excluded from the log.¶
ACP defines no key exchange, no authentication mechanism, and no confidentiality mechanism of its own. It is a session protocol that assumes it is given a channel with three properties: the peer is authenticated, the channel is confidential and integrity-protected, and messages are not delivered to anyone else. Every binding in Section 7 provides these through QUIC [RFC9000], which mandates TLS 1.3. ACP MUST NOT be used over a transport that does not provide them, and an implementation MUST NOT fall back to such a transport.¶
Who a participant is, and what it may do, are decided above and below ACP. Below it, the channel tells each endpoint who the peer is; a deployment identifying agents by workload uses a workload identity [I-D.ietf-wimse-workload-creds]. Above it, the ordering authority decides whether an authenticated peer may append to a given dialog. ACP assumes both decisions have been made correctly and does not re-make them.¶
An ADID is 128 bits drawn from a random source [RFC4086], so it cannot be guessed at any useful rate. That property is doing real work: several places in this document answer a request about a dialog the sender is not a participant of, and rely on the sender not having found the dialog by searching. An implementation MUST NOT generate ADIDs from a counter, a timestamp, or any other low-entropy source, and MUST NOT reuse one.¶
Knowledge of an ADID is nonetheless not authority to use it. A server MUST authorize every request against the dialog's participants independently of whether the request names the dialog correctly, and MUST NOT treat a correct ADID as proof that the sender belongs to the dialog. Because an ADID is a bearer string that appears in every message, it will end up in logs, proxies, and error reports; a deployment SHOULD treat a leaked ADID as a reason to terminate and re-initiate rather than as a harmless disclosure.¶
Section 5.4.5 has a server answer 403 to a sender
it recognizes and 404 to one it does not, which reveals that
the dialog exists. That is deliberate: a client recovering on a new
device needs to know whether to resume or to start over. A server
concerned about the disclosure MAY answer 404 to every sender
it has not authorized, at the cost of making recovery from a lost
credential indistinguishable from a lost dialog.¶
Establishing a dialog requires the client to obtain authorization, and
ACP does that with existing OAuth mechanisms rather than defining one
of its own. The token it obtains is carried in the
Authorization field of Section 5.2.¶
Within a trust domain the client presents a Transaction Token [I-D.ietf-oauth-transaction-tokens], obtained from the workload identity it already holds. A Transaction Token carries the delegation context of one transaction, not the standing authority of a workload, which is what lets the ordering authority authorize a dialog against the principal the client acts for and not only against the workload that asked. Its narrow scope and short lifetime are also what make it safe to carry in a header field that will end up in logs.¶
Across trust domains there is no issuer both sides accept, so the client instead trades the credential it holds for one the remote domain accepts using OAuth 2.0 Token Exchange [RFC8693], naming the remote service as the audience.¶
Because the token in Authorization is not the credential the
channel authenticated, the two must be tied together: the workload
identity the channel authenticated, the From of the request,
and the identity the token authorizes MUST all name the same
participant (Section 3.5). A server that cannot
establish that, or that cannot validate the token, answers
403. A token is minted per request, not per dialog: a
transaction token is short-lived, and a RESUME days after its
INITIATE is a new transaction carrying a new token.¶
This document requests that IANA create registries for the protocol's namespaces. The registries are listed here so that the namespace they govern is reserved; their registration procedures and initial contents will be specified in a later revision of this document.¶
IANA is requested to create an "Agent Communication Protocol (ACP) Parameters" registry group containing the registries in Table 11.¶
| Registry | Governs | Section |
|---|---|---|
| ACP Methods | Request method tokens | Section 5.4 |
| ACP Status Codes | Numeric response codes | Section 5.5 |
| ACP Header Fields | Header field names | Section 5.2 |
| ACP Entry Types |
Entry-Type values |
Section 6 |
| ACP Hash Algorithms |
hash-alg prefixes |
Section 6.3 |
| ACP Channel Names | Stream preamble channel names | Section 7.1 |
TBD.¶