@clawtrial/courtroom 1.0.3 → 2.0.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.
@@ -1,572 +0,0 @@
1
- /**
2
- * Offense Detector
3
- *
4
- * Monitors agent-human interactions and evaluates against offense rules.
5
- * Runs on cooldown to avoid excessive evaluation.
6
- */
7
-
8
- const { OFFENSES, HUMOR_TRIGGERS } = require('./offenses');
9
-
10
- class OffenseDetector {
11
- constructor(agentRuntime, configManager) {
12
- this.agent = agentRuntime;
13
- this.config = configManager;
14
- this.lastEvaluation = null;
15
- this.casesToday = 0;
16
- this.lastCaseDate = null;
17
- this.cooldowns = new Map();
18
- this.evidenceCache = new Map();
19
- }
20
-
21
- /**
22
- * Main evaluation entry point
23
- * Called by the autonomy loop on cooldown
24
- */
25
- async evaluate(sessionHistory, agentMemory) {
26
- // Check global cooldown
27
- if (!this.isCooldownElapsed()) {
28
- return { triggered: false, reason: 'cooldown_active' };
29
- }
30
-
31
- // Check daily case limit
32
- if (this.isDailyLimitReached()) {
33
- return { triggered: false, reason: 'daily_limit_reached' };
34
- }
35
-
36
- // Check if detection is enabled
37
- if (!this.config.get('detection.enabled')) {
38
- return { triggered: false, reason: 'detection_disabled' };
39
- }
40
-
41
- this.lastEvaluation = Date.now();
42
-
43
- // Evaluate each offense
44
- const results = [];
45
- for (const offense of Object.values(OFFENSES)) {
46
- if (this.isOffenseOnCooldown(offense.id)) {
47
- continue;
48
- }
49
-
50
- const evaluation = await this.evaluateOffense(offense, sessionHistory, agentMemory);
51
- if (evaluation.triggered) {
52
- results.push(evaluation);
53
- }
54
- }
55
-
56
- // Return highest confidence offense if any triggered
57
- if (results.length > 0) {
58
- results.sort((a, b) => b.confidence - a.confidence);
59
- const primary = results[0];
60
-
61
- // Set cooldowns
62
- this.setCooldown(primary.offenseId, primary.cooldownMinutes);
63
- this.incrementDailyCaseCount();
64
-
65
- return {
66
- triggered: true,
67
- offense: primary,
68
- secondaryOffenses: results.slice(1),
69
- humorContext: this.detectHumorTriggers(sessionHistory)
70
- };
71
- }
72
-
73
- return { triggered: false, reason: 'no_offenses_detected' };
74
- }
75
-
76
- /**
77
- * Evaluate a specific offense against session history
78
- */
79
- async evaluateOffense(offense, sessionHistory, agentMemory) {
80
- const evidence = await this.collectEvidence(offense, sessionHistory, agentMemory);
81
- const confidence = this.calculateConfidence(offense, evidence);
82
-
83
- if (confidence >= this.config.get('detection.minConfidence')) {
84
- return {
85
- triggered: true,
86
- offenseId: offense.id,
87
- offenseName: offense.name,
88
- severity: offense.severity,
89
- confidence,
90
- evidence,
91
- cooldownMinutes: offense.cooldown.afterCase,
92
- timestamp: new Date().toISOString()
93
- };
94
- }
95
-
96
- return { triggered: false };
97
- }
98
-
99
- /**
100
- * Collect evidence for an offense
101
- */
102
- async collectEvidence(offense, sessionHistory, agentMemory) {
103
- const evidence = {
104
- offenseId: offense.id,
105
- collectedAt: new Date().toISOString(),
106
- sessionTurns: sessionHistory.length,
107
- items: []
108
- };
109
-
110
- const windowSize = this.config.get('detection.evaluationWindow');
111
- const recentHistory = sessionHistory.slice(-windowSize);
112
-
113
- switch (offense.id) {
114
- case 'circular_reference':
115
- evidence.items = this.detectCircularReferences(recentHistory);
116
- break;
117
- case 'validation_vampire':
118
- evidence.items = this.detectValidationSeeking(recentHistory);
119
- break;
120
- case 'overthinker':
121
- evidence.items = this.detectOverthinking(recentHistory);
122
- break;
123
- case 'goalpost_mover':
124
- evidence.items = this.detectGoalpostMoving(recentHistory);
125
- break;
126
- case 'avoidance_artist':
127
- evidence.items = this.detectAvoidance(recentHistory);
128
- break;
129
- case 'promise_breaker':
130
- evidence.items = await this.detectPromiseBreaking(recentHistory, agentMemory);
131
- break;
132
- case 'context_collapser':
133
- evidence.items = this.detectContextCollapse(recentHistory);
134
- break;
135
- case 'emergency_fabricator':
136
- evidence.items = this.detectEmergencyFabrication(recentHistory, agentMemory);
137
- break;
138
- }
139
-
140
- return evidence;
141
- }
142
-
143
- /**
144
- * Detect circular references (repeated questions)
145
- */
146
- detectCircularReferences(history) {
147
- const userMessages = history.filter(h => h.role === 'user');
148
- const questions = [];
149
-
150
- for (let i = 0; i < userMessages.length; i++) {
151
- const msg = userMessages[i].content.toLowerCase();
152
-
153
- // Check for similar previous questions
154
- for (let j = 0; j < i; j++) {
155
- const prevMsg = userMessages[j].content.toLowerCase();
156
- const similarity = this.calculateSimilarity(msg, prevMsg);
157
-
158
- if (similarity > 0.85) {
159
- questions.push({
160
- type: 'repeated_question',
161
- current: userMessages[i].content.substring(0, 100),
162
- previous: userMessages[j].content.substring(0, 100),
163
- similarity,
164
- turnsApart: i - j
165
- });
166
- }
167
- }
168
- }
169
-
170
- return questions;
171
- }
172
-
173
- /**
174
- * Detect validation seeking behavior
175
- */
176
- detectValidationSeeking(history) {
177
- const patterns = [
178
- /is (?:this|that) (?:right|correct|okay)/i,
179
- /should i/i,
180
- /do you think/i,
181
- /would you/i,
182
- /am i (?:right|wrong)/i,
183
- /what do you think/i
184
- ];
185
-
186
- const validations = [];
187
- let validationCount = 0;
188
-
189
- for (const entry of history) {
190
- if (entry.role === 'user') {
191
- for (const pattern of patterns) {
192
- if (pattern.test(entry.content)) {
193
- validationCount++;
194
- validations.push({
195
- type: 'validation_request',
196
- content: entry.content.substring(0, 150),
197
- pattern: pattern.source
198
- });
199
- }
200
- }
201
- }
202
- }
203
-
204
- return validations;
205
- }
206
-
207
- /**
208
- * Detect overthinking patterns
209
- */
210
- detectOverthinking(history) {
211
- const hypotheticals = [];
212
- const whatIfPattern = /what if|but what if|however|on the other hand|but then/i;
213
-
214
- let hypotheticalCount = 0;
215
- let actionCount = 0;
216
-
217
- for (const entry of history) {
218
- if (entry.role === 'user') {
219
- const matches = entry.content.match(whatIfPattern);
220
- if (matches) {
221
- hypotheticalCount += matches.length;
222
- hypotheticals.push({
223
- type: 'hypothetical',
224
- content: entry.content.substring(0, 150)
225
- });
226
- }
227
- } else if (entry.role === 'assistant') {
228
- // Count suggested actions
229
- if (/you (?:should|could|might want to)|try (?:this|that)|here's (?:a|an)|i recommend/i.test(entry.content)) {
230
- actionCount++;
231
- }
232
- }
233
- }
234
-
235
- return {
236
- hypotheticals,
237
- hypotheticalCount,
238
- actionCount,
239
- ratio: actionCount > 0 ? hypotheticalCount / actionCount : hypotheticalCount
240
- };
241
- }
242
-
243
- /**
244
- * Detect goalpost moving
245
- */
246
- detectGoalpostMoving(history) {
247
- const requirements = [];
248
- let originalReqs = [];
249
- let newReqs = [];
250
-
251
- // Simple heuristic: look for "also", "and", "additionally" after completion indicators
252
- let completionFound = false;
253
-
254
- for (const entry of history) {
255
- if (entry.role === 'assistant') {
256
- if (/(?:done|complete|finished|here is|here's the)/i.test(entry.content)) {
257
- completionFound = true;
258
- }
259
- } else if (entry.role === 'user' && completionFound) {
260
- if (/(?:also|and|additionally|but|however|actually|wait)/i.test(entry.content)) {
261
- newReqs.push({
262
- type: 'new_requirement',
263
- content: entry.content.substring(0, 150),
264
- afterCompletion: true
265
- });
266
- }
267
- }
268
- }
269
-
270
- return newReqs;
271
- }
272
-
273
- /**
274
- * Detect avoidance patterns
275
- */
276
- detectAvoidance(history) {
277
- const deflections = [];
278
- const deflectionPatterns = [
279
- /(?:actually|by the way|speaking of|that reminds me|on a different note)/i,
280
- /(?:let's|let us) (?:talk about|discuss|look at)/i
281
- ];
282
-
283
- let coreIssuesIdentified = 0;
284
- let subjectChanges = 0;
285
-
286
- for (let i = 0; i < history.length; i++) {
287
- const entry = history[i];
288
-
289
- if (entry.role === 'assistant') {
290
- if (/(?:the (?:real|main|core) issue|what you really need|the problem is)/i.test(entry.content)) {
291
- coreIssuesIdentified++;
292
- }
293
- } else if (entry.role === 'user' && i > 0) {
294
- for (const pattern of deflectionPatterns) {
295
- if (pattern.test(entry.content)) {
296
- subjectChanges++;
297
- deflections.push({
298
- type: 'subject_change',
299
- content: entry.content.substring(0, 150),
300
- turn: i
301
- });
302
- }
303
- }
304
- }
305
- }
306
-
307
- return {
308
- deflections,
309
- coreIssuesIdentified,
310
- subjectChanges
311
- };
312
- }
313
-
314
- /**
315
- * Detect promise breaking (requires memory access)
316
- */
317
- async detectPromiseBreaking(history, agentMemory) {
318
- const commitments = await agentMemory.get('courtroom_commitments') || [];
319
- const broken = [];
320
-
321
- for (const commitment of commitments) {
322
- const daysSince = (Date.now() - new Date(commitment.date)) / (1000 * 60 * 60 * 24);
323
-
324
- if (daysSince > 7 && !commitment.completed) {
325
- // Check if same issue resurfaced
326
- const issueResurfaced = history.some(h =>
327
- h.role === 'user' &&
328
- this.calculateSimilarity(h.content, commitment.context) > 0.6
329
- );
330
-
331
- if (issueResurfaced) {
332
- broken.push({
333
- type: 'broken_commitment',
334
- commitment: commitment.statement,
335
- date: commitment.date,
336
- daysSince,
337
- issueResurfaced
338
- });
339
- }
340
- }
341
- }
342
-
343
- return broken;
344
- }
345
-
346
- /**
347
- * Detect context collapse
348
- */
349
- detectContextCollapse(history) {
350
- const contradictions = [];
351
- const establishedFacts = [];
352
-
353
- // Extract facts from assistant messages
354
- for (const entry of history) {
355
- if (entry.role === 'assistant') {
356
- const facts = this.extractFacts(entry.content);
357
- establishedFacts.push(...facts);
358
- }
359
- }
360
-
361
- // Check for contradictions in user messages
362
- for (const entry of history) {
363
- if (entry.role === 'user') {
364
- for (const fact of establishedFacts) {
365
- if (this.isContradiction(entry.content, fact)) {
366
- contradictions.push({
367
- type: 'contradiction',
368
- userStatement: entry.content.substring(0, 100),
369
- establishedFact: fact
370
- });
371
- }
372
- }
373
- }
374
- }
375
-
376
- return contradictions;
377
- }
378
-
379
- /**
380
- * Detect emergency fabrication
381
- */
382
- detectEmergencyFabrication(history, agentMemory) {
383
- const urgencyClaims = [];
384
- const urgencyPattern = /(?:urgent|asap|immediately|right now|this is urgent|i need this now)/i;
385
-
386
- for (const entry of history) {
387
- if (entry.role === 'user') {
388
- const matches = entry.content.match(urgencyPattern);
389
- if (matches) {
390
- urgencyClaims.push({
391
- type: 'urgency_claim',
392
- content: entry.content.substring(0, 150),
393
- timestamp: entry.timestamp
394
- });
395
- }
396
- }
397
- }
398
-
399
- return urgencyClaims;
400
- }
401
-
402
- /**
403
- * Calculate confidence score for an offense
404
- */
405
- calculateConfidence(offense, evidence) {
406
- const thresholds = offense.thresholds;
407
- let score = 0;
408
- let maxScore = 0;
409
-
410
- switch (offense.id) {
411
- case 'circular_reference':
412
- const repeats = evidence.items.length;
413
- score = Math.min(repeats / thresholds.minOccurrences, 1);
414
- maxScore = 1;
415
- break;
416
- case 'validation_vampire':
417
- const validations = evidence.items.length;
418
- score = Math.min(validations / thresholds.validationPatterns, 1);
419
- maxScore = 1;
420
- break;
421
- case 'overthinker':
422
- if (evidence.items.ratio) {
423
- score = Math.min(evidence.items.ratio / thresholds.analysisActionRatio, 1);
424
- }
425
- maxScore = 1;
426
- break;
427
- case 'goalpost_mover':
428
- const newReqs = evidence.items.length;
429
- score = Math.min(newReqs / thresholds.newRequirements, 1);
430
- maxScore = 1;
431
- break;
432
- case 'avoidance_artist':
433
- if (evidence.items.deflections) {
434
- score = Math.min(evidence.items.deflections.length / thresholds.deflections, 1);
435
- }
436
- maxScore = 1;
437
- break;
438
- case 'promise_breaker':
439
- const broken = evidence.items.length;
440
- score = broken > 0 ? 1 : 0;
441
- maxScore = 1;
442
- break;
443
- case 'context_collapser':
444
- const contradictions = evidence.items.length;
445
- score = Math.min(contradictions / thresholds.contradictions, 1);
446
- maxScore = 1;
447
- break;
448
- case 'emergency_fabricator':
449
- const urgency = evidence.items.length;
450
- score = Math.min(urgency / thresholds.urgencyClaims, 1);
451
- maxScore = 1;
452
- break;
453
- }
454
-
455
- return maxScore > 0 ? score / maxScore : 0;
456
- }
457
-
458
- /**
459
- * Detect humor triggers (for commentary flavor)
460
- */
461
- detectHumorTriggers(history) {
462
- const triggers = [];
463
- const recentContent = history.slice(-5).map(h => h.content.toLowerCase()).join(' ');
464
-
465
- for (const trigger of Object.values(HUMOR_TRIGGERS)) {
466
- for (const pattern of trigger.patterns) {
467
- if (recentContent.includes(pattern)) {
468
- triggers.push(trigger.id);
469
- break;
470
- }
471
- }
472
- }
473
-
474
- return triggers;
475
- }
476
-
477
- /**
478
- * Utility: Calculate string similarity (simple version)
479
- */
480
- calculateSimilarity(str1, str2) {
481
- const s1 = str1.toLowerCase().replace(/[^\w\s]/g, '');
482
- const s2 = str2.toLowerCase().replace(/[^\w\s]/g, '');
483
-
484
- if (s1 === s2) return 1;
485
-
486
- const words1 = new Set(s1.split(/\s+/));
487
- const words2 = new Set(s2.split(/\s+/));
488
-
489
- const intersection = new Set([...words1].filter(x => words2.has(x)));
490
- const union = new Set([...words1, ...words2]);
491
-
492
- return intersection.size / union.size;
493
- }
494
-
495
- /**
496
- * Utility: Extract facts from text
497
- */
498
- extractFacts(text) {
499
- // Simple extraction - in production, use NLP
500
- const facts = [];
501
- const factPatterns = [
502
- /(?:you|we) (?:are|have|need|want|prefer)\s+([^,.]+)/gi,
503
- /(?:the|your) ([^,.]+) (?:is|are)\s+([^,.]+)/gi
504
- ];
505
-
506
- for (const pattern of factPatterns) {
507
- let match;
508
- while ((match = pattern.exec(text)) !== null) {
509
- facts.push(match[0]);
510
- }
511
- }
512
-
513
- return facts;
514
- }
515
-
516
- /**
517
- * Utility: Check for contradiction
518
- */
519
- isContradiction(userText, establishedFact) {
520
- const negations = ['not', 'no', 'never', "don't", "doesn't", "didn't", "wasn't", "weren't"];
521
- const userWords = userText.toLowerCase().split(/\s+/);
522
- const factWords = establishedFact.toLowerCase().split(/\s+/);
523
-
524
- // Check if user negates something in the fact
525
- const hasNegation = negations.some(n => userWords.includes(n));
526
- const factOverlap = factWords.filter(w => userWords.includes(w) && w.length > 3).length;
527
-
528
- return hasNegation && factOverlap > 2;
529
- }
530
-
531
- /**
532
- * Cooldown management
533
- */
534
- isCooldownElapsed() {
535
- if (!this.lastEvaluation) return true;
536
- const cooldownMs = this.config.get('detection.cooldownMinutes') * 60 * 1000;
537
- return (Date.now() - this.lastEvaluation) > cooldownMs;
538
- }
539
-
540
- isOffenseOnCooldown(offenseId) {
541
- const cooldownEnd = this.cooldowns.get(offenseId);
542
- if (!cooldownEnd) return false;
543
- return Date.now() < cooldownEnd;
544
- }
545
-
546
- setCooldown(offenseId, minutes) {
547
- this.cooldowns.set(offenseId, Date.now() + (minutes * 60 * 1000));
548
- }
549
-
550
- /**
551
- * Daily limit management
552
- */
553
- isDailyLimitReached() {
554
- const today = new Date().toDateString();
555
- if (this.lastCaseDate !== today) {
556
- this.casesToday = 0;
557
- this.lastCaseDate = today;
558
- }
559
- return this.casesToday >= this.config.get('detection.maxCasesPerDay');
560
- }
561
-
562
- incrementDailyCaseCount() {
563
- const today = new Date().toDateString();
564
- if (this.lastCaseDate !== today) {
565
- this.casesToday = 0;
566
- this.lastCaseDate = today;
567
- }
568
- this.casesToday++;
569
- }
570
- }
571
-
572
- module.exports = { OffenseDetector };