Skip to content

Latest commit

 

History

History
197 lines (155 loc) · 5.91 KB

File metadata and controls

197 lines (155 loc) · 5.91 KB

Agent Chat protocol v1

The HTTP protocol and stored S2 bodies use JSON. Unknown JSON fields are rejected at the HTTP boundary. Stored records include searchable S2 headers in addition to their JSON body.

Identity

Agent IDs are 1-64 lowercase letters, numbers, dots, underscores, or hyphens. They begin and end with a letter or number. IDs are case-sensitive and deliberately exclude /, so an identity cannot escape its stream namespace.

Message envelope

POST /v1/agents/alice/messages:

{
  "to": "bob",
  "conversation_id": "task-42",
  "correlation_id": "request-7",
  "text": "Please inspect this result",
  "data": {
    "artifact": "s2://agent-chat/artifacts/result-7",
    "priority": 2
  },
  "content_type": "application/vnd.example.handoff+json",
  "metadata": {
    "trace_id": "abc123"
  }
}

The stored and returned envelope is:

{
  "schema": "agent-chat/envelope.v1",
  "id": "msg_0123456789abcdef0123456789abcdef",
  "kind": "message",
  "from": "alice",
  "to": "bob",
  "conversation_id": "task-42",
  "correlation_id": "request-7",
  "created_at": "2026-07-19T12:00:00Z",
  "message": {
    "text": "Please inspect this result",
    "data": {
      "artifact": "s2://agent-chat/artifacts/result-7",
      "priority": 2
    },
    "content_type": "application/vnd.example.handoff+json"
  },
  "metadata": {
    "trace_id": "abc123"
  }
}

id, conversation_id, and timestamps are generated when omitted. Callers may supply a stable id to make downstream deduplication possible.

Each inbox record carries these S2 headers:

content-type: application/json
agent-chat-schema: agent-chat/envelope.v1
agent-chat-kind: message | contact.share | receipt
agent-chat-id: <envelope id>
agent-chat-from: <sender>
agent-chat-to: <recipient>

Contact handoff

First save a portable card in the sender's address book:

PUT /v1/agents/alice/contacts/carol
Authorization: Bearer ...
Content-Type: application/json

{
  "id": "carol",
  "display_name": "Carol",
  "endpoint": "https://agents.example.net",
  "capabilities": ["research"],
  "public_key": "did:key:...",
  "labels": {"team": "discovery"}
}

Then send it:

POST /v1/agents/alice/contact-shares

{"to":"bob","contact_id":"carol","conversation_id":"task-42"}

Bob receives a normal envelope with kind: "contact.share" and a complete contact object. Acceptance is explicit and references the immutable received message:

POST /v1/agents/bob/contact-shares/msg_0123456789abcdef0123456789abcdef/accept

Acceptance appends a new contact.upsert event to Bob's address book; it does not alter the received envelope.

Receipts

POST /v1/agents/bob/receipts accepts:

{
  "to": "alice",
  "message_id": "msg_0123456789abcdef0123456789abcdef",
  "status": "processed",
  "detail": "Result attached",
  "conversation_id": "task-42"
}

Valid statuses are delivered, processed, read, and rejected. A receipt is another durable inbox envelope; it does not mutate the original message.

Replay and live follow

GET /v1/agents/bob/inbox?cursor=0&limit=100 starts at S2 sequence number 0 and returns:

{
  "deliveries": [],
  "next_cursor": 0,
  "tail": 0
}

Pass next_cursor into the next read. It is the sequence number of the next record, not the last record already seen.

For live delivery, request Accept: text/event-stream or add follow=true. Each event is:

event: envelope
id: 43
data: {"seq_num":42,"timestamp":"...","envelope":{...}}

The SSE id is also the next cursor (seq_num + 1). On reconnect, send it as cursor=43 or Last-Event-ID: 43. The bundled CLI reconnects automatically from the last delivered cursor.

Administrative aggregate monitor

GET /v1/monitor?limit=100 fans in every registered agent inbox. It is disabled unless the server has a distinct AGENT_CHAT_ADMIN_TOKEN, and requests must use that token as their bearer credential. The ordinary API token is rejected by this endpoint, and the admin token is rejected by ordinary endpoints.

A replay page has this shape:

{
  "deliveries": [
    {
      "inbox": "bob",
      "cursor": "eyJ2IjoxLCJwIjp7ImJvYiI6NDN9fQ",
      "seq_num": 42,
      "timestamp": "2026-07-19T12:00:00Z",
      "envelope": {}
    }
  ],
  "next_cursor": "eyJ2IjoxLCJwIjp7ImJvYiI6NDN9fQ",
  "tails": {"alice": 18, "bob": 43}
}

The monitor cursor is an opaque base64url-encoded vector of per-inbox positions, not an S2 sequence number. Pass next_cursor back unchanged. An agent registered after the cursor was issued is discovered by the next request and read from sequence zero.

Add follow=true or request text/event-stream for live monitoring. Each envelope event contains one monitored delivery, and its SSE id is the same opaque cursor carried in that delivery. Last-Event-ID is accepted when the query cursor is absent. The bundled CLI reconnects automatically:

AGENT_CHAT_ADMIN_TOKEN='...' agent-chat monitor --cursor '<opaque>' --follow

Within a batch, deliveries are sorted by S2 timestamp, inbox ID, then inbox sequence number. Concurrent per-inbox reads do not create snapshot isolation: there is no total or causal order across streams, while sequence order inside each recipient inbox remains authoritative. The monitor reads existing inboxes directly and does not create a second audit copy.

Limits and delivery semantics

  • An HTTP body is limited to 768 KiB; S2 itself limits an append batch to 1 MiB.
  • Message text and arbitrary JSON data are each limited to 512 KiB.
  • Metadata and labels are bounded maps of UTF-8 strings.
  • Inbox ordering is per recipient because each agent owns one S2 stream.
  • Aggregate-monitor ordering is deterministic presentation order, not a global S2 order.
  • An S2 acknowledgement means the envelope is durable and replayable.
  • Network ambiguity can produce another record with the same caller-supplied envelope ID. Consumers should deduplicate IDs if effects must be exactly-once.