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,761 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import readline from 'readline';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { EXIT_INVALID_ARGS, EXIT_GENERAL_ERROR } from '../../utils/exit-codes';
|
|
6
|
+
import { warnUser, infoUser, writeStderr } from '../index';
|
|
7
|
+
import { SystemConfig } from '../../types/config.types';
|
|
8
|
+
import { AgentConfig, AGENT_ROLES, LLM_PROVIDERS, PROMPT_SOURCES, AgentPromptMetadata, JudgePromptMetadata, PromptSource } from '../../types/agent.types';
|
|
9
|
+
import { DebateConfig, DebateResult, DebateRound, Contribution, ContributionType, TERMINATION_TYPES, SYNTHESIS_METHODS, CONTRIBUTION_TYPES, SummarizationConfig, AgentClarifications } from '../../types/debate.types';
|
|
10
|
+
import { DEFAULT_SUMMARIZATION_ENABLED, DEFAULT_SUMMARIZATION_THRESHOLD, DEFAULT_SUMMARIZATION_MAX_LENGTH, DEFAULT_SUMMARIZATION_METHOD } from '../../types/config.types';
|
|
11
|
+
import { LLMProvider } from '../../providers/llm-provider';
|
|
12
|
+
import { createProvider } from '../../providers/provider-factory';
|
|
13
|
+
import { RoleBasedAgent } from '../../agents/role-based-agent';
|
|
14
|
+
import { JudgeAgent } from '../../core/judge';
|
|
15
|
+
import { StateManager } from '../../core/state-manager';
|
|
16
|
+
import { DebateOrchestrator } from '../../core/orchestrator';
|
|
17
|
+
import { resolvePrompt } from '../../utils/prompt-loader';
|
|
18
|
+
import { loadEnvironmentFile } from '../../utils/env-loader';
|
|
19
|
+
import { Agent } from '../../core/agent';
|
|
20
|
+
import { DebateProgressUI } from '../../utils/progress-ui';
|
|
21
|
+
import { generateDebateReport } from '../../utils/report-generator';
|
|
22
|
+
import { collectClarifications } from '../../core/clarifications';
|
|
23
|
+
import { createValidationError, writeFileWithDirectories } from '../../utils/common';
|
|
24
|
+
|
|
25
|
+
const DEFAULT_CONFIG_PATH = path.resolve(process.cwd(), 'debate-config.json');
|
|
26
|
+
const DEFAULT_ROUNDS = 3;
|
|
27
|
+
const DEFAULT_CLARIFICATIONS_MAX_PER_AGENT = 5;
|
|
28
|
+
|
|
29
|
+
// File handling constants
|
|
30
|
+
const FILE_ENCODING_UTF8 = 'utf-8';
|
|
31
|
+
const JSON_FILE_EXTENSION = '.json';
|
|
32
|
+
const MARKDOWN_FILE_EXTENSION = '.md';
|
|
33
|
+
const JSON_INDENT_SPACES = 2;
|
|
34
|
+
|
|
35
|
+
// Problem resolution error messages
|
|
36
|
+
const ERROR_BOTH_PROBLEM_SOURCES = 'Invalid arguments: provide exactly one of <problem> or --problemDescription';
|
|
37
|
+
const ERROR_NO_PROBLEM_SOURCE = 'Invalid arguments: problem is required (provide <problem> or --problemDescription)';
|
|
38
|
+
const ERROR_FILE_NOT_FOUND = 'Invalid arguments: problem description file not found';
|
|
39
|
+
const ERROR_PATH_IS_DIRECTORY = 'Invalid arguments: problem description path is a directory';
|
|
40
|
+
const ERROR_FILE_EMPTY = 'Invalid arguments: problem description file is empty';
|
|
41
|
+
const ERROR_FILE_READ_FAILED = 'Failed to read problem description file';
|
|
42
|
+
|
|
43
|
+
// Default agent identifiers and names
|
|
44
|
+
const DEFAULT_ARCHITECT_ID = 'agent-architect';
|
|
45
|
+
const DEFAULT_ARCHITECT_NAME = 'System Architect';
|
|
46
|
+
const DEFAULT_PERFORMANCE_ID = 'agent-performance';
|
|
47
|
+
const DEFAULT_PERFORMANCE_NAME = 'Performance Engineer';
|
|
48
|
+
const DEFAULT_KISS_ID = 'agent-kiss';
|
|
49
|
+
const DEFAULT_KISS_NAME = 'Simplicity Advocate';
|
|
50
|
+
const DEFAULT_JUDGE_ID = 'judge-main';
|
|
51
|
+
const DEFAULT_JUDGE_NAME = 'Technical Judge';
|
|
52
|
+
|
|
53
|
+
// Default LLM configuration
|
|
54
|
+
const DEFAULT_LLM_MODEL = 'gpt-4';
|
|
55
|
+
const DEFAULT_AGENT_TEMPERATURE = 0.5;
|
|
56
|
+
const DEFAULT_JUDGE_TEMPERATURE = 0.3;
|
|
57
|
+
|
|
58
|
+
// Default summary prompt fallback
|
|
59
|
+
const DEFAULT_SUMMARY_PROMPT_FALLBACK = 'Summarize the following debate history from your perspective, preserving key points and decisions.';
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Collects clarifying questions from agents and prompts the user for answers.
|
|
63
|
+
* Returns the collected clarifications with user-provided answers.
|
|
64
|
+
*
|
|
65
|
+
* @param resolvedProblem - The problem statement to clarify
|
|
66
|
+
* @param agents - Array of agents to collect questions from
|
|
67
|
+
* @param maxPerAgent - Maximum questions per agent
|
|
68
|
+
* @returns Promise resolving to collected clarifications with answers
|
|
69
|
+
*/
|
|
70
|
+
async function collectAndAnswerClarifications( resolvedProblem: string, agents: Agent[], maxPerAgent: number ): Promise<AgentClarifications[]>
|
|
71
|
+
{
|
|
72
|
+
// Collect questions from agents (grouped, truncated with warnings)
|
|
73
|
+
const collected: AgentClarifications[] = await collectClarifications( resolvedProblem, agents, maxPerAgent, (msg) => warnUser(msg) );
|
|
74
|
+
|
|
75
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async function askUser(promptText: string): Promise<string> { // Inline helper to convert readline callback to Promise, scoped to this readline instance
|
|
79
|
+
return await new Promise((resolve) => rl.question(promptText, resolve));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try { // Prompt user for answers (empty line => NA)
|
|
83
|
+
for (const group of collected) {
|
|
84
|
+
if (group.items.length === 0) continue;
|
|
85
|
+
writeStderr(`\n[${group.agentName}] Clarifying Questions\n`);
|
|
86
|
+
for (const item of group.items) {
|
|
87
|
+
const ans = (await askUser(`Q (${item.id}): ${item.question}\n> `)).trim();
|
|
88
|
+
item.answer = ans.length === 0 ? 'NA' : ans;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} finally {
|
|
92
|
+
rl.close();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return collected;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Creates orchestrator hooks that drive the progress UI during debate execution.
|
|
100
|
+
*
|
|
101
|
+
* @param progressUI - The progress UI instance to drive
|
|
102
|
+
* @param options - CLI options containing verbose flag
|
|
103
|
+
* @returns Object containing all orchestrator hook functions
|
|
104
|
+
*/
|
|
105
|
+
function createOrchestratorHooks(progressUI: DebateProgressUI, options: any) {
|
|
106
|
+
const SUMMARY_ACTIVITY_LABEL = 'summarizing context';
|
|
107
|
+
return {
|
|
108
|
+
onRoundStart: (roundNumber: number, _totalRounds: number) => {
|
|
109
|
+
progressUI.startRound(roundNumber);
|
|
110
|
+
},
|
|
111
|
+
onPhaseStart: (_roundNumber: number, phase: ContributionType, expectedTaskCount: number) => {
|
|
112
|
+
progressUI.startPhase(phase, expectedTaskCount);
|
|
113
|
+
},
|
|
114
|
+
onAgentStart: (agentName: string, activity: string) => {
|
|
115
|
+
progressUI.startAgentActivity(agentName, activity);
|
|
116
|
+
},
|
|
117
|
+
onAgentComplete: (agentName: string, activity: string) => {
|
|
118
|
+
progressUI.completeAgentActivity(agentName, activity);
|
|
119
|
+
},
|
|
120
|
+
onPhaseComplete: (_roundNumber: number, phase: ContributionType) => {
|
|
121
|
+
progressUI.completePhase(phase);
|
|
122
|
+
},
|
|
123
|
+
onSummarizationStart: (agentName: string) => {
|
|
124
|
+
progressUI.startAgentActivity(agentName, SUMMARY_ACTIVITY_LABEL);
|
|
125
|
+
},
|
|
126
|
+
onSummarizationComplete: (agentName: string, beforeChars: number, afterChars: number) => {
|
|
127
|
+
progressUI.completeAgentActivity(agentName, SUMMARY_ACTIVITY_LABEL);
|
|
128
|
+
if (options.verbose) {
|
|
129
|
+
progressUI.log(` [${agentName}] Summarized: ${beforeChars} → ${afterChars} chars`);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
// Ensure activity is cleared even when no summary is produced
|
|
133
|
+
onSummarizationEnd: (agentName: string) => {
|
|
134
|
+
progressUI.completeAgentActivity(agentName, SUMMARY_ACTIVITY_LABEL);
|
|
135
|
+
},
|
|
136
|
+
onSynthesisStart: () => {
|
|
137
|
+
progressUI.startSynthesis();
|
|
138
|
+
},
|
|
139
|
+
onSynthesisComplete: () => {
|
|
140
|
+
progressUI.completeSynthesis();
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Creates a judge agent with resolved prompts and metadata collection.
|
|
147
|
+
*
|
|
148
|
+
* @param sysConfig - System configuration containing judge settings
|
|
149
|
+
* @param systemSummaryConfig - System-wide summarization configuration
|
|
150
|
+
* @param promptSources - Collection object to record prompt source metadata
|
|
151
|
+
* @returns Configured JudgeAgent instance
|
|
152
|
+
*/
|
|
153
|
+
function createJudgeWithPromptResolution(
|
|
154
|
+
sysConfig: SystemConfig,
|
|
155
|
+
systemSummaryConfig: SummarizationConfig,
|
|
156
|
+
promptSources: { judge: JudgePromptMetadata }
|
|
157
|
+
): JudgeAgent {
|
|
158
|
+
// Judge prompt resolution
|
|
159
|
+
const judgeDefault = JudgeAgent.defaultSystemPrompt();
|
|
160
|
+
const jres = resolvePrompt({
|
|
161
|
+
label: sysConfig.judge!.name,
|
|
162
|
+
configDir: sysConfig.configDir || process.cwd(),
|
|
163
|
+
...((sysConfig.judge!.systemPromptPath !== undefined) && { promptPath: sysConfig.judge!.systemPromptPath }),
|
|
164
|
+
defaultText: judgeDefault
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Judge summary prompt resolution
|
|
168
|
+
const judgeSummaryDefault = JudgeAgent.defaultSummaryPrompt('', systemSummaryConfig.maxLength);
|
|
169
|
+
const jsres = resolvePrompt({
|
|
170
|
+
label: `${sysConfig.judge!.name} (summary)`,
|
|
171
|
+
configDir: sysConfig.configDir || process.cwd(),
|
|
172
|
+
...((sysConfig.judge!.summaryPromptPath !== undefined) && { promptPath: sysConfig.judge!.summaryPromptPath }),
|
|
173
|
+
defaultText: judgeSummaryDefault
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const judgeProvider = createProvider(sysConfig.judge!.provider);
|
|
177
|
+
const judge = new JudgeAgent(
|
|
178
|
+
sysConfig.judge!,
|
|
179
|
+
judgeProvider,
|
|
180
|
+
jres.text,
|
|
181
|
+
jres.source === PROMPT_SOURCES.FILE ? ({ source: PROMPT_SOURCES.FILE, ...(jres.absPath !== undefined && { absPath: jres.absPath }) }) : ({ source: PROMPT_SOURCES.BUILT_IN }),
|
|
182
|
+
systemSummaryConfig,
|
|
183
|
+
jsres.source === PROMPT_SOURCES.FILE ? ({ source: PROMPT_SOURCES.FILE, ...(jsres.absPath !== undefined && { absPath: jsres.absPath }) }) : ({ source: PROMPT_SOURCES.BUILT_IN })
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Record prompt source metadata
|
|
187
|
+
promptSources.judge = {
|
|
188
|
+
id: sysConfig.judge!.id,
|
|
189
|
+
source: jres.source,
|
|
190
|
+
...(jres.absPath !== undefined && { path: jres.absPath }),
|
|
191
|
+
summarySource: jsres.source,
|
|
192
|
+
...(jsres.absPath !== undefined && { summaryPath: jsres.absPath })
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return judge;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Returns the built-in default system configuration for the debate system.
|
|
200
|
+
*
|
|
201
|
+
* This includes:
|
|
202
|
+
* - Two default agents: a System Architect and a Performance Engineer, both using the GPT-4 model via OpenAI.
|
|
203
|
+
* - A default judge agent with the role of "generalist" and a lower temperature for more deterministic judging.
|
|
204
|
+
* - Default debate configuration: 3 rounds, fixed termination, judge-based synthesis, full history included, and a 5-minute timeout per round.
|
|
205
|
+
*
|
|
206
|
+
* @returns {SystemConfig} The default system configuration object.
|
|
207
|
+
*/
|
|
208
|
+
function builtInDefaults(): SystemConfig {
|
|
209
|
+
const defaultAgents: AgentConfig[] = [
|
|
210
|
+
{ id: DEFAULT_ARCHITECT_ID, name: DEFAULT_ARCHITECT_NAME, role: AGENT_ROLES.ARCHITECT,
|
|
211
|
+
model: DEFAULT_LLM_MODEL, provider: LLM_PROVIDERS.OPENAI, temperature: DEFAULT_AGENT_TEMPERATURE, enabled: true },
|
|
212
|
+
{ id: DEFAULT_PERFORMANCE_ID, name: DEFAULT_PERFORMANCE_NAME, role: AGENT_ROLES.PERFORMANCE,
|
|
213
|
+
model: DEFAULT_LLM_MODEL, provider: LLM_PROVIDERS.OPENAI, temperature: DEFAULT_AGENT_TEMPERATURE, enabled: true },
|
|
214
|
+
{ id: DEFAULT_KISS_ID, name: DEFAULT_KISS_NAME, role: AGENT_ROLES.KISS,
|
|
215
|
+
model: DEFAULT_LLM_MODEL, provider: LLM_PROVIDERS.OPENAI, temperature: DEFAULT_AGENT_TEMPERATURE, enabled: true },
|
|
216
|
+
];
|
|
217
|
+
const judge: AgentConfig = { id: DEFAULT_JUDGE_ID, name: DEFAULT_JUDGE_NAME, role: AGENT_ROLES.GENERALIST,
|
|
218
|
+
model: DEFAULT_LLM_MODEL, provider: LLM_PROVIDERS.OPENAI, temperature: DEFAULT_JUDGE_TEMPERATURE };
|
|
219
|
+
|
|
220
|
+
// Default summarization configuration
|
|
221
|
+
const summarization = {
|
|
222
|
+
enabled: DEFAULT_SUMMARIZATION_ENABLED,
|
|
223
|
+
threshold: DEFAULT_SUMMARIZATION_THRESHOLD,
|
|
224
|
+
maxLength: DEFAULT_SUMMARIZATION_MAX_LENGTH,
|
|
225
|
+
method: DEFAULT_SUMMARIZATION_METHOD,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const debate: DebateConfig = {
|
|
229
|
+
rounds: DEFAULT_ROUNDS,
|
|
230
|
+
terminationCondition: { type: TERMINATION_TYPES.FIXED },
|
|
231
|
+
synthesisMethod: SYNTHESIS_METHODS.JUDGE,
|
|
232
|
+
includeFullHistory: true,
|
|
233
|
+
timeoutPerRound: 300000,
|
|
234
|
+
summarization,
|
|
235
|
+
};
|
|
236
|
+
return { agents: defaultAgents, judge, debate } as SystemConfig;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Loads the system configuration for the debate system from a specified file path,
|
|
241
|
+
* or falls back to a default location and built-in defaults if the file is missing or incomplete.
|
|
242
|
+
*
|
|
243
|
+
* This function attempts to read and parse a JSON configuration file containing agent, judge,
|
|
244
|
+
* and debate settings. If the file does not exist, or if required fields are missing,
|
|
245
|
+
* it prints a warning to stderr and uses built-in defaults for the missing parts.
|
|
246
|
+
*
|
|
247
|
+
* The function ensures that:
|
|
248
|
+
* - If the config file is missing, the entire built-in default configuration is used.
|
|
249
|
+
* - If the config file is present but missing the 'agents' array or it is empty,
|
|
250
|
+
* the entire built-in default configuration is used.
|
|
251
|
+
* - If the config file is missing the 'judge' or 'debate' fields, those fields are filled in
|
|
252
|
+
* from the built-in defaults, and a warning is printed.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} [configPath] - Optional path to the configuration file. If not provided,
|
|
255
|
+
* uses the default path ('debate-config.json' in the current working directory).
|
|
256
|
+
* @returns {Promise<SystemConfig>} The loaded and validated system configuration object.
|
|
257
|
+
*/
|
|
258
|
+
export async function loadConfig(configPath?: string): Promise<SystemConfig> {
|
|
259
|
+
const finalPath = configPath ? path.resolve(process.cwd(), configPath) : DEFAULT_CONFIG_PATH;
|
|
260
|
+
const defaults = builtInDefaults();
|
|
261
|
+
|
|
262
|
+
if (!fs.existsSync(finalPath)) {
|
|
263
|
+
warnUser(`Config not found at ${finalPath}. Using built-in defaults.`);
|
|
264
|
+
defaults.configDir = process.cwd();
|
|
265
|
+
return defaults;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const raw = await fs.promises.readFile(finalPath, FILE_ENCODING_UTF8);
|
|
269
|
+
const parsed = JSON.parse(raw);
|
|
270
|
+
|
|
271
|
+
// Ensure shape minimal
|
|
272
|
+
if (!Array.isArray(parsed.agents) || parsed.agents.length === 0) {
|
|
273
|
+
warnUser('Config missing agents. Using built-in defaults.');
|
|
274
|
+
defaults.configDir = path.dirname(finalPath);
|
|
275
|
+
return defaults;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (!parsed.judge) {
|
|
279
|
+
warnUser('Config missing judge. Using default judge.');
|
|
280
|
+
parsed.judge = defaults.judge;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!parsed.debate) {
|
|
284
|
+
parsed.debate = defaults.debate;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
parsed.configDir = path.dirname(finalPath);
|
|
288
|
+
return parsed as SystemConfig;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Helper to create an agent instance with prompt resolution and metadata collection.
|
|
293
|
+
* Resolves system and summary prompts from files or defaults, merges summarization config,
|
|
294
|
+
* creates the agent, and records provenance.
|
|
295
|
+
*
|
|
296
|
+
* @param cfg - Agent configuration containing role, model, and other settings.
|
|
297
|
+
* @param provider - LLM provider instance for LLM interactions.
|
|
298
|
+
* @param configDir - Directory path where the configuration file is located.
|
|
299
|
+
* @param systemSummaryConfig - System-wide summarization configuration.
|
|
300
|
+
* @param collect - Collection object to record prompt source metadata.
|
|
301
|
+
* @returns A configured RoleBasedAgent instance.
|
|
302
|
+
*/
|
|
303
|
+
function createAgentWithPromptResolution(
|
|
304
|
+
cfg: AgentConfig,
|
|
305
|
+
provider: LLMProvider,
|
|
306
|
+
configDir: string,
|
|
307
|
+
systemSummaryConfig: SummarizationConfig,
|
|
308
|
+
collect: { agents: AgentPromptMetadata[] }
|
|
309
|
+
): Agent {
|
|
310
|
+
// Resolve system prompt
|
|
311
|
+
const defaultText = RoleBasedAgent.defaultSystemPrompt(cfg.role);
|
|
312
|
+
const res = resolvePrompt({
|
|
313
|
+
label: cfg.name,
|
|
314
|
+
configDir,
|
|
315
|
+
...(cfg.systemPromptPath !== undefined && { promptPath: cfg.systemPromptPath }),
|
|
316
|
+
defaultText
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const promptSource: PromptSource = res.source === PROMPT_SOURCES.FILE
|
|
320
|
+
? { source: PROMPT_SOURCES.FILE, ...(res.absPath !== undefined && { absPath: res.absPath }) }
|
|
321
|
+
: { source: PROMPT_SOURCES.BUILT_IN };
|
|
322
|
+
|
|
323
|
+
// Merge summarization config (agent-level overrides system-level)
|
|
324
|
+
const mergedSummaryConfig: SummarizationConfig = {
|
|
325
|
+
...systemSummaryConfig,
|
|
326
|
+
...cfg.summarization,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// Resolve summary prompt
|
|
330
|
+
// For the default, we use a generic fallback prompt
|
|
331
|
+
const summaryRes = resolvePrompt({
|
|
332
|
+
label: `${cfg.name} (summary)`,
|
|
333
|
+
configDir,
|
|
334
|
+
...(cfg.summaryPromptPath !== undefined && { promptPath: cfg.summaryPromptPath }),
|
|
335
|
+
defaultText: DEFAULT_SUMMARY_PROMPT_FALLBACK
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const summaryPromptSource: PromptSource = summaryRes.source === PROMPT_SOURCES.FILE
|
|
339
|
+
? { source: PROMPT_SOURCES.FILE, ...(summaryRes.absPath !== undefined && { absPath: summaryRes.absPath }) }
|
|
340
|
+
: { source: PROMPT_SOURCES.BUILT_IN };
|
|
341
|
+
|
|
342
|
+
// Resolve clarification prompt (optional)
|
|
343
|
+
const clarificationRes = resolvePrompt({
|
|
344
|
+
label: `${cfg.name} (clarifications)`,
|
|
345
|
+
configDir,
|
|
346
|
+
...(cfg.clarificationPromptPath !== undefined && { promptPath: cfg.clarificationPromptPath }),
|
|
347
|
+
defaultText: ''
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Create agent with all resolved parameters
|
|
351
|
+
const agent = RoleBasedAgent.create( cfg, provider, res.text, promptSource,
|
|
352
|
+
mergedSummaryConfig, summaryPromptSource,
|
|
353
|
+
clarificationRes.text );
|
|
354
|
+
|
|
355
|
+
collect.agents.push({
|
|
356
|
+
agentId: cfg.id,
|
|
357
|
+
role: cfg.role,
|
|
358
|
+
source: res.source,
|
|
359
|
+
...(res.absPath !== undefined && { path: res.absPath })
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return agent;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Builds an array of Agent instances based on the provided configuration and LLM provider.
|
|
367
|
+
*
|
|
368
|
+
* Creates RoleBasedAgent instances for each agent configuration, resolving system and summary
|
|
369
|
+
* prompts from files or defaults, merging summarization configs, and collecting metadata about
|
|
370
|
+
* prompt sources. The RoleBasedAgent class handles all roles through a prompt registry,
|
|
371
|
+
* eliminating the need for role-specific agent classes.
|
|
372
|
+
*
|
|
373
|
+
* @param agentConfigs - Array of agent configurations.
|
|
374
|
+
* @param configDir - Directory where the config file is located, used for resolving relative prompt paths.
|
|
375
|
+
* @param systemSummaryConfig - System-wide summarization configuration.
|
|
376
|
+
* @param collect - Object to collect prompt metadata.
|
|
377
|
+
* @returns Array of Agent instances.
|
|
378
|
+
*/
|
|
379
|
+
function buildAgents(
|
|
380
|
+
agentConfigs: AgentConfig[],
|
|
381
|
+
configDir: string,
|
|
382
|
+
systemSummaryConfig: SummarizationConfig,
|
|
383
|
+
collect: { agents: AgentPromptMetadata[] }
|
|
384
|
+
): Agent[] {
|
|
385
|
+
return agentConfigs.map((cfg) => {
|
|
386
|
+
const provider = createProvider(cfg.provider);
|
|
387
|
+
return createAgentWithPromptResolution(cfg, provider, configDir, systemSummaryConfig, collect);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Creates a DebateConfig from the system configuration and command-line options.
|
|
393
|
+
* Validates that the number of rounds is at least 1.
|
|
394
|
+
*
|
|
395
|
+
* @param {SystemConfig} sysConfig - The system configuration.
|
|
396
|
+
* @param {any} options - Command-line options containing optional rounds override.
|
|
397
|
+
* @returns {DebateConfig} The debate configuration.
|
|
398
|
+
* @throws {Error} If rounds is less than 1.
|
|
399
|
+
*/
|
|
400
|
+
function debateConfigFromSysConfig(sysConfig: SystemConfig, options: any): DebateConfig {
|
|
401
|
+
const debateCfg: DebateConfig = {
|
|
402
|
+
...sysConfig.debate!,
|
|
403
|
+
rounds: options.rounds ? parseInt(options.rounds, 10) : (sysConfig.debate?.rounds ?? DEFAULT_ROUNDS),
|
|
404
|
+
} as DebateConfig;
|
|
405
|
+
|
|
406
|
+
if (!debateCfg.rounds || debateCfg.rounds < 1) {
|
|
407
|
+
throw createValidationError('Invalid arguments: --rounds must be >= 1', EXIT_INVALID_ARGS);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return debateCfg;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Filters and returns agent configurations from the system configuration based on command-line options.
|
|
415
|
+
* If specific agent roles are provided via options, only agents with matching roles are included.
|
|
416
|
+
* If no agents are selected after filtering, defaults to built-in agents.
|
|
417
|
+
*
|
|
418
|
+
* @param {SystemConfig} sysConfig - The system configuration.
|
|
419
|
+
* @param {any} options - Command-line options containing optional agent roles filter.
|
|
420
|
+
* @returns {AgentConfig[]} Array of filtered agent configurations.
|
|
421
|
+
*/
|
|
422
|
+
function agentConfigsFromSysConfig(sysConfig: SystemConfig, options: any): AgentConfig[] {
|
|
423
|
+
let agentConfigs = sysConfig.agents.filter((a) => a.enabled !== false);
|
|
424
|
+
|
|
425
|
+
if (options.agents) {
|
|
426
|
+
const roles = String(options.agents).split(',').map((r: string) => r.trim());
|
|
427
|
+
agentConfigs = agentConfigs.filter((a) => roles.includes(a.role));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (agentConfigs.length === 0) {
|
|
431
|
+
warnUser('No agents selected; defaulting to architect,performance.');
|
|
432
|
+
const defaults = builtInDefaults();
|
|
433
|
+
agentConfigs = defaults.agents;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return agentConfigs;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Validates the exactly-one constraint for problem sources.
|
|
442
|
+
* @param hasProblem - Whether problem string is provided.
|
|
443
|
+
* @param hasFile - Whether problemDescription file is provided.
|
|
444
|
+
* @throws {Error} If both or neither are provided.
|
|
445
|
+
*/
|
|
446
|
+
function validateExactlyOneProblemSource(hasProblem: boolean, hasFile: boolean): void {
|
|
447
|
+
if (hasProblem && hasFile) {
|
|
448
|
+
throw createValidationError(ERROR_BOTH_PROBLEM_SOURCES, EXIT_INVALID_ARGS);
|
|
449
|
+
}
|
|
450
|
+
if (!hasProblem && !hasFile) {
|
|
451
|
+
throw createValidationError(ERROR_NO_PROBLEM_SOURCE, EXIT_INVALID_ARGS);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Validates that the file path exists and is a file (not directory).
|
|
457
|
+
* @param filePath - Absolute path to the file.
|
|
458
|
+
* @throws {Error} If file doesn't exist or is a directory.
|
|
459
|
+
*/
|
|
460
|
+
function validateFilePathExists(filePath: string): void {
|
|
461
|
+
if (!fs.existsSync(filePath)) {
|
|
462
|
+
throw createValidationError(`${ERROR_FILE_NOT_FOUND}: ${filePath}`, EXIT_INVALID_ARGS);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const stats = fs.statSync(filePath);
|
|
466
|
+
if (stats.isDirectory()) {
|
|
467
|
+
throw createValidationError(`${ERROR_PATH_IS_DIRECTORY}: ${filePath}`, EXIT_INVALID_ARGS);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Reads and validates file content.
|
|
473
|
+
* @param filePath - Absolute path to the file.
|
|
474
|
+
* @returns File content as string.
|
|
475
|
+
* @throws {Error} If file is empty or read fails.
|
|
476
|
+
*/
|
|
477
|
+
async function readAndValidateFileContent(filePath: string): Promise<string> {
|
|
478
|
+
try {
|
|
479
|
+
const content = await fs.promises.readFile(filePath, FILE_ENCODING_UTF8);
|
|
480
|
+
|
|
481
|
+
// Check if content is non-empty after trimming (whitespace-only = empty)
|
|
482
|
+
if (content.trim().length === 0) {
|
|
483
|
+
throw createValidationError(`${ERROR_FILE_EMPTY}: ${filePath}`, EXIT_INVALID_ARGS);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Return raw content (preserve formatting)
|
|
487
|
+
return content;
|
|
488
|
+
} catch (error: any) {
|
|
489
|
+
if (error.code === EXIT_INVALID_ARGS) {
|
|
490
|
+
throw error;
|
|
491
|
+
}
|
|
492
|
+
// Handle read errors
|
|
493
|
+
throw createValidationError(`${ERROR_FILE_READ_FAILED}: ${error.message}`, EXIT_GENERAL_ERROR);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Resolves the problem description from either command line string or file.
|
|
499
|
+
* Enforces exactly-one constraint and validates file content.
|
|
500
|
+
*
|
|
501
|
+
* @param {string | undefined} problem - Optional problem string from command line.
|
|
502
|
+
* @param {any} options - Command-line options containing optional problemDescription.
|
|
503
|
+
* @returns {Promise<string>} The resolved problem description.
|
|
504
|
+
* @throws {Error} If validation fails or file operations fail.
|
|
505
|
+
*/
|
|
506
|
+
async function resolveProblemDescription(problem: string | undefined, options: any): Promise<string> {
|
|
507
|
+
const hasProblem = !!(problem && problem.trim().length > 0);
|
|
508
|
+
const hasFile = !!options.problemDescription;
|
|
509
|
+
|
|
510
|
+
// Validate exactly-one constraint
|
|
511
|
+
validateExactlyOneProblemSource(hasProblem, hasFile);
|
|
512
|
+
|
|
513
|
+
// Return problem string if provided
|
|
514
|
+
if (hasProblem) {
|
|
515
|
+
return problem!.trim();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Handle file-based problem description
|
|
519
|
+
const filePath = path.resolve(process.cwd(), options.problemDescription);
|
|
520
|
+
validateFilePathExists(filePath);
|
|
521
|
+
return await readAndValidateFileContent(filePath);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Outputs a summary of a single debate round to stderr for verbose mode.
|
|
526
|
+
* Lists all contributions (proposals, critiques, refinements) and summaries with metadata.
|
|
527
|
+
*
|
|
528
|
+
* @param {DebateRound} round - The debate round to summarize.
|
|
529
|
+
*/
|
|
530
|
+
function outputRoundSummary(round: DebateRound): void {
|
|
531
|
+
writeStderr(`Round ${round.roundNumber}\n`);
|
|
532
|
+
|
|
533
|
+
// Output summaries if present
|
|
534
|
+
if (round.summaries && Object.keys(round.summaries).length > 0) {
|
|
535
|
+
writeStderr(` summaries:\n`);
|
|
536
|
+
Object.values(round.summaries).forEach((s) => {
|
|
537
|
+
const tokens = s.metadata.tokensUsed != null ? s.metadata.tokensUsed : 'N/A';
|
|
538
|
+
const lat = s.metadata.latencyMs != null ? `${s.metadata.latencyMs}ms` : 'N/A';
|
|
539
|
+
writeStderr(` [${s.agentRole}] ${s.metadata.beforeChars} → ${s.metadata.afterChars} chars\n`);
|
|
540
|
+
writeStderr(` (latency=${lat}, tokens=${tokens}, method=${s.metadata.method})\n`);
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const types = [CONTRIBUTION_TYPES.PROPOSAL, CONTRIBUTION_TYPES.CRITIQUE, CONTRIBUTION_TYPES.REFINEMENT] as const;
|
|
545
|
+
types.forEach((t) => {
|
|
546
|
+
const items = round.contributions.filter((c: Contribution) => c.type === t);
|
|
547
|
+
if (items.length > 0) {
|
|
548
|
+
writeStderr(` ${t}:\n`);
|
|
549
|
+
items.forEach((c: Contribution) => {
|
|
550
|
+
const firstLine = c.content.split('\n')[0];
|
|
551
|
+
const tokens = (c.metadata && c.metadata.tokensUsed != null) ? c.metadata.tokensUsed : 'N/A';
|
|
552
|
+
const lat = (c.metadata && c.metadata.latencyMs != null) ? `${c.metadata.latencyMs}ms` : 'N/A';
|
|
553
|
+
writeStderr(` [${c.agentRole}] ${firstLine}\n`);
|
|
554
|
+
writeStderr(` (latency=${lat}, tokens=${tokens})\n`);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Outputs the debate results to a file or stdout, with optional verbose summary.
|
|
562
|
+
* If an output path is provided and ends with .json, writes the full debate state.
|
|
563
|
+
* Otherwise, writes the final solution text. If no output path is provided, writes to stdout.
|
|
564
|
+
* When verbose mode is enabled and no output file is specified, also writes a detailed summary.
|
|
565
|
+
*
|
|
566
|
+
* @param {DebateResult} result - The debate result containing the solution and metadata.
|
|
567
|
+
* @param {StateManager} stateManager - The state manager to retrieve the full debate state.
|
|
568
|
+
* @param {any} options - Command-line options containing output path and verbose flag.
|
|
569
|
+
* @returns {Promise<void>} A promise that resolves when output is complete.
|
|
570
|
+
*/
|
|
571
|
+
async function outputResults(result: DebateResult, stateManager: StateManager, options: any): Promise<void> {
|
|
572
|
+
const outputPath = options.output ? path.resolve(process.cwd(), options.output) : undefined;
|
|
573
|
+
const finalText = result.solution.description + '\n';
|
|
574
|
+
|
|
575
|
+
if (outputPath) {
|
|
576
|
+
if (outputPath.toLowerCase().endsWith(JSON_FILE_EXTENSION)) {
|
|
577
|
+
const fullState = await stateManager.getDebate(result.debateId);
|
|
578
|
+
await fs.promises.writeFile(outputPath, JSON.stringify(fullState, null, JSON_INDENT_SPACES), FILE_ENCODING_UTF8);
|
|
579
|
+
} else {
|
|
580
|
+
await fs.promises.writeFile(outputPath, finalText, FILE_ENCODING_UTF8);
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
// stdout minimal
|
|
584
|
+
process.stdout.write(finalText);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Verbose summary after solution to stderr (only when not writing to a file)
|
|
588
|
+
if (!outputPath && options.verbose) {
|
|
589
|
+
const debate = await stateManager.getDebate(result.debateId);
|
|
590
|
+
if (debate) {
|
|
591
|
+
writeStderr('\nSummary (verbose)\n');
|
|
592
|
+
debate.rounds.forEach(outputRoundSummary);
|
|
593
|
+
const totalTokens = debate.rounds.reduce((sum, r) => sum + r.contributions.reduce((s, c) => s + (c.metadata.tokensUsed ?? 0), 0), 0);
|
|
594
|
+
writeStderr(`\nTotals: rounds=${result.metadata.totalRounds}, duration=${result.metadata.durationMs}ms, tokens=${totalTokens ?? 'N/A'}\n`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Generates and writes a markdown report file for the debate.
|
|
601
|
+
*
|
|
602
|
+
* This function retrieves the full debate state, generates a markdown report using
|
|
603
|
+
* generateDebateReport, enforces the .md file extension, and writes the report to a file.
|
|
604
|
+
* Report generation failures are caught and logged but do not fail the debate.
|
|
605
|
+
*
|
|
606
|
+
* @param result - The debate result containing the debate ID and metadata.
|
|
607
|
+
* @param stateManager - The state manager to retrieve the full debate state.
|
|
608
|
+
* @param agentConfigs - Array of agent configurations for the report.
|
|
609
|
+
* @param judgeConfig - Judge configuration for the report.
|
|
610
|
+
* @param problemDescription - The full problem description text.
|
|
611
|
+
* @param options - CLI options including report path and verbose flag.
|
|
612
|
+
*/
|
|
613
|
+
async function generateReport(
|
|
614
|
+
result: DebateResult,
|
|
615
|
+
stateManager: StateManager,
|
|
616
|
+
agentConfigs: AgentConfig[],
|
|
617
|
+
judgeConfig: AgentConfig,
|
|
618
|
+
problemDescription: string,
|
|
619
|
+
options: any
|
|
620
|
+
): Promise<void> {
|
|
621
|
+
try {
|
|
622
|
+
// Validate and normalize report path
|
|
623
|
+
let reportPath = path.resolve(process.cwd(), options.report);
|
|
624
|
+
|
|
625
|
+
// Enforce .md extension
|
|
626
|
+
if (!reportPath.toLowerCase().endsWith(MARKDOWN_FILE_EXTENSION)) {
|
|
627
|
+
reportPath += MARKDOWN_FILE_EXTENSION;
|
|
628
|
+
warnUser(`Report path does not end with ${MARKDOWN_FILE_EXTENSION}, appending ${MARKDOWN_FILE_EXTENSION} extension: ${path.basename(reportPath)}`);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Get full debate state
|
|
632
|
+
const debateState = await stateManager.getDebate(result.debateId);
|
|
633
|
+
if (!debateState) {
|
|
634
|
+
throw new Error(`Debate state not found for ID: ${result.debateId}`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// Generate report content
|
|
638
|
+
const reportContent = generateDebateReport(
|
|
639
|
+
debateState,
|
|
640
|
+
agentConfigs,
|
|
641
|
+
judgeConfig,
|
|
642
|
+
problemDescription,
|
|
643
|
+
{ verbose: options.verbose }
|
|
644
|
+
);
|
|
645
|
+
|
|
646
|
+
// Write report file (handles path normalization and directory creation)
|
|
647
|
+
const writtenPath = await writeFileWithDirectories(reportPath, reportContent);
|
|
648
|
+
|
|
649
|
+
// Notify user
|
|
650
|
+
infoUser(`Generated report: ${writtenPath}`);
|
|
651
|
+
} catch (error: any) {
|
|
652
|
+
warnUser(`Failed to generate report: ${error.message}`);
|
|
653
|
+
// Don't throw - report generation failure shouldn't fail the debate
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
export function debateCommand(program: Command) {
|
|
658
|
+
program
|
|
659
|
+
.command('debate')
|
|
660
|
+
.argument('[problem]', 'Problem statement to debate (provide exactly one of this or --problemDescription)')
|
|
661
|
+
.option('-a, --agents <roles>', 'Comma-separated agent roles (architect,performance,...)')
|
|
662
|
+
.option('-r, --rounds <number>', `Number of rounds (default ${DEFAULT_ROUNDS})`)
|
|
663
|
+
.option('-c, --config <path>', 'Path to configuration file (default ./debate-config.json)')
|
|
664
|
+
.option('-o, --output <path>', 'Output file; .json writes full state, others write final solution text')
|
|
665
|
+
.option('-p, --problemDescription <path>', 'Path to a text file containing the problem description')
|
|
666
|
+
.option('-e, --env-file <path>', 'Path to environment file (default: .env)')
|
|
667
|
+
.option('-v, --verbose', 'Verbose output')
|
|
668
|
+
.option('--report <path>', 'Generate markdown report file')
|
|
669
|
+
.option('--clarify', 'Run a one-time pre-debate clarifications phase')
|
|
670
|
+
.action(async (problem: string | undefined, options: any) => {
|
|
671
|
+
try {
|
|
672
|
+
// Load environment variables from .env file
|
|
673
|
+
loadEnvironmentFile(options.envFile, options.verbose);
|
|
674
|
+
|
|
675
|
+
const resolvedProblem = await resolveProblemDescription(problem, options);
|
|
676
|
+
|
|
677
|
+
const sysConfig = await loadConfig(options.config);
|
|
678
|
+
const debateCfg = debateConfigFromSysConfig(sysConfig, options);
|
|
679
|
+
const agentConfigs = agentConfigsFromSysConfig(sysConfig, options);
|
|
680
|
+
|
|
681
|
+
const promptSources: { agents: AgentPromptMetadata[]; judge: JudgePromptMetadata } = {
|
|
682
|
+
agents: [],
|
|
683
|
+
judge: { id: sysConfig.judge!.id, source: PROMPT_SOURCES.BUILT_IN },
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// Get system-wide summarization config (use defaults if not in config)
|
|
687
|
+
const systemSummaryConfig: SummarizationConfig = debateCfg.summarization || {
|
|
688
|
+
enabled: DEFAULT_SUMMARIZATION_ENABLED,
|
|
689
|
+
threshold: DEFAULT_SUMMARIZATION_THRESHOLD,
|
|
690
|
+
maxLength: DEFAULT_SUMMARIZATION_MAX_LENGTH,
|
|
691
|
+
method: DEFAULT_SUMMARIZATION_METHOD,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
const agents = buildAgents(agentConfigs, sysConfig.configDir || process.cwd(), systemSummaryConfig, promptSources);
|
|
695
|
+
|
|
696
|
+
// Create judge with prompt resolution
|
|
697
|
+
const judge = createJudgeWithPromptResolution(sysConfig, systemSummaryConfig, promptSources);
|
|
698
|
+
|
|
699
|
+
const stateManager = new StateManager();
|
|
700
|
+
|
|
701
|
+
// Clarifications phase (optional)
|
|
702
|
+
const shouldClarify: boolean = (options.clarify === true) || (sysConfig.debate?.interactiveClarifications === true);
|
|
703
|
+
let finalClarifications: AgentClarifications[] | undefined = undefined;
|
|
704
|
+
if (shouldClarify) {
|
|
705
|
+
const maxPer = sysConfig.debate?.clarificationsMaxPerAgent ?? DEFAULT_CLARIFICATIONS_MAX_PER_AGENT;
|
|
706
|
+
finalClarifications = await collectAndAnswerClarifications(resolvedProblem, agents, maxPer);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Verbose header before run
|
|
710
|
+
if (options.verbose) {
|
|
711
|
+
writeStderr('Running debate (verbose)\n');
|
|
712
|
+
writeStderr('Active Agents:\n');
|
|
713
|
+
agentConfigs.forEach(a => {
|
|
714
|
+
const used = promptSources.agents.find(p => p.agentId === a.id);
|
|
715
|
+
writeStderr(` • ${a.name} (${a.model})\n`);
|
|
716
|
+
writeStderr(` - System prompt: ${used?.source === 'file' ? (used.path || 'file') : 'built-in default'}\n`);
|
|
717
|
+
});
|
|
718
|
+
writeStderr(`Judge: ${sysConfig.judge!.name} (${sysConfig.judge!.model})\n`);
|
|
719
|
+
writeStderr(` - System prompt: ${promptSources.judge.source === 'file' ? (promptSources.judge.path || 'file') : 'built-in default'}\n`);
|
|
720
|
+
writeStderr('\nSummarization:\n');
|
|
721
|
+
writeStderr(` - Enabled: ${systemSummaryConfig.enabled}\n`);
|
|
722
|
+
writeStderr(` - Threshold: ${systemSummaryConfig.threshold} characters\n`);
|
|
723
|
+
writeStderr(` - Max summary length: ${systemSummaryConfig.maxLength} characters\n`);
|
|
724
|
+
writeStderr(` - Method: ${systemSummaryConfig.method}\n`);
|
|
725
|
+
writeStderr('\n');
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// Initialize progress UI
|
|
729
|
+
const progressUI = new DebateProgressUI();
|
|
730
|
+
progressUI.initialize(debateCfg.rounds);
|
|
731
|
+
|
|
732
|
+
// Create orchestrator hooks to drive progress UI
|
|
733
|
+
const hooks = createOrchestratorHooks(progressUI, options);
|
|
734
|
+
|
|
735
|
+
const orchestrator = new DebateOrchestrator(agents, judge, stateManager, debateCfg, hooks);
|
|
736
|
+
|
|
737
|
+
// Start progress UI and run debate
|
|
738
|
+
await progressUI.start();
|
|
739
|
+
const result: DebateResult = await orchestrator.runDebate(resolvedProblem, undefined, finalClarifications);
|
|
740
|
+
await progressUI.complete();
|
|
741
|
+
|
|
742
|
+
// Persist prompt sources once per debate
|
|
743
|
+
await stateManager.setPromptSources(result.debateId, promptSources);
|
|
744
|
+
|
|
745
|
+
// Persist path notice (StateManager already persisted during run)
|
|
746
|
+
infoUser(`Saved debate to ./debates/${result.debateId}.json`);
|
|
747
|
+
|
|
748
|
+
await outputResults(result, stateManager, options);
|
|
749
|
+
|
|
750
|
+
// Generate report if requested
|
|
751
|
+
if (options.report) {
|
|
752
|
+
await generateReport(result, stateManager, agentConfigs, sysConfig.judge!, resolvedProblem, options);
|
|
753
|
+
}
|
|
754
|
+
} catch (err: any) {
|
|
755
|
+
const code = typeof err?.code === 'number' ? err.code : EXIT_GENERAL_ERROR;
|
|
756
|
+
writeStderr((err?.message || 'Unknown error') + '\n');
|
|
757
|
+
// Rethrow for runCli catch to set process exit when direct run
|
|
758
|
+
throw Object.assign(new Error(err?.message || 'Unknown error'), { code });
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
}
|