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.
- package/LICENSE +21 -0
- package/README.md +499 -3
- package/README.ru.md +500 -0
- package/README.zh.md +188 -0
- package/bin/cli.js +19 -0
- package/bin/mcp.js +24 -0
- package/consilium.config.example.json +84 -0
- package/dist/agents.d.ts +79 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +1051 -0
- package/dist/agents.js.map +1 -0
- package/dist/artifacts.d.ts +75 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +464 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1392 -0
- package/dist/cli.js.map +1 -0
- package/dist/compat.d.ts +160 -0
- package/dist/compat.d.ts.map +1 -0
- package/dist/compat.js +419 -0
- package/dist/compat.js.map +1 -0
- package/dist/config.d.ts +112 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +728 -0
- package/dist/config.js.map +1 -0
- package/dist/consensus.d.ts +73 -0
- package/dist/consensus.d.ts.map +1 -0
- package/dist/consensus.js +273 -0
- package/dist/consensus.js.map +1 -0
- package/dist/defaults.d.ts +34 -0
- package/dist/defaults.d.ts.map +1 -0
- package/dist/defaults.js +102 -0
- package/dist/defaults.js.map +1 -0
- package/dist/doctor.d.ts +27 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +798 -0
- package/dist/doctor.js.map +1 -0
- package/dist/fallback.d.ts +50 -0
- package/dist/fallback.d.ts.map +1 -0
- package/dist/fallback.js +134 -0
- package/dist/fallback.js.map +1 -0
- package/dist/git.d.ts +28 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +71 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +653 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp.d.ts +20 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +1215 -0
- package/dist/mcp.js.map +1 -0
- package/dist/models.d.ts +97 -0
- package/dist/models.d.ts.map +1 -0
- package/dist/models.js +873 -0
- package/dist/models.js.map +1 -0
- package/dist/plugin.d.ts +59 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +223 -0
- package/dist/plugin.js.map +1 -0
- package/dist/presets.d.ts +27 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +86 -0
- package/dist/presets.js.map +1 -0
- package/dist/replay.d.ts +37 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +103 -0
- package/dist/replay.js.map +1 -0
- package/dist/report.d.ts +28 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +300 -0
- package/dist/report.js.map +1 -0
- package/dist/smart-experts.d.ts +44 -0
- package/dist/smart-experts.d.ts.map +1 -0
- package/dist/smart-experts.js +242 -0
- package/dist/smart-experts.js.map +1 -0
- package/dist/stream.d.ts +50 -0
- package/dist/stream.d.ts.map +1 -0
- package/dist/stream.js +219 -0
- package/dist/stream.js.map +1 -0
- package/dist/tool-run.d.ts +49 -0
- package/dist/tool-run.d.ts.map +1 -0
- package/dist/tool-run.js +143 -0
- package/dist/tool-run.js.map +1 -0
- package/dist/types.d.ts +352 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/variants.d.ts +30 -0
- package/dist/variants.d.ts.map +1 -0
- package/dist/variants.js +111 -0
- package/dist/variants.js.map +1 -0
- package/dist/verify.d.ts +33 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +80 -0
- package/dist/verify.js.map +1 -0
- package/dist/watch.d.ts +73 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +350 -0
- package/dist/watch.js.map +1 -0
- package/docs/session-and-plugin.md +642 -0
- package/package.json +90 -6
- 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
|