@dropout-ai/runtime 0.2.5 → 0.2.7
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 +182 -134
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
* Role: Passive observer inside the user’s app.
|
|
4
4
|
*
|
|
5
5
|
* Runtime never reasons, never interprets, never stores intelligence.
|
|
6
|
-
*
|
|
6
|
+
* it only: Observes, Normalizes, Emits signals.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
|
|
11
|
-
// ---
|
|
11
|
+
// --- Identity & State ---
|
|
12
12
|
const GLOBAL_OBJ = typeof window !== 'undefined' ? window : global;
|
|
13
13
|
|
|
14
14
|
function generateSessionId() {
|
|
@@ -19,15 +19,16 @@ function generateSessionId() {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
// One session = one continuous user attempt
|
|
23
|
-
// Resets on page reload (Browser) or process restart (Node)
|
|
24
22
|
if (!GLOBAL_OBJ.__dropout_session_id__) {
|
|
25
23
|
GLOBAL_OBJ.__dropout_session_id__ = generateSessionId();
|
|
26
24
|
}
|
|
27
25
|
|
|
28
26
|
let turnIndex = 0;
|
|
27
|
+
let lastTurnConfirmed = true;
|
|
28
|
+
let lastPromptHash = null;
|
|
29
|
+
let lastResponseHash = null;
|
|
29
30
|
|
|
30
|
-
// ---
|
|
31
|
+
// --- Runtime Guarantees ---
|
|
31
32
|
let config = {
|
|
32
33
|
maxOutputBytes: 32768,
|
|
33
34
|
captureEndpoint: 'http://localhost:4000/capture',
|
|
@@ -35,7 +36,7 @@ let config = {
|
|
|
35
36
|
privacyMode: (typeof process !== 'undefined' && process.env.DROPOUT_PRIVACY_MODE) || 'safe'
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
//
|
|
39
|
+
// Remote config fetch (Non-blocking)
|
|
39
40
|
setTimeout(async () => {
|
|
40
41
|
const fetchFn = GLOBAL_OBJ.__dropout_original_fetch__ || GLOBAL_OBJ.fetch;
|
|
41
42
|
if (typeof fetchFn !== 'function') return;
|
|
@@ -65,9 +66,42 @@ function emit(payload) {
|
|
|
65
66
|
}, 0);
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
// ---
|
|
69
|
+
// --- Lifecycle Signals ---
|
|
69
70
|
|
|
70
|
-
function
|
|
71
|
+
function emitSessionEnd(reason) {
|
|
72
|
+
if (GLOBAL_OBJ.__dropout_session_ended__) return;
|
|
73
|
+
GLOBAL_OBJ.__dropout_session_ended__ = true;
|
|
74
|
+
|
|
75
|
+
emit({
|
|
76
|
+
identity: {
|
|
77
|
+
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
78
|
+
turn_index: turnIndex,
|
|
79
|
+
direction: 'meta',
|
|
80
|
+
turn_role: 'system'
|
|
81
|
+
},
|
|
82
|
+
timing: { created_at: Date.now() },
|
|
83
|
+
metadata_flags: {
|
|
84
|
+
session_end: true,
|
|
85
|
+
end_reason: reason
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Browser: Navigation/Reload
|
|
91
|
+
if (typeof window !== 'undefined' && window.addEventListener) {
|
|
92
|
+
window.addEventListener('beforeunload', () => emitSessionEnd('navigation'));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Node.js: Process Exit
|
|
96
|
+
if (typeof process !== 'undefined' && process.on) {
|
|
97
|
+
process.on('exit', () => emitSessionEnd('process_exit'));
|
|
98
|
+
process.on('SIGINT', () => { emitSessionEnd('sigint'); process.exit(); });
|
|
99
|
+
process.on('SIGTERM', () => { emitSessionEnd('sigterm'); process.exit(); });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- Content Utilities ---
|
|
103
|
+
|
|
104
|
+
function hash(text) {
|
|
71
105
|
if (!text) return null;
|
|
72
106
|
try {
|
|
73
107
|
return crypto.createHash('sha256').update(text.toLowerCase().trim()).digest('hex');
|
|
@@ -76,34 +110,7 @@ function getSemanticHash(text) {
|
|
|
76
110
|
}
|
|
77
111
|
}
|
|
78
112
|
|
|
79
|
-
|
|
80
|
-
if (!text) return null;
|
|
81
|
-
return {
|
|
82
|
-
chars: text.length,
|
|
83
|
-
words: text.split(/\s+/).length,
|
|
84
|
-
lines: text.split('\n').length,
|
|
85
|
-
has_code: /```/.test(text),
|
|
86
|
-
has_list: /^\s*[-*•\d+.]/m.test(text)
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const PATTERNS = {
|
|
91
|
-
negative: ["wrong", "bad", "not helpful", "incorrect", "stupid", "error", "worst"],
|
|
92
|
-
positive: ["thanks", "good", "perfect", "correct", "great", "helpful"],
|
|
93
|
-
struggle: ["but", "why", "stop", "don't", "no", "again", "explain"]
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
function getFlags(text) {
|
|
97
|
-
if (!text) return [];
|
|
98
|
-
const t = text.toLowerCase();
|
|
99
|
-
const flags = [];
|
|
100
|
-
if (PATTERNS.negative.some(p => t.includes(p))) flags.push('neg');
|
|
101
|
-
if (PATTERNS.positive.some(p => t.includes(p))) flags.push('pos');
|
|
102
|
-
if (PATTERNS.struggle.some(p => t.includes(p))) flags.push('clash');
|
|
103
|
-
return flags;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// --- B. Event Capture (Provider-agnostic) ---
|
|
113
|
+
// --- Provider Normalization ---
|
|
107
114
|
|
|
108
115
|
function normalize(url, body) {
|
|
109
116
|
let provider = 'unknown';
|
|
@@ -131,16 +138,13 @@ function normalize(url, body) {
|
|
|
131
138
|
return { provider, model };
|
|
132
139
|
}
|
|
133
140
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
*/
|
|
141
|
+
// --- The Monkey Patch ---
|
|
142
|
+
|
|
137
143
|
if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patched__) {
|
|
138
144
|
GLOBAL_OBJ.__dropout_original_fetch__ = GLOBAL_OBJ.fetch;
|
|
139
145
|
|
|
140
146
|
GLOBAL_OBJ.fetch = async function (input, init) {
|
|
141
|
-
const start = Date.now();
|
|
142
147
|
const url = typeof input === 'string' ? input : (input && input.url);
|
|
143
|
-
|
|
144
148
|
const isAI = url && (
|
|
145
149
|
url.includes('openai.com') ||
|
|
146
150
|
url.includes('anthropic.com') ||
|
|
@@ -149,50 +153,107 @@ if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patche
|
|
|
149
153
|
(init && init.body && (init.body.includes('"model"') || init.body.includes('"messages"')))
|
|
150
154
|
);
|
|
151
155
|
|
|
152
|
-
|
|
156
|
+
if (!isAI) return GLOBAL_OBJ.__dropout_original_fetch__(input, init);
|
|
153
157
|
|
|
154
|
-
|
|
155
|
-
const latency = Date.now() - start;
|
|
156
|
-
const turn = turnIndex++;
|
|
157
|
-
const { provider, model } = normalize(url, init && init.body);
|
|
158
|
+
const start = Date.now();
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
// --- Explicit Turn Increment Rule ---
|
|
161
|
+
let activeTurn;
|
|
162
|
+
if (lastTurnConfirmed) {
|
|
163
|
+
activeTurn = turnIndex++;
|
|
164
|
+
lastTurnConfirmed = false;
|
|
165
|
+
} else {
|
|
166
|
+
activeTurn = turnIndex > 0 ? turnIndex - 1 : 0;
|
|
167
|
+
}
|
|
163
168
|
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
const cloned = response.clone();
|
|
167
|
-
oText = await cloned.text();
|
|
168
|
-
if (oText && oText.length > config.maxOutputBytes) {
|
|
169
|
-
oText = oText.slice(0, config.maxOutputBytes);
|
|
170
|
-
}
|
|
171
|
-
} catch (e) { }
|
|
169
|
+
const { provider, model } = normalize(url, init && init.body);
|
|
172
170
|
|
|
173
|
-
|
|
171
|
+
// --- 1. Emit Request Event ---
|
|
172
|
+
let pText = "";
|
|
173
|
+
if (init && init.body) {
|
|
174
|
+
try { pText = typeof init.body === 'string' ? init.body : JSON.stringify(init.body); } catch (e) { }
|
|
175
|
+
}
|
|
176
|
+
const pHash = hash(pText);
|
|
177
|
+
const isRetry = pHash && pHash === lastPromptHash;
|
|
178
|
+
|
|
179
|
+
const requestEvent = {
|
|
180
|
+
identity: {
|
|
174
181
|
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
182
|
+
turn_index: activeTurn,
|
|
183
|
+
direction: 'request',
|
|
184
|
+
turn_role: 'user'
|
|
185
|
+
},
|
|
186
|
+
timing: {
|
|
187
|
+
created_at: Date.now()
|
|
188
|
+
},
|
|
189
|
+
provider_context: {
|
|
178
190
|
provider,
|
|
179
|
-
model
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
payload.prompt_hash = getSemanticHash(pText);
|
|
188
|
-
payload.output_hash = getSemanticHash(oText);
|
|
189
|
-
payload.markers = { p: getMarkers(pText), o: getMarkers(oText) };
|
|
190
|
-
payload.flags = getFlags(pText + " " + oText);
|
|
191
|
+
model
|
|
192
|
+
},
|
|
193
|
+
content: {
|
|
194
|
+
content_raw: config.privacyMode === 'full' ? pText : null,
|
|
195
|
+
content_hash: pHash
|
|
196
|
+
},
|
|
197
|
+
metadata_flags: {
|
|
198
|
+
retry_like: isRetry ? 1 : 0
|
|
191
199
|
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
emit(requestEvent);
|
|
203
|
+
lastPromptHash = pHash;
|
|
192
204
|
|
|
193
|
-
|
|
205
|
+
// Execute actual fetch
|
|
206
|
+
let response;
|
|
207
|
+
let oText = "";
|
|
208
|
+
try {
|
|
209
|
+
response = await GLOBAL_OBJ.__dropout_original_fetch__(input, init);
|
|
210
|
+
} catch (err) {
|
|
211
|
+
throw err;
|
|
194
212
|
}
|
|
195
213
|
|
|
214
|
+
const latency = Date.now() - start;
|
|
215
|
+
|
|
216
|
+
// --- 2. Emit Response Event ---
|
|
217
|
+
try {
|
|
218
|
+
const cloned = response.clone();
|
|
219
|
+
oText = await cloned.text();
|
|
220
|
+
if (oText && oText.length > config.maxOutputBytes) {
|
|
221
|
+
oText = oText.slice(0, config.maxOutputBytes);
|
|
222
|
+
}
|
|
223
|
+
} catch (e) { }
|
|
224
|
+
|
|
225
|
+
const oHash = hash(oText);
|
|
226
|
+
const isNonAdaptive = oHash && oHash === lastResponseHash;
|
|
227
|
+
|
|
228
|
+
const responseEvent = {
|
|
229
|
+
identity: {
|
|
230
|
+
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
231
|
+
turn_index: activeTurn,
|
|
232
|
+
direction: 'response',
|
|
233
|
+
turn_role: 'assistant'
|
|
234
|
+
},
|
|
235
|
+
timing: {
|
|
236
|
+
created_at: Date.now(),
|
|
237
|
+
latency_ms: latency
|
|
238
|
+
},
|
|
239
|
+
provider_context: {
|
|
240
|
+
provider,
|
|
241
|
+
model
|
|
242
|
+
},
|
|
243
|
+
content: {
|
|
244
|
+
content_raw: config.privacyMode === 'full' ? oText : null,
|
|
245
|
+
content_hash: oHash
|
|
246
|
+
},
|
|
247
|
+
metadata_flags: {
|
|
248
|
+
non_adaptive_response: isNonAdaptive ? 1 : 0,
|
|
249
|
+
turn_boundary_confirmed: 1
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
emit(responseEvent);
|
|
254
|
+
lastResponseHash = oHash;
|
|
255
|
+
lastTurnConfirmed = true; // Confirmation Rule locked
|
|
256
|
+
|
|
196
257
|
return response;
|
|
197
258
|
};
|
|
198
259
|
|
|
@@ -205,78 +266,65 @@ if (typeof GLOBAL_OBJ.fetch === 'function' && !GLOBAL_OBJ.fetch.__dropout_patche
|
|
|
205
266
|
async function capture(target, options = {}) {
|
|
206
267
|
const start = Date.now();
|
|
207
268
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
} else if (target && typeof target.then === 'function') {
|
|
213
|
-
result = await target;
|
|
269
|
+
let activeTurn;
|
|
270
|
+
if (lastTurnConfirmed) {
|
|
271
|
+
activeTurn = turnIndex++;
|
|
272
|
+
lastTurnConfirmed = false;
|
|
214
273
|
} else {
|
|
215
|
-
|
|
216
|
-
const { prompt, output } = target;
|
|
217
|
-
const latency = options.latency || 0;
|
|
218
|
-
const turn = turnIndex++;
|
|
219
|
-
const mode = options.privacy || config.privacyMode;
|
|
220
|
-
|
|
221
|
-
const payload = {
|
|
222
|
-
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
223
|
-
timestamp: Date.now(),
|
|
224
|
-
latency,
|
|
225
|
-
turn_position: turn,
|
|
226
|
-
provider: options.provider || 'manual',
|
|
227
|
-
model: options.model || 'unknown',
|
|
228
|
-
mode
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
if (mode === 'full') {
|
|
232
|
-
payload.prompt = prompt;
|
|
233
|
-
payload.output = output;
|
|
234
|
-
} else {
|
|
235
|
-
payload.prompt_hash = getSemanticHash(prompt);
|
|
236
|
-
payload.output_hash = getSemanticHash(output);
|
|
237
|
-
payload.markers = { p: getMarkers(prompt), o: getMarkers(output) };
|
|
238
|
-
payload.flags = getFlags((prompt || "") + " " + (output || ""));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
emit(payload);
|
|
242
|
-
return output;
|
|
274
|
+
activeTurn = turnIndex > 0 ? turnIndex - 1 : 0;
|
|
243
275
|
}
|
|
244
276
|
|
|
245
|
-
// Wrapped execution capture
|
|
246
|
-
const latency = Date.now() - start;
|
|
247
|
-
const turn = turnIndex++;
|
|
248
277
|
const mode = options.privacy || config.privacyMode;
|
|
249
|
-
const prompt = options.prompt;
|
|
250
|
-
const output = typeof result === 'string' ? result : JSON.stringify(result);
|
|
251
|
-
|
|
252
|
-
const payload = {
|
|
253
|
-
session_id: GLOBAL_OBJ.__dropout_session_id__,
|
|
254
|
-
timestamp: Date.now(),
|
|
255
|
-
latency,
|
|
256
|
-
turn_position: turn,
|
|
257
|
-
provider: options.provider || 'manual',
|
|
258
|
-
model: options.model || 'unknown',
|
|
259
|
-
mode
|
|
260
|
-
};
|
|
261
278
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
279
|
+
let prompt, output, latency_ms = options.latency || 0;
|
|
280
|
+
|
|
281
|
+
if (typeof target === 'function' || (target && typeof target.then === 'function')) {
|
|
282
|
+
prompt = options.prompt;
|
|
283
|
+
const result = typeof target === 'function' ? await target() : await target;
|
|
284
|
+
output = typeof result === 'string' ? result : JSON.stringify(result);
|
|
285
|
+
latency_ms = Date.now() - start;
|
|
265
286
|
} else {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
payload.markers = { p: getMarkers(prompt), o: getMarkers(output) };
|
|
269
|
-
payload.flags = getFlags((prompt || "") + " " + (output || ""));
|
|
287
|
+
prompt = target.prompt;
|
|
288
|
+
output = target.output;
|
|
270
289
|
}
|
|
271
290
|
|
|
272
|
-
|
|
273
|
-
|
|
291
|
+
const pHash = hash(prompt);
|
|
292
|
+
const oHash = hash(output);
|
|
293
|
+
|
|
294
|
+
// Emit Request
|
|
295
|
+
emit({
|
|
296
|
+
identity: { session_id: GLOBAL_OBJ.__dropout_session_id__, turn_index: activeTurn, direction: 'request', turn_role: 'user' },
|
|
297
|
+
timing: { created_at: Date.now() },
|
|
298
|
+
provider_context: { provider: options.provider || 'manual', model: options.model || 'unknown' },
|
|
299
|
+
content: { content_raw: mode === 'full' ? prompt : null, content_hash: pHash },
|
|
300
|
+
metadata_flags: { retry_like: pHash === lastPromptHash ? 1 : 0 }
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Emit Response
|
|
304
|
+
emit({
|
|
305
|
+
identity: { session_id: GLOBAL_OBJ.__dropout_session_id__, turn_index: activeTurn, direction: 'response', turn_role: 'assistant' },
|
|
306
|
+
timing: { created_at: Date.now(), latency_ms },
|
|
307
|
+
provider_context: { provider: options.provider || 'manual', model: options.model || 'unknown' },
|
|
308
|
+
content: { content_raw: mode === 'full' ? output : null, content_hash: oHash },
|
|
309
|
+
metadata_flags: { non_adaptive_response: oHash === lastResponseHash ? 1 : 0, turn_boundary_confirmed: 1 }
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
lastPromptHash = pHash;
|
|
313
|
+
lastResponseHash = oHash;
|
|
314
|
+
lastTurnConfirmed = true;
|
|
315
|
+
|
|
316
|
+
return output;
|
|
274
317
|
}
|
|
275
318
|
|
|
276
319
|
module.exports = {
|
|
277
320
|
capture,
|
|
278
|
-
reset: () => {
|
|
321
|
+
reset: (reason = 'manual_reset') => {
|
|
322
|
+
emitSessionEnd(reason);
|
|
279
323
|
GLOBAL_OBJ.__dropout_session_id__ = generateSessionId();
|
|
324
|
+
GLOBAL_OBJ.__dropout_session_ended__ = false;
|
|
280
325
|
turnIndex = 0;
|
|
326
|
+
lastTurnConfirmed = true;
|
|
327
|
+
lastPromptHash = null;
|
|
328
|
+
lastResponseHash = null;
|
|
281
329
|
}
|
|
282
330
|
};
|