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.
- package/.cursor/commands/setup-test.mdc +175 -0
- package/.cursor/rules/basic-code-cleanup.mdc +1110 -0
- package/.cursor/rules/riper5.mdc +96 -0
- package/.env.example +6 -0
- package/AGENTS.md +1052 -0
- package/LICENSE +21 -0
- package/README.md +93 -0
- package/WARP.md +113 -0
- package/dialectic-1.0.0.tgz +0 -0
- package/dialectic.js +10 -0
- package/docs/commands.md +375 -0
- package/docs/configuration.md +882 -0
- package/docs/context_summarization.md +1023 -0
- package/docs/debate_flow.md +1127 -0
- package/docs/eval_flow.md +795 -0
- package/docs/evaluator.md +141 -0
- package/examples/debate-config-openrouter.json +48 -0
- package/examples/debate_config1.json +48 -0
- package/examples/eval/eval1/eval_config1.json +13 -0
- package/examples/eval/eval1/result1.json +62 -0
- package/examples/eval/eval1/result2.json +97 -0
- package/examples/eval_summary_format.md +11 -0
- package/examples/example3/debate-config.json +64 -0
- package/examples/example3/eval_config2.json +25 -0
- package/examples/example3/problem.md +17 -0
- package/examples/example3/rounds_test/eval_run.sh +16 -0
- package/examples/example3/rounds_test/run_test.sh +16 -0
- package/examples/kata1/architect-only-solution_2-rounds.json +121 -0
- package/examples/kata1/architect-perf-solution_2-rounds.json +234 -0
- package/examples/kata1/debate-config-kata1.json +54 -0
- package/examples/kata1/eval_architect-only_2-rounds.json +97 -0
- package/examples/kata1/eval_architect-perf_2-rounds.json +97 -0
- package/examples/kata1/kata1-report.md +12224 -0
- package/examples/kata1/kata1-report_temps-01_01_01_07.md +2451 -0
- package/examples/kata1/kata1.md +5 -0
- package/examples/kata1/meta.txt +1 -0
- package/examples/kata2/debate-config.json +54 -0
- package/examples/kata2/eval_config1.json +21 -0
- package/examples/kata2/eval_config2.json +25 -0
- package/examples/kata2/kata2.md +5 -0
- package/examples/kata2/only_architect/debate-config.json +45 -0
- package/examples/kata2/only_architect/eval_run.sh +11 -0
- package/examples/kata2/only_architect/run_test.sh +5 -0
- package/examples/kata2/rounds_test/eval_run.sh +11 -0
- package/examples/kata2/rounds_test/run_test.sh +5 -0
- package/examples/kata2/summary_length_test/eval_run.sh +11 -0
- package/examples/kata2/summary_length_test/eval_run_w_clarify.sh +7 -0
- package/examples/kata2/summary_length_test/run_test.sh +5 -0
- package/examples/task-queue/debate-config.json +76 -0
- package/examples/task-queue/debate_report.md +566 -0
- package/examples/task-queue/task-queue-system.md +25 -0
- package/jest.config.ts +13 -0
- package/multi_agent_debate_spec.md +2980 -0
- package/package.json +38 -0
- package/sanity-check-problem.txt +9 -0
- package/src/agents/prompts/architect-prompts.ts +203 -0
- package/src/agents/prompts/generalist-prompts.ts +157 -0
- package/src/agents/prompts/index.ts +41 -0
- package/src/agents/prompts/judge-prompts.ts +19 -0
- package/src/agents/prompts/kiss-prompts.ts +230 -0
- package/src/agents/prompts/performance-prompts.ts +142 -0
- package/src/agents/prompts/prompt-types.ts +68 -0
- package/src/agents/prompts/security-prompts.ts +149 -0
- package/src/agents/prompts/shared.ts +144 -0
- package/src/agents/prompts/testing-prompts.ts +149 -0
- package/src/agents/role-based-agent.ts +386 -0
- package/src/cli/commands/debate.ts +761 -0
- package/src/cli/commands/eval.ts +475 -0
- package/src/cli/commands/report.ts +265 -0
- package/src/cli/index.ts +79 -0
- package/src/core/agent.ts +198 -0
- package/src/core/clarifications.ts +34 -0
- package/src/core/judge.ts +257 -0
- package/src/core/orchestrator.ts +432 -0
- package/src/core/state-manager.ts +322 -0
- package/src/eval/evaluator-agent.ts +130 -0
- package/src/eval/prompts/system.md +41 -0
- package/src/eval/prompts/user.md +64 -0
- package/src/providers/llm-provider.ts +25 -0
- package/src/providers/openai-provider.ts +84 -0
- package/src/providers/openrouter-provider.ts +122 -0
- package/src/providers/provider-factory.ts +64 -0
- package/src/types/agent.types.ts +141 -0
- package/src/types/config.types.ts +47 -0
- package/src/types/debate.types.ts +237 -0
- package/src/types/eval.types.ts +85 -0
- package/src/utils/common.ts +104 -0
- package/src/utils/context-formatter.ts +102 -0
- package/src/utils/context-summarizer.ts +143 -0
- package/src/utils/env-loader.ts +46 -0
- package/src/utils/exit-codes.ts +5 -0
- package/src/utils/id.ts +11 -0
- package/src/utils/logger.ts +48 -0
- package/src/utils/paths.ts +10 -0
- package/src/utils/progress-ui.ts +313 -0
- package/src/utils/prompt-loader.ts +79 -0
- package/src/utils/report-generator.ts +301 -0
- package/tests/clarifications.spec.ts +128 -0
- package/tests/cli.debate.spec.ts +144 -0
- package/tests/config-loading.spec.ts +206 -0
- package/tests/context-summarizer.spec.ts +131 -0
- package/tests/debate-config-custom.json +38 -0
- package/tests/env-loader.spec.ts +149 -0
- package/tests/eval.command.spec.ts +1191 -0
- package/tests/logger.spec.ts +19 -0
- package/tests/openai-provider.spec.ts +26 -0
- package/tests/openrouter-provider.spec.ts +279 -0
- package/tests/orchestrator-summary.spec.ts +386 -0
- package/tests/orchestrator.spec.ts +207 -0
- package/tests/prompt-loader.spec.ts +52 -0
- package/tests/prompts/architect.md +16 -0
- package/tests/provider-factory.spec.ts +150 -0
- package/tests/report.command.spec.ts +546 -0
- package/tests/role-based-agent-summary.spec.ts +476 -0
- package/tests/security-agent.spec.ts +221 -0
- package/tests/shared-prompts.spec.ts +318 -0
- package/tests/state-manager.spec.ts +251 -0
- package/tests/summary-prompts.spec.ts +153 -0
- 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 = '→';
|
|
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
|
+
}
|