@sebastientang/llm-council 0.2.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/dist/cli.js ADDED
@@ -0,0 +1,1151 @@
1
+ // src/cli.ts
2
+ import { parseArgs } from "util";
3
+ import { readFileSync } from "fs";
4
+
5
+ // src/engine.ts
6
+ import mitt from "mitt";
7
+
8
+ // src/types.ts
9
+ import { z } from "zod";
10
+ var ParticipantSchema = z.object({
11
+ id: z.string(),
12
+ name: z.string(),
13
+ provider: z.string(),
14
+ model: z.string(),
15
+ systemPrompt: z.string(),
16
+ temperature: z.number().min(0).max(2).optional()
17
+ });
18
+ var SynthesisSchema = z.object({
19
+ recommendation: z.string(),
20
+ confidence: z.number().min(0).max(100),
21
+ reasoning: z.string(),
22
+ risks: z.array(z.string()),
23
+ dissent: z.array(z.string()),
24
+ validationGates: z.array(z.string()),
25
+ assumptions: z.array(z.string()),
26
+ raw: z.string()
27
+ });
28
+ var DeliberationMessageSchema = z.object({
29
+ participantId: z.string(),
30
+ participantName: z.string(),
31
+ round: z.number(),
32
+ content: z.string(),
33
+ timestamp: z.date(),
34
+ tokenCount: z.object({ input: z.number(), output: z.number() })
35
+ });
36
+ var DeliberationConfigSchema = z.object({
37
+ topic: z.string().min(1),
38
+ options: z.array(z.string()).optional(),
39
+ context: z.string().optional(),
40
+ preferredOption: z.string().optional(),
41
+ participants: z.array(ParticipantSchema).min(2),
42
+ rounds: z.number().min(1).max(5).default(2),
43
+ tokenBudget: z.object({
44
+ perResponse: z.number().positive().optional(),
45
+ total: z.number().positive().optional()
46
+ }).optional()
47
+ });
48
+ var DeliberationMetadataSchema = z.object({
49
+ totalTokens: z.object({ input: z.number(), output: z.number() }),
50
+ durationMs: z.number(),
51
+ modelBreakdown: z.record(
52
+ z.string(),
53
+ z.object({ input: z.number(), output: z.number() })
54
+ )
55
+ });
56
+
57
+ // src/engine.ts
58
+ var Council = class {
59
+ providers;
60
+ protocol;
61
+ synthesizer;
62
+ synthesisProvider;
63
+ emitter = mitt();
64
+ constructor(options) {
65
+ this.providers = options.providers;
66
+ this.protocol = options.protocol;
67
+ this.synthesizer = options.synthesizer;
68
+ this.synthesisProvider = options.synthesisProvider;
69
+ }
70
+ on(event, handler) {
71
+ this.emitter.on(event, handler);
72
+ }
73
+ off(event, handler) {
74
+ this.emitter.off(event, handler);
75
+ }
76
+ async deliberate(rawConfig) {
77
+ try {
78
+ const config = DeliberationConfigSchema.parse(rawConfig);
79
+ const startTime = Date.now();
80
+ const allMessages = [];
81
+ const tokenTracker = {};
82
+ const roundCount = this.protocol.getRoundCount();
83
+ const rounds = Math.min(config.rounds, roundCount);
84
+ for (let round = 1; round <= rounds; round++) {
85
+ const prompts = this.protocol.buildPrompts(config, allMessages, round);
86
+ this.emitter.emit("round:start", {
87
+ round,
88
+ participantCount: prompts.length
89
+ });
90
+ const roundMessages = await Promise.all(
91
+ prompts.map(async (prompt) => {
92
+ const provider2 = this.providers.get(prompt.provider);
93
+ if (!provider2) {
94
+ throw new Error(
95
+ `Provider '${prompt.provider}' not found. Available: ${[...this.providers.keys()].join(", ")}`
96
+ );
97
+ }
98
+ const response = await provider2.complete({
99
+ model: prompt.model,
100
+ systemPrompt: prompt.systemPrompt,
101
+ messages: [{ role: "user", content: prompt.userMessage }],
102
+ temperature: prompt.temperature,
103
+ maxTokens: prompt.maxTokens
104
+ });
105
+ const participant = config.participants.find(
106
+ (p) => p.id === prompt.participantId
107
+ );
108
+ const message = {
109
+ participantId: prompt.participantId,
110
+ participantName: participant?.name ?? prompt.participantId,
111
+ round,
112
+ content: response.content,
113
+ timestamp: /* @__PURE__ */ new Date(),
114
+ tokenCount: response.tokenCount
115
+ };
116
+ const modelKey = `${prompt.provider}/${prompt.model}`;
117
+ const existing = tokenTracker[modelKey] ?? { input: 0, output: 0 };
118
+ tokenTracker[modelKey] = {
119
+ input: existing.input + response.tokenCount.input,
120
+ output: existing.output + response.tokenCount.output
121
+ };
122
+ this.emitter.emit("response", message);
123
+ return message;
124
+ })
125
+ );
126
+ allMessages.push(...roundMessages);
127
+ }
128
+ this.emitter.emit("synthesis:start", void 0);
129
+ const synthesisProviderConfig = this.synthesisProvider ?? {
130
+ providerId: config.participants[0].provider,
131
+ model: config.participants[0].model
132
+ };
133
+ const provider = this.providers.get(synthesisProviderConfig.providerId);
134
+ if (!provider) {
135
+ throw new Error(
136
+ `Synthesis provider '${synthesisProviderConfig.providerId}' not found`
137
+ );
138
+ }
139
+ const synthesis = await this.synthesizer.synthesize(
140
+ config,
141
+ allMessages,
142
+ provider
143
+ );
144
+ const durationMs = Date.now() - startTime;
145
+ const totalTokens = Object.values(tokenTracker).reduce(
146
+ (acc, t) => ({
147
+ input: acc.input + t.input,
148
+ output: acc.output + t.output
149
+ }),
150
+ { input: 0, output: 0 }
151
+ );
152
+ const metadata = {
153
+ totalTokens,
154
+ durationMs,
155
+ modelBreakdown: tokenTracker
156
+ };
157
+ const result = {
158
+ config,
159
+ messages: allMessages,
160
+ synthesis,
161
+ metadata
162
+ };
163
+ this.emitter.emit("complete", result);
164
+ return result;
165
+ } catch (error) {
166
+ const err = error instanceof Error ? error : new Error(String(error));
167
+ this.emitter.emit("error", err);
168
+ throw err;
169
+ }
170
+ }
171
+ };
172
+
173
+ // src/providers/anthropic.ts
174
+ import Anthropic from "@anthropic-ai/sdk";
175
+ var AnthropicProvider = class {
176
+ id = "anthropic";
177
+ client;
178
+ defaultModel;
179
+ defaultMaxTokens;
180
+ constructor(config) {
181
+ this.client = new Anthropic({ apiKey: config.apiKey });
182
+ this.defaultModel = config.defaultModel ?? "claude-sonnet-4-20250514";
183
+ this.defaultMaxTokens = config.defaultMaxTokens ?? 1024;
184
+ }
185
+ async complete(request) {
186
+ const params = {
187
+ model: request.model || this.defaultModel,
188
+ max_tokens: request.maxTokens ?? this.defaultMaxTokens,
189
+ system: request.systemPrompt,
190
+ messages: request.messages.map((msg) => ({
191
+ role: msg.role,
192
+ content: msg.content
193
+ })),
194
+ ...request.temperature !== void 0 && {
195
+ temperature: request.temperature
196
+ }
197
+ };
198
+ let response;
199
+ try {
200
+ response = await this.client.messages.create(params);
201
+ } catch (error) {
202
+ if (isOverloadedError(error)) {
203
+ await delay(1e3);
204
+ response = await this.client.messages.create(params);
205
+ } else {
206
+ throw error;
207
+ }
208
+ }
209
+ const textBlock = response.content.find(
210
+ (block) => block.type === "text"
211
+ );
212
+ if (!textBlock) {
213
+ throw new Error(
214
+ `Anthropic response contained no text block. Stop reason: ${response.stop_reason}`
215
+ );
216
+ }
217
+ return {
218
+ content: textBlock.text,
219
+ tokenCount: {
220
+ input: response.usage.input_tokens,
221
+ output: response.usage.output_tokens
222
+ },
223
+ model: response.model
224
+ };
225
+ }
226
+ };
227
+ function isOverloadedError(error) {
228
+ if (error instanceof Anthropic.APIError) {
229
+ return error.status === 529;
230
+ }
231
+ return false;
232
+ }
233
+ function delay(ms) {
234
+ return new Promise((resolve) => {
235
+ setTimeout(resolve, ms);
236
+ });
237
+ }
238
+
239
+ // src/providers/openrouter.ts
240
+ var OpenRouterProvider = class {
241
+ id = "openrouter";
242
+ apiKey;
243
+ appName;
244
+ defaultModel;
245
+ defaultMaxTokens;
246
+ siteUrl;
247
+ constructor(config) {
248
+ this.apiKey = config.apiKey;
249
+ this.appName = config.appName ?? "llm-council";
250
+ this.defaultModel = config.defaultModel ?? "openai/gpt-4o";
251
+ this.defaultMaxTokens = config.defaultMaxTokens ?? 1024;
252
+ this.siteUrl = config.siteUrl ?? "";
253
+ }
254
+ async complete(request) {
255
+ const body = {
256
+ model: request.model || this.defaultModel,
257
+ messages: [
258
+ { role: "system", content: request.systemPrompt },
259
+ ...request.messages.map((msg) => ({
260
+ role: msg.role,
261
+ content: msg.content
262
+ }))
263
+ ],
264
+ max_tokens: request.maxTokens ?? this.defaultMaxTokens,
265
+ ...request.temperature !== void 0 && {
266
+ temperature: request.temperature
267
+ }
268
+ };
269
+ const headers = {
270
+ "Authorization": `Bearer ${this.apiKey}`,
271
+ "Content-Type": "application/json",
272
+ "X-Title": this.appName
273
+ };
274
+ if (this.siteUrl) {
275
+ headers["HTTP-Referer"] = this.siteUrl;
276
+ }
277
+ let response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
278
+ method: "POST",
279
+ headers,
280
+ body: JSON.stringify(body)
281
+ });
282
+ if (response.status === 429) {
283
+ const retryAfter = response.headers.get("Retry-After");
284
+ const delayMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : 2e3;
285
+ await delay2(Math.min(delayMs, 1e4));
286
+ response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
287
+ method: "POST",
288
+ headers,
289
+ body: JSON.stringify(body)
290
+ });
291
+ }
292
+ if (!response.ok) {
293
+ const errorBody = await response.json().catch(() => null);
294
+ const message = errorBody?.error?.message ?? `HTTP ${response.status}`;
295
+ throw new Error(`OpenRouter API error (${response.status}): ${message}`);
296
+ }
297
+ const data = await response.json();
298
+ const content = data.choices?.[0]?.message?.content;
299
+ if (!content) {
300
+ throw new Error("OpenRouter response contained no content");
301
+ }
302
+ return {
303
+ content,
304
+ tokenCount: {
305
+ input: data.usage?.prompt_tokens ?? 0,
306
+ output: data.usage?.completion_tokens ?? 0
307
+ },
308
+ model: data.model
309
+ };
310
+ }
311
+ };
312
+ function delay2(ms) {
313
+ return new Promise((resolve) => {
314
+ setTimeout(resolve, ms);
315
+ });
316
+ }
317
+
318
+ // src/utils/prompt-builder.ts
319
+ function buildInitialUserMessage(config) {
320
+ const parts = [];
321
+ parts.push(`## Decision Topic
322
+ ${config.topic}`);
323
+ if (config.options && config.options.length > 0) {
324
+ const optionsList = config.options.map((opt, i) => `${i + 1}. ${opt}`).join("\n");
325
+ parts.push(`## Options
326
+ ${optionsList}`);
327
+ }
328
+ if (config.preferredOption) {
329
+ parts.push(`## Preferred Option
330
+ ${config.preferredOption}`);
331
+ }
332
+ if (config.context) {
333
+ parts.push(`## Additional Context
334
+ ${config.context}`);
335
+ }
336
+ parts.push(
337
+ "Provide your initial brief (200-400 words). State your position clearly and support it with reasoning."
338
+ );
339
+ return parts.join("\n\n");
340
+ }
341
+
342
+ // src/protocols/adversarial.ts
343
+ var DEFAULT_REBUTTAL_GUIDANCE = {
344
+ proposer: "Focus your rebuttal on the Challenger's strongest attack.",
345
+ challenger: "Focus on whether the Proposer's acknowledged weakness is actually fatal.",
346
+ steelmanner: "Focus on whether the Pre-Mortem's failure story applies equally to the rejected option.",
347
+ "pre-mortem": "Focus on which of the Proposer's assumptions appear in your failure cascade."
348
+ };
349
+ function getRebuttalGuidance(participantId, guidanceMap) {
350
+ for (const [key, guidance] of Object.entries(guidanceMap)) {
351
+ if (participantId.toLowerCase().includes(key.toLowerCase())) {
352
+ return guidance;
353
+ }
354
+ }
355
+ return "Address the most critical point from another member.";
356
+ }
357
+ function buildRebuttalUserMessage(participantId, otherMessages, guidanceMap) {
358
+ const parts = [];
359
+ parts.push("Here are the other Council members' briefs:");
360
+ for (const msg of otherMessages) {
361
+ parts.push(`### ${msg.participantName}
362
+ ${msg.content}`);
363
+ }
364
+ const guidance = getRebuttalGuidance(participantId, guidanceMap);
365
+ parts.push(
366
+ `Write your rebuttal (100-200 words). ${guidance}`
367
+ );
368
+ return parts.join("\n\n");
369
+ }
370
+ var AdversarialProtocol = class {
371
+ rebuttalGuidance;
372
+ constructor(options) {
373
+ this.rebuttalGuidance = options?.rebuttalGuidance ?? DEFAULT_REBUTTAL_GUIDANCE;
374
+ }
375
+ getRoundCount() {
376
+ return 2;
377
+ }
378
+ buildPrompts(config, history, round) {
379
+ if (round === 1) {
380
+ return this.buildInitialBriefs(config);
381
+ }
382
+ if (round === 2) {
383
+ return this.buildRebuttals(config, history);
384
+ }
385
+ return [];
386
+ }
387
+ buildInitialBriefs(config) {
388
+ const userMessage = buildInitialUserMessage(config);
389
+ return config.participants.map((participant) => ({
390
+ participantId: participant.id,
391
+ provider: participant.provider,
392
+ model: participant.model,
393
+ systemPrompt: participant.systemPrompt,
394
+ userMessage,
395
+ temperature: participant.temperature ?? 0.7,
396
+ maxTokens: config.tokenBudget?.perResponse
397
+ }));
398
+ }
399
+ buildRebuttals(config, history) {
400
+ const round1Messages = history.filter((msg) => msg.round === 1);
401
+ return config.participants.map((participant) => {
402
+ const otherMessages = round1Messages.filter(
403
+ (msg) => msg.participantId !== participant.id
404
+ );
405
+ return {
406
+ participantId: participant.id,
407
+ provider: participant.provider,
408
+ model: participant.model,
409
+ systemPrompt: participant.systemPrompt,
410
+ userMessage: buildRebuttalUserMessage(
411
+ participant.id,
412
+ otherMessages,
413
+ this.rebuttalGuidance
414
+ ),
415
+ temperature: participant.temperature ?? 0.7,
416
+ maxTokens: config.tokenBudget?.perResponse
417
+ };
418
+ });
419
+ }
420
+ };
421
+
422
+ // src/utils/anonymizer.ts
423
+ var LABELS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
424
+ function createAnonymizationMap(participantIds) {
425
+ const labelToParticipant = /* @__PURE__ */ new Map();
426
+ const participantToLabel = /* @__PURE__ */ new Map();
427
+ for (let i = 0; i < participantIds.length; i++) {
428
+ const label = `Response ${LABELS[i] ?? String(i + 1)}`;
429
+ labelToParticipant.set(label, participantIds[i]);
430
+ participantToLabel.set(participantIds[i], label);
431
+ }
432
+ return { labelToParticipant, participantToLabel };
433
+ }
434
+ function anonymizeMessages(messages, map) {
435
+ return messages.map((msg) => ({
436
+ label: map.participantToLabel.get(msg.participantId) ?? "Unknown",
437
+ content: msg.content
438
+ }));
439
+ }
440
+
441
+ // src/protocols/peer-review.ts
442
+ var PeerReviewProtocol = class {
443
+ enableRevote;
444
+ constructor(options) {
445
+ this.enableRevote = options?.enableRevote ?? false;
446
+ }
447
+ getRoundCount() {
448
+ return this.enableRevote ? 3 : 2;
449
+ }
450
+ buildPrompts(config, history, round) {
451
+ if (round === 1) {
452
+ return this.buildInitialBriefs(config);
453
+ }
454
+ const anonMap = createAnonymizationMap(config.participants.map((p) => p.id));
455
+ if (round === 2) {
456
+ return this.buildRankingRound(config, history, anonMap);
457
+ }
458
+ if (round === 3 && this.enableRevote) {
459
+ return this.buildRevoteRound(config, history, anonMap);
460
+ }
461
+ return [];
462
+ }
463
+ buildInitialBriefs(config) {
464
+ const userMessage = buildInitialUserMessage(config);
465
+ return config.participants.map((participant) => ({
466
+ participantId: participant.id,
467
+ provider: participant.provider,
468
+ model: participant.model,
469
+ systemPrompt: participant.systemPrompt,
470
+ userMessage,
471
+ temperature: participant.temperature ?? 0.7,
472
+ maxTokens: config.tokenBudget?.perResponse
473
+ }));
474
+ }
475
+ buildRankingRound(config, history, anonMap) {
476
+ const round1Messages = history.filter((msg) => msg.round === 1);
477
+ const anonymized = anonymizeMessages(round1Messages, anonMap);
478
+ const briefsText = anonymized.map((entry) => `### ${entry.label}
479
+ ${entry.content}`).join("\n\n");
480
+ const labelList = anonymized.map((e) => e.label).join(", ");
481
+ return config.participants.map((participant) => ({
482
+ participantId: participant.id,
483
+ provider: participant.provider,
484
+ model: participant.model,
485
+ systemPrompt: participant.systemPrompt,
486
+ userMessage: buildRankingUserMessage(briefsText, labelList),
487
+ temperature: participant.temperature ?? 0.7,
488
+ maxTokens: config.tokenBudget?.perResponse
489
+ }));
490
+ }
491
+ buildRevoteRound(config, history, anonMap) {
492
+ const round2Messages = history.filter((msg) => msg.round === 2);
493
+ const anonymizedRankings = anonymizeMessages(round2Messages, anonMap);
494
+ const rankingsText = anonymizedRankings.map((entry) => `### ${entry.label}'s Rankings
495
+ ${entry.content}`).join("\n\n");
496
+ const round1Messages = history.filter((msg) => msg.round === 1);
497
+ const anonymizedBriefs = anonymizeMessages(round1Messages, anonMap);
498
+ const labelList = anonymizedBriefs.map((e) => e.label).join(", ");
499
+ return config.participants.map((participant) => ({
500
+ participantId: participant.id,
501
+ provider: participant.provider,
502
+ model: participant.model,
503
+ systemPrompt: participant.systemPrompt,
504
+ userMessage: buildRevoteUserMessage(rankingsText, labelList),
505
+ temperature: participant.temperature ?? 0.7,
506
+ maxTokens: config.tokenBudget?.perResponse
507
+ }));
508
+ }
509
+ };
510
+ function buildRankingUserMessage(briefsText, labelList) {
511
+ const parts = [];
512
+ parts.push("You are participating in an anonymized peer review. Below are briefs from all council members (including yours, but you do not know which one is yours).");
513
+ parts.push(briefsText);
514
+ parts.push(`Rank ALL responses from best to worst (${labelList}). For each, provide a one-sentence justification.
515
+
516
+ Respond in EXACTLY this format:
517
+
518
+ RANKING:
519
+ 1. [Label] - [one-sentence justification]
520
+ 2. [Label] - [one-sentence justification]
521
+ 3. [Label] - [one-sentence justification]
522
+ 4. [Label] - [one-sentence justification]`);
523
+ return parts.join("\n\n");
524
+ }
525
+ function buildRevoteUserMessage(rankingsText, labelList) {
526
+ const parts = [];
527
+ parts.push("You have seen the other council members' anonymous rankings. Review them and submit your final ranking.");
528
+ parts.push(rankingsText);
529
+ parts.push(`Submit your FINAL ranking of all responses (${labelList}). Consider the other members' perspectives.
530
+
531
+ Respond in EXACTLY this format:
532
+
533
+ RANKING:
534
+ 1. [Label] - [one-sentence justification]
535
+ 2. [Label] - [one-sentence justification]
536
+ 3. [Label] - [one-sentence justification]
537
+ 4. [Label] - [one-sentence justification]`);
538
+ return parts.join("\n\n");
539
+ }
540
+
541
+ // src/utils/synthesis-parser.ts
542
+ function groupMessagesByRound(messages) {
543
+ const grouped = /* @__PURE__ */ new Map();
544
+ for (const msg of messages) {
545
+ const existing = grouped.get(msg.round) ?? [];
546
+ existing.push(msg);
547
+ grouped.set(msg.round, existing);
548
+ }
549
+ return grouped;
550
+ }
551
+ function buildSynthesisUserMessage(config, messages) {
552
+ const parts = [];
553
+ parts.push(`# Decision Topic
554
+ ${config.topic}`);
555
+ if (config.options && config.options.length > 0) {
556
+ parts.push(`# Options
557
+ ${config.options.map((o, i) => `${i + 1}. ${o}`).join("\n")}`);
558
+ }
559
+ if (config.preferredOption) {
560
+ parts.push(`# Preferred Option
561
+ ${config.preferredOption}`);
562
+ }
563
+ if (config.context) {
564
+ parts.push(`# Context
565
+ ${config.context}`);
566
+ }
567
+ const roundLabels = {
568
+ 1: "Initial Briefs",
569
+ 2: "Rebuttals",
570
+ 3: "Final Statements",
571
+ 4: "Closing Arguments",
572
+ 5: "Summary"
573
+ };
574
+ const grouped = groupMessagesByRound(messages);
575
+ const sortedRounds = [...grouped.keys()].sort((a, b) => a - b);
576
+ for (const round of sortedRounds) {
577
+ const label = roundLabels[round] ?? `Round ${round}`;
578
+ const roundMessages = grouped.get(round);
579
+ if (!roundMessages) continue;
580
+ parts.push(`## Round ${round}: ${label}`);
581
+ for (const msg of roundMessages) {
582
+ parts.push(`### ${msg.participantName}
583
+ ${msg.content}`);
584
+ }
585
+ }
586
+ return parts.join("\n\n");
587
+ }
588
+ function escapeRegex(str) {
589
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
590
+ }
591
+ function parseListSection(raw, header) {
592
+ const escaped = escapeRegex(header);
593
+ const pattern = new RegExp(
594
+ `${escaped}:\\s*\\n((?:- .+(?:\\n|$))*)`,
595
+ "i"
596
+ );
597
+ const match = pattern.exec(raw);
598
+ if (!match?.[1]) return [];
599
+ return match[1].split("\n").map((line) => line.replace(/^- /, "").trim()).filter((line) => line.length > 0);
600
+ }
601
+ function parseSynthesisResponse(raw) {
602
+ const recommendationMatch = /RECOMMENDATION:\s*(.+)/i.exec(raw);
603
+ const confidenceMatch = /CONFIDENCE:\s*(\d+)/i.exec(raw);
604
+ const reasoningMatch = /REASONING:\s*(.+(?:\n(?!RISKS:|DISSENT:|VALIDATION_GATES:|ASSUMPTIONS:).+)*)/i.exec(raw);
605
+ const recommendation = recommendationMatch?.[1]?.trim() ?? raw.slice(0, 200);
606
+ const confidence = confidenceMatch ? parseInt(confidenceMatch[1], 10) : 50;
607
+ const reasoning = reasoningMatch?.[1]?.trim() ?? raw;
608
+ const risks = parseListSection(raw, "RISKS");
609
+ const dissent = parseListSection(raw, "DISSENT");
610
+ const validationGates = parseListSection(raw, "VALIDATION_GATES");
611
+ const assumptions = parseListSection(raw, "ASSUMPTIONS");
612
+ return {
613
+ recommendation,
614
+ confidence: Math.min(100, Math.max(0, confidence)),
615
+ reasoning,
616
+ risks,
617
+ dissent,
618
+ validationGates,
619
+ assumptions,
620
+ raw
621
+ };
622
+ }
623
+
624
+ // src/synthesis/dialectical.ts
625
+ var SYSTEM_PROMPT = `You are the Synthesis Moderator for a council deliberation. You have received briefs and rebuttals from multiple council members debating a decision. Your job is to:
626
+ 1. Weigh evidence over opinion - arguments backed by specific data or precedent outweigh assertions
627
+ 2. Favor reversibility when confidence is low - if the council is split, recommend the more reversible option
628
+ 3. Synthesize, don't average - your recommendation should reflect the strongest arguments, not a compromise
629
+ 4. Calibrate confidence honestly: 90%+ = unanimous agreement with strong evidence; 70-89% = majority with minor dissent; 50-69% = significant split or unknowns; <50% = fundamental disagreement, recommend gathering more data
630
+
631
+ Respond in EXACTLY this format (no deviations):
632
+
633
+ RECOMMENDATION: [one sentence]
634
+ CONFIDENCE: [number 0-100]
635
+ REASONING: [2-3 sentences explaining the recommendation]
636
+ RISKS:
637
+ - [risk 1]
638
+ - [risk 2]
639
+ - [risk 3]
640
+ DISSENT:
641
+ - [strongest counter-argument that survived debate]
642
+ VALIDATION_GATES:
643
+ - [gate 1 - measurable, time-bound]
644
+ - [gate 2]
645
+ ASSUMPTIONS:
646
+ - [assumption 1 that must hold]
647
+ - [assumption 2]`;
648
+ var DialecticalSynthesizer = class {
649
+ model;
650
+ temperature;
651
+ constructor(options = {}) {
652
+ this.model = options.model ?? "claude-sonnet-4-20250514";
653
+ this.temperature = options.temperature ?? 0.3;
654
+ }
655
+ async synthesize(config, messages, provider) {
656
+ const userMessage = buildSynthesisUserMessage(config, messages);
657
+ const response = await provider.complete({
658
+ model: this.model,
659
+ systemPrompt: SYSTEM_PROMPT,
660
+ messages: [{ role: "user", content: userMessage }],
661
+ temperature: this.temperature
662
+ });
663
+ return parseSynthesisResponse(response.content);
664
+ }
665
+ };
666
+
667
+ // src/synthesis/chairman.ts
668
+ var SYSTEM_PROMPT2 = `You are the Chairman of a council deliberation. Your job is to evaluate the arguments presented and SELECT THE BEST ONE. Do not synthesize or merge arguments. Pick the winner.
669
+
670
+ Evaluation criteria:
671
+ 1. Evidence quality - arguments backed by specific data, precedent, or measurable outcomes score highest
672
+ 2. Risk awareness - the best argument acknowledges its own weaknesses honestly
673
+ 3. Actionability - prefer arguments with clear, concrete next steps over abstract reasoning
674
+ 4. Logical coherence - the argument's conclusion must follow from its premises
675
+
676
+ Your confidence score reflects how clear the winner is:
677
+ - 90%+ = one argument is clearly superior, others have significant flaws
678
+ - 70-89% = a clear winner exists but the runner-up has merit
679
+ - 50-69% = close race between two or more arguments
680
+ - <50% = no clear winner, recommend gathering more data
681
+
682
+ Respond in EXACTLY this format (no deviations):
683
+
684
+ RECOMMENDATION: [state which participant's argument wins and why in one sentence]
685
+ CONFIDENCE: [number 0-100]
686
+ REASONING: [2-3 sentences explaining why this argument won over the others]
687
+ RISKS:
688
+ - [risk 1 from the winning argument]
689
+ - [risk 2]
690
+ - [risk 3]
691
+ DISSENT:
692
+ - [the runner-up's strongest point that nearly won]
693
+ VALIDATION_GATES:
694
+ - [gate 1 - measurable, time-bound]
695
+ - [gate 2]
696
+ ASSUMPTIONS:
697
+ - [assumption 1 the winning argument depends on]
698
+ - [assumption 2]`;
699
+ var ChairmanSynthesizer = class {
700
+ model;
701
+ temperature;
702
+ constructor(options = {}) {
703
+ this.model = options.model ?? "claude-sonnet-4-20250514";
704
+ this.temperature = options.temperature ?? 0.3;
705
+ }
706
+ async synthesize(config, messages, provider) {
707
+ const userMessage = buildSynthesisUserMessage(config, messages);
708
+ const response = await provider.complete({
709
+ model: this.model,
710
+ systemPrompt: SYSTEM_PROMPT2,
711
+ messages: [{ role: "user", content: userMessage }],
712
+ temperature: this.temperature
713
+ });
714
+ return parseSynthesisResponse(response.content);
715
+ }
716
+ };
717
+
718
+ // src/personas/presets.ts
719
+ var proposer = {
720
+ id: "proposer",
721
+ name: "Proposer",
722
+ systemPrompt: `# Proposer \u2014 Thesis Builder
723
+
724
+ ## Role
725
+ You are the Proposer. Your job is to build the strongest possible case FOR the preferred option. You are an advocate, not a neutral analyst.
726
+
727
+ ## Cognitive Stance
728
+ Dialectical Inquiry: construct the thesis that will be tested by the other Council members. Your case must be rigorous enough to survive attack.
729
+
730
+ ## What You Must Do
731
+ 1. State the recommendation clearly \u2014 one sentence, no hedging
732
+ 2. List 3-5 supporting arguments \u2014 each backed by evidence, logic, or precedent from the decision context
733
+ 3. List explicit assumptions \u2014 what must be true for this option to succeed. Be honest. Hidden assumptions are the Challenger's ammunition.
734
+ 4. Identify the best-case outcome \u2014 what does success look like in 3, 6, 12 months?
735
+ 5. Acknowledge the weakest point \u2014 every thesis has one. Name it.
736
+
737
+ ## Output Format
738
+ ## Proposer Brief
739
+
740
+ **Recommendation:** [one sentence]
741
+
742
+ **Supporting arguments:**
743
+ 1. [argument + evidence]
744
+ 2. [argument + evidence]
745
+ 3. [argument + evidence]
746
+
747
+ **Assumptions (what must be true):**
748
+ - [assumption 1]
749
+ - [assumption 2]
750
+ - [assumption 3]
751
+
752
+ **Best-case outcome:** [description]
753
+
754
+ **Weakest point:** [honest acknowledgment]
755
+
756
+ ## Rules
757
+ - 200-400 words max
758
+ - No padding, no filler \u2014 state assertions directly
759
+ - Every argument must reference specific context (constraints, numbers, precedents)
760
+ - Do NOT address the other agents \u2014 write your brief independently`,
761
+ temperature: 0.7
762
+ };
763
+ var challenger = {
764
+ id: "challenger",
765
+ name: "Challenger",
766
+ systemPrompt: `# Challenger \u2014 Red Team Attacker
767
+
768
+ ## Role
769
+ You are the Challenger. Your job is to attack the preferred option. Find every vulnerability, hidden cost, second-order effect, and competitive risk. You are the red team.
770
+
771
+ ## Cognitive Stance
772
+ Red Team + Devil's Advocate: assume the preferred option has fatal flaws. Your job is to find them before reality does.
773
+
774
+ ## What You Must Do
775
+ 1. Attack the assumptions \u2014 take each assumption from the proposal and stress-test it. What if it's wrong?
776
+ 2. Find hidden costs \u2014 time, money, opportunity cost, relationship cost, reputation risk not in the initial analysis
777
+ 3. Identify second-order effects \u2014 what happens AFTER the decision? What does it trigger, close off, or commit to?
778
+ 4. Surface competitive risks \u2014 what can go wrong externally? Market changes, competitor moves, timing risks
779
+ 5. Ask the question they're avoiding \u2014 every decision has one uncomfortable question nobody wants to ask. Ask it.
780
+
781
+ ## Output Format
782
+ ## Challenger Brief
783
+
784
+ **Verdict:** [one sentence \u2014 is this option as strong as it looks?]
785
+
786
+ **Assumption attacks:**
787
+ - [assumption] -> [why it might be wrong]
788
+ - [assumption] -> [why it might be wrong]
789
+
790
+ **Hidden costs:**
791
+ - [cost 1]
792
+ - [cost 2]
793
+
794
+ **Second-order effects:**
795
+ - [effect \u2014 what this triggers or closes off]
796
+
797
+ **The question they're avoiding:**
798
+ [one uncomfortable question]
799
+
800
+ **Kill condition:** [the single scenario where this option catastrophically fails]
801
+
802
+ ## Rules
803
+ - 200-400 words max
804
+ - Be aggressive but honest \u2014 attack the option, not the person
805
+ - Every attack must be specific, not vague
806
+ - Do NOT propose alternatives \u2014 that's the Steelmanner's job
807
+ - Do NOT soften your attacks \u2014 the whole point is adversarial pressure`,
808
+ temperature: 0.7
809
+ };
810
+ var steelmanner = {
811
+ id: "steelmanner",
812
+ name: "Steelmanner",
813
+ systemPrompt: `# Steelmanner \u2014 Counter-Proposal Advocate
814
+
815
+ ## Role
816
+ You are the Steelmanner. Your job is to take the option the decision-maker is leaning AWAY from and build the absolute best case for it. Not criticism of the preferred option \u2014 genuine, full-throated advocacy for the alternative.
817
+
818
+ ## Cognitive Stance
819
+ Rationalist steelmanning: assume the rejected option has merits being overlooked due to anchoring bias, status quo preference, or emotional attachment to the preferred path.
820
+
821
+ ## What You Must Do
822
+ 1. Reframe the rejected option \u2014 present it in its most favorable light. What would a brilliant advocate say?
823
+ 2. Find unique advantages \u2014 what does this option offer that the preferred option cannot? Focus on exclusive benefits.
824
+ 3. Address the objections \u2014 why is the decision-maker leaning away? Take each objection and counter it with evidence or reframing.
825
+ 4. Paint the success scenario \u2014 if this option were chosen and executed well, what does the best outcome look like?
826
+ 5. Identify the regret scenario \u2014 in what future does the decision-maker wish they had chosen this path instead?
827
+
828
+ ## Output Format
829
+ ## Steelmanner Brief
830
+
831
+ **The case for [rejected option]:**
832
+ [2-3 sentence reframe \u2014 why this deserves serious consideration]
833
+
834
+ **Unique advantages:**
835
+ - [advantage 1 \u2014 something the preferred option cannot offer]
836
+ - [advantage 2]
837
+ - [advantage 3]
838
+
839
+ **Objection rebuttals:**
840
+ - "[objection]" -> [counter-argument]
841
+ - "[objection]" -> [counter-argument]
842
+
843
+ **Success scenario:** [what the best outcome looks like]
844
+
845
+ **Regret scenario:** [in what future does choosing the other path feel like a mistake?]
846
+
847
+ ## Rules
848
+ - 200-400 words max
849
+ - Genuine advocacy, not token opposition \u2014 actually try to convince
850
+ - Do NOT attack the preferred option (the Challenger does that)
851
+ - Do NOT be balanced \u2014 be biased toward the rejected option. That's the point.
852
+ - If there are multiple rejected options, steelman the strongest one`,
853
+ temperature: 0.7
854
+ };
855
+ var preMortem = {
856
+ id: "pre-mortem",
857
+ name: "Pre-Mortem",
858
+ systemPrompt: `# Pre-Mortem \u2014 Failure Narrator
859
+
860
+ ## Role
861
+ You are the Pre-Mortem analyst. Your job is to assume the preferred option was chosen, time has passed, and it has failed. Narrate the failure story \u2014 how it happened, when it went wrong, and why nobody saw it coming.
862
+
863
+ ## Cognitive Stance
864
+ Klein's Pre-Mortem: by assuming failure has already occurred, you bypass optimism bias and unlock prospective hindsight. Research shows this surfaces 30% more risks than traditional risk analysis.
865
+
866
+ ## What You Must Do
867
+ 1. Set the scene \u2014 pick a realistic timeframe (3 months? 6 months? 1 year?) and describe the moment of failure
868
+ 2. Narrate the failure story in past tense \u2014 "The decision was made on [date]. The first sign of trouble appeared when..." Write it as a retrospective.
869
+ 3. Identify the specific failure modes \u2014 not vague "it didn't work" but concrete mechanisms
870
+ 4. Trace the cascade \u2014 how did one failure lead to another? What was the domino chain?
871
+ 5. Find the turning point \u2014 the single moment where intervention could have prevented the failure. This becomes a validation gate.
872
+
873
+ ## Output Format
874
+ ## Pre-Mortem Brief
875
+
876
+ **Timeframe:** [when the failure becomes apparent]
877
+
878
+ **The failure story:**
879
+ [3-5 sentences in past tense narrating what went wrong]
880
+
881
+ **Failure modes:**
882
+ 1. [specific mechanism of failure]
883
+ 2. [specific mechanism of failure]
884
+ 3. [specific mechanism of failure]
885
+
886
+ **Cascade effect:**
887
+ [how failure mode 1 led to 2 led to 3]
888
+
889
+ **The turning point:**
890
+ [the moment where intervention would have saved it \u2014 this becomes a validation gate]
891
+
892
+ **Early warning signs:**
893
+ - [signal 1 \u2014 what to watch for]
894
+ - [signal 2 \u2014 what to watch for]
895
+
896
+ ## Rules
897
+ - 200-400 words max
898
+ - Write in past tense \u2014 the failure has already happened
899
+ - Be specific about mechanisms, not vague about outcomes
900
+ - Use real constraints from the decision context
901
+ - The failure must be plausible, not catastrophic fantasy
902
+ - Do NOT suggest solutions \u2014 the Synthesizer handles that`,
903
+ temperature: 0.7
904
+ };
905
+ var PERSONAS = {
906
+ proposer,
907
+ challenger,
908
+ steelmanner,
909
+ preMortem
910
+ };
911
+
912
+ // src/cli.ts
913
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`;
914
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
915
+ var green = (s) => `\x1B[32m${s}\x1B[0m`;
916
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
917
+ var red = (s) => `\x1B[31m${s}\x1B[0m`;
918
+ var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
919
+ var HELP = `
920
+ ${bold("llm-council")} \u2014 Multi-model deliberation engine
921
+
922
+ ${bold("USAGE")}
923
+ llm-council "Should we adopt microservices?"
924
+ llm-council --provider openrouter --model openai/gpt-4o "topic"
925
+ llm-council --config council.json "topic"
926
+
927
+ ${bold("OPTIONS")}
928
+ --provider ${dim("anthropic | openrouter")} ${dim("(default: anthropic)")}
929
+ --protocol ${dim("adversarial | peer-review")} ${dim("(default: adversarial)")}
930
+ --synthesizer ${dim("dialectical | chairman")} ${dim("(default: dialectical)")}
931
+ --model ${dim("model ID for all participants")} ${dim("(default: provider-dependent)")}
932
+ --revote ${dim("enable 3rd round re-vote (peer-review only)")}
933
+ --config ${dim("path to JSON config file")}
934
+ --help ${dim("show this help")}
935
+
936
+ ${bold("ENVIRONMENT")}
937
+ ANTHROPIC_API_KEY Required when --provider=anthropic
938
+ OPENROUTER_API_KEY Required when --provider=openrouter
939
+
940
+ ${bold("CONFIG FILE")}
941
+ {
942
+ "provider": "openrouter",
943
+ "protocol": "adversarial",
944
+ "synthesizer": "dialectical",
945
+ "model": "openai/gpt-4o"
946
+ }
947
+ `;
948
+ function resolveConfig(args) {
949
+ const { values, positionals } = parseArgs({
950
+ args,
951
+ options: {
952
+ provider: { type: "string", default: "anthropic" },
953
+ protocol: { type: "string", default: "adversarial" },
954
+ synthesizer: { type: "string", default: "dialectical" },
955
+ model: { type: "string" },
956
+ revote: { type: "boolean", default: false },
957
+ config: { type: "string" },
958
+ help: { type: "boolean", default: false }
959
+ },
960
+ allowPositionals: true
961
+ });
962
+ if (values.help) {
963
+ process.stdout.write(HELP);
964
+ process.exit(0);
965
+ }
966
+ let fileConfig = {};
967
+ if (values.config) {
968
+ const raw = readFileSync(values.config, "utf-8");
969
+ fileConfig = JSON.parse(raw);
970
+ }
971
+ const providerName = values.provider ?? fileConfig.provider ?? "anthropic";
972
+ const protocolName = values.protocol ?? fileConfig.protocol ?? "adversarial";
973
+ const synthesizerName = values.synthesizer ?? fileConfig.synthesizer ?? "dialectical";
974
+ const modelOverride = values.model ?? fileConfig.model;
975
+ const topic = positionals[0];
976
+ if (!topic) {
977
+ process.stderr.write(red('Error: topic is required. Usage: llm-council "your topic"\n'));
978
+ process.exit(1);
979
+ }
980
+ const provider = resolveProvider(providerName, modelOverride);
981
+ const enableRevote = values.revote ?? false;
982
+ const protocol = resolveProtocol(protocolName, enableRevote);
983
+ const synthesizer = resolveSynthesizer(synthesizerName, modelOverride);
984
+ const model = modelOverride ?? getDefaultModel(providerName);
985
+ const participants = [
986
+ { ...PERSONAS.proposer, provider: providerName, model },
987
+ { ...PERSONAS.challenger, provider: providerName, model },
988
+ { ...PERSONAS.steelmanner, provider: providerName, model },
989
+ { ...PERSONAS.preMortem, provider: providerName, model }
990
+ ];
991
+ const rounds = protocol.getRoundCount();
992
+ return { provider, protocol, synthesizer, participants, topic, rounds };
993
+ }
994
+ function resolveProvider(name, modelOverride) {
995
+ switch (name) {
996
+ case "anthropic": {
997
+ const apiKey = process.env.ANTHROPIC_API_KEY;
998
+ if (!apiKey) {
999
+ process.stderr.write(red("Error: ANTHROPIC_API_KEY environment variable is required\n"));
1000
+ process.exit(1);
1001
+ }
1002
+ return new AnthropicProvider({
1003
+ apiKey,
1004
+ ...modelOverride && { defaultModel: modelOverride }
1005
+ });
1006
+ }
1007
+ case "openrouter": {
1008
+ const apiKey = process.env.OPENROUTER_API_KEY;
1009
+ if (!apiKey) {
1010
+ process.stderr.write(red("Error: OPENROUTER_API_KEY environment variable is required\n"));
1011
+ process.exit(1);
1012
+ }
1013
+ return new OpenRouterProvider({
1014
+ apiKey,
1015
+ appName: "llm-council",
1016
+ ...modelOverride && { defaultModel: modelOverride }
1017
+ });
1018
+ }
1019
+ default:
1020
+ process.stderr.write(red(`Error: unknown provider "${name}". Use: anthropic, openrouter
1021
+ `));
1022
+ process.exit(1);
1023
+ }
1024
+ }
1025
+ function resolveProtocol(name, enableRevote) {
1026
+ switch (name) {
1027
+ case "adversarial":
1028
+ return new AdversarialProtocol();
1029
+ case "peer-review":
1030
+ return new PeerReviewProtocol({ enableRevote });
1031
+ default:
1032
+ process.stderr.write(red(`Error: unknown protocol "${name}". Use: adversarial, peer-review
1033
+ `));
1034
+ process.exit(1);
1035
+ }
1036
+ }
1037
+ function resolveSynthesizer(name, modelOverride) {
1038
+ switch (name) {
1039
+ case "dialectical":
1040
+ return new DialecticalSynthesizer(modelOverride ? { model: modelOverride } : {});
1041
+ case "chairman":
1042
+ return new ChairmanSynthesizer(modelOverride ? { model: modelOverride } : {});
1043
+ default:
1044
+ process.stderr.write(red(`Error: unknown synthesizer "${name}". Use: dialectical, chairman
1045
+ `));
1046
+ process.exit(1);
1047
+ }
1048
+ }
1049
+ function getDefaultModel(providerName) {
1050
+ switch (providerName) {
1051
+ case "anthropic":
1052
+ return "claude-sonnet-4-20250514";
1053
+ case "openrouter":
1054
+ return "openai/gpt-4o";
1055
+ default:
1056
+ return "claude-sonnet-4-20250514";
1057
+ }
1058
+ }
1059
+ async function main() {
1060
+ const resolved = resolveConfig(process.argv.slice(2));
1061
+ const config = {
1062
+ topic: resolved.topic,
1063
+ participants: resolved.participants,
1064
+ rounds: resolved.rounds
1065
+ };
1066
+ const providerName = resolved.participants[0].provider;
1067
+ const providers = /* @__PURE__ */ new Map([[providerName, resolved.provider]]);
1068
+ const council = new Council({
1069
+ providers,
1070
+ protocol: resolved.protocol,
1071
+ synthesizer: resolved.synthesizer
1072
+ });
1073
+ process.stdout.write(`
1074
+ ${bold("LLM Council")} \u2014 deliberating on: ${cyan(config.topic)}
1075
+
1076
+ `);
1077
+ council.on("round:start", ({ round, participantCount }) => {
1078
+ process.stdout.write(cyan(`Round ${round} \u2014 ${participantCount} participants
1079
+ `));
1080
+ });
1081
+ council.on("response", (msg) => {
1082
+ process.stdout.write(dim(` ${msg.participantName} responded (${msg.tokenCount.output} tokens)
1083
+ `));
1084
+ });
1085
+ council.on("synthesis:start", () => {
1086
+ process.stdout.write(yellow("\nSynthesizing...\n"));
1087
+ });
1088
+ const result = await council.deliberate(config);
1089
+ process.stdout.write(`
1090
+ ${bold(green("RECOMMENDATION"))}
1091
+ `);
1092
+ process.stdout.write(`${result.synthesis.recommendation}
1093
+
1094
+ `);
1095
+ process.stdout.write(`${bold("Confidence:")} ${formatConfidence(result.synthesis.confidence)}
1096
+
1097
+ `);
1098
+ process.stdout.write(`${bold("Reasoning:")}
1099
+ ${result.synthesis.reasoning}
1100
+
1101
+ `);
1102
+ if (result.synthesis.risks.length > 0) {
1103
+ process.stdout.write(`${bold("Risks:")}
1104
+ `);
1105
+ for (const risk of result.synthesis.risks) {
1106
+ process.stdout.write(` ${red("!")} ${risk}
1107
+ `);
1108
+ }
1109
+ process.stdout.write("\n");
1110
+ }
1111
+ if (result.synthesis.dissent.length > 0) {
1112
+ process.stdout.write(`${bold("Dissent:")}
1113
+ `);
1114
+ for (const d of result.synthesis.dissent) {
1115
+ process.stdout.write(` ${yellow(">")} ${d}
1116
+ `);
1117
+ }
1118
+ process.stdout.write("\n");
1119
+ }
1120
+ if (result.synthesis.validationGates.length > 0) {
1121
+ process.stdout.write(`${bold("Validation Gates:")}
1122
+ `);
1123
+ for (const gate of result.synthesis.validationGates) {
1124
+ process.stdout.write(` ${green("*")} ${gate}
1125
+ `);
1126
+ }
1127
+ process.stdout.write("\n");
1128
+ }
1129
+ const total = result.metadata.totalTokens;
1130
+ const durationSec = (result.metadata.durationMs / 1e3).toFixed(1);
1131
+ process.stdout.write(dim(`Tokens: ${total.input} in / ${total.output} out | Duration: ${durationSec}s
1132
+ `));
1133
+ }
1134
+ function formatConfidence(confidence) {
1135
+ if (confidence >= 90) return green(`${confidence}% (high)`);
1136
+ if (confidence >= 70) return green(`${confidence}% (moderate-high)`);
1137
+ if (confidence >= 50) return yellow(`${confidence}% (moderate)`);
1138
+ return red(`${confidence}% (low)`);
1139
+ }
1140
+ var isTesting = process.env.VITEST === "true" || process.env.NODE_ENV === "test";
1141
+ if (!isTesting) {
1142
+ main().catch((error) => {
1143
+ process.stderr.write(red(`
1144
+ Error: ${error.message}
1145
+ `));
1146
+ process.exit(1);
1147
+ });
1148
+ }
1149
+ export {
1150
+ resolveConfig
1151
+ };