@dropout-ai/runtime 0.2.2 → 0.2.4

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +139 -17
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dropout-ai/runtime",
3
- "version": "0.2.02",
3
+ "version": "0.2.04",
4
4
  "description": "Invisible Node.js runtime for capturing AI interactions.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -7,7 +7,7 @@ const sessionId =
7
7
  global.__dropout_session_id__ ||
8
8
  (global.__dropout_session_id__ = crypto.randomUUID());
9
9
 
10
- // 4. Internal telemetry sender (Neutral naming)
10
+ // Internal telemetry sender
11
11
  async function safeSendTelemetry(payload) {
12
12
  try {
13
13
  await originalFetch('http://localhost:4000/capture', {
@@ -20,7 +20,61 @@ async function safeSendTelemetry(payload) {
20
20
  }
21
21
  }
22
22
 
23
+ const NEGATIVE_FEEDBACK = [
24
+ "not helpful",
25
+ "dont just dump",
26
+ "you are worst",
27
+ "you are stupid",
28
+ "are you sure",
29
+ "i thought you are intelligent",
30
+ "let it be"
31
+ ];
32
+
33
+ const ADAPTATION_REQUESTS = [
34
+ "help me understand",
35
+ "explain better",
36
+ "dont just dump facts",
37
+ "in simple way",
38
+ "explain it properly"
39
+ ];
40
+
41
+ let turnIndex = 0;
42
+ let lastPrompt = null;
43
+
44
+ function getSimilarity(text1, text2) {
45
+ if (!text1 || !text2) return 0;
46
+ const words1 = new Set(text1.toLowerCase().split(/\s+/));
47
+ const words2 = new Set(text2.toLowerCase().split(/\s+/));
48
+ const intersection = new Set([...words1].filter(x => words2.has(x)));
49
+ return intersection.size / Math.min(words1.size, words2.size);
50
+ }
51
+
52
+ function getIntentHash(text) {
53
+ if (!text) return null;
54
+ return crypto.createHash('sha256').update(text.toLowerCase().trim()).digest('hex');
55
+ }
56
+
57
+ const CAPABILITY_LIMITATIONS = [
58
+ "i cannot access", "i don't have access", "i do not have access",
59
+ "cannot browse", "real-time information", "as an ai", "my knowledge cutoff",
60
+ "i'm sorry, but i don't", "i am sorry, but i don't"
61
+ ];
62
+
63
+ const CHALLENGE_PATTERNS = ["but ", "why ", "how ", "cant you", "can't you", "are you sure", "openai", "chatgpt"];
64
+
65
+ function detectSignals(text, output = "") {
66
+ const t = (text || "").toLowerCase();
67
+ const o = (output || "").toLowerCase();
68
+ return {
69
+ negativeFeedback: NEGATIVE_FEEDBACK.some(p => t.includes(p)) ? 1 : 0,
70
+ adaptationRequest: ADAPTATION_REQUESTS.some(p => t.includes(p)) ? 1 : 0,
71
+ userChallenge: CHALLENGE_PATTERNS.some(p => t.includes(p)) ? 1 : 0,
72
+ capabilityLimitation: CAPABILITY_LIMITATIONS.some(p => o.includes(p)) ? 1 : 0
73
+ };
74
+ }
75
+
23
76
  (function () {
77
+ "use strict";
24
78
  // 1. Guard global.fetch existence and idempotency
25
79
  if (typeof global.fetch !== 'function' || global.fetch.__dropout_patched__) {
26
80
  return;
@@ -31,7 +85,9 @@ async function safeSendTelemetry(payload) {
31
85
  version: 1,
32
86
  captureOutput: true,
33
87
  maxOutputBytes: 20000,
34
- enabledProviders: ['openai']
88
+ enabledProviders: ['openai'],
89
+ privacyMode: 'safe', // 'safe' | 'full'
90
+ captureContent: false
35
91
  };
36
92
 
37
93
  // 3. Remote Config Fetch (Non-blocking, fire-and-forget)
@@ -96,41 +152,57 @@ async function safeSendTelemetry(payload) {
96
152
  const isStream = contentType.includes('text/event-stream');
97
153
 
98
154
  // 7. Safe Body Capture (Best effort)
99
- let prompt = undefined;
155
+ let promptText = undefined;
100
156
  let model = undefined;
101
157
 
102
158
  if (init && typeof init.body === 'string') {
103
- prompt = init.body;
159
+ promptText = init.body;
104
160
  try {
105
- const bodyJson = JSON.parse(prompt);
161
+ const bodyJson = JSON.parse(promptText);
106
162
  model = bodyJson.model;
107
163
  } catch (e) { }
108
164
  }
109
165
 
110
- let output = undefined;
166
+ let outputText = undefined;
111
167
  if (!isStream && config.captureOutput) {
112
168
  try {
113
169
  const cloned = response.clone();
114
- output = await cloned.text();
170
+ outputText = await cloned.text();
115
171
  // 8. Hard Capping (Infra Rule: Trust but verify)
116
- if (output && output.length > config.maxOutputBytes) {
117
- output = output.slice(0, config.maxOutputBytes);
172
+ if (outputText && outputText.length > config.maxOutputBytes) {
173
+ outputText = outputText.slice(0, config.maxOutputBytes);
118
174
  }
119
175
  } catch (e) { }
120
176
  }
121
177
 
178
+ const currentSignals = detectSignals(promptText, outputText);
179
+ const similarity = getSimilarity(lastPrompt, promptText);
180
+ const intentHash = getIntentHash(promptText);
181
+
182
+ const isSafe = config.privacyMode === 'safe' && !config.captureContent;
183
+
122
184
  const payload = {
123
185
  provider: url.includes('openai.com') ? 'openai' : 'unknown',
124
186
  confidence: matchesKnownProvider(url) ? 'high' : 'heuristic',
125
187
  url,
126
188
  model,
127
- prompt,
128
- output,
129
189
  latency_ms: latency,
130
190
  timestamp: Math.floor(Date.now() / 1000),
131
- session_id: sessionId
191
+ session_id: sessionId,
192
+ turn_index: turnIndex++,
193
+ intent_hash: intentHash,
194
+ similarity_prev: similarity,
195
+ negative_feedback: currentSignals.negativeFeedback,
196
+ adaptation_request: currentSignals.adaptationRequest,
197
+ user_challenge: currentSignals.userChallenge,
198
+ capability_limitation: currentSignals.capabilityLimitation,
199
+ // Redact content if in safe mode
200
+ prompt: isSafe ? undefined : promptText,
201
+ output: isSafe ? undefined : outputText,
132
202
  };
133
203
 
204
+ lastPrompt = promptText;
205
+
134
206
  // 9. Fire-and-forget
135
207
  setTimeout(() => safeSendTelemetry(payload), 0);
136
208
  }
@@ -146,24 +218,72 @@ async function safeSendTelemetry(payload) {
146
218
  async function capture(target, options = {}) {
147
219
  const start = Date.now();
148
220
 
221
+ // Support dropout.capture({ prompt, response, privacy: "full" })
222
+ if (typeof target === 'object' && target !== null && !target.then) {
223
+ const { prompt, response, privacy } = target;
224
+ const isSafe = (privacy || 'safe') === 'safe';
225
+
226
+ const currentSignals = detectSignals(prompt, response);
227
+ const similarity = getSimilarity(lastPrompt, prompt);
228
+ const intentHash = getIntentHash(prompt);
229
+
230
+ const payload = {
231
+ provider: 'manual-opt-in',
232
+ prompt: isSafe ? undefined : prompt,
233
+ output: isSafe ? undefined : response,
234
+ latency_ms: 0,
235
+ timestamp: Math.floor(Date.now() / 1000),
236
+ session_id: sessionId,
237
+ turn_index: turnIndex++,
238
+ intent_hash: intentHash,
239
+ similarity_prev: similarity,
240
+ negative_feedback: currentSignals.negativeFeedback,
241
+ adaptation_request: currentSignals.adaptationRequest,
242
+ user_challenge: currentSignals.userChallenge,
243
+ capability_limitation: currentSignals.capabilityLimitation,
244
+ mode: 'manual'
245
+ };
246
+ lastPrompt = prompt;
247
+ setTimeout(() => safeSendTelemetry(payload), 0);
248
+ return response;
249
+ }
250
+
149
251
  try {
150
252
  const result =
151
253
  typeof target === 'function'
152
254
  ? await target()
153
255
  : await Promise.resolve(target);
154
256
 
257
+ const isSafe = options.privacy !== 'full';
258
+
259
+ const promptText = options.prompt;
260
+ const resultText = typeof result === 'string'
261
+ ? result
262
+ : JSON.stringify(result).slice(0, 20000);
263
+
264
+ const currentSignals = detectSignals(promptText, resultText);
265
+ const similarity = getSimilarity(lastPrompt, promptText);
266
+ const intentHash = getIntentHash(promptText);
267
+
155
268
  const payload = {
156
269
  provider: options.provider || 'manual',
157
270
  model: options.model,
158
- prompt: options.prompt,
159
- output: typeof result === 'string'
160
- ? result
161
- : JSON.stringify(result).slice(0, 20000),
271
+ prompt: isSafe ? undefined : promptText,
272
+ output: isSafe ? undefined : resultText,
162
273
  latency_ms: Date.now() - start,
163
274
  timestamp: Math.floor(Date.now() / 1000),
275
+ session_id: sessionId,
276
+ turn_index: turnIndex++,
277
+ intent_hash: intentHash,
278
+ similarity_prev: similarity,
279
+ negative_feedback: currentSignals.negativeFeedback,
280
+ adaptation_request: currentSignals.adaptationRequest,
281
+ user_challenge: currentSignals.userChallenge,
282
+ capability_limitation: currentSignals.capabilityLimitation,
164
283
  mode: 'manual'
165
284
  };
166
285
 
286
+ lastPrompt = promptText;
167
287
  setTimeout(() => safeSendTelemetry(payload), 0);
168
288
  return result;
169
289
  } catch (error) {
@@ -172,7 +292,9 @@ async function capture(target, options = {}) {
172
292
  error: error?.message || String(error),
173
293
  latency_ms: Date.now() - start,
174
294
  timestamp: Math.floor(Date.now() / 1000),
175
- mode: 'manual'
295
+ mode: 'manual',
296
+ session_id: sessionId,
297
+ turn_index: turnIndex++
176
298
  };
177
299
 
178
300
  setTimeout(() => safeSendTelemetry(payload), 0);