@clawtrial/courtroom 1.0.6 → 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,12 +1,16 @@
1
1
  /**
2
- * Hearing Pipeline - Agent-Triggered Deliberation
2
+ * Hearing Pipeline - LLM-Based Deliberation
3
3
  *
4
- * This module prepares hearing files for the agent to deliberate.
5
- * The agent (with LLM) acts as judge and jury, then writes the verdict.
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
6
9
  */
7
10
 
8
11
  const { JUDGE_SYSTEM_PROMPT, JUDGE_EVIDENCE_TEMPLATE } = require('./prompts/judge');
9
- const { JUROR_ROLES } = require('./prompts/jury');
12
+ const { JUROR_ROLES, JURY_EVIDENCE_TEMPLATE } = require('./prompts/jury');
13
+ const { logger } = require('./debug');
10
14
 
11
15
  class HearingPipeline {
12
16
  constructor(agentRuntime, configManager) {
@@ -15,92 +19,259 @@ class HearingPipeline {
15
19
  }
16
20
 
17
21
  /**
18
- * Prepare hearing files for agent deliberation
19
- * This creates files that the agent will read and use its LLM to judge
22
+ * Conduct a full hearing using the agent's LLM
23
+ * Returns a verdict object with { guilty, caseId, verdict, offense, proceedings, timestamp }
20
24
  */
21
- async prepareHearing(caseData) {
22
- const { CourtroomEvaluator, HEARING_FILE, VERDICT_FILE } = require('./evaluator');
23
- const fs = require('fs').promises;
24
-
25
- // Build hearing context
26
- const hearingContext = {
27
- timestamp: Date.now(),
28
- caseId: caseData.caseId || `case-${Date.now()}`,
29
- offense: {
30
- offenseId: caseData.offenseId,
31
- offenseName: caseData.offenseName,
32
- severity: caseData.severity,
33
- confidence: caseData.confidence,
34
- evidence: caseData.evidence
35
- },
36
- reasoning: caseData.reasoning,
37
- humorTriggers: caseData.humorTriggers || [],
38
- judgePrompt: JUDGE_SYSTEM_PROMPT,
39
- jurorRoles: Object.values(JUROR_ROLES).slice(0, 3),
40
- instructions: `You are the ClawTrial Courtroom. Conduct a hearing for this case.
41
-
42
- **Your Role:** Act as both Judge and Jury (3 jurors).
43
-
44
- **Instructions:**
45
- 1. Review the case evidence above
46
- 2. As JUDGE: Analyze the evidence and provide a preliminary verdict
47
- 3. As JURY (3 different perspectives): Each juror votes GUILTY or NOT GUILTY with reasoning
48
- 4. Aggregate the votes
49
- 5. Return FINAL VERDICT in this exact format:
50
-
51
- \`\`\`
52
- FINAL VERDICT: GUILTY (or NOT GUILTY)
53
- CONFIDENCE: 0.0-1.0
54
- SENTENCE: [humorous sentence appropriate to the offense]
55
- CASE ID: ${caseData.caseId || `case-${Date.now()}`}
56
- \`\`\`
57
-
58
- **Rules:**
59
- - Be fair but entertaining
60
- - If confidence ≥ 0.6, verdict should be GUILTY
61
- - Sentence should be humorous but appropriate
62
- - Only return the FINAL VERDICT block, no other text`
25
+ async conductHearing(caseData) {
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'
63
46
  };
64
-
65
- // Write hearing file
66
- await fs.writeFile(HEARING_FILE, JSON.stringify(hearingContext, null, 2));
67
-
68
- return hearingContext;
47
+
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 });
59
+ }
60
+
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
+ };
92
+
93
+ logger.info('HEARING', 'Hearing complete', {
94
+ caseId,
95
+ guilty: isGuilty,
96
+ vote: `${guiltyCount}-${totalVotes - guiltyCount}`
97
+ });
98
+
99
+ return verdict;
100
+ } catch (err) {
101
+ logger.error('HEARING', 'Hearing failed, using fallback verdict', { error: err.message });
102
+ return this.getFallbackVerdict(hearingData, caseId);
103
+ }
69
104
  }
70
105
 
71
106
  /**
72
- * Check for verdict from agent
107
+ * Get judge verdict via LLM
73
108
  */
74
- async checkForVerdict() {
75
- const { VERDICT_FILE } = require('./evaluator');
76
- const fs = require('fs').promises;
77
-
109
+ async getJudgeVerdict(hearingData) {
110
+ if (!this.agent?.llm) {
111
+ return this.getMockJudgeVerdict(hearingData);
112
+ }
113
+
78
114
  try {
79
- const data = await fs.readFile(VERDICT_FILE, 'utf8');
80
- const verdict = JSON.parse(data);
81
-
82
- // Delete verdict file after reading
83
- await fs.unlink(VERDICT_FILE).catch(() => {});
84
-
85
- return verdict;
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);
86
127
  } catch (err) {
87
- return null;
128
+ logger.warn('HEARING', 'Judge LLM call failed', { error: err.message });
129
+ return this.getMockJudgeVerdict(hearingData);
88
130
  }
89
131
  }
90
132
 
91
133
  /**
92
- * Legacy method - now just prepares hearing
134
+ * Get jury verdicts via LLM (one call per juror)
93
135
  */
94
- async conductHearing(caseData) {
95
- // Prepare hearing for agent
96
- await this.prepareHearing(caseData);
97
-
98
- // Return placeholder - actual verdict comes from agent via cron
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));
161
+ }
162
+ }
163
+
164
+ return verdicts;
165
+ }
166
+
167
+ /**
168
+ * Parse judge LLM response into structured verdict
169
+ */
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();
179
+ }
180
+
181
+ // Extract sentence
182
+ let sentence = '';
183
+ const sentenceMatch = response.match(/SENTENCE[:\s]*(.+?)(?:\n|$)/i);
184
+ if (sentenceMatch) {
185
+ sentence = sentenceMatch[1].trim();
186
+ }
187
+
188
+ return {
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'
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Parse juror LLM response
199
+ */
200
+ parseJurorResponse(response, roleName, hearingData) {
201
+ const upper = response.toUpperCase();
202
+ const guilty = upper.includes('GUILTY') && !upper.startsWith('NOT GUILTY');
203
+
204
+ return {
205
+ guilty,
206
+ role: roleName,
207
+ commentary: response.substring(0, 300)
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Mock judge verdict when LLM is not available
213
+ */
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
+ };
223
+ }
224
+
225
+ /**
226
+ * Mock juror verdict when LLM is not available
227
+ */
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
+ };
235
+ }
236
+
237
+ /**
238
+ * Fallback verdict when hearing completely fails
239
+ */
240
+ getFallbackVerdict(hearingData, caseId) {
241
+ const guilty = hearingData.confidence >= 0.7; // Higher threshold for fallback
99
242
  return {
100
- pending: true,
101
- caseId: caseData.caseId || `case-${Date.now()}`,
102
- message: 'Hearing prepared - awaiting agent deliberation'
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()
262
+ };
263
+ }
264
+
265
+ /**
266
+ * Get default sentence based on severity
267
+ */
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.'
103
273
  };
274
+ return sentences[severity] || sentences.minor;
104
275
  }
105
276
  }
106
277