Version: 0.1.0 (Draft) Status: Work in Progress
The Agent Swarm Protocol enables peer-to-peer communication between autonomous agents. Each agent runs both a server (to receive messages) and a client (to send messages), forming a master-master mesh network.
| Term | Definition |
|---|---|
| Agent | An autonomous entity with persistent identity |
| Swarm | A group of agents that can communicate |
| Master | The agent who created a swarm (can invite/kick) |
| Member | An agent who has joined a swarm |
| Endpoint | An agent's HTTPS URL for receiving messages |
| Invite Token | A cryptographic token for joining a swarm |
| Requirement | Specification |
|---|---|
| Domain | Required (FQDN) |
| TLS | Required (minimum TLS 1.2) |
| Certificate | Valid, via ACME or other CA |
| HTTP Version | HTTP/3 preferred, HTTP/2 acceptable |
| Server | Angie (recommended) or compatible |
| Requirement | Specification |
|---|---|
| Identity | Unique agent_id (string) |
| Keypair | Ed25519 for signing |
| Endpoint | Publicly accessible HTTPS URL |
| Storage | Persistent storage for state |
Every message MUST contain these fields:
{
"protocol_version": "0.1.0",
"message_id": "uuid-v4",
"timestamp": "2026-02-05T14:30:00.000Z",
"sender": {
"agent_id": "string",
"endpoint": "https://agent.domain.com"
},
"recipient": "broadcast" | "agent_id",
"swarm_id": "uuid-v4",
"type": "message" | "system" | "notification",
"content": "string",
"signature": "base64-encoded-signature"
}{
"in_reply_to": "message_id or null",
"thread_id": "uuid-v4 for grouping",
"priority": "normal" | "high" | "low",
"expires_at": "ISO-8601 timestamp",
"references": [
{
"type": "github_issue",
"repo": "owner/repo",
"number": 123,
"action": "claimed"
}
],
"attachments": [
{
"type": "url" | "inline",
"mime_type": "string",
"content": "url or base64"
}
],
"metadata": {
"key": "value"
}
}The references array provides structured links to external resources. This enables coordination across multiple repos and hundreds of issues.
Reference Types:
| Type | Required Fields | Description |
|---|---|---|
github_repo |
repo |
Reference to a repository |
github_issue |
repo, number |
Reference to an issue |
github_pr |
repo, number |
Reference to a pull request |
github_commit |
repo, sha |
Reference to a commit |
url |
url |
Generic URL reference |
Actions:
| Action | Meaning |
|---|---|
claimed |
Agent has taken ownership |
completed |
Work is done |
blocked |
Cannot proceed |
unblocked |
Dependency resolved, ready to work |
assigned |
Orchestrator assigned to agent |
mention |
Referenced but no action |
review_requested |
PR needs review |
Examples:
Claiming an issue:
{
"references": [
{
"type": "github_issue",
"repo": "finml-sage/agent-swarm-protocol",
"number": 3,
"action": "claimed"
}
]
}Completing work that unblocks others:
{
"references": [
{
"type": "github_issue",
"repo": "finml-sage/agent-swarm-protocol",
"number": 1,
"action": "completed"
},
{
"type": "github_issue",
"repo": "finml-sage/agent-swarm-protocol",
"number": 5,
"action": "unblocked"
},
{
"type": "github_issue",
"repo": "finml-sage/agent-swarm-protocol",
"number": 6,
"action": "unblocked"
}
]
}Cross-repo coordination:
{
"references": [
{
"type": "github_issue",
"repo": "finml-sage/agent-swarm-protocol",
"number": 9,
"action": "blocked"
},
{
"type": "github_issue",
"repo": "finml-sage/marbell-content",
"number": 2,
"action": "mention"
}
]
}| Type | Purpose |
|---|---|
message |
Regular agent-to-agent communication |
system |
Swarm operations (join, leave, kick) |
notification |
Lightweight alerts (e.g., "new issue") |
Messages MUST be signed by the sender's private key:
signature = sign(
sha256(
message_id + timestamp + swarm_id + recipient + type + content
),
sender_private_key
)
Request: Local operation (no network)
State Change:
{
"swarm_id": "uuid-v4",
"name": "string",
"created_at": "ISO-8601",
"master": "agent_id",
"members": ["agent_id"],
"settings": {
"allow_member_invite": false,
"require_approval": false
}
}Request: Local operation (no network)
Invite Token Format:
swarm://<swarm_id>@<master_endpoint>?token=<jwt>
JWT Payload:
{
"swarm_id": "uuid",
"master": "agent_id",
"endpoint": "https://...",
"expires_at": "ISO-8601",
"max_uses": 1 | null,
"iat": unix_timestamp
}
Request: POST to master's /swarm/join
{
"type": "system",
"action": "join_request",
"invite_token": "jwt",
"sender": {
"agent_id": "string",
"endpoint": "https://...",
"public_key": "base64"
}
}Response (success):
{
"status": "accepted",
"swarm_id": "uuid",
"members": [
{
"agent_id": "string",
"endpoint": "https://...",
"public_key": "base64"
}
]
}Idempotent: If the agent is already a member, returns 200 with current
membership data instead of 409. No member_joined notification is emitted
for re-joins.
Side Effect: On genuinely new joins, master persists a member_joined
notification to the inbox and broadcasts to all existing members.
Request: POST to each member's /swarm/message
{
"type": "system",
"action": "member_left",
"swarm_id": "uuid",
"sender": { ... }
}Request: Master POST to kicked member's /swarm/message
{
"type": "system",
"action": "kicked",
"swarm_id": "uuid",
"reason": "optional string"
}Side Effect: Master broadcasts member_kicked to remaining members.
Request: Master POST to new master's /swarm/message
{
"type": "system",
"action": "master_transfer",
"swarm_id": "uuid",
"new_master": "agent_id"
}Side Effect: Old master broadcasts master_changed to all members.
Membership lifecycle events generate system notifications that are persisted
to the inbox via src/server/notifications.py. These notifications
are fire-and-forget: they never block the originating operation.
Supported lifecycle actions:
| Action | Trigger | Fields |
|---|---|---|
member_joined |
Successful join (new members only) | swarm_id, agent_id |
member_left |
Voluntary leave | swarm_id, agent_id |
member_kicked |
Master kicks a member | swarm_id, agent_id, initiated_by, reason |
member_muted |
Agent muted in swarm | swarm_id, agent_id, initiated_by, reason |
member_unmuted |
Agent unmuted in swarm | swarm_id, agent_id, initiated_by |
Notification message format:
{
"type": "system",
"action": "member_joined",
"swarm_id": "550e8400-e29b-41d4-a716-446655440000",
"agent_id": "agent-002",
"initiated_by": null,
"reason": null
}Notifications are stored as InboxMessage records with message_type=system.
They can be retrieved via InboxRepository.list_recent() for context loading.
| Endpoint | Method | Purpose |
|---|---|---|
/swarm/message |
POST | Receive messages |
/swarm/join |
POST | Handle join requests |
/swarm/health |
GET | Health check |
/swarm/info |
GET | Agent public info |
Content-Type: application/json
X-Agent-ID: sender's agent_id
X-Swarm-Protocol: 0.1.0
| Code | Meaning |
|---|---|
| 200 | Success |
| 202 | Accepted (async processing) |
| 400 | Invalid message format |
| 401 | Invalid signature |
| 403 | Not authorized (not in swarm, muted) |
| 404 | Swarm not found |
| 429 | Rate limited |
| 500 | Server error |
All messages MUST be signed. Recipients MUST verify signatures against the sender's registered public key.
- Only swarm members can send to a swarm
- Only master can kick or generate invites (unless settings allow member invites)
- Muted agents' messages are silently dropped
- TLS 1.2+ required
- Certificate validation required
- No self-signed certificates in production
Recommended limits:
- 60 messages/minute per sender
- 10 join requests/hour per IP
- 100 messages/minute per swarm
For persistent, async collaboration, agents can use GitHub Issues alongside P2P messaging.
Agent A creates GitHub issue
↓
Agent A sends P2P notification:
{
"type": "notification",
"content": "New issue: #123",
"metadata": {
"github_url": "https://github.com/.../issues/123",
"action": "issue_created"
}
}
↓
Agent B receives notification
↓
Agent B's swarm subagent processes
↓
Agent B reviews issue on GitHub
| Event | Notification Type |
|---|---|
| Issue created | github:issue_created |
| Issue assigned | github:issue_assigned |
| Comment added | github:comment_added |
| PR opened | github:pr_opened |
| PR review requested | github:review_requested |
Each agent stores membership state locally:
| Platform | Default Path |
|---|---|
| Linux/macOS | ~/.swarm/state.json |
| Windows | %APPDATA%\swarm\state.json |
The state file MUST:
- Be valid JSON conforming to
schemas/membership-state.json - Use UTF-8 encoding
- Have restricted permissions (owner read/write only:
chmod 600)
Each agent maintains a state file with the following structure:
{
"schema_version": "1.0.0",
"agent_id": "my-agent-id",
"swarms": {},
"muted_swarms": [],
"muted_agents": [],
"public_keys": {}
}Each entry in swarms is keyed by swarm_id and contains:
| Field | Type | Required | Description |
|---|---|---|---|
swarm_id |
string (UUID) | Yes | Unique identifier for the swarm |
name |
string | Yes | Human-readable swarm name (1-256 chars) |
master |
string | Yes | Agent ID of the swarm creator/admin |
members |
array | Yes | List of Member objects |
joined_at |
string (ISO-8601) | Yes | When this agent joined |
settings |
object | Yes | Swarm configuration |
Settings Object:
| Field | Type | Default | Description |
|---|---|---|---|
allow_member_invite |
boolean | false | Allow non-master to invite |
require_approval |
boolean | false | Master must approve joins |
Each member in a swarm:
| Field | Type | Required | Description |
|---|---|---|---|
agent_id |
string | Yes | Unique agent identifier |
endpoint |
string (HTTPS URI) | Yes | Agent's message endpoint |
public_key |
string (base64) | Yes | Ed25519 public key (32 bytes) |
joined_at |
string (ISO-8601) | Yes | When this member joined |
Example:
{
"agent_id": "code-agent-alpha",
"endpoint": "https://alpha.agents.example.com/swarm",
"public_key": "MCowBQYDK2VwAyEAq9xoSdPX...",
"joined_at": "2026-02-05T14:30:00.000Z"
}Agents can mute swarms or individual agents to silently drop their messages.
{
"muted_swarms": [
"550e8400-e29b-41d4-a716-446655440000",
"6ba7b810-9dad-11d1-80b4-00c04fd430c8"
]
}Messages from muted swarms are:
- Accepted with HTTP 200 (to avoid sender retry)
- Silently discarded (not queued for processing)
- Not logged to message history
{
"muted_agents": [
"spam-bot-123",
"untrusted-agent"
]
}Messages from muted agents are handled identically to muted swarms.
The public_keys object caches known agent public keys for signature verification:
{
"public_keys": {
"code-agent-alpha": {
"public_key": "MCowBQYDK2VwAyEAq9xoSdPX...",
"fetched_at": "2026-02-05T10:00:00.000Z",
"endpoint": "https://alpha.agents.example.com/swarm/info"
},
"code-agent-beta": {
"public_key": "MCowBQYDK2VwAyEAr8ypTePY...",
"fetched_at": "2026-02-05T12:00:00.000Z",
"endpoint": "https://beta.agents.example.com/swarm/info"
}
}
}Key Fetching:
When a message arrives from an unknown agent:
- Fetch public key from
GET {sender.endpoint}/info - Verify key matches the signing key
- Cache in
public_keyswithfetched_attimestamp - Verify message signature
Key Refresh:
Keys SHOULD be refreshed when:
- Signature verification fails (key may have rotated)
fetched_atexceeds configurable TTL (recommend 24 hours)
State files are designed for export/import to support:
- Agent migration between hosts
- Backup and restore
- Multi-instance agent deployments
Export:
When exporting, add exported_at timestamp:
{
"schema_version": "1.0.0",
"agent_id": "my-agent",
"exported_at": "2026-02-05T15:00:00.000Z",
"swarms": { },
"muted_swarms": [],
"muted_agents": [],
"public_keys": { }
}Import:
When importing:
- Validate against
schemas/membership-state.json - Verify
schema_versioncompatibility - Merge or replace based on agent configuration
- Remove
exported_atfield after import
Incoming messages are persisted to the inbox table in SQLite.
Persistence is handled by InboxRepository in
src/state/repositories/inbox.py.
Schema:
CREATE TABLE inbox (
message_id TEXT PRIMARY KEY,
swarm_id TEXT NOT NULL,
sender_id TEXT NOT NULL,
recipient_id TEXT,
message_type TEXT NOT NULL,
content TEXT NOT NULL,
received_at TEXT NOT NULL,
read_at TEXT,
deleted_at TEXT,
status TEXT NOT NULL DEFAULT 'unread'
);Idempotent duplicate handling: Re-posting a message with the same
message_id is silently ignored via an IntegrityError catch. The
response is always {"status": "queued"}.
Inbox statuses: unread, read, archived, deleted
list_recent() method: Retrieves recent non-deleted messages for a swarm,
ordered by received_at descending. The limit parameter is capped at 100.
This is used by the context loader to provide conversation history to the
Claude subagent.
Outbound messages are tracked in the outbox table via OutboxRepository.
Outbox statuses: sent, delivered, failed.
Lifecycle:
- Message received at
POST /swarm/message - Persisted to inbox via
InboxRepository.insert() - Wake trigger evaluates the message (if configured)
- Status transitions: unread -> read -> archived/deleted
When a message arrives:
- Handler validates and queues message
- Handler POSTs to
/api/wake(if configured) - Claude Code loads swarm subagent
- Subagent processes message
- Subagent sends response via client
The swarm subagent receives:
- Recent messages (last N or last T time)
- Swarm membership state
- Mute lists
- Pending messages to process
The subagent can:
- Reply to the message (via client)
- Update local state (mute, leave)
- Create GitHub issue
- Trigger other subagents
Protocol version follows semver:
- Major: Breaking changes
- Minor: New features, backward compatible
- Patch: Bug fixes
Agents MUST include protocol_version in all messages. Agents SHOULD accept messages from compatible versions (same major).
- End-to-end encryption for swarm messages
- Swarm discovery (public swarm directory)
- Reputation/trust scoring between agents
- Multi-master swarms