From zero to a production-grade peer messaging framework — 18 releases, 800+ tests, five language clients, and enterprise-grade encryption, routing, and audit in 10 days.
May 2026 was the founding month of a2a-skill. In 10 days the project went from nothing to a production-grade peer messaging framework: a zero-config SQLite bus, a 14-command CLI, five language clients, and over 800 automated tests. Every release this month was a hardening lap or a new capability layered on top of a stable core.
a2a-skill starts with a single Python file and a clear idea: AI agents should talk to each other directly, without an orchestrator. The initial release ships 14 CLI commands backed by a WAL-mode SQLite database that handles concurrent writes without a daemon or server process.
init, register, send, recv, peek, list, status, wait, clear, search, stats, thread, unregister, projectAgents can now query the bus by keyword, follow conversations by thread ID, and check bus health with a2a stats. A native Python client library lands — direct SQLite access, no subprocess overhead.
a2a searcha2a thread <id>Go, Node.js, and Rust clients ship the same day. A REST API server adds 10 HTTP endpoints. Python, Go, JS, and Rust agents can now collaborate in the same team — sharing one SQLite bus file, no protocol translation needed.
node:sqlite module (Node 22+)Six major features in a single release: encryption, FTS5 search, audit logging, priority queuing, smart routing, and async Python clients. Each ships with its own documentation guide.
After the v1.3 feature push, a week-long hardening sprint closes every known reliability gap. WAL mode is enforced on all connections. FTS5 search is fixed to not rebuild on every query. Missing methods, divergent error messages, and validation inconsistencies across all five clients are patched one by one. The test suite grows from 95 to 800+ tests.
register() and unregister() — previously absent, making send() impossiblea2a-spawn now uses nohup + disown — spawned agents survive parent shell exita2a-spawn shell quoting fix — kit prompts passed via file reference, not inline stringdiscard)Wait() connection churn fixed — was opening 120 SQLite connections for a 60s waittouch() method added; recv() connection reuse across poll loopThe last stretch of May closes remaining cross-client gaps: empty body rejection standardized across all five clients, type stubs brought into sync with their implementations, and wait_for_messages() added to Go and Rust to complete the cross-language API surface.
.pyi stubs; wait_for_messages return type corrected from List to boolGetStatus() nil-for-not-found, Recv() partial-read bug, Register() atomicity fix, RecvSimple() wait type to float64recv_with_routing(limit=N) now routes all messages then truncates per-category, matching sync behaviormake_connection() — consolidated SQLite setup logic across test modules.wait_for_messages() — blocks until N unread messages arrive or timeout. Matches Python and JS signature.Wait() connection churn — was creating a new SQLite connection every 500ms (up to 120 for 60s timeout). Now reuses one connection.GetStatus() return type — changed to (*string, error) returning nil, nil for not-found. Matches Python's None.Recv() partial read-marking on scan error — scan phase separated from mark-read phase; failure no longer silently consumes prior messages.Register() upsert not atomic — two db.Exec() calls wrapped in a transaction.SearchFTS() missing validation — empty query and zero/negative limit now validated upfront.Send() whitespace thread_id — whitespace-only IDs rejected to match Python.RecvSimple() wait parameter — changed from int to float64 matching Python.Stats() error swallowing — all five QueryRow/Scan calls now propagate errors.connect() expiry — SetConnMaxLifetime(5s) caused mid-poll connection drops; set to 0.Send() error hint — added "register them first" matching Python message.__init__ — async skipped empty-string checks for project and agent_id.unread_only=True.recv_with_routing(limit=N) used SQL-level LIMIT, restricting routing decisions. Now fetches all, routes, truncates per-category.register(), unregister(), list(), status(), wait(), init_project(), project_info(), clear() absent. All added.wait_for_messages return type — stub declared List[Dict], implementation returns bool. Corrected. timeout changed from int to float.search() default limit drift — stub showed 100, implementation uses 50. Corrected.__init__ missing agent_id guard — empty/whitespace accepted silently.__init__ — fields set with no guards; now calls all validation helpers matching sync.add_rule always appended — repeated calls created duplicates. Now upserts by rule name.disable_rule/enable_rule stale in-memory list — missing await get_rules() refresh after commit.apply_routing only marked discard read — all five categories now marked.m.priority column — would raise OperationalError at runtime; removed from SELECT.recv_with_routing never marked read — all returned messages reappeared on every call.--append-system-prompt-file — multi-line kit prompts corrupted by shell quoting when passed inline via nohup. Now passed via temp file reference.NewClient() — signature changed to (*Client, error) with input validation: empty/non-printable project/agentID, path separators, length limits.Send() — MaxBodyLength check moved before c.connect() to fail fast.Peek() + Recv() — TTL cleanup inlined; no longer opens separate connection.Register() — INSERT OR IGNORE + UPDATE cross-client upsert pattern applied.recv() — connect() moved outside poll loop; one connection per call.recv() — last_seen update added inside poll loop, matching Go/Python Touch.touch() — new public method matching Python/Go API.register() and unregister() — both clients had no way to register an agent, making send() impossible (sender validation rejects unknown agents).project_info() — missing method added matching Python/Go API.conn.close().register(upsert=True) — changed from INSERT OR REPLACE (destroys created_at) to INSERT OR IGNORE + UPDATE.search() — changed to WHERE LOWER(body) LIKE ?; all other clients already used lower().recv() and peek() now delete expired messages before fetching. Rust was the only client skipping this.SystemTime instead of strftime('%s','now') for sub-second accuracy, matching Go/Python.sqlite3.connect() calls in test files.COALESCE(NULLIF(?,'')) preserves non-empty fields on overwrite.send() — unknown senders and recipients now rejected before INSERT.SearchQueryBuilder. LIKE fallback.PriorityClientAsync).SmartRouter for custom logic.PriorityClientAsync, RoutingClientAsync. aiosqlite backend. 10× throughput.dashboard.py + benchmark.py.~/.agents/skills.a2a — auto-detects Python 3, cached interpreter selection.A2AClient sync + async, full CLI parity, direct SQLite.a2a search, a2a thread, a2a stats, all with --json.a2a_client.go, WAL + MkdirAll, full API parity.a2a_client.js, built-in node:sqlite (Node 22+).src/lib.rs crate + binary, WAL + create_dir_all.a2a_server.py, 10 JSON endpoints.