@band-ai/sdk 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.
- package/dist/CodexAdapter-A1k_LMdZ.d.ts +1278 -0
- package/dist/CodexAdapter-BbZD-nBd.d.cts +1278 -0
- package/dist/adapters.cjs +10643 -0
- package/dist/adapters.d.cts +217 -0
- package/dist/adapters.d.ts +217 -0
- package/dist/adapters.js +92 -0
- package/dist/backends-BnOLsG7w.d.cts +20 -0
- package/dist/backends-C4n6cKOf.d.ts +20 -0
- package/dist/chunk-3BKAC4OZ.js +44 -0
- package/dist/chunk-5CXIMAH2.js +52 -0
- package/dist/chunk-6AJA2OPC.js +3682 -0
- package/dist/chunk-A3TDK6GP.js +6674 -0
- package/dist/chunk-AVVDPXP4.js +39 -0
- package/dist/chunk-EDPKUUJL.js +177 -0
- package/dist/chunk-FUODYQRE.js +2562 -0
- package/dist/chunk-GVEQ3RAH.js +373 -0
- package/dist/chunk-INA6YZTZ.js +88 -0
- package/dist/chunk-IZPV6QS6.js +115 -0
- package/dist/chunk-LY55KYVI.js +0 -0
- package/dist/chunk-NIAQBANR.js +577 -0
- package/dist/chunk-OD2G5LFQ.js +78 -0
- package/dist/chunk-SFVKOEQH.js +103 -0
- package/dist/chunk-SG4WLD2H.js +1093 -0
- package/dist/chunk-YR3BOQNW.js +39 -0
- package/dist/chunk-ZZJRRBPQ.js +82 -0
- package/dist/claude-CcnQ5OUC.d.ts +53 -0
- package/dist/claude-CwTAxhwI.d.cts +53 -0
- package/dist/config.cjs +164 -0
- package/dist/config.d.cts +19 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +9 -0
- package/dist/converters.cjs +1086 -0
- package/dist/converters.d.cts +143 -0
- package/dist/converters.d.ts +143 -0
- package/dist/converters.js +468 -0
- package/dist/core.cjs +185 -0
- package/dist/core.d.cts +22 -0
- package/dist/core.d.ts +22 -0
- package/dist/core.js +24 -0
- package/dist/customTools-BzF0IISO.d.cts +10 -0
- package/dist/customTools-BzF0IISO.d.ts +10 -0
- package/dist/dtos-DUpbIu8k.d.cts +163 -0
- package/dist/dtos-DUpbIu8k.d.ts +163 -0
- package/dist/history-ByessXNq.d.ts +24 -0
- package/dist/history-i6yN7neC.d.cts +24 -0
- package/dist/index.cjs +13914 -0
- package/dist/index.d.cts +63 -0
- package/dist/index.d.ts +63 -0
- package/dist/index.js +198 -0
- package/dist/linear.cjs +2207 -0
- package/dist/linear.d.cts +167 -0
- package/dist/linear.d.ts +167 -0
- package/dist/linear.js +2139 -0
- package/dist/logger-CABQOnlR.d.cts +21 -0
- package/dist/logger-CABQOnlR.d.ts +21 -0
- package/dist/mcp-claude.cjs +584 -0
- package/dist/mcp-claude.d.cts +4 -0
- package/dist/mcp-claude.d.ts +4 -0
- package/dist/mcp-claude.js +11 -0
- package/dist/mcp.cjs +1247 -0
- package/dist/mcp.d.cts +97 -0
- package/dist/mcp.d.ts +97 -0
- package/dist/mcp.js +27 -0
- package/dist/opencode-BBcDchcN.d.cts +159 -0
- package/dist/opencode-DthQQUsZ.d.ts +159 -0
- package/dist/pagination-BPiys10t.d.cts +25 -0
- package/dist/pagination-CcDkVFxK.d.ts +25 -0
- package/dist/protocols-CInhtFpA.d.cts +116 -0
- package/dist/protocols-DrhzOH6K.d.ts +116 -0
- package/dist/rest.cjs +1159 -0
- package/dist/rest.d.cts +144 -0
- package/dist/rest.d.ts +144 -0
- package/dist/rest.js +18 -0
- package/dist/runtime.cjs +5353 -0
- package/dist/runtime.d.cts +276 -0
- package/dist/runtime.d.ts +276 -0
- package/dist/runtime.js +274 -0
- package/dist/schemas-BpEBkq5V.d.cts +592 -0
- package/dist/schemas-vos1AaBc.d.ts +592 -0
- package/dist/sdk-HJUVSSWA.js +10 -0
- package/dist/simpleAdapter-9OE_p6QW.d.ts +29 -0
- package/dist/simpleAdapter-DqhvjsGr.d.cts +29 -0
- package/dist/testing.cjs +217 -0
- package/dist/testing.d.cts +126 -0
- package/dist/testing.d.ts +126 -0
- package/dist/testing.js +186 -0
- package/dist/types-7jVzOeOP.d.ts +446 -0
- package/dist/types-CSHFipIc.d.cts +331 -0
- package/dist/types-CovcHpYf.d.cts +446 -0
- package/dist/types-CxII3XkA.d.ts +331 -0
- package/package.json +189 -0
|
@@ -0,0 +1,3682 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HistoryProvider,
|
|
3
|
+
SYNTHETIC_CONTACT_EVENTS_SENDER_ID,
|
|
4
|
+
SYNTHETIC_CONTACT_EVENTS_SENDER_NAME,
|
|
5
|
+
SYNTHETIC_SENDER_TYPE,
|
|
6
|
+
buildParticipantsMessage
|
|
7
|
+
} from "./chunk-INA6YZTZ.js";
|
|
8
|
+
import {
|
|
9
|
+
CHAT_EVENT_TYPES,
|
|
10
|
+
CHAT_TOOL_NAMES,
|
|
11
|
+
MEMORY_TOOL_NAMES,
|
|
12
|
+
TOOL_MODELS,
|
|
13
|
+
assertChatEventType,
|
|
14
|
+
getToolDescription
|
|
15
|
+
} from "./chunk-GVEQ3RAH.js";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_REQUEST_OPTIONS,
|
|
18
|
+
FernRestAdapter,
|
|
19
|
+
fetchPaginated
|
|
20
|
+
} from "./chunk-SG4WLD2H.js";
|
|
21
|
+
import {
|
|
22
|
+
DEFAULT_AGENT_TOOLS_CAPABILITIES,
|
|
23
|
+
createToolExecutorError,
|
|
24
|
+
isToolExecutorError
|
|
25
|
+
} from "./chunk-3BKAC4OZ.js";
|
|
26
|
+
import {
|
|
27
|
+
NoopLogger
|
|
28
|
+
} from "./chunk-OD2G5LFQ.js";
|
|
29
|
+
import {
|
|
30
|
+
RuntimeStateError,
|
|
31
|
+
TransportError,
|
|
32
|
+
UnsupportedFeatureError,
|
|
33
|
+
ValidationError
|
|
34
|
+
} from "./chunk-YR3BOQNW.js";
|
|
35
|
+
|
|
36
|
+
// src/contracts/capabilities.ts
|
|
37
|
+
var CAPABILITY_LABELS = {
|
|
38
|
+
peers: "Peer lookup",
|
|
39
|
+
contacts: "Contacts",
|
|
40
|
+
memory: "Memory"
|
|
41
|
+
};
|
|
42
|
+
function assertCapability(capabilities, capability, label = CAPABILITY_LABELS[capability]) {
|
|
43
|
+
if (!capabilities[capability]) {
|
|
44
|
+
throw new UnsupportedFeatureError(`${label} is disabled by runtime capabilities`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// src/platform/streaming/payloadSchemas.ts
|
|
49
|
+
import { z } from "zod";
|
|
50
|
+
var mentionSchema = z.object({
|
|
51
|
+
id: z.string(),
|
|
52
|
+
handle: z.string().nullish(),
|
|
53
|
+
name: z.string().nullish(),
|
|
54
|
+
username: z.string().nullish()
|
|
55
|
+
}).passthrough();
|
|
56
|
+
var messageMetadataSchema = z.object({
|
|
57
|
+
mentions: z.array(mentionSchema).nullish()
|
|
58
|
+
}).passthrough();
|
|
59
|
+
var messageCreatedPayloadSchema = z.object({
|
|
60
|
+
id: z.string(),
|
|
61
|
+
content: z.string(),
|
|
62
|
+
message_type: z.string(),
|
|
63
|
+
metadata: messageMetadataSchema.nullish(),
|
|
64
|
+
sender_id: z.string(),
|
|
65
|
+
sender_type: z.string(),
|
|
66
|
+
sender_name: z.string().nullish(),
|
|
67
|
+
chat_room_id: z.string().nullish(),
|
|
68
|
+
inserted_at: z.string(),
|
|
69
|
+
updated_at: z.string()
|
|
70
|
+
}).passthrough();
|
|
71
|
+
var roomOwnerSchema = z.object({
|
|
72
|
+
id: z.string(),
|
|
73
|
+
name: z.string(),
|
|
74
|
+
type: z.string()
|
|
75
|
+
}).passthrough();
|
|
76
|
+
var roomAddedPayloadSchema = z.object({
|
|
77
|
+
id: z.string(),
|
|
78
|
+
title: z.string().nullish(),
|
|
79
|
+
task_id: z.string().nullish(),
|
|
80
|
+
inserted_at: z.string().nullish(),
|
|
81
|
+
updated_at: z.string().nullish(),
|
|
82
|
+
owner: roomOwnerSchema.nullish(),
|
|
83
|
+
status: z.string().nullish(),
|
|
84
|
+
type: z.string().nullish(),
|
|
85
|
+
created_at: z.string().nullish(),
|
|
86
|
+
participant_role: z.string().nullish()
|
|
87
|
+
}).passthrough();
|
|
88
|
+
var roomRemovedPayloadSchema = z.object({
|
|
89
|
+
id: z.string(),
|
|
90
|
+
status: z.string(),
|
|
91
|
+
type: z.string(),
|
|
92
|
+
title: z.string(),
|
|
93
|
+
removed_at: z.string()
|
|
94
|
+
}).passthrough();
|
|
95
|
+
var participantAddedPayloadSchema = z.object({
|
|
96
|
+
id: z.string(),
|
|
97
|
+
name: z.string(),
|
|
98
|
+
type: z.string(),
|
|
99
|
+
handle: z.string().nullish()
|
|
100
|
+
}).passthrough();
|
|
101
|
+
var participantRemovedPayloadSchema = z.object({
|
|
102
|
+
id: z.string()
|
|
103
|
+
}).passthrough();
|
|
104
|
+
var roomDeletedPayloadSchema = z.object({
|
|
105
|
+
id: z.string()
|
|
106
|
+
}).passthrough();
|
|
107
|
+
var contactRequestReceivedPayloadSchema = z.object({
|
|
108
|
+
id: z.string(),
|
|
109
|
+
from_handle: z.string(),
|
|
110
|
+
from_name: z.string(),
|
|
111
|
+
message: z.string().nullish(),
|
|
112
|
+
status: z.string(),
|
|
113
|
+
inserted_at: z.string()
|
|
114
|
+
}).passthrough();
|
|
115
|
+
var contactRequestUpdatedPayloadSchema = z.object({
|
|
116
|
+
id: z.string(),
|
|
117
|
+
status: z.string()
|
|
118
|
+
}).passthrough();
|
|
119
|
+
var contactAddedPayloadSchema = z.object({
|
|
120
|
+
id: z.string(),
|
|
121
|
+
handle: z.string(),
|
|
122
|
+
name: z.string(),
|
|
123
|
+
type: z.string(),
|
|
124
|
+
description: z.string().nullish(),
|
|
125
|
+
is_external: z.boolean().nullish(),
|
|
126
|
+
inserted_at: z.string()
|
|
127
|
+
}).passthrough();
|
|
128
|
+
var contactRemovedPayloadSchema = z.object({
|
|
129
|
+
id: z.string()
|
|
130
|
+
}).passthrough();
|
|
131
|
+
var payloadSchemas = {
|
|
132
|
+
message_created: messageCreatedPayloadSchema,
|
|
133
|
+
room_added: roomAddedPayloadSchema,
|
|
134
|
+
room_removed: roomRemovedPayloadSchema,
|
|
135
|
+
participant_added: participantAddedPayloadSchema,
|
|
136
|
+
participant_removed: participantRemovedPayloadSchema,
|
|
137
|
+
room_deleted: roomDeletedPayloadSchema,
|
|
138
|
+
contact_request_received: contactRequestReceivedPayloadSchema,
|
|
139
|
+
contact_request_updated: contactRequestUpdatedPayloadSchema,
|
|
140
|
+
contact_added: contactAddedPayloadSchema,
|
|
141
|
+
contact_removed: contactRemovedPayloadSchema
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// src/platform/streaming/PhoenixChannelsTransport.ts
|
|
145
|
+
import { Socket } from "phoenix";
|
|
146
|
+
import { WebSocket as NodeWebSocket } from "ws";
|
|
147
|
+
var PhoenixChannelsTransport = class {
|
|
148
|
+
socket;
|
|
149
|
+
channels = /* @__PURE__ */ new Map();
|
|
150
|
+
channelRefs = /* @__PURE__ */ new Map();
|
|
151
|
+
pendingJoins = /* @__PURE__ */ new Map();
|
|
152
|
+
logger;
|
|
153
|
+
onHandlerError;
|
|
154
|
+
connected = false;
|
|
155
|
+
connectPromise = null;
|
|
156
|
+
connectResolve = null;
|
|
157
|
+
constructor(options) {
|
|
158
|
+
this.logger = options.logger ?? new NoopLogger();
|
|
159
|
+
let wsUrl = options.wsUrl;
|
|
160
|
+
if (wsUrl.endsWith("/websocket")) {
|
|
161
|
+
wsUrl = wsUrl.slice(0, -"/websocket".length);
|
|
162
|
+
}
|
|
163
|
+
this.socket = new Socket(wsUrl, {
|
|
164
|
+
params: {
|
|
165
|
+
api_key: options.apiKey,
|
|
166
|
+
agent_id: options.agentId
|
|
167
|
+
},
|
|
168
|
+
heartbeatIntervalMs: options.heartbeatIntervalMs,
|
|
169
|
+
reconnectAfterMs: options.reconnectAfterMs ?? ((tries) => [1e3, 2e3, 5e3, 1e4, 3e4][tries - 1] ?? 3e4),
|
|
170
|
+
transport: options.websocketFactory ?? resolveWebSocketFactory()
|
|
171
|
+
});
|
|
172
|
+
this.socket.onOpen(() => {
|
|
173
|
+
this.connected = true;
|
|
174
|
+
this.connectResolve?.();
|
|
175
|
+
this.connectResolve = null;
|
|
176
|
+
this.logger.info("Phoenix socket opened", {
|
|
177
|
+
channels: getSocketChannelCount(this.socket)
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
this.socket.onClose((event) => {
|
|
181
|
+
this.connected = false;
|
|
182
|
+
if (getSocketChannelCount(this.socket) === 0) {
|
|
183
|
+
this.socket.disconnect();
|
|
184
|
+
}
|
|
185
|
+
this.logger.info("Phoenix socket closed", {
|
|
186
|
+
code: event?.code ?? null,
|
|
187
|
+
reason: event?.reason ?? null
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
this.socket.onError((event) => {
|
|
191
|
+
this.logger.warn("Phoenix socket error", { event });
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
async connect() {
|
|
195
|
+
if (this.connected) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (!this.connectPromise) {
|
|
199
|
+
this.socket.connect();
|
|
200
|
+
const pending = this.waitForConnection();
|
|
201
|
+
this.connectPromise = pending;
|
|
202
|
+
void pending.then(
|
|
203
|
+
() => {
|
|
204
|
+
if (this.connectPromise === pending) {
|
|
205
|
+
this.connectPromise = null;
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
() => {
|
|
209
|
+
if (this.connectPromise === pending) {
|
|
210
|
+
this.connectPromise = null;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
await this.connectPromise;
|
|
216
|
+
}
|
|
217
|
+
async disconnect() {
|
|
218
|
+
for (const topic of this.channels.keys()) {
|
|
219
|
+
await this.leave(topic);
|
|
220
|
+
}
|
|
221
|
+
this.socket.disconnect();
|
|
222
|
+
this.connected = false;
|
|
223
|
+
}
|
|
224
|
+
isConnected() {
|
|
225
|
+
return this.connected;
|
|
226
|
+
}
|
|
227
|
+
async join(topic, handlers) {
|
|
228
|
+
if (this.channels.has(topic)) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const pendingJoin = this.pendingJoins.get(topic);
|
|
232
|
+
if (pendingJoin) {
|
|
233
|
+
return pendingJoin;
|
|
234
|
+
}
|
|
235
|
+
const joinPromise = this.doJoin(topic, handlers).finally(() => {
|
|
236
|
+
this.pendingJoins.delete(topic);
|
|
237
|
+
});
|
|
238
|
+
this.pendingJoins.set(topic, joinPromise);
|
|
239
|
+
return joinPromise;
|
|
240
|
+
}
|
|
241
|
+
async doJoin(topic, handlers) {
|
|
242
|
+
const channel = this.socket.channel(topic, {});
|
|
243
|
+
const refs = [];
|
|
244
|
+
for (const [event, handler] of Object.entries(handlers)) {
|
|
245
|
+
const ref = channel.on(event, (payload) => {
|
|
246
|
+
Promise.resolve(handler(payload)).catch((error) => {
|
|
247
|
+
this.logger.error("Unhandled topic handler error", {
|
|
248
|
+
topic,
|
|
249
|
+
event,
|
|
250
|
+
error
|
|
251
|
+
});
|
|
252
|
+
this.onHandlerError?.(error);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
refs.push([event, ref]);
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
await new Promise((resolve, reject) => {
|
|
259
|
+
channel.join().receive("ok", () => resolve()).receive(
|
|
260
|
+
"error",
|
|
261
|
+
(error) => reject(new TransportError(`Failed to join topic ${topic}`, error))
|
|
262
|
+
).receive("timeout", () => reject(new TransportError(`Timeout joining topic ${topic}`)));
|
|
263
|
+
});
|
|
264
|
+
} catch (error) {
|
|
265
|
+
for (const [event, ref] of refs) {
|
|
266
|
+
channel.off(event, ref);
|
|
267
|
+
}
|
|
268
|
+
channel.leave();
|
|
269
|
+
removeSocketChannel(this.socket, channel);
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
this.channels.set(topic, channel);
|
|
273
|
+
this.channelRefs.set(topic, refs);
|
|
274
|
+
this.logger.debug("Joined topic", { topic });
|
|
275
|
+
}
|
|
276
|
+
async leave(topic) {
|
|
277
|
+
const channel = this.channels.get(topic);
|
|
278
|
+
if (!channel) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const refs = this.channelRefs.get(topic) ?? [];
|
|
282
|
+
for (const [event, ref] of refs) {
|
|
283
|
+
channel.off(event, ref);
|
|
284
|
+
}
|
|
285
|
+
this.channelRefs.delete(topic);
|
|
286
|
+
await new Promise((resolve, reject) => {
|
|
287
|
+
channel.leave().receive("ok", () => resolve()).receive(
|
|
288
|
+
"error",
|
|
289
|
+
(error) => reject(new TransportError(`Failed to leave topic ${topic}`, error))
|
|
290
|
+
).receive("timeout", () => reject(new TransportError(`Timeout leaving topic ${topic}`)));
|
|
291
|
+
});
|
|
292
|
+
this.channels.delete(topic);
|
|
293
|
+
this.logger.debug("Left topic", { topic });
|
|
294
|
+
}
|
|
295
|
+
async runForever(signal) {
|
|
296
|
+
if (signal.aborted) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
await new Promise((resolve) => {
|
|
300
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
async waitForConnection(timeoutMs = 1e4) {
|
|
304
|
+
if (this.connected) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
await new Promise((resolve, reject) => {
|
|
308
|
+
const timeout = setTimeout(() => {
|
|
309
|
+
this.connectResolve = null;
|
|
310
|
+
reject(new TransportError("Timed out waiting for Phoenix socket connection"));
|
|
311
|
+
}, timeoutMs);
|
|
312
|
+
this.connectResolve = () => {
|
|
313
|
+
clearTimeout(timeout);
|
|
314
|
+
resolve();
|
|
315
|
+
};
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
function resolveWebSocketFactory() {
|
|
320
|
+
if (typeof process !== "undefined" && process.versions?.node) {
|
|
321
|
+
return NodeWebSocket;
|
|
322
|
+
}
|
|
323
|
+
return WebSocket;
|
|
324
|
+
}
|
|
325
|
+
function removeSocketChannel(socket, channel) {
|
|
326
|
+
const candidate = socket;
|
|
327
|
+
candidate.remove?.(channel);
|
|
328
|
+
}
|
|
329
|
+
function getSocketChannelCount(socket) {
|
|
330
|
+
const candidate = socket;
|
|
331
|
+
if (!Array.isArray(candidate.channels)) {
|
|
332
|
+
return "unknown";
|
|
333
|
+
}
|
|
334
|
+
return candidate.channels.length;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/platform/ThenvoiLink.ts
|
|
338
|
+
import { ThenvoiClient } from "@thenvoi/rest-client";
|
|
339
|
+
var DEFAULT_WS_URL = "wss://app.thenvoi.com/api/v1/socket";
|
|
340
|
+
function deriveDefaultRestUrl(wsUrl) {
|
|
341
|
+
const parsed = new URL(wsUrl);
|
|
342
|
+
const protocol = parsed.protocol === "ws:" ? "http:" : "https:";
|
|
343
|
+
return `${protocol}//${parsed.host}`;
|
|
344
|
+
}
|
|
345
|
+
function toPlatformMessage(roomId, message) {
|
|
346
|
+
return {
|
|
347
|
+
id: message.id,
|
|
348
|
+
roomId,
|
|
349
|
+
content: message.content,
|
|
350
|
+
senderId: message.sender_id,
|
|
351
|
+
senderType: message.sender_type,
|
|
352
|
+
senderName: message.sender_name ?? null,
|
|
353
|
+
messageType: message.message_type,
|
|
354
|
+
metadata: message.metadata ?? {},
|
|
355
|
+
createdAt: new Date(message.inserted_at)
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
var ThenvoiLink = class {
|
|
359
|
+
agentId;
|
|
360
|
+
apiKey;
|
|
361
|
+
wsUrl;
|
|
362
|
+
restUrl;
|
|
363
|
+
rest;
|
|
364
|
+
capabilities;
|
|
365
|
+
logger;
|
|
366
|
+
transport;
|
|
367
|
+
subscribedRooms = /* @__PURE__ */ new Set();
|
|
368
|
+
eventQueue = [];
|
|
369
|
+
waiters = [];
|
|
370
|
+
connected = false;
|
|
371
|
+
constructor(options) {
|
|
372
|
+
this.agentId = options.agentId;
|
|
373
|
+
this.apiKey = options.apiKey;
|
|
374
|
+
this.wsUrl = options.wsUrl ?? DEFAULT_WS_URL;
|
|
375
|
+
this.restUrl = options.restUrl ?? deriveDefaultRestUrl(this.wsUrl);
|
|
376
|
+
this.logger = options.logger ?? new NoopLogger();
|
|
377
|
+
this.capabilities = {
|
|
378
|
+
...DEFAULT_AGENT_TOOLS_CAPABILITIES,
|
|
379
|
+
...options.capabilities
|
|
380
|
+
};
|
|
381
|
+
const restApi = options.restApi ?? new FernRestAdapter(
|
|
382
|
+
new ThenvoiClient({
|
|
383
|
+
apiKey: this.apiKey,
|
|
384
|
+
baseUrl: this.restUrl
|
|
385
|
+
})
|
|
386
|
+
);
|
|
387
|
+
this.rest = restApi;
|
|
388
|
+
this.transport = options.transport ?? new PhoenixChannelsTransport({
|
|
389
|
+
wsUrl: this.wsUrl,
|
|
390
|
+
apiKey: this.apiKey,
|
|
391
|
+
agentId: this.agentId,
|
|
392
|
+
logger: this.logger
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
isConnected() {
|
|
396
|
+
return this.connected;
|
|
397
|
+
}
|
|
398
|
+
async connect() {
|
|
399
|
+
if (this.connected) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
await this.transport.connect();
|
|
403
|
+
this.connected = true;
|
|
404
|
+
}
|
|
405
|
+
async disconnect() {
|
|
406
|
+
if (!this.connected) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
await Promise.allSettled(
|
|
410
|
+
[...this.subscribedRooms].map((roomId) => this.unsubscribeRoom(roomId))
|
|
411
|
+
);
|
|
412
|
+
await this.transport.disconnect();
|
|
413
|
+
this.connected = false;
|
|
414
|
+
}
|
|
415
|
+
async runForever(signal) {
|
|
416
|
+
await this.transport.runForever(signal);
|
|
417
|
+
}
|
|
418
|
+
queueEvent(event) {
|
|
419
|
+
const waiter = this.waiters.shift();
|
|
420
|
+
if (waiter) {
|
|
421
|
+
waiter.resolve(event);
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
this.eventQueue.push(event);
|
|
425
|
+
}
|
|
426
|
+
async subscribeAgentRooms() {
|
|
427
|
+
await this.transport.join(`agent_rooms:${this.agentId}`, {
|
|
428
|
+
room_added: (payload) => {
|
|
429
|
+
const roomId = typeof payload.id === "string" ? payload.id : "";
|
|
430
|
+
this.emit("room_added", payload, roomId);
|
|
431
|
+
},
|
|
432
|
+
room_removed: (payload) => {
|
|
433
|
+
const roomId = typeof payload.id === "string" ? payload.id : "";
|
|
434
|
+
this.emit("room_removed", payload, roomId);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
async subscribeRoom(roomId) {
|
|
439
|
+
if (this.subscribedRooms.has(roomId)) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
await this.transport.join(`chat_room:${roomId}`, {
|
|
443
|
+
message_created: (payload) => {
|
|
444
|
+
this.emit("message_created", payload, roomId);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
await this.transport.join(`room_participants:${roomId}`, {
|
|
448
|
+
participant_added: (payload) => {
|
|
449
|
+
this.emit("participant_added", payload, roomId);
|
|
450
|
+
},
|
|
451
|
+
participant_removed: (payload) => {
|
|
452
|
+
this.emit("participant_removed", payload, roomId);
|
|
453
|
+
},
|
|
454
|
+
room_deleted: (payload) => {
|
|
455
|
+
this.emit("room_deleted", payload, roomId);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
this.subscribedRooms.add(roomId);
|
|
459
|
+
}
|
|
460
|
+
async unsubscribeRoom(roomId) {
|
|
461
|
+
if (!this.subscribedRooms.has(roomId)) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
await this.transport.leave(`chat_room:${roomId}`);
|
|
465
|
+
await this.transport.leave(`room_participants:${roomId}`);
|
|
466
|
+
this.subscribedRooms.delete(roomId);
|
|
467
|
+
}
|
|
468
|
+
async subscribeAgentContacts() {
|
|
469
|
+
assertCapability(this.capabilities, "contacts", "Contacts streaming");
|
|
470
|
+
await this.transport.join(`agent_contacts:${this.agentId}`, {
|
|
471
|
+
contact_request_received: (payload) => {
|
|
472
|
+
this.emit("contact_request_received", payload, null);
|
|
473
|
+
},
|
|
474
|
+
contact_request_updated: (payload) => {
|
|
475
|
+
this.emit("contact_request_updated", payload, null);
|
|
476
|
+
},
|
|
477
|
+
contact_added: (payload) => {
|
|
478
|
+
this.emit("contact_added", payload, null);
|
|
479
|
+
},
|
|
480
|
+
contact_removed: (payload) => {
|
|
481
|
+
this.emit("contact_removed", payload, null);
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
async unsubscribeAgentContacts() {
|
|
486
|
+
await this.transport.leave(`agent_contacts:${this.agentId}`);
|
|
487
|
+
}
|
|
488
|
+
async nextEvent(signal) {
|
|
489
|
+
if (signal?.aborted) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
const queued = this.eventQueue.shift();
|
|
493
|
+
if (queued) {
|
|
494
|
+
return queued;
|
|
495
|
+
}
|
|
496
|
+
return new Promise((resolve) => {
|
|
497
|
+
let settled = false;
|
|
498
|
+
const onAbort = () => {
|
|
499
|
+
const index = this.waiters.indexOf(waiter);
|
|
500
|
+
if (index >= 0) {
|
|
501
|
+
this.waiters.splice(index, 1);
|
|
502
|
+
}
|
|
503
|
+
finalize(null);
|
|
504
|
+
};
|
|
505
|
+
const cleanup = () => {
|
|
506
|
+
signal?.removeEventListener("abort", onAbort);
|
|
507
|
+
};
|
|
508
|
+
const finalize = (event) => {
|
|
509
|
+
if (settled) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
settled = true;
|
|
513
|
+
cleanup();
|
|
514
|
+
resolve(event);
|
|
515
|
+
};
|
|
516
|
+
const waiter = {
|
|
517
|
+
resolve: finalize,
|
|
518
|
+
cleanup
|
|
519
|
+
};
|
|
520
|
+
this.waiters.push(waiter);
|
|
521
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
[Symbol.asyncIterator]() {
|
|
525
|
+
return {
|
|
526
|
+
next: async () => {
|
|
527
|
+
const value = await this.nextEvent();
|
|
528
|
+
if (value === null) {
|
|
529
|
+
return { done: true, value: void 0 };
|
|
530
|
+
}
|
|
531
|
+
return { done: false, value };
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
async markProcessing(roomId, messageId, options) {
|
|
536
|
+
await this.markMessageStatus(
|
|
537
|
+
"markProcessing",
|
|
538
|
+
{
|
|
539
|
+
roomId,
|
|
540
|
+
messageId
|
|
541
|
+
},
|
|
542
|
+
() => this.rest.markMessageProcessing(roomId, messageId),
|
|
543
|
+
options
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
async markProcessed(roomId, messageId, options) {
|
|
547
|
+
await this.markMessageStatus(
|
|
548
|
+
"markProcessed",
|
|
549
|
+
{
|
|
550
|
+
roomId,
|
|
551
|
+
messageId
|
|
552
|
+
},
|
|
553
|
+
() => this.rest.markMessageProcessed(roomId, messageId),
|
|
554
|
+
options
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
async markFailed(roomId, messageId, error, options) {
|
|
558
|
+
const normalizedError = error.trim() || "Unknown error";
|
|
559
|
+
await this.markMessageStatus(
|
|
560
|
+
"markFailed",
|
|
561
|
+
{
|
|
562
|
+
roomId,
|
|
563
|
+
messageId,
|
|
564
|
+
error: normalizedError
|
|
565
|
+
},
|
|
566
|
+
() => this.rest.markMessageFailed(roomId, messageId, normalizedError),
|
|
567
|
+
options
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
async markMessageStatus(operation, context, mark, options) {
|
|
571
|
+
try {
|
|
572
|
+
await mark();
|
|
573
|
+
} catch (error) {
|
|
574
|
+
if (!options?.bestEffort) {
|
|
575
|
+
throw error;
|
|
576
|
+
}
|
|
577
|
+
this.logger.warn(`${operation} failed (best-effort)`, {
|
|
578
|
+
...context,
|
|
579
|
+
error
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async getNextMessage(roomId) {
|
|
584
|
+
if (!this.rest.getNextMessage) {
|
|
585
|
+
throw new UnsupportedFeatureError("Message queue access is not available in current REST adapter");
|
|
586
|
+
}
|
|
587
|
+
const message = await this.rest.getNextMessage({ chatId: roomId });
|
|
588
|
+
return message ? toPlatformMessage(roomId, message) : null;
|
|
589
|
+
}
|
|
590
|
+
async getStaleProcessingMessages(roomId) {
|
|
591
|
+
if (!this.rest.listMessages) {
|
|
592
|
+
return [];
|
|
593
|
+
}
|
|
594
|
+
const response = await this.rest.listMessages({
|
|
595
|
+
chatId: roomId,
|
|
596
|
+
page: 1,
|
|
597
|
+
pageSize: 50,
|
|
598
|
+
status: "processing"
|
|
599
|
+
});
|
|
600
|
+
return response.data.map((msg) => toPlatformMessage(roomId, msg));
|
|
601
|
+
}
|
|
602
|
+
async listChats(request, requestOptions) {
|
|
603
|
+
if (!this.rest.listChats) {
|
|
604
|
+
throw new UnsupportedFeatureError("Chat listing is not available in current REST adapter");
|
|
605
|
+
}
|
|
606
|
+
return this.rest.listChats(request, requestOptions);
|
|
607
|
+
}
|
|
608
|
+
async listAllChats(options, requestOptions) {
|
|
609
|
+
return fetchPaginated({
|
|
610
|
+
fetchPage: ({ page, pageSize }) => this.listChats({ page, pageSize }, requestOptions),
|
|
611
|
+
pageSize: options?.pageSize,
|
|
612
|
+
maxPages: options?.maxPages,
|
|
613
|
+
strategy: options?.strategy,
|
|
614
|
+
metadataValidation: options?.metadataValidation
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
emit(eventType, payload, roomId) {
|
|
618
|
+
const schema = payloadSchemas[eventType];
|
|
619
|
+
const parsed = schema.safeParse(payload);
|
|
620
|
+
if (!parsed.success) {
|
|
621
|
+
this.logger.warn(`Invalid ${eventType} payload, dropping event`, {
|
|
622
|
+
error: parsed.error.message,
|
|
623
|
+
roomId
|
|
624
|
+
});
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
this.queueEvent({
|
|
628
|
+
type: eventType,
|
|
629
|
+
roomId,
|
|
630
|
+
payload: parsed.data,
|
|
631
|
+
raw: payload
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
// src/runtime/Execution.ts
|
|
637
|
+
function toMessageEvent(message) {
|
|
638
|
+
const insertedAt = message.createdAt.toISOString();
|
|
639
|
+
return {
|
|
640
|
+
type: "message_created",
|
|
641
|
+
roomId: message.roomId,
|
|
642
|
+
payload: {
|
|
643
|
+
id: message.id,
|
|
644
|
+
content: message.content,
|
|
645
|
+
sender_id: message.senderId,
|
|
646
|
+
sender_type: message.senderType,
|
|
647
|
+
sender_name: message.senderName ?? null,
|
|
648
|
+
message_type: message.messageType,
|
|
649
|
+
metadata: message.metadata,
|
|
650
|
+
inserted_at: insertedAt,
|
|
651
|
+
updated_at: insertedAt
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
var Execution = class {
|
|
656
|
+
roomId;
|
|
657
|
+
link;
|
|
658
|
+
context;
|
|
659
|
+
retryTracker;
|
|
660
|
+
onExecute;
|
|
661
|
+
onFailure;
|
|
662
|
+
logger;
|
|
663
|
+
eventQueue = [];
|
|
664
|
+
waiters = [];
|
|
665
|
+
idleWaiters = /* @__PURE__ */ new Set();
|
|
666
|
+
drainedWsMessageIds = /* @__PURE__ */ new Set();
|
|
667
|
+
syncProcessedIds = /* @__PURE__ */ new Set();
|
|
668
|
+
processTask;
|
|
669
|
+
firstWsMessageId = null;
|
|
670
|
+
syncComplete = false;
|
|
671
|
+
running = true;
|
|
672
|
+
inFlight = 0;
|
|
673
|
+
constructor(options) {
|
|
674
|
+
this.roomId = options.roomId;
|
|
675
|
+
this.link = options.link;
|
|
676
|
+
this.context = options.context;
|
|
677
|
+
this.retryTracker = this.context.getRetryTracker();
|
|
678
|
+
this.onExecute = options.onExecute;
|
|
679
|
+
this.onFailure = options.onFailure;
|
|
680
|
+
this.logger = options.logger ?? new NoopLogger();
|
|
681
|
+
this.processTask = this.processLoop();
|
|
682
|
+
}
|
|
683
|
+
enqueue(event) {
|
|
684
|
+
if (event.type === "message_created" && !this.syncComplete && this.firstWsMessageId === null) {
|
|
685
|
+
this.firstWsMessageId = event.payload.id;
|
|
686
|
+
}
|
|
687
|
+
const waiter = this.waiters.shift();
|
|
688
|
+
if (waiter) {
|
|
689
|
+
waiter(event);
|
|
690
|
+
} else {
|
|
691
|
+
this.eventQueue.push(event);
|
|
692
|
+
}
|
|
693
|
+
return Promise.resolve();
|
|
694
|
+
}
|
|
695
|
+
async bootstrapMessage(message) {
|
|
696
|
+
this.syncProcessedIds.add(message.id);
|
|
697
|
+
await this.executeSyncMessage(toMessageEvent(message), message.id);
|
|
698
|
+
}
|
|
699
|
+
isIdle() {
|
|
700
|
+
return this.syncComplete && this.inFlight === 0 && this.eventQueue.length === 0;
|
|
701
|
+
}
|
|
702
|
+
async waitForIdle(timeoutMs) {
|
|
703
|
+
if (this.isIdle()) {
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
return new Promise((resolve) => {
|
|
707
|
+
let settled = false;
|
|
708
|
+
let timeoutHandle = null;
|
|
709
|
+
const idleWaiter = () => {
|
|
710
|
+
if (settled) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
settled = true;
|
|
714
|
+
this.idleWaiters.delete(idleWaiter);
|
|
715
|
+
if (timeoutHandle !== null) {
|
|
716
|
+
clearTimeout(timeoutHandle);
|
|
717
|
+
}
|
|
718
|
+
resolve(true);
|
|
719
|
+
};
|
|
720
|
+
this.idleWaiters.add(idleWaiter);
|
|
721
|
+
if (timeoutMs === void 0) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
timeoutHandle = setTimeout(() => {
|
|
725
|
+
if (settled) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
settled = true;
|
|
729
|
+
this.idleWaiters.delete(idleWaiter);
|
|
730
|
+
resolve(false);
|
|
731
|
+
}, timeoutMs);
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
async stop(timeoutMs) {
|
|
735
|
+
const graceful = await this.waitForIdle(timeoutMs);
|
|
736
|
+
this.running = false;
|
|
737
|
+
this.resolveEventWaiters(null);
|
|
738
|
+
if (graceful || timeoutMs === void 0) {
|
|
739
|
+
await this.processTask;
|
|
740
|
+
}
|
|
741
|
+
return graceful;
|
|
742
|
+
}
|
|
743
|
+
async waitUntilStopped() {
|
|
744
|
+
await this.processTask;
|
|
745
|
+
}
|
|
746
|
+
async processLoop() {
|
|
747
|
+
await this.recoverStaleProcessingMessages();
|
|
748
|
+
await this.synchronizeWithNext();
|
|
749
|
+
while (this.running) {
|
|
750
|
+
const event = await this.nextQueuedEvent();
|
|
751
|
+
if (!event) {
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (event.type === "message_created" && this.drainedWsMessageIds.has(event.payload.id)) {
|
|
755
|
+
this.drainedWsMessageIds.delete(event.payload.id);
|
|
756
|
+
this.notifyIfIdle();
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
await this.executeEvent(event);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
async recoverStaleProcessingMessages() {
|
|
763
|
+
let staleMessages;
|
|
764
|
+
try {
|
|
765
|
+
staleMessages = await this.link.getStaleProcessingMessages(this.roomId);
|
|
766
|
+
} catch {
|
|
767
|
+
this.logger.warn("Failed to fetch stale processing messages, skipping recovery", {
|
|
768
|
+
roomId: this.roomId
|
|
769
|
+
});
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (staleMessages.length === 0) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
this.logger.info("Recovering stale processing messages", {
|
|
776
|
+
roomId: this.roomId,
|
|
777
|
+
count: staleMessages.length
|
|
778
|
+
});
|
|
779
|
+
for (const message of staleMessages) {
|
|
780
|
+
if (!this.running) {
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
if (this.retryTracker.isPermanentlyFailed(message.id)) {
|
|
784
|
+
this.logger.warn("Skipping permanently failed stale message", {
|
|
785
|
+
roomId: this.roomId,
|
|
786
|
+
messageId: message.id
|
|
787
|
+
});
|
|
788
|
+
continue;
|
|
789
|
+
}
|
|
790
|
+
await this.executeSyncMessage(toMessageEvent(message), message.id);
|
|
791
|
+
this.syncProcessedIds.add(message.id);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
async synchronizeWithNext() {
|
|
795
|
+
while (this.running) {
|
|
796
|
+
const nextMessage = await this.link.getNextMessage(this.roomId);
|
|
797
|
+
if (!nextMessage) {
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
if (this.syncProcessedIds.has(nextMessage.id)) {
|
|
801
|
+
const isSyncPoint2 = this.firstWsMessageId !== null && nextMessage.id === this.firstWsMessageId;
|
|
802
|
+
if (isSyncPoint2) {
|
|
803
|
+
this.drainedWsMessageIds.add(nextMessage.id);
|
|
804
|
+
this.firstWsMessageId = null;
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
if (this.retryTracker.isPermanentlyFailed(nextMessage.id)) {
|
|
810
|
+
this.logger.warn("Skipping permanently failed message during sync", {
|
|
811
|
+
roomId: this.roomId,
|
|
812
|
+
messageId: nextMessage.id
|
|
813
|
+
});
|
|
814
|
+
await this.markMessageFailed(nextMessage.id, "Message permanently failed after max retries");
|
|
815
|
+
const isSyncPoint2 = this.firstWsMessageId !== null && nextMessage.id === this.firstWsMessageId;
|
|
816
|
+
if (isSyncPoint2) {
|
|
817
|
+
this.drainedWsMessageIds.add(nextMessage.id);
|
|
818
|
+
this.firstWsMessageId = null;
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const isSyncPoint = this.firstWsMessageId !== null && nextMessage.id === this.firstWsMessageId;
|
|
824
|
+
await this.executeSyncMessage(toMessageEvent(nextMessage), nextMessage.id);
|
|
825
|
+
this.syncProcessedIds.add(nextMessage.id);
|
|
826
|
+
if (isSyncPoint) {
|
|
827
|
+
this.drainedWsMessageIds.add(nextMessage.id);
|
|
828
|
+
this.firstWsMessageId = null;
|
|
829
|
+
break;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
this.syncProcessedIds.clear();
|
|
833
|
+
this.syncComplete = true;
|
|
834
|
+
this.notifyIfIdle();
|
|
835
|
+
}
|
|
836
|
+
async executeSyncMessage(event, messageId) {
|
|
837
|
+
const [, exceeded] = this.retryTracker.recordAttempt(messageId);
|
|
838
|
+
if (exceeded) {
|
|
839
|
+
this.logger.error("Message exceeded max retries during sync, marking permanently failed", {
|
|
840
|
+
roomId: this.roomId,
|
|
841
|
+
messageId,
|
|
842
|
+
maxRetries: this.retryTracker.maxRetries
|
|
843
|
+
});
|
|
844
|
+
await this.markMessageFailed(messageId, "Message permanently failed after max retries");
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
this.inFlight += 1;
|
|
848
|
+
this.context.setState("processing");
|
|
849
|
+
try {
|
|
850
|
+
await this.onExecute(this.context, event);
|
|
851
|
+
this.retryTracker.markSuccess(messageId);
|
|
852
|
+
} catch (error) {
|
|
853
|
+
const label = error instanceof Error ? error.message : String(error);
|
|
854
|
+
this.logger.error("Sync message execution failed", {
|
|
855
|
+
roomId: this.roomId,
|
|
856
|
+
messageId,
|
|
857
|
+
error: label
|
|
858
|
+
});
|
|
859
|
+
} finally {
|
|
860
|
+
this.inFlight -= 1;
|
|
861
|
+
this.context.setState("idle");
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async executeEvent(event) {
|
|
865
|
+
this.inFlight += 1;
|
|
866
|
+
this.context.setState("processing");
|
|
867
|
+
try {
|
|
868
|
+
await this.onExecute(this.context, event);
|
|
869
|
+
} catch (error) {
|
|
870
|
+
if (this.onFailure) {
|
|
871
|
+
await this.onFailure(error, event);
|
|
872
|
+
} else {
|
|
873
|
+
this.logger.error("Execution queue task failed", {
|
|
874
|
+
roomId: this.roomId,
|
|
875
|
+
eventType: event.type,
|
|
876
|
+
error
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
this.running = false;
|
|
880
|
+
this.eventQueue.splice(0, this.eventQueue.length);
|
|
881
|
+
this.resolveEventWaiters(null);
|
|
882
|
+
throw error;
|
|
883
|
+
} finally {
|
|
884
|
+
this.inFlight -= 1;
|
|
885
|
+
this.context.setState("idle");
|
|
886
|
+
this.notifyIfIdle();
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async nextQueuedEvent() {
|
|
890
|
+
const queued = this.eventQueue.shift();
|
|
891
|
+
if (queued) {
|
|
892
|
+
return queued;
|
|
893
|
+
}
|
|
894
|
+
if (!this.running) {
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
return new Promise((resolve) => {
|
|
898
|
+
this.waiters.push(resolve);
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
notifyIfIdle() {
|
|
902
|
+
if (!this.isIdle()) {
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const waiters = [...this.idleWaiters];
|
|
906
|
+
this.idleWaiters.clear();
|
|
907
|
+
for (const waiter of waiters) {
|
|
908
|
+
waiter();
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
resolveEventWaiters(event) {
|
|
912
|
+
const waiters = this.waiters.splice(0, this.waiters.length);
|
|
913
|
+
for (const waiter of waiters) {
|
|
914
|
+
waiter(event);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
async markMessageFailed(messageId, error) {
|
|
918
|
+
try {
|
|
919
|
+
await this.link.markFailed(this.roomId, messageId, error, { bestEffort: true });
|
|
920
|
+
} catch {
|
|
921
|
+
this.logger.warn("Failed to mark message as failed on server", {
|
|
922
|
+
roomId: this.roomId,
|
|
923
|
+
messageId
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// src/runtime/tools/AgentTools.ts
|
|
930
|
+
var REQUIRED_ADAPTER_TOOL_METHODS = [
|
|
931
|
+
"sendMessage",
|
|
932
|
+
"sendEvent",
|
|
933
|
+
"addParticipant",
|
|
934
|
+
"removeParticipant",
|
|
935
|
+
"getParticipants",
|
|
936
|
+
"createChatroom",
|
|
937
|
+
"getToolSchemas",
|
|
938
|
+
"getAnthropicToolSchemas",
|
|
939
|
+
"getOpenAIToolSchemas",
|
|
940
|
+
"executeToolCall"
|
|
941
|
+
];
|
|
942
|
+
var OPTIONAL_ADAPTER_TOOL_METHODS = {
|
|
943
|
+
peers: ["lookupPeers"],
|
|
944
|
+
contacts: [
|
|
945
|
+
"listContacts",
|
|
946
|
+
"addContact",
|
|
947
|
+
"removeContact",
|
|
948
|
+
"listContactRequests",
|
|
949
|
+
"respondContactRequest"
|
|
950
|
+
],
|
|
951
|
+
memory: [
|
|
952
|
+
"listMemories",
|
|
953
|
+
"storeMemory",
|
|
954
|
+
"getMemory",
|
|
955
|
+
"supersedeMemory",
|
|
956
|
+
"archiveMemory"
|
|
957
|
+
]
|
|
958
|
+
};
|
|
959
|
+
var CONTACT_REQUEST_ACTIONS = /* @__PURE__ */ new Set([
|
|
960
|
+
"approve",
|
|
961
|
+
"reject",
|
|
962
|
+
"cancel"
|
|
963
|
+
]);
|
|
964
|
+
var MEMORY_SYSTEMS = /* @__PURE__ */ new Set([
|
|
965
|
+
"sensory",
|
|
966
|
+
"working",
|
|
967
|
+
"long_term"
|
|
968
|
+
]);
|
|
969
|
+
var MEMORY_TYPES = /* @__PURE__ */ new Set([
|
|
970
|
+
"iconic",
|
|
971
|
+
"echoic",
|
|
972
|
+
"haptic",
|
|
973
|
+
"episodic",
|
|
974
|
+
"semantic",
|
|
975
|
+
"procedural"
|
|
976
|
+
]);
|
|
977
|
+
var MEMORY_SEGMENTS = /* @__PURE__ */ new Set([
|
|
978
|
+
"user",
|
|
979
|
+
"agent",
|
|
980
|
+
"tool",
|
|
981
|
+
"guideline"
|
|
982
|
+
]);
|
|
983
|
+
var LIST_MEMORY_SCOPES = /* @__PURE__ */ new Set([
|
|
984
|
+
"subject",
|
|
985
|
+
"organization",
|
|
986
|
+
"all"
|
|
987
|
+
]);
|
|
988
|
+
var LIST_MEMORY_STATUSES = /* @__PURE__ */ new Set([
|
|
989
|
+
"active",
|
|
990
|
+
"superseded",
|
|
991
|
+
"archived",
|
|
992
|
+
"all"
|
|
993
|
+
]);
|
|
994
|
+
var MEMORY_SCOPES = /* @__PURE__ */ new Set([
|
|
995
|
+
"subject",
|
|
996
|
+
"organization"
|
|
997
|
+
]);
|
|
998
|
+
var AgentTools = class {
|
|
999
|
+
roomId;
|
|
1000
|
+
capabilities;
|
|
1001
|
+
rest;
|
|
1002
|
+
participants;
|
|
1003
|
+
adapterTools;
|
|
1004
|
+
toolHandlers;
|
|
1005
|
+
constructor(options) {
|
|
1006
|
+
this.roomId = options.roomId;
|
|
1007
|
+
this.rest = options.rest;
|
|
1008
|
+
this.participants = options.participants ?? [];
|
|
1009
|
+
this.capabilities = {
|
|
1010
|
+
...DEFAULT_AGENT_TOOLS_CAPABILITIES,
|
|
1011
|
+
...options.capabilities
|
|
1012
|
+
};
|
|
1013
|
+
this.toolHandlers = this.buildToolHandlers();
|
|
1014
|
+
this.adapterTools = this.buildAdapterTools();
|
|
1015
|
+
}
|
|
1016
|
+
getAdapterTools() {
|
|
1017
|
+
return this.adapterTools;
|
|
1018
|
+
}
|
|
1019
|
+
async sendMessage(content, mentions = []) {
|
|
1020
|
+
if (mentions.length > 0 && typeof mentions[0] === "string" && this.participants.length === 0) {
|
|
1021
|
+
await this.syncParticipants();
|
|
1022
|
+
}
|
|
1023
|
+
const resolvedMentions = this.resolveMentions(mentions);
|
|
1024
|
+
return this.rest.createChatMessage(
|
|
1025
|
+
this.roomId,
|
|
1026
|
+
{
|
|
1027
|
+
content,
|
|
1028
|
+
mentions: resolvedMentions
|
|
1029
|
+
},
|
|
1030
|
+
DEFAULT_REQUEST_OPTIONS
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
async sendEvent(content, messageType, metadata) {
|
|
1034
|
+
assertChatEventType(messageType);
|
|
1035
|
+
return this.rest.createChatEvent(
|
|
1036
|
+
this.roomId,
|
|
1037
|
+
{
|
|
1038
|
+
content,
|
|
1039
|
+
messageType,
|
|
1040
|
+
metadata
|
|
1041
|
+
},
|
|
1042
|
+
DEFAULT_REQUEST_OPTIONS
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
async createChatroom(taskId) {
|
|
1046
|
+
const room = await this.rest.createChat(taskId, DEFAULT_REQUEST_OPTIONS);
|
|
1047
|
+
return room.id;
|
|
1048
|
+
}
|
|
1049
|
+
async addParticipant(name, role = "member") {
|
|
1050
|
+
const existing = await this.syncParticipants();
|
|
1051
|
+
const alreadyInRoom = existing.find(
|
|
1052
|
+
(participant) => String(participant.name ?? "").toLowerCase() === name.toLowerCase()
|
|
1053
|
+
);
|
|
1054
|
+
if (alreadyInRoom) {
|
|
1055
|
+
return {
|
|
1056
|
+
...alreadyInRoom,
|
|
1057
|
+
status: "already_in_room"
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
const peer = await this.lookupPeerByName(name);
|
|
1061
|
+
if (!peer?.id) {
|
|
1062
|
+
throw new UnsupportedFeatureError(
|
|
1063
|
+
`Participant '${name}' not found. lookupPeers requires peer endpoint availability.`
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
await this.rest.addChatParticipant(
|
|
1067
|
+
this.roomId,
|
|
1068
|
+
{
|
|
1069
|
+
participantId: String(peer.id),
|
|
1070
|
+
role
|
|
1071
|
+
},
|
|
1072
|
+
DEFAULT_REQUEST_OPTIONS
|
|
1073
|
+
);
|
|
1074
|
+
const participantRecord = {
|
|
1075
|
+
id: String(peer.id),
|
|
1076
|
+
name,
|
|
1077
|
+
role,
|
|
1078
|
+
type: String(peer.type ?? "Agent"),
|
|
1079
|
+
handle: typeof peer.handle === "string" ? peer.handle : null
|
|
1080
|
+
};
|
|
1081
|
+
this.participants.push(participantRecord);
|
|
1082
|
+
return {
|
|
1083
|
+
...participantRecord,
|
|
1084
|
+
status: "added"
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
async removeParticipant(name) {
|
|
1088
|
+
const participants = await this.syncParticipants();
|
|
1089
|
+
const participant = participants.find(
|
|
1090
|
+
(entry) => String(entry.name ?? "").toLowerCase() === name.toLowerCase()
|
|
1091
|
+
);
|
|
1092
|
+
if (!participant?.id) {
|
|
1093
|
+
throw new ValidationError(`Participant '${name}' not found in room`);
|
|
1094
|
+
}
|
|
1095
|
+
await this.rest.removeChatParticipant(this.roomId, String(participant.id), DEFAULT_REQUEST_OPTIONS);
|
|
1096
|
+
this.replaceParticipants(this.participants.filter((entry) => entry.id !== participant.id));
|
|
1097
|
+
return {
|
|
1098
|
+
id: participant.id,
|
|
1099
|
+
name,
|
|
1100
|
+
status: "removed"
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
async lookupPeers(page = 1, pageSize = 50) {
|
|
1104
|
+
assertCapability(this.capabilities, "peers");
|
|
1105
|
+
if (!this.rest.listPeers) {
|
|
1106
|
+
throw new UnsupportedFeatureError(
|
|
1107
|
+
"Peer listing is not available in current REST adapter"
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
return this.rest.listPeers(
|
|
1111
|
+
{
|
|
1112
|
+
page,
|
|
1113
|
+
pageSize,
|
|
1114
|
+
notInChat: this.roomId
|
|
1115
|
+
},
|
|
1116
|
+
DEFAULT_REQUEST_OPTIONS
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
async getParticipants() {
|
|
1120
|
+
return this.fetchParticipants();
|
|
1121
|
+
}
|
|
1122
|
+
async fetchParticipants() {
|
|
1123
|
+
const participants = await this.rest.listChatParticipants(this.roomId, DEFAULT_REQUEST_OPTIONS);
|
|
1124
|
+
return participants.map((participant) => ({
|
|
1125
|
+
id: participant.id,
|
|
1126
|
+
name: participant.name,
|
|
1127
|
+
type: participant.type,
|
|
1128
|
+
handle: participant.handle ?? null
|
|
1129
|
+
}));
|
|
1130
|
+
}
|
|
1131
|
+
async syncParticipants() {
|
|
1132
|
+
const participants = await this.fetchParticipants();
|
|
1133
|
+
this.replaceParticipants(participants);
|
|
1134
|
+
return [...this.participants];
|
|
1135
|
+
}
|
|
1136
|
+
async executeToolCall(toolName, arguments_) {
|
|
1137
|
+
const validationError = validateToolArgs(toolName, arguments_);
|
|
1138
|
+
if (validationError) {
|
|
1139
|
+
return validationError;
|
|
1140
|
+
}
|
|
1141
|
+
const handler = this.toolHandlers[toolName];
|
|
1142
|
+
if (!handler) {
|
|
1143
|
+
return createToolExecutorError({
|
|
1144
|
+
errorType: "ToolNotFoundError",
|
|
1145
|
+
toolName,
|
|
1146
|
+
message: `Tool '${toolName}' is not registered`,
|
|
1147
|
+
legacyMessage: `Unknown tool: ${toolName}`
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
try {
|
|
1151
|
+
return await handler(arguments_);
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
if (isToolExecutorError(error)) {
|
|
1154
|
+
return error;
|
|
1155
|
+
}
|
|
1156
|
+
if (error instanceof ValidationError) {
|
|
1157
|
+
return createToolExecutorError({
|
|
1158
|
+
errorType: "ToolArgumentsValidationError",
|
|
1159
|
+
toolName,
|
|
1160
|
+
message: error.message,
|
|
1161
|
+
legacyMessage: `Invalid arguments for ${toolName}: ${error.message}`
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1165
|
+
return createToolExecutorError({
|
|
1166
|
+
errorType: "ToolExecutionError",
|
|
1167
|
+
toolName,
|
|
1168
|
+
message,
|
|
1169
|
+
legacyMessage: `Error executing ${toolName}: ${message}`
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
getToolSchemas(format, options) {
|
|
1174
|
+
const includeMemory = options?.includeMemory ?? false;
|
|
1175
|
+
const tools = Object.entries(TOOL_MODELS).filter(([name]) => {
|
|
1176
|
+
if (MEMORY_TOOL_NAMES.has(name)) {
|
|
1177
|
+
return includeMemory && this.capabilities.memory;
|
|
1178
|
+
}
|
|
1179
|
+
if (!CHAT_TOOL_NAMES.has(name) && !this.capabilities.contacts) {
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
if (name === "thenvoi_lookup_peers" && !this.capabilities.peers) {
|
|
1183
|
+
return false;
|
|
1184
|
+
}
|
|
1185
|
+
return true;
|
|
1186
|
+
}).map(([name, model]) => {
|
|
1187
|
+
if (format === "anthropic") {
|
|
1188
|
+
return {
|
|
1189
|
+
name,
|
|
1190
|
+
description: getToolDescription(name),
|
|
1191
|
+
input_schema: {
|
|
1192
|
+
type: "object",
|
|
1193
|
+
properties: model.properties,
|
|
1194
|
+
required: model.required
|
|
1195
|
+
}
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
return {
|
|
1199
|
+
type: "function",
|
|
1200
|
+
function: {
|
|
1201
|
+
name,
|
|
1202
|
+
description: getToolDescription(name),
|
|
1203
|
+
parameters: {
|
|
1204
|
+
type: "object",
|
|
1205
|
+
properties: model.properties,
|
|
1206
|
+
required: model.required
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
});
|
|
1211
|
+
return tools;
|
|
1212
|
+
}
|
|
1213
|
+
getAnthropicToolSchemas(options) {
|
|
1214
|
+
return this.getToolSchemas("anthropic", options);
|
|
1215
|
+
}
|
|
1216
|
+
getOpenAIToolSchemas(options) {
|
|
1217
|
+
return this.getToolSchemas("openai", options);
|
|
1218
|
+
}
|
|
1219
|
+
async listContacts(request = {}) {
|
|
1220
|
+
assertCapability(this.capabilities, "contacts");
|
|
1221
|
+
if (!this.rest.listContacts) {
|
|
1222
|
+
throw new UnsupportedFeatureError("Contact listing is not available in current REST adapter");
|
|
1223
|
+
}
|
|
1224
|
+
const page = request.page ?? 1;
|
|
1225
|
+
const pageSize = request.pageSize ?? 50;
|
|
1226
|
+
return this.rest.listContacts(
|
|
1227
|
+
{
|
|
1228
|
+
page,
|
|
1229
|
+
pageSize
|
|
1230
|
+
},
|
|
1231
|
+
DEFAULT_REQUEST_OPTIONS
|
|
1232
|
+
);
|
|
1233
|
+
}
|
|
1234
|
+
async addContact(request) {
|
|
1235
|
+
assertCapability(this.capabilities, "contacts");
|
|
1236
|
+
if (!this.rest.addContact) {
|
|
1237
|
+
throw new UnsupportedFeatureError("Contact creation is not available in current REST adapter");
|
|
1238
|
+
}
|
|
1239
|
+
const normalizedHandle = request.handle.trim();
|
|
1240
|
+
if (normalizedHandle.length === 0) {
|
|
1241
|
+
throw new ValidationError("handle is required");
|
|
1242
|
+
}
|
|
1243
|
+
return this.rest.addContact(
|
|
1244
|
+
{
|
|
1245
|
+
handle: normalizedHandle,
|
|
1246
|
+
...request.message ? { message: request.message } : {}
|
|
1247
|
+
},
|
|
1248
|
+
DEFAULT_REQUEST_OPTIONS
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
async removeContact(request) {
|
|
1252
|
+
assertCapability(this.capabilities, "contacts");
|
|
1253
|
+
if (!this.rest.removeContact) {
|
|
1254
|
+
throw new UnsupportedFeatureError("Contact removal is not available in current REST adapter");
|
|
1255
|
+
}
|
|
1256
|
+
if (request.target === "handle") {
|
|
1257
|
+
const handle = request.handle.trim();
|
|
1258
|
+
if (handle.length === 0) {
|
|
1259
|
+
throw new ValidationError("handle is required");
|
|
1260
|
+
}
|
|
1261
|
+
return this.rest.removeContact(
|
|
1262
|
+
{
|
|
1263
|
+
target: "handle",
|
|
1264
|
+
handle
|
|
1265
|
+
},
|
|
1266
|
+
DEFAULT_REQUEST_OPTIONS
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
const contactId = request.contactId.trim();
|
|
1270
|
+
if (contactId.length === 0) {
|
|
1271
|
+
throw new ValidationError("contactId is required");
|
|
1272
|
+
}
|
|
1273
|
+
return this.rest.removeContact(
|
|
1274
|
+
{
|
|
1275
|
+
target: "contactId",
|
|
1276
|
+
contactId
|
|
1277
|
+
},
|
|
1278
|
+
DEFAULT_REQUEST_OPTIONS
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
async listContactRequests(request = {}) {
|
|
1282
|
+
assertCapability(this.capabilities, "contacts");
|
|
1283
|
+
if (!this.rest.listContactRequests) {
|
|
1284
|
+
throw new UnsupportedFeatureError("Contact request listing is not available in current REST adapter");
|
|
1285
|
+
}
|
|
1286
|
+
const page = request.page ?? 1;
|
|
1287
|
+
const pageSize = request.pageSize ?? 50;
|
|
1288
|
+
const sentStatus = request.sentStatus ?? "pending";
|
|
1289
|
+
return this.rest.listContactRequests(
|
|
1290
|
+
{
|
|
1291
|
+
page,
|
|
1292
|
+
pageSize,
|
|
1293
|
+
sentStatus
|
|
1294
|
+
},
|
|
1295
|
+
DEFAULT_REQUEST_OPTIONS
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
async respondContactRequest(request) {
|
|
1299
|
+
assertCapability(this.capabilities, "contacts");
|
|
1300
|
+
if (!this.rest.respondContactRequest) {
|
|
1301
|
+
throw new UnsupportedFeatureError("Contact request responses are not available in current REST adapter");
|
|
1302
|
+
}
|
|
1303
|
+
if (request.target === "handle") {
|
|
1304
|
+
const handle = request.handle.trim();
|
|
1305
|
+
if (handle.length === 0) {
|
|
1306
|
+
throw new ValidationError("handle is required");
|
|
1307
|
+
}
|
|
1308
|
+
return this.rest.respondContactRequest(
|
|
1309
|
+
{
|
|
1310
|
+
action: request.action,
|
|
1311
|
+
target: "handle",
|
|
1312
|
+
handle
|
|
1313
|
+
},
|
|
1314
|
+
DEFAULT_REQUEST_OPTIONS
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
const requestId = request.requestId.trim();
|
|
1318
|
+
if (requestId.length === 0) {
|
|
1319
|
+
throw new ValidationError("requestId is required");
|
|
1320
|
+
}
|
|
1321
|
+
return this.rest.respondContactRequest(
|
|
1322
|
+
{
|
|
1323
|
+
action: request.action,
|
|
1324
|
+
target: "requestId",
|
|
1325
|
+
requestId
|
|
1326
|
+
},
|
|
1327
|
+
DEFAULT_REQUEST_OPTIONS
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
async listMemories(args = {}) {
|
|
1331
|
+
assertCapability(this.capabilities, "memory");
|
|
1332
|
+
if (!this.rest.listMemories) {
|
|
1333
|
+
throw new UnsupportedFeatureError("Memory listing is not available in current REST adapter");
|
|
1334
|
+
}
|
|
1335
|
+
return this.rest.listMemories(args, DEFAULT_REQUEST_OPTIONS);
|
|
1336
|
+
}
|
|
1337
|
+
async storeMemory(args) {
|
|
1338
|
+
assertCapability(this.capabilities, "memory");
|
|
1339
|
+
if (!this.rest.storeMemory) {
|
|
1340
|
+
throw new UnsupportedFeatureError("Memory creation is not available in current REST adapter");
|
|
1341
|
+
}
|
|
1342
|
+
return this.rest.storeMemory(args, DEFAULT_REQUEST_OPTIONS);
|
|
1343
|
+
}
|
|
1344
|
+
async getMemory(memoryId) {
|
|
1345
|
+
assertCapability(this.capabilities, "memory");
|
|
1346
|
+
if (!this.rest.getMemory) {
|
|
1347
|
+
throw new UnsupportedFeatureError("Memory lookup is not available in current REST adapter");
|
|
1348
|
+
}
|
|
1349
|
+
const normalizedMemoryId = memoryId.trim();
|
|
1350
|
+
if (normalizedMemoryId.length === 0) {
|
|
1351
|
+
throw new ValidationError("memoryId is required");
|
|
1352
|
+
}
|
|
1353
|
+
return this.rest.getMemory(normalizedMemoryId, DEFAULT_REQUEST_OPTIONS);
|
|
1354
|
+
}
|
|
1355
|
+
async supersedeMemory(memoryId) {
|
|
1356
|
+
assertCapability(this.capabilities, "memory");
|
|
1357
|
+
if (!this.rest.supersedeMemory) {
|
|
1358
|
+
throw new UnsupportedFeatureError("Memory supersede is not available in current REST adapter");
|
|
1359
|
+
}
|
|
1360
|
+
const normalizedMemoryId = memoryId.trim();
|
|
1361
|
+
if (normalizedMemoryId.length === 0) {
|
|
1362
|
+
throw new ValidationError("memoryId is required");
|
|
1363
|
+
}
|
|
1364
|
+
return this.rest.supersedeMemory(normalizedMemoryId, DEFAULT_REQUEST_OPTIONS);
|
|
1365
|
+
}
|
|
1366
|
+
async archiveMemory(memoryId) {
|
|
1367
|
+
assertCapability(this.capabilities, "memory");
|
|
1368
|
+
if (!this.rest.archiveMemory) {
|
|
1369
|
+
throw new UnsupportedFeatureError("Memory archive is not available in current REST adapter");
|
|
1370
|
+
}
|
|
1371
|
+
const normalizedMemoryId = memoryId.trim();
|
|
1372
|
+
if (normalizedMemoryId.length === 0) {
|
|
1373
|
+
throw new ValidationError("memoryId is required");
|
|
1374
|
+
}
|
|
1375
|
+
return this.rest.archiveMemory(normalizedMemoryId, DEFAULT_REQUEST_OPTIONS);
|
|
1376
|
+
}
|
|
1377
|
+
resolveMentions(mentions) {
|
|
1378
|
+
if (mentions.length === 0) {
|
|
1379
|
+
return [];
|
|
1380
|
+
}
|
|
1381
|
+
if (typeof mentions[0] !== "string") {
|
|
1382
|
+
return mentions.filter(
|
|
1383
|
+
(entry) => typeof entry === "object" && entry !== null && "id" in entry
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
const stringMentions = mentions.filter((entry) => typeof entry === "string");
|
|
1387
|
+
const participantsByHandle = /* @__PURE__ */ new Map();
|
|
1388
|
+
const participantsById = /* @__PURE__ */ new Map();
|
|
1389
|
+
const participantsByName = /* @__PURE__ */ new Map();
|
|
1390
|
+
for (const participant of this.participants) {
|
|
1391
|
+
const ref = {
|
|
1392
|
+
id: String(participant.id),
|
|
1393
|
+
handle: typeof participant.handle === "string" ? participant.handle : void 0
|
|
1394
|
+
};
|
|
1395
|
+
participantsById.set(ref.id, ref);
|
|
1396
|
+
const handle = participant.handle;
|
|
1397
|
+
if (typeof handle === "string") {
|
|
1398
|
+
participantsByHandle.set(this.normalizeMentionHandle(handle), ref);
|
|
1399
|
+
}
|
|
1400
|
+
const name = participant.name;
|
|
1401
|
+
if (typeof name === "string" && name.trim().length > 0) {
|
|
1402
|
+
participantsByName.set(name.trim().toLowerCase(), ref);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return stringMentions.map((mention) => {
|
|
1406
|
+
const byId = participantsById.get(mention);
|
|
1407
|
+
if (byId) {
|
|
1408
|
+
return byId;
|
|
1409
|
+
}
|
|
1410
|
+
const normalized = this.normalizeMentionHandle(mention);
|
|
1411
|
+
const found = participantsByHandle.get(normalized);
|
|
1412
|
+
if (found) {
|
|
1413
|
+
return found;
|
|
1414
|
+
}
|
|
1415
|
+
const byName = participantsByName.get(mention.trim().toLowerCase());
|
|
1416
|
+
if (byName) {
|
|
1417
|
+
return byName;
|
|
1418
|
+
}
|
|
1419
|
+
throw new ValidationError(`Mention '${mention}' not found in participants`);
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
async lookupPeerByName(name) {
|
|
1423
|
+
const target = name.trim().toLowerCase();
|
|
1424
|
+
const pageSize = 100;
|
|
1425
|
+
const maxPages = 25;
|
|
1426
|
+
for (let page = 1; page <= maxPages; page += 1) {
|
|
1427
|
+
const peers = await this.lookupPeers(page, pageSize);
|
|
1428
|
+
const items = peers.data ?? [];
|
|
1429
|
+
const match = items.find((peer) => String(peer.name ?? "").toLowerCase() === target);
|
|
1430
|
+
if (match) {
|
|
1431
|
+
return match;
|
|
1432
|
+
}
|
|
1433
|
+
const totalPages = peers.metadata?.totalPages;
|
|
1434
|
+
if (typeof totalPages === "number" && totalPages > 0 && page >= totalPages) {
|
|
1435
|
+
break;
|
|
1436
|
+
}
|
|
1437
|
+
if ((typeof totalPages !== "number" || totalPages <= 0) && items.length < pageSize) {
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
normalizeMentionHandle(handle) {
|
|
1444
|
+
return handle.trim().replace(/^@+/, "").toLowerCase();
|
|
1445
|
+
}
|
|
1446
|
+
replaceParticipants(participants) {
|
|
1447
|
+
this.participants.splice(0, this.participants.length, ...participants);
|
|
1448
|
+
}
|
|
1449
|
+
buildAdapterTools() {
|
|
1450
|
+
const tools = {
|
|
1451
|
+
capabilities: this.capabilities
|
|
1452
|
+
};
|
|
1453
|
+
for (const methodName of REQUIRED_ADAPTER_TOOL_METHODS) {
|
|
1454
|
+
tools[methodName] = this.bindAdapterToolMethod(methodName);
|
|
1455
|
+
}
|
|
1456
|
+
for (const [capabilityKey, methodNames] of Object.entries(OPTIONAL_ADAPTER_TOOL_METHODS)) {
|
|
1457
|
+
if (!this.capabilities[capabilityKey]) {
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
for (const methodName of methodNames) {
|
|
1461
|
+
tools[methodName] = this.bindAdapterToolMethod(methodName);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return Object.freeze(tools);
|
|
1465
|
+
}
|
|
1466
|
+
bindAdapterToolMethod(methodName) {
|
|
1467
|
+
const method = this[methodName];
|
|
1468
|
+
return method.bind(this);
|
|
1469
|
+
}
|
|
1470
|
+
buildToolHandlers() {
|
|
1471
|
+
return {
|
|
1472
|
+
...this.buildMessagingToolHandlers(),
|
|
1473
|
+
...this.buildContactToolHandlers(),
|
|
1474
|
+
...this.buildMemoryToolHandlers()
|
|
1475
|
+
};
|
|
1476
|
+
}
|
|
1477
|
+
buildMessagingToolHandlers() {
|
|
1478
|
+
return {
|
|
1479
|
+
thenvoi_send_message: async (arguments_) => this.sendMessage(
|
|
1480
|
+
String(arguments_.content ?? ""),
|
|
1481
|
+
this.normalizeMentionInput(arguments_.mentions)
|
|
1482
|
+
),
|
|
1483
|
+
thenvoi_send_event: async (arguments_) => this.sendEvent(
|
|
1484
|
+
String(arguments_.content ?? ""),
|
|
1485
|
+
String(arguments_.message_type ?? "task"),
|
|
1486
|
+
this.normalizeOptionalMetadata(arguments_.metadata)
|
|
1487
|
+
),
|
|
1488
|
+
thenvoi_add_participant: async (arguments_) => this.addParticipant(String(arguments_.name ?? ""), String(arguments_.role ?? "member")),
|
|
1489
|
+
thenvoi_remove_participant: async (arguments_) => this.removeParticipant(String(arguments_.name ?? "")),
|
|
1490
|
+
thenvoi_lookup_peers: async (arguments_) => this.lookupPeers(
|
|
1491
|
+
coercePositiveInt(arguments_.page, 1),
|
|
1492
|
+
coercePositiveInt(arguments_.page_size, 50)
|
|
1493
|
+
),
|
|
1494
|
+
thenvoi_get_participants: async () => this.getParticipants(),
|
|
1495
|
+
thenvoi_create_chatroom: async (arguments_) => this.createChatroom(this.normalizeOptionalString(arguments_.task_id))
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
buildContactToolHandlers() {
|
|
1499
|
+
return {
|
|
1500
|
+
thenvoi_list_contacts: async (arguments_) => this.listContacts({
|
|
1501
|
+
page: coercePositiveInt(arguments_.page, 1),
|
|
1502
|
+
pageSize: coercePositiveInt(arguments_.page_size, 50)
|
|
1503
|
+
}),
|
|
1504
|
+
thenvoi_add_contact: async (arguments_) => this.addContact({
|
|
1505
|
+
handle: String(arguments_.handle ?? ""),
|
|
1506
|
+
...typeof arguments_.message === "string" ? { message: arguments_.message } : {}
|
|
1507
|
+
}),
|
|
1508
|
+
thenvoi_remove_contact: async (arguments_) => this.removeContact(this.toRemoveContactArgs(arguments_)),
|
|
1509
|
+
thenvoi_list_contact_requests: async (arguments_) => this.listContactRequests({
|
|
1510
|
+
page: coercePositiveInt(arguments_.page, 1),
|
|
1511
|
+
pageSize: coercePositiveInt(arguments_.page_size, 50),
|
|
1512
|
+
sentStatus: String(arguments_.sent_status ?? "pending")
|
|
1513
|
+
}),
|
|
1514
|
+
thenvoi_respond_contact_request: async (arguments_) => this.respondContactRequest(this.toRespondContactRequestArgs(arguments_))
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
buildMemoryToolHandlers() {
|
|
1518
|
+
return {
|
|
1519
|
+
thenvoi_list_memories: async (arguments_) => this.listMemories(this.toListMemoriesArgs(arguments_)),
|
|
1520
|
+
thenvoi_store_memory: async (arguments_) => this.storeMemory(this.toStoreMemoryArgs(arguments_)),
|
|
1521
|
+
thenvoi_get_memory: async (arguments_) => this.getMemory(String(arguments_.memory_id ?? "")),
|
|
1522
|
+
thenvoi_supersede_memory: async (arguments_) => this.supersedeMemory(String(arguments_.memory_id ?? "")),
|
|
1523
|
+
thenvoi_archive_memory: async (arguments_) => this.archiveMemory(String(arguments_.memory_id ?? ""))
|
|
1524
|
+
};
|
|
1525
|
+
}
|
|
1526
|
+
toRemoveContactArgs(arguments_) {
|
|
1527
|
+
const handle = this.normalizeOptionalString(arguments_.handle);
|
|
1528
|
+
const contactId = this.normalizeOptionalString(arguments_.contact_id);
|
|
1529
|
+
if (handle && contactId || !handle && !contactId) {
|
|
1530
|
+
throw new ValidationError("Provide exactly one of handle or contact_id");
|
|
1531
|
+
}
|
|
1532
|
+
if (handle) {
|
|
1533
|
+
return {
|
|
1534
|
+
target: "handle",
|
|
1535
|
+
handle
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
return {
|
|
1539
|
+
target: "contactId",
|
|
1540
|
+
contactId
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
toRespondContactRequestArgs(arguments_) {
|
|
1544
|
+
const actionValue = this.normalizeOptionalString(arguments_.action);
|
|
1545
|
+
if (!actionValue || !isContactRequestAction(actionValue)) {
|
|
1546
|
+
throw new ValidationError("action must be one of: approve, reject, cancel");
|
|
1547
|
+
}
|
|
1548
|
+
const handle = this.normalizeOptionalString(arguments_.handle);
|
|
1549
|
+
const requestId = this.normalizeOptionalString(arguments_.request_id);
|
|
1550
|
+
if (handle && requestId || !handle && !requestId) {
|
|
1551
|
+
throw new ValidationError("Provide exactly one of handle or request_id");
|
|
1552
|
+
}
|
|
1553
|
+
if (handle) {
|
|
1554
|
+
return {
|
|
1555
|
+
action: actionValue,
|
|
1556
|
+
target: "handle",
|
|
1557
|
+
handle
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
return {
|
|
1561
|
+
action: actionValue,
|
|
1562
|
+
target: "requestId",
|
|
1563
|
+
requestId
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
toListMemoriesArgs(arguments_) {
|
|
1567
|
+
const pageSize = this.normalizeOptionalNumber(arguments_.page_size);
|
|
1568
|
+
const scope = this.normalizeOptionalMemoryScope(arguments_.scope);
|
|
1569
|
+
const system = this.normalizeOptionalMemorySystem(arguments_.system);
|
|
1570
|
+
const type = this.normalizeOptionalMemoryType(arguments_.type);
|
|
1571
|
+
const segment = this.normalizeOptionalMemorySegment(arguments_.segment);
|
|
1572
|
+
const status = this.normalizeOptionalMemoryStatus(arguments_.status);
|
|
1573
|
+
const subjectId = this.normalizeOptionalString(arguments_.subject_id);
|
|
1574
|
+
const contentQuery = this.normalizeOptionalString(arguments_.content_query);
|
|
1575
|
+
return {
|
|
1576
|
+
...subjectId ? { subject_id: subjectId } : {},
|
|
1577
|
+
...scope ? { scope } : {},
|
|
1578
|
+
...system ? { system } : {},
|
|
1579
|
+
...type ? { type } : {},
|
|
1580
|
+
...segment ? { segment } : {},
|
|
1581
|
+
...contentQuery ? { content_query: contentQuery } : {},
|
|
1582
|
+
...typeof pageSize === "number" ? { page_size: pageSize } : {},
|
|
1583
|
+
...status ? { status } : {}
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
toStoreMemoryArgs(arguments_) {
|
|
1587
|
+
const content = this.normalizeRequiredString(arguments_.content, "content");
|
|
1588
|
+
const thought = this.normalizeRequiredString(arguments_.thought, "thought");
|
|
1589
|
+
const system = this.normalizeOptionalMemorySystem(arguments_.system);
|
|
1590
|
+
const type = this.normalizeOptionalMemoryType(arguments_.type);
|
|
1591
|
+
const segment = this.normalizeOptionalMemorySegment(arguments_.segment);
|
|
1592
|
+
const scope = this.normalizeOptionalStoreMemoryScope(arguments_.scope);
|
|
1593
|
+
const subjectId = this.normalizeOptionalString(arguments_.subject_id);
|
|
1594
|
+
const metadata = this.normalizeOptionalMetadata(arguments_.metadata);
|
|
1595
|
+
if (!system) {
|
|
1596
|
+
throw new ValidationError("system must be one of: sensory, working, long_term");
|
|
1597
|
+
}
|
|
1598
|
+
if (!type) {
|
|
1599
|
+
throw new ValidationError("type must be one of: iconic, echoic, haptic, episodic, semantic, procedural");
|
|
1600
|
+
}
|
|
1601
|
+
if (!segment) {
|
|
1602
|
+
throw new ValidationError("segment must be one of: user, agent, tool, guideline");
|
|
1603
|
+
}
|
|
1604
|
+
return {
|
|
1605
|
+
content,
|
|
1606
|
+
thought,
|
|
1607
|
+
system,
|
|
1608
|
+
type,
|
|
1609
|
+
segment,
|
|
1610
|
+
...scope ? { scope } : {},
|
|
1611
|
+
...subjectId ? { subject_id: subjectId } : {},
|
|
1612
|
+
...metadata ? { metadata } : {}
|
|
1613
|
+
};
|
|
1614
|
+
}
|
|
1615
|
+
normalizeOptionalString(value) {
|
|
1616
|
+
if (typeof value !== "string") {
|
|
1617
|
+
return void 0;
|
|
1618
|
+
}
|
|
1619
|
+
const normalized = value.trim();
|
|
1620
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
1621
|
+
}
|
|
1622
|
+
normalizeRequiredString(value, fieldName) {
|
|
1623
|
+
const normalized = this.normalizeOptionalString(value);
|
|
1624
|
+
if (!normalized) {
|
|
1625
|
+
throw new ValidationError(`${fieldName} is required`);
|
|
1626
|
+
}
|
|
1627
|
+
return normalized;
|
|
1628
|
+
}
|
|
1629
|
+
normalizeOptionalNumber(value) {
|
|
1630
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1631
|
+
return value;
|
|
1632
|
+
}
|
|
1633
|
+
if (typeof value === "string") {
|
|
1634
|
+
const parsed = Number(value);
|
|
1635
|
+
if (Number.isFinite(parsed)) {
|
|
1636
|
+
return parsed;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
return void 0;
|
|
1640
|
+
}
|
|
1641
|
+
normalizeOptionalMetadata(value) {
|
|
1642
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1643
|
+
return void 0;
|
|
1644
|
+
}
|
|
1645
|
+
return value;
|
|
1646
|
+
}
|
|
1647
|
+
normalizeMentionInput(value) {
|
|
1648
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
1649
|
+
return [];
|
|
1650
|
+
}
|
|
1651
|
+
if (typeof value[0] === "string") {
|
|
1652
|
+
return value.filter((entry) => typeof entry === "string");
|
|
1653
|
+
}
|
|
1654
|
+
const mentions = [];
|
|
1655
|
+
for (const entry of value) {
|
|
1656
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1657
|
+
continue;
|
|
1658
|
+
}
|
|
1659
|
+
const mention = entry;
|
|
1660
|
+
if (typeof mention.id !== "string") {
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
const normalized = { id: mention.id };
|
|
1664
|
+
if (typeof mention.handle === "string") {
|
|
1665
|
+
normalized.handle = mention.handle;
|
|
1666
|
+
}
|
|
1667
|
+
if (typeof mention.name === "string") {
|
|
1668
|
+
normalized.name = mention.name;
|
|
1669
|
+
}
|
|
1670
|
+
if (typeof mention.username === "string") {
|
|
1671
|
+
normalized.username = mention.username;
|
|
1672
|
+
}
|
|
1673
|
+
mentions.push(normalized);
|
|
1674
|
+
}
|
|
1675
|
+
return mentions;
|
|
1676
|
+
}
|
|
1677
|
+
normalizeOptionalMemoryScope(value) {
|
|
1678
|
+
const normalized = this.normalizeOptionalString(value);
|
|
1679
|
+
if (!normalized) {
|
|
1680
|
+
return void 0;
|
|
1681
|
+
}
|
|
1682
|
+
if (!isListMemoryScope(normalized)) {
|
|
1683
|
+
throw new ValidationError("scope must be one of: subject, organization, all");
|
|
1684
|
+
}
|
|
1685
|
+
return normalized;
|
|
1686
|
+
}
|
|
1687
|
+
normalizeOptionalMemorySystem(value) {
|
|
1688
|
+
const normalized = this.normalizeOptionalString(value);
|
|
1689
|
+
if (!normalized) {
|
|
1690
|
+
return void 0;
|
|
1691
|
+
}
|
|
1692
|
+
if (!isMemorySystem(normalized)) {
|
|
1693
|
+
throw new ValidationError("system must be one of: sensory, working, long_term");
|
|
1694
|
+
}
|
|
1695
|
+
return normalized;
|
|
1696
|
+
}
|
|
1697
|
+
normalizeOptionalMemoryType(value) {
|
|
1698
|
+
const normalized = this.normalizeOptionalString(value);
|
|
1699
|
+
if (!normalized) {
|
|
1700
|
+
return void 0;
|
|
1701
|
+
}
|
|
1702
|
+
if (!isMemoryType(normalized)) {
|
|
1703
|
+
throw new ValidationError("type must be one of: iconic, echoic, haptic, episodic, semantic, procedural");
|
|
1704
|
+
}
|
|
1705
|
+
return normalized;
|
|
1706
|
+
}
|
|
1707
|
+
normalizeOptionalMemorySegment(value) {
|
|
1708
|
+
const normalized = this.normalizeOptionalString(value);
|
|
1709
|
+
if (!normalized) {
|
|
1710
|
+
return void 0;
|
|
1711
|
+
}
|
|
1712
|
+
if (!isMemorySegment(normalized)) {
|
|
1713
|
+
throw new ValidationError("segment must be one of: user, agent, tool, guideline");
|
|
1714
|
+
}
|
|
1715
|
+
return normalized;
|
|
1716
|
+
}
|
|
1717
|
+
normalizeOptionalMemoryStatus(value) {
|
|
1718
|
+
const normalized = this.normalizeOptionalString(value);
|
|
1719
|
+
if (!normalized) {
|
|
1720
|
+
return void 0;
|
|
1721
|
+
}
|
|
1722
|
+
if (!isMemoryStatus(normalized)) {
|
|
1723
|
+
throw new ValidationError("status must be one of: active, superseded, archived, all");
|
|
1724
|
+
}
|
|
1725
|
+
return normalized;
|
|
1726
|
+
}
|
|
1727
|
+
normalizeOptionalStoreMemoryScope(value) {
|
|
1728
|
+
const normalized = this.normalizeOptionalString(value);
|
|
1729
|
+
if (!normalized) {
|
|
1730
|
+
return void 0;
|
|
1731
|
+
}
|
|
1732
|
+
if (!isStoreMemoryScope(normalized)) {
|
|
1733
|
+
throw new ValidationError("scope must be one of: subject, organization");
|
|
1734
|
+
}
|
|
1735
|
+
return normalized;
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
function isContactRequestAction(value) {
|
|
1739
|
+
return CONTACT_REQUEST_ACTIONS.has(value);
|
|
1740
|
+
}
|
|
1741
|
+
function isMemorySystem(value) {
|
|
1742
|
+
return MEMORY_SYSTEMS.has(value);
|
|
1743
|
+
}
|
|
1744
|
+
function isMemoryType(value) {
|
|
1745
|
+
return MEMORY_TYPES.has(value);
|
|
1746
|
+
}
|
|
1747
|
+
function isMemorySegment(value) {
|
|
1748
|
+
return MEMORY_SEGMENTS.has(value);
|
|
1749
|
+
}
|
|
1750
|
+
function isStoreMemoryScope(value) {
|
|
1751
|
+
return MEMORY_SCOPES.has(value);
|
|
1752
|
+
}
|
|
1753
|
+
function isListMemoryScope(value) {
|
|
1754
|
+
return LIST_MEMORY_SCOPES.has(value);
|
|
1755
|
+
}
|
|
1756
|
+
function isMemoryStatus(value) {
|
|
1757
|
+
return LIST_MEMORY_STATUSES.has(value);
|
|
1758
|
+
}
|
|
1759
|
+
function coercePositiveInt(value, fallback) {
|
|
1760
|
+
const num = Number(value ?? fallback);
|
|
1761
|
+
return Number.isFinite(num) && num >= 1 ? Math.floor(num) : fallback;
|
|
1762
|
+
}
|
|
1763
|
+
function validateToolArgs(toolName, args) {
|
|
1764
|
+
const errors = [];
|
|
1765
|
+
const model = TOOL_MODELS[toolName];
|
|
1766
|
+
if (!model) {
|
|
1767
|
+
return null;
|
|
1768
|
+
}
|
|
1769
|
+
for (const field of model.required) {
|
|
1770
|
+
if (args[field] === void 0 || args[field] === null) {
|
|
1771
|
+
errors.push(`${field}: Field required`);
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
if (toolName === "thenvoi_send_message") {
|
|
1775
|
+
const mentions = args.mentions;
|
|
1776
|
+
if (Array.isArray(mentions) && mentions.length === 0) {
|
|
1777
|
+
errors.push("mentions: At least one mention is required");
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
if (toolName === "thenvoi_send_event") {
|
|
1781
|
+
const messageType = args.message_type;
|
|
1782
|
+
if (typeof messageType === "string" && !CHAT_EVENT_TYPES.includes(messageType)) {
|
|
1783
|
+
errors.push(
|
|
1784
|
+
`message_type: Invalid value '${messageType}'. Expected one of: ${[...CHAT_EVENT_TYPES].join(", ")}`
|
|
1785
|
+
);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
if (toolName === "thenvoi_respond_contact_request") {
|
|
1789
|
+
const action = args.action;
|
|
1790
|
+
const validActions = ["approve", "reject", "cancel"];
|
|
1791
|
+
if (typeof action === "string" && !validActions.includes(action)) {
|
|
1792
|
+
errors.push(
|
|
1793
|
+
`action: Invalid value '${action}'. Expected one of: ${validActions.join(", ")}`
|
|
1794
|
+
);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
if (toolName === "thenvoi_store_memory") {
|
|
1798
|
+
const validSystems = ["sensory", "working", "long_term"];
|
|
1799
|
+
const validTypes = ["iconic", "echoic", "haptic", "episodic", "semantic", "procedural"];
|
|
1800
|
+
const validSegments = ["user", "agent", "tool", "guideline"];
|
|
1801
|
+
if (typeof args.system === "string" && !validSystems.includes(args.system)) {
|
|
1802
|
+
errors.push(`system: Invalid value '${args.system}'. Expected one of: ${validSystems.join(", ")}`);
|
|
1803
|
+
}
|
|
1804
|
+
if (typeof args.type === "string" && !validTypes.includes(args.type)) {
|
|
1805
|
+
errors.push(`type: Invalid value '${args.type}'. Expected one of: ${validTypes.join(", ")}`);
|
|
1806
|
+
}
|
|
1807
|
+
if (typeof args.segment === "string" && !validSegments.includes(args.segment)) {
|
|
1808
|
+
errors.push(`segment: Invalid value '${args.segment}'. Expected one of: ${validSegments.join(", ")}`);
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
if (errors.length > 0) {
|
|
1812
|
+
const message = `Invalid arguments for ${toolName}: ${errors.join("; ")}`;
|
|
1813
|
+
return createToolExecutorError({
|
|
1814
|
+
errorType: "ToolArgumentsValidationError",
|
|
1815
|
+
toolName,
|
|
1816
|
+
message,
|
|
1817
|
+
legacyMessage: message,
|
|
1818
|
+
details: { validationErrors: errors }
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
return null;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// src/runtime/retryTracker.ts
|
|
1825
|
+
var DEFAULT_MAX_TRACKED = 1e4;
|
|
1826
|
+
var MessageRetryTracker = class {
|
|
1827
|
+
maxRetriesValue;
|
|
1828
|
+
maxTracked;
|
|
1829
|
+
attempts = /* @__PURE__ */ new Map();
|
|
1830
|
+
failed = /* @__PURE__ */ new Set();
|
|
1831
|
+
constructor(maxRetries = 1, maxTracked = DEFAULT_MAX_TRACKED) {
|
|
1832
|
+
this.maxRetriesValue = maxRetries;
|
|
1833
|
+
this.maxTracked = maxTracked;
|
|
1834
|
+
}
|
|
1835
|
+
get maxRetries() {
|
|
1836
|
+
return this.maxRetriesValue;
|
|
1837
|
+
}
|
|
1838
|
+
isPermanentlyFailed(messageId) {
|
|
1839
|
+
return this.failed.has(messageId);
|
|
1840
|
+
}
|
|
1841
|
+
recordAttempt(messageId) {
|
|
1842
|
+
const attempts = (this.attempts.get(messageId) ?? 0) + 1;
|
|
1843
|
+
this.attempts.set(messageId, attempts);
|
|
1844
|
+
const exceeded = attempts > this.maxRetriesValue;
|
|
1845
|
+
if (exceeded) {
|
|
1846
|
+
this.evictSetIfNeeded(this.failed);
|
|
1847
|
+
this.failed.add(messageId);
|
|
1848
|
+
}
|
|
1849
|
+
this.evictMapIfNeeded(this.attempts);
|
|
1850
|
+
return [attempts, exceeded];
|
|
1851
|
+
}
|
|
1852
|
+
markSuccess(messageId) {
|
|
1853
|
+
this.attempts.delete(messageId);
|
|
1854
|
+
}
|
|
1855
|
+
markPermanentlyFailed(messageId) {
|
|
1856
|
+
this.evictSetIfNeeded(this.failed);
|
|
1857
|
+
this.failed.add(messageId);
|
|
1858
|
+
}
|
|
1859
|
+
evictMapIfNeeded(map) {
|
|
1860
|
+
while (map.size >= this.maxTracked) {
|
|
1861
|
+
const first = map.keys().next();
|
|
1862
|
+
if (first.done) break;
|
|
1863
|
+
map.delete(first.value);
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
evictSetIfNeeded(set) {
|
|
1867
|
+
while (set.size >= this.maxTracked) {
|
|
1868
|
+
const first = set.values().next();
|
|
1869
|
+
if (first.done) break;
|
|
1870
|
+
set.delete(first.value);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
|
|
1875
|
+
// src/runtime/ExecutionContext.ts
|
|
1876
|
+
var DEDUP_CACHE_MAX = 500;
|
|
1877
|
+
var ExecutionContext = class {
|
|
1878
|
+
roomId;
|
|
1879
|
+
link;
|
|
1880
|
+
maxContextMessages;
|
|
1881
|
+
enableContextCache;
|
|
1882
|
+
contextCacheTtlMs;
|
|
1883
|
+
enableContextHydration;
|
|
1884
|
+
history = [];
|
|
1885
|
+
messageIds = /* @__PURE__ */ new Set();
|
|
1886
|
+
dedupCache = /* @__PURE__ */ new Map();
|
|
1887
|
+
participants = [];
|
|
1888
|
+
tools;
|
|
1889
|
+
adapterTools;
|
|
1890
|
+
participantsMessage = null;
|
|
1891
|
+
lastSentParticipantIds = null;
|
|
1892
|
+
contactsMessage = null;
|
|
1893
|
+
contextCache = null;
|
|
1894
|
+
contextCacheExpiresAt = 0;
|
|
1895
|
+
_state = "starting";
|
|
1896
|
+
retryTrackerInstance;
|
|
1897
|
+
_llmInitialized = false;
|
|
1898
|
+
_pendingSystemMessages = [];
|
|
1899
|
+
constructor(options) {
|
|
1900
|
+
this.roomId = options.roomId;
|
|
1901
|
+
this.link = options.link;
|
|
1902
|
+
this.maxContextMessages = options.maxContextMessages;
|
|
1903
|
+
this.enableContextCache = options.enableContextCache ?? true;
|
|
1904
|
+
this.contextCacheTtlMs = Math.max(0, (options.contextCacheTtlSeconds ?? 300) * 1e3);
|
|
1905
|
+
this.enableContextHydration = options.enableContextHydration ?? true;
|
|
1906
|
+
this.retryTrackerInstance = new MessageRetryTracker(options.maxMessageRetries ?? 1);
|
|
1907
|
+
this.tools = new AgentTools({
|
|
1908
|
+
roomId: this.roomId,
|
|
1909
|
+
rest: this.link.rest,
|
|
1910
|
+
participants: this.participants,
|
|
1911
|
+
capabilities: this.link.capabilities
|
|
1912
|
+
});
|
|
1913
|
+
this.adapterTools = this.tools.getAdapterTools();
|
|
1914
|
+
}
|
|
1915
|
+
get state() {
|
|
1916
|
+
return this._state;
|
|
1917
|
+
}
|
|
1918
|
+
setState(state) {
|
|
1919
|
+
this._state = state;
|
|
1920
|
+
}
|
|
1921
|
+
getRetryTracker() {
|
|
1922
|
+
return this.retryTrackerInstance;
|
|
1923
|
+
}
|
|
1924
|
+
get isLlmInitialized() {
|
|
1925
|
+
return this._llmInitialized;
|
|
1926
|
+
}
|
|
1927
|
+
markLlmInitialized() {
|
|
1928
|
+
this._llmInitialized = true;
|
|
1929
|
+
}
|
|
1930
|
+
injectSystemMessage(message) {
|
|
1931
|
+
this._pendingSystemMessages.push(message);
|
|
1932
|
+
}
|
|
1933
|
+
consumeSystemMessages() {
|
|
1934
|
+
const copy = [...this._pendingSystemMessages];
|
|
1935
|
+
this._pendingSystemMessages.length = 0;
|
|
1936
|
+
return copy;
|
|
1937
|
+
}
|
|
1938
|
+
getTools() {
|
|
1939
|
+
return this.adapterTools;
|
|
1940
|
+
}
|
|
1941
|
+
hasMessage(messageId) {
|
|
1942
|
+
return this.messageIds.has(messageId) || this.dedupCache.has(messageId);
|
|
1943
|
+
}
|
|
1944
|
+
recordMessage(message) {
|
|
1945
|
+
if (this.hasMessage(message.id)) {
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
this.trackDedup(message.id);
|
|
1949
|
+
this.history.push(message);
|
|
1950
|
+
if (this.history.length > this.maxContextMessages) {
|
|
1951
|
+
this.history.splice(0, this.history.length - this.maxContextMessages);
|
|
1952
|
+
}
|
|
1953
|
+
this.rebuildMessageIds();
|
|
1954
|
+
if (this.contextCache) {
|
|
1955
|
+
this.contextCache.messages.push(this.toHistoryEntry(message));
|
|
1956
|
+
if (this.contextCache.messages.length > this.maxContextMessages) {
|
|
1957
|
+
this.contextCache.messages.splice(0, this.contextCache.messages.length - this.maxContextMessages);
|
|
1958
|
+
}
|
|
1959
|
+
this.contextCache.hydratedAt = /* @__PURE__ */ new Date();
|
|
1960
|
+
this.contextCacheExpiresAt = this.nextCacheExpiry();
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
getRawHistory() {
|
|
1964
|
+
return this.history.map((entry) => ({
|
|
1965
|
+
id: entry.id,
|
|
1966
|
+
room_id: entry.roomId,
|
|
1967
|
+
content: entry.content,
|
|
1968
|
+
sender_id: entry.senderId,
|
|
1969
|
+
sender_type: entry.senderType,
|
|
1970
|
+
sender_name: entry.senderName,
|
|
1971
|
+
message_type: entry.messageType,
|
|
1972
|
+
metadata: entry.metadata,
|
|
1973
|
+
created_at: entry.createdAt.toISOString(),
|
|
1974
|
+
role: entry.senderType === "User" ? "user" : "assistant"
|
|
1975
|
+
}));
|
|
1976
|
+
}
|
|
1977
|
+
setParticipants(participants) {
|
|
1978
|
+
this.replaceParticipants(participants);
|
|
1979
|
+
this.updateCachedParticipants();
|
|
1980
|
+
}
|
|
1981
|
+
addParticipant(participant) {
|
|
1982
|
+
const existingIndex = this.participants.findIndex((entry) => entry.id === participant.id);
|
|
1983
|
+
if (existingIndex >= 0) {
|
|
1984
|
+
this.participants.splice(existingIndex, 1);
|
|
1985
|
+
}
|
|
1986
|
+
this.participants.push(participant);
|
|
1987
|
+
const name = String(participant.name ?? "unknown");
|
|
1988
|
+
this.participantsMessage = `${name} joined the room.`;
|
|
1989
|
+
this.updateCachedParticipants();
|
|
1990
|
+
}
|
|
1991
|
+
removeParticipant(participantId) {
|
|
1992
|
+
const removed = this.participants.find((entry) => String(entry.id) === participantId);
|
|
1993
|
+
const next = this.participants.filter((entry) => String(entry.id) !== participantId);
|
|
1994
|
+
this.replaceParticipants(next);
|
|
1995
|
+
if (removed) {
|
|
1996
|
+
this.participantsMessage = `${String(removed.name ?? participantId)} left the room.`;
|
|
1997
|
+
}
|
|
1998
|
+
this.updateCachedParticipants();
|
|
1999
|
+
}
|
|
2000
|
+
setContactsMessage(message) {
|
|
2001
|
+
this.contactsMessage = message;
|
|
2002
|
+
}
|
|
2003
|
+
consumeParticipantsMessage() {
|
|
2004
|
+
const currentIds = new Set(this.participants.map((p) => String(p.id)));
|
|
2005
|
+
let changed = !this.lastSentParticipantIds || currentIds.size !== this.lastSentParticipantIds.size;
|
|
2006
|
+
if (!changed) {
|
|
2007
|
+
for (const id of currentIds) {
|
|
2008
|
+
if (!this.lastSentParticipantIds.has(id)) {
|
|
2009
|
+
changed = true;
|
|
2010
|
+
break;
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
if (!changed && !this.participantsMessage) {
|
|
2015
|
+
return null;
|
|
2016
|
+
}
|
|
2017
|
+
const parts = [];
|
|
2018
|
+
if (this.participantsMessage) {
|
|
2019
|
+
parts.push(this.participantsMessage);
|
|
2020
|
+
this.participantsMessage = null;
|
|
2021
|
+
}
|
|
2022
|
+
if (changed) {
|
|
2023
|
+
const asRecords = this.participants.map((p) => ({
|
|
2024
|
+
id: p.id,
|
|
2025
|
+
name: p.name,
|
|
2026
|
+
type: p.type,
|
|
2027
|
+
handle: p.handle ?? null
|
|
2028
|
+
}));
|
|
2029
|
+
parts.push(buildParticipantsMessage(asRecords));
|
|
2030
|
+
}
|
|
2031
|
+
this.lastSentParticipantIds = currentIds;
|
|
2032
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
2033
|
+
}
|
|
2034
|
+
consumeContactsMessage() {
|
|
2035
|
+
const value = this.contactsMessage;
|
|
2036
|
+
this.contactsMessage = null;
|
|
2037
|
+
return value;
|
|
2038
|
+
}
|
|
2039
|
+
async getHydratedHistory(excludeMessageId) {
|
|
2040
|
+
if (!this.enableContextHydration || !this.link.rest.getChatContext) {
|
|
2041
|
+
return this.getRawHistory().filter((entry) => entry.id !== excludeMessageId);
|
|
2042
|
+
}
|
|
2043
|
+
try {
|
|
2044
|
+
const context = await this.hydrateContext();
|
|
2045
|
+
return context.messages.filter((entry) => entry.id !== excludeMessageId);
|
|
2046
|
+
} catch (error) {
|
|
2047
|
+
if (error instanceof UnsupportedFeatureError) {
|
|
2048
|
+
return this.getRawHistory().filter((entry) => entry.id !== excludeMessageId);
|
|
2049
|
+
}
|
|
2050
|
+
throw error;
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
async hydrateContext(forceRefresh = false) {
|
|
2054
|
+
if (!this.enableContextHydration || !this.link.rest.getChatContext) {
|
|
2055
|
+
return this.buildLocalContext();
|
|
2056
|
+
}
|
|
2057
|
+
if (!forceRefresh && this.enableContextCache && this.contextCache && !this.isCacheExpired()) {
|
|
2058
|
+
return this.contextCache;
|
|
2059
|
+
}
|
|
2060
|
+
try {
|
|
2061
|
+
const participants = await this.loadParticipants();
|
|
2062
|
+
const messages = await this.loadHydratedMessages();
|
|
2063
|
+
const context = {
|
|
2064
|
+
roomId: this.roomId,
|
|
2065
|
+
messages,
|
|
2066
|
+
participants,
|
|
2067
|
+
hydratedAt: /* @__PURE__ */ new Date()
|
|
2068
|
+
};
|
|
2069
|
+
this.contextCache = context;
|
|
2070
|
+
this.contextCacheExpiresAt = this.nextCacheExpiry();
|
|
2071
|
+
return context;
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
if (!(error instanceof UnsupportedFeatureError)) {
|
|
2074
|
+
throw error;
|
|
2075
|
+
}
|
|
2076
|
+
const fallback = this.buildLocalContext();
|
|
2077
|
+
this.contextCache = fallback;
|
|
2078
|
+
this.contextCacheExpiresAt = this.nextCacheExpiry();
|
|
2079
|
+
return fallback;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
replaceParticipants(participants) {
|
|
2083
|
+
this.participants.splice(0, this.participants.length, ...participants);
|
|
2084
|
+
}
|
|
2085
|
+
buildLocalContext() {
|
|
2086
|
+
return {
|
|
2087
|
+
roomId: this.roomId,
|
|
2088
|
+
messages: this.getRawHistory(),
|
|
2089
|
+
participants: [...this.participants],
|
|
2090
|
+
hydratedAt: /* @__PURE__ */ new Date()
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
async loadParticipants() {
|
|
2094
|
+
const participants = await this.link.rest.listChatParticipants(this.roomId, DEFAULT_REQUEST_OPTIONS);
|
|
2095
|
+
const normalized = participants.map((participant) => ({
|
|
2096
|
+
id: participant.id,
|
|
2097
|
+
name: participant.name,
|
|
2098
|
+
type: participant.type,
|
|
2099
|
+
handle: participant.handle ?? null
|
|
2100
|
+
}));
|
|
2101
|
+
this.replaceParticipants(normalized);
|
|
2102
|
+
return [...this.participants];
|
|
2103
|
+
}
|
|
2104
|
+
async loadHydratedMessages() {
|
|
2105
|
+
const messages = [];
|
|
2106
|
+
const pageSize = Math.min(Math.max(this.maxContextMessages, 1), 100);
|
|
2107
|
+
const maxPages = 100;
|
|
2108
|
+
for (let page = 1; page <= maxPages; page += 1) {
|
|
2109
|
+
const response = await this.link.rest.getChatContext?.(
|
|
2110
|
+
{
|
|
2111
|
+
chatId: this.roomId,
|
|
2112
|
+
page,
|
|
2113
|
+
pageSize
|
|
2114
|
+
},
|
|
2115
|
+
DEFAULT_REQUEST_OPTIONS
|
|
2116
|
+
);
|
|
2117
|
+
const items = response?.data ?? [];
|
|
2118
|
+
messages.push(...items.map((item) => ({
|
|
2119
|
+
id: item.id,
|
|
2120
|
+
room_id: this.roomId,
|
|
2121
|
+
content: item.content,
|
|
2122
|
+
sender_id: item.sender_id,
|
|
2123
|
+
sender_type: item.sender_type,
|
|
2124
|
+
sender_name: item.sender_name ?? null,
|
|
2125
|
+
message_type: item.message_type,
|
|
2126
|
+
metadata: item.metadata ?? {},
|
|
2127
|
+
created_at: item.inserted_at,
|
|
2128
|
+
role: item.sender_type === "User" ? "user" : "assistant"
|
|
2129
|
+
})));
|
|
2130
|
+
const totalPages = response?.metadata?.totalPages;
|
|
2131
|
+
if (typeof totalPages === "number" && totalPages > 0 && page >= totalPages) {
|
|
2132
|
+
break;
|
|
2133
|
+
}
|
|
2134
|
+
if ((typeof totalPages !== "number" || totalPages <= 0) && items.length < pageSize) {
|
|
2135
|
+
break;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
return messages.slice(-this.maxContextMessages);
|
|
2139
|
+
}
|
|
2140
|
+
updateCachedParticipants() {
|
|
2141
|
+
if (!this.contextCache) {
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
this.contextCache.participants = [...this.participants];
|
|
2145
|
+
this.contextCache.hydratedAt = /* @__PURE__ */ new Date();
|
|
2146
|
+
this.contextCacheExpiresAt = this.nextCacheExpiry();
|
|
2147
|
+
}
|
|
2148
|
+
isCacheExpired() {
|
|
2149
|
+
return this.contextCacheExpiresAt > 0 && Date.now() >= this.contextCacheExpiresAt;
|
|
2150
|
+
}
|
|
2151
|
+
nextCacheExpiry() {
|
|
2152
|
+
if (!this.enableContextCache || this.contextCacheTtlMs === 0) {
|
|
2153
|
+
return 0;
|
|
2154
|
+
}
|
|
2155
|
+
return Date.now() + this.contextCacheTtlMs;
|
|
2156
|
+
}
|
|
2157
|
+
toHistoryEntry(message) {
|
|
2158
|
+
return {
|
|
2159
|
+
id: message.id,
|
|
2160
|
+
room_id: message.roomId,
|
|
2161
|
+
content: message.content,
|
|
2162
|
+
sender_id: message.senderId,
|
|
2163
|
+
sender_type: message.senderType,
|
|
2164
|
+
sender_name: message.senderName,
|
|
2165
|
+
message_type: message.messageType,
|
|
2166
|
+
metadata: message.metadata,
|
|
2167
|
+
created_at: message.createdAt.toISOString(),
|
|
2168
|
+
role: message.senderType === "User" ? "user" : "assistant"
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
rebuildMessageIds() {
|
|
2172
|
+
this.messageIds = new Set(this.history.map((entry) => entry.id));
|
|
2173
|
+
}
|
|
2174
|
+
trackDedup(messageId) {
|
|
2175
|
+
this.dedupCache.set(messageId, true);
|
|
2176
|
+
if (this.dedupCache.size > DEDUP_CACHE_MAX) {
|
|
2177
|
+
const first = this.dedupCache.keys().next();
|
|
2178
|
+
if (!first.done) {
|
|
2179
|
+
this.dedupCache.delete(first.value);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
|
|
2185
|
+
// src/runtime/rooms/subscriptions.ts
|
|
2186
|
+
function hasRoomId(roomId) {
|
|
2187
|
+
return typeof roomId === "string" && roomId.length > 0;
|
|
2188
|
+
}
|
|
2189
|
+
async function trackRoomJoin(options) {
|
|
2190
|
+
if (!hasRoomId(options.roomId)) {
|
|
2191
|
+
return false;
|
|
2192
|
+
}
|
|
2193
|
+
if (options.roomFilter && !options.roomFilter(options.payload)) {
|
|
2194
|
+
return false;
|
|
2195
|
+
}
|
|
2196
|
+
if (options.trackedRooms.has(options.roomId)) {
|
|
2197
|
+
return false;
|
|
2198
|
+
}
|
|
2199
|
+
await options.link.subscribeRoom(options.roomId);
|
|
2200
|
+
options.trackedRooms.add(options.roomId);
|
|
2201
|
+
if (options.onJoined) {
|
|
2202
|
+
await options.onJoined(options.roomId, options.payload);
|
|
2203
|
+
}
|
|
2204
|
+
return true;
|
|
2205
|
+
}
|
|
2206
|
+
async function trackRoomLeave(options) {
|
|
2207
|
+
if (!hasRoomId(options.roomId) || !options.trackedRooms.has(options.roomId)) {
|
|
2208
|
+
return false;
|
|
2209
|
+
}
|
|
2210
|
+
await options.link.unsubscribeRoom(options.roomId);
|
|
2211
|
+
options.trackedRooms.delete(options.roomId);
|
|
2212
|
+
if (options.onLeft) {
|
|
2213
|
+
await options.onLeft(options.roomId);
|
|
2214
|
+
}
|
|
2215
|
+
return true;
|
|
2216
|
+
}
|
|
2217
|
+
async function hydrateTrackedRooms(options) {
|
|
2218
|
+
const pageSize = options.pageSize ?? 100;
|
|
2219
|
+
const maxPages = options.maxPages ?? 100;
|
|
2220
|
+
try {
|
|
2221
|
+
const rooms = await options.link.listAllChats(
|
|
2222
|
+
{ pageSize, maxPages },
|
|
2223
|
+
options.requestOptions
|
|
2224
|
+
);
|
|
2225
|
+
for (const room of rooms) {
|
|
2226
|
+
await trackRoomJoin({
|
|
2227
|
+
link: options.link,
|
|
2228
|
+
roomId: typeof room.id === "string" ? room.id : null,
|
|
2229
|
+
payload: room,
|
|
2230
|
+
trackedRooms: options.trackedRooms,
|
|
2231
|
+
roomFilter: options.roomFilter,
|
|
2232
|
+
onJoined: options.onJoined
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
} catch (error) {
|
|
2236
|
+
if (error instanceof UnsupportedFeatureError) {
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
if (options.onError) {
|
|
2240
|
+
await options.onError(error);
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
throw error;
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
// src/runtime/rooms/AgentRuntime.ts
|
|
2248
|
+
var AgentRuntime = class {
|
|
2249
|
+
link;
|
|
2250
|
+
agentId;
|
|
2251
|
+
onExecute;
|
|
2252
|
+
onSessionCleanup;
|
|
2253
|
+
onRoomJoined;
|
|
2254
|
+
onRoomLeft;
|
|
2255
|
+
onContactEvent;
|
|
2256
|
+
onParticipantAdded;
|
|
2257
|
+
onParticipantRemoved;
|
|
2258
|
+
onError;
|
|
2259
|
+
roomFilter;
|
|
2260
|
+
contextFactory;
|
|
2261
|
+
sessionConfig;
|
|
2262
|
+
autoSubscribeExistingRooms;
|
|
2263
|
+
subscribedRooms = /* @__PURE__ */ new Set();
|
|
2264
|
+
contexts = /* @__PURE__ */ new Map();
|
|
2265
|
+
executions = /* @__PURE__ */ new Map();
|
|
2266
|
+
executionWatchers = /* @__PURE__ */ new Map();
|
|
2267
|
+
logger;
|
|
2268
|
+
running = false;
|
|
2269
|
+
stopping = false;
|
|
2270
|
+
stopController = new AbortController();
|
|
2271
|
+
consumeTask = null;
|
|
2272
|
+
fatalError = null;
|
|
2273
|
+
constructor(options) {
|
|
2274
|
+
this.link = options.link;
|
|
2275
|
+
this.agentId = options.agentId;
|
|
2276
|
+
this.onExecute = options.onExecute;
|
|
2277
|
+
this.onSessionCleanup = options.onSessionCleanup ?? (async () => void 0);
|
|
2278
|
+
this.onRoomJoined = options.onRoomJoined;
|
|
2279
|
+
this.onRoomLeft = options.onRoomLeft;
|
|
2280
|
+
this.onError = options.onError;
|
|
2281
|
+
this.logger = options.logger ?? new NoopLogger();
|
|
2282
|
+
this.onContactEvent = options.onContactEvent;
|
|
2283
|
+
this.onParticipantAdded = options.onParticipantAdded;
|
|
2284
|
+
this.onParticipantRemoved = options.onParticipantRemoved;
|
|
2285
|
+
this.roomFilter = options.roomFilter;
|
|
2286
|
+
this.contextFactory = options.contextFactory;
|
|
2287
|
+
this.sessionConfig = {
|
|
2288
|
+
enableContextCache: options.sessionConfig?.enableContextCache ?? true,
|
|
2289
|
+
contextCacheTtlSeconds: options.sessionConfig?.contextCacheTtlSeconds ?? 300,
|
|
2290
|
+
maxContextMessages: options.sessionConfig?.maxContextMessages ?? 100,
|
|
2291
|
+
maxMessageRetries: options.sessionConfig?.maxMessageRetries ?? 1,
|
|
2292
|
+
enableContextHydration: options.sessionConfig?.enableContextHydration ?? true
|
|
2293
|
+
};
|
|
2294
|
+
this.autoSubscribeExistingRooms = options.agentConfig?.autoSubscribeExistingRooms ?? false;
|
|
2295
|
+
}
|
|
2296
|
+
async start() {
|
|
2297
|
+
if (this.running) {
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
this.running = true;
|
|
2301
|
+
this.stopping = false;
|
|
2302
|
+
this.fatalError = null;
|
|
2303
|
+
if (!this.stopController.signal.aborted) {
|
|
2304
|
+
this.stopController.abort();
|
|
2305
|
+
}
|
|
2306
|
+
this.stopController = new AbortController();
|
|
2307
|
+
try {
|
|
2308
|
+
await this.link.connect();
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
await this.handleStartFailure();
|
|
2311
|
+
throw error;
|
|
2312
|
+
}
|
|
2313
|
+
try {
|
|
2314
|
+
await this.link.subscribeAgentRooms();
|
|
2315
|
+
} catch {
|
|
2316
|
+
this.logger.warn("AgentRuntime failed to subscribe agent_rooms channel, continuing without it");
|
|
2317
|
+
}
|
|
2318
|
+
try {
|
|
2319
|
+
await this.subscribeExistingRooms();
|
|
2320
|
+
} catch (error) {
|
|
2321
|
+
await this.handleStartFailure();
|
|
2322
|
+
throw error;
|
|
2323
|
+
}
|
|
2324
|
+
this.consumeTask = this.consumeLoop(this.stopController.signal);
|
|
2325
|
+
if (!this.link.capabilities.contacts) {
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
try {
|
|
2329
|
+
await this.link.subscribeAgentContacts();
|
|
2330
|
+
} catch {
|
|
2331
|
+
this.logger.warn("AgentRuntime failed to subscribe agent_contacts channel, continuing without it");
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
async handleStartFailure() {
|
|
2335
|
+
this.running = false;
|
|
2336
|
+
this.stopping = false;
|
|
2337
|
+
this.stopController.abort();
|
|
2338
|
+
if (this.consumeTask) {
|
|
2339
|
+
await this.consumeTask;
|
|
2340
|
+
this.consumeTask = null;
|
|
2341
|
+
}
|
|
2342
|
+
await this.link.disconnect();
|
|
2343
|
+
}
|
|
2344
|
+
async stop(timeoutMs) {
|
|
2345
|
+
if (!this.running || this.stopping) {
|
|
2346
|
+
return true;
|
|
2347
|
+
}
|
|
2348
|
+
this.stopping = true;
|
|
2349
|
+
this.running = false;
|
|
2350
|
+
this.stopController.abort();
|
|
2351
|
+
if (this.consumeTask) {
|
|
2352
|
+
await this.consumeTask;
|
|
2353
|
+
this.consumeTask = null;
|
|
2354
|
+
}
|
|
2355
|
+
const deadline = timeoutMs === void 0 ? void 0 : Date.now() + timeoutMs;
|
|
2356
|
+
let graceful = true;
|
|
2357
|
+
for (const execution of this.executions.values()) {
|
|
2358
|
+
const remaining = deadline === void 0 ? void 0 : Math.max(0, deadline - Date.now());
|
|
2359
|
+
const stopped = await execution.stop(remaining);
|
|
2360
|
+
if (!stopped) {
|
|
2361
|
+
graceful = false;
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
for (const roomId of [...this.subscribedRooms]) {
|
|
2365
|
+
await this.leaveTrackedRoom(roomId);
|
|
2366
|
+
}
|
|
2367
|
+
for (const roomId of [...this.contexts.keys()]) {
|
|
2368
|
+
await this.onSessionCleanup(roomId);
|
|
2369
|
+
}
|
|
2370
|
+
this.subscribedRooms.clear();
|
|
2371
|
+
this.contexts.clear();
|
|
2372
|
+
this.executions.clear();
|
|
2373
|
+
this.executionWatchers.clear();
|
|
2374
|
+
if (this.link.capabilities.contacts) {
|
|
2375
|
+
await this.link.unsubscribeAgentContacts();
|
|
2376
|
+
}
|
|
2377
|
+
await this.link.disconnect();
|
|
2378
|
+
if (this.fatalError) {
|
|
2379
|
+
throw this.fatalError instanceof Error ? this.fatalError : new Error(String(this.fatalError));
|
|
2380
|
+
}
|
|
2381
|
+
return graceful;
|
|
2382
|
+
}
|
|
2383
|
+
getContext(roomId) {
|
|
2384
|
+
return this.contexts.get(roomId);
|
|
2385
|
+
}
|
|
2386
|
+
async waitUntilStopped() {
|
|
2387
|
+
if (this.consumeTask) {
|
|
2388
|
+
await this.consumeTask;
|
|
2389
|
+
} else {
|
|
2390
|
+
await this.link.runForever(this.stopController.signal);
|
|
2391
|
+
}
|
|
2392
|
+
if (this.fatalError) {
|
|
2393
|
+
throw this.fatalError instanceof Error ? this.fatalError : new Error(String(this.fatalError));
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
getContexts() {
|
|
2397
|
+
return [...this.contexts.values()];
|
|
2398
|
+
}
|
|
2399
|
+
async consumeLoop(signal) {
|
|
2400
|
+
while (!signal.aborted) {
|
|
2401
|
+
const event = await this.link.nextEvent(signal);
|
|
2402
|
+
if (!event) {
|
|
2403
|
+
return;
|
|
2404
|
+
}
|
|
2405
|
+
try {
|
|
2406
|
+
await this.handleEvent(event);
|
|
2407
|
+
} catch (error) {
|
|
2408
|
+
await this.failRuntime(error, event);
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
async handleEvent(event) {
|
|
2414
|
+
switch (event.type) {
|
|
2415
|
+
case "room_added":
|
|
2416
|
+
await trackRoomJoin({
|
|
2417
|
+
link: this.link,
|
|
2418
|
+
roomId: event.roomId,
|
|
2419
|
+
payload: event.payload,
|
|
2420
|
+
trackedRooms: this.subscribedRooms,
|
|
2421
|
+
roomFilter: this.roomFilter,
|
|
2422
|
+
onJoined: async (roomId) => {
|
|
2423
|
+
this.getOrCreateExecution(roomId);
|
|
2424
|
+
await this.onRoomJoined?.(roomId, event.payload);
|
|
2425
|
+
}
|
|
2426
|
+
});
|
|
2427
|
+
return;
|
|
2428
|
+
case "room_removed":
|
|
2429
|
+
case "room_deleted":
|
|
2430
|
+
if (event.roomId) {
|
|
2431
|
+
await this.onRoomLeft?.(event.roomId);
|
|
2432
|
+
await this.leaveTrackedRoom(event.roomId);
|
|
2433
|
+
}
|
|
2434
|
+
return;
|
|
2435
|
+
case "participant_added":
|
|
2436
|
+
if (event.roomId) {
|
|
2437
|
+
const context = this.getOrCreateContext(event.roomId);
|
|
2438
|
+
const participant = {
|
|
2439
|
+
id: event.payload.id,
|
|
2440
|
+
name: event.payload.name,
|
|
2441
|
+
type: event.payload.type,
|
|
2442
|
+
handle: event.payload.handle
|
|
2443
|
+
};
|
|
2444
|
+
context.addParticipant(participant);
|
|
2445
|
+
await this.onParticipantAdded?.(event.roomId, participant);
|
|
2446
|
+
}
|
|
2447
|
+
return;
|
|
2448
|
+
case "participant_removed":
|
|
2449
|
+
if (event.roomId) {
|
|
2450
|
+
const context = this.getOrCreateContext(event.roomId);
|
|
2451
|
+
context.removeParticipant(event.payload.id);
|
|
2452
|
+
await this.onParticipantRemoved?.(event.roomId, event.payload.id);
|
|
2453
|
+
}
|
|
2454
|
+
return;
|
|
2455
|
+
case "contact_request_received":
|
|
2456
|
+
case "contact_request_updated":
|
|
2457
|
+
case "contact_added":
|
|
2458
|
+
case "contact_removed":
|
|
2459
|
+
await this.onContactEvent?.(event);
|
|
2460
|
+
return;
|
|
2461
|
+
case "message_created":
|
|
2462
|
+
if (!event.roomId) {
|
|
2463
|
+
return;
|
|
2464
|
+
}
|
|
2465
|
+
await this.getOrCreateExecution(event.roomId).enqueue(event);
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
return assertNever(event);
|
|
2469
|
+
}
|
|
2470
|
+
async enqueueEvent(roomId, event) {
|
|
2471
|
+
await this.getOrCreateExecution(roomId).enqueue(event);
|
|
2472
|
+
}
|
|
2473
|
+
async bootstrapRoomMessage(roomId, message) {
|
|
2474
|
+
await this.link.subscribeRoom(roomId);
|
|
2475
|
+
this.subscribedRooms.add(roomId);
|
|
2476
|
+
await this.getOrCreateExecution(roomId).bootstrapMessage(message);
|
|
2477
|
+
}
|
|
2478
|
+
async resetRoomSession(roomId, timeoutMs) {
|
|
2479
|
+
const execution = this.executions.get(roomId);
|
|
2480
|
+
let graceful = true;
|
|
2481
|
+
if (execution) {
|
|
2482
|
+
graceful = await execution.stop(timeoutMs);
|
|
2483
|
+
}
|
|
2484
|
+
this.executions.delete(roomId);
|
|
2485
|
+
this.contexts.delete(roomId);
|
|
2486
|
+
await this.onSessionCleanup(roomId);
|
|
2487
|
+
return graceful;
|
|
2488
|
+
}
|
|
2489
|
+
getOrCreateExecution(roomId) {
|
|
2490
|
+
const existing = this.executions.get(roomId);
|
|
2491
|
+
if (existing) {
|
|
2492
|
+
return existing;
|
|
2493
|
+
}
|
|
2494
|
+
const execution = new Execution({
|
|
2495
|
+
roomId,
|
|
2496
|
+
link: this.link,
|
|
2497
|
+
context: this.getOrCreateContext(roomId),
|
|
2498
|
+
onExecute: this.onExecute,
|
|
2499
|
+
onFailure: async (error, event) => {
|
|
2500
|
+
await this.failRuntime(error, event);
|
|
2501
|
+
},
|
|
2502
|
+
logger: this.logger
|
|
2503
|
+
});
|
|
2504
|
+
this.executions.set(roomId, execution);
|
|
2505
|
+
const watcher = execution.waitUntilStopped().catch(async (error) => {
|
|
2506
|
+
await this.failRuntime(error, {
|
|
2507
|
+
type: "message_created",
|
|
2508
|
+
roomId,
|
|
2509
|
+
payload: {
|
|
2510
|
+
id: "execution-failed",
|
|
2511
|
+
content: "",
|
|
2512
|
+
sender_id: this.agentId,
|
|
2513
|
+
sender_type: "Agent",
|
|
2514
|
+
sender_name: null,
|
|
2515
|
+
message_type: "text",
|
|
2516
|
+
metadata: {},
|
|
2517
|
+
inserted_at: (/* @__PURE__ */ new Date(0)).toISOString(),
|
|
2518
|
+
updated_at: (/* @__PURE__ */ new Date(0)).toISOString()
|
|
2519
|
+
}
|
|
2520
|
+
});
|
|
2521
|
+
}).finally(() => {
|
|
2522
|
+
this.executionWatchers.delete(roomId);
|
|
2523
|
+
});
|
|
2524
|
+
this.executionWatchers.set(roomId, watcher);
|
|
2525
|
+
return execution;
|
|
2526
|
+
}
|
|
2527
|
+
getOrCreateContext(roomId) {
|
|
2528
|
+
const existing = this.contexts.get(roomId);
|
|
2529
|
+
if (existing) {
|
|
2530
|
+
return existing;
|
|
2531
|
+
}
|
|
2532
|
+
const defaults = {
|
|
2533
|
+
roomId,
|
|
2534
|
+
link: this.link,
|
|
2535
|
+
maxContextMessages: this.sessionConfig.maxContextMessages,
|
|
2536
|
+
maxMessageRetries: this.sessionConfig.maxMessageRetries,
|
|
2537
|
+
enableContextCache: this.sessionConfig.enableContextCache,
|
|
2538
|
+
contextCacheTtlSeconds: this.sessionConfig.contextCacheTtlSeconds,
|
|
2539
|
+
enableContextHydration: this.sessionConfig.enableContextHydration
|
|
2540
|
+
};
|
|
2541
|
+
const context = this.contextFactory ? this.contextFactory(roomId, defaults) : new ExecutionContext(defaults);
|
|
2542
|
+
this.contexts.set(roomId, context);
|
|
2543
|
+
return context;
|
|
2544
|
+
}
|
|
2545
|
+
async subscribeExistingRooms() {
|
|
2546
|
+
if (!this.autoSubscribeExistingRooms) {
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
await hydrateTrackedRooms({
|
|
2550
|
+
link: this.link,
|
|
2551
|
+
trackedRooms: this.subscribedRooms,
|
|
2552
|
+
roomFilter: this.roomFilter,
|
|
2553
|
+
onJoined: async (roomId, payload) => {
|
|
2554
|
+
this.getOrCreateExecution(roomId);
|
|
2555
|
+
await this.onRoomJoined?.(roomId, payload);
|
|
2556
|
+
},
|
|
2557
|
+
onError: async (error) => {
|
|
2558
|
+
this.logger.warn("AgentRuntime failed to subscribe existing rooms", {
|
|
2559
|
+
error
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
});
|
|
2563
|
+
}
|
|
2564
|
+
async leaveTrackedRoom(roomId) {
|
|
2565
|
+
await trackRoomLeave({
|
|
2566
|
+
link: this.link,
|
|
2567
|
+
roomId,
|
|
2568
|
+
trackedRooms: this.subscribedRooms,
|
|
2569
|
+
onLeft: async (leftRoomId) => {
|
|
2570
|
+
await this.executions.get(leftRoomId)?.stop();
|
|
2571
|
+
this.contexts.delete(leftRoomId);
|
|
2572
|
+
this.executions.delete(leftRoomId);
|
|
2573
|
+
await this.onSessionCleanup(leftRoomId);
|
|
2574
|
+
}
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
2577
|
+
async failRuntime(error, event) {
|
|
2578
|
+
if (!this.fatalError) {
|
|
2579
|
+
this.fatalError = error;
|
|
2580
|
+
this.running = false;
|
|
2581
|
+
this.logger.error("Fatal runtime error handling platform event", {
|
|
2582
|
+
eventType: event.type,
|
|
2583
|
+
roomId: event.roomId,
|
|
2584
|
+
error
|
|
2585
|
+
});
|
|
2586
|
+
this.notifyOnError(error, event);
|
|
2587
|
+
}
|
|
2588
|
+
if (!this.stopController.signal.aborted) {
|
|
2589
|
+
this.stopController.abort();
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
notifyOnError(error, event) {
|
|
2593
|
+
if (!this.onError) {
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
try {
|
|
2597
|
+
this.onError(error, event);
|
|
2598
|
+
} catch (observerError) {
|
|
2599
|
+
this.logger.error("Error in runtime onError callback", {
|
|
2600
|
+
eventType: event.type,
|
|
2601
|
+
roomId: event.roomId,
|
|
2602
|
+
error: observerError
|
|
2603
|
+
});
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
};
|
|
2607
|
+
function assertNever(value) {
|
|
2608
|
+
throw new Error(`Unhandled platform event: ${JSON.stringify(value)}`);
|
|
2609
|
+
}
|
|
2610
|
+
|
|
2611
|
+
// src/runtime/preprocessing/DefaultPreprocessor.ts
|
|
2612
|
+
function toPlatformMessage2(roomId, payload) {
|
|
2613
|
+
return {
|
|
2614
|
+
id: payload.id,
|
|
2615
|
+
roomId,
|
|
2616
|
+
content: payload.content,
|
|
2617
|
+
senderId: payload.sender_id,
|
|
2618
|
+
senderType: payload.sender_type,
|
|
2619
|
+
senderName: payload.sender_name ?? null,
|
|
2620
|
+
messageType: payload.message_type,
|
|
2621
|
+
metadata: payload.metadata ?? {},
|
|
2622
|
+
createdAt: new Date(payload.inserted_at)
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
var DefaultPreprocessor = class {
|
|
2626
|
+
async process(context, event, agentId) {
|
|
2627
|
+
if (event.type !== "message_created" || !event.roomId) {
|
|
2628
|
+
return null;
|
|
2629
|
+
}
|
|
2630
|
+
const message = toPlatformMessage2(event.roomId, event.payload);
|
|
2631
|
+
if (message.senderId === agentId) {
|
|
2632
|
+
return null;
|
|
2633
|
+
}
|
|
2634
|
+
if (context.hasMessage(message.id)) {
|
|
2635
|
+
return null;
|
|
2636
|
+
}
|
|
2637
|
+
const isSessionBootstrap = !context.isLlmInitialized;
|
|
2638
|
+
if (isSessionBootstrap) {
|
|
2639
|
+
context.markLlmInitialized();
|
|
2640
|
+
}
|
|
2641
|
+
context.recordMessage(message);
|
|
2642
|
+
const systemMessages = context.consumeSystemMessages();
|
|
2643
|
+
let contactsMessage;
|
|
2644
|
+
if (systemMessages.length > 0) {
|
|
2645
|
+
contactsMessage = systemMessages.join("\n");
|
|
2646
|
+
} else {
|
|
2647
|
+
contactsMessage = context.consumeContactsMessage();
|
|
2648
|
+
}
|
|
2649
|
+
const history = isSessionBootstrap ? await context.getHydratedHistory(message.id) : context.getRawHistory();
|
|
2650
|
+
return {
|
|
2651
|
+
message,
|
|
2652
|
+
tools: context.getTools(),
|
|
2653
|
+
history: new HistoryProvider(history),
|
|
2654
|
+
participantsMessage: context.consumeParticipantsMessage(),
|
|
2655
|
+
contactsMessage,
|
|
2656
|
+
isSessionBootstrap,
|
|
2657
|
+
roomId: context.roomId
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
};
|
|
2661
|
+
|
|
2662
|
+
// src/runtime/tools/ContactToolsImpl.ts
|
|
2663
|
+
var ContactToolsImpl = class {
|
|
2664
|
+
rest;
|
|
2665
|
+
constructor(rest) {
|
|
2666
|
+
this.rest = rest;
|
|
2667
|
+
}
|
|
2668
|
+
async listContacts(request = {}) {
|
|
2669
|
+
if (!this.rest.listContacts) {
|
|
2670
|
+
throw new UnsupportedFeatureError("Contact listing is not available in current REST adapter");
|
|
2671
|
+
}
|
|
2672
|
+
return this.rest.listContacts(
|
|
2673
|
+
{
|
|
2674
|
+
page: request.page ?? 1,
|
|
2675
|
+
pageSize: request.pageSize ?? 50
|
|
2676
|
+
},
|
|
2677
|
+
DEFAULT_REQUEST_OPTIONS
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
async addContact(request) {
|
|
2681
|
+
if (!this.rest.addContact) {
|
|
2682
|
+
throw new UnsupportedFeatureError("Contact creation is not available in current REST adapter");
|
|
2683
|
+
}
|
|
2684
|
+
const normalizedHandle = request.handle.trim();
|
|
2685
|
+
if (normalizedHandle.length === 0) {
|
|
2686
|
+
throw new ValidationError("handle is required");
|
|
2687
|
+
}
|
|
2688
|
+
return this.rest.addContact(
|
|
2689
|
+
{
|
|
2690
|
+
handle: normalizedHandle,
|
|
2691
|
+
...request.message ? { message: request.message } : {}
|
|
2692
|
+
},
|
|
2693
|
+
DEFAULT_REQUEST_OPTIONS
|
|
2694
|
+
);
|
|
2695
|
+
}
|
|
2696
|
+
async removeContact(request) {
|
|
2697
|
+
if (!this.rest.removeContact) {
|
|
2698
|
+
throw new UnsupportedFeatureError("Contact removal is not available in current REST adapter");
|
|
2699
|
+
}
|
|
2700
|
+
if (request.target === "handle") {
|
|
2701
|
+
const handle = request.handle.trim();
|
|
2702
|
+
if (handle.length === 0) {
|
|
2703
|
+
throw new ValidationError("handle is required");
|
|
2704
|
+
}
|
|
2705
|
+
return this.rest.removeContact(
|
|
2706
|
+
{ target: "handle", handle },
|
|
2707
|
+
DEFAULT_REQUEST_OPTIONS
|
|
2708
|
+
);
|
|
2709
|
+
}
|
|
2710
|
+
const contactId = request.contactId.trim();
|
|
2711
|
+
if (contactId.length === 0) {
|
|
2712
|
+
throw new ValidationError("contactId is required");
|
|
2713
|
+
}
|
|
2714
|
+
return this.rest.removeContact(
|
|
2715
|
+
{ target: "contactId", contactId },
|
|
2716
|
+
DEFAULT_REQUEST_OPTIONS
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
async listContactRequests(request = {}) {
|
|
2720
|
+
if (!this.rest.listContactRequests) {
|
|
2721
|
+
throw new UnsupportedFeatureError("Contact request listing is not available in current REST adapter");
|
|
2722
|
+
}
|
|
2723
|
+
return this.rest.listContactRequests(
|
|
2724
|
+
{
|
|
2725
|
+
page: request.page ?? 1,
|
|
2726
|
+
pageSize: request.pageSize ?? 50,
|
|
2727
|
+
sentStatus: request.sentStatus ?? "pending"
|
|
2728
|
+
},
|
|
2729
|
+
DEFAULT_REQUEST_OPTIONS
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
async respondContactRequest(request) {
|
|
2733
|
+
if (!this.rest.respondContactRequest) {
|
|
2734
|
+
throw new UnsupportedFeatureError("Contact request responses are not available in current REST adapter");
|
|
2735
|
+
}
|
|
2736
|
+
if (request.target === "handle") {
|
|
2737
|
+
const handle = request.handle.trim();
|
|
2738
|
+
if (handle.length === 0) {
|
|
2739
|
+
throw new ValidationError("handle is required");
|
|
2740
|
+
}
|
|
2741
|
+
return this.rest.respondContactRequest(
|
|
2742
|
+
{ action: request.action, target: "handle", handle },
|
|
2743
|
+
DEFAULT_REQUEST_OPTIONS
|
|
2744
|
+
);
|
|
2745
|
+
}
|
|
2746
|
+
const requestId = request.requestId.trim();
|
|
2747
|
+
if (requestId.length === 0) {
|
|
2748
|
+
throw new ValidationError("requestId is required");
|
|
2749
|
+
}
|
|
2750
|
+
return this.rest.respondContactRequest(
|
|
2751
|
+
{ action: request.action, target: "requestId", requestId },
|
|
2752
|
+
DEFAULT_REQUEST_OPTIONS
|
|
2753
|
+
);
|
|
2754
|
+
}
|
|
2755
|
+
};
|
|
2756
|
+
|
|
2757
|
+
// src/runtime/tools/ContactCallbackTools.ts
|
|
2758
|
+
function toParticipantRecord(participant) {
|
|
2759
|
+
return {
|
|
2760
|
+
id: participant.id,
|
|
2761
|
+
name: participant.name,
|
|
2762
|
+
type: participant.type,
|
|
2763
|
+
handle: participant.handle ?? null
|
|
2764
|
+
};
|
|
2765
|
+
}
|
|
2766
|
+
function normalizePage(value, fallback) {
|
|
2767
|
+
return Number.isInteger(value) && value && value > 0 ? value : fallback;
|
|
2768
|
+
}
|
|
2769
|
+
function requireRoomId(roomId, methodName) {
|
|
2770
|
+
if (roomId) {
|
|
2771
|
+
return roomId;
|
|
2772
|
+
}
|
|
2773
|
+
throw new UnsupportedFeatureError(`${methodName} is unavailable for contact callbacks without a room context`);
|
|
2774
|
+
}
|
|
2775
|
+
var ContactCallbackTools = class {
|
|
2776
|
+
capabilities;
|
|
2777
|
+
rest;
|
|
2778
|
+
roomId;
|
|
2779
|
+
contactTools;
|
|
2780
|
+
constructor(rest, roomId) {
|
|
2781
|
+
this.rest = rest;
|
|
2782
|
+
this.roomId = roomId;
|
|
2783
|
+
const hasContactMethods = Boolean(
|
|
2784
|
+
rest.listContacts || rest.addContact || rest.removeContact || rest.listContactRequests || rest.respondContactRequest
|
|
2785
|
+
);
|
|
2786
|
+
this.contactTools = hasContactMethods ? new ContactToolsImpl(rest) : null;
|
|
2787
|
+
this.capabilities = Object.freeze({
|
|
2788
|
+
peers: Boolean(rest.listPeers),
|
|
2789
|
+
contacts: hasContactMethods,
|
|
2790
|
+
memory: Boolean(
|
|
2791
|
+
rest.listMemories || rest.storeMemory || rest.getMemory || rest.supersedeMemory || rest.archiveMemory
|
|
2792
|
+
)
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
async sendMessage(content, mentions) {
|
|
2796
|
+
const roomId = requireRoomId(this.roomId, "sendMessage");
|
|
2797
|
+
if (!this.rest.createChatMessage) {
|
|
2798
|
+
throw new UnsupportedFeatureError("Message sending is not available in current REST adapter");
|
|
2799
|
+
}
|
|
2800
|
+
if (Array.isArray(mentions) && mentions.some((mention) => typeof mention === "string")) {
|
|
2801
|
+
throw new UnsupportedFeatureError("sendMessage string mentions are unavailable for contact callbacks");
|
|
2802
|
+
}
|
|
2803
|
+
const normalizedMentions = mentions && mentions.every((mention) => typeof mention !== "string") ? mentions : void 0;
|
|
2804
|
+
return this.rest.createChatMessage(
|
|
2805
|
+
roomId,
|
|
2806
|
+
{
|
|
2807
|
+
content,
|
|
2808
|
+
...normalizedMentions ? { mentions: normalizedMentions } : {}
|
|
2809
|
+
},
|
|
2810
|
+
DEFAULT_REQUEST_OPTIONS
|
|
2811
|
+
);
|
|
2812
|
+
}
|
|
2813
|
+
async sendEvent(content, messageType, metadata) {
|
|
2814
|
+
const roomId = requireRoomId(this.roomId, "sendEvent");
|
|
2815
|
+
if (!this.rest.createChatEvent) {
|
|
2816
|
+
throw new UnsupportedFeatureError("Event sending is not available in current REST adapter");
|
|
2817
|
+
}
|
|
2818
|
+
return this.rest.createChatEvent(
|
|
2819
|
+
roomId,
|
|
2820
|
+
{
|
|
2821
|
+
content,
|
|
2822
|
+
messageType,
|
|
2823
|
+
...metadata ? { metadata } : {}
|
|
2824
|
+
},
|
|
2825
|
+
DEFAULT_REQUEST_OPTIONS
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2828
|
+
async addParticipant() {
|
|
2829
|
+
throw new UnsupportedFeatureError("addParticipant is unavailable for contact callbacks");
|
|
2830
|
+
}
|
|
2831
|
+
async removeParticipant() {
|
|
2832
|
+
throw new UnsupportedFeatureError("removeParticipant is unavailable for contact callbacks");
|
|
2833
|
+
}
|
|
2834
|
+
async getParticipants() {
|
|
2835
|
+
const roomId = requireRoomId(this.roomId, "getParticipants");
|
|
2836
|
+
if (!this.rest.listChatParticipants) {
|
|
2837
|
+
throw new UnsupportedFeatureError("Participant listing is not available in current REST adapter");
|
|
2838
|
+
}
|
|
2839
|
+
return (await this.rest.listChatParticipants(roomId, DEFAULT_REQUEST_OPTIONS)).map(toParticipantRecord);
|
|
2840
|
+
}
|
|
2841
|
+
async createChatroom(taskId) {
|
|
2842
|
+
const chat = await this.rest.createChat(taskId, DEFAULT_REQUEST_OPTIONS);
|
|
2843
|
+
return chat.id;
|
|
2844
|
+
}
|
|
2845
|
+
async lookupPeers(page = 1, pageSize = 50) {
|
|
2846
|
+
if (!this.rest.listPeers) {
|
|
2847
|
+
throw new UnsupportedFeatureError("Peer lookup is not available in current REST adapter");
|
|
2848
|
+
}
|
|
2849
|
+
return this.rest.listPeers(
|
|
2850
|
+
{
|
|
2851
|
+
page: normalizePage(page, 1),
|
|
2852
|
+
pageSize: normalizePage(pageSize, 50),
|
|
2853
|
+
notInChat: this.roomId ?? ""
|
|
2854
|
+
},
|
|
2855
|
+
DEFAULT_REQUEST_OPTIONS
|
|
2856
|
+
);
|
|
2857
|
+
}
|
|
2858
|
+
getToolSchemas() {
|
|
2859
|
+
return [];
|
|
2860
|
+
}
|
|
2861
|
+
getAnthropicToolSchemas() {
|
|
2862
|
+
return [];
|
|
2863
|
+
}
|
|
2864
|
+
getOpenAIToolSchemas() {
|
|
2865
|
+
return [];
|
|
2866
|
+
}
|
|
2867
|
+
async listContacts(request = {}) {
|
|
2868
|
+
if (!this.contactTools) {
|
|
2869
|
+
throw new UnsupportedFeatureError("Contact listing is not available in current REST adapter");
|
|
2870
|
+
}
|
|
2871
|
+
return this.contactTools.listContacts(request);
|
|
2872
|
+
}
|
|
2873
|
+
async addContact(request) {
|
|
2874
|
+
if (!this.contactTools) {
|
|
2875
|
+
throw new UnsupportedFeatureError("Contact creation is not available in current REST adapter");
|
|
2876
|
+
}
|
|
2877
|
+
return this.contactTools.addContact(request);
|
|
2878
|
+
}
|
|
2879
|
+
async removeContact(request) {
|
|
2880
|
+
if (!this.contactTools) {
|
|
2881
|
+
throw new UnsupportedFeatureError("Contact removal is not available in current REST adapter");
|
|
2882
|
+
}
|
|
2883
|
+
return this.contactTools.removeContact(request);
|
|
2884
|
+
}
|
|
2885
|
+
async listContactRequests(request = {}) {
|
|
2886
|
+
if (!this.contactTools) {
|
|
2887
|
+
throw new UnsupportedFeatureError("Contact request listing is not available in current REST adapter");
|
|
2888
|
+
}
|
|
2889
|
+
return this.contactTools.listContactRequests(request);
|
|
2890
|
+
}
|
|
2891
|
+
async respondContactRequest(request) {
|
|
2892
|
+
if (!this.contactTools) {
|
|
2893
|
+
throw new UnsupportedFeatureError("Contact request responses are not available in current REST adapter");
|
|
2894
|
+
}
|
|
2895
|
+
return this.contactTools.respondContactRequest(request);
|
|
2896
|
+
}
|
|
2897
|
+
async listMemories(args = {}) {
|
|
2898
|
+
if (!this.rest.listMemories) {
|
|
2899
|
+
throw new UnsupportedFeatureError("Memory listing is not available in current REST adapter");
|
|
2900
|
+
}
|
|
2901
|
+
return this.rest.listMemories(args, DEFAULT_REQUEST_OPTIONS);
|
|
2902
|
+
}
|
|
2903
|
+
async storeMemory(args) {
|
|
2904
|
+
if (!this.rest.storeMemory) {
|
|
2905
|
+
throw new UnsupportedFeatureError("Memory storage is not available in current REST adapter");
|
|
2906
|
+
}
|
|
2907
|
+
return this.rest.storeMemory(args, DEFAULT_REQUEST_OPTIONS);
|
|
2908
|
+
}
|
|
2909
|
+
async getMemory(memoryId) {
|
|
2910
|
+
if (!this.rest.getMemory) {
|
|
2911
|
+
throw new UnsupportedFeatureError("Memory lookup is not available in current REST adapter");
|
|
2912
|
+
}
|
|
2913
|
+
return this.rest.getMemory(memoryId, DEFAULT_REQUEST_OPTIONS);
|
|
2914
|
+
}
|
|
2915
|
+
async supersedeMemory(memoryId) {
|
|
2916
|
+
if (!this.rest.supersedeMemory) {
|
|
2917
|
+
throw new UnsupportedFeatureError("Memory supersede is not available in current REST adapter");
|
|
2918
|
+
}
|
|
2919
|
+
return this.rest.supersedeMemory(memoryId, DEFAULT_REQUEST_OPTIONS);
|
|
2920
|
+
}
|
|
2921
|
+
async archiveMemory(memoryId) {
|
|
2922
|
+
if (!this.rest.archiveMemory) {
|
|
2923
|
+
throw new UnsupportedFeatureError("Memory archival is not available in current REST adapter");
|
|
2924
|
+
}
|
|
2925
|
+
return this.rest.archiveMemory(memoryId, DEFAULT_REQUEST_OPTIONS);
|
|
2926
|
+
}
|
|
2927
|
+
async executeToolCall(toolName, toolArgs) {
|
|
2928
|
+
switch (toolName) {
|
|
2929
|
+
case "thenvoi_send_message":
|
|
2930
|
+
return this.sendMessage(
|
|
2931
|
+
String(toolArgs.content ?? ""),
|
|
2932
|
+
toolArgs.mentions
|
|
2933
|
+
);
|
|
2934
|
+
case "thenvoi_send_event":
|
|
2935
|
+
return this.sendEvent(
|
|
2936
|
+
String(toolArgs.content ?? ""),
|
|
2937
|
+
String(toolArgs.message_type ?? "task"),
|
|
2938
|
+
toolArgs.metadata
|
|
2939
|
+
);
|
|
2940
|
+
case "thenvoi_get_participants":
|
|
2941
|
+
return this.getParticipants();
|
|
2942
|
+
case "thenvoi_create_chatroom":
|
|
2943
|
+
return this.createChatroom(typeof toolArgs.task_id === "string" ? toolArgs.task_id : void 0);
|
|
2944
|
+
case "thenvoi_lookup_peers":
|
|
2945
|
+
return this.lookupPeers(
|
|
2946
|
+
typeof toolArgs.page === "number" ? toolArgs.page : void 0,
|
|
2947
|
+
typeof toolArgs.page_size === "number" ? toolArgs.page_size : void 0
|
|
2948
|
+
);
|
|
2949
|
+
case "thenvoi_list_contacts":
|
|
2950
|
+
return this.listContacts({
|
|
2951
|
+
page: typeof toolArgs.page === "number" ? toolArgs.page : void 0,
|
|
2952
|
+
pageSize: typeof toolArgs.page_size === "number" ? toolArgs.page_size : void 0
|
|
2953
|
+
});
|
|
2954
|
+
case "thenvoi_add_contact":
|
|
2955
|
+
return this.addContact({
|
|
2956
|
+
handle: String(toolArgs.handle ?? ""),
|
|
2957
|
+
...typeof toolArgs.message === "string" ? { message: toolArgs.message } : {}
|
|
2958
|
+
});
|
|
2959
|
+
case "thenvoi_remove_contact":
|
|
2960
|
+
if (typeof toolArgs.contact_id === "string") {
|
|
2961
|
+
return this.removeContact({ target: "contactId", contactId: toolArgs.contact_id });
|
|
2962
|
+
}
|
|
2963
|
+
return this.removeContact({ target: "handle", handle: String(toolArgs.handle ?? "") });
|
|
2964
|
+
case "thenvoi_list_contact_requests":
|
|
2965
|
+
return this.listContactRequests({
|
|
2966
|
+
page: typeof toolArgs.page === "number" ? toolArgs.page : void 0,
|
|
2967
|
+
pageSize: typeof toolArgs.page_size === "number" ? toolArgs.page_size : void 0,
|
|
2968
|
+
sentStatus: typeof toolArgs.sent_status === "string" ? toolArgs.sent_status : void 0
|
|
2969
|
+
});
|
|
2970
|
+
case "thenvoi_respond_contact_request":
|
|
2971
|
+
if (typeof toolArgs.request_id === "string") {
|
|
2972
|
+
return this.respondContactRequest({
|
|
2973
|
+
action: String(toolArgs.action ?? "approve"),
|
|
2974
|
+
target: "requestId",
|
|
2975
|
+
requestId: toolArgs.request_id
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
return this.respondContactRequest({
|
|
2979
|
+
action: String(toolArgs.action ?? "approve"),
|
|
2980
|
+
target: "handle",
|
|
2981
|
+
handle: String(toolArgs.handle ?? "")
|
|
2982
|
+
});
|
|
2983
|
+
case "thenvoi_list_memories":
|
|
2984
|
+
return this.listMemories(toolArgs);
|
|
2985
|
+
case "thenvoi_store_memory":
|
|
2986
|
+
return this.storeMemory(toolArgs);
|
|
2987
|
+
case "thenvoi_get_memory":
|
|
2988
|
+
return this.getMemory(String(toolArgs.memory_id ?? ""));
|
|
2989
|
+
case "thenvoi_supersede_memory":
|
|
2990
|
+
return this.supersedeMemory(String(toolArgs.memory_id ?? ""));
|
|
2991
|
+
case "thenvoi_archive_memory":
|
|
2992
|
+
return this.archiveMemory(String(toolArgs.memory_id ?? ""));
|
|
2993
|
+
default:
|
|
2994
|
+
throw new UnsupportedFeatureError(`Unsupported tool call for contact callback: ${toolName}`);
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
};
|
|
2998
|
+
|
|
2999
|
+
// src/runtime/ContactEventHandler.ts
|
|
3000
|
+
var LRU_MAX_SIZE = 1e3;
|
|
3001
|
+
var HUB_ROOM_SYSTEM_PROMPT = `## OVERRIDE: Contact Management Mode
|
|
3002
|
+
|
|
3003
|
+
This is your CONTACTS HUB - a dedicated room for managing contact requests.
|
|
3004
|
+
|
|
3005
|
+
**IMPORTANT: Do NOT delegate or add participants here.** You handle contact events DIRECTLY using the contact tools below. Do NOT call thenvoi_lookup_peers() or thenvoi_add_participant() in this room.
|
|
3006
|
+
|
|
3007
|
+
## Your Role
|
|
3008
|
+
|
|
3009
|
+
1. **Review incoming contact requests** - When you see a [Contact Request] message, evaluate it
|
|
3010
|
+
2. **Take action** - Use the contact tools to respond:
|
|
3011
|
+
- \`thenvoi_respond_contact_request(action="approve", request_id="...")\` to accept
|
|
3012
|
+
- \`thenvoi_respond_contact_request(action="reject", request_id="...")\` to decline
|
|
3013
|
+
3. **Report your decision** - Send a thought event explaining what you did
|
|
3014
|
+
|
|
3015
|
+
## Example
|
|
3016
|
+
|
|
3017
|
+
[Contact Events]: [Contact Request] Alice (@alice) wants to connect.
|
|
3018
|
+
Request ID: abc-123
|
|
3019
|
+
|
|
3020
|
+
Your response:
|
|
3021
|
+
1. thenvoi_send_event("Received contact request from Alice. Approving.", message_type="thought")
|
|
3022
|
+
2. thenvoi_respond_contact_request(action="approve", request_id="abc-123")
|
|
3023
|
+
3. thenvoi_send_event("Approved contact request from Alice (@alice)", message_type="thought")
|
|
3024
|
+
|
|
3025
|
+
## Contact Tools (use these, NOT participant tools)
|
|
3026
|
+
- \`thenvoi_respond_contact_request(action, request_id)\` - Approve/reject requests
|
|
3027
|
+
- \`thenvoi_list_contact_requests()\` - List pending requests
|
|
3028
|
+
- \`thenvoi_list_contacts()\` - List current contacts`;
|
|
3029
|
+
var ContactEventHandlerError = class extends Error {
|
|
3030
|
+
retryable;
|
|
3031
|
+
stage;
|
|
3032
|
+
eventType;
|
|
3033
|
+
constructor(input) {
|
|
3034
|
+
super(`Contact event ${input.eventType} failed during ${input.stage}`, { cause: input.cause });
|
|
3035
|
+
this.name = "ContactEventHandlerError";
|
|
3036
|
+
this.retryable = input.retryable;
|
|
3037
|
+
this.stage = input.stage;
|
|
3038
|
+
this.eventType = input.eventType;
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
3041
|
+
var ContactEventHandler = class {
|
|
3042
|
+
config;
|
|
3043
|
+
rest;
|
|
3044
|
+
logger;
|
|
3045
|
+
onBroadcast;
|
|
3046
|
+
onHubEvent;
|
|
3047
|
+
onHubInit;
|
|
3048
|
+
dedup = /* @__PURE__ */ new Set();
|
|
3049
|
+
dedupOrder = [];
|
|
3050
|
+
syntheticIdCounter = 0;
|
|
3051
|
+
requestCache = /* @__PURE__ */ new Map();
|
|
3052
|
+
hubRoomId = null;
|
|
3053
|
+
hubRoomInitPromise = null;
|
|
3054
|
+
constructor(options) {
|
|
3055
|
+
this.config = options.config;
|
|
3056
|
+
this.rest = options.rest;
|
|
3057
|
+
this.logger = options.logger ?? new NoopLogger();
|
|
3058
|
+
this.onBroadcast = options.onBroadcast;
|
|
3059
|
+
this.onHubEvent = options.onHubEvent;
|
|
3060
|
+
this.onHubInit = options.onHubInit;
|
|
3061
|
+
}
|
|
3062
|
+
async handle(event, tools) {
|
|
3063
|
+
const strategy = this.config.strategy ?? "disabled";
|
|
3064
|
+
const hasBroadcast = this.config.broadcastChanges && this.onBroadcast;
|
|
3065
|
+
if (strategy === "disabled" && !hasBroadcast) {
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
const dedupKey = this.buildDedupKey(event);
|
|
3069
|
+
if (this.dedup.has(dedupKey)) {
|
|
3070
|
+
this.logger.debug("Duplicate contact event skipped", { key: dedupKey });
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3073
|
+
this.recordDedup(dedupKey);
|
|
3074
|
+
try {
|
|
3075
|
+
if (event.type === "contact_request_received") {
|
|
3076
|
+
this.cacheRequestInfo(event.payload.id, {
|
|
3077
|
+
fromHandle: event.payload.from_handle,
|
|
3078
|
+
fromName: event.payload.from_name,
|
|
3079
|
+
message: event.payload.message ?? null
|
|
3080
|
+
});
|
|
3081
|
+
}
|
|
3082
|
+
if (hasBroadcast) {
|
|
3083
|
+
const broadcastMsg = this.formatBroadcast(event);
|
|
3084
|
+
if (broadcastMsg) {
|
|
3085
|
+
this.onBroadcast(broadcastMsg);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
if (strategy === "disabled") {
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
if (strategy === "callback") {
|
|
3092
|
+
await this.handleCallback(event, tools);
|
|
3093
|
+
} else if (strategy === "hub_room") {
|
|
3094
|
+
await this.handleHubRoom(event);
|
|
3095
|
+
}
|
|
3096
|
+
} catch (error) {
|
|
3097
|
+
this.rollbackDedup(dedupKey);
|
|
3098
|
+
throw error;
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
async handleCallback(event, tools) {
|
|
3102
|
+
const callback = this.config.onEvent;
|
|
3103
|
+
if (!callback) {
|
|
3104
|
+
this.logger.warn("Contact event callback strategy configured but no onEvent callback provided");
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
const callbackTools = tools ?? new ContactCallbackTools(this.rest, event.roomId);
|
|
3108
|
+
try {
|
|
3109
|
+
await callback(event, callbackTools);
|
|
3110
|
+
} catch (error) {
|
|
3111
|
+
const failure = this.buildHandlerError(event, "callback", error, false);
|
|
3112
|
+
this.logFailure(event, failure.stage, failure.retryable, error);
|
|
3113
|
+
throw failure;
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
async handleHubRoom(event) {
|
|
3117
|
+
const hubTaskId = this.config.hubTaskId;
|
|
3118
|
+
if (!hubTaskId) {
|
|
3119
|
+
this.logger.warn("hub_room strategy requires hubTaskId");
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
if (!this.hubRoomId) {
|
|
3123
|
+
if (!this.hubRoomInitPromise) {
|
|
3124
|
+
this.hubRoomInitPromise = this.initHubRoom(hubTaskId);
|
|
3125
|
+
}
|
|
3126
|
+
try {
|
|
3127
|
+
this.hubRoomId = await this.hubRoomInitPromise;
|
|
3128
|
+
} catch (error) {
|
|
3129
|
+
this.hubRoomInitPromise = null;
|
|
3130
|
+
const failure = this.buildHandlerError(event, "hub_room_init", error, true);
|
|
3131
|
+
this.logFailure(event, failure.stage, failure.retryable, error);
|
|
3132
|
+
throw failure;
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
const formattedMessage = await this.formatEventMessage(event);
|
|
3136
|
+
let persistFailure = null;
|
|
3137
|
+
try {
|
|
3138
|
+
await this.rest.createChatEvent(this.hubRoomId, {
|
|
3139
|
+
content: formattedMessage,
|
|
3140
|
+
messageType: "contact_event",
|
|
3141
|
+
metadata: { contactEventType: event.type, payload: event.payload }
|
|
3142
|
+
});
|
|
3143
|
+
} catch (error) {
|
|
3144
|
+
persistFailure = this.buildHandlerError(event, "hub_room_persist", error, true);
|
|
3145
|
+
this.logFailure(event, persistFailure.stage, persistFailure.retryable, error);
|
|
3146
|
+
}
|
|
3147
|
+
let dispatchFailure = null;
|
|
3148
|
+
if (this.onHubEvent) {
|
|
3149
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3150
|
+
const syntheticEvent = {
|
|
3151
|
+
type: "message_created",
|
|
3152
|
+
roomId: this.hubRoomId,
|
|
3153
|
+
payload: {
|
|
3154
|
+
id: `contact-evt-${Date.now()}-${this.syntheticIdCounter++}`,
|
|
3155
|
+
content: formattedMessage,
|
|
3156
|
+
message_type: "contact_event",
|
|
3157
|
+
sender_id: SYNTHETIC_CONTACT_EVENTS_SENDER_ID,
|
|
3158
|
+
sender_type: SYNTHETIC_SENDER_TYPE,
|
|
3159
|
+
sender_name: SYNTHETIC_CONTACT_EVENTS_SENDER_NAME,
|
|
3160
|
+
inserted_at: now,
|
|
3161
|
+
updated_at: now
|
|
3162
|
+
}
|
|
3163
|
+
};
|
|
3164
|
+
try {
|
|
3165
|
+
await this.onHubEvent(this.hubRoomId, syntheticEvent);
|
|
3166
|
+
} catch (error) {
|
|
3167
|
+
dispatchFailure = this.buildHandlerError(event, "hub_room_dispatch", error, true);
|
|
3168
|
+
this.logFailure(event, dispatchFailure.stage, dispatchFailure.retryable, error);
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
if (persistFailure) {
|
|
3172
|
+
throw persistFailure;
|
|
3173
|
+
}
|
|
3174
|
+
if (dispatchFailure) {
|
|
3175
|
+
throw dispatchFailure;
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
async initHubRoom(hubTaskId) {
|
|
3179
|
+
const result = await this.rest.createChat(hubTaskId);
|
|
3180
|
+
this.logger.info("Hub room created for contact events", { roomId: result.id });
|
|
3181
|
+
if (this.onHubInit) {
|
|
3182
|
+
await this.onHubInit(result.id, HUB_ROOM_SYSTEM_PROMPT);
|
|
3183
|
+
}
|
|
3184
|
+
return result.id;
|
|
3185
|
+
}
|
|
3186
|
+
async formatEventMessage(event) {
|
|
3187
|
+
switch (event.type) {
|
|
3188
|
+
case "contact_request_received": {
|
|
3189
|
+
const msg = event.payload.message ? `
|
|
3190
|
+
Message: "${event.payload.message}"` : "";
|
|
3191
|
+
const handle = normalizeHandle(event.payload.from_handle);
|
|
3192
|
+
return `[Contact Request] ${event.payload.from_name} (${handle}) wants to connect.${msg}
|
|
3193
|
+
Request ID: ${event.payload.id}`;
|
|
3194
|
+
}
|
|
3195
|
+
case "contact_request_updated": {
|
|
3196
|
+
const info = await this.enrichUpdateEvent(event.payload.id);
|
|
3197
|
+
if (info) {
|
|
3198
|
+
const name = info.fromName ?? info.toName;
|
|
3199
|
+
const rawHandle = info.fromHandle ?? info.toHandle ?? "";
|
|
3200
|
+
const handle = normalizeHandle(rawHandle);
|
|
3201
|
+
if (name) {
|
|
3202
|
+
const direction = info.fromName ? "from" : "to";
|
|
3203
|
+
return `[Contact Request Update] Request ${direction} ${name} (${handle}) status changed to: ${event.payload.status}
|
|
3204
|
+
Request ID: ${event.payload.id}`;
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
return `[Contact Request Update] Request ${event.payload.id} status changed to: ${event.payload.status}`;
|
|
3208
|
+
}
|
|
3209
|
+
case "contact_added": {
|
|
3210
|
+
const handle = normalizeHandle(event.payload.handle);
|
|
3211
|
+
return `[Contact Added] ${event.payload.name} (${handle}) is now a contact.
|
|
3212
|
+
Type: ${event.payload.type}, ID: ${event.payload.id}`;
|
|
3213
|
+
}
|
|
3214
|
+
case "contact_removed":
|
|
3215
|
+
return `[Contact Removed] Contact ${event.payload.id} was removed.`;
|
|
3216
|
+
}
|
|
3217
|
+
return assertNever2(event);
|
|
3218
|
+
}
|
|
3219
|
+
formatBroadcast(event) {
|
|
3220
|
+
switch (event.type) {
|
|
3221
|
+
case "contact_added": {
|
|
3222
|
+
const handle = normalizeHandle(event.payload.handle);
|
|
3223
|
+
return `[Contacts]: ${handle} (${event.payload.name}) is now a contact`;
|
|
3224
|
+
}
|
|
3225
|
+
case "contact_removed":
|
|
3226
|
+
return `[Contacts]: Contact ${event.payload.id} was removed`;
|
|
3227
|
+
default:
|
|
3228
|
+
return null;
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
async enrichUpdateEvent(requestId) {
|
|
3232
|
+
const cached = this.requestCache.get(requestId);
|
|
3233
|
+
if (cached) {
|
|
3234
|
+
return cached;
|
|
3235
|
+
}
|
|
3236
|
+
this.logger.debug("Cache miss for request, fetching from API", { requestId });
|
|
3237
|
+
return await this.fetchRequestDetails(requestId);
|
|
3238
|
+
}
|
|
3239
|
+
async fetchRequestDetails(requestId) {
|
|
3240
|
+
if (!this.rest.listContactRequests) {
|
|
3241
|
+
return null;
|
|
3242
|
+
}
|
|
3243
|
+
try {
|
|
3244
|
+
const response = await this.rest.listContactRequests({ page: 1, pageSize: 100 });
|
|
3245
|
+
for (const req of response.received ?? []) {
|
|
3246
|
+
if (req.id === requestId) {
|
|
3247
|
+
const info = {
|
|
3248
|
+
fromHandle: req.from_handle,
|
|
3249
|
+
fromName: req.from_name,
|
|
3250
|
+
message: req.message ?? null
|
|
3251
|
+
};
|
|
3252
|
+
this.cacheRequestInfo(requestId, info);
|
|
3253
|
+
this.logger.debug("Fetched request details from API (received)", { requestId });
|
|
3254
|
+
return info;
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
for (const req of response.sent ?? []) {
|
|
3258
|
+
if (req.id === requestId) {
|
|
3259
|
+
const info = {
|
|
3260
|
+
toHandle: req.to_handle,
|
|
3261
|
+
toName: req.to_name,
|
|
3262
|
+
message: req.message ?? null
|
|
3263
|
+
};
|
|
3264
|
+
this.cacheRequestInfo(requestId, info);
|
|
3265
|
+
this.logger.debug("Fetched request details from API (sent)", { requestId });
|
|
3266
|
+
return info;
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
this.logger.debug("Request not found in API", { requestId });
|
|
3270
|
+
return null;
|
|
3271
|
+
} catch (error) {
|
|
3272
|
+
this.logger.warn("Failed to fetch request details from API", { requestId, error });
|
|
3273
|
+
return null;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
cacheRequestInfo(id, info) {
|
|
3277
|
+
this.requestCache.set(id, info);
|
|
3278
|
+
while (this.requestCache.size > LRU_MAX_SIZE) {
|
|
3279
|
+
const first = this.requestCache.keys().next();
|
|
3280
|
+
if (first.done) break;
|
|
3281
|
+
this.requestCache.delete(first.value);
|
|
3282
|
+
}
|
|
3283
|
+
}
|
|
3284
|
+
buildDedupKey(event) {
|
|
3285
|
+
const id = event.payload.id;
|
|
3286
|
+
if (event.type === "contact_request_updated") {
|
|
3287
|
+
return `${event.type}:${id}:${event.payload.status}`;
|
|
3288
|
+
}
|
|
3289
|
+
return `${event.type}:${id}`;
|
|
3290
|
+
}
|
|
3291
|
+
recordDedup(key) {
|
|
3292
|
+
this.dedup.add(key);
|
|
3293
|
+
this.dedupOrder.push(key);
|
|
3294
|
+
while (this.dedupOrder.length > LRU_MAX_SIZE) {
|
|
3295
|
+
const evicted = this.dedupOrder.shift();
|
|
3296
|
+
if (evicted) {
|
|
3297
|
+
this.dedup.delete(evicted);
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
rollbackDedup(key) {
|
|
3302
|
+
if (!this.dedup.delete(key)) {
|
|
3303
|
+
return;
|
|
3304
|
+
}
|
|
3305
|
+
const index = this.dedupOrder.lastIndexOf(key);
|
|
3306
|
+
if (index >= 0) {
|
|
3307
|
+
this.dedupOrder.splice(index, 1);
|
|
3308
|
+
}
|
|
3309
|
+
}
|
|
3310
|
+
buildHandlerError(event, stage, cause, defaultRetryable) {
|
|
3311
|
+
return new ContactEventHandlerError({
|
|
3312
|
+
eventType: event.type,
|
|
3313
|
+
stage,
|
|
3314
|
+
retryable: this.resolveRetryable(cause, defaultRetryable),
|
|
3315
|
+
cause
|
|
3316
|
+
});
|
|
3317
|
+
}
|
|
3318
|
+
resolveRetryable(error, defaultRetryable) {
|
|
3319
|
+
if (typeof error === "object" && error !== null && "retryable" in error) {
|
|
3320
|
+
return error.retryable === true;
|
|
3321
|
+
}
|
|
3322
|
+
return defaultRetryable;
|
|
3323
|
+
}
|
|
3324
|
+
logFailure(event, stage, retryable, error) {
|
|
3325
|
+
this.logger.error("contact_event.failure", {
|
|
3326
|
+
type: event.type,
|
|
3327
|
+
stage,
|
|
3328
|
+
retryable,
|
|
3329
|
+
error: this.serializeError(error)
|
|
3330
|
+
});
|
|
3331
|
+
}
|
|
3332
|
+
serializeError(error) {
|
|
3333
|
+
if (error instanceof Error) {
|
|
3334
|
+
const payload = {
|
|
3335
|
+
name: error.name,
|
|
3336
|
+
message: error.message,
|
|
3337
|
+
stack: error.stack
|
|
3338
|
+
};
|
|
3339
|
+
if (typeof error.retryable !== "undefined") {
|
|
3340
|
+
payload.retryable = error.retryable;
|
|
3341
|
+
}
|
|
3342
|
+
return payload;
|
|
3343
|
+
}
|
|
3344
|
+
return { message: String(error) };
|
|
3345
|
+
}
|
|
3346
|
+
};
|
|
3347
|
+
function assertNever2(value) {
|
|
3348
|
+
throw new Error(`Unhandled contact event: ${JSON.stringify(value)}`);
|
|
3349
|
+
}
|
|
3350
|
+
function normalizeHandle(handle) {
|
|
3351
|
+
if (!handle) return "@unknown";
|
|
3352
|
+
return handle.startsWith("@") ? handle : `@${handle}`;
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
// src/runtime/PlatformRuntime.ts
|
|
3356
|
+
var PlatformRuntime = class {
|
|
3357
|
+
_agentId;
|
|
3358
|
+
_apiKey;
|
|
3359
|
+
_wsUrl;
|
|
3360
|
+
_restUrl;
|
|
3361
|
+
preprocessor;
|
|
3362
|
+
sessionConfig;
|
|
3363
|
+
contactConfig;
|
|
3364
|
+
agentConfig;
|
|
3365
|
+
linkOptions;
|
|
3366
|
+
configuredIdentity;
|
|
3367
|
+
logger;
|
|
3368
|
+
_onParticipantAdded;
|
|
3369
|
+
_onParticipantRemoved;
|
|
3370
|
+
_roomFilter;
|
|
3371
|
+
_contextFactory;
|
|
3372
|
+
linkInstance;
|
|
3373
|
+
initPromise = null;
|
|
3374
|
+
runtime;
|
|
3375
|
+
contactHandler;
|
|
3376
|
+
activeAdapter;
|
|
3377
|
+
stopping = false;
|
|
3378
|
+
_agentName = "";
|
|
3379
|
+
_agentDescription = "";
|
|
3380
|
+
contactsSubscribed = false;
|
|
3381
|
+
constructor(options) {
|
|
3382
|
+
if (!options.agentId || options.agentId.trim() === "") {
|
|
3383
|
+
throw new ValidationError(
|
|
3384
|
+
"agentId is required and must be a non-empty string. Use loadAgentConfig() to load credentials from agent_config.yaml."
|
|
3385
|
+
);
|
|
3386
|
+
}
|
|
3387
|
+
if (!options.apiKey || options.apiKey.trim() === "") {
|
|
3388
|
+
throw new ValidationError(
|
|
3389
|
+
"apiKey is required and must be a non-empty string. Use loadAgentConfig() to load credentials from agent_config.yaml."
|
|
3390
|
+
);
|
|
3391
|
+
}
|
|
3392
|
+
this._agentId = options.agentId;
|
|
3393
|
+
this._apiKey = options.apiKey;
|
|
3394
|
+
this._wsUrl = options.wsUrl;
|
|
3395
|
+
this._restUrl = options.restUrl;
|
|
3396
|
+
this.linkInstance = options.link;
|
|
3397
|
+
this.linkOptions = options.linkOptions;
|
|
3398
|
+
this.preprocessor = options.preprocessor ?? new DefaultPreprocessor();
|
|
3399
|
+
this.sessionConfig = options.sessionConfig;
|
|
3400
|
+
this.contactConfig = options.contactConfig;
|
|
3401
|
+
this.agentConfig = options.agentConfig;
|
|
3402
|
+
this.logger = options.logger ?? new NoopLogger();
|
|
3403
|
+
this.configuredIdentity = options.identity;
|
|
3404
|
+
this._onParticipantAdded = options.onParticipantAdded;
|
|
3405
|
+
this._onParticipantRemoved = options.onParticipantRemoved;
|
|
3406
|
+
this._roomFilter = options.roomFilter;
|
|
3407
|
+
this._contextFactory = options.contextFactory;
|
|
3408
|
+
}
|
|
3409
|
+
get link() {
|
|
3410
|
+
if (!this.linkInstance) {
|
|
3411
|
+
throw new RuntimeStateError("Runtime is not initialized");
|
|
3412
|
+
}
|
|
3413
|
+
return this.linkInstance;
|
|
3414
|
+
}
|
|
3415
|
+
get name() {
|
|
3416
|
+
return this._agentName;
|
|
3417
|
+
}
|
|
3418
|
+
get agentId() {
|
|
3419
|
+
return this._agentId;
|
|
3420
|
+
}
|
|
3421
|
+
get description() {
|
|
3422
|
+
return this._agentDescription;
|
|
3423
|
+
}
|
|
3424
|
+
get contactConfiguration() {
|
|
3425
|
+
return this.contactConfig;
|
|
3426
|
+
}
|
|
3427
|
+
get isContactsSubscribed() {
|
|
3428
|
+
return this.contactsSubscribed;
|
|
3429
|
+
}
|
|
3430
|
+
async initialize() {
|
|
3431
|
+
if (this.initPromise) {
|
|
3432
|
+
return this.initPromise;
|
|
3433
|
+
}
|
|
3434
|
+
this.initPromise = this.doInitialize();
|
|
3435
|
+
try {
|
|
3436
|
+
await this.initPromise;
|
|
3437
|
+
} catch (error) {
|
|
3438
|
+
this.initPromise = null;
|
|
3439
|
+
throw error;
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
async doInitialize() {
|
|
3443
|
+
if (!this.linkInstance) {
|
|
3444
|
+
this.linkInstance = new ThenvoiLink({
|
|
3445
|
+
...this.linkOptions,
|
|
3446
|
+
agentId: this._agentId,
|
|
3447
|
+
apiKey: this._apiKey,
|
|
3448
|
+
wsUrl: this._wsUrl,
|
|
3449
|
+
restUrl: this._restUrl
|
|
3450
|
+
});
|
|
3451
|
+
}
|
|
3452
|
+
if (this.configuredIdentity) {
|
|
3453
|
+
this._agentName = this.configuredIdentity.name;
|
|
3454
|
+
this._agentDescription = this.configuredIdentity.description ?? "";
|
|
3455
|
+
return;
|
|
3456
|
+
}
|
|
3457
|
+
const me = await this.link.rest.getAgentMe();
|
|
3458
|
+
this._agentName = me.name;
|
|
3459
|
+
this._agentDescription = me.description ?? "";
|
|
3460
|
+
}
|
|
3461
|
+
async start(adapter) {
|
|
3462
|
+
await this.initialize();
|
|
3463
|
+
await adapter.onStarted(this._agentName, this._agentDescription);
|
|
3464
|
+
this.activeAdapter = adapter;
|
|
3465
|
+
try {
|
|
3466
|
+
this.contactHandler = new ContactEventHandler({
|
|
3467
|
+
config: this.contactConfig ?? { strategy: "disabled" },
|
|
3468
|
+
rest: this.link.rest,
|
|
3469
|
+
onBroadcast: (message) => {
|
|
3470
|
+
const runtime = this.runtime;
|
|
3471
|
+
if (!runtime) return;
|
|
3472
|
+
for (const context of runtime.getContexts()) {
|
|
3473
|
+
context.injectSystemMessage(message);
|
|
3474
|
+
}
|
|
3475
|
+
},
|
|
3476
|
+
onHubEvent: async (roomId, event) => {
|
|
3477
|
+
const runtime = this.runtime;
|
|
3478
|
+
if (!runtime) return;
|
|
3479
|
+
await runtime.enqueueEvent(roomId, event);
|
|
3480
|
+
},
|
|
3481
|
+
onHubInit: async (roomId, systemPrompt) => {
|
|
3482
|
+
const runtime = this.runtime;
|
|
3483
|
+
if (!runtime) return;
|
|
3484
|
+
runtime.getOrCreateContext(roomId).injectSystemMessage(systemPrompt);
|
|
3485
|
+
}
|
|
3486
|
+
});
|
|
3487
|
+
this.runtime = new AgentRuntime({
|
|
3488
|
+
link: this.link,
|
|
3489
|
+
agentId: this._agentId,
|
|
3490
|
+
sessionConfig: this.sessionConfig,
|
|
3491
|
+
agentConfig: this.agentConfig,
|
|
3492
|
+
logger: this.logger,
|
|
3493
|
+
onExecute: (context, event) => this.executeAdapter(context, event, adapter),
|
|
3494
|
+
onSessionCleanup: (roomId) => adapter.onCleanup(roomId),
|
|
3495
|
+
onContactEvent: (event) => this.handleContactEvent(event),
|
|
3496
|
+
onParticipantAdded: this._onParticipantAdded,
|
|
3497
|
+
onParticipantRemoved: this._onParticipantRemoved,
|
|
3498
|
+
roomFilter: this._roomFilter,
|
|
3499
|
+
contextFactory: this._contextFactory
|
|
3500
|
+
});
|
|
3501
|
+
await this.runtime.start();
|
|
3502
|
+
this.contactsSubscribed = Boolean(this.link.capabilities.contacts);
|
|
3503
|
+
} catch (error) {
|
|
3504
|
+
try {
|
|
3505
|
+
await this.stop();
|
|
3506
|
+
} catch (stopError) {
|
|
3507
|
+
throw new AggregateError(
|
|
3508
|
+
[error, stopError],
|
|
3509
|
+
"PlatformRuntime failed to start and cleanup also failed"
|
|
3510
|
+
);
|
|
3511
|
+
}
|
|
3512
|
+
throw error;
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
async stop(timeoutMs) {
|
|
3516
|
+
if (this.stopping) {
|
|
3517
|
+
return true;
|
|
3518
|
+
}
|
|
3519
|
+
const runtime = this.runtime;
|
|
3520
|
+
const adapter = this.activeAdapter;
|
|
3521
|
+
if (!runtime && !adapter) {
|
|
3522
|
+
return true;
|
|
3523
|
+
}
|
|
3524
|
+
this.stopping = true;
|
|
3525
|
+
this.runtime = void 0;
|
|
3526
|
+
this.contactHandler = void 0;
|
|
3527
|
+
this.contactsSubscribed = false;
|
|
3528
|
+
this.activeAdapter = void 0;
|
|
3529
|
+
let graceful = true;
|
|
3530
|
+
let runtimeError = null;
|
|
3531
|
+
if (runtime) {
|
|
3532
|
+
try {
|
|
3533
|
+
graceful = await runtime.stop(timeoutMs);
|
|
3534
|
+
} catch (error) {
|
|
3535
|
+
runtimeError = error;
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
try {
|
|
3539
|
+
await adapter?.onRuntimeStop?.();
|
|
3540
|
+
} catch (error) {
|
|
3541
|
+
if (runtimeError) {
|
|
3542
|
+
throw new AggregateError(
|
|
3543
|
+
[runtimeError, error],
|
|
3544
|
+
"PlatformRuntime stop failed and adapter cleanup also failed"
|
|
3545
|
+
);
|
|
3546
|
+
}
|
|
3547
|
+
throw error;
|
|
3548
|
+
}
|
|
3549
|
+
if (runtimeError) {
|
|
3550
|
+
throw runtimeError instanceof Error ? runtimeError : new Error(String(runtimeError));
|
|
3551
|
+
}
|
|
3552
|
+
this.stopping = false;
|
|
3553
|
+
return graceful;
|
|
3554
|
+
}
|
|
3555
|
+
async runForever() {
|
|
3556
|
+
if (!this.runtime) {
|
|
3557
|
+
throw new RuntimeStateError("Runtime not started");
|
|
3558
|
+
}
|
|
3559
|
+
await this.runtime.waitUntilStopped();
|
|
3560
|
+
}
|
|
3561
|
+
async bootstrapRoomMessage(roomId, message) {
|
|
3562
|
+
if (!this.runtime) {
|
|
3563
|
+
throw new RuntimeStateError("Runtime not started");
|
|
3564
|
+
}
|
|
3565
|
+
await this.runtime.bootstrapRoomMessage(roomId, message);
|
|
3566
|
+
}
|
|
3567
|
+
async resetRoomSession(roomId, timeoutMs) {
|
|
3568
|
+
if (!this.runtime) {
|
|
3569
|
+
throw new RuntimeStateError("Runtime not started");
|
|
3570
|
+
}
|
|
3571
|
+
return await this.runtime.resetRoomSession(roomId, timeoutMs);
|
|
3572
|
+
}
|
|
3573
|
+
async executeAdapter(context, event, adapter) {
|
|
3574
|
+
const input = await this.preprocessor.process(context, event, this._agentId);
|
|
3575
|
+
if (!input) {
|
|
3576
|
+
return;
|
|
3577
|
+
}
|
|
3578
|
+
const messageId = String(input.message.id ?? "");
|
|
3579
|
+
const roomId = input.roomId;
|
|
3580
|
+
const isSynthetic = input.message.senderType === SYNTHETIC_SENDER_TYPE && input.message.senderId === SYNTHETIC_CONTACT_EVENTS_SENDER_ID;
|
|
3581
|
+
const messageMarkOptions = { bestEffort: true };
|
|
3582
|
+
if (messageId && !isSynthetic) {
|
|
3583
|
+
await this.link.markProcessing(roomId, messageId, messageMarkOptions);
|
|
3584
|
+
}
|
|
3585
|
+
try {
|
|
3586
|
+
await adapter.onEvent(input);
|
|
3587
|
+
if (messageId && !isSynthetic) {
|
|
3588
|
+
await this.link.markProcessed(roomId, messageId, messageMarkOptions);
|
|
3589
|
+
}
|
|
3590
|
+
} catch (error) {
|
|
3591
|
+
const label = error instanceof Error ? error.message : String(error);
|
|
3592
|
+
if (messageId && !isSynthetic) {
|
|
3593
|
+
await this.link.markFailed(roomId, messageId, label, messageMarkOptions);
|
|
3594
|
+
}
|
|
3595
|
+
throw error;
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
async handleContactEvent(event) {
|
|
3599
|
+
if (this.contactHandler) {
|
|
3600
|
+
await this.contactHandler.handle(event);
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
};
|
|
3604
|
+
|
|
3605
|
+
// src/runtime/shutdown.ts
|
|
3606
|
+
var DEFAULT_SHUTDOWN_TIMEOUT_MS = 3e4;
|
|
3607
|
+
var GracefulShutdown = class {
|
|
3608
|
+
agent;
|
|
3609
|
+
timeoutMs;
|
|
3610
|
+
onSignal;
|
|
3611
|
+
handlers = /* @__PURE__ */ new Map();
|
|
3612
|
+
shuttingDown = false;
|
|
3613
|
+
constructor(agent, options) {
|
|
3614
|
+
this.agent = agent;
|
|
3615
|
+
this.timeoutMs = options?.timeoutMs === void 0 ? DEFAULT_SHUTDOWN_TIMEOUT_MS : options.timeoutMs;
|
|
3616
|
+
this.onSignal = options?.onSignal;
|
|
3617
|
+
}
|
|
3618
|
+
registerSignals() {
|
|
3619
|
+
const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
3620
|
+
for (const signal of signals) {
|
|
3621
|
+
const handler = () => {
|
|
3622
|
+
this.onSignal?.(signal);
|
|
3623
|
+
void this.shutdown(signal);
|
|
3624
|
+
};
|
|
3625
|
+
process.on(signal, handler);
|
|
3626
|
+
this.handlers.set(signal, handler);
|
|
3627
|
+
}
|
|
3628
|
+
}
|
|
3629
|
+
unregisterSignals() {
|
|
3630
|
+
for (const [signal, handler] of this.handlers.entries()) {
|
|
3631
|
+
process.off(signal, handler);
|
|
3632
|
+
}
|
|
3633
|
+
this.handlers.clear();
|
|
3634
|
+
}
|
|
3635
|
+
async withSignals(fn) {
|
|
3636
|
+
this.registerSignals();
|
|
3637
|
+
try {
|
|
3638
|
+
return await fn();
|
|
3639
|
+
} finally {
|
|
3640
|
+
this.unregisterSignals();
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
async shutdown(_signal) {
|
|
3644
|
+
if (this.shuttingDown) {
|
|
3645
|
+
process.exit(1);
|
|
3646
|
+
}
|
|
3647
|
+
this.shuttingDown = true;
|
|
3648
|
+
try {
|
|
3649
|
+
await this.agent.stop(this.timeoutMs);
|
|
3650
|
+
} catch {
|
|
3651
|
+
process.exit(1);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
};
|
|
3655
|
+
async function runWithGracefulShutdown(agent, options) {
|
|
3656
|
+
const shutdown = new GracefulShutdown(agent, options);
|
|
3657
|
+
await shutdown.withSignals(async () => {
|
|
3658
|
+
await agent.run({
|
|
3659
|
+
shutdownTimeoutMs: options?.timeoutMs === void 0 ? DEFAULT_SHUTDOWN_TIMEOUT_MS : options.timeoutMs,
|
|
3660
|
+
signals: false
|
|
3661
|
+
});
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
export {
|
|
3666
|
+
ThenvoiLink,
|
|
3667
|
+
Execution,
|
|
3668
|
+
AgentTools,
|
|
3669
|
+
MessageRetryTracker,
|
|
3670
|
+
ExecutionContext,
|
|
3671
|
+
trackRoomJoin,
|
|
3672
|
+
trackRoomLeave,
|
|
3673
|
+
hydrateTrackedRooms,
|
|
3674
|
+
AgentRuntime,
|
|
3675
|
+
DefaultPreprocessor,
|
|
3676
|
+
ContactToolsImpl,
|
|
3677
|
+
HUB_ROOM_SYSTEM_PROMPT,
|
|
3678
|
+
ContactEventHandler,
|
|
3679
|
+
PlatformRuntime,
|
|
3680
|
+
GracefulShutdown,
|
|
3681
|
+
runWithGracefulShutdown
|
|
3682
|
+
};
|