agent-inbox 0.1.8 → 0.1.9
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.md +44 -7
- package/README.md +67 -24
- package/dist/federation/connection-manager.d.ts +13 -2
- package/dist/federation/connection-manager.d.ts.map +1 -1
- package/dist/federation/connection-manager.js +109 -10
- package/dist/federation/connection-manager.js.map +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +58 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ipc/ipc-server.d.ts +2 -0
- package/dist/ipc/ipc-server.d.ts.map +1 -1
- package/dist/ipc/ipc-server.js +48 -0
- package/dist/ipc/ipc-server.js.map +1 -1
- package/dist/map/map-client.d.ts +100 -0
- package/dist/map/map-client.d.ts.map +1 -1
- package/dist/map/map-client.js +61 -0
- package/dist/map/map-client.js.map +1 -1
- package/dist/mcp/mcp-proxy.d.ts +28 -0
- package/dist/mcp/mcp-proxy.d.ts.map +1 -0
- package/dist/mcp/mcp-proxy.js +280 -0
- package/dist/mcp/mcp-proxy.js.map +1 -0
- package/dist/mesh/delivery-bridge.d.ts +47 -0
- package/dist/mesh/delivery-bridge.d.ts.map +1 -0
- package/dist/mesh/delivery-bridge.js +73 -0
- package/dist/mesh/delivery-bridge.js.map +1 -0
- package/dist/mesh/mesh-connector.d.ts +29 -0
- package/dist/mesh/mesh-connector.d.ts.map +1 -0
- package/dist/mesh/mesh-connector.js +36 -0
- package/dist/mesh/mesh-connector.js.map +1 -0
- package/dist/mesh/mesh-transport.d.ts +70 -0
- package/dist/mesh/mesh-transport.d.ts.map +1 -0
- package/dist/mesh/mesh-transport.js +92 -0
- package/dist/mesh/mesh-transport.js.map +1 -0
- package/dist/mesh/type-mapper.d.ts +67 -0
- package/dist/mesh/type-mapper.d.ts.map +1 -0
- package/dist/mesh/type-mapper.js +165 -0
- package/dist/mesh/type-mapper.js.map +1 -0
- package/dist/types.d.ts +29 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/CLAUDE-CODE-SWARM-PROPOSAL.md +137 -0
- package/package.json +7 -2
- package/src/federation/connection-manager.ts +125 -10
- package/src/index.ts +96 -5
- package/src/ipc/ipc-server.ts +58 -0
- package/src/map/map-client.ts +152 -0
- package/src/mcp/mcp-proxy.ts +326 -0
- package/src/mesh/delivery-bridge.ts +110 -0
- package/src/mesh/mesh-connector.ts +41 -0
- package/src/mesh/mesh-transport.ts +157 -0
- package/src/mesh/type-mapper.ts +239 -0
- package/src/types.ts +33 -1
- package/test/federation/integration.test.ts +37 -3
- package/test/federation/sdk-integration.test.ts +4 -8
- package/test/ipc-new-commands.test.ts +200 -0
- package/test/mcp-proxy.test.ts +191 -0
- package/test/mesh/delivery-bridge.test.ts +178 -0
- package/test/mesh/e2e-mesh.test.ts +527 -0
- package/test/mesh/e2e-real-meshpeer.test.ts +629 -0
- package/test/mesh/federation-mesh.test.ts +269 -0
- package/test/mesh/mesh-connector.test.ts +66 -0
- package/test/mesh/mesh-transport.test.ts +191 -0
- package/test/mesh/meshpeer-integration.test.ts +442 -0
- package/test/mesh/mock-mesh.ts +125 -0
- package/test/mesh/mock-meshpeer.ts +266 -0
- package/test/mesh/type-mapper.test.ts +226 -0
- package/docs/PLAN.md +0 -545
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type mapping between agentic-mesh MAP messages and agent-inbox messages.
|
|
3
|
+
*
|
|
4
|
+
* Translates MAP Message (from agentic-mesh's MapServer/FederationGateway)
|
|
5
|
+
* into agent-inbox's internal Message format and vice versa.
|
|
6
|
+
*
|
|
7
|
+
* Inbox-specific fields (subject, thread_tag, importance, recipientKind)
|
|
8
|
+
* are encoded in the MAP message's `_meta` vendor extension field.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { ulid } from "ulid";
|
|
12
|
+
import { normalizeContent } from "../router/message-router.js";
|
|
13
|
+
import type {
|
|
14
|
+
Message,
|
|
15
|
+
Recipient,
|
|
16
|
+
RecipientKind,
|
|
17
|
+
Importance,
|
|
18
|
+
} from "../types.js";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Minimal MAP message type stubs (matches agentic-mesh v0.2.0)
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** MAP Message as delivered by agentic-mesh's MapServer. */
|
|
25
|
+
export interface MapMessage {
|
|
26
|
+
id: string;
|
|
27
|
+
from: string;
|
|
28
|
+
to: MapAddress;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
payload?: unknown;
|
|
31
|
+
meta?: MapMessageMeta;
|
|
32
|
+
_meta?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type MapAddress =
|
|
36
|
+
| string
|
|
37
|
+
| { agent: string }
|
|
38
|
+
| { agents: string[] }
|
|
39
|
+
| { scope: string }
|
|
40
|
+
| { role: string; within?: string }
|
|
41
|
+
| { broadcast: true }
|
|
42
|
+
| { system: string; agent: string }
|
|
43
|
+
| Record<string, unknown>;
|
|
44
|
+
|
|
45
|
+
export interface MapMessageMeta {
|
|
46
|
+
priority?: "urgent" | "high" | "normal" | "low";
|
|
47
|
+
correlationId?: string;
|
|
48
|
+
delivery?: "fire-and-forget" | "acknowledged" | "guaranteed";
|
|
49
|
+
_meta?: Record<string, unknown>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// MAP → Inbox
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Convert a MAP Message from agentic-mesh into an agent-inbox Message.
|
|
58
|
+
*
|
|
59
|
+
* Inbox-specific fields are read from `_meta` (or `meta._meta`):
|
|
60
|
+
* - `_meta.subject` → `subject`
|
|
61
|
+
* - `_meta.threadTag` / `meta.correlationId` → `thread_tag`
|
|
62
|
+
* - `_meta.inReplyTo` → `in_reply_to`
|
|
63
|
+
* - `_meta.recipientKind` → per-recipient `kind`
|
|
64
|
+
* - `_meta.inboxMessageId` → used as `id` if present
|
|
65
|
+
* - `meta.priority` → `importance`
|
|
66
|
+
*/
|
|
67
|
+
export function mapMessageToInbox(
|
|
68
|
+
mapMsg: MapMessage,
|
|
69
|
+
scope: string = "default"
|
|
70
|
+
): Message {
|
|
71
|
+
const vendorMeta = mapMsg._meta ?? mapMsg.meta?._meta ?? {};
|
|
72
|
+
const recipients = resolveMapAddress(mapMsg.to, vendorMeta);
|
|
73
|
+
|
|
74
|
+
const priority = mapMsg.meta?.priority;
|
|
75
|
+
const importance: Importance =
|
|
76
|
+
priority === "urgent"
|
|
77
|
+
? "urgent"
|
|
78
|
+
: priority === "high"
|
|
79
|
+
? "high"
|
|
80
|
+
: priority === "low"
|
|
81
|
+
? "low"
|
|
82
|
+
: "normal";
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
id: (vendorMeta.inboxMessageId as string) ?? ulid(),
|
|
86
|
+
scope,
|
|
87
|
+
sender_id: mapMsg.from,
|
|
88
|
+
recipients,
|
|
89
|
+
content: normalizeContent(mapMsg.payload),
|
|
90
|
+
importance,
|
|
91
|
+
subject: vendorMeta.subject as string | undefined,
|
|
92
|
+
thread_tag:
|
|
93
|
+
(vendorMeta.threadTag as string) ?? mapMsg.meta?.correlationId,
|
|
94
|
+
in_reply_to: vendorMeta.inReplyTo as string | undefined,
|
|
95
|
+
conversation_id: vendorMeta.conversationId as string | undefined,
|
|
96
|
+
metadata: stripInboxKeys(vendorMeta),
|
|
97
|
+
created_at: new Date(mapMsg.timestamp).toISOString(),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resolve a MAP Address into agent-inbox Recipients.
|
|
103
|
+
*/
|
|
104
|
+
function resolveMapAddress(
|
|
105
|
+
addr: MapAddress,
|
|
106
|
+
vendorMeta: Record<string, unknown>
|
|
107
|
+
): Recipient[] {
|
|
108
|
+
const kind = (vendorMeta.recipientKind as RecipientKind) ?? "to";
|
|
109
|
+
const now = new Date().toISOString();
|
|
110
|
+
|
|
111
|
+
if (typeof addr === "string") {
|
|
112
|
+
return [{ agent_id: addr, kind, delivered_at: now }];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Federated address must be checked before simple agent address
|
|
116
|
+
// since { system: "x", agent: "y" } also matches "agent" in addr
|
|
117
|
+
if (
|
|
118
|
+
"system" in addr &&
|
|
119
|
+
"agent" in addr &&
|
|
120
|
+
typeof addr.system === "string" &&
|
|
121
|
+
typeof addr.agent === "string"
|
|
122
|
+
) {
|
|
123
|
+
return [
|
|
124
|
+
{ agent_id: `${addr.agent}@${addr.system}`, kind, delivered_at: now },
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if ("agent" in addr && typeof addr.agent === "string") {
|
|
129
|
+
return [{ agent_id: addr.agent, kind, delivered_at: now }];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if ("agents" in addr && Array.isArray(addr.agents)) {
|
|
133
|
+
return addr.agents.map((agentId: string) => ({
|
|
134
|
+
agent_id: agentId,
|
|
135
|
+
kind,
|
|
136
|
+
delivered_at: now,
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if ("scope" in addr && typeof addr.scope === "string") {
|
|
141
|
+
return [{ agent_id: addr.scope, kind, delivered_at: now }];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if ("broadcast" in addr) {
|
|
145
|
+
return [{ agent_id: "*", kind, delivered_at: now }];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Keys that belong to the inbox layer, not stored in metadata
|
|
152
|
+
const INBOX_META_KEYS = new Set([
|
|
153
|
+
"subject",
|
|
154
|
+
"threadTag",
|
|
155
|
+
"inReplyTo",
|
|
156
|
+
"recipientKind",
|
|
157
|
+
"inboxMessageId",
|
|
158
|
+
"conversationId",
|
|
159
|
+
"importance",
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
function stripInboxKeys(
|
|
163
|
+
meta: Record<string, unknown>
|
|
164
|
+
): Record<string, unknown> {
|
|
165
|
+
const result: Record<string, unknown> = {};
|
|
166
|
+
for (const [k, v] of Object.entries(meta)) {
|
|
167
|
+
if (!INBOX_META_KEYS.has(k)) {
|
|
168
|
+
result[k] = v;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// Inbox → MAP
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Convert an agent-inbox Message into partial MAP message fields
|
|
180
|
+
* suitable for sending via `AgentConnection.send()` or `FederationGateway.route()`.
|
|
181
|
+
*
|
|
182
|
+
* Returns `to`, `payload`, and `meta` (including `_meta` for inbox fields).
|
|
183
|
+
*/
|
|
184
|
+
export function inboxMessageToMap(msg: Message): {
|
|
185
|
+
to: MapAddress;
|
|
186
|
+
payload: unknown;
|
|
187
|
+
meta: MapMessageMeta & { _meta: Record<string, unknown> };
|
|
188
|
+
} {
|
|
189
|
+
// Build the MAP address from recipients
|
|
190
|
+
const primaryRecipients = msg.recipients.filter((r) => r.kind === "to");
|
|
191
|
+
const to = resolveRecipientsToAddress(primaryRecipients);
|
|
192
|
+
|
|
193
|
+
const priority: MapMessageMeta["priority"] =
|
|
194
|
+
msg.importance === "urgent"
|
|
195
|
+
? "urgent"
|
|
196
|
+
: msg.importance === "high"
|
|
197
|
+
? "high"
|
|
198
|
+
: msg.importance === "low"
|
|
199
|
+
? "low"
|
|
200
|
+
: "normal";
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
to,
|
|
204
|
+
payload: msg.content,
|
|
205
|
+
meta: {
|
|
206
|
+
priority,
|
|
207
|
+
correlationId: msg.thread_tag,
|
|
208
|
+
_meta: {
|
|
209
|
+
inboxMessageId: msg.id,
|
|
210
|
+
...(msg.subject ? { subject: msg.subject } : {}),
|
|
211
|
+
...(msg.thread_tag ? { threadTag: msg.thread_tag } : {}),
|
|
212
|
+
...(msg.in_reply_to ? { inReplyTo: msg.in_reply_to } : {}),
|
|
213
|
+
...(msg.conversation_id
|
|
214
|
+
? { conversationId: msg.conversation_id }
|
|
215
|
+
: {}),
|
|
216
|
+
...msg.metadata,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function resolveRecipientsToAddress(recipients: Recipient[]): MapAddress {
|
|
223
|
+
if (recipients.length === 0) {
|
|
224
|
+
return { broadcast: true };
|
|
225
|
+
}
|
|
226
|
+
if (recipients.length === 1) {
|
|
227
|
+
const agentId = recipients[0].agent_id;
|
|
228
|
+
// Check for federated address (agent@system)
|
|
229
|
+
const atIdx = agentId.indexOf("@");
|
|
230
|
+
if (atIdx > 0) {
|
|
231
|
+
return {
|
|
232
|
+
system: agentId.slice(atIdx + 1),
|
|
233
|
+
agent: agentId.slice(0, atIdx),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return { agent: agentId };
|
|
237
|
+
}
|
|
238
|
+
return { agents: recipients.map((r) => r.agent_id) };
|
|
239
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -140,6 +140,18 @@ export interface IpcCheckInboxCommand {
|
|
|
140
140
|
clear?: boolean;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
export interface IpcReadThreadCommand {
|
|
144
|
+
action: "read_thread";
|
|
145
|
+
threadTag: string;
|
|
146
|
+
scope?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface IpcListAgentsCommand {
|
|
150
|
+
action: "list_agents";
|
|
151
|
+
scope?: string;
|
|
152
|
+
includeFederated?: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
143
155
|
export interface IpcPingCommand {
|
|
144
156
|
action: "ping";
|
|
145
157
|
}
|
|
@@ -149,12 +161,26 @@ export type IpcCommand =
|
|
|
149
161
|
| IpcEmitCommand
|
|
150
162
|
| IpcNotifyCommand
|
|
151
163
|
| IpcCheckInboxCommand
|
|
164
|
+
| IpcReadThreadCommand
|
|
165
|
+
| IpcListAgentsCommand
|
|
152
166
|
| IpcPingCommand;
|
|
153
167
|
|
|
154
168
|
export interface IpcResponse {
|
|
155
169
|
ok: boolean;
|
|
156
170
|
messageId?: string;
|
|
157
171
|
messages?: Message[];
|
|
172
|
+
agents?: Array<{
|
|
173
|
+
agentId: string;
|
|
174
|
+
name?: string;
|
|
175
|
+
scope: string;
|
|
176
|
+
status: string;
|
|
177
|
+
program?: string;
|
|
178
|
+
lastActive?: string;
|
|
179
|
+
location: "local" | "federated";
|
|
180
|
+
peerId?: string;
|
|
181
|
+
}>;
|
|
182
|
+
count?: number;
|
|
183
|
+
threadTag?: string;
|
|
158
184
|
error?: string;
|
|
159
185
|
pid?: number;
|
|
160
186
|
}
|
|
@@ -180,7 +206,10 @@ export interface FederatedAddress {
|
|
|
180
206
|
|
|
181
207
|
export interface FederationPeerConfig {
|
|
182
208
|
systemId: string;
|
|
183
|
-
|
|
209
|
+
/** WebSocket URL for MAP SDK connections. */
|
|
210
|
+
url?: string;
|
|
211
|
+
/** Mesh peer ID for agentic-mesh transport connections. */
|
|
212
|
+
meshPeerId?: string;
|
|
184
213
|
auth?: FederationAuth;
|
|
185
214
|
exposure?: ExposurePolicy;
|
|
186
215
|
}
|
|
@@ -199,12 +228,15 @@ export interface ExposurePolicy {
|
|
|
199
228
|
events?: ExposureLevel;
|
|
200
229
|
}
|
|
201
230
|
|
|
231
|
+
export type FederationTransport = "websocket" | "mesh";
|
|
232
|
+
|
|
202
233
|
export interface FederationLink {
|
|
203
234
|
peerId: string;
|
|
204
235
|
sessionId: string;
|
|
205
236
|
status: "connected" | "disconnected" | "authenticating";
|
|
206
237
|
exposure: ExposurePolicy;
|
|
207
238
|
url: string;
|
|
239
|
+
transport: FederationTransport;
|
|
208
240
|
connectedAt?: string;
|
|
209
241
|
}
|
|
210
242
|
|
|
@@ -185,7 +185,7 @@ describe("Federation Integration", () => {
|
|
|
185
185
|
await hubFederation.destroy();
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
-
it("should
|
|
188
|
+
it("should relay via upstream hub when system-qualified address has no direct peer link", async () => {
|
|
189
189
|
const hubFederation = new ConnectionManager(events, {
|
|
190
190
|
systemId: "system-1",
|
|
191
191
|
trust: {
|
|
@@ -204,7 +204,8 @@ describe("Federation Integration", () => {
|
|
|
204
204
|
url: "ws://hub:3000",
|
|
205
205
|
});
|
|
206
206
|
|
|
207
|
-
// Route to far-system which has no peer link
|
|
207
|
+
// Route to far-system which has no direct peer link.
|
|
208
|
+
// With hierarchical strategy, falls through to upstream hub relay.
|
|
208
209
|
const msg: Message = {
|
|
209
210
|
id: "msg-hub-2",
|
|
210
211
|
scope: "default",
|
|
@@ -217,11 +218,44 @@ describe("Federation Integration", () => {
|
|
|
217
218
|
};
|
|
218
219
|
|
|
219
220
|
const result = await hubFederation.route(msg);
|
|
221
|
+
// Hierarchical strategy delegates to upstream hub instead of queuing
|
|
222
|
+
expect(result.delivered).toBe(true);
|
|
223
|
+
expect(result.peerId).toBe("hub-system");
|
|
224
|
+
|
|
225
|
+
await hubFederation.destroy();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("should queue when system-qualified address has no peer link (table strategy)", async () => {
|
|
229
|
+
const tableFederation = new ConnectionManager(events, {
|
|
230
|
+
systemId: "system-1",
|
|
231
|
+
trust: {
|
|
232
|
+
allowedServers: [],
|
|
233
|
+
scopePermissions: {},
|
|
234
|
+
requireAuth: false,
|
|
235
|
+
},
|
|
236
|
+
routing: {
|
|
237
|
+
strategy: "table",
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// No peers connected — route to far-system queues immediately
|
|
242
|
+
const msg: Message = {
|
|
243
|
+
id: "msg-table-1",
|
|
244
|
+
scope: "default",
|
|
245
|
+
sender_id: "agent-a",
|
|
246
|
+
recipients: [{ agent_id: "agent-x@far-system", kind: "to" }],
|
|
247
|
+
content: { type: "text", text: "no direct link" },
|
|
248
|
+
importance: "normal",
|
|
249
|
+
metadata: {},
|
|
250
|
+
created_at: new Date().toISOString(),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const result = await tableFederation.route(msg);
|
|
220
254
|
expect(result.delivered).toBe(false);
|
|
221
255
|
expect(result.queued).toBe(true);
|
|
222
256
|
expect(result.peerId).toBe("far-system");
|
|
223
257
|
|
|
224
|
-
await
|
|
258
|
+
await tableFederation.destroy();
|
|
225
259
|
});
|
|
226
260
|
});
|
|
227
261
|
|
|
@@ -477,14 +477,10 @@ describe("Federation SDK Integration (two-system)", () => {
|
|
|
477
477
|
});
|
|
478
478
|
|
|
479
479
|
describe("broadcast strategy with real transport", () => {
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
485
|
-
// System-qualified addresses like "bob@system-2" always resolve via addr.system,
|
|
486
|
-
// bypassing the strategy entirely. This is a known design gap documented in
|
|
487
|
-
// integration.test.ts.
|
|
480
|
+
// For system-qualified addresses (bob@system-2) where the target system IS a
|
|
481
|
+
// connected peer, delivery goes directly via transport (no broadcast needed).
|
|
482
|
+
// Broadcast/hierarchical strategies activate when the resolved peer has no
|
|
483
|
+
// active link — e.g., targeting a system we're not directly connected to.
|
|
488
484
|
//
|
|
489
485
|
// These tests verify broadcast transport by:
|
|
490
486
|
// 1. Testing system-qualified delivery to multiple peers (direct transport)
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as net from "node:net";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { EventEmitter } from "node:events";
|
|
6
|
+
import { InMemoryStorage } from "../src/storage/memory.js";
|
|
7
|
+
import { MessageRouter } from "../src/router/message-router.js";
|
|
8
|
+
import { IpcServer } from "../src/ipc/ipc-server.js";
|
|
9
|
+
|
|
10
|
+
function tmpSocketPath(): string {
|
|
11
|
+
return path.join(os.tmpdir(), `inbox-test-${Date.now()}-${Math.random().toString(36).slice(2)}.sock`);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function sendCommand(socketPath: string, command: object): Promise<Record<string, unknown>> {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const client = net.createConnection(socketPath, () => {
|
|
17
|
+
client.write(JSON.stringify(command) + "\n");
|
|
18
|
+
});
|
|
19
|
+
let buffer = "";
|
|
20
|
+
client.on("data", (data) => {
|
|
21
|
+
buffer += data.toString();
|
|
22
|
+
const idx = buffer.indexOf("\n");
|
|
23
|
+
if (idx !== -1) {
|
|
24
|
+
const line = buffer.slice(0, idx);
|
|
25
|
+
client.end();
|
|
26
|
+
resolve(JSON.parse(line));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
client.on("error", reject);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("IPC read_thread command", () => {
|
|
34
|
+
let storage: InMemoryStorage;
|
|
35
|
+
let events: EventEmitter;
|
|
36
|
+
let router: MessageRouter;
|
|
37
|
+
let server: IpcServer;
|
|
38
|
+
let socketPath: string;
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
storage = new InMemoryStorage();
|
|
42
|
+
events = new EventEmitter();
|
|
43
|
+
router = new MessageRouter(storage, events, "default");
|
|
44
|
+
socketPath = tmpSocketPath();
|
|
45
|
+
server = new IpcServer(socketPath, router, storage);
|
|
46
|
+
await server.start();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(async () => {
|
|
50
|
+
await server.stop();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should return empty thread for unknown tag", async () => {
|
|
54
|
+
const resp = await sendCommand(socketPath, {
|
|
55
|
+
action: "read_thread",
|
|
56
|
+
threadTag: "nonexistent",
|
|
57
|
+
});
|
|
58
|
+
expect(resp.ok).toBe(true);
|
|
59
|
+
expect(resp.threadTag).toBe("nonexistent");
|
|
60
|
+
expect(resp.count).toBe(0);
|
|
61
|
+
expect(resp.messages).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("should return messages in a thread", async () => {
|
|
65
|
+
// Send messages with same threadTag
|
|
66
|
+
await sendCommand(socketPath, {
|
|
67
|
+
action: "send",
|
|
68
|
+
from: "alice",
|
|
69
|
+
to: "bob",
|
|
70
|
+
payload: "first message",
|
|
71
|
+
threadTag: "task-42",
|
|
72
|
+
});
|
|
73
|
+
await sendCommand(socketPath, {
|
|
74
|
+
action: "send",
|
|
75
|
+
from: "bob",
|
|
76
|
+
to: "alice",
|
|
77
|
+
payload: "reply to first",
|
|
78
|
+
threadTag: "task-42",
|
|
79
|
+
});
|
|
80
|
+
// Different thread
|
|
81
|
+
await sendCommand(socketPath, {
|
|
82
|
+
action: "send",
|
|
83
|
+
from: "alice",
|
|
84
|
+
to: "charlie",
|
|
85
|
+
payload: "unrelated",
|
|
86
|
+
threadTag: "task-99",
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const resp = await sendCommand(socketPath, {
|
|
90
|
+
action: "read_thread",
|
|
91
|
+
threadTag: "task-42",
|
|
92
|
+
scope: "default",
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
expect(resp.ok).toBe(true);
|
|
96
|
+
expect(resp.count).toBe(2);
|
|
97
|
+
const messages = resp.messages as Array<{ sender_id: string }>;
|
|
98
|
+
expect(messages).toHaveLength(2);
|
|
99
|
+
const senders = messages.map((m) => m.sender_id);
|
|
100
|
+
expect(senders).toContain("alice");
|
|
101
|
+
expect(senders).toContain("bob");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("IPC list_agents command", () => {
|
|
106
|
+
let storage: InMemoryStorage;
|
|
107
|
+
let events: EventEmitter;
|
|
108
|
+
let router: MessageRouter;
|
|
109
|
+
let server: IpcServer;
|
|
110
|
+
let socketPath: string;
|
|
111
|
+
|
|
112
|
+
beforeEach(async () => {
|
|
113
|
+
storage = new InMemoryStorage();
|
|
114
|
+
events = new EventEmitter();
|
|
115
|
+
router = new MessageRouter(storage, events, "default");
|
|
116
|
+
socketPath = tmpSocketPath();
|
|
117
|
+
server = new IpcServer(socketPath, router, storage);
|
|
118
|
+
await server.start();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
afterEach(async () => {
|
|
122
|
+
await server.stop();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should return empty list with no agents", async () => {
|
|
126
|
+
const resp = await sendCommand(socketPath, {
|
|
127
|
+
action: "list_agents",
|
|
128
|
+
});
|
|
129
|
+
expect(resp.ok).toBe(true);
|
|
130
|
+
expect(resp.count).toBe(0);
|
|
131
|
+
expect(resp.agents).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should list registered agents", async () => {
|
|
135
|
+
// Register agents via notify
|
|
136
|
+
await sendCommand(socketPath, {
|
|
137
|
+
action: "notify",
|
|
138
|
+
event: {
|
|
139
|
+
type: "agent.spawn",
|
|
140
|
+
agent: {
|
|
141
|
+
agentId: "gsd-executor",
|
|
142
|
+
name: "executor",
|
|
143
|
+
scopes: ["swarm:gsd"],
|
|
144
|
+
metadata: { role: "executor" },
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
await sendCommand(socketPath, {
|
|
149
|
+
action: "notify",
|
|
150
|
+
event: {
|
|
151
|
+
type: "agent.spawn",
|
|
152
|
+
agent: {
|
|
153
|
+
agentId: "gsd-verifier",
|
|
154
|
+
name: "verifier",
|
|
155
|
+
scopes: ["swarm:gsd"],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const resp = await sendCommand(socketPath, {
|
|
161
|
+
action: "list_agents",
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
expect(resp.ok).toBe(true);
|
|
165
|
+
expect(resp.count).toBe(2);
|
|
166
|
+
const agents = resp.agents as Array<{ agentId: string; location: string }>;
|
|
167
|
+
expect(agents).toHaveLength(2);
|
|
168
|
+
const ids = agents.map((a) => a.agentId);
|
|
169
|
+
expect(ids).toContain("gsd-executor");
|
|
170
|
+
expect(ids).toContain("gsd-verifier");
|
|
171
|
+
expect(agents[0].location).toBe("local");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("should filter agents by scope", async () => {
|
|
175
|
+
await sendCommand(socketPath, {
|
|
176
|
+
action: "notify",
|
|
177
|
+
event: {
|
|
178
|
+
type: "agent.spawn",
|
|
179
|
+
agent: { agentId: "team1-a", name: "a", scopes: ["team1"] },
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
await sendCommand(socketPath, {
|
|
183
|
+
action: "notify",
|
|
184
|
+
event: {
|
|
185
|
+
type: "agent.spawn",
|
|
186
|
+
agent: { agentId: "team2-b", name: "b", scopes: ["team2"] },
|
|
187
|
+
},
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const resp = await sendCommand(socketPath, {
|
|
191
|
+
action: "list_agents",
|
|
192
|
+
scope: "team1",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(resp.ok).toBe(true);
|
|
196
|
+
expect(resp.count).toBe(1);
|
|
197
|
+
const agents = resp.agents as Array<{ agentId: string }>;
|
|
198
|
+
expect(agents[0].agentId).toBe("team1-a");
|
|
199
|
+
});
|
|
200
|
+
});
|