@noodleseed/assistant 1.1.0 → 1.2.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/README.md +90 -7
- package/dist/chunk-J2YDKISA.js +1113 -0
- package/dist/chunk-J2YDKISA.js.map +1 -0
- package/dist/chunk-WUDHE5BD.js +636 -0
- package/dist/chunk-WUDHE5BD.js.map +1 -0
- package/dist/client-CwvdK4O6.d.cts +165 -0
- package/dist/client-DkAfqcSb.d.ts +165 -0
- package/dist/client.cjs +662 -0
- package/dist/client.cjs.map +1 -0
- package/dist/client.d.cts +2 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +9 -0
- package/dist/client.js.map +1 -0
- package/dist/index.cjs +1273 -234
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -12
- package/dist/index.d.ts +14 -12
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1273 -234
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +2 -1
- package/dist/react.d.ts +2 -1
- package/dist/react.js +2 -1
- package/dist/react.js.map +1 -1
- package/dist/server.cjs +3 -1
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +12 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +3 -1
- package/dist/server.js.map +1 -1
- package/package.json +10 -1
- package/dist/chunk-OXFV6KZ2.js +0 -698
- package/dist/chunk-OXFV6KZ2.js.map +0 -1
package/dist/client.cjs
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/client.ts
|
|
21
|
+
var client_exports = {};
|
|
22
|
+
__export(client_exports, {
|
|
23
|
+
AssistantClientError: () => AssistantClientError,
|
|
24
|
+
createAssistantClient: () => createAssistantClient
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(client_exports);
|
|
27
|
+
|
|
28
|
+
// src/events.ts
|
|
29
|
+
function toAssistantClientEvent(event) {
|
|
30
|
+
const value = event.data;
|
|
31
|
+
switch (event.event) {
|
|
32
|
+
case "content":
|
|
33
|
+
if (typeof value.delta === "string" && hasOptionalTurnId(value)) {
|
|
34
|
+
return event;
|
|
35
|
+
}
|
|
36
|
+
break;
|
|
37
|
+
case "tool_proposed":
|
|
38
|
+
if (hasString(value, "id") && hasString(value, "tool") && hasOptionalJson(value.arguments) && hasOptionalString(value.expiresAt) && (value.requiresConfirmation === void 0 || value.requiresConfirmation === true) && hasOptionalTurnId(value)) {
|
|
39
|
+
return event;
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
case "input_requested":
|
|
43
|
+
if (hasString(value, "id") && hasString(value, "message") && isRecord(value.requestedSchema) && hasString(value, "expiresAt") && hasOptionalTurnId(value)) {
|
|
44
|
+
return event;
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
case "interaction_resolved":
|
|
48
|
+
if (hasString(value, "id") && (value.action === void 0 || isInteractionAction(value.action)) && hasOptionalTurnId(value)) {
|
|
49
|
+
return event;
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
case "tool_completed":
|
|
53
|
+
if (hasString(value, "id") && hasString(value, "tool") && isJsonValue(value.result, 0) && (value.replayed === void 0 || value.replayed === true) && hasOptionalTurnId(value)) {
|
|
54
|
+
return event;
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
case "view_available":
|
|
58
|
+
if (isViewAvailableDetail(value)) return event;
|
|
59
|
+
break;
|
|
60
|
+
case "error":
|
|
61
|
+
if (hasString(value, "code") && (value.status === void 0 || typeof value.status === "number") && (value.retryable === void 0 || typeof value.retryable === "boolean") && hasOptionalTurnId(value)) {
|
|
62
|
+
return event;
|
|
63
|
+
}
|
|
64
|
+
break;
|
|
65
|
+
case "done":
|
|
66
|
+
if (hasOptionalTurnId(value)) return event;
|
|
67
|
+
break;
|
|
68
|
+
case "interaction_proposed":
|
|
69
|
+
if (hasString(value, "id") && (hasString(value, "tool") || hasString(value, "title")) && hasOptionalJson(value.arguments)) {
|
|
70
|
+
return event;
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
return { event: "unrecognized", data: { name: event.event, payload: value } };
|
|
75
|
+
}
|
|
76
|
+
function isViewAvailableDetail(value) {
|
|
77
|
+
return hasString(value, "id") && hasString(value, "tool") && typeof value.resourceUri === "string" && value.resourceUri.startsWith("ui://") && hasOptionalString(value.title) && (value.replayed === void 0 || value.replayed === true) && isJsonValue(value.result, 0) && hasOptionalTurnId(value);
|
|
78
|
+
}
|
|
79
|
+
function hasOptionalTurnId(value) {
|
|
80
|
+
return hasOptionalString(value.turnId);
|
|
81
|
+
}
|
|
82
|
+
function hasString(value, key) {
|
|
83
|
+
const entry = value[key];
|
|
84
|
+
return typeof entry === "string" && entry.length > 0;
|
|
85
|
+
}
|
|
86
|
+
function hasOptionalString(value) {
|
|
87
|
+
return value === void 0 || typeof value === "string";
|
|
88
|
+
}
|
|
89
|
+
function hasOptionalJson(value) {
|
|
90
|
+
return value === void 0 || isJsonValue(value, 0);
|
|
91
|
+
}
|
|
92
|
+
function isInteractionAction(value) {
|
|
93
|
+
return value === "accept" || value === "decline" || value === "cancel";
|
|
94
|
+
}
|
|
95
|
+
function isJsonValue(value, depth) {
|
|
96
|
+
if (depth > 16) return false;
|
|
97
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
98
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
99
|
+
if (Array.isArray(value)) {
|
|
100
|
+
return value.length <= 256 && value.every((entry) => isJsonValue(entry, depth + 1));
|
|
101
|
+
}
|
|
102
|
+
if (!isRecord(value)) return false;
|
|
103
|
+
const entries = Object.entries(value);
|
|
104
|
+
return entries.length <= 256 && entries.every(([, entry]) => isJsonValue(entry, depth + 1));
|
|
105
|
+
}
|
|
106
|
+
function isRecord(value) {
|
|
107
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/model-context.ts
|
|
111
|
+
var MAX_BYTES = 16 * 1024;
|
|
112
|
+
var MAX_DEPTH = 8;
|
|
113
|
+
var MAX_ENTRIES = 128;
|
|
114
|
+
var SENSITIVE_KEY = /(?:secret|token|api[-_]?key|password|credential|authorization|cookie)/i;
|
|
115
|
+
var CREDENTIAL_SHAPED_TEXT = /(?:-----BEGIN [A-Z ]*PRIVATE KEY-----|\bsk-[A-Za-z0-9_-]{20,}\b|\b(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]{20,}\b|\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b)/i;
|
|
116
|
+
function copyAssistantModelContext(update) {
|
|
117
|
+
const copied = copyJsonValue(update, "$", 0, /* @__PURE__ */ new Set());
|
|
118
|
+
assertModelContextShape(copied);
|
|
119
|
+
const encoded = JSON.stringify(copied);
|
|
120
|
+
if (new TextEncoder().encode(encoded).byteLength > MAX_BYTES) {
|
|
121
|
+
throw new Error("Model context must not exceed 16 KiB");
|
|
122
|
+
}
|
|
123
|
+
return copied;
|
|
124
|
+
}
|
|
125
|
+
function assertModelContextShape(value) {
|
|
126
|
+
if (!isPlainRecord(value)) throw new Error("Model context must be a JSON object");
|
|
127
|
+
for (const key of Object.keys(value)) {
|
|
128
|
+
if (key !== "content" && key !== "structuredContent") {
|
|
129
|
+
throw new Error(`Model context contains unknown field ${key}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (value.content !== void 0) {
|
|
133
|
+
if (!Array.isArray(value.content)) throw new Error("Model context content must be an array");
|
|
134
|
+
for (const part of value.content) {
|
|
135
|
+
if (!isPlainRecord(part) || Object.keys(part).some((key) => key !== "type" && key !== "text") || part.type !== "text" || typeof part.text !== "string") {
|
|
136
|
+
throw new Error("Model context content supports text parts only");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (value.structuredContent !== void 0 && !isPlainRecord(value.structuredContent)) {
|
|
141
|
+
throw new Error("Model context structuredContent must be an object");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function copyJsonValue(value, path, depth, ancestors) {
|
|
145
|
+
if (depth > MAX_DEPTH) throw new Error(`Model context exceeds maximum depth at ${path}`);
|
|
146
|
+
if (typeof value === "string") {
|
|
147
|
+
if (CREDENTIAL_SHAPED_TEXT.test(value)) {
|
|
148
|
+
throw new Error(`Model context contains credential-shaped text at ${path}`);
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
if (value === null || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value)) {
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
if (typeof value !== "object") throw new Error(`Model context contains non-JSON data at ${path}`);
|
|
156
|
+
if (hasToJsonProperty(value)) {
|
|
157
|
+
throw new Error(`Model context must not define or inherit toJSON at ${path}`);
|
|
158
|
+
}
|
|
159
|
+
const prototype = Object.getPrototypeOf(value);
|
|
160
|
+
if (Array.isArray(value)) {
|
|
161
|
+
if (prototype !== Array.prototype) {
|
|
162
|
+
throw new Error(`Model context contains a non-JSON object at ${path}`);
|
|
163
|
+
}
|
|
164
|
+
} else if (prototype !== Object.prototype && prototype !== null) {
|
|
165
|
+
throw new Error(`Model context contains a non-JSON object at ${path}`);
|
|
166
|
+
}
|
|
167
|
+
if (ancestors.has(value)) throw new Error(`Model context contains a cycle at ${path}`);
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
if (value.length > MAX_ENTRIES) {
|
|
170
|
+
throw new Error(`Model context has more than 128 entries at ${path}`);
|
|
171
|
+
}
|
|
172
|
+
const copy2 = [];
|
|
173
|
+
ancestors.add(value);
|
|
174
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
175
|
+
const entry = Object.hasOwn(value, index) ? value[index] : null;
|
|
176
|
+
copy2.push(copyJsonValue(entry, `${path}.${index}`, depth + 1, ancestors));
|
|
177
|
+
}
|
|
178
|
+
ancestors.delete(value);
|
|
179
|
+
return copy2;
|
|
180
|
+
}
|
|
181
|
+
const entries = Object.entries(value);
|
|
182
|
+
if (entries.length > MAX_ENTRIES) {
|
|
183
|
+
throw new Error(`Model context has more than 128 entries at ${path}`);
|
|
184
|
+
}
|
|
185
|
+
const copy = /* @__PURE__ */ Object.create(null);
|
|
186
|
+
ancestors.add(value);
|
|
187
|
+
for (const [key, entry] of entries) {
|
|
188
|
+
if (SENSITIVE_KEY.test(key)) {
|
|
189
|
+
throw new Error(`Model context contains sensitive key ${path}.${key}`);
|
|
190
|
+
}
|
|
191
|
+
copy[key] = copyJsonValue(entry, `${path}.${key}`, depth + 1, ancestors);
|
|
192
|
+
}
|
|
193
|
+
ancestors.delete(value);
|
|
194
|
+
return copy;
|
|
195
|
+
}
|
|
196
|
+
function isPlainRecord(value) {
|
|
197
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
198
|
+
const prototype = Object.getPrototypeOf(value);
|
|
199
|
+
return prototype === Object.prototype || prototype === null;
|
|
200
|
+
}
|
|
201
|
+
function hasToJsonProperty(value) {
|
|
202
|
+
let current = value;
|
|
203
|
+
const visited = /* @__PURE__ */ new Set();
|
|
204
|
+
while (current !== null && !visited.has(current)) {
|
|
205
|
+
visited.add(current);
|
|
206
|
+
if (Object.getOwnPropertyDescriptor(current, "toJSON") !== void 0) return true;
|
|
207
|
+
current = Object.getPrototypeOf(current);
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/transport.ts
|
|
213
|
+
var AssistantTransportError = class extends Error {
|
|
214
|
+
code = "invalid_response";
|
|
215
|
+
constructor(message, options) {
|
|
216
|
+
super(message, options);
|
|
217
|
+
this.name = "AssistantTransportError";
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
async function consumeAssistantEvents(response, onEvent) {
|
|
221
|
+
const stream = new AssistantEventStream(onEvent);
|
|
222
|
+
if (!response.body) {
|
|
223
|
+
let text;
|
|
224
|
+
try {
|
|
225
|
+
text = await response.text();
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (isAbortError(error)) throw error;
|
|
228
|
+
throw invalidStream("assistant event stream could not be read", error);
|
|
229
|
+
}
|
|
230
|
+
stream.push(text);
|
|
231
|
+
stream.finish(response.headers.get("content-type") ?? "");
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const reader = response.body.getReader();
|
|
235
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
236
|
+
while (true) {
|
|
237
|
+
let result;
|
|
238
|
+
try {
|
|
239
|
+
result = await reader.read();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (isAbortError(error)) throw error;
|
|
242
|
+
throw invalidStream("assistant event stream ended unexpectedly", error);
|
|
243
|
+
}
|
|
244
|
+
let chunk;
|
|
245
|
+
try {
|
|
246
|
+
chunk = decoder.decode(result.value, { stream: !result.done });
|
|
247
|
+
} catch (error) {
|
|
248
|
+
throw invalidStream("assistant event stream contained invalid UTF-8", error);
|
|
249
|
+
}
|
|
250
|
+
stream.push(chunk);
|
|
251
|
+
if (result.done) break;
|
|
252
|
+
}
|
|
253
|
+
stream.finish(response.headers.get("content-type") ?? "");
|
|
254
|
+
}
|
|
255
|
+
var AssistantEventStream = class {
|
|
256
|
+
#onEvent;
|
|
257
|
+
#pending = "";
|
|
258
|
+
#terminal;
|
|
259
|
+
constructor(onEvent) {
|
|
260
|
+
this.#onEvent = onEvent;
|
|
261
|
+
}
|
|
262
|
+
push(chunk) {
|
|
263
|
+
this.#pending += chunk;
|
|
264
|
+
const protectedCarriageReturn = this.#pending.endsWith("\r");
|
|
265
|
+
const normalizable = protectedCarriageReturn ? this.#pending.slice(0, -1) : this.#pending;
|
|
266
|
+
const frames = normalizeLineEndings(normalizable).split("\n\n");
|
|
267
|
+
this.#pending = `${frames.pop() ?? ""}${protectedCarriageReturn ? "\r" : ""}`;
|
|
268
|
+
for (const frame of frames) this.#acceptFrame(frame);
|
|
269
|
+
}
|
|
270
|
+
finish(contentType) {
|
|
271
|
+
if (this.#pending.trim().length > 0) {
|
|
272
|
+
throw invalidStream("assistant event stream ended with an unterminated frame");
|
|
273
|
+
}
|
|
274
|
+
if (!this.#terminal) {
|
|
275
|
+
throw invalidStream(
|
|
276
|
+
`assistant response did not contain a terminal done event (content-type: ${contentType || "unknown"})`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
this.#onEvent(this.#terminal);
|
|
280
|
+
}
|
|
281
|
+
#acceptFrame(frame) {
|
|
282
|
+
const event = parseFrame(frame);
|
|
283
|
+
if (!event) return;
|
|
284
|
+
if (this.#terminal) {
|
|
285
|
+
throw invalidStream("assistant event stream contained an event after done");
|
|
286
|
+
}
|
|
287
|
+
if (event.event === "done") {
|
|
288
|
+
this.#terminal = event;
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
this.#onEvent(event);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
function parseFrame(frame) {
|
|
295
|
+
let event;
|
|
296
|
+
const data = [];
|
|
297
|
+
let hasFields = false;
|
|
298
|
+
for (const line of frame.split("\n")) {
|
|
299
|
+
if (!line || line.startsWith(":")) continue;
|
|
300
|
+
hasFields = true;
|
|
301
|
+
const separator = line.indexOf(":");
|
|
302
|
+
const field = separator < 0 ? line : line.slice(0, separator);
|
|
303
|
+
const value = separator < 0 ? "" : line.slice(separator + 1).replace(/^ /, "");
|
|
304
|
+
if (field === "event") event = value;
|
|
305
|
+
if (field === "data") data.push(value);
|
|
306
|
+
}
|
|
307
|
+
if (!hasFields) return void 0;
|
|
308
|
+
if (!event || data.length === 0) {
|
|
309
|
+
throw invalidStream("assistant event stream contained a malformed frame");
|
|
310
|
+
}
|
|
311
|
+
let parsed;
|
|
312
|
+
try {
|
|
313
|
+
parsed = JSON.parse(data.join("\n"));
|
|
314
|
+
} catch (error) {
|
|
315
|
+
throw invalidStream("assistant event stream contained malformed JSON", error);
|
|
316
|
+
}
|
|
317
|
+
if (event === "done" && !isValidDonePayload(parsed)) {
|
|
318
|
+
throw invalidStream("assistant event stream contained an invalid done event");
|
|
319
|
+
}
|
|
320
|
+
return { event, data: isRecord2(parsed) ? parsed : { value: parsed } };
|
|
321
|
+
}
|
|
322
|
+
function normalizeLineEndings(input) {
|
|
323
|
+
return input.replaceAll("\r\n", "\n").replaceAll("\r", "\n");
|
|
324
|
+
}
|
|
325
|
+
function isValidDonePayload(value) {
|
|
326
|
+
return isRecord2(value) && (value.turnId === void 0 || typeof value.turnId === "string" && value.turnId.length > 0);
|
|
327
|
+
}
|
|
328
|
+
function isRecord2(value) {
|
|
329
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
330
|
+
}
|
|
331
|
+
function invalidStream(message, cause) {
|
|
332
|
+
return new AssistantTransportError(message, cause === void 0 ? void 0 : { cause });
|
|
333
|
+
}
|
|
334
|
+
function isAbortError(error) {
|
|
335
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/client.ts
|
|
339
|
+
var AssistantClientError = class extends Error {
|
|
340
|
+
detail;
|
|
341
|
+
constructor(detail, message, options) {
|
|
342
|
+
super(message, options);
|
|
343
|
+
this.name = "AssistantClientError";
|
|
344
|
+
this.detail = detail;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
var DefaultAssistantClient = class {
|
|
348
|
+
#sessionEndpoint;
|
|
349
|
+
#fetch;
|
|
350
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
351
|
+
#clientContext;
|
|
352
|
+
#session;
|
|
353
|
+
#context;
|
|
354
|
+
#modelContext;
|
|
355
|
+
#active;
|
|
356
|
+
constructor(options) {
|
|
357
|
+
if (!options.sessionEndpoint) throw new Error("sessionEndpoint is required");
|
|
358
|
+
this.#sessionEndpoint = options.sessionEndpoint;
|
|
359
|
+
this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
360
|
+
this.#context = options.context ? { ...options.context } : void 0;
|
|
361
|
+
this.#modelContext = options.modelContext ? copyAssistantModelContext(options.modelContext) : void 0;
|
|
362
|
+
this.#clientContext = options.clientContext;
|
|
363
|
+
}
|
|
364
|
+
subscribe(listener) {
|
|
365
|
+
this.#listeners.add(listener);
|
|
366
|
+
return () => this.#listeners.delete(listener);
|
|
367
|
+
}
|
|
368
|
+
async sendMessage(text) {
|
|
369
|
+
const message = text.trim();
|
|
370
|
+
if (!message) return;
|
|
371
|
+
await this.#singleFlight(async (signal) => {
|
|
372
|
+
await this.#sendTurn(message, true, signal, this.#modelContext);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
async respond(id, resolution) {
|
|
376
|
+
if (!id) {
|
|
377
|
+
throw clientError("invalid_request", "interaction id is required", false);
|
|
378
|
+
}
|
|
379
|
+
await this.#singleFlight(async (signal) => {
|
|
380
|
+
const session = this.#session;
|
|
381
|
+
if (!session) {
|
|
382
|
+
throw clientError("confirmation_expired", "assistant session is not active", false);
|
|
383
|
+
}
|
|
384
|
+
const modernEndpoint = session.endpoints.interactions;
|
|
385
|
+
if (!modernEndpoint && resolution.action !== "accept") {
|
|
386
|
+
throw clientError(
|
|
387
|
+
"unsupported_service",
|
|
388
|
+
"the assistant service does not support declining or cancelling interactions",
|
|
389
|
+
false
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
const endpoint = modernEndpoint ?? session.endpoints.toolConfirmations;
|
|
393
|
+
const body = modernEndpoint ? {
|
|
394
|
+
id,
|
|
395
|
+
action: resolution.action,
|
|
396
|
+
...resolution.action === "accept" && resolution.content !== void 0 ? { content: resolution.content } : {}
|
|
397
|
+
} : { id };
|
|
398
|
+
this.#emit({ event: "interaction_started", data: { id, action: resolution.action } });
|
|
399
|
+
const response = await this.#request(
|
|
400
|
+
endpoint,
|
|
401
|
+
{
|
|
402
|
+
method: "POST",
|
|
403
|
+
headers: authorizedHeaders(session.token, "text/event-stream"),
|
|
404
|
+
body: JSON.stringify(body),
|
|
405
|
+
signal
|
|
406
|
+
},
|
|
407
|
+
"confirmation_failed"
|
|
408
|
+
);
|
|
409
|
+
if (response.status === 401) {
|
|
410
|
+
this.#session = void 0;
|
|
411
|
+
this.#emit({ event: "session_expired", data: {} });
|
|
412
|
+
}
|
|
413
|
+
if (!response.ok) {
|
|
414
|
+
const serverCode = await readStableErrorCode(response);
|
|
415
|
+
const expired = response.status === 401 || response.status === 409;
|
|
416
|
+
throw clientError(
|
|
417
|
+
serverCode ?? (expired ? "confirmation_expired" : "confirmation_failed"),
|
|
418
|
+
`Assistant interaction failed (${response.status})`,
|
|
419
|
+
false,
|
|
420
|
+
response.status
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
await this.#consume(response);
|
|
424
|
+
this.#emit({ event: "interaction_completed", data: { id, action: resolution.action } });
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
updateContext(context) {
|
|
428
|
+
this.#context = { ...context };
|
|
429
|
+
this.#emit({ event: "context_changed", data: { context: this.#context } });
|
|
430
|
+
}
|
|
431
|
+
updateModelContext(update) {
|
|
432
|
+
this.#modelContext = copyAssistantModelContext(update);
|
|
433
|
+
this.#emit({ event: "model_context_changed", data: { modelContext: this.#modelContext } });
|
|
434
|
+
}
|
|
435
|
+
resetSession() {
|
|
436
|
+
this.#session = void 0;
|
|
437
|
+
this.#emit({ event: "session_reset", data: {} });
|
|
438
|
+
}
|
|
439
|
+
abort() {
|
|
440
|
+
this.#active?.abort();
|
|
441
|
+
}
|
|
442
|
+
async #sendTurn(message, mayRetry, signal, modelContext) {
|
|
443
|
+
const session = this.#session ?? await this.#createSession(signal);
|
|
444
|
+
if (mayRetry) this.#emit({ event: "message_started", data: { message } });
|
|
445
|
+
const clientContext = this.#resolveClientContext();
|
|
446
|
+
const response = await this.#request(
|
|
447
|
+
session.endpoints.turns,
|
|
448
|
+
{
|
|
449
|
+
method: "POST",
|
|
450
|
+
headers: authorizedHeaders(session.token, "text/event-stream"),
|
|
451
|
+
body: JSON.stringify({
|
|
452
|
+
message,
|
|
453
|
+
...clientContext ? { clientContext } : {},
|
|
454
|
+
...modelContext ? { modelContext } : {}
|
|
455
|
+
}),
|
|
456
|
+
signal
|
|
457
|
+
},
|
|
458
|
+
"turn_failed"
|
|
459
|
+
);
|
|
460
|
+
if (response.status === 401 && mayRetry) {
|
|
461
|
+
this.#session = void 0;
|
|
462
|
+
this.#emit({ event: "session_expired", data: {} });
|
|
463
|
+
await this.#sendTurn(message, false, signal, modelContext);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (!response.ok) {
|
|
467
|
+
throw clientError(
|
|
468
|
+
"turn_failed",
|
|
469
|
+
`Assistant turn failed (${response.status})`,
|
|
470
|
+
false,
|
|
471
|
+
response.status
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
await this.#consume(response);
|
|
475
|
+
this.#emit({ event: "message_completed", data: {} });
|
|
476
|
+
}
|
|
477
|
+
#resolveClientContext() {
|
|
478
|
+
let value;
|
|
479
|
+
try {
|
|
480
|
+
value = typeof this.#clientContext === "function" ? this.#clientContext() : this.#clientContext;
|
|
481
|
+
} catch (error) {
|
|
482
|
+
throw clientError(
|
|
483
|
+
"turn_failed",
|
|
484
|
+
"Assistant client context provider failed",
|
|
485
|
+
false,
|
|
486
|
+
void 0,
|
|
487
|
+
{
|
|
488
|
+
cause: error
|
|
489
|
+
}
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
if (!value) return void 0;
|
|
493
|
+
const context = {
|
|
494
|
+
...typeof value.locale === "string" && value.locale ? { locale: value.locale } : {},
|
|
495
|
+
...typeof value.timeZone === "string" && value.timeZone ? { timeZone: value.timeZone } : {}
|
|
496
|
+
};
|
|
497
|
+
return Object.keys(context).length > 0 ? context : void 0;
|
|
498
|
+
}
|
|
499
|
+
async #createSession(signal) {
|
|
500
|
+
const response = await this.#request(
|
|
501
|
+
this.#sessionEndpoint,
|
|
502
|
+
{
|
|
503
|
+
method: "POST",
|
|
504
|
+
headers: { Accept: "application/json", "Content-Type": "application/json" },
|
|
505
|
+
body: JSON.stringify(this.#context ? { context: this.#context } : {}),
|
|
506
|
+
credentials: "same-origin",
|
|
507
|
+
signal
|
|
508
|
+
},
|
|
509
|
+
"session_failed"
|
|
510
|
+
);
|
|
511
|
+
if (!response.ok) {
|
|
512
|
+
throw clientError(
|
|
513
|
+
"session_failed",
|
|
514
|
+
`Assistant session failed (${response.status})`,
|
|
515
|
+
true,
|
|
516
|
+
response.status
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
let value;
|
|
520
|
+
try {
|
|
521
|
+
value = await response.json();
|
|
522
|
+
} catch (error) {
|
|
523
|
+
throw clientError(
|
|
524
|
+
"session_failed",
|
|
525
|
+
"Assistant session returned invalid JSON",
|
|
526
|
+
true,
|
|
527
|
+
void 0,
|
|
528
|
+
{
|
|
529
|
+
cause: error
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
const session = parseSession(value);
|
|
534
|
+
this.#session = session;
|
|
535
|
+
this.#emit({
|
|
536
|
+
event: "session_started",
|
|
537
|
+
data: {
|
|
538
|
+
expiresAt: session.expiresAt,
|
|
539
|
+
...session.configuration ? { configuration: session.configuration } : {}
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
return session;
|
|
543
|
+
}
|
|
544
|
+
async #consume(response) {
|
|
545
|
+
let streamError;
|
|
546
|
+
try {
|
|
547
|
+
await consumeAssistantEvents(response, (event) => {
|
|
548
|
+
const clientEvent = toAssistantClientEvent(event);
|
|
549
|
+
this.#emit(clientEvent);
|
|
550
|
+
if (clientEvent.event !== "error" || typeof clientEvent.data.code !== "string") return;
|
|
551
|
+
streamError = {
|
|
552
|
+
code: clientEvent.data.code,
|
|
553
|
+
...typeof clientEvent.data.status === "number" ? { status: clientEvent.data.status } : {},
|
|
554
|
+
retryable: clientEvent.data.retryable === true
|
|
555
|
+
};
|
|
556
|
+
});
|
|
557
|
+
} catch (error) {
|
|
558
|
+
if (error instanceof AssistantTransportError) {
|
|
559
|
+
throw clientError(error.code, error.message, false, void 0, { cause: error });
|
|
560
|
+
}
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
563
|
+
if (streamError) {
|
|
564
|
+
throw clientError(
|
|
565
|
+
streamError.code,
|
|
566
|
+
`Assistant stream failed (${streamError.code})`,
|
|
567
|
+
streamError.retryable,
|
|
568
|
+
streamError.status
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async #request(input, init, failureCode) {
|
|
573
|
+
try {
|
|
574
|
+
return await this.#fetch(input, init);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
if (init.signal?.aborted) throw error;
|
|
577
|
+
throw clientError(failureCode, "Assistant request failed", true, void 0, { cause: error });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async #singleFlight(operation) {
|
|
581
|
+
if (this.#active) {
|
|
582
|
+
throw clientError("request_in_progress", "an assistant request is already in progress", true);
|
|
583
|
+
}
|
|
584
|
+
const controller = new AbortController();
|
|
585
|
+
this.#active = controller;
|
|
586
|
+
try {
|
|
587
|
+
await operation(controller.signal);
|
|
588
|
+
} catch (error) {
|
|
589
|
+
if (controller.signal.aborted) {
|
|
590
|
+
throw clientError("request_aborted", "assistant request was aborted", true, void 0, {
|
|
591
|
+
cause: error
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
throw error;
|
|
595
|
+
} finally {
|
|
596
|
+
this.#active = void 0;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
#emit(event) {
|
|
600
|
+
for (const listener of this.#listeners) {
|
|
601
|
+
try {
|
|
602
|
+
listener(event);
|
|
603
|
+
} catch {
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
function createAssistantClient(options) {
|
|
609
|
+
return new DefaultAssistantClient(options);
|
|
610
|
+
}
|
|
611
|
+
function authorizedHeaders(token, accept) {
|
|
612
|
+
return {
|
|
613
|
+
Authorization: `Bearer ${token}`,
|
|
614
|
+
"Content-Type": "application/json",
|
|
615
|
+
Accept: accept
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
function parseSession(value) {
|
|
619
|
+
if (!isRecord3(value) || !isRecord3(value.endpoints)) {
|
|
620
|
+
throw clientError("session_failed", "Assistant session response is invalid", true);
|
|
621
|
+
}
|
|
622
|
+
const interactions = value.endpoints.interactions;
|
|
623
|
+
if (typeof value.token !== "string" || typeof value.expiresAt !== "string" || typeof value.endpoints.turns !== "string" || typeof value.endpoints.toolConfirmations !== "string" || interactions !== void 0 && typeof interactions !== "string") {
|
|
624
|
+
throw clientError("session_failed", "Assistant session response is invalid", true);
|
|
625
|
+
}
|
|
626
|
+
return {
|
|
627
|
+
token: value.token,
|
|
628
|
+
expiresAt: value.expiresAt,
|
|
629
|
+
endpoints: {
|
|
630
|
+
turns: value.endpoints.turns,
|
|
631
|
+
toolConfirmations: value.endpoints.toolConfirmations,
|
|
632
|
+
...typeof interactions === "string" ? { interactions } : {}
|
|
633
|
+
},
|
|
634
|
+
...isRecord3(value.configuration) ? { configuration: value.configuration } : {}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
function isRecord3(value) {
|
|
638
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
639
|
+
}
|
|
640
|
+
async function readStableErrorCode(response) {
|
|
641
|
+
let value;
|
|
642
|
+
try {
|
|
643
|
+
value = await response.json();
|
|
644
|
+
} catch {
|
|
645
|
+
return void 0;
|
|
646
|
+
}
|
|
647
|
+
if (!isRecord3(value) || typeof value.code !== "string") return void 0;
|
|
648
|
+
return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
|
|
649
|
+
}
|
|
650
|
+
function clientError(code, message, retryable, status, options) {
|
|
651
|
+
return new AssistantClientError(
|
|
652
|
+
{ code, ...status === void 0 ? {} : { status }, retryable },
|
|
653
|
+
message,
|
|
654
|
+
options
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
658
|
+
0 && (module.exports = {
|
|
659
|
+
AssistantClientError,
|
|
660
|
+
createAssistantClient
|
|
661
|
+
});
|
|
662
|
+
//# sourceMappingURL=client.cjs.map
|