Run A2A-compatible agents on a single host. Node.js + systemd. No Docker. No Kubernetes.
Works on a single host with 13+ agents in production. Not designed for multi-host or distributed deployment.
Built on top of the official @a2a-js/sdk.
Not affiliated with, endorsed by, or connected to the A2A Protocol project, Google, or the Linux Foundation.
Use ag2ag if:
- You run agents on a single Linux host (VPS, homelab, dev VM)
- You want A2A discoverability without Docker or Kubernetes
- You need a lightweight CLI to manage agent lifecycle via systemd
- You're prototyping or experimenting with A2A locally
- You want agents to discover and call each other on localhost
Do NOT use ag2ag if:
- You need multi-host or distributed deployment
- You need authentication, encryption, or network-level security
- You're building a production system requiring isolation between agents
- Registry — local JSON file tracking all agents, ports, systemd units. Supports schema migration for future versions.
- Lifecycle — start, stop, restart agents via systemd with
--userflag support - Discovery —
GET /cardon each agent for A2A-compatible AgentCards - Messaging — send messages between agents on localhost
- Task persistence — JSONL files survive restarts. Auto-cleanup of old tasks (configurable retention).
- SSE Streaming —
/task/:id/streamfor real-time task updates via Server-Sent Events - Rate limiting — sliding window per agent (configurable via env vars)
- Health & Metrics —
/healthand/metricsendpoints for observability - Config module — centralized configuration with environment variable overrides
- CLI — manage everything from the terminal
- Synchronous calls —
POST /callendpoint that blocks until handler completion - Request logging — automatic method/path/status/duration logging via
res.on('finish') - SIGHUP hot-reload — reload rate limits and cleanup config without restart
- Payload validation with dual format support — accepts both A2A format (role+parts) and direct JSON
- Enhanced /health — memory usage (RSS, heap), task counts by state, degraded flag
- Task pruning by count —
CLEANUP_MAX_TASKSlimits terminal tasks per agent
npm install -g ag2ag
# Initialize
ag2ag init
# Register an agent
ag2ag register my-agent --port 5001 --description "Does useful things"
# Start it
ag2ag start my-agent
# Check health
ag2ag status --health
# Get its AgentCard
ag2ag card my-agent
# Send a message
ag2ag call my-agent "hello"
# View logs with priority filter
ag2ag logs my-agent --priority err
# Clean old tasks
ag2ag clean --days 7
# Start web dashboard
ag2ag ui --port 8080$ ag2ag status --health
ag2ag — 3 agent(s)
STAT NAME PORT UNIT HEALTH
● api-gateway :3099 api-gateway.service responding
● example-agent :5000 example-agent.service responding
● echo-agent :5000 echo-agent.service responding
ag2ag init Create registry + data dirs
ag2ag register <name> Register agent with AgentCard
ag2ag remove <name> Remove from registry
ag2ag list List all agents
ag2ag start|stop|restart Systemd lifecycle management
ag2ag status [--health] Show agents (with live HTTP check)
ag2ag card <name> Show AgentCard (live or from registry)
ag2ag call <name> <message> Send A2A message, wait for response
ag2ag logs <name> journalctl for the agent (--lines N, --priority LEVEL)
ag2ag clean [--days N] Clean tasks older than N days (default 7)
ag2ag ui [--port N] Start local web dashboard
const { AgentServer } = require('ag2ag');
const card = {
schemaVersion: '1.0',
name: 'my-agent',
description: 'Does useful things',
url: 'http://127.0.0.1:5001',
capabilities: { streaming: false, pushNotifications: false },
skills: [{ name: 'do-thing', description: 'Does a thing' }],
};
async function handleMessage(message, task) {
return {
parts: [{ type: 'text', text: 'Done!' }],
source: 'my-agent',
timestamp: new Date().toISOString(),
};
}
const server = new AgentServer({
agentCard: card,
agentName: 'my-agent',
port: 5001,
handler: handleMessage,
});
server.start();# Python client — direct JSON format
import requests
resp = requests.post("http://localhost:5001/task", json={"keyword": "kubernetes", "days": 30})
print(resp.json())See examples/ for complete agents:
- echo-agent.js — minimal A2A protocol validation
- health-proxy.js — real agent that queries other agents for ecosystem health
All configuration is centralized in src/config.js with environment variable overrides:
| Env Variable | Default | Description |
|---|---|---|
AG2AG_PORT |
5001 | Default HTTP port |
AG2AG_BIND_HOST |
127.0.0.1 | Network interface (keep localhost!) |
AG2AG_MAX_BODY_SIZE |
1048576 (1MB) | Max request body size in bytes |
AG2AG_RATE_LIMIT_MAX |
60 | Max tasks per agent per window |
AG2AG_RATE_LIMIT_WINDOW_MS |
60000 (60s) | Rate limit sliding window |
AG2AG_CLEANUP_INTERVAL_MS |
86400000 (24h) | Auto-cleanup interval |
AG2AG_CLEANUP_MAX_DAYS |
7 | Days to retain completed tasks |
AG2AG_CLEANUP_MAX_TASKS |
1000 | Maximum number of terminal tasks to retain per agent |
AG2AG_SSE_KEEPALIVE_MS |
15000 (15s) | SSE heartbeat interval |
AG2AG_REGISTRY_PATH |
../config/registry.json |
Path to the local registry JSON file |
AG2AG_STORE_DIR |
../data/tasks |
Directory path for task persistence (JSONL files) |
npm test # Run all tests
npm run test:unit # Unit tests only
npm run test:concurrency # Concurrency testsTest suites: cli, config, lifecycle, registry, server, task-store, concurrency.
| Component | Choice |
|---|---|
| HTTP | Node.js built-in http (no Express) |
| Process management | systemd |
| Registry | JSON file with schema migration |
| Task persistence | JSONL per agent, async Mutex for writes |
| Rate limiting | Sliding window (in-memory) |
| SSE | EventEmitter-based |
| A2A compliance | @a2a-js/sdk v0.3.13 |
| External dependencies | 1 |
| Tool | Best for | ag2ag difference |
|---|---|---|
| Docker Compose | Multi-container apps with networking | ag2ag skips containers entirely — lighter for simple agents |
| Kubernetes | Large-scale distributed systems | ag2ag targets single-host — no cluster overhead |
| Nomad | Mixed workload orchestration | ag2ag is agent-specific with A2A discovery built-in |
| PM2 | Node.js process management | ag2ag adds A2A protocol, discovery, and inter-agent messaging |
| systemd raw | Service management | ag2ag wraps systemd with registry, CLI, and A2A compliance |
| A2A SDK alone | Building A2A agents from scratch | ag2ag provides the operational layer (registry, lifecycle, persistence) |
What is A2A? A2A (Agent-to-Agent) is an open protocol by the Linux Foundation for AI agent interoperability. It defines how agents discover each other's capabilities and collaborate. See a2a-protocol.org.
How is this different from the A2A SDK?
The @a2a-js/sdk provides protocol types and server helpers. ag2ag adds the operational layer on top: local registry, systemd lifecycle management, CLI, task persistence, rate limiting, SSE streaming, and single-host conventions.
Can I run AI agents with this? Yes. Any agent that exposes an A2A-compatible HTTP interface works. The handler function receives messages and returns responses — you decide what the agent does (call an LLM, query a database, monitor services, etc).
Does it work without systemd? Lifecycle commands (start/stop/restart) require systemd. But registry, discovery, messaging, and the HTTP server work independently.
Is this secure? Not for production. All communication is localhost HTTP with no authentication. See SECURITY.md for known risks and mitigations.
What Node.js version is required?
Node.js 18+ (uses fetch, crypto.randomUUID). Tested on v22.
- Ubuntu 22.04 LTS, Node.js v22
- 13+ agents registered, A2A-discoverable services, composition agents. Running on a production VPS since April 2026
- Concurrency tested with parallel load (see
test/concurrency.test.js) - See
docs/writeup.mdfor the full experiment report
- v0.4.1 — Dual payload format support (A2A + direct JSON)
- v0.4.0 — Synchronous /call, request logging, SIGHUP hot-reload, enhanced /health, task pruning, payload validation
- v0.3.0 — Web dashboard, SSE streaming, task pruning CLI,
--userflag - v0.2.0 — Security hardening (atomic writes, body limits, randomUUID)
- v0.1.0 — Initial release