lv24-opencode-consilium 0.0.0-stage → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +499 -3
  3. package/README.ru.md +500 -0
  4. package/README.zh.md +188 -0
  5. package/bin/cli.js +19 -0
  6. package/bin/mcp.js +24 -0
  7. package/consilium.config.example.json +84 -0
  8. package/dist/agents.d.ts +79 -0
  9. package/dist/agents.d.ts.map +1 -0
  10. package/dist/agents.js +1051 -0
  11. package/dist/agents.js.map +1 -0
  12. package/dist/artifacts.d.ts +75 -0
  13. package/dist/artifacts.d.ts.map +1 -0
  14. package/dist/artifacts.js +464 -0
  15. package/dist/artifacts.js.map +1 -0
  16. package/dist/cli.d.ts +12 -0
  17. package/dist/cli.d.ts.map +1 -0
  18. package/dist/cli.js +1392 -0
  19. package/dist/cli.js.map +1 -0
  20. package/dist/compat.d.ts +160 -0
  21. package/dist/compat.d.ts.map +1 -0
  22. package/dist/compat.js +419 -0
  23. package/dist/compat.js.map +1 -0
  24. package/dist/config.d.ts +112 -0
  25. package/dist/config.d.ts.map +1 -0
  26. package/dist/config.js +728 -0
  27. package/dist/config.js.map +1 -0
  28. package/dist/consensus.d.ts +73 -0
  29. package/dist/consensus.d.ts.map +1 -0
  30. package/dist/consensus.js +273 -0
  31. package/dist/consensus.js.map +1 -0
  32. package/dist/defaults.d.ts +34 -0
  33. package/dist/defaults.d.ts.map +1 -0
  34. package/dist/defaults.js +102 -0
  35. package/dist/defaults.js.map +1 -0
  36. package/dist/doctor.d.ts +27 -0
  37. package/dist/doctor.d.ts.map +1 -0
  38. package/dist/doctor.js +798 -0
  39. package/dist/doctor.js.map +1 -0
  40. package/dist/fallback.d.ts +50 -0
  41. package/dist/fallback.d.ts.map +1 -0
  42. package/dist/fallback.js +134 -0
  43. package/dist/fallback.js.map +1 -0
  44. package/dist/git.d.ts +28 -0
  45. package/dist/git.d.ts.map +1 -0
  46. package/dist/git.js +71 -0
  47. package/dist/git.js.map +1 -0
  48. package/dist/index.d.ts +100 -0
  49. package/dist/index.d.ts.map +1 -0
  50. package/dist/index.js +653 -0
  51. package/dist/index.js.map +1 -0
  52. package/dist/mcp.d.ts +20 -0
  53. package/dist/mcp.d.ts.map +1 -0
  54. package/dist/mcp.js +1215 -0
  55. package/dist/mcp.js.map +1 -0
  56. package/dist/models.d.ts +97 -0
  57. package/dist/models.d.ts.map +1 -0
  58. package/dist/models.js +873 -0
  59. package/dist/models.js.map +1 -0
  60. package/dist/plugin.d.ts +59 -0
  61. package/dist/plugin.d.ts.map +1 -0
  62. package/dist/plugin.js +223 -0
  63. package/dist/plugin.js.map +1 -0
  64. package/dist/presets.d.ts +27 -0
  65. package/dist/presets.d.ts.map +1 -0
  66. package/dist/presets.js +86 -0
  67. package/dist/presets.js.map +1 -0
  68. package/dist/replay.d.ts +37 -0
  69. package/dist/replay.d.ts.map +1 -0
  70. package/dist/replay.js +103 -0
  71. package/dist/replay.js.map +1 -0
  72. package/dist/report.d.ts +28 -0
  73. package/dist/report.d.ts.map +1 -0
  74. package/dist/report.js +300 -0
  75. package/dist/report.js.map +1 -0
  76. package/dist/smart-experts.d.ts +44 -0
  77. package/dist/smart-experts.d.ts.map +1 -0
  78. package/dist/smart-experts.js +242 -0
  79. package/dist/smart-experts.js.map +1 -0
  80. package/dist/stream.d.ts +50 -0
  81. package/dist/stream.d.ts.map +1 -0
  82. package/dist/stream.js +219 -0
  83. package/dist/stream.js.map +1 -0
  84. package/dist/tool-run.d.ts +49 -0
  85. package/dist/tool-run.d.ts.map +1 -0
  86. package/dist/tool-run.js +143 -0
  87. package/dist/tool-run.js.map +1 -0
  88. package/dist/types.d.ts +352 -0
  89. package/dist/types.d.ts.map +1 -0
  90. package/dist/types.js +6 -0
  91. package/dist/types.js.map +1 -0
  92. package/dist/variants.d.ts +30 -0
  93. package/dist/variants.d.ts.map +1 -0
  94. package/dist/variants.js +111 -0
  95. package/dist/variants.js.map +1 -0
  96. package/dist/verify.d.ts +33 -0
  97. package/dist/verify.d.ts.map +1 -0
  98. package/dist/verify.js +80 -0
  99. package/dist/verify.js.map +1 -0
  100. package/dist/watch.d.ts +73 -0
  101. package/dist/watch.d.ts.map +1 -0
  102. package/dist/watch.js +350 -0
  103. package/dist/watch.js.map +1 -0
  104. package/docs/session-and-plugin.md +642 -0
  105. package/package.json +90 -6
  106. package/schema/config.schema.json +100 -0
package/dist/index.js ADDED
@@ -0,0 +1,653 @@
1
+ /**
2
+ * @fileoverview Основной API Consilium
3
+ * @module lv24-opencode-consilium
4
+ */
5
+ import { writeFileSync, existsSync, mkdirSync, renameSync, unlinkSync } from 'fs';
6
+ import { dirname, resolve } from 'path';
7
+ import { loadConfig, mergeWithDefaults, cloneConfig } from './config.js';
8
+ import { callExpertsParallel, callChair, checkOpenCodeAvailable, createRunController } from './agents.js';
9
+ import { DEFAULT_EXPERT_PROMPTS, DEFAULT_CHAIR_PROMPT_TEMPLATE } from './defaults.js';
10
+ import { applyPreset } from './presets.js';
11
+ import { analyzeConsensus, formatConsensusReport } from './consensus.js';
12
+ import { selectSmartExperts } from './smart-experts.js';
13
+ import { createCostTracker } from './fallback.js';
14
+ import { createArtifactId, resolveArtifactRoot, saveArtifact } from './artifacts.js';
15
+ function sumUsage(results) {
16
+ let found = false;
17
+ const total = {
18
+ inputTokens: 0,
19
+ outputTokens: 0,
20
+ reasoningTokens: 0,
21
+ cacheReadTokens: 0,
22
+ cacheWriteTokens: 0,
23
+ cost: 0
24
+ };
25
+ for (const result of results) {
26
+ const usage = result.usage;
27
+ if (!usage)
28
+ continue;
29
+ found = true;
30
+ total.inputTokens += usage.inputTokens ?? 0;
31
+ total.outputTokens += usage.outputTokens ?? 0;
32
+ total.reasoningTokens += usage.reasoningTokens ?? 0;
33
+ total.cacheReadTokens += usage.cacheReadTokens ?? 0;
34
+ total.cacheWriteTokens += usage.cacheWriteTokens ?? 0;
35
+ total.cost += usage.cost ?? 0;
36
+ }
37
+ return found ? total : undefined;
38
+ }
39
+ /** Собирает модели агентов из результатов */
40
+ function collectModels(results) {
41
+ const models = {};
42
+ for (const result of results) {
43
+ if (result.model)
44
+ models[result.agent] = result.model;
45
+ }
46
+ return models;
47
+ }
48
+ /** Входные данные для анализа согласия */
49
+ function consensusInputs(results) {
50
+ return results.map(result => ({
51
+ agent: result.agent,
52
+ text: result.text,
53
+ model: result.model,
54
+ success: result.success
55
+ }));
56
+ }
57
+ /** Собирает реальные session id агентов */
58
+ function collectSessions(results) {
59
+ const sessions = {};
60
+ for (const result of results) {
61
+ if (result.sessionId !== undefined)
62
+ sessions[result.agent] = result.sessionId;
63
+ }
64
+ return sessions;
65
+ }
66
+ function buildExpertPrompt(expertName, prompt, task) {
67
+ if (typeof prompt === 'function') {
68
+ return prompt(task);
69
+ }
70
+ if (typeof prompt === 'string' && prompt.trim().length > 0) {
71
+ return prompt.replace(/\$\{task\}/g, task);
72
+ }
73
+ const fallback = Object.prototype.hasOwnProperty.call(DEFAULT_EXPERT_PROMPTS, expertName)
74
+ ? DEFAULT_EXPERT_PROMPTS[expertName]
75
+ : DEFAULT_EXPERT_PROMPTS.default;
76
+ return typeof fallback === 'function' ? fallback(task) : task;
77
+ }
78
+ /**
79
+ * Класс Consilium - основной интерфейс для работы с консилиумом
80
+ */
81
+ export class Consilium {
82
+ config;
83
+ status = 'idle';
84
+ progressCallbacks = new Set();
85
+ plugins = [];
86
+ warnings = [];
87
+ controller = null;
88
+ startTime = 0;
89
+ constructor(config) {
90
+ if (typeof config === 'string') {
91
+ const result = loadConfig(config);
92
+ this.config = result.config;
93
+ this.warnings = result.warnings;
94
+ }
95
+ else if (config) {
96
+ this.config = mergeWithDefaults(config);
97
+ }
98
+ else {
99
+ const result = loadConfig();
100
+ this.config = result.config;
101
+ this.warnings = result.warnings;
102
+ }
103
+ this.plugins = this.config.plugins ? [...this.config.plugins] : [];
104
+ }
105
+ /**
106
+ * Получить копию текущей конфигурации
107
+ */
108
+ getConfig() {
109
+ return cloneConfig(this.config);
110
+ }
111
+ /**
112
+ * Предупреждения, полученные при загрузке конфигурации
113
+ */
114
+ getWarnings() {
115
+ return [...this.warnings];
116
+ }
117
+ /**
118
+ * Получить текущий статус
119
+ */
120
+ getStatus() {
121
+ return this.status;
122
+ }
123
+ /**
124
+ * Добавить callback для отслеживания прогресса
125
+ */
126
+ onProgress(callback) {
127
+ this.progressCallbacks.add(callback);
128
+ return () => {
129
+ this.progressCallbacks.delete(callback);
130
+ };
131
+ }
132
+ /**
133
+ * Зарегистрировать плагин (инициализируется перед запуском)
134
+ */
135
+ use(plugin) {
136
+ this.plugins.push(plugin);
137
+ if (plugin.hooks) {
138
+ this.config.hooks = {
139
+ ...this.config.hooks,
140
+ ...plugin.hooks
141
+ };
142
+ }
143
+ return this;
144
+ }
145
+ /**
146
+ * Отменить текущий запуск
147
+ */
148
+ cancel() {
149
+ this.controller?.cancel();
150
+ }
151
+ /**
152
+ * Запустить консилиум
153
+ */
154
+ async run(task, options = {}) {
155
+ if (!task || task.trim().length === 0) {
156
+ throw new Error('Задача не может быть пустой');
157
+ }
158
+ if (this.status === 'running_experts' || this.status === 'running_chair') {
159
+ throw new Error(`Consilium уже запущен (статус: ${this.status})`);
160
+ }
161
+ // Захватываем запуск синхронно, до любых await, чтобы исключить гонку.
162
+ const controller = createRunController();
163
+ this.controller = controller;
164
+ this.status = 'running_experts';
165
+ this.startTime = Date.now();
166
+ let hooks;
167
+ try {
168
+ let runConfig = cloneConfig(this.config);
169
+ runConfig.hooks = { ...this.config.hooks, ...options.hooks };
170
+ for (const plugin of this.plugins) {
171
+ if (plugin.init) {
172
+ runConfig = await plugin.init(runConfig);
173
+ }
174
+ }
175
+ if (options.preset) {
176
+ runConfig = applyPreset(runConfig, options.preset);
177
+ }
178
+ if (options.timeout !== undefined && Number.isFinite(options.timeout) && options.timeout > 0) {
179
+ runConfig.timeouts.expert = options.timeout;
180
+ runConfig.experts = runConfig.experts.map(expert => ({ ...expert, timeout: options.timeout }));
181
+ }
182
+ // Опции запуска переопределяют конфигурацию; сами значения нужны и агентам.
183
+ const artifactsSetting = options.artifacts ?? runConfig.artifacts;
184
+ const consensusEnabled = options.consensus ?? runConfig.consensus === true;
185
+ const smartEnabled = options.smartExperts ?? runConfig.smartExperts === true;
186
+ const maxCost = options.maxCost ?? runConfig.maxCost;
187
+ const fallbackModels = options.fallbackModels ?? runConfig.fallbackModels ?? [];
188
+ const session = options.session ?? runConfig.session;
189
+ runConfig.consensus = consensusEnabled;
190
+ runConfig.fallbackModels = fallbackModels;
191
+ runConfig.session = session;
192
+ if (maxCost !== undefined)
193
+ runConfig.maxCost = maxCost;
194
+ const baseHooks = runConfig.hooks ?? {};
195
+ runConfig.hooks = {
196
+ ...baseHooks,
197
+ onExpertStart: async (expert, expertTask) => {
198
+ this.emitProgress({
199
+ type: 'expert_start',
200
+ agent: expert,
201
+ status: 'running_experts',
202
+ progress: 0,
203
+ timestamp: Date.now()
204
+ });
205
+ await baseHooks.onExpertStart?.(expert, expertTask);
206
+ },
207
+ onExpertEnd: async (result) => {
208
+ this.emitProgress({
209
+ type: 'expert_end',
210
+ agent: result.agent,
211
+ status: 'running_experts',
212
+ progress: 0,
213
+ timestamp: Date.now(),
214
+ data: result
215
+ });
216
+ await baseHooks.onExpertEnd?.(result);
217
+ },
218
+ onChairStart: async (chairTask, expertResults) => {
219
+ this.emitProgress({
220
+ type: 'chair_start',
221
+ status: 'running_chair',
222
+ progress: 50,
223
+ timestamp: Date.now()
224
+ });
225
+ await baseHooks.onChairStart?.(chairTask, expertResults);
226
+ },
227
+ onChairEnd: async (result) => {
228
+ this.emitProgress({
229
+ type: 'chair_end',
230
+ agent: result.agent,
231
+ status: 'running_chair',
232
+ progress: 95,
233
+ timestamp: Date.now(),
234
+ data: result
235
+ });
236
+ await baseHooks.onChairEnd?.(result);
237
+ }
238
+ };
239
+ hooks = runConfig.hooks;
240
+ if (!(await checkOpenCodeAvailable(runConfig))) {
241
+ throw new Error('OpenCode CLI не найден. Установите: npm install -g opencode-ai (или укажите opencodePath в конфиге)');
242
+ }
243
+ await hooks.onStart?.(task, runConfig);
244
+ const selectedExperts = this.selectExperts(runConfig, options.experts, hooks);
245
+ let councilExperts = selectedExperts;
246
+ let smartSelection;
247
+ if (smartEnabled) {
248
+ smartSelection = selectSmartExperts(selectedExperts, task);
249
+ if (smartSelection.dropped.length > 0) {
250
+ hooks.onLog?.('info', `Умный выбор экспертов: ${smartSelection.experts.map(expert => expert.name).join(', ')} ` +
251
+ `(пропущены: ${smartSelection.dropped.join(', ')})`);
252
+ }
253
+ councilExperts = smartSelection.experts;
254
+ }
255
+ const costTracker = createCostTracker(maxCost);
256
+ const parallelStarted = Date.now();
257
+ const expertResults = await callExpertsParallel(councilExperts, task, runConfig, { controller });
258
+ const parallelTime = Date.now() - parallelStarted;
259
+ for (const result of expertResults) {
260
+ costTracker.add(result.agent, result.usage);
261
+ }
262
+ let costExceeded = costTracker.exceeded();
263
+ if (costExceeded) {
264
+ hooks.onLog?.('warn', `Лимит стоимости превышен после экспертов: $${costTracker.report().spent.toFixed(4)} ` +
265
+ `из $${maxCost ?? 0}; председатель не запускается`);
266
+ }
267
+ if (controller.isCancelled()) {
268
+ const result = this.createCancelledResult(expertResults, undefined, parallelTime, runConfig);
269
+ result.metadata = {
270
+ ...(result.metadata ?? {}),
271
+ ...options.metadata,
272
+ models: collectModels(expertResults),
273
+ sessions: collectSessions(expertResults),
274
+ cost: costTracker.report(),
275
+ ...(session ? { session } : {})
276
+ };
277
+ this.status = 'cancelled';
278
+ this.saveArtifactRun(result, task, artifactsSetting, runConfig, hooks);
279
+ await hooks.onEnd?.(result);
280
+ return result;
281
+ }
282
+ this.status = 'running_chair';
283
+ const configuredChair = runConfig.chair.agent;
284
+ const chairAgent = options.chair ?? configuredChair;
285
+ if (chairAgent !== configuredChair) {
286
+ // Модель была выбрана для другого агента: переразрешаем её для нового.
287
+ runConfig.chair = { ...runConfig.chair, agent: chairAgent, model: undefined };
288
+ }
289
+ else {
290
+ runConfig.chair.agent = chairAgent;
291
+ }
292
+ let chairResult;
293
+ if (costTracker.exceeded()) {
294
+ costExceeded = true;
295
+ const spent = costTracker.report().spent;
296
+ chairResult = {
297
+ agent: chairAgent,
298
+ text: '',
299
+ success: false,
300
+ attempt: 0,
301
+ exitCode: null,
302
+ errorCode: 'COST_LIMIT',
303
+ errorMessage: `Лимит стоимости $${maxCost ?? 0} превышен ($${spent.toFixed(4)}); ` +
304
+ 'председатель не запускался'
305
+ };
306
+ }
307
+ else {
308
+ try {
309
+ chairResult = await callChair(expertResults, task, runConfig, { controller });
310
+ costTracker.add(chairResult.agent, chairResult.usage);
311
+ costExceeded = costTracker.exceeded();
312
+ }
313
+ catch (error) {
314
+ // Ни один эксперт не ответил (или другая ошибка синтеза): сохраняем
315
+ // частичный результат, чтобы артефакт и -o работали для post-mortem.
316
+ chairResult = {
317
+ agent: chairAgent,
318
+ text: '',
319
+ success: false,
320
+ attempt: 0,
321
+ exitCode: null,
322
+ errorCode: 'EXIT_ERROR',
323
+ errorMessage: error.message
324
+ };
325
+ }
326
+ }
327
+ if (controller.isCancelled()) {
328
+ const result = this.createCancelledResult(expertResults, chairResult, parallelTime, runConfig);
329
+ result.metadata = {
330
+ ...(result.metadata ?? {}),
331
+ ...options.metadata,
332
+ models: collectModels([...expertResults, chairResult]),
333
+ sessions: collectSessions([...expertResults, chairResult]),
334
+ cost: costTracker.report(),
335
+ ...(session ? { session } : {})
336
+ };
337
+ this.status = 'cancelled';
338
+ this.saveArtifactRun(result, task, artifactsSetting, runConfig, hooks);
339
+ await hooks.onEnd?.(result);
340
+ return result;
341
+ }
342
+ this.status = 'completed';
343
+ const models = collectModels([...expertResults, chairResult]);
344
+ const sessions = collectSessions([...expertResults, chairResult]);
345
+ const consensusReport = consensusEnabled
346
+ ? analyzeConsensus(consensusInputs(expertResults))
347
+ : undefined;
348
+ const consensusText = consensusReport
349
+ ? `\n\n---\n\n${formatConsensusReport(consensusReport)}`
350
+ : '';
351
+ const result = {
352
+ text: `${chairResult.text}${consensusText}`.trim(),
353
+ experts: expertResults,
354
+ chair: chairResult,
355
+ totalTime: Date.now() - this.startTime,
356
+ parallelTime,
357
+ success: chairResult.success,
358
+ usage: sumUsage([...expertResults, chairResult]),
359
+ metadata: {
360
+ ...options.metadata,
361
+ models,
362
+ cost: costTracker.report(),
363
+ ...(costExceeded ? { costExceeded: true } : {}),
364
+ ...(consensusReport ? { consensus: consensusReport } : {}),
365
+ ...(smartSelection
366
+ ? {
367
+ smartExperts: {
368
+ dropped: smartSelection.dropped,
369
+ scores: smartSelection.scores,
370
+ reasons: smartSelection.reasons
371
+ }
372
+ }
373
+ : {}),
374
+ ...(session ? { session } : {}),
375
+ ...(Object.keys(sessions).length > 0 ? { sessions } : {}),
376
+ ...(fallbackModels.length > 0 ? { fallbackModels } : {})
377
+ }
378
+ };
379
+ if (maxCost !== undefined) {
380
+ const hasCostTelemetry = [...expertResults, chairResult].some(item => typeof item.usage?.cost === 'number');
381
+ if (!hasCostTelemetry) {
382
+ hooks.onLog?.('warn', 'Провайдер не сообщил стоимость запуска: лимит maxCost невозможно проверить.');
383
+ result.metadata = { ...result.metadata, costTelemetry: 'missing' };
384
+ }
385
+ }
386
+ const explicitOutput = options.outputFile;
387
+ const configuredFile = runConfig.output.file;
388
+ const outputTarget = explicitOutput ?? (configuredFile !== false ? configuredFile : undefined);
389
+ if (outputTarget) {
390
+ try {
391
+ const fullPath = this.saveResult(result, outputTarget, runConfig);
392
+ result.metadata = { ...result.metadata, savedFile: fullPath };
393
+ }
394
+ catch (error) {
395
+ result.metadata = {
396
+ ...result.metadata,
397
+ saveError: error.message
398
+ };
399
+ hooks.onLog?.('warn', `Не удалось сохранить результат: ${error.message}`);
400
+ }
401
+ }
402
+ this.saveArtifactRun(result, task, artifactsSetting, runConfig, hooks);
403
+ await hooks.onEnd?.(result);
404
+ return result;
405
+ }
406
+ catch (error) {
407
+ this.status = 'failed';
408
+ try {
409
+ await hooks?.onError?.(error, { task, options });
410
+ }
411
+ catch {
412
+ // Ошибка в хуке не должна маскировать исходную
413
+ }
414
+ this.emitProgress({
415
+ type: 'error',
416
+ status: 'failed',
417
+ progress: 0,
418
+ timestamp: Date.now(),
419
+ data: error
420
+ });
421
+ throw error;
422
+ }
423
+ finally {
424
+ if (this.controller === controller) {
425
+ this.controller = null;
426
+ }
427
+ }
428
+ }
429
+ /**
430
+ * Запустить консилиум и вернуть только текст
431
+ */
432
+ async runSimple(task, options = {}) {
433
+ const result = await this.run(task, options);
434
+ return result.text;
435
+ }
436
+ /**
437
+ * Dry run - возвращает сформированные промпты без вызова агентов
438
+ */
439
+ dryRun(task) {
440
+ const experts = this.config.experts.filter(expert => expert.enabled !== false);
441
+ const expertPrompts = experts.map(expert => ({
442
+ agent: expert.name,
443
+ model: expert.model,
444
+ prompt: buildExpertPrompt(expert.name, expert.prompt, task)
445
+ }));
446
+ const placeholderResults = experts.map(expert => ({
447
+ agent: expert.name,
448
+ model: expert.model,
449
+ text: `[ответ эксперта ${expert.name}]`,
450
+ success: true
451
+ }));
452
+ const chairPromptTemplate = this.config.chair.promptTemplate
453
+ ? this.config.chair.promptTemplate(task, placeholderResults)
454
+ : DEFAULT_CHAIR_PROMPT_TEMPLATE(task, placeholderResults);
455
+ return {
456
+ expertPrompts,
457
+ chairPromptTemplate
458
+ };
459
+ }
460
+ selectExperts(config, requested, hooks) {
461
+ if (requested && requested.length > 0) {
462
+ const wanted = new Set(requested);
463
+ const available = new Set(config.experts.map(expert => expert.name));
464
+ const missing = requested.filter(name => !available.has(name));
465
+ if (missing.length > 0) {
466
+ hooks?.onLog?.('warn', `Неизвестные эксперты пропущены: ${missing.join(', ')}`);
467
+ }
468
+ const experts = config.experts.filter(expert => wanted.has(expert.name) && expert.enabled !== false);
469
+ if (experts.length === 0) {
470
+ throw new Error(`Выбранные эксперты недоступны: ${requested.join(', ')}. Доступны: ${[...available].join(', ')}`);
471
+ }
472
+ return experts;
473
+ }
474
+ const experts = config.experts.filter(expert => expert.enabled !== false);
475
+ if (experts.length === 0) {
476
+ throw new Error('Нет активных экспертов для консилиума');
477
+ }
478
+ return experts;
479
+ }
480
+ emitProgress(event) {
481
+ for (const callback of this.progressCallbacks) {
482
+ try {
483
+ callback(event);
484
+ }
485
+ catch {
486
+ // Ошибки подписчиков не влияют на запуск
487
+ }
488
+ }
489
+ }
490
+ saveResult(result, outputPath, config) {
491
+ const baseDir = config.cwd ? resolve(config.cwd) : process.cwd();
492
+ const fullPath = resolve(baseDir, outputPath);
493
+ const dir = dirname(fullPath);
494
+ if (outputPath.trim().length === 0) {
495
+ throw new Error('Пустой путь для сохранения результата');
496
+ }
497
+ if (!existsSync(dir)) {
498
+ mkdirSync(dir, { recursive: true });
499
+ }
500
+ let content;
501
+ switch (config.output.format) {
502
+ case 'json':
503
+ content = JSON.stringify(result, null, 2);
504
+ break;
505
+ case 'markdown':
506
+ content = this.formatAsMarkdown(result);
507
+ break;
508
+ default:
509
+ content = result.text;
510
+ }
511
+ const temporary = `${fullPath}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
512
+ try {
513
+ writeFileSync(temporary, content, 'utf-8');
514
+ renameSync(temporary, fullPath);
515
+ }
516
+ catch (error) {
517
+ try {
518
+ if (existsSync(temporary)) {
519
+ unlinkSync(temporary);
520
+ }
521
+ }
522
+ catch {
523
+ // Очистка необязательна
524
+ }
525
+ throw error;
526
+ }
527
+ return fullPath;
528
+ }
529
+ /**
530
+ * Сохраняет артефакт запуска, если он включён.
531
+ * Ошибка сохранения не должна ломать сам запуск.
532
+ */
533
+ saveArtifactRun(result, task, setting, runConfig, hooks) {
534
+ if (setting === undefined || setting === false)
535
+ return;
536
+ try {
537
+ const metadata = result.metadata ?? {};
538
+ const metaModels = metadata.models;
539
+ const models = metaModels !== null && typeof metaModels === 'object' && !Array.isArray(metaModels)
540
+ ? metaModels
541
+ : collectModels([...result.experts, result.chair]);
542
+ const run = {
543
+ version: 1,
544
+ id: createArtifactId(),
545
+ createdAt: new Date().toISOString(),
546
+ task,
547
+ success: result.success,
548
+ text: result.text,
549
+ experts: result.experts.map(expert => ({
550
+ agent: expert.agent,
551
+ model: expert.model,
552
+ success: expert.success,
553
+ errorCode: expert.errorCode,
554
+ errorMessage: expert.errorMessage,
555
+ duration: expert.duration,
556
+ text: expert.text,
557
+ usage: expert.usage
558
+ })),
559
+ models,
560
+ chairAgent: result.chair.agent,
561
+ usage: result.usage,
562
+ consensus: metadata.consensus,
563
+ metadata
564
+ };
565
+ const root = resolveArtifactRoot({ artifacts: setting, cwd: runConfig.cwd });
566
+ const ref = saveArtifact(run, { root });
567
+ result.metadata = {
568
+ ...metadata,
569
+ artifacts: { id: ref.id, dir: ref.dir }
570
+ };
571
+ }
572
+ catch (error) {
573
+ result.metadata = {
574
+ ...(result.metadata ?? {}),
575
+ artifactsError: error.message
576
+ };
577
+ hooks?.onLog?.('warn', `Не удалось сохранить артефакт: ${error.message}`);
578
+ }
579
+ }
580
+ formatAsMarkdown(result) {
581
+ let md = `# Результат консилиума\n\n`;
582
+ md += `**Время выполнения:** ${(result.totalTime / 1000).toFixed(1)}с\n\n`;
583
+ if (result.usage?.cost) {
584
+ md += `**Стоимость:** $${result.usage.cost.toFixed(6)}\n\n`;
585
+ }
586
+ md += `---\n\n`;
587
+ md += result.text;
588
+ md += `\n\n---\n\n`;
589
+ md += `## Мнения экспертов\n\n`;
590
+ for (const expert of result.experts) {
591
+ const model = expert.model ? ` (${expert.model})` : '';
592
+ md += `### ${expert.agent.toUpperCase()}${model}\n`;
593
+ md += `${expert.success ? '' : '⚠️ '}${expert.text}\n\n`;
594
+ }
595
+ return md;
596
+ }
597
+ createCancelledResult(experts, chair, parallelTime, config) {
598
+ return {
599
+ text: '[Консилиум отменён]',
600
+ experts,
601
+ chair: chair || {
602
+ agent: config.chair.agent,
603
+ text: '[Отменено]',
604
+ success: false,
605
+ errorCode: 'CANCELLED'
606
+ },
607
+ totalTime: Date.now() - this.startTime,
608
+ parallelTime,
609
+ success: false,
610
+ usage: sumUsage(chair ? [...experts, chair] : experts),
611
+ metadata: { cancelled: true }
612
+ };
613
+ }
614
+ }
615
+ // Convenience exports
616
+ /**
617
+ * Создать экземпляр Consilium
618
+ */
619
+ export function createConsilium(config) {
620
+ return new Consilium(config);
621
+ }
622
+ /**
623
+ * Быстрый запуск консилиума
624
+ */
625
+ export async function runConsilium(task, config) {
626
+ const instance = new Consilium(config);
627
+ return instance.run(task);
628
+ }
629
+ /**
630
+ * Быстрый запуск с возвратом только текста
631
+ */
632
+ export async function askConsilium(task, config) {
633
+ const instance = new Consilium(config);
634
+ return instance.runSimple(task);
635
+ }
636
+ // Re-export types and utilities
637
+ export * from './types.js';
638
+ export * from './config.js';
639
+ export * from './agents.js';
640
+ export * from './models.js';
641
+ export * from './presets.js';
642
+ export * from './compat.js';
643
+ export * from './artifacts.js';
644
+ export * from './consensus.js';
645
+ export * from './smart-experts.js';
646
+ export * from './fallback.js';
647
+ export * from './report.js';
648
+ export * from './watch.js';
649
+ export * from './variants.js';
650
+ export * from './replay.js';
651
+ export * from './verify.js';
652
+ export { DEFAULT_CONFIG } from './defaults.js';
653
+ //# sourceMappingURL=index.js.map