@copilotkit/learning 0.0.1

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/dist/index.cjs ADDED
@@ -0,0 +1,1134 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/types.ts
4
+ /** Event names the collector and CopilotKit Core emit. `emit()` rejects them. */
5
+ const BUILT_IN_EVENT_NAMES = [
6
+ "page",
7
+ "navigation",
8
+ "click",
9
+ "input",
10
+ "network",
11
+ "thread.linked",
12
+ "agent.run",
13
+ "agent.message",
14
+ "tool.call"
15
+ ];
16
+ /** Replaces values that never leave the browser, even with full capture: passwords and credentials. */
17
+ const REDACTED = "[redacted]";
18
+
19
+ //#endregion
20
+ //#region src/redact.ts
21
+ const CREDENTIAL = /password|passwd|passphrase|passcode|credential|api(key|token)|privatekey|subscriptionkey|(access|refresh|auth|session)token|clientsecret/;
22
+ const CREDENTIAL_WORD = / (secret|id ?token)s? | (authorization|cookies?|jwt|bearer ?token|[cx]srf ?token) $|^ (pwd|pass|token|otp) $/;
23
+ const METADATA = / (ids?|at|in|expires?|expiry|length|count|enabled|required|policy|hint|type) $/;
24
+ const PASSWORD_AUTOCOMPLETE = /(current|new)-password|one-time-code/;
25
+ const TOKEN = /"((?:\\[\s\S]|[^"\\])*)("?)(\s*:\s*)?/g;
26
+ const STRING = /"(?:\\[\s\S]?|[^"\\])*"?/y;
27
+ const PAIR = /([\w$.%+[\]-]{1,64})(\s*(?:=|:(?=\s|$))\s*)((?:(?:basic|bearer|digest|token) +)?[^\s&,;)}\]]*)/gi;
28
+ const USERINFO = /([a-z][\w+.-]{0,31}:\/\/)[^\s/?#"]*@/gi;
29
+ /** Text beyond this is neither redacted nor kept; snapshots keep 4 KiB of the result. */
30
+ const TEXT_LIMIT = 16 * 1024;
31
+ const REMEMBERED_VALUES = 20;
32
+ const isCredentialKey = (key) => {
33
+ const words = ` ${key.replace(/%([\da-f]{2})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))).replace(/([a-z\d])(?=[A-Z])/g, "$1 ").toLowerCase().split(/[^a-z\d]+/).filter(Boolean).join(" ")} `;
34
+ return !METADATA.test(words) && (CREDENTIAL.test(words.replace(/ /g, "")) || CREDENTIAL_WORD.test(words));
35
+ };
36
+ const decode = (body) => {
37
+ try {
38
+ return JSON.parse(`"${body}"`);
39
+ } catch {
40
+ return body;
41
+ }
42
+ };
43
+ /** The end of the JSON value at `start`: a string, a balanced object or array, or a bare literal. */
44
+ const endOfValue = (text, start) => {
45
+ let depth = 0;
46
+ for (let i = start; i < text.length; i++) {
47
+ const char = text[i];
48
+ if (char === "\"") {
49
+ STRING.lastIndex = i;
50
+ STRING.test(text);
51
+ i = STRING.lastIndex - 1;
52
+ } else if (char === "{" || char === "[") {
53
+ depth++;
54
+ continue;
55
+ } else if (char === "}" || char === "]") {
56
+ if (--depth < 0) return i;
57
+ } else {
58
+ if (!depth && (char === "," || /\s/.test(char))) return i;
59
+ continue;
60
+ }
61
+ if (!depth) return i + 1;
62
+ }
63
+ return text.length;
64
+ };
65
+ /**
66
+ * Redacts credential values in JSON (valid or cut off) and in key/value text,
67
+ * and recurses into string values that hold JSON, forms, or URLs. Unchanged
68
+ * parts keep their exact original text.
69
+ */
70
+ const redactText = (text, depth = 0) => {
71
+ const token = new RegExp(TOKEN);
72
+ let pending = false;
73
+ const pairs = (part) => {
74
+ const segment = part.replace(USERINFO, "$1");
75
+ return segment.replace(PAIR, (pair, key, separator, value, offset) => {
76
+ if (!isCredentialKey(key)) return pair;
77
+ if (offset + pair.length === segment.length) {
78
+ pending = true;
79
+ if (!value) return pair;
80
+ }
81
+ return key + separator + REDACTED;
82
+ });
83
+ };
84
+ let out = "";
85
+ let last = 0;
86
+ let match;
87
+ while (match = token.exec(text)) {
88
+ const [raw, body, closed, colon] = match;
89
+ pending = false;
90
+ out += pairs(text.slice(last, match.index));
91
+ let end = token.lastIndex;
92
+ let replacement = raw;
93
+ const value = body.includes("\\") ? decode(body) : body;
94
+ if (colon) {
95
+ if (isCredentialKey(value)) {
96
+ const valueEnd = endOfValue(text, end);
97
+ if (!/^(true|false|null|)$/.test(text.slice(end, valueEnd))) {
98
+ replacement += `"${REDACTED}"`;
99
+ end = valueEnd;
100
+ }
101
+ }
102
+ } else if (pending) replacement = `"${REDACTED}"`;
103
+ else if (depth < 4 && /[=:"]/.test(value)) {
104
+ const redacted = redactText(value, depth + 1);
105
+ if (redacted !== value) replacement = JSON.stringify(redacted).slice(0, closed ? void 0 : -1);
106
+ }
107
+ out += replacement;
108
+ last = token.lastIndex = end;
109
+ }
110
+ return out + pairs(text.slice(last));
111
+ };
112
+ /** Per-session redaction state: password-like fields and the values typed into them. */
113
+ function createRedactor() {
114
+ const seen = /* @__PURE__ */ new WeakSet();
115
+ const remembered = /* @__PURE__ */ new Map();
116
+ let observer;
117
+ const toggled = (records) => {
118
+ for (const record of records) if (record.oldValue?.toLowerCase() === "password") seen.add(record.target);
119
+ };
120
+ /** Replaces remembered values, raw and in their JSON and URL encodings. */
121
+ const scrub = (text) => {
122
+ for (const value of [...remembered.values()].sort((a, b) => b.length - a.length)) for (const form of [
123
+ value,
124
+ JSON.stringify(value).slice(1, -1),
125
+ encodeURIComponent(value),
126
+ new URLSearchParams({ v: value }).toString().slice(2)
127
+ ]) text = /^\d+$/.test(form) ? text.replace(RegExp(`(?<!\\d)${form}(?!\\d)`, "g"), REDACTED) : text.split(form).join(REDACTED);
128
+ return text;
129
+ };
130
+ /** True for current, former (show-password toggles), and autocomplete password fields. */
131
+ const isPassword = (element) => {
132
+ if (observer) toggled(observer.takeRecords());
133
+ if (!(element instanceof HTMLInputElement) || !(element.type === "password" || seen.has(element) || isCredentialKey(element.name) || PASSWORD_AUTOCOMPLETE.test(element.getAttribute("autocomplete") ?? ""))) return false;
134
+ seen.add(element);
135
+ if (element.value.length >= 4) {
136
+ remembered.delete(element);
137
+ remembered.set(element, element.value);
138
+ if (remembered.size > REMEMBERED_VALUES) remembered.delete(remembered.keys().next().value);
139
+ }
140
+ return true;
141
+ };
142
+ return {
143
+ isPassword,
144
+ watch() {
145
+ try {
146
+ for (const input of document.querySelectorAll("input[type=password]")) seen.add(input);
147
+ } catch {}
148
+ let current;
149
+ try {
150
+ current = new MutationObserver(toggled);
151
+ current.observe(document, {
152
+ subtree: true,
153
+ attributeFilter: ["type"],
154
+ attributeOldValue: true
155
+ });
156
+ observer = current;
157
+ } catch {
158
+ current = void 0;
159
+ }
160
+ const remember = (event) => {
161
+ try {
162
+ const [target] = event.composedPath();
163
+ if (target instanceof Element) isPassword(target);
164
+ } catch {}
165
+ };
166
+ for (const type of ["input", "change"]) window.addEventListener(type, remember, true);
167
+ return () => {
168
+ current?.disconnect();
169
+ if (current !== void 0 && observer === current) observer = void 0;
170
+ for (const type of ["input", "change"]) window.removeEventListener(type, remember, true);
171
+ };
172
+ },
173
+ field: (key, value) => isCredentialKey(key) ? REDACTED : scrub(redactText(value)),
174
+ body(text) {
175
+ try {
176
+ return scrub(redactText(text.slice(0, TEXT_LIMIT)));
177
+ } catch {
178
+ return;
179
+ }
180
+ },
181
+ url(value) {
182
+ try {
183
+ return scrub(value.replace(/[^#]+/g, (part) => redactText(part)));
184
+ } catch {
185
+ return REDACTED;
186
+ }
187
+ }
188
+ };
189
+ }
190
+
191
+ //#endregion
192
+ //#region src/clicks.ts
193
+ const INTERACTIVE = "button, a[href], input, select, textarea, label, summary, [role], [data-copilotkit-action], [contenteditable]:not([contenteditable=\"false\"])";
194
+ const RECOVERY_DISTANCE_PX = 5;
195
+ const MAX_TEXT_LENGTH = 1e3;
196
+ /** `closest()` that continues through shadow roots to their hosts. */
197
+ function closestAcrossShadow(el, selector) {
198
+ let current = el;
199
+ while (current !== null) {
200
+ const match = current.closest(selector);
201
+ if (match !== null) return match;
202
+ const root = current.getRootNode();
203
+ current = root instanceof ShadowRoot ? root.host : null;
204
+ }
205
+ return null;
206
+ }
207
+ /**
208
+ * Finds the nearest interactive element and retains its text, attributes, and value.
209
+ * The explicit `data-copilotkit-ignore` opt-out still applies.
210
+ */
211
+ function describeTarget(el, redact) {
212
+ if (closestAcrossShadow(el, "[data-copilotkit-ignore]") !== null) return null;
213
+ const element = el.closest(INTERACTIVE) ?? el;
214
+ const password = redact.isPassword(element);
215
+ return {
216
+ element,
217
+ target: {
218
+ tag: element.localName,
219
+ role: element.getAttribute("role"),
220
+ action: element.getAttribute("data-copilotkit-action"),
221
+ text: (element.textContent ?? "").slice(0, MAX_TEXT_LENGTH),
222
+ attributes: Object.fromEntries(Array.from(element.attributes, ({ name, value }) => [name, password && name === "value" || isCredentialKey(name.replace(/^data-/, "")) ? REDACTED : redact.url(value)])),
223
+ ...readControlValue(element, redact)
224
+ }
225
+ };
226
+ }
227
+ /** Snapshot live control properties; HTML attributes do not reflect edited values. */
228
+ function readControlValue(element, redact) {
229
+ if (element instanceof HTMLInputElement) return {
230
+ value: redact.isPassword(element) ? REDACTED : element.value,
231
+ ...["checkbox", "radio"].includes(element.type) ? { checked: element.checked } : {},
232
+ ...element.type === "file" ? { files: Array.from(element.files ?? [], (file) => ({
233
+ name: file.name,
234
+ type: file.type,
235
+ size: file.size,
236
+ lastModified: file.lastModified
237
+ })) } : {}
238
+ };
239
+ if (element instanceof HTMLSelectElement) return {
240
+ value: element.value,
241
+ selectedValues: Array.from(element.selectedOptions, (option) => option.value)
242
+ };
243
+ if (element instanceof HTMLTextAreaElement) return { value: element.value };
244
+ if (element.matches("[contenteditable]:not([contenteditable=\"false\"])")) return { value: element.textContent ?? "" };
245
+ return {};
246
+ }
247
+ function eventElement(event) {
248
+ const [first] = event.composedPath();
249
+ return first instanceof Element ? first : null;
250
+ }
251
+ function isRootOrDetached(el) {
252
+ return el.localName === "body" || el.localName === "html" || !el.isConnected;
253
+ }
254
+ function isNear(origin, event) {
255
+ return Math.abs(origin.x - event.clientX) <= RECOVERY_DISTANCE_PX && Math.abs(origin.y - event.clientY) <= RECOVERY_DISTANCE_PX;
256
+ }
257
+ /**
258
+ * Listens for clicks in the capture phase on `window`, so handlers that stop
259
+ * propagation do not hide them. The target is described at `pointerdown`,
260
+ * because a re-render before `click` can move the click to `<body>`.
261
+ */
262
+ function installClickCapture(params) {
263
+ const { emit, getRoute, enrich, isTrusted = (event) => event.isTrusted, redact = createRedactor() } = params;
264
+ let origin = null;
265
+ const onPointerDown = (event) => {
266
+ try {
267
+ if (event.isPrimary === false) return;
268
+ const el = eventElement(event);
269
+ origin = {
270
+ described: el === null ? null : describeTarget(el, redact),
271
+ x: event.clientX,
272
+ y: event.clientY
273
+ };
274
+ } catch {
275
+ origin = null;
276
+ }
277
+ };
278
+ const onClick = (event) => {
279
+ const pointerOrigin = origin;
280
+ origin = null;
281
+ try {
282
+ if (!isTrusted(event)) return;
283
+ const el = eventElement(event);
284
+ if (el === null) return;
285
+ const described = pointerOrigin !== null && isRootOrDetached(el) && isNear(pointerOrigin, event) ? pointerOrigin.described : describeTarget(el, redact);
286
+ if (described === null) return;
287
+ const input = event.detail === 0 ? "keyboard" : "pointer";
288
+ emit("click", {
289
+ target: {
290
+ ...described.target,
291
+ input
292
+ },
293
+ route: getRoute(),
294
+ url: redact.url(location.href),
295
+ ...enrich?.(described.element)
296
+ });
297
+ } catch {}
298
+ };
299
+ window.addEventListener("pointerdown", onPointerDown, {
300
+ capture: true,
301
+ passive: true
302
+ });
303
+ window.addEventListener("click", onClick, {
304
+ capture: true,
305
+ passive: true
306
+ });
307
+ return () => {
308
+ window.removeEventListener("pointerdown", onPointerDown, { capture: true });
309
+ window.removeEventListener("click", onClick, { capture: true });
310
+ };
311
+ }
312
+
313
+ //#endregion
314
+ //#region src/inputs.ts
315
+ /** Records user edits without changing the control or interfering with its listeners. */
316
+ function installInputCapture(params) {
317
+ const { emit, isTrusted = (event) => event.isTrusted, redact = createRedactor() } = params;
318
+ const onInput = (event) => {
319
+ try {
320
+ if (!isTrusted(event)) return;
321
+ const [element] = event.composedPath();
322
+ if (!(element instanceof Element)) return;
323
+ const described = describeTarget(element, redact);
324
+ if (described === null) return;
325
+ const value = readControlValue(element, redact);
326
+ if (!("value" in value)) return;
327
+ emit("input", {
328
+ eventType: event.type,
329
+ target: {
330
+ ...described.target,
331
+ ...value
332
+ },
333
+ url: redact.url(location.href),
334
+ route: location.pathname
335
+ });
336
+ } catch {}
337
+ };
338
+ window.addEventListener("input", onInput, {
339
+ capture: true,
340
+ passive: true
341
+ });
342
+ window.addEventListener("change", onInput, {
343
+ capture: true,
344
+ passive: true
345
+ });
346
+ return () => {
347
+ window.removeEventListener("input", onInput, { capture: true });
348
+ window.removeEventListener("change", onInput, { capture: true });
349
+ };
350
+ }
351
+
352
+ //#endregion
353
+ //#region src/navigation.ts
354
+ function isNavigationTarget(value) {
355
+ return typeof value === "object" && value !== null && "addEventListener" in value && "removeEventListener" in value;
356
+ }
357
+ function getNavigation() {
358
+ const navigation = Reflect.get(window, "navigation");
359
+ return isNavigationTarget(navigation) ? navigation : null;
360
+ }
361
+ function toNavigationType(value) {
362
+ switch (value) {
363
+ case "replace":
364
+ case "traverse":
365
+ case "reload": return value;
366
+ default: return "push";
367
+ }
368
+ }
369
+ /**
370
+ * Emits `navigation` when the full URL changes in a single-page app.
371
+ * Uses the Navigation API when it exists. Otherwise patches `pushState` and
372
+ * `replaceState` on the prototype and on the instance, because some routers
373
+ * (Next.js) wrap the instance method.
374
+ */
375
+ function installNavigationCapture(params) {
376
+ const { emit, redact = createRedactor() } = params;
377
+ let current = location.href;
378
+ const check = (navigationType) => {
379
+ try {
380
+ const next = location.href;
381
+ if (next === current) return;
382
+ emit("navigation", {
383
+ from: redact.url(current),
384
+ to: redact.url(next),
385
+ navigationType
386
+ });
387
+ current = next;
388
+ } catch {}
389
+ };
390
+ const navigation = getNavigation();
391
+ if (navigation !== null) {
392
+ const onEntryChange = (event) => check(toNavigationType(Reflect.get(event, "navigationType")));
393
+ navigation.addEventListener("currententrychange", onEntryChange);
394
+ return () => navigation.removeEventListener("currententrychange", onEntryChange);
395
+ }
396
+ return installHistoryFallback(check);
397
+ }
398
+ function installHistoryFallback(check) {
399
+ let active = true;
400
+ const restores = [];
401
+ const patch = (target, method) => {
402
+ const original = target[method];
403
+ const wrapper = function(...args) {
404
+ const result = original.apply(this, args);
405
+ if (active) check(method === "pushState" ? "push" : "replace");
406
+ return result;
407
+ };
408
+ target[method] = wrapper;
409
+ restores.push(() => {
410
+ if (target[method] === wrapper) target[method] = original;
411
+ });
412
+ };
413
+ for (const method of ["pushState", "replaceState"]) {
414
+ patch(History.prototype, method);
415
+ if (Object.prototype.hasOwnProperty.call(history, method)) patch(history, method);
416
+ }
417
+ const onPopState = () => check("traverse");
418
+ window.addEventListener("popstate", onPopState);
419
+ window.addEventListener("hashchange", onPopState);
420
+ return () => {
421
+ active = false;
422
+ window.removeEventListener("popstate", onPopState);
423
+ window.removeEventListener("hashchange", onPopState);
424
+ for (const restore of restores) restore();
425
+ };
426
+ }
427
+
428
+ //#endregion
429
+ //#region src/network-body.ts
430
+ const BODY_BYTES = 4096;
431
+ const READ_TIMEOUT_MS = 1e3;
432
+ const encoder = new TextEncoder();
433
+ const redactionFailed = () => ({
434
+ status: "unavailable",
435
+ reason: "redaction-failed"
436
+ });
437
+ /** Redacts up to TEXT_LIMIT bytes of text first, then cuts the result to BODY_BYTES. */
438
+ function snapshot(bytes, status, contentType, redact, reason) {
439
+ let text;
440
+ if (!isBinary(contentType)) try {
441
+ text = new TextDecoder("utf-8", { fatal: !contentType }).decode(bytes, { stream: status !== "complete" });
442
+ } catch {}
443
+ if (text !== void 0) {
444
+ text = redact(text);
445
+ if (text === void 0) return redactionFailed();
446
+ if (text.includes("\0")) {
447
+ bytes = encoder.encode(text);
448
+ text = void 0;
449
+ }
450
+ }
451
+ const binary = text === void 0;
452
+ text ??= btoa(String.fromCharCode(...bytes.subarray(0, BODY_BYTES)));
453
+ const result = {
454
+ status: status === "complete" && bytes.length === 0 ? "empty" : status,
455
+ text,
456
+ encoding: binary ? "base64" : "utf-8",
457
+ ...reason ? { reason } : {}
458
+ };
459
+ if (encoder.encode(JSON.stringify(result)).length <= BODY_BYTES) return result;
460
+ result.status = status === "complete" ? "truncated" : status;
461
+ result.truncated = true;
462
+ let low = 0;
463
+ let high = text.length;
464
+ while (low < high) {
465
+ const middle = Math.ceil((low + high) / 2);
466
+ result.text = text.slice(0, middle);
467
+ if (encoder.encode(JSON.stringify(result)).length <= BODY_BYTES) low = middle;
468
+ else high = middle - 1;
469
+ }
470
+ if (binary) low -= low % 4;
471
+ else if (low > 0 && /[\uD800-\uDBFF]/.test(text[low - 1])) low--;
472
+ result.text = text.slice(0, low);
473
+ return result;
474
+ }
475
+ function snapshotText(value, redact, status = "complete", reason) {
476
+ return snapshot(encoder.encode(value.slice(0, TEXT_LIMIT)), value.length > TEXT_LIMIT && status === "complete" ? "truncated" : status, "text/plain", redact, reason);
477
+ }
478
+ function isBinary(contentType) {
479
+ return contentType !== "" && !/(^text\/|json|xml|javascript|graphql|x-www-form-urlencoded|multipart\/form-data)/i.test(contentType);
480
+ }
481
+ /** Cancel only cloned/synthetic bodies. Cancelling a tee may wait for the app's branch. */
482
+ function createBodyCapture(redact) {
483
+ const cancellations = /* @__PURE__ */ new Set();
484
+ let active = true;
485
+ const read = (stream, contentType = "") => {
486
+ if (!stream) return Promise.resolve({ status: "empty" });
487
+ if (!active) return Promise.resolve({
488
+ status: "unavailable",
489
+ reason: "capture-stopped"
490
+ });
491
+ let reader;
492
+ try {
493
+ reader = stream.getReader();
494
+ } catch {
495
+ return Promise.resolve({
496
+ status: "unavailable",
497
+ reason: "body-locked"
498
+ });
499
+ }
500
+ return new Promise((resolve) => {
501
+ const bytes = new Uint8Array(TEXT_LIMIT);
502
+ let length = 0;
503
+ let finished = false;
504
+ const finish = (status, reason) => {
505
+ if (finished) return;
506
+ finished = true;
507
+ clearTimeout(timer);
508
+ cancellations.delete(cancel);
509
+ resolve(snapshot(bytes.subarray(0, length), status, contentType, redact.body, reason));
510
+ reader.cancel().catch(() => {});
511
+ };
512
+ const cancel = () => finish("interrupted", "capture-stopped");
513
+ const timer = setTimeout(() => finish("timeout", "body-read-timeout"), READ_TIMEOUT_MS);
514
+ cancellations.add(cancel);
515
+ (async () => {
516
+ try {
517
+ for (;;) {
518
+ const chunk = await reader.read();
519
+ if (finished) break;
520
+ if (chunk.done) {
521
+ finish("complete");
522
+ break;
523
+ }
524
+ const kept = chunk.value.subarray(0, TEXT_LIMIT - length);
525
+ bytes.set(kept, length);
526
+ length += kept.length;
527
+ if (length === TEXT_LIMIT) {
528
+ finish("truncated");
529
+ break;
530
+ }
531
+ }
532
+ } catch {
533
+ finish("interrupted", "body-read-failed");
534
+ } finally {
535
+ try {
536
+ reader.releaseLock();
537
+ } catch {}
538
+ }
539
+ })();
540
+ });
541
+ };
542
+ const body = (value, contentType = "") => {
543
+ try {
544
+ if (value == null) return Promise.resolve({ status: "empty" });
545
+ if (value instanceof URLSearchParams) value = value.toString();
546
+ if (typeof value === "string") return Promise.resolve(snapshotText(value, redact.body));
547
+ if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) return Promise.resolve({
548
+ status: "unavailable",
549
+ reason: "caller-owned-stream"
550
+ });
551
+ if (value instanceof FormData) {
552
+ const form = new FormData();
553
+ value.forEach((field, key) => form.append(key, typeof field === "string" ? redact.field(key, field) : field));
554
+ value = form;
555
+ }
556
+ const response = new Response(value);
557
+ return read(response.body, contentType || response.headers.get("content-type") || "");
558
+ } catch {
559
+ return Promise.resolve({
560
+ status: "unavailable",
561
+ reason: "body-not-readable"
562
+ });
563
+ }
564
+ };
565
+ return {
566
+ read,
567
+ body,
568
+ text: (value, status, reason) => snapshotText(value, redact.body, status, reason),
569
+ stop() {
570
+ active = false;
571
+ for (const cancel of cancellations) cancel();
572
+ }
573
+ };
574
+ }
575
+
576
+ //#endregion
577
+ //#region src/network.ts
578
+ const emptyBody = () => Promise.resolve({ status: "empty" });
579
+ const unavailableBody = (reason) => Promise.resolve({
580
+ status: "unavailable",
581
+ reason
582
+ });
583
+ const toAbsolute = (url) => new URL(url, location.href).href;
584
+ const CREDENTIAL_HEADERS = new Set([
585
+ "x-amz-security-token",
586
+ "private-token",
587
+ "x-token"
588
+ ]);
589
+ const headerValue = (name, value) => CREDENTIAL_HEADERS.has(name.toLowerCase()) || isCredentialKey(name) ? REDACTED : value;
590
+ const headerValues = (headers) => Object.fromEntries(Array.from(headers.entries(), ([name, value]) => [name, headerValue(name, value)]));
591
+ function isAbortError(error) {
592
+ return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
593
+ }
594
+ /** Records browser-visible request/response data without consuming the app's bodies. */
595
+ function installNetworkCapture(params) {
596
+ let active = true;
597
+ const { redact = createRedactor() } = params;
598
+ const bodies = createBodyCapture(redact);
599
+ const ignoreUrls = params.ignoreUrls.map((pattern) => typeof pattern === "string" ? toAbsolute(pattern) : new RegExp(pattern.source, pattern.flags));
600
+ const isIgnored = (url) => ignoreUrls.some((pattern) => {
601
+ if (typeof pattern === "string") return url.startsWith(pattern);
602
+ pattern.lastIndex = 0;
603
+ return pattern.test(url);
604
+ });
605
+ const record = (transport, request, requestBody, startedAt, status, outcome, response) => {
606
+ const durationMs = Math.round(performance.now() - startedAt);
607
+ const completedAt = Date.now();
608
+ Promise.all([requestBody, response?.body ?? unavailableBody("no-response")]).then(([sent, received]) => {
609
+ if (!active) return;
610
+ const href = redact.url(request.url);
611
+ const url = new URL(href);
612
+ params.emit("network", {
613
+ transport,
614
+ method: request.method,
615
+ url: href,
616
+ origin: url.origin,
617
+ route: url.pathname,
618
+ status,
619
+ durationMs,
620
+ completedAt,
621
+ outcome,
622
+ request: {
623
+ headers: request.headers,
624
+ body: sent
625
+ },
626
+ response: {
627
+ ...response?.url ? { url: redact.url(response.url) } : {},
628
+ headers: response?.headers ?? {},
629
+ body: received
630
+ }
631
+ });
632
+ }).catch(() => {});
633
+ };
634
+ const originalFetch = globalThis.fetch;
635
+ const wrappedFetch = (input, init) => {
636
+ if (!active) return originalFetch.call(globalThis, input, init);
637
+ let request;
638
+ let ignored;
639
+ try {
640
+ const source = typeof Request !== "undefined" && input instanceof Request ? input : void 0;
641
+ request = {
642
+ url: toAbsolute(source ? source.url : String(input)),
643
+ method: (init?.method ?? source?.method ?? "GET").toUpperCase(),
644
+ headers: headerValues(new Headers(init?.headers ?? source?.headers))
645
+ };
646
+ ignored = isIgnored(request.url);
647
+ } catch {
648
+ return originalFetch.call(globalThis, input, init);
649
+ }
650
+ if (ignored) return originalFetch.call(globalThis, input, init);
651
+ const startedAt = performance.now();
652
+ let requestBody;
653
+ try {
654
+ requestBody = init?.body != null ? bodies.body(init.body, request.headers["content-type"]) : typeof Request !== "undefined" && input instanceof Request ? bodies.read(input.clone().body, request.headers["content-type"]) : emptyBody();
655
+ } catch {
656
+ requestBody = unavailableBody("request-clone-failed");
657
+ }
658
+ const failed = (error) => record("fetch", request, requestBody, startedAt, null, isAbortError(error) ? "aborted" : "error");
659
+ let promise;
660
+ try {
661
+ promise = originalFetch.call(globalThis, input, init);
662
+ } catch (error) {
663
+ failed(error);
664
+ throw error;
665
+ }
666
+ promise.then((response) => {
667
+ if (!active) return;
668
+ let responseBody;
669
+ try {
670
+ responseBody = response.type === "opaque" || response.type === "opaqueredirect" ? unavailableBody("opaque-response") : bodies.read(response.clone().body, response.headers.get("content-type") ?? "");
671
+ } catch {
672
+ responseBody = unavailableBody("response-clone-failed");
673
+ }
674
+ record("fetch", request, requestBody, startedAt, response.status, "ok", {
675
+ url: response.url,
676
+ headers: headerValues(response.headers),
677
+ body: responseBody
678
+ });
679
+ }, failed).catch(() => {});
680
+ return promise;
681
+ };
682
+ if (typeof originalFetch === "function") globalThis.fetch = wrappedFetch;
683
+ const uninstallXhr = patchXhr(isIgnored, record, bodies);
684
+ return () => {
685
+ active = false;
686
+ bodies.stop();
687
+ if (globalThis.fetch === wrappedFetch) globalThis.fetch = originalFetch;
688
+ uninstallXhr();
689
+ };
690
+ }
691
+ function patchXhr(isIgnored, record, bodies) {
692
+ if (typeof XMLHttpRequest !== "function") return () => {};
693
+ const proto = XMLHttpRequest.prototype;
694
+ const originalOpen = proto.open;
695
+ const originalSend = proto.send;
696
+ const originalSetHeader = proto.setRequestHeader;
697
+ const requests = /* @__PURE__ */ new WeakMap();
698
+ const listeners = /* @__PURE__ */ new Set();
699
+ const inFlight = /* @__PURE__ */ new WeakMap();
700
+ let active = true;
701
+ const wrappedOpen = function(method, url, ...rest) {
702
+ inFlight.get(this)?.();
703
+ const result = Reflect.apply(originalOpen, this, [
704
+ method,
705
+ url,
706
+ ...rest
707
+ ]);
708
+ try {
709
+ requests.set(this, {
710
+ url: toAbsolute(String(url)),
711
+ method: method.toUpperCase(),
712
+ headers: {}
713
+ });
714
+ } catch {
715
+ requests.delete(this);
716
+ }
717
+ return result;
718
+ };
719
+ const wrappedSetHeader = function(name, value) {
720
+ const result = originalSetHeader.call(this, name, value);
721
+ const request = requests.get(this);
722
+ if (active && request) try {
723
+ const headers = new Headers(request.headers);
724
+ headers.append(name, value);
725
+ request.headers = headerValues(headers);
726
+ } catch {}
727
+ return result;
728
+ };
729
+ const wrappedSend = function(body) {
730
+ const request = requests.get(this);
731
+ if (!active || !request || isIgnored(request.url)) return originalSend.call(this, body);
732
+ const startedAt = performance.now();
733
+ let requestBody;
734
+ try {
735
+ requestBody = typeof Document !== "undefined" && body instanceof Document ? Promise.resolve(bodies.text(new XMLSerializer().serializeToString(body))) : bodies.body(body, request.headers["content-type"]);
736
+ } catch {
737
+ requestBody = unavailableBody("request-body-unavailable");
738
+ }
739
+ let aborted = false;
740
+ const onAbort = () => {
741
+ aborted = true;
742
+ };
743
+ const cleanup = () => {
744
+ this.removeEventListener("abort", onAbort);
745
+ this.removeEventListener("loadend", onEnd);
746
+ listeners.delete(cleanup);
747
+ if (inFlight.get(this) === cleanup) inFlight.delete(this);
748
+ };
749
+ const onEnd = () => {
750
+ cleanup();
751
+ if (!active) return;
752
+ const outcome = aborted ? "aborted" : this.status === 0 ? "error" : "ok";
753
+ const headers = {};
754
+ try {
755
+ for (const line of this.getAllResponseHeaders().split(/\r?\n/)) {
756
+ const split = line.indexOf(":");
757
+ if (split > 0) {
758
+ const name = line.slice(0, split).toLowerCase();
759
+ headers[name] = headerValue(name, line.slice(split + 1).trim());
760
+ }
761
+ }
762
+ } catch {}
763
+ let responseBody;
764
+ try {
765
+ if (this.responseType === "" || this.responseType === "text") responseBody = Promise.resolve(bodies.text(this.responseText, outcome === "ok" ? "complete" : "interrupted", outcome === "ok" ? void 0 : `request-${outcome}`));
766
+ else if (this.responseType === "json") responseBody = Promise.resolve(bodies.text(JSON.stringify(this.response)));
767
+ else if (this.responseType === "document") responseBody = this.responseXML ? Promise.resolve(bodies.text(new XMLSerializer().serializeToString(this.responseXML))) : unavailableBody("empty-document");
768
+ else responseBody = bodies.body(this.response, headers["content-type"] ?? "application/octet-stream");
769
+ } catch {
770
+ responseBody = unavailableBody("response-body-unavailable");
771
+ }
772
+ record("xhr", request, requestBody, startedAt, outcome === "ok" ? this.status : null, outcome, {
773
+ url: this.responseURL,
774
+ headers,
775
+ body: responseBody
776
+ });
777
+ };
778
+ this.addEventListener("abort", onAbort);
779
+ this.addEventListener("loadend", onEnd);
780
+ listeners.add(cleanup);
781
+ inFlight.set(this, cleanup);
782
+ try {
783
+ return originalSend.call(this, body);
784
+ } catch (error) {
785
+ cleanup();
786
+ record("xhr", request, requestBody, startedAt, null, isAbortError(error) ? "aborted" : "error");
787
+ throw error;
788
+ }
789
+ };
790
+ proto.open = wrappedOpen;
791
+ proto.send = wrappedSend;
792
+ proto.setRequestHeader = wrappedSetHeader;
793
+ return () => {
794
+ active = false;
795
+ for (const cleanup of listeners) cleanup();
796
+ if (proto.open === wrappedOpen) proto.open = originalOpen;
797
+ if (proto.send === wrappedSend) proto.send = originalSend;
798
+ if (proto.setRequestHeader === wrappedSetHeader) proto.setRequestHeader = originalSetHeader;
799
+ };
800
+ }
801
+
802
+ //#endregion
803
+ //#region src/routes.ts
804
+ /** Returns the original pathname. Capture no longer masks or categorizes routes. */
805
+ function toRoute(pathname, _templates = []) {
806
+ return pathname;
807
+ }
808
+ /** Resolves a URL without removing path segments, query parameters, or fragments. */
809
+ function toOriginAndRoute(url, _templates = [], base = globalThis.location?.href ?? "http://localhost/") {
810
+ const parsed = new URL(url, base);
811
+ return {
812
+ url: parsed.href,
813
+ origin: parsed.origin,
814
+ route: parsed.pathname
815
+ };
816
+ }
817
+
818
+ //#endregion
819
+ //#region src/collector.ts
820
+ const FLUSH_SIZE = 50;
821
+ const FLUSH_INTERVAL_MS = 2e3;
822
+ const BUILT_IN = new Set(BUILT_IN_EVENT_NAMES);
823
+ /**
824
+ * Creates a collector. Nothing is installed until `start()`: no listeners,
825
+ * no patches, no timers, no buffer. `stop()` removes all of them and sends
826
+ * queued events once, without retries.
827
+ *
828
+ * @example
829
+ * const collector = createCollector({ sink: httpSink("/api/learning-events") });
830
+ * collector.start({ trajectoryId: crypto.randomUUID() });
831
+ */
832
+ function createCollector(options) {
833
+ const { sink, routes, beforeSend, enrich } = options;
834
+ const capture = {
835
+ clicks: true,
836
+ navigation: true,
837
+ network: true,
838
+ inputs: true,
839
+ ...options.capture
840
+ };
841
+ let session = null;
842
+ let seq = 0;
843
+ const getRoute = () => toRoute(location.pathname, routes);
844
+ const flush = (sendOptions) => {
845
+ const current = session;
846
+ if (current === null) return;
847
+ if (current.queue.length === 0 && current.dropped === 0) return;
848
+ const events = current.queue.splice(0);
849
+ const dropped = current.dropped;
850
+ current.dropped = 0;
851
+ const batch = {
852
+ trajectoryId: current.trajectoryId,
853
+ learningContainerIds: current.learningContainerIds,
854
+ events,
855
+ dropped
856
+ };
857
+ const onFailure = () => {
858
+ if (session === current) current.dropped += events.length + dropped;
859
+ };
860
+ try {
861
+ Promise.resolve(sink(batch, sendOptions)).catch(onFailure);
862
+ } catch {
863
+ onFailure();
864
+ }
865
+ };
866
+ const record = (name, value) => {
867
+ const current = session;
868
+ if (current === null) return;
869
+ try {
870
+ seq += 1;
871
+ const event = {
872
+ type: "CUSTOM",
873
+ name,
874
+ value: {
875
+ ...value,
876
+ seq
877
+ },
878
+ timestamp: Date.now()
879
+ };
880
+ const kept = beforeSend === void 0 ? event : beforeSend(event);
881
+ if (kept === null) return;
882
+ current.queue.push(kept);
883
+ if (current.queue.length >= FLUSH_SIZE) flush();
884
+ } catch {}
885
+ };
886
+ const start = ({ trajectoryId, learningContainerIds }) => {
887
+ if (session !== null) {
888
+ if (session.trajectoryId !== trajectoryId) console.warn(`[@copilotkit/learning] Trajectory "${session.trajectoryId}" is active. Stop it before you start "${trajectoryId}".`);
889
+ return;
890
+ }
891
+ if (typeof window === "undefined" || typeof window.addEventListener !== "function") return;
892
+ const current = {
893
+ trajectoryId,
894
+ learningContainerIds,
895
+ queue: [],
896
+ dropped: 0,
897
+ uninstalls: [],
898
+ timer: void 0
899
+ };
900
+ session = current;
901
+ const recordCurrent = (name, value) => {
902
+ if (session === current) record(name, value);
903
+ };
904
+ const redact = createRedactor();
905
+ current.uninstalls.push(redact.watch());
906
+ const ignoreUrls = [...options.ignoreUrls ?? [], ...sink.url === void 0 ? [] : [sink.url]];
907
+ if (capture.clicks) current.uninstalls.push(installClickCapture({
908
+ emit: recordCurrent,
909
+ getRoute,
910
+ enrich,
911
+ redact
912
+ }));
913
+ if (capture.navigation) current.uninstalls.push(installNavigationCapture({
914
+ emit: recordCurrent,
915
+ routes,
916
+ redact
917
+ }));
918
+ if (capture.network) current.uninstalls.push(installNetworkCapture({
919
+ emit: recordCurrent,
920
+ routes,
921
+ ignoreUrls,
922
+ redact
923
+ }));
924
+ if (capture.inputs) current.uninstalls.push(installInputCapture({
925
+ emit: recordCurrent,
926
+ redact
927
+ }));
928
+ const onPageHide = () => flush({ beacon: true });
929
+ window.addEventListener("pagehide", onPageHide);
930
+ current.uninstalls.push(() => window.removeEventListener("pagehide", onPageHide));
931
+ current.timer = setInterval(() => flush(), FLUSH_INTERVAL_MS);
932
+ record("page", {
933
+ route: getRoute(),
934
+ url: redact.url(location.href),
935
+ title: document.title,
936
+ referrer: redact.url(document.referrer)
937
+ });
938
+ };
939
+ const stop = () => {
940
+ const current = session;
941
+ if (current === null) return;
942
+ clearInterval(current.timer);
943
+ for (const uninstall of current.uninstalls) uninstall();
944
+ flush();
945
+ session = null;
946
+ };
947
+ const emit = (name, value) => {
948
+ if (BUILT_IN.has(name)) {
949
+ console.warn(`[@copilotkit/learning] "${name}" is a built-in event name. Use your own name, such as "deal.approved".`);
950
+ return;
951
+ }
952
+ record(name, value);
953
+ };
954
+ const ɵemit = (name, value) => record(name, value);
955
+ return {
956
+ start,
957
+ stop,
958
+ emit,
959
+ ɵemit,
960
+ get trajectoryId() {
961
+ return session?.trajectoryId ?? null;
962
+ }
963
+ };
964
+ }
965
+
966
+ //#endregion
967
+ //#region src/http-sink.ts
968
+ /**
969
+ * A sink that POSTs each batch as JSON to `url`.
970
+ *
971
+ * Uses `fetch` with `keepalive`. When the page is going away it uses
972
+ * `navigator.sendBeacon`, which cannot send custom headers. There is no retry:
973
+ * a failed send rejects, and the collector counts those events as dropped.
974
+ *
975
+ * @example createCollector({ sink: httpSink("/api/learning-events") })
976
+ */
977
+ function httpSink(url, init = {}) {
978
+ const send = async (batch, options = {}) => {
979
+ const body = JSON.stringify(batch);
980
+ if (options.beacon === true && typeof navigator.sendBeacon === "function") {
981
+ if (!navigator.sendBeacon(url, new Blob([body], { type: "application/json" }))) throw new Error("sendBeacon refused the batch");
982
+ return;
983
+ }
984
+ const response = await fetch(url, {
985
+ method: "POST",
986
+ keepalive: true,
987
+ headers: {
988
+ "content-type": "application/json",
989
+ ...init.headers
990
+ },
991
+ body
992
+ });
993
+ if (!response.ok) throw new Error(`Sink responded with ${response.status}`);
994
+ };
995
+ return Object.assign(send, { url });
996
+ }
997
+
998
+ //#endregion
999
+ //#region src/trajectory-collector.ts
1000
+ const MAX_TRAJECTORY_EVENT_BYTES = 16 * 1024;
1001
+ const RESERVED_NAMES = new Set(BUILT_IN_EVENT_NAMES);
1002
+ function isJson(value, ancestors = /* @__PURE__ */ new Set()) {
1003
+ if (typeof value === "string") return !value.includes("\0");
1004
+ if (value === null || typeof value === "boolean") return true;
1005
+ if (typeof value === "number") return Number.isFinite(value);
1006
+ if (typeof value !== "object" || ancestors.has(value)) return false;
1007
+ if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) return false;
1008
+ ancestors.add(value);
1009
+ if (!Array.isArray(value) && Object.keys(value).some((key) => key.includes("\0"))) return false;
1010
+ const values = Array.isArray(value) ? value : Object.values(value);
1011
+ for (const item of values) if (!isJson(item, ancestors)) return false;
1012
+ ancestors.delete(value);
1013
+ return true;
1014
+ }
1015
+ /**
1016
+ * Captures outside-chat activity as plain AG-UI events, without a send queue.
1017
+ * Core starts this collector only after authentication and channel join.
1018
+ */
1019
+ function createTrajectoryCollector(options) {
1020
+ let active = false;
1021
+ let uninstalls = [];
1022
+ const report = (code, message) => {
1023
+ try {
1024
+ options.onError?.({
1025
+ code,
1026
+ message
1027
+ });
1028
+ } catch {}
1029
+ };
1030
+ const getRoute = () => toRoute(location.pathname);
1031
+ const record = (name, value) => {
1032
+ if (!active) return;
1033
+ let payload;
1034
+ try {
1035
+ const event = {
1036
+ type: "CUSTOM",
1037
+ name,
1038
+ timestamp: Date.now(),
1039
+ value
1040
+ };
1041
+ const kept = options.beforeSend === void 0 ? event : options.beforeSend(event);
1042
+ if (kept === null || !active) return;
1043
+ if (kept.type !== "CUSTOM" || typeof kept.name !== "string" || !kept.name.trim() || kept.name.length > 200 || kept.name.includes("\0") || !Number.isFinite(kept.timestamp) || kept.timestamp <= 0 || kept.timestamp >= 0xe677d21fdc00 || !isJson(kept.value)) {
1044
+ report("INVALID_EVENT", "The event must contain a name, timestamp, and JSON value.");
1045
+ return;
1046
+ }
1047
+ const serialized = JSON.stringify({
1048
+ type: kept.type,
1049
+ name: kept.name,
1050
+ timestamp: kept.timestamp,
1051
+ value: kept.value
1052
+ });
1053
+ if (new TextEncoder().encode(serialized).byteLength > MAX_TRAJECTORY_EVENT_BYTES) {
1054
+ report("EVENT_TOO_LARGE", "The serialized event exceeds 16 KiB.");
1055
+ return;
1056
+ }
1057
+ payload = JSON.parse(serialized);
1058
+ } catch {
1059
+ report("INVALID_EVENT", "The event could not be serialized as JSON.");
1060
+ return;
1061
+ }
1062
+ try {
1063
+ options.send(payload);
1064
+ } catch {
1065
+ report("PERSISTENCE_UNKNOWN", "Event delivery failed. The event will not be retried.");
1066
+ }
1067
+ };
1068
+ const capture = (name, value) => {
1069
+ record(name, value);
1070
+ };
1071
+ const stop = () => {
1072
+ active = false;
1073
+ const installed = uninstalls;
1074
+ uninstalls = [];
1075
+ for (const uninstall of installed) uninstall();
1076
+ };
1077
+ return {
1078
+ start() {
1079
+ if (active || typeof window === "undefined") return;
1080
+ active = true;
1081
+ if (typeof window.addEventListener !== "function") return;
1082
+ const redact = createRedactor();
1083
+ try {
1084
+ uninstalls.push(redact.watch());
1085
+ if (options.capture?.clicks !== false) uninstalls.push(installClickCapture({
1086
+ emit: capture,
1087
+ getRoute,
1088
+ redact
1089
+ }));
1090
+ if (options.capture?.navigation !== false) uninstalls.push(installNavigationCapture({
1091
+ emit: capture,
1092
+ redact
1093
+ }));
1094
+ if (options.capture?.inputs !== false) uninstalls.push(installInputCapture({
1095
+ emit: capture,
1096
+ redact
1097
+ }));
1098
+ if (options.capture?.network !== false) uninstalls.push(installNetworkCapture({
1099
+ emit: capture,
1100
+ ignoreUrls: options.ignoreUrls ?? [],
1101
+ redact
1102
+ }));
1103
+ record("page", {
1104
+ route: getRoute(),
1105
+ url: redact.url(location.href),
1106
+ title: document.title,
1107
+ referrer: redact.url(document.referrer)
1108
+ });
1109
+ } catch {
1110
+ stop();
1111
+ report("CAPTURE_FAILED", "Browser capture could not start.");
1112
+ }
1113
+ },
1114
+ stop,
1115
+ emit(name, value) {
1116
+ if (!active) return;
1117
+ if (RESERVED_NAMES.has(name)) {
1118
+ report("INVALID_EVENT", "Use a developer event name, not a reserved capture name.");
1119
+ return;
1120
+ }
1121
+ record(name, value);
1122
+ }
1123
+ };
1124
+ }
1125
+
1126
+ //#endregion
1127
+ exports.BUILT_IN_EVENT_NAMES = BUILT_IN_EVENT_NAMES;
1128
+ exports.MAX_TRAJECTORY_EVENT_BYTES = MAX_TRAJECTORY_EVENT_BYTES;
1129
+ exports.createCollector = createCollector;
1130
+ exports.createTrajectoryCollector = createTrajectoryCollector;
1131
+ exports.httpSink = httpSink;
1132
+ exports.toOriginAndRoute = toOriginAndRoute;
1133
+ exports.toRoute = toRoute;
1134
+ //# sourceMappingURL=index.cjs.map