aegiscode 3.1.8 → 3.1.10

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 (206) hide show
  1. package/README.md +23 -15
  2. package/dist/main.js +549 -552
  3. package/package.json +4 -2
  4. package/src/agent/Agent.ts +903 -0
  5. package/src/agent/SimpleAgent.ts +48 -0
  6. package/src/agent/index.ts +54 -0
  7. package/src/agent/orchestrator/AppBuilder.ts +443 -0
  8. package/src/agent/orchestrator/CouncilAgent.ts +310 -0
  9. package/src/agent/orchestrator/DiscussionRoom.ts +462 -0
  10. package/src/agent/orchestrator/OrchestratorAgent.ts +637 -0
  11. package/src/agent/orchestrator/index.ts +38 -0
  12. package/src/agent/orchestrator/utils.ts +397 -0
  13. package/src/agent/pricing.ts +115 -0
  14. package/src/agent/router.ts +74 -0
  15. package/src/agent/routerStats.ts +121 -0
  16. package/src/agent/types.ts +318 -0
  17. package/src/auth/login.ts +383 -0
  18. package/src/cli/config.ts +189 -0
  19. package/src/cli/index.ts +17 -0
  20. package/src/cli/middleware.ts +119 -0
  21. package/src/cli/types.ts +75 -0
  22. package/src/config/ConfigManager.ts +587 -0
  23. package/src/config/index.ts +7 -0
  24. package/src/config/types.ts +584 -0
  25. package/src/context/CompactionService.ts +300 -0
  26. package/src/context/ContextManager.ts +450 -0
  27. package/src/context/FileAnalyzer.ts +267 -0
  28. package/src/context/TokenCounter.ts +265 -0
  29. package/src/context/index.ts +27 -0
  30. package/src/context/storage/CacheStore.ts +176 -0
  31. package/src/context/storage/JSONLStore.ts +201 -0
  32. package/src/context/storage/MemoryStore.ts +205 -0
  33. package/src/context/storage/PersistentStore.ts +327 -0
  34. package/src/context/storage/index.ts +9 -0
  35. package/src/context/storage/pathUtils.ts +114 -0
  36. package/src/context/test.ts +309 -0
  37. package/src/context/types.ts +268 -0
  38. package/src/hooks/HookExecutor.ts +434 -0
  39. package/src/hooks/HookManager.ts +596 -0
  40. package/src/hooks/HookService.ts +269 -0
  41. package/src/hooks/Matcher.ts +157 -0
  42. package/src/hooks/index.ts +63 -0
  43. package/src/hooks/types.ts +424 -0
  44. package/src/main.tsx +596 -0
  45. package/src/mcp/HealthMonitor.ts +150 -0
  46. package/src/mcp/McpClient.ts +491 -0
  47. package/src/mcp/McpRegistry.ts +321 -0
  48. package/src/mcp/createMcpTool.ts +251 -0
  49. package/src/mcp/index.ts +15 -0
  50. package/src/mcp/server.ts +334 -0
  51. package/src/mcp/test-server.ts +88 -0
  52. package/src/mcp/test.ts +372 -0
  53. package/src/mcp/types.ts +247 -0
  54. package/src/memory/AgentMemoryBus.ts +432 -0
  55. package/src/memory/CloudSync.ts +99 -0
  56. package/src/memory/DriveSync.ts +106 -0
  57. package/src/memory/SharedMemory.ts +951 -0
  58. package/src/memory/index.ts +14 -0
  59. package/src/memory/machineFingerprint.ts +40 -0
  60. package/src/orchestrator/SubAgentMetadata.ts +136 -0
  61. package/src/prompts/builder.ts +213 -0
  62. package/src/prompts/default.ts +144 -0
  63. package/src/prompts/index.ts +16 -0
  64. package/src/prompts/plan.ts +64 -0
  65. package/src/prompts/test.ts +78 -0
  66. package/src/services/AnthropicChatService.ts +341 -0
  67. package/src/services/ChatService.ts +347 -0
  68. package/src/services/ClaudeCliChatService.ts +256 -0
  69. package/src/services/CloudSync.ts +168 -0
  70. package/src/services/CostLedger.ts +211 -0
  71. package/src/services/Heartbeat.ts +135 -0
  72. package/src/services/LearningCollector.ts +291 -0
  73. package/src/services/OllamaInstaller.ts +342 -0
  74. package/src/services/VersionChecker.ts +445 -0
  75. package/src/services/index.ts +57 -0
  76. package/src/services/streaming/OpenAIEventAdapter.ts +126 -0
  77. package/src/services/streaming/RenderingProfile.ts +90 -0
  78. package/src/services/streaming/StreamEventParser.ts +181 -0
  79. package/src/services/streaming/ThrottledRenderer.ts +139 -0
  80. package/src/services/streaming/TranscriptBuffer.ts +574 -0
  81. package/src/services/streaming/eventStatusMap.ts +52 -0
  82. package/src/services/streaming/index.ts +46 -0
  83. package/src/services/streaming/renderFormatting.ts +79 -0
  84. package/src/services/streaming/types.ts +234 -0
  85. package/src/skills/SkillLoader.ts +126 -0
  86. package/src/skills/SkillRegistry.ts +366 -0
  87. package/src/skills/index.ts +48 -0
  88. package/src/skills/types.ts +146 -0
  89. package/src/slash-commands/billing.ts +70 -0
  90. package/src/slash-commands/build.ts +413 -0
  91. package/src/slash-commands/builtinCommands.ts +2733 -0
  92. package/src/slash-commands/clone.ts +242 -0
  93. package/src/slash-commands/council.ts +125 -0
  94. package/src/slash-commands/custom/CustomCommandExecutor.ts +143 -0
  95. package/src/slash-commands/custom/CustomCommandLoader.ts +251 -0
  96. package/src/slash-commands/custom/CustomCommandRegistry.ts +144 -0
  97. package/src/slash-commands/custom/index.ts +7 -0
  98. package/src/slash-commands/debate.ts +254 -0
  99. package/src/slash-commands/gmail.ts +105 -0
  100. package/src/slash-commands/index.ts +388 -0
  101. package/src/slash-commands/mcpCommand.ts +205 -0
  102. package/src/slash-commands/types.ts +201 -0
  103. package/src/store/index.ts +76 -0
  104. package/src/store/selectors.ts +246 -0
  105. package/src/store/slices/appSlice.ts +205 -0
  106. package/src/store/slices/commandSlice.ts +115 -0
  107. package/src/store/slices/configSlice.ts +46 -0
  108. package/src/store/slices/focusSlice.ts +64 -0
  109. package/src/store/slices/index.ts +9 -0
  110. package/src/store/slices/sessionSlice.ts +424 -0
  111. package/src/store/streaming-buffer.ts +425 -0
  112. package/src/store/test.ts +296 -0
  113. package/src/store/types.ts +274 -0
  114. package/src/store/vanilla.ts +186 -0
  115. package/src/tools/builtin/bash.ts +236 -0
  116. package/src/tools/builtin/council.ts +105 -0
  117. package/src/tools/builtin/edit.ts +213 -0
  118. package/src/tools/builtin/glob.ts +136 -0
  119. package/src/tools/builtin/grep.ts +263 -0
  120. package/src/tools/builtin/index.ts +61 -0
  121. package/src/tools/builtin/memory.ts +66 -0
  122. package/src/tools/builtin/read.ts +168 -0
  123. package/src/tools/builtin/skill.ts +97 -0
  124. package/src/tools/builtin/snapshot.ts +40 -0
  125. package/src/tools/builtin/task.ts +106 -0
  126. package/src/tools/builtin/write.ts +134 -0
  127. package/src/tools/createTool.ts +221 -0
  128. package/src/tools/execution/ExecutionPipeline.ts +263 -0
  129. package/src/tools/execution/index.ts +40 -0
  130. package/src/tools/execution/stages/CacheStage.ts +131 -0
  131. package/src/tools/execution/stages/ConfirmationStage.ts +203 -0
  132. package/src/tools/execution/stages/DiscoveryStage.ts +46 -0
  133. package/src/tools/execution/stages/ExecutionStage.ts +48 -0
  134. package/src/tools/execution/stages/FormattingStage.ts +44 -0
  135. package/src/tools/execution/stages/HookStage.ts +72 -0
  136. package/src/tools/execution/stages/PermissionStage.ts +287 -0
  137. package/src/tools/execution/stages/PostHookStage.ts +71 -0
  138. package/src/tools/execution/stages/index.ts +12 -0
  139. package/src/tools/execution/test.ts +266 -0
  140. package/src/tools/execution/types.ts +273 -0
  141. package/src/tools/index.ts +81 -0
  142. package/src/tools/registry.ts +304 -0
  143. package/src/tools/schemas.ts +109 -0
  144. package/src/tools/test.ts +220 -0
  145. package/src/tools/types.ts +175 -0
  146. package/src/tools/validation/PermissionChecker.ts +242 -0
  147. package/src/tools/validation/SensitiveFileDetector.ts +210 -0
  148. package/src/tools/validation/index.ts +11 -0
  149. package/src/ui/App.tsx +166 -0
  150. package/src/ui/components/AegisInterface.tsx +484 -0
  151. package/src/ui/components/common/ChatSearch.tsx +150 -0
  152. package/src/ui/components/common/ErrorBoundary.tsx +82 -0
  153. package/src/ui/components/common/ExitMessage.tsx +120 -0
  154. package/src/ui/components/common/LoadingIndicator.tsx +49 -0
  155. package/src/ui/components/common/index.ts +6 -0
  156. package/src/ui/components/dialog/ConfirmationPrompt.tsx +208 -0
  157. package/src/ui/components/dialog/InteractiveSelector.tsx +149 -0
  158. package/src/ui/components/dialog/SetupWizard.tsx +297 -0
  159. package/src/ui/components/dialog/UpdatePrompt.tsx +155 -0
  160. package/src/ui/components/dialog/index.ts +8 -0
  161. package/src/ui/components/index.ts +28 -0
  162. package/src/ui/components/input/CommandSuggestions.tsx +139 -0
  163. package/src/ui/components/input/CustomTextInput.tsx +220 -0
  164. package/src/ui/components/input/InputArea.tsx +361 -0
  165. package/src/ui/components/input/PromptSuggestions.tsx +66 -0
  166. package/src/ui/components/input/index.ts +6 -0
  167. package/src/ui/components/layout/ChatStatusBar.tsx +90 -0
  168. package/src/ui/components/layout/ContextBar.tsx +79 -0
  169. package/src/ui/components/layout/MessageArea.tsx +96 -0
  170. package/src/ui/components/layout/MessageList.tsx +647 -0
  171. package/src/ui/components/layout/MessageSeparator.tsx +26 -0
  172. package/src/ui/components/layout/WelcomeMessage.tsx +93 -0
  173. package/src/ui/components/layout/index.ts +7 -0
  174. package/src/ui/components/markdown/CodeHighlighter.tsx +292 -0
  175. package/src/ui/components/markdown/MessageRenderer.tsx +1211 -0
  176. package/src/ui/components/markdown/index.ts +8 -0
  177. package/src/ui/components/markdown/parser.ts +336 -0
  178. package/src/ui/components/markdown/types.ts +66 -0
  179. package/src/ui/focus/FocusManager.ts +137 -0
  180. package/src/ui/focus/index.ts +13 -0
  181. package/src/ui/focus/types.ts +54 -0
  182. package/src/ui/focus/useFocus.ts +75 -0
  183. package/src/ui/hooks/index.ts +11 -0
  184. package/src/ui/hooks/useAgent.ts +284 -0
  185. package/src/ui/hooks/useCommandHistory.ts +87 -0
  186. package/src/ui/hooks/useCommandProcessor.ts +443 -0
  187. package/src/ui/hooks/useConfirmation.ts +99 -0
  188. package/src/ui/hooks/useCtrlCHandler.ts +100 -0
  189. package/src/ui/hooks/useInputBuffer.ts +122 -0
  190. package/src/ui/hooks/useTerminalSize.ts +68 -0
  191. package/src/ui/hooks/useTerminalWidth.ts +5 -0
  192. package/src/ui/hooks/useWindowedList.ts +118 -0
  193. package/src/ui/render-debugger.ts +621 -0
  194. package/src/ui/test.ts +189 -0
  195. package/src/ui/themes/ThemeManager.ts +332 -0
  196. package/src/ui/themes/aegisTheme.ts +87 -0
  197. package/src/ui/themes/darkTheme.ts +87 -0
  198. package/src/ui/themes/defaultTheme.ts +85 -0
  199. package/src/ui/themes/index.ts +10 -0
  200. package/src/ui/themes/lightTheme.ts +89 -0
  201. package/src/ui/themes/popularThemes.ts +187 -0
  202. package/src/ui/themes/types.ts +130 -0
  203. package/src/utils/clipboard.ts +48 -0
  204. package/src/utils/debug.ts +43 -0
  205. package/src/utils/environment.ts +68 -0
  206. package/src/utils/index.ts +10 -0
@@ -0,0 +1,462 @@
1
+ /**
2
+ * DiscussionRoom — Multi-model debate & discussion orchestrator
3
+ *
4
+ * Runs N AI models in a structured debate/discussion format.
5
+ * Each model responds round-robin, seeing all previous responses.
6
+ *
7
+ * Design:
8
+ * - Creates one IChatService per model (shares existing infra)
9
+ * - Runs configurable rounds with optional moderator
10
+ * - Each model gets role-specific system prompt + full transcript
11
+ * - Results streamed via callback or returned as structured data
12
+ */
13
+
14
+ import type { AgentConfig, IChatService, Message, ChatResponse } from '../types.js';
15
+ import { createChatService } from '../../services/ChatService.js';
16
+ import { agentMemoryBus } from '../../memory/AgentMemoryBus.js';
17
+ import { agentDebug } from '../../utils/debug.js';
18
+
19
+ // ── Types ────────────────────────────────────────────────────────────────────────
20
+
21
+ export type DiscussionFormat = 'debate' | 'discussion' | 'qa' | 'panel';
22
+
23
+ // ── Event system ───────────────────────────────────────────────────────────
24
+
25
+ export type DiscussionEventType =
26
+ | 'model:start' // Model starts generating a response
27
+ | 'model:chunk' // Streaming token from a model
28
+ | 'model:complete' // Model finished its response
29
+ | 'model:error' // Model failed
30
+ | 'round:start' // New round begins
31
+ | 'discussion:complete' // All rounds done
32
+ | 'summary:ready'; // Summary synthesized
33
+
34
+ export interface DiscussionEvent {
35
+ type: DiscussionEventType;
36
+ model?: string;
37
+ round?: number;
38
+ content?: string;
39
+ metadata?: Record<string, unknown>;
40
+ timestamp: number;
41
+ }
42
+
43
+ export type DiscussionEventHandler = (event: DiscussionEvent) => void;
44
+
45
+ // ── Model config ───────────────────────────────────────────────────────────
46
+
47
+ export interface DebateModelConfig {
48
+ /** Display name (e.g. "GPT-4o", "DeepSeek") */
49
+ name: string;
50
+ /** LLM config passed to createChatService */
51
+ config: AgentConfig;
52
+ /** Optional role description for system prompt */
53
+ role?: string;
54
+ /** Optional override color (ANSI 24-bit) */
55
+ color?: string;
56
+ }
57
+
58
+ export interface DiscussionConfig {
59
+ topic: string;
60
+ models: DebateModelConfig[];
61
+ /** Number of rounds (default 2) */
62
+ rounds?: number;
63
+ /** Format style (default 'debate') */
64
+ format?: DiscussionFormat;
65
+ /** Max tokens per response (default 500) */
66
+ maxTokensPerResponse?: number;
67
+ /** Optional moderator model index or name (pauses debate for mid-point synthesis) */
68
+ moderator?: number | string;
69
+ /** Timeout per model call in ms (default 60000) */
70
+ timeout?: number;
71
+ }
72
+
73
+ export interface DebateRound {
74
+ round: number;
75
+ speaker: string;
76
+ role: string;
77
+ content: string;
78
+ metadata?: {
79
+ tokensUsed?: number;
80
+ durationMs?: number;
81
+ };
82
+ }
83
+
84
+ export interface DiscussionResult {
85
+ topic: string;
86
+ format: DiscussionFormat;
87
+ rounds: DebateRound[];
88
+ summary: string;
89
+ metadata: {
90
+ modelsUsed: number;
91
+ totalRounds: number;
92
+ totalDurationMs: number;
93
+ totalTokens: number;
94
+ };
95
+ }
96
+
97
+ // ── Colors ───────────────────────────────────────────────────────────────────────
98
+
99
+ const COLORS = [
100
+ '\x1b[38;2;0;229;192m', // teal
101
+ '\x1b[38;2;124;111;212m', // purple
102
+ '\x1b[38;2;244;114;182m', // pink
103
+ '\x1b[38;2;249;115;22m', // orange
104
+ '\x1b[38;2;34;197;94m', // green
105
+ '\x1b[38;2;56;189;248m', // sky
106
+ '\x1b[38;2;250;204;21m', // yellow
107
+ '\x1b[38;2;239;68;68m', // red
108
+ ];
109
+
110
+ // ── DiscussionRoom ───────────────────────────────────────────────────────────────
111
+
112
+ export class DiscussionRoom {
113
+ private config: DiscussionConfig & {
114
+ rounds: number;
115
+ format: DiscussionFormat;
116
+ maxTokensPerResponse: number;
117
+ timeout: number;
118
+ };
119
+ private services: Map<string, IChatService> = new Map();
120
+ private listeners: Map<DiscussionEventType, Set<DiscussionEventHandler>> = new Map();
121
+ private anyListeners: Set<DiscussionEventHandler> = new Set();
122
+
123
+ constructor(config: DiscussionConfig) {
124
+ this.config = {
125
+ rounds: 2,
126
+ format: 'debate',
127
+ maxTokensPerResponse: 500,
128
+ timeout: 60000,
129
+ ...config,
130
+ } as typeof this.config;
131
+
132
+ // Create chat services for each model
133
+ for (const model of this.config.models) {
134
+ const svc = createChatService({
135
+ ...model.config,
136
+ timeout: this.config.timeout,
137
+ maxOutputTokens: this.config.maxTokensPerResponse,
138
+ });
139
+ this.services.set(model.name, svc);
140
+ }
141
+ }
142
+
143
+ // ── Event system ───────────────────────────────────────────────────────────
144
+
145
+ /**
146
+ * Subscribe to debate events.
147
+ * Returns unsubscribe function.
148
+ */
149
+ on(type: DiscussionEventType, handler: DiscussionEventHandler): () => void {
150
+ if (!this.listeners.has(type)) this.listeners.set(type, new Set());
151
+ this.listeners.get(type)!.add(handler);
152
+ return () => this.listeners.get(type)?.delete(handler);
153
+ }
154
+
155
+ /**
156
+ * Subscribe to all debate events.
157
+ */
158
+ onAny(handler: DiscussionEventHandler): () => void {
159
+ this.anyListeners.add(handler);
160
+ return () => this.anyListeners.delete(handler);
161
+ }
162
+
163
+ private emit(type: DiscussionEventType, data: Omit<DiscussionEvent, 'type' | 'timestamp'>): void {
164
+ const event: DiscussionEvent = { type, timestamp: Date.now(), ...data };
165
+ const typeListeners = this.listeners.get(type);
166
+ if (typeListeners) {
167
+ for (const h of typeListeners) try { h(event); } catch { /* handler error */ }
168
+ }
169
+ for (const h of this.anyListeners) try { h(event); } catch { /* handler error */ }
170
+ }
171
+
172
+ // ── Core ───────────────────────────────────────────────────────────────────
173
+
174
+ /**
175
+ * Run the full debate and return structured results.
176
+ */
177
+ async run(): Promise<DiscussionResult> {
178
+ const startTime = Date.now();
179
+ const allRounds: DebateRound[] = [];
180
+ const format = this.config.format;
181
+ const models = this.config.models;
182
+
183
+ // ── System prompt templates per format ──
184
+ const roleTemplate = this.buildRoleTemplate(format);
185
+ const debateGuidelines = this.buildDebateGuidelines(format);
186
+
187
+ // Assign colors
188
+ const namedColors = new Map(
189
+ models.map((m, i) => [m.name, m.color || COLORS[i % COLORS.length]])
190
+ );
191
+
192
+ // ── Run rounds ──
193
+ for (let round = 1; round <= this.config.rounds; round++) {
194
+ const roundLabel = format === 'debate' ? `Argumentation` :
195
+ format === 'qa' ? `Question` : `Discussion`;
196
+
197
+ this.emit('round:start', { round, content: roundLabel });
198
+
199
+ for (const model of models) {
200
+ this.emit('model:start', { model: model.name, round, content: '' });
201
+ const durationStart = Date.now();
202
+
203
+ // Build messages for this model: system + full transcript so far
204
+ const messages: Message[] = [];
205
+ messages.push({
206
+ role: 'system',
207
+ content: [
208
+ roleTemplate,
209
+ `\n\nYour name: ${model.name}`,
210
+ model.role ? `\nRole: ${model.role}` : '',
211
+ `\nTopic: ${this.config.topic}`,
212
+ `\n\n${debateGuidelines}`,
213
+ format === 'debate' ? `\nCurrent round: ${round}/${this.config.rounds}` : '',
214
+ ].join(''),
215
+ });
216
+
217
+ // Inject conversation transcript
218
+ if (allRounds.length > 0) {
219
+ const transcript = allRounds
220
+ .map(r => `[${r.speaker} (${r.role})]: ${r.content}`)
221
+ .join('\n\n');
222
+ messages.push({
223
+ role: 'user',
224
+ content: `Previous discussion:\n\n${transcript}\n\n---\n\n${
225
+ format === 'debate'
226
+ ? `Round ${round}/${this.config.rounds} — Present your arguments. Be specific and persuasive.`
227
+ : format === 'qa'
228
+ ? `Answer the question from your perspective.`
229
+ : `Continue the discussion. Build on or challenge previous points.`
230
+ }`,
231
+ });
232
+ } else {
233
+ messages.push({
234
+ role: 'user',
235
+ content: format === 'debate'
236
+ ? `Round 1/${this.config.rounds} — Opening statement. Present your position on: "${this.config.topic}"`
237
+ : format === 'qa'
238
+ ? `Question: "${this.config.topic}" — Provide your answer and reasoning.`
239
+ : `Opening thoughts on: "${this.config.topic}" — Share your initial perspective.`,
240
+ });
241
+ }
242
+
243
+ // Call the model
244
+ let response: ChatResponse;
245
+ try {
246
+ response = await this.services.get(model.name)!.chat(messages);
247
+ } catch (err) {
248
+ const errMsg = err instanceof Error ? err.message : String(err);
249
+ agentDebug.error(`[DiscussionRoom] ${model.name} failed: ${errMsg}`);
250
+
251
+ this.emit('model:error', {
252
+ model: model.name,
253
+ round,
254
+ content: errMsg,
255
+ });
256
+
257
+ // Publish error to agent memory bus
258
+ agentMemoryBus.publish({
259
+ channel: 'error',
260
+ sourceAgent: model.name,
261
+ sessionId: `debate-${format}-${this.config.topic.slice(0, 30)}`,
262
+ content: `Failed to respond: ${errMsg}`,
263
+ importance: 0.9,
264
+ tags: ['debate', 'error'],
265
+ }).catch(() => {});
266
+
267
+ allRounds.push({
268
+ round,
269
+ speaker: model.name,
270
+ role: model.role || 'participant',
271
+ content: `[Failed to respond: ${errMsg}]`,
272
+ metadata: { durationMs: Date.now() - durationStart },
273
+ });
274
+ continue;
275
+ }
276
+
277
+ const roundEntry: DebateRound = {
278
+ round,
279
+ speaker: model.name,
280
+ role: model.role || 'participant',
281
+ content: response.content,
282
+ metadata: {
283
+ tokensUsed: response.usage?.totalTokens,
284
+ durationMs: Date.now() - durationStart,
285
+ },
286
+ };
287
+
288
+ allRounds.push(roundEntry);
289
+
290
+ this.emit('model:complete', {
291
+ model: model.name,
292
+ round,
293
+ content: response.content,
294
+ metadata: {
295
+ tokensUsed: response.usage?.totalTokens,
296
+ durationMs: Date.now() - durationStart,
297
+ },
298
+ });
299
+
300
+ // Publish to agent memory bus so other components can observe
301
+ agentMemoryBus.publish({
302
+ channel: 'fact',
303
+ sourceAgent: model.name,
304
+ sessionId: `debate-${format}-${this.config.topic.slice(0, 30)}`,
305
+ content: `[Debate Round ${round}] ${response.content.slice(0, 300)}`,
306
+ importance: 0.5,
307
+ tags: ['debate', `round-${round}`, format],
308
+ metadata: { round, topic: this.config.topic },
309
+ }).catch(() => {});
310
+ }
311
+ }
312
+
313
+ // ── Synthesize summary ──
314
+ const summary = this.synthesize(allRounds);
315
+
316
+ this.emit('summary:ready', { content: summary });
317
+
318
+ const totalTokens = allRounds.reduce(
319
+ (sum, r) => sum + (r.metadata?.tokensUsed || 0), 0
320
+ );
321
+
322
+ const result: DiscussionResult = {
323
+ topic: this.config.topic,
324
+ format,
325
+ rounds: allRounds,
326
+ summary,
327
+ metadata: {
328
+ modelsUsed: models.length,
329
+ totalRounds: this.config.rounds,
330
+ totalDurationMs: Date.now() - startTime,
331
+ totalTokens,
332
+ },
333
+ };
334
+
335
+ this.emit('discussion:complete', { content: '', metadata: result as unknown as Record<string, unknown> });
336
+
337
+ return result;
338
+ }
339
+
340
+ /**
341
+ * Format debate results as a markdown string (for slash command output).
342
+ */
343
+ formatAsMarkdown(result: DiscussionResult): string {
344
+ const lines: string[] = [];
345
+ const formatIcon = result.format === 'debate' ? '⚖' :
346
+ result.format === 'qa' ? '❓' :
347
+ result.format === 'panel' ? '🎙' : '💬';
348
+ const formatLabel = result.format.charAt(0).toUpperCase() + result.format.slice(1);
349
+
350
+ lines.push(`## ${formatIcon} ${formatLabel}: ${result.topic}`);
351
+ lines.push('');
352
+
353
+ // Unique speakers with their displayed colors
354
+ const speakers = [...new Set(result.rounds.map(r => r.speaker))];
355
+ lines.push(`*Participants: ${speakers.join(', ')} · ${result.metadata.totalRounds} rounds*`);
356
+ lines.push('');
357
+
358
+ for (const round of result.rounds) {
359
+ const color = this.config.models.find(m => m.name === round.speaker)?.color || COLORS[0];
360
+ const r = round.round;
361
+ lines.push(`### ${color}${round.speaker}${'\x1b[0m'} · ${round.role} _(Round ${r})_`);
362
+ lines.push('');
363
+ lines.push(round.content);
364
+ lines.push('');
365
+ }
366
+
367
+ lines.push('---');
368
+ lines.push('### Summary');
369
+ lines.push('');
370
+ lines.push(result.summary);
371
+ lines.push('');
372
+ lines.push(`*${result.metadata.modelsUsed} models · ${result.metadata.totalRounds} rounds · ${(result.metadata.totalDurationMs / 1000).toFixed(1)}s · ${result.metadata.totalTokens.toLocaleString()} tokens*`);
373
+
374
+ return lines.join('\n');
375
+ }
376
+
377
+ // ── Private ─────────────────────────────────────────────────────────────────────
378
+
379
+ private buildRoleTemplate(format: DiscussionFormat): string {
380
+ switch (format) {
381
+ case 'debate':
382
+ return 'You are participating in a structured debate. Present your position clearly and persuasively. Address counter-arguments when appropriate. Be concise but thorough.';
383
+ case 'discussion':
384
+ return 'You are participating in a collaborative discussion. Share your perspective, ask questions, and build on others\' ideas. Be constructive and insightful.';
385
+ case 'qa':
386
+ return 'You are answering questions as an expert in your field. Provide accurate, well-reasoned answers with specific details and examples when possible.';
387
+ case 'panel':
388
+ return 'You are a panelist in an expert discussion. Offer your professional perspective, reference relevant experience, and engage with other panelists\' viewpoints.';
389
+ default:
390
+ return 'You are participating in a discussion. Share your thoughts clearly.';
391
+ }
392
+ }
393
+
394
+ private buildDebateGuidelines(format: DiscussionFormat): string {
395
+ switch (format) {
396
+ case 'debate':
397
+ return `Guidelines:
398
+ - Be concise (max 3 paragraphs per round)
399
+ - Support claims with reasoning or evidence
400
+ - Address the strongest points made by other participants
401
+ - Stay on topic — do not introduce unrelated arguments
402
+ - Conclude each round with a clear takeaway`;
403
+ case 'discussion':
404
+ return `Guidelines:
405
+ - Be concise (max 3 paragraphs per turn)
406
+ - Build on or respectfully challenge previous points
407
+ - Ask clarifying questions when needed
408
+ - Keep the discussion productive and focused`;
409
+ case 'qa':
410
+ return `Guidelines:
411
+ - Be concise (max 2 paragraphs per answer)
412
+ - Provide specific, accurate information
413
+ - Acknowledge uncertainty when applicable
414
+ - Reference sources or reasoning where relevant`;
415
+ case 'panel':
416
+ return `Guidelines:
417
+ - Be concise (max 3 paragraphs per turn)
418
+ - Share real experiences and specific examples
419
+ - Engage with other panelists' points
420
+ - Keep the discussion accessible to the audience`;
421
+ default:
422
+ return 'Be concise and stay on topic.';
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Synthesize all rounds into a summary using the last model or built-in logic.
428
+ */
429
+ private synthesize(allRounds: DebateRound[]): string {
430
+ if (allRounds.length === 0) return 'No discussion took place.';
431
+
432
+ // Extract key topics from all responses
433
+ const topics = new Set<string>();
434
+ const allText = allRounds.map(r => r.content).join(' ');
435
+
436
+ // Simple keyword extraction for summary
437
+ const sentences = allText.split(/[.!?]+/).filter(s => s.trim().length > 20);
438
+
439
+ // Build a structured summary
440
+ const lines: string[] = [];
441
+ lines.push(`Discussion on "${this.config.topic}" covered ${allRounds.length} contributions from ${this.config.models.length} participants.`);
442
+
443
+ // Get final positions
444
+ const lastWords = new Map<string, string>();
445
+ for (const r of allRounds) {
446
+ const sentences = r.content.split(/[.!?]+/).filter(Boolean);
447
+ if (sentences.length > 0) {
448
+ lastWords.set(r.speaker, sentences[sentences.length - 1].trim());
449
+ }
450
+ }
451
+
452
+ if (lastWords.size > 0) {
453
+ lines.push('');
454
+ lines.push('**Closing positions:**');
455
+ for (const [speaker, closing] of lastWords) {
456
+ lines.push(`- ${speaker}: ${closing.slice(0, 150)}`);
457
+ }
458
+ }
459
+
460
+ return lines.join('\n');
461
+ }
462
+ }