Package Exports
- @marsulta/mailman
Readme
Mailman
A structured interagent messaging protocol for multi-agent systems.
Mailman lets you send typed, validated packets between AI agents, tools, and services — inside a single process or across a multi-agent architecture. It gives every role a clear contract, every message a lifecycle, and every transaction a full trace.
Why Mailman?
In multi-agent systems, agents need to communicate — but raw function calls have no structure, no validation, and no observability. Mailman gives every message:
- ✅ A type (
task.assign,review.request,health.ping, ...) - ✅ A validated shape — required fields, confidence bounds, known targets
- ✅ A trace — every hop is logged with actor, event, and timestamp
- ✅ A lifecycle —
created → validated → delivered → completed - ✅ Middleware hooks — tap into the pipeline for logging, auth, or metrics
Install
npm install @marsulta/mailman
# or
pnpm add @marsulta/mailmanNode.js >= 18 required.
Quick Start
import { Runtime, createPacket } from "@marsulta/mailman";
const runtime = new Runtime();
runtime.start();
// 1. Register a role with a handler
runtime.registerRole(
{
name: "worker",
accepts: ["task.assign"],
description: "Processes tasks",
},
async (packet) => {
const result = String(packet.payload.prompt).toUpperCase();
return createPacket({
taskId: packet.taskId,
parentPacketId: packet.packetId,
type: "task.result",
sender: "worker",
target: packet.sender,
payload: { result },
status: "completed",
});
}
);
// 2. Send a packet
const reply = await runtime.send(
createPacket({
taskId: "task-1",
type: "task.assign",
sender: "orchestrator",
target: "worker",
payload: { prompt: "Hello, world!" },
})
);
console.log(reply.payload.result); // "HELLO, WORLD!"Core Concepts
Packet
The fundamental unit of communication. Every message is a MailmanPacket:
type MailmanPacket = {
packetId: string; // auto-generated
taskId: string; // links related packets
parentPacketId?: string; // reply chain
type: PacketType; // see below
sender: string; // role name
target: string; // role name
payload: Record<string, unknown>;
status?: PacketStatus;
// Optional
intent?: string;
scope?: { files?: string[]; repoWide?: boolean; artifacts?: string[] };
constraints?: { allowNewFiles?: boolean; readOnly?: boolean; ... };
confidence?: number; // 0–1
meta?: { timeoutMs?: number; replyRequired?: boolean; tags?: string[] };
error?: { code: string; message: string };
};Packet Types
| Type | Direction | Meaning |
|---|---|---|
task.assign |
→ agent | Assign a task |
task.accept |
← agent | Agent accepted the task |
task.reject |
← agent | Agent rejected the task |
task.result |
← agent | Task completed, here's the output |
review.request |
→ reviewer | Please review this result |
review.result |
← reviewer | Review decision |
route.request |
→ router | Route this packet |
route.result |
← router | Routing decision |
error.report |
← mailman | Something went wrong |
health.ping |
→ agent | Are you alive? |
health.pong |
← agent | Yes |
Roles
A role registers a name, the packet types it accepts, and a handler function:
runtime.registerRole(
{
name: "reviewer",
accepts: ["review.request"],
description: "Reviews worker output",
version: "1.0.0",
health: "healthy",
},
async (packet) => { ... }
);Middleware
Add interceptors that wrap the packet pipeline:
// Logging middleware
runtime.use(async (packet, next) => {
console.log("→", packet.type, packet.sender, "→", packet.target);
const reply = await next();
console.log("←", reply.type, reply.status);
return reply;
});
// Auth middleware
runtime.use(async (packet, next) => {
if (!isAuthorized(packet.sender, packet.target)) {
throw new Error("Unauthorized");
}
return next();
});Middleware runs in insertion order, koa-style.
Client (fluent API)
import { Runtime, createClient } from "@marsulta/mailman";
const client = createClient(new Runtime())
.start()
.registerRole({ name: "agent", accepts: ["task.assign"] }, handler);
const reply = await client.send(packet);Trace
Every packet flowing through the runtime is fully traced:
const trace = runtime.getTrace(packet.packetId);
// [
// { event: "packet.received", actor: "mailman", timestamp: "..." },
// { event: "packet.validated", actor: "mailman", timestamp: "..." },
// { event: "handler.started", actor: "worker", timestamp: "..." },
// { event: "packet.completed", actor: "mailman", timestamp: "..." },
// ]API Reference
Runtime
| Method | Description |
|---|---|
start() |
Start the runtime |
stop() |
Stop the runtime |
isRunning() |
Returns whether the runtime is active |
use(fn) |
Register a middleware function |
registerRole(role, handler) |
Register an agent role |
unregisterRole(name) |
Remove a registered role |
listRoles() |
List all registered roles |
send(packet) |
Send a packet, returns a reply |
ping(roleName) |
Check if a role is alive |
getTrace(packetId) |
Get trace events for a packet |
getTaskTrace(taskId) |
Get all trace events for a task |
getStats() |
Get runtime stats |
createPacket(init)
Factory that auto-fills packetId and timestamp. Pass any valid MailmanPacket fields minus those two.
createReply(original, type, sender, payload, overrides?)
Build a reply packet. Automatically:
- Sets
parentPacketIdtooriginal.packetId - Inherits
taskId - Flips
senderandtarget
createClient(runtime)
Returns a MailmanClient — a fluent chainable wrapper over Runtime.
CLI
npx mailman start # Start the runtime
npx mailman status # Show runtime status
npx mailman roles # List registered roles
npx mailman inspect <packetId> # Inspect a packet's trace
npx mailman doctor # Run health diagnosticsExamples
examples/basic-ping— Minimal health ping/pong demoexamples/task-pipeline— Three-role orchestrator → worker → reviewer pipeline
Roadmap
Mailman v0.x (current)
- In-process packet routing
- Typed packet schema + validation
- Full trace log
- Middleware pipeline
- CLI tools
- Persistent trace (SQLite)
- Cross-process transport (Unix sockets / HTTP)
- Retry + dead-letter queue
Postmaster (future)
Postmaster is the "big brother" of Mailman — a standalone daemon that can observe and interpret messages across entire systems, not just agents.
With Postmaster, every subsystem speaks Mailman:
- 🖨️ Printer spooler issues →
error.reportpacket - 🌐 Network/DNS failures →
error.reportpacket - 💾 Disk space warnings →
error.reportpacket - 🗄️ Database connection drops →
error.reportpacket
Your agents can subscribe to infrastructure signals the same way they subscribe to each other's messages. Postmaster makes the whole stack observable and actionable from a single protocol.
Contributing
See CONTRIBUTING.md.
License
MIT © junkyard22