@dropout-ai/runtime 0.2.14 → 0.3.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/package.json +1 -1
- package/src/index.js +95 -178
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -2,102 +2,48 @@
|
|
|
2
2
|
* @dropout-ai/runtime
|
|
3
3
|
* Role: Passive observer.
|
|
4
4
|
* Behavior: Fire-and-forget raw JSON to Supabase.
|
|
5
|
-
* Authentication:
|
|
5
|
+
* Authentication: API Key + Project ID.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const crypto = require('crypto');
|
|
9
9
|
|
|
10
|
-
// --- CONFIGURATION ---
|
|
10
|
+
// --- DEFAULT CONFIGURATION ---
|
|
11
11
|
const SUPABASE_FUNCTION_URL = "https://hipughmjlwmwjxzyxfzs.supabase.co/functions/v1/capture-sealed";
|
|
12
12
|
|
|
13
|
-
//
|
|
13
|
+
// Global State
|
|
14
14
|
const GLOBAL_OBJ = typeof window !== 'undefined' ? window : global;
|
|
15
|
+
let activeConfig = {
|
|
16
|
+
projectId: null,
|
|
17
|
+
apiKey: null,
|
|
18
|
+
captureEndpoint: SUPABASE_FUNCTION_URL,
|
|
19
|
+
maxOutputBytes: 32768,
|
|
20
|
+
privacyMode: (typeof process !== 'undefined' && process.env.DROPOUT_PRIVACY_MODE) || 'full'
|
|
21
|
+
};
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
return crypto.randomUUID();
|
|
19
|
-
} catch (e) {
|
|
20
|
-
return 'sess_' + Math.random().toString(36).substring(2, 12) + Date.now().toString(36);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (!GLOBAL_OBJ.__dropout_session_id__) {
|
|
25
|
-
GLOBAL_OBJ.__dropout_session_id__ = generateSessionId();
|
|
26
|
-
}
|
|
27
|
-
|
|
23
|
+
let isPatched = false;
|
|
28
24
|
let turnIndex = 0;
|
|
29
25
|
let lastTurnConfirmed = true;
|
|
30
26
|
let lastPromptHash = null;
|
|
31
27
|
let lastResponseHash = null;
|
|
28
|
+
let instance = null;
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
maxOutputBytes: 32768,
|
|
35
|
-
captureEndpoint: SUPABASE_FUNCTION_URL,
|
|
36
|
-
privacyMode: (typeof process !== 'undefined' && process.env.DROPOUT_PRIVACY_MODE) || 'full'
|
|
37
|
-
};
|
|
30
|
+
// --- UTILS ---
|
|
38
31
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
setTimeout(() => {
|
|
47
|
-
fetchFn(config.captureEndpoint, {
|
|
48
|
-
method: 'POST',
|
|
49
|
-
headers: {
|
|
50
|
-
'Content-Type': 'application/json'
|
|
51
|
-
},
|
|
52
|
-
body: JSON.stringify(payload),
|
|
53
|
-
keepalive: true
|
|
54
|
-
}).catch(() => { });
|
|
55
|
-
}, 0);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// --- Lifecycle Signals ---
|
|
59
|
-
|
|
60
|
-
function emitSessionEnd(reason) {
|
|
61
|
-
if (GLOBAL_OBJ.__dropout_session_ended__) return;
|
|
62
|
-
GLOBAL_OBJ.__dropout_session_ended__ = true;
|
|
63
|
-
|
|
64
|
-
emit({
|
|
65
|
-
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
66
|
-
turn_index: turnIndex,
|
|
67
|
-
direction: 'meta',
|
|
68
|
-
turn_role: 'system',
|
|
69
|
-
metadata_flags: {
|
|
70
|
-
session_end: true,
|
|
71
|
-
end_reason: reason
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Browser: Navigation/Reload
|
|
77
|
-
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
78
|
-
window.addEventListener('beforeunload', () => emitSessionEnd('navigation'));
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Node.js: Process Exit
|
|
82
|
-
if (typeof process !== 'undefined' && process.on) {
|
|
83
|
-
process.on('exit', () => emitSessionEnd('process_exit'));
|
|
84
|
-
process.on('SIGINT', () => { emitSessionEnd('sigint'); process.exit(); });
|
|
85
|
-
process.on('SIGTERM', () => { emitSessionEnd('sigterm'); process.exit(); });
|
|
32
|
+
function generateSessionId() {
|
|
33
|
+
try {
|
|
34
|
+
return crypto.randomUUID();
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return 'sess_' + Math.random().toString(36).substring(2, 12) + Date.now().toString(36);
|
|
37
|
+
}
|
|
86
38
|
}
|
|
87
39
|
|
|
88
|
-
// --- Content Utilities ---
|
|
89
|
-
|
|
90
40
|
function hash(text) {
|
|
91
41
|
if (!text) return null;
|
|
92
42
|
try {
|
|
93
43
|
return crypto.createHash('sha256').update(text.toLowerCase().trim()).digest('hex');
|
|
94
|
-
} catch (e) {
|
|
95
|
-
return 'hash_err';
|
|
96
|
-
}
|
|
44
|
+
} catch (e) { return 'hash_err'; }
|
|
97
45
|
}
|
|
98
46
|
|
|
99
|
-
// --- Provider Normalization ---
|
|
100
|
-
|
|
101
47
|
function normalize(url, body) {
|
|
102
48
|
let provider = 'unknown';
|
|
103
49
|
let model = 'unknown';
|
|
@@ -120,20 +66,49 @@ function normalize(url, body) {
|
|
|
120
66
|
}
|
|
121
67
|
} catch (e) { }
|
|
122
68
|
}
|
|
123
|
-
|
|
124
69
|
return { provider, model };
|
|
125
70
|
}
|
|
126
71
|
|
|
127
|
-
// ---
|
|
72
|
+
// --- EMITTER (Authenticated) ---
|
|
73
|
+
|
|
74
|
+
function emit(payload) {
|
|
75
|
+
// 1. Guard: Do not emit if not initialized
|
|
76
|
+
if (!activeConfig.apiKey || !activeConfig.projectId) return;
|
|
77
|
+
|
|
78
|
+
const fetchFn = GLOBAL_OBJ.__dropout_original_fetch__ || GLOBAL_OBJ.fetch;
|
|
79
|
+
if (typeof fetchFn !== 'function') return;
|
|
80
|
+
|
|
81
|
+
// 2. Attach Project ID to payload
|
|
82
|
+
const finalPayload = {
|
|
83
|
+
...payload,
|
|
84
|
+
project_id: activeConfig.projectId
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
fetchFn(activeConfig.captureEndpoint, {
|
|
89
|
+
method: 'POST',
|
|
90
|
+
headers: {
|
|
91
|
+
'Content-Type': 'application/json',
|
|
92
|
+
'x-dropout-key': activeConfig.apiKey // <--- AUTH HEADER
|
|
93
|
+
},
|
|
94
|
+
body: JSON.stringify(finalPayload),
|
|
95
|
+
keepalive: true
|
|
96
|
+
}).catch(() => { /* Silent Fail */ });
|
|
97
|
+
}, 0);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// --- MONKEY PATCH ---
|
|
101
|
+
|
|
102
|
+
function applyPatch() {
|
|
103
|
+
if (isPatched || !GLOBAL_OBJ.fetch) return;
|
|
128
104
|
|
|
129
|
-
if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patched__) {
|
|
130
105
|
GLOBAL_OBJ.__dropout_original_fetch__ = GLOBAL_OBJ.fetch;
|
|
131
106
|
|
|
132
107
|
GLOBAL_OBJ.fetch = async function (input, init) {
|
|
133
108
|
const url = typeof input === 'string' ? input : (input && input.url);
|
|
134
109
|
|
|
135
|
-
//
|
|
136
|
-
if (url && url.includes(
|
|
110
|
+
// Guard: Don't track our own calls
|
|
111
|
+
if (url && url.includes(activeConfig.captureEndpoint)) {
|
|
137
112
|
return GLOBAL_OBJ.__dropout_original_fetch__(input, init);
|
|
138
113
|
}
|
|
139
114
|
|
|
@@ -148,8 +123,6 @@ if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patche
|
|
|
148
123
|
if (!isAI) return GLOBAL_OBJ.__dropout_original_fetch__(input, init);
|
|
149
124
|
|
|
150
125
|
const start = Date.now();
|
|
151
|
-
|
|
152
|
-
// --- Explicit Turn Increment Rule ---
|
|
153
126
|
let activeTurn;
|
|
154
127
|
if (lastTurnConfirmed) {
|
|
155
128
|
activeTurn = turnIndex++;
|
|
@@ -160,56 +133,47 @@ if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patche
|
|
|
160
133
|
|
|
161
134
|
const { provider, model } = normalize(url, init && init.body);
|
|
162
135
|
|
|
163
|
-
//
|
|
136
|
+
// Emit Request
|
|
164
137
|
let pText = "";
|
|
165
138
|
if (init && init.body) {
|
|
166
139
|
try { pText = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
|
|
167
140
|
}
|
|
168
141
|
const pHash = hash(pText);
|
|
169
|
-
const isRetry = pHash && pHash === lastPromptHash;
|
|
170
142
|
|
|
171
|
-
|
|
143
|
+
emit({
|
|
172
144
|
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
173
145
|
turn_index: activeTurn,
|
|
174
146
|
direction: 'user_to_ai',
|
|
175
147
|
turn_role: 'user',
|
|
176
148
|
provider,
|
|
177
149
|
model,
|
|
178
|
-
|
|
179
|
-
content: config.privacyMode === 'full' ? pText : null,
|
|
150
|
+
content: activeConfig.privacyMode === 'full' ? pText : null,
|
|
180
151
|
content_hash: pHash,
|
|
181
|
-
metadata_flags: {
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
emit(requestEvent);
|
|
152
|
+
metadata_flags: { retry_like: pHash === lastPromptHash ? 1 : 0 }
|
|
153
|
+
});
|
|
187
154
|
lastPromptHash = pHash;
|
|
188
155
|
|
|
189
|
-
//
|
|
156
|
+
// Actual Call
|
|
190
157
|
let response;
|
|
191
158
|
let oText = "";
|
|
192
159
|
try {
|
|
193
160
|
response = await GLOBAL_OBJ.__dropout_original_fetch__(input, init);
|
|
194
|
-
} catch (err) {
|
|
195
|
-
throw err;
|
|
196
|
-
}
|
|
161
|
+
} catch (err) { throw err; }
|
|
197
162
|
|
|
198
163
|
const latency = Date.now() - start;
|
|
199
164
|
|
|
200
|
-
//
|
|
165
|
+
// Emit Response
|
|
201
166
|
try {
|
|
202
167
|
const cloned = response.clone();
|
|
203
168
|
oText = await cloned.text();
|
|
204
|
-
if (oText && oText.length >
|
|
205
|
-
oText = oText.slice(0,
|
|
169
|
+
if (oText && oText.length > activeConfig.maxOutputBytes) {
|
|
170
|
+
oText = oText.slice(0, activeConfig.maxOutputBytes);
|
|
206
171
|
}
|
|
207
172
|
} catch (e) { }
|
|
208
173
|
|
|
209
174
|
const oHash = hash(oText);
|
|
210
|
-
const isNonAdaptive = oHash && oHash === lastResponseHash;
|
|
211
175
|
|
|
212
|
-
|
|
176
|
+
emit({
|
|
213
177
|
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
214
178
|
turn_index: activeTurn,
|
|
215
179
|
direction: 'ai_to_user',
|
|
@@ -217,99 +181,52 @@ if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patche
|
|
|
217
181
|
latency_ms: latency,
|
|
218
182
|
provider,
|
|
219
183
|
model,
|
|
220
|
-
|
|
221
|
-
content: config.privacyMode === 'full' ? oText : null,
|
|
184
|
+
content: activeConfig.privacyMode === 'full' ? oText : null,
|
|
222
185
|
content_hash: oHash,
|
|
223
186
|
metadata_flags: {
|
|
224
|
-
non_adaptive_response:
|
|
187
|
+
non_adaptive_response: oHash === lastResponseHash ? 1 : 0,
|
|
225
188
|
turn_boundary_confirmed: 1
|
|
226
189
|
}
|
|
227
|
-
};
|
|
190
|
+
});
|
|
228
191
|
|
|
229
|
-
emit(responseEvent);
|
|
230
192
|
lastResponseHash = oHash;
|
|
231
193
|
lastTurnConfirmed = true;
|
|
232
|
-
|
|
233
194
|
return response;
|
|
234
195
|
};
|
|
235
196
|
|
|
236
197
|
GLOBAL_OBJ.fetch.__dropout_patched__ = true;
|
|
198
|
+
isPatched = true;
|
|
237
199
|
}
|
|
238
200
|
|
|
239
|
-
|
|
240
|
-
* Manual capture for framework-level integration
|
|
241
|
-
*/
|
|
242
|
-
async function capture(target, options = {}) {
|
|
243
|
-
const start = Date.now();
|
|
244
|
-
|
|
245
|
-
let activeTurn;
|
|
246
|
-
if (lastTurnConfirmed) {
|
|
247
|
-
activeTurn = turnIndex++;
|
|
248
|
-
lastTurnConfirmed = false;
|
|
249
|
-
} else {
|
|
250
|
-
activeTurn = turnIndex > 0 ? turnIndex - 1 : 0;
|
|
251
|
-
}
|
|
201
|
+
// --- MAIN CLASS EXPORT ---
|
|
252
202
|
|
|
253
|
-
|
|
203
|
+
class Dropout {
|
|
204
|
+
constructor(config = {}) {
|
|
205
|
+
if (instance) {
|
|
206
|
+
return instance;
|
|
207
|
+
}
|
|
254
208
|
|
|
255
|
-
|
|
209
|
+
if (!config.apiKey || !config.projectId) {
|
|
210
|
+
console.warn("[Dropout] Missing apiKey or projectId. Tracking disabled.");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
256
213
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
latency_ms = Date.now() - start;
|
|
262
|
-
} else {
|
|
263
|
-
prompt = target.prompt;
|
|
264
|
-
output = target.output;
|
|
265
|
-
}
|
|
214
|
+
// Update Global Configuration
|
|
215
|
+
activeConfig.apiKey = config.apiKey;
|
|
216
|
+
activeConfig.projectId = config.projectId;
|
|
217
|
+
if (config.privacyMode) activeConfig.privacyMode = config.privacyMode;
|
|
266
218
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
emit({
|
|
272
|
-
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
273
|
-
turn_index: activeTurn,
|
|
274
|
-
direction: 'user_to_ai',
|
|
275
|
-
turn_role: 'user',
|
|
276
|
-
provider: options.provider || 'manual',
|
|
277
|
-
model: options.model || 'unknown',
|
|
278
|
-
content: mode === 'full' ? prompt : null, // FIX: content_raw -> content
|
|
279
|
-
content_hash: pHash,
|
|
280
|
-
metadata_flags: { retry_like: pHash === lastPromptHash ? 1 : 0 }
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
// Emit Response
|
|
284
|
-
emit({
|
|
285
|
-
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
286
|
-
turn_index: activeTurn,
|
|
287
|
-
direction: 'ai_to_user',
|
|
288
|
-
turn_role: 'assistant',
|
|
289
|
-
latency_ms,
|
|
290
|
-
provider: options.provider || 'manual',
|
|
291
|
-
model: options.model || 'unknown',
|
|
292
|
-
content: mode === 'full' ? output : null, // FIX: content_raw -> content
|
|
293
|
-
content_hash: oHash,
|
|
294
|
-
metadata_flags: { non_adaptive_response: oHash === lastResponseHash ? 1 : 0, turn_boundary_confirmed: 1 }
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
lastPromptHash = pHash;
|
|
298
|
-
lastResponseHash = oHash;
|
|
299
|
-
lastTurnConfirmed = true;
|
|
300
|
-
|
|
301
|
-
return output;
|
|
302
|
-
}
|
|
219
|
+
// Initialize Identity
|
|
220
|
+
if (!GLOBAL_OBJ.__dropout_session_id__) {
|
|
221
|
+
GLOBAL_OBJ.__dropout_session_id__ = generateSessionId();
|
|
222
|
+
}
|
|
303
223
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
GLOBAL_OBJ.__dropout_session_ended__ = false;
|
|
310
|
-
turnIndex = 0;
|
|
311
|
-
lastTurnConfirmed = true;
|
|
312
|
-
lastPromptHash = null;
|
|
313
|
-
lastResponseHash = null;
|
|
224
|
+
// Apply the patch
|
|
225
|
+
applyPatch();
|
|
226
|
+
|
|
227
|
+
console.log("[Dropout] Initialized for project:", config.projectId);
|
|
228
|
+
instance = this;
|
|
314
229
|
}
|
|
315
|
-
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
module.exports = Dropout;
|