dialectic 0.1.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.
Files changed (119) hide show
  1. package/.cursor/commands/setup-test.mdc +175 -0
  2. package/.cursor/rules/basic-code-cleanup.mdc +1110 -0
  3. package/.cursor/rules/riper5.mdc +96 -0
  4. package/.env.example +6 -0
  5. package/AGENTS.md +1052 -0
  6. package/LICENSE +21 -0
  7. package/README.md +93 -0
  8. package/WARP.md +113 -0
  9. package/dialectic-1.0.0.tgz +0 -0
  10. package/dialectic.js +10 -0
  11. package/docs/commands.md +375 -0
  12. package/docs/configuration.md +882 -0
  13. package/docs/context_summarization.md +1023 -0
  14. package/docs/debate_flow.md +1127 -0
  15. package/docs/eval_flow.md +795 -0
  16. package/docs/evaluator.md +141 -0
  17. package/examples/debate-config-openrouter.json +48 -0
  18. package/examples/debate_config1.json +48 -0
  19. package/examples/eval/eval1/eval_config1.json +13 -0
  20. package/examples/eval/eval1/result1.json +62 -0
  21. package/examples/eval/eval1/result2.json +97 -0
  22. package/examples/eval_summary_format.md +11 -0
  23. package/examples/example3/debate-config.json +64 -0
  24. package/examples/example3/eval_config2.json +25 -0
  25. package/examples/example3/problem.md +17 -0
  26. package/examples/example3/rounds_test/eval_run.sh +16 -0
  27. package/examples/example3/rounds_test/run_test.sh +16 -0
  28. package/examples/kata1/architect-only-solution_2-rounds.json +121 -0
  29. package/examples/kata1/architect-perf-solution_2-rounds.json +234 -0
  30. package/examples/kata1/debate-config-kata1.json +54 -0
  31. package/examples/kata1/eval_architect-only_2-rounds.json +97 -0
  32. package/examples/kata1/eval_architect-perf_2-rounds.json +97 -0
  33. package/examples/kata1/kata1-report.md +12224 -0
  34. package/examples/kata1/kata1-report_temps-01_01_01_07.md +2451 -0
  35. package/examples/kata1/kata1.md +5 -0
  36. package/examples/kata1/meta.txt +1 -0
  37. package/examples/kata2/debate-config.json +54 -0
  38. package/examples/kata2/eval_config1.json +21 -0
  39. package/examples/kata2/eval_config2.json +25 -0
  40. package/examples/kata2/kata2.md +5 -0
  41. package/examples/kata2/only_architect/debate-config.json +45 -0
  42. package/examples/kata2/only_architect/eval_run.sh +11 -0
  43. package/examples/kata2/only_architect/run_test.sh +5 -0
  44. package/examples/kata2/rounds_test/eval_run.sh +11 -0
  45. package/examples/kata2/rounds_test/run_test.sh +5 -0
  46. package/examples/kata2/summary_length_test/eval_run.sh +11 -0
  47. package/examples/kata2/summary_length_test/eval_run_w_clarify.sh +7 -0
  48. package/examples/kata2/summary_length_test/run_test.sh +5 -0
  49. package/examples/task-queue/debate-config.json +76 -0
  50. package/examples/task-queue/debate_report.md +566 -0
  51. package/examples/task-queue/task-queue-system.md +25 -0
  52. package/jest.config.ts +13 -0
  53. package/multi_agent_debate_spec.md +2980 -0
  54. package/package.json +38 -0
  55. package/sanity-check-problem.txt +9 -0
  56. package/src/agents/prompts/architect-prompts.ts +203 -0
  57. package/src/agents/prompts/generalist-prompts.ts +157 -0
  58. package/src/agents/prompts/index.ts +41 -0
  59. package/src/agents/prompts/judge-prompts.ts +19 -0
  60. package/src/agents/prompts/kiss-prompts.ts +230 -0
  61. package/src/agents/prompts/performance-prompts.ts +142 -0
  62. package/src/agents/prompts/prompt-types.ts +68 -0
  63. package/src/agents/prompts/security-prompts.ts +149 -0
  64. package/src/agents/prompts/shared.ts +144 -0
  65. package/src/agents/prompts/testing-prompts.ts +149 -0
  66. package/src/agents/role-based-agent.ts +386 -0
  67. package/src/cli/commands/debate.ts +761 -0
  68. package/src/cli/commands/eval.ts +475 -0
  69. package/src/cli/commands/report.ts +265 -0
  70. package/src/cli/index.ts +79 -0
  71. package/src/core/agent.ts +198 -0
  72. package/src/core/clarifications.ts +34 -0
  73. package/src/core/judge.ts +257 -0
  74. package/src/core/orchestrator.ts +432 -0
  75. package/src/core/state-manager.ts +322 -0
  76. package/src/eval/evaluator-agent.ts +130 -0
  77. package/src/eval/prompts/system.md +41 -0
  78. package/src/eval/prompts/user.md +64 -0
  79. package/src/providers/llm-provider.ts +25 -0
  80. package/src/providers/openai-provider.ts +84 -0
  81. package/src/providers/openrouter-provider.ts +122 -0
  82. package/src/providers/provider-factory.ts +64 -0
  83. package/src/types/agent.types.ts +141 -0
  84. package/src/types/config.types.ts +47 -0
  85. package/src/types/debate.types.ts +237 -0
  86. package/src/types/eval.types.ts +85 -0
  87. package/src/utils/common.ts +104 -0
  88. package/src/utils/context-formatter.ts +102 -0
  89. package/src/utils/context-summarizer.ts +143 -0
  90. package/src/utils/env-loader.ts +46 -0
  91. package/src/utils/exit-codes.ts +5 -0
  92. package/src/utils/id.ts +11 -0
  93. package/src/utils/logger.ts +48 -0
  94. package/src/utils/paths.ts +10 -0
  95. package/src/utils/progress-ui.ts +313 -0
  96. package/src/utils/prompt-loader.ts +79 -0
  97. package/src/utils/report-generator.ts +301 -0
  98. package/tests/clarifications.spec.ts +128 -0
  99. package/tests/cli.debate.spec.ts +144 -0
  100. package/tests/config-loading.spec.ts +206 -0
  101. package/tests/context-summarizer.spec.ts +131 -0
  102. package/tests/debate-config-custom.json +38 -0
  103. package/tests/env-loader.spec.ts +149 -0
  104. package/tests/eval.command.spec.ts +1191 -0
  105. package/tests/logger.spec.ts +19 -0
  106. package/tests/openai-provider.spec.ts +26 -0
  107. package/tests/openrouter-provider.spec.ts +279 -0
  108. package/tests/orchestrator-summary.spec.ts +386 -0
  109. package/tests/orchestrator.spec.ts +207 -0
  110. package/tests/prompt-loader.spec.ts +52 -0
  111. package/tests/prompts/architect.md +16 -0
  112. package/tests/provider-factory.spec.ts +150 -0
  113. package/tests/report.command.spec.ts +546 -0
  114. package/tests/role-based-agent-summary.spec.ts +476 -0
  115. package/tests/security-agent.spec.ts +221 -0
  116. package/tests/shared-prompts.spec.ts +318 -0
  117. package/tests/state-manager.spec.ts +251 -0
  118. package/tests/summary-prompts.spec.ts +153 -0
  119. package/tsconfig.json +49 -0
@@ -0,0 +1,313 @@
1
+ import { ContributionType, CONTRIBUTION_TYPES } from '../types/debate.types';
2
+ import { writeStderr } from '../cli/index';
3
+
4
+ // Lazy load chalk for optional color support
5
+ let chalk: any;
6
+ try {
7
+ chalk = require('chalk');
8
+ } catch {
9
+ // If chalk is not available, create a pass-through mock
10
+ chalk = new Proxy({}, {
11
+ get: () => (text: string) => text
12
+ });
13
+ }
14
+
15
+ // Constants for UI strings and configuration
16
+ const PHASE_LABELS = {
17
+ [CONTRIBUTION_TYPES.PROPOSAL]: 'Proposals',
18
+ [CONTRIBUTION_TYPES.CRITIQUE]: 'Critiques',
19
+ [CONTRIBUTION_TYPES.REFINEMENT]: 'Refinements',
20
+ } as const;
21
+
22
+ const SYNTHESIS_LABEL = 'Synthesis';
23
+
24
+ // ANSI escape codes for terminal control
25
+ const ANSI_MOVE_UP = '\x1b[1A'; // Move cursor up one line
26
+ const ANSI_CLEAR_LINE = '\x1b[2K'; // Clear entire line
27
+
28
+ // UI styling constants
29
+ const SPINNER_ICON = '⠋';
30
+ const SUMMARIZATION_SECTION_LABEL = 'Summarization';
31
+ const COLOR_STRUCTURE = chalk.blue; // Blue for lines and round header
32
+ const COLOR_SPINNER = chalk.cyan; // Cyan (lighter blue) for spinner icon
33
+
34
+ // Progress state tracking
35
+ interface ProgressState {
36
+ currentRound: number;
37
+ currentPhase: string;
38
+ /** Map of agent names to their list of current activities (e.g., ["proposing", "critiquing architect"]) */
39
+ agentActivity: Map<string, string[]>;
40
+ /** Map of phase keys to progress counts (e.g., "1-proposal" -> { current: 2, total: 3 }) */
41
+ phaseProgress: Map<string, { current: number; total: number }>;
42
+ }
43
+
44
+ /**
45
+ * DebateProgressUI manages the real-time progress display for debate execution.
46
+ *
47
+ * This class provides a simple text-based progress indicator that shows:
48
+ * - Current round and phase
49
+ * - Individual agent activities
50
+ * - Progress counts for each phase
51
+ *
52
+ * The UI writes to stderr to maintain separation from stdout.
53
+ * Output is designed to be simple and informative without complex rendering.
54
+ */
55
+ export class DebateProgressUI {
56
+ private state: ProgressState;
57
+ private totalRounds: number = 0;
58
+ private lastOutput: string = '';
59
+
60
+ constructor() {
61
+ this.state = {
62
+ currentRound: 0,
63
+ currentPhase: '',
64
+ agentActivity: new Map<string, string[]>(),
65
+ phaseProgress: new Map(),
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Initializes the progress UI with debate configuration.
71
+ *
72
+ * @param totalRounds - Total number of rounds in the debate.
73
+ */
74
+ initialize(totalRounds: number): void {
75
+ this.totalRounds = totalRounds;
76
+ }
77
+
78
+ /**
79
+ * Starts the progress UI display.
80
+ * Must be called after initialize() and before any round/phase updates.
81
+ */
82
+ async start(): Promise<void> {
83
+ // Clear any previous output
84
+ this.clearOutput();
85
+ }
86
+
87
+ /**
88
+ * Writes a log message to stderr without leaving orphaned UI artifacts.
89
+ * Clears the current UI, writes the message, then redraws the UI.
90
+ */
91
+ log(message: string): void {
92
+ this.clearOutput();
93
+ const text = message.endsWith('\n') ? message : `${message}\n`;
94
+ writeStderr(text);
95
+ this.updateDisplay();
96
+ }
97
+
98
+ /**
99
+ * Signals the start of a new debate round.
100
+ *
101
+ * @param roundNumber - The round number (1-indexed).
102
+ */
103
+ startRound(roundNumber: number): void {
104
+ this.state.currentRound = roundNumber;
105
+ this.state.currentPhase = '';
106
+ this.updateDisplay();
107
+ }
108
+
109
+ /**
110
+ * Signals the start of a phase within the current round.
111
+ *
112
+ * @param phase - The phase type (proposal, critique, or refinement).
113
+ * @param expectedAgentCount - Expected number of agent tasks in this phase.
114
+ */
115
+ startPhase(phase: ContributionType, expectedAgentCount: number): void {
116
+ const phaseLabel = PHASE_LABELS[phase];
117
+ this.state.currentPhase = phaseLabel;
118
+ const phaseKey = `${this.state.currentRound}-${phase}`;
119
+ this.state.phaseProgress.set(phaseKey, { current: 0, total: expectedAgentCount });
120
+ this.state.agentActivity.clear();
121
+ this.updateDisplay();
122
+ }
123
+
124
+ /**
125
+ * Signals that an agent has started an activity.
126
+ *
127
+ * @param agentName - The name of the agent.
128
+ * @param activity - Description of the activity (e.g., "proposing", "critiquing architect"). Multiple activities per agent are supported.
129
+ */
130
+ startAgentActivity(agentName: string, activity: string): void {
131
+ // Append activity into Map<agentName, activities[]>
132
+ const activities = this.state.agentActivity.get(agentName) ?? [];
133
+ activities.push(activity);
134
+ this.state.agentActivity.set(agentName, activities);
135
+ this.updateDisplay();
136
+ }
137
+
138
+ /**
139
+ * Signals that an agent has completed an activity.
140
+ *
141
+ * @param agentName - The name of the agent.
142
+ * @param activity - Description of the activity to complete. Removes a single matching occurrence.
143
+ */
144
+ completeAgentActivity(agentName: string, activity: string): void {
145
+ // Remove a single occurrence of the activity from the agent's list; delete key if list becomes empty
146
+ const activities = this.state.agentActivity.get(agentName);
147
+ if (activities && activities.length > 0) {
148
+ const idx = activities.indexOf(activity);
149
+ if (idx >= 0) {
150
+ activities.splice(idx, 1);
151
+ }
152
+ if (activities.length === 0) {
153
+ this.state.agentActivity.delete(agentName);
154
+ } else {
155
+ this.state.agentActivity.set(agentName, activities);
156
+ }
157
+ }
158
+
159
+ // Update phase progress
160
+ const currentPhase = this.state.currentPhase.toLowerCase();
161
+ const phaseType = Object.keys(PHASE_LABELS).find(
162
+ key => PHASE_LABELS[key as ContributionType].toLowerCase() === currentPhase
163
+ ) as ContributionType | undefined;
164
+
165
+ if (phaseType) {
166
+ const phaseKey = `${this.state.currentRound}-${phaseType}`;
167
+ const progress = this.state.phaseProgress.get(phaseKey);
168
+ if (progress) {
169
+ progress.current++;
170
+ this.state.phaseProgress.set(phaseKey, progress);
171
+ }
172
+ }
173
+
174
+ this.updateDisplay();
175
+ }
176
+
177
+ /**
178
+ * Signals that a phase has completed.
179
+ *
180
+ * @param _phase - The phase type that completed (reserved for future use).
181
+ */
182
+ completePhase(_phase: ContributionType): void {
183
+ this.state.currentPhase = '';
184
+ this.state.agentActivity.clear();
185
+ this.updateDisplay();
186
+ }
187
+
188
+ /**
189
+ * Signals the start of the synthesis phase.
190
+ */
191
+ startSynthesis(): void {
192
+ this.state.currentPhase = SYNTHESIS_LABEL;
193
+ this.state.agentActivity.clear();
194
+ this.updateDisplay();
195
+ }
196
+
197
+ /**
198
+ * Signals that synthesis has completed.
199
+ */
200
+ completeSynthesis(): void {
201
+ this.state.currentPhase = '';
202
+ this.updateDisplay();
203
+ }
204
+
205
+ /**
206
+ * Completes the entire progress UI.
207
+ * Should be called when the debate finishes.
208
+ */
209
+ async complete(): Promise<void> {
210
+ this.clearOutput();
211
+ }
212
+
213
+ /**
214
+ * Handles errors in the progress UI.
215
+ *
216
+ * @param _error - The error that occurred (reserved for future use).
217
+ */
218
+ handleError(_error: Error): void {
219
+ // Error handling can be added here if needed
220
+ this.clearOutput();
221
+ }
222
+
223
+ /**
224
+ * Updates the progress display with current state.
225
+ */
226
+ private updateDisplay(): void {
227
+ let output = this.buildProgressText();
228
+
229
+ // Only update if output has changed
230
+ if (output !== this.lastOutput) {
231
+ this.clearOutput();
232
+ writeStderr(output);
233
+ this.lastOutput = output;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Builds the progress text from current state.
239
+ */
240
+ private buildProgressText(): string {
241
+ if (this.state.currentRound === 0) {
242
+ return '';
243
+ }
244
+
245
+ const lines: string[] = [];
246
+
247
+ // Round progress with blue color
248
+ lines.push(COLOR_STRUCTURE(`┌─ Round ${this.state.currentRound}/${this.totalRounds}`));
249
+
250
+ // Current phase
251
+ if (this.state.currentPhase) {
252
+ const currentPhase = this.state.currentPhase.toLowerCase();
253
+ const phaseType = Object.keys(PHASE_LABELS).find(
254
+ key => PHASE_LABELS[key as ContributionType].toLowerCase() === currentPhase
255
+ ) as ContributionType | undefined;
256
+
257
+ let phaseText = `${this.state.currentPhase}`;
258
+
259
+ if (phaseType) {
260
+ const phaseKey = `${this.state.currentRound}-${phaseType}`;
261
+ const progress = this.state.phaseProgress.get(phaseKey);
262
+ if (progress) {
263
+ phaseText += ` (${progress.current}/${progress.total})`;
264
+ }
265
+ }
266
+
267
+ lines.push(COLOR_STRUCTURE('│ ') + phaseText);
268
+
269
+ // Active agent activities with colored spinner
270
+ if (this.state.agentActivity.size > 0) {
271
+ // Map.forEach callback receives (value, key) = (activities[], agentName)
272
+ this.state.agentActivity.forEach((activities, agentName) => {
273
+ activities.forEach((activity) => {
274
+ lines.push(COLOR_STRUCTURE('│ ') + COLOR_SPINNER(SPINNER_ICON) + ` ${agentName} ${activity}...`);
275
+ });
276
+ });
277
+ }
278
+ } else if (this.state.agentActivity.size > 0) {
279
+ // No active phase, but there are activities (e.g., summarization)
280
+ lines.push(COLOR_STRUCTURE('│ ') + SUMMARIZATION_SECTION_LABEL);
281
+ this.state.agentActivity.forEach((activities, agentName) => {
282
+ activities.forEach((activity) => {
283
+ lines.push(COLOR_STRUCTURE('│ ') + COLOR_SPINNER(SPINNER_ICON) + ` ${agentName} ${activity}...`);
284
+ });
285
+ });
286
+ }
287
+
288
+ lines.push(COLOR_STRUCTURE('└─'));
289
+
290
+ // Return with newline at end so cursor is on next line
291
+ return lines.join('\n') + '\n';
292
+ }
293
+
294
+ /**
295
+ * Clears the previous output from the terminal.
296
+ */
297
+ private clearOutput(): void {
298
+ if (this.lastOutput) {
299
+ // Count lines in last output (number of newlines = number of lines)
300
+ const lineCount = (this.lastOutput.match(/\n/g) || []).length;
301
+
302
+ // Move cursor up and clear each line
303
+ // Since output ends with \n, cursor is on empty line after content
304
+ // We need to clear that line first, then move up and clear each content line
305
+ for (let i = 0; i < lineCount; i++) {
306
+ writeStderr(ANSI_MOVE_UP);
307
+ writeStderr(ANSI_CLEAR_LINE);
308
+ }
309
+
310
+ this.lastOutput = '';
311
+ }
312
+ }
313
+ }
@@ -0,0 +1,79 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { PromptSource, PROMPT_SOURCES } from '../types/agent.types';
4
+ import { warnUser } from '../cli/index';
5
+
6
+ export type PromptResolveResult = PromptSource & { text: string };
7
+
8
+ /**
9
+ * Reads a built-in prompt file with fallback support for different execution contexts.
10
+ *
11
+ * This function attempts to load a prompt file from the project source directory.
12
+ * It first tries to resolve relative to the compiled dist/ directory (for runtime),
13
+ * then falls back to resolving from the project root src/ directory (for tests).
14
+ * If both attempts fail, it returns the provided fallback text.
15
+ *
16
+ * @param relativePathFromSrc - Path relative to the src/ directory (e.g., 'eval/prompts/system.md')
17
+ * @param fallbackText - Text to return if the file cannot be read
18
+ * @returns The contents of the prompt file, or the fallback text if unavailable
19
+ */
20
+ export function readBuiltInPrompt(relativePathFromSrc: string, fallbackText: string): string {
21
+ try {
22
+ // Primary attempt: resolve from dist/ directory (../../ climbs out of dist/utils/ back to dist/)
23
+ return fs.readFileSync(path.resolve(__dirname, '../../', relativePathFromSrc), 'utf-8');
24
+ } catch (_e1) {
25
+ try {
26
+ // Secondary attempt: resolve from project root src/ directory (useful under ts-jest)
27
+ return fs.readFileSync(path.resolve(process.cwd(), 'src', relativePathFromSrc), 'utf-8');
28
+ } catch (_e2) {
29
+ return fallbackText;
30
+ }
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Resolves the system prompt text for an agent or judge, either from a specified file or from a built-in default.
36
+ *
37
+ * This function attempts to load a system prompt from a file path if provided. If the file is missing, unreadable,
38
+ * not a file, or empty (after trimming), it will warn the user and fall back to the provided default text.
39
+ * Relative file paths are resolved against the given configuration directory.
40
+ *
41
+ * @param params - An object containing:
42
+ * - label: A human-readable label for the agent/judge (used in warning messages).
43
+ * - configDir: The directory to resolve relative prompt file paths against.
44
+ * - promptPath: (Optional) The path to the prompt file (absolute or relative to configDir).
45
+ * - defaultText:The default prompt text to use if the file is not usable.
46
+ *
47
+ * @returns An object containing:
48
+ * - text: The resolved prompt text (from file or default).
49
+ * - source: The source of the prompt ('file' or 'built-in').
50
+ * - absPath: (If source is 'file') The absolute path to the prompt file.
51
+ */
52
+ export function resolvePrompt(params: { label: string; configDir: string; promptPath?: string; defaultText: string }): PromptResolveResult {
53
+ const { label, configDir, promptPath, defaultText } = params;
54
+ if (!promptPath || String(promptPath).trim().length === 0) {
55
+ return { text: defaultText, source: PROMPT_SOURCES.BUILT_IN };
56
+ }
57
+ const abs = path.isAbsolute(promptPath) ? promptPath : path.resolve(configDir, promptPath);
58
+ try {
59
+ if (!fs.existsSync(abs)) {
60
+ warnUser(`System prompt file not usable for ${label} at ${abs}. Falling back to built-in default.`);
61
+ return { text: defaultText, source: PROMPT_SOURCES.BUILT_IN };
62
+ }
63
+ const stat = fs.statSync(abs);
64
+ if (!stat.isFile()) {
65
+ warnUser(`System prompt file not usable for ${label} at ${abs}. Falling back to built-in default.`);
66
+ return { text: defaultText, source: PROMPT_SOURCES.BUILT_IN };
67
+ }
68
+ const raw = fs.readFileSync(abs, 'utf-8');
69
+ const trimmed = raw.trim();
70
+ if (trimmed.length === 0) {
71
+ warnUser(`System prompt file not usable for ${label} at ${abs}. Falling back to built-in default.`);
72
+ return { text: defaultText, source: PROMPT_SOURCES.BUILT_IN };
73
+ }
74
+ return { text: raw, source: PROMPT_SOURCES.FILE, absPath: abs };
75
+ } catch (_err) {
76
+ warnUser(`System prompt file not usable for ${label} at ${abs}. Falling back to built-in default.`);
77
+ return { text: defaultText, source: PROMPT_SOURCES.BUILT_IN };
78
+ }
79
+ }
@@ -0,0 +1,301 @@
1
+ import { DebateState, Contribution, CONTRIBUTION_TYPES } from '../types/debate.types';
2
+ import { AgentConfig } from '../types/agent.types';
3
+
4
+ // File-level constants to avoid magic strings and improve maintainability
5
+ const CODE_FENCE_LANG = 'text';
6
+ const NO_PROPOSALS_MSG = 'No proposals in this round.';
7
+ const NO_CRITIQUES_MSG = 'No critiques in this round.';
8
+ const NO_REFINEMENTS_MSG = 'No refinements in this round.';
9
+ const NA_TEXT = 'N/A';
10
+ const RIGHT_ARROW_HTML = '&rarr;';
11
+ const UNKNOWN_LABEL = 'unknown';
12
+
13
+ /**
14
+ * Formats a date to YYYY-MM-DD HH:mm:ss local time string.
15
+ * @param date - The date to format.
16
+ * @returns Formatted date string.
17
+ */
18
+ function formatLocalTime(date: Date): string {
19
+ const pad = (n: number) => n.toString().padStart(2, '0');
20
+ const year = date.getFullYear();
21
+ const month = pad(date.getMonth() + 1);
22
+ const day = pad(date.getDate());
23
+ const hours = pad(date.getHours());
24
+ const minutes = pad(date.getMinutes());
25
+ const seconds = pad(date.getSeconds());
26
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
27
+ }
28
+
29
+ /**
30
+ * Extracts the first non-empty line from a text string.
31
+ * @param text - The text to extract from.
32
+ * @returns The first non-empty line, or empty string if none found.
33
+ */
34
+ function extractFirstLine(text: string): string {
35
+ const lines = text.split('\n');
36
+ for (const line of lines) {
37
+ if (line.trim().length > 0) {
38
+ return line.trim();
39
+ }
40
+ }
41
+ return '';
42
+ }
43
+
44
+ /**
45
+ * Formats agent configuration as a markdown table.
46
+ * @param agents - Array of agent configurations.
47
+ * @returns Markdown table string.
48
+ */
49
+ function formatAgentsTable(agents: AgentConfig[]): string {
50
+ if (agents.length === 0) {
51
+ return 'No agents configured.';
52
+ }
53
+
54
+ // Include all defined fields as requested (omit undefined values in cells by rendering 'N/A')
55
+ let table = '| ID | Name | Role | Model | Provider | Temperature | Enabled | SystemPromptPath | SummaryPromptPath | Summarization |\n';
56
+ table += '|----|------|------|-------|----------|-------------|----------|------------------|-------------------|---------------|\n';
57
+
58
+ for (const agent of agents) {
59
+ const id = agent.id || NA_TEXT;
60
+ const name = agent.name || NA_TEXT;
61
+ const role = agent.role || NA_TEXT;
62
+ const model = agent.model || NA_TEXT;
63
+ const provider = agent.provider || NA_TEXT;
64
+ const temperature = agent.temperature !== undefined ? agent.temperature.toString() : NA_TEXT;
65
+ const enabled = agent.enabled !== undefined ? agent.enabled.toString() : NA_TEXT;
66
+ const systemPromptPath = agent.systemPromptPath !== undefined ? String(agent.systemPromptPath) : NA_TEXT;
67
+ const summaryPromptPath = agent.summaryPromptPath !== undefined ? String(agent.summaryPromptPath) : NA_TEXT;
68
+ const summarization = agent.summarization !== undefined ? JSON.stringify(agent.summarization) : NA_TEXT;
69
+
70
+ table += `| ${id} | ${name} | ${role} | ${model} | ${provider} | ${temperature} | ${enabled} | ${systemPromptPath} | ${summaryPromptPath} | ${summarization} |\n`;
71
+ }
72
+
73
+ return table;
74
+ }
75
+
76
+ /**
77
+ * Formats judge configuration as a markdown table.
78
+ * @param judge - Judge configuration.
79
+ * @returns Markdown table string.
80
+ */
81
+ function formatJudgeTable(judge: AgentConfig): string {
82
+ // Use the same columns as agents for consistency
83
+ let table = '| ID | Name | Role | Model | Provider | Temperature | Enabled | SystemPromptPath | SummaryPromptPath | Summarization |\n';
84
+ table += '|----|------|------|-------|----------|-------------|----------|------------------|-------------------|---------------|\n';
85
+
86
+ const id = judge.id || NA_TEXT;
87
+ const name = judge.name || NA_TEXT;
88
+ const role = judge.role || NA_TEXT;
89
+ const model = judge.model || NA_TEXT;
90
+ const provider = judge.provider || NA_TEXT;
91
+ const temperature = judge.temperature !== undefined ? judge.temperature.toString() : NA_TEXT;
92
+ // Guard for optional property without unsafe cast
93
+ const enabled = 'enabled' in judge ? String((judge as unknown as { enabled?: unknown }).enabled) : NA_TEXT;
94
+ const systemPromptPath = judge.systemPromptPath !== undefined ? String(judge.systemPromptPath) : NA_TEXT;
95
+ const summaryPromptPath = judge.summaryPromptPath !== undefined ? String(judge.summaryPromptPath) : NA_TEXT;
96
+ const summarization = judge.summarization !== undefined ? JSON.stringify(judge.summarization) : NA_TEXT;
97
+
98
+ table += `| ${id} | ${name} | ${role} | ${model} | ${provider} | ${temperature} | ${enabled} | ${systemPromptPath} | ${summaryPromptPath} | ${summarization} |\n`;
99
+
100
+ return table;
101
+ }
102
+
103
+ /**
104
+ * Formats contribution metadata for verbose mode.
105
+ * @param contribution - The contribution to format metadata for.
106
+ * @returns Metadata string or empty string if not verbose.
107
+ */
108
+ function formatContributionMetadata(contribution: Contribution, verbose: boolean): string {
109
+ if (!verbose)
110
+ return '';
111
+
112
+ const latency = contribution.metadata.latencyMs !== undefined ? contribution.metadata.latencyMs.toString() : NA_TEXT;
113
+ const tokens = contribution.metadata.tokensUsed !== undefined ? contribution.metadata.tokensUsed.toString() : NA_TEXT;
114
+
115
+ return ` (latency=${latency}ms, tokens=${tokens})`;
116
+ }
117
+
118
+ /**
119
+ * Structured representation of a contribution formatted for markdown output.
120
+ */
121
+ type FormattedContribution = { title: string; content: string };
122
+
123
+ /**
124
+ * Formats proposals for a round.
125
+ * @param contributions - Array of proposal contributions.
126
+ * @param verbose - Whether to include metadata.
127
+ * @returns Array of formatted proposal entries with title and content.
128
+ */
129
+ function formatProposals(contributions: Contribution[], verbose: boolean): FormattedContribution[] {
130
+ const proposals = contributions.filter(c => c.type === CONTRIBUTION_TYPES.PROPOSAL);
131
+
132
+ if (proposals.length === 0) {
133
+ return [];
134
+ }
135
+
136
+ const result: FormattedContribution[] = [];
137
+ for (const proposal of proposals) {
138
+ const metadata = formatContributionMetadata(proposal, verbose);
139
+ result.push({
140
+ title: `Agent *${proposal.agentId}*${metadata}:`,
141
+ content: proposal.content
142
+ });
143
+ }
144
+
145
+ return result;
146
+ }
147
+
148
+ /**
149
+ * Formats critiques for a round.
150
+ * @param contributions - Array of critique contributions.
151
+ * @param verbose - Whether to include metadata.
152
+ * @returns Array of formatted critique entries with title and content.
153
+ */
154
+ function formatCritiques(contributions: Contribution[], verbose: boolean): FormattedContribution[] {
155
+ const critiques = contributions.filter(c => c.type === CONTRIBUTION_TYPES.CRITIQUE);
156
+
157
+ if (critiques.length === 0) {
158
+ return [];
159
+ }
160
+
161
+ const result: FormattedContribution[] = [];
162
+ for (const critique of critiques) {
163
+ const metadata = formatContributionMetadata(critique, verbose);
164
+ const target = critique.targetAgentId || UNKNOWN_LABEL;
165
+ result.push({
166
+ title: `*${critique.agentId}* ${RIGHT_ARROW_HTML} *${target}*${metadata}:`,
167
+ content: critique.content
168
+ });
169
+ }
170
+
171
+ return result;
172
+ }
173
+
174
+ /**
175
+ * Formats refinements for a round.
176
+ * @param contributions - Array of refinement contributions.
177
+ * @param verbose - Whether to include metadata.
178
+ * @returns Array of formatted refinement entries with title and content.
179
+ */
180
+ function formatRefinements(contributions: Contribution[], verbose: boolean): FormattedContribution[] {
181
+ const refinements = contributions.filter(c => c.type === CONTRIBUTION_TYPES.REFINEMENT);
182
+
183
+ if (refinements.length === 0) {
184
+ return [];
185
+ }
186
+
187
+ const result: FormattedContribution[] = [];
188
+ for (const refinement of refinements) {
189
+ const metadata = formatContributionMetadata(refinement, verbose);
190
+ result.push({
191
+ title: `Agent *${refinement.agentId}*${metadata}:`,
192
+ content: refinement.content
193
+ });
194
+ }
195
+
196
+ return result;
197
+ }
198
+
199
+ /**
200
+ * Renders a contribution section with a heading, titles outside code fences, and fenced content.
201
+ * Extracted to remove repetition across proposals, critiques, and refinements.
202
+ * @param heading - The section heading label.
203
+ * @param items - The list of formatted contributions.
204
+ * @param emptyMessage - Message to display when there are no items.
205
+ */
206
+ function renderContributionSection(
207
+ heading: string,
208
+ items: FormattedContribution[],
209
+ emptyMessage: string
210
+ ): string {
211
+ let section = `#### ${heading}\n`;
212
+ if (items.length === 0) {
213
+ section += `${emptyMessage}\n\n`;
214
+ return section;
215
+ }
216
+ for (const item of items) {
217
+ section += `${item.title}\n`;
218
+ section += `\`\`\`${CODE_FENCE_LANG}\n${item.content}\n\`\`\`\n\n`;
219
+ }
220
+ return section;
221
+ }
222
+
223
+ /**
224
+ * Generates a complete debate report in markdown format.
225
+ * @param debateState - The complete debate state.
226
+ * @param agentConfigs - Array of agent configurations.
227
+ * @param judgeConfig - Judge configuration.
228
+ * @param problemDescription - The full problem description text.
229
+ * @param options - Options including verbose flag.
230
+ * @returns Complete markdown report string.
231
+ */
232
+ export function generateDebateReport(
233
+ debateState: DebateState,
234
+ agentConfigs: AgentConfig[],
235
+ judgeConfig: AgentConfig,
236
+ problemDescription: string,
237
+ options: { verbose?: boolean }
238
+ ): string {
239
+ const title = extractFirstLine(problemDescription);
240
+ const time = formatLocalTime(debateState.createdAt);
241
+ const verbose = options.verbose || false;
242
+
243
+ let report = `# Debate: ${title}\n`;
244
+ report += `Time: ${time}\n\n`;
245
+
246
+ // Problem Description section
247
+ report += `## Problem Description\n`;
248
+ report += `\`\`\`text\n${problemDescription}\n\`\`\`\n\n`;
249
+
250
+ // Agents section
251
+ report += `## Agents\n\n`;
252
+ report += formatAgentsTable(agentConfigs);
253
+ report += `\n\n`;
254
+
255
+ // Clarifications section (if any)
256
+ if (debateState.clarifications && debateState.clarifications.length > 0) {
257
+ report += `## Clarifications\n\n`;
258
+ for (const group of debateState.clarifications) {
259
+ report += `### ${group.agentName} (${group.role})\n`;
260
+ for (const item of group.items) {
261
+ report += `Question (${item.id}):\n\n\`\`\`${CODE_FENCE_LANG}\n${item.question}\n\`\`\`\n\n`;
262
+ report += `Answer:\n\n\`\`\`${CODE_FENCE_LANG}\n${item.answer}\n\`\`\`\n\n`;
263
+ }
264
+ }
265
+ report += `\n`;
266
+ }
267
+
268
+ // Judge section
269
+ report += `## Judge\n\n`;
270
+ report += formatJudgeTable(judgeConfig);
271
+ report += `\n\n`;
272
+
273
+ // Rounds section
274
+ report += `## Rounds\n\n`;
275
+
276
+ for (const round of debateState.rounds) {
277
+ report += `### Round ${round.roundNumber}\n\n`;
278
+
279
+ // Proposals
280
+ const formattedProposals = formatProposals(round.contributions, verbose);
281
+ report += renderContributionSection('Proposals', formattedProposals, NO_PROPOSALS_MSG);
282
+
283
+ // Critiques
284
+ const formattedCritiques = formatCritiques(round.contributions, verbose);
285
+ report += renderContributionSection('Critiques', formattedCritiques, NO_CRITIQUES_MSG);
286
+
287
+ // Refinements
288
+ const formattedRefinements = formatRefinements(round.contributions, verbose);
289
+ report += renderContributionSection('Refinements', formattedRefinements, NO_REFINEMENTS_MSG);
290
+ }
291
+
292
+ // Final Synthesis section
293
+ report += `### Final Synthesis\n`;
294
+ if (debateState.finalSolution) {
295
+ report += `\`\`\`text\n${debateState.finalSolution.description}\n\`\`\`\n`;
296
+ } else {
297
+ report += `\`\`\`text\nNo final solution available.\n\`\`\`\n`;
298
+ }
299
+
300
+ return report;
301
+ }