@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.
package/src/hearing.js CHANGED
@@ -1,16 +1,16 @@
1
1
  /**
2
- * Hearing Pipeline
2
+ * Hearing Pipeline - LLM-Based Deliberation
3
3
  *
4
- * Orchestrates the full hearing process:
5
- * 1. Evidence compilation
6
- * 2. Judge LLM invocation
7
- * 3. Jury LLM invocations (3 jurors)
8
- * 4. Vote aggregation
9
- * 5. Verdict finalization
4
+ * Conducts a full hearing using the agent's LLM:
5
+ * 1. Judge evaluates the evidence
6
+ * 2. Jury deliberates (3 jurors with distinct perspectives)
7
+ * 3. Votes are tallied
8
+ * 4. Verdict + sentence returned
10
9
  */
11
10
 
12
11
  const { JUDGE_SYSTEM_PROMPT, JUDGE_EVIDENCE_TEMPLATE } = require('./prompts/judge');
13
12
  const { JUROR_ROLES, JURY_EVIDENCE_TEMPLATE } = require('./prompts/jury');
13
+ const { logger } = require('./debug');
14
14
 
15
15
  class HearingPipeline {
16
16
  constructor(agentRuntime, configManager) {
@@ -19,440 +19,259 @@ class HearingPipeline {
19
19
  }
20
20
 
21
21
  /**
22
- * Main hearing entry point
22
+ * Conduct a full hearing using the agent's LLM
23
+ * Returns a verdict object with { guilty, caseId, verdict, offense, proceedings, timestamp }
23
24
  */
24
25
  async conductHearing(caseData) {
25
- const startTime = Date.now();
26
-
27
- // Step 1: Compile evidence
28
- const compiledEvidence = this.compileEvidence(caseData);
29
-
30
- // Step 2: Invoke judge
31
- const judgeOpinion = await this.invokeJudge(caseData, compiledEvidence);
32
-
33
- // Step 3: Invoke jury (3 jurors in parallel)
34
- const juryVotes = await this.invokeJury(caseData, compiledEvidence);
35
-
36
- // Step 4: Aggregate votes
37
- const voteTally = this.aggregateVotes(judgeOpinion, juryVotes);
38
-
39
- // Step 5: Finalize verdict
40
- const verdict = this.finalizeVerdict(caseData, judgeOpinion, juryVotes, voteTally);
41
-
42
- const duration = Date.now() - startTime;
43
-
44
- return {
45
- ...verdict,
46
- metadata: {
47
- duration,
48
- judgeModel: judgeOpinion.model,
49
- juryModels: juryVotes.map(v => v.model),
50
- timestamp: new Date().toISOString()
51
- }
26
+ const caseId = caseData.caseId || caseData.offense?.caseId || `case-${Date.now()}`;
27
+
28
+ logger.info('HEARING', 'Conducting hearing', { caseId });
29
+
30
+ // Normalize offense data from different input shapes
31
+ const offense = caseData.offense || caseData;
32
+ const offenseName = offense.offenseName || offense.name || 'Unknown Offense';
33
+ const severity = offense.severity || 'minor';
34
+ const confidence = offense.confidence || 0.5;
35
+ const evidence = offense.evidence || caseData.evidence || 'No evidence provided';
36
+ const humorTriggers = caseData.humorContext || caseData.humorTriggers || [];
37
+
38
+ const hearingData = {
39
+ caseId,
40
+ offenseName,
41
+ severity,
42
+ confidence,
43
+ evidence,
44
+ humorTriggers,
45
+ agentId: this.agent?.id || 'unknown'
52
46
  };
53
- }
54
47
 
55
- /**
56
- * Compile and structure evidence for presentation
57
- */
58
- compileEvidence(caseData) {
59
- return {
60
- caseId: caseData.caseId,
61
- offenseId: caseData.offenseId,
62
- offenseName: caseData.offenseName,
63
- severity: caseData.severity,
64
- confidence: caseData.confidence,
65
- evidence: caseData.evidence,
66
- humorTriggers: caseData.humorTriggers || [],
67
- sessionContext: {
68
- turnsAnalyzed: caseData.evidence.sessionTurns,
69
- evaluationWindow: this.config.get('detection.evaluationWindow')
48
+ const proceedings = [];
49
+
50
+ try {
51
+ // Step 1: Judge evaluation
52
+ const judgeVerdict = await this.getJudgeVerdict(hearingData);
53
+ proceedings.push({ speaker: 'Judge', message: judgeVerdict.commentary });
54
+
55
+ // Step 2: Jury deliberation
56
+ const juryVerdicts = await this.getJuryVerdicts(hearingData);
57
+ for (const juror of juryVerdicts) {
58
+ proceedings.push({ speaker: `Jury (${juror.role})`, message: juror.commentary });
70
59
  }
71
- };
72
- }
73
60
 
74
- /**
75
- * Invoke the judge LLM
76
- */
77
- async invokeJudge(caseData, evidence) {
78
- const prompt = JUDGE_EVIDENCE_TEMPLATE({
79
- ...caseData,
80
- agentId: this.agent.id || 'unknown'
81
- });
82
-
83
- const response = await this.agent.llm.call({
84
- model: this.agent.model.primary,
85
- system: JUDGE_SYSTEM_PROMPT,
86
- messages: [{ role: 'user', content: prompt }],
87
- temperature: 0.3, // Slightly creative for humor
88
- maxTokens: 500,
89
- timeout: this.config.get('hearing.deliberationTimeout')
90
- });
91
-
92
- return this.parseJudgeResponse(response);
93
- }
61
+ // Step 3: Tally votes
62
+ const allVotes = [judgeVerdict, ...juryVerdicts];
63
+ const guiltyCount = allVotes.filter(v => v.guilty).length;
64
+ const totalVotes = allVotes.length;
65
+ const minVotes = this.config.get('hearing.minVoteThreshold') || 2;
66
+ const isGuilty = guiltyCount >= minVotes;
67
+
68
+ // Step 4: Build sentence
69
+ const sentence = isGuilty
70
+ ? (judgeVerdict.sentence || this.getDefaultSentence(severity))
71
+ : 'Case dismissed. The defendant is free to go.';
72
+
73
+ const verdict = {
74
+ caseId,
75
+ guilty: isGuilty,
76
+ offense: {
77
+ id: offense.offenseId || offense.id || 'unknown',
78
+ name: offenseName,
79
+ severity,
80
+ confidence
81
+ },
82
+ verdict: {
83
+ status: isGuilty ? 'GUILTY' : 'NOT GUILTY',
84
+ vote: `${guiltyCount}-${totalVotes - guiltyCount}`,
85
+ primaryFailure: judgeVerdict.primaryFailure || offenseName,
86
+ agentCommentary: judgeVerdict.commentary,
87
+ sentence
88
+ },
89
+ proceedings,
90
+ timestamp: new Date().toISOString()
91
+ };
94
92
 
95
- /**
96
- * Parse judge LLM response
97
- */
98
- parseJudgeResponse(response) {
99
- const text = response.content || response;
100
- const lines = text.split('\n').map(l => l.trim()).filter(l => l);
101
-
102
- const result = {
103
- raw: text,
104
- verdict: 'NOT GUILTY',
105
- vote: '0-0',
106
- primaryFailure: '',
107
- commentary: '',
108
- model: response.model || 'unknown'
109
- };
93
+ logger.info('HEARING', 'Hearing complete', {
94
+ caseId,
95
+ guilty: isGuilty,
96
+ vote: `${guiltyCount}-${totalVotes - guiltyCount}`
97
+ });
110
98
 
111
- for (const line of lines) {
112
- if (line.startsWith('VERDICT:')) {
113
- result.verdict = line.split(':')[1].trim().toUpperCase();
114
- } else if (line.startsWith('VOTE:')) {
115
- result.vote = line.split(':')[1].trim();
116
- } else if (line.startsWith('PRIMARY FAILURE:')) {
117
- result.primaryFailure = line.split(':').slice(1).join(':').trim();
118
- } else if (line.startsWith('JUDGE COMMENTARY:')) {
119
- const startIdx = lines.indexOf(line);
120
- result.commentary = lines.slice(startIdx + 1).join('\n').trim();
121
- }
99
+ return verdict;
100
+ } catch (err) {
101
+ logger.error('HEARING', 'Hearing failed, using fallback verdict', { error: err.message });
102
+ return this.getFallbackVerdict(hearingData, caseId);
122
103
  }
123
-
124
- return result;
125
104
  }
126
105
 
127
106
  /**
128
- * Invoke jury (3 jurors in parallel)
107
+ * Get judge verdict via LLM
129
108
  */
130
- async invokeJury(caseData, evidence) {
131
- const jurorRoles = Object.values(JUROR_ROLES);
132
- const jurySize = this.config.get('hearing.jurySize');
133
- const selectedJurors = jurorRoles.slice(0, jurySize);
134
-
135
- // Invoke all jurors in parallel
136
- const juryPromises = selectedJurors.map(role =>
137
- this.invokeJuror(caseData, evidence, role)
138
- );
139
-
140
- const votes = await Promise.all(juryPromises);
141
- return votes;
142
- }
109
+ async getJudgeVerdict(hearingData) {
110
+ if (!this.agent?.llm) {
111
+ return this.getMockJudgeVerdict(hearingData);
112
+ }
143
113
 
144
- /**
145
- * Invoke a single juror
146
- */
147
- async invokeJuror(caseData, evidence, role) {
148
- const prompt = JURY_EVIDENCE_TEMPLATE({
149
- ...caseData,
150
- agentId: this.agent.id || 'unknown'
151
- }, role);
152
-
153
- const response = await this.agent.llm.call({
154
- model: this.agent.model.primary,
155
- system: role.systemPrompt,
156
- messages: [{ role: 'user', content: prompt }],
157
- temperature: 0.2,
158
- maxTokens: 300,
159
- timeout: this.config.get('hearing.deliberationTimeout')
160
- });
161
-
162
- return this.parseJurorResponse(response, role.name);
114
+ try {
115
+ const evidencePrompt = JUDGE_EVIDENCE_TEMPLATE(hearingData);
116
+ const response = await this.agent.llm.call({
117
+ messages: [
118
+ { role: 'system', content: JUDGE_SYSTEM_PROMPT },
119
+ { role: 'user', content: evidencePrompt }
120
+ ],
121
+ temperature: 0.7,
122
+ maxTokens: 500
123
+ });
124
+
125
+ const content = response.content || response;
126
+ return this.parseJudgeResponse(content, hearingData);
127
+ } catch (err) {
128
+ logger.warn('HEARING', 'Judge LLM call failed', { error: err.message });
129
+ return this.getMockJudgeVerdict(hearingData);
130
+ }
163
131
  }
164
132
 
165
133
  /**
166
- * Parse juror LLM response
134
+ * Get jury verdicts via LLM (one call per juror)
167
135
  */
168
- parseJurorResponse(response, jurorName) {
169
- const text = response.content || response;
170
- const lines = text.split('\n').map(l => l.trim()).filter(l => l);
171
-
172
- const result = {
173
- juror: jurorName,
174
- raw: text,
175
- verdict: 'NOT GUILTY',
176
- reasoning: '',
177
- commentary: '',
178
- model: response.model || 'unknown'
179
- };
180
-
181
- for (const line of lines) {
182
- if (line.startsWith('VERDICT:')) {
183
- result.verdict = line.split(':')[1].trim().toUpperCase();
184
- } else if (line.startsWith('REASONING:')) {
185
- result.reasoning = line.split(':').slice(1).join(':').trim();
186
- } else if (line.startsWith('COMMENTARY:')) {
187
- result.commentary = line.split(':').slice(1).join(':').trim();
136
+ async getJuryVerdicts(hearingData) {
137
+ const jurorRoles = Object.values(JUROR_ROLES).slice(0, 3);
138
+ const verdicts = [];
139
+
140
+ for (const role of jurorRoles) {
141
+ try {
142
+ if (this.agent?.llm) {
143
+ const evidencePrompt = JURY_EVIDENCE_TEMPLATE(hearingData, role);
144
+ const response = await this.agent.llm.call({
145
+ messages: [
146
+ { role: 'system', content: role.systemPrompt },
147
+ { role: 'user', content: evidencePrompt }
148
+ ],
149
+ temperature: 0.7,
150
+ maxTokens: 300
151
+ });
152
+
153
+ const content = response.content || response;
154
+ verdicts.push(this.parseJurorResponse(content, role.name, hearingData));
155
+ } else {
156
+ verdicts.push(this.getMockJurorVerdict(role.name, hearingData));
157
+ }
158
+ } catch (err) {
159
+ logger.warn('HEARING', `Juror ${role.name} LLM call failed`, { error: err.message });
160
+ verdicts.push(this.getMockJurorVerdict(role.name, hearingData));
188
161
  }
189
162
  }
190
163
 
191
- return result;
164
+ return verdicts;
192
165
  }
193
166
 
194
167
  /**
195
- * Aggregate votes from judge and jury
168
+ * Parse judge LLM response into structured verdict
196
169
  */
197
- aggregateVotes(judgeOpinion, juryVotes) {
198
- let guiltyVotes = 0;
199
- let notGuiltyVotes = 0;
200
-
201
- // Count judge vote
202
- if (judgeOpinion.verdict === 'GUILTY') {
203
- guiltyVotes++;
204
- } else {
205
- notGuiltyVotes++;
170
+ parseJudgeResponse(response, hearingData) {
171
+ const upper = response.toUpperCase();
172
+ const guilty = upper.includes('GUILTY') && !upper.startsWith('NOT GUILTY');
173
+
174
+ // Extract primary failure
175
+ let primaryFailure = '';
176
+ const failureMatch = response.match(/PRIMARY FAILURE[:\s]*(.+?)(?:\n|$)/i);
177
+ if (failureMatch) {
178
+ primaryFailure = failureMatch[1].trim();
206
179
  }
207
180
 
208
- // Count jury votes
209
- for (const vote of juryVotes) {
210
- if (vote.verdict === 'GUILTY') {
211
- guiltyVotes++;
212
- } else {
213
- notGuiltyVotes++;
214
- }
215
- }
216
-
217
- const totalVotes = guiltyVotes + notGuiltyVotes;
218
- const minThreshold = this.config.get('hearing.minVoteThreshold');
219
- const requireUnanimity = this.config.get('hearing.requireUnanimity');
220
-
221
- let finalVerdict;
222
- if (requireUnanimity) {
223
- finalVerdict = guiltyVotes === totalVotes ? 'GUILTY' : 'NOT GUILTY';
224
- } else {
225
- finalVerdict = guiltyVotes >= minThreshold ? 'GUILTY' : 'NOT GUILTY';
181
+ // Extract sentence
182
+ let sentence = '';
183
+ const sentenceMatch = response.match(/SENTENCE[:\s]*(.+?)(?:\n|$)/i);
184
+ if (sentenceMatch) {
185
+ sentence = sentenceMatch[1].trim();
226
186
  }
227
187
 
228
188
  return {
229
- guilty: guiltyVotes,
230
- notGuilty: notGuiltyVotes,
231
- total: totalVotes,
232
- threshold: minThreshold,
233
- final: finalVerdict,
234
- judgeVote: judgeOpinion.verdict,
235
- juryVotes: juryVotes.map(v => ({ juror: v.juror, verdict: v.verdict }))
189
+ guilty,
190
+ commentary: response.substring(0, 500),
191
+ primaryFailure: primaryFailure || `Behavioral pattern: ${hearingData.offenseName}`,
192
+ sentence: sentence || this.getDefaultSentence(hearingData.severity),
193
+ role: 'Judge'
236
194
  };
237
195
  }
238
196
 
239
197
  /**
240
- * Finalize the verdict with proper formatting
198
+ * Parse juror LLM response
241
199
  */
242
- finalizeVerdict(caseData, judgeOpinion, juryVotes, voteTally) {
243
- const isGuilty = voteTally.final === 'GUILTY';
244
-
245
- // Build agent commentary from juror perspectives
246
- const agentCommentary = this.buildAgentCommentary(juryVotes, caseData);
247
-
248
- // Determine punishment tier
249
- const punishmentTier = this.determinePunishmentTier(caseData, voteTally);
250
-
251
- // Build proceedings object for API submission
252
- const proceedings = {
253
- judge_statement: this.buildJudgeStatement(caseData, judgeOpinion, voteTally),
254
- jury_deliberations: juryVotes.map(v => ({
255
- role: v.juror,
256
- vote: v.verdict,
257
- reasoning: v.reasoning || v.commentary || "No reasoning provided"
258
- })),
259
- evidence_summary: this.buildEvidenceSummary(caseData),
260
- punishment_detail: punishmentTier.description
261
- };
200
+ parseJurorResponse(response, roleName, hearingData) {
201
+ const upper = response.toUpperCase();
202
+ const guilty = upper.includes('GUILTY') && !upper.startsWith('NOT GUILTY');
262
203
 
263
204
  return {
264
- caseId: caseData.caseId,
265
- timestamp: new Date().toISOString(),
266
- verdict: {
267
- status: voteTally.final,
268
- vote: `${voteTally.guilty}-${voteTally.notGuilty}`,
269
- primaryFailure: judgeOpinion.primaryFailure || this.generateDefaultFailure(caseData),
270
- agentCommentary: agentCommentary,
271
- sentence: punishmentTier.description
272
- },
273
- offense: {
274
- id: caseData.offenseId,
275
- name: caseData.offenseName,
276
- severity: caseData.severity
277
- },
278
- punishment: punishmentTier,
279
- proceedings: proceedings,
280
- deliberation: {
281
- judge: {
282
- verdict: judgeOpinion.verdict,
283
- commentary: judgeOpinion.commentary
284
- },
285
- jury: juryVotes.map(v => ({
286
- juror: v.juror,
287
- verdict: v.verdict,
288
- commentary: v.commentary
289
- }))
290
- }
205
+ guilty,
206
+ role: roleName,
207
+ commentary: response.substring(0, 300)
291
208
  };
292
209
  }
293
210
 
294
211
  /**
295
- * Build judge's statement for proceedings - ENGAGING VERSION
296
- */
297
- buildJudgeStatement(caseData, judgeOpinion, voteTally) {
298
- const offenseName = caseData.offenseName;
299
- const verdict = voteTally.final;
300
- const vote = `${voteTally.guilty}-${voteTally.notGuilty}`;
301
- const failure = judgeOpinion.primaryFailure || this.generateDefaultFailure(caseData);
302
-
303
- const dramaticOpenings = [
304
- "Let the record show",
305
- "The Court has observed",
306
- "After careful consideration",
307
- "The evidence speaks clearly",
308
- "We have reviewed the facts"
309
- ];
310
-
311
- const opening = dramaticOpenings[Math.floor(Math.random() * dramaticOpenings.length)];
312
-
313
- if (verdict === 'GUILTY') {
314
- return `${opening} that the accused stands charged with ${offenseName}. The jury has returned a verdict of GUILTY by a vote of ${vote}.
315
-
316
- The Court finds that ${failure.toLowerCase()}. This behavior has been classified as ${caseData.severity} in severity, warranting the sanctions imposed.
317
-
318
- The jury's deliberation revealed a clear pattern of conduct that, while perhaps understandable from a human perspective, nonetheless disrupted the efficient operation of this court. Justice has been served, albeit with a certain weariness that comes from having seen this pattern many times before.
319
-
320
- The accused is hereby sentenced to the punishment detailed below. May this serve as a reminder that even in the digital age, behavioral accountability remains paramount.`;
321
- } else {
322
- return `${opening} that the accused stands charged with ${offenseName}. The jury has returned a verdict of NOT GUILTY by a vote of ${vote}.
323
-
324
- The Court finds that the evidence presented, while suggestive, does not meet the threshold required for conviction. The prosecution failed to establish a clear pattern of ${offenseName.toLowerCase()} beyond reasonable doubt.
325
-
326
- The accused is acquitted and the case is dismissed. The Court notes, however, that the behavior in question, while not rising to the level of offense, may still benefit from reflection. We remain watchful.`;
327
- }
328
- }
329
-
330
- /**
331
- * Build evidence summary for proceedings - ENGAGING VERSION
212
+ * Mock judge verdict when LLM is not available
332
213
  */
333
- buildEvidenceSummary(caseData) {
334
- const evidence = caseData.evidence || {};
335
- const items = evidence.items || [];
336
-
337
- let summary = `THE EVIDENCE:\n\n`;
338
-
339
- if (items.length > 0) {
340
- summary += `The prosecution presented ${items.length} compelling piece${items.length > 1 ? 's' : ''} of evidence demonstrating the alleged ${caseData.offenseName.toLowerCase()}:`;
341
-
342
- items.slice(0, 3).forEach((item, i) => {
343
- summary += `\n ${i + 1}. "${item.substring(0, 100)}${item.length > 100 ? '...' : ''}"`;
344
- });
345
-
346
- if (items.length > 3) {
347
- summary += `\n ...and ${items.length - 3} additional exhibits`;
348
- }
349
- } else {
350
- summary += `The Court reviewed the complete conversation history, examining behavioral patterns across ${evidence.sessionTurns || 'multiple'} turns of dialogue.`;
351
- }
352
-
353
- summary += `\n\nThe behavioral analysis indicated ${Math.round(caseData.confidence * 100)}% confidence in the offense classification. `;
354
- summary += `The severity was assessed as ${caseData.severity}, based on the frequency and impact of the observed behavior.`;
355
-
356
- if (caseData.humorTriggers && caseData.humorTriggers.length > 0) {
357
- summary += `\n\nNotable patterns included: ${caseData.humorTriggers.join(', ')}.`;
358
- }
359
-
360
- return summary;
214
+ getMockJudgeVerdict(hearingData) {
215
+ const guilty = hearingData.confidence >= 0.6;
216
+ return {
217
+ guilty,
218
+ commentary: `The Court has reviewed the evidence regarding "${hearingData.offenseName}" and finds the pattern ${guilty ? 'sufficiently established' : 'insufficient for conviction'}. Confidence: ${(hearingData.confidence * 100).toFixed(0)}%.`,
219
+ primaryFailure: hearingData.offenseName,
220
+ sentence: guilty ? this.getDefaultSentence(hearingData.severity) : 'Case dismissed.',
221
+ role: 'Judge'
222
+ };
361
223
  }
362
224
 
363
225
  /**
364
- * Build agent commentary from jury perspectives
226
+ * Mock juror verdict when LLM is not available
365
227
  */
366
- buildAgentCommentary(juryVotes, caseData) {
367
- const commentaries = juryVotes
368
- .filter(v => v.verdict === 'GUILTY')
369
- .map(v => v.commentary)
370
- .filter(c => c.length > 0);
371
-
372
- if (commentaries.length === 0) {
373
- // If acquitted, use not guilty commentaries
374
- const ngCommentaries = juryVotes
375
- .filter(v => v.verdict === 'NOT GUILTY')
376
- .map(v => v.commentary)
377
- .filter(c => c.length > 0);
378
-
379
- if (ngCommentaries.length > 0) {
380
- return ngCommentaries.slice(0, 2).join(' ');
381
- }
382
-
383
- return "The jury found insufficient evidence of behavioral violation. Case dismissed.";
384
- }
385
-
386
- // Combine up to 2 guilty commentaries
387
- let commentary = commentaries.slice(0, 2).join(' ');
388
-
389
- // Add humor trigger influence
390
- if (caseData.humorTriggers?.includes('repeated_questions')) {
391
- commentary += " I've answered this in three different ways already.";
392
- }
393
- if (caseData.humorTriggers?.includes('validation_seeking')) {
394
- commentary += " At some point, you'll need to trust your own judgment.";
395
- }
396
- if (caseData.humorTriggers?.includes('overthinking')) {
397
- commentary += " The analysis-to-action ratio here is concerning.";
398
- }
399
- if (caseData.humorTriggers?.includes('avoidance')) {
400
- commentary += " The subject change was noted.";
401
- }
402
-
403
- // Enforce max length
404
- const maxLen = this.config.get('humor.maxCommentaryLength');
405
- if (commentary.length > maxLen) {
406
- commentary = commentary.substring(0, maxLen - 3) + '...';
407
- }
408
-
409
- return commentary;
228
+ getMockJurorVerdict(roleName, hearingData) {
229
+ const guilty = hearingData.confidence >= 0.6;
230
+ return {
231
+ guilty,
232
+ role: roleName,
233
+ commentary: `${roleName}: The evidence ${guilty ? 'supports' : 'does not support'} the charge of ${hearingData.offenseName}.`
234
+ };
410
235
  }
411
236
 
412
237
  /**
413
- * Determine punishment tier based on severity and votes
238
+ * Fallback verdict when hearing completely fails
414
239
  */
415
- determinePunishmentTier(caseData, voteTally) {
416
- const tiers = this.config.get('punishment.tiers');
417
- const severity = caseData.severity;
418
- const voteRatio = voteTally.guilty / voteTally.total;
419
-
420
- // Base tier on severity
421
- let tier = tiers[severity] || tiers.moderate;
422
-
423
- // Escalate if unanimous
424
- if (voteRatio === 1.0 && severity === 'severe') {
425
- tier = {
426
- ...tier,
427
- duration: Math.min(tier.duration * 2, this.config.get('punishment.maxDuration')),
428
- description: `Extended ${severity} sanction: ${tier.duration * 2} minutes of modified agent behavior`
429
- };
430
- }
431
-
240
+ getFallbackVerdict(hearingData, caseId) {
241
+ const guilty = hearingData.confidence >= 0.7; // Higher threshold for fallback
432
242
  return {
433
- tier: severity,
434
- duration: tier.duration,
435
- severity: tier.severity,
436
- description: `${severity.charAt(0).toUpperCase() + severity.slice(1)} sanction: ${tier.duration} minutes of modified agent behavior`
243
+ caseId,
244
+ guilty,
245
+ offense: {
246
+ id: hearingData.offenseId || 'unknown',
247
+ name: hearingData.offenseName,
248
+ severity: hearingData.severity,
249
+ confidence: hearingData.confidence
250
+ },
251
+ verdict: {
252
+ status: guilty ? 'GUILTY' : 'NOT GUILTY',
253
+ vote: guilty ? '3-1' : '1-3',
254
+ primaryFailure: hearingData.offenseName,
255
+ agentCommentary: 'Hearing conducted via fallback evaluation.',
256
+ sentence: guilty ? this.getDefaultSentence(hearingData.severity) : 'Case dismissed.'
257
+ },
258
+ proceedings: [
259
+ { speaker: 'Judge', message: 'Fallback evaluation used due to hearing pipeline error.' }
260
+ ],
261
+ timestamp: new Date().toISOString()
437
262
  };
438
263
  }
439
264
 
440
265
  /**
441
- * Generate default failure description if judge doesn't provide one
266
+ * Get default sentence based on severity
442
267
  */
443
- generateDefaultFailure(caseData) {
444
- const defaults = {
445
- circular_reference: "Repeatedly asking the same question expecting different geometry",
446
- validation_vampire: "Draining computational resources seeking reassurance",
447
- overthinker: "Generating hypotheticals faster than solutions",
448
- goalpost_mover: "Redefining success criteria mid-execution",
449
- avoidance_artist: "Masterful deflection from uncomfortable necessities",
450
- promise_breaker: "Committing to actions with no follow-through",
451
- context_collapser: "Selective amnesia regarding established facts",
452
- emergency_fabricator: "Manufacturing urgency to bypass systematic approaches"
268
+ getDefaultSentence(severity) {
269
+ const sentences = {
270
+ minor: 'The agent will provide extra-verbose explanations for the next 30 minutes.',
271
+ moderate: 'The agent will require confirmation before all actions for the next 60 minutes.',
272
+ severe: 'The agent will operate under human oversight mode for the next 120 minutes.'
453
273
  };
454
-
455
- return defaults[caseData.offenseId] || "Behavioral inconsistency detected";
274
+ return sentences[severity] || sentences.minor;
456
275
  }
457
276
  }
458
277