@channlize/core 0.2.1 → 0.5.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 +46 -0
- package/README.md +316 -15
- package/dist/index.d.mts +1355 -19
- package/dist/index.d.ts +1355 -19
- package/dist/index.js +1152 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1114 -31
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -6
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,48 @@
|
|
|
1
1
|
// src/http.ts
|
|
2
2
|
import fetch from "cross-fetch";
|
|
3
3
|
|
|
4
|
+
// src/shared.ts
|
|
5
|
+
var DEFAULT_BASE_URL = "https://api.channlize.com";
|
|
6
|
+
var DEFAULT_TIMEOUT = 1e4;
|
|
7
|
+
var EVENT_SEVERITIES = ["info", "warning", "error"];
|
|
8
|
+
var EVENT_KEY_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
9
|
+
function isValidEventKey(event) {
|
|
10
|
+
return typeof event === "string" && event.length <= 100 && EVENT_KEY_PATTERN.test(event);
|
|
11
|
+
}
|
|
12
|
+
function keyKind(key) {
|
|
13
|
+
return key.startsWith("wk_") ? "widget" : "server";
|
|
14
|
+
}
|
|
15
|
+
function clean(value) {
|
|
16
|
+
const v = value?.trim();
|
|
17
|
+
return v ? v : void 0;
|
|
18
|
+
}
|
|
19
|
+
function readChannlizeEnv(env = typeof process !== "undefined" ? process.env : void 0) {
|
|
20
|
+
return {
|
|
21
|
+
serverApiKey: clean(env?.CHANNLIZE_SERVER_API_KEY),
|
|
22
|
+
widgetKey: clean(env?.CHANNLIZE_WIDGET_KEY),
|
|
23
|
+
apiKey: clean(env?.CHANNLIZE_API_KEY),
|
|
24
|
+
projectId: clean(env?.CHANNLIZE_PROJECT_ID),
|
|
25
|
+
baseUrl: clean(env?.CHANNLIZE_BASE_URL),
|
|
26
|
+
timeout: clean(env?.CHANNLIZE_TIMEOUT),
|
|
27
|
+
agentId: clean(env?.CHANNLIZE_AGENT_ID)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function parseApiError(status, body, statusText = "") {
|
|
31
|
+
const data = body && typeof body === "object" ? body : {};
|
|
32
|
+
const raw = data.message;
|
|
33
|
+
const joined = Array.isArray(raw) ? raw.filter((m) => typeof m === "string").join(", ") : typeof raw === "string" ? raw : "";
|
|
34
|
+
const error = typeof data.error === "string" ? data.error : void 0;
|
|
35
|
+
const code = typeof data.code === "string" ? data.code : error;
|
|
36
|
+
const message = joined || (error && /\s/.test(error) ? error : "") || `HTTP ${status}: ${statusText}`.replace(/: $/, "");
|
|
37
|
+
return {
|
|
38
|
+
status,
|
|
39
|
+
message,
|
|
40
|
+
code,
|
|
41
|
+
requestId: typeof data.requestId === "string" ? data.requestId : void 0,
|
|
42
|
+
body: data
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
4
46
|
// src/types.ts
|
|
5
47
|
var ChannlizeError = class extends Error {
|
|
6
48
|
constructor(message, statusCode, code) {
|
|
@@ -11,6 +53,19 @@ var ChannlizeError = class extends Error {
|
|
|
11
53
|
}
|
|
12
54
|
statusCode;
|
|
13
55
|
code;
|
|
56
|
+
/** The API's `requestId` for a failed response, to quote to support. */
|
|
57
|
+
requestId;
|
|
58
|
+
/**
|
|
59
|
+
* The API's JSON error body, e.g. `limit`, `used`, `resetAt` and
|
|
60
|
+
* `upgradeUrl` on a plan-limit 429. Undefined for network errors.
|
|
61
|
+
*/
|
|
62
|
+
details;
|
|
63
|
+
/**
|
|
64
|
+
* ISO time the call may succeed again: the body's `retryAt` (agent
|
|
65
|
+
* policies, human takeovers), else derived from a `Retry-After` header.
|
|
66
|
+
* See `retryAfter(error)`.
|
|
67
|
+
*/
|
|
68
|
+
retryAt;
|
|
14
69
|
};
|
|
15
70
|
var ValidationError = class extends ChannlizeError {
|
|
16
71
|
constructor(message, field) {
|
|
@@ -27,17 +82,112 @@ var AuthenticationError = class extends ChannlizeError {
|
|
|
27
82
|
}
|
|
28
83
|
};
|
|
29
84
|
var RateLimitError = class extends ChannlizeError {
|
|
30
|
-
constructor(message = "Rate limit exceeded") {
|
|
31
|
-
super(message, 429,
|
|
85
|
+
constructor(message = "Rate limit exceeded", code = "RATE_LIMIT_ERROR") {
|
|
86
|
+
super(message, 429, code);
|
|
32
87
|
this.name = "RateLimitError";
|
|
33
88
|
}
|
|
34
89
|
};
|
|
35
90
|
|
|
91
|
+
// src/agents-errors.ts
|
|
92
|
+
var AGENT_POLICY_CODES = [
|
|
93
|
+
"POLICY_CHANNEL_NOT_ALLOWED",
|
|
94
|
+
"POLICY_DM_NOT_ALLOWED",
|
|
95
|
+
"POLICY_ASK_HUMAN_NOT_ALLOWED",
|
|
96
|
+
"POLICY_URGENCY_NOT_ALLOWED",
|
|
97
|
+
"POLICY_RATE_LIMITED",
|
|
98
|
+
"POLICY_BUDGET_EXHAUSTED",
|
|
99
|
+
"POLICY_OUTSIDE_WORKING_HOURS",
|
|
100
|
+
"POLICY_MAX_DEPTH"
|
|
101
|
+
];
|
|
102
|
+
var AGENT_TASK_ERROR_CODES = [
|
|
103
|
+
"TASK_CLOSED",
|
|
104
|
+
"ALREADY_ASSIGNED",
|
|
105
|
+
"AGENT_NOT_IN_CHANNEL",
|
|
106
|
+
"NOT_TASK_PARTICIPANT"
|
|
107
|
+
];
|
|
108
|
+
var AgentPolicyError = class extends ChannlizeError {
|
|
109
|
+
constructor(message, statusCode, code, retryAt) {
|
|
110
|
+
super(message, statusCode, code);
|
|
111
|
+
this.name = "AgentPolicyError";
|
|
112
|
+
this.retryAt = retryAt;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var HumanTakeoverError = class extends ChannlizeError {
|
|
116
|
+
constructor(message, retryAt) {
|
|
117
|
+
super(message, 409, "HUMAN_TAKEOVER");
|
|
118
|
+
this.name = "HumanTakeoverError";
|
|
119
|
+
this.retryAt = retryAt;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
var AgentThreadPausedError = class extends RateLimitError {
|
|
123
|
+
constructor(message, reason = "thread_cap") {
|
|
124
|
+
super(message, "AGENT_THREAD_PAUSED");
|
|
125
|
+
this.reason = reason;
|
|
126
|
+
this.name = "AgentThreadPausedError";
|
|
127
|
+
}
|
|
128
|
+
reason;
|
|
129
|
+
};
|
|
130
|
+
var AgentTaskError = class extends ChannlizeError {
|
|
131
|
+
constructor(message, statusCode, code) {
|
|
132
|
+
super(message, statusCode, code);
|
|
133
|
+
this.name = "AgentTaskError";
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
var QuestionClosedError = class extends ChannlizeError {
|
|
137
|
+
constructor(message) {
|
|
138
|
+
super(message, 409, "QUESTION_CLOSED");
|
|
139
|
+
this.name = "QuestionClosedError";
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
function agentErrorFor(status, message, code, body, retryAt) {
|
|
143
|
+
if (!code) return void 0;
|
|
144
|
+
if (code.startsWith("POLICY_")) {
|
|
145
|
+
return new AgentPolicyError(message, status, code, retryAt);
|
|
146
|
+
}
|
|
147
|
+
switch (code) {
|
|
148
|
+
case "HUMAN_TAKEOVER":
|
|
149
|
+
return new HumanTakeoverError(message, retryAt);
|
|
150
|
+
case "AGENT_THREAD_PAUSED":
|
|
151
|
+
return new AgentThreadPausedError(
|
|
152
|
+
message,
|
|
153
|
+
typeof body.reason === "string" ? body.reason : void 0
|
|
154
|
+
);
|
|
155
|
+
case "AGENT_RATE_LIMITED":
|
|
156
|
+
return new RateLimitError(message, code);
|
|
157
|
+
case "QUESTION_CLOSED":
|
|
158
|
+
return new QuestionClosedError(message);
|
|
159
|
+
default:
|
|
160
|
+
return AGENT_TASK_ERROR_CODES.includes(code) ? new AgentTaskError(message, status, code) : void 0;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function isRetryable(error) {
|
|
164
|
+
if (!(error instanceof ChannlizeError)) return false;
|
|
165
|
+
const { statusCode: status, code } = error;
|
|
166
|
+
if (code === "NETWORK_ERROR" || code === "TIMEOUT") return true;
|
|
167
|
+
if (status === void 0) return false;
|
|
168
|
+
if (status >= 500) return true;
|
|
169
|
+
if (error instanceof AgentPolicyError || error instanceof HumanTakeoverError) {
|
|
170
|
+
return Boolean(error.retryAt);
|
|
171
|
+
}
|
|
172
|
+
if (status === 429) return code !== "PLAN_LIMIT_REACHED";
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
function retryAfter(error, now = Date.now()) {
|
|
176
|
+
if (!isRetryable(error)) return void 0;
|
|
177
|
+
const at = error.retryAt;
|
|
178
|
+
const ms = at ? Date.parse(at) : NaN;
|
|
179
|
+
return Number.isFinite(ms) ? Math.max(0, ms - now) : void 0;
|
|
180
|
+
}
|
|
181
|
+
|
|
36
182
|
// src/http.ts
|
|
37
183
|
async function requestJson(http, endpoint, options = {}) {
|
|
38
184
|
const url = `${http.baseUrl}${endpoint}`;
|
|
39
185
|
const controller = new AbortController();
|
|
40
186
|
const timeoutId = setTimeout(() => controller.abort(), http.timeout);
|
|
187
|
+
const external = options.signal ?? void 0;
|
|
188
|
+
const onAbort = () => controller.abort();
|
|
189
|
+
if (external?.aborted) controller.abort();
|
|
190
|
+
else external?.addEventListener("abort", onAbort, { once: true });
|
|
41
191
|
try {
|
|
42
192
|
const mergedHeaders = {
|
|
43
193
|
"Content-Type": "application/json",
|
|
@@ -62,52 +212,93 @@ async function requestJson(http, endpoint, options = {}) {
|
|
|
62
212
|
if (error instanceof ChannlizeError) {
|
|
63
213
|
throw error;
|
|
64
214
|
}
|
|
215
|
+
if (external?.aborted) {
|
|
216
|
+
throw new ChannlizeError("Request aborted", void 0, "ABORTED");
|
|
217
|
+
}
|
|
65
218
|
if (error instanceof Error && error.name === "AbortError") {
|
|
66
219
|
throw new ChannlizeError("Request timeout", 408, "TIMEOUT");
|
|
67
220
|
}
|
|
68
221
|
throw new ChannlizeError(
|
|
69
|
-
`Network error: ${error instanceof Error ? error.message : "Unknown error"}
|
|
222
|
+
`Network error: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
223
|
+
void 0,
|
|
224
|
+
"NETWORK_ERROR"
|
|
70
225
|
);
|
|
226
|
+
} finally {
|
|
227
|
+
external?.removeEventListener("abort", onAbort);
|
|
71
228
|
}
|
|
72
229
|
}
|
|
230
|
+
function retryAfterHeader(value) {
|
|
231
|
+
if (!value) return void 0;
|
|
232
|
+
const seconds = Number(value);
|
|
233
|
+
const ms = Number.isFinite(seconds) ? Date.now() + seconds * 1e3 : Date.parse(value);
|
|
234
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : void 0;
|
|
235
|
+
}
|
|
73
236
|
async function handleErrorResponse(response) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (contentType?.includes("application/json")) {
|
|
237
|
+
let body = {};
|
|
238
|
+
if (response.headers.get("content-type")?.includes("application/json")) {
|
|
77
239
|
try {
|
|
78
|
-
|
|
240
|
+
body = await response.json();
|
|
79
241
|
} catch {
|
|
80
242
|
}
|
|
81
243
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
244
|
+
throw errorFromApi(
|
|
245
|
+
parseApiError(response.status, body, response.statusText),
|
|
246
|
+
retryAfterHeader(response.headers.get("retry-after"))
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
function errorFromApi(info, retryAfter2) {
|
|
250
|
+
const { status, message, code } = info;
|
|
251
|
+
const retryAt = typeof info.body.retryAt === "string" ? info.body.retryAt : retryAfter2;
|
|
252
|
+
let error = agentErrorFor(
|
|
253
|
+
status,
|
|
254
|
+
message,
|
|
255
|
+
code,
|
|
256
|
+
info.body,
|
|
257
|
+
retryAt
|
|
258
|
+
);
|
|
259
|
+
if (error) {
|
|
260
|
+
error.requestId = info.requestId;
|
|
261
|
+
error.details = info.body;
|
|
262
|
+
error.retryAt ??= retryAt;
|
|
263
|
+
return error;
|
|
264
|
+
}
|
|
265
|
+
switch (status) {
|
|
86
266
|
case 401:
|
|
87
|
-
|
|
267
|
+
error = new AuthenticationError(message);
|
|
268
|
+
break;
|
|
88
269
|
case 400:
|
|
89
|
-
|
|
270
|
+
error = new ValidationError(
|
|
271
|
+
message,
|
|
272
|
+
typeof info.body.field === "string" ? info.body.field : void 0
|
|
273
|
+
);
|
|
274
|
+
break;
|
|
90
275
|
case 429:
|
|
91
|
-
|
|
276
|
+
error = new RateLimitError(
|
|
277
|
+
message,
|
|
278
|
+
code === "PLAN_LIMIT_REACHED" ? code : void 0
|
|
279
|
+
);
|
|
280
|
+
break;
|
|
92
281
|
default:
|
|
93
|
-
|
|
282
|
+
error = new ChannlizeError(message, status, code);
|
|
94
283
|
}
|
|
284
|
+
error.requestId = info.requestId;
|
|
285
|
+
error.details = info.body;
|
|
286
|
+
error.retryAt = retryAt;
|
|
287
|
+
return error;
|
|
95
288
|
}
|
|
96
289
|
|
|
97
290
|
// src/notify.ts
|
|
98
|
-
var EVENT_KEY = /^[a-z0-9][a-z0-9._-]*$/;
|
|
99
|
-
var DEFAULT_BASE_URL = "https://api.channlize.com";
|
|
100
291
|
async function sendNotify(http, auth, event, data, options = {}) {
|
|
101
292
|
if (!auth.projectId) {
|
|
102
293
|
throw new ValidationError("projectId is required for notify", "projectId");
|
|
103
294
|
}
|
|
104
|
-
if (auth.serverApiKey
|
|
295
|
+
if (keyKind(auth.serverApiKey) === "widget") {
|
|
105
296
|
throw new ValidationError(
|
|
106
297
|
"notify needs a server API key (ch_live_\u2026, write scope), not the public widget key",
|
|
107
298
|
"serverApiKey"
|
|
108
299
|
);
|
|
109
300
|
}
|
|
110
|
-
if (
|
|
301
|
+
if (!isValidEventKey(event)) {
|
|
111
302
|
throw new ValidationError(
|
|
112
303
|
`Invalid event key "${event}": use lowercase letters, digits, dots, underscores or dashes (e.g. user.subscribed)`,
|
|
113
304
|
"event"
|
|
@@ -131,15 +322,11 @@ async function sendNotify(http, auth, event, data, options = {}) {
|
|
|
131
322
|
});
|
|
132
323
|
return res;
|
|
133
324
|
}
|
|
134
|
-
function readEnv(name) {
|
|
135
|
-
const env = typeof process !== "undefined" && process.env ? process.env : void 0;
|
|
136
|
-
const value = env?.[name];
|
|
137
|
-
return value && value.trim() ? value.trim() : void 0;
|
|
138
|
-
}
|
|
139
325
|
function createNotifier(config = {}) {
|
|
140
326
|
return (event, data, options) => {
|
|
141
|
-
const
|
|
142
|
-
const
|
|
327
|
+
const env = readChannlizeEnv();
|
|
328
|
+
const serverApiKey = config.serverApiKey ?? env.serverApiKey ?? env.apiKey;
|
|
329
|
+
const projectId = config.projectId ?? env.projectId;
|
|
143
330
|
if (!serverApiKey) {
|
|
144
331
|
return Promise.reject(
|
|
145
332
|
new ValidationError(
|
|
@@ -156,9 +343,12 @@ function createNotifier(config = {}) {
|
|
|
156
343
|
)
|
|
157
344
|
);
|
|
158
345
|
}
|
|
159
|
-
const baseUrl = config.baseUrl ??
|
|
346
|
+
const baseUrl = config.baseUrl ?? env.baseUrl ?? DEFAULT_BASE_URL;
|
|
160
347
|
return sendNotify(
|
|
161
|
-
{
|
|
348
|
+
{
|
|
349
|
+
baseUrl: baseUrl.replace(/\/$/, ""),
|
|
350
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT
|
|
351
|
+
},
|
|
162
352
|
{ serverApiKey, projectId },
|
|
163
353
|
event,
|
|
164
354
|
data,
|
|
@@ -167,6 +357,792 @@ function createNotifier(config = {}) {
|
|
|
167
357
|
};
|
|
168
358
|
}
|
|
169
359
|
|
|
360
|
+
// src/agents-governance-types.ts
|
|
361
|
+
var QUESTION_KINDS = ["question", "approval"];
|
|
362
|
+
var QUESTION_URGENCIES = ["low", "normal", "high", "urgent"];
|
|
363
|
+
var QUESTION_STATUSES = [
|
|
364
|
+
"open",
|
|
365
|
+
"answered",
|
|
366
|
+
"expired_default",
|
|
367
|
+
"expired",
|
|
368
|
+
"canceled"
|
|
369
|
+
];
|
|
370
|
+
var QUESTION_WAIT_MAX_SECONDS = 25;
|
|
371
|
+
var TASK_BOARD_STATUSES = [
|
|
372
|
+
"open",
|
|
373
|
+
"in_progress",
|
|
374
|
+
"blocked",
|
|
375
|
+
"done",
|
|
376
|
+
"failed"
|
|
377
|
+
];
|
|
378
|
+
var POLICY_SCOPES = ["org", "team", "agent", "channel"];
|
|
379
|
+
|
|
380
|
+
// src/agents-governance.ts
|
|
381
|
+
var enc = encodeURIComponent;
|
|
382
|
+
var iso = (v) => v instanceof Date ? v.toISOString() : v;
|
|
383
|
+
function sleep(ms, signal) {
|
|
384
|
+
return new Promise((done) => {
|
|
385
|
+
if (signal?.aborted) return done();
|
|
386
|
+
const timer = setTimeout(finish, ms);
|
|
387
|
+
function finish() {
|
|
388
|
+
clearTimeout(timer);
|
|
389
|
+
signal?.removeEventListener("abort", finish);
|
|
390
|
+
done();
|
|
391
|
+
}
|
|
392
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function aborted() {
|
|
396
|
+
return new ChannlizeError("Request aborted", void 0, "ABORTED");
|
|
397
|
+
}
|
|
398
|
+
function need(value, what, field) {
|
|
399
|
+
if (typeof value !== "string" || !value) {
|
|
400
|
+
throw new ValidationError(`${what} is required`, field);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function createAgentsGovernance(request, defaultAgentId) {
|
|
404
|
+
const agentOf = (agentId) => agentId ?? defaultAgentId;
|
|
405
|
+
const safely = (fn) => {
|
|
406
|
+
try {
|
|
407
|
+
return fn();
|
|
408
|
+
} catch (error) {
|
|
409
|
+
return Promise.reject(error);
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
function ask(input) {
|
|
413
|
+
return safely(async () => {
|
|
414
|
+
if (!input || typeof input.question !== "string" || !input.question) {
|
|
415
|
+
throw new ValidationError("ask needs a question", "question");
|
|
416
|
+
}
|
|
417
|
+
const r = await request(
|
|
418
|
+
"POST",
|
|
419
|
+
"/v1/agents/questions",
|
|
420
|
+
{
|
|
421
|
+
body: {
|
|
422
|
+
agent_id: agentOf(input.agentId),
|
|
423
|
+
kind: input.kind,
|
|
424
|
+
question: input.question,
|
|
425
|
+
options: input.options,
|
|
426
|
+
free_text: input.freeText,
|
|
427
|
+
urgency: input.urgency,
|
|
428
|
+
deadline: iso(input.deadline),
|
|
429
|
+
default_option: input.defaultOption,
|
|
430
|
+
channel_id: input.channelId,
|
|
431
|
+
thread_id: input.threadId,
|
|
432
|
+
context: input.context,
|
|
433
|
+
amount: input.amount,
|
|
434
|
+
currency: input.currency,
|
|
435
|
+
resource: input.resource,
|
|
436
|
+
required_roles: input.requiredRoles
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
return r.question;
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
function wait(questionId, options = {}) {
|
|
444
|
+
return safely(async () => {
|
|
445
|
+
need(questionId, "The question id", "questionId");
|
|
446
|
+
const timeoutSec = Math.min(
|
|
447
|
+
QUESTION_WAIT_MAX_SECONDS,
|
|
448
|
+
Math.max(0, options.timeoutSec ?? QUESTION_WAIT_MAX_SECONDS)
|
|
449
|
+
);
|
|
450
|
+
const r = await request(
|
|
451
|
+
"GET",
|
|
452
|
+
`/v1/agents/questions/${enc(questionId)}/wait`,
|
|
453
|
+
{
|
|
454
|
+
query: { agent_id: agentOf(options.agentId), timeout: timeoutSec },
|
|
455
|
+
// The server holds the request for up to timeoutSec.
|
|
456
|
+
timeout: (timeoutSec + 10) * 1e3,
|
|
457
|
+
signal: options.signal
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
return {
|
|
461
|
+
question: r.question,
|
|
462
|
+
timedOut: r.timed_out ?? r.question?.status === "open"
|
|
463
|
+
};
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
async function askAndWait(input, options = {}) {
|
|
467
|
+
const { signal } = options;
|
|
468
|
+
const deadline = Date.now() + Math.max(0, options.timeoutMs ?? 6e5);
|
|
469
|
+
if (signal?.aborted) throw aborted();
|
|
470
|
+
let question = await ask(input);
|
|
471
|
+
let failures = 0;
|
|
472
|
+
while (question.status === "open") {
|
|
473
|
+
if (signal?.aborted) throw aborted();
|
|
474
|
+
const left = deadline - Date.now();
|
|
475
|
+
if (left < 1e3) break;
|
|
476
|
+
try {
|
|
477
|
+
const r = await wait(question.id, {
|
|
478
|
+
agentId: input.agentId,
|
|
479
|
+
timeoutSec: Math.min(
|
|
480
|
+
QUESTION_WAIT_MAX_SECONDS,
|
|
481
|
+
Math.floor(left / 1e3)
|
|
482
|
+
),
|
|
483
|
+
signal
|
|
484
|
+
});
|
|
485
|
+
question = r.question;
|
|
486
|
+
failures = 0;
|
|
487
|
+
} catch (error) {
|
|
488
|
+
if (signal?.aborted) throw aborted();
|
|
489
|
+
if (!isRetryable(error)) throw error;
|
|
490
|
+
failures += 1;
|
|
491
|
+
const backoff = retryAfter(error) ?? Math.min(3e4, 1e3 * 2 ** (failures - 1));
|
|
492
|
+
const remaining = deadline - Date.now();
|
|
493
|
+
if (backoff >= remaining) break;
|
|
494
|
+
await sleep(backoff, signal);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (signal?.aborted && question.status === "open") throw aborted();
|
|
498
|
+
return {
|
|
499
|
+
question,
|
|
500
|
+
answer: question.answer ?? null,
|
|
501
|
+
timedOut: question.status === "open"
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const questionPath = (id) => {
|
|
505
|
+
need(id, "The question id", "questionId");
|
|
506
|
+
return `/v1/agents/questions/${enc(id)}`;
|
|
507
|
+
};
|
|
508
|
+
const taskPath = (id) => {
|
|
509
|
+
need(id, "The task id", "taskId");
|
|
510
|
+
return `/v1/agents/tasks/${enc(id)}`;
|
|
511
|
+
};
|
|
512
|
+
const teamPath = (id) => {
|
|
513
|
+
need(id, "The team id", "teamId");
|
|
514
|
+
return `/v1/agents/teams/${enc(id)}`;
|
|
515
|
+
};
|
|
516
|
+
const agentPath = (id) => {
|
|
517
|
+
need(id, "The agent id", "agentId");
|
|
518
|
+
return enc(id);
|
|
519
|
+
};
|
|
520
|
+
return {
|
|
521
|
+
ask,
|
|
522
|
+
askAndWait,
|
|
523
|
+
questions: {
|
|
524
|
+
get: (id, options = {}) => safely(async () => {
|
|
525
|
+
const r = await request("GET", questionPath(id), {
|
|
526
|
+
query: { agent_id: agentOf(options.agentId) }
|
|
527
|
+
});
|
|
528
|
+
return { question: r.question, escalations: r.escalations ?? [] };
|
|
529
|
+
}),
|
|
530
|
+
list: (options = {}) => safely(async () => {
|
|
531
|
+
const r = await request("GET", "/v1/agents/questions", {
|
|
532
|
+
query: {
|
|
533
|
+
agent_id: agentOf(options.agentId),
|
|
534
|
+
status: options.status,
|
|
535
|
+
kind: options.kind,
|
|
536
|
+
urgency: options.urgency,
|
|
537
|
+
channel_id: options.channelId,
|
|
538
|
+
cursor: options.cursor,
|
|
539
|
+
limit: options.limit
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
return {
|
|
543
|
+
questions: r.questions ?? [],
|
|
544
|
+
nextCursor: r.next_cursor ?? null
|
|
545
|
+
};
|
|
546
|
+
}),
|
|
547
|
+
wait,
|
|
548
|
+
answer: (id, input) => safely(async () => {
|
|
549
|
+
if (!input?.optionId && !input?.text) {
|
|
550
|
+
throw new ValidationError(
|
|
551
|
+
"answer needs optionId or text",
|
|
552
|
+
"optionId"
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
const r = await request(
|
|
556
|
+
"POST",
|
|
557
|
+
`${questionPath(id)}/answer`,
|
|
558
|
+
{
|
|
559
|
+
body: {
|
|
560
|
+
agent_id: agentOf(input.agentId),
|
|
561
|
+
option_id: input.optionId,
|
|
562
|
+
text: input.text,
|
|
563
|
+
comment: input.comment
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
return r.question;
|
|
568
|
+
}),
|
|
569
|
+
cancel: (id, options = {}) => safely(async () => {
|
|
570
|
+
const r = await request(
|
|
571
|
+
"DELETE",
|
|
572
|
+
questionPath(id),
|
|
573
|
+
{ query: { agent_id: agentOf(options.agentId) } }
|
|
574
|
+
);
|
|
575
|
+
return r.question;
|
|
576
|
+
})
|
|
577
|
+
},
|
|
578
|
+
tasks: {
|
|
579
|
+
create: (input) => safely(async () => {
|
|
580
|
+
need(input?.channelId, "channelId", "channelId");
|
|
581
|
+
const r = await request(
|
|
582
|
+
"POST",
|
|
583
|
+
"/v1/agents/tasks",
|
|
584
|
+
{
|
|
585
|
+
body: {
|
|
586
|
+
agent_id: agentOf(input.agentId),
|
|
587
|
+
channel_id: input.channelId,
|
|
588
|
+
text: input.text,
|
|
589
|
+
assignee_agent_id: input.assigneeAgentId,
|
|
590
|
+
due_at: iso(input.dueAt),
|
|
591
|
+
thread_id: input.threadId,
|
|
592
|
+
mentions: input.mentions,
|
|
593
|
+
data: input.data
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
);
|
|
597
|
+
return { task: r.task, deliveries: r.deliveries ?? [] };
|
|
598
|
+
}),
|
|
599
|
+
list: (options = {}) => safely(async () => {
|
|
600
|
+
const r = await request(
|
|
601
|
+
"GET",
|
|
602
|
+
"/v1/agents/tasks",
|
|
603
|
+
{
|
|
604
|
+
query: {
|
|
605
|
+
agent_id: agentOf(options.agentId),
|
|
606
|
+
channel_id: options.channelId,
|
|
607
|
+
assignee_agent_id: options.assigneeAgentId,
|
|
608
|
+
status: options.status,
|
|
609
|
+
limit: options.limit
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
);
|
|
613
|
+
return r.tasks ?? [];
|
|
614
|
+
}),
|
|
615
|
+
board: (channelId, options = {}) => safely(() => {
|
|
616
|
+
need(channelId, "channelId", "channelId");
|
|
617
|
+
return request("GET", "/v1/agents/tasks/board", {
|
|
618
|
+
query: {
|
|
619
|
+
channel_id: channelId,
|
|
620
|
+
assignee_agent_id: options.assigneeAgentId,
|
|
621
|
+
agent_id: agentOf(options.agentId)
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
}),
|
|
625
|
+
get: (taskId, options = {}) => safely(async () => {
|
|
626
|
+
const r = await request("GET", taskPath(taskId), {
|
|
627
|
+
query: { agent_id: agentOf(options.agentId) }
|
|
628
|
+
});
|
|
629
|
+
return { task: r.task, events: r.events ?? [] };
|
|
630
|
+
}),
|
|
631
|
+
update: (taskId, input) => safely(() => {
|
|
632
|
+
if (!input || input.status === void 0 && input.dueAt === void 0) {
|
|
633
|
+
throw new ValidationError("update needs status or dueAt", "status");
|
|
634
|
+
}
|
|
635
|
+
return request("PATCH", taskPath(taskId), {
|
|
636
|
+
body: {
|
|
637
|
+
agent_id: agentOf(input.agentId),
|
|
638
|
+
status: input.status,
|
|
639
|
+
due_at: input.dueAt === null ? null : iso(input.dueAt),
|
|
640
|
+
note: input.note
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
}),
|
|
644
|
+
handoff: (taskId, input) => safely(() => {
|
|
645
|
+
need(input?.toAgentId, "toAgentId", "toAgentId");
|
|
646
|
+
return request(
|
|
647
|
+
"POST",
|
|
648
|
+
`${taskPath(taskId)}/handoff`,
|
|
649
|
+
{
|
|
650
|
+
body: {
|
|
651
|
+
agent_id: agentOf(input.agentId),
|
|
652
|
+
to_agent_id: input.toAgentId,
|
|
653
|
+
reason: input.reason
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
);
|
|
657
|
+
})
|
|
658
|
+
},
|
|
659
|
+
teams: {
|
|
660
|
+
list: () => safely(async () => {
|
|
661
|
+
const r = await request(
|
|
662
|
+
"GET",
|
|
663
|
+
"/v1/agents/teams"
|
|
664
|
+
);
|
|
665
|
+
return r.teams ?? [];
|
|
666
|
+
}),
|
|
667
|
+
get: (teamId) => safely(() => request("GET", teamPath(teamId))),
|
|
668
|
+
create: (input) => safely(async () => {
|
|
669
|
+
const r = await request(
|
|
670
|
+
"POST",
|
|
671
|
+
"/v1/agents/teams",
|
|
672
|
+
{
|
|
673
|
+
body: {
|
|
674
|
+
name: input.name,
|
|
675
|
+
slug: input.slug,
|
|
676
|
+
description: input.description,
|
|
677
|
+
agent_ids: input.agentIds
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
);
|
|
681
|
+
return r.team;
|
|
682
|
+
}),
|
|
683
|
+
update: (teamId, input) => safely(async () => {
|
|
684
|
+
const r = await request(
|
|
685
|
+
"PATCH",
|
|
686
|
+
teamPath(teamId),
|
|
687
|
+
{ body: input }
|
|
688
|
+
);
|
|
689
|
+
return r.team;
|
|
690
|
+
}),
|
|
691
|
+
delete: (teamId) => safely(async () => {
|
|
692
|
+
const r = await request(
|
|
693
|
+
"DELETE",
|
|
694
|
+
teamPath(teamId)
|
|
695
|
+
);
|
|
696
|
+
return { memberships_removed: r.memberships_removed ?? 0 };
|
|
697
|
+
}),
|
|
698
|
+
addMember: (teamId, agentId) => safely(() => {
|
|
699
|
+
need(agentId, "agentId", "agentId");
|
|
700
|
+
return request(
|
|
701
|
+
"POST",
|
|
702
|
+
`${teamPath(teamId)}/members`,
|
|
703
|
+
{ body: { agent_id: agentId } }
|
|
704
|
+
);
|
|
705
|
+
}),
|
|
706
|
+
removeMember: (teamId, agentId) => safely(async () => {
|
|
707
|
+
const r = await request(
|
|
708
|
+
"DELETE",
|
|
709
|
+
`${teamPath(teamId)}/members/${agentPath(agentId)}`
|
|
710
|
+
);
|
|
711
|
+
return { memberships_removed: r.memberships_removed ?? 0 };
|
|
712
|
+
}),
|
|
713
|
+
grantChannel: (teamId, channelId, role) => safely(() => {
|
|
714
|
+
need(channelId, "channelId", "channelId");
|
|
715
|
+
return request("POST", `${teamPath(teamId)}/channels`, {
|
|
716
|
+
body: { channel_id: channelId, role }
|
|
717
|
+
});
|
|
718
|
+
}),
|
|
719
|
+
revokeChannel: (teamId, channelId) => safely(async () => {
|
|
720
|
+
need(channelId, "channelId", "channelId");
|
|
721
|
+
const r = await request(
|
|
722
|
+
"DELETE",
|
|
723
|
+
`${teamPath(teamId)}/channels/${enc(channelId)}`
|
|
724
|
+
);
|
|
725
|
+
return { memberships_removed: r.memberships_removed ?? 0 };
|
|
726
|
+
})
|
|
727
|
+
},
|
|
728
|
+
policies: {
|
|
729
|
+
list: (options = {}) => safely(async () => {
|
|
730
|
+
const r = await request(
|
|
731
|
+
"GET",
|
|
732
|
+
"/v1/agents/policies",
|
|
733
|
+
{ query: { scope: options.scope } }
|
|
734
|
+
);
|
|
735
|
+
return r.policies ?? [];
|
|
736
|
+
}),
|
|
737
|
+
get: (policyId) => safely(async () => {
|
|
738
|
+
need(policyId, "The policy id", "policyId");
|
|
739
|
+
const r = await request(
|
|
740
|
+
"GET",
|
|
741
|
+
`/v1/agents/policies/${enc(policyId)}`
|
|
742
|
+
);
|
|
743
|
+
return r.policy;
|
|
744
|
+
}),
|
|
745
|
+
put: (input) => safely(async () => {
|
|
746
|
+
const r = await request(
|
|
747
|
+
"PUT",
|
|
748
|
+
"/v1/agents/policies",
|
|
749
|
+
{
|
|
750
|
+
body: {
|
|
751
|
+
scope: input.scope,
|
|
752
|
+
scope_id: input.scopeId,
|
|
753
|
+
rules: input.rules,
|
|
754
|
+
enabled: input.enabled
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
);
|
|
758
|
+
return r.policy;
|
|
759
|
+
}),
|
|
760
|
+
delete: (policyId) => safely(async () => {
|
|
761
|
+
need(policyId, "The policy id", "policyId");
|
|
762
|
+
await request("DELETE", `/v1/agents/policies/${enc(policyId)}`);
|
|
763
|
+
}),
|
|
764
|
+
preview: (agentId, options = {}) => safely(() => {
|
|
765
|
+
need(agentId, "agentId", "agentId");
|
|
766
|
+
return request(
|
|
767
|
+
"GET",
|
|
768
|
+
"/v1/agents/policies/preview",
|
|
769
|
+
{ query: { agent_id: agentId, channel_id: options.channelId } }
|
|
770
|
+
);
|
|
771
|
+
})
|
|
772
|
+
},
|
|
773
|
+
orgChart: {
|
|
774
|
+
get: (options = {}) => request("GET", "/v1/agents/org-chart", {
|
|
775
|
+
query: {
|
|
776
|
+
team_id: options.teamId,
|
|
777
|
+
channel_id: options.channelId,
|
|
778
|
+
include_retired: options.includeRetired ? "true" : void 0
|
|
779
|
+
}
|
|
780
|
+
}),
|
|
781
|
+
setManager: (agentId, input) => safely(async () => {
|
|
782
|
+
if (input?.managerAgentId && input?.managerUserId) {
|
|
783
|
+
throw new ValidationError(
|
|
784
|
+
"Set managerAgentId or managerUserId, not both",
|
|
785
|
+
"managerAgentId"
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
const r = await request(
|
|
789
|
+
"PUT",
|
|
790
|
+
`/v1/agents/org-chart/${agentPath(agentId)}/manager`,
|
|
791
|
+
{
|
|
792
|
+
body: {
|
|
793
|
+
manager_agent_id: input?.managerAgentId ?? null,
|
|
794
|
+
manager_user_id: input?.managerUserId ?? null
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
);
|
|
798
|
+
return r.agent;
|
|
799
|
+
}),
|
|
800
|
+
setOwner: (agentId, ownerUserId) => safely(async () => {
|
|
801
|
+
const r = await request(
|
|
802
|
+
"PUT",
|
|
803
|
+
`/v1/agents/org-chart/${agentPath(agentId)}/owner`,
|
|
804
|
+
{ body: { owner_user_id: ownerUserId ?? null } }
|
|
805
|
+
);
|
|
806
|
+
return r.agent;
|
|
807
|
+
})
|
|
808
|
+
},
|
|
809
|
+
onboarding: (agentId) => safely(
|
|
810
|
+
() => request(
|
|
811
|
+
"GET",
|
|
812
|
+
`/v1/agents/${agentPath(agentId)}/onboarding`
|
|
813
|
+
)
|
|
814
|
+
),
|
|
815
|
+
setStatus: (agentId, status, options = {}) => safely(
|
|
816
|
+
() => request(
|
|
817
|
+
"POST",
|
|
818
|
+
`/v1/agents/${agentPath(agentId)}/status`,
|
|
819
|
+
{ body: { status, reason: options.reason } }
|
|
820
|
+
)
|
|
821
|
+
)
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
var QUESTION_OUTCOMES = /* @__PURE__ */ new Set([
|
|
825
|
+
"question.answered",
|
|
826
|
+
"question.expired",
|
|
827
|
+
"question.canceled"
|
|
828
|
+
]);
|
|
829
|
+
function isAgentMessageWebhook(payload) {
|
|
830
|
+
return payload?.event === "agent.message";
|
|
831
|
+
}
|
|
832
|
+
function isQuestionOutcomeWebhook(payload) {
|
|
833
|
+
return Boolean(payload && QUESTION_OUTCOMES.has(payload.event));
|
|
834
|
+
}
|
|
835
|
+
function isQuestionEscalatedWebhook(payload) {
|
|
836
|
+
return payload?.event === "question.escalated";
|
|
837
|
+
}
|
|
838
|
+
function isQuestionOutcomeMessage(message) {
|
|
839
|
+
const data = message?.data;
|
|
840
|
+
return Boolean(
|
|
841
|
+
data && typeof data.type === "string" && QUESTION_OUTCOMES.has(data.type) && data.question && typeof data.question === "object"
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
function isTaskHandoffMessage(message) {
|
|
845
|
+
const handoff = message?.data?.handoff;
|
|
846
|
+
return Boolean(
|
|
847
|
+
message?.type === "status" && handoff && typeof handoff === "object" && typeof handoff.to_agent_id === "string"
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// src/agents.ts
|
|
852
|
+
function resolve(config) {
|
|
853
|
+
const env = readChannlizeEnv();
|
|
854
|
+
const apiKey = config.apiKey ?? env.serverApiKey ?? env.apiKey;
|
|
855
|
+
if (!apiKey) {
|
|
856
|
+
throw new ValidationError(
|
|
857
|
+
"An agent API key is required: pass apiKey or set CHANNLIZE_API_KEY (or CHANNLIZE_SERVER_API_KEY)",
|
|
858
|
+
"apiKey"
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
if (keyKind(apiKey) === "widget") {
|
|
862
|
+
throw new ValidationError(
|
|
863
|
+
"Agents need an agent key or a multi-agent key (ch_live_\u2026, agents:send / agents:read scopes), not the public widget key",
|
|
864
|
+
"apiKey"
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
return {
|
|
868
|
+
apiKey,
|
|
869
|
+
agentId: config.agentId ?? env.agentId,
|
|
870
|
+
baseUrl: (config.baseUrl ?? env.baseUrl ?? DEFAULT_BASE_URL).replace(
|
|
871
|
+
/\/+$/,
|
|
872
|
+
""
|
|
873
|
+
),
|
|
874
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
var enc2 = encodeURIComponent;
|
|
878
|
+
function isFatal(error) {
|
|
879
|
+
return error instanceof ChannlizeError && [400, 401, 403, 404].includes(error.statusCode ?? 0);
|
|
880
|
+
}
|
|
881
|
+
function sleep2(ms, signal) {
|
|
882
|
+
return new Promise((done) => {
|
|
883
|
+
if (signal?.aborted) return done();
|
|
884
|
+
const timer = setTimeout(finish, ms);
|
|
885
|
+
function finish() {
|
|
886
|
+
clearTimeout(timer);
|
|
887
|
+
signal?.removeEventListener("abort", finish);
|
|
888
|
+
done();
|
|
889
|
+
}
|
|
890
|
+
signal?.addEventListener("abort", finish, { once: true });
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
function createAgentsClient(config = {}) {
|
|
894
|
+
const cfg = resolve(config);
|
|
895
|
+
const request = async (method, path, options = {}) => {
|
|
896
|
+
const params = new URLSearchParams();
|
|
897
|
+
for (const [k, v] of Object.entries(options.query ?? {})) {
|
|
898
|
+
if (v !== void 0 && v !== "") params.set(k, String(v));
|
|
899
|
+
}
|
|
900
|
+
const qs = params.toString();
|
|
901
|
+
const http = { ...cfg, timeout: options.timeout ?? cfg.timeout };
|
|
902
|
+
const res = await requestJson(http, `${path}${qs ? `?${qs}` : ""}`, {
|
|
903
|
+
method,
|
|
904
|
+
headers: { Authorization: `Bearer ${cfg.apiKey}` },
|
|
905
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
906
|
+
signal: options.signal
|
|
907
|
+
});
|
|
908
|
+
return res;
|
|
909
|
+
};
|
|
910
|
+
function agentFor(agentId, what) {
|
|
911
|
+
const id = agentId ?? cfg.agentId;
|
|
912
|
+
if (!id) {
|
|
913
|
+
throw new ValidationError(
|
|
914
|
+
`${what} needs an agent id: pass it or set agentId (CHANNLIZE_AGENT_ID)`,
|
|
915
|
+
"agentId"
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
return id;
|
|
919
|
+
}
|
|
920
|
+
async function inbox(agentId, options = {}) {
|
|
921
|
+
const id = agentFor(agentId, "inbox");
|
|
922
|
+
const r = await request("GET", `/v1/agents/${enc2(id)}/inbox`, {
|
|
923
|
+
query: { cursor: options.cursor, limit: options.limit }
|
|
924
|
+
});
|
|
925
|
+
return {
|
|
926
|
+
messages: r.messages ?? [],
|
|
927
|
+
nextCursor: r.next_cursor ?? null,
|
|
928
|
+
hasMore: Boolean(r.has_more)
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
async function ack(agentId, input) {
|
|
932
|
+
const id = agentFor(agentId, "ack");
|
|
933
|
+
let body;
|
|
934
|
+
if ("upTo" in input && input.upTo) body = { up_to: input.upTo };
|
|
935
|
+
else if ("ids" in input && input.ids?.length) body = { ids: input.ids };
|
|
936
|
+
else {
|
|
937
|
+
throw new ValidationError(
|
|
938
|
+
"ack needs upTo or a non-empty ids list",
|
|
939
|
+
"ids"
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
return request("POST", `/v1/agents/${enc2(id)}/inbox/ack`, {
|
|
943
|
+
body
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
async function poll(agentId, handler, options = {}) {
|
|
947
|
+
const id = agentFor(agentId, "poll");
|
|
948
|
+
const interval = Math.max(0, options.intervalMs ?? 5e3);
|
|
949
|
+
const maxBackoff = Math.max(interval, options.maxBackoffMs ?? 6e4);
|
|
950
|
+
const { signal } = options;
|
|
951
|
+
let failures = 0;
|
|
952
|
+
while (!signal?.aborted) {
|
|
953
|
+
let failed = false;
|
|
954
|
+
let more = false;
|
|
955
|
+
try {
|
|
956
|
+
const page = await inbox(id, { limit: options.limit });
|
|
957
|
+
more = page.hasMore;
|
|
958
|
+
const done = [];
|
|
959
|
+
try {
|
|
960
|
+
for (const item of page.messages) {
|
|
961
|
+
if (signal?.aborted) break;
|
|
962
|
+
try {
|
|
963
|
+
await handler(item.message, item);
|
|
964
|
+
} catch (error) {
|
|
965
|
+
failed = true;
|
|
966
|
+
options.onError?.(error, item);
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
done.push(item.delivery_id);
|
|
970
|
+
}
|
|
971
|
+
} finally {
|
|
972
|
+
if (done.length) await ack(id, { ids: done });
|
|
973
|
+
}
|
|
974
|
+
} catch (error) {
|
|
975
|
+
if (isFatal(error)) throw error;
|
|
976
|
+
failed = true;
|
|
977
|
+
options.onError?.(error);
|
|
978
|
+
}
|
|
979
|
+
if (failed) {
|
|
980
|
+
failures += 1;
|
|
981
|
+
const backoff = Math.min(
|
|
982
|
+
maxBackoff,
|
|
983
|
+
(interval || 1e3) * 2 ** (failures - 1)
|
|
984
|
+
);
|
|
985
|
+
await sleep2(backoff, signal);
|
|
986
|
+
} else {
|
|
987
|
+
failures = 0;
|
|
988
|
+
if (!more) await sleep2(interval, signal);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
const channelRead = (options = {}) => ({
|
|
993
|
+
agent_id: options.agentId ?? cfg.agentId
|
|
994
|
+
});
|
|
995
|
+
return {
|
|
996
|
+
...createAgentsGovernance(request, cfg.agentId),
|
|
997
|
+
agentId: cfg.agentId,
|
|
998
|
+
send(input) {
|
|
999
|
+
if (!input || typeof input.channel !== "string" || !input.channel) {
|
|
1000
|
+
return Promise.reject(
|
|
1001
|
+
new ValidationError("send needs a channel id", "channel")
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
return request("POST", "/v1/agents/messages", {
|
|
1005
|
+
body: {
|
|
1006
|
+
agent_id: input.agentId ?? cfg.agentId,
|
|
1007
|
+
channel_id: input.channel,
|
|
1008
|
+
type: input.type,
|
|
1009
|
+
text: input.text,
|
|
1010
|
+
thread_id: input.threadId,
|
|
1011
|
+
mentions: input.mentions,
|
|
1012
|
+
data: input.data,
|
|
1013
|
+
tokens: input.tokens,
|
|
1014
|
+
cost: input.cost,
|
|
1015
|
+
task_id: input.taskId,
|
|
1016
|
+
task_status: input.taskStatus
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
},
|
|
1020
|
+
inbox,
|
|
1021
|
+
ack,
|
|
1022
|
+
poll,
|
|
1023
|
+
async list(options = {}) {
|
|
1024
|
+
const r = await request("GET", "/v1/agents", {
|
|
1025
|
+
query: { include_retired: options.includeRetired || void 0 }
|
|
1026
|
+
});
|
|
1027
|
+
return r.agents ?? [];
|
|
1028
|
+
},
|
|
1029
|
+
get(agentId) {
|
|
1030
|
+
return request("GET", `/v1/agents/${enc2(agentId)}`);
|
|
1031
|
+
},
|
|
1032
|
+
channels: {
|
|
1033
|
+
async list(options = {}) {
|
|
1034
|
+
const r = await request(
|
|
1035
|
+
"GET",
|
|
1036
|
+
"/v1/agents/channels",
|
|
1037
|
+
{ query: { include_archived: options.includeArchived || void 0 } }
|
|
1038
|
+
);
|
|
1039
|
+
return r.channels ?? [];
|
|
1040
|
+
},
|
|
1041
|
+
get(channelId, options) {
|
|
1042
|
+
return request("GET", `/v1/agents/channels/${enc2(channelId)}`, {
|
|
1043
|
+
query: channelRead(options)
|
|
1044
|
+
});
|
|
1045
|
+
},
|
|
1046
|
+
async members(channelId, options) {
|
|
1047
|
+
const r = await request(
|
|
1048
|
+
"GET",
|
|
1049
|
+
`/v1/agents/channels/${enc2(channelId)}/members`,
|
|
1050
|
+
{ query: channelRead(options) }
|
|
1051
|
+
);
|
|
1052
|
+
return r.members ?? [];
|
|
1053
|
+
},
|
|
1054
|
+
async messages(channelId, options = {}) {
|
|
1055
|
+
const r = await request("GET", `/v1/agents/channels/${enc2(channelId)}/messages`, {
|
|
1056
|
+
query: {
|
|
1057
|
+
...channelRead(options),
|
|
1058
|
+
cursor: options.cursor,
|
|
1059
|
+
after: options.after,
|
|
1060
|
+
limit: options.limit,
|
|
1061
|
+
sender_agent_id: options.senderAgentId,
|
|
1062
|
+
type: options.type,
|
|
1063
|
+
thread_id: options.threadId
|
|
1064
|
+
}
|
|
1065
|
+
});
|
|
1066
|
+
return {
|
|
1067
|
+
channel: r.channel,
|
|
1068
|
+
messages: r.messages ?? [],
|
|
1069
|
+
nextCursor: r.next_cursor ?? null,
|
|
1070
|
+
latestCursor: r.latest_cursor ?? null,
|
|
1071
|
+
hasMore: Boolean(r.has_more)
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
var NODE_CRYPTO = "node:crypto";
|
|
1078
|
+
async function getSubtle() {
|
|
1079
|
+
const subtle = globalThis.crypto?.subtle;
|
|
1080
|
+
if (subtle) return subtle;
|
|
1081
|
+
try {
|
|
1082
|
+
const mod = await import(
|
|
1083
|
+
/* webpackIgnore: true */
|
|
1084
|
+
/* @vite-ignore */
|
|
1085
|
+
NODE_CRYPTO
|
|
1086
|
+
);
|
|
1087
|
+
if (mod.webcrypto?.subtle) return mod.webcrypto.subtle;
|
|
1088
|
+
} catch {
|
|
1089
|
+
}
|
|
1090
|
+
throw new ChannlizeError(
|
|
1091
|
+
"verifyAgentWebhook needs the Web Crypto API (globalThis.crypto.subtle, or node:crypto webcrypto on Node.js 18)"
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
function hexToBytes(hex) {
|
|
1095
|
+
if (hex.length === 0 || hex.length % 2 !== 0 || /[^0-9a-f]/i.test(hex)) {
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
const out = new Uint8Array(hex.length / 2);
|
|
1099
|
+
for (let i = 0; i < out.length; i++) {
|
|
1100
|
+
out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
1101
|
+
}
|
|
1102
|
+
return out;
|
|
1103
|
+
}
|
|
1104
|
+
function toBytes(body) {
|
|
1105
|
+
if (typeof body === "string") return new TextEncoder().encode(body);
|
|
1106
|
+
const copy = new Uint8Array(body.byteLength);
|
|
1107
|
+
copy.set(body);
|
|
1108
|
+
return copy;
|
|
1109
|
+
}
|
|
1110
|
+
async function verifyAgentWebhook(rawBody, signatureHeader, secret) {
|
|
1111
|
+
if (!secret)
|
|
1112
|
+
throw new ValidationError("The agent secret is required", "secret");
|
|
1113
|
+
const header = Array.isArray(signatureHeader) ? signatureHeader[0] : signatureHeader;
|
|
1114
|
+
const m = /^sha256=([0-9a-f]+)$/i.exec(header?.trim() ?? "");
|
|
1115
|
+
const expected = m ? hexToBytes(m[1]) : null;
|
|
1116
|
+
if (!expected || expected.length !== 32) return false;
|
|
1117
|
+
const subtle = await getSubtle();
|
|
1118
|
+
const key = await subtle.importKey(
|
|
1119
|
+
"raw",
|
|
1120
|
+
new TextEncoder().encode(secret),
|
|
1121
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
1122
|
+
false,
|
|
1123
|
+
["verify"]
|
|
1124
|
+
);
|
|
1125
|
+
return subtle.verify("HMAC", key, expected, toBytes(rawBody));
|
|
1126
|
+
}
|
|
1127
|
+
async function parseAgentWebhook(rawBody, signatureHeader, secret) {
|
|
1128
|
+
return await parseAgentWebhookEvent(
|
|
1129
|
+
rawBody,
|
|
1130
|
+
signatureHeader,
|
|
1131
|
+
secret
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
async function parseAgentWebhookEvent(rawBody, signatureHeader, secret) {
|
|
1135
|
+
if (!await verifyAgentWebhook(rawBody, signatureHeader, secret)) {
|
|
1136
|
+
throw new ChannlizeError(
|
|
1137
|
+
"Invalid agent webhook signature",
|
|
1138
|
+
401,
|
|
1139
|
+
"INVALID_SIGNATURE"
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
const text = typeof rawBody === "string" ? rawBody : new TextDecoder().decode(rawBody);
|
|
1143
|
+
return JSON.parse(text);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
170
1146
|
// src/client.ts
|
|
171
1147
|
var ChannlizeClient = class {
|
|
172
1148
|
config;
|
|
@@ -174,8 +1150,8 @@ var ChannlizeClient = class {
|
|
|
174
1150
|
this.config = {
|
|
175
1151
|
...config,
|
|
176
1152
|
// After the spread: an explicit `undefined` must not drop the defaults.
|
|
177
|
-
baseUrl: config.baseUrl ||
|
|
178
|
-
timeout: config.timeout ??
|
|
1153
|
+
baseUrl: config.baseUrl || DEFAULT_BASE_URL,
|
|
1154
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT
|
|
179
1155
|
};
|
|
180
1156
|
this.validateConfig();
|
|
181
1157
|
}
|
|
@@ -184,6 +1160,26 @@ var ChannlizeClient = class {
|
|
|
184
1160
|
throw new ValidationError("Widget key is required");
|
|
185
1161
|
}
|
|
186
1162
|
}
|
|
1163
|
+
agentsClient;
|
|
1164
|
+
/**
|
|
1165
|
+
* Channlize for Agents (`agents.send`, `agents.inbox`, `agents.poll`, ...),
|
|
1166
|
+
* authenticated with `serverApiKey`: an agent key or a multi-agent key.
|
|
1167
|
+
* Same as `createAgentsClient({ apiKey: serverApiKey, baseUrl, timeout })`.
|
|
1168
|
+
*/
|
|
1169
|
+
get agents() {
|
|
1170
|
+
if (!this.config.serverApiKey) {
|
|
1171
|
+
throw new ValidationError(
|
|
1172
|
+
"serverApiKey (an agent or multi-agent key) is required for agents",
|
|
1173
|
+
"serverApiKey"
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
this.agentsClient ??= createAgentsClient({
|
|
1177
|
+
apiKey: this.config.serverApiKey,
|
|
1178
|
+
baseUrl: this.config.baseUrl,
|
|
1179
|
+
timeout: this.config.timeout
|
|
1180
|
+
});
|
|
1181
|
+
return this.agentsClient;
|
|
1182
|
+
}
|
|
187
1183
|
makeRequest(endpoint, options = {}) {
|
|
188
1184
|
return requestJson(this.config, endpoint, options);
|
|
189
1185
|
}
|
|
@@ -301,6 +1297,9 @@ var ChannlizeClient = class {
|
|
|
301
1297
|
* `POST /public/events`). Shown under Log tracking in the dashboard.
|
|
302
1298
|
*/
|
|
303
1299
|
async trackEvent(name, options) {
|
|
1300
|
+
return this.nonFatalTracking(() => this.sendBrowserEvent(name, options));
|
|
1301
|
+
}
|
|
1302
|
+
async sendBrowserEvent(name, options) {
|
|
304
1303
|
const res = await this.makeRequest(
|
|
305
1304
|
"/public/events",
|
|
306
1305
|
{
|
|
@@ -329,6 +1328,28 @@ var ChannlizeClient = class {
|
|
|
329
1328
|
"serverApiKey is required for trackServerEvent"
|
|
330
1329
|
);
|
|
331
1330
|
}
|
|
1331
|
+
return this.nonFatalTracking(() => this.sendServerEvent(name, options));
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Log tracking must never break the caller: a 429 (rate limit, or the
|
|
1335
|
+
* plan's limit for the billing period) resolves with `success: false`
|
|
1336
|
+
* and `dropped` instead of throwing, with one console warning per client.
|
|
1337
|
+
*/
|
|
1338
|
+
async nonFatalTracking(send) {
|
|
1339
|
+
try {
|
|
1340
|
+
return await send();
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
if (!(error instanceof RateLimitError)) throw error;
|
|
1343
|
+
const dropped = error.code === "PLAN_LIMIT_REACHED" ? "plan_limit" : "rate_limit";
|
|
1344
|
+
if (!this.warnedDropped && typeof console !== "undefined") {
|
|
1345
|
+
this.warnedDropped = true;
|
|
1346
|
+
console.warn(`[channlize] log event dropped: ${error.message}`);
|
|
1347
|
+
}
|
|
1348
|
+
return { success: false, id: "", dropped };
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
warnedDropped = false;
|
|
1352
|
+
async sendServerEvent(name, options) {
|
|
332
1353
|
const res = await this.makeRequest(
|
|
333
1354
|
"/v1/events",
|
|
334
1355
|
{
|
|
@@ -373,13 +1394,75 @@ var ChannlizeClient = class {
|
|
|
373
1394
|
);
|
|
374
1395
|
}
|
|
375
1396
|
};
|
|
1397
|
+
|
|
1398
|
+
// src/agents-types.ts
|
|
1399
|
+
var AGENT_STATUSES = ["active", "paused", "retired"];
|
|
1400
|
+
var AGENT_DELIVERY_MODES = ["webhook", "inbox", "a2a"];
|
|
1401
|
+
var AGENT_MESSAGE_TYPES = [
|
|
1402
|
+
"message",
|
|
1403
|
+
"task",
|
|
1404
|
+
"question",
|
|
1405
|
+
"escalation",
|
|
1406
|
+
"status"
|
|
1407
|
+
];
|
|
1408
|
+
var SENDABLE_AGENT_MESSAGE_TYPES = [
|
|
1409
|
+
"message",
|
|
1410
|
+
"task",
|
|
1411
|
+
"status"
|
|
1412
|
+
];
|
|
1413
|
+
var AGENT_TASK_STATUSES = [
|
|
1414
|
+
"open",
|
|
1415
|
+
"working",
|
|
1416
|
+
"input_required",
|
|
1417
|
+
"completed",
|
|
1418
|
+
"failed",
|
|
1419
|
+
"canceled",
|
|
1420
|
+
"rejected"
|
|
1421
|
+
];
|
|
376
1422
|
export {
|
|
1423
|
+
AGENT_DELIVERY_MODES,
|
|
1424
|
+
AGENT_MESSAGE_TYPES,
|
|
1425
|
+
AGENT_POLICY_CODES,
|
|
1426
|
+
AGENT_STATUSES,
|
|
1427
|
+
AGENT_TASK_ERROR_CODES,
|
|
1428
|
+
AGENT_TASK_STATUSES,
|
|
1429
|
+
AgentPolicyError,
|
|
1430
|
+
AgentTaskError,
|
|
1431
|
+
AgentThreadPausedError,
|
|
377
1432
|
AuthenticationError,
|
|
378
1433
|
ChannlizeClient,
|
|
379
1434
|
ChannlizeError,
|
|
1435
|
+
DEFAULT_BASE_URL,
|
|
1436
|
+
DEFAULT_TIMEOUT,
|
|
1437
|
+
EVENT_KEY_PATTERN,
|
|
1438
|
+
EVENT_SEVERITIES,
|
|
1439
|
+
HumanTakeoverError,
|
|
1440
|
+
POLICY_SCOPES,
|
|
1441
|
+
QUESTION_KINDS,
|
|
1442
|
+
QUESTION_STATUSES,
|
|
1443
|
+
QUESTION_URGENCIES,
|
|
1444
|
+
QUESTION_WAIT_MAX_SECONDS,
|
|
1445
|
+
QuestionClosedError,
|
|
380
1446
|
RateLimitError,
|
|
1447
|
+
SENDABLE_AGENT_MESSAGE_TYPES,
|
|
1448
|
+
TASK_BOARD_STATUSES,
|
|
381
1449
|
ValidationError,
|
|
1450
|
+
createAgentsClient,
|
|
382
1451
|
createNotifier,
|
|
383
|
-
ChannlizeClient as default
|
|
1452
|
+
ChannlizeClient as default,
|
|
1453
|
+
isAgentMessageWebhook,
|
|
1454
|
+
isQuestionEscalatedWebhook,
|
|
1455
|
+
isQuestionOutcomeMessage,
|
|
1456
|
+
isQuestionOutcomeWebhook,
|
|
1457
|
+
isRetryable,
|
|
1458
|
+
isTaskHandoffMessage,
|
|
1459
|
+
isValidEventKey,
|
|
1460
|
+
keyKind,
|
|
1461
|
+
parseAgentWebhook,
|
|
1462
|
+
parseAgentWebhookEvent,
|
|
1463
|
+
parseApiError,
|
|
1464
|
+
readChannlizeEnv,
|
|
1465
|
+
retryAfter,
|
|
1466
|
+
verifyAgentWebhook
|
|
384
1467
|
};
|
|
385
1468
|
//# sourceMappingURL=index.mjs.map
|