agent-inbox 0.1.8 → 0.2.0

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.
Files changed (75) hide show
  1. package/CLAUDE.md +44 -7
  2. package/README.md +67 -24
  3. package/dist/cli.d.ts +20 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +89 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/federation/connection-manager.d.ts +13 -2
  8. package/dist/federation/connection-manager.d.ts.map +1 -1
  9. package/dist/federation/connection-manager.js +109 -10
  10. package/dist/federation/connection-manager.js.map +1 -1
  11. package/dist/index.d.mts +2 -0
  12. package/dist/index.d.ts +25 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +58 -5
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +1 -0
  17. package/dist/index.mjs.map +1 -0
  18. package/dist/ipc/ipc-server.d.ts +2 -0
  19. package/dist/ipc/ipc-server.d.ts.map +1 -1
  20. package/dist/ipc/ipc-server.js +48 -0
  21. package/dist/ipc/ipc-server.js.map +1 -1
  22. package/dist/map/map-client.d.ts +100 -0
  23. package/dist/map/map-client.d.ts.map +1 -1
  24. package/dist/map/map-client.js +61 -0
  25. package/dist/map/map-client.js.map +1 -1
  26. package/dist/mcp/mcp-proxy.d.ts +28 -0
  27. package/dist/mcp/mcp-proxy.d.ts.map +1 -0
  28. package/dist/mcp/mcp-proxy.js +280 -0
  29. package/dist/mcp/mcp-proxy.js.map +1 -0
  30. package/dist/mesh/delivery-bridge.d.ts +47 -0
  31. package/dist/mesh/delivery-bridge.d.ts.map +1 -0
  32. package/dist/mesh/delivery-bridge.js +73 -0
  33. package/dist/mesh/delivery-bridge.js.map +1 -0
  34. package/dist/mesh/mesh-connector.d.ts +29 -0
  35. package/dist/mesh/mesh-connector.d.ts.map +1 -0
  36. package/dist/mesh/mesh-connector.js +36 -0
  37. package/dist/mesh/mesh-connector.js.map +1 -0
  38. package/dist/mesh/mesh-transport.d.ts +70 -0
  39. package/dist/mesh/mesh-transport.d.ts.map +1 -0
  40. package/dist/mesh/mesh-transport.js +92 -0
  41. package/dist/mesh/mesh-transport.js.map +1 -0
  42. package/dist/mesh/type-mapper.d.ts +67 -0
  43. package/dist/mesh/type-mapper.d.ts.map +1 -0
  44. package/dist/mesh/type-mapper.js +165 -0
  45. package/dist/mesh/type-mapper.js.map +1 -0
  46. package/dist/types.d.ts +29 -2
  47. package/dist/types.d.ts.map +1 -1
  48. package/docs/CLAUDE-CODE-SWARM-PROPOSAL.md +137 -0
  49. package/package.json +10 -2
  50. package/src/cli.ts +94 -0
  51. package/src/federation/connection-manager.ts +125 -10
  52. package/src/index.ts +96 -5
  53. package/src/ipc/ipc-server.ts +58 -0
  54. package/src/map/map-client.ts +152 -0
  55. package/src/mcp/mcp-proxy.ts +326 -0
  56. package/src/mesh/delivery-bridge.ts +110 -0
  57. package/src/mesh/mesh-connector.ts +41 -0
  58. package/src/mesh/mesh-transport.ts +157 -0
  59. package/src/mesh/type-mapper.ts +239 -0
  60. package/src/types.ts +33 -1
  61. package/test/federation/integration.test.ts +37 -3
  62. package/test/federation/sdk-integration.test.ts +4 -8
  63. package/test/ipc-new-commands.test.ts +200 -0
  64. package/test/mcp-proxy.test.ts +191 -0
  65. package/test/mesh/delivery-bridge.test.ts +178 -0
  66. package/test/mesh/e2e-mesh.test.ts +527 -0
  67. package/test/mesh/e2e-real-meshpeer.test.ts +629 -0
  68. package/test/mesh/federation-mesh.test.ts +269 -0
  69. package/test/mesh/mesh-connector.test.ts +66 -0
  70. package/test/mesh/mesh-transport.test.ts +191 -0
  71. package/test/mesh/meshpeer-integration.test.ts +442 -0
  72. package/test/mesh/mock-mesh.ts +125 -0
  73. package/test/mesh/mock-meshpeer.ts +266 -0
  74. package/test/mesh/type-mapper.test.ts +226 -0
  75. package/docs/PLAN.md +0 -545
@@ -4,6 +4,8 @@ import type { MessageRouter } from "../router/message-router.js";
4
4
  import { normalizeContent } from "../router/message-router.js";
5
5
  import { ulid } from "ulid";
6
6
  import type { Message, Recipient, InboxConfig } from "../types.js";
7
+ import { mapMessageToInbox } from "../mesh/type-mapper.js";
8
+ import type { MapMessage } from "../mesh/type-mapper.js";
7
9
 
8
10
  /**
9
11
  * Wrapper around @multi-agent-protocol/sdk for Agent Inbox's MAP connection.
@@ -15,6 +17,9 @@ export class MapClient {
15
17
  private conn: MapConnection | null = null;
16
18
  private agentConnectionClass: MapAgentConnectionClass | null = null;
17
19
  private externalConn = false;
20
+ private meshPeer: MeshPeerLike | null = null;
21
+ private meshAgentConn: MeshAgentConnection | null = null;
22
+ private meshMessageHandler: ((agentId: string, message: MeshMapMessage) => void) | null = null;
18
23
 
19
24
  constructor(
20
25
  private storage: Storage,
@@ -78,7 +83,60 @@ export class MapClient {
78
83
  });
79
84
  }
80
85
 
86
+ /**
87
+ * Connect to an agentic-mesh MeshPeer as a local agent.
88
+ *
89
+ * Registers agent-inbox as an agent on the MeshPeer's MapServer,
90
+ * receives messages via the MapServer's message handler mechanism.
91
+ * This is the Phase 2 integration path — full MAP protocol over mesh.
92
+ *
93
+ * Returns the MapServer's systemId for federation system ID resolution.
94
+ */
95
+ async connectViaMesh(meshPeer: MeshPeerLike): Promise<string> {
96
+ this.meshPeer = meshPeer;
97
+
98
+ // Register agent-inbox as an agent on the MeshPeer's MapServer
99
+ this.meshAgentConn = await meshPeer.createAgent({
100
+ agentId: "agent-inbox",
101
+ name: "Agent Inbox",
102
+ role: "inbox",
103
+ scopes: ["default"],
104
+ capabilities: { trajectory: { canReport: false } },
105
+ metadata: { type: "agent-inbox" },
106
+ });
107
+
108
+ // Set up message handler on the MapServer for this agent
109
+ this.meshMessageHandler = (_agentId: string, message: MeshMapMessage) => {
110
+ this.handleMeshMessage(message);
111
+ };
112
+ meshPeer.server.setMessageHandler("agent-inbox", this.meshMessageHandler);
113
+
114
+ return meshPeer.server.systemId;
115
+ }
116
+
117
+ /**
118
+ * Get the MeshPeer this client is connected to, if any.
119
+ */
120
+ getMeshPeer(): MeshPeerLike | null {
121
+ return this.meshPeer;
122
+ }
123
+
81
124
  async disconnect(): Promise<void> {
125
+ // Disconnect mesh agent connection
126
+ if (this.meshAgentConn) {
127
+ if (this.meshPeer && this.meshMessageHandler) {
128
+ this.meshPeer.server.removeMessageHandler("agent-inbox");
129
+ }
130
+ try {
131
+ await this.meshAgentConn.unregister();
132
+ } catch {
133
+ // Best-effort
134
+ }
135
+ this.meshAgentConn = null;
136
+ this.meshPeer = null;
137
+ this.meshMessageHandler = null;
138
+ }
139
+
82
140
  if (this.conn) {
83
141
  if (!this.externalConn) {
84
142
  await this.conn.disconnect();
@@ -174,6 +232,16 @@ export class MapClient {
174
232
  );
175
233
  }
176
234
 
235
+ /**
236
+ * Handle an incoming MAP message from the MeshPeer's MapServer.
237
+ * Uses the type-mapper for full MAP Message → Inbox Message translation.
238
+ */
239
+ private handleMeshMessage(msg: MeshMapMessage): void {
240
+ const inboxMsg = mapMessageToInbox(msg as MapMessage);
241
+ this.storage.putMessage(inboxMsg);
242
+ this.events.emit("message.created", inboxMsg);
243
+ }
244
+
177
245
  private handleIncoming(msg: IncomingMapMessage): void {
178
246
  const now = new Date().toISOString();
179
247
  const content = normalizeContent(msg.payload);
@@ -220,6 +288,90 @@ export class MapClient {
220
288
  }
221
289
  }
222
290
 
291
+ // ---------------------------------------------------------------------------
292
+ // MeshPeer integration (Phase 2)
293
+ // ---------------------------------------------------------------------------
294
+
295
+ /** Subset of agentic-mesh AgentConnection used by MapClient. */
296
+ export interface MeshAgentConnection {
297
+ readonly agentId: string;
298
+ readonly isRegistered: boolean;
299
+ register(): Promise<unknown>;
300
+ unregister(reason?: string): Promise<void>;
301
+ send(
302
+ to: unknown,
303
+ payload: unknown,
304
+ meta?: Record<string, unknown>
305
+ ): Promise<{ messageId: string; delivered: string[] }>;
306
+ on(event: "message", handler: (message: MeshMapMessage) => void): this;
307
+ off(event: string, handler: (...args: unknown[]) => void): this;
308
+ }
309
+
310
+ /** Subset of agentic-mesh MapServer used by MapClient (v0.2.0+). */
311
+ export interface MeshMapServer {
312
+ readonly systemId: string;
313
+ readonly systemName?: string;
314
+ /** Replace the default delivery handler. Returns the previous handler. */
315
+ setDeliveryHandler(handler: MeshDeliveryHandler): MeshDeliveryHandler;
316
+ setMessageHandler(agentId: string, handler: (agentId: string, message: MeshMapMessage) => void | Promise<void>): void;
317
+ removeMessageHandler(agentId: string): void;
318
+ registerAgent(params: {
319
+ agentId?: string;
320
+ name?: string;
321
+ role?: string;
322
+ scopes?: string[];
323
+ capabilities?: Record<string, unknown>;
324
+ metadata?: Record<string, unknown>;
325
+ }): unknown;
326
+ unregisterAgent(agentId: string, reason?: string): unknown;
327
+ }
328
+
329
+ /** Subset of agentic-mesh DeliveryHandler (v0.2.0+). */
330
+ export interface MeshDeliveryHandler {
331
+ deliverToAgent(agentId: string, message: MeshMapMessage): Promise<boolean>;
332
+ forwardToPeer(peerId: string, agentIds: string[], message: MeshMapMessage): Promise<boolean>;
333
+ routeToFederation?(systemId: string, agentIds: string[], message: MeshMapMessage): Promise<boolean>;
334
+ }
335
+
336
+ /** Subset of agentic-mesh MeshPeer used by MapClient and ConnectionManager (v0.2.0+). */
337
+ export interface MeshPeerLike {
338
+ readonly peerId: string;
339
+ readonly server: MeshMapServer;
340
+ createAgent(config: {
341
+ agentId?: string;
342
+ name?: string;
343
+ role?: string;
344
+ scopes?: string[];
345
+ capabilities?: Record<string, unknown>;
346
+ metadata?: Record<string, unknown>;
347
+ }): Promise<MeshAgentConnection>;
348
+ /** Establish a federation link with a remote MAP system. */
349
+ federateWith(remoteSystemId: string, config?: unknown): Promise<MeshFederationGateway>;
350
+ /** Get an existing federation gateway by remote system ID. */
351
+ getFederationGateway(remoteSystemId: string): MeshFederationGateway | undefined;
352
+ }
353
+
354
+ /** Subset of agentic-mesh FederationGateway (v0.2.0+). */
355
+ export interface MeshFederationGateway {
356
+ readonly localSystemId: string;
357
+ readonly remoteSystemId: string;
358
+ readonly isConnected: boolean;
359
+ route(message: unknown, targetAgentIds: string[]): Promise<boolean>;
360
+ on(event: string, handler: (...args: unknown[]) => void): this;
361
+ off(event: string, handler: (...args: unknown[]) => void): this;
362
+ }
363
+
364
+ /** Subset of agentic-mesh MAP Message. */
365
+ export interface MeshMapMessage {
366
+ id: string;
367
+ from: string;
368
+ to: unknown;
369
+ timestamp: number;
370
+ payload?: unknown;
371
+ meta?: { priority?: string; correlationId?: string; _meta?: Record<string, unknown>; [key: string]: unknown };
372
+ _meta?: Record<string, unknown>;
373
+ }
374
+
223
375
  // --- MAP SDK type stubs (for when the SDK isn't installed) ---
224
376
 
225
377
  export interface IncomingMapMessage {
@@ -0,0 +1,326 @@
1
+ /**
2
+ * mcp-proxy.ts — MCP server that proxies all tools to an existing inbox IPC socket.
3
+ *
4
+ * Instead of creating its own storage/router, this connects to a running
5
+ * agent-inbox IPC server (e.g., the sidecar's inbox instance) and translates
6
+ * MCP tool calls into IPC commands.
7
+ *
8
+ * This ensures a single source of truth for messages, agents, and routing.
9
+ */
10
+
11
+ import * as net from "node:net";
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { z } from "zod";
15
+ import type { IpcResponse } from "../types.js";
16
+
17
+ const IPC_TIMEOUT_MS = 5000;
18
+ const CONNECT_RETRY_MS = 500;
19
+ const CONNECT_MAX_RETRIES = 10;
20
+
21
+ export class InboxMcpProxy {
22
+ private mcp: McpServer;
23
+
24
+ constructor(
25
+ private socketPath: string,
26
+ private defaultAgentId: string = "anonymous",
27
+ private defaultScope: string = "default"
28
+ ) {
29
+ this.mcp = new McpServer({
30
+ name: "agent-inbox",
31
+ version: "0.1.0",
32
+ });
33
+
34
+ this.registerTools();
35
+ }
36
+
37
+ /**
38
+ * Send an IPC command to the inbox socket and return the response.
39
+ * Retries connection if socket is not yet available.
40
+ */
41
+ private async sendIpc(command: Record<string, unknown>): Promise<IpcResponse> {
42
+ let lastError: Error | null = null;
43
+
44
+ for (let attempt = 0; attempt < CONNECT_MAX_RETRIES; attempt++) {
45
+ try {
46
+ return await this.sendIpcOnce(command);
47
+ } catch (err) {
48
+ lastError = err instanceof Error ? err : new Error(String(err));
49
+ // Only retry on connection errors (socket not ready yet)
50
+ if (lastError.message.includes("ENOENT") || lastError.message.includes("ECONNREFUSED")) {
51
+ if (attempt < CONNECT_MAX_RETRIES - 1) {
52
+ await new Promise((r) => setTimeout(r, CONNECT_RETRY_MS));
53
+ continue;
54
+ }
55
+ }
56
+ break;
57
+ }
58
+ }
59
+
60
+ return { ok: false, error: `Inbox unavailable: ${lastError?.message ?? "unknown error"}` };
61
+ }
62
+
63
+ private sendIpcOnce(command: Record<string, unknown>): Promise<IpcResponse> {
64
+ return new Promise((resolve, reject) => {
65
+ const client = net.createConnection(this.socketPath);
66
+ let buffer = "";
67
+ let settled = false;
68
+
69
+ const timer = setTimeout(() => {
70
+ if (!settled) {
71
+ settled = true;
72
+ client.destroy();
73
+ reject(new Error("IPC timeout"));
74
+ }
75
+ }, IPC_TIMEOUT_MS);
76
+
77
+ client.on("connect", () => {
78
+ client.write(JSON.stringify(command) + "\n");
79
+ });
80
+
81
+ client.on("data", (data) => {
82
+ buffer += data.toString();
83
+ const newlineIdx = buffer.indexOf("\n");
84
+ if (newlineIdx !== -1) {
85
+ clearTimeout(timer);
86
+ settled = true;
87
+ const line = buffer.slice(0, newlineIdx).trim();
88
+ client.destroy();
89
+ try {
90
+ resolve(JSON.parse(line) as IpcResponse);
91
+ } catch {
92
+ reject(new Error("Invalid IPC response"));
93
+ }
94
+ }
95
+ });
96
+
97
+ client.on("error", (err) => {
98
+ if (!settled) {
99
+ clearTimeout(timer);
100
+ settled = true;
101
+ reject(err);
102
+ }
103
+ });
104
+ });
105
+ }
106
+
107
+ private registerTools(): void {
108
+ this.mcp.tool(
109
+ "send_message",
110
+ "Send a message to one or more agents. Supports replies (inReplyTo), threading (threadTag), and federated addressing (agent@system).",
111
+ {
112
+ to: z
113
+ .union([z.string(), z.array(z.string())])
114
+ .describe(
115
+ "Recipient agent ID(s). Use 'agent@system' for federated addressing."
116
+ ),
117
+ body: z
118
+ .string()
119
+ .optional()
120
+ .describe("Plain text message body (shorthand for content)"),
121
+ content: z
122
+ .object({ type: z.string() })
123
+ .passthrough()
124
+ .optional()
125
+ .describe("Structured message content"),
126
+ from: z
127
+ .string()
128
+ .optional()
129
+ .describe("Sender agent ID (defaults to caller)"),
130
+ threadTag: z
131
+ .string()
132
+ .optional()
133
+ .describe("Thread tag for grouping related messages"),
134
+ inReplyTo: z
135
+ .string()
136
+ .optional()
137
+ .describe("Message ID this is a reply to"),
138
+ importance: z
139
+ .enum(["low", "normal", "high", "urgent"])
140
+ .optional()
141
+ .describe("Message importance level"),
142
+ subject: z.string().optional().describe("Message subject"),
143
+ },
144
+ async ({ to, body, content, from, threadTag, inReplyTo, importance, subject }) => {
145
+ const payload = content ?? body ?? "";
146
+ const senderId = from ?? this.defaultAgentId;
147
+ const resp = await this.sendIpc({
148
+ action: "send",
149
+ from: senderId,
150
+ to,
151
+ payload,
152
+ threadTag,
153
+ inReplyTo,
154
+ importance,
155
+ subject,
156
+ });
157
+ return {
158
+ content: [
159
+ {
160
+ type: "text" as const,
161
+ text: JSON.stringify(
162
+ resp.ok
163
+ ? { ok: true, messageId: resp.messageId }
164
+ : { ok: false, error: resp.error }
165
+ ),
166
+ },
167
+ ],
168
+ };
169
+ }
170
+ );
171
+
172
+ this.mcp.tool(
173
+ "check_inbox",
174
+ "Check inbox for messages addressed to an agent. Auto-registers the agent if not already registered.",
175
+ {
176
+ agentId: z.string().describe("Agent ID to check inbox for"),
177
+ unreadOnly: z
178
+ .boolean()
179
+ .optional()
180
+ .describe("Only return unread messages (default true)"),
181
+ limit: z
182
+ .number()
183
+ .optional()
184
+ .describe("Max messages to return"),
185
+ },
186
+ async ({ agentId, unreadOnly, limit }) => {
187
+ const resp = await this.sendIpc({
188
+ action: "check_inbox",
189
+ agentId,
190
+ unreadOnly: unreadOnly ?? true,
191
+ clear: true, // Mark as read after retrieval
192
+ });
193
+
194
+ if (!resp.ok) {
195
+ return {
196
+ content: [
197
+ { type: "text" as const, text: JSON.stringify({ ok: false, error: resp.error }) },
198
+ ],
199
+ };
200
+ }
201
+
202
+ let messages = resp.messages ?? [];
203
+ if (limit && messages.length > limit) {
204
+ messages = messages.slice(0, limit);
205
+ }
206
+
207
+ return {
208
+ content: [
209
+ {
210
+ type: "text" as const,
211
+ text: JSON.stringify({
212
+ count: messages.length,
213
+ messages: messages.map((m) => ({
214
+ id: m.id,
215
+ from: m.sender_id,
216
+ subject: m.subject,
217
+ content: m.content,
218
+ threadTag: m.thread_tag,
219
+ importance: m.importance,
220
+ createdAt: m.created_at,
221
+ inReplyTo: m.in_reply_to,
222
+ })),
223
+ }),
224
+ },
225
+ ],
226
+ };
227
+ }
228
+ );
229
+
230
+ this.mcp.tool(
231
+ "read_thread",
232
+ "Read all messages in a thread (by thread_tag)",
233
+ {
234
+ threadTag: z.string().describe("Thread tag to read"),
235
+ scope: z
236
+ .string()
237
+ .optional()
238
+ .describe("Scope (defaults to 'default')"),
239
+ },
240
+ async ({ threadTag, scope }) => {
241
+ const resp = await this.sendIpc({
242
+ action: "read_thread",
243
+ threadTag,
244
+ scope: scope ?? this.defaultScope,
245
+ });
246
+
247
+ if (!resp.ok) {
248
+ return {
249
+ content: [
250
+ { type: "text" as const, text: JSON.stringify({ ok: false, error: resp.error }) },
251
+ ],
252
+ };
253
+ }
254
+
255
+ const messages = resp.messages ?? [];
256
+ return {
257
+ content: [
258
+ {
259
+ type: "text" as const,
260
+ text: JSON.stringify({
261
+ threadTag,
262
+ count: messages.length,
263
+ messages: messages.map((m) => ({
264
+ id: m.id,
265
+ from: m.sender_id,
266
+ content: m.content,
267
+ createdAt: m.created_at,
268
+ inReplyTo: m.in_reply_to,
269
+ })),
270
+ }),
271
+ },
272
+ ],
273
+ };
274
+ }
275
+ );
276
+
277
+ this.mcp.tool(
278
+ "list_agents",
279
+ "List agents registered in the inbox (local and optionally federated)",
280
+ {
281
+ scope: z.string().optional().describe("Filter by scope"),
282
+ includeFederated: z
283
+ .boolean()
284
+ .optional()
285
+ .describe("Include agents known from federation routing table"),
286
+ },
287
+ async ({ scope, includeFederated }) => {
288
+ const resp = await this.sendIpc({
289
+ action: "list_agents",
290
+ scope,
291
+ includeFederated,
292
+ });
293
+
294
+ if (!resp.ok) {
295
+ return {
296
+ content: [
297
+ { type: "text" as const, text: JSON.stringify({ ok: false, error: resp.error }) },
298
+ ],
299
+ };
300
+ }
301
+
302
+ return {
303
+ content: [
304
+ {
305
+ type: "text" as const,
306
+ text: JSON.stringify({
307
+ count: resp.count ?? resp.agents?.length ?? 0,
308
+ agents: resp.agents ?? [],
309
+ }),
310
+ },
311
+ ],
312
+ };
313
+ }
314
+ );
315
+ }
316
+
317
+ async start(): Promise<void> {
318
+ const transport = new StdioServerTransport();
319
+ await this.mcp.connect(transport);
320
+ }
321
+
322
+ /** Expose for testing */
323
+ get server(): McpServer {
324
+ return this.mcp;
325
+ }
326
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * DeliveryHandler bridge: connects agentic-mesh's MapServer message routing
3
+ * to agent-inbox's storage layer.
4
+ *
5
+ * When MapServer's MessageRouter resolves a message to a local agent,
6
+ * it calls `deliverToAgent()`. This bridge translates the MAP Message
7
+ * into an agent-inbox Message and stores it, triggering the full inbox
8
+ * pipeline (traceability, push notifications, etc.).
9
+ *
10
+ * For `forwardToPeer()` and `routeToFederation()`, we delegate back to
11
+ * the previous (default) handler so MeshPeer's own transport handles it.
12
+ */
13
+
14
+ import { EventEmitter } from "node:events";
15
+ import type { Storage } from "../storage/interface.js";
16
+ import { mapMessageToInbox } from "./type-mapper.js";
17
+ import type { MapMessage } from "./type-mapper.js";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // DeliveryHandler interface (matches agentic-mesh v0.2.0)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Subset of agentic-mesh's DeliveryHandler interface.
25
+ * We define it here to avoid importing the full package at compile time.
26
+ */
27
+ export interface MeshDeliveryHandler {
28
+ deliverToAgent(agentId: string, message: MapMessage): Promise<boolean>;
29
+ forwardToPeer(
30
+ peerId: string,
31
+ agentIds: string[],
32
+ message: MapMessage
33
+ ): Promise<boolean>;
34
+ routeToFederation?(
35
+ systemId: string,
36
+ agentIds: string[],
37
+ message: MapMessage
38
+ ): Promise<boolean>;
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // DeliveryBridge
43
+ // ---------------------------------------------------------------------------
44
+
45
+ export class DeliveryBridge implements MeshDeliveryHandler {
46
+ constructor(
47
+ private storage: Storage,
48
+ private events: EventEmitter,
49
+ private scope: string = "default",
50
+ private previousHandler?: MeshDeliveryHandler
51
+ ) {}
52
+
53
+ /**
54
+ * Called by MapServer's MessageRouter when a message is resolved
55
+ * to a local agent. Translates and stores in agent-inbox.
56
+ */
57
+ async deliverToAgent(agentId: string, message: MapMessage): Promise<boolean> {
58
+ try {
59
+ const inboxMsg = mapMessageToInbox(message, this.scope);
60
+
61
+ // Ensure the target agent is in the recipients list
62
+ const hasAgent = inboxMsg.recipients.some(
63
+ (r) => r.agent_id === agentId
64
+ );
65
+ if (!hasAgent) {
66
+ inboxMsg.recipients.push({
67
+ agent_id: agentId,
68
+ kind: "to",
69
+ delivered_at: new Date().toISOString(),
70
+ });
71
+ }
72
+
73
+ this.storage.putMessage(inboxMsg);
74
+ this.events.emit("message.created", inboxMsg);
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Forward to a connected peer. Delegates to the previous handler
83
+ * (MeshPeer's default transport handles actual peer forwarding).
84
+ */
85
+ async forwardToPeer(
86
+ peerId: string,
87
+ agentIds: string[],
88
+ message: MapMessage
89
+ ): Promise<boolean> {
90
+ if (this.previousHandler) {
91
+ return this.previousHandler.forwardToPeer(peerId, agentIds, message);
92
+ }
93
+ return false;
94
+ }
95
+
96
+ /**
97
+ * Route to a federated system. Delegates to the previous handler
98
+ * (FederationGateway handles cross-mesh routing).
99
+ */
100
+ async routeToFederation(
101
+ systemId: string,
102
+ agentIds: string[],
103
+ message: MapMessage
104
+ ): Promise<boolean> {
105
+ if (this.previousHandler?.routeToFederation) {
106
+ return this.previousHandler.routeToFederation(systemId, agentIds, message);
107
+ }
108
+ return false;
109
+ }
110
+ }
@@ -0,0 +1,41 @@
1
+ import type { MapAgentConnectionClass, MapConnection } from "../map/map-client.js";
2
+ import type { MeshContextLike } from "./mesh-transport.js";
3
+ import { MeshTransport, DEFAULT_CHANNEL_NAME } from "./mesh-transport.js";
4
+
5
+ /**
6
+ * Factory for creating MeshTransport connections to remote peers.
7
+ *
8
+ * Implements MapAgentConnectionClass so it can be injected into
9
+ * ConnectionManager alongside (or instead of) the MAP SDK class.
10
+ *
11
+ * Usage:
12
+ * ```ts
13
+ * const connector = new MeshConnector(mesh, "my-peer-id");
14
+ * const conn = await connector.connect("remote-peer-id");
15
+ * ```
16
+ */
17
+ export class MeshConnector implements MapAgentConnectionClass {
18
+ constructor(
19
+ private mesh: MeshContextLike,
20
+ private localPeerId: string,
21
+ private channelName: string = DEFAULT_CHANNEL_NAME
22
+ ) {}
23
+
24
+ /**
25
+ * Create a MeshTransport connection to a remote peer.
26
+ *
27
+ * The `server` parameter is the remote peer ID (not a URL).
28
+ * The `opts` parameter is ignored — mesh peers don't need
29
+ * MAP handshake parameters.
30
+ */
31
+ async connect(server: string, _opts?: unknown): Promise<MapConnection> {
32
+ const transport = new MeshTransport(
33
+ this.mesh,
34
+ this.localPeerId,
35
+ server,
36
+ this.channelName
37
+ );
38
+ await transport.open();
39
+ return transport;
40
+ }
41
+ }