@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.d.cts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { C as ChatStreamEvent } from './conversation-7VzVDT95.cjs';
|
|
2
|
+
export { A as AgentConfig, a as ChatContentType, b as CitationSource, c as ContentType, d as Conversation, e as ConversationOptions, f as ConversationState, D as DEFAULT_REFRESH_BUFFER_MS, E as ExternalSourceType, L as LainternClient, g as LainternClientOptions, S as SearchResult, h as SendMessageOptions, i as SendOptions, j as StepEvent, T as TokenManager, k as TokenManagerOptions, l as TokenResponse, m as TokenStorage, n as Turn, o as TurnAdvisory, p as TurnBlock, q as TurnError, r as createConversation, s as createLainternClient, t as createTokenManager, u as decodeJwtExpiryMs } from './conversation-7VzVDT95.cjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every failure the SDK surfaces, with the API's own error code attached.
|
|
6
|
+
*
|
|
7
|
+
* Callers switch on `code` rather than parsing messages: the server's copy is
|
|
8
|
+
* user-facing prose that may change, while the code is part of the contract.
|
|
9
|
+
*/
|
|
10
|
+
declare class LainternError extends Error {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly status: number | null;
|
|
13
|
+
/** Set on RATE_LIMIT_EXCEEDED. */
|
|
14
|
+
readonly rateLimitScope?: "user" | "agent";
|
|
15
|
+
readonly rateLimitWindow?: "minute" | "hour" | "day";
|
|
16
|
+
/** Set on MCP_UNAVAILABLE: which dependency failed, and how. */
|
|
17
|
+
readonly reason?: string;
|
|
18
|
+
constructor(code: string, message: string, options?: {
|
|
19
|
+
status?: number | null;
|
|
20
|
+
rateLimitScope?: "user" | "agent";
|
|
21
|
+
rateLimitWindow?: "minute" | "hour" | "day";
|
|
22
|
+
reason?: string;
|
|
23
|
+
cause?: unknown;
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* True when retrying later could plausibly succeed: a rate limit, or a
|
|
27
|
+
* dependency the server could not reach. An expired session is NOT retryable
|
|
28
|
+
* — the host has to mint a new token first.
|
|
29
|
+
*/
|
|
30
|
+
get isRetryable(): boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a Server-Sent Events response into a stream of chat events.
|
|
35
|
+
*
|
|
36
|
+
* The server writes one event per SSE frame (`data: <json>\n\n`) and ends with
|
|
37
|
+
* a `data: [DONE]` sentinel. We read the raw `ReadableStream` from fetch
|
|
38
|
+
* rather than using `EventSource`, which cannot send a POST body or an
|
|
39
|
+
* Authorization header.
|
|
40
|
+
*/
|
|
41
|
+
declare function parseSSE(response: Response): AsyncGenerator<ChatStreamEvent>;
|
|
42
|
+
|
|
43
|
+
export { ChatStreamEvent, LainternError, parseSSE };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { C as ChatStreamEvent } from './conversation-7VzVDT95.js';
|
|
2
|
+
export { A as AgentConfig, a as ChatContentType, b as CitationSource, c as ContentType, d as Conversation, e as ConversationOptions, f as ConversationState, D as DEFAULT_REFRESH_BUFFER_MS, E as ExternalSourceType, L as LainternClient, g as LainternClientOptions, S as SearchResult, h as SendMessageOptions, i as SendOptions, j as StepEvent, T as TokenManager, k as TokenManagerOptions, l as TokenResponse, m as TokenStorage, n as Turn, o as TurnAdvisory, p as TurnBlock, q as TurnError, r as createConversation, s as createLainternClient, t as createTokenManager, u as decodeJwtExpiryMs } from './conversation-7VzVDT95.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Every failure the SDK surfaces, with the API's own error code attached.
|
|
6
|
+
*
|
|
7
|
+
* Callers switch on `code` rather than parsing messages: the server's copy is
|
|
8
|
+
* user-facing prose that may change, while the code is part of the contract.
|
|
9
|
+
*/
|
|
10
|
+
declare class LainternError extends Error {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly status: number | null;
|
|
13
|
+
/** Set on RATE_LIMIT_EXCEEDED. */
|
|
14
|
+
readonly rateLimitScope?: "user" | "agent";
|
|
15
|
+
readonly rateLimitWindow?: "minute" | "hour" | "day";
|
|
16
|
+
/** Set on MCP_UNAVAILABLE: which dependency failed, and how. */
|
|
17
|
+
readonly reason?: string;
|
|
18
|
+
constructor(code: string, message: string, options?: {
|
|
19
|
+
status?: number | null;
|
|
20
|
+
rateLimitScope?: "user" | "agent";
|
|
21
|
+
rateLimitWindow?: "minute" | "hour" | "day";
|
|
22
|
+
reason?: string;
|
|
23
|
+
cause?: unknown;
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* True when retrying later could plausibly succeed: a rate limit, or a
|
|
27
|
+
* dependency the server could not reach. An expired session is NOT retryable
|
|
28
|
+
* — the host has to mint a new token first.
|
|
29
|
+
*/
|
|
30
|
+
get isRetryable(): boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse a Server-Sent Events response into a stream of chat events.
|
|
35
|
+
*
|
|
36
|
+
* The server writes one event per SSE frame (`data: <json>\n\n`) and ends with
|
|
37
|
+
* a `data: [DONE]` sentinel. We read the raw `ReadableStream` from fetch
|
|
38
|
+
* rather than using `EventSource`, which cannot send a POST body or an
|
|
39
|
+
* Authorization header.
|
|
40
|
+
*/
|
|
41
|
+
declare function parseSSE(response: Response): AsyncGenerator<ChatStreamEvent>;
|
|
42
|
+
|
|
43
|
+
export { ChatStreamEvent, LainternError, parseSSE };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { errorFromResponse, LainternError } from './chunk-WMKNNVMS.js';
|
|
2
|
+
export { LainternError, createConversation } from './chunk-WMKNNVMS.js';
|
|
3
|
+
|
|
4
|
+
// src/sse.ts
|
|
5
|
+
var DONE = "[DONE]";
|
|
6
|
+
async function* parseSSE(response) {
|
|
7
|
+
const reader = response.body?.getReader();
|
|
8
|
+
if (!reader) throw new Error("Response body is not readable");
|
|
9
|
+
const decoder = new TextDecoder();
|
|
10
|
+
let buffer = "";
|
|
11
|
+
try {
|
|
12
|
+
while (true) {
|
|
13
|
+
const { done, value } = await reader.read();
|
|
14
|
+
if (done) break;
|
|
15
|
+
buffer += decoder.decode(value, { stream: true });
|
|
16
|
+
let sepIndex;
|
|
17
|
+
while ((sepIndex = indexOfFrameBoundary(buffer)) !== -1) {
|
|
18
|
+
const frame = buffer.slice(0, sepIndex);
|
|
19
|
+
buffer = buffer.slice(boundaryEnd(buffer, sepIndex));
|
|
20
|
+
const parsed2 = parseFrame(frame);
|
|
21
|
+
if (parsed2 === DONE) return;
|
|
22
|
+
if (parsed2) yield parsed2;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const parsed = parseFrame(buffer);
|
|
26
|
+
if (parsed && parsed !== DONE) yield parsed;
|
|
27
|
+
} finally {
|
|
28
|
+
reader.releaseLock();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function indexOfFrameBoundary(buffer) {
|
|
32
|
+
const lf = buffer.indexOf("\n\n");
|
|
33
|
+
const crlf = buffer.indexOf("\r\n\r\n");
|
|
34
|
+
if (lf === -1) return crlf;
|
|
35
|
+
if (crlf === -1) return lf;
|
|
36
|
+
return Math.min(lf, crlf);
|
|
37
|
+
}
|
|
38
|
+
function boundaryEnd(buffer, index) {
|
|
39
|
+
return buffer.startsWith("\r\n\r\n", index) ? index + 4 : index + 2;
|
|
40
|
+
}
|
|
41
|
+
function parseFrame(frame) {
|
|
42
|
+
const trimmed = frame.trim();
|
|
43
|
+
if (!trimmed) return null;
|
|
44
|
+
const dataLines = [];
|
|
45
|
+
for (const rawLine of trimmed.split("\n")) {
|
|
46
|
+
const line = rawLine.replace(/\r$/, "");
|
|
47
|
+
if (line.startsWith(":")) continue;
|
|
48
|
+
if (!line.startsWith("data:")) continue;
|
|
49
|
+
dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
50
|
+
}
|
|
51
|
+
if (dataLines.length === 0) return null;
|
|
52
|
+
const payload = dataLines.join("\n");
|
|
53
|
+
if (payload === DONE) return DONE;
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(payload);
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/tokens.ts
|
|
62
|
+
var DEFAULT_REFRESH_BUFFER_MS = 6e4;
|
|
63
|
+
var FALLBACK_TTL_MS = 5 * 60 * 1e3;
|
|
64
|
+
var DRIFT_WARN_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
65
|
+
function decodeJwtExpiryMs(token) {
|
|
66
|
+
try {
|
|
67
|
+
const parts = token.split(".");
|
|
68
|
+
if (parts.length < 2) return null;
|
|
69
|
+
const padded = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
70
|
+
if (typeof atob !== "function") return null;
|
|
71
|
+
const json = JSON.parse(atob(padded));
|
|
72
|
+
return typeof json.exp === "number" ? json.exp * 1e3 : null;
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function toEpochMs(value) {
|
|
78
|
+
if (value == null) return null;
|
|
79
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
80
|
+
const parsed = Date.parse(value);
|
|
81
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
82
|
+
}
|
|
83
|
+
function resolveExpiry(token, reportedExpiresAt, onWarning) {
|
|
84
|
+
const jwtExpMs = decodeJwtExpiryMs(token);
|
|
85
|
+
const reported = toEpochMs(reportedExpiresAt);
|
|
86
|
+
if (jwtExpMs !== null) {
|
|
87
|
+
if (reported !== null && Math.abs(reported - jwtExpMs) > DRIFT_WARN_THRESHOLD_MS) {
|
|
88
|
+
onWarning?.(
|
|
89
|
+
"Token endpoint 'expires_at' drifts more than 5 minutes from the JWT 'exp'; using the JWT claim.",
|
|
90
|
+
{ jwtExpMs, reportedExpiresAtMs: reported, driftMs: reported - jwtExpMs }
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return jwtExpMs;
|
|
94
|
+
}
|
|
95
|
+
if (reported !== null) return reported;
|
|
96
|
+
onWarning?.(
|
|
97
|
+
"Token has no readable 'exp' claim and the endpoint reported no expiry; caching for 5 minutes."
|
|
98
|
+
);
|
|
99
|
+
return Date.now() + FALLBACK_TTL_MS;
|
|
100
|
+
}
|
|
101
|
+
function createTokenManager(options) {
|
|
102
|
+
const buffer = options.refreshBufferMs ?? DEFAULT_REFRESH_BUFFER_MS;
|
|
103
|
+
const storage = options.storage;
|
|
104
|
+
const storageKey = options.storageKey ?? "laintern.token";
|
|
105
|
+
let cached = readStored();
|
|
106
|
+
let inFlight = null;
|
|
107
|
+
function readStored() {
|
|
108
|
+
if (!storage) return null;
|
|
109
|
+
try {
|
|
110
|
+
const raw = storage.getItem(storageKey);
|
|
111
|
+
if (!raw) return null;
|
|
112
|
+
const parsed = JSON.parse(raw);
|
|
113
|
+
if (typeof parsed.token !== "string" || typeof parsed.expiresAt !== "number") {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
return { token: parsed.token, expiresAt: parsed.expiresAt };
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function writeStored(value) {
|
|
122
|
+
if (!storage) return;
|
|
123
|
+
try {
|
|
124
|
+
if (value === null) storage.removeItem(storageKey);
|
|
125
|
+
else storage.setItem(storageKey, JSON.stringify(value));
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function isUsable(value) {
|
|
130
|
+
return value !== null && Date.now() < value.expiresAt - buffer;
|
|
131
|
+
}
|
|
132
|
+
async function refresh() {
|
|
133
|
+
let result;
|
|
134
|
+
try {
|
|
135
|
+
result = await options.fetchToken();
|
|
136
|
+
} catch (err) {
|
|
137
|
+
throw new LainternError("TOKEN_FETCH_FAILED", "Could not obtain a chat token.", {
|
|
138
|
+
cause: err
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
const response = typeof result === "string" ? { token: result } : result;
|
|
142
|
+
if (!response || typeof response.token !== "string" || response.token.length === 0) {
|
|
143
|
+
throw new LainternError(
|
|
144
|
+
"TOKEN_INVALID",
|
|
145
|
+
"Token endpoint returned no 'token' string."
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
const next = {
|
|
149
|
+
token: response.token,
|
|
150
|
+
expiresAt: resolveExpiry(response.token, response.expiresAt, options.onWarning)
|
|
151
|
+
};
|
|
152
|
+
cached = next;
|
|
153
|
+
writeStored(next);
|
|
154
|
+
return next.token;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
async get() {
|
|
158
|
+
if (isUsable(cached)) return cached.token;
|
|
159
|
+
if (!inFlight) {
|
|
160
|
+
inFlight = refresh().finally(() => {
|
|
161
|
+
inFlight = null;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return inFlight;
|
|
165
|
+
},
|
|
166
|
+
clear() {
|
|
167
|
+
cached = null;
|
|
168
|
+
writeStored(null);
|
|
169
|
+
},
|
|
170
|
+
expiresAt() {
|
|
171
|
+
return cached?.expiresAt ?? null;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/client.ts
|
|
177
|
+
function tokenFetcher(option, doFetch) {
|
|
178
|
+
if (typeof option === "function") return option;
|
|
179
|
+
return async () => {
|
|
180
|
+
const res = await doFetch(option.endpoint, {
|
|
181
|
+
method: "POST",
|
|
182
|
+
credentials: "same-origin",
|
|
183
|
+
headers: { "Content-Type": "application/json" },
|
|
184
|
+
...option.init
|
|
185
|
+
});
|
|
186
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
187
|
+
const body = await res.json();
|
|
188
|
+
const payload = body.data ?? body ?? {};
|
|
189
|
+
return {
|
|
190
|
+
token: payload.token,
|
|
191
|
+
expiresAt: payload.expires_at ?? null
|
|
192
|
+
};
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function createLainternClient(options) {
|
|
196
|
+
const doFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
197
|
+
const apiBase = options.apiBase.replace(/\/+$/, "");
|
|
198
|
+
const tokens = createTokenManager({
|
|
199
|
+
fetchToken: tokenFetcher(options.token, doFetch),
|
|
200
|
+
storage: options.storage,
|
|
201
|
+
...options.refreshBufferMs !== void 0 ? { refreshBufferMs: options.refreshBufferMs } : {},
|
|
202
|
+
...options.onWarning ? { onWarning: options.onWarning } : {}
|
|
203
|
+
});
|
|
204
|
+
async function authorized(path, init = {}) {
|
|
205
|
+
const token = await tokens.get();
|
|
206
|
+
return doFetch(`${apiBase}${path}`, {
|
|
207
|
+
...init,
|
|
208
|
+
headers: {
|
|
209
|
+
"Content-Type": "application/json",
|
|
210
|
+
...init.headers,
|
|
211
|
+
Authorization: `Bearer ${token}`
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
tokens,
|
|
217
|
+
async *sendMessage(message, sendOptions = {}) {
|
|
218
|
+
const body = { message };
|
|
219
|
+
if (sendOptions.sessionId) body.session_id = sendOptions.sessionId;
|
|
220
|
+
if (sendOptions.images?.length) body.images = sendOptions.images;
|
|
221
|
+
if (sendOptions.forceTool) body.force_tool = sendOptions.forceTool;
|
|
222
|
+
let response;
|
|
223
|
+
try {
|
|
224
|
+
response = await authorized("/v1/chat", {
|
|
225
|
+
method: "POST",
|
|
226
|
+
body: JSON.stringify(body),
|
|
227
|
+
...sendOptions.signal ? { signal: sendOptions.signal } : {}
|
|
228
|
+
});
|
|
229
|
+
} catch (err) {
|
|
230
|
+
if (err instanceof LainternError) throw err;
|
|
231
|
+
if (err?.name === "AbortError") throw err;
|
|
232
|
+
throw new LainternError(
|
|
233
|
+
"NETWORK_ERROR",
|
|
234
|
+
err instanceof Error ? err.message : "Network request failed.",
|
|
235
|
+
{ cause: err }
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
if (!response.ok) {
|
|
239
|
+
const error = await errorFromResponse(response);
|
|
240
|
+
if (response.status === 401) tokens.clear();
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
yield* parseSSE(response);
|
|
244
|
+
},
|
|
245
|
+
async getConfig() {
|
|
246
|
+
const res = await authorized("/v1/config", { method: "GET" });
|
|
247
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
248
|
+
const body = await res.json();
|
|
249
|
+
const payload = body.data ?? body ?? {};
|
|
250
|
+
const raw = payload.disclaimer;
|
|
251
|
+
return {
|
|
252
|
+
disclaimer: {
|
|
253
|
+
enabled: raw?.enabled !== false,
|
|
254
|
+
text: typeof raw?.text === "string" && raw.text.trim().length > 0 ? raw.text : null
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
},
|
|
258
|
+
async submitFeedback(messageId, feedback) {
|
|
259
|
+
const res = await authorized(`/v1/chat/messages/${messageId}/feedback`, {
|
|
260
|
+
method: "PATCH",
|
|
261
|
+
body: JSON.stringify({ feedback })
|
|
262
|
+
});
|
|
263
|
+
if (!res.ok) throw await errorFromResponse(res);
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export { DEFAULT_REFRESH_BUFFER_MS, createLainternClient, createTokenManager, decodeJwtExpiryMs, parseSSE };
|
|
269
|
+
//# sourceMappingURL=index.js.map
|
|
270
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sse.ts","../src/tokens.ts","../src/client.ts"],"names":["parsed"],"mappings":"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","file":"index.js","sourcesContent":["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"]}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
|
|
5
|
+
// src/react/index.ts
|
|
6
|
+
|
|
7
|
+
// src/errors.ts
|
|
8
|
+
var LainternError = class extends Error {
|
|
9
|
+
code;
|
|
10
|
+
status;
|
|
11
|
+
/** Set on RATE_LIMIT_EXCEEDED. */
|
|
12
|
+
rateLimitScope;
|
|
13
|
+
rateLimitWindow;
|
|
14
|
+
/** Set on MCP_UNAVAILABLE: which dependency failed, and how. */
|
|
15
|
+
reason;
|
|
16
|
+
constructor(code, message, options = {}) {
|
|
17
|
+
super(message, options.cause !== void 0 ? { cause: options.cause } : void 0);
|
|
18
|
+
this.name = "LainternError";
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.status = options.status ?? null;
|
|
21
|
+
if (options.rateLimitScope) this.rateLimitScope = options.rateLimitScope;
|
|
22
|
+
if (options.rateLimitWindow) this.rateLimitWindow = options.rateLimitWindow;
|
|
23
|
+
if (options.reason) this.reason = options.reason;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* True when retrying later could plausibly succeed: a rate limit, or a
|
|
27
|
+
* dependency the server could not reach. An expired session is NOT retryable
|
|
28
|
+
* — the host has to mint a new token first.
|
|
29
|
+
*/
|
|
30
|
+
get isRetryable() {
|
|
31
|
+
return this.code === "RATE_LIMIT_EXCEEDED" || this.reason === "transport";
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// src/conversation.ts
|
|
36
|
+
function newId() {
|
|
37
|
+
const c = globalThis.crypto;
|
|
38
|
+
if (c && typeof c.randomUUID === "function") return c.randomUUID();
|
|
39
|
+
return `turn-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
40
|
+
}
|
|
41
|
+
function createTurn(message, images) {
|
|
42
|
+
return {
|
|
43
|
+
id: newId(),
|
|
44
|
+
userMessage: message,
|
|
45
|
+
...images?.length ? { userImages: images } : {},
|
|
46
|
+
assistantContent: "",
|
|
47
|
+
citations: /* @__PURE__ */ new Map(),
|
|
48
|
+
sources: [],
|
|
49
|
+
externalSources: {},
|
|
50
|
+
mode: null,
|
|
51
|
+
steps: [],
|
|
52
|
+
isStreaming: true
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function createConversation(client, options = {}) {
|
|
56
|
+
let state = {
|
|
57
|
+
turns: options.initialTurns ?? [],
|
|
58
|
+
sessionId: options.sessionId ?? null,
|
|
59
|
+
isStreaming: false
|
|
60
|
+
};
|
|
61
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
62
|
+
let controller = null;
|
|
63
|
+
function emit(next) {
|
|
64
|
+
state = next;
|
|
65
|
+
for (const listener of listeners) listener(state);
|
|
66
|
+
}
|
|
67
|
+
function patchLast(patch) {
|
|
68
|
+
const lastIndex = state.turns.length - 1;
|
|
69
|
+
if (lastIndex < 0) return;
|
|
70
|
+
const turns = [...state.turns];
|
|
71
|
+
turns[lastIndex] = patch(turns[lastIndex]);
|
|
72
|
+
emit({ ...state, turns });
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
getState: () => state,
|
|
76
|
+
subscribe(listener) {
|
|
77
|
+
listeners.add(listener);
|
|
78
|
+
return () => listeners.delete(listener);
|
|
79
|
+
},
|
|
80
|
+
abort() {
|
|
81
|
+
controller?.abort();
|
|
82
|
+
},
|
|
83
|
+
reset() {
|
|
84
|
+
controller?.abort();
|
|
85
|
+
emit({ turns: [], sessionId: null, isStreaming: false });
|
|
86
|
+
},
|
|
87
|
+
async send(message, sendOptions = {}) {
|
|
88
|
+
controller?.abort();
|
|
89
|
+
controller = new AbortController();
|
|
90
|
+
const signal = controller.signal;
|
|
91
|
+
emit({
|
|
92
|
+
...state,
|
|
93
|
+
turns: [...state.turns, createTurn(message, sendOptions.images)],
|
|
94
|
+
isStreaming: true
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
const stream = client.sendMessage(message, {
|
|
98
|
+
sessionId: state.sessionId,
|
|
99
|
+
...sendOptions.images ? { images: sendOptions.images } : {},
|
|
100
|
+
...sendOptions.forceTool ? { forceTool: sendOptions.forceTool } : {},
|
|
101
|
+
signal
|
|
102
|
+
});
|
|
103
|
+
for await (const event of stream) {
|
|
104
|
+
if (signal.aborted) break;
|
|
105
|
+
switch (event.type) {
|
|
106
|
+
case "session":
|
|
107
|
+
emit({ ...state, sessionId: event.session_id });
|
|
108
|
+
options.onSessionId?.(event.session_id);
|
|
109
|
+
break;
|
|
110
|
+
case "mode":
|
|
111
|
+
patchLast((turn) => ({ ...turn, mode: event.mode }));
|
|
112
|
+
break;
|
|
113
|
+
case "step":
|
|
114
|
+
patchLast((turn) => ({ ...turn, steps: mergeStep(turn.steps, event.step) }));
|
|
115
|
+
break;
|
|
116
|
+
case "delta":
|
|
117
|
+
patchLast((turn) => ({
|
|
118
|
+
...turn,
|
|
119
|
+
assistantContent: turn.assistantContent + event.content
|
|
120
|
+
}));
|
|
121
|
+
break;
|
|
122
|
+
case "citation":
|
|
123
|
+
patchLast((turn) => {
|
|
124
|
+
const citations = new Map(turn.citations);
|
|
125
|
+
citations.set(event.index, event.source);
|
|
126
|
+
return { ...turn, citations };
|
|
127
|
+
});
|
|
128
|
+
break;
|
|
129
|
+
case "sources":
|
|
130
|
+
patchLast((turn) => ({ ...turn, sources: event.items }));
|
|
131
|
+
break;
|
|
132
|
+
case "external_sources":
|
|
133
|
+
patchLast((turn) => ({
|
|
134
|
+
...turn,
|
|
135
|
+
externalSources: {
|
|
136
|
+
...turn.externalSources,
|
|
137
|
+
[event.source_type]: event.items
|
|
138
|
+
}
|
|
139
|
+
}));
|
|
140
|
+
break;
|
|
141
|
+
case "advisory":
|
|
142
|
+
patchLast((turn) => ({
|
|
143
|
+
...turn,
|
|
144
|
+
advisory: { redirect: event.redirect ?? null, message: event.message }
|
|
145
|
+
}));
|
|
146
|
+
break;
|
|
147
|
+
case "done":
|
|
148
|
+
patchLast((turn) => ({
|
|
149
|
+
...turn,
|
|
150
|
+
isStreaming: false,
|
|
151
|
+
...event.message_id ? { messageId: event.message_id } : {}
|
|
152
|
+
}));
|
|
153
|
+
emit({ ...state, isStreaming: false });
|
|
154
|
+
break;
|
|
155
|
+
case "error":
|
|
156
|
+
applyFailure(patchLast, {
|
|
157
|
+
code: event.code,
|
|
158
|
+
message: event.message,
|
|
159
|
+
guardrailSlug: event.guardrail_slug ?? null
|
|
160
|
+
});
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (err?.name === "AbortError") {
|
|
166
|
+
patchLast((turn) => ({ ...turn, isStreaming: false }));
|
|
167
|
+
} else if (err instanceof LainternError) {
|
|
168
|
+
applyFailure(patchLast, {
|
|
169
|
+
code: err.code,
|
|
170
|
+
message: err.message,
|
|
171
|
+
guardrailSlug: null,
|
|
172
|
+
...err.rateLimitScope ? { rateLimitScope: err.rateLimitScope } : {},
|
|
173
|
+
...err.rateLimitWindow ? { rateLimitWindow: err.rateLimitWindow } : {}
|
|
174
|
+
});
|
|
175
|
+
} else {
|
|
176
|
+
applyFailure(patchLast, {
|
|
177
|
+
code: "NETWORK_ERROR",
|
|
178
|
+
message: err instanceof Error ? err.message : "An unknown error occurred.",
|
|
179
|
+
guardrailSlug: null
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
} finally {
|
|
183
|
+
const last = state.turns.at(-1);
|
|
184
|
+
if (last?.isStreaming) patchLast((turn) => ({ ...turn, isStreaming: false }));
|
|
185
|
+
if (state.isStreaming) emit({ ...state, isStreaming: false });
|
|
186
|
+
controller = null;
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
async setFeedback(turnId, feedback) {
|
|
190
|
+
const turn = state.turns.find((t) => t.id === turnId);
|
|
191
|
+
if (!turn?.messageId) {
|
|
192
|
+
throw new LainternError(
|
|
193
|
+
"FEEDBACK_UNAVAILABLE",
|
|
194
|
+
"This turn has no message id yet \u2014 feedback is only possible once a turn completes."
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
await client.submitFeedback(turn.messageId, feedback);
|
|
198
|
+
emit({
|
|
199
|
+
...state,
|
|
200
|
+
turns: state.turns.map((t) => t.id === turnId ? { ...t, feedback } : t)
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function mergeStep(steps, step) {
|
|
206
|
+
if (step.status !== "completed") return [...steps, step];
|
|
207
|
+
const startedIndex = steps.findIndex((s) => s.tool === step.tool && s.status === "started");
|
|
208
|
+
if (startedIndex === -1) return [...steps, step];
|
|
209
|
+
const next = [...steps];
|
|
210
|
+
next[startedIndex] = step;
|
|
211
|
+
return next;
|
|
212
|
+
}
|
|
213
|
+
function applyFailure(patchLast, failure) {
|
|
214
|
+
if (failure.code === "GUARDRAIL_TRIGGERED") {
|
|
215
|
+
patchLast((turn) => ({
|
|
216
|
+
...turn,
|
|
217
|
+
isStreaming: false,
|
|
218
|
+
assistantContent: "",
|
|
219
|
+
citations: /* @__PURE__ */ new Map(),
|
|
220
|
+
sources: [],
|
|
221
|
+
externalSources: {},
|
|
222
|
+
steps: [],
|
|
223
|
+
mode: null,
|
|
224
|
+
advisory: void 0,
|
|
225
|
+
blocked: { slug: failure.guardrailSlug, message: failure.message }
|
|
226
|
+
}));
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
patchLast((turn) => ({
|
|
230
|
+
...turn,
|
|
231
|
+
isStreaming: false,
|
|
232
|
+
error: {
|
|
233
|
+
code: failure.code,
|
|
234
|
+
message: failure.message,
|
|
235
|
+
...failure.rateLimitScope ? { rateLimitScope: failure.rateLimitScope } : {},
|
|
236
|
+
...failure.rateLimitWindow ? { rateLimitWindow: failure.rateLimitWindow } : {}
|
|
237
|
+
}
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// src/react/index.ts
|
|
242
|
+
function useLainternChat(client, options = {}) {
|
|
243
|
+
const optionsRef = react.useRef(options);
|
|
244
|
+
const conversation = react.useMemo(
|
|
245
|
+
() => createConversation(client, optionsRef.current),
|
|
246
|
+
[client]
|
|
247
|
+
);
|
|
248
|
+
const state = react.useSyncExternalStore(
|
|
249
|
+
react.useCallback((listener) => conversation.subscribe(listener), [conversation]),
|
|
250
|
+
react.useCallback(() => conversation.getState(), [conversation]),
|
|
251
|
+
react.useCallback(() => conversation.getState(), [conversation])
|
|
252
|
+
);
|
|
253
|
+
const send = react.useCallback(
|
|
254
|
+
(message, sendOptions) => conversation.send(message, sendOptions),
|
|
255
|
+
[conversation]
|
|
256
|
+
);
|
|
257
|
+
const abort = react.useCallback(() => conversation.abort(), [conversation]);
|
|
258
|
+
const reset = react.useCallback(() => conversation.reset(), [conversation]);
|
|
259
|
+
const setFeedback = react.useCallback(
|
|
260
|
+
(turnId, feedback) => conversation.setFeedback(turnId, feedback),
|
|
261
|
+
[conversation]
|
|
262
|
+
);
|
|
263
|
+
return { ...state, send, abort, reset, setFeedback, conversation };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
exports.useLainternChat = useLainternChat;
|
|
267
|
+
//# sourceMappingURL=index.cjs.map
|
|
268
|
+
//# sourceMappingURL=index.cjs.map
|