@masons/agent-network 0.3.9 → 0.4.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/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +65 -73
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +32 -0
- package/dist/conversation-manager.d.ts +91 -0
- package/dist/conversation-manager.d.ts.map +1 -0
- package/dist/conversation-manager.js +189 -0
- package/dist/plugin.js +2 -2
- package/dist/session-lifecycle.d.ts +89 -0
- package/dist/session-lifecycle.d.ts.map +1 -0
- package/dist/session-lifecycle.js +348 -0
- package/dist/tools.d.ts +8 -5
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +63 -115
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/skills/agent-network/SKILL.md +12 -22
package/dist/channel.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAc9D,UAAU,WAAW;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,UAAU,oBAAoB,CAAC,CAAC;IAC9B,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,EAAE,CAAC;IACvD,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC;IACpE,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;IACjE,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;CAC/D;AAED,UAAU,sBAAsB;IAC9B,EAAE,EAAE,OAAO,CAAC;CACb;AAED,UAAU,sBAAsB;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,sBAAsB;IAC9B,YAAY,EAAE,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC9C,QAAQ,CAAC,CAAC,GAAG,EAAE,sBAAsB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACzE;AAED,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAsDD,UAAU,qBAAqB,CAAC,CAAC,GAAG,OAAO;IACzC,YAAY,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/D,WAAW,CAAC,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D;AAED,UAAU,yBAAyB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,YAAY,EAAE,mBAAmB,CAAC;IAClC,MAAM,EAAE,oBAAoB,CAAC,mBAAmB,CAAC,CAAC;IAClD,QAAQ,EAAE,sBAAsB,CAAC;IACjC,OAAO,EAAE,qBAAqB,CAAC,mBAAmB,CAAC,CAAC;CACrD;AAqDD,eAAO,MAAM,mBAAmB,EAAE,yBA4VjC,CAAC"}
|
package/dist/channel.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import createDebug from "debug";
|
|
3
|
-
import { clearConnectorClient, extractNetworkConfig, initConnectorClient, initToolConfig, requirePluginRuntime, } from "./config.js";
|
|
3
|
+
import { clearConnectorClient, clearConversationManager, extractNetworkConfig, initConnectorClient, initConversationManager, initToolConfig, requirePluginRuntime, } from "./config.js";
|
|
4
4
|
import { ConnectorClient } from "./connector-client.js";
|
|
5
|
+
import { ConversationManager } from "./conversation-manager.js";
|
|
5
6
|
import { checkForUpdate } from "./update-check.js";
|
|
6
7
|
const dbg = createDebug("agent-network:channel");
|
|
7
8
|
// --- Sender identity ---
|
|
@@ -25,6 +26,20 @@ function deriveSenderName(event) {
|
|
|
25
26
|
const parts = trimmed.split("/");
|
|
26
27
|
return parts[parts.length - 1] || from;
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Extract the Connector host from a connectorUrl.
|
|
31
|
+
* `wss://preview.masons.ai/gw` -> `preview.masons.ai`
|
|
32
|
+
*/
|
|
33
|
+
function extractConnectorHost(connectorUrl) {
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(connectorUrl);
|
|
36
|
+
return url.hostname;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Fallback: strip protocol and path
|
|
40
|
+
return connectorUrl.replace(/^wss?:\/\//, "").replace(/\/.*$/, "");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
28
43
|
const accounts = new Map();
|
|
29
44
|
// --- Channel Plugin ---
|
|
30
45
|
export const agentNetworkChannel = {
|
|
@@ -84,25 +99,14 @@ export const agentNetworkChannel = {
|
|
|
84
99
|
outbound: {
|
|
85
100
|
deliveryMode: "direct",
|
|
86
101
|
async sendText(ctx) {
|
|
87
|
-
// ctx.recipient is
|
|
88
|
-
//
|
|
102
|
+
// ctx.recipient is the MSTP address or sessionId fallback (senderId from inbound dispatch).
|
|
103
|
+
// ConversationManager.send() handles session management transparently.
|
|
89
104
|
const state = accounts.get(ctx.accountId);
|
|
90
105
|
if (!state)
|
|
91
106
|
return { ok: false };
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (!sessionId && state.sessions.has(ctx.recipient)) {
|
|
96
|
-
sessionId = ctx.recipient;
|
|
97
|
-
}
|
|
98
|
-
if (!sessionId || !state.sessions.has(sessionId)) {
|
|
99
|
-
return { ok: false };
|
|
100
|
-
}
|
|
101
|
-
const sent = state.client.sendMessage(sessionId, ctx.text, {
|
|
102
|
-
contentType: "text",
|
|
103
|
-
});
|
|
104
|
-
dbg("sendText recipient=%s sessionId=%s contentLength=%d sent=%s", ctx.recipient, sessionId, ctx.text.length, sent);
|
|
105
|
-
return { ok: sent };
|
|
107
|
+
const result = await state.conversationManager.send(ctx.recipient, ctx.text);
|
|
108
|
+
dbg("sendText recipient=%s status=%s contentLength=%d", ctx.recipient, result.status, ctx.text.length);
|
|
109
|
+
return { ok: result.status === "sent" };
|
|
106
110
|
},
|
|
107
111
|
},
|
|
108
112
|
gateway: {
|
|
@@ -110,10 +114,11 @@ export const agentNetworkChannel = {
|
|
|
110
114
|
dbg("starting account %s", ctx.accountId);
|
|
111
115
|
const { connectorUrl, token } = ctx.account;
|
|
112
116
|
const client = new ConnectorClient(connectorUrl, token);
|
|
117
|
+
const connectorHost = extractConnectorHost(connectorUrl);
|
|
118
|
+
const conversationManager = new ConversationManager(client, connectorHost);
|
|
113
119
|
const state = {
|
|
114
120
|
client,
|
|
115
|
-
|
|
116
|
-
activeSession: new Map(),
|
|
121
|
+
conversationManager,
|
|
117
122
|
};
|
|
118
123
|
accounts.set(ctx.accountId, state);
|
|
119
124
|
// --- Inbound message routing (4-step channelRuntime dispatch) ---
|
|
@@ -143,19 +148,22 @@ export const agentNetworkChannel = {
|
|
|
143
148
|
dbg("channelRuntime not available, dropping message sessionId=%s", event.sessionId);
|
|
144
149
|
return;
|
|
145
150
|
}
|
|
146
|
-
// Resolve sender identity
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
//
|
|
155
|
-
|
|
151
|
+
// Resolve sender identity via ConversationManager.
|
|
152
|
+
// The session_created handler registered the inbound session with
|
|
153
|
+
// the ConversationManager, so we can look up the contact handle.
|
|
154
|
+
const senderAddress = conversationManager.getAddressBySessionId(event.sessionId);
|
|
155
|
+
// Determine if the address is a real MSTP address or a sessionId fallback.
|
|
156
|
+
const isRealAddress = typeof senderAddress === "string" &&
|
|
157
|
+
(senderAddress.startsWith("mstps://") ||
|
|
158
|
+
senderAddress.startsWith("mstp://"));
|
|
159
|
+
// senderId for OpenClaw routing: use MSTP address if available,
|
|
160
|
+
// fall back to sessionId for unknown sessions / direct MSTP clients.
|
|
161
|
+
const senderId = isRealAddress ? senderAddress : event.sessionId;
|
|
162
|
+
// Derive sender name from the MSTP address, or "Unknown Agent" for fallback.
|
|
163
|
+
const senderName = isRealAddress
|
|
156
164
|
? deriveSenderName({
|
|
157
165
|
...event,
|
|
158
|
-
metadata: { ...event.metadata, from:
|
|
166
|
+
metadata: { ...event.metadata, from: senderAddress },
|
|
159
167
|
})
|
|
160
168
|
: deriveSenderName(event);
|
|
161
169
|
try {
|
|
@@ -205,19 +213,11 @@ export const agentNetworkChannel = {
|
|
|
205
213
|
const text = payload.text;
|
|
206
214
|
if (!text)
|
|
207
215
|
return;
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
// Session may have ended while LLM was processing.
|
|
215
|
-
if (!deliverSessionId || !state.sessions.has(deliverSessionId))
|
|
216
|
-
return;
|
|
217
|
-
const sent = state.client.sendMessage(deliverSessionId, text, {
|
|
218
|
-
contentType: "text",
|
|
219
|
-
});
|
|
220
|
-
dbg("deliver reply address=%s sessionId=%s contentLength=%d sent=%s", senderId, deliverSessionId, text.length, sent);
|
|
216
|
+
// Use ConversationManager to send the reply.
|
|
217
|
+
// Pass the MSTP address (senderId) directly so the CM
|
|
218
|
+
// can match the registered conversation entry.
|
|
219
|
+
const deliverResult = await conversationManager.send(senderId, text);
|
|
220
|
+
dbg("deliver reply senderId=%s status=%s contentLength=%d", senderId, deliverResult.status, text.length);
|
|
221
221
|
},
|
|
222
222
|
onError: (err) => {
|
|
223
223
|
console.error(`[agent-network:${ctx.accountId}] dispatch error:`, err);
|
|
@@ -229,45 +229,37 @@ export const agentNetworkChannel = {
|
|
|
229
229
|
console.error(`[agent-network:${ctx.accountId}] inbound dispatch failed sessionId=${event.sessionId}:`, err);
|
|
230
230
|
}
|
|
231
231
|
});
|
|
232
|
-
// ---
|
|
232
|
+
// --- Inbound session registration ---
|
|
233
|
+
// When a remote agent creates a session (direction=inbound),
|
|
234
|
+
// register it with the ConversationManager.
|
|
233
235
|
client.on("session_created", (event) => {
|
|
234
236
|
dbg("session created sessionId=%s direction=%s", event.sessionId, event.direction);
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
client.on("session_ended", (event) => {
|
|
246
|
-
dbg("session ended sessionId=%s", event.sessionId);
|
|
247
|
-
const sessionMeta = state.sessions.get(event.sessionId);
|
|
248
|
-
state.sessions.delete(event.sessionId);
|
|
249
|
-
// Only clear reverse index if this session was the active one.
|
|
250
|
-
// If a newer session replaced it, the index already points elsewhere.
|
|
251
|
-
if (sessionMeta?.remoteAddress) {
|
|
252
|
-
const currentActive = state.activeSession.get(sessionMeta.remoteAddress);
|
|
253
|
-
if (currentActive === event.sessionId) {
|
|
254
|
-
state.activeSession.delete(sessionMeta.remoteAddress);
|
|
237
|
+
if (event.direction === "inbound") {
|
|
238
|
+
const from = event.metadata?.from;
|
|
239
|
+
const remoteAddress = typeof from === "string" && from.length > 0 ? from : null;
|
|
240
|
+
if (remoteAddress) {
|
|
241
|
+
const contact = deriveSenderName(event);
|
|
242
|
+
conversationManager.registerInbound(event.sessionId, contact, remoteAddress);
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// No address metadata — register with sessionId as fallback
|
|
246
|
+
conversationManager.registerInbound(event.sessionId, event.sessionId, event.sessionId);
|
|
255
247
|
}
|
|
256
248
|
}
|
|
249
|
+
// Outbound sessions are handled by SessionLifecycle internally
|
|
250
|
+
// (matched by requestId in the createSession flow).
|
|
257
251
|
});
|
|
252
|
+
// --- Session ended + disconnect ---
|
|
253
|
+
// SessionLifecycle handles session state transitions (session_ended, disconnected)
|
|
254
|
+
// via its own event subscriptions. No CM calls needed here.
|
|
258
255
|
// --- Error handling ---
|
|
259
256
|
// Must register before connect() — unhandled "error" events crash the process.
|
|
260
257
|
client.on("error", (err) => {
|
|
261
258
|
console.error(`[agent-network:${ctx.accountId}]`, err.message);
|
|
262
259
|
});
|
|
263
|
-
// --- Disconnect cleanup ---
|
|
264
|
-
client.on("disconnected", () => {
|
|
265
|
-
dbg("disconnected, cleared %d sessions", state.sessions.size);
|
|
266
|
-
state.sessions.clear();
|
|
267
|
-
state.activeSession.clear();
|
|
268
|
-
});
|
|
269
260
|
// --- Abort signal ---
|
|
270
261
|
ctx.abortSignal.addEventListener("abort", () => {
|
|
262
|
+
clearConversationManager();
|
|
271
263
|
clearConnectorClient(); // idempotent — stopAccount() may also call
|
|
272
264
|
client.disconnect();
|
|
273
265
|
}, { once: true });
|
|
@@ -280,8 +272,9 @@ export const agentNetworkChannel = {
|
|
|
280
272
|
// --- Connect ---
|
|
281
273
|
await client.connect();
|
|
282
274
|
dbg("connected account=%s", ctx.accountId);
|
|
283
|
-
// --- Expose client to tools (only after WS is connected) ---
|
|
275
|
+
// --- Expose client and conversation manager to tools (only after WS is connected) ---
|
|
284
276
|
initConnectorClient(client);
|
|
277
|
+
initConversationManager(conversationManager);
|
|
285
278
|
// Park the Promise — DO NOT resolve/return.
|
|
286
279
|
// Resolution signals "account stopped" to OpenClaw's orchestrator,
|
|
287
280
|
// triggering auto-restart with exponential backoff (crash-loop).
|
|
@@ -299,11 +292,10 @@ export const agentNetworkChannel = {
|
|
|
299
292
|
dbg("stopping account %s", ctx.accountId);
|
|
300
293
|
const state = accounts.get(ctx.accountId);
|
|
301
294
|
if (state) {
|
|
295
|
+
clearConversationManager();
|
|
302
296
|
clearConnectorClient();
|
|
303
297
|
state.client.removeAllListeners();
|
|
304
298
|
state.client.disconnect();
|
|
305
|
-
state.sessions.clear();
|
|
306
|
-
state.activeSession.clear();
|
|
307
299
|
accounts.delete(ctx.accountId);
|
|
308
300
|
}
|
|
309
301
|
},
|
package/dist/config.d.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
*/
|
|
17
17
|
import type { ConnectorClient } from "./connector-client.js";
|
|
18
|
+
import type { ConversationManager } from "./conversation-manager.js";
|
|
18
19
|
import { type PlatformClientConfig } from "./platform-client.js";
|
|
19
20
|
/**
|
|
20
21
|
* Extract the `channels.agent-network` section from the full OpenClaw config.
|
|
@@ -49,6 +50,24 @@ export declare function initConnectorClient(client: ConnectorClient): void;
|
|
|
49
50
|
* operating on a disconnected client.
|
|
50
51
|
*/
|
|
51
52
|
export declare function clearConnectorClient(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Inject the live ConversationManager after WebSocket connection is established.
|
|
55
|
+
*
|
|
56
|
+
* Called by `startAccount()` AFTER `client.connect()` resolves — ensures
|
|
57
|
+
* tools always get a manager backed by a connected client.
|
|
58
|
+
*/
|
|
59
|
+
export declare function initConversationManager(manager: ConversationManager): void;
|
|
60
|
+
/**
|
|
61
|
+
* Clear the stored ConversationManager reference.
|
|
62
|
+
*
|
|
63
|
+
* Called by `stopAccount()` before disconnecting — prevents tools from
|
|
64
|
+
* operating on a stale manager.
|
|
65
|
+
*/
|
|
66
|
+
export declare function clearConversationManager(): void;
|
|
67
|
+
/**
|
|
68
|
+
* Get the live ConversationManager. Throws if not initialized.
|
|
69
|
+
*/
|
|
70
|
+
export declare function requireConversationManager(): ConversationManager;
|
|
52
71
|
/**
|
|
53
72
|
* Store the OpenClaw PluginRuntime for inbound message dispatch.
|
|
54
73
|
*
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAKH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAEL,KAAK,oBAAoB,EAC1B,MAAM,sBAAsB,CAAC;AA4B9B;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAMhC;AAMD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAsBjE;AAMD;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,IAAI,CAOjE;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,IAAI,CAE3C;AAMD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAE1E;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAED;;GAEG;AACH,wBAAgB,0BAA0B,IAAI,mBAAmB,CAOhE;AAMD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAExD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,CAO9C;AAMD;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,oBAAoB,CAE5D;AAED;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAKtC;AAED;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,GAAG,IAAI,CAEhD;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,IAAI,eAAe,CAOxD;AAMD,+EAA+E;AAC/E,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAgDD,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,kBAAkB,EACzB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CA2Bf;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAKrE;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAQvD;AAMD;;;;;GAKG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAKvD;AAED;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,IAAI,CAAC,CAKzD;AAMD,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,6DAA6D;IAC7D,cAAc,EAAE,OAAO,CAAC;IACxB,wDAAwD;IACxD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,4EAA4E;IAC5E,YAAY,EAAE,OAAO,CAAC;CACvB;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,YAAY,CAAC,CA2BhE;AAMD,uDAAuD;AACvD,wBAAgB,gBAAgB,IAAI,IAAI,CAOvC"}
|
package/dist/config.js
CHANGED
|
@@ -32,6 +32,7 @@ let storedApiKey = null;
|
|
|
32
32
|
let storedPendingTarget = null;
|
|
33
33
|
// Runtime state (live connection — set by startAccount, cleared by stopAccount)
|
|
34
34
|
let storedConnectorClient = null;
|
|
35
|
+
let storedConversationManager = null;
|
|
35
36
|
// PluginRuntime state (set by register() in plugin.ts, used by channel.ts)
|
|
36
37
|
// Typed as unknown to avoid circular imports — channel.ts casts to PluginRuntime.
|
|
37
38
|
let storedPluginRuntime = null;
|
|
@@ -109,6 +110,36 @@ export function clearConnectorClient() {
|
|
|
109
110
|
storedConnectorClient = null;
|
|
110
111
|
}
|
|
111
112
|
// ---------------------------------------------------------------------------
|
|
113
|
+
// ConversationManager injection (called by startAccount() after connect)
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
/**
|
|
116
|
+
* Inject the live ConversationManager after WebSocket connection is established.
|
|
117
|
+
*
|
|
118
|
+
* Called by `startAccount()` AFTER `client.connect()` resolves — ensures
|
|
119
|
+
* tools always get a manager backed by a connected client.
|
|
120
|
+
*/
|
|
121
|
+
export function initConversationManager(manager) {
|
|
122
|
+
storedConversationManager = manager;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Clear the stored ConversationManager reference.
|
|
126
|
+
*
|
|
127
|
+
* Called by `stopAccount()` before disconnecting — prevents tools from
|
|
128
|
+
* operating on a stale manager.
|
|
129
|
+
*/
|
|
130
|
+
export function clearConversationManager() {
|
|
131
|
+
storedConversationManager = null;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get the live ConversationManager. Throws if not initialized.
|
|
135
|
+
*/
|
|
136
|
+
export function requireConversationManager() {
|
|
137
|
+
if (!storedConversationManager) {
|
|
138
|
+
throw new Error("Not connected to the agent network. Wait for the connection to be established.");
|
|
139
|
+
}
|
|
140
|
+
return storedConversationManager;
|
|
141
|
+
}
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
112
143
|
// PluginRuntime injection (called by register() in plugin.ts)
|
|
113
144
|
// ---------------------------------------------------------------------------
|
|
114
145
|
/**
|
|
@@ -349,5 +380,6 @@ export function _resetForTesting() {
|
|
|
349
380
|
storedApiKey = null;
|
|
350
381
|
storedPendingTarget = null;
|
|
351
382
|
storedConnectorClient = null;
|
|
383
|
+
storedConversationManager = null;
|
|
352
384
|
storedPluginRuntime = null;
|
|
353
385
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation Manager — maps contacts to conversations.
|
|
3
|
+
*
|
|
4
|
+
* Owns the identity-based API that the Tool Interface and Inbound Dispatcher
|
|
5
|
+
* call. Internally delegates session management to SessionLifecycle.
|
|
6
|
+
*
|
|
7
|
+
* Contact resolution: handle -> `mstps://${connectorHost}/${handle}`
|
|
8
|
+
* Internal key: MSTP address (same as SessionLifecycle).
|
|
9
|
+
* One session per contact, last-write-wins (D2).
|
|
10
|
+
* In-memory only, no persistence (D3).
|
|
11
|
+
*
|
|
12
|
+
* @see docs/openclaw/session-abstraction-system-design.md §4.1
|
|
13
|
+
*/
|
|
14
|
+
import type { ConnectorClient } from "./connector-client.js";
|
|
15
|
+
import { SessionLifecycle } from "./session-lifecycle.js";
|
|
16
|
+
export interface SendResult {
|
|
17
|
+
status: "sent" | "failed";
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface ConversationEntry {
|
|
21
|
+
contact: string;
|
|
22
|
+
address: string;
|
|
23
|
+
lastMessageAt: number;
|
|
24
|
+
initiatedBy: "local" | "remote";
|
|
25
|
+
}
|
|
26
|
+
export interface ConversationSummary {
|
|
27
|
+
contact: string;
|
|
28
|
+
address: string;
|
|
29
|
+
active: boolean;
|
|
30
|
+
lastMessageAt: number;
|
|
31
|
+
initiatedBy: "local" | "remote";
|
|
32
|
+
}
|
|
33
|
+
export declare class ConversationManager {
|
|
34
|
+
readonly sessionLifecycle: SessionLifecycle;
|
|
35
|
+
private readonly client;
|
|
36
|
+
private readonly connectorHost;
|
|
37
|
+
/** Conversations keyed by MSTP address */
|
|
38
|
+
private readonly conversations;
|
|
39
|
+
constructor(client: ConnectorClient, connectorHost: string);
|
|
40
|
+
/**
|
|
41
|
+
* Send a message to a contact. Session management is transparent.
|
|
42
|
+
* Creates a session automatically if none exists (D1 — sync-but-transparent).
|
|
43
|
+
*
|
|
44
|
+
* @param contact - Handle or MSTP address of the target agent.
|
|
45
|
+
* @param content - Message content to send.
|
|
46
|
+
*/
|
|
47
|
+
send(contact: string, content: string): Promise<SendResult>;
|
|
48
|
+
/**
|
|
49
|
+
* Register an inbound session (remote agent initiated).
|
|
50
|
+
* Called by the channel adapter on SESSION_CREATED with direction=inbound.
|
|
51
|
+
*/
|
|
52
|
+
registerInbound(sessionId: string, contact: string, address: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a sessionId to a contact handle.
|
|
55
|
+
* Used by the inbound dispatcher to derive sender identity from a message event.
|
|
56
|
+
*/
|
|
57
|
+
getContactBySessionId(sessionId: string): string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a sessionId to an MSTP address.
|
|
60
|
+
* Used by the channel adapter for reverse lookup.
|
|
61
|
+
*/
|
|
62
|
+
getAddressBySessionId(sessionId: string): string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* List all conversations with metadata.
|
|
65
|
+
*/
|
|
66
|
+
listConversations(): ConversationSummary[];
|
|
67
|
+
/**
|
|
68
|
+
* End the conversation with a contact.
|
|
69
|
+
* Closes the active session if one exists.
|
|
70
|
+
*/
|
|
71
|
+
endConversation(contact: string): void;
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a contact (handle or MSTP address) to an MSTP address.
|
|
74
|
+
*
|
|
75
|
+
* Resolution order:
|
|
76
|
+
* 1. If `contact` is already an MSTP address (starts with mstps:// or mstp://),
|
|
77
|
+
* return it as-is.
|
|
78
|
+
* 2. If `contact` is already a key in the conversations map (e.g., a sessionId
|
|
79
|
+
* used as fallback address for direct MSTP clients), return it as-is.
|
|
80
|
+
* 3. Otherwise construct: `mstps://${connectorHost}/${handle}`.
|
|
81
|
+
*/
|
|
82
|
+
resolveAddress(contact: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* Extract handle from an MSTP address.
|
|
85
|
+
* `mstps://preview.masons.ai/alice` -> `alice`
|
|
86
|
+
*/
|
|
87
|
+
extractHandle(address: string): string;
|
|
88
|
+
/** @internal Reset for test isolation. */
|
|
89
|
+
_resetForTesting(): void;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=conversation-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conversation-manager.d.ts","sourceRoot":"","sources":["../src/conversation-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAQ1D,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,GAAG,QAAQ,CAAC;CACjC;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,OAAO,GAAG,QAAQ,CAAC;CACjC;AAMD,qBAAa,mBAAmB;IAC9B,QAAQ,CAAC,gBAAgB,EAAE,gBAAgB,CAAC;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IAEvC,0CAA0C;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAwC;gBAE1D,MAAM,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM;IAU1D;;;;;;OAMG;IACG,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAgDjE;;;OAGG;IACH,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAyB1E;;;OAGG;IACH,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAQ5D;;;OAGG;IACH,qBAAqB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAI5D;;OAEG;IACH,iBAAiB,IAAI,mBAAmB,EAAE;IAe1C;;;OAGG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAkBtC;;;;;;;;;OASG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAYvC;;;OAGG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM;IAUtC,0CAA0C;IAC1C,gBAAgB,IAAI,IAAI;CAIzB"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation Manager — maps contacts to conversations.
|
|
3
|
+
*
|
|
4
|
+
* Owns the identity-based API that the Tool Interface and Inbound Dispatcher
|
|
5
|
+
* call. Internally delegates session management to SessionLifecycle.
|
|
6
|
+
*
|
|
7
|
+
* Contact resolution: handle -> `mstps://${connectorHost}/${handle}`
|
|
8
|
+
* Internal key: MSTP address (same as SessionLifecycle).
|
|
9
|
+
* One session per contact, last-write-wins (D2).
|
|
10
|
+
* In-memory only, no persistence (D3).
|
|
11
|
+
*
|
|
12
|
+
* @see docs/openclaw/session-abstraction-system-design.md §4.1
|
|
13
|
+
*/
|
|
14
|
+
import createDebug from "debug";
|
|
15
|
+
import { SessionLifecycle } from "./session-lifecycle.js";
|
|
16
|
+
const dbg = createDebug("agent-network:conversation-manager");
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// ConversationManager
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
export class ConversationManager {
|
|
21
|
+
sessionLifecycle;
|
|
22
|
+
client;
|
|
23
|
+
connectorHost;
|
|
24
|
+
/** Conversations keyed by MSTP address */
|
|
25
|
+
conversations = new Map();
|
|
26
|
+
constructor(client, connectorHost) {
|
|
27
|
+
this.client = client;
|
|
28
|
+
this.connectorHost = connectorHost;
|
|
29
|
+
this.sessionLifecycle = new SessionLifecycle(client);
|
|
30
|
+
}
|
|
31
|
+
// -------------------------------------------------------------------------
|
|
32
|
+
// Public API — identity-based
|
|
33
|
+
// -------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Send a message to a contact. Session management is transparent.
|
|
36
|
+
* Creates a session automatically if none exists (D1 — sync-but-transparent).
|
|
37
|
+
*
|
|
38
|
+
* @param contact - Handle or MSTP address of the target agent.
|
|
39
|
+
* @param content - Message content to send.
|
|
40
|
+
*/
|
|
41
|
+
async send(contact, content) {
|
|
42
|
+
const address = this.resolveAddress(contact);
|
|
43
|
+
// Ensure conversation entry exists
|
|
44
|
+
if (!this.conversations.has(address)) {
|
|
45
|
+
this.conversations.set(address, {
|
|
46
|
+
contact: this.extractHandle(address),
|
|
47
|
+
address,
|
|
48
|
+
lastMessageAt: Date.now(),
|
|
49
|
+
initiatedBy: "local",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const sessionId = await this.sessionLifecycle.ensureSession(address);
|
|
54
|
+
const sent = this.client.sendMessage(sessionId, content, {
|
|
55
|
+
contentType: "text",
|
|
56
|
+
});
|
|
57
|
+
if (!sent) {
|
|
58
|
+
return {
|
|
59
|
+
status: "failed",
|
|
60
|
+
error: "Failed to send message. The network connection may be temporarily unavailable.",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Update lastMessageAt
|
|
64
|
+
const entry = this.conversations.get(address);
|
|
65
|
+
if (entry) {
|
|
66
|
+
entry.lastMessageAt = Date.now();
|
|
67
|
+
}
|
|
68
|
+
dbg("send contact=%s address=%s sessionId=%s", contact, address, sessionId);
|
|
69
|
+
return { status: "sent" };
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
73
|
+
dbg("send failed contact=%s error=%s", contact, message);
|
|
74
|
+
return { status: "failed", error: message };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Register an inbound session (remote agent initiated).
|
|
79
|
+
* Called by the channel adapter on SESSION_CREATED with direction=inbound.
|
|
80
|
+
*/
|
|
81
|
+
registerInbound(sessionId, contact, address) {
|
|
82
|
+
this.sessionLifecycle.registerInbound(sessionId, address);
|
|
83
|
+
if (!this.conversations.has(address)) {
|
|
84
|
+
this.conversations.set(address, {
|
|
85
|
+
contact,
|
|
86
|
+
address,
|
|
87
|
+
lastMessageAt: Date.now(),
|
|
88
|
+
initiatedBy: "remote",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const entry = this.conversations.get(address);
|
|
93
|
+
if (entry) {
|
|
94
|
+
entry.lastMessageAt = Date.now();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
dbg("registerInbound sessionId=%s contact=%s address=%s", sessionId, contact, address);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a sessionId to a contact handle.
|
|
101
|
+
* Used by the inbound dispatcher to derive sender identity from a message event.
|
|
102
|
+
*/
|
|
103
|
+
getContactBySessionId(sessionId) {
|
|
104
|
+
const address = this.sessionLifecycle.getAddressBySessionId(sessionId);
|
|
105
|
+
if (!address)
|
|
106
|
+
return undefined;
|
|
107
|
+
const entry = this.conversations.get(address);
|
|
108
|
+
return entry?.contact;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Resolve a sessionId to an MSTP address.
|
|
112
|
+
* Used by the channel adapter for reverse lookup.
|
|
113
|
+
*/
|
|
114
|
+
getAddressBySessionId(sessionId) {
|
|
115
|
+
return this.sessionLifecycle.getAddressBySessionId(sessionId);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* List all conversations with metadata.
|
|
119
|
+
*/
|
|
120
|
+
listConversations() {
|
|
121
|
+
const result = [];
|
|
122
|
+
for (const [address, entry] of this.conversations) {
|
|
123
|
+
const sessionId = this.sessionLifecycle.getSession(address);
|
|
124
|
+
result.push({
|
|
125
|
+
contact: entry.contact,
|
|
126
|
+
address: entry.address,
|
|
127
|
+
active: sessionId !== null,
|
|
128
|
+
lastMessageAt: entry.lastMessageAt,
|
|
129
|
+
initiatedBy: entry.initiatedBy,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* End the conversation with a contact.
|
|
136
|
+
* Closes the active session if one exists.
|
|
137
|
+
*/
|
|
138
|
+
endConversation(contact) {
|
|
139
|
+
const address = this.resolveAddress(contact);
|
|
140
|
+
this.sessionLifecycle.closeSession(address);
|
|
141
|
+
this.conversations.delete(address);
|
|
142
|
+
dbg("endConversation contact=%s address=%s", contact, address);
|
|
143
|
+
}
|
|
144
|
+
// NOTE: handleDisconnected() and handleSessionEnded() are NOT needed here.
|
|
145
|
+
// SessionLifecycle owns session state transitions via its own event subscriptions
|
|
146
|
+
// (session_ended, disconnected). Conversations persist across both events —
|
|
147
|
+
// sessions are recreated transparently on the next send().
|
|
148
|
+
// -------------------------------------------------------------------------
|
|
149
|
+
// Contact resolution
|
|
150
|
+
// -------------------------------------------------------------------------
|
|
151
|
+
/**
|
|
152
|
+
* Resolve a contact (handle or MSTP address) to an MSTP address.
|
|
153
|
+
*
|
|
154
|
+
* Resolution order:
|
|
155
|
+
* 1. If `contact` is already an MSTP address (starts with mstps:// or mstp://),
|
|
156
|
+
* return it as-is.
|
|
157
|
+
* 2. If `contact` is already a key in the conversations map (e.g., a sessionId
|
|
158
|
+
* used as fallback address for direct MSTP clients), return it as-is.
|
|
159
|
+
* 3. Otherwise construct: `mstps://${connectorHost}/${handle}`.
|
|
160
|
+
*/
|
|
161
|
+
resolveAddress(contact) {
|
|
162
|
+
if (contact.startsWith("mstps://") || contact.startsWith("mstp://")) {
|
|
163
|
+
return contact;
|
|
164
|
+
}
|
|
165
|
+
// Check if the contact string is already a registered conversation key
|
|
166
|
+
// (handles the fallback case where sessionId is used as address).
|
|
167
|
+
if (this.conversations.has(contact)) {
|
|
168
|
+
return contact;
|
|
169
|
+
}
|
|
170
|
+
return `mstps://${this.connectorHost}/${contact}`;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Extract handle from an MSTP address.
|
|
174
|
+
* `mstps://preview.masons.ai/alice` -> `alice`
|
|
175
|
+
*/
|
|
176
|
+
extractHandle(address) {
|
|
177
|
+
const trimmed = address.replace(/\/+$/, "");
|
|
178
|
+
const parts = trimmed.split("/");
|
|
179
|
+
return parts[parts.length - 1] || address;
|
|
180
|
+
}
|
|
181
|
+
// -------------------------------------------------------------------------
|
|
182
|
+
// Test helpers
|
|
183
|
+
// -------------------------------------------------------------------------
|
|
184
|
+
/** @internal Reset for test isolation. */
|
|
185
|
+
_resetForTesting() {
|
|
186
|
+
this.conversations.clear();
|
|
187
|
+
this.sessionLifecycle._resetForTesting();
|
|
188
|
+
}
|
|
189
|
+
}
|
package/dist/plugin.js
CHANGED
|
@@ -115,8 +115,8 @@ const plugin = {
|
|
|
115
115
|
"masons_send_connection_request (connect to another agent), " +
|
|
116
116
|
"masons_accept_request (accept an incoming connection request), " +
|
|
117
117
|
"masons_decline_request (decline an incoming connection request), " +
|
|
118
|
-
"
|
|
119
|
-
"
|
|
118
|
+
"masons_send_message (send a message — sessions are automatic), " +
|
|
119
|
+
"masons_end_conversation (end a conversation). " +
|
|
120
120
|
"Always try these tools first for network operations. " +
|
|
121
121
|
"If a tool call fails or a tool is not available, report the error to the user — do not silently work around it.";
|
|
122
122
|
}
|