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.
- package/CLAUDE.md +44 -7
- package/README.md +67 -24
- package/dist/cli.d.ts +20 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +89 -0
- package/dist/cli.js.map +1 -0
- 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 +10 -2
- package/src/cli.ts +94 -0
- 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,157 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MapConnection,
|
|
3
|
+
IncomingMapMessage,
|
|
4
|
+
} from "../map/map-client.js";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Minimal type stubs for agentic-mesh.
|
|
8
|
+
// We only reference the subset of MeshContext / IMessageChannel that
|
|
9
|
+
// MeshTransport actually uses, so agent-inbox compiles without importing
|
|
10
|
+
// the full agentic-mesh package (it's an optional peer dep).
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** Subset of agentic-mesh PeerInfo used by MeshTransport. */
|
|
14
|
+
export interface MeshPeerInfo {
|
|
15
|
+
id: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
groups: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Subset of agentic-mesh IMessageChannel<T>. */
|
|
21
|
+
export interface MeshChannel<T> {
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly isOpen: boolean;
|
|
24
|
+
open(): Promise<void>;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
send(peerId: string, message: T): boolean;
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
on(event: string | symbol, listener: (...args: any[]) => void): this;
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
30
|
+
off(event: string | symbol, listener: (...args: any[]) => void): this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Subset of agentic-mesh MeshContext used by MeshTransport. */
|
|
34
|
+
export interface MeshContextLike {
|
|
35
|
+
createChannel<T>(name: string): MeshChannel<T>;
|
|
36
|
+
_getPeerId(): string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Wire format
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Payload exchanged over the mesh channel between agent-inbox instances.
|
|
45
|
+
* Flat structure that maps 1-to-1 with IncomingMapMessage.
|
|
46
|
+
*/
|
|
47
|
+
export interface InboxWireMessage {
|
|
48
|
+
to?: { agentId?: string; scope?: string };
|
|
49
|
+
payload: unknown;
|
|
50
|
+
from: string;
|
|
51
|
+
timestamp?: string;
|
|
52
|
+
meta?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// MeshTransport
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
/** Default channel name used for agent-inbox federation traffic.
|
|
60
|
+
* Uses the "proto:" prefix to distinguish protocol channels from
|
|
61
|
+
* application channels on the same mesh. */
|
|
62
|
+
export const DEFAULT_CHANNEL_NAME = "proto:agent-inbox";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Implements the MapConnection interface over an agentic-mesh MessageChannel.
|
|
66
|
+
*
|
|
67
|
+
* Each instance represents a connection to a single remote peer. Multiple
|
|
68
|
+
* MeshTransport instances share the same underlying channel (one channel
|
|
69
|
+
* per mesh, not per peer) and filter incoming messages by remotePeerId.
|
|
70
|
+
*/
|
|
71
|
+
export class MeshTransport implements MapConnection {
|
|
72
|
+
private channel: MeshChannel<InboxWireMessage>;
|
|
73
|
+
private handlers: ((msg: IncomingMapMessage) => void)[] = [];
|
|
74
|
+
private messageListener: ((message: InboxWireMessage, from: MeshPeerInfo) => void) | null = null;
|
|
75
|
+
private closed = false;
|
|
76
|
+
readonly systemName: string;
|
|
77
|
+
|
|
78
|
+
constructor(
|
|
79
|
+
private mesh: MeshContextLike,
|
|
80
|
+
private localPeerId: string,
|
|
81
|
+
private remotePeerId: string,
|
|
82
|
+
channelName: string = DEFAULT_CHANNEL_NAME
|
|
83
|
+
) {
|
|
84
|
+
this.systemName = remotePeerId;
|
|
85
|
+
this.channel = mesh.createChannel<InboxWireMessage>(channelName);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Open the underlying channel and start listening for messages.
|
|
90
|
+
* Must be called before send() or onMessage().
|
|
91
|
+
*/
|
|
92
|
+
async open(): Promise<void> {
|
|
93
|
+
if (!this.channel.isOpen) {
|
|
94
|
+
await this.channel.open();
|
|
95
|
+
}
|
|
96
|
+
this.messageListener = (message: InboxWireMessage, from: MeshPeerInfo) => {
|
|
97
|
+
if (this.closed) return;
|
|
98
|
+
// Only process messages from our remote peer
|
|
99
|
+
if (from.id !== this.remotePeerId) return;
|
|
100
|
+
const incoming = wireToIncoming(message);
|
|
101
|
+
for (const handler of this.handlers) {
|
|
102
|
+
handler(incoming);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
this.channel.on("message", this.messageListener);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async send(
|
|
109
|
+
to: { agentId?: string; scope?: string },
|
|
110
|
+
payload: unknown,
|
|
111
|
+
meta?: Record<string, unknown>
|
|
112
|
+
): Promise<void> {
|
|
113
|
+
if (this.closed) {
|
|
114
|
+
throw new Error("MeshTransport is closed");
|
|
115
|
+
}
|
|
116
|
+
const wire: InboxWireMessage = {
|
|
117
|
+
to,
|
|
118
|
+
payload,
|
|
119
|
+
from: this.localPeerId,
|
|
120
|
+
timestamp: new Date().toISOString(),
|
|
121
|
+
meta,
|
|
122
|
+
};
|
|
123
|
+
const sent = this.channel.send(this.remotePeerId, wire);
|
|
124
|
+
if (!sent) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Failed to send to peer "${this.remotePeerId}" — peer may be offline`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
onMessage(handler: (msg: IncomingMapMessage) => void): void {
|
|
132
|
+
this.handlers.push(handler);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async disconnect(): Promise<void> {
|
|
136
|
+
this.closed = true;
|
|
137
|
+
if (this.messageListener) {
|
|
138
|
+
this.channel.off("message", this.messageListener);
|
|
139
|
+
this.messageListener = null;
|
|
140
|
+
}
|
|
141
|
+
this.handlers = [];
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// Helpers
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
function wireToIncoming(wire: InboxWireMessage): IncomingMapMessage {
|
|
150
|
+
return {
|
|
151
|
+
from: wire.from,
|
|
152
|
+
to: wire.to,
|
|
153
|
+
payload: wire.payload,
|
|
154
|
+
timestamp: wire.timestamp,
|
|
155
|
+
meta: wire.meta,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -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)
|