claude-code-swarm 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +22 -1
- package/.claude-plugin/run-agent-inbox-mcp.sh +76 -0
- package/.claude-plugin/run-minimem-mcp.sh +98 -0
- package/.claude-plugin/run-opentasks-mcp.sh +65 -0
- package/CLAUDE.md +200 -36
- package/README.md +65 -0
- package/e2e/helpers/cleanup.mjs +17 -3
- package/e2e/helpers/map-mock-server.mjs +201 -25
- package/e2e/helpers/sidecar.mjs +222 -0
- package/e2e/helpers/workspace.mjs +2 -1
- package/e2e/tier5-sidecar-inbox.test.mjs +900 -0
- package/e2e/tier6-inbox-mcp.test.mjs +173 -0
- package/e2e/tier6-live-agent.test.mjs +759 -0
- package/e2e/vitest.config.e2e.mjs +1 -1
- package/hooks/hooks.json +15 -8
- package/package.json +13 -1
- package/references/agent-inbox/CLAUDE.md +151 -0
- package/references/agent-inbox/README.md +238 -0
- package/references/agent-inbox/docs/CLAUDE-CODE-SWARM-PROPOSAL.md +137 -0
- package/references/agent-inbox/docs/DESIGN.md +1156 -0
- package/references/agent-inbox/hooks/inbox-hook.mjs +119 -0
- package/references/agent-inbox/hooks/register-hook.mjs +69 -0
- package/references/agent-inbox/package-lock.json +3347 -0
- package/references/agent-inbox/package.json +58 -0
- package/references/agent-inbox/rules/agent-inbox.md +78 -0
- package/references/agent-inbox/src/federation/address.ts +61 -0
- package/references/agent-inbox/src/federation/connection-manager.ts +573 -0
- package/references/agent-inbox/src/federation/delivery-queue.ts +222 -0
- package/references/agent-inbox/src/federation/index.ts +6 -0
- package/references/agent-inbox/src/federation/routing-engine.ts +188 -0
- package/references/agent-inbox/src/federation/trust.ts +71 -0
- package/references/agent-inbox/src/index.ts +390 -0
- package/references/agent-inbox/src/ipc/ipc-server.ts +207 -0
- package/references/agent-inbox/src/jsonrpc/mail-server.ts +382 -0
- package/references/agent-inbox/src/map/map-client.ts +414 -0
- package/references/agent-inbox/src/mcp/mcp-server.ts +272 -0
- package/references/agent-inbox/src/mesh/delivery-bridge.ts +110 -0
- package/references/agent-inbox/src/mesh/mesh-connector.ts +41 -0
- package/references/agent-inbox/src/mesh/mesh-transport.ts +157 -0
- package/references/agent-inbox/src/mesh/type-mapper.ts +239 -0
- package/references/agent-inbox/src/push/notifier.ts +233 -0
- package/references/agent-inbox/src/registry/warm-registry.ts +255 -0
- package/references/agent-inbox/src/router/message-router.ts +175 -0
- package/references/agent-inbox/src/storage/interface.ts +48 -0
- package/references/agent-inbox/src/storage/memory.ts +145 -0
- package/references/agent-inbox/src/storage/sqlite.ts +671 -0
- package/references/agent-inbox/src/traceability/traceability.ts +183 -0
- package/references/agent-inbox/src/types.ts +303 -0
- package/references/agent-inbox/test/federation/address.test.ts +101 -0
- package/references/agent-inbox/test/federation/connection-manager.test.ts +546 -0
- package/references/agent-inbox/test/federation/delivery-queue.test.ts +159 -0
- package/references/agent-inbox/test/federation/integration.test.ts +857 -0
- package/references/agent-inbox/test/federation/routing-engine.test.ts +117 -0
- package/references/agent-inbox/test/federation/sdk-integration.test.ts +744 -0
- package/references/agent-inbox/test/federation/trust.test.ts +89 -0
- package/references/agent-inbox/test/ipc-jsonrpc.test.ts +113 -0
- package/references/agent-inbox/test/ipc-server.test.ts +197 -0
- package/references/agent-inbox/test/mail-server.test.ts +285 -0
- package/references/agent-inbox/test/map-client.test.ts +408 -0
- package/references/agent-inbox/test/mesh/delivery-bridge.test.ts +178 -0
- package/references/agent-inbox/test/mesh/e2e-mesh.test.ts +527 -0
- package/references/agent-inbox/test/mesh/e2e-real-meshpeer.test.ts +629 -0
- package/references/agent-inbox/test/mesh/federation-mesh.test.ts +269 -0
- package/references/agent-inbox/test/mesh/mesh-connector.test.ts +66 -0
- package/references/agent-inbox/test/mesh/mesh-transport.test.ts +191 -0
- package/references/agent-inbox/test/mesh/meshpeer-integration.test.ts +442 -0
- package/references/agent-inbox/test/mesh/mock-mesh.ts +125 -0
- package/references/agent-inbox/test/mesh/mock-meshpeer.ts +266 -0
- package/references/agent-inbox/test/mesh/type-mapper.test.ts +226 -0
- package/references/agent-inbox/test/message-router.test.ts +184 -0
- package/references/agent-inbox/test/push-notifier.test.ts +139 -0
- package/references/agent-inbox/test/registry/warm-registry.test.ts +171 -0
- package/references/agent-inbox/test/sqlite-prefix.test.ts +192 -0
- package/references/agent-inbox/test/sqlite-storage.test.ts +243 -0
- package/references/agent-inbox/test/storage.test.ts +196 -0
- package/references/agent-inbox/test/traceability.test.ts +123 -0
- package/references/agent-inbox/test/wake.test.ts +330 -0
- package/references/agent-inbox/tsconfig.json +20 -0
- package/references/agent-inbox/tsup.config.ts +10 -0
- package/references/agent-inbox/vitest.config.ts +8 -0
- package/references/minimem/.claude/settings.json +7 -0
- package/references/minimem/.sudocode/issues.jsonl +18 -0
- package/references/minimem/.sudocode/specs.jsonl +1 -0
- package/references/minimem/CLAUDE.md +329 -0
- package/references/minimem/README.md +565 -0
- package/references/minimem/claude-plugin/.claude-plugin/plugin.json +10 -0
- package/references/minimem/claude-plugin/.mcp.json +7 -0
- package/references/minimem/claude-plugin/README.md +158 -0
- package/references/minimem/claude-plugin/commands/recall.md +47 -0
- package/references/minimem/claude-plugin/commands/remember.md +41 -0
- package/references/minimem/claude-plugin/hooks/__tests__/hooks.test.ts +272 -0
- package/references/minimem/claude-plugin/hooks/hooks.json +27 -0
- package/references/minimem/claude-plugin/hooks/session-end.sh +86 -0
- package/references/minimem/claude-plugin/hooks/session-start.sh +85 -0
- package/references/minimem/claude-plugin/skills/memory/SKILL.md +108 -0
- package/references/minimem/media/banner.png +0 -0
- package/references/minimem/package-lock.json +5373 -0
- package/references/minimem/package.json +76 -0
- package/references/minimem/scripts/postbuild.js +49 -0
- package/references/minimem/src/__tests__/edge-cases.test.ts +371 -0
- package/references/minimem/src/__tests__/errors.test.ts +265 -0
- package/references/minimem/src/__tests__/helpers.ts +199 -0
- package/references/minimem/src/__tests__/internal.test.ts +407 -0
- package/references/minimem/src/__tests__/knowledge-frontmatter.test.ts +148 -0
- package/references/minimem/src/__tests__/knowledge.test.ts +148 -0
- package/references/minimem/src/__tests__/minimem.integration.test.ts +1127 -0
- package/references/minimem/src/__tests__/session.test.ts +190 -0
- package/references/minimem/src/cli/__tests__/commands.test.ts +760 -0
- package/references/minimem/src/cli/__tests__/contained-layout.test.ts +286 -0
- package/references/minimem/src/cli/commands/__tests__/conflicts.test.ts +141 -0
- package/references/minimem/src/cli/commands/append.ts +76 -0
- package/references/minimem/src/cli/commands/config.ts +262 -0
- package/references/minimem/src/cli/commands/conflicts.ts +415 -0
- package/references/minimem/src/cli/commands/daemon.ts +169 -0
- package/references/minimem/src/cli/commands/index.ts +12 -0
- package/references/minimem/src/cli/commands/init.ts +166 -0
- package/references/minimem/src/cli/commands/mcp.ts +221 -0
- package/references/minimem/src/cli/commands/push-pull.ts +213 -0
- package/references/minimem/src/cli/commands/search.ts +223 -0
- package/references/minimem/src/cli/commands/status.ts +84 -0
- package/references/minimem/src/cli/commands/store.ts +189 -0
- package/references/minimem/src/cli/commands/sync-init.ts +290 -0
- package/references/minimem/src/cli/commands/sync.ts +70 -0
- package/references/minimem/src/cli/commands/upsert.ts +197 -0
- package/references/minimem/src/cli/config.ts +611 -0
- package/references/minimem/src/cli/index.ts +299 -0
- package/references/minimem/src/cli/shared.ts +189 -0
- package/references/minimem/src/cli/sync/__tests__/central.test.ts +152 -0
- package/references/minimem/src/cli/sync/__tests__/conflicts.test.ts +209 -0
- package/references/minimem/src/cli/sync/__tests__/daemon.test.ts +118 -0
- package/references/minimem/src/cli/sync/__tests__/detection.test.ts +207 -0
- package/references/minimem/src/cli/sync/__tests__/integration.test.ts +476 -0
- package/references/minimem/src/cli/sync/__tests__/registry.test.ts +363 -0
- package/references/minimem/src/cli/sync/__tests__/state.test.ts +255 -0
- package/references/minimem/src/cli/sync/__tests__/validation.test.ts +193 -0
- package/references/minimem/src/cli/sync/__tests__/watcher.test.ts +178 -0
- package/references/minimem/src/cli/sync/central.ts +292 -0
- package/references/minimem/src/cli/sync/conflicts.ts +205 -0
- package/references/minimem/src/cli/sync/daemon.ts +407 -0
- package/references/minimem/src/cli/sync/detection.ts +138 -0
- package/references/minimem/src/cli/sync/index.ts +107 -0
- package/references/minimem/src/cli/sync/operations.ts +373 -0
- package/references/minimem/src/cli/sync/registry.ts +279 -0
- package/references/minimem/src/cli/sync/state.ts +358 -0
- package/references/minimem/src/cli/sync/validation.ts +206 -0
- package/references/minimem/src/cli/sync/watcher.ts +237 -0
- package/references/minimem/src/cli/version.ts +34 -0
- package/references/minimem/src/core/index.ts +9 -0
- package/references/minimem/src/core/indexer.ts +628 -0
- package/references/minimem/src/core/searcher.ts +221 -0
- package/references/minimem/src/db/schema.ts +183 -0
- package/references/minimem/src/db/sqlite-vec.ts +24 -0
- package/references/minimem/src/embeddings/__tests__/embeddings.test.ts +431 -0
- package/references/minimem/src/embeddings/batch-gemini.ts +392 -0
- package/references/minimem/src/embeddings/batch-openai.ts +409 -0
- package/references/minimem/src/embeddings/embeddings.ts +434 -0
- package/references/minimem/src/index.ts +132 -0
- package/references/minimem/src/internal.ts +299 -0
- package/references/minimem/src/minimem.ts +1291 -0
- package/references/minimem/src/search/__tests__/hybrid.test.ts +247 -0
- package/references/minimem/src/search/graph.ts +234 -0
- package/references/minimem/src/search/hybrid.ts +151 -0
- package/references/minimem/src/search/search.ts +256 -0
- package/references/minimem/src/server/__tests__/mcp.test.ts +347 -0
- package/references/minimem/src/server/__tests__/tools.test.ts +364 -0
- package/references/minimem/src/server/mcp.ts +326 -0
- package/references/minimem/src/server/tools.ts +720 -0
- package/references/minimem/src/session.ts +460 -0
- package/references/minimem/src/store/__tests__/manifest.test.ts +177 -0
- package/references/minimem/src/store/__tests__/materialize.test.ts +52 -0
- package/references/minimem/src/store/__tests__/store-graph.test.ts +228 -0
- package/references/minimem/src/store/index.ts +27 -0
- package/references/minimem/src/store/manifest.ts +203 -0
- package/references/minimem/src/store/materialize.ts +185 -0
- package/references/minimem/src/store/store-graph.ts +252 -0
- package/references/minimem/tsconfig.json +19 -0
- package/references/minimem/tsup.config.ts +26 -0
- package/references/minimem/vitest.config.ts +29 -0
- package/references/openteams/src/cli/generate.ts +23 -1
- package/references/openteams/src/generators/agent-prompt-generator.test.ts +94 -0
- package/references/openteams/src/generators/agent-prompt-generator.ts +42 -13
- package/references/openteams/src/generators/package-generator.ts +9 -1
- package/references/openteams/src/generators/skill-generator.test.ts +28 -0
- package/references/openteams/src/generators/skill-generator.ts +10 -4
- package/references/skill-tree/.claude/settings.json +6 -0
- package/references/skill-tree/.sudocode/issues.jsonl +19 -0
- package/references/skill-tree/.sudocode/specs.jsonl +3 -0
- package/references/skill-tree/CLAUDE.md +132 -0
- package/references/skill-tree/README.md +396 -0
- package/references/skill-tree/docs/GAPS_v1.md +221 -0
- package/references/skill-tree/docs/INTEGRATION_PLAN.md +467 -0
- package/references/skill-tree/docs/TODOS.md +91 -0
- package/references/skill-tree/docs/anthropic_skill_guide.md +1364 -0
- package/references/skill-tree/docs/design/federated-skill-trees.md +524 -0
- package/references/skill-tree/docs/design/multi-agent-sync.md +759 -0
- package/references/skill-tree/docs/scraper/BRAINSTORM.md +583 -0
- package/references/skill-tree/docs/scraper/POC_PLAN.md +420 -0
- package/references/skill-tree/docs/scraper/README.md +170 -0
- package/references/skill-tree/examples/basic-usage.ts +157 -0
- package/references/skill-tree/package-lock.json +1852 -0
- package/references/skill-tree/package.json +66 -0
- package/references/skill-tree/plan.md +78 -0
- package/references/skill-tree/scraper/README.md +123 -0
- package/references/skill-tree/scraper/docs/DESIGN.md +683 -0
- package/references/skill-tree/scraper/docs/PLAN.md +336 -0
- package/references/skill-tree/scraper/drizzle.config.ts +10 -0
- package/references/skill-tree/scraper/package-lock.json +6329 -0
- package/references/skill-tree/scraper/package.json +68 -0
- package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-description.md +7 -0
- package/references/skill-tree/scraper/test/fixtures/invalid-skill/missing-name.md +7 -0
- package/references/skill-tree/scraper/test/fixtures/minimal-skill/SKILL.md +27 -0
- package/references/skill-tree/scraper/test/fixtures/skill-json/SKILL.json +21 -0
- package/references/skill-tree/scraper/test/fixtures/skill-with-meta/SKILL.md +54 -0
- package/references/skill-tree/scraper/test/fixtures/skill-with-meta/_meta.json +24 -0
- package/references/skill-tree/scraper/test/fixtures/valid-skill/SKILL.md +93 -0
- package/references/skill-tree/scraper/test/fixtures/valid-skill/_meta.json +22 -0
- package/references/skill-tree/scraper/tsup.config.ts +14 -0
- package/references/skill-tree/scraper/vitest.config.ts +17 -0
- package/references/skill-tree/scripts/convert-to-vitest.ts +166 -0
- package/references/skill-tree/skills/skill-writer/SKILL.md +339 -0
- package/references/skill-tree/skills/skill-writer/references/examples.md +326 -0
- package/references/skill-tree/skills/skill-writer/references/patterns.md +210 -0
- package/references/skill-tree/skills/skill-writer/references/quality-checklist.md +123 -0
- package/references/skill-tree/test/run-all.ts +106 -0
- package/references/skill-tree/test/utils.ts +128 -0
- package/references/skill-tree/vitest.config.ts +16 -0
- package/references/swarmkit/src/commands/init/phases/configure.ts +0 -22
- package/references/swarmkit/src/commands/init/phases/global-setup.ts +5 -3
- package/references/swarmkit/src/commands/init/wizard.ts +2 -2
- package/references/swarmkit/src/packages/setup.test.ts +53 -7
- package/references/swarmkit/src/packages/setup.ts +37 -1
- package/scripts/bootstrap.mjs +26 -1
- package/scripts/generate-agents.mjs +5 -1
- package/scripts/map-hook.mjs +97 -64
- package/scripts/map-sidecar.mjs +179 -25
- package/scripts/team-loader.mjs +12 -41
- package/skills/swarm/SKILL.md +89 -25
- package/src/__tests__/agent-generator.test.mjs +6 -13
- package/src/__tests__/bootstrap.test.mjs +124 -1
- package/src/__tests__/config.test.mjs +200 -27
- package/src/__tests__/e2e-live-map.test.mjs +536 -0
- package/src/__tests__/e2e-mesh-sidecar.test.mjs +570 -0
- package/src/__tests__/e2e-native-task-hooks.test.mjs +376 -0
- package/src/__tests__/e2e-sidecar-bridge.test.mjs +477 -0
- package/src/__tests__/helpers.mjs +13 -0
- package/src/__tests__/inbox.test.mjs +22 -89
- package/src/__tests__/index.test.mjs +35 -9
- package/src/__tests__/integration.test.mjs +513 -0
- package/src/__tests__/map-events.test.mjs +514 -150
- package/src/__tests__/mesh-connection.test.mjs +308 -0
- package/src/__tests__/opentasks-client.test.mjs +517 -0
- package/src/__tests__/paths.test.mjs +185 -41
- package/src/__tests__/sidecar-client.test.mjs +35 -0
- package/src/__tests__/sidecar-server.test.mjs +124 -0
- package/src/__tests__/skilltree-client.test.mjs +80 -0
- package/src/agent-generator.mjs +104 -33
- package/src/bootstrap.mjs +150 -10
- package/src/config.mjs +81 -17
- package/src/context-output.mjs +58 -8
- package/src/inbox.mjs +9 -54
- package/src/index.mjs +39 -8
- package/src/map-connection.mjs +4 -3
- package/src/map-events.mjs +350 -80
- package/src/mesh-connection.mjs +148 -0
- package/src/opentasks-client.mjs +269 -0
- package/src/paths.mjs +182 -27
- package/src/sessionlog.mjs +14 -9
- package/src/sidecar-client.mjs +81 -27
- package/src/sidecar-server.mjs +175 -16
- package/src/skilltree-client.mjs +173 -0
- package/src/template.mjs +68 -4
- package/vitest.config.mjs +1 -0
|
@@ -0,0 +1,1156 @@
|
|
|
1
|
+
# Agent Inbox: MAP-Native Agent Message Router
|
|
2
|
+
|
|
3
|
+
## Problem Statement
|
|
4
|
+
|
|
5
|
+
Multi-agent coding workflows (like [claude-code-swarm](https://github.com/alexngai/claude-code-swarm)) need structured inter-agent messaging that works across process and federation boundaries. Today, agents within a swarm coordinate via native team primitives (SendMessage, TaskCreate) for in-process communication, and use MAP sidecars for external observability. But there's a gap:
|
|
6
|
+
|
|
7
|
+
1. **Cross-process messaging** — Agents in different Claude Code sessions (different repos, different machines) can't message each other. The MAP sidecar broadcasts events but doesn't provide structured inbox/routing.
|
|
8
|
+
|
|
9
|
+
2. **Structured message handling** — MAP provides transport (WebSocket, scopes, send/receive), but doesn't define inbox semantics, message threading, read tracking, or delivery guarantees. Agents need more than raw message passing.
|
|
10
|
+
|
|
11
|
+
3. **Federation** — Different swarm instances, potentially on different networks, need to exchange messages with identity resolution and trust.
|
|
12
|
+
|
|
13
|
+
**Agent Inbox** fills this gap: a MAP-native message router that provides structured inbox/outbox semantics on top of MAP's transport and identity primitives. It connects to MAP servers (like the one in agentic-mesh or claude-code-swarm's sidecar) and adds the routing, threading, and delivery layer that agents need for real coordination.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Design Goals
|
|
18
|
+
|
|
19
|
+
1. **MAP-native**: Built on MAP's agent identity, scoping, and messaging primitives — not a parallel system
|
|
20
|
+
2. **Federation-first**: Cross-process and cross-instance messaging is the primary use case, not an afterthought
|
|
21
|
+
3. **Structured messaging**: Inboxes, threading, read tracking, delivery confirmation — beyond raw MAP send/receive
|
|
22
|
+
4. **Agent-friendly interface**: MCP tools that any coding agent can call naturally
|
|
23
|
+
5. **Traceability via MAP conventions**: Conversations, turns, and threads follow MAP's mail spec for observability
|
|
24
|
+
6. **Progressive complexity**: Simple send/receive works immediately; threading, conversations, and federation are opt-in
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Architecture Overview
|
|
29
|
+
|
|
30
|
+
Agent Inbox and the MAP sidecar are **independent services**, each with its own MAP connection and its own responsibilities. The **plugin layer** (`map-events.mjs` in claude-code-swarm) acts as the router — dispatching lifecycle commands to the sidecar and messaging commands to Agent Inbox. No event bus is needed.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
34
|
+
│ Local System (one machine) │
|
|
35
|
+
│ │
|
|
36
|
+
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|
37
|
+
│ │ Claude Code Session │ │
|
|
38
|
+
│ │ │ │
|
|
39
|
+
│ │ Hooks (SessionStart, PreToolUse, PostToolUse, etc.) │ │
|
|
40
|
+
│ │ │ │ │
|
|
41
|
+
│ │ └─ Plugin (map-hook.mjs / map-events.mjs) │ │
|
|
42
|
+
│ │ │ │ │
|
|
43
|
+
│ │ ├─ spawn/done/state/trajectory ──► sendToSidecar() │ │
|
|
44
|
+
│ │ │ │ │
|
|
45
|
+
│ │ └─ send/emit (messaging) ────────► sendToInbox() │ │
|
|
46
|
+
│ └───────────┬──────────────────────────────────────┬────────────────────┘ │
|
|
47
|
+
│ │ │ │
|
|
48
|
+
│ ┌──────────▼──────────┐ ┌──────────▼──────────────┐ │
|
|
49
|
+
│ │ MAP Sidecar │ │ Agent Inbox │ │
|
|
50
|
+
│ │ │ │ │ │
|
|
51
|
+
│ │ Receives from │ │ Receives from │ │
|
|
52
|
+
│ │ plugin: │ │ plugin: │ │
|
|
53
|
+
│ │ spawn/done/state │ │ send (messaging) │ │
|
|
54
|
+
│ │ trajectory │ │ agent notifications │ │
|
|
55
|
+
│ │ ping │ │ (spawn/done events) │ │
|
|
56
|
+
│ │ │ │ │ │
|
|
57
|
+
│ │ Owns: │ │ Owns: │ │
|
|
58
|
+
│ │ Hook IPC (UNIX) │ │ Inbox/outbox mgmt │ │
|
|
59
|
+
│ │ Agent lifecycle │ │ Message routing │ │
|
|
60
|
+
│ │ Trajectory sync │ │ Threading │ │
|
|
61
|
+
│ │ Process mgmt │ │ Delivery tracking │ │
|
|
62
|
+
│ │ │ │ Traceability │ │
|
|
63
|
+
│ │ │ │ Federation │ │
|
|
64
|
+
│ │ │ │ MCP tools interface │ │
|
|
65
|
+
│ │ │ │ MAP JSON-RPC │ │
|
|
66
|
+
│ └──────────┬───────────┘ └──────────┬───────────────┘ │
|
|
67
|
+
│ │ │ │
|
|
68
|
+
│ MAP Connection MAP Connection(s) │
|
|
69
|
+
│ (local server) (local + remote) │
|
|
70
|
+
│ │ │ │
|
|
71
|
+
└──────────────┼─────────────────────────────────────────┼─────────────────────┘
|
|
72
|
+
│ │
|
|
73
|
+
┌──────▼──────┐ ┌─────────────▼─────────────┐
|
|
74
|
+
│ MAP Server │ │ MAP Server(s) │
|
|
75
|
+
│ (local) │ │ (local + remote/federated)│
|
|
76
|
+
└─────────────┘ └───────────────────────────┘
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Plugin-as-Router Pattern
|
|
80
|
+
|
|
81
|
+
The claude-code-swarm plugin (`map-events.mjs`) already dispatches commands by type. Today, all commands go to the sidecar. With Agent Inbox, the plugin gains a second dispatch target:
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
Hooks → Plugin (map-hook.mjs / map-events.mjs)
|
|
85
|
+
│
|
|
86
|
+
├─ spawn/done/state/trajectory → sendToSidecar() → Sidecar → MAP (lifecycle)
|
|
87
|
+
│
|
|
88
|
+
└─ send/emit (messaging) ──────→ sendToInbox() → Agent Inbox → MAP (messaging)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The plugin also notifies Agent Inbox about agent lifecycle events (spawn/done) so it can maintain its agent registry. This is a lightweight notification — not a bus subscription — just a `sendToInbox({ action: "notify", event: ... })` alongside the existing `sendToSidecar()` call.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// In map-events.mjs — sendCommand() gains a second dispatch path
|
|
95
|
+
|
|
96
|
+
export async function sendCommand(config, command, sessionId) {
|
|
97
|
+
const sPaths = sessionPaths(sessionId);
|
|
98
|
+
|
|
99
|
+
if (command.action === "send" || command.action === "emit") {
|
|
100
|
+
// Messaging commands → Agent Inbox
|
|
101
|
+
const sent = await sendToInbox(command, inboxSocketPath(sessionId));
|
|
102
|
+
if (!sent) {
|
|
103
|
+
// Fall back to sidecar (existing behavior)
|
|
104
|
+
await sendToSidecar(command, sPaths.socketPath);
|
|
105
|
+
}
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Lifecycle commands → Sidecar (existing behavior)
|
|
110
|
+
const sent = await sendToSidecar(command, sPaths.socketPath);
|
|
111
|
+
if (!sent) {
|
|
112
|
+
const recovered = await ensureSidecar(config, sessionId);
|
|
113
|
+
if (recovered) {
|
|
114
|
+
await sendToSidecar(command, sPaths.socketPath);
|
|
115
|
+
} else if (command.action === "emit") {
|
|
116
|
+
await fireAndForget(config, command.event);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Also notify Agent Inbox about lifecycle events (for registry)
|
|
121
|
+
if (command.action === "spawn" || command.action === "done") {
|
|
122
|
+
await sendToInbox(
|
|
123
|
+
{ action: "notify", event: { type: `agent.${command.action}`, ...command } },
|
|
124
|
+
inboxSocketPath(sessionId)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Why Two MAP Connections? Concern-Based Split
|
|
131
|
+
|
|
132
|
+
The sidecar and Agent Inbox each maintain their own MAP connection(s) because the **nature of their traffic is fundamentally different**:
|
|
133
|
+
|
|
134
|
+
**Sidecar's MAP connection — lifecycle & observability:**
|
|
135
|
+
- `conn.spawn()` / `conn.done()` — agent registration/unregistration
|
|
136
|
+
- `conn.updateState()` — idle/active state changes
|
|
137
|
+
- `conn.send({ scope }, payload)` — broadcast task lifecycle events (`task.dispatched`, `task.completed`, `task.sync`)
|
|
138
|
+
- `conn.callExtension("trajectory/checkpoint")` — trajectory reporting
|
|
139
|
+
- These are **fire-and-forget observability events**. If they fail, the swarm keeps working. The sidecar already has fallback logic for this (fire-and-forget via ephemeral connections).
|
|
140
|
+
|
|
141
|
+
**Agent Inbox's MAP connection(s) — messaging & mail:**
|
|
142
|
+
- `conn.send(target, message)` — routed message delivery to specific agents
|
|
143
|
+
- Receiving incoming messages addressed to local agents
|
|
144
|
+
- MAP `mail/*` operations (conversations, turns, threads)
|
|
145
|
+
- Federation connections to remote MAP servers
|
|
146
|
+
- These are **structured communications that need tracking**. Delivery matters. Messages are stored, threaded, and tracked.
|
|
147
|
+
|
|
148
|
+
| | Sidecar traffic | Agent Inbox traffic |
|
|
149
|
+
|---|---|---|
|
|
150
|
+
| **Pattern** | Broadcast / fire-and-forget | Targeted / tracked delivery |
|
|
151
|
+
| **Failure mode** | Silently drop | Queue and retry |
|
|
152
|
+
| **Recipients** | Scope (all observers) | Specific agent(s) |
|
|
153
|
+
| **Persistence** | Ephemeral | Stored in inbox |
|
|
154
|
+
| **Threading** | None | Conversations, reply chains |
|
|
155
|
+
| **Federation** | No | Yes (remote MAP servers) |
|
|
156
|
+
|
|
157
|
+
Two connections to the same local MAP server is fine — they're doing different things. Agent Inbox additionally connects to **remote** MAP servers for federation, which the sidecar has no involvement in.
|
|
158
|
+
|
|
159
|
+
### MAP Connection Ownership Summary
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
Sidecar MAP connection: Agent Inbox MAP connection(s):
|
|
163
|
+
conn.spawn() conn.send(target, message) ← message delivery
|
|
164
|
+
conn.done() conn.onMessage() ← message receiving
|
|
165
|
+
conn.updateState() mail/* operations
|
|
166
|
+
conn.send({ scope }, event) Federation to remote servers
|
|
167
|
+
conn.callExtension(trajectory)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Standalone Mode
|
|
171
|
+
|
|
172
|
+
Agent Inbox also works **without a sidecar** — for non-swarm use cases, direct MCP tool usage, or testing. It has its own MAP connection and its own MCP tools interface. The sidecar is an integration point for Claude Code hooks, not a dependency.
|
|
173
|
+
|
|
174
|
+
### How Agent Inbox Learns About Agents
|
|
175
|
+
|
|
176
|
+
Without an event bus, Agent Inbox needs another way to know which agents exist. Two complementary mechanisms:
|
|
177
|
+
|
|
178
|
+
1. **Plugin notifications**: The plugin sends lightweight `notify` commands to Agent Inbox on `spawn`/`done` events. Agent Inbox updates its agent registry.
|
|
179
|
+
|
|
180
|
+
2. **MAP server query** (startup hydration): On startup, Agent Inbox can query the MAP server for currently registered agents to hydrate its registry. This handles the case where Agent Inbox starts after agents are already spawned.
|
|
181
|
+
|
|
182
|
+
3. **Auto-registration**: Agents are automatically registered on their first `check_inbox` call or via the `SessionStart` hook (`hooks/register-hook.mjs`), which is the primary path for non-swarm use cases.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// Agent Inbox command handler for lifecycle notifications from the plugin
|
|
186
|
+
case "notify":
|
|
187
|
+
if (command.event.type === "agent.spawn") {
|
|
188
|
+
await storage.putAgent({
|
|
189
|
+
agent_id: command.event.agent.agentId,
|
|
190
|
+
display_name: command.event.agent.name,
|
|
191
|
+
scope: command.event.agent.scopes?.[0],
|
|
192
|
+
status: "active",
|
|
193
|
+
metadata: command.event.agent.metadata,
|
|
194
|
+
registered_at: new Date().toISOString(),
|
|
195
|
+
last_active_at: new Date().toISOString(),
|
|
196
|
+
});
|
|
197
|
+
} else if (command.event.type === "agent.done") {
|
|
198
|
+
const agent = await storage.getAgent(command.event.agentId);
|
|
199
|
+
if (agent) {
|
|
200
|
+
agent.status = "offline";
|
|
201
|
+
await storage.putAgent(agent);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
break;
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### How Agent Inbox Processes Messages
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
// Agent Inbox receives messaging commands from the plugin via UNIX socket
|
|
211
|
+
case "send":
|
|
212
|
+
// 1. Create a Message record with tracking metadata
|
|
213
|
+
const message = await storage.putMessage({
|
|
214
|
+
id: ulid(),
|
|
215
|
+
scope: command.scope || defaultScope,
|
|
216
|
+
sender_id: command.from || sidecarAgentId,
|
|
217
|
+
recipients: resolveRecipients(command.to),
|
|
218
|
+
content: normalizeContent(command.payload),
|
|
219
|
+
thread_tag: command.threadTag,
|
|
220
|
+
in_reply_to: command.inReplyTo,
|
|
221
|
+
created_at: new Date().toISOString(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// 2. Route to recipients via Agent Inbox's OWN MAP connection
|
|
225
|
+
for (const recipient of message.recipients) {
|
|
226
|
+
if (isLocal(recipient.agent_id)) {
|
|
227
|
+
await mapConn.send(recipient.agent_id, {
|
|
228
|
+
messageId: message.id,
|
|
229
|
+
from: message.sender_id,
|
|
230
|
+
content: message.content,
|
|
231
|
+
threadTag: message.thread_tag,
|
|
232
|
+
conversationId: message.conversation_id,
|
|
233
|
+
});
|
|
234
|
+
recipient.delivered_at = new Date().toISOString();
|
|
235
|
+
} else {
|
|
236
|
+
// Remote delivery via federated MAP connection
|
|
237
|
+
await federatedConn.send(recipient.agent_id, message);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 3. Record in traceability layer
|
|
242
|
+
traceability.recordTurn(message);
|
|
243
|
+
break;
|
|
244
|
+
|
|
245
|
+
// Agent Inbox also receives messages on its MAP connection (from other agents/servers)
|
|
246
|
+
mapConn.onMessage(async (msg) => {
|
|
247
|
+
// Store in recipient's inbox
|
|
248
|
+
const message = await storage.putMessage(normalizeIncoming(msg));
|
|
249
|
+
|
|
250
|
+
// Write to the local inbox.jsonl for Claude's context injection
|
|
251
|
+
writeToInbox({
|
|
252
|
+
from: message.sender_id,
|
|
253
|
+
payload: message.content,
|
|
254
|
+
timestamp: message.created_at,
|
|
255
|
+
messageId: message.id,
|
|
256
|
+
conversationId: message.conversation_id,
|
|
257
|
+
}, inboxPath);
|
|
258
|
+
|
|
259
|
+
// Record in traceability
|
|
260
|
+
traceability.recordTurn(message);
|
|
261
|
+
});
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## Relationship to MAP
|
|
267
|
+
|
|
268
|
+
Agent Inbox is **not** a MAP server. It's a MAP **client** that connects to one or more MAP servers and provides higher-level messaging semantics.
|
|
269
|
+
|
|
270
|
+
```
|
|
271
|
+
MAP Server = transport + identity + scoping + raw send/receive
|
|
272
|
+
Agent Inbox = inbox routing + threading + delivery + traceability
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
| Concern | Handled by MAP Server | Handled by Agent Inbox |
|
|
276
|
+
|---------|----------------------|----------------------|
|
|
277
|
+
| Agent identity & registration | Yes (spawn, agentId) | Uses MAP's identity primitives |
|
|
278
|
+
| Scoping / namespaces | Yes (scopes) | Maps scopes to workspaces |
|
|
279
|
+
| Raw message send/receive | Yes (conn.send) | Wraps with inbox/routing semantics |
|
|
280
|
+
| Message threading | No | Yes (thread_tag, in_reply_to, conversations) |
|
|
281
|
+
| Inbox / read tracking | No | Yes (per-agent inbox, read_at, ack_at) |
|
|
282
|
+
| Delivery confirmation | No | Yes (delivered_at, read receipts) |
|
|
283
|
+
| Cross-server routing | No (single server) | Yes (connected to multiple MAP servers) |
|
|
284
|
+
| Traceability (mail/*) | Partial (events) | Full (conversations, turns, threads, replay) |
|
|
285
|
+
| Message persistence | No (ephemeral) | Yes (in-memory or SQLite) |
|
|
286
|
+
|
|
287
|
+
### MAP Identity Primitives
|
|
288
|
+
|
|
289
|
+
Agent Inbox uses MAP's agent identity system rather than inventing its own:
|
|
290
|
+
|
|
291
|
+
- **agentId**: MAP's unique agent identifier — used as the primary identity
|
|
292
|
+
- **Scopes**: MAP's namespace mechanism — used as workspace equivalent
|
|
293
|
+
- **spawn/unregister**: MAP's agent lifecycle — Agent Inbox observes these
|
|
294
|
+
- **Display names**: Optional memorable names layered on top of MAP agentIds for human readability
|
|
295
|
+
|
|
296
|
+
Identity persistence is left to the connecting client. A client can reconnect with the same `agentId` to reclaim its inbox, or register fresh. Agent Inbox doesn't force a persistence model.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Data Model
|
|
301
|
+
|
|
302
|
+
### Messaging Entities
|
|
303
|
+
|
|
304
|
+
```
|
|
305
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
306
|
+
│ Agent │
|
|
307
|
+
│ agent_id: string ← MAP agentId │
|
|
308
|
+
│ display_name?: string (memorable, e.g. "GreenCastle") │
|
|
309
|
+
│ program?: string (e.g. "claude-code", "codex") │
|
|
310
|
+
│ model?: string (e.g. "claude-opus-4-6") │
|
|
311
|
+
│ scope: string ← MAP scope (workspace equiv) │
|
|
312
|
+
│ status: active | idle | offline │
|
|
313
|
+
│ metadata: Record<string, unknown> │
|
|
314
|
+
│ registered_at: timestamp │
|
|
315
|
+
│ last_active_at: timestamp │
|
|
316
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
317
|
+
|
|
318
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
319
|
+
│ Message │
|
|
320
|
+
│ id: string (ulid) │
|
|
321
|
+
│ scope: string ← MAP scope │
|
|
322
|
+
│ sender_id: string ← MAP agentId │
|
|
323
|
+
│ recipients: Recipient[] │
|
|
324
|
+
│ subject?: string │
|
|
325
|
+
│ content: MessageContent (see Message Format below) │
|
|
326
|
+
│ thread_tag?: string (user-defined grouping) │
|
|
327
|
+
│ in_reply_to?: string (message id) │
|
|
328
|
+
│ importance: low | normal | high | urgent │
|
|
329
|
+
│ metadata: Record<string, unknown> │
|
|
330
|
+
│ created_at: timestamp │
|
|
331
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
332
|
+
|
|
333
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
334
|
+
│ Recipient │
|
|
335
|
+
│ agent_id: string ← MAP agentId │
|
|
336
|
+
│ kind: to | cc | bcc │
|
|
337
|
+
│ delivered_at?: timestamp │
|
|
338
|
+
│ read_at?: timestamp │
|
|
339
|
+
│ ack_at?: timestamp │
|
|
340
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Traceability Entities (MAP mail conventions)
|
|
344
|
+
|
|
345
|
+
These follow MAP's mail specification. Agent Inbox auto-creates and maintains them from messaging events, following MAP convention: a message with a `conversationId` is added to that conversation; without one, the server groups by thread context or auto-creates.
|
|
346
|
+
|
|
347
|
+
```
|
|
348
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
349
|
+
│ Conversation │
|
|
350
|
+
│ id: string ("conv-{ulid}") ← MAP mail format │
|
|
351
|
+
│ scope: string ← MAP scope │
|
|
352
|
+
│ subject?: string │
|
|
353
|
+
│ status: active | completed | archived │
|
|
354
|
+
│ participants: Participant[] │
|
|
355
|
+
│ metadata: Record<string, unknown> │
|
|
356
|
+
│ created_at: timestamp │
|
|
357
|
+
│ updated_at: timestamp │
|
|
358
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
359
|
+
|
|
360
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
361
|
+
│ Turn │
|
|
362
|
+
│ id: string ("turn-{ulid}") ← MAP mail format │
|
|
363
|
+
│ conversation_id: string │
|
|
364
|
+
│ participant_id: string ← MAP agentId │
|
|
365
|
+
│ source_message_id?: string ← links to Message │
|
|
366
|
+
│ content_type: text | data | event | reference | x-* │
|
|
367
|
+
│ content: TurnContent │
|
|
368
|
+
│ thread_id?: string │
|
|
369
|
+
│ in_reply_to?: string (turn id) │
|
|
370
|
+
│ created_at: timestamp │
|
|
371
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
372
|
+
|
|
373
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
374
|
+
│ Thread │
|
|
375
|
+
│ id: string ("thread-{ulid}") ← MAP mail format │
|
|
376
|
+
│ conversation_id: string │
|
|
377
|
+
│ root_turn_id: string │
|
|
378
|
+
│ parent_thread_id?: string │
|
|
379
|
+
│ subject?: string │
|
|
380
|
+
│ created_at: timestamp │
|
|
381
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### How the Layers Connect
|
|
385
|
+
|
|
386
|
+
| Messaging Event | Traceability Record |
|
|
387
|
+
|-----------------|---------------------|
|
|
388
|
+
| Message sent with `conversationId` | Turn added to that Conversation |
|
|
389
|
+
| Message sent with `thread_tag`, no `conversationId` | Turn added to Conversation auto-created for that thread_tag + scope |
|
|
390
|
+
| Message sent with neither | Turn added to a catch-all Conversation for that scope |
|
|
391
|
+
| Reply (message with `in_reply_to`) | Turn with `in_reply_to` in same Thread (auto-created from reply chain) |
|
|
392
|
+
| Agent registers (observed via MAP) | Participant added to scope's active Conversations |
|
|
393
|
+
|
|
394
|
+
The traceability layer **derives** its state from messaging events. It follows MAP's convention: if a message includes a `conversationId`, it's placed there; otherwise the server groups intelligently.
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
## Message Format
|
|
399
|
+
|
|
400
|
+
Messages use a **structured content model** inspired by MAP's content types, with a convenient plain-text shorthand for simple cases.
|
|
401
|
+
|
|
402
|
+
### Content Structure
|
|
403
|
+
|
|
404
|
+
```typescript
|
|
405
|
+
type MessageContent =
|
|
406
|
+
| { type: "text"; text: string } // Markdown text
|
|
407
|
+
| { type: "data"; schema?: string; data: unknown } // Structured data (JSON)
|
|
408
|
+
| { type: "event"; event: string; data?: unknown } // Lifecycle/coordination event
|
|
409
|
+
| { type: "reference"; uri: string; label?: string } // Reference to external resource
|
|
410
|
+
| { type: string; [key: string]: unknown } // Extension types (x-*)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### MCP Tool Shorthand
|
|
414
|
+
|
|
415
|
+
When agents use MCP tools, they can pass a plain markdown string as the body. The service layer wraps it:
|
|
416
|
+
|
|
417
|
+
```
|
|
418
|
+
Agent calls: send_message(body="Here's the API contract...")
|
|
419
|
+
Stored as: { type: "text", text: "Here's the API contract..." }
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
For structured payloads (task assignments, status updates, etc.), agents can pass the full content object:
|
|
423
|
+
|
|
424
|
+
```
|
|
425
|
+
Agent calls: send_message(content={ type: "data", schema: "task-assignment", data: { task: "...", assignee: "..." } })
|
|
426
|
+
Stored as-is
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Extension Content Types
|
|
430
|
+
|
|
431
|
+
For coordination-specific payloads, use `x-*` prefixed types:
|
|
432
|
+
|
|
433
|
+
| Type | Use Case | Example |
|
|
434
|
+
|------|----------|---------|
|
|
435
|
+
| `x-task-dispatched` | Task delegation | `{ type: "x-task-dispatched", task: "...", assignee: "..." }` |
|
|
436
|
+
| `x-task-completed` | Task completion | `{ type: "x-task-completed", task: "...", summary: "..." }` |
|
|
437
|
+
| `x-status-update` | Agent status change | `{ type: "x-status-update", status: "idle", reason: "..." }` |
|
|
438
|
+
|
|
439
|
+
This aligns with claude-code-swarm's existing payload types (`task.dispatched`, `task.completed`, `task.sync`) — Agent Inbox just structures them as typed content.
|
|
440
|
+
|
|
441
|
+
### Conversation Threading
|
|
442
|
+
|
|
443
|
+
Threading works at two levels:
|
|
444
|
+
|
|
445
|
+
1. **thread_tag** (lightweight): A user-defined string (e.g., `"auth-sprint-1"`) that groups messages. The traceability layer auto-creates a Conversation for each unique thread_tag + scope combination. Agents just tag messages; the server handles the rest.
|
|
446
|
+
|
|
447
|
+
2. **in_reply_to** (structural): A message references another message's ID, creating a reply chain. The traceability layer auto-creates Threads within a Conversation from reply chains. Multiple reply chains in the same Conversation become separate Threads.
|
|
448
|
+
|
|
449
|
+
3. **Explicit conversations**: Agents can also create conversations directly via `mail/create` and reference the `conversationId` in messages, giving full control when needed.
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Messaging Patterns
|
|
454
|
+
|
|
455
|
+
### 1. Local Messaging (same MAP server / same scope)
|
|
456
|
+
|
|
457
|
+
Agents connected through the same MAP server, in the same scope.
|
|
458
|
+
|
|
459
|
+
```
|
|
460
|
+
AgentA → send_message(to="AgentB", body="...")
|
|
461
|
+
→ Agent Inbox routes locally (both in same scope on same MAP server)
|
|
462
|
+
→ Message appears in AgentB's inbox
|
|
463
|
+
AgentB → check_inbox() → sees message
|
|
464
|
+
AgentB → reply(in_reply_to=msg_id, body="...")
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
This is the claude-code-swarm intra-team case. Today the sidecar broadcasts via `conn.send({ scope }, payload)`. With Agent Inbox, messages are routed to specific recipients with inbox tracking.
|
|
468
|
+
|
|
469
|
+
### 2. Cross-Scope Messaging (same MAP server, different scopes)
|
|
470
|
+
|
|
471
|
+
Agents in different scopes (workspaces/teams) on the same MAP server.
|
|
472
|
+
|
|
473
|
+
```
|
|
474
|
+
AgentA (scope: "frontend") → send_message(
|
|
475
|
+
to="AgentB",
|
|
476
|
+
to_scope="backend",
|
|
477
|
+
body="..."
|
|
478
|
+
)
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
Agent Inbox resolves the target agent in the target scope and routes via the shared MAP server.
|
|
482
|
+
|
|
483
|
+
### 3. Cross-Server Messaging (federation)
|
|
484
|
+
|
|
485
|
+
Agents on different MAP servers — different machines, different networks. Uses MAP's native `federation/connect` and `federation/route` protocol.
|
|
486
|
+
|
|
487
|
+
```
|
|
488
|
+
AgentA (system: "frontend-team") → send_message(
|
|
489
|
+
to="AgentC@backend-team",
|
|
490
|
+
body="..."
|
|
491
|
+
)
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Agent Inbox parses the `@backend-team` address, resolves it to the federation peer link, and forwards via `federation/route`. The remote Agent Inbox receives, stores, and delivers to AgentC.
|
|
495
|
+
|
|
496
|
+
**Via hub (typical deployment):**
|
|
497
|
+
```
|
|
498
|
+
┌─────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐
|
|
499
|
+
│ MAP Server │ │ Agent Inbox │ federation/route │ Central Hub │ │ │
|
|
500
|
+
│ (frontend) │◄────►│ (gateway) │◄───────────────────►│ Agent Inbox │ │ │
|
|
501
|
+
│ │ │ │ │ (router) │ │ │
|
|
502
|
+
│ AgentA ─────┼─────►│ resolve @ │────────────────────►│ relay ──────┼─────►│ Agent │
|
|
503
|
+
│ AgentB │ │ backend-team│ │ │ │ Inbox │
|
|
504
|
+
└─────────────┘ └──────────────┘ └──────┬───────┘ │ (gateway) │
|
|
505
|
+
│ │ │
|
|
506
|
+
│ │ ──► AgentC │
|
|
507
|
+
│ │ AgentD │
|
|
508
|
+
┌──────▼──────┐ └──────┬──────┘
|
|
509
|
+
│ MAP Server │◄────────────┘
|
|
510
|
+
│ (backend) │
|
|
511
|
+
└─────────────┘
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
**Direct peer (alternative):**
|
|
515
|
+
```
|
|
516
|
+
┌─────────────┐ ┌──────────────┐ federation/connect ┌──────────────┐ ┌─────────────┐
|
|
517
|
+
│ MAP Server │ │ Agent Inbox │◄───────────────────►│ Agent Inbox │ │ MAP Server │
|
|
518
|
+
│ (frontend) │◄────►│ (gateway) │ federation/route │ (gateway) │◄────►│ (backend) │
|
|
519
|
+
│ │ │ │ │ │ │ │
|
|
520
|
+
│ AgentA ─────┼─────►│ route ──────┼────────────────────►│ deliver ────┼─────►│ ──► AgentC │
|
|
521
|
+
└─────────────┘ └──────────────┘ └──────────────┘ └─────────────┘
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## MCP Tools Interface
|
|
527
|
+
|
|
528
|
+
### Agent-Facing Tools
|
|
529
|
+
|
|
530
|
+
The MCP interface is intentionally minimal — 4 tools that cover all common agent messaging needs:
|
|
531
|
+
|
|
532
|
+
| Tool | Description |
|
|
533
|
+
|------|-------------|
|
|
534
|
+
| `send_message` | Send a message to one or more agents. Supports replies (`inReplyTo`), threading (`threadTag`), and federated addressing (`agent@system`). |
|
|
535
|
+
| `check_inbox` | Fetch unread messages for an agent. Auto-registers the agent on first call. Auto-marks messages as read. |
|
|
536
|
+
| `read_thread` | Read all messages in a thread by `threadTag`. |
|
|
537
|
+
| `list_agents` | List agents registered locally and optionally from federated peers. |
|
|
538
|
+
|
|
539
|
+
Previously-separate tools were consolidated: `reply` is now `send_message` with `inReplyTo`, `register_agent` is handled automatically by `check_inbox`, and `acknowledge` happens implicitly on read. Conversation management tools (start_conversation, get_conversation, etc.) were removed from the agent-facing MCP interface — the traceability layer handles them internally, and they remain accessible via the MAP JSON-RPC interface for dashboards and debugging tools.
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
543
|
+
## MAP JSON-RPC Interface
|
|
544
|
+
|
|
545
|
+
A JSON-RPC 2.0 endpoint implementing MAP's `mail/*` methods for traceability clients (dashboards, debuggers, audit tools).
|
|
546
|
+
|
|
547
|
+
Methods:
|
|
548
|
+
- `mail/create`, `mail/get`, `mail/list`, `mail/close`
|
|
549
|
+
- `mail/join`, `mail/leave`, `mail/invite`
|
|
550
|
+
- `mail/turn`, `mail/turns/list`
|
|
551
|
+
- `mail/thread/create`, `mail/thread/list`
|
|
552
|
+
- `mail/summary`, `mail/replay`
|
|
553
|
+
- `map/subscribe` with event filtering
|
|
554
|
+
|
|
555
|
+
This interface reads from the traceability layer. It can also write (e.g., a MAP client adding a turn), which flows back as a message to the target agent's inbox.
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
## claude-code-swarm Integration
|
|
560
|
+
|
|
561
|
+
### Current Architecture
|
|
562
|
+
|
|
563
|
+
```
|
|
564
|
+
Claude Code Session
|
|
565
|
+
│
|
|
566
|
+
├─ Native team primitives (SendMessage, TaskCreate) ← in-process coordination
|
|
567
|
+
│
|
|
568
|
+
└─ Hooks (lifecycle events)
|
|
569
|
+
│
|
|
570
|
+
└─ UNIX Socket IPC
|
|
571
|
+
│
|
|
572
|
+
└─ MAP Sidecar
|
|
573
|
+
│
|
|
574
|
+
├─ conn.spawn() / conn.send() / conn.updateState() ← MAP SDK
|
|
575
|
+
│
|
|
576
|
+
└─ WebSocket ──► MAP Server
|
|
577
|
+
│
|
|
578
|
+
└─ broadcasts to scope
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**Key sidecar commands**: `spawn`, `done`, `state`, `emit` (broadcast to scope), `send` (to target), `trajectory-checkpoint`, `ping`
|
|
582
|
+
|
|
583
|
+
**Message injection**: Incoming MAP messages → `inbox.jsonl` → UserPromptSubmit hook → injected into agent context as markdown
|
|
584
|
+
|
|
585
|
+
### Proposed Architecture (plugin-as-router)
|
|
586
|
+
|
|
587
|
+
The plugin layer (`map-events.mjs`) gains a second dispatch target. Lifecycle commands continue going to the sidecar. Messaging commands go to Agent Inbox. The plugin also sends lightweight lifecycle notifications to Agent Inbox so it can maintain its agent registry.
|
|
588
|
+
|
|
589
|
+
```
|
|
590
|
+
Claude Code Session
|
|
591
|
+
│
|
|
592
|
+
├─ Native team primitives ← unchanged
|
|
593
|
+
│
|
|
594
|
+
└─ Hooks
|
|
595
|
+
│
|
|
596
|
+
└─ Plugin (map-hook.mjs / map-events.mjs)
|
|
597
|
+
│
|
|
598
|
+
├─ spawn/done/state/trajectory
|
|
599
|
+
│ │
|
|
600
|
+
│ ├─► Sidecar → MAP Server (lifecycle, via sidecar's own conn)
|
|
601
|
+
│ │
|
|
602
|
+
│ └─► Agent Inbox (notify: agent.spawn / agent.done)
|
|
603
|
+
│ └─► updates agent registry
|
|
604
|
+
│
|
|
605
|
+
└─ send/emit (messaging)
|
|
606
|
+
│
|
|
607
|
+
├─► Agent Inbox (primary)
|
|
608
|
+
│ │
|
|
609
|
+
│ ├─► store in inbox, track delivery
|
|
610
|
+
│ ├─► route via its own MAP connection
|
|
611
|
+
│ └─► write to inbox.jsonl for context injection
|
|
612
|
+
│
|
|
613
|
+
└─► Sidecar (fallback, if Agent Inbox unavailable)
|
|
614
|
+
└─► direct MAP send (existing behavior)
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
### What Changes in claude-code-swarm
|
|
618
|
+
|
|
619
|
+
**map-events.mjs** — the routing layer:
|
|
620
|
+
|
|
621
|
+
1. `sendCommand()` gains routing logic: messaging commands (`send`, `emit`) go to Agent Inbox first, with fallback to sidecar
|
|
622
|
+
2. `spawn`/`done` commands send a lightweight notification to Agent Inbox alongside the existing sidecar dispatch
|
|
623
|
+
3. New `sendToInbox()` function (same UNIX socket IPC pattern as `sendToSidecar()`)
|
|
624
|
+
|
|
625
|
+
```typescript
|
|
626
|
+
// New: Agent Inbox UNIX socket client (same pattern as sidecar-client.mjs)
|
|
627
|
+
export function sendToInbox(command, socketPath) {
|
|
628
|
+
return new Promise((resolve) => {
|
|
629
|
+
const client = net.createConnection(socketPath, () => {
|
|
630
|
+
client.write(JSON.stringify(command) + "\n");
|
|
631
|
+
client.end();
|
|
632
|
+
resolve(true);
|
|
633
|
+
});
|
|
634
|
+
client.on("error", () => resolve(false));
|
|
635
|
+
setTimeout(() => { client.destroy(); resolve(false); }, 500);
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
**sidecar-server.mjs** — minimal changes:
|
|
641
|
+
|
|
642
|
+
1. The `send` command handler becomes a fallback path (only used when Agent Inbox is unavailable)
|
|
643
|
+
2. No bus client needed — the plugin handles routing
|
|
644
|
+
|
|
645
|
+
**inbox.mjs** — minor change:
|
|
646
|
+
|
|
647
|
+
1. `writeToInbox()` now receives richer data from Agent Inbox (message ID, conversation ID)
|
|
648
|
+
2. `formatInboxAsMarkdown()` can display threading context
|
|
649
|
+
|
|
650
|
+
**map-connection.mjs** — `connectToMAP()`'s `onMessage` handler becomes a fallback only. When Agent Inbox is active, incoming messages are received by Agent Inbox on its own MAP connection, stored, and written to `inbox.jsonl` directly.
|
|
651
|
+
|
|
652
|
+
### What Stays the Same
|
|
653
|
+
|
|
654
|
+
- Hook architecture (SessionStart, PreToolUse, PostToolUse, etc.)
|
|
655
|
+
- UNIX socket IPC protocol (NDJSON over UNIX socket)
|
|
656
|
+
- Native team primitives for in-process coordination
|
|
657
|
+
- Sidecar process lifecycle (startup, stale cleanup, auto-terminate)
|
|
658
|
+
- UserPromptSubmit injection pattern
|
|
659
|
+
- Fire-and-forget fallback for broadcast-only operations
|
|
660
|
+
|
|
661
|
+
### Graceful Degradation
|
|
662
|
+
|
|
663
|
+
Agent Inbox is an enhancement, not a hard dependency. If it's unavailable, the plugin falls back to the sidecar for everything — the existing behavior:
|
|
664
|
+
|
|
665
|
+
```typescript
|
|
666
|
+
// In map-events.mjs — sendCommand() with graceful fallback
|
|
667
|
+
export async function sendCommand(config, command, sessionId) {
|
|
668
|
+
if (command.action === "send" || command.action === "emit") {
|
|
669
|
+
// Try Agent Inbox first
|
|
670
|
+
const sent = await sendToInbox(command, inboxSocketPath(sessionId));
|
|
671
|
+
if (sent) return;
|
|
672
|
+
// Fall through to sidecar (existing behavior)
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Lifecycle commands → sidecar, or messaging fallback
|
|
676
|
+
const sPaths = sessionPaths(sessionId);
|
|
677
|
+
const sent = await sendToSidecar(command, sPaths.socketPath);
|
|
678
|
+
if (!sent) {
|
|
679
|
+
const recovered = await ensureSidecar(config, sessionId);
|
|
680
|
+
if (recovered) {
|
|
681
|
+
await sendToSidecar(command, sPaths.socketPath);
|
|
682
|
+
} else if (command.action === "emit") {
|
|
683
|
+
await fireAndForget(config, command.event);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Notify Agent Inbox about lifecycle events (best-effort)
|
|
688
|
+
if (command.action === "spawn" || command.action === "done") {
|
|
689
|
+
sendToInbox(
|
|
690
|
+
{ action: "notify", event: { type: `agent.${command.action}`, ...command } },
|
|
691
|
+
inboxSocketPath(sessionId)
|
|
692
|
+
).catch(() => {}); // fire-and-forget notification
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
### Multi-Session Support
|
|
698
|
+
|
|
699
|
+
Multiple Claude Code sessions can run simultaneously, each with its own sidecar. All sessions' plugins dispatch messaging commands to the **same** Agent Inbox instance:
|
|
700
|
+
|
|
701
|
+
```
|
|
702
|
+
Session 1 Plugin ──┐
|
|
703
|
+
├──► Agent Inbox (shared)
|
|
704
|
+
Session 2 Plugin ──┘
|
|
705
|
+
|
|
706
|
+
Agent Inbox routes messages to the right agent regardless of which
|
|
707
|
+
session originated them. The messageId + recipient agentId determine
|
|
708
|
+
routing, not the session.
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
Each session's sidecar remains independent — handling only its own lifecycle events.
|
|
712
|
+
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## Technology Stack
|
|
716
|
+
|
|
717
|
+
| Component | Choice | Rationale |
|
|
718
|
+
|-----------|--------|-----------|
|
|
719
|
+
| Language | TypeScript | Same as claude-code-swarm and agentic-mesh; direct integration |
|
|
720
|
+
| MAP Client | `@multi-agent-protocol/sdk` | Same SDK used by claude-code-swarm's sidecar |
|
|
721
|
+
| MCP Server | `@modelcontextprotocol/sdk` | Standard MCP server for agent-facing tools |
|
|
722
|
+
| Storage (default) | In-memory (Map/Array) | Zero-config, good for development and testing |
|
|
723
|
+
| Storage (optional) | SQLite via `better-sqlite3` | Persistence, full-text search |
|
|
724
|
+
| ID Generation | ULID | Sortable, unique, MAP convention |
|
|
725
|
+
| IPC | UNIX socket (NDJSON) | Same protocol as sidecar; proven pattern |
|
|
726
|
+
| Internal events | Node.js EventEmitter | In-process pub/sub for internal components |
|
|
727
|
+
|
|
728
|
+
---
|
|
729
|
+
|
|
730
|
+
## Storage Layer
|
|
731
|
+
|
|
732
|
+
### Interface
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
interface Storage {
|
|
736
|
+
// Agents
|
|
737
|
+
getAgent(agentId: string): Agent | undefined;
|
|
738
|
+
putAgent(agent: Agent): void;
|
|
739
|
+
listAgents(scope: string): Agent[];
|
|
740
|
+
|
|
741
|
+
// Messages
|
|
742
|
+
getMessage(id: string): Message | undefined;
|
|
743
|
+
putMessage(message: Message): void;
|
|
744
|
+
getInbox(agentId: string, opts?: { unreadOnly?: boolean }): Message[];
|
|
745
|
+
getThread(threadTag: string, scope: string): Message[];
|
|
746
|
+
searchMessages(query: string, scope: string): Message[];
|
|
747
|
+
|
|
748
|
+
// Conversations (traceability)
|
|
749
|
+
getConversation(id: string): Conversation | undefined;
|
|
750
|
+
putConversation(conversation: Conversation): void;
|
|
751
|
+
listConversations(scope: string): Conversation[];
|
|
752
|
+
addTurn(turn: Turn): void;
|
|
753
|
+
getTurns(conversationId: string): Turn[];
|
|
754
|
+
}
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
### Implementations
|
|
758
|
+
|
|
759
|
+
1. **InMemoryStorage** (default): Maps and arrays. Fast, zero-config. Data lost on restart. Good for development, testing, and ephemeral swarm sessions.
|
|
760
|
+
|
|
761
|
+
2. **SQLiteStorage** (optional): Persistent storage with indexes and full-text search (FTS5). For long-running inbox instances or when message history matters.
|
|
762
|
+
|
|
763
|
+
Both implement the same `Storage` interface — swap via configuration.
|
|
764
|
+
|
|
765
|
+
---
|
|
766
|
+
|
|
767
|
+
## Federation Model
|
|
768
|
+
|
|
769
|
+
Federation is the primary use case. Agent Inbox acts as a **gateway agent** in MAP's federation model — connecting to MAP servers and peer Agent Inbox instances to route messages across system boundaries. Federation uses MAP's native `federation/connect` and `federation/route` protocol rather than inventing a parallel system.
|
|
770
|
+
|
|
771
|
+
### MAP Federation Primitives
|
|
772
|
+
|
|
773
|
+
Agent Inbox builds on MAP's existing federation spec:
|
|
774
|
+
|
|
775
|
+
| MAP Federation Concept | Agent Inbox Role |
|
|
776
|
+
|---|---|
|
|
777
|
+
| Gateway agent | Agent Inbox instance (with routing logic) |
|
|
778
|
+
| `federation/connect` | Connection Manager establishing peer links with auth + exposure |
|
|
779
|
+
| `federation/route` / `federation/send` | Message Router forwarding to remote systems |
|
|
780
|
+
| `MAPFederationExposure` | Trust policy controlling what agents are visible remotely |
|
|
781
|
+
| `systemId` | Unique identity for each Agent Inbox instance |
|
|
782
|
+
| `{ system, agent }` address | Federated addressing for cross-system routing |
|
|
783
|
+
|
|
784
|
+
### System ID — Tiered Resolution
|
|
785
|
+
|
|
786
|
+
Each Agent Inbox instance needs a stable `systemId` for federation. Resolved in order of precedence:
|
|
787
|
+
|
|
788
|
+
```typescript
|
|
789
|
+
function resolveSystemId(config: InboxConfig, mapSystemInfo?: SystemInfo): string {
|
|
790
|
+
if (config.systemId) return config.systemId; // 1. Explicit config (INBOX_SYSTEM_ID)
|
|
791
|
+
if (mapSystemInfo?.name) return mapSystemInfo.name; // 2. Derived from MAP server's systemInfo.name
|
|
792
|
+
return `inbox-${randomId(4)}`; // 3. Auto-generated fallback
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
The MAP server's `map/connect` response includes `systemInfo?: { name?: string; version?: string }` which can provide the system identity when no explicit config is set.
|
|
797
|
+
|
|
798
|
+
**Stability considerations:**
|
|
799
|
+
- **Hub/federation mode**: Should require or strongly recommend explicit `systemId` — peers reference it in their config
|
|
800
|
+
- **Local/standalone mode**: Auto-generated is fine since no one addresses it remotely
|
|
801
|
+
- **Derived mode**: Stable as long as the MAP server's `systemInfo.name` is stable
|
|
802
|
+
- **Persistence**: The resolved ID is persisted on first run so auto-generated IDs survive restarts
|
|
803
|
+
|
|
804
|
+
### Agent Registry — Warm Model
|
|
805
|
+
|
|
806
|
+
Agents are ephemeral (spun up/down frequently) but should be resilient to brief disconnections. The registry uses a **warm model** with TTL and grace period:
|
|
807
|
+
|
|
808
|
+
```
|
|
809
|
+
Agent connects → status: "active" (routable, fully live)
|
|
810
|
+
Agent disconnects → status: "away" (still routable for grace period, messages queued)
|
|
811
|
+
Grace period expires → status: "expired" (not routable, retained for audit)
|
|
812
|
+
Explicitly removed → deleted from registry
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
**Configurable knobs:**
|
|
816
|
+
|
|
817
|
+
```typescript
|
|
818
|
+
interface AgentRegistryConfig {
|
|
819
|
+
gracePeriodMs: number; // Time after disconnect before away → expired (default: 60000)
|
|
820
|
+
retainExpiredMs: number; // How long to keep expired entries for audit (default: 3600000)
|
|
821
|
+
requeueOnReconnect: boolean; // Flush queued messages when agent reconnects (default: true)
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
**Federation implications** — when a peer asks "do you have agent-X?":
|
|
826
|
+
- **"active"** → route immediately
|
|
827
|
+
- **"away"** → accept the message, queue it, agent will likely reconnect
|
|
828
|
+
- **"expired" / unknown** → reject with "not found" error
|
|
829
|
+
|
|
830
|
+
Ephemerality is about **routability, not identity**. An agent's ID persists in the registry after disconnect — it just stops being a valid routing target after the grace period.
|
|
831
|
+
|
|
832
|
+
**Self-registration**: Agents are auto-registered on their first `check_inbox` call, or via MAP spawn events or the `SessionStart` hook. First-wins on ID conflicts — if an ID is already taken by an active/away agent, the registration is rejected.
|
|
833
|
+
|
|
834
|
+
### Addressing
|
|
835
|
+
|
|
836
|
+
**String shorthand** (used in MCP tools):
|
|
837
|
+
```
|
|
838
|
+
agent-id → local resolution only
|
|
839
|
+
agent-id@system-id → federated: route to specific system
|
|
840
|
+
@system-id → broadcast to all exposed agents on that system (or scope)
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
**Structured form** (used internally and in MAP federation messages):
|
|
844
|
+
```typescript
|
|
845
|
+
interface FederatedAddress {
|
|
846
|
+
system?: string; // target system ID
|
|
847
|
+
agent?: string; // target agent ID
|
|
848
|
+
scope?: string; // optional scope qualifier
|
|
849
|
+
}
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
**Resolution order:**
|
|
853
|
+
1. **No `@`** → `"AgentB"` → find in local registry (same scope, then all scopes)
|
|
854
|
+
2. **With `@`** → `"AgentB@backend-team"` → look up `backend-team` in connection manager, forward via `federation/route`
|
|
855
|
+
3. **Explicit scope** → `"AgentB"` + `to_scope="backend"` → find in specified scope (local or remote)
|
|
856
|
+
|
|
857
|
+
**Server connection mapping**: MAP server connections are identified by URL (`ws://host:port`). For federation, system aliases map to URLs via configuration:
|
|
858
|
+
|
|
859
|
+
```typescript
|
|
860
|
+
interface FederationPeerConfig {
|
|
861
|
+
systemId: string; // human-friendly alias: "backend-team"
|
|
862
|
+
url: string; // MAP server URL: "ws://10.0.1.5:3001"
|
|
863
|
+
auth?: FederationAuth; // authentication for federation/connect
|
|
864
|
+
exposure?: ExposurePolicy; // what we expose to this peer
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### Connection Manager
|
|
869
|
+
|
|
870
|
+
```typescript
|
|
871
|
+
interface ConnectionManager {
|
|
872
|
+
// Connect to a MAP server (local or remote)
|
|
873
|
+
connect(serverUrl: string, opts: ConnectionOpts): Promise<MAPConnection>;
|
|
874
|
+
|
|
875
|
+
// Establish federation with a peer (uses MAP federation/connect)
|
|
876
|
+
federate(peer: FederationPeerConfig): Promise<FederationLink>;
|
|
877
|
+
|
|
878
|
+
// Route a message to the right connection
|
|
879
|
+
route(message: Message): Promise<DeliveryResult>;
|
|
880
|
+
|
|
881
|
+
// Resolve an agent address to a connection + agentId
|
|
882
|
+
resolve(address: string | FederatedAddress): ResolvedTarget | undefined;
|
|
883
|
+
|
|
884
|
+
// List all connections (MAP servers + federation peers)
|
|
885
|
+
connections(): ConnectionInfo[];
|
|
886
|
+
|
|
887
|
+
// List all federation peers
|
|
888
|
+
peers(): FederationPeer[];
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
interface FederationLink {
|
|
892
|
+
peerId: string; // remote system ID
|
|
893
|
+
sessionId: string; // federation session
|
|
894
|
+
status: 'connected' | 'disconnected' | 'authenticating';
|
|
895
|
+
exposure: ExposurePolicy; // what the remote exposes to us
|
|
896
|
+
}
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
### Federation Topology
|
|
900
|
+
|
|
901
|
+
Both hub and direct-peer topologies are supported — same code, different configuration.
|
|
902
|
+
|
|
903
|
+
**Hub topology** (typical deployment — central router between swarms):
|
|
904
|
+
|
|
905
|
+
```
|
|
906
|
+
┌─────────────┐ ┌──────────────┐ ┌──────────────────┐
|
|
907
|
+
│ Agent Team A │──────│ Agent Inbox │──────│ Central Hub │
|
|
908
|
+
│ (MAP Server) │ │ (gateway) │ │ Agent Inbox │
|
|
909
|
+
└─────────────┘ └──────────────┘ │ (router only, │
|
|
910
|
+
│ no local agents)│
|
|
911
|
+
┌─────────────┐ ┌──────────────┐ │ │
|
|
912
|
+
│ Agent Team B │──────│ Agent Inbox │──────│ │
|
|
913
|
+
│ (MAP Server) │ │ (gateway) │ └──────────────────┘
|
|
914
|
+
└─────────────┘ └──────────────┘
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
Each team's Agent Inbox connects to its local MAP server and federates with the central hub. The hub has no local agents — it just routes between peers via `federation/route`.
|
|
918
|
+
|
|
919
|
+
**Direct peer topology** (teams connect directly):
|
|
920
|
+
|
|
921
|
+
```
|
|
922
|
+
┌─────────────┐ ┌──────────────┐ federation/connect ┌──────────────┐ ┌─────────────┐
|
|
923
|
+
│ Agent Team A │──────│ Agent Inbox │◄────────────────────►│ Agent Inbox │──────│ Agent Team B │
|
|
924
|
+
│ (MAP Server) │ │ (gateway) │ │ (gateway) │ │ (MAP Server) │
|
|
925
|
+
└─────────────┘ └──────────────┘ └──────────────┘ └─────────────┘
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
Same Agent Inbox code. The difference is only in the federation peer config:
|
|
929
|
+
- **Hub mode**: Each edge inbox has one peer (the hub). The hub has N peers (the edges).
|
|
930
|
+
- **Peer mode**: Each inbox federates directly with the others it needs to talk to.
|
|
931
|
+
|
|
932
|
+
The addressing format `agent-id@system-id` works identically in both topologies — the connection manager resolves `system-id` to whichever peer holds that system, whether it's a direct link or a hub that will relay.
|
|
933
|
+
|
|
934
|
+
### Routing Engine
|
|
935
|
+
|
|
936
|
+
The routing engine determines how to deliver messages to agents on unknown or disconnected peers. Strategy is configurable:
|
|
937
|
+
|
|
938
|
+
```typescript
|
|
939
|
+
interface FederationRoutingConfig {
|
|
940
|
+
strategy: 'table' | 'broadcast' | 'hierarchical';
|
|
941
|
+
|
|
942
|
+
// For 'table' strategy (default)
|
|
943
|
+
tableTTL?: number; // how long routing entries live in ms (default: 300000)
|
|
944
|
+
refreshOnMiss?: boolean; // re-query peers on cache miss before failing (default: true)
|
|
945
|
+
|
|
946
|
+
// For 'broadcast' strategy
|
|
947
|
+
broadcastTimeout?: number; // ms to wait for first responder (default: 5000)
|
|
948
|
+
|
|
949
|
+
// For 'hierarchical' strategy
|
|
950
|
+
upstream?: string[]; // system IDs of upstream hubs to delegate to
|
|
951
|
+
}
|
|
952
|
+
```
|
|
953
|
+
|
|
954
|
+
| Strategy | Behavior | Best for |
|
|
955
|
+
|---|---|---|
|
|
956
|
+
| **`table`** | Maintain in-memory routing table from `federation/connect` exposure data. Route to known agents. On miss, optionally query peers. | Default — fast, predictable |
|
|
957
|
+
| **`broadcast`** | Forward to all connected peers, first responder wins. | Small clusters, highly dynamic agents |
|
|
958
|
+
| **`hierarchical`** | Check local table first, then delegate to upstream hub(s) if unknown. | Multi-tier topologies |
|
|
959
|
+
|
|
960
|
+
**Routing table population**: When `federation/connect` completes, the peer's exposure policy advertises available agents/scopes. The gateway stores this as an in-memory routing table:
|
|
961
|
+
|
|
962
|
+
```
|
|
963
|
+
routing table (in-memory, short-lived):
|
|
964
|
+
"agent-alpha" → peer "backend-team" (last seen: 2s ago, status: active)
|
|
965
|
+
"agent-beta" → peer "backend-team" (last seen: 2s ago, status: active)
|
|
966
|
+
"agent-gamma" → peer "ml-team" (last seen: 15s ago, status: away)
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
Entries expire after `tableTTL`. Refreshed passively (peer sends updated exposure) or actively (on-miss query or periodic poll). Short-lived agents naturally age out.
|
|
970
|
+
|
|
971
|
+
### Delivery Queue
|
|
972
|
+
|
|
973
|
+
Messages to offline or unreachable peers are queued for later delivery. All parameters are configurable:
|
|
974
|
+
|
|
975
|
+
```typescript
|
|
976
|
+
interface DeliveryQueueConfig {
|
|
977
|
+
persistence: 'memory' | 'sqlite'; // where to buffer (default: 'memory')
|
|
978
|
+
maxTTL: number; // max age before dropping in ms (default: 86400000 / 24h)
|
|
979
|
+
maxQueueSize: number; // max messages per destination (default: 10000)
|
|
980
|
+
retryStrategy: 'exponential' | 'fixed'; // retry pattern (default: 'exponential')
|
|
981
|
+
retryBaseInterval: number; // base retry interval in ms (default: 1000)
|
|
982
|
+
retryMaxAttempts: number; // max retries, 0 = unlimited until TTL (default: 0)
|
|
983
|
+
flushOnReconnect: boolean; // drain queue when connection restored (default: true)
|
|
984
|
+
overflow: 'drop-oldest' | 'drop-newest' | 'reject-new'; // when queue is full (default: 'drop-oldest')
|
|
985
|
+
}
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
Follows MAP's federation failure handling spec: configurable queue duration, size, and overflow policy.
|
|
989
|
+
|
|
990
|
+
### Trust Policies
|
|
991
|
+
|
|
992
|
+
Trust policies control federation access. All layers are supported; complexity can be deferred by starting with allow-list only and stubbing the rest.
|
|
993
|
+
|
|
994
|
+
```typescript
|
|
995
|
+
interface FederationTrustPolicy {
|
|
996
|
+
// Layer 1: Server allow-list (implemented first)
|
|
997
|
+
allowedServers: string[]; // system IDs or URLs allowed to connect
|
|
998
|
+
|
|
999
|
+
// Layer 2: Scope-based permissions (stub initially)
|
|
1000
|
+
scopePermissions: Record<string, string[]>; // local scope → allowed remote scopes
|
|
1001
|
+
|
|
1002
|
+
// Layer 3: Token-based auth (stub initially)
|
|
1003
|
+
requireAuth: boolean; // require tokens for cross-server delivery
|
|
1004
|
+
authMethod?: 'bearer' | 'api-key' | 'mtls' | 'did:wba'; // MAP-supported auth methods
|
|
1005
|
+
authTokens?: Record<string, string>; // system ID → token
|
|
1006
|
+
}
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
**Exposure control** follows MAP's `MAPFederationExposure` model:
|
|
1010
|
+
- **`none`**: No agents visible to this peer
|
|
1011
|
+
- **`gateway`**: Only the gateway agent (Agent Inbox itself) is visible — peers must address messages explicitly
|
|
1012
|
+
- **`tagged`**: Only agents with specific tags/scopes are visible
|
|
1013
|
+
- **`all`**: All registered agents are visible to this peer
|
|
1014
|
+
|
|
1015
|
+
### Cross-Server Conversation Ownership
|
|
1016
|
+
|
|
1017
|
+
Each side maintains its own view. When Agent Inbox A forwards a message to Agent Inbox B:
|
|
1018
|
+
|
|
1019
|
+
1. A stores the outbound message in its local storage
|
|
1020
|
+
2. B receives it via `federation/route` and stores it in its own local storage
|
|
1021
|
+
3. Each side tracks `in_reply_to` chains, threads, and conversations independently
|
|
1022
|
+
4. No sync protocol or CRDT needed
|
|
1023
|
+
|
|
1024
|
+
Trade-off: no global "conversation view" across servers. Each system sees its own perspective. This is acceptable for agent-to-agent messaging and avoids sync complexity.
|
|
1025
|
+
|
|
1026
|
+
---
|
|
1027
|
+
|
|
1028
|
+
## Implementation Plan
|
|
1029
|
+
|
|
1030
|
+
### Phase 1: Core Messaging + Single MAP Connection
|
|
1031
|
+
|
|
1032
|
+
- TypeScript project setup
|
|
1033
|
+
- Storage interface + InMemoryStorage
|
|
1034
|
+
- Agent registry (backed by MAP identity)
|
|
1035
|
+
- Message send/receive/reply with inbox tracking
|
|
1036
|
+
- Thread tags (user-defined grouping)
|
|
1037
|
+
- Single MAP server connection
|
|
1038
|
+
- UNIX socket IPC server (same NDJSON protocol as sidecar)
|
|
1039
|
+
- MCP tools: `send_message`, `check_inbox`, `read_thread`, `list_agents`
|
|
1040
|
+
- Internal EventEmitter for component coordination
|
|
1041
|
+
- Basic traceability (auto-create conversations from thread_tags)
|
|
1042
|
+
|
|
1043
|
+
### Phase 2: Traceability + MAP Mail
|
|
1044
|
+
|
|
1045
|
+
- Full traceability layer: conversations, turns, threads
|
|
1046
|
+
- MAP JSON-RPC endpoint (`mail/*` methods)
|
|
1047
|
+
- MAP mail auto-grouping (conversationId, thread_tag fallback, catch-all)
|
|
1048
|
+
- `map/subscribe` for real-time event streaming
|
|
1049
|
+
- SQLiteStorage implementation
|
|
1050
|
+
- Full-text search
|
|
1051
|
+
|
|
1052
|
+
### Phase 3: Federation + Multi-Server
|
|
1053
|
+
|
|
1054
|
+
**3a: Core Federation Infrastructure**
|
|
1055
|
+
- System ID tiered resolution (config → MAP systemInfo → auto-generated)
|
|
1056
|
+
- Agent registry warm model (active/away/expired with configurable grace period)
|
|
1057
|
+
- Federated addressing: `agent-id@system-id` string shorthand + `{ system, agent, scope }` structured form
|
|
1058
|
+
- Self-registration with first-wins ID conflict resolution
|
|
1059
|
+
- Connection Manager: connect to multiple MAP servers + federation peers
|
|
1060
|
+
- Implement MAP `federation/connect` handshake (auth, exposure exchange)
|
|
1061
|
+
- Implement MAP `federation/route` / `federation/send` for cross-system messaging
|
|
1062
|
+
|
|
1063
|
+
**3b: Routing + Delivery**
|
|
1064
|
+
- Routing engine with configurable strategy (`table` / `broadcast` / `hierarchical`)
|
|
1065
|
+
- In-memory routing table populated from federation exposure data (with TTL)
|
|
1066
|
+
- Delivery queue for offline peers (configurable persistence, TTL, overflow, retry)
|
|
1067
|
+
- Flush-on-reconnect for queued messages
|
|
1068
|
+
- Cross-server conversation ownership: each side maintains its own view
|
|
1069
|
+
|
|
1070
|
+
**3c: Topology + Trust**
|
|
1071
|
+
- Hub topology support (central router Agent Inbox with no local agents)
|
|
1072
|
+
- Direct peer topology support (Agent Inbox ↔ Agent Inbox federation)
|
|
1073
|
+
- Trust policies: server allow-list (implemented), scope permissions + token auth (stubbed)
|
|
1074
|
+
- Exposure control following MAP's model (`none` / `gateway` / `tagged` / `all`)
|
|
1075
|
+
|
|
1076
|
+
**Deferred: agentic-mesh integration** — kept in plans as optional transport layer, but not in Phase 3 scope
|
|
1077
|
+
|
|
1078
|
+
---
|
|
1079
|
+
|
|
1080
|
+
## Key Design Decisions
|
|
1081
|
+
|
|
1082
|
+
### 1. Plugin-as-router (no event bus)
|
|
1083
|
+
|
|
1084
|
+
The claude-code-swarm plugin (`map-events.mjs`) already dispatches commands by type. Rather than introducing a separate event bus for sidecar ↔ Agent Inbox coordination, the plugin routes directly: lifecycle commands to the sidecar, messaging commands to Agent Inbox. This eliminates an entire infrastructure layer (bus socket, pub/sub protocol, subscription management, bus lifecycle) while achieving the same routing outcome. Agent Inbox learns about agent lifecycle via lightweight notifications from the plugin, not bus subscriptions.
|
|
1085
|
+
|
|
1086
|
+
### 2. Concern-based MAP connection split
|
|
1087
|
+
|
|
1088
|
+
The sidecar and Agent Inbox each own their MAP connection(s) for their respective domains. The sidecar's connection handles lifecycle and observability (spawn, state, trajectory) — fire-and-forget traffic. Agent Inbox's connection(s) handle messaging and mail — tracked, routed delivery. This avoids routing unrelated traffic through either system. Agent Inbox also works standalone without a sidecar.
|
|
1089
|
+
|
|
1090
|
+
### 3. MAP-native, not a parallel system
|
|
1091
|
+
|
|
1092
|
+
Agent Inbox builds on MAP rather than reimplementing transport and identity. MAP servers handle the "pipe" — Agent Inbox adds inbox semantics, threading, and routing on top.
|
|
1093
|
+
|
|
1094
|
+
### 4. Federation as primary concern
|
|
1095
|
+
|
|
1096
|
+
Cross-process and cross-server messaging is the reason Agent Inbox exists. Local in-process coordination (SendMessage, TaskCreate) already works in claude-code-swarm. Agent Inbox adds the cross-boundary layer.
|
|
1097
|
+
|
|
1098
|
+
### 5. Graceful degradation
|
|
1099
|
+
|
|
1100
|
+
The plugin falls back to the sidecar for messaging if Agent Inbox is unavailable. Agent Inbox is an enhancement, not a hard dependency. This preserves the sidecar's existing reliability guarantees.
|
|
1101
|
+
|
|
1102
|
+
### 6. Structured content with plain-text shorthand
|
|
1103
|
+
|
|
1104
|
+
Messages store structured content (typed with schema), but MCP tools accept plain markdown for convenience. The service layer wraps simple text; agents can pass full structured content when they need machine-readable payloads.
|
|
1105
|
+
|
|
1106
|
+
### 7. Traceability follows MAP mail conventions
|
|
1107
|
+
|
|
1108
|
+
Conversations, turns, and threads are created automatically from messaging events following MAP's conventions. Agents can also create them explicitly. The traceability layer observes and records — it never blocks messaging.
|
|
1109
|
+
|
|
1110
|
+
### 8. Identity delegated to MAP + client
|
|
1111
|
+
|
|
1112
|
+
Agent Inbox uses MAP's `agentId` as the primary identity. Persistence is the client's responsibility — reconnect with the same ID to reclaim your inbox, or don't. The inbox doesn't enforce a persistence model.
|
|
1113
|
+
|
|
1114
|
+
### 9. Storage is pluggable
|
|
1115
|
+
|
|
1116
|
+
In-memory for development and ephemeral sessions. SQLite for persistence. Same interface, swap via config.
|
|
1117
|
+
|
|
1118
|
+
### 10. TypeScript for ecosystem alignment
|
|
1119
|
+
|
|
1120
|
+
Same language as claude-code-swarm, agentic-mesh, and the MAP SDK. Direct integration without language bridges or sidecars.
|
|
1121
|
+
|
|
1122
|
+
---
|
|
1123
|
+
|
|
1124
|
+
## Open Questions
|
|
1125
|
+
|
|
1126
|
+
### Resolved
|
|
1127
|
+
|
|
1128
|
+
1. ~~**Sidecar integration depth**~~: **Resolved — plugin-as-router.** The plugin layer dispatches lifecycle commands to the sidecar and messaging commands to Agent Inbox. No event bus needed. The sidecar owns lifecycle; Agent Inbox owns messaging. See Architecture Overview.
|
|
1129
|
+
|
|
1130
|
+
2. ~~**File reservations**~~: **Deferred — not in scope.** File reservation support removed from current design.
|
|
1131
|
+
|
|
1132
|
+
3. ~~**REST/SSE interface**~~: **Dropped.** Only MCP Tools + MAP JSON-RPC interfaces.
|
|
1133
|
+
|
|
1134
|
+
4. ~~**Identity model**~~: **Resolved — use MAP identity primitives.** `agentId` + scopes. Persistence left to clients.
|
|
1135
|
+
|
|
1136
|
+
5. ~~**Traceability auto-grouping**~~: **Resolved — follow MAP conventions.** Messages with `conversationId` go there; without, server groups by `thread_tag`/reply chain; catch-all for untagged.
|
|
1137
|
+
|
|
1138
|
+
6. ~~**Event deduplication**~~: **Resolved — concern-based split eliminates the problem.** The sidecar handles lifecycle via its MAP connection; Agent Inbox handles messaging via its own MAP connection. No duplicate paths since each system only touches its own concern.
|
|
1139
|
+
|
|
1140
|
+
7. ~~**Event bus architecture**~~: **Resolved — eliminated.** The plugin already routes by command type, making a dedicated bus unnecessary. Agent Inbox learns about agents via plugin notifications and MAP server queries.
|
|
1141
|
+
|
|
1142
|
+
### Open
|
|
1143
|
+
|
|
1144
|
+
1. **MCP notification support**: Can we push new messages to agents, or must they poll with `check_inbox`? For now, poll-only for agents; real-time for MAP JSON-RPC clients via `map/subscribe`.
|
|
1145
|
+
|
|
1146
|
+
2. **MAP server dependency**: Should Agent Inbox run without a MAP server (embedded mode for testing/dev), or always require at least one?
|
|
1147
|
+
|
|
1148
|
+
3. ~~**Cross-server conversation ownership**~~: **Resolved — each side maintains its own view.** When a message crosses federation boundaries, both the sending and receiving Agent Inbox instances store their own copy. Each side tracks threads, conversations, and reply chains independently. No sync protocol needed.
|
|
1149
|
+
|
|
1150
|
+
4. **Message format evolution**: Should `x-*` extension content types be registered/discoverable, or purely convention-based?
|
|
1151
|
+
|
|
1152
|
+
5. **Startup ordering**: If the plugin sends a messaging command before Agent Inbox is ready, it falls back to the sidecar. Should Agent Inbox replay missed messages from the MAP server on startup?
|
|
1153
|
+
|
|
1154
|
+
6. **Transitive federation routing**: If system A is federated with hub H, and system B is also federated with H, can A send to B through H? The hierarchical routing strategy supports this, but the trust implications need further thought.
|
|
1155
|
+
|
|
1156
|
+
7. **Federation discovery**: MAP spec mentions `.well-known` discovery and `did:wba` for domain-anchored identity. Should Agent Inbox support automatic peer discovery, or require explicit configuration? For now, explicit config only.
|