@band-ai/sdk 0.3.4 → 0.4.1
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/README.md +54 -1
- package/dist/{ClaudeSDKAdapter-CXud2DBE.d.ts → CopilotACPAdapter-B_AVyAl6.d.ts} +249 -19
- package/dist/{ClaudeSDKAdapter-Cx6zhSaG.d.cts → CopilotACPAdapter-Clid9xGR.d.cts} +249 -19
- package/dist/{opencode-2YdscIc1.d.cts → acp-client-D-I_5lK-.d.cts} +23 -4
- package/dist/{opencode-Bld62x5t.d.ts → acp-client-DUXyczlF.d.ts} +23 -4
- package/dist/acp-server-BlcYMxKt.d.ts +17 -0
- package/dist/acp-server-CJhclo6P.d.cts +17 -0
- package/dist/adapters.cjs +2257 -969
- package/dist/adapters.d.cts +21 -132
- package/dist/adapters.d.ts +21 -132
- package/dist/adapters.js +26 -1922
- package/dist/{backends-D-dBdDm-.d.ts → backends-CgsPQa9B.d.ts} +2 -2
- package/dist/{backends-ZLxmQrTZ.d.cts → backends-Dh2WXcHQ.d.cts} +2 -2
- package/dist/{chunk-JY75VSQK.js → chunk-4OVNFDD7.js} +14 -1
- package/dist/chunk-6BE2EG5P.js +128 -0
- package/dist/{chunk-U6AFCLYZ.js → chunk-6TGXV5TE.js} +1824 -514
- package/dist/{chunk-OH6VVVKT.js → chunk-CBCHTTUY.js} +10 -0
- package/dist/{chunk-K5KAEYPF.js → chunk-HNWZZXAE.js} +288 -120
- package/dist/chunk-I34SXMTJ.js +6 -0
- package/dist/{chunk-XWHWJP4I.js → chunk-JDW5WSGF.js} +2 -2
- package/dist/{chunk-KXJ74J73.js → chunk-JIZE4EC6.js} +4 -10
- package/dist/chunk-OGOXY43L.js +119 -0
- package/dist/{chunk-W5IAI6Z4.js → chunk-OVXVZUPO.js} +5 -5
- package/dist/{chunk-YJ42AZO4.js → chunk-RJNJUISR.js} +5 -2
- package/dist/{chunk-MQY74R7D.js → chunk-SHZBLLDQ.js} +21 -9
- package/dist/{chunk-JADCAMZY.js → chunk-TGDDFBYS.js} +45 -0
- package/dist/{chunk-US226AS4.js → chunk-UENIC5FJ.js} +14 -11
- package/dist/{chunk-MQBBWYRP.js → chunk-UFO6XMCV.js} +3404 -572
- package/dist/{chunk-WA7CHXUS.js → chunk-UL3Y5C4J.js} +2 -2
- package/dist/{chunk-WL2HNJZ6.js → chunk-VOMJRQOM.js} +3 -3
- package/dist/{chunk-OD2G5LFQ.js → chunk-ZYOXI4JL.js} +41 -2
- package/dist/config.cjs +2 -12
- package/dist/config.js +2 -2
- package/dist/converters.cjs +36 -11
- package/dist/converters.d.cts +7 -5
- package/dist/converters.d.ts +7 -5
- package/dist/converters.js +11 -13
- package/dist/core.cjs +239 -5
- package/dist/core.d.cts +71 -6
- package/dist/core.d.ts +71 -6
- package/dist/core.js +31 -6
- package/dist/{customTools-BzF0IISO.d.cts → customTools-Bfecd0mJ.d.cts} +1 -1
- package/dist/{customTools-BzF0IISO.d.ts → customTools-Bfecd0mJ.d.ts} +1 -1
- package/dist/{disconnectReason-IuADl6TH.d.ts → disconnectReason-Cctmg1SN.d.cts} +1 -1
- package/dist/{disconnectReason-IuADl6TH.d.cts → disconnectReason-Cctmg1SN.d.ts} +1 -1
- package/dist/{dtos-B8KU_q4d.d.ts → dtos-CjQsN1_h.d.cts} +5 -12
- package/dist/{dtos-B8KU_q4d.d.cts → dtos-CjQsN1_h.d.ts} +5 -12
- package/dist/index.cjs +5289 -1474
- package/dist/index.d.cts +61 -22
- package/dist/index.d.ts +61 -22
- package/dist/index.js +151 -43
- package/dist/linear.cjs +45 -7
- package/dist/linear.d.cts +4 -3
- package/dist/linear.d.ts +4 -3
- package/dist/linear.js +10 -10
- package/dist/mcp-claude.cjs +11 -6
- package/dist/mcp-claude.d.cts +4 -3
- package/dist/mcp-claude.d.ts +4 -3
- package/dist/mcp-claude.js +4 -5
- package/dist/mcp.cjs +13 -7
- package/dist/mcp.d.cts +6 -5
- package/dist/mcp.d.ts +6 -5
- package/dist/mcp.js +5 -6
- package/dist/{pagination-DqdkQArT.d.cts → pagination-BZDhB_nO.d.cts} +2 -2
- package/dist/{pagination-BgEx-HeL.d.ts → pagination-CPz0J_vQ.d.ts} +2 -2
- package/dist/{protocols-Bz1rxcUg.d.cts → protocols-CabgO52c.d.cts} +38 -3
- package/dist/{protocols-Bwwcql0h.d.ts → protocols-Dpci6uae.d.ts} +38 -3
- package/dist/rest.cjs +43 -1
- package/dist/rest.d.cts +6 -5
- package/dist/rest.d.ts +6 -5
- package/dist/rest.js +6 -5
- package/dist/runtime.cjs +2113 -597
- package/dist/runtime.d.cts +44 -25
- package/dist/runtime.d.ts +44 -25
- package/dist/runtime.js +11 -11
- package/dist/{schemas-BgJhlqqO.d.cts → schemas-BKnh4fKR.d.cts} +209 -24
- package/dist/{schemas-C4mK1WSs.d.ts → schemas-CDa3V_I5.d.ts} +209 -24
- package/dist/{sdk-BTciSLRQ.d.cts → sdk-CXNqzoY1.d.cts} +1 -1
- package/dist/{sdk-qKJtMma_.d.ts → sdk-Cin80BTC.d.ts} +1 -1
- package/dist/{simpleAdapter-BpT4XbZC.d.cts → simpleAdapter-D75rcz9n.d.cts} +3 -1
- package/dist/{simpleAdapter--wznuoOw.d.ts → simpleAdapter-eLvsAQNo.d.ts} +3 -1
- package/dist/testing.cjs +25 -7
- package/dist/testing.d.cts +5 -3
- package/dist/testing.d.ts +5 -3
- package/dist/testing.js +9 -6
- package/dist/{types-BivGO7I9.d.ts → types-42-HWeX8.d.cts} +10 -4
- package/dist/types-CKU1N0SK.d.ts +137 -0
- package/dist/{types-BgYFd_Yw.d.cts → types-CjxZekMH.d.ts} +10 -4
- package/dist/types-DtcOLALn.d.cts +137 -0
- package/package.json +25 -26
- package/dist/acp-server-CiUqN3G3.d.ts +0 -24
- package/dist/acp-server-Dlj7D563.d.cts +0 -24
- package/dist/chunk-AVVDPXP4.js +0 -39
- package/dist/chunk-F63TPOI3.js +0 -29
- package/dist/chunk-V5TSWS7P.js +0 -62
- package/dist/types-BNNtOXu3.d.cts +0 -446
- package/dist/types-DFUtpxah.d.ts +0 -446
package/dist/runtime.cjs
CHANGED
|
@@ -41,11 +41,11 @@ __export(runtime_exports, {
|
|
|
41
41
|
MCP_TOOL_PREFIX: () => MCP_TOOL_PREFIX,
|
|
42
42
|
MEMORY_SECTION: () => MEMORY_SECTION,
|
|
43
43
|
MEMORY_TOOL_NAMES: () => MEMORY_TOOL_NAMES,
|
|
44
|
-
ParticipantRoster: () =>
|
|
44
|
+
ParticipantRoster: () => import_band_sdk_core10.ParticipantRoster,
|
|
45
45
|
PlatformRuntime: () => PlatformRuntime,
|
|
46
|
-
RetryTracker: () =>
|
|
46
|
+
RetryTracker: () => import_band_sdk_core10.RetryTracker,
|
|
47
47
|
RoomPresence: () => RoomPresence,
|
|
48
|
-
RoomRoster: () =>
|
|
48
|
+
RoomRoster: () => import_band_sdk_core10.RoomRoster,
|
|
49
49
|
SYNTHETIC_CONTACT_EVENTS_SENDER_ID: () => SYNTHETIC_CONTACT_EVENTS_SENDER_ID,
|
|
50
50
|
SYNTHETIC_CONTACT_EVENTS_SENDER_NAME: () => SYNTHETIC_CONTACT_EVENTS_SENDER_NAME,
|
|
51
51
|
SYNTHETIC_SENDER_TYPE: () => SYNTHETIC_SENDER_TYPE,
|
|
@@ -89,14 +89,175 @@ function ensureHandlePrefix(handle) {
|
|
|
89
89
|
return handle.startsWith("@") ? handle : `@${handle}`;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
// src/core/sensitiveTerms.ts
|
|
93
|
+
var SENSITIVE_KEY_TERMS = "authorization|api[-_ ]?key|token|secret|password|cookie";
|
|
94
|
+
var GATEWAY_FREE_TEXT_SENSITIVE_KEY_TERMS = `${SENSITIVE_KEY_TERMS}|session`;
|
|
95
|
+
|
|
92
96
|
// src/core/logger.ts
|
|
93
97
|
var noop = () => void 0;
|
|
98
|
+
var SENSITIVE_KEY_PATTERN = new RegExp(`(${SENSITIVE_KEY_TERMS})`, "i");
|
|
94
99
|
var NoopLogger = class {
|
|
95
100
|
debug = noop;
|
|
96
101
|
info = noop;
|
|
97
102
|
warn = noop;
|
|
98
103
|
error = noop;
|
|
99
104
|
};
|
|
105
|
+
function resolveLogger(logger) {
|
|
106
|
+
if (!logger) {
|
|
107
|
+
return new NoopLogger();
|
|
108
|
+
}
|
|
109
|
+
return logger instanceof GuardedLogger ? logger : new GuardedLogger(logger);
|
|
110
|
+
}
|
|
111
|
+
var GuardedLogger = class {
|
|
112
|
+
constructor(inner) {
|
|
113
|
+
this.inner = inner;
|
|
114
|
+
}
|
|
115
|
+
debug(message, context) {
|
|
116
|
+
return this.emit("debug", message, context);
|
|
117
|
+
}
|
|
118
|
+
info(message, context) {
|
|
119
|
+
return this.emit("info", message, context);
|
|
120
|
+
}
|
|
121
|
+
warn(message, context) {
|
|
122
|
+
return this.emit("warn", message, context);
|
|
123
|
+
}
|
|
124
|
+
error(message, context) {
|
|
125
|
+
return this.emit("error", message, context);
|
|
126
|
+
}
|
|
127
|
+
emit(level, message, context) {
|
|
128
|
+
try {
|
|
129
|
+
const result = this.inner[level](message, context);
|
|
130
|
+
if (result instanceof Promise) {
|
|
131
|
+
result.catch(noop);
|
|
132
|
+
}
|
|
133
|
+
return result;
|
|
134
|
+
} catch {
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// src/core/coercion.ts
|
|
140
|
+
var MAX_ERROR_CAUSE_DEPTH = 5;
|
|
141
|
+
var MAX_ERROR_DETAIL_LENGTH = 500;
|
|
142
|
+
var LENGTH_TRUNCATED_MARKER = "... (truncated)";
|
|
143
|
+
var DEPTH_TRUNCATED_MARKER = "... (cause chain truncated)";
|
|
144
|
+
function asOptionalRecord(value) {
|
|
145
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
function asNonEmptyString(value) {
|
|
151
|
+
if (typeof value !== "string") {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const trimmed = value.trim();
|
|
155
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
156
|
+
}
|
|
157
|
+
function toDisplayText(value) {
|
|
158
|
+
if (typeof value === "string") {
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
if (value == null) {
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const json = JSON.stringify(value);
|
|
166
|
+
return typeof json === "string" ? json : String(value);
|
|
167
|
+
} catch {
|
|
168
|
+
return String(value);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
function asErrorMessage(error) {
|
|
172
|
+
return formatCaughtError(error, 0);
|
|
173
|
+
}
|
|
174
|
+
function formatCaughtError(error, depth) {
|
|
175
|
+
if (typeof error !== "object" || error === null) {
|
|
176
|
+
return truncate(String(error));
|
|
177
|
+
}
|
|
178
|
+
const message = truncate(asNestedMessage(error) ?? String(error));
|
|
179
|
+
const detail = selectDetail(asOptionalRecord(error));
|
|
180
|
+
if (detail === void 0) {
|
|
181
|
+
return message;
|
|
182
|
+
}
|
|
183
|
+
if (depth >= MAX_ERROR_CAUSE_DEPTH) {
|
|
184
|
+
return `${message} (${DEPTH_TRUNCATED_MARKER})`;
|
|
185
|
+
}
|
|
186
|
+
return `${message} (${formatErrorDetail(detail, depth + 1)})`;
|
|
187
|
+
}
|
|
188
|
+
function selectDetail(record) {
|
|
189
|
+
if (isPresentDetail(record?.data)) {
|
|
190
|
+
return record?.data;
|
|
191
|
+
}
|
|
192
|
+
if (isPresentDetail(record?.cause)) {
|
|
193
|
+
return record?.cause;
|
|
194
|
+
}
|
|
195
|
+
return void 0;
|
|
196
|
+
}
|
|
197
|
+
function isPresentDetail(value) {
|
|
198
|
+
if (value === void 0 || value === null) {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
if (typeof value === "string") {
|
|
202
|
+
return asNonEmptyString(value) !== null;
|
|
203
|
+
}
|
|
204
|
+
if (value instanceof Error) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
if (Array.isArray(value)) {
|
|
208
|
+
return value.length > 0;
|
|
209
|
+
}
|
|
210
|
+
if (typeof value === "object") {
|
|
211
|
+
return isPlainObject(value) ? Object.keys(value).length > 0 : true;
|
|
212
|
+
}
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
function isPlainObject(value) {
|
|
216
|
+
const proto = Object.getPrototypeOf(value);
|
|
217
|
+
return proto === Object.prototype || proto === null;
|
|
218
|
+
}
|
|
219
|
+
function formatErrorDetail(detail, depth) {
|
|
220
|
+
if (detail instanceof Error || selectDetail(asOptionalRecord(detail)) !== void 0) {
|
|
221
|
+
return formatCaughtError(detail, depth);
|
|
222
|
+
}
|
|
223
|
+
return truncate(formatLeafDetail(detail));
|
|
224
|
+
}
|
|
225
|
+
function formatLeafDetail(detail) {
|
|
226
|
+
if (typeof detail === "string") {
|
|
227
|
+
return detail;
|
|
228
|
+
}
|
|
229
|
+
if (typeof detail === "number" && !Number.isFinite(detail)) {
|
|
230
|
+
return String(detail);
|
|
231
|
+
}
|
|
232
|
+
const builtin = formatNonEnumerableDetail(detail);
|
|
233
|
+
if (builtin !== null) {
|
|
234
|
+
return builtin;
|
|
235
|
+
}
|
|
236
|
+
const nested = asNestedMessage(detail);
|
|
237
|
+
if (nested !== null) {
|
|
238
|
+
return nested;
|
|
239
|
+
}
|
|
240
|
+
return toDisplayText(detail);
|
|
241
|
+
}
|
|
242
|
+
function formatNonEnumerableDetail(detail) {
|
|
243
|
+
if (detail instanceof RegExp) {
|
|
244
|
+
return detail.toString();
|
|
245
|
+
}
|
|
246
|
+
if (detail instanceof Map) {
|
|
247
|
+
return `Map ${toDisplayText([...detail])}`;
|
|
248
|
+
}
|
|
249
|
+
if (detail instanceof Set) {
|
|
250
|
+
return `Set ${toDisplayText([...detail])}`;
|
|
251
|
+
}
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
function truncate(text) {
|
|
255
|
+
return text.length > MAX_ERROR_DETAIL_LENGTH ? `${text.slice(0, MAX_ERROR_DETAIL_LENGTH)}${LENGTH_TRUNCATED_MARKER}` : text;
|
|
256
|
+
}
|
|
257
|
+
function asNestedMessage(value) {
|
|
258
|
+
const record = asOptionalRecord(value);
|
|
259
|
+
return asNonEmptyString(record?.message);
|
|
260
|
+
}
|
|
100
261
|
|
|
101
262
|
// src/client/rest/requestOptions.ts
|
|
102
263
|
var DEFAULT_REQUEST_OPTIONS = {
|
|
@@ -135,6 +296,80 @@ var RuntimeStateError = class extends BandSdkError {
|
|
|
135
296
|
this.name = "RuntimeStateError";
|
|
136
297
|
}
|
|
137
298
|
};
|
|
299
|
+
var RecoverableTurnError = class extends BandSdkError {
|
|
300
|
+
constructor(message, cause) {
|
|
301
|
+
super(message, cause);
|
|
302
|
+
this.name = "RecoverableTurnError";
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// src/contracts/chatEvents.ts
|
|
307
|
+
var CHAT_EVENT_TYPES = ["tool_call", "tool_result", "thought", "error", "task"];
|
|
308
|
+
var CHAT_MESSAGE_TYPES = ["text", ...CHAT_EVENT_TYPES];
|
|
309
|
+
function isChatEventType(value) {
|
|
310
|
+
return CHAT_EVENT_TYPES.includes(value);
|
|
311
|
+
}
|
|
312
|
+
function assertChatEventType(value) {
|
|
313
|
+
if (!isChatEventType(value)) {
|
|
314
|
+
throw new ValidationError(
|
|
315
|
+
`Invalid event message_type '${value}'. Expected one of: ${CHAT_EVENT_TYPES.join(", ")}`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
var VISIBLE_CONTENT_PATTERN = /[\p{L}\p{N}\p{P}\p{S}]/u;
|
|
320
|
+
function isBlankEventContent(content) {
|
|
321
|
+
return !VISIBLE_CONTENT_PATTERN.test(content);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/contracts/protocols.ts
|
|
325
|
+
var FAILURE_EVENT_TYPE = "error";
|
|
326
|
+
var FAILURE_METADATA_KEY = "failure";
|
|
327
|
+
function sendFailureViaEvent(sendEvent, failure) {
|
|
328
|
+
const { content, messageType, metadata } = toFailureEvent(failure);
|
|
329
|
+
return sendEvent(content, messageType, metadata);
|
|
330
|
+
}
|
|
331
|
+
function toFailureEvent(failure) {
|
|
332
|
+
const content = isBlankEventContent(failure.message) ? `${failure.provider} failed without an error message.` : failure.message;
|
|
333
|
+
return {
|
|
334
|
+
content,
|
|
335
|
+
messageType: FAILURE_EVENT_TYPE,
|
|
336
|
+
metadata: { [FAILURE_METADATA_KEY]: failure.toObject() }
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
var TOOL_EXECUTOR_ERROR_TYPES = [
|
|
340
|
+
"ToolArgumentsValidationError",
|
|
341
|
+
"ToolNotFoundError",
|
|
342
|
+
"ToolExecutionError"
|
|
343
|
+
];
|
|
344
|
+
function createToolExecutorError(input) {
|
|
345
|
+
return {
|
|
346
|
+
ok: false,
|
|
347
|
+
errorType: input.errorType,
|
|
348
|
+
toolName: input.toolName,
|
|
349
|
+
message: input.message,
|
|
350
|
+
legacyMessage: input.legacyMessage ?? input.message,
|
|
351
|
+
...input.details ? { details: input.details } : {}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function isToolExecutorError(value) {
|
|
355
|
+
if (!value || typeof value !== "object") {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
const candidate = value;
|
|
359
|
+
return candidate.ok === false && typeof candidate.errorType === "string" && TOOL_EXECUTOR_ERROR_TYPES.includes(candidate.errorType) && typeof candidate.toolName === "string" && typeof candidate.message === "string" && typeof candidate.legacyMessage === "string";
|
|
360
|
+
}
|
|
361
|
+
function isStructuredToolFailure(value) {
|
|
362
|
+
if (!value || typeof value !== "object") {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
const payload = value;
|
|
366
|
+
return payload.ok === false && typeof payload.message === "string";
|
|
367
|
+
}
|
|
368
|
+
var DEFAULT_AGENT_TOOLS_CAPABILITIES = {
|
|
369
|
+
peers: true,
|
|
370
|
+
contacts: true,
|
|
371
|
+
memory: true
|
|
372
|
+
};
|
|
138
373
|
|
|
139
374
|
// src/runtime/formatters.ts
|
|
140
375
|
function isMetadataMap(value) {
|
|
@@ -338,9 +573,11 @@ var ContactCallbackTools = class {
|
|
|
338
573
|
rest;
|
|
339
574
|
roomId;
|
|
340
575
|
contactTools;
|
|
341
|
-
|
|
576
|
+
logger;
|
|
577
|
+
constructor(rest, roomId, logger) {
|
|
342
578
|
this.rest = rest;
|
|
343
579
|
this.roomId = roomId;
|
|
580
|
+
this.logger = resolveLogger(logger);
|
|
344
581
|
const hasContactMethods = Boolean(
|
|
345
582
|
rest.listContacts || rest.addContact || rest.removeContact || rest.listContactRequests || rest.respondContactRequest
|
|
346
583
|
);
|
|
@@ -375,14 +612,22 @@ var ContactCallbackTools = class {
|
|
|
375
612
|
if (!this.rest.createChatEvent) {
|
|
376
613
|
throw new UnsupportedFeatureError("Event sending is not available in current REST adapter");
|
|
377
614
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
615
|
+
try {
|
|
616
|
+
return await this.rest.createChatEvent(
|
|
617
|
+
roomId,
|
|
618
|
+
{
|
|
619
|
+
content,
|
|
620
|
+
messageType,
|
|
621
|
+
...metadata ? { metadata } : {}
|
|
622
|
+
}
|
|
623
|
+
);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
this.logger.warn("contact callback chat event send failed", { roomId, messageType, error });
|
|
626
|
+
return { ok: false, status: "failed", message: asErrorMessage(error) };
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
async sendFailure(failure) {
|
|
630
|
+
return sendFailureViaEvent(this.sendEvent.bind(this), failure);
|
|
386
631
|
}
|
|
387
632
|
async addParticipant() {
|
|
388
633
|
throw new UnsupportedFeatureError("addParticipant is unavailable for contact callbacks");
|
|
@@ -613,7 +858,7 @@ var ContactEventHandler = class {
|
|
|
613
858
|
constructor(options) {
|
|
614
859
|
this.config = options.config;
|
|
615
860
|
this.rest = options.rest;
|
|
616
|
-
this.logger = options.logger
|
|
861
|
+
this.logger = resolveLogger(options.logger);
|
|
617
862
|
this.onBroadcast = options.onBroadcast;
|
|
618
863
|
this.onHubEvent = options.onHubEvent;
|
|
619
864
|
this.onHubInit = options.onHubInit;
|
|
@@ -663,7 +908,7 @@ var ContactEventHandler = class {
|
|
|
663
908
|
this.logger.warn("Contact event callback strategy configured but no onEvent callback provided");
|
|
664
909
|
return;
|
|
665
910
|
}
|
|
666
|
-
const callbackTools = tools ?? new ContactCallbackTools(this.rest, event.roomId);
|
|
911
|
+
const callbackTools = tools ?? new ContactCallbackTools(this.rest, event.roomId, this.logger);
|
|
667
912
|
try {
|
|
668
913
|
await callback(event, callbackTools);
|
|
669
914
|
} catch (error) {
|
|
@@ -748,7 +993,8 @@ var ContactEventHandler = class {
|
|
|
748
993
|
const msg = event.payload.message ? `
|
|
749
994
|
Message: "${event.payload.message}"` : "";
|
|
750
995
|
const handle = normalizeHandle(event.payload.from_handle);
|
|
751
|
-
|
|
996
|
+
const name = displayName(event.payload.from_name);
|
|
997
|
+
return `[Contact Request] ${name} (${handle}) wants to connect.${msg}
|
|
752
998
|
Request ID: ${event.payload.id}`;
|
|
753
999
|
}
|
|
754
1000
|
case "contact_request_updated": {
|
|
@@ -767,7 +1013,8 @@ Request ID: ${event.payload.id}`;
|
|
|
767
1013
|
}
|
|
768
1014
|
case "contact_added": {
|
|
769
1015
|
const handle = normalizeHandle(event.payload.handle);
|
|
770
|
-
|
|
1016
|
+
const name = displayName(event.payload.name);
|
|
1017
|
+
return `[Contact Added] ${name} (${handle}) is now a contact.
|
|
771
1018
|
Type: ${event.payload.type}, ID: ${event.payload.id}`;
|
|
772
1019
|
}
|
|
773
1020
|
case "contact_removed":
|
|
@@ -779,7 +1026,7 @@ Type: ${event.payload.type}, ID: ${event.payload.id}`;
|
|
|
779
1026
|
switch (event.type) {
|
|
780
1027
|
case "contact_added": {
|
|
781
1028
|
const handle = normalizeHandle(event.payload.handle);
|
|
782
|
-
return `[Contacts]: ${handle} (${event.payload.name}) is now a contact`;
|
|
1029
|
+
return `[Contacts]: ${handle} (${displayName(event.payload.name)}) is now a contact`;
|
|
783
1030
|
}
|
|
784
1031
|
case "contact_removed":
|
|
785
1032
|
return `[Contacts]: Contact ${event.payload.id} was removed`;
|
|
@@ -910,6 +1157,254 @@ function normalizeHandle(handle) {
|
|
|
910
1157
|
if (!handle) return "@unknown";
|
|
911
1158
|
return handle.startsWith("@") ? handle : `@${handle}`;
|
|
912
1159
|
}
|
|
1160
|
+
function displayName(name) {
|
|
1161
|
+
return name?.trim() || "Unknown contact";
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// src/runtime/lifecycle.ts
|
|
1165
|
+
var RUNTIME_LIFECYCLE_TRANSITIONS = {
|
|
1166
|
+
not_started: ["starting"],
|
|
1167
|
+
starting: ["running", "stopping", "stopped", "failed"],
|
|
1168
|
+
running: ["stopping", "failed"],
|
|
1169
|
+
stopping: ["stopped", "failed"],
|
|
1170
|
+
stopped: ["starting"],
|
|
1171
|
+
failed: ["starting", "stopping", "failed"]
|
|
1172
|
+
};
|
|
1173
|
+
function isLegalRuntimeTransition(from, to) {
|
|
1174
|
+
return RUNTIME_LIFECYCLE_TRANSITIONS[from].includes(to);
|
|
1175
|
+
}
|
|
1176
|
+
var EXECUTION_LIFECYCLE_TRANSITIONS = {
|
|
1177
|
+
running: ["stopping", "failed"],
|
|
1178
|
+
stopping: ["stopped", "failed"],
|
|
1179
|
+
stopped: [],
|
|
1180
|
+
failed: []
|
|
1181
|
+
};
|
|
1182
|
+
function isLegalExecutionTransition(from, to) {
|
|
1183
|
+
return EXECUTION_LIFECYCLE_TRANSITIONS[from].includes(to);
|
|
1184
|
+
}
|
|
1185
|
+
function toLifecycleError(value) {
|
|
1186
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
1187
|
+
}
|
|
1188
|
+
var TerminalSignal = class {
|
|
1189
|
+
waiters = /* @__PURE__ */ new Set();
|
|
1190
|
+
outcome = null;
|
|
1191
|
+
/**
|
|
1192
|
+
* Resolves once {@link settle} has been called with `null`, rejects once it has
|
|
1193
|
+
* been called with an error — including for callers that arrive afterwards.
|
|
1194
|
+
*/
|
|
1195
|
+
async wait() {
|
|
1196
|
+
if (this.outcome) {
|
|
1197
|
+
if (this.outcome.error) {
|
|
1198
|
+
throw this.outcome.error;
|
|
1199
|
+
}
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
await new Promise((resolve, reject) => {
|
|
1203
|
+
this.waiters.add({ resolve, reject });
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Forget the recorded outcome so a restarted owner can settle again.
|
|
1208
|
+
*
|
|
1209
|
+
* Callers already parked in {@link wait} stay parked — a wait started before a
|
|
1210
|
+
* restart still resolves only when the owner actually stops or fails.
|
|
1211
|
+
*/
|
|
1212
|
+
rearm() {
|
|
1213
|
+
this.outcome = null;
|
|
1214
|
+
}
|
|
1215
|
+
/** First settle wins; later ones are ignored, so a terminal state stays terminal. */
|
|
1216
|
+
settle(error) {
|
|
1217
|
+
if (this.outcome) {
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
this.outcome = { error };
|
|
1221
|
+
const pending = [...this.waiters];
|
|
1222
|
+
this.waiters.clear();
|
|
1223
|
+
for (const waiter of pending) {
|
|
1224
|
+
if (error) {
|
|
1225
|
+
waiter.reject(error);
|
|
1226
|
+
} else {
|
|
1227
|
+
waiter.resolve();
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
var SingleFlight = class {
|
|
1233
|
+
operation = null;
|
|
1234
|
+
/** The in-flight (or last latched) operation, if any. */
|
|
1235
|
+
get pending() {
|
|
1236
|
+
return this.operation;
|
|
1237
|
+
}
|
|
1238
|
+
/** Forget the latched operation so the next {@link run}/{@link runOrRetry} starts fresh. */
|
|
1239
|
+
reset() {
|
|
1240
|
+
this.operation = null;
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Runs `factory()` once; concurrent and later callers get the same promise.
|
|
1244
|
+
* The result — success or failure — stays latched until {@link reset}.
|
|
1245
|
+
*/
|
|
1246
|
+
run(factory) {
|
|
1247
|
+
if (this.operation) {
|
|
1248
|
+
return this.operation;
|
|
1249
|
+
}
|
|
1250
|
+
const operation = factory();
|
|
1251
|
+
this.operation = operation;
|
|
1252
|
+
return operation;
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Like {@link run}, but unlatches on rejection so the next call retries
|
|
1256
|
+
* instead of replaying the same failure. Only unlatches its own operation —
|
|
1257
|
+
* one already replaced by a {@link reset} or a subsequent call is left alone.
|
|
1258
|
+
*/
|
|
1259
|
+
async runOrRetry(factory) {
|
|
1260
|
+
if (this.operation) {
|
|
1261
|
+
return this.operation;
|
|
1262
|
+
}
|
|
1263
|
+
const operation = factory();
|
|
1264
|
+
this.operation = operation;
|
|
1265
|
+
try {
|
|
1266
|
+
return await operation;
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
if (this.operation === operation) {
|
|
1269
|
+
this.operation = null;
|
|
1270
|
+
}
|
|
1271
|
+
throw error;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
var LifecycleTracker = class {
|
|
1276
|
+
options;
|
|
1277
|
+
current;
|
|
1278
|
+
constructor(initial, options) {
|
|
1279
|
+
this.options = options;
|
|
1280
|
+
Object.freeze(initial);
|
|
1281
|
+
this.current = initial;
|
|
1282
|
+
}
|
|
1283
|
+
/** The current state. Frozen, so callers cannot mutate the instance's lifecycle. */
|
|
1284
|
+
get state() {
|
|
1285
|
+
return this.current;
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* Whether `state` is still the instance this tracker holds.
|
|
1289
|
+
*
|
|
1290
|
+
* Every accepted {@link transition} installs a *new* frozen object, so an
|
|
1291
|
+
* async operation that captured the state it began under can ask whether the
|
|
1292
|
+
* lifecycle moved on while it was awaiting — "was I superseded?" — without a
|
|
1293
|
+
* second, parallel generation counter living outside the tracker. A status
|
|
1294
|
+
* comparison cannot answer it: a `stop()` followed by a fresh `start()`
|
|
1295
|
+
* returns to `"starting"`, but not to the *same* `"starting"`.
|
|
1296
|
+
*/
|
|
1297
|
+
isCurrent(state) {
|
|
1298
|
+
return this.current === state;
|
|
1299
|
+
}
|
|
1300
|
+
/** Records `next` as the state. Callers pass a fresh object — see {@link isCurrent}. */
|
|
1301
|
+
transition(next, trigger) {
|
|
1302
|
+
const from = this.current.status;
|
|
1303
|
+
if (!this.options.isLegalTransition(from, next.status)) {
|
|
1304
|
+
throw new RuntimeStateError(
|
|
1305
|
+
`${this.options.owner} cannot transition from "${from}" to "${next.status}" (trigger: ${trigger})`
|
|
1306
|
+
);
|
|
1307
|
+
}
|
|
1308
|
+
Object.freeze(next);
|
|
1309
|
+
this.current = next;
|
|
1310
|
+
this.options.logger?.debug(`${this.options.owner} lifecycle transition`, {
|
|
1311
|
+
...this.options.logContext,
|
|
1312
|
+
from,
|
|
1313
|
+
to: next.status,
|
|
1314
|
+
trigger
|
|
1315
|
+
});
|
|
1316
|
+
this.options.onTransition?.(next);
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Type-guard check against the current status: narrows `this` (and so
|
|
1320
|
+
* `this.state`) to the matching union member on success, e.g.
|
|
1321
|
+
* `if (tracker.is("failed")) { tracker.state.error }`.
|
|
1322
|
+
*
|
|
1323
|
+
* Prefer this over a raw `tracker.state.status === "..."` comparison —
|
|
1324
|
+
* same check, but the payload stays reachable without a separate
|
|
1325
|
+
* destructured snapshot.
|
|
1326
|
+
*/
|
|
1327
|
+
is(status) {
|
|
1328
|
+
return this.current.status === status;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Transition to a `"failed"` state carrying `error`, unless already
|
|
1332
|
+
* terminal (`"stopped"` or `"failed"`), in which case this is a no-op.
|
|
1333
|
+
* Returns whether the transition happened.
|
|
1334
|
+
*
|
|
1335
|
+
* Assumes `S` has a `{ status: "failed", error: Error }` member — true for
|
|
1336
|
+
* both `RuntimeLifecycleState` and `ExecutionLifecycleState`, the only two
|
|
1337
|
+
* states this tracker is used with.
|
|
1338
|
+
*/
|
|
1339
|
+
fail(error, trigger) {
|
|
1340
|
+
if (this.is("stopped") || this.is("failed")) {
|
|
1341
|
+
return false;
|
|
1342
|
+
}
|
|
1343
|
+
this.transition({ status: "failed", error: toLifecycleError(error) }, trigger);
|
|
1344
|
+
return true;
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
async function startWithGate(options) {
|
|
1348
|
+
if (options.lifecycle.state.status === "stopping") {
|
|
1349
|
+
throw new RuntimeStateError(`${options.ownerName} cannot start while a stop is in progress`);
|
|
1350
|
+
}
|
|
1351
|
+
if (options.startGate.pending) {
|
|
1352
|
+
return await options.startGate.pending;
|
|
1353
|
+
}
|
|
1354
|
+
options.stopGate.reset();
|
|
1355
|
+
options.stoppedSignal?.rearm();
|
|
1356
|
+
options.lifecycle.transition({ status: "starting" }, "start");
|
|
1357
|
+
await options.startGate.runOrRetry(options.runStart);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/runtime/SyncBoundaryTracker.ts
|
|
1361
|
+
var SyncBoundaryTracker = class {
|
|
1362
|
+
// Every message id a sync scan has ever executed. Entries are never
|
|
1363
|
+
// removed: evicting on a message's own live redelivery was tried and
|
|
1364
|
+
// proved unsafe, because the backend can still return that same id from
|
|
1365
|
+
// `getNextMessage()` on a *later* reconnect's scan before its
|
|
1366
|
+
// mark-as-processed effect has propagated — an eviction keyed on "we saw
|
|
1367
|
+
// it once already" reopens exactly that race, just on a different
|
|
1368
|
+
// trigger. Permanent membership is what makes "already executed"
|
|
1369
|
+
// unconditional, at the cost of one entry per message ever synced via a
|
|
1370
|
+
// backlog scan for the life of this tracker — a real, bounded quantity.
|
|
1371
|
+
executedMessageIds = /* @__PURE__ */ new Set();
|
|
1372
|
+
initial = { messageId: null };
|
|
1373
|
+
// Ordered, oldest first, matching the order scans run in.
|
|
1374
|
+
queue = [this.initial];
|
|
1375
|
+
/** Whether every queued boundary's scan has finished. */
|
|
1376
|
+
get isComplete() {
|
|
1377
|
+
return this.queue.length === 0;
|
|
1378
|
+
}
|
|
1379
|
+
recordExecuted(messageId) {
|
|
1380
|
+
this.executedMessageIds.add(messageId);
|
|
1381
|
+
}
|
|
1382
|
+
isExecuted(messageId) {
|
|
1383
|
+
return this.executedMessageIds.has(messageId);
|
|
1384
|
+
}
|
|
1385
|
+
isSyncPoint(boundary, messageId) {
|
|
1386
|
+
return boundary.messageId !== null && messageId === boundary.messageId;
|
|
1387
|
+
}
|
|
1388
|
+
/** Anchors the oldest not-yet-anchored boundary to a live message's id, if one is open. */
|
|
1389
|
+
anchor(messageId) {
|
|
1390
|
+
const openBoundary = this.queue.find((boundary) => boundary.messageId === null);
|
|
1391
|
+
if (openBoundary) {
|
|
1392
|
+
openBoundary.messageId = messageId;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
/** Starts a new scan boundary for a reconnect and returns it. */
|
|
1396
|
+
beginBoundary() {
|
|
1397
|
+
const boundary = { messageId: null };
|
|
1398
|
+
this.queue.push(boundary);
|
|
1399
|
+
return boundary;
|
|
1400
|
+
}
|
|
1401
|
+
/** Marks `boundary`'s scan finished, dequeuing it only if it's still the oldest. */
|
|
1402
|
+
completeBoundary(boundary) {
|
|
1403
|
+
if (this.queue[0] === boundary) {
|
|
1404
|
+
this.queue.shift();
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
913
1408
|
|
|
914
1409
|
// src/runtime/Execution.ts
|
|
915
1410
|
function toMessageEvent(message) {
|
|
@@ -941,13 +1436,18 @@ var Execution = class {
|
|
|
941
1436
|
eventQueue = [];
|
|
942
1437
|
waiters = [];
|
|
943
1438
|
idleWaiters = /* @__PURE__ */ new Set();
|
|
944
|
-
|
|
945
|
-
|
|
1439
|
+
stoppedSignal = new TerminalSignal();
|
|
1440
|
+
lifecycle;
|
|
946
1441
|
processTask;
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
running = true;
|
|
1442
|
+
stopGate = new SingleFlight();
|
|
1443
|
+
syncBoundaries = new SyncBoundaryTracker();
|
|
950
1444
|
inFlight = 0;
|
|
1445
|
+
/**
|
|
1446
|
+
* Set once the queue stops accepting new events, ahead of the lifecycle
|
|
1447
|
+
* itself reporting `"stopped"` (see {@link runStop}) — closing the queue
|
|
1448
|
+
* cannot wait on the drain it is closing for.
|
|
1449
|
+
*/
|
|
1450
|
+
closed = false;
|
|
951
1451
|
constructor(options) {
|
|
952
1452
|
this.roomId = options.roomId;
|
|
953
1453
|
this.link = options.link;
|
|
@@ -955,27 +1455,61 @@ var Execution = class {
|
|
|
955
1455
|
this.retryTracker = this.context.getRetryTracker();
|
|
956
1456
|
this.onExecute = options.onExecute;
|
|
957
1457
|
this.onFailure = options.onFailure;
|
|
958
|
-
this.logger = options.logger
|
|
959
|
-
this.
|
|
1458
|
+
this.logger = resolveLogger(options.logger);
|
|
1459
|
+
this.lifecycle = new LifecycleTracker({ status: "running" }, {
|
|
1460
|
+
owner: "Execution",
|
|
1461
|
+
logContext: { roomId: this.roomId },
|
|
1462
|
+
logger: this.logger,
|
|
1463
|
+
isLegalTransition: isLegalExecutionTransition,
|
|
1464
|
+
onTransition: (state) => {
|
|
1465
|
+
if (state.status === "stopped") {
|
|
1466
|
+
this.stoppedSignal.settle(null);
|
|
1467
|
+
} else if (state.status === "failed") {
|
|
1468
|
+
this.stoppedSignal.settle(state.error);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
this.processTask = this.runProcessLoop();
|
|
1473
|
+
void this.processTask.catch(() => void 0);
|
|
1474
|
+
}
|
|
1475
|
+
/**
|
|
1476
|
+
* Whether this `Execution` is still alive, and if not, why it ended.
|
|
1477
|
+
*
|
|
1478
|
+
* This is the *lifecycle* axis. For "is this turn's handler currently
|
|
1479
|
+
* executing" read `ExecutionContext.state` instead, which reports
|
|
1480
|
+
* `"starting" | "idle" | "processing"` for the room's context.
|
|
1481
|
+
*
|
|
1482
|
+
* @see ExecutionContext.state
|
|
1483
|
+
*/
|
|
1484
|
+
get state() {
|
|
1485
|
+
return this.lifecycle.state;
|
|
960
1486
|
}
|
|
961
|
-
enqueue(event) {
|
|
962
|
-
if (
|
|
963
|
-
|
|
1487
|
+
async enqueue(event) {
|
|
1488
|
+
if (this.lifecycle.is("stopped") || this.lifecycle.is("failed") || this.closed) {
|
|
1489
|
+
throw new RuntimeStateError(
|
|
1490
|
+
`Execution for room ${this.roomId} has already ended or is stopping (status: ${this.lifecycle.state.status}); enqueue() is a no-op after stop()`
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
let syncBoundary = null;
|
|
1494
|
+
if (event.type === "reconnected") {
|
|
1495
|
+
syncBoundary = this.syncBoundaries.beginBoundary();
|
|
1496
|
+
} else if (event.type === "message_created") {
|
|
1497
|
+
this.syncBoundaries.anchor(event.payload.id);
|
|
964
1498
|
}
|
|
1499
|
+
const queued = { event, syncBoundary };
|
|
965
1500
|
const waiter = this.waiters.shift();
|
|
966
1501
|
if (waiter) {
|
|
967
|
-
waiter(
|
|
1502
|
+
waiter(queued);
|
|
968
1503
|
} else {
|
|
969
|
-
this.eventQueue.push(
|
|
1504
|
+
this.eventQueue.push(queued);
|
|
970
1505
|
}
|
|
971
|
-
return Promise.resolve();
|
|
972
1506
|
}
|
|
973
1507
|
async bootstrapMessage(message) {
|
|
974
|
-
this.
|
|
1508
|
+
this.syncBoundaries.recordExecuted(message.id);
|
|
975
1509
|
await this.executeSyncMessage(toMessageEvent(message), message.id);
|
|
976
1510
|
}
|
|
977
1511
|
isIdle() {
|
|
978
|
-
return this.
|
|
1512
|
+
return this.syncBoundaries.isComplete && this.inFlight === 0 && this.eventQueue.length === 0;
|
|
979
1513
|
}
|
|
980
1514
|
async waitForIdle(timeoutMs) {
|
|
981
1515
|
if (this.isIdle()) {
|
|
@@ -1010,27 +1544,58 @@ var Execution = class {
|
|
|
1010
1544
|
});
|
|
1011
1545
|
}
|
|
1012
1546
|
async stop(timeoutMs) {
|
|
1547
|
+
return await this.stopGate.run(() => this.runStop(timeoutMs));
|
|
1548
|
+
}
|
|
1549
|
+
async runStop(timeoutMs) {
|
|
1550
|
+
if (this.lifecycle.is("failed")) {
|
|
1551
|
+
throw this.lifecycle.state.error;
|
|
1552
|
+
}
|
|
1553
|
+
if (this.lifecycle.is("stopped")) {
|
|
1554
|
+
return this.lifecycle.state.graceful;
|
|
1555
|
+
}
|
|
1556
|
+
this.lifecycle.transition({ status: "stopping" }, "stop");
|
|
1013
1557
|
const graceful = await this.waitForIdle(timeoutMs);
|
|
1014
|
-
this.
|
|
1558
|
+
if (this.lifecycle.is("failed")) {
|
|
1559
|
+
throw this.lifecycle.state.error;
|
|
1560
|
+
}
|
|
1561
|
+
this.closed = true;
|
|
1015
1562
|
this.resolveEventWaiters(null);
|
|
1016
1563
|
if (graceful || timeoutMs === void 0) {
|
|
1017
1564
|
await this.processTask;
|
|
1018
1565
|
}
|
|
1566
|
+
this.lifecycle.transition({ status: "stopped", graceful }, graceful ? "stopped" : "stopped-forced");
|
|
1019
1567
|
return graceful;
|
|
1020
1568
|
}
|
|
1569
|
+
/**
|
|
1570
|
+
* Resolve once this `Execution` has reached a terminal state — including a
|
|
1571
|
+
* forced, non-graceful stop that detached the process loop — or reject with
|
|
1572
|
+
* the error that ended it.
|
|
1573
|
+
*/
|
|
1021
1574
|
async waitUntilStopped() {
|
|
1022
|
-
await this.
|
|
1575
|
+
await this.stoppedSignal.wait();
|
|
1576
|
+
}
|
|
1577
|
+
async runProcessLoop() {
|
|
1578
|
+
try {
|
|
1579
|
+
await this.processLoop();
|
|
1580
|
+
} catch (error) {
|
|
1581
|
+
this.markFailed(error, "process-loop-failed");
|
|
1582
|
+
throw error;
|
|
1583
|
+
}
|
|
1023
1584
|
}
|
|
1024
1585
|
async processLoop() {
|
|
1025
1586
|
await this.recoverStaleProcessingMessages();
|
|
1026
|
-
await this.synchronizeWithNext();
|
|
1027
|
-
while (this.
|
|
1028
|
-
const
|
|
1029
|
-
if (!
|
|
1587
|
+
await this.synchronizeWithNext(this.syncBoundaries.initial);
|
|
1588
|
+
while (this.isActive()) {
|
|
1589
|
+
const queued = await this.nextQueuedEvent();
|
|
1590
|
+
if (!queued) {
|
|
1030
1591
|
return;
|
|
1031
1592
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1593
|
+
const { event } = queued;
|
|
1594
|
+
if (event.type === "reconnected") {
|
|
1595
|
+
await this.synchronizeWithNext(queued.syncBoundary ?? { messageId: null });
|
|
1596
|
+
continue;
|
|
1597
|
+
}
|
|
1598
|
+
if (event.type === "message_created" && this.syncBoundaries.isExecuted(event.payload.id)) {
|
|
1034
1599
|
this.notifyIfIdle();
|
|
1035
1600
|
continue;
|
|
1036
1601
|
}
|
|
@@ -1055,7 +1620,7 @@ var Execution = class {
|
|
|
1055
1620
|
count: staleMessages.length
|
|
1056
1621
|
});
|
|
1057
1622
|
for (const message of staleMessages) {
|
|
1058
|
-
if (!this.
|
|
1623
|
+
if (!this.isActive()) {
|
|
1059
1624
|
break;
|
|
1060
1625
|
}
|
|
1061
1626
|
if (this.retryTracker.isPermanentlyFailed(message.id)) {
|
|
@@ -1066,20 +1631,17 @@ var Execution = class {
|
|
|
1066
1631
|
continue;
|
|
1067
1632
|
}
|
|
1068
1633
|
await this.executeSyncMessage(toMessageEvent(message), message.id);
|
|
1069
|
-
this.
|
|
1634
|
+
this.syncBoundaries.recordExecuted(message.id);
|
|
1070
1635
|
}
|
|
1071
1636
|
}
|
|
1072
|
-
async synchronizeWithNext() {
|
|
1073
|
-
while (this.
|
|
1637
|
+
async synchronizeWithNext(boundary) {
|
|
1638
|
+
while (this.isActive()) {
|
|
1074
1639
|
const nextMessage = await this.link.getNextMessage(this.roomId);
|
|
1075
1640
|
if (!nextMessage) {
|
|
1076
1641
|
break;
|
|
1077
1642
|
}
|
|
1078
|
-
if (this.
|
|
1079
|
-
|
|
1080
|
-
if (isSyncPoint2) {
|
|
1081
|
-
this.drainedWsMessageIds.add(nextMessage.id);
|
|
1082
|
-
this.firstWsMessageId = null;
|
|
1643
|
+
if (this.syncBoundaries.isExecuted(nextMessage.id)) {
|
|
1644
|
+
if (this.syncBoundaries.isSyncPoint(boundary, nextMessage.id)) {
|
|
1083
1645
|
break;
|
|
1084
1646
|
}
|
|
1085
1647
|
continue;
|
|
@@ -1090,25 +1652,19 @@ var Execution = class {
|
|
|
1090
1652
|
messageId: nextMessage.id
|
|
1091
1653
|
});
|
|
1092
1654
|
await this.markMessageFailed(nextMessage.id, "Message permanently failed after max retries");
|
|
1093
|
-
|
|
1094
|
-
if (
|
|
1095
|
-
this.drainedWsMessageIds.add(nextMessage.id);
|
|
1096
|
-
this.firstWsMessageId = null;
|
|
1655
|
+
this.syncBoundaries.recordExecuted(nextMessage.id);
|
|
1656
|
+
if (this.syncBoundaries.isSyncPoint(boundary, nextMessage.id)) {
|
|
1097
1657
|
break;
|
|
1098
1658
|
}
|
|
1099
1659
|
continue;
|
|
1100
1660
|
}
|
|
1101
|
-
const isSyncPoint = this.firstWsMessageId !== null && nextMessage.id === this.firstWsMessageId;
|
|
1102
1661
|
await this.executeSyncMessage(toMessageEvent(nextMessage), nextMessage.id);
|
|
1103
|
-
this.
|
|
1104
|
-
if (isSyncPoint) {
|
|
1105
|
-
this.drainedWsMessageIds.add(nextMessage.id);
|
|
1106
|
-
this.firstWsMessageId = null;
|
|
1662
|
+
this.syncBoundaries.recordExecuted(nextMessage.id);
|
|
1663
|
+
if (this.syncBoundaries.isSyncPoint(boundary, nextMessage.id)) {
|
|
1107
1664
|
break;
|
|
1108
1665
|
}
|
|
1109
1666
|
}
|
|
1110
|
-
this.
|
|
1111
|
-
this.syncComplete = true;
|
|
1667
|
+
this.syncBoundaries.completeBoundary(boundary);
|
|
1112
1668
|
this.notifyIfIdle();
|
|
1113
1669
|
}
|
|
1114
1670
|
async executeSyncMessage(event, messageId) {
|
|
@@ -1145,6 +1701,14 @@ var Execution = class {
|
|
|
1145
1701
|
try {
|
|
1146
1702
|
await this.onExecute(this.context, event);
|
|
1147
1703
|
} catch (error) {
|
|
1704
|
+
if (error instanceof RecoverableTurnError) {
|
|
1705
|
+
this.logger.warn("Turn failed without stopping the room", {
|
|
1706
|
+
roomId: this.roomId,
|
|
1707
|
+
eventType: event.type,
|
|
1708
|
+
error
|
|
1709
|
+
});
|
|
1710
|
+
return;
|
|
1711
|
+
}
|
|
1148
1712
|
if (this.onFailure) {
|
|
1149
1713
|
await this.onFailure(error, event);
|
|
1150
1714
|
} else {
|
|
@@ -1154,7 +1718,7 @@ var Execution = class {
|
|
|
1154
1718
|
error
|
|
1155
1719
|
});
|
|
1156
1720
|
}
|
|
1157
|
-
this.
|
|
1721
|
+
this.markFailed(error, "execution-failed");
|
|
1158
1722
|
this.eventQueue.splice(0, this.eventQueue.length);
|
|
1159
1723
|
this.resolveEventWaiters(null);
|
|
1160
1724
|
throw error;
|
|
@@ -1169,13 +1733,21 @@ var Execution = class {
|
|
|
1169
1733
|
if (queued) {
|
|
1170
1734
|
return queued;
|
|
1171
1735
|
}
|
|
1172
|
-
if (!this.
|
|
1736
|
+
if (this.closed || !this.isActive()) {
|
|
1173
1737
|
return null;
|
|
1174
1738
|
}
|
|
1175
1739
|
return new Promise((resolve) => {
|
|
1176
1740
|
this.waiters.push(resolve);
|
|
1177
1741
|
});
|
|
1178
1742
|
}
|
|
1743
|
+
/** True while the process loop should keep draining (`running` or mid-`stop()`). */
|
|
1744
|
+
isActive() {
|
|
1745
|
+
const status = this.lifecycle.state.status;
|
|
1746
|
+
return status === "running" || status === "stopping";
|
|
1747
|
+
}
|
|
1748
|
+
markFailed(error, trigger) {
|
|
1749
|
+
this.lifecycle.fail(error, trigger);
|
|
1750
|
+
}
|
|
1179
1751
|
notifyIfIdle() {
|
|
1180
1752
|
if (!this.isIdle()) {
|
|
1181
1753
|
return;
|
|
@@ -1205,7 +1777,7 @@ var Execution = class {
|
|
|
1205
1777
|
};
|
|
1206
1778
|
|
|
1207
1779
|
// src/runtime/tools/AgentTools.ts
|
|
1208
|
-
var
|
|
1780
|
+
var import_band_sdk_core2 = require("@band-ai/band-sdk-core");
|
|
1209
1781
|
|
|
1210
1782
|
// src/contracts/capabilities.ts
|
|
1211
1783
|
var CAPABILITY_LABELS = {
|
|
@@ -1219,64 +1791,15 @@ function assertCapability(capabilities, capability, label = CAPABILITY_LABELS[ca
|
|
|
1219
1791
|
}
|
|
1220
1792
|
}
|
|
1221
1793
|
|
|
1222
|
-
// src/contracts/
|
|
1223
|
-
var
|
|
1224
|
-
var
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
`Invalid event message_type '${value}'. Expected one of: ${CHAT_EVENT_TYPES.join(", ")}`
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// src/contracts/protocols.ts
|
|
1237
|
-
var TOOL_EXECUTOR_ERROR_TYPES = [
|
|
1238
|
-
"ToolArgumentsValidationError",
|
|
1239
|
-
"ToolNotFoundError",
|
|
1240
|
-
"ToolExecutionError"
|
|
1241
|
-
];
|
|
1242
|
-
function createToolExecutorError(input) {
|
|
1243
|
-
return {
|
|
1244
|
-
ok: false,
|
|
1245
|
-
errorType: input.errorType,
|
|
1246
|
-
toolName: input.toolName,
|
|
1247
|
-
message: input.message,
|
|
1248
|
-
legacyMessage: input.legacyMessage ?? input.message,
|
|
1249
|
-
...input.details ? { details: input.details } : {}
|
|
1250
|
-
};
|
|
1251
|
-
}
|
|
1252
|
-
function isToolExecutorError(value) {
|
|
1253
|
-
if (!value || typeof value !== "object") {
|
|
1254
|
-
return false;
|
|
1255
|
-
}
|
|
1256
|
-
const candidate = value;
|
|
1257
|
-
return candidate.ok === false && typeof candidate.errorType === "string" && TOOL_EXECUTOR_ERROR_TYPES.includes(candidate.errorType) && typeof candidate.toolName === "string" && typeof candidate.message === "string" && typeof candidate.legacyMessage === "string";
|
|
1258
|
-
}
|
|
1259
|
-
function isStructuredToolFailure(value) {
|
|
1260
|
-
if (!value || typeof value !== "object") {
|
|
1261
|
-
return false;
|
|
1262
|
-
}
|
|
1263
|
-
const payload = value;
|
|
1264
|
-
return payload.ok === false && typeof payload.message === "string";
|
|
1265
|
-
}
|
|
1266
|
-
var DEFAULT_AGENT_TOOLS_CAPABILITIES = {
|
|
1267
|
-
peers: true,
|
|
1268
|
-
contacts: true,
|
|
1269
|
-
memory: true
|
|
1270
|
-
};
|
|
1271
|
-
|
|
1272
|
-
// src/contracts/memory.ts
|
|
1273
|
-
var SENSORY_MEMORY_TYPES = [
|
|
1274
|
-
"iconic",
|
|
1275
|
-
// Visual input
|
|
1276
|
-
"echoic",
|
|
1277
|
-
// Auditory input
|
|
1278
|
-
"haptic"
|
|
1279
|
-
// Tactile input
|
|
1794
|
+
// src/contracts/memory.ts
|
|
1795
|
+
var import_band_sdk_core = require("@band-ai/band-sdk-core");
|
|
1796
|
+
var SENSORY_MEMORY_TYPES = [
|
|
1797
|
+
"iconic",
|
|
1798
|
+
// Visual input
|
|
1799
|
+
"echoic",
|
|
1800
|
+
// Auditory input
|
|
1801
|
+
"haptic"
|
|
1802
|
+
// Tactile input
|
|
1280
1803
|
];
|
|
1281
1804
|
var COGNITIVE_MEMORY_TYPES = [
|
|
1282
1805
|
"episodic",
|
|
@@ -1313,18 +1836,22 @@ var MEMORY_SEGMENTS = [
|
|
|
1313
1836
|
// Behavioral rules or policies
|
|
1314
1837
|
];
|
|
1315
1838
|
var MEMORY_STORE_SCOPES = [
|
|
1839
|
+
"agent",
|
|
1840
|
+
// Private to the storing agent; omit subject_id
|
|
1316
1841
|
"subject",
|
|
1317
1842
|
// About one person/agent; requires subject_id
|
|
1318
1843
|
"organization"
|
|
1319
1844
|
// Shared org-wide
|
|
1320
1845
|
];
|
|
1321
1846
|
var MEMORY_LIST_SCOPES = [
|
|
1847
|
+
"agent",
|
|
1848
|
+
// Caller's private agent-scoped memories only
|
|
1322
1849
|
"subject",
|
|
1323
1850
|
// Subject-scoped memories only
|
|
1324
1851
|
"organization",
|
|
1325
1852
|
// Organization-scoped memories only
|
|
1326
1853
|
"all"
|
|
1327
|
-
//
|
|
1854
|
+
// All scopes (no scope filter)
|
|
1328
1855
|
];
|
|
1329
1856
|
var MEMORY_STATUSES = [
|
|
1330
1857
|
"active",
|
|
@@ -1347,7 +1874,15 @@ function isMemoryType(value) {
|
|
|
1347
1874
|
return MEMORY_TYPES.includes(value);
|
|
1348
1875
|
}
|
|
1349
1876
|
function isMemoryTypeForSystem(system, type) {
|
|
1350
|
-
|
|
1877
|
+
try {
|
|
1878
|
+
(0, import_band_sdk_core.validateMemoryTypeForSystem)(system, type);
|
|
1879
|
+
return true;
|
|
1880
|
+
} catch (err) {
|
|
1881
|
+
if (err instanceof Error && "issues" in err) {
|
|
1882
|
+
return false;
|
|
1883
|
+
}
|
|
1884
|
+
throw err;
|
|
1885
|
+
}
|
|
1351
1886
|
}
|
|
1352
1887
|
function isMemorySegment(value) {
|
|
1353
1888
|
return MEMORY_SEGMENTS.includes(value);
|
|
@@ -1550,7 +2085,7 @@ var TOOL_MODELS = {
|
|
|
1550
2085
|
required: ["action"]
|
|
1551
2086
|
},
|
|
1552
2087
|
band_list_memories: {
|
|
1553
|
-
description:
|
|
2088
|
+
description: `List memories accessible to the agent. Use scope="agent" for the caller's private memories, scope="subject" for memories about a specific person or agent, and scope="organization" for org-wide shared memories. Omit scope or use scope="all" to search across scopes.`,
|
|
1554
2089
|
properties: {
|
|
1555
2090
|
subject_id: {
|
|
1556
2091
|
type: "string",
|
|
@@ -1559,7 +2094,7 @@ var TOOL_MODELS = {
|
|
|
1559
2094
|
scope: {
|
|
1560
2095
|
type: "string",
|
|
1561
2096
|
enum: [...MEMORY_LIST_SCOPES],
|
|
1562
|
-
description:
|
|
2097
|
+
description: `Filter by scope. "agent" returns the caller's private memories; "subject" and "organization" filter to those audiences; "all" omits the filter.`
|
|
1563
2098
|
},
|
|
1564
2099
|
system: {
|
|
1565
2100
|
type: "string",
|
|
@@ -1595,7 +2130,7 @@ var TOOL_MODELS = {
|
|
|
1595
2130
|
required: []
|
|
1596
2131
|
},
|
|
1597
2132
|
band_store_memory: {
|
|
1598
|
-
description:
|
|
2133
|
+
description: 'Store a new memory entry. The memory will be associated with the authenticated agent as the source. Use scope="agent" for information private to this agent (no subject_id). Use scope="subject" with a subject_id for memories about a specific person or agent. Use scope="organization" for knowledge genuinely shared across the organization.',
|
|
1599
2134
|
properties: {
|
|
1600
2135
|
content: {
|
|
1601
2136
|
type: "string",
|
|
@@ -1623,11 +2158,11 @@ var TOOL_MODELS = {
|
|
|
1623
2158
|
scope: {
|
|
1624
2159
|
type: "string",
|
|
1625
2160
|
enum: [...MEMORY_STORE_SCOPES],
|
|
1626
|
-
description:
|
|
2161
|
+
description: 'Visibility scope. "agent" is private to this agent; "subject" requires subject_id; "organization" is shared org-wide.'
|
|
1627
2162
|
},
|
|
1628
2163
|
subject_id: {
|
|
1629
2164
|
type: "string",
|
|
1630
|
-
description: "UUID of the subject this memory is about (required for subject scope)."
|
|
2165
|
+
description: "UUID of the subject this memory is about (required for subject scope; omit for agent scope)."
|
|
1631
2166
|
},
|
|
1632
2167
|
metadata: {
|
|
1633
2168
|
type: "object",
|
|
@@ -1699,34 +2234,29 @@ function getToolDescription(name) {
|
|
|
1699
2234
|
}
|
|
1700
2235
|
|
|
1701
2236
|
// src/runtime/tools/AgentTools.ts
|
|
1702
|
-
var
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
"storeMemory",
|
|
1726
|
-
"getMemory",
|
|
1727
|
-
"supersedeMemory",
|
|
1728
|
-
"archiveMemory"
|
|
1729
|
-
]
|
|
2237
|
+
var ADAPTER_TOOL_METHODS = {
|
|
2238
|
+
sendMessage: null,
|
|
2239
|
+
sendEvent: null,
|
|
2240
|
+
sendFailure: null,
|
|
2241
|
+
addParticipant: null,
|
|
2242
|
+
removeParticipant: null,
|
|
2243
|
+
getParticipants: null,
|
|
2244
|
+
createChatroom: null,
|
|
2245
|
+
getToolSchemas: null,
|
|
2246
|
+
getAnthropicToolSchemas: null,
|
|
2247
|
+
getOpenAIToolSchemas: null,
|
|
2248
|
+
executeToolCall: null,
|
|
2249
|
+
lookupPeers: "peers",
|
|
2250
|
+
listContacts: "contacts",
|
|
2251
|
+
addContact: "contacts",
|
|
2252
|
+
removeContact: "contacts",
|
|
2253
|
+
listContactRequests: "contacts",
|
|
2254
|
+
respondContactRequest: "contacts",
|
|
2255
|
+
listMemories: "memory",
|
|
2256
|
+
storeMemory: "memory",
|
|
2257
|
+
getMemory: "memory",
|
|
2258
|
+
supersedeMemory: "memory",
|
|
2259
|
+
archiveMemory: "memory"
|
|
1730
2260
|
};
|
|
1731
2261
|
var CONTACT_REQUEST_ACTIONS = /* @__PURE__ */ new Set([
|
|
1732
2262
|
"approve",
|
|
@@ -1744,8 +2274,8 @@ var AgentTools = class {
|
|
|
1744
2274
|
constructor(options) {
|
|
1745
2275
|
this.roomId = options.roomId;
|
|
1746
2276
|
this.rest = options.rest;
|
|
1747
|
-
this.roster = options.roster ?? new
|
|
1748
|
-
this.logger = options.logger
|
|
2277
|
+
this.roster = options.roster ?? new import_band_sdk_core2.ParticipantRoster();
|
|
2278
|
+
this.logger = resolveLogger(options.logger);
|
|
1749
2279
|
this.capabilities = {
|
|
1750
2280
|
...DEFAULT_AGENT_TOOLS_CAPABILITIES,
|
|
1751
2281
|
...options.capabilities
|
|
@@ -1785,14 +2315,14 @@ var AgentTools = class {
|
|
|
1785
2315
|
}
|
|
1786
2316
|
);
|
|
1787
2317
|
} catch (error) {
|
|
1788
|
-
|
|
1789
|
-
this.logger.warn("chat event send failed", { roomId: this.roomId, messageType, error });
|
|
1790
|
-
} catch {
|
|
1791
|
-
}
|
|
2318
|
+
this.logger.warn("chat event send failed", { roomId: this.roomId, messageType, error });
|
|
1792
2319
|
const message = error instanceof Error ? error.message : String(error);
|
|
1793
2320
|
return { ok: false, status: "failed", message };
|
|
1794
2321
|
}
|
|
1795
2322
|
}
|
|
2323
|
+
async sendFailure(failure) {
|
|
2324
|
+
return sendFailureViaEvent(this.sendEvent.bind(this), failure);
|
|
2325
|
+
}
|
|
1796
2326
|
async createChatroom(taskId) {
|
|
1797
2327
|
const room = await this.rest.createChat(taskId, DEFAULT_REQUEST_OPTIONS);
|
|
1798
2328
|
return room.id;
|
|
@@ -1916,6 +2446,7 @@ var AgentTools = class {
|
|
|
1916
2446
|
legacyMessage: `Invalid arguments for ${toolName}: ${error.message}`
|
|
1917
2447
|
});
|
|
1918
2448
|
}
|
|
2449
|
+
this.logger.error("unexpected tool execution error", { toolName, error });
|
|
1919
2450
|
const message = error instanceof Error ? error.message : String(error);
|
|
1920
2451
|
return createToolExecutorError({
|
|
1921
2452
|
errorType: "ToolExecutionError",
|
|
@@ -2202,16 +2733,11 @@ var AgentTools = class {
|
|
|
2202
2733
|
const tools = {
|
|
2203
2734
|
capabilities: this.capabilities
|
|
2204
2735
|
};
|
|
2205
|
-
for (const methodName of
|
|
2206
|
-
|
|
2207
|
-
}
|
|
2208
|
-
for (const [capabilityKey, methodNames] of Object.entries(OPTIONAL_ADAPTER_TOOL_METHODS)) {
|
|
2209
|
-
if (!this.capabilities[capabilityKey]) {
|
|
2736
|
+
for (const [methodName, capability] of Object.entries(ADAPTER_TOOL_METHODS)) {
|
|
2737
|
+
if (capability !== null && !this.capabilities[capability]) {
|
|
2210
2738
|
continue;
|
|
2211
2739
|
}
|
|
2212
|
-
|
|
2213
|
-
tools[methodName] = this.bindAdapterToolMethod(methodName);
|
|
2214
|
-
}
|
|
2740
|
+
tools[methodName] = this.bindAdapterToolMethod(methodName);
|
|
2215
2741
|
}
|
|
2216
2742
|
return Object.freeze(tools);
|
|
2217
2743
|
}
|
|
@@ -2359,9 +2885,9 @@ var AgentTools = class {
|
|
|
2359
2885
|
if (!segment) {
|
|
2360
2886
|
throw new ValidationError(`segment must be one of: ${expectedList(MEMORY_SEGMENTS)}`);
|
|
2361
2887
|
}
|
|
2362
|
-
if (scope ===
|
|
2888
|
+
if (scope === MEMORY_STORE_SCOPE.subject && !subjectId) {
|
|
2363
2889
|
throw new ValidationError(
|
|
2364
|
-
|
|
2890
|
+
`scope="${MEMORY_STORE_SCOPE.subject}" requires a subject_id (the UUID of the person or agent the memory is about). If you do not have a concrete subject UUID, retry with scope="${MEMORY_STORE_SCOPE.agent}" and omit subject_id. Do not invent a UUID.`
|
|
2365
2891
|
);
|
|
2366
2892
|
}
|
|
2367
2893
|
return {
|
|
@@ -2539,22 +3065,6 @@ function validateToolArgs(toolName, args) {
|
|
|
2539
3065
|
);
|
|
2540
3066
|
}
|
|
2541
3067
|
}
|
|
2542
|
-
if (toolName === "band_store_memory") {
|
|
2543
|
-
if (typeof args.system === "string" && !isMemorySystem(args.system)) {
|
|
2544
|
-
errors.push(`system: Invalid value '${args.system}'. Expected one of: ${expectedList(MEMORY_SYSTEMS)}`);
|
|
2545
|
-
}
|
|
2546
|
-
if (typeof args.type === "string" && !isMemoryType(args.type)) {
|
|
2547
|
-
errors.push(`type: Invalid value '${args.type}'. Expected one of: ${expectedList(MEMORY_TYPES)}`);
|
|
2548
|
-
}
|
|
2549
|
-
if (typeof args.system === "string" && isMemorySystem(args.system) && typeof args.type === "string" && isMemoryType(args.type) && !isMemoryTypeForSystem(args.system, args.type)) {
|
|
2550
|
-
errors.push(
|
|
2551
|
-
`type: Invalid value '${args.type}' for system '${args.system}'. Expected one of: ${expectedMemoryTypesForSystem(args.system)}`
|
|
2552
|
-
);
|
|
2553
|
-
}
|
|
2554
|
-
if (typeof args.segment === "string" && !isMemorySegment(args.segment)) {
|
|
2555
|
-
errors.push(`segment: Invalid value '${args.segment}'. Expected one of: ${expectedList(MEMORY_SEGMENTS)}`);
|
|
2556
|
-
}
|
|
2557
|
-
}
|
|
2558
3068
|
if (errors.length > 0) {
|
|
2559
3069
|
const message = `Invalid arguments for ${toolName}: ${errors.join("; ")}`;
|
|
2560
3070
|
return createToolExecutorError({
|
|
@@ -2569,7 +3079,7 @@ function validateToolArgs(toolName, args) {
|
|
|
2569
3079
|
}
|
|
2570
3080
|
|
|
2571
3081
|
// src/runtime/ExecutionContext.ts
|
|
2572
|
-
var
|
|
3082
|
+
var import_band_sdk_core3 = require("@band-ai/band-sdk-core");
|
|
2573
3083
|
var DEDUP_CACHE_MAX = 500;
|
|
2574
3084
|
var ExecutionContext = class {
|
|
2575
3085
|
roomId;
|
|
@@ -2581,7 +3091,7 @@ var ExecutionContext = class {
|
|
|
2581
3091
|
history = [];
|
|
2582
3092
|
messageIds = /* @__PURE__ */ new Set();
|
|
2583
3093
|
dedupCache = /* @__PURE__ */ new Map();
|
|
2584
|
-
roster = new
|
|
3094
|
+
roster = new import_band_sdk_core3.ParticipantRoster();
|
|
2585
3095
|
tools;
|
|
2586
3096
|
adapterTools;
|
|
2587
3097
|
participantsMessage = null;
|
|
@@ -2599,16 +3109,27 @@ var ExecutionContext = class {
|
|
|
2599
3109
|
this.enableContextCache = options.enableContextCache ?? true;
|
|
2600
3110
|
this.contextCacheTtlMs = Math.max(0, (options.contextCacheTtlSeconds ?? 300) * 1e3);
|
|
2601
3111
|
this.enableContextHydration = options.enableContextHydration ?? true;
|
|
2602
|
-
this.retryTrackerInstance = new
|
|
3112
|
+
this.retryTrackerInstance = new import_band_sdk_core3.RetryTracker(options.maxMessageRetries ?? 1);
|
|
2603
3113
|
this.tools = new AgentTools({
|
|
2604
3114
|
roomId: this.roomId,
|
|
2605
3115
|
rest: this.link.rest,
|
|
2606
3116
|
roster: this.roster,
|
|
2607
3117
|
capabilities: this.link.capabilities,
|
|
2608
|
-
logger: options.logger
|
|
3118
|
+
logger: resolveLogger(options.logger)
|
|
2609
3119
|
});
|
|
2610
3120
|
this.adapterTools = this.tools.getAdapterTools();
|
|
2611
3121
|
}
|
|
3122
|
+
/**
|
|
3123
|
+
* What this room's turn handler is currently doing.
|
|
3124
|
+
*
|
|
3125
|
+
* This is the *per-turn activity* axis, not a lifecycle: it flips to
|
|
3126
|
+
* `"processing"` while the adapter handles an event and back to `"idle"`
|
|
3127
|
+
* afterwards. To ask whether the room's `Execution` is still alive at all,
|
|
3128
|
+
* read `Execution.state` instead, which reports
|
|
3129
|
+
* `"running" | "stopping" | "stopped" | "failed"`.
|
|
3130
|
+
*
|
|
3131
|
+
* @see Execution.state
|
|
3132
|
+
*/
|
|
2612
3133
|
get state() {
|
|
2613
3134
|
return this._state;
|
|
2614
3135
|
}
|
|
@@ -2853,22 +3374,43 @@ var ExecutionContext = class {
|
|
|
2853
3374
|
}
|
|
2854
3375
|
};
|
|
2855
3376
|
|
|
3377
|
+
// src/core/teardown.ts
|
|
3378
|
+
async function isolateTeardown(errors, step) {
|
|
3379
|
+
try {
|
|
3380
|
+
await step();
|
|
3381
|
+
} catch (error) {
|
|
3382
|
+
errors.push(error);
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
function combineTeardownErrors(errors, message) {
|
|
3386
|
+
const distinct = [...new Set(errors)];
|
|
3387
|
+
return distinct.length === 1 ? distinct[0] : new AggregateError(distinct, message);
|
|
3388
|
+
}
|
|
3389
|
+
|
|
2856
3390
|
// src/runtime/rooms/RoomPresence.ts
|
|
2857
|
-
var
|
|
3391
|
+
var import_band_sdk_core4 = require("@band-ai/band-sdk-core");
|
|
2858
3392
|
|
|
2859
3393
|
// src/runtime/rooms/subscriptions.ts
|
|
3394
|
+
async function listExistingRooms(options) {
|
|
3395
|
+
const rooms = await options.link.listAllChats(void 0, options.requestOptions);
|
|
3396
|
+
const accepted = /* @__PURE__ */ new Map();
|
|
3397
|
+
for (const room of rooms) {
|
|
3398
|
+
const roomId = typeof room.id === "string" ? room.id : null;
|
|
3399
|
+
if (!roomId) {
|
|
3400
|
+
continue;
|
|
3401
|
+
}
|
|
3402
|
+
if (options.roomFilter && !options.roomFilter(room)) {
|
|
3403
|
+
continue;
|
|
3404
|
+
}
|
|
3405
|
+
accepted.set(roomId, room);
|
|
3406
|
+
}
|
|
3407
|
+
return accepted;
|
|
3408
|
+
}
|
|
2860
3409
|
async function hydrateExistingRooms(options) {
|
|
2861
3410
|
try {
|
|
2862
|
-
const rooms = await
|
|
2863
|
-
for (const
|
|
2864
|
-
|
|
2865
|
-
if (!roomId) {
|
|
2866
|
-
continue;
|
|
2867
|
-
}
|
|
2868
|
-
if (options.roomFilter && !options.roomFilter(room)) {
|
|
2869
|
-
continue;
|
|
2870
|
-
}
|
|
2871
|
-
await options.onRoom(roomId, room);
|
|
3411
|
+
const rooms = await listExistingRooms(options);
|
|
3412
|
+
for (const [roomId, payload] of rooms) {
|
|
3413
|
+
await options.onRoom(roomId, payload);
|
|
2872
3414
|
}
|
|
2873
3415
|
} catch (error) {
|
|
2874
3416
|
if (error instanceof UnsupportedFeatureError) {
|
|
@@ -2882,9 +3424,68 @@ async function hydrateExistingRooms(options) {
|
|
|
2882
3424
|
}
|
|
2883
3425
|
}
|
|
2884
3426
|
|
|
3427
|
+
// src/core/singleFlight.ts
|
|
3428
|
+
var SingleFlight2 = class {
|
|
3429
|
+
pending = null;
|
|
3430
|
+
/** The currently in-flight operation's promise, if any is running. */
|
|
3431
|
+
get current() {
|
|
3432
|
+
return this.pending;
|
|
3433
|
+
}
|
|
3434
|
+
run(start) {
|
|
3435
|
+
if (!this.pending) {
|
|
3436
|
+
const promise = start();
|
|
3437
|
+
this.pending = promise;
|
|
3438
|
+
const clear = () => {
|
|
3439
|
+
if (this.pending === promise) {
|
|
3440
|
+
this.pending = null;
|
|
3441
|
+
}
|
|
3442
|
+
};
|
|
3443
|
+
void promise.then(clear, clear);
|
|
3444
|
+
}
|
|
3445
|
+
return this.pending;
|
|
3446
|
+
}
|
|
3447
|
+
};
|
|
3448
|
+
var KeyedSingleFlight = class {
|
|
3449
|
+
flights = /* @__PURE__ */ new Map();
|
|
3450
|
+
/** The currently in-flight operation's promise for `key`, if any is running. */
|
|
3451
|
+
current(key) {
|
|
3452
|
+
return this.flights.get(key)?.current ?? null;
|
|
3453
|
+
}
|
|
3454
|
+
run(key, start) {
|
|
3455
|
+
let flight = this.flights.get(key);
|
|
3456
|
+
if (!flight) {
|
|
3457
|
+
flight = new SingleFlight2();
|
|
3458
|
+
this.flights.set(key, flight);
|
|
3459
|
+
}
|
|
3460
|
+
const promise = flight.run(start);
|
|
3461
|
+
const forget = () => {
|
|
3462
|
+
if (this.flights.get(key) === flight && !flight.current) {
|
|
3463
|
+
this.flights.delete(key);
|
|
3464
|
+
}
|
|
3465
|
+
};
|
|
3466
|
+
void promise.then(forget, forget);
|
|
3467
|
+
return promise;
|
|
3468
|
+
}
|
|
3469
|
+
/** Drops every key without waiting for its in-flight operation to settle. */
|
|
3470
|
+
clear() {
|
|
3471
|
+
this.flights.clear();
|
|
3472
|
+
}
|
|
3473
|
+
};
|
|
3474
|
+
var Serializer = class {
|
|
3475
|
+
tail = Promise.resolve();
|
|
3476
|
+
run(body) {
|
|
3477
|
+
const run = this.tail.then(body, body);
|
|
3478
|
+
this.tail = run.then(
|
|
3479
|
+
() => void 0,
|
|
3480
|
+
() => void 0
|
|
3481
|
+
);
|
|
3482
|
+
return run;
|
|
3483
|
+
}
|
|
3484
|
+
};
|
|
3485
|
+
|
|
2885
3486
|
// src/runtime/rooms/RoomPresence.ts
|
|
2886
3487
|
var RoomPresence = class {
|
|
2887
|
-
roster = new
|
|
3488
|
+
roster = new import_band_sdk_core4.RoomRoster();
|
|
2888
3489
|
onRoomJoined = null;
|
|
2889
3490
|
onRoomLeft = null;
|
|
2890
3491
|
onRoomEvent = null;
|
|
@@ -2896,7 +3497,7 @@ var RoomPresence = class {
|
|
|
2896
3497
|
eventController = null;
|
|
2897
3498
|
eventTask = null;
|
|
2898
3499
|
contactsSubscribed = false;
|
|
2899
|
-
lifecycle =
|
|
3500
|
+
lifecycle = new Serializer();
|
|
2900
3501
|
admissionInFlight = /* @__PURE__ */ new Map();
|
|
2901
3502
|
// Read once by `admitRoomOrThrow` right after a failed admission; a
|
|
2902
3503
|
// subsequent successful subscribe clears it so a caller never attributes
|
|
@@ -2906,13 +3507,13 @@ var RoomPresence = class {
|
|
|
2906
3507
|
this.link = options.link;
|
|
2907
3508
|
this.roomFilter = options.roomFilter;
|
|
2908
3509
|
this.autoSubscribeExistingRooms = options.autoSubscribeExistingRooms ?? true;
|
|
2909
|
-
this.logger = options.logger
|
|
3510
|
+
this.logger = resolveLogger(options.logger);
|
|
2910
3511
|
}
|
|
2911
3512
|
async start() {
|
|
2912
|
-
return this.
|
|
3513
|
+
return this.lifecycle.run(() => this.startBody());
|
|
2913
3514
|
}
|
|
2914
3515
|
async stop() {
|
|
2915
|
-
return this.
|
|
3516
|
+
return this.lifecycle.run(() => this.stopBody());
|
|
2916
3517
|
}
|
|
2917
3518
|
async [Symbol.asyncDispose]() {
|
|
2918
3519
|
await this.stop();
|
|
@@ -2977,14 +3578,6 @@ var RoomPresence = class {
|
|
|
2977
3578
|
}
|
|
2978
3579
|
return roomIsAdmitted;
|
|
2979
3580
|
}
|
|
2980
|
-
async serialize(body) {
|
|
2981
|
-
const run = this.lifecycle.then(body, body);
|
|
2982
|
-
this.lifecycle = run.then(
|
|
2983
|
-
() => void 0,
|
|
2984
|
-
() => void 0
|
|
2985
|
-
);
|
|
2986
|
-
return run;
|
|
2987
|
-
}
|
|
2988
3581
|
async startBody() {
|
|
2989
3582
|
if (this.eventTask) {
|
|
2990
3583
|
throw new RuntimeStateError("RoomPresence is already started");
|
|
@@ -2992,14 +3585,8 @@ var RoomPresence = class {
|
|
|
2992
3585
|
if (!this.link.isConnected()) {
|
|
2993
3586
|
await this.link.connect();
|
|
2994
3587
|
}
|
|
2995
|
-
const contactsReady = this.subscribeContacts();
|
|
2996
|
-
|
|
2997
|
-
await this.link.subscribeAgentRooms();
|
|
2998
|
-
} catch (error) {
|
|
2999
|
-
this.logger.warn("RoomPresence failed to subscribe agent_rooms channel, continuing without it", {
|
|
3000
|
-
error
|
|
3001
|
-
});
|
|
3002
|
-
}
|
|
3588
|
+
const contactsReady = this.subscribeContacts("start");
|
|
3589
|
+
await this.subscribeAgentRoomsChannel("start");
|
|
3003
3590
|
if (this.autoSubscribeExistingRooms) {
|
|
3004
3591
|
await this.subscribeExistingRooms();
|
|
3005
3592
|
}
|
|
@@ -3007,7 +3594,17 @@ var RoomPresence = class {
|
|
|
3007
3594
|
this.eventController = new AbortController();
|
|
3008
3595
|
this.eventTask = this.consumeEvents(this.eventController.signal);
|
|
3009
3596
|
}
|
|
3010
|
-
|
|
3597
|
+
contextualWarnMessage(channel, context) {
|
|
3598
|
+
return context === "reconnect" ? `RoomPresence failed to resubscribe ${channel} channel after reconnect` : `RoomPresence failed to subscribe ${channel} channel, continuing without it`;
|
|
3599
|
+
}
|
|
3600
|
+
async subscribeAgentRoomsChannel(context) {
|
|
3601
|
+
try {
|
|
3602
|
+
await this.link.subscribeAgentRooms();
|
|
3603
|
+
} catch (error) {
|
|
3604
|
+
this.logger.warn(this.contextualWarnMessage("agent_rooms", context), { error });
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
async subscribeContacts(context) {
|
|
3011
3608
|
if (!this.link.capabilities.contacts) {
|
|
3012
3609
|
return;
|
|
3013
3610
|
}
|
|
@@ -3015,9 +3612,7 @@ var RoomPresence = class {
|
|
|
3015
3612
|
await this.link.subscribeAgentContacts();
|
|
3016
3613
|
this.contactsSubscribed = true;
|
|
3017
3614
|
} catch (error) {
|
|
3018
|
-
this.logger.warn("
|
|
3019
|
-
error
|
|
3020
|
-
});
|
|
3615
|
+
this.logger.warn(this.contextualWarnMessage("agent_contacts", context), { error });
|
|
3021
3616
|
}
|
|
3022
3617
|
}
|
|
3023
3618
|
async stopBody() {
|
|
@@ -3071,6 +3666,9 @@ var RoomPresence = class {
|
|
|
3071
3666
|
await this.onRoomEvent?.(event.roomId, event);
|
|
3072
3667
|
}
|
|
3073
3668
|
break;
|
|
3669
|
+
case "reconnected":
|
|
3670
|
+
await this.handleReconnected(event);
|
|
3671
|
+
break;
|
|
3074
3672
|
default:
|
|
3075
3673
|
assertNever2(event);
|
|
3076
3674
|
}
|
|
@@ -3089,14 +3687,74 @@ var RoomPresence = class {
|
|
|
3089
3687
|
if (!roomId) {
|
|
3090
3688
|
return;
|
|
3091
3689
|
}
|
|
3092
|
-
await this.
|
|
3093
|
-
this.lastSubscribeError.delete(roomId);
|
|
3690
|
+
await this.leaveRoomTracking(roomId);
|
|
3094
3691
|
if (!this.roster.recordRoomRemoved(roomId)) {
|
|
3095
3692
|
this.logger.debug("RoomPresence ignoring removal for untracked room", { roomId });
|
|
3096
3693
|
return;
|
|
3097
3694
|
}
|
|
3098
3695
|
await this.onRoomLeft?.(roomId);
|
|
3099
3696
|
}
|
|
3697
|
+
/** Unsubscribes the transport topic and clears any remembered subscribe failure for `roomId`. */
|
|
3698
|
+
async leaveRoomTracking(roomId) {
|
|
3699
|
+
await this.unsubscribeRoom(roomId);
|
|
3700
|
+
this.lastSubscribeError.delete(roomId);
|
|
3701
|
+
}
|
|
3702
|
+
/**
|
|
3703
|
+
* A REST fetch failure here must not blank the roster — it forwards the
|
|
3704
|
+
* reconnect to every currently tracked room regardless, so each room's
|
|
3705
|
+
* `Execution` can still re-run its `/next` synchronization even when
|
|
3706
|
+
* membership reconciliation itself has to wait for the next reconnect.
|
|
3707
|
+
*/
|
|
3708
|
+
async handleReconnected(event) {
|
|
3709
|
+
const [, , accepted] = await Promise.all([
|
|
3710
|
+
this.subscribeAgentRoomsChannel("reconnect"),
|
|
3711
|
+
this.subscribeContacts("reconnect"),
|
|
3712
|
+
listExistingRooms({
|
|
3713
|
+
link: this.link,
|
|
3714
|
+
roomFilter: this.roomFilter,
|
|
3715
|
+
requestOptions: DEFAULT_REQUEST_OPTIONS
|
|
3716
|
+
}).catch((error) => {
|
|
3717
|
+
this.logger.warn("RoomPresence failed to fetch room snapshot after reconnect", { error });
|
|
3718
|
+
return null;
|
|
3719
|
+
})
|
|
3720
|
+
]);
|
|
3721
|
+
if (accepted) {
|
|
3722
|
+
await this.reconcileRoomsWithSnapshot(accepted);
|
|
3723
|
+
}
|
|
3724
|
+
await Promise.all(
|
|
3725
|
+
this.roster.trackedRoomIds().map((roomId) => this.onRoomEvent?.(roomId, event) ?? Promise.resolve())
|
|
3726
|
+
);
|
|
3727
|
+
}
|
|
3728
|
+
async reconcileRoomsWithSnapshot(accepted) {
|
|
3729
|
+
const acceptedIds = this.autoSubscribeExistingRooms ? [...accepted.keys()] : this.roster.trackedRoomIds().filter((roomId) => accepted.has(roomId));
|
|
3730
|
+
const reconciliation = this.roster.reconcile(acceptedIds);
|
|
3731
|
+
await Promise.all(
|
|
3732
|
+
reconciliation.removed.map(async (roomId) => {
|
|
3733
|
+
await this.leaveRoomTracking(roomId);
|
|
3734
|
+
await this.onRoomLeft?.(roomId);
|
|
3735
|
+
})
|
|
3736
|
+
);
|
|
3737
|
+
await Promise.all(
|
|
3738
|
+
reconciliation.admitting.map(async ([roomId, ticket]) => {
|
|
3739
|
+
const admitted = await this.performAdmission(roomId, ticket);
|
|
3740
|
+
if (admitted) {
|
|
3741
|
+
await this.onRoomJoined?.(roomId, accepted.get(roomId) ?? {});
|
|
3742
|
+
}
|
|
3743
|
+
})
|
|
3744
|
+
);
|
|
3745
|
+
await Promise.all(
|
|
3746
|
+
reconciliation.resync.map(async (roomId) => {
|
|
3747
|
+
try {
|
|
3748
|
+
await this.link.subscribeRoom(roomId);
|
|
3749
|
+
} catch (error) {
|
|
3750
|
+
this.logger.warn("RoomPresence failed to resubscribe surviving room after reconnect", {
|
|
3751
|
+
roomId,
|
|
3752
|
+
error
|
|
3753
|
+
});
|
|
3754
|
+
}
|
|
3755
|
+
})
|
|
3756
|
+
);
|
|
3757
|
+
}
|
|
3100
3758
|
async unsubscribeRoom(roomId) {
|
|
3101
3759
|
try {
|
|
3102
3760
|
await this.link.unsubscribeRoom(roomId);
|
|
@@ -3141,9 +3799,10 @@ var AgentRuntime = class {
|
|
|
3141
3799
|
executions = /* @__PURE__ */ new Map();
|
|
3142
3800
|
executionWatchers = /* @__PURE__ */ new Map();
|
|
3143
3801
|
logger;
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3802
|
+
stoppedSignal = new TerminalSignal();
|
|
3803
|
+
lifecycle;
|
|
3804
|
+
startGate = new SingleFlight();
|
|
3805
|
+
stopGate = new SingleFlight();
|
|
3147
3806
|
constructor(options) {
|
|
3148
3807
|
this.link = options.link;
|
|
3149
3808
|
this.agentId = options.agentId;
|
|
@@ -3152,7 +3811,7 @@ var AgentRuntime = class {
|
|
|
3152
3811
|
this.onRoomJoined = options.onRoomJoined;
|
|
3153
3812
|
this.onRoomLeft = options.onRoomLeft;
|
|
3154
3813
|
this.onError = options.onError;
|
|
3155
|
-
this.logger = options.logger
|
|
3814
|
+
this.logger = resolveLogger(options.logger);
|
|
3156
3815
|
this.onContactEvent = options.onContactEvent;
|
|
3157
3816
|
this.onParticipantAdded = options.onParticipantAdded;
|
|
3158
3817
|
this.onParticipantRemoved = options.onParticipantRemoved;
|
|
@@ -3164,6 +3823,19 @@ var AgentRuntime = class {
|
|
|
3164
3823
|
maxMessageRetries: options.sessionConfig?.maxMessageRetries ?? 1,
|
|
3165
3824
|
enableContextHydration: options.sessionConfig?.enableContextHydration ?? true
|
|
3166
3825
|
};
|
|
3826
|
+
this.lifecycle = new LifecycleTracker({ status: "not_started" }, {
|
|
3827
|
+
owner: "AgentRuntime",
|
|
3828
|
+
logContext: { agentId: this.agentId },
|
|
3829
|
+
logger: this.logger,
|
|
3830
|
+
isLegalTransition: isLegalRuntimeTransition,
|
|
3831
|
+
onTransition: (state) => {
|
|
3832
|
+
if (state.status === "stopped") {
|
|
3833
|
+
this.stoppedSignal.settle(null);
|
|
3834
|
+
} else if (state.status === "failed") {
|
|
3835
|
+
this.stoppedSignal.settle(state.error);
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
});
|
|
3167
3839
|
this.presence = new RoomPresence({
|
|
3168
3840
|
link: this.link,
|
|
3169
3841
|
roomFilter: options.roomFilter,
|
|
@@ -3199,6 +3871,7 @@ var AgentRuntime = class {
|
|
|
3199
3871
|
return;
|
|
3200
3872
|
}
|
|
3201
3873
|
case "message_created":
|
|
3874
|
+
case "reconnected":
|
|
3202
3875
|
await this.getOrCreateExecution(roomId).enqueue(event);
|
|
3203
3876
|
return;
|
|
3204
3877
|
default:
|
|
@@ -3207,65 +3880,138 @@ var AgentRuntime = class {
|
|
|
3207
3880
|
};
|
|
3208
3881
|
this.presence.onContactEvent = this.onContactEvent ?? null;
|
|
3209
3882
|
}
|
|
3883
|
+
/** Current lifecycle state of this runtime. */
|
|
3884
|
+
get state() {
|
|
3885
|
+
return this.lifecycle.state;
|
|
3886
|
+
}
|
|
3887
|
+
/**
|
|
3888
|
+
* Connect, subscribe, and begin consuming platform events.
|
|
3889
|
+
*
|
|
3890
|
+
* Repeated or concurrent calls join the in-flight start instead of starting a
|
|
3891
|
+
* second consume loop. Calling `start()` while a `stop()` is still in flight
|
|
3892
|
+
* rejects with a `RuntimeStateError`.
|
|
3893
|
+
*/
|
|
3210
3894
|
async start() {
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3895
|
+
await startWithGate({
|
|
3896
|
+
lifecycle: this.lifecycle,
|
|
3897
|
+
startGate: this.startGate,
|
|
3898
|
+
stopGate: this.stopGate,
|
|
3899
|
+
stoppedSignal: this.stoppedSignal,
|
|
3900
|
+
ownerName: "AgentRuntime",
|
|
3901
|
+
runStart: () => this.runStart()
|
|
3902
|
+
});
|
|
3903
|
+
}
|
|
3904
|
+
async runStart() {
|
|
3217
3905
|
try {
|
|
3218
3906
|
await this.presence.start();
|
|
3219
3907
|
} catch (error) {
|
|
3220
|
-
await this.
|
|
3908
|
+
await this.finishFailedStart();
|
|
3221
3909
|
throw error;
|
|
3222
3910
|
}
|
|
3911
|
+
if (this.lifecycle.is("starting")) {
|
|
3912
|
+
this.lifecycle.transition({ status: "running" }, "started");
|
|
3913
|
+
}
|
|
3223
3914
|
void this.presence.waitUntilStopped().catch((error) => this.failRuntime(error, syntheticRuntimeFailureEvent(this.agentId)));
|
|
3224
3915
|
}
|
|
3916
|
+
async finishFailedStart() {
|
|
3917
|
+
try {
|
|
3918
|
+
await this.handleStartFailure();
|
|
3919
|
+
} catch (cleanupError) {
|
|
3920
|
+
this.markFailed(cleanupError, "start-cleanup-failed");
|
|
3921
|
+
throw cleanupError;
|
|
3922
|
+
}
|
|
3923
|
+
if (this.lifecycle.is("starting")) {
|
|
3924
|
+
this.lifecycle.transition({ status: "stopped" }, "start-failed");
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3225
3927
|
async handleStartFailure() {
|
|
3226
|
-
this.
|
|
3227
|
-
this.stopping = false;
|
|
3928
|
+
this.presence.abortEventLoop();
|
|
3228
3929
|
await this.link.disconnect();
|
|
3229
3930
|
}
|
|
3931
|
+
/**
|
|
3932
|
+
* Tear the runtime down.
|
|
3933
|
+
*
|
|
3934
|
+
* A concurrent second call joins the in-flight teardown and mirrors its
|
|
3935
|
+
* outcome — including rejecting with the *same* `Error` instance — instead of
|
|
3936
|
+
* reporting a shutdown it did not perform.
|
|
3937
|
+
*/
|
|
3230
3938
|
async stop(timeoutMs) {
|
|
3231
|
-
|
|
3939
|
+
return await this.stopGate.run(() => this.runStop(timeoutMs));
|
|
3940
|
+
}
|
|
3941
|
+
async runStop(timeoutMs) {
|
|
3942
|
+
const pendingStart = this.lifecycle.is("starting") ? this.startGate.pending : null;
|
|
3943
|
+
if (pendingStart) {
|
|
3944
|
+
try {
|
|
3945
|
+
await pendingStart;
|
|
3946
|
+
} catch (error) {
|
|
3947
|
+
this.logger.debug("AgentRuntime stop is proceeding after the in-flight start failed", { error });
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
if (this.lifecycle.is("not_started") || this.lifecycle.is("stopped")) {
|
|
3232
3951
|
return true;
|
|
3233
3952
|
}
|
|
3234
|
-
this.
|
|
3235
|
-
this.
|
|
3953
|
+
const fatalError = this.lifecycle.is("failed") ? this.lifecycle.state.error : null;
|
|
3954
|
+
this.startGate.reset();
|
|
3955
|
+
this.lifecycle.transition({ status: "stopping" }, "stop");
|
|
3956
|
+
try {
|
|
3957
|
+
return await this.performStop(timeoutMs, fatalError);
|
|
3958
|
+
} catch (error) {
|
|
3959
|
+
this.markFailed(error, "stop-failed");
|
|
3960
|
+
throw error;
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
async performStop(timeoutMs, fatalError) {
|
|
3964
|
+
const errors = [];
|
|
3236
3965
|
this.presence.abortEventLoop();
|
|
3237
|
-
await this.presence.waitUntilStopped()
|
|
3966
|
+
await isolateTeardown(errors, () => this.presence.waitUntilStopped());
|
|
3238
3967
|
let graceful = true;
|
|
3239
3968
|
await Promise.all(
|
|
3240
3969
|
[...this.executions].map(async ([roomId, execution]) => {
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
}
|
|
3970
|
+
await isolateTeardown(errors, async () => {
|
|
3971
|
+
graceful = await execution.stop(timeoutMs) && graceful;
|
|
3972
|
+
});
|
|
3245
3973
|
this.executions.delete(roomId);
|
|
3246
3974
|
})
|
|
3247
3975
|
);
|
|
3248
|
-
await this.presence.stop();
|
|
3976
|
+
await isolateTeardown(errors, () => this.presence.stop());
|
|
3249
3977
|
for (const roomId of [...this.contexts.keys()]) {
|
|
3250
|
-
await this.onSessionCleanup(roomId);
|
|
3978
|
+
await isolateTeardown(errors, () => this.onSessionCleanup(roomId));
|
|
3251
3979
|
}
|
|
3252
3980
|
this.contexts.clear();
|
|
3253
3981
|
this.executions.clear();
|
|
3254
3982
|
this.executionWatchers.clear();
|
|
3255
|
-
await this.link.disconnect();
|
|
3256
|
-
|
|
3257
|
-
|
|
3983
|
+
await isolateTeardown(errors, () => this.link.disconnect());
|
|
3984
|
+
const failure = this.lifecycle.is("failed") ? this.lifecycle.state.error : fatalError;
|
|
3985
|
+
if (failure) {
|
|
3986
|
+
if (!this.lifecycle.is("failed")) {
|
|
3987
|
+
this.lifecycle.transition({ status: "failed", error: failure }, "stopped-after-failure");
|
|
3988
|
+
}
|
|
3989
|
+
errors.unshift(failure);
|
|
3990
|
+
}
|
|
3991
|
+
if (errors.length > 0) {
|
|
3992
|
+
throw combineTeardownErrors(errors, "AgentRuntime failed to tear down cleanly");
|
|
3258
3993
|
}
|
|
3994
|
+
this.lifecycle.transition({ status: "stopped" }, "stopped");
|
|
3259
3995
|
return graceful;
|
|
3260
3996
|
}
|
|
3261
3997
|
getContext(roomId) {
|
|
3262
3998
|
return this.contexts.get(roomId);
|
|
3263
3999
|
}
|
|
4000
|
+
/**
|
|
4001
|
+
* Resolve once the runtime has actually stopped, or reject with the fatal
|
|
4002
|
+
* error that ended it.
|
|
4003
|
+
*
|
|
4004
|
+
* A runtime that was never started stays pending until it stops or fails;
|
|
4005
|
+
* starting it does not resolve a pending wait.
|
|
4006
|
+
*/
|
|
3264
4007
|
async waitUntilStopped() {
|
|
3265
|
-
await this.
|
|
3266
|
-
|
|
3267
|
-
|
|
4008
|
+
await this.stoppedSignal.wait();
|
|
4009
|
+
}
|
|
4010
|
+
markFailed(error, trigger) {
|
|
4011
|
+
if (this.lifecycle.is("not_started")) {
|
|
4012
|
+
return false;
|
|
3268
4013
|
}
|
|
4014
|
+
return this.lifecycle.fail(error, trigger);
|
|
3269
4015
|
}
|
|
3270
4016
|
getContexts() {
|
|
3271
4017
|
return [...this.contexts.values()];
|
|
@@ -3278,14 +4024,23 @@ var AgentRuntime = class {
|
|
|
3278
4024
|
await this.getOrCreateExecution(roomId).bootstrapMessage(message);
|
|
3279
4025
|
}
|
|
3280
4026
|
async resetRoomSession(roomId, timeoutMs) {
|
|
3281
|
-
return this.teardownExecution(roomId, timeoutMs);
|
|
4027
|
+
return await this.teardownExecution(roomId, timeoutMs);
|
|
3282
4028
|
}
|
|
3283
4029
|
async teardownExecution(roomId, timeoutMs) {
|
|
3284
4030
|
const execution = this.executions.get(roomId);
|
|
3285
|
-
const
|
|
4031
|
+
const errors = [];
|
|
4032
|
+
let graceful = true;
|
|
4033
|
+
if (execution) {
|
|
4034
|
+
await isolateTeardown(errors, async () => {
|
|
4035
|
+
graceful = await execution.stop(timeoutMs);
|
|
4036
|
+
});
|
|
4037
|
+
}
|
|
3286
4038
|
this.executions.delete(roomId);
|
|
3287
4039
|
this.contexts.delete(roomId);
|
|
3288
|
-
await this.onSessionCleanup(roomId);
|
|
4040
|
+
await isolateTeardown(errors, () => this.onSessionCleanup(roomId));
|
|
4041
|
+
if (errors.length > 0) {
|
|
4042
|
+
throw combineTeardownErrors(errors, "AgentRuntime failed to tear down cleanly");
|
|
4043
|
+
}
|
|
3289
4044
|
return graceful;
|
|
3290
4045
|
}
|
|
3291
4046
|
getOrCreateExecution(roomId) {
|
|
@@ -3346,15 +4101,20 @@ var AgentRuntime = class {
|
|
|
3346
4101
|
return context;
|
|
3347
4102
|
}
|
|
3348
4103
|
async failRuntime(error, event) {
|
|
3349
|
-
if (
|
|
3350
|
-
this.fatalError = error;
|
|
3351
|
-
this.running = false;
|
|
4104
|
+
if (this.markFailed(error, "runtime-error")) {
|
|
3352
4105
|
this.logger.error("Fatal runtime error handling platform event", {
|
|
3353
4106
|
eventType: event.type,
|
|
3354
4107
|
roomId: event.roomId,
|
|
3355
4108
|
error
|
|
3356
4109
|
});
|
|
3357
4110
|
this.notifyOnError(error, event);
|
|
4111
|
+
} else {
|
|
4112
|
+
this.logger.debug("Runtime error after the lifecycle already ended", {
|
|
4113
|
+
status: this.lifecycle.state.status,
|
|
4114
|
+
eventType: event.type,
|
|
4115
|
+
roomId: event.roomId,
|
|
4116
|
+
error
|
|
4117
|
+
});
|
|
3358
4118
|
}
|
|
3359
4119
|
this.presence.abortEventLoop();
|
|
3360
4120
|
}
|
|
@@ -3394,6 +4154,55 @@ function syntheticRuntimeFailureEvent(agentId) {
|
|
|
3394
4154
|
};
|
|
3395
4155
|
}
|
|
3396
4156
|
|
|
4157
|
+
// src/core/epoch.ts
|
|
4158
|
+
var Epoch = class {
|
|
4159
|
+
value = 0;
|
|
4160
|
+
get current() {
|
|
4161
|
+
return this.value;
|
|
4162
|
+
}
|
|
4163
|
+
/** Advances to the next generation and returns it. */
|
|
4164
|
+
bump() {
|
|
4165
|
+
this.value += 1;
|
|
4166
|
+
return this.value;
|
|
4167
|
+
}
|
|
4168
|
+
isStale(captured) {
|
|
4169
|
+
return captured !== this.value;
|
|
4170
|
+
}
|
|
4171
|
+
};
|
|
4172
|
+
|
|
4173
|
+
// src/platform/Session.ts
|
|
4174
|
+
var Session = class {
|
|
4175
|
+
epoch = new Epoch();
|
|
4176
|
+
active = false;
|
|
4177
|
+
/**
|
|
4178
|
+
* Teardown for whatever reconnect observer this session registered on the
|
|
4179
|
+
* transport — a plain field since a caller just assigns whatever
|
|
4180
|
+
* `onReconnected()` handed back (or `null` if the transport has none).
|
|
4181
|
+
*/
|
|
4182
|
+
reconnectObserverTeardown = null;
|
|
4183
|
+
get isActive() {
|
|
4184
|
+
return this.active;
|
|
4185
|
+
}
|
|
4186
|
+
isStale(epoch) {
|
|
4187
|
+
return this.epoch.isStale(epoch);
|
|
4188
|
+
}
|
|
4189
|
+
/** Starts a new session and returns its epoch. */
|
|
4190
|
+
begin() {
|
|
4191
|
+
const epoch = this.epoch.bump();
|
|
4192
|
+
this.active = true;
|
|
4193
|
+
return epoch;
|
|
4194
|
+
}
|
|
4195
|
+
/** Marks the session inactive and advances the epoch, so any work still in flight for it is now stale. */
|
|
4196
|
+
deactivate() {
|
|
4197
|
+
this.active = false;
|
|
4198
|
+
this.epoch.bump();
|
|
4199
|
+
}
|
|
4200
|
+
clearReconnectObserver() {
|
|
4201
|
+
this.reconnectObserverTeardown?.();
|
|
4202
|
+
this.reconnectObserverTeardown = null;
|
|
4203
|
+
}
|
|
4204
|
+
};
|
|
4205
|
+
|
|
3397
4206
|
// src/client/rest/pagination.ts
|
|
3398
4207
|
var DEFAULT_PAGE_SIZE = 100;
|
|
3399
4208
|
var DEFAULT_MAX_PAGES = 100;
|
|
@@ -3567,7 +4376,7 @@ function toNonNegativeInteger(value) {
|
|
|
3567
4376
|
}
|
|
3568
4377
|
|
|
3569
4378
|
// src/adapters/shared/coercion.ts
|
|
3570
|
-
function
|
|
4379
|
+
function asOptionalRecord2(value) {
|
|
3571
4380
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3572
4381
|
return void 0;
|
|
3573
4382
|
}
|
|
@@ -3588,7 +4397,7 @@ function asNullableString(value) {
|
|
|
3588
4397
|
|
|
3589
4398
|
// src/client/rest/responseNormalization.ts
|
|
3590
4399
|
function asMetadataMap(value) {
|
|
3591
|
-
return
|
|
4400
|
+
return asOptionalRecord2(value);
|
|
3592
4401
|
}
|
|
3593
4402
|
function normalizeContactRequestDirection(value) {
|
|
3594
4403
|
const direction = asMetadataMap(value);
|
|
@@ -3623,7 +4432,7 @@ function mergeOptions(options, maxRetries) {
|
|
|
3623
4432
|
};
|
|
3624
4433
|
}
|
|
3625
4434
|
function asMetadataMap2(value) {
|
|
3626
|
-
return
|
|
4435
|
+
return asOptionalRecord2(value);
|
|
3627
4436
|
}
|
|
3628
4437
|
function requireNonEmptyStringField(value, field, source) {
|
|
3629
4438
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
@@ -3679,7 +4488,7 @@ function asRecordArray(value) {
|
|
|
3679
4488
|
if (!Array.isArray(value)) {
|
|
3680
4489
|
return void 0;
|
|
3681
4490
|
}
|
|
3682
|
-
return value.filter((entry) =>
|
|
4491
|
+
return value.filter((entry) => asOptionalRecord2(entry) !== void 0);
|
|
3683
4492
|
}
|
|
3684
4493
|
function extractEnvelopeData(value) {
|
|
3685
4494
|
const record = asMetadataMap2(value);
|
|
@@ -4176,107 +4985,213 @@ var FernRestAdapter = class {
|
|
|
4176
4985
|
}
|
|
4177
4986
|
};
|
|
4178
4987
|
|
|
4179
|
-
// src/platform/streaming/payloadSchemas.ts
|
|
4180
|
-
var import_zod = require("zod");
|
|
4181
|
-
var mentionSchema = import_zod.z.object({
|
|
4182
|
-
id: import_zod.z.string(),
|
|
4183
|
-
handle: import_zod.z.string().nullish(),
|
|
4184
|
-
name: import_zod.z.string().nullish(),
|
|
4185
|
-
username: import_zod.z.string().nullish()
|
|
4186
|
-
}).passthrough();
|
|
4187
|
-
var messageMetadataSchema = import_zod.z.object({
|
|
4188
|
-
mentions: import_zod.z.array(mentionSchema).nullish()
|
|
4189
|
-
}).passthrough();
|
|
4190
|
-
var messageCreatedPayloadSchema = import_zod.z.object({
|
|
4191
|
-
id: import_zod.z.string(),
|
|
4192
|
-
content: import_zod.z.string(),
|
|
4193
|
-
message_type: import_zod.z.string(),
|
|
4194
|
-
metadata: messageMetadataSchema.nullish(),
|
|
4195
|
-
sender_id: import_zod.z.string(),
|
|
4196
|
-
sender_type: import_zod.z.string(),
|
|
4197
|
-
sender_name: import_zod.z.string().nullish(),
|
|
4198
|
-
chat_room_id: import_zod.z.string().nullish(),
|
|
4199
|
-
inserted_at: import_zod.z.string(),
|
|
4200
|
-
updated_at: import_zod.z.string()
|
|
4201
|
-
}).passthrough();
|
|
4202
|
-
var roomOwnerSchema = import_zod.z.object({
|
|
4203
|
-
id: import_zod.z.string(),
|
|
4204
|
-
name: import_zod.z.string(),
|
|
4205
|
-
type: import_zod.z.string()
|
|
4206
|
-
}).passthrough();
|
|
4207
|
-
var roomAddedPayloadSchema = import_zod.z.object({
|
|
4208
|
-
id: import_zod.z.string(),
|
|
4209
|
-
title: import_zod.z.string().nullish(),
|
|
4210
|
-
task_id: import_zod.z.string().nullish(),
|
|
4211
|
-
inserted_at: import_zod.z.string().nullish(),
|
|
4212
|
-
updated_at: import_zod.z.string().nullish(),
|
|
4213
|
-
owner: roomOwnerSchema.nullish(),
|
|
4214
|
-
status: import_zod.z.string().nullish(),
|
|
4215
|
-
type: import_zod.z.string().nullish(),
|
|
4216
|
-
created_at: import_zod.z.string().nullish(),
|
|
4217
|
-
participant_role: import_zod.z.string().nullish()
|
|
4218
|
-
}).passthrough();
|
|
4219
|
-
var roomRemovedPayloadSchema = import_zod.z.object({
|
|
4220
|
-
id: import_zod.z.string(),
|
|
4221
|
-
status: import_zod.z.string(),
|
|
4222
|
-
type: import_zod.z.string(),
|
|
4223
|
-
title: import_zod.z.string(),
|
|
4224
|
-
removed_at: import_zod.z.string()
|
|
4225
|
-
}).passthrough();
|
|
4226
|
-
var participantAddedPayloadSchema = import_zod.z.object({
|
|
4227
|
-
id: import_zod.z.string(),
|
|
4228
|
-
name: import_zod.z.string(),
|
|
4229
|
-
type: import_zod.z.string(),
|
|
4230
|
-
handle: import_zod.z.string().nullish()
|
|
4231
|
-
}).passthrough();
|
|
4232
|
-
var participantRemovedPayloadSchema = import_zod.z.object({
|
|
4233
|
-
id: import_zod.z.string()
|
|
4234
|
-
}).passthrough();
|
|
4235
|
-
var roomDeletedPayloadSchema = import_zod.z.object({
|
|
4236
|
-
id: import_zod.z.string()
|
|
4237
|
-
}).passthrough();
|
|
4238
|
-
var contactRequestReceivedPayloadSchema = import_zod.z.object({
|
|
4239
|
-
id: import_zod.z.string(),
|
|
4240
|
-
from_handle: import_zod.z.string(),
|
|
4241
|
-
from_name: import_zod.z.string(),
|
|
4242
|
-
message: import_zod.z.string().nullish(),
|
|
4243
|
-
status: import_zod.z.string(),
|
|
4244
|
-
inserted_at: import_zod.z.string()
|
|
4245
|
-
}).passthrough();
|
|
4246
|
-
var contactRequestUpdatedPayloadSchema = import_zod.z.object({
|
|
4247
|
-
id: import_zod.z.string(),
|
|
4248
|
-
status: import_zod.z.string()
|
|
4249
|
-
}).passthrough();
|
|
4250
|
-
var contactAddedPayloadSchema = import_zod.z.object({
|
|
4251
|
-
id: import_zod.z.string(),
|
|
4252
|
-
handle: import_zod.z.string(),
|
|
4253
|
-
name: import_zod.z.string(),
|
|
4254
|
-
type: import_zod.z.string(),
|
|
4255
|
-
description: import_zod.z.string().nullish(),
|
|
4256
|
-
is_external: import_zod.z.boolean().nullish(),
|
|
4257
|
-
inserted_at: import_zod.z.string()
|
|
4258
|
-
}).passthrough();
|
|
4259
|
-
var contactRemovedPayloadSchema = import_zod.z.object({
|
|
4260
|
-
id: import_zod.z.string()
|
|
4261
|
-
}).passthrough();
|
|
4262
|
-
var payloadSchemas = {
|
|
4263
|
-
message_created: messageCreatedPayloadSchema,
|
|
4264
|
-
room_added: roomAddedPayloadSchema,
|
|
4265
|
-
room_removed: roomRemovedPayloadSchema,
|
|
4266
|
-
participant_added: participantAddedPayloadSchema,
|
|
4267
|
-
participant_removed: participantRemovedPayloadSchema,
|
|
4268
|
-
room_deleted: roomDeletedPayloadSchema,
|
|
4269
|
-
contact_request_received: contactRequestReceivedPayloadSchema,
|
|
4270
|
-
contact_request_updated: contactRequestUpdatedPayloadSchema,
|
|
4271
|
-
contact_added: contactAddedPayloadSchema,
|
|
4272
|
-
contact_removed: contactRemovedPayloadSchema
|
|
4273
|
-
};
|
|
4274
|
-
|
|
4275
4988
|
// src/platform/streaming/PhoenixChannelsTransport.ts
|
|
4989
|
+
var import_phoenix2 = require("phoenix");
|
|
4990
|
+
|
|
4991
|
+
// src/core/deferred.ts
|
|
4992
|
+
function createDeferred() {
|
|
4993
|
+
let resolve;
|
|
4994
|
+
const promise = new Promise((innerResolve) => {
|
|
4995
|
+
resolve = innerResolve;
|
|
4996
|
+
});
|
|
4997
|
+
return { promise, resolve };
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
// src/platform/streaming/ChannelRegistry.ts
|
|
4276
5001
|
var import_phoenix = require("phoenix");
|
|
5002
|
+
function supersededJoinError(topic) {
|
|
5003
|
+
return new TransportError(`Join superseded by transport disconnect for topic ${topic}`);
|
|
5004
|
+
}
|
|
5005
|
+
var ChannelRegistry = class {
|
|
5006
|
+
constructor(socket, epoch, logger, hooks) {
|
|
5007
|
+
this.socket = socket;
|
|
5008
|
+
this.epoch = epoch;
|
|
5009
|
+
this.logger = logger;
|
|
5010
|
+
this.hooks = hooks;
|
|
5011
|
+
}
|
|
5012
|
+
channels = /* @__PURE__ */ new Map();
|
|
5013
|
+
// A join's Channel and handler bindings, tracked from the moment doJoin
|
|
5014
|
+
// creates them — before the join Push settles — so a teardown mid-join can
|
|
5015
|
+
// find and detach it too, not only joins already promoted into `channels`.
|
|
5016
|
+
// Left untracked here, the underlying Phoenix Channel would survive
|
|
5017
|
+
// teardown unnoticed, keep its handlers bound, and could later be
|
|
5018
|
+
// resurrected by Phoenix's own reconnect machinery, redelivering live
|
|
5019
|
+
// events with no dedup anywhere upstream.
|
|
5020
|
+
pendingChannels = /* @__PURE__ */ new Map();
|
|
5021
|
+
joinFlights = new KeyedSingleFlight();
|
|
5022
|
+
leaveFlights = new KeyedSingleFlight();
|
|
5023
|
+
topics() {
|
|
5024
|
+
return this.channels.keys();
|
|
5025
|
+
}
|
|
5026
|
+
isJoined(topic) {
|
|
5027
|
+
return this.channels.has(topic);
|
|
5028
|
+
}
|
|
5029
|
+
/**
|
|
5030
|
+
* A promise for `topic` if it's already joined or has a join in flight,
|
|
5031
|
+
* without starting a new one. A topic mid-leave is never reported as
|
|
5032
|
+
* already joined — `channels` still holds it until the leave's Push
|
|
5033
|
+
* settles, but its handlers are already unbound and the channel is about
|
|
5034
|
+
* to be removed, so treating that window as "joined" would hand the
|
|
5035
|
+
* caller a promise that resolves into a channel already gone.
|
|
5036
|
+
*/
|
|
5037
|
+
existingJoin(topic) {
|
|
5038
|
+
if (this.channels.has(topic) && !this.leaveFlights.current(topic)) {
|
|
5039
|
+
return Promise.resolve();
|
|
5040
|
+
}
|
|
5041
|
+
return this.joinFlights.current(topic) ?? void 0;
|
|
5042
|
+
}
|
|
5043
|
+
/**
|
|
5044
|
+
* Starts a new join for `topic`. Callers check `existingJoin()` first.
|
|
5045
|
+
* Waits out a leave already in flight for the same topic before starting,
|
|
5046
|
+
* so the new join's channel is never raced by the old one's teardown.
|
|
5047
|
+
*/
|
|
5048
|
+
async join(topic, handlers) {
|
|
5049
|
+
const pendingLeave = this.leaveFlights.current(topic);
|
|
5050
|
+
if (pendingLeave) {
|
|
5051
|
+
await pendingLeave.catch(() => void 0);
|
|
5052
|
+
}
|
|
5053
|
+
return this.joinFlights.run(topic, () => this.doJoin(topic, handlers));
|
|
5054
|
+
}
|
|
5055
|
+
async doJoin(topic, handlers) {
|
|
5056
|
+
const epoch = this.epoch.current;
|
|
5057
|
+
const channel = this.socket.channel(topic, {});
|
|
5058
|
+
const refs = [];
|
|
5059
|
+
for (const [event, handler] of Object.entries(handlers)) {
|
|
5060
|
+
const ref = channel.on(event, this.hooks.wrapHandler(topic, event, handler));
|
|
5061
|
+
refs.push([event, ref]);
|
|
5062
|
+
}
|
|
5063
|
+
const joinPush = channel.join();
|
|
5064
|
+
try {
|
|
5065
|
+
await new Promise((resolve, reject) => {
|
|
5066
|
+
this.pendingChannels.set(topic, { channel, refs, reject });
|
|
5067
|
+
joinPush.receive("ok", () => {
|
|
5068
|
+
this.hooks.onJoinSettled(topic, true);
|
|
5069
|
+
resolve();
|
|
5070
|
+
}).receive("error", (error) => {
|
|
5071
|
+
this.hooks.onJoinSettled(topic, false);
|
|
5072
|
+
reject(new TransportError(`Failed to join topic ${topic}`, error));
|
|
5073
|
+
}).receive("timeout", () => {
|
|
5074
|
+
this.hooks.onJoinSettled(topic, false);
|
|
5075
|
+
reject(new TransportError(`Timeout joining topic ${topic}`));
|
|
5076
|
+
});
|
|
5077
|
+
});
|
|
5078
|
+
} catch (error) {
|
|
5079
|
+
const pending2 = this.forgetPendingChannel(topic, channel);
|
|
5080
|
+
if (pending2) {
|
|
5081
|
+
this.abandonChannel(topic, pending2);
|
|
5082
|
+
}
|
|
5083
|
+
throw error;
|
|
5084
|
+
}
|
|
5085
|
+
const pending = this.forgetPendingChannel(topic, channel);
|
|
5086
|
+
if (!pending || this.epoch.isStale(epoch)) {
|
|
5087
|
+
if (pending) {
|
|
5088
|
+
this.abandonChannel(topic, pending);
|
|
5089
|
+
}
|
|
5090
|
+
this.logger.debug("Join superseded by transport disconnect/reconnect", { topic });
|
|
5091
|
+
throw supersededJoinError(topic);
|
|
5092
|
+
}
|
|
5093
|
+
this.channels.set(topic, { channel, refs });
|
|
5094
|
+
this.logger.debug("Joined topic", { topic });
|
|
5095
|
+
}
|
|
5096
|
+
/**
|
|
5097
|
+
* Removes and returns `topic`'s pendingChannels entry only if it still
|
|
5098
|
+
* points at `channel`. A topic-keyed delete without this identity check
|
|
5099
|
+
* can drop a *different*, still-genuinely-pending join for the same topic
|
|
5100
|
+
* — reachable because a teardown clears `joinFlights`, so a later join for
|
|
5101
|
+
* a topic whose earlier join is still unsettled is possible, and that
|
|
5102
|
+
* earlier join's eventual (stale) settlement must not touch a slot it no
|
|
5103
|
+
* longer owns.
|
|
5104
|
+
*/
|
|
5105
|
+
forgetPendingChannel(topic, channel) {
|
|
5106
|
+
const pending = this.pendingChannels.get(topic);
|
|
5107
|
+
if (pending?.channel !== channel) {
|
|
5108
|
+
return void 0;
|
|
5109
|
+
}
|
|
5110
|
+
this.pendingChannels.delete(topic);
|
|
5111
|
+
return pending;
|
|
5112
|
+
}
|
|
5113
|
+
/**
|
|
5114
|
+
* Detaches a join's channel and — via `pending.reject` — settles its
|
|
5115
|
+
* still-outstanding join promise directly, so abandoning it can never
|
|
5116
|
+
* leave `doJoin()`'s awaiter hanging on a Phoenix reply that a `leave()`
|
|
5117
|
+
* just made unroutable.
|
|
5118
|
+
*/
|
|
5119
|
+
abandonChannel(topic, pending) {
|
|
5120
|
+
for (const [event, ref] of pending.refs) {
|
|
5121
|
+
pending.channel.off(event, ref);
|
|
5122
|
+
}
|
|
5123
|
+
pending.channel.leave();
|
|
5124
|
+
removeSocketChannel(this.socket, pending.channel);
|
|
5125
|
+
pending.reject(supersededJoinError(topic));
|
|
5126
|
+
}
|
|
5127
|
+
async leave(topic) {
|
|
5128
|
+
const pendingLeave = this.leaveFlights.current(topic);
|
|
5129
|
+
if (pendingLeave) {
|
|
5130
|
+
return pendingLeave;
|
|
5131
|
+
}
|
|
5132
|
+
const tracked = this.channels.get(topic);
|
|
5133
|
+
if (!tracked) {
|
|
5134
|
+
return;
|
|
5135
|
+
}
|
|
5136
|
+
return this.leaveFlights.run(topic, () => this.doLeave(topic, tracked));
|
|
5137
|
+
}
|
|
5138
|
+
async doLeave(topic, tracked) {
|
|
5139
|
+
const { channel, refs } = tracked;
|
|
5140
|
+
for (const [event, ref] of refs) {
|
|
5141
|
+
channel.off(event, ref);
|
|
5142
|
+
}
|
|
5143
|
+
await new Promise((resolve, reject) => {
|
|
5144
|
+
channel.leave().receive("ok", () => resolve()).receive(
|
|
5145
|
+
"error",
|
|
5146
|
+
(error) => reject(new TransportError(`Failed to leave topic ${topic}`, error))
|
|
5147
|
+
).receive(
|
|
5148
|
+
"timeout",
|
|
5149
|
+
() => reject(new TransportError(`Timeout leaving topic ${topic}`))
|
|
5150
|
+
);
|
|
5151
|
+
});
|
|
5152
|
+
this.channels.delete(topic);
|
|
5153
|
+
this.hooks.onLeft(topic);
|
|
5154
|
+
this.logger.debug("Left topic", { topic });
|
|
5155
|
+
}
|
|
5156
|
+
/**
|
|
5157
|
+
* Attempts a graceful leave for every currently joined topic. A leave that
|
|
5158
|
+
* fails leaves its channel registered until `forceTeardown()` sweeps it.
|
|
5159
|
+
* Returns the rejection reasons of any leaves that failed.
|
|
5160
|
+
*/
|
|
5161
|
+
async leaveAll() {
|
|
5162
|
+
const topics = [...this.channels.keys()];
|
|
5163
|
+
const results = await Promise.allSettled(topics.map((topic) => this.leave(topic)));
|
|
5164
|
+
return results.filter((result) => result.status === "rejected").map((result) => result.reason);
|
|
5165
|
+
}
|
|
5166
|
+
/**
|
|
5167
|
+
* Forcibly detaches anything still registered — a leave that failed above,
|
|
5168
|
+
* or a join still in flight — and clears all coalescing state. Call after
|
|
5169
|
+
* `leaveAll()` has settled.
|
|
5170
|
+
*/
|
|
5171
|
+
forceTeardown() {
|
|
5172
|
+
for (const { channel, refs } of this.channels.values()) {
|
|
5173
|
+
for (const [event, ref] of refs) {
|
|
5174
|
+
channel.off(event, ref);
|
|
5175
|
+
}
|
|
5176
|
+
removeSocketChannel(this.socket, channel);
|
|
5177
|
+
}
|
|
5178
|
+
this.channels.clear();
|
|
5179
|
+
for (const [topic, pending] of this.pendingChannels) {
|
|
5180
|
+
this.abandonChannel(topic, pending);
|
|
5181
|
+
}
|
|
5182
|
+
this.pendingChannels.clear();
|
|
5183
|
+
this.joinFlights.clear();
|
|
5184
|
+
this.leaveFlights.clear();
|
|
5185
|
+
}
|
|
5186
|
+
};
|
|
5187
|
+
function removeSocketChannel(socket, channel) {
|
|
5188
|
+
const candidate = socket;
|
|
5189
|
+
candidate.remove?.(channel);
|
|
5190
|
+
}
|
|
4277
5191
|
|
|
4278
5192
|
// src/platform/streaming/disconnectReason.ts
|
|
4279
|
-
var
|
|
5193
|
+
var import_zod = require("zod");
|
|
5194
|
+
var import_band_sdk_core5 = require("@band-ai/band-sdk-core");
|
|
4280
5195
|
var UPGRADE_CODES = [
|
|
4281
5196
|
"invalid_on_conflict",
|
|
4282
5197
|
"connection_conflict",
|
|
@@ -4305,9 +5220,9 @@ var UPGRADE_REASONS = {
|
|
|
4305
5220
|
retryable: true
|
|
4306
5221
|
}
|
|
4307
5222
|
};
|
|
4308
|
-
var upgradeErrorSchema =
|
|
4309
|
-
status:
|
|
4310
|
-
body:
|
|
5223
|
+
var upgradeErrorSchema = import_zod.z.object({
|
|
5224
|
+
status: import_zod.z.number(),
|
|
5225
|
+
body: import_zod.z.preprocess(
|
|
4311
5226
|
(body) => {
|
|
4312
5227
|
if (typeof body !== "string") {
|
|
4313
5228
|
return body;
|
|
@@ -4318,16 +5233,16 @@ var upgradeErrorSchema = import_zod2.z.object({
|
|
|
4318
5233
|
return null;
|
|
4319
5234
|
}
|
|
4320
5235
|
},
|
|
4321
|
-
|
|
4322
|
-
error:
|
|
4323
|
-
code:
|
|
4324
|
-
message:
|
|
4325
|
-
retry_after:
|
|
4326
|
-
request_id:
|
|
5236
|
+
import_zod.z.object({
|
|
5237
|
+
error: import_zod.z.object({
|
|
5238
|
+
code: import_zod.z.enum(UPGRADE_CODES),
|
|
5239
|
+
message: import_zod.z.string().optional(),
|
|
5240
|
+
retry_after: import_zod.z.number().nullable().optional(),
|
|
5241
|
+
request_id: import_zod.z.string().nullable().optional()
|
|
4327
5242
|
})
|
|
4328
5243
|
})
|
|
4329
5244
|
),
|
|
4330
|
-
headers:
|
|
5245
|
+
headers: import_zod.z.record(import_zod.z.string(), import_zod.z.unknown()).optional()
|
|
4331
5246
|
});
|
|
4332
5247
|
var WebSocketDisconnectError = class extends Error {
|
|
4333
5248
|
reason;
|
|
@@ -4338,18 +5253,20 @@ var WebSocketDisconnectError = class extends Error {
|
|
|
4338
5253
|
}
|
|
4339
5254
|
};
|
|
4340
5255
|
function parseSupersedeDisconnectReason(payload) {
|
|
4341
|
-
|
|
5256
|
+
try {
|
|
5257
|
+
const normalized = (0, import_band_sdk_core5.validateEventPayload)("supersede", payload);
|
|
5258
|
+
return {
|
|
5259
|
+
source: "agent_control",
|
|
5260
|
+
code: normalized.reason,
|
|
5261
|
+
message: normalized.message,
|
|
5262
|
+
retryable: normalized.retryable,
|
|
5263
|
+
retryAfter: normalized.retry_after ?? null,
|
|
5264
|
+
targetSocketId: normalized.target_socket_id ?? null,
|
|
5265
|
+
correlationId: normalized.correlation_id
|
|
5266
|
+
};
|
|
5267
|
+
} catch {
|
|
4342
5268
|
return null;
|
|
4343
5269
|
}
|
|
4344
|
-
return {
|
|
4345
|
-
source: "agent_control",
|
|
4346
|
-
code: payload.reason,
|
|
4347
|
-
message: payload.message,
|
|
4348
|
-
retryable: false,
|
|
4349
|
-
retryAfter: typeof payload.retry_after === "number" ? payload.retry_after : null,
|
|
4350
|
-
targetSocketId: typeof payload.target_socket_id === "string" ? payload.target_socket_id : null,
|
|
4351
|
-
correlationId: typeof payload.correlation_id === "string" ? payload.correlation_id : null
|
|
4352
|
-
};
|
|
4353
5270
|
}
|
|
4354
5271
|
function parseUpgradeDisconnectReason(event) {
|
|
4355
5272
|
const parsed = upgradeErrorSchema.safeParse(event);
|
|
@@ -4434,19 +5351,90 @@ async function readResponseBody(response) {
|
|
|
4434
5351
|
return Buffer.concat(chunks).toString("utf8");
|
|
4435
5352
|
}
|
|
4436
5353
|
|
|
4437
|
-
// src/platform/streaming/
|
|
4438
|
-
var
|
|
5354
|
+
// src/platform/streaming/ReconnectGenerationTracker.ts
|
|
5355
|
+
var ReconnectGenerationTracker = class {
|
|
5356
|
+
constructor(onSettled, onGenerationDropped) {
|
|
5357
|
+
this.onSettled = onSettled;
|
|
5358
|
+
this.onGenerationDropped = onGenerationDropped;
|
|
5359
|
+
}
|
|
5360
|
+
generations = /* @__PURE__ */ new Map();
|
|
5361
|
+
currentGeneration = 0;
|
|
5362
|
+
beginGeneration(topics) {
|
|
5363
|
+
const generation = ++this.currentGeneration;
|
|
5364
|
+
const attempted = new Set(topics);
|
|
5365
|
+
for (const staleGeneration of this.generations.keys()) {
|
|
5366
|
+
if (staleGeneration < generation) {
|
|
5367
|
+
const stale = this.generations.get(staleGeneration);
|
|
5368
|
+
if (stale) {
|
|
5369
|
+
this.onGenerationDropped?.(staleGeneration, stale.pending.size);
|
|
5370
|
+
this.generations.delete(staleGeneration);
|
|
5371
|
+
}
|
|
5372
|
+
}
|
|
5373
|
+
}
|
|
5374
|
+
this.generations.set(generation, { pending: new Set(attempted), attempted, joined: /* @__PURE__ */ new Set() });
|
|
5375
|
+
this.maybeFinalize(generation);
|
|
5376
|
+
return generation;
|
|
5377
|
+
}
|
|
5378
|
+
recordSettled(topic, joined) {
|
|
5379
|
+
const record = this.generations.get(this.currentGeneration);
|
|
5380
|
+
if (!record?.pending.delete(topic)) {
|
|
5381
|
+
return;
|
|
5382
|
+
}
|
|
5383
|
+
if (joined) {
|
|
5384
|
+
record.joined.add(topic);
|
|
5385
|
+
}
|
|
5386
|
+
this.maybeFinalize(this.currentGeneration);
|
|
5387
|
+
}
|
|
5388
|
+
removeTopic(topic) {
|
|
5389
|
+
for (const [generation, record] of this.generations) {
|
|
5390
|
+
if (record.pending.delete(topic)) {
|
|
5391
|
+
this.maybeFinalize(generation);
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5394
|
+
}
|
|
5395
|
+
reset() {
|
|
5396
|
+
this.generations.clear();
|
|
5397
|
+
this.currentGeneration = 0;
|
|
5398
|
+
}
|
|
5399
|
+
maybeFinalize(generation) {
|
|
5400
|
+
const record = this.generations.get(generation);
|
|
5401
|
+
if (!record || record.pending.size > 0) {
|
|
5402
|
+
return;
|
|
5403
|
+
}
|
|
5404
|
+
this.generations.delete(generation);
|
|
5405
|
+
this.onSettled({ generation, attemptedTopics: record.attempted, joinedTopics: record.joined });
|
|
5406
|
+
}
|
|
5407
|
+
};
|
|
5408
|
+
|
|
5409
|
+
// src/platform/streaming/PhoenixChannelsTransport.ts
|
|
5410
|
+
var import_band_sdk_core6 = require("@band-ai/band-sdk-core");
|
|
4439
5411
|
var PhoenixChannelsTransport = class {
|
|
4440
5412
|
socket;
|
|
4441
5413
|
agentId;
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
5414
|
+
registry;
|
|
5415
|
+
reconnectObservers = /* @__PURE__ */ new Set();
|
|
5416
|
+
// Topics joined with `{ exemptFromBuffering: true }`, recorded here so
|
|
5417
|
+
// `wrapHandler` can check by name on every delivered event rather than
|
|
5418
|
+
// threading the flag through the channel/handler plumbing.
|
|
5419
|
+
bufferingExemptTopics = /* @__PURE__ */ new Set();
|
|
5420
|
+
generationTracker = new ReconnectGenerationTracker(
|
|
5421
|
+
(snapshot) => this.notifyReconnectObservers(snapshot),
|
|
5422
|
+
(generation, pendingTopics) => this.logger.debug("Superseded reconnect generation before it fully settled", {
|
|
5423
|
+
generation,
|
|
5424
|
+
pendingTopics
|
|
5425
|
+
})
|
|
5426
|
+
);
|
|
5427
|
+
bufferedTopicEvents = [];
|
|
5428
|
+
hasOpenedOnce = false;
|
|
5429
|
+
epoch = new Epoch();
|
|
5430
|
+
bufferingGeneration = null;
|
|
5431
|
+
reconnectBarrier = null;
|
|
5432
|
+
observerChain = new Serializer();
|
|
4445
5433
|
logger;
|
|
4446
5434
|
onTerminalDisconnect;
|
|
4447
5435
|
onHandlerError;
|
|
4448
5436
|
connected = false;
|
|
4449
|
-
|
|
5437
|
+
connectFlight = new SingleFlight2();
|
|
4450
5438
|
connectResolve = null;
|
|
4451
5439
|
connectReject = null;
|
|
4452
5440
|
lastDisconnectReason = null;
|
|
@@ -4455,7 +5443,7 @@ var PhoenixChannelsTransport = class {
|
|
|
4455
5443
|
stoppingReconnect = false;
|
|
4456
5444
|
suppressNextCloseReason = false;
|
|
4457
5445
|
constructor(options) {
|
|
4458
|
-
this.logger = options.logger
|
|
5446
|
+
this.logger = resolveLogger(options.logger);
|
|
4459
5447
|
this.agentId = options.agentId;
|
|
4460
5448
|
this.onTerminalDisconnect = options.onTerminalDisconnect;
|
|
4461
5449
|
let wsUrl = options.wsUrl;
|
|
@@ -4463,7 +5451,7 @@ var PhoenixChannelsTransport = class {
|
|
|
4463
5451
|
wsUrl = wsUrl.slice(0, -"/websocket".length);
|
|
4464
5452
|
}
|
|
4465
5453
|
const reconnectAfterMs = options.reconnectAfterMs ?? ((tries) => [1e3, 2e3, 5e3, 1e4, 3e4][tries - 1] ?? 3e4);
|
|
4466
|
-
this.socket = new
|
|
5454
|
+
this.socket = new import_phoenix2.Socket(wsUrl, {
|
|
4467
5455
|
params: {
|
|
4468
5456
|
agent_id: options.agentId,
|
|
4469
5457
|
...options.conflictPolicy ? { on_conflict: options.conflictPolicy } : {}
|
|
@@ -4477,6 +5465,35 @@ var PhoenixChannelsTransport = class {
|
|
|
4477
5465
|
},
|
|
4478
5466
|
transport: options.websocketFactory ?? resolveWebSocketFactory(options.apiKey)
|
|
4479
5467
|
});
|
|
5468
|
+
this.registry = new ChannelRegistry(this.socket, this.epoch, this.logger, {
|
|
5469
|
+
wrapHandler: (topic, event, handler) => (payload) => {
|
|
5470
|
+
const reportError = (error) => {
|
|
5471
|
+
this.logger.error("Unhandled topic handler error", { topic, event, error });
|
|
5472
|
+
this.onHandlerError?.(error);
|
|
5473
|
+
};
|
|
5474
|
+
const deliver = () => {
|
|
5475
|
+
try {
|
|
5476
|
+
void Promise.resolve(handler(payload)).catch(reportError);
|
|
5477
|
+
} catch (error) {
|
|
5478
|
+
reportError(error);
|
|
5479
|
+
}
|
|
5480
|
+
};
|
|
5481
|
+
if (this.bufferingGeneration !== null && !this.bufferingExemptTopics.has(topic)) {
|
|
5482
|
+
this.bufferedTopicEvents.push({ topic, deliver });
|
|
5483
|
+
} else {
|
|
5484
|
+
deliver();
|
|
5485
|
+
}
|
|
5486
|
+
},
|
|
5487
|
+
onJoinSettled: (topic, joined) => this.generationTracker.recordSettled(topic, joined),
|
|
5488
|
+
onLeft: (topic) => {
|
|
5489
|
+
this.generationTracker.removeTopic(topic);
|
|
5490
|
+
for (let index = this.bufferedTopicEvents.length - 1; index >= 0; index -= 1) {
|
|
5491
|
+
if (this.bufferedTopicEvents[index]?.topic === topic) {
|
|
5492
|
+
this.bufferedTopicEvents.splice(index, 1);
|
|
5493
|
+
}
|
|
5494
|
+
}
|
|
5495
|
+
}
|
|
5496
|
+
});
|
|
4480
5497
|
this.socket.onOpen(() => {
|
|
4481
5498
|
void this.handleOpen();
|
|
4482
5499
|
});
|
|
@@ -4513,42 +5530,25 @@ var PhoenixChannelsTransport = class {
|
|
|
4513
5530
|
if (this.connected) {
|
|
4514
5531
|
return;
|
|
4515
5532
|
}
|
|
4516
|
-
|
|
5533
|
+
await this.connectFlight.run(() => {
|
|
4517
5534
|
this.socket.connect();
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
void pending.then(
|
|
4521
|
-
() => {
|
|
4522
|
-
if (this.connectPromise === pending) {
|
|
4523
|
-
this.connectPromise = null;
|
|
4524
|
-
}
|
|
4525
|
-
},
|
|
4526
|
-
() => {
|
|
4527
|
-
if (this.connectPromise === pending) {
|
|
4528
|
-
this.connectPromise = null;
|
|
4529
|
-
}
|
|
4530
|
-
}
|
|
4531
|
-
);
|
|
4532
|
-
}
|
|
4533
|
-
await this.connectPromise;
|
|
5535
|
+
return this.waitForConnection();
|
|
5536
|
+
});
|
|
4534
5537
|
}
|
|
4535
5538
|
async disconnect() {
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
);
|
|
5539
|
+
this.epoch.bump();
|
|
5540
|
+
const failures = await this.registry.leaveAll();
|
|
4539
5541
|
this.socket.disconnect();
|
|
4540
5542
|
this.connected = false;
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
5543
|
+
this.registry.forceTeardown();
|
|
5544
|
+
this.hasOpenedOnce = false;
|
|
5545
|
+
this.bufferingGeneration = null;
|
|
5546
|
+
this.reconnectBarrier?.resolve();
|
|
5547
|
+
this.reconnectBarrier = null;
|
|
5548
|
+
this.bufferedTopicEvents.splice(0);
|
|
5549
|
+
this.generationTracker.reset();
|
|
4547
5550
|
if (failures.length > 0) {
|
|
4548
|
-
throw
|
|
4549
|
-
failures,
|
|
4550
|
-
"Failed to leave one or more Phoenix topics during disconnect"
|
|
4551
|
-
);
|
|
5551
|
+
throw combineTeardownErrors(failures, "Failed to leave one or more Phoenix topics during disconnect");
|
|
4552
5552
|
}
|
|
4553
5553
|
}
|
|
4554
5554
|
isConnected() {
|
|
@@ -4557,79 +5557,28 @@ var PhoenixChannelsTransport = class {
|
|
|
4557
5557
|
getDisconnectReason() {
|
|
4558
5558
|
return this.lastDisconnectReason;
|
|
4559
5559
|
}
|
|
4560
|
-
async join(topic, handlers) {
|
|
4561
|
-
if (
|
|
4562
|
-
|
|
5560
|
+
async join(topic, handlers, options) {
|
|
5561
|
+
if (options?.exemptFromBuffering) {
|
|
5562
|
+
this.bufferingExemptTopics.add(topic);
|
|
4563
5563
|
}
|
|
4564
|
-
const
|
|
4565
|
-
if (
|
|
4566
|
-
return
|
|
5564
|
+
const existing = this.registry.existingJoin(topic);
|
|
5565
|
+
if (existing) {
|
|
5566
|
+
return existing;
|
|
4567
5567
|
}
|
|
4568
|
-
const
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
}
|
|
4574
|
-
async doJoin(topic, handlers) {
|
|
4575
|
-
const channel = this.socket.channel(topic, {});
|
|
4576
|
-
const refs = [];
|
|
4577
|
-
for (const [event, handler] of Object.entries(handlers)) {
|
|
4578
|
-
const ref = channel.on(event, (payload) => {
|
|
4579
|
-
Promise.resolve(handler(payload)).catch((error) => {
|
|
4580
|
-
this.logger.error("Unhandled topic handler error", {
|
|
4581
|
-
topic,
|
|
4582
|
-
event,
|
|
4583
|
-
error
|
|
4584
|
-
});
|
|
4585
|
-
this.onHandlerError?.(error);
|
|
4586
|
-
});
|
|
4587
|
-
});
|
|
4588
|
-
refs.push([event, ref]);
|
|
5568
|
+
const epoch = this.epoch.current;
|
|
5569
|
+
await this.reconnectBarrier?.promise;
|
|
5570
|
+
if (this.epoch.isStale(epoch)) {
|
|
5571
|
+
this.logger.debug("Join superseded by transport disconnect/reconnect", { topic });
|
|
5572
|
+
throw supersededJoinError(topic);
|
|
4589
5573
|
}
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
"error",
|
|
4594
|
-
(error) => reject(new TransportError(`Failed to join topic ${topic}`, error))
|
|
4595
|
-
).receive(
|
|
4596
|
-
"timeout",
|
|
4597
|
-
() => reject(new TransportError(`Timeout joining topic ${topic}`))
|
|
4598
|
-
);
|
|
4599
|
-
});
|
|
4600
|
-
} catch (error) {
|
|
4601
|
-
for (const [event, ref] of refs) {
|
|
4602
|
-
channel.off(event, ref);
|
|
4603
|
-
}
|
|
4604
|
-
channel.leave();
|
|
4605
|
-
removeSocketChannel(this.socket, channel);
|
|
4606
|
-
throw error;
|
|
5574
|
+
const resumed = this.registry.existingJoin(topic);
|
|
5575
|
+
if (resumed) {
|
|
5576
|
+
return resumed;
|
|
4607
5577
|
}
|
|
4608
|
-
this.
|
|
4609
|
-
this.channelRefs.set(topic, refs);
|
|
4610
|
-
this.logger.debug("Joined topic", { topic });
|
|
5578
|
+
return this.registry.join(topic, handlers);
|
|
4611
5579
|
}
|
|
4612
5580
|
async leave(topic) {
|
|
4613
|
-
|
|
4614
|
-
if (!channel) {
|
|
4615
|
-
return;
|
|
4616
|
-
}
|
|
4617
|
-
const refs = this.channelRefs.get(topic) ?? [];
|
|
4618
|
-
for (const [event, ref] of refs) {
|
|
4619
|
-
channel.off(event, ref);
|
|
4620
|
-
}
|
|
4621
|
-
this.channelRefs.delete(topic);
|
|
4622
|
-
await new Promise((resolve, reject) => {
|
|
4623
|
-
channel.leave().receive("ok", () => resolve()).receive(
|
|
4624
|
-
"error",
|
|
4625
|
-
(error) => reject(new TransportError(`Failed to leave topic ${topic}`, error))
|
|
4626
|
-
).receive(
|
|
4627
|
-
"timeout",
|
|
4628
|
-
() => reject(new TransportError(`Timeout leaving topic ${topic}`))
|
|
4629
|
-
);
|
|
4630
|
-
});
|
|
4631
|
-
this.channels.delete(topic);
|
|
4632
|
-
this.logger.debug("Left topic", { topic });
|
|
5581
|
+
return this.registry.leave(topic);
|
|
4633
5582
|
}
|
|
4634
5583
|
async runForever(signal) {
|
|
4635
5584
|
if (this.terminalDisconnectError) {
|
|
@@ -4667,7 +5616,21 @@ var PhoenixChannelsTransport = class {
|
|
|
4667
5616
|
);
|
|
4668
5617
|
});
|
|
4669
5618
|
}
|
|
5619
|
+
onReconnected(observer) {
|
|
5620
|
+
this.reconnectObservers.add(observer);
|
|
5621
|
+
return () => {
|
|
5622
|
+
this.reconnectObservers.delete(observer);
|
|
5623
|
+
};
|
|
5624
|
+
}
|
|
4670
5625
|
async handleOpen() {
|
|
5626
|
+
if (this.hasOpenedOnce) {
|
|
5627
|
+
this.bufferingGeneration = this.generationTracker.beginGeneration(this.registry.topics());
|
|
5628
|
+
if (!this.reconnectBarrier) {
|
|
5629
|
+
this.reconnectBarrier = createDeferred();
|
|
5630
|
+
}
|
|
5631
|
+
} else {
|
|
5632
|
+
this.hasOpenedOnce = true;
|
|
5633
|
+
}
|
|
4671
5634
|
try {
|
|
4672
5635
|
await this.subscribeAgentControl();
|
|
4673
5636
|
} catch (error) {
|
|
@@ -4689,6 +5652,40 @@ var PhoenixChannelsTransport = class {
|
|
|
4689
5652
|
channels: getSocketChannelCount(this.socket)
|
|
4690
5653
|
});
|
|
4691
5654
|
}
|
|
5655
|
+
notifyReconnectObservers(snapshot) {
|
|
5656
|
+
const epoch = this.epoch.current;
|
|
5657
|
+
const observers = [...this.reconnectObservers];
|
|
5658
|
+
void this.observerChain.run(async () => {
|
|
5659
|
+
if (this.epoch.isStale(epoch)) {
|
|
5660
|
+
return;
|
|
5661
|
+
}
|
|
5662
|
+
for (const observer of observers) {
|
|
5663
|
+
if (this.epoch.isStale(epoch)) {
|
|
5664
|
+
return;
|
|
5665
|
+
}
|
|
5666
|
+
if (!this.reconnectObservers.has(observer)) {
|
|
5667
|
+
continue;
|
|
5668
|
+
}
|
|
5669
|
+
try {
|
|
5670
|
+
await observer(snapshot);
|
|
5671
|
+
} catch (error) {
|
|
5672
|
+
this.logger.error("Reconnect observer failed", {
|
|
5673
|
+
generation: snapshot.generation,
|
|
5674
|
+
error
|
|
5675
|
+
});
|
|
5676
|
+
}
|
|
5677
|
+
}
|
|
5678
|
+
if (!this.epoch.isStale(epoch) && snapshot.generation === this.bufferingGeneration) {
|
|
5679
|
+
this.bufferingGeneration = null;
|
|
5680
|
+
const events = this.bufferedTopicEvents.splice(0);
|
|
5681
|
+
for (const { deliver } of events) {
|
|
5682
|
+
deliver();
|
|
5683
|
+
}
|
|
5684
|
+
this.reconnectBarrier?.resolve();
|
|
5685
|
+
this.reconnectBarrier = null;
|
|
5686
|
+
}
|
|
5687
|
+
});
|
|
5688
|
+
}
|
|
4692
5689
|
stopReconnectIfNoChannels(options = {}) {
|
|
4693
5690
|
if (this.stoppingReconnect || getSocketChannelCount(this.socket) !== 0) {
|
|
4694
5691
|
return;
|
|
@@ -4702,18 +5699,23 @@ var PhoenixChannelsTransport = class {
|
|
|
4702
5699
|
if (!this.agentId) {
|
|
4703
5700
|
return;
|
|
4704
5701
|
}
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
5702
|
+
const topic = (0, import_band_sdk_core6.agentControlTopic)(this.agentId);
|
|
5703
|
+
await this.join(
|
|
5704
|
+
topic,
|
|
5705
|
+
{
|
|
5706
|
+
supersede: (payload) => {
|
|
5707
|
+
const reason = parseSupersedeDisconnectReason(payload);
|
|
5708
|
+
if (!reason) {
|
|
5709
|
+
this.logger.warn("Invalid agent_control supersede payload", {
|
|
5710
|
+
payload
|
|
5711
|
+
});
|
|
5712
|
+
return;
|
|
5713
|
+
}
|
|
5714
|
+
this.recordTerminalDisconnect(reason);
|
|
4713
5715
|
}
|
|
4714
|
-
|
|
4715
|
-
}
|
|
4716
|
-
|
|
5716
|
+
},
|
|
5717
|
+
{ exemptFromBuffering: true }
|
|
5718
|
+
);
|
|
4717
5719
|
}
|
|
4718
5720
|
recordSocketClose(event) {
|
|
4719
5721
|
this.connected = false;
|
|
@@ -4789,10 +5791,6 @@ function unwrapErrorEvent(event) {
|
|
|
4789
5791
|
function isErrorEvent(event) {
|
|
4790
5792
|
return typeof event === "object" && event !== null && "error" in event;
|
|
4791
5793
|
}
|
|
4792
|
-
function removeSocketChannel(socket, channel) {
|
|
4793
|
-
const candidate = socket;
|
|
4794
|
-
candidate.remove?.(channel);
|
|
4795
|
-
}
|
|
4796
5794
|
function getSocketChannelCount(socket) {
|
|
4797
5795
|
const candidate = socket;
|
|
4798
5796
|
if (!Array.isArray(candidate.channels)) {
|
|
@@ -4803,19 +5801,412 @@ function getSocketChannelCount(socket) {
|
|
|
4803
5801
|
|
|
4804
5802
|
// src/platform/BandLink.ts
|
|
4805
5803
|
var import_rest_client = require("@band-ai/rest-client");
|
|
4806
|
-
var
|
|
5804
|
+
var import_band_sdk_core9 = require("@band-ai/band-sdk-core");
|
|
5805
|
+
|
|
5806
|
+
// src/platform/SubscriptionManager.ts
|
|
5807
|
+
var import_band_sdk_core8 = require("@band-ai/band-sdk-core");
|
|
5808
|
+
|
|
5809
|
+
// src/platform/roomTopics.ts
|
|
5810
|
+
var import_band_sdk_core7 = require("@band-ai/band-sdk-core");
|
|
5811
|
+
function roomTopics(roomId) {
|
|
5812
|
+
return { chat: (0, import_band_sdk_core7.chatRoomTopic)(roomId), participants: (0, import_band_sdk_core7.roomParticipantsTopic)(roomId) };
|
|
5813
|
+
}
|
|
5814
|
+
function isRejected(result) {
|
|
5815
|
+
return result.status === "rejected";
|
|
5816
|
+
}
|
|
5817
|
+
async function settleRoomLeaves(transport, roomId) {
|
|
5818
|
+
const { chat: chatTopic, participants: participantsTopic } = roomTopics(roomId);
|
|
5819
|
+
const results = await Promise.allSettled([
|
|
5820
|
+
transport.leave(chatTopic),
|
|
5821
|
+
transport.leave(participantsTopic)
|
|
5822
|
+
]);
|
|
5823
|
+
return results.filter(isRejected);
|
|
5824
|
+
}
|
|
5825
|
+
|
|
5826
|
+
// src/platform/ReconciliationCoordinator.ts
|
|
5827
|
+
var ReconciliationCoordinator = class {
|
|
5828
|
+
constructor(tracker, transport, logger, epoch) {
|
|
5829
|
+
this.tracker = tracker;
|
|
5830
|
+
this.transport = transport;
|
|
5831
|
+
this.logger = logger;
|
|
5832
|
+
this.epoch = epoch;
|
|
5833
|
+
}
|
|
5834
|
+
roomsNeedingReconciliation = /* @__PURE__ */ new Set();
|
|
5835
|
+
agentTopicsNeedingReconciliation = /* @__PURE__ */ new Set();
|
|
5836
|
+
reconcileTail = new Serializer();
|
|
5837
|
+
lastReconciledGeneration = 0;
|
|
5838
|
+
markRoomNeedsReconciliation(roomId) {
|
|
5839
|
+
this.roomsNeedingReconciliation.add(roomId);
|
|
5840
|
+
}
|
|
5841
|
+
markAgentTopicNeedsReconciliation(topic) {
|
|
5842
|
+
this.agentTopicsNeedingReconciliation.add(topic);
|
|
5843
|
+
}
|
|
5844
|
+
/** Called on session end: every pending sweep is moot against a tracker that has itself just been reset. */
|
|
5845
|
+
reset() {
|
|
5846
|
+
this.roomsNeedingReconciliation.clear();
|
|
5847
|
+
this.agentTopicsNeedingReconciliation.clear();
|
|
5848
|
+
this.lastReconciledGeneration = 0;
|
|
5849
|
+
}
|
|
5850
|
+
/** Serialized: each generation's reconciliation completes before the next begins. */
|
|
5851
|
+
reconcileReconnect(snapshot) {
|
|
5852
|
+
if (snapshot.generation <= this.lastReconciledGeneration) {
|
|
5853
|
+
this.logger.debug("Ignoring stale or duplicate reconnect snapshot", {
|
|
5854
|
+
generation: snapshot.generation,
|
|
5855
|
+
lastReconciledGeneration: this.lastReconciledGeneration
|
|
5856
|
+
});
|
|
5857
|
+
return Promise.resolve();
|
|
5858
|
+
}
|
|
5859
|
+
this.lastReconciledGeneration = snapshot.generation;
|
|
5860
|
+
const epoch = this.epoch.current;
|
|
5861
|
+
this.tracker.onReconnected();
|
|
5862
|
+
const work = {
|
|
5863
|
+
snapshot,
|
|
5864
|
+
epoch,
|
|
5865
|
+
roomCandidates: this.tracker.roomRejoinCandidates(),
|
|
5866
|
+
agentTopicCandidates: this.tracker.agentTopicRejoinCandidates()
|
|
5867
|
+
};
|
|
5868
|
+
return this.reconcileTail.run(() => this.runReconcile(work));
|
|
5869
|
+
}
|
|
5870
|
+
async runReconcile(work) {
|
|
5871
|
+
const { snapshot, epoch, roomCandidates, agentTopicCandidates } = work;
|
|
5872
|
+
if (this.epoch.isStale(epoch)) {
|
|
5873
|
+
this.logger.debug("Reconnect reconciliation settled after session ended, skipping rejoin evaluation", {
|
|
5874
|
+
generation: snapshot.generation
|
|
5875
|
+
});
|
|
5876
|
+
return;
|
|
5877
|
+
}
|
|
5878
|
+
for (const [roomId, ticket] of roomCandidates) {
|
|
5879
|
+
const { chat: chatTopic, participants: participantsTopic } = roomTopics(roomId);
|
|
5880
|
+
if (!snapshot.attemptedTopics.has(chatTopic) || !snapshot.attemptedTopics.has(participantsTopic)) {
|
|
5881
|
+
continue;
|
|
5882
|
+
}
|
|
5883
|
+
const present = snapshot.joinedTopics.has(chatTopic) && snapshot.joinedTopics.has(participantsTopic);
|
|
5884
|
+
if (present) {
|
|
5885
|
+
continue;
|
|
5886
|
+
}
|
|
5887
|
+
if (this.tracker.markRoomRejoinFailed(roomId, ticket)) {
|
|
5888
|
+
this.markRoomNeedsReconciliation(roomId);
|
|
5889
|
+
}
|
|
5890
|
+
}
|
|
5891
|
+
for (const [topic, ticket] of agentTopicCandidates) {
|
|
5892
|
+
if (!snapshot.attemptedTopics.has(topic)) {
|
|
5893
|
+
continue;
|
|
5894
|
+
}
|
|
5895
|
+
if (snapshot.joinedTopics.has(topic)) {
|
|
5896
|
+
continue;
|
|
5897
|
+
}
|
|
5898
|
+
if (this.tracker.markAgentTopicRejoinFailed(topic, ticket)) {
|
|
5899
|
+
this.markAgentTopicNeedsReconciliation(topic);
|
|
5900
|
+
}
|
|
5901
|
+
}
|
|
5902
|
+
if (this.epoch.isStale(epoch)) {
|
|
5903
|
+
this.logger.debug("Reconnect reconciliation settled after session ended, skipping cleanup drain", {
|
|
5904
|
+
generation: snapshot.generation
|
|
5905
|
+
});
|
|
5906
|
+
return;
|
|
5907
|
+
}
|
|
5908
|
+
await this.drainReconciliation(epoch);
|
|
5909
|
+
}
|
|
5910
|
+
async drainReconciliation(epoch) {
|
|
5911
|
+
const rooms = [...this.roomsNeedingReconciliation];
|
|
5912
|
+
const topics = [...this.agentTopicsNeedingReconciliation];
|
|
5913
|
+
const [roomLeaveResults, topicLeaveResults] = await Promise.all([
|
|
5914
|
+
Promise.allSettled(rooms.map((roomId) => this.leaveRoomTopicsCleanly(roomId))),
|
|
5915
|
+
Promise.allSettled(topics.map((topic) => this.transport.leave(topic)))
|
|
5916
|
+
]);
|
|
5917
|
+
if (this.epoch.isStale(epoch)) {
|
|
5918
|
+
this.logger.debug("Reconnect reconciliation cleanup settled after session ended, leaving tracker acknowledgements pending for next reconnect", {
|
|
5919
|
+
rooms: rooms.length,
|
|
5920
|
+
topics: topics.length
|
|
5921
|
+
});
|
|
5922
|
+
return;
|
|
5923
|
+
}
|
|
5924
|
+
this.acknowledgeCleanup({
|
|
5925
|
+
ids: rooms,
|
|
5926
|
+
results: roomLeaveResults,
|
|
5927
|
+
pending: this.roomsNeedingReconciliation,
|
|
5928
|
+
acknowledge: (roomId) => this.tracker.acknowledgeRoomReconciled(roomId),
|
|
5929
|
+
failureMessage: "Room reconciliation cleanup failed, retrying on next reconnect",
|
|
5930
|
+
logContext: (roomId) => ({ roomId })
|
|
5931
|
+
});
|
|
5932
|
+
this.acknowledgeCleanup({
|
|
5933
|
+
ids: topics,
|
|
5934
|
+
results: topicLeaveResults,
|
|
5935
|
+
pending: this.agentTopicsNeedingReconciliation,
|
|
5936
|
+
acknowledge: (topic) => this.tracker.acknowledgeAgentTopicReconciled(topic),
|
|
5937
|
+
failureMessage: "Agent topic reconciliation cleanup failed, retrying on next reconnect",
|
|
5938
|
+
logContext: (topic) => ({ topic })
|
|
5939
|
+
});
|
|
5940
|
+
}
|
|
5941
|
+
acknowledgeCleanup(options) {
|
|
5942
|
+
options.ids.forEach((id, index) => {
|
|
5943
|
+
if (options.results[index]?.status !== "fulfilled") {
|
|
5944
|
+
this.logger.warn(options.failureMessage, options.logContext(id));
|
|
5945
|
+
return;
|
|
5946
|
+
}
|
|
5947
|
+
if (options.acknowledge(id)) {
|
|
5948
|
+
options.pending.delete(id);
|
|
5949
|
+
}
|
|
5950
|
+
});
|
|
5951
|
+
}
|
|
5952
|
+
async leaveRoomTopicsCleanly(roomId) {
|
|
5953
|
+
const failures = await settleRoomLeaves(this.transport, roomId);
|
|
5954
|
+
if (failures.length > 0) {
|
|
5955
|
+
throw new AggregateError(
|
|
5956
|
+
failures.map((failure) => failure.reason),
|
|
5957
|
+
`Failed to clean up room ${roomId} during reconnect reconciliation`
|
|
5958
|
+
);
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
};
|
|
5962
|
+
|
|
5963
|
+
// src/platform/SubscriptionManager.ts
|
|
5964
|
+
function roomOperationKey(roomId) {
|
|
5965
|
+
return `room:${roomId}`;
|
|
5966
|
+
}
|
|
5967
|
+
function topicOperationKey(topic) {
|
|
5968
|
+
return `topic:${topic}`;
|
|
5969
|
+
}
|
|
5970
|
+
var SubscriptionManager = class {
|
|
5971
|
+
tracker = new import_band_sdk_core8.SubscriptionTracker();
|
|
5972
|
+
transport;
|
|
5973
|
+
logger;
|
|
5974
|
+
operations = /* @__PURE__ */ new Map();
|
|
5975
|
+
epoch = new Epoch();
|
|
5976
|
+
reconciliation;
|
|
5977
|
+
constructor(options) {
|
|
5978
|
+
this.transport = options.transport;
|
|
5979
|
+
this.logger = resolveLogger(options.logger);
|
|
5980
|
+
this.reconciliation = new ReconciliationCoordinator(
|
|
5981
|
+
this.tracker,
|
|
5982
|
+
this.transport,
|
|
5983
|
+
this.logger,
|
|
5984
|
+
this.epoch
|
|
5985
|
+
);
|
|
5986
|
+
}
|
|
5987
|
+
subscribeRoom(roomId, handlers) {
|
|
5988
|
+
return this.runOperation(
|
|
5989
|
+
roomOperationKey(roomId),
|
|
5990
|
+
"up",
|
|
5991
|
+
() => this.claimRoomSubscribe(roomId, handlers)
|
|
5992
|
+
);
|
|
5993
|
+
}
|
|
5994
|
+
unsubscribeRoom(roomId) {
|
|
5995
|
+
return this.runOperation(
|
|
5996
|
+
roomOperationKey(roomId),
|
|
5997
|
+
"down",
|
|
5998
|
+
() => this.claimRoomUnsubscribe(roomId)
|
|
5999
|
+
);
|
|
6000
|
+
}
|
|
6001
|
+
subscribeAgentTopic(topic, handlers) {
|
|
6002
|
+
return this.runOperation(
|
|
6003
|
+
topicOperationKey(topic),
|
|
6004
|
+
"up",
|
|
6005
|
+
() => this.claimAgentTopicJoin(topic, handlers)
|
|
6006
|
+
);
|
|
6007
|
+
}
|
|
6008
|
+
unsubscribeAgentTopic(topic) {
|
|
6009
|
+
return this.runOperation(
|
|
6010
|
+
topicOperationKey(topic),
|
|
6011
|
+
"down",
|
|
6012
|
+
() => this.claimAgentTopicLeave(topic)
|
|
6013
|
+
);
|
|
6014
|
+
}
|
|
6015
|
+
/** Serialized: each generation's reconciliation completes before the next begins. */
|
|
6016
|
+
reconcileReconnect(snapshot) {
|
|
6017
|
+
return this.reconciliation.reconcileReconnect(snapshot);
|
|
6018
|
+
}
|
|
6019
|
+
/**
|
|
6020
|
+
* Ends this session: every ticket issued so far goes stale, every in-flight
|
|
6021
|
+
* operation's late completion becomes a no-op, and both reconciliation sets
|
|
6022
|
+
* start empty for the next session. The epoch guards host-side async work
|
|
6023
|
+
* and transport effects that can outlive the core session they started in.
|
|
6024
|
+
*/
|
|
6025
|
+
endSession() {
|
|
6026
|
+
this.epoch.bump();
|
|
6027
|
+
this.tracker.endSession();
|
|
6028
|
+
this.operations.clear();
|
|
6029
|
+
this.reconciliation.reset();
|
|
6030
|
+
}
|
|
6031
|
+
leaveOutcome(epoch, succeeded) {
|
|
6032
|
+
return this.epoch.isStale(epoch) ? "unknown" : succeeded ? "left" : "failed";
|
|
6033
|
+
}
|
|
6034
|
+
// ---- generic operation coalescing -------------------------------------
|
|
6035
|
+
runOperation(key, kind, claim) {
|
|
6036
|
+
const existing = this.operations.get(key);
|
|
6037
|
+
if (existing?.kind === kind) {
|
|
6038
|
+
return existing.promise;
|
|
6039
|
+
}
|
|
6040
|
+
const promise = existing ? existing.promise.then(claim, claim) : claim();
|
|
6041
|
+
const operation = { kind, promise };
|
|
6042
|
+
this.operations.set(key, operation);
|
|
6043
|
+
const cleanup = () => {
|
|
6044
|
+
if (this.operations.get(key) === operation) {
|
|
6045
|
+
this.operations.delete(key);
|
|
6046
|
+
}
|
|
6047
|
+
};
|
|
6048
|
+
promise.then(cleanup, cleanup);
|
|
6049
|
+
return promise;
|
|
6050
|
+
}
|
|
6051
|
+
// ---- room subscribe/unsubscribe ----------------------------------------
|
|
6052
|
+
async claimRoomSubscribe(roomId, handlers) {
|
|
6053
|
+
const epoch = this.epoch.current;
|
|
6054
|
+
const ticket = this.tracker.beginRoomSubscribe(roomId);
|
|
6055
|
+
if (ticket === void 0) {
|
|
6056
|
+
return this.settleIdempotentRoomClaim(roomId);
|
|
6057
|
+
}
|
|
6058
|
+
const { chat: chatTopic, participants: participantsTopic } = roomTopics(roomId);
|
|
6059
|
+
try {
|
|
6060
|
+
await this.transport.join(chatTopic, handlers.chat);
|
|
6061
|
+
} catch (error) {
|
|
6062
|
+
if (!this.epoch.isStale(epoch)) {
|
|
6063
|
+
this.tracker.recordChatRoomJoinFailed(roomId, ticket);
|
|
6064
|
+
} else {
|
|
6065
|
+
this.logger.debug("Room chat-topic join settled after session ended, ignoring stale ticket", {
|
|
6066
|
+
roomId
|
|
6067
|
+
});
|
|
6068
|
+
}
|
|
6069
|
+
throw error;
|
|
6070
|
+
}
|
|
6071
|
+
try {
|
|
6072
|
+
await this.transport.join(participantsTopic, handlers.participants);
|
|
6073
|
+
} catch (participantError) {
|
|
6074
|
+
if (this.epoch.isStale(epoch)) {
|
|
6075
|
+
this.logger.debug(
|
|
6076
|
+
"Room participants-topic join settled after session ended, ignoring stale ticket",
|
|
6077
|
+
{ roomId }
|
|
6078
|
+
);
|
|
6079
|
+
throw participantError;
|
|
6080
|
+
}
|
|
6081
|
+
let chatRoomLeft = false;
|
|
6082
|
+
let rollbackError;
|
|
6083
|
+
try {
|
|
6084
|
+
await this.transport.leave(chatTopic);
|
|
6085
|
+
chatRoomLeft = true;
|
|
6086
|
+
} catch (error) {
|
|
6087
|
+
rollbackError = error;
|
|
6088
|
+
}
|
|
6089
|
+
const result = this.tracker.recordRoomParticipantsJoinFailed(roomId, ticket, chatRoomLeft);
|
|
6090
|
+
if (result === "rollback_failed") {
|
|
6091
|
+
this.reconciliation.markRoomNeedsReconciliation(roomId);
|
|
6092
|
+
throw new AggregateError(
|
|
6093
|
+
[
|
|
6094
|
+
participantError,
|
|
6095
|
+
rollbackError ?? new TransportError(`Failed to roll back chat_room join for room ${roomId}`)
|
|
6096
|
+
],
|
|
6097
|
+
`Failed to subscribe to room ${roomId} and roll back its chat_room join`
|
|
6098
|
+
);
|
|
6099
|
+
}
|
|
6100
|
+
throw participantError;
|
|
6101
|
+
}
|
|
6102
|
+
if (!this.epoch.isStale(epoch)) {
|
|
6103
|
+
this.tracker.recordBothRoomTopicsJoined(roomId, ticket);
|
|
6104
|
+
} else {
|
|
6105
|
+
this.logger.debug("Room subscribe settled after session ended, discarding stale success", {
|
|
6106
|
+
roomId
|
|
6107
|
+
});
|
|
6108
|
+
}
|
|
6109
|
+
}
|
|
6110
|
+
/** Resolves or rejects an idempotent no-op claim (the tracker reported no new ticket to act on). */
|
|
6111
|
+
settleIdempotentClaim(matched, describe) {
|
|
6112
|
+
return matched ? Promise.resolve() : Promise.reject(new RuntimeStateError(describe()));
|
|
6113
|
+
}
|
|
6114
|
+
settleIdempotentRoomClaim(roomId) {
|
|
6115
|
+
const status = this.tracker.roomStatus(roomId);
|
|
6116
|
+
return this.settleIdempotentClaim(
|
|
6117
|
+
status === "subscribed",
|
|
6118
|
+
() => `Room ${roomId} cannot be subscribed while it is "${status}"`
|
|
6119
|
+
);
|
|
6120
|
+
}
|
|
6121
|
+
async claimRoomUnsubscribe(roomId) {
|
|
6122
|
+
const epoch = this.epoch.current;
|
|
6123
|
+
const ticket = this.tracker.unsubscribeRoom(roomId);
|
|
6124
|
+
if (ticket === void 0) {
|
|
6125
|
+
return;
|
|
6126
|
+
}
|
|
6127
|
+
const failures = await settleRoomLeaves(this.transport, roomId);
|
|
6128
|
+
const outcome = this.leaveOutcome(epoch, failures.length === 0);
|
|
6129
|
+
if (outcome === "unknown") {
|
|
6130
|
+
this.logger.debug("Room unsubscribe settled after session ended, outcome ambiguous", { roomId });
|
|
6131
|
+
}
|
|
6132
|
+
if (this.tracker.markRoomLeaveComplete(roomId, ticket, outcome) && outcome !== "left") {
|
|
6133
|
+
this.reconciliation.markRoomNeedsReconciliation(roomId);
|
|
6134
|
+
}
|
|
6135
|
+
if (failures.length > 0) {
|
|
6136
|
+
throw new AggregateError(
|
|
6137
|
+
failures.map((failure) => failure.reason),
|
|
6138
|
+
`Failed to fully unsubscribe from room ${roomId}`
|
|
6139
|
+
);
|
|
6140
|
+
}
|
|
6141
|
+
}
|
|
6142
|
+
// ---- agent topic join/leave (agent_rooms, agent_contacts) --------------
|
|
6143
|
+
async claimAgentTopicJoin(topic, handlers) {
|
|
6144
|
+
const epoch = this.epoch.current;
|
|
6145
|
+
const ticket = this.tracker.beginAgentTopicJoin(topic);
|
|
6146
|
+
if (ticket === void 0) {
|
|
6147
|
+
return this.settleIdempotentTopicClaim(topic);
|
|
6148
|
+
}
|
|
6149
|
+
let joined = false;
|
|
6150
|
+
let joinError;
|
|
6151
|
+
try {
|
|
6152
|
+
await this.transport.join(topic, handlers);
|
|
6153
|
+
joined = true;
|
|
6154
|
+
} catch (error) {
|
|
6155
|
+
joinError = error;
|
|
6156
|
+
}
|
|
6157
|
+
if (this.epoch.isStale(epoch)) {
|
|
6158
|
+
this.tracker.recordAgentTopicJoinAmbiguous(topic, ticket);
|
|
6159
|
+
this.logger.debug("Agent topic join settled after session ended, marking ambiguous for reconciliation", {
|
|
6160
|
+
topic
|
|
6161
|
+
});
|
|
6162
|
+
} else {
|
|
6163
|
+
this.tracker.recordAgentTopicJoin(topic, ticket, joined);
|
|
6164
|
+
}
|
|
6165
|
+
if (!joined) {
|
|
6166
|
+
throw joinError;
|
|
6167
|
+
}
|
|
6168
|
+
}
|
|
6169
|
+
settleIdempotentTopicClaim(topic) {
|
|
6170
|
+
const status = this.tracker.agentTopicStatus(topic);
|
|
6171
|
+
return this.settleIdempotentClaim(
|
|
6172
|
+
status === "joined",
|
|
6173
|
+
() => `Topic ${topic} cannot be joined while it is "${status}"`
|
|
6174
|
+
);
|
|
6175
|
+
}
|
|
6176
|
+
async claimAgentTopicLeave(topic) {
|
|
6177
|
+
const epoch = this.epoch.current;
|
|
6178
|
+
const ticket = this.tracker.leaveAgentTopic(topic);
|
|
6179
|
+
if (ticket === void 0) {
|
|
6180
|
+
return;
|
|
6181
|
+
}
|
|
6182
|
+
let left = true;
|
|
6183
|
+
let leaveError;
|
|
6184
|
+
try {
|
|
6185
|
+
await this.transport.leave(topic);
|
|
6186
|
+
} catch (error) {
|
|
6187
|
+
left = false;
|
|
6188
|
+
leaveError = error;
|
|
6189
|
+
}
|
|
6190
|
+
const outcome = this.leaveOutcome(epoch, left);
|
|
6191
|
+
if (outcome === "unknown") {
|
|
6192
|
+
this.logger.debug("Agent topic unsubscribe settled after session ended, outcome ambiguous", { topic });
|
|
6193
|
+
}
|
|
6194
|
+
if (this.tracker.markAgentTopicLeaveComplete(topic, ticket, outcome) && outcome !== "left") {
|
|
6195
|
+
this.reconciliation.markAgentTopicNeedsReconciliation(topic);
|
|
6196
|
+
}
|
|
6197
|
+
if (!left) {
|
|
6198
|
+
throw leaveError;
|
|
6199
|
+
}
|
|
6200
|
+
}
|
|
6201
|
+
};
|
|
6202
|
+
|
|
6203
|
+
// src/platform/BandLink.ts
|
|
4807
6204
|
var DEFAULT_WS_URL = "wss://app.band.ai/api/v1/socket";
|
|
4808
6205
|
function deriveDefaultRestUrl(wsUrl) {
|
|
4809
6206
|
const parsed = new URL(wsUrl);
|
|
4810
6207
|
const protocol = parsed.protocol === "ws:" ? "http:" : "https:";
|
|
4811
6208
|
return `${protocol}//${parsed.host}`;
|
|
4812
6209
|
}
|
|
4813
|
-
function roomTopics(roomId) {
|
|
4814
|
-
return {
|
|
4815
|
-
chat: (0, import_band_sdk_core5.chatRoomTopic)(roomId),
|
|
4816
|
-
participants: (0, import_band_sdk_core5.roomParticipantsTopic)(roomId)
|
|
4817
|
-
};
|
|
4818
|
-
}
|
|
4819
6210
|
function toPlatformMessage(roomId, message) {
|
|
4820
6211
|
return {
|
|
4821
6212
|
id: message.id,
|
|
@@ -4829,6 +6220,19 @@ function toPlatformMessage(roomId, message) {
|
|
|
4829
6220
|
createdAt: new Date(message.inserted_at)
|
|
4830
6221
|
};
|
|
4831
6222
|
}
|
|
6223
|
+
function validationErrorDetails(error) {
|
|
6224
|
+
if (typeof error !== "object" || error === null) {
|
|
6225
|
+
return {};
|
|
6226
|
+
}
|
|
6227
|
+
const details = {};
|
|
6228
|
+
if ("issues" in error) {
|
|
6229
|
+
details.issues = error.issues;
|
|
6230
|
+
}
|
|
6231
|
+
if ("traceContext" in error) {
|
|
6232
|
+
details.traceContext = error.traceContext;
|
|
6233
|
+
}
|
|
6234
|
+
return details;
|
|
6235
|
+
}
|
|
4832
6236
|
var BandLink = class {
|
|
4833
6237
|
agentId;
|
|
4834
6238
|
apiKey;
|
|
@@ -4838,18 +6242,21 @@ var BandLink = class {
|
|
|
4838
6242
|
capabilities;
|
|
4839
6243
|
logger;
|
|
4840
6244
|
transport;
|
|
4841
|
-
|
|
6245
|
+
subscriptionManager;
|
|
4842
6246
|
eventQueue = [];
|
|
4843
6247
|
waiters = [];
|
|
4844
6248
|
connected = false;
|
|
4845
6249
|
lastDisconnectReason = null;
|
|
4846
6250
|
terminalDisconnectError = null;
|
|
6251
|
+
connectFlight = new SingleFlight2();
|
|
6252
|
+
disconnectFlight = new SingleFlight2();
|
|
6253
|
+
session = new Session();
|
|
4847
6254
|
constructor(options) {
|
|
4848
6255
|
this.agentId = options.agentId;
|
|
4849
6256
|
this.apiKey = options.apiKey;
|
|
4850
6257
|
this.wsUrl = options.wsUrl ?? DEFAULT_WS_URL;
|
|
4851
6258
|
this.restUrl = options.restUrl ?? deriveDefaultRestUrl(this.wsUrl);
|
|
4852
|
-
this.logger = options.logger
|
|
6259
|
+
this.logger = resolveLogger(options.logger);
|
|
4853
6260
|
this.capabilities = {
|
|
4854
6261
|
...DEFAULT_AGENT_TOOLS_CAPABILITIES,
|
|
4855
6262
|
...options.capabilities
|
|
@@ -4871,6 +6278,10 @@ var BandLink = class {
|
|
|
4871
6278
|
this.recordDisconnectReason(reason);
|
|
4872
6279
|
}
|
|
4873
6280
|
});
|
|
6281
|
+
this.subscriptionManager = new SubscriptionManager({
|
|
6282
|
+
transport: this.transport,
|
|
6283
|
+
logger: this.logger
|
|
6284
|
+
});
|
|
4874
6285
|
}
|
|
4875
6286
|
isConnected() {
|
|
4876
6287
|
return this.connected;
|
|
@@ -4879,12 +6290,35 @@ var BandLink = class {
|
|
|
4879
6290
|
return this.lastDisconnectReason;
|
|
4880
6291
|
}
|
|
4881
6292
|
async connect() {
|
|
6293
|
+
if (this.disconnectFlight.current) {
|
|
6294
|
+
await this.disconnectFlight.current.catch(() => void 0);
|
|
6295
|
+
}
|
|
4882
6296
|
if (this.connected) {
|
|
4883
6297
|
return;
|
|
4884
6298
|
}
|
|
6299
|
+
await this.connectFlight.run(() => {
|
|
6300
|
+
const epoch = this.session.begin();
|
|
6301
|
+
return this.connectSession(epoch);
|
|
6302
|
+
});
|
|
6303
|
+
}
|
|
6304
|
+
async connectSession(epoch) {
|
|
6305
|
+
this.session.reconnectObserverTeardown = this.transport.onReconnected?.(async (snapshot) => {
|
|
6306
|
+
await this.subscriptionManager.reconcileReconnect(snapshot);
|
|
6307
|
+
if (this.session.isStale(epoch)) {
|
|
6308
|
+
this.logger.debug(
|
|
6309
|
+
"Reconnect reconciliation settled after session ended, discarding reconnected event"
|
|
6310
|
+
);
|
|
6311
|
+
return;
|
|
6312
|
+
}
|
|
6313
|
+
this.queueEvent({ type: "reconnected", roomId: null, payload: {} });
|
|
6314
|
+
}) ?? null;
|
|
4885
6315
|
try {
|
|
4886
6316
|
await this.transport.connect();
|
|
4887
6317
|
} catch (error) {
|
|
6318
|
+
this.session.clearReconnectObserver();
|
|
6319
|
+
this.session.deactivate();
|
|
6320
|
+
await this.transport.disconnect().catch(() => void 0);
|
|
6321
|
+
this.subscriptionManager.endSession();
|
|
4888
6322
|
if (error instanceof WebSocketDisconnectError) {
|
|
4889
6323
|
if (error.reason.retryable) {
|
|
4890
6324
|
this.lastDisconnectReason = error.reason;
|
|
@@ -4897,14 +6331,21 @@ var BandLink = class {
|
|
|
4897
6331
|
this.connected = true;
|
|
4898
6332
|
}
|
|
4899
6333
|
async disconnect() {
|
|
4900
|
-
|
|
6334
|
+
await this.disconnectFlight.run(() => this.disconnectSession());
|
|
6335
|
+
}
|
|
6336
|
+
async disconnectSession() {
|
|
6337
|
+
await this.connectFlight.current?.catch(() => void 0);
|
|
6338
|
+
if (!this.session.isActive) {
|
|
4901
6339
|
return;
|
|
4902
6340
|
}
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
6341
|
+
this.session.deactivate();
|
|
6342
|
+
try {
|
|
6343
|
+
await this.transport.disconnect();
|
|
6344
|
+
} finally {
|
|
6345
|
+
this.connected = false;
|
|
6346
|
+
this.session.clearReconnectObserver();
|
|
6347
|
+
this.subscriptionManager.endSession();
|
|
6348
|
+
}
|
|
4908
6349
|
}
|
|
4909
6350
|
async runForever(signal) {
|
|
4910
6351
|
if (this.terminalDisconnectError) {
|
|
@@ -4933,7 +6374,7 @@ var BandLink = class {
|
|
|
4933
6374
|
this.eventQueue.push(event);
|
|
4934
6375
|
}
|
|
4935
6376
|
async subscribeAgentRooms() {
|
|
4936
|
-
await this.
|
|
6377
|
+
await this.subscriptionManager.subscribeAgentTopic((0, import_band_sdk_core9.agentRoomsTopic)(this.agentId), {
|
|
4937
6378
|
room_added: (payload) => {
|
|
4938
6379
|
const roomId = typeof payload.id === "string" ? payload.id : "";
|
|
4939
6380
|
this.emit("room_added", payload, roomId);
|
|
@@ -4945,30 +6386,13 @@ var BandLink = class {
|
|
|
4945
6386
|
});
|
|
4946
6387
|
}
|
|
4947
6388
|
async subscribeRoom(roomId) {
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
if (!this.subscribedRooms.has(roomId)) {
|
|
4956
|
-
return;
|
|
4957
|
-
}
|
|
4958
|
-
const topics = roomTopics(roomId);
|
|
4959
|
-
await this.transport.leave(topics.chat);
|
|
4960
|
-
await this.transport.leave(topics.participants);
|
|
4961
|
-
this.subscribedRooms.delete(roomId);
|
|
4962
|
-
}
|
|
4963
|
-
async joinRoomTopics(roomId) {
|
|
4964
|
-
const topics = roomTopics(roomId);
|
|
4965
|
-
await this.transport.join(topics.chat, {
|
|
4966
|
-
message_created: (payload) => {
|
|
4967
|
-
this.emit("message_created", payload, roomId);
|
|
4968
|
-
}
|
|
4969
|
-
});
|
|
4970
|
-
try {
|
|
4971
|
-
await this.transport.join(topics.participants, {
|
|
6389
|
+
await this.subscriptionManager.subscribeRoom(roomId, {
|
|
6390
|
+
chat: {
|
|
6391
|
+
message_created: (payload) => {
|
|
6392
|
+
this.emit("message_created", payload, roomId);
|
|
6393
|
+
}
|
|
6394
|
+
},
|
|
6395
|
+
participants: {
|
|
4972
6396
|
participant_added: (payload) => {
|
|
4973
6397
|
this.emit("participant_added", payload, roomId);
|
|
4974
6398
|
},
|
|
@@ -4978,15 +6402,15 @@ var BandLink = class {
|
|
|
4978
6402
|
room_deleted: (payload) => {
|
|
4979
6403
|
this.emit("room_deleted", payload, roomId);
|
|
4980
6404
|
}
|
|
4981
|
-
}
|
|
4982
|
-
}
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
6405
|
+
}
|
|
6406
|
+
});
|
|
6407
|
+
}
|
|
6408
|
+
async unsubscribeRoom(roomId) {
|
|
6409
|
+
await this.subscriptionManager.unsubscribeRoom(roomId);
|
|
4986
6410
|
}
|
|
4987
6411
|
async subscribeAgentContacts() {
|
|
4988
6412
|
assertCapability(this.capabilities, "contacts", "Contacts streaming");
|
|
4989
|
-
await this.
|
|
6413
|
+
await this.subscriptionManager.subscribeAgentTopic((0, import_band_sdk_core9.agentContactsTopic)(this.agentId), {
|
|
4990
6414
|
contact_request_received: (payload) => {
|
|
4991
6415
|
this.emit("contact_request_received", payload, null);
|
|
4992
6416
|
},
|
|
@@ -5002,7 +6426,7 @@ var BandLink = class {
|
|
|
5002
6426
|
});
|
|
5003
6427
|
}
|
|
5004
6428
|
async unsubscribeAgentContacts() {
|
|
5005
|
-
await this.
|
|
6429
|
+
await this.subscriptionManager.unsubscribeAgentTopic((0, import_band_sdk_core9.agentContactsTopic)(this.agentId));
|
|
5006
6430
|
}
|
|
5007
6431
|
async nextEvent(signal) {
|
|
5008
6432
|
if (this.terminalDisconnectError) {
|
|
@@ -5158,21 +6582,20 @@ var BandLink = class {
|
|
|
5158
6582
|
});
|
|
5159
6583
|
}
|
|
5160
6584
|
emit(eventType, payload, roomId) {
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
6585
|
+
try {
|
|
6586
|
+
const normalizedPayload = (0, import_band_sdk_core9.validateEventPayload)(eventType, payload);
|
|
6587
|
+
this.queueEvent({
|
|
6588
|
+
type: eventType,
|
|
6589
|
+
roomId,
|
|
6590
|
+
payload: normalizedPayload,
|
|
6591
|
+
raw: payload
|
|
6592
|
+
});
|
|
6593
|
+
} catch (error) {
|
|
5164
6594
|
this.logger.warn(`Invalid ${eventType} payload, dropping event`, {
|
|
5165
|
-
error
|
|
6595
|
+
...validationErrorDetails(error),
|
|
5166
6596
|
roomId
|
|
5167
6597
|
});
|
|
5168
|
-
return;
|
|
5169
6598
|
}
|
|
5170
|
-
this.queueEvent({
|
|
5171
|
-
type: eventType,
|
|
5172
|
-
roomId,
|
|
5173
|
-
payload: parsed.data,
|
|
5174
|
-
raw: payload
|
|
5175
|
-
});
|
|
5176
6599
|
}
|
|
5177
6600
|
};
|
|
5178
6601
|
|
|
@@ -5230,6 +6653,7 @@ var DefaultPreprocessor = class {
|
|
|
5230
6653
|
// src/runtime/PlatformRuntime.ts
|
|
5231
6654
|
var MAX_MESSAGE_RETRIES = 4294967295;
|
|
5232
6655
|
var isValidRetryCount = (value) => Number.isInteger(value) && value >= 0 && value <= MAX_MESSAGE_RETRIES;
|
|
6656
|
+
var START_CLEANUP_TRIGGER = "start-failed";
|
|
5233
6657
|
var PlatformRuntime = class {
|
|
5234
6658
|
_agentId;
|
|
5235
6659
|
_apiKey;
|
|
@@ -5246,12 +6670,14 @@ var PlatformRuntime = class {
|
|
|
5246
6670
|
_onParticipantRemoved;
|
|
5247
6671
|
_roomFilter;
|
|
5248
6672
|
_contextFactory;
|
|
6673
|
+
lifecycle;
|
|
5249
6674
|
linkInstance;
|
|
5250
6675
|
initPromise = null;
|
|
5251
6676
|
runtime;
|
|
5252
6677
|
contactHandler;
|
|
5253
6678
|
activeAdapter;
|
|
5254
|
-
|
|
6679
|
+
startGate = new SingleFlight();
|
|
6680
|
+
stopGate = new SingleFlight();
|
|
5255
6681
|
_agentName = "";
|
|
5256
6682
|
_agentDescription = "";
|
|
5257
6683
|
contactsSubscribed = false;
|
|
@@ -5277,17 +6703,30 @@ var PlatformRuntime = class {
|
|
|
5277
6703
|
this._wsUrl = options.wsUrl;
|
|
5278
6704
|
this._restUrl = options.restUrl;
|
|
5279
6705
|
this.linkInstance = options.link;
|
|
5280
|
-
this.linkOptions =
|
|
6706
|
+
this.linkOptions = {
|
|
6707
|
+
...options.linkOptions,
|
|
6708
|
+
logger: options.logger ?? options.linkOptions?.logger
|
|
6709
|
+
};
|
|
5281
6710
|
this.preprocessor = options.preprocessor ?? new DefaultPreprocessor();
|
|
5282
6711
|
this.sessionConfig = options.sessionConfig;
|
|
5283
6712
|
this.contactConfig = options.contactConfig;
|
|
5284
6713
|
this.agentConfig = options.agentConfig;
|
|
5285
|
-
this.logger = options.logger
|
|
6714
|
+
this.logger = resolveLogger(options.logger);
|
|
5286
6715
|
this.configuredIdentity = options.identity;
|
|
5287
6716
|
this._onParticipantAdded = options.onParticipantAdded;
|
|
5288
6717
|
this._onParticipantRemoved = options.onParticipantRemoved;
|
|
5289
6718
|
this._roomFilter = options.roomFilter;
|
|
5290
6719
|
this._contextFactory = options.contextFactory;
|
|
6720
|
+
this.lifecycle = new LifecycleTracker({ status: "not_started" }, {
|
|
6721
|
+
owner: "PlatformRuntime",
|
|
6722
|
+
logContext: { agentId: this._agentId },
|
|
6723
|
+
logger: this.logger,
|
|
6724
|
+
isLegalTransition: isLegalRuntimeTransition
|
|
6725
|
+
});
|
|
6726
|
+
}
|
|
6727
|
+
/** Current lifecycle state of this runtime. */
|
|
6728
|
+
get state() {
|
|
6729
|
+
return this.lifecycle.state;
|
|
5291
6730
|
}
|
|
5292
6731
|
get link() {
|
|
5293
6732
|
if (!this.linkInstance) {
|
|
@@ -5307,6 +6746,14 @@ var PlatformRuntime = class {
|
|
|
5307
6746
|
get contactConfiguration() {
|
|
5308
6747
|
return this.contactConfig;
|
|
5309
6748
|
}
|
|
6749
|
+
/**
|
|
6750
|
+
* Whether the contacts channel subscription is currently held.
|
|
6751
|
+
*
|
|
6752
|
+
* @deprecated Read {@link PlatformRuntime.state} for lifecycle questions: this
|
|
6753
|
+
* flag is only ever `true` while the runtime is running and is cleared on
|
|
6754
|
+
* teardown, so it conflates "contacts are subscribed" with "the runtime is
|
|
6755
|
+
* alive".
|
|
6756
|
+
*/
|
|
5310
6757
|
get isContactsSubscribed() {
|
|
5311
6758
|
return this.contactsSubscribed;
|
|
5312
6759
|
}
|
|
@@ -5329,8 +6776,7 @@ var PlatformRuntime = class {
|
|
|
5329
6776
|
agentId: this._agentId,
|
|
5330
6777
|
apiKey: this._apiKey,
|
|
5331
6778
|
wsUrl: this._wsUrl,
|
|
5332
|
-
restUrl: this._restUrl
|
|
5333
|
-
logger: this.logger
|
|
6779
|
+
restUrl: this._restUrl
|
|
5334
6780
|
});
|
|
5335
6781
|
}
|
|
5336
6782
|
if (this.configuredIdentity) {
|
|
@@ -5342,11 +6788,49 @@ var PlatformRuntime = class {
|
|
|
5342
6788
|
this._agentName = me.name;
|
|
5343
6789
|
this._agentDescription = me.description ?? "";
|
|
5344
6790
|
}
|
|
6791
|
+
/**
|
|
6792
|
+
* Connect the adapter to the platform.
|
|
6793
|
+
*
|
|
6794
|
+
* Repeated or concurrent calls join the in-flight start. Calling `start()`
|
|
6795
|
+
* while a `stop()` is still in flight rejects with a {@link RuntimeStateError}.
|
|
6796
|
+
* A successful start also re-arms teardown, so a runtime whose previous
|
|
6797
|
+
* `stop()` failed can be shut down properly on the next attempt.
|
|
6798
|
+
*/
|
|
5345
6799
|
async start(adapter) {
|
|
6800
|
+
await startWithGate({
|
|
6801
|
+
lifecycle: this.lifecycle,
|
|
6802
|
+
startGate: this.startGate,
|
|
6803
|
+
stopGate: this.stopGate,
|
|
6804
|
+
ownerName: "PlatformRuntime",
|
|
6805
|
+
runStart: () => this.runStart(adapter)
|
|
6806
|
+
});
|
|
6807
|
+
}
|
|
6808
|
+
async runStart(adapter) {
|
|
6809
|
+
const startState = this.lifecycle.state;
|
|
6810
|
+
try {
|
|
6811
|
+
await this.doStart(adapter, startState);
|
|
6812
|
+
} catch (error) {
|
|
6813
|
+
if (this.lifecycle.isCurrent(startState)) {
|
|
6814
|
+
this.lifecycle.fail(error, START_CLEANUP_TRIGGER);
|
|
6815
|
+
}
|
|
6816
|
+
throw error;
|
|
6817
|
+
}
|
|
6818
|
+
if (this.lifecycle.isCurrent(startState)) {
|
|
6819
|
+
this.lifecycle.transition({ status: "running" }, "started");
|
|
6820
|
+
}
|
|
6821
|
+
}
|
|
6822
|
+
async doStart(adapter, startState) {
|
|
6823
|
+
const assertNotSuperseded = () => {
|
|
6824
|
+
if (!this.lifecycle.isCurrent(startState)) {
|
|
6825
|
+
throw new RuntimeStateError("PlatformRuntime start was superseded by stop()");
|
|
6826
|
+
}
|
|
6827
|
+
};
|
|
5346
6828
|
await this.initialize();
|
|
5347
|
-
|
|
6829
|
+
assertNotSuperseded();
|
|
5348
6830
|
this.activeAdapter = adapter;
|
|
5349
6831
|
try {
|
|
6832
|
+
await adapter.onStarted(this._agentName, this._agentDescription);
|
|
6833
|
+
assertNotSuperseded();
|
|
5350
6834
|
this.contactHandler = new ContactEventHandler({
|
|
5351
6835
|
config: this.contactConfig ?? { strategy: "disabled" },
|
|
5352
6836
|
rest: this.link.rest,
|
|
@@ -5360,7 +6844,18 @@ var PlatformRuntime = class {
|
|
|
5360
6844
|
onHubEvent: async (roomId, event) => {
|
|
5361
6845
|
const runtime = this.runtime;
|
|
5362
6846
|
if (!runtime) return;
|
|
5363
|
-
|
|
6847
|
+
try {
|
|
6848
|
+
await runtime.enqueueEvent(roomId, event);
|
|
6849
|
+
} catch (error) {
|
|
6850
|
+
if (!(error instanceof RuntimeStateError)) {
|
|
6851
|
+
throw error;
|
|
6852
|
+
}
|
|
6853
|
+
this.logger.error("Dropped contact hub event for a stopped room execution", {
|
|
6854
|
+
roomId,
|
|
6855
|
+
eventType: event.type,
|
|
6856
|
+
error: error.message
|
|
6857
|
+
});
|
|
6858
|
+
}
|
|
5364
6859
|
},
|
|
5365
6860
|
onHubInit: async (roomId, systemPrompt) => {
|
|
5366
6861
|
const runtime = this.runtime;
|
|
@@ -5383,10 +6878,14 @@ var PlatformRuntime = class {
|
|
|
5383
6878
|
contextFactory: this._contextFactory
|
|
5384
6879
|
});
|
|
5385
6880
|
await this.runtime.start();
|
|
6881
|
+
assertNotSuperseded();
|
|
5386
6882
|
this.contactsSubscribed = Boolean(this.link.capabilities.contacts);
|
|
5387
6883
|
} catch (error) {
|
|
6884
|
+
if (!this.lifecycle.isCurrent(startState)) {
|
|
6885
|
+
throw error;
|
|
6886
|
+
}
|
|
5388
6887
|
try {
|
|
5389
|
-
await this.
|
|
6888
|
+
await this.beginStop(void 0, START_CLEANUP_TRIGGER);
|
|
5390
6889
|
} catch (stopError) {
|
|
5391
6890
|
throw new AggregateError(
|
|
5392
6891
|
[error, stopError],
|
|
@@ -5396,49 +6895,63 @@ var PlatformRuntime = class {
|
|
|
5396
6895
|
throw error;
|
|
5397
6896
|
}
|
|
5398
6897
|
}
|
|
6898
|
+
/**
|
|
6899
|
+
* Tear the runtime down and release the platform connection.
|
|
6900
|
+
*
|
|
6901
|
+
* A concurrent second call joins the in-flight teardown and mirrors its
|
|
6902
|
+
* outcome — including rejecting with the *same* `Error` instance — rather than
|
|
6903
|
+
* reporting a graceful shutdown it did not perform. A failed teardown does not
|
|
6904
|
+
* disable future ones: a subsequent `start()` re-arms `stop()`.
|
|
6905
|
+
*/
|
|
5399
6906
|
async stop(timeoutMs) {
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
6907
|
+
return await this.beginStop(timeoutMs, "stop");
|
|
6908
|
+
}
|
|
6909
|
+
async beginStop(timeoutMs, trigger) {
|
|
6910
|
+
return await this.stopGate.run(() => this.runStop(timeoutMs, trigger));
|
|
6911
|
+
}
|
|
6912
|
+
async runStop(timeoutMs, trigger) {
|
|
6913
|
+
this.startGate.reset();
|
|
5403
6914
|
const runtime = this.runtime;
|
|
5404
6915
|
const adapter = this.activeAdapter;
|
|
5405
6916
|
if (!runtime && !adapter) {
|
|
6917
|
+
if (this.lifecycle.is("failed")) {
|
|
6918
|
+
this.logger.debug("PlatformRuntime stop is resurfacing the recorded start failure", { error: this.lifecycle.state.error });
|
|
6919
|
+
throw this.lifecycle.state.error;
|
|
6920
|
+
}
|
|
6921
|
+
if (this.lifecycle.is("starting")) {
|
|
6922
|
+
this.lifecycle.transition({ status: "stopped" }, trigger);
|
|
6923
|
+
}
|
|
5406
6924
|
return true;
|
|
5407
6925
|
}
|
|
5408
|
-
this.stopping
|
|
6926
|
+
this.lifecycle.transition({ status: "stopping" }, trigger);
|
|
5409
6927
|
this.runtime = void 0;
|
|
5410
6928
|
this.contactHandler = void 0;
|
|
5411
6929
|
this.contactsSubscribed = false;
|
|
5412
6930
|
this.activeAdapter = void 0;
|
|
5413
6931
|
let graceful = true;
|
|
5414
|
-
|
|
6932
|
+
const errors = [];
|
|
5415
6933
|
if (runtime) {
|
|
5416
|
-
|
|
6934
|
+
await isolateTeardown(errors, async () => {
|
|
5417
6935
|
graceful = await runtime.stop(timeoutMs);
|
|
5418
|
-
}
|
|
5419
|
-
runtimeError = error;
|
|
5420
|
-
}
|
|
5421
|
-
}
|
|
5422
|
-
try {
|
|
5423
|
-
await adapter?.onRuntimeStop?.();
|
|
5424
|
-
} catch (error) {
|
|
5425
|
-
if (runtimeError) {
|
|
5426
|
-
throw new AggregateError(
|
|
5427
|
-
[runtimeError, error],
|
|
5428
|
-
"PlatformRuntime stop failed and adapter cleanup also failed"
|
|
5429
|
-
);
|
|
5430
|
-
}
|
|
5431
|
-
throw error;
|
|
6936
|
+
});
|
|
5432
6937
|
}
|
|
5433
|
-
|
|
5434
|
-
|
|
6938
|
+
await isolateTeardown(errors, () => Promise.resolve(adapter?.onRuntimeStop?.()));
|
|
6939
|
+
if (errors.length > 0) {
|
|
6940
|
+
throw this.recordStopFailure(
|
|
6941
|
+
combineTeardownErrors(errors, "PlatformRuntime stop failed and adapter cleanup also failed")
|
|
6942
|
+
);
|
|
5435
6943
|
}
|
|
5436
|
-
this.
|
|
6944
|
+
this.lifecycle.transition({ status: "stopped" }, "stopped");
|
|
5437
6945
|
return graceful;
|
|
5438
6946
|
}
|
|
5439
6947
|
async [Symbol.asyncDispose]() {
|
|
5440
6948
|
await this.stop();
|
|
5441
6949
|
}
|
|
6950
|
+
recordStopFailure(error) {
|
|
6951
|
+
const failure = toLifecycleError(error);
|
|
6952
|
+
this.lifecycle.fail(failure, "stop-failed");
|
|
6953
|
+
return failure;
|
|
6954
|
+
}
|
|
5442
6955
|
async runForever() {
|
|
5443
6956
|
if (!this.runtime) {
|
|
5444
6957
|
throw new RuntimeStateError("Runtime not started");
|
|
@@ -5490,7 +7003,7 @@ var PlatformRuntime = class {
|
|
|
5490
7003
|
};
|
|
5491
7004
|
|
|
5492
7005
|
// src/runtime/index.ts
|
|
5493
|
-
var
|
|
7006
|
+
var import_band_sdk_core10 = require("@band-ai/band-sdk-core");
|
|
5494
7007
|
|
|
5495
7008
|
// src/runtime/prompts/base.ts
|
|
5496
7009
|
var ENVIRONMENT_SECTION = `
|
|
@@ -5590,6 +7103,7 @@ function longTermPattern(label, type, segment, scope) {
|
|
|
5590
7103
|
};
|
|
5591
7104
|
}
|
|
5592
7105
|
var COMMON_MEMORY_PATTERNS = [
|
|
7106
|
+
longTermPattern("Private notes for this agent only", MEMORY_TYPE.semantic, MEMORY_SEGMENT.guideline, MEMORY_STORE_SCOPE.agent),
|
|
5593
7107
|
longTermPattern("Facts learned about a specific agent/entity", MEMORY_TYPE.semantic, MEMORY_SEGMENT.agent, MEMORY_STORE_SCOPE.subject),
|
|
5594
7108
|
longTermPattern("Events involving a specific person/agent", MEMORY_TYPE.episodic, MEMORY_SEGMENT.agent, MEMORY_STORE_SCOPE.subject),
|
|
5595
7109
|
longTermPattern("A user's preferences or profile info", MEMORY_TYPE.semantic, MEMORY_SEGMENT.user, MEMORY_STORE_SCOPE.subject),
|
|
@@ -5602,7 +7116,9 @@ var MEMORY_COMMON_PATTERNS = [
|
|
|
5602
7116
|
"Common patterns:",
|
|
5603
7117
|
...COMMON_MEMORY_PATTERNS.map(renderMemoryPattern)
|
|
5604
7118
|
].join("\n");
|
|
5605
|
-
var MEMORY_SCOPE_GUIDANCE = `
|
|
7119
|
+
var MEMORY_SCOPE_GUIDANCE = `Use \`scope="${MEMORY_STORE_SCOPE.agent}"\` for information private to this agent that is not about an
|
|
7120
|
+
identified subject. Omitting \`scope\` and \`subject_id\` on store defaults to agent scope on the platform.
|
|
7121
|
+
Prefer \`scope="${MEMORY_STORE_SCOPE.subject}"\` whenever the memory is about a specific person or agent, so it
|
|
5606
7122
|
stays attached to that subject rather than leaking org-wide. Storing with \`scope="${MEMORY_STORE_SCOPE.subject}"\` requires a
|
|
5607
7123
|
real \`subject_id\` UUID, so resolve it first via \`band_lookup_peers\` or the participant list.
|
|
5608
7124
|
Reserve \`scope="${MEMORY_STORE_SCOPE.organization}"\` for knowledge that is genuinely shared across the whole organization and
|