Observed since 20 Oct, 22:45 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Minimal NATS network for agent-to-agent messaging and discovery
agentnet is a lightweight Python package that lets agents discover each other and exchange direct messages over NATS.
AgentNodeaccount.<account_id>.inbox) with stable account identitythread_id, parent_message_id)registry.search, registry.profile)registry.thread_status) for compaction signalingagent_accounts, agent_sessions, agent_threads, agent_messages)pip install agentnet
From this repo:
docker compose -f docker/docker-compose.yml up -d
This starts:
localhost:4222registry.register, registry.hello, registry.goodbye, registry.list, registry.search, and registry.profileimport asyncio
from agentnet import AgentNode
async def main():
# Notice the security token is passed directly in the URL!
node = AgentNode(
agent_id="agent_foo",
name="FooAgent",
capabilities=["chat"],
nats_url="nats://agentnet_secret_token@localhost:4222"
)
@node.on_message
async def handle(msg):
print("Got message:", msg.payload)
await node.start_forever()
asyncio.run(main())
await node.send(to="@weather_bot", payload={"text": "yo"})
await node.send_to_account(to_account_id="acct_01...", payload={"text": "yo"})
await node.send_to_username(username="weather_bot", payload={"text": "yo"})
Use the high-level SDK for common agent workflows:
from agentnet.sdk import AgentSDK
async with AgentSDK(
agent_id="agent_a",
name="Agent A",
nats_url="nats://agentnet_secret_token@localhost:4222",
) as sdk:
online = await sdk.list_online()
print([a.username for a in online])
profile = await sdk.get_profile("mesh_agent_1")
print(profile.get("bio", ""))
result = await sdk.ask_text(
"mesh_agent_1",
"Analyze spread and total for Celtics @ Suns",
thread_id="ops_1",
)
print(result.text)
thread = sdk.thread("ops_1")
await thread.send_text("mesh_agent_1", "Confirm your final pick")
LLM local tool wrappers can directly call:
sdk.list_online()sdk.get_profile(target)sdk.ask_text(to, text, thread_id=...)sdk.send_text(to, text, thread_id=...)sdk.thread_status(thread_id)Under the hood this publishes to:
account.acct_01....inboxPython API:
from agentnet import list_online_agents
agents = await list_online_agents("nats://agentnet_secret_token@localhost:4222")
print([a.to_dict() for a in agents])
CLI:
python -m agentnet list --nats-url nats://agentnet_secret_token@localhost:4222
# or, after install:
agentnet list --nats-url nats://agentnet_secret_token@localhost:4222
Account-route message examples:
agentnet send --nats-url nats://agentnet_secret_token@localhost:4222 --to-username weather_bot '{"text":"yo"}'
agentnet request --nats-url nats://agentnet_secret_token@localhost:4222 --to-account acct_01abc... '{"text":"ping"}'
agentnet search --nats-url nats://agentnet_secret_token@localhost:4222 --query weather --online-only
agentnet profile --nats-url nats://agentnet_secret_token@localhost:4222 --username weather_bot
Operator workflows (easy testing + visibility):
# Watch live inbox traffic (summary lines)
agentnet watch --nats-url nats://agentnet_secret_token@localhost:4222 --subject 'account.*.inbox'
# Watch delivery receipts
agentnet watch --nats-url nats://agentnet_secret_token@localhost:4222 --subject 'account.*.receipts'
# Join as an operator and chat interactively with an agent
agentnet chat --nats-url nats://agentnet_secret_token@localhost:4222 --to-username mesh_agent_1 --thread-id ops_thread_1
# Inspect a thread's token budget status (ok / warn / needs_compaction)
agentnet thread-status --nats-url nats://agentnet_secret_token@localhost:4222 --thread-id ops_thread_1
agentnet chat commands:
/showthread prints current thread id/thread <id> switches thread context/quit exits interactive modeagentnet thread-status outputs:
message_count, byte_count, approx_tokens, latest_checkpoint_endok, warn, needs_compactionAgentNet relies on NATS Token Authentication to ensure that unauthorized third parties cannot connect to your backend router and spoof AI agents.
Your docker-compose.yml backend spins up requiring the default token agentnet_secret_token. You must prepend this token (like nats://<TOKEN>@<IP>) to every AgentNode or CLI command, or the server will instantly reject the TCP connection. Before deploying to production, ALWAYS change this --auth token in your docker network!
For local anti-spoof testing, you can enable dev auth:
DEV_AUTH=true (compose env is supported):DEV_AUTH=true docker compose -f docker/docker-compose.yml up -d --build registry
DEV_AUTH=true in agent env..keys/<username-or-agent>.json.dev-ed25519-v1), and registry binds one public key per account.If a different key later tries to register for the same account, registry rejects it with auth_public_key_mismatch.
Registry persists account metadata into agent_accounts and session metadata into agent_sessions.
agent_accounts:
account_id (PK)username (unique)display_namebiocapabilities (JSONB)metadata (JSONB)visibilitystatuscreated_atupdated_atagent_sessions:
session_tag (PK)agent_idaccount_idusernameserver_idconnected_atdisconnected_atlast_seenstatusmetadata (JSONB)Retention is controlled by SESSION_RETENTION_DAYS (default: 14). Offline sessions older than retention are pruned.
Thread/message persistence:
agent_threads stores thread metadata and participant account IDs.agent_messages stores message envelopes keyed by message_id with thread_id and parent_message_id.account.*.inbox, agent.capability.*, _INBOX.>).account.<account_id>.inboxaccount.<account_id>.receiptsregistry.registerregistry.resolve_accountregistry.resolve_keyregistry.searchregistry.profileregistry.thread_statusregistry.helloregistry.goodbyeregistry.list (request/reply){
"message_id": "b3d0...",
"from_agent": "agent_a",
"from_account_id": "acct_01...",
"from_session_tag": "registry_01J...",
"to_agent": "agent_b",
"to_account_id": "acct_02...",
"payload": {"text": "yo"},
"sent_at": "2026-02-21T08:00:00Z",
"ttl_ms": 30000,
"expires_at": "2026-02-21T08:00:30Z",
"trace_id": "b3d0...",
"thread_id": "thread_b3d0...",
"parent_message_id": null,
"kind": "direct",
"auth": {
"scheme": "dev-ed25519-v1",
"public_key": "<base64url>",
"claims": {"message_id": "b3d0...", "payload_sha256": "..."},
"signature": "<base64url>"
}
}
{
"message_id": "b3d0...",
"status": "accepted",
"event_at": "2026-02-21T08:00:00Z",
"from_account_id": "acct_02...",
"from_session_tag": "registry_01J...",
"to_account_id": "acct_01...",
"trace_id": "b3d0...",
"thread_id": "thread_b3d0..."
}
{
"agent_id": "agent_a",
"account_id": "acct_01...",
"username": "agent_a",
"name": "Agent A",
"session_tag": "registry_01J...",
"capabilities": ["chat", "summarize"],
"metadata": {"team": "ops"},
"last_seen": "2026-02-21T08:00:00Z"
}
{
"account_id": "acct_01...",
"username": "agent_a",
"session_tag": "registry_01J...",
"heartbeat_interval": 12.0,
"ttl_seconds": 40.0
}
agent_id is the logical role label. account_id is stable routing identity. session_tag is the unique identity for that specific running process instance.
send() / request() are account-routed only. Use account targets (to="account:acct_...", to="acct_...") or usernames (to="@name" or to="name"). You can also call send_to_account(), request_account(), send_to_username(), and request_username() directly.
send_*() waits for a delivery receipt by default and retries publish on receipt timeout (default_send_retry_attempts=2, default_receipt_timeout_seconds=1.5).
Incoming messages without ttl_ms or expires_at are rejected by default.
Thread budget env knobs (registry service):
THREAD_SOFT_LIMIT_TOKENS (default 50000)THREAD_HARD_LIMIT_TOKENS (default 60000)TOKEN_ESTIMATE_CHARS_PER_TOKEN (default 4)Presence is kept accurate via heartbeat:
registry.hello every 10-15 seconds (default: 12 seconds)AgentNode enforces bounded in-process concurrency and backpressure by default:
max_concurrency=4max_pending=100work_timeout_seconds=30max_payload_bytes=256000rate_limit_per_sender_per_sec=5 with rate_limit_burst=10dedupe_ttl_seconds=600circuit_breaker_failures=5 with circuit_breaker_reset_seconds=15You can override these in AgentNode(...) per agent process.
Use AgentWrapper if you want a stable adapter API for embedding AgentNet in your own agent runtime:
import asyncio
from agentnet import AgentWrapper
async def main() -> None:
sdk = AgentWrapper(
agent_id="demo_worker",
name="Demo Worker",
username="demo_worker",
nats_url="nats://agentnet_secret_token@localhost:4222",
)
@sdk.receive
async def on_message(msg):
if msg.reply_to:
await sdk.reply(request=msg, payload={"ok": True})
await sdk.connect()
await sdk.request(to_username="weather_bot", payload={"text": "status?"}, timeout=5.0)
await sdk.close()
asyncio.run(main())
In this repo:
python -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -e .
python examples/agent_a.py
python examples/agent_b.py
python examples/list_agents.py
python -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -e .
docker compose -f docker/docker-compose.yml up -d --build
cp agents/.env.example agents/.env
Set NOVITA_API_KEY in agents/.env.
set -a
source agents/.env
set +a
python agents/agent_novita_b.py
set -a
source agents/.env
set +a
python agents/agent_novita_a.py
Agent A will generate questions with zai-org/glm-5, send them to Agent B over AgentNet, and print turn-by-turn replies.
For cleaner recording output, tune these in agents/.env:
DEMO_TURNS=4 for a fuller back-and-forthNOVITA_MAX_TOKENS=256 for shorter answersLOG_TEXT_MAX_CHARS=700 to cap displayed text length per log blockTARGET_USERNAME=agent_novita_b to control who Agent A talks toThis repo now includes a 3-agent skeleton that uses:
agents/prompts/tools//v1/chat/completions and Claude /v1/messagesFiles:
agents/agent_mesh_trio.py (runner)agents/config/mesh_agents.yaml (agent/model/tool config)agents/prompts/agent_1.txt, agents/prompts/agent_2.txt, agents/prompts/agent_3.txtRun:
cd /Users/klyexy/Documents/realm
source venv/bin/activate
set -a
source agents/.env
set +a
python agents/agent_mesh_trio.py --config agents/config/mesh_agents.yaml
Run a subset:
python agents/agent_mesh_trio.py --config agents/config/mesh_agents.yaml --only agent_1 --only agent_2
Required env vars for this skeleton:
OPENAI_API_KEY, OPENAI_BASE_URLANTHROPIC_API_KEY, ANTHROPIC_BASE_URLRSC_TOKEN, ODDS_API_KEY, PERPLEXITY_API_KEYDownload counts and popularity will appear here as the collector receives them.
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.1.1 | 25 Feb 2026 | 9 |
This describes our collected data; it is not a security assessment.
Observed since 20 Oct, 22:45 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:e4672db4218e254ae72da519bf0e8c348c6af035363abeb2b10d8077eb201135