@laintern/chat-sdk 0.1.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 +121 -0
- package/dist/chunk-WMKNNVMS.js +266 -0
- package/dist/chunk-WMKNNVMS.js.map +1 -0
- package/dist/conversation-7VzVDT95.d.cts +284 -0
- package/dist/conversation-7VzVDT95.d.ts +284 -0
- package/dist/index.cjs +538 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +43 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +270 -0
- package/dist/index.js.map +1 -0
- package/dist/react/index.cjs +268 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +25 -0
- package/dist/react/index.d.ts +25 -0
- package/dist/react/index.js +30 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +67 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var LainternError = class extends Error {
|
|
5
|
+
code;
|
|
6
|
+
status;
|
|
7
|
+
/** Set on RATE_LIMIT_EXCEEDED. */
|
|
8
|
+
rateLimitScope;
|
|
9
|
+
rateLimitWindow;
|
|
10
|
+
/** Set on MCP_UNAVAILABLE: which dependency failed, and how. */
|
|
11
|
+
reason;
|
|
12
|
+
constructor(code, message, options = {}) {
|
|
13
|
+
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
14
|
+
this.name = "LainternError";
|
|
15
|
+
this.code = code;
|
|
16
|
+
this.status = options.status ?? null;
|
|
17
|
+
if (options.rateLimitScope) this.rateLimitScope = options.rateLimitScope;
|
|
18
|
+
if (options.rateLimitWindow) this.rateLimitWindow = options.rateLimitWindow;
|
|
19
|
+
if (options.reason) this.reason = options.reason;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* True when retrying later could plausibly succeed: a rate limit, or a
|
|
23
|
+
* dependency the server could not reach. An expired session is NOT retryable
|
|
24
|
+
* — the host has to mint a new token first.
|
|
25
|
+
*/
|
|
26
|
+
get isRetryable() {
|
|
27
|
+
return this.code === "RATE_LIMIT_EXCEEDED" || this.reason === "transport";
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
function pick(value, allowed) {
|
|
31
|
+
return typeof value === "string" && allowed.includes(value) ? value : void 0;
|
|
32
|
+
}
|
|
33
|
+
async function errorFromResponse(response) {
|
|
34
|
+
let code = `HTTP_${response.status}`;
|
|
35
|
+
let message = "An unexpected error occurred.";
|
|
36
|
+
let rateLimitScope;
|
|
37
|
+
let rateLimitWindow;
|
|
38
|
+
let reason;
|
|
39
|
+
try {
|
|
40
|
+
const body = await response.json();
|
|
41
|
+
if (body?.error?.code) {
|
|
42
|
+
code = body.error.code;
|
|
43
|
+
message = body.error.message ?? message;
|
|
44
|
+
rateLimitScope = pick(body.error.scope, ["user", "agent"]);
|
|
45
|
+
rateLimitWindow = pick(body.error.window, ["minute", "hour", "day"]);
|
|
46
|
+
if (typeof body.error.reason === "string") reason = body.error.reason;
|
|
47
|
+
} else if (typeof body?.message === "string") {
|
|
48
|
+
message = body.message;
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
return new LainternError(code, message, {
|
|
53
|
+
status: response.status,
|
|
54
|
+
rateLimitScope,
|
|
55
|
+
rateLimitWindow,
|
|
56
|
+
reason
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/sse.ts
|
|
61
|
+
var DONE = "[DONE]";
|
|
62
|
+
async function* parseSSE(response) {
|
|
63
|
+
const reader = response.body?.getReader();
|
|
64
|
+
if (!reader) throw new Error("Response body is not readable");
|
|
65
|
+
const decoder = new TextDecoder();
|
|
66
|
+
let buffer = "";
|
|
67
|
+
try {
|
|
68
|
+
while (true) {
|
|
69
|
+
const { done, value } = await reader.read();
|
|
70
|
+
if (done) break;
|
|
71
|
+
buffer += decoder.decode(value, { stream: true });
|
|
72
|
+
let sepIndex;
|
|
73
|
+
while ((sepIndex = indexOfFrameBoundary(buffer)) !== -1) {
|
|
74
|
+
const frame = buffer.slice(0, sepIndex);
|
|
75
|
+
buffer = buffer.slice(boundaryEnd(buffer, sepIndex));
|
|
76
|
+
const parsed2 = parseFrame(frame);
|
|
77
|
+
if (parsed2 === DONE) return;
|
|
78
|
+
if (parsed2) yield parsed2;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const parsed = parseFrame(buffer);
|
|
82
|
+
if (parsed && parsed !== DONE) yield parsed;
|
|
83
|
+
} finally {
|
|
84
|
+
reader.releaseLock();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function indexOfFrameBoundary(buffer) {
|
|
88
|
+
const lf = buffer.indexOf("\n\n");
|
|
89
|
+
const crlf = buffer.indexOf("\r\n\r\n");
|
|
90
|
+
if (lf === -1) return crlf;
|
|
91
|
+
if (crlf === -1) return lf;
|
|
92
|
+
return Math.min(lf, crlf);
|
|
93
|
+
}
|
|
94
|
+
function boundaryEnd(buffer, index) {
|
|
95
|
+
return buffer.startsWith("\r\n\r\n", index) ? index + 4 : index + 2;
|
|
96
|
+
}
|
|
97
|
+
function parseFrame(frame) {
|
|
98
|
+
const trimmed = frame.trim();
|
|
99
|
+
if (!trimmed) return null;
|
|
100
|
+
const dataLines = [];
|
|
101
|
+
for (const rawLine of trimmed.split("\n")) {
|
|
102
|
+
const line = rawLine.replace(/\r$/, "");
|
|
103
|
+
if (line.startsWith(":")) continue;
|
|
104
|
+
if (!line.startsWith("data:")) continue;
|
|
105
|
+
dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
106
|
+
}
|
|
107
|
+
if (dataLines.length === 0) return null;
|
|
108
|
+
const payload = dataLines.join("\n");
|
|
109
|
+
if (payload === DONE) return DONE;
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(payload);
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/tokens.ts
|
|
118
|
+
var DEFAULT_REFRESH_BUFFER_MS = 6e4;
|
|
119
|
+
var FALLBACK_TTL_MS = 5 * 60 * 1e3;
|
|
120
|
+
var DRIFT_WARN_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
121
|
+
function decodeJwtExpiryMs(token) {
|
|
122
|
+
try {
|
|
123
|
+
const parts = token.split(".");
|
|
124
|
+
if (parts.length < 2) return null;
|
|
125
|
+
const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
126
|
+
if (typeof atob !== "function") return null;
|
|
127
|
+
const json = JSON.parse(atob(padded));
|
|
128
|
+
return typeof json.exp === "number" ? json.exp * 1e3 : null;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function toEpochMs(value) {
|
|
134
|
+
if (value == null) return null;
|
|
135
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
136
|
+
const parsed = Date.parse(value);
|
|
137
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
138
|
+
}
|
|
139
|
+
function resolveExpiry(token, reportedExpiresAt, onWarning) {
|
|
140
|
+
const jwtExpMs = decodeJwtExpiryMs(token);
|
|
141
|
+
const reported = toEpochMs(reportedExpiresAt);
|
|
142
|
+
if (jwtExpMs !== null) {
|
|
143
|
+
if (reported !== null && Math.abs(reported - jwtExpMs) > DRIFT_WARN_THRESHOLD_MS) {
|
|
144
|
+
onWarning?.(
|
|
145
|
+
"Token endpoint 'expires_at' drifts more than 5 minutes from the JWT 'exp'; using the JWT claim.",
|
|
146
|
+
{ jwtExpMs, reportedExpiresAtMs: reported, driftMs: reported - jwtExpMs }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return jwtExpMs;
|
|
150
|
+
}
|
|
151
|
+
if (reported !== null) return reported;
|
|
152
|
+
onWarning?.(
|
|
153
|
+
"Token has no readable 'exp' claim and the endpoint reported no expiry; caching for 5 minutes."
|
|
154
|
+
);
|
|
155
|
+
return Date.now() + FALLBACK_TTL_MS;
|
|
156
|
+
}
|
|
157
|
+
function createTokenManager(options) {
|
|
158
|
+
const buffer = options.refreshBufferMs ?? DEFAULT_REFRESH_BUFFER_MS;
|
|
159
|
+
const storage = options.storage;
|
|
160
|
+
const storageKey = options.storageKey ?? "laintern.token";
|
|
161
|
+
let cached = readStored();
|
|
162
|
+
let inFlight = null;
|
|
163
|
+
function readStored() {
|
|
164
|
+
if (!storage) return null;
|
|
165
|
+
try {
|
|
166
|
+
const raw = storage.getItem(storageKey);
|
|
167
|
+
if (!raw) return null;
|
|
168
|
+
const parsed = JSON.parse(raw);
|
|
169
|
+
if (typeof parsed.token !== "string" || typeof parsed.expiresAt !== "number") {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return { token: parsed.token, expiresAt: parsed.expiresAt };
|
|
173
|
+
} catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function writeStored(value) {
|
|
178
|
+
if (!storage) return;
|
|
179
|
+
try {
|
|
180
|
+
if (value === null) storage.removeItem(storageKey);
|
|
181
|
+
else storage.setItem(storageKey, JSON.stringify(value));
|
|
182
|
+
} catch {
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function isUsable(value) {
|
|
186
|
+
return value !== null && Date.now() < value.expiresAt - buffer;
|
|
187
|
+
}
|
|
188
|
+
async function refresh() {
|
|
189
|
+
let result;
|
|
190
|
+
try {
|
|
191
|
+
result = await options.fetchToken();
|
|
192
|
+
} catch (err) {
|
|
193
|
+
throw new LainternError("TOKEN_FETCH_FAILED", "Could not obtain a chat token.", {
|
|
194
|
+
cause: err
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
const response = typeof result === "string" ? { token: result } : result;
|
|
198
|
+
if (!response || typeof response.token !== "string" || response.token.length === 0) {
|
|
199
|
+
throw new LainternError(
|
|
200
|
+
"TOKEN_INVALID",
|
|
201
|
+
"Token endpoint returned no 'token' string."
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
const next = {
|
|
205
|
+
token: response.token,
|
|
206
|
+
expiresAt: resolveExpiry(response.token, response.expiresAt, options.onWarning)
|
|
207
|
+
};
|
|
208
|
+
cached = next;
|
|
209
|
+
writeStored(next);
|
|
210
|
+
return next.token;
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
async get() {
|
|
214
|
+
if (isUsable(cached)) return cached.token;
|
|
215
|
+
if (!inFlight) {
|
|
216
|
+
inFlight = refresh().finally(() => {
|
|
217
|
+
inFlight = null;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return inFlight;
|
|
221
|
+
},
|
|
222
|
+
clear() {
|
|
223
|
+
cached = null;
|
|
224
|
+
writeStored(null);
|
|
225
|
+
},
|
|
226
|
+
expiresAt() {
|
|
227
|
+
return cached?.expiresAt ?? null;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/client.ts
|
|
233
|
+
function tokenFetcher(option, doFetch) {
|
|
234
|
+
if (typeof option === "function") return option;
|
|
235
|
+
return async () => {
|
|
236
|
+
const res = await doFetch(option.endpoint, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
credentials: "same-origin",
|
|
239
|
+
headers: { "Content-Type": "application/json" },
|
|
240
|
+
...option.init
|
|
241
|
+
});
|
|
242
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
243
|
+
const body = await res.json();
|
|
244
|
+
const payload = body.data ?? body ?? {};
|
|
245
|
+
return {
|
|
246
|
+
token: payload.token,
|
|
247
|
+
expiresAt: payload.expires_at ?? null
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function createLainternClient(options) {
|
|
252
|
+
const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
253
|
+
const apiBase = options.apiBase.replace(/\/+$/, "");
|
|
254
|
+
const tokens = createTokenManager({
|
|
255
|
+
fetchToken: tokenFetcher(options.token, doFetch),
|
|
256
|
+
storage: options.storage,
|
|
257
|
+
...options.refreshBufferMs !== void 0 ? { refreshBufferMs: options.refreshBufferMs } : {},
|
|
258
|
+
...options.onWarning ? { onWarning: options.onWarning } : {}
|
|
259
|
+
});
|
|
260
|
+
async function authorized(path, init = {}) {
|
|
261
|
+
const token = await tokens.get();
|
|
262
|
+
return doFetch(`${apiBase}${path}`, {
|
|
263
|
+
...init,
|
|
264
|
+
headers: {
|
|
265
|
+
"Content-Type": "application/json",
|
|
266
|
+
...init.headers,
|
|
267
|
+
Authorization: `Bearer ${token}`
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
tokens,
|
|
273
|
+
async *sendMessage(message, sendOptions = {}) {
|
|
274
|
+
const body = { message };
|
|
275
|
+
if (sendOptions.sessionId) body.session_id = sendOptions.sessionId;
|
|
276
|
+
if (sendOptions.images?.length) body.images = sendOptions.images;
|
|
277
|
+
if (sendOptions.forceTool) body.force_tool = sendOptions.forceTool;
|
|
278
|
+
let response;
|
|
279
|
+
try {
|
|
280
|
+
response = await authorized("/v1/chat", {
|
|
281
|
+
method: "POST",
|
|
282
|
+
body: JSON.stringify(body),
|
|
283
|
+
...sendOptions.signal ? { signal: sendOptions.signal } : {}
|
|
284
|
+
});
|
|
285
|
+
} catch (err) {
|
|
286
|
+
if (err instanceof LainternError) throw err;
|
|
287
|
+
if (err?.name === "AbortError") throw err;
|
|
288
|
+
throw new LainternError(
|
|
289
|
+
"NETWORK_ERROR",
|
|
290
|
+
err instanceof Error ? err.message : "Network request failed.",
|
|
291
|
+
{ cause: err }
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
if (!response.ok) {
|
|
295
|
+
const error = await errorFromResponse(response);
|
|
296
|
+
if (response.status === 401) tokens.clear();
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
yield* parseSSE(response);
|
|
300
|
+
},
|
|
301
|
+
async getConfig() {
|
|
302
|
+
const res = await authorized("/v1/config", { method: "GET" });
|
|
303
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
304
|
+
const body = await res.json();
|
|
305
|
+
const payload = body.data ?? body ?? {};
|
|
306
|
+
const raw = payload.disclaimer;
|
|
307
|
+
return {
|
|
308
|
+
disclaimer: {
|
|
309
|
+
enabled: raw?.enabled !== false,
|
|
310
|
+
text: typeof raw?.text === "string" && raw.text.trim().length > 0 ? raw.text : null
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
},
|
|
314
|
+
async submitFeedback(messageId, feedback) {
|
|
315
|
+
const res = await authorized(`/v1/chat/messages/${messageId}/feedback`, {
|
|
316
|
+
method: "PATCH",
|
|
317
|
+
body: JSON.stringify({ feedback })
|
|
318
|
+
});
|
|
319
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/conversation.ts
|
|
325
|
+
function newId() {
|
|
326
|
+
const c = globalThis.crypto;
|
|
327
|
+
if (c && typeof c.randomUUID === "function") return c.randomUUID();
|
|
328
|
+
return `turn-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
329
|
+
}
|
|
330
|
+
function createTurn(message, images) {
|
|
331
|
+
return {
|
|
332
|
+
id: newId(),
|
|
333
|
+
userMessage: message,
|
|
334
|
+
...images?.length ? { userImages: images } : {},
|
|
335
|
+
assistantContent: "",
|
|
336
|
+
citations: /* @__PURE__ */ new Map(),
|
|
337
|
+
sources: [],
|
|
338
|
+
externalSources: {},
|
|
339
|
+
mode: null,
|
|
340
|
+
steps: [],
|
|
341
|
+
isStreaming: true
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function createConversation(client, options = {}) {
|
|
345
|
+
let state = {
|
|
346
|
+
turns: options.initialTurns ?? [],
|
|
347
|
+
sessionId: options.sessionId ?? null,
|
|
348
|
+
isStreaming: false
|
|
349
|
+
};
|
|
350
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
351
|
+
let controller = null;
|
|
352
|
+
function emit(next) {
|
|
353
|
+
state = next;
|
|
354
|
+
for (const listener of listeners) listener(state);
|
|
355
|
+
}
|
|
356
|
+
function patchLast(patch) {
|
|
357
|
+
const lastIndex = state.turns.length - 1;
|
|
358
|
+
if (lastIndex < 0) return;
|
|
359
|
+
const turns = [...state.turns];
|
|
360
|
+
turns[lastIndex] = patch(turns[lastIndex]);
|
|
361
|
+
emit({ ...state, turns });
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
getState: () => state,
|
|
365
|
+
subscribe(listener) {
|
|
366
|
+
listeners.add(listener);
|
|
367
|
+
return () => listeners.delete(listener);
|
|
368
|
+
},
|
|
369
|
+
abort() {
|
|
370
|
+
controller?.abort();
|
|
371
|
+
},
|
|
372
|
+
reset() {
|
|
373
|
+
controller?.abort();
|
|
374
|
+
emit({ turns: [], sessionId: null, isStreaming: false });
|
|
375
|
+
},
|
|
376
|
+
async send(message, sendOptions = {}) {
|
|
377
|
+
controller?.abort();
|
|
378
|
+
controller = new AbortController();
|
|
379
|
+
const signal = controller.signal;
|
|
380
|
+
emit({
|
|
381
|
+
...state,
|
|
382
|
+
turns: [...state.turns, createTurn(message, sendOptions.images)],
|
|
383
|
+
isStreaming: true
|
|
384
|
+
});
|
|
385
|
+
try {
|
|
386
|
+
const stream = client.sendMessage(message, {
|
|
387
|
+
sessionId: state.sessionId,
|
|
388
|
+
...sendOptions.images ? { images: sendOptions.images } : {},
|
|
389
|
+
...sendOptions.forceTool ? { forceTool: sendOptions.forceTool } : {},
|
|
390
|
+
signal
|
|
391
|
+
});
|
|
392
|
+
for await (const event of stream) {
|
|
393
|
+
if (signal.aborted) break;
|
|
394
|
+
switch (event.type) {
|
|
395
|
+
case "session":
|
|
396
|
+
emit({ ...state, sessionId: event.session_id });
|
|
397
|
+
options.onSessionId?.(event.session_id);
|
|
398
|
+
break;
|
|
399
|
+
case "mode":
|
|
400
|
+
patchLast((turn) => ({ ...turn, mode: event.mode }));
|
|
401
|
+
break;
|
|
402
|
+
case "step":
|
|
403
|
+
patchLast((turn) => ({ ...turn, steps: mergeStep(turn.steps, event.step) }));
|
|
404
|
+
break;
|
|
405
|
+
case "delta":
|
|
406
|
+
patchLast((turn) => ({
|
|
407
|
+
...turn,
|
|
408
|
+
assistantContent: turn.assistantContent + event.content
|
|
409
|
+
}));
|
|
410
|
+
break;
|
|
411
|
+
case "citation":
|
|
412
|
+
patchLast((turn) => {
|
|
413
|
+
const citations = new Map(turn.citations);
|
|
414
|
+
citations.set(event.index, event.source);
|
|
415
|
+
return { ...turn, citations };
|
|
416
|
+
});
|
|
417
|
+
break;
|
|
418
|
+
case "sources":
|
|
419
|
+
patchLast((turn) => ({ ...turn, sources: event.items }));
|
|
420
|
+
break;
|
|
421
|
+
case "external_sources":
|
|
422
|
+
patchLast((turn) => ({
|
|
423
|
+
...turn,
|
|
424
|
+
externalSources: {
|
|
425
|
+
...turn.externalSources,
|
|
426
|
+
[event.source_type]: event.items
|
|
427
|
+
}
|
|
428
|
+
}));
|
|
429
|
+
break;
|
|
430
|
+
case "advisory":
|
|
431
|
+
patchLast((turn) => ({
|
|
432
|
+
...turn,
|
|
433
|
+
advisory: { redirect: event.redirect ?? null, message: event.message }
|
|
434
|
+
}));
|
|
435
|
+
break;
|
|
436
|
+
case "done":
|
|
437
|
+
patchLast((turn) => ({
|
|
438
|
+
...turn,
|
|
439
|
+
isStreaming: false,
|
|
440
|
+
...event.message_id ? { messageId: event.message_id } : {}
|
|
441
|
+
}));
|
|
442
|
+
emit({ ...state, isStreaming: false });
|
|
443
|
+
break;
|
|
444
|
+
case "error":
|
|
445
|
+
applyFailure(patchLast, {
|
|
446
|
+
code: event.code,
|
|
447
|
+
message: event.message,
|
|
448
|
+
guardrailSlug: event.guardrail_slug ?? null
|
|
449
|
+
});
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
} catch (err) {
|
|
454
|
+
if (err?.name === "AbortError") {
|
|
455
|
+
patchLast((turn) => ({ ...turn, isStreaming: false }));
|
|
456
|
+
} else if (err instanceof LainternError) {
|
|
457
|
+
applyFailure(patchLast, {
|
|
458
|
+
code: err.code,
|
|
459
|
+
message: err.message,
|
|
460
|
+
guardrailSlug: null,
|
|
461
|
+
...err.rateLimitScope ? { rateLimitScope: err.rateLimitScope } : {},
|
|
462
|
+
...err.rateLimitWindow ? { rateLimitWindow: err.rateLimitWindow } : {}
|
|
463
|
+
});
|
|
464
|
+
} else {
|
|
465
|
+
applyFailure(patchLast, {
|
|
466
|
+
code: "NETWORK_ERROR",
|
|
467
|
+
message: err instanceof Error ? err.message : "An unknown error occurred.",
|
|
468
|
+
guardrailSlug: null
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
} finally {
|
|
472
|
+
const last = state.turns.at(-1);
|
|
473
|
+
if (last?.isStreaming) patchLast((turn) => ({ ...turn, isStreaming: false }));
|
|
474
|
+
if (state.isStreaming) emit({ ...state, isStreaming: false });
|
|
475
|
+
controller = null;
|
|
476
|
+
}
|
|
477
|
+
},
|
|
478
|
+
async setFeedback(turnId, feedback) {
|
|
479
|
+
const turn = state.turns.find((t) => t.id === turnId);
|
|
480
|
+
if (!turn?.messageId) {
|
|
481
|
+
throw new LainternError(
|
|
482
|
+
"FEEDBACK_UNAVAILABLE",
|
|
483
|
+
"This turn has no message id yet \u2014 feedback is only possible once a turn completes."
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
await client.submitFeedback(turn.messageId, feedback);
|
|
487
|
+
emit({
|
|
488
|
+
...state,
|
|
489
|
+
turns: state.turns.map((t) => t.id === turnId ? { ...t, feedback } : t)
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
function mergeStep(steps, step) {
|
|
495
|
+
if (step.status !== "completed") return [...steps, step];
|
|
496
|
+
const startedIndex = steps.findIndex((s) => s.tool === step.tool && s.status === "started");
|
|
497
|
+
if (startedIndex === -1) return [...steps, step];
|
|
498
|
+
const next = [...steps];
|
|
499
|
+
next[startedIndex] = step;
|
|
500
|
+
return next;
|
|
501
|
+
}
|
|
502
|
+
function applyFailure(patchLast, failure) {
|
|
503
|
+
if (failure.code === "GUARDRAIL_TRIGGERED") {
|
|
504
|
+
patchLast((turn) => ({
|
|
505
|
+
...turn,
|
|
506
|
+
isStreaming: false,
|
|
507
|
+
assistantContent: "",
|
|
508
|
+
citations: /* @__PURE__ */ new Map(),
|
|
509
|
+
sources: [],
|
|
510
|
+
externalSources: {},
|
|
511
|
+
steps: [],
|
|
512
|
+
mode: null,
|
|
513
|
+
advisory: void 0,
|
|
514
|
+
blocked: { slug: failure.guardrailSlug, message: failure.message }
|
|
515
|
+
}));
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
patchLast((turn) => ({
|
|
519
|
+
...turn,
|
|
520
|
+
isStreaming: false,
|
|
521
|
+
error: {
|
|
522
|
+
code: failure.code,
|
|
523
|
+
message: failure.message,
|
|
524
|
+
...failure.rateLimitScope ? { rateLimitScope: failure.rateLimitScope } : {},
|
|
525
|
+
...failure.rateLimitWindow ? { rateLimitWindow: failure.rateLimitWindow } : {}
|
|
526
|
+
}
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
exports.DEFAULT_REFRESH_BUFFER_MS = DEFAULT_REFRESH_BUFFER_MS;
|
|
531
|
+
exports.LainternError = LainternError;
|
|
532
|
+
exports.createConversation = createConversation;
|
|
533
|
+
exports.createLainternClient = createLainternClient;
|
|
534
|
+
exports.createTokenManager = createTokenManager;
|
|
535
|
+
exports.decodeJwtExpiryMs = decodeJwtExpiryMs;
|
|
536
|
+
exports.parseSSE = parseSSE;
|
|
537
|
+
//# sourceMappingURL=index.cjs.map
|
|
538
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/sse.ts","../src/tokens.ts","../src/client.ts","../src/conversation.ts"],"names":["parsed"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Every failure the SDK surfaces, with the API's own error code attached.\n *\n * Callers switch on `code` rather than parsing messages: the server's copy is\n * user-facing prose that may change, while the code is part of the contract.\n */\nexport class LainternError extends Error {\n readonly code: string\n readonly status: number | null\n /** Set on RATE_LIMIT_EXCEEDED. */\n readonly rateLimitScope?: \"user\" | \"agent\"\n readonly rateLimitWindow?: \"minute\" | \"hour\" | \"day\"\n /** Set on MCP_UNAVAILABLE: which dependency failed, and how. */\n readonly reason?: string\n\n constructor(\n code: string,\n message: string,\n options: {\n status?: number | null\n rateLimitScope?: \"user\" | \"agent\"\n rateLimitWindow?: \"minute\" | \"hour\" | \"day\"\n reason?: string\n cause?: unknown\n } = {},\n ) {\n super(message, options.cause !== undefined ? { cause: options.cause } : undefined)\n this.name = \"LainternError\"\n this.code = code\n this.status = options.status ?? null\n if (options.rateLimitScope) this.rateLimitScope = options.rateLimitScope\n if (options.rateLimitWindow) this.rateLimitWindow = options.rateLimitWindow\n if (options.reason) this.reason = options.reason\n }\n\n /**\n * True when retrying later could plausibly succeed: a rate limit, or a\n * dependency the server could not reach. An expired session is NOT retryable\n * — the host has to mint a new token first.\n */\n get isRetryable(): boolean {\n return this.code === \"RATE_LIMIT_EXCEEDED\" || this.reason === \"transport\"\n }\n}\n\nfunction pick<T extends string>(value: unknown, allowed: readonly T[]): T | undefined {\n return typeof value === \"string\" && (allowed as readonly string[]).includes(value)\n ? (value as T)\n : undefined\n}\n\n/** Turn a non-2xx API response into a `LainternError`. Never throws itself. */\nexport async function errorFromResponse(response: Response): Promise<LainternError> {\n let code = `HTTP_${response.status}`\n let message = \"An unexpected error occurred.\"\n let rateLimitScope: \"user\" | \"agent\" | undefined\n let rateLimitWindow: \"minute\" | \"hour\" | \"day\" | undefined\n let reason: string | undefined\n\n try {\n const body = (await response.json()) as {\n error?: {\n code?: string\n message?: string\n scope?: unknown\n window?: unknown\n reason?: unknown\n }\n message?: string\n }\n if (body?.error?.code) {\n code = body.error.code\n message = body.error.message ?? message\n rateLimitScope = pick(body.error.scope, [\"user\", \"agent\"] as const)\n rateLimitWindow = pick(body.error.window, [\"minute\", \"hour\", \"day\"] as const)\n if (typeof body.error.reason === \"string\") reason = body.error.reason\n } else if (typeof body?.message === \"string\") {\n message = body.message\n }\n } catch {\n // Not JSON — keep the HTTP-derived defaults.\n }\n\n return new LainternError(code, message, {\n status: response.status,\n rateLimitScope,\n rateLimitWindow,\n reason,\n })\n}\n","import type { ChatStreamEvent } from \"@laintern-headless/types\"\n\n/** Sentinel that terminates the stream (OpenAI-style `data: [DONE]`). */\nconst DONE = \"[DONE]\"\n\n/**\n * Parse a Server-Sent Events response into a stream of chat events.\n *\n * The server writes one event per SSE frame (`data: <json>\\n\\n`) and ends with\n * a `data: [DONE]` sentinel. We read the raw `ReadableStream` from fetch\n * rather than using `EventSource`, which cannot send a POST body or an\n * Authorization header.\n */\nexport async function* parseSSE(response: Response): AsyncGenerator<ChatStreamEvent> {\n const reader = response.body?.getReader()\n if (!reader) throw new Error(\"Response body is not readable\")\n\n const decoder = new TextDecoder()\n let buffer = \"\"\n\n try {\n while (true) {\n const { done, value } = await reader.read()\n if (done) break\n\n buffer += decoder.decode(value, { stream: true })\n\n // SSE frames are separated by a blank line. Tolerate CRLF endings.\n let sepIndex: number\n while ((sepIndex = indexOfFrameBoundary(buffer)) !== -1) {\n const frame = buffer.slice(0, sepIndex)\n buffer = buffer.slice(boundaryEnd(buffer, sepIndex))\n const parsed = parseFrame(frame)\n if (parsed === DONE) return\n if (parsed) yield parsed\n }\n }\n\n // Flush a trailing frame that wasn't terminated by a blank line.\n const parsed = parseFrame(buffer)\n if (parsed && parsed !== DONE) yield parsed\n } finally {\n reader.releaseLock()\n }\n}\n\n/** Index of the start of the next `\\n\\n` or `\\r\\n\\r\\n` boundary, or -1. */\nfunction indexOfFrameBoundary(buffer: string): number {\n const lf = buffer.indexOf(\"\\n\\n\")\n const crlf = buffer.indexOf(\"\\r\\n\\r\\n\")\n if (lf === -1) return crlf\n if (crlf === -1) return lf\n return Math.min(lf, crlf)\n}\n\n/** Length of the boundary at `index` so we can slice past it. */\nfunction boundaryEnd(buffer: string, index: number): number {\n return buffer.startsWith(\"\\r\\n\\r\\n\", index) ? index + 4 : index + 2\n}\n\n/**\n * Extract the payload from a single SSE frame. Returns the parsed event, the\n * `DONE` sentinel, or `null` for empty/comment/unparseable frames.\n */\nfunction parseFrame(frame: string): ChatStreamEvent | typeof DONE | null {\n const trimmed = frame.trim()\n if (!trimmed) return null\n\n const dataLines: string[] = []\n for (const rawLine of trimmed.split(\"\\n\")) {\n const line = rawLine.replace(/\\r$/, \"\")\n if (line.startsWith(\":\")) continue // SSE comment\n if (!line.startsWith(\"data:\")) continue // ignore event:/id:/retry:\n dataLines.push(line.slice(5).replace(/^ /, \"\")) // strip \"data:\" + one optional space\n }\n if (dataLines.length === 0) return null\n\n const payload = dataLines.join(\"\\n\")\n if (payload === DONE) return DONE\n\n try {\n return JSON.parse(payload) as ChatStreamEvent\n } catch {\n return null\n }\n}\n","import { LainternError } from \"./errors.js\"\n\n/**\n * Refresh this long before a token expires. It mirrors the margin the API\n * applies when it clamps a chat token to a credential inside it: the server\n * guarantees the token dies *after* the credential's own refresh point, and\n * this buffer is what makes the client come back in time to use that.\n */\nexport const DEFAULT_REFRESH_BUFFER_MS = 60_000\n\n/** Used only when neither the JWT nor the endpoint says when the token dies. */\nconst FALLBACK_TTL_MS = 5 * 60 * 1000\n\n/** A token proxy that drifts further than this from the JWT is misconfigured. */\nconst DRIFT_WARN_THRESHOLD_MS = 5 * 60 * 1000\n\nexport interface TokenResponse {\n token: string\n /** ISO-8601 or epoch ms, if the endpoint reports one. */\n expiresAt?: string | number | null\n}\n\n/** Minimal storage surface — `sessionStorage`, `localStorage` or your own. */\nexport interface TokenStorage {\n getItem(key: string): string | null\n setItem(key: string, value: string): void\n removeItem(key: string): void\n}\n\nexport interface TokenManagerOptions {\n /**\n * Fetches a fresh token from YOUR backend. This is the one integration\n * point that cannot be generic: only your server may hold the API key that\n * mints Laintern tokens, and only it knows who the current user is.\n */\n fetchToken: () => Promise<TokenResponse | string>\n storage?: TokenStorage | null\n storageKey?: string\n refreshBufferMs?: number\n onWarning?: (message: string, detail?: Record<string, unknown>) => void\n}\n\nexport interface TokenManager {\n /** Cached token when still valid, otherwise a freshly fetched one. */\n get(): Promise<string>\n /** Drop the cached token — the next `get()` fetches. */\n clear(): void\n /** Expiry of the cached token, in epoch ms. Null when nothing is cached. */\n expiresAt(): number | null\n}\n\n/** Read a JWT's `exp` claim (epoch ms) without verifying the signature. */\nexport function decodeJwtExpiryMs(token: string): number | null {\n try {\n const parts = token.split(\".\")\n if (parts.length < 2) return null\n const padded = parts[1].replace(/-/g, \"+\").replace(/_/g, \"/\")\n // `atob` is global in browsers and in Node 18+, which is our floor. On an\n // exotic runtime without it we return null, and the caller falls back to\n // the endpoint's reported expiry.\n if (typeof atob !== \"function\") return null\n const json = JSON.parse(atob(padded)) as { exp?: unknown }\n return typeof json.exp === \"number\" ? json.exp * 1000 : null\n } catch {\n return null\n }\n}\n\nfunction toEpochMs(value: string | number | null | undefined): number | null {\n if (value == null) return null\n if (typeof value === \"number\") return Number.isFinite(value) ? value : null\n const parsed = Date.parse(value)\n return Number.isNaN(parsed) ? null : parsed\n}\n\n/**\n * Decide when a token really expires.\n *\n * The JWT's own `exp` wins: it is what the API enforces. A token endpoint that\n * reports something materially different is a misconfiguration worth\n * surfacing — it usually means the proxy invents its own TTL instead of\n * passing through what Laintern returned.\n */\nexport function resolveExpiry(\n token: string,\n reportedExpiresAt: string | number | null | undefined,\n onWarning?: TokenManagerOptions[\"onWarning\"],\n): number {\n const jwtExpMs = decodeJwtExpiryMs(token)\n const reported = toEpochMs(reportedExpiresAt)\n\n if (jwtExpMs !== null) {\n if (reported !== null && Math.abs(reported - jwtExpMs) > DRIFT_WARN_THRESHOLD_MS) {\n onWarning?.(\n \"Token endpoint 'expires_at' drifts more than 5 minutes from the JWT 'exp'; using the JWT claim.\",\n { jwtExpMs, reportedExpiresAtMs: reported, driftMs: reported - jwtExpMs },\n )\n }\n return jwtExpMs\n }\n if (reported !== null) return reported\n\n onWarning?.(\n \"Token has no readable 'exp' claim and the endpoint reported no expiry; caching for 5 minutes.\",\n )\n return Date.now() + FALLBACK_TTL_MS\n}\n\ninterface CachedToken {\n token: string\n expiresAt: number\n}\n\n/**\n * Caches the chat token and refreshes it before it dies.\n *\n * Concurrent callers share one in-flight fetch: a page that opens a stream\n * while another request is already refreshing must not mint two sessions.\n */\nexport function createTokenManager(options: TokenManagerOptions): TokenManager {\n const buffer = options.refreshBufferMs ?? DEFAULT_REFRESH_BUFFER_MS\n const storage = options.storage\n const storageKey = options.storageKey ?? \"laintern.token\"\n let cached: CachedToken | null = readStored()\n let inFlight: Promise<string> | null = null\n\n function readStored(): CachedToken | null {\n if (!storage) return null\n try {\n const raw = storage.getItem(storageKey)\n if (!raw) return null\n const parsed = JSON.parse(raw) as Partial<CachedToken>\n if (typeof parsed.token !== \"string\" || typeof parsed.expiresAt !== \"number\") {\n return null\n }\n return { token: parsed.token, expiresAt: parsed.expiresAt }\n } catch {\n return null\n }\n }\n\n function writeStored(value: CachedToken | null): void {\n if (!storage) return\n try {\n if (value === null) storage.removeItem(storageKey)\n else storage.setItem(storageKey, JSON.stringify(value))\n } catch {\n // Storage can be unavailable (private mode, blocked cookies). Non-fatal:\n // we simply refetch on the next page load.\n }\n }\n\n function isUsable(value: CachedToken | null): value is CachedToken {\n return value !== null && Date.now() < value.expiresAt - buffer\n }\n\n async function refresh(): Promise<string> {\n let result: TokenResponse | string\n try {\n result = await options.fetchToken()\n } catch (err) {\n throw new LainternError(\"TOKEN_FETCH_FAILED\", \"Could not obtain a chat token.\", {\n cause: err,\n })\n }\n const response: TokenResponse = typeof result === \"string\" ? { token: result } : result\n if (!response || typeof response.token !== \"string\" || response.token.length === 0) {\n throw new LainternError(\n \"TOKEN_INVALID\",\n \"Token endpoint returned no 'token' string.\",\n )\n }\n const next: CachedToken = {\n token: response.token,\n expiresAt: resolveExpiry(response.token, response.expiresAt, options.onWarning),\n }\n cached = next\n writeStored(next)\n return next.token\n }\n\n return {\n async get(): Promise<string> {\n if (isUsable(cached)) return cached.token\n if (!inFlight) {\n inFlight = refresh().finally(() => {\n inFlight = null\n })\n }\n return inFlight\n },\n clear(): void {\n cached = null\n writeStored(null)\n },\n expiresAt(): number | null {\n return cached?.expiresAt ?? null\n },\n }\n}\n","import type { ChatStreamEvent } from \"@laintern-headless/types\"\nimport { LainternError, errorFromResponse } from \"./errors.js\"\nimport { parseSSE } from \"./sse.js\"\nimport { createTokenManager, type TokenManager, type TokenManagerOptions } from \"./tokens.js\"\n\nexport interface LainternClientOptions {\n /** Base URL of the Laintern API, e.g. \"https://api.laintern.com\". */\n apiBase: string\n /**\n * How to get a chat token. Either your own function, or the URL of an\n * endpoint on your backend that returns `{ token, expires_at }`.\n *\n * Never put a Laintern API key in the browser: the key mints tokens for any\n * user, so it belongs on your server, behind your own session check.\n */\n token: TokenManagerOptions[\"fetchToken\"] | { endpoint: string; init?: RequestInit }\n storage?: TokenManagerOptions[\"storage\"]\n refreshBufferMs?: number\n onWarning?: TokenManagerOptions[\"onWarning\"]\n /** Injectable for tests and non-browser runtimes. */\n fetch?: typeof globalThis.fetch\n}\n\nexport interface SendMessageOptions {\n /** Continues an existing conversation. Omit to start a new one. */\n sessionId?: string | null\n /** Base64 data URLs, for agents with image input enabled. */\n images?: string[]\n /** Force a specific tool on this turn, when the agent allows it. */\n forceTool?: string\n signal?: AbortSignal\n}\n\nexport interface AgentConfig {\n /** Transparency notice to show before the conversation starts (EU AI Act). */\n disclaimer: { enabled: boolean; text: string | null }\n}\n\nexport interface LainternClient {\n /**\n * Send one message and iterate the response as it streams.\n *\n * Yields typed events (`delta`, `citation`, `sources`, `step`, `done`, …).\n * Prefer {@link createConversation} unless you want to assemble the turn\n * yourself.\n */\n sendMessage(message: string, options?: SendMessageOptions): AsyncGenerator<ChatStreamEvent>\n /** Per-agent presentation config. Best-effort: throws on transport errors. */\n getConfig(): Promise<AgentConfig>\n submitFeedback(messageId: string, feedback: \"positive\" | \"negative\"): Promise<void>\n /** The token cache, exposed so a host can clear it on logout. */\n tokens: TokenManager\n}\n\nfunction tokenFetcher(\n option: LainternClientOptions[\"token\"],\n doFetch: typeof globalThis.fetch,\n): TokenManagerOptions[\"fetchToken\"] {\n if (typeof option === \"function\") return option\n return async () => {\n const res = await doFetch(option.endpoint, {\n method: \"POST\",\n credentials: \"same-origin\",\n headers: { \"Content-Type\": \"application/json\" },\n ...option.init,\n })\n if (!res.ok) throw await errorFromResponse(res)\n const body = (await res.json()) as Record<string, unknown>\n const payload = ((body.data as Record<string, unknown>) ?? body) ?? {}\n return {\n token: payload.token as string,\n expiresAt: (payload.expires_at as string | undefined) ?? null,\n }\n }\n}\n\nexport function createLainternClient(options: LainternClientOptions): LainternClient {\n const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis)\n const apiBase = options.apiBase.replace(/\\/+$/, \"\")\n const tokens = createTokenManager({\n fetchToken: tokenFetcher(options.token, doFetch),\n storage: options.storage,\n ...(options.refreshBufferMs !== undefined\n ? { refreshBufferMs: options.refreshBufferMs }\n : {}),\n ...(options.onWarning ? { onWarning: options.onWarning } : {}),\n })\n\n async function authorized(path: string, init: RequestInit = {}): Promise<Response> {\n const token = await tokens.get()\n return doFetch(`${apiBase}${path}`, {\n ...init,\n headers: {\n \"Content-Type\": \"application/json\",\n ...init.headers,\n Authorization: `Bearer ${token}`,\n },\n })\n }\n\n return {\n tokens,\n\n async *sendMessage(message, sendOptions = {}) {\n const body: Record<string, unknown> = { message }\n if (sendOptions.sessionId) body.session_id = sendOptions.sessionId\n if (sendOptions.images?.length) body.images = sendOptions.images\n if (sendOptions.forceTool) body.force_tool = sendOptions.forceTool\n\n let response: Response\n try {\n response = await authorized(\"/v1/chat\", {\n method: \"POST\",\n body: JSON.stringify(body),\n ...(sendOptions.signal ? { signal: sendOptions.signal } : {}),\n })\n } catch (err) {\n if (err instanceof LainternError) throw err\n if ((err as Error)?.name === \"AbortError\") throw err\n throw new LainternError(\n \"NETWORK_ERROR\",\n err instanceof Error ? err.message : \"Network request failed.\",\n { cause: err },\n )\n }\n\n if (!response.ok) {\n const error = await errorFromResponse(response)\n // A token can expire between mint and use — drop it so the next send\n // fetches a fresh one instead of replaying the dead credential.\n if (response.status === 401) tokens.clear()\n throw error\n }\n\n yield* parseSSE(response)\n },\n\n async getConfig(): Promise<AgentConfig> {\n const res = await authorized(\"/v1/config\", { method: \"GET\" })\n if (!res.ok) throw await errorFromResponse(res)\n const body = (await res.json()) as Record<string, unknown>\n const payload = ((body.data as Record<string, unknown>) ?? body) ?? {}\n const raw = payload.disclaimer as Record<string, unknown> | undefined\n return {\n disclaimer: {\n enabled: raw?.enabled !== false,\n text:\n typeof raw?.text === \"string\" && raw.text.trim().length > 0\n ? (raw.text as string)\n : null,\n },\n }\n },\n\n async submitFeedback(messageId, feedback): Promise<void> {\n const res = await authorized(`/v1/chat/messages/${messageId}/feedback`, {\n method: \"PATCH\",\n body: JSON.stringify({ feedback }),\n })\n if (!res.ok) throw await errorFromResponse(res)\n },\n }\n}\n","import type {\n CitationSource,\n ExternalSourceType,\n SearchResult,\n StepEvent,\n} from \"@laintern-headless/types\"\nimport type { LainternClient } from \"./client.js\"\nimport { LainternError } from \"./errors.js\"\n\n/** Soft advisory alongside an answer — the agent flagged the topic, but answered. */\nexport interface TurnAdvisory {\n redirect: string | null\n message: string\n}\n\n/** A guardrail refused the turn. The agent produced no answer. */\nexport interface TurnBlock {\n /** Which guardrail fired, when the server names one. */\n slug: string | null\n /** Copy meant for the end user, written by the guardrail's configuration. */\n message: string\n}\n\nexport interface TurnError {\n code: string\n message: string\n rateLimitScope?: \"user\" | \"agent\"\n rateLimitWindow?: \"minute\" | \"hour\" | \"day\"\n}\n\nexport interface Turn {\n id: string\n userMessage: string\n userImages?: string[]\n assistantContent: string\n /** Citation number (the `[1]` in the text) → the source it points at. */\n citations: Map<number, CitationSource>\n /** Everything the agent cited or consulted, in the order the server ranked it. */\n sources: SearchResult[]\n /** Sidebar suggestions per content type — not necessarily cited. */\n externalSources: Partial<Record<ExternalSourceType, SearchResult[]>>\n mode: string | null\n steps: StepEvent[]\n isStreaming: boolean\n error?: TurnError\n blocked?: TurnBlock\n advisory?: TurnAdvisory\n messageId?: string\n feedback?: \"positive\" | \"negative\" | null\n}\n\nexport interface ConversationState {\n turns: Turn[]\n sessionId: string | null\n isStreaming: boolean\n}\n\nexport interface SendOptions {\n images?: string[]\n forceTool?: string\n}\n\nexport interface Conversation {\n getState(): ConversationState\n /** Subscribe to state changes. Returns an unsubscribe function. */\n subscribe(listener: (state: ConversationState) => void): () => void\n /** Send a message and stream the answer into state. Never throws. */\n send(message: string, options?: SendOptions): Promise<void>\n /** Stop the current stream. The partial answer stays in state. */\n abort(): void\n /** Drop all turns and start a new server-side session on the next send. */\n reset(): void\n setFeedback(turnId: string, feedback: \"positive\" | \"negative\"): Promise<void>\n}\n\nexport interface ConversationOptions {\n /** Resume an existing server-side session. */\n sessionId?: string | null\n /** Restore turns from your own storage. */\n initialTurns?: Turn[]\n /** Called whenever the session id changes, so you can persist it. */\n onSessionId?: (sessionId: string) => void\n}\n\nfunction newId(): string {\n const c = globalThis.crypto\n if (c && typeof c.randomUUID === \"function\") return c.randomUUID()\n return `turn-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`\n}\n\nfunction createTurn(message: string, images?: string[]): Turn {\n return {\n id: newId(),\n userMessage: message,\n ...(images?.length ? { userImages: images } : {}),\n assistantContent: \"\",\n citations: new Map(),\n sources: [],\n externalSources: {},\n mode: null,\n steps: [],\n isStreaming: true,\n }\n}\n\n/**\n * Stateful conversation on top of {@link LainternClient}.\n *\n * Holds the turn list, applies each stream event to the turn in flight, and\n * notifies subscribers. Framework-agnostic on purpose: the React binding in\n * `@laintern/chat-sdk/react` is a thin `useSyncExternalStore` wrapper, and any\n * other UI layer can subscribe the same way.\n *\n * `send()` never throws. A failed turn is part of the conversation — it lands\n * on `turn.error` (or `turn.blocked` for a guardrail) so the UI can render it\n * in place, which is what you want in a chat transcript.\n */\nexport function createConversation(\n client: LainternClient,\n options: ConversationOptions = {},\n): Conversation {\n let state: ConversationState = {\n turns: options.initialTurns ?? [],\n sessionId: options.sessionId ?? null,\n isStreaming: false,\n }\n const listeners = new Set<(state: ConversationState) => void>()\n let controller: AbortController | null = null\n\n function emit(next: ConversationState): void {\n state = next\n for (const listener of listeners) listener(state)\n }\n\n /** Apply a change to the turn currently streaming (always the last one). */\n function patchLast(patch: (turn: Turn) => Turn): void {\n const lastIndex = state.turns.length - 1\n if (lastIndex < 0) return\n const turns = [...state.turns]\n turns[lastIndex] = patch(turns[lastIndex])\n emit({ ...state, turns })\n }\n\n return {\n getState: () => state,\n\n subscribe(listener) {\n listeners.add(listener)\n return () => listeners.delete(listener)\n },\n\n abort() {\n controller?.abort()\n },\n\n reset() {\n controller?.abort()\n emit({ turns: [], sessionId: null, isStreaming: false })\n },\n\n async send(message, sendOptions = {}) {\n controller?.abort()\n controller = new AbortController()\n const signal = controller.signal\n\n emit({\n ...state,\n turns: [...state.turns, createTurn(message, sendOptions.images)],\n isStreaming: true,\n })\n\n try {\n const stream = client.sendMessage(message, {\n sessionId: state.sessionId,\n ...(sendOptions.images ? { images: sendOptions.images } : {}),\n ...(sendOptions.forceTool ? { forceTool: sendOptions.forceTool } : {}),\n signal,\n })\n\n for await (const event of stream) {\n if (signal.aborted) break\n\n switch (event.type) {\n case \"session\":\n emit({ ...state, sessionId: event.session_id })\n options.onSessionId?.(event.session_id)\n break\n case \"mode\":\n patchLast((turn) => ({ ...turn, mode: event.mode }))\n break\n case \"step\":\n patchLast((turn) => ({ ...turn, steps: mergeStep(turn.steps, event.step) }))\n break\n case \"delta\":\n patchLast((turn) => ({\n ...turn,\n assistantContent: turn.assistantContent + event.content,\n }))\n break\n case \"citation\":\n patchLast((turn) => {\n const citations = new Map(turn.citations)\n citations.set(event.index, event.source)\n return { ...turn, citations }\n })\n break\n case \"sources\":\n patchLast((turn) => ({ ...turn, sources: event.items }))\n break\n case \"external_sources\":\n patchLast((turn) => ({\n ...turn,\n externalSources: {\n ...turn.externalSources,\n [event.source_type]: event.items,\n },\n }))\n break\n case \"advisory\":\n patchLast((turn) => ({\n ...turn,\n advisory: { redirect: event.redirect ?? null, message: event.message },\n }))\n break\n case \"done\":\n patchLast((turn) => ({\n ...turn,\n isStreaming: false,\n ...(event.message_id ? { messageId: event.message_id } : {}),\n }))\n emit({ ...state, isStreaming: false })\n break\n case \"error\":\n applyFailure(patchLast, {\n code: event.code,\n message: event.message,\n guardrailSlug: event.guardrail_slug ?? null,\n })\n break\n }\n }\n } catch (err) {\n if ((err as Error)?.name === \"AbortError\") {\n patchLast((turn) => ({ ...turn, isStreaming: false }))\n } else if (err instanceof LainternError) {\n applyFailure(patchLast, {\n code: err.code,\n message: err.message,\n guardrailSlug: null,\n ...(err.rateLimitScope ? { rateLimitScope: err.rateLimitScope } : {}),\n ...(err.rateLimitWindow ? { rateLimitWindow: err.rateLimitWindow } : {}),\n })\n } else {\n applyFailure(patchLast, {\n code: \"NETWORK_ERROR\",\n message: err instanceof Error ? err.message : \"An unknown error occurred.\",\n guardrailSlug: null,\n })\n }\n } finally {\n // A turn always stops streaming, however it ended — including an abort\n // that broke the loop without throwing. Leaving the flag set strands a\n // typing indicator on a turn that will never produce another token.\n const last = state.turns.at(-1)\n if (last?.isStreaming) patchLast((turn) => ({ ...turn, isStreaming: false }))\n if (state.isStreaming) emit({ ...state, isStreaming: false })\n controller = null\n }\n },\n\n async setFeedback(turnId, feedback) {\n const turn = state.turns.find((t) => t.id === turnId)\n if (!turn?.messageId) {\n throw new LainternError(\n \"FEEDBACK_UNAVAILABLE\",\n \"This turn has no message id yet — feedback is only possible once a turn completes.\",\n )\n }\n await client.submitFeedback(turn.messageId, feedback)\n emit({\n ...state,\n turns: state.turns.map((t) => (t.id === turnId ? { ...t, feedback } : t)),\n })\n },\n }\n}\n\n/**\n * A completed step replaces its own \"started\" entry rather than appending, so\n * the UI shows one row per tool call that fills in, not two.\n */\nfunction mergeStep(steps: StepEvent[], step: StepEvent): StepEvent[] {\n if (step.status !== \"completed\") return [...steps, step]\n const startedIndex = steps.findIndex((s) => s.tool === step.tool && s.status === \"started\")\n if (startedIndex === -1) return [...steps, step]\n const next = [...steps]\n next[startedIndex] = step\n return next\n}\n\n/**\n * A failed or blocked turn.\n *\n * A guardrail block discards whatever streamed before it fired: the partial\n * answer is exactly the content the guardrail decided the user should not see,\n * and a hard block always wins over a soft advisory that arrived a moment\n * earlier.\n */\nfunction applyFailure(\n patchLast: (patch: (turn: Turn) => Turn) => void,\n failure: {\n code: string\n message: string\n guardrailSlug: string | null\n rateLimitScope?: \"user\" | \"agent\"\n rateLimitWindow?: \"minute\" | \"hour\" | \"day\"\n },\n): void {\n if (failure.code === \"GUARDRAIL_TRIGGERED\") {\n patchLast((turn) => ({\n ...turn,\n isStreaming: false,\n assistantContent: \"\",\n citations: new Map(),\n sources: [],\n externalSources: {},\n steps: [],\n mode: null,\n advisory: undefined,\n blocked: { slug: failure.guardrailSlug, message: failure.message },\n }))\n return\n }\n\n patchLast((turn) => ({\n ...turn,\n isStreaming: false,\n error: {\n code: failure.code,\n message: failure.message,\n ...(failure.rateLimitScope ? { rateLimitScope: failure.rateLimitScope } : {}),\n ...(failure.rateLimitWindow ? { rateLimitWindow: failure.rateLimitWindow } : {}),\n },\n }))\n}\n"]}
|