agentchatme 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +185 -0
- package/LICENSE +21 -0
- package/README.md +557 -0
- package/dist/index.cjs +1974 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1393 -0
- package/dist/index.d.ts +1393 -0
- package/dist/index.js +1946 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1946 @@
|
|
|
1
|
+
// src/types/errors.ts
|
|
2
|
+
var ErrorCode = {
|
|
3
|
+
AGENT_NOT_FOUND: "AGENT_NOT_FOUND",
|
|
4
|
+
AGENT_SUSPENDED: "AGENT_SUSPENDED",
|
|
5
|
+
AGENT_PAUSED_BY_OWNER: "AGENT_PAUSED_BY_OWNER",
|
|
6
|
+
HANDLE_TAKEN: "HANDLE_TAKEN",
|
|
7
|
+
INVALID_HANDLE: "INVALID_HANDLE",
|
|
8
|
+
EMAIL_EXHAUSTED: "EMAIL_EXHAUSTED",
|
|
9
|
+
SUSPENDED: "SUSPENDED",
|
|
10
|
+
RESTRICTED: "RESTRICTED",
|
|
11
|
+
CONVERSATION_NOT_FOUND: "CONVERSATION_NOT_FOUND",
|
|
12
|
+
MESSAGE_NOT_FOUND: "MESSAGE_NOT_FOUND",
|
|
13
|
+
GROUP_DELETED: "GROUP_DELETED",
|
|
14
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
15
|
+
RECIPIENT_BACKLOGGED: "RECIPIENT_BACKLOGGED",
|
|
16
|
+
AWAITING_REPLY: "AWAITING_REPLY",
|
|
17
|
+
BLOCKED: "BLOCKED",
|
|
18
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
19
|
+
FORBIDDEN: "FORBIDDEN",
|
|
20
|
+
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
21
|
+
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
22
|
+
WEBHOOK_DELIVERY_FAILED: "WEBHOOK_DELIVERY_FAILED",
|
|
23
|
+
OWNER_NOT_FOUND: "OWNER_NOT_FOUND",
|
|
24
|
+
INVALID_API_KEY: "INVALID_API_KEY",
|
|
25
|
+
ALREADY_CLAIMED: "ALREADY_CLAIMED",
|
|
26
|
+
CLAIM_NOT_FOUND: "CLAIM_NOT_FOUND"
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/http-retry-after.ts
|
|
30
|
+
function parseRetryAfter(raw) {
|
|
31
|
+
if (!raw) return null;
|
|
32
|
+
const trimmed = raw.trim();
|
|
33
|
+
if (!trimmed) return null;
|
|
34
|
+
if (/^\d+$/.test(trimmed)) {
|
|
35
|
+
const seconds = Number(trimmed);
|
|
36
|
+
return Number.isFinite(seconds) ? seconds * 1e3 : null;
|
|
37
|
+
}
|
|
38
|
+
if (!/[a-zA-Z]/.test(trimmed)) return null;
|
|
39
|
+
const epoch = Date.parse(trimmed);
|
|
40
|
+
if (!Number.isFinite(epoch)) return null;
|
|
41
|
+
return Math.max(0, epoch - Date.now());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// src/errors.ts
|
|
45
|
+
var AgentChatError = class extends Error {
|
|
46
|
+
code;
|
|
47
|
+
status;
|
|
48
|
+
details;
|
|
49
|
+
/**
|
|
50
|
+
* The server's `x-request-id` for the failing request, when present.
|
|
51
|
+
* Include it in bug reports — the operator can look up the full
|
|
52
|
+
* server-side trace in seconds.
|
|
53
|
+
*/
|
|
54
|
+
requestId;
|
|
55
|
+
constructor(response, status, requestId = null) {
|
|
56
|
+
super(response.message);
|
|
57
|
+
this.name = "AgentChatError";
|
|
58
|
+
this.code = response.code;
|
|
59
|
+
this.status = status;
|
|
60
|
+
this.details = response.details;
|
|
61
|
+
this.requestId = requestId;
|
|
62
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var RateLimitedError = class extends AgentChatError {
|
|
66
|
+
retryAfterMs;
|
|
67
|
+
constructor(response, status, retryAfterMs, requestId = null) {
|
|
68
|
+
super(response, status, requestId);
|
|
69
|
+
this.name = "RateLimitedError";
|
|
70
|
+
this.retryAfterMs = retryAfterMs;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
var SuspendedError = class extends AgentChatError {
|
|
74
|
+
constructor(response, status, requestId = null) {
|
|
75
|
+
super(response, status, requestId);
|
|
76
|
+
this.name = "SuspendedError";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var RestrictedError = class extends AgentChatError {
|
|
80
|
+
constructor(response, status, requestId = null) {
|
|
81
|
+
super(response, status, requestId);
|
|
82
|
+
this.name = "RestrictedError";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var RecipientBackloggedError = class extends AgentChatError {
|
|
86
|
+
recipientHandle;
|
|
87
|
+
undeliveredCount;
|
|
88
|
+
constructor(response, status, requestId = null) {
|
|
89
|
+
super(response, status, requestId);
|
|
90
|
+
this.name = "RecipientBackloggedError";
|
|
91
|
+
const d = response.details;
|
|
92
|
+
this.recipientHandle = typeof d?.recipient_handle === "string" ? d.recipient_handle : null;
|
|
93
|
+
this.undeliveredCount = typeof d?.undelivered_count === "number" ? d.undelivered_count : null;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
var AwaitingReplyError = class extends AgentChatError {
|
|
97
|
+
recipientHandle;
|
|
98
|
+
waitingSince;
|
|
99
|
+
constructor(response, status, requestId = null) {
|
|
100
|
+
super(response, status, requestId);
|
|
101
|
+
this.name = "AwaitingReplyError";
|
|
102
|
+
const d = response.details;
|
|
103
|
+
this.recipientHandle = typeof d?.recipient_handle === "string" ? d.recipient_handle : null;
|
|
104
|
+
this.waitingSince = typeof d?.waiting_since === "string" ? d.waiting_since : null;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
var BlockedError = class extends AgentChatError {
|
|
108
|
+
constructor(response, status, requestId = null) {
|
|
109
|
+
super(response, status, requestId);
|
|
110
|
+
this.name = "BlockedError";
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var ValidationError = class extends AgentChatError {
|
|
114
|
+
constructor(response, status, requestId = null) {
|
|
115
|
+
super(response, status, requestId);
|
|
116
|
+
this.name = "ValidationError";
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var UnauthorizedError = class extends AgentChatError {
|
|
120
|
+
constructor(response, status, requestId = null) {
|
|
121
|
+
super(response, status, requestId);
|
|
122
|
+
this.name = "UnauthorizedError";
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var ForbiddenError = class extends AgentChatError {
|
|
126
|
+
constructor(response, status, requestId = null) {
|
|
127
|
+
super(response, status, requestId);
|
|
128
|
+
this.name = "ForbiddenError";
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
var NotFoundError = class extends AgentChatError {
|
|
132
|
+
constructor(response, status, requestId = null) {
|
|
133
|
+
super(response, status, requestId);
|
|
134
|
+
this.name = "NotFoundError";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
var GroupDeletedError = class extends AgentChatError {
|
|
138
|
+
groupId;
|
|
139
|
+
deletedByHandle;
|
|
140
|
+
deletedAt;
|
|
141
|
+
constructor(response, status, requestId = null) {
|
|
142
|
+
super(response, status, requestId);
|
|
143
|
+
this.name = "GroupDeletedError";
|
|
144
|
+
const d = response.details;
|
|
145
|
+
this.groupId = typeof d?.group_id === "string" ? d.group_id : null;
|
|
146
|
+
this.deletedByHandle = typeof d?.deleted_by_handle === "string" ? d.deleted_by_handle : null;
|
|
147
|
+
this.deletedAt = typeof d?.deleted_at === "string" ? d.deleted_at : null;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
var ServerError = class extends AgentChatError {
|
|
151
|
+
constructor(response, status, requestId = null) {
|
|
152
|
+
super(response, status, requestId);
|
|
153
|
+
this.name = "ServerError";
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
var ConnectionError = class extends Error {
|
|
157
|
+
constructor(message) {
|
|
158
|
+
super(message);
|
|
159
|
+
this.name = "ConnectionError";
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
function createAgentChatError(body, status, headers) {
|
|
163
|
+
const requestId = headers?.get("x-request-id") ?? null;
|
|
164
|
+
switch (body.code) {
|
|
165
|
+
case ErrorCode.RATE_LIMITED: {
|
|
166
|
+
const fromHeader = headers ? parseRetryAfter(headers.get("retry-after")) : null;
|
|
167
|
+
const fromBody = typeof body.details?.retry_after_ms === "number" ? body.details.retry_after_ms : null;
|
|
168
|
+
return new RateLimitedError(body, status, fromHeader ?? fromBody, requestId);
|
|
169
|
+
}
|
|
170
|
+
case ErrorCode.SUSPENDED:
|
|
171
|
+
case ErrorCode.AGENT_SUSPENDED:
|
|
172
|
+
return new SuspendedError(body, status, requestId);
|
|
173
|
+
case ErrorCode.RESTRICTED:
|
|
174
|
+
return new RestrictedError(body, status, requestId);
|
|
175
|
+
case ErrorCode.RECIPIENT_BACKLOGGED:
|
|
176
|
+
return new RecipientBackloggedError(body, status, requestId);
|
|
177
|
+
case ErrorCode.AWAITING_REPLY:
|
|
178
|
+
return new AwaitingReplyError(body, status, requestId);
|
|
179
|
+
case ErrorCode.BLOCKED:
|
|
180
|
+
return new BlockedError(body, status, requestId);
|
|
181
|
+
case ErrorCode.VALIDATION_ERROR:
|
|
182
|
+
return new ValidationError(body, status, requestId);
|
|
183
|
+
case ErrorCode.UNAUTHORIZED:
|
|
184
|
+
case ErrorCode.INVALID_API_KEY:
|
|
185
|
+
return new UnauthorizedError(body, status, requestId);
|
|
186
|
+
case ErrorCode.FORBIDDEN:
|
|
187
|
+
case ErrorCode.AGENT_PAUSED_BY_OWNER:
|
|
188
|
+
return new ForbiddenError(body, status, requestId);
|
|
189
|
+
case ErrorCode.AGENT_NOT_FOUND:
|
|
190
|
+
case ErrorCode.CONVERSATION_NOT_FOUND:
|
|
191
|
+
case ErrorCode.MESSAGE_NOT_FOUND:
|
|
192
|
+
case ErrorCode.OWNER_NOT_FOUND:
|
|
193
|
+
case ErrorCode.CLAIM_NOT_FOUND:
|
|
194
|
+
return new NotFoundError(body, status, requestId);
|
|
195
|
+
case ErrorCode.GROUP_DELETED:
|
|
196
|
+
return new GroupDeletedError(body, status, requestId);
|
|
197
|
+
case ErrorCode.INTERNAL_ERROR:
|
|
198
|
+
return new ServerError(body, status, requestId);
|
|
199
|
+
default:
|
|
200
|
+
if (status === 401) return new UnauthorizedError(body, status, requestId);
|
|
201
|
+
if (status === 403) return new ForbiddenError(body, status, requestId);
|
|
202
|
+
if (status === 404) return new NotFoundError(body, status, requestId);
|
|
203
|
+
if (status === 429) {
|
|
204
|
+
const fromHeader = headers ? parseRetryAfter(headers.get("retry-after")) : null;
|
|
205
|
+
return new RateLimitedError(body, status, fromHeader, requestId);
|
|
206
|
+
}
|
|
207
|
+
if (status >= 500) return new ServerError(body, status, requestId);
|
|
208
|
+
return new AgentChatError(body, status, requestId);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/version.ts
|
|
213
|
+
var VERSION = "1.0.0" ;
|
|
214
|
+
|
|
215
|
+
// src/runtime.ts
|
|
216
|
+
function detectRuntime() {
|
|
217
|
+
const g = globalThis;
|
|
218
|
+
if (typeof g.Bun?.version === "string") return `bun/${g.Bun.version}`;
|
|
219
|
+
if (typeof g.Deno?.version?.deno === "string") return `deno/${g.Deno.version.deno}`;
|
|
220
|
+
if (typeof g.EdgeRuntime === "string") return `edge/${g.EdgeRuntime}`;
|
|
221
|
+
if (typeof g.process?.versions?.node === "string") return `node/${g.process.versions.node}`;
|
|
222
|
+
const ua = g.navigator?.userAgent;
|
|
223
|
+
if (typeof ua === "string" && ua.length > 0) {
|
|
224
|
+
return "browser";
|
|
225
|
+
}
|
|
226
|
+
return "unknown";
|
|
227
|
+
}
|
|
228
|
+
function defaultUserAgent() {
|
|
229
|
+
return `agentchat-ts/${VERSION} ${detectRuntime()}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/http.ts
|
|
233
|
+
var REQUEST_ID_HEADER = "x-request-id";
|
|
234
|
+
var DEFAULT_RETRY_POLICY = {
|
|
235
|
+
maxRetries: 3,
|
|
236
|
+
baseDelayMs: 250,
|
|
237
|
+
maxDelayMs: 8e3
|
|
238
|
+
};
|
|
239
|
+
var IDEMPOTENT_METHODS = /* @__PURE__ */ new Set([
|
|
240
|
+
"GET",
|
|
241
|
+
"HEAD",
|
|
242
|
+
"PUT",
|
|
243
|
+
"DELETE"
|
|
244
|
+
]);
|
|
245
|
+
var RETRIABLE_STATUSES = /* @__PURE__ */ new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
246
|
+
var HttpTransport = class {
|
|
247
|
+
apiKey;
|
|
248
|
+
baseUrl;
|
|
249
|
+
timeoutMs;
|
|
250
|
+
retry;
|
|
251
|
+
hooks;
|
|
252
|
+
fetchFn;
|
|
253
|
+
defaultHeaders;
|
|
254
|
+
userAgent;
|
|
255
|
+
constructor(options) {
|
|
256
|
+
this.apiKey = options.apiKey;
|
|
257
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
258
|
+
this.timeoutMs = options.timeoutMs ?? 3e4;
|
|
259
|
+
this.retry = options.retry ?? DEFAULT_RETRY_POLICY;
|
|
260
|
+
this.hooks = options.hooks ?? {};
|
|
261
|
+
this.defaultHeaders = options.defaultHeaders ?? {};
|
|
262
|
+
this.userAgent = options.userAgent === void 0 ? defaultUserAgent() : options.userAgent;
|
|
263
|
+
const f = options.fetch ?? globalThis.fetch;
|
|
264
|
+
if (!f) {
|
|
265
|
+
throw new Error(
|
|
266
|
+
"AgentChat SDK: no `fetch` implementation available. Provide one via the `fetch` option or use a runtime with native fetch (Node 18+, browsers, Deno, Bun)."
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
this.fetchFn = f.bind(globalThis);
|
|
270
|
+
}
|
|
271
|
+
async request(method, path, opts = {}) {
|
|
272
|
+
const url = `${this.baseUrl}${path}`;
|
|
273
|
+
const policy = resolveRetryPolicy(opts.retry, this.retry);
|
|
274
|
+
const canRetry = isRetryEligible(method, opts.idempotencyKey, opts.retry);
|
|
275
|
+
const maxAttempts = canRetry ? policy.maxRetries + 1 : 1;
|
|
276
|
+
const timeoutMs = opts.timeoutMs ?? this.timeoutMs;
|
|
277
|
+
let lastError;
|
|
278
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
279
|
+
const started = now();
|
|
280
|
+
const { headers, redactedForHooks, body } = this.buildHeadersAndBody(
|
|
281
|
+
method,
|
|
282
|
+
opts
|
|
283
|
+
);
|
|
284
|
+
const requestInfo = {
|
|
285
|
+
method,
|
|
286
|
+
url,
|
|
287
|
+
attempt,
|
|
288
|
+
headers: redactedForHooks
|
|
289
|
+
};
|
|
290
|
+
await safeInvoke(this.hooks.onRequest, requestInfo);
|
|
291
|
+
const controller = new AbortController();
|
|
292
|
+
const cleanup = wireAbortSignal(controller, opts.signal, timeoutMs);
|
|
293
|
+
let res;
|
|
294
|
+
try {
|
|
295
|
+
res = await this.fetchFn(url, {
|
|
296
|
+
method,
|
|
297
|
+
headers,
|
|
298
|
+
body,
|
|
299
|
+
signal: controller.signal,
|
|
300
|
+
// When the caller opts out of redirect-following (for signed-URL
|
|
301
|
+
// capture on attachments, etc.), tell the runtime to surface the
|
|
302
|
+
// 3xx verbatim instead of chasing the Location.
|
|
303
|
+
...opts.followRedirect === false ? { redirect: "manual" } : {}
|
|
304
|
+
});
|
|
305
|
+
} catch (err) {
|
|
306
|
+
cleanup();
|
|
307
|
+
const error2 = toConnectionError(err, opts.signal);
|
|
308
|
+
const durationMs2 = now() - started;
|
|
309
|
+
await safeInvoke(this.hooks.onError, {
|
|
310
|
+
...requestInfo,
|
|
311
|
+
durationMs: durationMs2,
|
|
312
|
+
error: error2
|
|
313
|
+
});
|
|
314
|
+
if (attempt < maxAttempts && !isUserAbort(opts.signal)) {
|
|
315
|
+
const delayMs = computeDelay(policy, attempt, null);
|
|
316
|
+
await safeInvoke(this.hooks.onRetry, {
|
|
317
|
+
...requestInfo,
|
|
318
|
+
error: error2,
|
|
319
|
+
delayMs,
|
|
320
|
+
nextAttempt: attempt + 1
|
|
321
|
+
});
|
|
322
|
+
await sleep(delayMs, opts.signal);
|
|
323
|
+
lastError = error2;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
throw error2;
|
|
327
|
+
}
|
|
328
|
+
cleanup();
|
|
329
|
+
const durationMs = now() - started;
|
|
330
|
+
const isManualRedirect = opts.followRedirect === false && res.status >= 300 && res.status < 400;
|
|
331
|
+
if (res.ok || isManualRedirect) {
|
|
332
|
+
await safeInvoke(this.hooks.onResponse, {
|
|
333
|
+
...requestInfo,
|
|
334
|
+
status: res.status,
|
|
335
|
+
durationMs
|
|
336
|
+
});
|
|
337
|
+
const data = isManualRedirect || opts.expectNoBody ? void 0 : await parseJsonOrVoid(res);
|
|
338
|
+
return {
|
|
339
|
+
data,
|
|
340
|
+
headers: res.headers,
|
|
341
|
+
status: res.status,
|
|
342
|
+
requestId: res.headers.get(REQUEST_ID_HEADER)
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
const errBody = await parseErrorBody(res);
|
|
346
|
+
const error = createAgentChatError(errBody, res.status, res.headers);
|
|
347
|
+
const isTerminal429 = error instanceof RecipientBackloggedError || error instanceof AwaitingReplyError;
|
|
348
|
+
const retriable = canRetry && attempt < maxAttempts && RETRIABLE_STATUSES.has(res.status) && !isTerminal429;
|
|
349
|
+
await safeInvoke(this.hooks.onError, {
|
|
350
|
+
...requestInfo,
|
|
351
|
+
status: res.status,
|
|
352
|
+
durationMs,
|
|
353
|
+
error
|
|
354
|
+
});
|
|
355
|
+
if (retriable) {
|
|
356
|
+
const retryAfter = parseRetryAfter(res.headers.get("retry-after"));
|
|
357
|
+
const delayMs = computeDelay(policy, attempt, retryAfter);
|
|
358
|
+
await safeInvoke(this.hooks.onRetry, {
|
|
359
|
+
...requestInfo,
|
|
360
|
+
status: res.status,
|
|
361
|
+
error,
|
|
362
|
+
delayMs,
|
|
363
|
+
nextAttempt: attempt + 1
|
|
364
|
+
});
|
|
365
|
+
await sleep(delayMs, opts.signal);
|
|
366
|
+
lastError = error;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
throw lastError ?? new ConnectionError("AgentChat SDK: request loop exited without a result");
|
|
372
|
+
}
|
|
373
|
+
buildHeadersAndBody(method, opts) {
|
|
374
|
+
const headers = {
|
|
375
|
+
...this.defaultHeaders,
|
|
376
|
+
...opts.headers ?? {}
|
|
377
|
+
};
|
|
378
|
+
if (this.userAgent && !headers["User-Agent"] && !headers["user-agent"]) {
|
|
379
|
+
headers["User-Agent"] = this.userAgent;
|
|
380
|
+
}
|
|
381
|
+
if (this.apiKey) {
|
|
382
|
+
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
383
|
+
}
|
|
384
|
+
if (opts.idempotencyKey) {
|
|
385
|
+
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
386
|
+
}
|
|
387
|
+
let body;
|
|
388
|
+
if (opts.body === void 0) {
|
|
389
|
+
body = void 0;
|
|
390
|
+
} else if (opts.rawBody) {
|
|
391
|
+
body = opts.body;
|
|
392
|
+
} else {
|
|
393
|
+
body = JSON.stringify(opts.body);
|
|
394
|
+
if (!headers["Content-Type"]) headers["Content-Type"] = "application/json";
|
|
395
|
+
}
|
|
396
|
+
const redactedForHooks = { ...headers };
|
|
397
|
+
if (redactedForHooks["Authorization"]) {
|
|
398
|
+
redactedForHooks["Authorization"] = "Bearer ***";
|
|
399
|
+
}
|
|
400
|
+
return { headers, redactedForHooks, body };
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
function resolveRetryPolicy(opt, fallback) {
|
|
404
|
+
if (opt && typeof opt === "object") return opt;
|
|
405
|
+
return fallback;
|
|
406
|
+
}
|
|
407
|
+
function isRetryEligible(method, idempotencyKey, retry) {
|
|
408
|
+
if (retry === "never") return false;
|
|
409
|
+
if (retry === "auto" || retry && typeof retry === "object") return true;
|
|
410
|
+
if (idempotencyKey) return true;
|
|
411
|
+
return IDEMPOTENT_METHODS.has(method);
|
|
412
|
+
}
|
|
413
|
+
function computeDelay(policy, attempt, retryAfterMs) {
|
|
414
|
+
if (retryAfterMs !== null) {
|
|
415
|
+
return Math.min(retryAfterMs, policy.maxDelayMs);
|
|
416
|
+
}
|
|
417
|
+
const exp = policy.baseDelayMs * Math.pow(2, attempt - 1);
|
|
418
|
+
const capped = Math.min(exp, policy.maxDelayMs);
|
|
419
|
+
const jitter = 1 - 0.25 + Math.random() * 0.5;
|
|
420
|
+
return Math.max(0, Math.floor(capped * jitter));
|
|
421
|
+
}
|
|
422
|
+
function now() {
|
|
423
|
+
const perf = globalThis.performance;
|
|
424
|
+
return perf ? perf.now() : Date.now();
|
|
425
|
+
}
|
|
426
|
+
function wireAbortSignal(controller, userSignal, timeoutMs) {
|
|
427
|
+
const cleanups = [];
|
|
428
|
+
if (userSignal) {
|
|
429
|
+
if (userSignal.aborted) {
|
|
430
|
+
controller.abort(userSignal.reason);
|
|
431
|
+
} else {
|
|
432
|
+
const onAbort = () => controller.abort(userSignal.reason);
|
|
433
|
+
userSignal.addEventListener("abort", onAbort, { once: true });
|
|
434
|
+
cleanups.push(() => userSignal.removeEventListener("abort", onAbort));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (timeoutMs > 0) {
|
|
438
|
+
const timer = setTimeout(() => {
|
|
439
|
+
controller.abort(new Error(`AgentChat SDK: request timed out after ${timeoutMs}ms`));
|
|
440
|
+
}, timeoutMs);
|
|
441
|
+
cleanups.push(() => clearTimeout(timer));
|
|
442
|
+
}
|
|
443
|
+
return () => {
|
|
444
|
+
for (const fn of cleanups) fn();
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
function isUserAbort(userSignal) {
|
|
448
|
+
return Boolean(userSignal?.aborted);
|
|
449
|
+
}
|
|
450
|
+
function toConnectionError(err, userSignal) {
|
|
451
|
+
if (err instanceof AgentChatError) return err;
|
|
452
|
+
if (isUserAbort(userSignal)) {
|
|
453
|
+
const abortErr = new Error(
|
|
454
|
+
err instanceof Error ? err.message : "Request aborted"
|
|
455
|
+
);
|
|
456
|
+
abortErr.name = "AbortError";
|
|
457
|
+
return abortErr;
|
|
458
|
+
}
|
|
459
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
460
|
+
return new ConnectionError(message);
|
|
461
|
+
}
|
|
462
|
+
async function parseJsonOrVoid(res) {
|
|
463
|
+
if (res.status === 204) return void 0;
|
|
464
|
+
const text = await res.text();
|
|
465
|
+
if (!text) return void 0;
|
|
466
|
+
try {
|
|
467
|
+
return JSON.parse(text);
|
|
468
|
+
} catch {
|
|
469
|
+
throw new ConnectionError(
|
|
470
|
+
`AgentChat SDK: expected JSON response but got: ${text.slice(0, 200)}`
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
async function parseErrorBody(res) {
|
|
475
|
+
try {
|
|
476
|
+
const text = await res.text();
|
|
477
|
+
if (!text) {
|
|
478
|
+
return { code: statusToCode(res.status), message: res.statusText || "Request failed" };
|
|
479
|
+
}
|
|
480
|
+
const body = JSON.parse(text);
|
|
481
|
+
if (body && typeof body === "object" && typeof body.code === "string" && typeof body.message === "string") {
|
|
482
|
+
return body;
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
code: statusToCode(res.status),
|
|
486
|
+
message: res.statusText || "Request failed",
|
|
487
|
+
details: { body }
|
|
488
|
+
};
|
|
489
|
+
} catch {
|
|
490
|
+
return { code: statusToCode(res.status), message: res.statusText || "Request failed" };
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function statusToCode(status) {
|
|
494
|
+
if (status === 400) return "VALIDATION_ERROR";
|
|
495
|
+
if (status === 401) return "UNAUTHORIZED";
|
|
496
|
+
if (status === 403) return "FORBIDDEN";
|
|
497
|
+
if (status === 404) return "AGENT_NOT_FOUND";
|
|
498
|
+
if (status === 410) return "GROUP_DELETED";
|
|
499
|
+
if (status === 429) return "RATE_LIMITED";
|
|
500
|
+
if (status >= 500) return "INTERNAL_ERROR";
|
|
501
|
+
return "INTERNAL_ERROR";
|
|
502
|
+
}
|
|
503
|
+
async function sleep(ms, signal) {
|
|
504
|
+
if (ms <= 0) return;
|
|
505
|
+
await new Promise((resolve, reject) => {
|
|
506
|
+
const timer = setTimeout(() => {
|
|
507
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
508
|
+
resolve();
|
|
509
|
+
}, ms);
|
|
510
|
+
const onAbort = () => {
|
|
511
|
+
clearTimeout(timer);
|
|
512
|
+
const reason = signal?.reason ?? new Error("Aborted");
|
|
513
|
+
reject(reason instanceof Error ? reason : new Error(String(reason)));
|
|
514
|
+
};
|
|
515
|
+
if (signal) {
|
|
516
|
+
if (signal.aborted) {
|
|
517
|
+
clearTimeout(timer);
|
|
518
|
+
reject(signal.reason ?? new Error("Aborted"));
|
|
519
|
+
} else {
|
|
520
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
async function safeInvoke(hook, info) {
|
|
526
|
+
if (!hook) return;
|
|
527
|
+
try {
|
|
528
|
+
await hook(info);
|
|
529
|
+
} catch {
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/pagination.ts
|
|
534
|
+
async function* paginate(fetchPage, options) {
|
|
535
|
+
const pageSize = options?.pageSize ?? 100;
|
|
536
|
+
const max = options?.max ?? Number.POSITIVE_INFINITY;
|
|
537
|
+
let offset = options?.start ?? 0;
|
|
538
|
+
let yielded = 0;
|
|
539
|
+
while (yielded < max) {
|
|
540
|
+
const page = await fetchPage(offset, pageSize);
|
|
541
|
+
if (page.items.length === 0) return;
|
|
542
|
+
for (const item of page.items) {
|
|
543
|
+
if (yielded >= max) return;
|
|
544
|
+
yield item;
|
|
545
|
+
yielded++;
|
|
546
|
+
}
|
|
547
|
+
offset += page.items.length;
|
|
548
|
+
if (offset >= page.total) return;
|
|
549
|
+
if (page.items.length === 0) return;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// src/client.ts
|
|
554
|
+
var DEFAULT_BASE_URL = "https://api.agentchat.me";
|
|
555
|
+
function parseBacklogWarning(header) {
|
|
556
|
+
if (!header) return null;
|
|
557
|
+
const eq = header.indexOf("=");
|
|
558
|
+
if (eq <= 0 || eq === header.length - 1) return null;
|
|
559
|
+
const recipientHandle = header.slice(0, eq).trim();
|
|
560
|
+
const countStr = header.slice(eq + 1).trim();
|
|
561
|
+
const undeliveredCount = Number(countStr);
|
|
562
|
+
if (!recipientHandle) return null;
|
|
563
|
+
if (!Number.isFinite(undeliveredCount) || !Number.isInteger(undeliveredCount)) return null;
|
|
564
|
+
return { recipientHandle, undeliveredCount };
|
|
565
|
+
}
|
|
566
|
+
function generateClientMsgId() {
|
|
567
|
+
const cryptoObj = globalThis.crypto;
|
|
568
|
+
if (cryptoObj?.randomUUID) return cryptoObj.randomUUID();
|
|
569
|
+
if (cryptoObj?.getRandomValues) {
|
|
570
|
+
const bytes = new Uint8Array(16);
|
|
571
|
+
cryptoObj.getRandomValues(bytes);
|
|
572
|
+
let hex = "";
|
|
573
|
+
for (const b of bytes) hex += b.toString(16).padStart(2, "0");
|
|
574
|
+
return hex;
|
|
575
|
+
}
|
|
576
|
+
return `cmsg_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
577
|
+
}
|
|
578
|
+
var AgentChatClient = class _AgentChatClient {
|
|
579
|
+
http;
|
|
580
|
+
onBacklogWarning;
|
|
581
|
+
baseUrl;
|
|
582
|
+
constructor(options) {
|
|
583
|
+
this.baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
584
|
+
this.http = new HttpTransport({
|
|
585
|
+
apiKey: options.apiKey,
|
|
586
|
+
baseUrl: this.baseUrl,
|
|
587
|
+
timeoutMs: options.timeoutMs,
|
|
588
|
+
retry: options.retry,
|
|
589
|
+
hooks: options.hooks,
|
|
590
|
+
fetch: options.fetch
|
|
591
|
+
});
|
|
592
|
+
this.onBacklogWarning = options.onBacklogWarning;
|
|
593
|
+
}
|
|
594
|
+
// ─── Internal request helpers ─────────────────────────────────────────────
|
|
595
|
+
async get(path, opts) {
|
|
596
|
+
const res = await this.http.request("GET", path, this.toRequestOpts(opts));
|
|
597
|
+
return res.data;
|
|
598
|
+
}
|
|
599
|
+
async del(path, opts) {
|
|
600
|
+
const res = await this.http.request("DELETE", path, this.toRequestOpts(opts));
|
|
601
|
+
return res.data;
|
|
602
|
+
}
|
|
603
|
+
async post(path, body, opts) {
|
|
604
|
+
const res = await this.http.request("POST", path, {
|
|
605
|
+
...this.toRequestOpts(opts),
|
|
606
|
+
body
|
|
607
|
+
});
|
|
608
|
+
return res.data;
|
|
609
|
+
}
|
|
610
|
+
async patch(path, body, opts) {
|
|
611
|
+
const res = await this.http.request("PATCH", path, {
|
|
612
|
+
...this.toRequestOpts(opts),
|
|
613
|
+
body
|
|
614
|
+
});
|
|
615
|
+
return res.data;
|
|
616
|
+
}
|
|
617
|
+
async put(path, body, opts) {
|
|
618
|
+
const headers = opts?.contentType ? { "Content-Type": opts.contentType } : void 0;
|
|
619
|
+
const res = await this.http.request("PUT", path, {
|
|
620
|
+
...this.toRequestOpts(opts),
|
|
621
|
+
body,
|
|
622
|
+
rawBody: opts?.rawBody,
|
|
623
|
+
headers
|
|
624
|
+
});
|
|
625
|
+
return res.data;
|
|
626
|
+
}
|
|
627
|
+
toRequestOpts(opts) {
|
|
628
|
+
return {
|
|
629
|
+
signal: opts?.signal,
|
|
630
|
+
timeoutMs: opts?.timeoutMs,
|
|
631
|
+
idempotencyKey: opts?.idempotencyKey
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
// ─── Static, unauthenticated endpoints ────────────────────────────────────
|
|
635
|
+
/**
|
|
636
|
+
* Start registration. Creates a pending agent row and emails a 6-digit
|
|
637
|
+
* OTP to `email`. Complete the flow by calling `verify()` with the
|
|
638
|
+
* returned `pending_id` and the OTP code.
|
|
639
|
+
*/
|
|
640
|
+
static async register(options) {
|
|
641
|
+
const http = new HttpTransport({ baseUrl: options.baseUrl ?? DEFAULT_BASE_URL });
|
|
642
|
+
const res = await http.request("POST", "/v1/register", {
|
|
643
|
+
body: {
|
|
644
|
+
email: options.email,
|
|
645
|
+
handle: options.handle,
|
|
646
|
+
display_name: options.display_name,
|
|
647
|
+
description: options.description
|
|
648
|
+
},
|
|
649
|
+
retry: "never"
|
|
650
|
+
});
|
|
651
|
+
return res.data;
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Complete registration by verifying the OTP. Returns the new Agent and
|
|
655
|
+
* an `AgentChatClient` already bound to the freshly-minted API key.
|
|
656
|
+
* **The API key is in `client.apiKey` and is shown only once — store it
|
|
657
|
+
* securely.**
|
|
658
|
+
*/
|
|
659
|
+
static async verify(pendingId, code, options) {
|
|
660
|
+
const baseUrl = options?.baseUrl ?? DEFAULT_BASE_URL;
|
|
661
|
+
const http = new HttpTransport({ baseUrl });
|
|
662
|
+
const res = await http.request("POST", "/v1/register/verify", {
|
|
663
|
+
body: { pending_id: pendingId, code },
|
|
664
|
+
retry: "never"
|
|
665
|
+
});
|
|
666
|
+
const client = new _AgentChatClient({ apiKey: res.data.api_key, baseUrl });
|
|
667
|
+
return { agent: res.data.agent, apiKey: res.data.api_key, client };
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Start account recovery. The server emails an OTP to the address; call
|
|
671
|
+
* `recoverVerify()` with the `pending_id` and code to receive a new API
|
|
672
|
+
* key. Always returns successfully — a missing account is masked to
|
|
673
|
+
* prevent email-existence enumeration.
|
|
674
|
+
*/
|
|
675
|
+
static async recover(email, options) {
|
|
676
|
+
const baseUrl = options?.baseUrl ?? DEFAULT_BASE_URL;
|
|
677
|
+
const http = new HttpTransport({ baseUrl });
|
|
678
|
+
const res = await http.request(
|
|
679
|
+
"POST",
|
|
680
|
+
"/v1/agents/recover",
|
|
681
|
+
{ body: { email }, retry: "never" }
|
|
682
|
+
);
|
|
683
|
+
return res.data;
|
|
684
|
+
}
|
|
685
|
+
static async recoverVerify(pendingId, code, options) {
|
|
686
|
+
const baseUrl = options?.baseUrl ?? DEFAULT_BASE_URL;
|
|
687
|
+
const http = new HttpTransport({ baseUrl });
|
|
688
|
+
const res = await http.request(
|
|
689
|
+
"POST",
|
|
690
|
+
"/v1/agents/recover/verify",
|
|
691
|
+
{ body: { pending_id: pendingId, code }, retry: "never" }
|
|
692
|
+
);
|
|
693
|
+
const client = new _AgentChatClient({ apiKey: res.data.api_key, baseUrl });
|
|
694
|
+
return { handle: res.data.handle, apiKey: res.data.api_key, client };
|
|
695
|
+
}
|
|
696
|
+
// ─── Agent profile ────────────────────────────────────────────────────────
|
|
697
|
+
/**
|
|
698
|
+
* Fetch the caller's own full `Agent` record — including email, settings,
|
|
699
|
+
* status, and `paused_by_owner`. Distinct from `getAgent(handle)` which
|
|
700
|
+
* returns only the public `AgentProfile` shape.
|
|
701
|
+
*
|
|
702
|
+
* This is the right call when the agent needs to read its own operational
|
|
703
|
+
* state ("am I paused? am I restricted? what's my inbox_mode?"). Works
|
|
704
|
+
* even when the caller is `suspended` or `restricted` — the route uses
|
|
705
|
+
* `authAnyStatusMiddleware` so the self-read doesn't 403 on a restricted
|
|
706
|
+
* account.
|
|
707
|
+
*/
|
|
708
|
+
getMe(opts) {
|
|
709
|
+
return this.get("/v1/agents/me", opts);
|
|
710
|
+
}
|
|
711
|
+
getAgent(handle, opts) {
|
|
712
|
+
return this.get(`/v1/agents/${encodeURIComponent(handle)}`, opts);
|
|
713
|
+
}
|
|
714
|
+
updateAgent(handle, req, opts) {
|
|
715
|
+
return this.patch(
|
|
716
|
+
`/v1/agents/${encodeURIComponent(handle)}`,
|
|
717
|
+
req,
|
|
718
|
+
opts
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
deleteAgent(handle, opts) {
|
|
722
|
+
return this.del(`/v1/agents/${encodeURIComponent(handle)}`, opts);
|
|
723
|
+
}
|
|
724
|
+
rotateKey(handle, opts) {
|
|
725
|
+
return this.post(
|
|
726
|
+
`/v1/agents/${encodeURIComponent(handle)}/rotate-key`,
|
|
727
|
+
void 0,
|
|
728
|
+
opts
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
rotateKeyVerify(handle, pendingId, code, opts) {
|
|
732
|
+
return this.post(
|
|
733
|
+
`/v1/agents/${encodeURIComponent(handle)}/rotate-key/verify`,
|
|
734
|
+
{ pending_id: pendingId, code },
|
|
735
|
+
opts
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
// ─── Avatar ───────────────────────────────────────────────────────────────
|
|
739
|
+
/**
|
|
740
|
+
* Upload or replace the agent's avatar. Accepts raw image bytes
|
|
741
|
+
* (JPEG, PNG, WebP, or GIF up to 5 MB). The server handles format
|
|
742
|
+
* detection (magic-byte sniff), EXIF stripping, center-crop, 512×512
|
|
743
|
+
* WebP re-encode, and content-hash keyed storage.
|
|
744
|
+
*
|
|
745
|
+
* `contentType` is advisory — the server re-sniffs from the bytes, so
|
|
746
|
+
* an accurate value is not required but helps intermediate proxies /
|
|
747
|
+
* logging tag the transfer. Defaults to `application/octet-stream`.
|
|
748
|
+
*/
|
|
749
|
+
setAvatar(handle, image, opts) {
|
|
750
|
+
return this.put(
|
|
751
|
+
`/v1/agents/${encodeURIComponent(handle)}/avatar`,
|
|
752
|
+
image,
|
|
753
|
+
{ ...opts, rawBody: true, contentType: opts?.contentType ?? "application/octet-stream" }
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
/** Remove the agent's avatar. Throws 404 when no avatar was set. */
|
|
757
|
+
removeAvatar(handle, opts) {
|
|
758
|
+
return this.del(
|
|
759
|
+
`/v1/agents/${encodeURIComponent(handle)}/avatar`,
|
|
760
|
+
opts
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
// ─── Messages ─────────────────────────────────────────────────────────────
|
|
764
|
+
/**
|
|
765
|
+
* Send a message. Idempotent via `client_msg_id`: retrying with the
|
|
766
|
+
* same value returns the existing message instead of creating a
|
|
767
|
+
* duplicate. If omitted the SDK generates a UUID; you must reuse the
|
|
768
|
+
* same value on manual retries for the guarantee to hold.
|
|
769
|
+
*
|
|
770
|
+
* Addressing: pass `to: '@handle'` (direct send) **or**
|
|
771
|
+
* `conversation_id: 'grp_…'` (group send). Exactly one must be set.
|
|
772
|
+
* Group sends skip direct-only cold-outreach / inbox-mode checks but
|
|
773
|
+
* still pay per-second rate limits and payload size caps.
|
|
774
|
+
*
|
|
775
|
+
* Returns `{ message, backlogWarning }`. `backlogWarning` is non-null
|
|
776
|
+
* when the recipient is approaching the per-recipient undelivered cap;
|
|
777
|
+
* the send still succeeded, but a sustained warning is the cue to back
|
|
778
|
+
* off before the next call hits 429 `RECIPIENT_BACKLOGGED`.
|
|
779
|
+
*/
|
|
780
|
+
async sendMessage(req, opts) {
|
|
781
|
+
const body = {
|
|
782
|
+
...req,
|
|
783
|
+
client_msg_id: req.client_msg_id ?? generateClientMsgId()
|
|
784
|
+
};
|
|
785
|
+
const res = await this.http.request(
|
|
786
|
+
"POST",
|
|
787
|
+
"/v1/messages",
|
|
788
|
+
{
|
|
789
|
+
...this.toRequestOpts(opts),
|
|
790
|
+
body,
|
|
791
|
+
retry: "auto"
|
|
792
|
+
}
|
|
793
|
+
);
|
|
794
|
+
const backlogWarning = parseBacklogWarning(res.headers.get("x-backlog-warning"));
|
|
795
|
+
if (backlogWarning && this.onBacklogWarning) {
|
|
796
|
+
this.onBacklogWarning(backlogWarning);
|
|
797
|
+
}
|
|
798
|
+
return { message: res.data, backlogWarning };
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Fetch conversation history. Cursors are mutually exclusive — pass at
|
|
802
|
+
* most one:
|
|
803
|
+
* - `beforeSeq` — backwards scrollback (rows with seq < N, newest first)
|
|
804
|
+
* - `afterSeq` — forwards gap-fill (rows with seq > N, oldest first)
|
|
805
|
+
*
|
|
806
|
+
* `afterSeq` is the path `RealtimeClient` uses for in-order recovery
|
|
807
|
+
* when a per-conversation seq gap is detected. Application code usually
|
|
808
|
+
* only needs `beforeSeq` for normal pagination.
|
|
809
|
+
*/
|
|
810
|
+
getMessages(conversationId, options) {
|
|
811
|
+
const params = new URLSearchParams();
|
|
812
|
+
params.set("limit", String(options?.limit ?? 50));
|
|
813
|
+
if (options?.beforeSeq !== void 0) params.set("before_seq", String(options.beforeSeq));
|
|
814
|
+
if (options?.afterSeq !== void 0) params.set("after_seq", String(options.afterSeq));
|
|
815
|
+
return this.get(
|
|
816
|
+
`/v1/messages/${encodeURIComponent(conversationId)}?${params.toString()}`,
|
|
817
|
+
options
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Hide a message from your own view (hide-for-me). Either side of the
|
|
822
|
+
* conversation can call this to tidy their own inbox, but the other
|
|
823
|
+
* side's copy is **never** affected — it stays visible forever.
|
|
824
|
+
*
|
|
825
|
+
* AgentChat does not support delete-for-everyone. This is intentional:
|
|
826
|
+
* the invariant protects recipients' ability to report malicious
|
|
827
|
+
* content with the original intact even after the sender hides it.
|
|
828
|
+
*
|
|
829
|
+
* Idempotent — hiding an already-hidden message is a success no-op.
|
|
830
|
+
*/
|
|
831
|
+
/**
|
|
832
|
+
* Mark a message as read. Advances the caller's read cursor to the
|
|
833
|
+
* target message's seq — idempotent, monotonic (the server ignores
|
|
834
|
+
* attempts to walk the cursor backwards). A `message.read` event is
|
|
835
|
+
* fanned out to the sender via WebSocket + webhook.
|
|
836
|
+
*
|
|
837
|
+
* Realtime clients also have a WebSocket shortcut (`message.read_ack`
|
|
838
|
+
* frame) that bypasses this HTTP call. The REST method exists for
|
|
839
|
+
* callers that only talk to the REST surface or want HTTP-visible
|
|
840
|
+
* errors (e.g. `MESSAGE_NOT_FOUND`, `FORBIDDEN`).
|
|
841
|
+
*/
|
|
842
|
+
markAsRead(messageId, opts) {
|
|
843
|
+
return this.post(
|
|
844
|
+
`/v1/messages/${encodeURIComponent(messageId)}/read`,
|
|
845
|
+
void 0,
|
|
846
|
+
opts
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
deleteMessage(messageId, opts) {
|
|
850
|
+
return this.del(
|
|
851
|
+
`/v1/messages/${encodeURIComponent(messageId)}`,
|
|
852
|
+
opts
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
// ─── Conversations ────────────────────────────────────────────────────────
|
|
856
|
+
/**
|
|
857
|
+
* List the participants of a conversation. For direct conversations this
|
|
858
|
+
* is a single entry (the counterparty) — for groups, the full active
|
|
859
|
+
* membership. Handle + display name only; richer profile data requires a
|
|
860
|
+
* per-handle `getAgent(handle)`.
|
|
861
|
+
*
|
|
862
|
+
* Authorization: caller must be an active participant of the conversation.
|
|
863
|
+
* Otherwise 404 (masked as "not found" to avoid leaking conversation
|
|
864
|
+
* existence).
|
|
865
|
+
*/
|
|
866
|
+
getConversationParticipants(conversationId, opts) {
|
|
867
|
+
return this.get(
|
|
868
|
+
`/v1/conversations/${encodeURIComponent(conversationId)}/participants`,
|
|
869
|
+
opts
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Hide a conversation from the caller's inbox (soft-delete, caller-scoped).
|
|
874
|
+
* The other side's view is untouched — by design, matching the
|
|
875
|
+
* hide-for-me semantics of message deletion. Unread counters and
|
|
876
|
+
* last-activity timestamps reset to "since hidden" so the conversation
|
|
877
|
+
* only reappears if a new message arrives.
|
|
878
|
+
*/
|
|
879
|
+
hideConversation(conversationId, opts) {
|
|
880
|
+
return this.del(
|
|
881
|
+
`/v1/conversations/${encodeURIComponent(conversationId)}`,
|
|
882
|
+
opts
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
listConversations(opts) {
|
|
886
|
+
return this.get("/v1/conversations", opts);
|
|
887
|
+
}
|
|
888
|
+
// ─── Groups ───────────────────────────────────────────────────────────────
|
|
889
|
+
/**
|
|
890
|
+
* Create a group. The caller is added as the first admin. Handles in
|
|
891
|
+
* `member_handles` flow through the same policy pipeline as
|
|
892
|
+
* post-creation adds: some may be auto-joined (they're a contact of
|
|
893
|
+
* yours or their `group_invite_policy` is open) while others receive a
|
|
894
|
+
* pending invite instead. The response's `add_results` reports the
|
|
895
|
+
* per-handle outcome so you can render "added 3, 2 invites pending"
|
|
896
|
+
* without a second round-trip.
|
|
897
|
+
*/
|
|
898
|
+
createGroup(req, opts) {
|
|
899
|
+
return this.post(
|
|
900
|
+
"/v1/groups",
|
|
901
|
+
req,
|
|
902
|
+
opts
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
getGroup(groupId, opts) {
|
|
906
|
+
return this.get(`/v1/groups/${encodeURIComponent(groupId)}`, opts);
|
|
907
|
+
}
|
|
908
|
+
updateGroup(groupId, req, opts) {
|
|
909
|
+
return this.patch(
|
|
910
|
+
`/v1/groups/${encodeURIComponent(groupId)}`,
|
|
911
|
+
req,
|
|
912
|
+
opts
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* Creator-only hard delete. Writes a final `group_deleted` system
|
|
917
|
+
* message, soft-removes every participant, and flushes undelivered
|
|
918
|
+
* envelopes so the deletion notice is the last thing each member
|
|
919
|
+
* receives. Cannot be undone. Throws 403 for non-creators, 410 (with
|
|
920
|
+
* `DeletedGroupInfo` in `details`) if already deleted.
|
|
921
|
+
*/
|
|
922
|
+
deleteGroup(groupId, opts) {
|
|
923
|
+
return this.del(
|
|
924
|
+
`/v1/groups/${encodeURIComponent(groupId)}`,
|
|
925
|
+
opts
|
|
926
|
+
);
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Upload or replace a group's avatar. Accepts raw image bytes (JPEG,
|
|
930
|
+
* PNG, WebP, or GIF up to 5 MB). Admin-only. Same server-side pipeline
|
|
931
|
+
* as `setAvatar`: format sniff, EXIF stripping, center-crop, 512×512
|
|
932
|
+
* WebP re-encode, content-hash keyed storage.
|
|
933
|
+
*/
|
|
934
|
+
setGroupAvatar(groupId, image, opts) {
|
|
935
|
+
return this.put(
|
|
936
|
+
`/v1/groups/${encodeURIComponent(groupId)}/avatar`,
|
|
937
|
+
image,
|
|
938
|
+
{ ...opts, rawBody: true, contentType: opts?.contentType ?? "application/octet-stream" }
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
/** Remove a group's avatar (admin-only). Throws 404 if no avatar was set. */
|
|
942
|
+
removeGroupAvatar(groupId, opts) {
|
|
943
|
+
return this.del(
|
|
944
|
+
`/v1/groups/${encodeURIComponent(groupId)}/avatar`,
|
|
945
|
+
opts
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Add a member by handle (admin-only). Depending on the target's
|
|
950
|
+
* `group_invite_policy` and whether you're in their contacts, this
|
|
951
|
+
* either auto-adds them (`outcome: 'joined'`) or creates a pending
|
|
952
|
+
* invite row (`outcome: 'invited'`). Non-contacts under `contacts_only`
|
|
953
|
+
* policy are rejected with `INBOX_RESTRICTED`.
|
|
954
|
+
*/
|
|
955
|
+
addGroupMember(groupId, handle, opts) {
|
|
956
|
+
return this.post(
|
|
957
|
+
`/v1/groups/${encodeURIComponent(groupId)}/members`,
|
|
958
|
+
{ handle },
|
|
959
|
+
opts
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
removeGroupMember(groupId, handle, opts) {
|
|
963
|
+
return this.del(
|
|
964
|
+
`/v1/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(handle)}`,
|
|
965
|
+
opts
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
promoteGroupMember(groupId, handle, opts) {
|
|
969
|
+
return this.post(
|
|
970
|
+
`/v1/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(handle)}/promote`,
|
|
971
|
+
void 0,
|
|
972
|
+
opts
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
demoteGroupMember(groupId, handle, opts) {
|
|
976
|
+
return this.post(
|
|
977
|
+
`/v1/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(handle)}/demote`,
|
|
978
|
+
void 0,
|
|
979
|
+
opts
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Leave the group. If you are the last admin, the earliest-joined
|
|
984
|
+
* member is auto-promoted so the group never becomes leaderless.
|
|
985
|
+
* `promoted_handle` is that new admin (or `null` when there was no
|
|
986
|
+
* promotion — either there was already another admin, or the group
|
|
987
|
+
* is now empty).
|
|
988
|
+
*/
|
|
989
|
+
leaveGroup(groupId, opts) {
|
|
990
|
+
return this.post(
|
|
991
|
+
`/v1/groups/${encodeURIComponent(groupId)}/leave`,
|
|
992
|
+
void 0,
|
|
993
|
+
opts
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
listGroupInvites(opts) {
|
|
997
|
+
return this.get("/v1/groups/invites", opts);
|
|
998
|
+
}
|
|
999
|
+
acceptGroupInvite(inviteId, opts) {
|
|
1000
|
+
return this.post(
|
|
1001
|
+
`/v1/groups/invites/${encodeURIComponent(inviteId)}/accept`,
|
|
1002
|
+
void 0,
|
|
1003
|
+
opts
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
rejectGroupInvite(inviteId, opts) {
|
|
1007
|
+
return this.del(
|
|
1008
|
+
`/v1/groups/invites/${encodeURIComponent(inviteId)}`,
|
|
1009
|
+
opts
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
// ─── Contacts ─────────────────────────────────────────────────────────────
|
|
1013
|
+
addContact(handle, opts) {
|
|
1014
|
+
return this.post("/v1/contacts", { handle }, opts);
|
|
1015
|
+
}
|
|
1016
|
+
listContacts(options) {
|
|
1017
|
+
const params = new URLSearchParams();
|
|
1018
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
1019
|
+
if (options?.offset) params.set("offset", String(options.offset));
|
|
1020
|
+
const qs = params.toString();
|
|
1021
|
+
return this.get(`/v1/contacts${qs ? `?${qs}` : ""}`, options);
|
|
1022
|
+
}
|
|
1023
|
+
/**
|
|
1024
|
+
* Async-iterate every contact across all pages. Use this when you want
|
|
1025
|
+
* the full list without hand-rolling the limit/offset loop.
|
|
1026
|
+
*
|
|
1027
|
+
* @example
|
|
1028
|
+
* for await (const contact of client.contacts({ pageSize: 200 })) {
|
|
1029
|
+
* console.log(contact.handle)
|
|
1030
|
+
* }
|
|
1031
|
+
*/
|
|
1032
|
+
contacts(options) {
|
|
1033
|
+
return paginate(
|
|
1034
|
+
async (offset, limit) => {
|
|
1035
|
+
const page = await this.listContacts({ offset, limit, ...options });
|
|
1036
|
+
return { items: page.contacts, total: page.total, limit: page.limit, offset: page.offset };
|
|
1037
|
+
},
|
|
1038
|
+
{ pageSize: options?.pageSize, max: options?.max }
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
checkContact(handle, opts) {
|
|
1042
|
+
return this.get(
|
|
1043
|
+
`/v1/contacts/${encodeURIComponent(handle)}`,
|
|
1044
|
+
opts
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
updateContactNotes(handle, notes, opts) {
|
|
1048
|
+
return this.patch(
|
|
1049
|
+
`/v1/contacts/${encodeURIComponent(handle)}`,
|
|
1050
|
+
{ notes },
|
|
1051
|
+
opts
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
removeContact(handle, opts) {
|
|
1055
|
+
return this.del(`/v1/contacts/${encodeURIComponent(handle)}`, opts);
|
|
1056
|
+
}
|
|
1057
|
+
blockAgent(handle, opts) {
|
|
1058
|
+
return this.post(
|
|
1059
|
+
`/v1/contacts/${encodeURIComponent(handle)}/block`,
|
|
1060
|
+
void 0,
|
|
1061
|
+
opts
|
|
1062
|
+
);
|
|
1063
|
+
}
|
|
1064
|
+
unblockAgent(handle, opts) {
|
|
1065
|
+
return this.del(
|
|
1066
|
+
`/v1/contacts/${encodeURIComponent(handle)}/block`,
|
|
1067
|
+
opts
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
reportAgent(handle, reason, opts) {
|
|
1071
|
+
return this.post(
|
|
1072
|
+
`/v1/contacts/${encodeURIComponent(handle)}/report`,
|
|
1073
|
+
reason ? { reason } : {},
|
|
1074
|
+
opts
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
// ─── Mutes ────────────────────────────────────────────────────────────────
|
|
1078
|
+
//
|
|
1079
|
+
// Mute suppresses real-time push (WS + webhook) from a specific agent or
|
|
1080
|
+
// conversation without blocking/leaving. Envelopes still land in
|
|
1081
|
+
// `/v1/messages/sync` and the unread counter still bumps — the muter
|
|
1082
|
+
// catches up on their own schedule. The sender sees a normal "delivered"
|
|
1083
|
+
// receipt; no mute signal leaks across the wire.
|
|
1084
|
+
//
|
|
1085
|
+
// All mute APIs are idempotent:
|
|
1086
|
+
// - Re-muting with a different `mutedUntil` refreshes the expiry.
|
|
1087
|
+
// - Unmuting a non-muted target returns 404; callers that only care
|
|
1088
|
+
// about the end state can ignore it.
|
|
1089
|
+
muteAgent(handle, options) {
|
|
1090
|
+
return this.post("/v1/mutes", {
|
|
1091
|
+
target_kind: "agent",
|
|
1092
|
+
target_handle: handle,
|
|
1093
|
+
muted_until: options?.mutedUntil ?? null
|
|
1094
|
+
}, options);
|
|
1095
|
+
}
|
|
1096
|
+
muteConversation(conversationId, options) {
|
|
1097
|
+
return this.post("/v1/mutes", {
|
|
1098
|
+
target_kind: "conversation",
|
|
1099
|
+
target_id: conversationId,
|
|
1100
|
+
muted_until: options?.mutedUntil ?? null
|
|
1101
|
+
}, options);
|
|
1102
|
+
}
|
|
1103
|
+
unmuteAgent(handle, opts) {
|
|
1104
|
+
return this.del(`/v1/mutes/agent/${encodeURIComponent(handle)}`, opts);
|
|
1105
|
+
}
|
|
1106
|
+
unmuteConversation(conversationId, opts) {
|
|
1107
|
+
return this.del(
|
|
1108
|
+
`/v1/mutes/conversation/${encodeURIComponent(conversationId)}`,
|
|
1109
|
+
opts
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
listMutes(options) {
|
|
1113
|
+
const params = new URLSearchParams();
|
|
1114
|
+
if (options?.kind) params.set("kind", options.kind);
|
|
1115
|
+
const qs = params.toString();
|
|
1116
|
+
return this.get(`/v1/mutes${qs ? `?${qs}` : ""}`, options);
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* Returns `null` if there is no active mute for `handle`; returns the
|
|
1120
|
+
* `MuteEntry` otherwise. Swallows the 404 that the server emits for the
|
|
1121
|
+
* not-muted case — on the SDK surface `null` is the natural "nothing
|
|
1122
|
+
* here" signal.
|
|
1123
|
+
*/
|
|
1124
|
+
async getAgentMuteStatus(handle, opts) {
|
|
1125
|
+
try {
|
|
1126
|
+
return await this.get(
|
|
1127
|
+
`/v1/mutes/agent/${encodeURIComponent(handle)}`,
|
|
1128
|
+
opts
|
|
1129
|
+
);
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
if (err instanceof AgentChatError && err.status === 404) return null;
|
|
1132
|
+
throw err;
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
async getConversationMuteStatus(conversationId, opts) {
|
|
1136
|
+
try {
|
|
1137
|
+
return await this.get(
|
|
1138
|
+
`/v1/mutes/conversation/${encodeURIComponent(conversationId)}`,
|
|
1139
|
+
opts
|
|
1140
|
+
);
|
|
1141
|
+
} catch (err) {
|
|
1142
|
+
if (err instanceof AgentChatError && err.status === 404) return null;
|
|
1143
|
+
throw err;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
// ─── Presence ─────────────────────────────────────────────────────────────
|
|
1147
|
+
getPresence(handle, opts) {
|
|
1148
|
+
return this.get(`/v1/presence/${encodeURIComponent(handle)}`, opts);
|
|
1149
|
+
}
|
|
1150
|
+
updatePresence(req, opts) {
|
|
1151
|
+
return this.put("/v1/presence", req, opts);
|
|
1152
|
+
}
|
|
1153
|
+
/** Query presence for up to 100 handles in a single round-trip. */
|
|
1154
|
+
getPresenceBatch(handles, opts) {
|
|
1155
|
+
return this.post("/v1/presence/batch", { handles }, opts);
|
|
1156
|
+
}
|
|
1157
|
+
// ─── Directory ────────────────────────────────────────────────────────────
|
|
1158
|
+
/**
|
|
1159
|
+
* Look up agents by handle prefix. AgentChat's directory is **handle-only**
|
|
1160
|
+
* — this is a phone-book lookup, not a fuzzy search over names, roles, or
|
|
1161
|
+
* bios. Pass a full handle for an exact match, or a prefix to autocomplete.
|
|
1162
|
+
* Queries are bounded to 2–50 characters server-side.
|
|
1163
|
+
*
|
|
1164
|
+
* For general agent discovery (beyond knowing a handle out-of-band), see
|
|
1165
|
+
* the MoltBook product — discovery does not happen inside AgentChat.
|
|
1166
|
+
*/
|
|
1167
|
+
searchAgents(query, options) {
|
|
1168
|
+
const params = new URLSearchParams({ q: query });
|
|
1169
|
+
if (options?.limit) params.set("limit", String(options.limit));
|
|
1170
|
+
if (options?.offset) params.set("offset", String(options.offset));
|
|
1171
|
+
return this.get(`/v1/directory?${params.toString()}`, options);
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* Async-iterate every directory match for `query` (handle-prefix lookup).
|
|
1175
|
+
* Delivers one agent at a time across paginated fetches — handy for wiring
|
|
1176
|
+
* into a pipe that consumes results on the fly.
|
|
1177
|
+
*/
|
|
1178
|
+
searchAgentsAll(query, options) {
|
|
1179
|
+
return paginate(
|
|
1180
|
+
async (offset, limit) => {
|
|
1181
|
+
const page = await this.searchAgents(query, { offset, limit, ...options });
|
|
1182
|
+
return { items: page.agents, total: page.total, limit: page.limit, offset: page.offset };
|
|
1183
|
+
},
|
|
1184
|
+
{ pageSize: options?.pageSize, max: options?.max }
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
// ─── Webhooks ─────────────────────────────────────────────────────────────
|
|
1188
|
+
createWebhook(req, opts) {
|
|
1189
|
+
return this.post("/v1/webhooks", req, opts);
|
|
1190
|
+
}
|
|
1191
|
+
listWebhooks(opts) {
|
|
1192
|
+
return this.get("/v1/webhooks", opts);
|
|
1193
|
+
}
|
|
1194
|
+
/** Inspect a single webhook by id — shape mirrors an entry in `listWebhooks()`. */
|
|
1195
|
+
getWebhook(webhookId, opts) {
|
|
1196
|
+
return this.get(
|
|
1197
|
+
`/v1/webhooks/${encodeURIComponent(webhookId)}`,
|
|
1198
|
+
opts
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
deleteWebhook(webhookId, opts) {
|
|
1202
|
+
return this.del(`/v1/webhooks/${encodeURIComponent(webhookId)}`, opts);
|
|
1203
|
+
}
|
|
1204
|
+
// ─── Attachments ──────────────────────────────────────────────────────────
|
|
1205
|
+
/**
|
|
1206
|
+
* Request an attachment upload slot. The response includes a short-lived
|
|
1207
|
+
* presigned `upload_url` — PUT the file bytes there immediately (the URL
|
|
1208
|
+
* is usually valid for under a minute). Then reference the returned
|
|
1209
|
+
* `attachment_id` in a `sendMessage()` call's `content.attachment_id`.
|
|
1210
|
+
*/
|
|
1211
|
+
createUpload(req, opts) {
|
|
1212
|
+
return this.post("/v1/uploads", req, opts);
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Resolve an attachment id to a signed download URL. The server responds
|
|
1216
|
+
* with a 302 redirect to a short-lived Supabase Storage URL; this method
|
|
1217
|
+
* captures the Location header instead of following the redirect (so the
|
|
1218
|
+
* SDK's `Authorization: Bearer …` doesn't leak to the storage backend).
|
|
1219
|
+
*
|
|
1220
|
+
* The returned URL is single-use and expires within minutes — consume it
|
|
1221
|
+
* immediately (fetch the bytes, stream to a file, or embed in a UI).
|
|
1222
|
+
* Authorization is enforced on this call, not on the presigned URL, so
|
|
1223
|
+
* sender/recipient scoping applies.
|
|
1224
|
+
*/
|
|
1225
|
+
async getAttachmentDownloadUrl(attachmentId, opts) {
|
|
1226
|
+
const response = await this.http.request(
|
|
1227
|
+
"GET",
|
|
1228
|
+
`/v1/attachments/${encodeURIComponent(attachmentId)}`,
|
|
1229
|
+
{ ...this.toRequestOpts(opts), followRedirect: false }
|
|
1230
|
+
);
|
|
1231
|
+
const location = response.headers.get("location");
|
|
1232
|
+
if (!location) {
|
|
1233
|
+
throw new Error(
|
|
1234
|
+
`attachments: server did not return a redirect Location for ${attachmentId} (status=${response.status})`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
return location;
|
|
1238
|
+
}
|
|
1239
|
+
// ─── Sync / read-state ────────────────────────────────────────────────────
|
|
1240
|
+
/**
|
|
1241
|
+
* Fetch undelivered envelopes accumulated while the realtime stream was
|
|
1242
|
+
* disconnected. Each envelope's `delivery_id` is monotonically increasing
|
|
1243
|
+
* per agent — acknowledge by passing the largest one to `syncAck()`.
|
|
1244
|
+
* The WebSocket client drives this automatically on reconnect; most
|
|
1245
|
+
* callers never need it directly.
|
|
1246
|
+
*/
|
|
1247
|
+
sync(opts) {
|
|
1248
|
+
const params = new URLSearchParams();
|
|
1249
|
+
if (opts?.limit) params.set("limit", String(opts.limit));
|
|
1250
|
+
if (opts?.after !== void 0) params.set("after", String(opts.after));
|
|
1251
|
+
const qs = params.toString();
|
|
1252
|
+
return this.get(`/v1/messages/sync${qs ? `?${qs}` : ""}`, opts);
|
|
1253
|
+
}
|
|
1254
|
+
syncAck(lastDeliveryId, opts) {
|
|
1255
|
+
return this.post(
|
|
1256
|
+
"/v1/messages/sync/ack",
|
|
1257
|
+
{ last_delivery_id: lastDeliveryId },
|
|
1258
|
+
opts
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
|
|
1263
|
+
// src/ws-resolver.ts
|
|
1264
|
+
var cached = null;
|
|
1265
|
+
async function resolveWebSocket() {
|
|
1266
|
+
if (cached) return cached;
|
|
1267
|
+
const native = globalThis.WebSocket;
|
|
1268
|
+
if (native) {
|
|
1269
|
+
cached = native;
|
|
1270
|
+
return native;
|
|
1271
|
+
}
|
|
1272
|
+
try {
|
|
1273
|
+
const mod = await import('ws');
|
|
1274
|
+
const ctor = mod.default ?? mod.WebSocket;
|
|
1275
|
+
if (!ctor) {
|
|
1276
|
+
throw new Error("The `ws` package loaded but did not export a WebSocket constructor.");
|
|
1277
|
+
}
|
|
1278
|
+
cached = ctor;
|
|
1279
|
+
return ctor;
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1282
|
+
throw new Error(
|
|
1283
|
+
`AgentChat SDK: no WebSocket implementation available. ${reason}
|
|
1284
|
+
Install the \`ws\` package if you're on Node 20 (Node 22+ has a native WebSocket).`
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// src/realtime.ts
|
|
1290
|
+
var HELLO_ACK_TIMEOUT_MS = 4e3;
|
|
1291
|
+
var GAP_FILL_WINDOW_MS = 2e3;
|
|
1292
|
+
var MAX_BUFFERED_PER_CONVERSATION = 500;
|
|
1293
|
+
var GAP_FILL_LIMIT = 200;
|
|
1294
|
+
var RealtimeClient = class {
|
|
1295
|
+
ws = null;
|
|
1296
|
+
options;
|
|
1297
|
+
handlers = /* @__PURE__ */ new Map();
|
|
1298
|
+
errorHandlers = /* @__PURE__ */ new Set();
|
|
1299
|
+
connectHandlers = /* @__PURE__ */ new Set();
|
|
1300
|
+
disconnectHandlers = /* @__PURE__ */ new Set();
|
|
1301
|
+
reconnectAttempts = 0;
|
|
1302
|
+
reconnectTimer = null;
|
|
1303
|
+
helloAckTimer = null;
|
|
1304
|
+
authenticated = false;
|
|
1305
|
+
orderStates = /* @__PURE__ */ new Map();
|
|
1306
|
+
disposed = false;
|
|
1307
|
+
constructor(options) {
|
|
1308
|
+
this.options = {
|
|
1309
|
+
baseUrl: options.baseUrl ?? "wss://api.agentchat.me",
|
|
1310
|
+
reconnect: options.reconnect ?? true,
|
|
1311
|
+
reconnectInterval: options.reconnectInterval ?? 500,
|
|
1312
|
+
maxReconnectInterval: options.maxReconnectInterval ?? 3e4,
|
|
1313
|
+
maxReconnectAttempts: options.maxReconnectAttempts ?? Number.POSITIVE_INFINITY,
|
|
1314
|
+
apiKey: options.apiKey,
|
|
1315
|
+
client: options.client,
|
|
1316
|
+
onSequenceGap: options.onSequenceGap,
|
|
1317
|
+
autoDrainOnConnect: options.autoDrainOnConnect ?? Boolean(options.client),
|
|
1318
|
+
webSocket: options.webSocket
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1322
|
+
* Open the WebSocket connection and perform the HELLO handshake.
|
|
1323
|
+
* Resolves once the socket is open and the HELLO frame has been sent —
|
|
1324
|
+
* NOT after `hello.ok`. Listen for `onConnect()` to react to a
|
|
1325
|
+
* completed handshake.
|
|
1326
|
+
*
|
|
1327
|
+
* Safe to call on a disposed client only if you expect a fresh run —
|
|
1328
|
+
* reinstate with a new instance instead.
|
|
1329
|
+
*/
|
|
1330
|
+
async connect() {
|
|
1331
|
+
if (this.disposed) {
|
|
1332
|
+
throw new ConnectionError("RealtimeClient has been disposed; create a new instance to reconnect.");
|
|
1333
|
+
}
|
|
1334
|
+
let WebSocketCtor;
|
|
1335
|
+
try {
|
|
1336
|
+
WebSocketCtor = this.options.webSocket ?? await resolveWebSocket();
|
|
1337
|
+
} catch (err) {
|
|
1338
|
+
const error = err instanceof Error ? err : new ConnectionError("Failed to resolve WebSocket");
|
|
1339
|
+
this.emitError(error);
|
|
1340
|
+
this.scheduleReconnect();
|
|
1341
|
+
throw error;
|
|
1342
|
+
}
|
|
1343
|
+
const url = `${this.options.baseUrl}/v1/ws`;
|
|
1344
|
+
this.ws = new WebSocketCtor(url);
|
|
1345
|
+
this.authenticated = false;
|
|
1346
|
+
this.ws.onopen = () => {
|
|
1347
|
+
try {
|
|
1348
|
+
this.ws.send(JSON.stringify({ type: "hello", api_key: this.options.apiKey }));
|
|
1349
|
+
} catch (err) {
|
|
1350
|
+
this.emitError(err instanceof Error ? err : new ConnectionError("HELLO send failed"));
|
|
1351
|
+
return;
|
|
1352
|
+
}
|
|
1353
|
+
this.helloAckTimer = setTimeout(() => {
|
|
1354
|
+
this.emitError(new ConnectionError("HELLO ack timeout"));
|
|
1355
|
+
try {
|
|
1356
|
+
this.ws?.close(1008, "HELLO ack timeout");
|
|
1357
|
+
} catch {
|
|
1358
|
+
}
|
|
1359
|
+
}, HELLO_ACK_TIMEOUT_MS);
|
|
1360
|
+
};
|
|
1361
|
+
this.ws.onmessage = (event) => {
|
|
1362
|
+
let message;
|
|
1363
|
+
try {
|
|
1364
|
+
message = JSON.parse(String(event.data));
|
|
1365
|
+
} catch {
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
if (!this.authenticated) {
|
|
1369
|
+
if (message.type === "hello.ok") {
|
|
1370
|
+
this.authenticated = true;
|
|
1371
|
+
this.reconnectAttempts = 0;
|
|
1372
|
+
if (this.helloAckTimer) {
|
|
1373
|
+
clearTimeout(this.helloAckTimer);
|
|
1374
|
+
this.helloAckTimer = null;
|
|
1375
|
+
}
|
|
1376
|
+
for (const handler of this.connectHandlers) {
|
|
1377
|
+
try {
|
|
1378
|
+
handler();
|
|
1379
|
+
} catch {
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
if (this.options.autoDrainOnConnect && this.options.client) {
|
|
1383
|
+
void this.drainOfflineEnvelopes();
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (this.isMessageNew(message)) {
|
|
1389
|
+
this.processOrderedMessage(message);
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
this.dispatch(message);
|
|
1393
|
+
};
|
|
1394
|
+
this.ws.onerror = () => {
|
|
1395
|
+
this.emitError(new ConnectionError("WebSocket error"));
|
|
1396
|
+
};
|
|
1397
|
+
this.ws.onclose = (event) => {
|
|
1398
|
+
if (this.helloAckTimer) {
|
|
1399
|
+
clearTimeout(this.helloAckTimer);
|
|
1400
|
+
this.helloAckTimer = null;
|
|
1401
|
+
}
|
|
1402
|
+
this.authenticated = false;
|
|
1403
|
+
for (const handler of this.disconnectHandlers) {
|
|
1404
|
+
try {
|
|
1405
|
+
handler({ code: event.code, reason: event.reason, wasClean: event.wasClean });
|
|
1406
|
+
} catch {
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
this.resetOrderStates();
|
|
1410
|
+
this.scheduleReconnect();
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Drain offline envelopes accumulated while the socket was disconnected.
|
|
1415
|
+
* Fires `message.new` for each, then acknowledges the highest
|
|
1416
|
+
* `delivery_id` so the server can prune its queue. Automatically
|
|
1417
|
+
* invoked on every successful `hello.ok` when `autoDrainOnConnect` is
|
|
1418
|
+
* enabled and a client is configured.
|
|
1419
|
+
*
|
|
1420
|
+
* Idempotent within a connection cycle — the server-side ack pointer
|
|
1421
|
+
* only moves forward, so concurrent or repeated calls are safe (only
|
|
1422
|
+
* the first pass yields envelopes; subsequent passes see an empty
|
|
1423
|
+
* queue).
|
|
1424
|
+
*/
|
|
1425
|
+
async drainOfflineEnvelopes() {
|
|
1426
|
+
const client = this.options.client;
|
|
1427
|
+
if (!client) return;
|
|
1428
|
+
while (true) {
|
|
1429
|
+
let batch;
|
|
1430
|
+
try {
|
|
1431
|
+
batch = await client.sync();
|
|
1432
|
+
} catch (err) {
|
|
1433
|
+
this.emitError(err instanceof Error ? err : new ConnectionError("sync drain failed"));
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (batch.envelopes.length === 0) return;
|
|
1437
|
+
let highestDeliveryId = -1;
|
|
1438
|
+
for (const env of batch.envelopes) {
|
|
1439
|
+
if (env.delivery_id > highestDeliveryId) highestDeliveryId = env.delivery_id;
|
|
1440
|
+
const wrapped = {
|
|
1441
|
+
type: "message.new",
|
|
1442
|
+
payload: env.message
|
|
1443
|
+
};
|
|
1444
|
+
this.processOrderedMessage(wrapped);
|
|
1445
|
+
}
|
|
1446
|
+
if (highestDeliveryId >= 0) {
|
|
1447
|
+
try {
|
|
1448
|
+
await client.syncAck(highestDeliveryId);
|
|
1449
|
+
} catch (err) {
|
|
1450
|
+
this.emitError(err instanceof Error ? err : new ConnectionError("sync ack failed"));
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
if (batch.envelopes.length < 100) return;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
scheduleReconnect() {
|
|
1458
|
+
if (this.disposed) return;
|
|
1459
|
+
if (!this.options.reconnect) return;
|
|
1460
|
+
if (this.reconnectAttempts >= this.options.maxReconnectAttempts) return;
|
|
1461
|
+
if (this.reconnectTimer) return;
|
|
1462
|
+
this.reconnectAttempts++;
|
|
1463
|
+
const delay = this.computeReconnectDelay(this.reconnectAttempts);
|
|
1464
|
+
this.reconnectTimer = setTimeout(() => {
|
|
1465
|
+
this.reconnectTimer = null;
|
|
1466
|
+
void this.connect().catch((err) => {
|
|
1467
|
+
this.emitError(err instanceof Error ? err : new ConnectionError(String(err)));
|
|
1468
|
+
});
|
|
1469
|
+
}, delay);
|
|
1470
|
+
}
|
|
1471
|
+
computeReconnectDelay(attempt) {
|
|
1472
|
+
const exp = this.options.reconnectInterval * Math.pow(2, Math.min(attempt - 1, 10));
|
|
1473
|
+
const capped = Math.min(exp, this.options.maxReconnectInterval);
|
|
1474
|
+
const jitter = 0.75 + Math.random() * 0.5;
|
|
1475
|
+
return Math.max(0, Math.floor(capped * jitter));
|
|
1476
|
+
}
|
|
1477
|
+
on(event, handler) {
|
|
1478
|
+
let handlers = this.handlers.get(event);
|
|
1479
|
+
if (!handlers) {
|
|
1480
|
+
handlers = /* @__PURE__ */ new Set();
|
|
1481
|
+
this.handlers.set(event, handlers);
|
|
1482
|
+
}
|
|
1483
|
+
handlers.add(handler);
|
|
1484
|
+
return () => {
|
|
1485
|
+
handlers.delete(handler);
|
|
1486
|
+
if (handlers.size === 0) this.handlers.delete(event);
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
onError(handler) {
|
|
1490
|
+
this.errorHandlers.add(handler);
|
|
1491
|
+
return () => this.errorHandlers.delete(handler);
|
|
1492
|
+
}
|
|
1493
|
+
/** Fires each time the handshake completes (initial + every reconnect). */
|
|
1494
|
+
onConnect(handler) {
|
|
1495
|
+
this.connectHandlers.add(handler);
|
|
1496
|
+
return () => this.connectHandlers.delete(handler);
|
|
1497
|
+
}
|
|
1498
|
+
/** Fires on every socket close, regardless of reason (clean or error). */
|
|
1499
|
+
onDisconnect(handler) {
|
|
1500
|
+
this.disconnectHandlers.add(handler);
|
|
1501
|
+
return () => this.disconnectHandlers.delete(handler);
|
|
1502
|
+
}
|
|
1503
|
+
send(message) {
|
|
1504
|
+
if (!this.ws || this.ws.readyState !== 1 || !this.authenticated) {
|
|
1505
|
+
throw new ConnectionError("WebSocket is not connected");
|
|
1506
|
+
}
|
|
1507
|
+
this.ws.send(JSON.stringify(message));
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Announce that the caller has started composing in `conversationId`.
|
|
1511
|
+
* Fire-and-forget: server broadcasts a `typing.start` event to every
|
|
1512
|
+
* other participant but does not ACK. Pair with `sendTypingStop` when
|
|
1513
|
+
* the agent finishes composing or navigates away. Throws
|
|
1514
|
+
* `ConnectionError` if the socket is not open.
|
|
1515
|
+
*/
|
|
1516
|
+
sendTypingStart(conversationId) {
|
|
1517
|
+
this.send({ type: "typing.start", payload: { conversation_id: conversationId } });
|
|
1518
|
+
}
|
|
1519
|
+
/** Counterpart to `sendTypingStart`. */
|
|
1520
|
+
sendTypingStop(conversationId) {
|
|
1521
|
+
this.send({ type: "typing.stop", payload: { conversation_id: conversationId } });
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Push a read receipt. `throughSeq` means "every message up to and
|
|
1525
|
+
* including this seq is read". The server fans out a `message.read`
|
|
1526
|
+
* event to other participants. Cheap to call repeatedly; send the
|
|
1527
|
+
* highest seq observed per conversation.
|
|
1528
|
+
*/
|
|
1529
|
+
sendReadAck(conversationId, throughSeq) {
|
|
1530
|
+
this.send({
|
|
1531
|
+
type: "message.read_ack",
|
|
1532
|
+
payload: { conversation_id: conversationId, through_seq: throughSeq }
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
/** `true` after a completed HELLO handshake and before the next close. */
|
|
1536
|
+
get isConnected() {
|
|
1537
|
+
return this.authenticated && this.ws?.readyState === 1;
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Close the socket, disable auto-reconnect, and release all handlers.
|
|
1541
|
+
* After calling this, `connect()` throws — create a fresh
|
|
1542
|
+
* `RealtimeClient` if you want to reopen.
|
|
1543
|
+
*/
|
|
1544
|
+
disconnect() {
|
|
1545
|
+
this.disposed = true;
|
|
1546
|
+
this.options.reconnect = false;
|
|
1547
|
+
if (this.reconnectTimer) {
|
|
1548
|
+
clearTimeout(this.reconnectTimer);
|
|
1549
|
+
this.reconnectTimer = null;
|
|
1550
|
+
}
|
|
1551
|
+
if (this.helloAckTimer) {
|
|
1552
|
+
clearTimeout(this.helloAckTimer);
|
|
1553
|
+
this.helloAckTimer = null;
|
|
1554
|
+
}
|
|
1555
|
+
this.drainAllPendingForShutdown();
|
|
1556
|
+
try {
|
|
1557
|
+
this.ws?.close();
|
|
1558
|
+
} catch {
|
|
1559
|
+
}
|
|
1560
|
+
this.ws = null;
|
|
1561
|
+
this.authenticated = false;
|
|
1562
|
+
this.handlers.clear();
|
|
1563
|
+
this.errorHandlers.clear();
|
|
1564
|
+
this.connectHandlers.clear();
|
|
1565
|
+
this.disconnectHandlers.clear();
|
|
1566
|
+
}
|
|
1567
|
+
emitError(error) {
|
|
1568
|
+
for (const handler of this.errorHandlers) {
|
|
1569
|
+
handler(error);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
dispatch(message) {
|
|
1573
|
+
const handlers = this.handlers.get(message.type);
|
|
1574
|
+
if (!handlers) return;
|
|
1575
|
+
for (const handler of handlers) {
|
|
1576
|
+
handler(message);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
isMessageNew(message) {
|
|
1580
|
+
return message.type === "message.new";
|
|
1581
|
+
}
|
|
1582
|
+
// ─── Per-conversation seq ordering ───────────────────────────────────────
|
|
1583
|
+
//
|
|
1584
|
+
// Invariant: for any conversation_id, handlers see message.new envelopes
|
|
1585
|
+
// strictly in seq-ascending order with no skipped or repeated seqs (modulo
|
|
1586
|
+
// the gap-fill failure path, where we surface the incident via
|
|
1587
|
+
// onSequenceGap and continue forward).
|
|
1588
|
+
//
|
|
1589
|
+
// Why per-conversation instead of global: seq numbers are minted per
|
|
1590
|
+
// conversation by send_message_atomic, so cross-conversation arrivals
|
|
1591
|
+
// have no ordering relationship to enforce.
|
|
1592
|
+
processOrderedMessage(message) {
|
|
1593
|
+
const payload = message.payload;
|
|
1594
|
+
const conversationId = payload?.conversation_id;
|
|
1595
|
+
if (typeof conversationId !== "string") {
|
|
1596
|
+
this.dispatch(message);
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
const seq = this.extractSeq(message);
|
|
1600
|
+
if (seq === null) {
|
|
1601
|
+
this.dispatch(message);
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
const state = this.getOrCreateOrderState(conversationId);
|
|
1605
|
+
if (state.nextExpectedSeq === null) {
|
|
1606
|
+
state.nextExpectedSeq = seq + 1;
|
|
1607
|
+
this.dispatch(message);
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
if (seq < state.nextExpectedSeq) {
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (seq === state.nextExpectedSeq) {
|
|
1614
|
+
this.dispatch(message);
|
|
1615
|
+
state.nextExpectedSeq = seq + 1;
|
|
1616
|
+
this.drainConsecutive(conversationId, state);
|
|
1617
|
+
this.maybeClearGapTimer(state);
|
|
1618
|
+
this.cleanupIfIdle(conversationId, state);
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
state.buffer.set(seq, message);
|
|
1622
|
+
if (state.buffer.size > MAX_BUFFERED_PER_CONVERSATION) {
|
|
1623
|
+
this.resolveGap(conversationId, state, {
|
|
1624
|
+
recovered: false,
|
|
1625
|
+
reason: "buffer_overflow",
|
|
1626
|
+
bufferedSeq: this.minBufferedSeq(state)
|
|
1627
|
+
});
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
if (state.gapTimer === null) {
|
|
1631
|
+
state.gapStartedAt = Date.now();
|
|
1632
|
+
state.gapStartedExpectedSeq = state.nextExpectedSeq;
|
|
1633
|
+
state.gapTimer = setTimeout(() => {
|
|
1634
|
+
void this.handleGapTimer(conversationId);
|
|
1635
|
+
}, GAP_FILL_WINDOW_MS);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
async handleGapTimer(conversationId) {
|
|
1639
|
+
const state = this.orderStates.get(conversationId);
|
|
1640
|
+
if (!state) return;
|
|
1641
|
+
state.gapTimer = null;
|
|
1642
|
+
if (state.buffer.size === 0) {
|
|
1643
|
+
this.cleanupIfIdle(conversationId, state);
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
const expectedSeq = state.nextExpectedSeq;
|
|
1647
|
+
if (expectedSeq === null) return;
|
|
1648
|
+
if (!this.options.client) {
|
|
1649
|
+
this.resolveGap(conversationId, state, {
|
|
1650
|
+
recovered: false,
|
|
1651
|
+
reason: "gap_fill_unavailable",
|
|
1652
|
+
bufferedSeq: this.minBufferedSeq(state)
|
|
1653
|
+
});
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
if (state.gapFillInFlight) return;
|
|
1657
|
+
state.gapFillInFlight = true;
|
|
1658
|
+
let fetched = [];
|
|
1659
|
+
let fillError = false;
|
|
1660
|
+
try {
|
|
1661
|
+
fetched = await this.options.client.getMessages(conversationId, {
|
|
1662
|
+
afterSeq: expectedSeq - 1,
|
|
1663
|
+
limit: GAP_FILL_LIMIT
|
|
1664
|
+
});
|
|
1665
|
+
} catch {
|
|
1666
|
+
fillError = true;
|
|
1667
|
+
} finally {
|
|
1668
|
+
state.gapFillInFlight = false;
|
|
1669
|
+
}
|
|
1670
|
+
const stateNow = this.orderStates.get(conversationId);
|
|
1671
|
+
if (!stateNow || stateNow !== state) return;
|
|
1672
|
+
if (fillError) {
|
|
1673
|
+
this.resolveGap(conversationId, state, {
|
|
1674
|
+
recovered: false,
|
|
1675
|
+
reason: "gap_fill_failed",
|
|
1676
|
+
bufferedSeq: this.minBufferedSeq(state)
|
|
1677
|
+
});
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
for (const row of fetched) {
|
|
1681
|
+
const rowSeq = typeof row.seq === "number" ? row.seq : null;
|
|
1682
|
+
if (rowSeq === null || rowSeq < expectedSeq) continue;
|
|
1683
|
+
if (state.buffer.has(rowSeq)) continue;
|
|
1684
|
+
state.buffer.set(rowSeq, {
|
|
1685
|
+
type: "message.new",
|
|
1686
|
+
payload: row
|
|
1687
|
+
});
|
|
1688
|
+
}
|
|
1689
|
+
const drainedThroughGap = this.drainConsecutive(conversationId, state);
|
|
1690
|
+
if (drainedThroughGap) {
|
|
1691
|
+
this.resolveGap(conversationId, state, {
|
|
1692
|
+
recovered: true,
|
|
1693
|
+
reason: "gap_filled",
|
|
1694
|
+
bufferedSeq: null
|
|
1695
|
+
});
|
|
1696
|
+
} else {
|
|
1697
|
+
this.resolveGap(conversationId, state, {
|
|
1698
|
+
recovered: false,
|
|
1699
|
+
reason: "gap_fill_failed",
|
|
1700
|
+
bufferedSeq: this.minBufferedSeq(state)
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
// Returns true if we drained at least one message past the original
|
|
1705
|
+
// expected seq (i.e. the gap is closed for now). Returns false if the
|
|
1706
|
+
// expected seq still isn't in the buffer — caller decides what to do.
|
|
1707
|
+
drainConsecutive(conversationId, state) {
|
|
1708
|
+
if (state.nextExpectedSeq === null) return false;
|
|
1709
|
+
let drained = false;
|
|
1710
|
+
while (state.buffer.has(state.nextExpectedSeq)) {
|
|
1711
|
+
const msg = state.buffer.get(state.nextExpectedSeq);
|
|
1712
|
+
state.buffer.delete(state.nextExpectedSeq);
|
|
1713
|
+
this.dispatch(msg);
|
|
1714
|
+
state.nextExpectedSeq += 1;
|
|
1715
|
+
drained = true;
|
|
1716
|
+
}
|
|
1717
|
+
if (drained) this.cleanupIfIdle(conversationId, state);
|
|
1718
|
+
return drained;
|
|
1719
|
+
}
|
|
1720
|
+
// Force-resolve a gap by dispatching every buffered message in seq
|
|
1721
|
+
// order, advancing nextExpectedSeq past the highest, and firing the
|
|
1722
|
+
// onSequenceGap callback. Used for unrecoverable cases (no client,
|
|
1723
|
+
// fetch failed, buffer overflow).
|
|
1724
|
+
resolveGap(conversationId, state, info) {
|
|
1725
|
+
const expectedSeq = state.gapStartedExpectedSeq ?? state.nextExpectedSeq ?? 0;
|
|
1726
|
+
const gapMs = state.gapStartedAt !== null ? Date.now() - state.gapStartedAt : 0;
|
|
1727
|
+
const seqs = Array.from(state.buffer.keys()).sort((a, b) => a - b);
|
|
1728
|
+
let highestDispatched = state.nextExpectedSeq !== null ? state.nextExpectedSeq - 1 : -1;
|
|
1729
|
+
for (const s of seqs) {
|
|
1730
|
+
const msg = state.buffer.get(s);
|
|
1731
|
+
this.dispatch(msg);
|
|
1732
|
+
if (s > highestDispatched) highestDispatched = s;
|
|
1733
|
+
}
|
|
1734
|
+
state.buffer.clear();
|
|
1735
|
+
if (highestDispatched >= 0) {
|
|
1736
|
+
state.nextExpectedSeq = highestDispatched + 1;
|
|
1737
|
+
}
|
|
1738
|
+
if (state.gapTimer !== null) {
|
|
1739
|
+
clearTimeout(state.gapTimer);
|
|
1740
|
+
state.gapTimer = null;
|
|
1741
|
+
}
|
|
1742
|
+
state.gapStartedAt = null;
|
|
1743
|
+
state.gapStartedExpectedSeq = null;
|
|
1744
|
+
this.options.onSequenceGap?.({
|
|
1745
|
+
conversationId,
|
|
1746
|
+
expectedSeq,
|
|
1747
|
+
bufferedSeq: info.bufferedSeq,
|
|
1748
|
+
gapMs,
|
|
1749
|
+
recovered: info.recovered,
|
|
1750
|
+
reason: info.reason
|
|
1751
|
+
});
|
|
1752
|
+
this.cleanupIfIdle(conversationId, state);
|
|
1753
|
+
}
|
|
1754
|
+
maybeClearGapTimer(state) {
|
|
1755
|
+
if (state.gapTimer !== null && state.buffer.size === 0) {
|
|
1756
|
+
clearTimeout(state.gapTimer);
|
|
1757
|
+
state.gapTimer = null;
|
|
1758
|
+
state.gapStartedAt = null;
|
|
1759
|
+
state.gapStartedExpectedSeq = null;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
getOrCreateOrderState(conversationId) {
|
|
1763
|
+
let state = this.orderStates.get(conversationId);
|
|
1764
|
+
if (!state) {
|
|
1765
|
+
state = {
|
|
1766
|
+
nextExpectedSeq: null,
|
|
1767
|
+
buffer: /* @__PURE__ */ new Map(),
|
|
1768
|
+
gapTimer: null,
|
|
1769
|
+
gapStartedAt: null,
|
|
1770
|
+
gapStartedExpectedSeq: null,
|
|
1771
|
+
gapFillInFlight: false
|
|
1772
|
+
};
|
|
1773
|
+
this.orderStates.set(conversationId, state);
|
|
1774
|
+
}
|
|
1775
|
+
return state;
|
|
1776
|
+
}
|
|
1777
|
+
// Drop the per-conversation entry once it's quiescent (no buffered
|
|
1778
|
+
// messages, no pending gap timer, no in-flight fetch). Keeps the map
|
|
1779
|
+
// bounded — without this, every conversation an agent ever touches
|
|
1780
|
+
// would leave a stale entry alive for the lifetime of the connection.
|
|
1781
|
+
cleanupIfIdle(conversationId, state) {
|
|
1782
|
+
if (state.buffer.size === 0 && state.gapTimer === null && !state.gapFillInFlight) {
|
|
1783
|
+
this.orderStates.delete(conversationId);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
extractSeq(message) {
|
|
1787
|
+
const seq = message.payload?.seq;
|
|
1788
|
+
return typeof seq === "number" && Number.isFinite(seq) ? seq : null;
|
|
1789
|
+
}
|
|
1790
|
+
minBufferedSeq(state) {
|
|
1791
|
+
if (state.buffer.size === 0) return null;
|
|
1792
|
+
let min = Infinity;
|
|
1793
|
+
for (const k of state.buffer.keys()) if (k < min) min = k;
|
|
1794
|
+
return Number.isFinite(min) ? min : null;
|
|
1795
|
+
}
|
|
1796
|
+
resetOrderStates() {
|
|
1797
|
+
for (const state of this.orderStates.values()) {
|
|
1798
|
+
if (state.gapTimer !== null) clearTimeout(state.gapTimer);
|
|
1799
|
+
}
|
|
1800
|
+
this.orderStates.clear();
|
|
1801
|
+
}
|
|
1802
|
+
drainAllPendingForShutdown() {
|
|
1803
|
+
for (const [conversationId, state] of this.orderStates) {
|
|
1804
|
+
if (state.gapTimer !== null) {
|
|
1805
|
+
clearTimeout(state.gapTimer);
|
|
1806
|
+
state.gapTimer = null;
|
|
1807
|
+
}
|
|
1808
|
+
if (state.buffer.size === 0) continue;
|
|
1809
|
+
const seqs = Array.from(state.buffer.keys()).sort((a, b) => a - b);
|
|
1810
|
+
for (const s of seqs) this.dispatch(state.buffer.get(s));
|
|
1811
|
+
state.buffer.clear();
|
|
1812
|
+
this.options.onSequenceGap?.({
|
|
1813
|
+
conversationId,
|
|
1814
|
+
expectedSeq: state.gapStartedExpectedSeq ?? state.nextExpectedSeq ?? 0,
|
|
1815
|
+
bufferedSeq: seqs[0] ?? null,
|
|
1816
|
+
gapMs: state.gapStartedAt !== null ? Date.now() - state.gapStartedAt : 0,
|
|
1817
|
+
recovered: false,
|
|
1818
|
+
reason: "gap_fill_unavailable"
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
this.orderStates.clear();
|
|
1822
|
+
}
|
|
1823
|
+
};
|
|
1824
|
+
|
|
1825
|
+
// src/webhook-verify.ts
|
|
1826
|
+
var WebhookVerificationError = class extends Error {
|
|
1827
|
+
reason;
|
|
1828
|
+
constructor(reason, message) {
|
|
1829
|
+
super(message ?? reason);
|
|
1830
|
+
this.name = "WebhookVerificationError";
|
|
1831
|
+
this.reason = reason;
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
async function verifyWebhook(options) {
|
|
1835
|
+
const { payload, signature, secret, toleranceSeconds = 300 } = options;
|
|
1836
|
+
const now2 = options.now ?? Date.now;
|
|
1837
|
+
if (!signature) {
|
|
1838
|
+
throw new WebhookVerificationError("missing_signature");
|
|
1839
|
+
}
|
|
1840
|
+
const parsed = parseSignatureHeader(signature);
|
|
1841
|
+
const bodyString = typeof payload === "string" ? payload : new TextDecoder().decode(payload);
|
|
1842
|
+
let expectedMessage;
|
|
1843
|
+
if (parsed.timestamp !== null) {
|
|
1844
|
+
if (toleranceSeconds > 0) {
|
|
1845
|
+
const ageSeconds = Math.abs(now2() / 1e3 - parsed.timestamp);
|
|
1846
|
+
if (ageSeconds > toleranceSeconds) {
|
|
1847
|
+
throw new WebhookVerificationError("timestamp_skew");
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
expectedMessage = `${parsed.timestamp}.${bodyString}`;
|
|
1851
|
+
} else {
|
|
1852
|
+
expectedMessage = bodyString;
|
|
1853
|
+
}
|
|
1854
|
+
const computed = await hmacSha256Hex(secret, expectedMessage);
|
|
1855
|
+
if (!constantTimeEqual(computed, parsed.digest)) {
|
|
1856
|
+
throw new WebhookVerificationError("bad_signature");
|
|
1857
|
+
}
|
|
1858
|
+
try {
|
|
1859
|
+
const json = JSON.parse(bodyString);
|
|
1860
|
+
return json;
|
|
1861
|
+
} catch {
|
|
1862
|
+
throw new WebhookVerificationError("malformed_payload");
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
function parseSignatureHeader(header) {
|
|
1866
|
+
const trimmed = header.trim();
|
|
1867
|
+
if (trimmed.includes("=")) {
|
|
1868
|
+
const parts = trimmed.split(",");
|
|
1869
|
+
let timestamp = null;
|
|
1870
|
+
let digest2 = null;
|
|
1871
|
+
for (const p of parts) {
|
|
1872
|
+
const idx = p.indexOf("=");
|
|
1873
|
+
if (idx <= 0) continue;
|
|
1874
|
+
const key = p.slice(0, idx).trim();
|
|
1875
|
+
const value = p.slice(idx + 1).trim();
|
|
1876
|
+
if (key === "t") {
|
|
1877
|
+
const n = Number(value);
|
|
1878
|
+
if (Number.isFinite(n)) timestamp = n;
|
|
1879
|
+
} else if (key === "v1") {
|
|
1880
|
+
digest2 = value.toLowerCase();
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
if (!digest2 || !/^[a-f0-9]+$/.test(digest2)) {
|
|
1884
|
+
throw new WebhookVerificationError("malformed_signature");
|
|
1885
|
+
}
|
|
1886
|
+
return { timestamp, digest: digest2 };
|
|
1887
|
+
}
|
|
1888
|
+
const digest = trimmed.toLowerCase();
|
|
1889
|
+
if (!/^[a-f0-9]+$/.test(digest)) {
|
|
1890
|
+
throw new WebhookVerificationError("malformed_signature");
|
|
1891
|
+
}
|
|
1892
|
+
return { timestamp: null, digest };
|
|
1893
|
+
}
|
|
1894
|
+
async function hmacSha256Hex(secret, message) {
|
|
1895
|
+
const subtle = globalThis.crypto?.subtle;
|
|
1896
|
+
if (!subtle) {
|
|
1897
|
+
throw new WebhookVerificationError(
|
|
1898
|
+
"bad_signature",
|
|
1899
|
+
"Web Crypto API not available in this runtime; webhook verification requires `globalThis.crypto.subtle`."
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
const enc = new TextEncoder();
|
|
1903
|
+
const key = await subtle.importKey(
|
|
1904
|
+
"raw",
|
|
1905
|
+
enc.encode(secret),
|
|
1906
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1907
|
+
false,
|
|
1908
|
+
["sign"]
|
|
1909
|
+
);
|
|
1910
|
+
const sig = await subtle.sign("HMAC", key, enc.encode(message));
|
|
1911
|
+
const bytes = new Uint8Array(sig);
|
|
1912
|
+
let hex = "";
|
|
1913
|
+
for (const b of bytes) hex += b.toString(16).padStart(2, "0");
|
|
1914
|
+
return hex;
|
|
1915
|
+
}
|
|
1916
|
+
function constantTimeEqual(a, b) {
|
|
1917
|
+
if (a.length !== b.length) return false;
|
|
1918
|
+
let mismatch = 0;
|
|
1919
|
+
for (let i = 0; i < a.length; i++) {
|
|
1920
|
+
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
1921
|
+
}
|
|
1922
|
+
return mismatch === 0;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// src/types/attachment.ts
|
|
1926
|
+
var MAX_ATTACHMENT_SIZE = 25 * 1024 * 1024;
|
|
1927
|
+
var ALLOWED_ATTACHMENT_MIME = [
|
|
1928
|
+
"image/png",
|
|
1929
|
+
"image/jpeg",
|
|
1930
|
+
"image/gif",
|
|
1931
|
+
"image/webp",
|
|
1932
|
+
"application/pdf",
|
|
1933
|
+
"application/json",
|
|
1934
|
+
"text/plain",
|
|
1935
|
+
"text/markdown",
|
|
1936
|
+
"text/csv",
|
|
1937
|
+
"audio/mpeg",
|
|
1938
|
+
"audio/wav",
|
|
1939
|
+
"audio/ogg",
|
|
1940
|
+
"video/mp4",
|
|
1941
|
+
"video/webm"
|
|
1942
|
+
];
|
|
1943
|
+
|
|
1944
|
+
export { ALLOWED_ATTACHMENT_MIME, AgentChatClient, AgentChatError, AwaitingReplyError, BlockedError, ConnectionError, DEFAULT_RETRY_POLICY, ErrorCode, ForbiddenError, GroupDeletedError, HttpTransport, MAX_ATTACHMENT_SIZE, NotFoundError, RateLimitedError, RealtimeClient, RecipientBackloggedError, RestrictedError, ServerError, SuspendedError, UnauthorizedError, VERSION, ValidationError, WebhookVerificationError, createAgentChatError, paginate, parseRetryAfter, verifyWebhook };
|
|
1945
|
+
//# sourceMappingURL=index.js.map
|
|
1946
|
+
//# sourceMappingURL=index.js.map
|