@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.
@@ -0,0 +1,193 @@
1
+ import { installClickCapture } from "./clicks";
2
+ import { installNavigationCapture } from "./navigation";
3
+ import { installInputCapture } from "./inputs";
4
+ import { installNetworkCapture } from "./network";
5
+ import { createRedactor } from "./redact";
6
+ import { toRoute } from "./routes";
7
+ import { BUILT_IN_EVENT_NAMES } from "./types";
8
+ import type {
9
+ JsonValue,
10
+ TrajectoryCaptureOptions,
11
+ TrajectoryCollector,
12
+ TrajectoryEvent,
13
+ } from "./trajectory-types";
14
+
15
+ export const MAX_TRAJECTORY_EVENT_BYTES = 16 * 1024;
16
+ const RESERVED_NAMES = new Set<string>(BUILT_IN_EVENT_NAMES);
17
+
18
+ function isJson(
19
+ value: unknown,
20
+ ancestors = new Set<object>(),
21
+ ): value is JsonValue {
22
+ if (typeof value === "string") return !value.includes("\0");
23
+ if (value === null || typeof value === "boolean") return true;
24
+ if (typeof value === "number") return Number.isFinite(value);
25
+ if (typeof value !== "object" || ancestors.has(value)) return false;
26
+ if (
27
+ !Array.isArray(value) &&
28
+ Object.getPrototypeOf(value) !== Object.prototype &&
29
+ Object.getPrototypeOf(value) !== null
30
+ )
31
+ return false;
32
+ ancestors.add(value);
33
+ if (
34
+ !Array.isArray(value) &&
35
+ Object.keys(value).some((key) => key.includes("\0"))
36
+ )
37
+ return false;
38
+ const values = Array.isArray(value) ? value : Object.values(value);
39
+ for (const item of values) {
40
+ if (!isJson(item, ancestors)) return false;
41
+ }
42
+ ancestors.delete(value);
43
+ return true;
44
+ }
45
+
46
+ /**
47
+ * Captures outside-chat activity as plain AG-UI events, without a send queue.
48
+ * Core starts this collector only after authentication and channel join.
49
+ */
50
+ export function createTrajectoryCollector(
51
+ options: TrajectoryCaptureOptions & {
52
+ send: (event: TrajectoryEvent) => void;
53
+ },
54
+ ): TrajectoryCollector {
55
+ let active = false;
56
+ let uninstalls: (() => void)[] = [];
57
+
58
+ const report = (code: string, message: string) => {
59
+ try {
60
+ options.onError?.({ code, message });
61
+ } catch {
62
+ // Diagnostic callbacks must not interrupt the host app.
63
+ }
64
+ };
65
+
66
+ const getRoute = () => toRoute(location.pathname);
67
+
68
+ const record = (name: string, value: JsonValue) => {
69
+ if (!active) return;
70
+ let payload: TrajectoryEvent;
71
+ try {
72
+ const event: TrajectoryEvent = {
73
+ type: "CUSTOM",
74
+ name,
75
+ timestamp: Date.now(),
76
+ value,
77
+ };
78
+ const kept =
79
+ options.beforeSend === undefined ? event : options.beforeSend(event);
80
+ if (kept === null || !active) return;
81
+ if (
82
+ kept.type !== "CUSTOM" ||
83
+ typeof kept.name !== "string" ||
84
+ !kept.name.trim() ||
85
+ kept.name.length > 200 ||
86
+ kept.name.includes("\0") ||
87
+ !Number.isFinite(kept.timestamp) ||
88
+ kept.timestamp <= 0 ||
89
+ kept.timestamp >= 253_402_300_800_000 ||
90
+ !isJson(kept.value)
91
+ ) {
92
+ report(
93
+ "INVALID_EVENT",
94
+ "The event must contain a name, timestamp, and JSON value.",
95
+ );
96
+ return;
97
+ }
98
+ // Keep only the contract envelope and snapshot the value before delivery.
99
+ const serialized = JSON.stringify({
100
+ type: kept.type,
101
+ name: kept.name,
102
+ timestamp: kept.timestamp,
103
+ value: kept.value,
104
+ });
105
+ if (
106
+ new TextEncoder().encode(serialized).byteLength >
107
+ MAX_TRAJECTORY_EVENT_BYTES
108
+ ) {
109
+ report("EVENT_TOO_LARGE", "The serialized event exceeds 16 KiB.");
110
+ return;
111
+ }
112
+ payload = JSON.parse(serialized) as TrajectoryEvent;
113
+ } catch {
114
+ report("INVALID_EVENT", "The event could not be serialized as JSON.");
115
+ return;
116
+ }
117
+ try {
118
+ options.send(payload);
119
+ } catch {
120
+ report(
121
+ "PERSISTENCE_UNKNOWN",
122
+ "Event delivery failed. The event will not be retried.",
123
+ );
124
+ }
125
+ };
126
+
127
+ const capture = (name: string, value: Record<string, unknown>) => {
128
+ // Shared capture modules produce JSON; record validates and snapshots before sending.
129
+ record(name, value as JsonValue);
130
+ };
131
+
132
+ const stop = () => {
133
+ active = false;
134
+ const installed = uninstalls;
135
+ uninstalls = [];
136
+ for (const uninstall of installed) uninstall();
137
+ };
138
+
139
+ return {
140
+ start() {
141
+ if (active || typeof window === "undefined") return;
142
+ active = true;
143
+ // Native callers can emit developer events without browser observers.
144
+ if (typeof window.addEventListener !== "function") return;
145
+ // One per session: fields seen and values typed are forgotten on stop().
146
+ const redact = createRedactor();
147
+ try {
148
+ uninstalls.push(redact.watch());
149
+ if (options.capture?.clicks !== false) {
150
+ uninstalls.push(
151
+ installClickCapture({ emit: capture, getRoute, redact }),
152
+ );
153
+ }
154
+ if (options.capture?.navigation !== false) {
155
+ uninstalls.push(installNavigationCapture({ emit: capture, redact }));
156
+ }
157
+ if (options.capture?.inputs !== false) {
158
+ uninstalls.push(installInputCapture({ emit: capture, redact }));
159
+ }
160
+ if (options.capture?.network !== false) {
161
+ uninstalls.push(
162
+ installNetworkCapture({
163
+ emit: capture,
164
+ ignoreUrls: options.ignoreUrls ?? [],
165
+ redact,
166
+ }),
167
+ );
168
+ }
169
+ record("page", {
170
+ route: getRoute(),
171
+ url: redact.url(location.href),
172
+ title: document.title,
173
+ referrer: redact.url(document.referrer),
174
+ });
175
+ } catch {
176
+ stop();
177
+ report("CAPTURE_FAILED", "Browser capture could not start.");
178
+ }
179
+ },
180
+ stop,
181
+ emit(name, value) {
182
+ if (!active) return;
183
+ if (RESERVED_NAMES.has(name)) {
184
+ report(
185
+ "INVALID_EVENT",
186
+ "Use a developer event name, not a reserved capture name.",
187
+ );
188
+ return;
189
+ }
190
+ record(name, value);
191
+ },
192
+ };
193
+ }
@@ -0,0 +1,50 @@
1
+ import type { CaptureOptions } from "./types";
2
+
3
+ /** JSON values accepted by developer events. */
4
+ export type JsonValue =
5
+ | null
6
+ | boolean
7
+ | number
8
+ | string
9
+ | JsonValue[]
10
+ | { [key: string]: JsonValue };
11
+
12
+ /** One captured event. Core adds sequence metadata when it creates Gateway batches. */
13
+ export interface TrajectoryEvent<V = JsonValue> {
14
+ type: "CUSTOM";
15
+ name: string;
16
+ timestamp: number;
17
+ value: V;
18
+ }
19
+
20
+ export type StartResult =
21
+ | { status: "started"; trajectoryId: string }
22
+ | { status: "error"; code: string };
23
+
24
+ export interface ConnectionGrant {
25
+ joinToken: string;
26
+ realtime: { clientUrl: string; topic: string };
27
+ }
28
+
29
+ export interface TrajectoryError {
30
+ code: string;
31
+ message: string;
32
+ }
33
+
34
+ export interface TrajectoryCaptureOptions {
35
+ /** @deprecated Capture retains raw URLs; route templates no longer transform events. */
36
+ routes?: string[];
37
+ capture?: CaptureOptions;
38
+ /** Explicit app exclusions, plus Core transport URLs to prevent self-capture. */
39
+ ignoreUrls?: (string | RegExp)[];
40
+ /** Runs in the browser before validation and delivery. Return null to discard. */
41
+ beforeSend?: (event: TrajectoryEvent) => TrajectoryEvent | null;
42
+ /** Reports rejected events or delivery failures without interrupting the app. */
43
+ onError?: (error: TrajectoryError) => void;
44
+ }
45
+
46
+ export interface TrajectoryCollector {
47
+ start(): void;
48
+ stop(): void;
49
+ emit(name: string, value: JsonValue): void;
50
+ }
package/src/types.ts ADDED
@@ -0,0 +1,98 @@
1
+ /** Event names the collector and CopilotKit Core emit. `emit()` rejects them. */
2
+ export const BUILT_IN_EVENT_NAMES = [
3
+ "page",
4
+ "navigation",
5
+ "click",
6
+ "input",
7
+ "network",
8
+ "thread.linked",
9
+ "agent.run",
10
+ "agent.message",
11
+ "tool.call",
12
+ ] as const;
13
+
14
+ export type BuiltInEventName = (typeof BUILT_IN_EVENT_NAMES)[number];
15
+
16
+ /** Replaces values that never leave the browser, even with full capture: passwords and credentials. */
17
+ export const REDACTED = "[redacted]";
18
+
19
+ /** One captured interaction, shaped as an AG-UI `CUSTOM` event. */
20
+ export interface LearningEvent<V = Record<string, unknown>> {
21
+ type: "CUSTOM";
22
+ name: string;
23
+ value: V;
24
+ timestamp: number;
25
+ }
26
+
27
+ /** What a sink receives. The Trajectory scope sits on the batch, not on each event. */
28
+ export interface LearningBatch {
29
+ trajectoryId: string;
30
+ learningContainerIds?: string[];
31
+ events: LearningEvent[];
32
+ /** Events lost since the previous batch (queue overflow or a failed send). */
33
+ dropped: number;
34
+ }
35
+
36
+ export interface SinkSendOptions {
37
+ /** True when the page is going away; use a transport that survives unload. */
38
+ beacon?: boolean;
39
+ }
40
+
41
+ /** Receives batches. `url`, when set, is never captured as a network event. */
42
+ export type LearningSink = ((
43
+ batch: LearningBatch,
44
+ options?: SinkSendOptions,
45
+ ) => void | Promise<void>) & { readonly url?: string };
46
+
47
+ export interface ClickTarget {
48
+ tag: string;
49
+ role: string | null;
50
+ action: string | null;
51
+ text: string;
52
+ attributes: Record<string, string>;
53
+ value?: string;
54
+ checked?: boolean;
55
+ selectedValues?: string[];
56
+ files?: { name: string; type: string; size: number; lastModified: number }[];
57
+ input: "pointer" | "keyboard";
58
+ }
59
+
60
+ /** Adds fields to a click event, from the element the user clicked. */
61
+ export type EnrichFn = (target: Element) => Record<string, unknown> | undefined;
62
+
63
+ export type Emit = (name: string, value: Record<string, unknown>) => void;
64
+
65
+ export interface CaptureOptions {
66
+ clicks?: boolean;
67
+ navigation?: boolean;
68
+ network?: boolean;
69
+ inputs?: boolean;
70
+ }
71
+
72
+ export interface CollectorOptions {
73
+ sink: LearningSink;
74
+ /** @deprecated Capture retains raw URLs; route templates no longer transform events. */
75
+ routes?: string[];
76
+ /** URLs (prefix match on the absolute URL) or patterns that network capture skips. */
77
+ ignoreUrls?: (string | RegExp)[];
78
+ /** Turns capture modules on or off. All are on by default. */
79
+ capture?: CaptureOptions;
80
+ /** Runs last, in the browser. Return the event to send, or `null` to drop it. */
81
+ beforeSend?: (event: LearningEvent) => LearningEvent | null;
82
+ enrich?: EnrichFn;
83
+ }
84
+
85
+ export interface StartOptions {
86
+ trajectoryId: string;
87
+ learningContainerIds?: string[];
88
+ }
89
+
90
+ export interface Collector {
91
+ start(options: StartOptions): void;
92
+ stop(): void;
93
+ /** Developer events. Built-in names warn and are ignored. */
94
+ emit(name: string, value: Record<string, unknown>): void;
95
+ /** @internal For @copilotkit/core only: emits built-in names. */
96
+ ɵemit(name: BuiltInEventName, value: Record<string, unknown>): void;
97
+ readonly trajectoryId: string | null;
98
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "@copilotkit/typescript-config/base.json",
3
+ "compilerOptions": {
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ // Browser-only package: no ambient @types from ancestor node_modules.
7
+ "types": [],
8
+ "outDir": "dist",
9
+ "rootDir": "src",
10
+ "declaration": true,
11
+ "declarationMap": true,
12
+ "sourceMap": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["dist", "node_modules"]
16
+ }
@@ -0,0 +1,11 @@
1
+ import { defineConfig } from "tsdown";
2
+
3
+ export default defineConfig({
4
+ entry: ["src/index.ts"],
5
+ format: ["esm", "cjs"],
6
+ dts: true,
7
+ sourcemap: true,
8
+ target: "es2022",
9
+ outDir: "dist",
10
+ exports: true,
11
+ });
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "./src/__tests__/vitest-environment.mjs",
6
+ include: ["src/**/__tests__/**/*.test.ts"],
7
+ reporters: [["default", { summary: false }]],
8
+ },
9
+ });