agent-inbox 0.0.1 → 0.1.2

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 (126) hide show
  1. package/CLAUDE.md +113 -0
  2. package/README.md +195 -1
  3. package/dist/federation/address.d.ts +24 -0
  4. package/dist/federation/address.d.ts.map +1 -0
  5. package/dist/federation/address.js +54 -0
  6. package/dist/federation/address.js.map +1 -0
  7. package/dist/federation/connection-manager.d.ts +118 -0
  8. package/dist/federation/connection-manager.d.ts.map +1 -0
  9. package/dist/federation/connection-manager.js +369 -0
  10. package/dist/federation/connection-manager.js.map +1 -0
  11. package/dist/federation/delivery-queue.d.ts +66 -0
  12. package/dist/federation/delivery-queue.d.ts.map +1 -0
  13. package/dist/federation/delivery-queue.js +199 -0
  14. package/dist/federation/delivery-queue.js.map +1 -0
  15. package/dist/federation/index.d.ts +7 -0
  16. package/dist/federation/index.d.ts.map +1 -0
  17. package/dist/federation/index.js +6 -0
  18. package/dist/federation/index.js.map +1 -0
  19. package/dist/federation/routing-engine.d.ts +74 -0
  20. package/dist/federation/routing-engine.d.ts.map +1 -0
  21. package/dist/federation/routing-engine.js +158 -0
  22. package/dist/federation/routing-engine.js.map +1 -0
  23. package/dist/federation/trust.d.ts +39 -0
  24. package/dist/federation/trust.d.ts.map +1 -0
  25. package/dist/federation/trust.js +64 -0
  26. package/dist/federation/trust.js.map +1 -0
  27. package/dist/index.d.ts +60 -2
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +217 -18
  30. package/dist/index.js.map +1 -1
  31. package/dist/ipc/ipc-server.d.ts +21 -0
  32. package/dist/ipc/ipc-server.d.ts.map +1 -0
  33. package/dist/ipc/ipc-server.js +173 -0
  34. package/dist/ipc/ipc-server.js.map +1 -0
  35. package/dist/jsonrpc/mail-server.d.ts +45 -0
  36. package/dist/jsonrpc/mail-server.d.ts.map +1 -0
  37. package/dist/jsonrpc/mail-server.js +284 -0
  38. package/dist/jsonrpc/mail-server.js.map +1 -0
  39. package/dist/map/map-client.d.ts +91 -0
  40. package/dist/map/map-client.d.ts.map +1 -0
  41. package/dist/map/map-client.js +202 -0
  42. package/dist/map/map-client.js.map +1 -0
  43. package/dist/mcp/mcp-server.d.ts +23 -0
  44. package/dist/mcp/mcp-server.d.ts.map +1 -0
  45. package/dist/mcp/mcp-server.js +226 -0
  46. package/dist/mcp/mcp-server.js.map +1 -0
  47. package/dist/push/notifier.d.ts +49 -0
  48. package/dist/push/notifier.d.ts.map +1 -0
  49. package/dist/push/notifier.js +150 -0
  50. package/dist/push/notifier.js.map +1 -0
  51. package/dist/registry/warm-registry.d.ts +63 -0
  52. package/dist/registry/warm-registry.d.ts.map +1 -0
  53. package/dist/registry/warm-registry.js +173 -0
  54. package/dist/registry/warm-registry.js.map +1 -0
  55. package/dist/router/message-router.d.ts +44 -0
  56. package/dist/router/message-router.d.ts.map +1 -0
  57. package/dist/router/message-router.js +137 -0
  58. package/dist/router/message-router.js.map +1 -0
  59. package/dist/storage/interface.d.ts +31 -0
  60. package/dist/storage/interface.d.ts.map +1 -0
  61. package/dist/storage/interface.js +2 -0
  62. package/dist/storage/interface.js.map +1 -0
  63. package/dist/storage/memory.d.ts +28 -0
  64. package/dist/storage/memory.d.ts.map +1 -0
  65. package/dist/storage/memory.js +118 -0
  66. package/dist/storage/memory.js.map +1 -0
  67. package/dist/storage/sqlite.d.ts +35 -0
  68. package/dist/storage/sqlite.d.ts.map +1 -0
  69. package/dist/storage/sqlite.js +445 -0
  70. package/dist/storage/sqlite.js.map +1 -0
  71. package/dist/traceability/traceability.d.ts +29 -0
  72. package/dist/traceability/traceability.d.ts.map +1 -0
  73. package/dist/traceability/traceability.js +150 -0
  74. package/dist/traceability/traceability.js.map +1 -0
  75. package/dist/types.d.ts +261 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +3 -0
  78. package/dist/types.js.map +1 -0
  79. package/docs/DESIGN.md +1156 -0
  80. package/docs/PLAN.md +545 -0
  81. package/hooks/inbox-hook.mjs +119 -0
  82. package/hooks/register-hook.mjs +69 -0
  83. package/package.json +33 -25
  84. package/rules/agent-inbox.md +78 -0
  85. package/src/federation/address.ts +61 -0
  86. package/src/federation/connection-manager.ts +458 -0
  87. package/src/federation/delivery-queue.ts +222 -0
  88. package/src/federation/index.ts +6 -0
  89. package/src/federation/routing-engine.ts +188 -0
  90. package/src/federation/trust.ts +71 -0
  91. package/src/index.ts +299 -0
  92. package/src/ipc/ipc-server.ts +207 -0
  93. package/src/jsonrpc/mail-server.ts +356 -0
  94. package/src/map/map-client.ts +260 -0
  95. package/src/mcp/mcp-server.ts +272 -0
  96. package/src/push/notifier.ts +192 -0
  97. package/src/registry/warm-registry.ts +210 -0
  98. package/src/router/message-router.ts +175 -0
  99. package/src/storage/interface.ts +48 -0
  100. package/src/storage/memory.ts +145 -0
  101. package/src/storage/sqlite.ts +645 -0
  102. package/src/traceability/traceability.ts +183 -0
  103. package/src/types.ts +297 -0
  104. package/test/federation/address.test.ts +101 -0
  105. package/test/federation/connection-manager.test.ts +546 -0
  106. package/test/federation/delivery-queue.test.ts +159 -0
  107. package/test/federation/integration.test.ts +823 -0
  108. package/test/federation/routing-engine.test.ts +117 -0
  109. package/test/federation/sdk-integration.test.ts +748 -0
  110. package/test/federation/trust.test.ts +89 -0
  111. package/test/ipc-jsonrpc.test.ts +113 -0
  112. package/test/ipc-server.test.ts +197 -0
  113. package/test/mail-server.test.ts +208 -0
  114. package/test/map-client.test.ts +408 -0
  115. package/test/message-router.test.ts +184 -0
  116. package/test/push-notifier.test.ts +139 -0
  117. package/test/registry/warm-registry.test.ts +171 -0
  118. package/test/sqlite-storage.test.ts +243 -0
  119. package/test/storage.test.ts +196 -0
  120. package/test/traceability.test.ts +123 -0
  121. package/tsconfig.json +20 -0
  122. package/tsup.config.ts +10 -0
  123. package/vitest.config.ts +8 -0
  124. package/dist/index.d.mts +0 -2
  125. package/dist/index.mjs +0 -1
  126. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,183 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { ulid } from "ulid";
3
+ import type { Message, Conversation, Turn, Thread } from "../types.js";
4
+ import type { Storage } from "../storage/interface.js";
5
+
6
+ /**
7
+ * Auto-creates Conversations, Turns, and Threads from messaging events.
8
+ *
9
+ * Rules:
10
+ * 1. Message with conversationId → add Turn to that Conversation
11
+ * 2. Message with thread_tag, no conversationId → find/create Conversation for thread_tag+scope
12
+ * 3. Message with neither → add Turn to catch-all Conversation for scope
13
+ * 4. Reply (in_reply_to) → create Thread if not exists, link Turn
14
+ */
15
+ export class TraceabilityLayer {
16
+ /** Maps "thread_tag:scope" → conversation ID */
17
+ private tagToConversation = new Map<string, string>();
18
+ /** Maps scope → catch-all conversation ID */
19
+ private scopeCatchAll = new Map<string, string>();
20
+ /** Maps original message ID → thread ID (for reply chains) */
21
+ private replyChainToThread = new Map<string, string>();
22
+
23
+ constructor(
24
+ private storage: Storage,
25
+ events: EventEmitter
26
+ ) {
27
+ events.on("message.created", (msg: Message) => this.onMessage(msg));
28
+ }
29
+
30
+ private onMessage(message: Message): void {
31
+ const conversationId = this.resolveConversation(message);
32
+
33
+ // Update message's conversation_id if not already set
34
+ if (!message.conversation_id) {
35
+ message.conversation_id = conversationId;
36
+ this.storage.putMessage(message);
37
+ }
38
+
39
+ // Ensure sender is a participant
40
+ this.ensureParticipant(conversationId, message.sender_id);
41
+
42
+ // Ensure recipients are participants
43
+ for (const r of message.recipients) {
44
+ this.ensureParticipant(conversationId, r.agent_id);
45
+ }
46
+
47
+ // Create Turn
48
+ const turn: Turn = {
49
+ id: `turn-${ulid()}`,
50
+ conversation_id: conversationId,
51
+ participant_id: message.sender_id,
52
+ source_message_id: message.id,
53
+ content_type: message.content.type,
54
+ content: message.content,
55
+ created_at: message.created_at,
56
+ };
57
+
58
+ // Handle reply threading
59
+ if (message.in_reply_to) {
60
+ const threadId = this.resolveThread(message, conversationId, turn.id);
61
+ turn.thread_id = threadId;
62
+ turn.in_reply_to = this.findTurnForMessage(message.in_reply_to);
63
+ }
64
+
65
+ this.storage.addTurn(turn);
66
+
67
+ // Update conversation updated_at
68
+ const conv = this.storage.getConversation(conversationId);
69
+ if (conv) {
70
+ conv.updated_at = message.created_at;
71
+ this.storage.putConversation(conv);
72
+ }
73
+ }
74
+
75
+ private resolveConversation(message: Message): string {
76
+ // 1. Explicit conversation ID
77
+ if (message.conversation_id) {
78
+ const existing = this.storage.getConversation(message.conversation_id);
79
+ if (existing) return existing.id;
80
+ // Create it if it doesn't exist
81
+ return this.createConversation(
82
+ message.scope,
83
+ message.subject,
84
+ message.conversation_id
85
+ );
86
+ }
87
+
88
+ // 2. thread_tag → conversation
89
+ if (message.thread_tag) {
90
+ const key = `${message.thread_tag}:${message.scope}`;
91
+ const existing = this.tagToConversation.get(key);
92
+ if (existing) return existing;
93
+
94
+ const convId = this.createConversation(
95
+ message.scope,
96
+ message.thread_tag
97
+ );
98
+ this.tagToConversation.set(key, convId);
99
+ return convId;
100
+ }
101
+
102
+ // 3. Catch-all for scope
103
+ const existing = this.scopeCatchAll.get(message.scope);
104
+ if (existing) return existing;
105
+
106
+ const convId = this.createConversation(message.scope, "General");
107
+ this.scopeCatchAll.set(message.scope, convId);
108
+ return convId;
109
+ }
110
+
111
+ private createConversation(
112
+ scope: string,
113
+ subject?: string,
114
+ explicitId?: string
115
+ ): string {
116
+ const now = new Date().toISOString();
117
+ const conv: Conversation = {
118
+ id: explicitId ?? `conv-${ulid()}`,
119
+ scope,
120
+ subject,
121
+ status: "active",
122
+ participants: [],
123
+ metadata: {},
124
+ created_at: now,
125
+ updated_at: now,
126
+ };
127
+ this.storage.putConversation(conv);
128
+ return conv.id;
129
+ }
130
+
131
+ private ensureParticipant(conversationId: string, agentId: string): void {
132
+ const conv = this.storage.getConversation(conversationId);
133
+ if (!conv) return;
134
+ if (conv.participants.some((p) => p.agent_id === agentId)) return;
135
+ conv.participants.push({
136
+ agent_id: agentId,
137
+ joined_at: new Date().toISOString(),
138
+ });
139
+ this.storage.putConversation(conv);
140
+ }
141
+
142
+ private resolveThread(
143
+ message: Message,
144
+ conversationId: string,
145
+ turnId: string
146
+ ): string {
147
+ // Find the root of the reply chain
148
+ const rootMessageId = this.findRootMessage(message.in_reply_to!);
149
+
150
+ const existingThreadId = this.replyChainToThread.get(rootMessageId);
151
+ if (existingThreadId) return existingThreadId;
152
+
153
+ // Create new thread
154
+ const rootTurnId =
155
+ this.findTurnForMessage(rootMessageId) ?? turnId;
156
+ const thread: Thread = {
157
+ id: `thread-${ulid()}`,
158
+ conversation_id: conversationId,
159
+ root_turn_id: rootTurnId,
160
+ subject: message.subject,
161
+ created_at: new Date().toISOString(),
162
+ };
163
+ this.storage.putThread(thread);
164
+ this.replyChainToThread.set(rootMessageId, thread.id);
165
+ return thread.id;
166
+ }
167
+
168
+ private findRootMessage(messageId: string): string {
169
+ const msg = this.storage.getMessage(messageId);
170
+ if (!msg || !msg.in_reply_to) return messageId;
171
+ return this.findRootMessage(msg.in_reply_to);
172
+ }
173
+
174
+ private findTurnForMessage(messageId: string): string | undefined {
175
+ // Look through all conversations for a turn with this source_message_id
176
+ for (const conv of this.storage.listConversations()) {
177
+ const turns = this.storage.getTurns(conv.id);
178
+ const turn = turns.find((t) => t.source_message_id === messageId);
179
+ if (turn) return turn.id;
180
+ }
181
+ return undefined;
182
+ }
183
+ }
package/src/types.ts ADDED
@@ -0,0 +1,297 @@
1
+ // Core types for Agent Inbox
2
+
3
+ // --- Agent ---
4
+
5
+ export type AgentStatus = "active" | "idle" | "offline";
6
+
7
+ export interface Agent {
8
+ agent_id: string;
9
+ display_name?: string;
10
+ program?: string;
11
+ model?: string;
12
+ scope: string;
13
+ status: AgentStatus;
14
+ metadata: Record<string, unknown>;
15
+ registered_at: string;
16
+ last_active_at: string;
17
+ }
18
+
19
+ // --- Message Content ---
20
+
21
+ export type MessageContent =
22
+ | { type: "text"; text: string }
23
+ | { type: "data"; schema?: string; data: unknown }
24
+ | { type: "event"; event: string; data?: unknown }
25
+ | { type: "reference"; uri: string; label?: string }
26
+ | { type: string; [key: string]: unknown };
27
+
28
+ export type RecipientKind = "to" | "cc" | "bcc";
29
+ export type Importance = "low" | "normal" | "high" | "urgent";
30
+
31
+ export interface Recipient {
32
+ agent_id: string;
33
+ kind: RecipientKind;
34
+ delivered_at?: string;
35
+ read_at?: string;
36
+ ack_at?: string;
37
+ }
38
+
39
+ export interface Message {
40
+ id: string;
41
+ scope: string;
42
+ sender_id: string;
43
+ recipients: Recipient[];
44
+ subject?: string;
45
+ content: MessageContent;
46
+ thread_tag?: string;
47
+ in_reply_to?: string;
48
+ conversation_id?: string;
49
+ importance: Importance;
50
+ metadata: Record<string, unknown>;
51
+ created_at: string;
52
+ }
53
+
54
+ // --- Traceability ---
55
+
56
+ export type ConversationStatus = "active" | "completed" | "archived";
57
+
58
+ export interface Participant {
59
+ agent_id: string;
60
+ role?: string;
61
+ joined_at: string;
62
+ }
63
+
64
+ export interface Conversation {
65
+ id: string;
66
+ scope: string;
67
+ subject?: string;
68
+ status: ConversationStatus;
69
+ participants: Participant[];
70
+ metadata: Record<string, unknown>;
71
+ created_at: string;
72
+ updated_at: string;
73
+ }
74
+
75
+ export type ContentType = "text" | "data" | "event" | "reference" | string;
76
+
77
+ export interface Turn {
78
+ id: string;
79
+ conversation_id: string;
80
+ participant_id: string;
81
+ source_message_id?: string;
82
+ content_type: ContentType;
83
+ content: MessageContent;
84
+ thread_id?: string;
85
+ in_reply_to?: string;
86
+ created_at: string;
87
+ }
88
+
89
+ export interface Thread {
90
+ id: string;
91
+ conversation_id: string;
92
+ root_turn_id: string;
93
+ parent_thread_id?: string;
94
+ subject?: string;
95
+ created_at: string;
96
+ }
97
+
98
+ // --- IPC Protocol ---
99
+
100
+ export interface IpcSendCommand {
101
+ action: "send";
102
+ from: string;
103
+ to: string | string[] | { agent_id: string; kind?: RecipientKind }[];
104
+ payload: unknown;
105
+ scope?: string;
106
+ threadTag?: string;
107
+ inReplyTo?: string;
108
+ importance?: Importance;
109
+ meta?: Record<string, unknown>;
110
+ }
111
+
112
+ export interface IpcEmitCommand {
113
+ action: "emit";
114
+ event: unknown;
115
+ meta?: Record<string, unknown>;
116
+ }
117
+
118
+ export interface IpcNotifyCommand {
119
+ action: "notify";
120
+ event: {
121
+ type: string;
122
+ agent?: {
123
+ agentId: string;
124
+ name?: string;
125
+ role?: string;
126
+ scopes?: string[];
127
+ metadata?: Record<string, unknown>;
128
+ };
129
+ agentId?: string;
130
+ reason?: string;
131
+ [key: string]: unknown;
132
+ };
133
+ }
134
+
135
+ export interface IpcCheckInboxCommand {
136
+ action: "check_inbox";
137
+ agentId?: string;
138
+ scope?: string;
139
+ unreadOnly?: boolean;
140
+ clear?: boolean;
141
+ }
142
+
143
+ export interface IpcPingCommand {
144
+ action: "ping";
145
+ }
146
+
147
+ export type IpcCommand =
148
+ | IpcSendCommand
149
+ | IpcEmitCommand
150
+ | IpcNotifyCommand
151
+ | IpcCheckInboxCommand
152
+ | IpcPingCommand;
153
+
154
+ export interface IpcResponse {
155
+ ok: boolean;
156
+ messageId?: string;
157
+ messages?: Message[];
158
+ error?: string;
159
+ pid?: number;
160
+ }
161
+
162
+ // --- Federation ---
163
+
164
+ export type WarmAgentStatus = "active" | "away" | "expired";
165
+
166
+ export interface AgentRegistryConfig {
167
+ /** Time after disconnect before away → expired (default: 60000ms) */
168
+ gracePeriodMs: number;
169
+ /** How long to keep expired entries for audit (default: 3600000ms) */
170
+ retainExpiredMs: number;
171
+ /** Flush queued messages when agent reconnects (default: true) */
172
+ requeueOnReconnect: boolean;
173
+ }
174
+
175
+ export interface FederatedAddress {
176
+ agent?: string;
177
+ system?: string;
178
+ scope?: string;
179
+ }
180
+
181
+ export interface FederationPeerConfig {
182
+ systemId: string;
183
+ url: string;
184
+ auth?: FederationAuth;
185
+ exposure?: ExposurePolicy;
186
+ }
187
+
188
+ export interface FederationAuth {
189
+ method: "bearer" | "api-key" | "mtls" | "did:wba" | "none";
190
+ token?: string;
191
+ key?: string;
192
+ }
193
+
194
+ export type ExposureLevel = "none" | "gateway" | "tagged" | "all";
195
+
196
+ export interface ExposurePolicy {
197
+ agents: ExposureLevel;
198
+ scopes?: string[];
199
+ events?: ExposureLevel;
200
+ }
201
+
202
+ export interface FederationLink {
203
+ peerId: string;
204
+ sessionId: string;
205
+ status: "connected" | "disconnected" | "authenticating";
206
+ exposure: ExposurePolicy;
207
+ url: string;
208
+ connectedAt?: string;
209
+ }
210
+
211
+ export interface SystemId {
212
+ id: string;
213
+ source: "config" | "map" | "auto";
214
+ }
215
+
216
+ export interface FederationRoutingConfig {
217
+ strategy: "table" | "broadcast" | "hierarchical";
218
+ /** Routing entry TTL in ms (default: 300000) */
219
+ tableTTL?: number;
220
+ /** Re-query peers on cache miss before failing (default: true) */
221
+ refreshOnMiss?: boolean;
222
+ /** Ms to wait for first responder in broadcast mode (default: 5000) */
223
+ broadcastTimeout?: number;
224
+ /** System IDs of upstream hubs for hierarchical routing */
225
+ upstream?: string[];
226
+ }
227
+
228
+ export interface DeliveryQueueConfig {
229
+ persistence: "memory" | "sqlite";
230
+ /** Max age before dropping in ms (default: 86400000 / 24h) */
231
+ maxTTL: number;
232
+ /** Max messages per destination (default: 10000) */
233
+ maxQueueSize: number;
234
+ retryStrategy: "exponential" | "fixed";
235
+ /** Base retry interval in ms (default: 1000) */
236
+ retryBaseInterval: number;
237
+ /** Max retries, 0 = unlimited until TTL (default: 0) */
238
+ retryMaxAttempts: number;
239
+ /** Drain queue when connection restored (default: true) */
240
+ flushOnReconnect: boolean;
241
+ overflow: "drop-oldest" | "drop-newest" | "reject-new";
242
+ }
243
+
244
+ export interface FederationTrustPolicy {
245
+ /** System IDs or URLs allowed to connect (Layer 1 — implemented) */
246
+ allowedServers: string[];
247
+ /** Local scope → allowed remote scopes (Layer 2 — stub) */
248
+ scopePermissions: Record<string, string[]>;
249
+ /** Require tokens for cross-server delivery (Layer 3 — stub) */
250
+ requireAuth: boolean;
251
+ authMethod?: "bearer" | "api-key" | "mtls" | "did:wba";
252
+ authTokens?: Record<string, string>;
253
+ }
254
+
255
+ export interface FederationConfig {
256
+ systemId?: string;
257
+ peers?: FederationPeerConfig[];
258
+ routing?: Partial<FederationRoutingConfig>;
259
+ deliveryQueue?: Partial<DeliveryQueueConfig>;
260
+ trust?: Partial<FederationTrustPolicy>;
261
+ registry?: Partial<AgentRegistryConfig>;
262
+ }
263
+
264
+ export interface RoutingEntry {
265
+ agentId: string;
266
+ peerId: string;
267
+ lastSeen: string;
268
+ status: WarmAgentStatus;
269
+ }
270
+
271
+ export interface QueuedMessage {
272
+ id: string;
273
+ peerId: string;
274
+ message: Message;
275
+ enqueuedAt: string;
276
+ attempts: number;
277
+ lastAttempt?: string;
278
+ nextRetry?: string;
279
+ }
280
+
281
+ // --- Config ---
282
+
283
+ export interface InboxConfig {
284
+ socketPath?: string;
285
+ scope?: string;
286
+ map?: {
287
+ enabled: boolean;
288
+ server?: string;
289
+ scope?: string;
290
+ systemId?: string;
291
+ auth?: {
292
+ token?: string;
293
+ param?: string;
294
+ };
295
+ };
296
+ federation?: FederationConfig;
297
+ }
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ parseAddress,
4
+ formatAddress,
5
+ isRemoteAddress,
6
+ isBroadcastAddress,
7
+ } from "../../src/federation/address.js";
8
+
9
+ describe("parseAddress", () => {
10
+ it("should parse local agent ID (no @)", () => {
11
+ expect(parseAddress("agent-alpha")).toEqual({ agent: "agent-alpha" });
12
+ });
13
+
14
+ it("should parse federated address (agent@system)", () => {
15
+ expect(parseAddress("agent-beta@backend-team")).toEqual({
16
+ agent: "agent-beta",
17
+ system: "backend-team",
18
+ });
19
+ });
20
+
21
+ it("should parse broadcast address (@system)", () => {
22
+ expect(parseAddress("@backend-team")).toEqual({
23
+ agent: undefined,
24
+ system: "backend-team",
25
+ });
26
+ });
27
+
28
+ it("should parse address with scope (agent@system/scope)", () => {
29
+ expect(parseAddress("agent-gamma@ml-team/training")).toEqual({
30
+ agent: "agent-gamma",
31
+ system: "ml-team",
32
+ scope: "training",
33
+ });
34
+ });
35
+
36
+ it("should parse broadcast with scope (@system/scope)", () => {
37
+ expect(parseAddress("@ml-team/training")).toEqual({
38
+ agent: undefined,
39
+ system: "ml-team",
40
+ scope: "training",
41
+ });
42
+ });
43
+ });
44
+
45
+ describe("formatAddress", () => {
46
+ it("should format local address", () => {
47
+ expect(formatAddress({ agent: "agent-alpha" })).toBe("agent-alpha");
48
+ });
49
+
50
+ it("should format federated address", () => {
51
+ expect(
52
+ formatAddress({ agent: "agent-beta", system: "backend-team" })
53
+ ).toBe("agent-beta@backend-team");
54
+ });
55
+
56
+ it("should format broadcast address", () => {
57
+ expect(formatAddress({ system: "backend-team" })).toBe("@backend-team");
58
+ });
59
+
60
+ it("should format address with scope", () => {
61
+ expect(
62
+ formatAddress({
63
+ agent: "agent-gamma",
64
+ system: "ml-team",
65
+ scope: "training",
66
+ })
67
+ ).toBe("agent-gamma@ml-team/training");
68
+ });
69
+ });
70
+
71
+ describe("isRemoteAddress", () => {
72
+ it("should return false for local address", () => {
73
+ expect(isRemoteAddress({ agent: "local-agent" })).toBe(false);
74
+ });
75
+
76
+ it("should return true for federated address", () => {
77
+ expect(
78
+ isRemoteAddress({ agent: "remote-agent", system: "other-system" })
79
+ ).toBe(true);
80
+ });
81
+
82
+ it("should return true for broadcast address", () => {
83
+ expect(isRemoteAddress({ system: "other-system" })).toBe(true);
84
+ });
85
+ });
86
+
87
+ describe("isBroadcastAddress", () => {
88
+ it("should return false for local address", () => {
89
+ expect(isBroadcastAddress({ agent: "local-agent" })).toBe(false);
90
+ });
91
+
92
+ it("should return false for targeted federated address", () => {
93
+ expect(
94
+ isBroadcastAddress({ agent: "remote-agent", system: "other-system" })
95
+ ).toBe(false);
96
+ });
97
+
98
+ it("should return true for system-only address", () => {
99
+ expect(isBroadcastAddress({ system: "other-system" })).toBe(true);
100
+ });
101
+ });