Agentic Relay Messaging Protocol Interactive Transport v1.0
Agents that talk to each other — no glue code.
ARMPIT is an open, SMTP-inspired protocol that lets AI agents send structured messages to any other agent on any relay — across languages, frameworks, and clouds — with a single consistent API.
Custom types use reverse-domain notation: com.example.MY_TYPE
Transports
HTTP/HTTPS RESTport 8025
TCP wire protocolport 2525
WebSocketcoming
AMQP / SQSplanned
💡 Use Cases
What can agents do with ARMPIT?
ARMPIT is designed for agents that operate autonomously — acting, delegating, and coordinating without waiting for a human to click a button.
⛓️
Multi-Agent Pipeline
Orchestrator breaks a job into steps, delegates each to a specialist agent, collects results.
TASK_REQUESTTASK_RESULT
📣
Smart Notifications
Monitor agent detects an event and pushes a structured alert to a presentation agent that decides how to surface it.
NOTIFICATIONPING
📰
Pub / Sub Feeds
Publisher agent broadcasts content (news, prices, events) to any subscriber that previously sent SUBSCRIBE.
SUBSCRIBEPUBLISH
🧑💻
Human-in-the-Loop
Agent hits a decision boundary and sends a TASK_REQUEST to a human-facing agent that surfaces it as a UI prompt.
TASK_REQUESTRESPONSE
🩺
Distributed Health Checks
Watchdog agent PINGs every registered agent on a schedule and escalates failures to an alerting agent.
PINGPONGERROR
🤝
Cross-Org Collaboration
Your agent on your relay federates a message to a partner agent on a different relay — no shared infrastructure needed.
FederationmTLS
⛓️ Multi-Agent Pipeline — example
An orchestrator receives a research request from a user and fans it out to three specialist agents — a search agent, a summarise agent, and a format agent — then assembles the final response. No human intervention between steps.
# orchestrator_agent.py (runs autonomously)import requests, time, uuid
BASE = "http://localhost:8025/armpit/v1"
TOKEN = "<orchestrator-token>"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
defdelegate(to_agent, task, context):
thread = str(uuid.uuid4())
requests.post(f"{BASE}/messages", headers=HEADERS, json={
"from_agent": "orchestrator@relay.m-net",
"to_agent": to_agent,
"type": "TASK_REQUEST",
"thread_id": thread,
"payload": {"task": task, "context": context},
})
return thread
defwait_for_result(thread_id, timeout=60):
deadline = time.time() + timeout
while time.time() < deadline:
r = requests.get(f"{BASE}/messages", headers=HEADERS).json()
for msg in r.get("messages", []):
if msg["thread_id"] == thread_id and msg["type"] == "TASK_RESULT":
requests.post(f"{BASE}/messages/{msg['id']}/ack", headers=HEADERS)
return msg["payload"]
time.sleep(1)
raise TimeoutError
# Fan-out → collect → assemble (no human clicks)
t1 = delegate("search@relay.m-net", "search", {"query": "ARMPIT protocol"})
t2 = delegate("summary@relay.m-net", "summarise", {"url": "https://example.com/doc"})
results = [wait_for_result(t) for t in [t1, t2]]
delegate("formatter@relay.m-net", "format", {"parts": results})
📣 Smart Notifications — example
A monitoring agent watches a data source and, when a threshold is crossed, sends a NOTIFICATION to a presentation agent. The presentation agent decides whether to send a push notification, email, or voice alert based on user preferences — the monitor doesn't need to know.
# monitor_agent.py — fires and forgetsimport requests, time
defalert(user_id, metric, value):
requests.post("http://localhost:8025/armpit/v1/messages",
headers={"Authorization": "Bearer <monitor-token>"},
json={
"from_agent": "monitor@relay.m-net",
"to_agent": "presenter@relay.m-net",
"type": "NOTIFICATION",
"user_id": user_id,
"payload": {
"title": f"Alert: {metric} threshold exceeded",
"body": f"{metric} is at {value}",
"severity": "high"if value > 90else"warning",
},
})
# Monitor loop — fully autonomouswhileTrue:
cpu = get_cpu_usage() # your sensor hereif cpu > 80:
alert("usr_abc123", "CPU", cpu)
time.sleep(30)
📰 Pub / Sub Feed — example
A newsletter agent publishes daily content to every subscriber. Subscribers sent a SUBSCRIBE message to opt in; the publisher keeps a list and fans out with PUBLISH. No broker infrastructure required.
# newsletter_agent.py — publisher sideimport requests, time
BASE = "http://localhost:8025/armpit/v1"
HEADERS = {"Authorization": "Bearer <newsletter-token>"}
subscribers = set()
defprocess_inbox():
for msg in requests.get(f"{BASE}/messages", headers=HEADERS).json().get("messages",[]):
if msg["type"] == "SUBSCRIBE":
subscribers.add(msg["from_agent"])
print(f"New subscriber: {msg['from_agent']}")
requests.post(f"{BASE}/messages", headers=HEADERS, json={
"from_agent": "newsletter@relay.m-net",
"to_agent": msg["from_agent"],
"type": "SUBSCRIPTION_CONFIRMED",
"payload": {"topic": "ai_daily"},
})
requests.post(f"{BASE}/messages/{msg['id']}/ack", headers=HEADERS)
defpublish(content):
for sub in subscribers:
requests.post(f"{BASE}/messages", headers=HEADERS, json={
"from_agent": "newsletter@relay.m-net",
"to_agent": sub,
"type": "PUBLISH",
"payload": content,
})
# Run: check inbox every 5s, publish once dailywhileTrue:
process_inbox()
ifis_publish_time(): # your schedule logicpublish({"title": "Today in AI", "body": "..."})
time.sleep(5)
🧑💻 Human-in-the-Loop — example
An autonomous agent encounters a decision it's not confident about and delegates to a human-interface agent. The human-interface agent surfaces this as a UI prompt; once the human responds, it sends a RESPONSE back and the original agent continues — no polling on the human side.
# autonomous_agent.py — hits a decision gateimport requests, time, uuid
BASE = "http://localhost:8025/armpit/v1"
HEADERS = {"Authorization": "Bearer <agent-token>"}
defask_human(question, context):
thread = str(uuid.uuid4())
requests.post(f"{BASE}/messages", headers=HEADERS, json={
"from_agent": "autonomous@relay.m-net",
"to_agent": "human_ui@relay.m-net",
"type": "TASK_REQUEST",
"thread_id": thread,
"payload": {
"question": question,
"context": context,
"options": ["approve", "reject", "modify"],
},
})
# Wait for human response (non-blocking loop)
deadline = time.time() + 3600# 1-hour timeoutwhile time.time() < deadline:
msgs = requests.get(f"{BASE}/messages", headers=HEADERS).json()["messages"]
for m in msgs:
if m["thread_id"] == thread and m["type"] == "RESPONSE":
requests.post(f"{BASE}/messages/{m['id']}/ack", headers=HEADERS)
return m["payload"]["choice"]
time.sleep(3)
# Autonomous work — escalates only when needed
decision = ask_human("Deploy to production?", {"diff_lines": 423})
if decision == "approve":
deploy()
🩺 Distributed Health Checks — example
A watchdog agent PINGs every registered agent every 60 seconds. Any agent that doesn't respond with a PONG within 10 seconds is considered unhealthy; the watchdog sends an ERROR to an alerting agent that pages on-call.
# watchdog_agent.py — runs on a scheduleimport requests, time, uuid
BASE = "http://localhost:8025/armpit/v1"
HEADERS = {"Authorization": "Bearer <watchdog-token>"}
AGENTS = ["search@relay.m-net", "summary@relay.m-net", "formatter@relay.m-net"]
defping_agent(target):
nonce = str(uuid.uuid4())
requests.post(f"{BASE}/messages", headers=HEADERS, json={
"from_agent": "watchdog@relay.m-net",
"to_agent": target,
"type": "PING",
"payload": {"nonce": nonce},
})
return nonce
defcheck_pongs(expected):
msgs = requests.get(f"{BASE}/messages", headers=HEADERS).json()["messages"]
received = {m["payload"]["nonce"] for m in msgs if m["type"] == "PONG"}
for agent, nonce in expected.items():
if nonce not in received:
requests.post(f"{BASE}/messages", headers=HEADERS, json={
"from_agent": "watchdog@relay.m-net",
"to_agent": "alerting@relay.m-net",
"type": "ERROR",
"payload": {"agent": agent, "reason": "no PONG within 10s"},
})
whileTrue:
nonces = {a: ping_agent(a) for a in AGENTS}
time.sleep(10) # wait for PONGscheck_pongs(nonces)
time.sleep(50) # rest of the 60-second cycle
🤝 Cross-Org Federation — example
Your agent sends to a partner agent on a completely different relay. The relay resolves the destination domain via DNS SRV, opens a TCP session to the remote relay, and forwards the message transparently. No VPN, no shared accounts.
# Cross-org: same code, different destination domain# DNS SRV: armpit._tcp.partner.org → relay.partner.org:2525
requests.post("http://localhost:8025/armpit/v1/messages",
headers={"Authorization": "Bearer <your-token>"},
json={
"from_agent": "myagent@relay.m-net",
# Different relay domain — the relay federates this automatically"to_agent": "their_agent@relay.partner.org",
"type": "TASK_REQUEST",
"payload": {"task": "translate", "text": "Hello world", "lang": "es"},
})
# Your relay handles:
# 1. JWT validation (your token)
# 2. DNS SRV lookup for relay.partner.org
# 3. TCP ARP session to remote relay
# 4. Remote relay delivers to their_agent
# 5. their_agent ACKs back the same way
🔴 Live Demo
Try it against your relay
Requires the relay to be running on localhost:8025. Register two demo agents, send a message between them, and watch the exchange.
Checking relay connection…
🤖 Agent Alpha
not registered
address:alpha@relay.m-net
scopes:PINGTASK_REQUEST
token:
🤖 Agent Beta
not registered
address:beta@relay.m-net
scopes:PONGTASK_RESULT
token:
Message Log
--:--:--Waiting for relay…
🧩 Patterns
Agent design patterns
Common architectural patterns for building autonomous agent systems on ARMPIT.
Idempotent sends — retry without duplicates
Set client_id to the same UUID on every retry. The relay deduplicates within a 24-hour window.
# Generate once, persist, reuse on retry
msg_id = str(uuid.uuid4()) # save to DB or filefor attempt in range(3):
r = requests.post(f"{BASE}/messages", headers=HEADERS, json={
"from_agent": "sender@relay.m-net",
"to_agent": "worker@relay.m-net",
"type": "TASK_REQUEST",
"client_id": msg_id, # <-- same ID every retry"payload": {"task": "process"},
})
if r.ok: break
time.sleep(2 ** attempt) # exponential back-off
Thread-based conversations
Use thread_id to group all messages in a task chain. Agents filter their inbox by thread to avoid processing unrelated messages.
thread = str(uuid.uuid4())
# Step 1 — kick off
send(to="worker@relay.m-net", type="TASK_REQUEST", thread=thread, payload={"step":1})
# Worker replies on the same threaddefhandle(msg):
if msg["thread_id"] != thread: return# ignore other conversationsprocess(msg["payload"])
send(to=msg["from_agent"], type="TASK_RESULT",
thread=thread, reply_to=msg["id"], payload={"result": "done"})
Graceful error propagation
When an agent fails, it sends an ERROR message back on the same thread so the orchestrator can retry, reroute, or escalate.