neuro-cli 4.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.
Files changed (175) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +510 -0
  3. package/dist/advisor/advisor.d.ts +50 -0
  4. package/dist/advisor/advisor.js +178 -0
  5. package/dist/agents/base.d.ts +62 -0
  6. package/dist/agents/base.js +215 -0
  7. package/dist/agents/orchestrator.d.ts +46 -0
  8. package/dist/agents/orchestrator.js +192 -0
  9. package/dist/agents/team.d.ts +51 -0
  10. package/dist/agents/team.js +210 -0
  11. package/dist/api/models.d.ts +23 -0
  12. package/dist/api/models.js +514 -0
  13. package/dist/api/ollama.d.ts +153 -0
  14. package/dist/api/ollama.js +751 -0
  15. package/dist/api/openrouter.d.ts +55 -0
  16. package/dist/api/openrouter.js +223 -0
  17. package/dist/commands/commands.d.ts +43 -0
  18. package/dist/commands/commands.js +308 -0
  19. package/dist/config/config.d.ts +8 -0
  20. package/dist/config/config.js +311 -0
  21. package/dist/context/compaction.d.ts +54 -0
  22. package/dist/context/compaction.js +251 -0
  23. package/dist/context/custom-agents.d.ts +107 -0
  24. package/dist/context/custom-agents.js +397 -0
  25. package/dist/context/custom-tools.d.ts +120 -0
  26. package/dist/context/custom-tools.js +564 -0
  27. package/dist/context/git-checkpoint.d.ts +52 -0
  28. package/dist/context/git-checkpoint.js +240 -0
  29. package/dist/context/neuro-md.d.ts +48 -0
  30. package/dist/context/neuro-md.js +202 -0
  31. package/dist/context/neuroignore.d.ts +102 -0
  32. package/dist/context/neuroignore.js +441 -0
  33. package/dist/context/repo-map.d.ts +38 -0
  34. package/dist/context/repo-map.js +220 -0
  35. package/dist/context/skill-standard.d.ts +262 -0
  36. package/dist/context/skill-standard.js +1156 -0
  37. package/dist/context/skill-system.d.ts +75 -0
  38. package/dist/context/skill-system.js +578 -0
  39. package/dist/context/tree-sitter.d.ts +99 -0
  40. package/dist/context/tree-sitter.js +1956 -0
  41. package/dist/core/acp.d.ts +325 -0
  42. package/dist/core/acp.js +1498 -0
  43. package/dist/core/api-server.d.ts +143 -0
  44. package/dist/core/api-server.js +550 -0
  45. package/dist/core/approval.d.ts +81 -0
  46. package/dist/core/approval.js +432 -0
  47. package/dist/core/auto-compact.d.ts +127 -0
  48. package/dist/core/auto-compact.js +436 -0
  49. package/dist/core/auto-mode.d.ts +232 -0
  50. package/dist/core/auto-mode.js +831 -0
  51. package/dist/core/background-session.d.ts +166 -0
  52. package/dist/core/background-session.js +696 -0
  53. package/dist/core/cicd.d.ts +257 -0
  54. package/dist/core/cicd.js +1443 -0
  55. package/dist/core/cloud-sync.d.ts +156 -0
  56. package/dist/core/cloud-sync.js +582 -0
  57. package/dist/core/code-review.d.ts +132 -0
  58. package/dist/core/code-review.js +1191 -0
  59. package/dist/core/completion.d.ts +49 -0
  60. package/dist/core/completion.js +384 -0
  61. package/dist/core/context.d.ts +38 -0
  62. package/dist/core/context.js +144 -0
  63. package/dist/core/diff-preview.d.ts +35 -0
  64. package/dist/core/diff-preview.js +173 -0
  65. package/dist/core/doom-loop.d.ts +51 -0
  66. package/dist/core/doom-loop.js +179 -0
  67. package/dist/core/engine.d.ts +183 -0
  68. package/dist/core/engine.js +942 -0
  69. package/dist/core/extended-thinking.d.ts +103 -0
  70. package/dist/core/extended-thinking.js +269 -0
  71. package/dist/core/fallback.d.ts +54 -0
  72. package/dist/core/fallback.js +104 -0
  73. package/dist/core/git-worktree.d.ts +139 -0
  74. package/dist/core/git-worktree.js +614 -0
  75. package/dist/core/headless.d.ts +30 -0
  76. package/dist/core/headless.js +101 -0
  77. package/dist/core/i18n.d.ts +83 -0
  78. package/dist/core/i18n.js +739 -0
  79. package/dist/core/linting.d.ts +129 -0
  80. package/dist/core/linting.js +699 -0
  81. package/dist/core/model-router.d.ts +109 -0
  82. package/dist/core/model-router.js +581 -0
  83. package/dist/core/multi-model.d.ts +243 -0
  84. package/dist/core/multi-model.js +1099 -0
  85. package/dist/core/multi-session.d.ts +144 -0
  86. package/dist/core/multi-session.js +442 -0
  87. package/dist/core/multimodal.d.ts +125 -0
  88. package/dist/core/multimodal.js +286 -0
  89. package/dist/core/observability.d.ts +93 -0
  90. package/dist/core/observability.js +737 -0
  91. package/dist/core/os-sandbox.d.ts +122 -0
  92. package/dist/core/os-sandbox.js +1193 -0
  93. package/dist/core/outcome-grading.d.ts +228 -0
  94. package/dist/core/outcome-grading.js +1123 -0
  95. package/dist/core/output-styles.d.ts +57 -0
  96. package/dist/core/output-styles.js +382 -0
  97. package/dist/core/parallel-agents.d.ts +183 -0
  98. package/dist/core/parallel-agents.js +563 -0
  99. package/dist/core/plugin-bundle.d.ts +236 -0
  100. package/dist/core/plugin-bundle.js +887 -0
  101. package/dist/core/plugin-sdk.d.ts +139 -0
  102. package/dist/core/plugin-sdk.js +273 -0
  103. package/dist/core/prompt-cache.d.ts +163 -0
  104. package/dist/core/prompt-cache.js +599 -0
  105. package/dist/core/sandbox.d.ts +127 -0
  106. package/dist/core/sandbox.js +369 -0
  107. package/dist/core/scheduled-tasks.d.ts +151 -0
  108. package/dist/core/scheduled-tasks.js +502 -0
  109. package/dist/core/security-scanner.d.ts +160 -0
  110. package/dist/core/security-scanner.js +1494 -0
  111. package/dist/core/session.d.ts +83 -0
  112. package/dist/core/session.js +269 -0
  113. package/dist/core/shell-completion.d.ts +51 -0
  114. package/dist/core/shell-completion.js +674 -0
  115. package/dist/core/smart-monitor.d.ts +146 -0
  116. package/dist/core/smart-monitor.js +1199 -0
  117. package/dist/core/spec-driven.d.ts +233 -0
  118. package/dist/core/spec-driven.js +1485 -0
  119. package/dist/core/spending-warnings.d.ts +123 -0
  120. package/dist/core/spending-warnings.js +456 -0
  121. package/dist/core/sub-agent.d.ts +298 -0
  122. package/dist/core/sub-agent.js +1023 -0
  123. package/dist/core/telemetry.d.ts +157 -0
  124. package/dist/core/telemetry.js +412 -0
  125. package/dist/core/terminal-ux.d.ts +134 -0
  126. package/dist/core/terminal-ux.js +649 -0
  127. package/dist/core/testing.d.ts +146 -0
  128. package/dist/core/testing.js +1200 -0
  129. package/dist/core/types.d.ts +439 -0
  130. package/dist/core/types.js +6 -0
  131. package/dist/core/undo-redo.d.ts +112 -0
  132. package/dist/core/undo-redo.js +290 -0
  133. package/dist/core/updater.d.ts +159 -0
  134. package/dist/core/updater.js +608 -0
  135. package/dist/core/vim-mode.d.ts +151 -0
  136. package/dist/core/vim-mode.js +771 -0
  137. package/dist/core/voice.d.ts +137 -0
  138. package/dist/core/voice.js +538 -0
  139. package/dist/core/web-dashboard.d.ts +109 -0
  140. package/dist/core/web-dashboard.js +484 -0
  141. package/dist/hooks/hooks.d.ts +74 -0
  142. package/dist/hooks/hooks.js +160 -0
  143. package/dist/hooks/llm-evaluator.d.ts +165 -0
  144. package/dist/hooks/llm-evaluator.js +560 -0
  145. package/dist/index.d.ts +3 -0
  146. package/dist/index.js +1186 -0
  147. package/dist/lsp/lsp-manager.d.ts +63 -0
  148. package/dist/lsp/lsp-manager.js +351 -0
  149. package/dist/mcp/client.d.ts +133 -0
  150. package/dist/mcp/client.js +684 -0
  151. package/dist/mcp/mcp-apps.d.ts +70 -0
  152. package/dist/mcp/mcp-apps.js +1007 -0
  153. package/dist/tools/bash.d.ts +5 -0
  154. package/dist/tools/bash.js +195 -0
  155. package/dist/tools/browser.d.ts +92 -0
  156. package/dist/tools/browser.js +1570 -0
  157. package/dist/tools/extended.d.ts +6 -0
  158. package/dist/tools/extended.js +191 -0
  159. package/dist/tools/file.d.ts +10 -0
  160. package/dist/tools/file.js +382 -0
  161. package/dist/tools/github.d.ts +389 -0
  162. package/dist/tools/github.js +759 -0
  163. package/dist/tools/index.d.ts +9 -0
  164. package/dist/tools/index.js +40 -0
  165. package/dist/tools/memory.d.ts +6 -0
  166. package/dist/tools/memory.js +197 -0
  167. package/dist/tools/registry.d.ts +29 -0
  168. package/dist/tools/registry.js +64 -0
  169. package/dist/tools/web.d.ts +6 -0
  170. package/dist/tools/web.js +150 -0
  171. package/dist/ui/renderer.d.ts +97 -0
  172. package/dist/ui/renderer.js +279 -0
  173. package/dist/ui/theme.d.ts +27 -0
  174. package/dist/ui/theme.js +106 -0
  175. package/package.json +83 -0
@@ -0,0 +1,1099 @@
1
+ // ============================================================
2
+ // NeuroCLI - Multi-Model Orchestrator/Worker Pattern (GAP-33)
3
+ // Cost-stratified orchestration: expensive model plans,
4
+ // cheap model executes, medium model evaluates.
5
+ // Inspired by Aider's Architect mode.
6
+ // ============================================================
7
+ import { MODELS } from '../api/models.js';
8
+ import { FallbackChain } from './fallback.js';
9
+ import { SpendingMonitor } from './spending-warnings.js';
10
+ // ============================================================
11
+ // Classification Prompt Builder
12
+ // ============================================================
13
+ const CLASSIFICATION_SYSTEM_PROMPT = `You are a task classifier for a multi-model AI orchestration system. Analyze the user's prompt and classify it according to these dimensions:
14
+
15
+ 1. COMPLEXITY: How difficult is this task?
16
+ - "trivial": Simple lookup, greeting, single factual answer
17
+ - "simple": Single-step task, basic code edit, short answer
18
+ - "moderate": Multi-step task, moderate code changes, debugging
19
+ - "complex": Multi-phase implementation, architecture decisions, large refactors
20
+ - "critical": Production-critical changes, security-sensitive, data migration
21
+
22
+ 2. SUGGESTED ROLE: Which model role should handle this?
23
+ - "worker": For trivial/simple tasks (cheap, fast execution)
24
+ - "orchestrator": For complex/critical tasks (expensive, capable planning)
25
+ - "evaluator": For quality assessment tasks
26
+ - "reviewer": For code review tasks
27
+
28
+ 3. ESTIMATED TOKENS: Rough token count for input + expected output
29
+
30
+ Respond ONLY with valid JSON in this exact format:
31
+ {"complexity":"<trivial|simple|moderate|complex|critical>","suggestedRole":"<worker|orchestrator|evaluator|reviewer>","estimatedTokens":<number>,"reasoning":"<brief explanation>"}`;
32
+ // ============================================================
33
+ // Default Configuration
34
+ // ============================================================
35
+ export const DEFAULT_ORCHESTRATOR_CONFIG = {
36
+ roles: {
37
+ orchestrator: {
38
+ name: 'orchestrator',
39
+ model: 'qwen/qwen3-coder:free',
40
+ fallbackModels: [
41
+ 'nvidia/nemotron-3-super-120b-a12b:free',
42
+ 'meta-llama/llama-3.3-70b-instruct:free',
43
+ ],
44
+ maxTokensPerRequest: 262000,
45
+ description: 'Expensive/capable model for planning and decomposition',
46
+ },
47
+ worker: {
48
+ name: 'worker',
49
+ model: 'google/gemma-4-31b-it:free',
50
+ fallbackModels: [
51
+ 'meta-llama/llama-3.3-70b-instruct:free',
52
+ 'nvidia/nemotron-3-nano-30b-a3b:free',
53
+ ],
54
+ maxTokensPerRequest: 32768,
55
+ description: 'Cheap/fast model for execution and editing',
56
+ },
57
+ evaluator: {
58
+ name: 'evaluator',
59
+ model: 'qwen/qwen3-coder:free',
60
+ fallbackModels: [
61
+ 'nvidia/nemotron-3-super-120b-a12b:free',
62
+ ],
63
+ maxTokensPerRequest: 32768,
64
+ description: 'Medium model for quality checks and validation',
65
+ },
66
+ reviewer: {
67
+ name: 'reviewer',
68
+ model: 'qwen/qwen3-coder:free',
69
+ fallbackModels: [
70
+ 'nvidia/nemotron-3-super-120b-a12b:free',
71
+ ],
72
+ maxTokensPerRequest: 32768,
73
+ description: 'Medium model for code review',
74
+ },
75
+ },
76
+ costBudget: {
77
+ maxPerSession: 1.0,
78
+ maxPerTask: 0.25,
79
+ warnThreshold: 75,
80
+ },
81
+ qualityGates: {
82
+ enabled: true,
83
+ evaluatorModel: 'qwen/qwen3-coder:free',
84
+ minConfidence: 0.7,
85
+ },
86
+ dynamicSwitching: true,
87
+ };
88
+ // ============================================================
89
+ // Complexity Heuristic Tables (Local fallback for classification)
90
+ // ============================================================
91
+ const TRIVIAL_PATTERNS = [
92
+ /^(hi|hello|hey|thanks|ok|yes|no|done|sure)\b/i,
93
+ /^(what\s+is|define|list)\b/i,
94
+ /\b(trivial|tiny|quick\s+question)\b/i,
95
+ ];
96
+ const SIMPLE_PATTERNS = [
97
+ /\b(simple|basic|quick|easy|short|single|minor)\b/i,
98
+ /\b(fix\s+a?\s*typo|rename|add\s+a\s+comment|format|lint)\b/i,
99
+ ];
100
+ const COMPLEX_PATTERNS = [
101
+ /\b(implement\s+(a\s+)?(full|complete|comprehensive|end-to-end|production))\b/i,
102
+ /\b(build\s+(a\s+)?(system|application|platform|framework|service|architecture))\b/i,
103
+ /\b(design\s+(and\s+implement|the\s+architecture|from\s+scratch))\b/i,
104
+ /\b(multi[\s-]*(step|phase|stage|part|agent|service|module))\b/i,
105
+ /\b(orchestrat|coordinat|integrat|pipeline|workflow)\b/i,
106
+ /\b(complex|comprehensive|sophisticated|enterprise|large-scale)\b/i,
107
+ ];
108
+ const CRITICAL_PATTERNS = [
109
+ /\b(production|critical|security|migration|data\s*loss|irreversible|deploy)\b/i,
110
+ /\b(security\s+(audit|hardening|fix|vulnerability))\b/i,
111
+ /\b(database\s+migration|schema\s+change|breaking\s+change)\b/i,
112
+ ];
113
+ // ============================================================
114
+ // MultiModelOrchestrator
115
+ // ============================================================
116
+ export class MultiModelOrchestrator {
117
+ config;
118
+ client;
119
+ fallbackChain;
120
+ spendingMonitor;
121
+ // Per-role cost tracking
122
+ roleCosts = {
123
+ orchestrator: 0,
124
+ worker: 0,
125
+ evaluator: 0,
126
+ reviewer: 0,
127
+ };
128
+ // Per-model cost tracking
129
+ modelCosts = {};
130
+ // Per-task cost entries
131
+ taskCostEntries = [];
132
+ // Session totals
133
+ sessionTotalCost = 0;
134
+ sessionTotalTokens = 0;
135
+ sessionStartTime;
136
+ // Dynamic switching: worker failure tracking
137
+ workerFailures = [];
138
+ workerUpgradeUntil = 0;
139
+ // Current effective model per role (may differ from config during fallback)
140
+ effectiveModels;
141
+ // Budget exceeded flag
142
+ budgetExceeded = false;
143
+ constructor(config, apiClient) {
144
+ this.config = config;
145
+ this.client = apiClient;
146
+ this.sessionStartTime = Date.now();
147
+ // Initialize effective models from config
148
+ this.effectiveModels = {
149
+ orchestrator: config.roles.orchestrator.model,
150
+ worker: config.roles.worker.model,
151
+ evaluator: config.roles.evaluator.model,
152
+ reviewer: config.roles.reviewer.model,
153
+ };
154
+ // Set up fallback chain with all fallback models from all roles
155
+ const allFallbacks = new Set();
156
+ for (const role of Object.values(config.roles)) {
157
+ for (const fb of role.fallbackModels) {
158
+ allFallbacks.add(fb);
159
+ }
160
+ }
161
+ this.fallbackChain = new FallbackChain(apiClient, {
162
+ models: [...allFallbacks],
163
+ maxRetries: 2,
164
+ retryDelayMs: 3000,
165
+ fallbackOnErrors: ['rate_limit', 'overloaded', 'context_length_exceeded', 'timeout', 'server_error'],
166
+ });
167
+ // Set up spending monitor with budget from config
168
+ this.spendingMonitor = new SpendingMonitor({
169
+ sessionLimit: config.costBudget.maxPerSession,
170
+ dailyLimit: 0,
171
+ warnAtPercent: [config.costBudget.warnThreshold],
172
+ autoStopAtLimit: true,
173
+ trackByModel: true,
174
+ });
175
+ }
176
+ // ===================================================================
177
+ // Task Routing
178
+ // ===================================================================
179
+ /**
180
+ * Classify a task by complexity and suggest the appropriate model role.
181
+ * Uses a lightweight LLM call when possible; falls back to local heuristics.
182
+ */
183
+ async classifyTask(prompt, context) {
184
+ const fullPrompt = context ? `${context}\n\n---\n\n${prompt}` : prompt;
185
+ // Try LLM-based classification using the cheapest available model
186
+ try {
187
+ const classificationModel = this.cheapestAvailableModel();
188
+ const messages = [
189
+ { role: 'system', content: CLASSIFICATION_SYSTEM_PROMPT },
190
+ { role: 'user', content: fullPrompt },
191
+ ];
192
+ const response = await this.client.quickChat(classificationModel, messages, undefined, 0.1);
193
+ const parsed = this.parseClassificationResponse(response.content);
194
+ if (parsed) {
195
+ // Record the cost of classification
196
+ this.recordCost('orchestrator', classificationModel, response.usage, 'task-classification');
197
+ return parsed;
198
+ }
199
+ }
200
+ catch {
201
+ // Fall through to heuristic classification
202
+ }
203
+ // Local heuristic fallback
204
+ return this.classifyTaskHeuristic(prompt);
205
+ }
206
+ /**
207
+ * Route a classification to the appropriate model ID.
208
+ */
209
+ routeToModel(classification) {
210
+ // Check for dynamic upgrade: if worker has been failing, upgrade
211
+ if (classification.suggestedRole === 'worker' && this.isWorkerUpgraded()) {
212
+ return this.getEffectiveModel('orchestrator');
213
+ }
214
+ return this.getEffectiveModel(classification.suggestedRole);
215
+ }
216
+ // ===================================================================
217
+ // Execution Patterns
218
+ // ===================================================================
219
+ /**
220
+ * Full orchestration pipeline:
221
+ * orchestrator plans -> worker executes -> evaluator checks -> reviewer reviews
222
+ *
223
+ * Quality gates can re-route to orchestrator if evaluator confidence is low.
224
+ */
225
+ async orchestrateAndExecute(prompt, context) {
226
+ const startTime = Date.now();
227
+ const phases = [];
228
+ let escalated = false;
229
+ let escalationReason;
230
+ // Check budget before starting
231
+ if (!this.isWithinBudget()) {
232
+ return {
233
+ plan: this.emptyResponse('orchestrator'),
234
+ execution: this.emptyResponse('worker'),
235
+ totalCost: 0,
236
+ totalTokens: 0,
237
+ phases: [],
238
+ escalated: false,
239
+ success: false,
240
+ timestamp: Date.now(),
241
+ };
242
+ }
243
+ // --- Phase 1: Orchestrator plans ---
244
+ const planStart = Date.now();
245
+ let plan;
246
+ try {
247
+ plan = await this.callOrchestrator(`Plan the approach for the following task. Break it into clear steps.\n\n${context ? `Context:\n${context}\n\n` : ''}Task: ${prompt}`);
248
+ }
249
+ catch (err) {
250
+ const errMsg = err instanceof Error ? err.message : String(err);
251
+ return {
252
+ plan: this.emptyResponse('orchestrator'),
253
+ execution: this.emptyResponse('worker'),
254
+ totalCost: 0,
255
+ totalTokens: 0,
256
+ phases: [{ role: 'orchestrator', model: this.getEffectiveModel('orchestrator'), inputTokens: 0, outputTokens: 0, cost: 0, durationMs: Date.now() - planStart, success: false }],
257
+ escalated: false,
258
+ escalationReason: `Orchestrator failed: ${errMsg}`,
259
+ success: false,
260
+ timestamp: Date.now(),
261
+ };
262
+ }
263
+ phases.push({
264
+ role: 'orchestrator',
265
+ model: plan.model,
266
+ inputTokens: plan.usage.inputTokens,
267
+ outputTokens: plan.usage.outputTokens,
268
+ cost: plan.usage.cost,
269
+ durationMs: Date.now() - planStart,
270
+ success: true,
271
+ });
272
+ // Check budget after orchestrator
273
+ if (!this.isWithinBudget()) {
274
+ return {
275
+ plan,
276
+ execution: this.emptyResponse('worker'),
277
+ evaluation: undefined,
278
+ review: undefined,
279
+ totalCost: this.sumPhaseCosts(phases),
280
+ totalTokens: this.sumPhaseTokens(phases),
281
+ phases,
282
+ escalated: false,
283
+ escalationReason: 'Budget exceeded after orchestrator phase',
284
+ success: false,
285
+ timestamp: Date.now(),
286
+ };
287
+ }
288
+ // --- Phase 2: Worker executes ---
289
+ const execStart = Date.now();
290
+ const workerPrompt = `Based on this plan:\n\n${plan.content}\n\nExecute the task:\n${prompt}${context ? `\n\nContext:\n${context}` : ''}`;
291
+ let execution;
292
+ try {
293
+ execution = await this.callWorker(workerPrompt);
294
+ }
295
+ catch (err) {
296
+ const errMsg = err instanceof Error ? err.message : String(err);
297
+ this.recordWorkerFailure(this.getEffectiveModel('worker'), errMsg);
298
+ phases.push({
299
+ role: 'worker',
300
+ model: this.getEffectiveModel('worker'),
301
+ inputTokens: 0,
302
+ outputTokens: 0,
303
+ cost: 0,
304
+ durationMs: Date.now() - execStart,
305
+ success: false,
306
+ });
307
+ return {
308
+ plan,
309
+ execution: this.emptyResponse('worker'),
310
+ totalCost: this.sumPhaseCosts(phases),
311
+ totalTokens: this.sumPhaseTokens(phases),
312
+ phases,
313
+ escalated: false,
314
+ escalationReason: `Worker failed: ${errMsg}`,
315
+ success: false,
316
+ timestamp: Date.now(),
317
+ };
318
+ }
319
+ phases.push({
320
+ role: 'worker',
321
+ model: execution.model,
322
+ inputTokens: execution.usage.inputTokens,
323
+ outputTokens: execution.usage.outputTokens,
324
+ cost: execution.usage.cost,
325
+ durationMs: Date.now() - execStart,
326
+ success: true,
327
+ });
328
+ // Check budget after worker
329
+ if (!this.isWithinBudget()) {
330
+ return {
331
+ plan,
332
+ execution,
333
+ evaluation: undefined,
334
+ review: undefined,
335
+ totalCost: this.sumPhaseCosts(phases),
336
+ totalTokens: this.sumPhaseTokens(phases),
337
+ phases,
338
+ escalated: false,
339
+ escalationReason: 'Budget exceeded after worker phase',
340
+ success: true,
341
+ timestamp: Date.now(),
342
+ };
343
+ }
344
+ // --- Phase 3: Evaluator checks (if quality gates enabled) ---
345
+ let evaluation;
346
+ if (this.config.qualityGates.enabled) {
347
+ const evalStart = Date.now();
348
+ const evalPrompt = `Evaluate the quality of this work output.\n\nOriginal task: ${prompt}\n\nPlan:\n${plan.content}\n\nExecution output:\n${execution.content}\n\nRate your confidence that the execution correctly and completely fulfills the task on a scale of 0.0 to 1.0. Respond with JSON: {"confidence": <number>, "issues": [<string>], "passed": <boolean>}`;
349
+ try {
350
+ evaluation = await this.callEvaluator(evalPrompt);
351
+ // Parse evaluator confidence
352
+ const evalResult = this.parseEvaluationResponse(evaluation.content);
353
+ if (evalResult) {
354
+ evaluation.confidence = evalResult.confidence;
355
+ // If confidence is below threshold, escalate to orchestrator
356
+ if (evalResult.confidence < this.config.qualityGates.minConfidence) {
357
+ escalated = true;
358
+ escalationReason = `Evaluator confidence ${evalResult.confidence.toFixed(2)} below threshold ${this.config.qualityGates.minConfidence}. Issues: ${evalResult.issues.join('; ')}`;
359
+ // Re-execute with orchestrator model
360
+ const retryStart = Date.now();
361
+ const retryPrompt = `The worker's output was evaluated as insufficient (confidence: ${evalResult.confidence}).\n\nIssues: ${evalResult.issues.join('; ')}\n\nOriginal task: ${prompt}\n\nPrevious plan:\n${plan.content}\n\nPrevious output:\n${execution.content}\n\nPlease produce a better execution that addresses these issues.`;
362
+ try {
363
+ const retryExecution = await this.callOrchestrator(retryPrompt);
364
+ phases.push({
365
+ role: 'orchestrator',
366
+ model: retryExecution.model,
367
+ inputTokens: retryExecution.usage.inputTokens,
368
+ outputTokens: retryExecution.usage.outputTokens,
369
+ cost: retryExecution.usage.cost,
370
+ durationMs: Date.now() - retryStart,
371
+ success: true,
372
+ });
373
+ execution = retryExecution;
374
+ }
375
+ catch {
376
+ phases.push({
377
+ role: 'orchestrator',
378
+ model: this.getEffectiveModel('orchestrator'),
379
+ inputTokens: 0,
380
+ outputTokens: 0,
381
+ cost: 0,
382
+ durationMs: Date.now() - retryStart,
383
+ success: false,
384
+ });
385
+ }
386
+ }
387
+ }
388
+ phases.push({
389
+ role: 'evaluator',
390
+ model: evaluation.model,
391
+ inputTokens: evaluation.usage.inputTokens,
392
+ outputTokens: evaluation.usage.outputTokens,
393
+ cost: evaluation.usage.cost,
394
+ durationMs: Date.now() - evalStart,
395
+ success: true,
396
+ });
397
+ }
398
+ catch {
399
+ // Evaluator failure is non-fatal; continue without evaluation
400
+ phases.push({
401
+ role: 'evaluator',
402
+ model: this.getEffectiveModel('evaluator'),
403
+ inputTokens: 0,
404
+ outputTokens: 0,
405
+ cost: 0,
406
+ durationMs: Date.now() - evalStart,
407
+ success: false,
408
+ });
409
+ }
410
+ }
411
+ // Check budget after evaluator
412
+ if (!this.isWithinBudget()) {
413
+ return {
414
+ plan,
415
+ execution,
416
+ evaluation,
417
+ review: undefined,
418
+ totalCost: this.sumPhaseCosts(phases),
419
+ totalTokens: this.sumPhaseTokens(phases),
420
+ phases,
421
+ escalated,
422
+ escalationReason: escalated ? escalationReason : 'Budget exceeded after evaluator phase',
423
+ success: true,
424
+ timestamp: Date.now(),
425
+ };
426
+ }
427
+ // --- Phase 4: Reviewer reviews (if quality gates enabled) ---
428
+ let review;
429
+ if (this.config.qualityGates.enabled) {
430
+ const reviewStart = Date.now();
431
+ const reviewPrompt = `Review the following code/output for correctness, style, and best practices.\n\nOriginal task: ${prompt}\n\nExecution output:\n${execution.content}\n\nProvide a brief review with: {"approved": <boolean>, "comments": [<string>], "severity": "low"|"medium"|"high"}`;
432
+ try {
433
+ review = await this.callReviewer(reviewPrompt);
434
+ phases.push({
435
+ role: 'reviewer',
436
+ model: review.model,
437
+ inputTokens: review.usage.inputTokens,
438
+ outputTokens: review.usage.outputTokens,
439
+ cost: review.usage.cost,
440
+ durationMs: Date.now() - reviewStart,
441
+ success: true,
442
+ });
443
+ }
444
+ catch {
445
+ // Reviewer failure is non-fatal
446
+ phases.push({
447
+ role: 'reviewer',
448
+ model: this.getEffectiveModel('reviewer'),
449
+ inputTokens: 0,
450
+ outputTokens: 0,
451
+ cost: 0,
452
+ durationMs: Date.now() - reviewStart,
453
+ success: false,
454
+ });
455
+ }
456
+ }
457
+ const totalCost = this.sumPhaseCosts(phases);
458
+ const totalTokens = this.sumPhaseTokens(phases);
459
+ return {
460
+ plan,
461
+ execution,
462
+ evaluation,
463
+ review,
464
+ totalCost,
465
+ totalTokens,
466
+ phases,
467
+ escalated,
468
+ escalationReason: escalated ? escalationReason : undefined,
469
+ success: true,
470
+ timestamp: Date.now(),
471
+ };
472
+ }
473
+ // ===================================================================
474
+ // Direct Model Access by Role
475
+ // ===================================================================
476
+ /**
477
+ * Call the orchestrator model with a prompt.
478
+ */
479
+ async callOrchestrator(prompt) {
480
+ return this.callRole('orchestrator', prompt);
481
+ }
482
+ /**
483
+ * Call the worker model with a prompt.
484
+ */
485
+ async callWorker(prompt) {
486
+ // Dynamic switching: if worker has been failing, use orchestrator model
487
+ if (this.isWorkerUpgraded()) {
488
+ return this.callRole('orchestrator', prompt);
489
+ }
490
+ return this.callRole('worker', prompt);
491
+ }
492
+ /**
493
+ * Call the evaluator model with a prompt.
494
+ */
495
+ async callEvaluator(prompt) {
496
+ return this.callRole('evaluator', prompt);
497
+ }
498
+ /**
499
+ * Call the reviewer model with a prompt.
500
+ */
501
+ async callReviewer(prompt) {
502
+ return this.callRole('reviewer', prompt);
503
+ }
504
+ // ===================================================================
505
+ // Cost Management
506
+ // ===================================================================
507
+ /**
508
+ * Get the full cost breakdown for this session.
509
+ */
510
+ getSessionCost() {
511
+ return {
512
+ total: this.sessionTotalCost,
513
+ byRole: { ...this.roleCosts },
514
+ byModel: { ...this.modelCosts },
515
+ perTask: [...this.taskCostEntries],
516
+ sessionStart: this.sessionStartTime,
517
+ };
518
+ }
519
+ /**
520
+ * Get cost grouped by role.
521
+ */
522
+ getCostByRole() {
523
+ return { ...this.roleCosts };
524
+ }
525
+ /**
526
+ * Check whether spending is within budget.
527
+ */
528
+ isWithinBudget() {
529
+ if (this.budgetExceeded)
530
+ return false;
531
+ if (this.config.costBudget.maxPerSession > 0 && this.sessionTotalCost >= this.config.costBudget.maxPerSession) {
532
+ this.budgetExceeded = true;
533
+ return false;
534
+ }
535
+ const limitCheck = this.spendingMonitor.checkLimit();
536
+ if (!limitCheck.allowed) {
537
+ this.budgetExceeded = true;
538
+ return false;
539
+ }
540
+ return true;
541
+ }
542
+ // ===================================================================
543
+ // Configuration
544
+ // ===================================================================
545
+ /**
546
+ * Update role configurations partially.
547
+ */
548
+ updateRoles(roles) {
549
+ if (roles.orchestrator) {
550
+ this.config.roles.orchestrator = { ...this.config.roles.orchestrator, ...roles.orchestrator };
551
+ this.effectiveModels.orchestrator = this.config.roles.orchestrator.model;
552
+ }
553
+ if (roles.worker) {
554
+ this.config.roles.worker = { ...this.config.roles.worker, ...roles.worker };
555
+ this.effectiveModels.worker = this.config.roles.worker.model;
556
+ }
557
+ if (roles.evaluator) {
558
+ this.config.roles.evaluator = { ...this.config.roles.evaluator, ...roles.evaluator };
559
+ this.effectiveModels.evaluator = this.config.roles.evaluator.model;
560
+ }
561
+ if (roles.reviewer) {
562
+ this.config.roles.reviewer = { ...this.config.roles.reviewer, ...roles.reviewer };
563
+ this.effectiveModels.reviewer = this.config.roles.reviewer.model;
564
+ }
565
+ // Rebuild fallback chain with updated models
566
+ this.rebuildFallbackChain();
567
+ }
568
+ /**
569
+ * Set a new cost budget.
570
+ */
571
+ setCostBudget(budget) {
572
+ this.config.costBudget = { ...budget };
573
+ this.budgetExceeded = false;
574
+ // Update spending monitor
575
+ this.spendingMonitor = new SpendingMonitor({
576
+ sessionLimit: budget.maxPerSession,
577
+ dailyLimit: 0,
578
+ warnAtPercent: [budget.warnThreshold],
579
+ autoStopAtLimit: true,
580
+ trackByModel: true,
581
+ });
582
+ }
583
+ /**
584
+ * Get the currently effective model for a given role.
585
+ * This may differ from the configured model during fallback scenarios.
586
+ */
587
+ getEffectiveModel(role) {
588
+ return this.effectiveModels[role] ?? this.config.roles.orchestrator.model;
589
+ }
590
+ /**
591
+ * Get the full orchestrator config.
592
+ */
593
+ getConfig() {
594
+ return { ...this.config };
595
+ }
596
+ /**
597
+ * Reset session-level cost tracking and worker failure state.
598
+ */
599
+ resetSession() {
600
+ this.roleCosts = { orchestrator: 0, worker: 0, evaluator: 0, reviewer: 0 };
601
+ this.modelCosts = {};
602
+ this.taskCostEntries = [];
603
+ this.sessionTotalCost = 0;
604
+ this.sessionTotalTokens = 0;
605
+ this.sessionStartTime = Date.now();
606
+ this.workerFailures = [];
607
+ this.workerUpgradeUntil = 0;
608
+ this.budgetExceeded = false;
609
+ this.effectiveModels = {
610
+ orchestrator: this.config.roles.orchestrator.model,
611
+ worker: this.config.roles.worker.model,
612
+ evaluator: this.config.roles.evaluator.model,
613
+ reviewer: this.config.roles.reviewer.model,
614
+ };
615
+ this.spendingMonitor.resetSession();
616
+ }
617
+ // ===================================================================
618
+ // Private: Role Call with Fallback
619
+ // ===================================================================
620
+ /**
621
+ * Call a model for a specific role, with fallback chain support.
622
+ */
623
+ async callRole(roleName, prompt) {
624
+ const role = this.getRoleByName(roleName);
625
+ if (!role) {
626
+ throw new Error(`Unknown role: ${roleName}`);
627
+ }
628
+ const primaryModel = this.effectiveModels[roleName] ?? role.model;
629
+ const messages = [
630
+ { role: 'system', content: this.getSystemPromptForRole(roleName) },
631
+ { role: 'user', content: prompt },
632
+ ];
633
+ const startTime = Date.now();
634
+ try {
635
+ // Try primary model first
636
+ const response = await this.client.chat({
637
+ model: primaryModel,
638
+ messages,
639
+ maxTokens: role.maxTokensPerRequest,
640
+ stream: false,
641
+ temperature: this.getTemperatureForRole(roleName),
642
+ });
643
+ const modelResponse = {
644
+ content: response.content,
645
+ model: primaryModel,
646
+ role: roleName,
647
+ usage: response.usage,
648
+ toolCalls: response.toolCalls ?? [],
649
+ timestamp: Date.now(),
650
+ };
651
+ this.recordCost(roleName, primaryModel, response.usage, prompt.slice(0, 80));
652
+ return modelResponse;
653
+ }
654
+ catch (primaryError) {
655
+ // Primary model failed; try fallback chain
656
+ const errMsg = primaryError instanceof Error ? primaryError.message : String(primaryError);
657
+ for (const fallbackModel of role.fallbackModels) {
658
+ try {
659
+ const response = await this.client.chat({
660
+ model: fallbackModel,
661
+ messages,
662
+ maxTokens: role.maxTokensPerRequest,
663
+ stream: false,
664
+ temperature: this.getTemperatureForRole(roleName),
665
+ });
666
+ // Update effective model for this role
667
+ this.effectiveModels[roleName] = fallbackModel;
668
+ const modelResponse = {
669
+ content: response.content,
670
+ model: fallbackModel,
671
+ role: roleName,
672
+ usage: response.usage,
673
+ toolCalls: response.toolCalls ?? [],
674
+ timestamp: Date.now(),
675
+ };
676
+ this.recordCost(roleName, fallbackModel, response.usage, prompt.slice(0, 80));
677
+ return modelResponse;
678
+ }
679
+ catch {
680
+ // Continue to next fallback
681
+ continue;
682
+ }
683
+ }
684
+ // All models failed including fallbacks
685
+ throw new Error(`All models failed for role "${roleName}" (primary: ${primaryModel}). Primary error: ${errMsg}`);
686
+ }
687
+ }
688
+ // ===================================================================
689
+ // Private: Cost Recording
690
+ // ===================================================================
691
+ /**
692
+ * Record a cost entry for a role/model combination.
693
+ */
694
+ recordCost(role, model, usage, taskDescription) {
695
+ const cost = usage.cost;
696
+ this.roleCosts[role] = (this.roleCosts[role] ?? 0) + cost;
697
+ this.modelCosts[model] = (this.modelCosts[model] ?? 0) + cost;
698
+ this.sessionTotalCost += cost;
699
+ this.sessionTotalTokens += usage.inputTokens + usage.outputTokens;
700
+ this.taskCostEntries.push({
701
+ taskDescription: taskDescription.slice(0, 120),
702
+ cost,
703
+ role,
704
+ model,
705
+ timestamp: Date.now(),
706
+ });
707
+ // Also record in the spending monitor for integration with the broader system
708
+ this.spendingMonitor.record({
709
+ model,
710
+ inputTokens: usage.inputTokens,
711
+ outputTokens: usage.outputTokens,
712
+ cost,
713
+ sessionId: 'multi-model-session',
714
+ });
715
+ // Check warn threshold
716
+ if (this.config.costBudget.maxPerSession > 0) {
717
+ const percentUsed = (this.sessionTotalCost / this.config.costBudget.maxPerSession) * 100;
718
+ if (percentUsed >= this.config.costBudget.warnThreshold) {
719
+ console.warn(`\x1b[33m\x1b[1m[BUDGET WARNING]\x1b[0m Session spending at ${percentUsed.toFixed(1)}% of budget ($${this.sessionTotalCost.toFixed(4)} / $${this.config.costBudget.maxPerSession.toFixed(2)})`);
720
+ }
721
+ }
722
+ }
723
+ // ===================================================================
724
+ // Private: Task Classification
725
+ // ===================================================================
726
+ /**
727
+ * Parse the LLM classification response.
728
+ */
729
+ parseClassificationResponse(content) {
730
+ try {
731
+ // Try to extract JSON from the response (may be wrapped in markdown)
732
+ const jsonMatch = content.match(/\{[\s\S]*?\}/);
733
+ if (!jsonMatch)
734
+ return null;
735
+ const parsed = JSON.parse(jsonMatch[0]);
736
+ const validComplexities = ['trivial', 'simple', 'moderate', 'complex', 'critical'];
737
+ const validRoles = ['worker', 'orchestrator', 'evaluator', 'reviewer'];
738
+ const complexity = validComplexities.includes(parsed.complexity) ? parsed.complexity : 'moderate';
739
+ const suggestedRole = validRoles.includes(parsed.suggestedRole) ? parsed.suggestedRole : 'worker';
740
+ return {
741
+ complexity,
742
+ suggestedRole,
743
+ suggestedModel: this.getEffectiveModel(suggestedRole),
744
+ estimatedTokens: typeof parsed.estimatedTokens === 'number' ? parsed.estimatedTokens : 1000,
745
+ reasoning: typeof parsed.reasoning === 'string' ? parsed.reasoning : 'LLM-classified',
746
+ };
747
+ }
748
+ catch {
749
+ return null;
750
+ }
751
+ }
752
+ /**
753
+ * Local heuristic classification fallback.
754
+ * Used when the LLM classification call fails or is unavailable.
755
+ */
756
+ classifyTaskHeuristic(prompt) {
757
+ const length = prompt.length;
758
+ // Check critical patterns first
759
+ for (const pattern of CRITICAL_PATTERNS) {
760
+ if (pattern.test(prompt)) {
761
+ return {
762
+ complexity: 'critical',
763
+ suggestedRole: 'orchestrator',
764
+ suggestedModel: this.getEffectiveModel('orchestrator'),
765
+ estimatedTokens: Math.max(length * 3, 2000),
766
+ reasoning: 'Critical patterns detected (production/security/migration)',
767
+ };
768
+ }
769
+ }
770
+ // Check complex patterns
771
+ for (const pattern of COMPLEX_PATTERNS) {
772
+ if (pattern.test(prompt)) {
773
+ return {
774
+ complexity: 'complex',
775
+ suggestedRole: 'orchestrator',
776
+ suggestedModel: this.getEffectiveModel('orchestrator'),
777
+ estimatedTokens: Math.max(length * 3, 2000),
778
+ reasoning: 'Complex multi-phase task indicators detected',
779
+ };
780
+ }
781
+ }
782
+ // Check simple patterns
783
+ for (const pattern of SIMPLE_PATTERNS) {
784
+ if (pattern.test(prompt)) {
785
+ return {
786
+ complexity: 'simple',
787
+ suggestedRole: 'worker',
788
+ suggestedModel: this.getEffectiveModel('worker'),
789
+ estimatedTokens: Math.max(Math.ceil(length / 4), 500),
790
+ reasoning: 'Simple task indicators detected',
791
+ };
792
+ }
793
+ }
794
+ // Check trivial patterns
795
+ for (const pattern of TRIVIAL_PATTERNS) {
796
+ if (pattern.test(prompt)) {
797
+ return {
798
+ complexity: 'trivial',
799
+ suggestedRole: 'worker',
800
+ suggestedModel: this.getEffectiveModel('worker'),
801
+ estimatedTokens: Math.max(Math.ceil(length / 4), 100),
802
+ reasoning: 'Trivial interaction detected',
803
+ };
804
+ }
805
+ }
806
+ // Default to moderate based on length
807
+ if (length > 500) {
808
+ return {
809
+ complexity: 'moderate',
810
+ suggestedRole: 'orchestrator',
811
+ suggestedModel: this.getEffectiveModel('orchestrator'),
812
+ estimatedTokens: Math.ceil(length / 3) + 1000,
813
+ reasoning: 'Long prompt suggests moderate complexity',
814
+ };
815
+ }
816
+ return {
817
+ complexity: 'moderate',
818
+ suggestedRole: 'worker',
819
+ suggestedModel: this.getEffectiveModel('worker'),
820
+ estimatedTokens: Math.ceil(length / 3) + 500,
821
+ reasoning: 'Default moderate classification',
822
+ };
823
+ }
824
+ // ===================================================================
825
+ // Private: Evaluation Parsing
826
+ // ===================================================================
827
+ /**
828
+ * Parse the evaluator's confidence response.
829
+ */
830
+ parseEvaluationResponse(content) {
831
+ try {
832
+ const jsonMatch = content.match(/\{[\s\S]*?\}/);
833
+ if (!jsonMatch)
834
+ return null;
835
+ const parsed = JSON.parse(jsonMatch[0]);
836
+ const confidence = typeof parsed.confidence === 'number'
837
+ ? Math.max(0, Math.min(1, parsed.confidence))
838
+ : 0.5;
839
+ const issues = Array.isArray(parsed.issues)
840
+ ? parsed.issues.filter((i) => typeof i === 'string')
841
+ : [];
842
+ const passed = typeof parsed.passed === 'boolean' ? parsed.passed : confidence >= this.config.qualityGates.minConfidence;
843
+ return { confidence, issues, passed };
844
+ }
845
+ catch {
846
+ return null;
847
+ }
848
+ }
849
+ // ===================================================================
850
+ // Private: Dynamic Switching
851
+ // ===================================================================
852
+ /**
853
+ * Record a worker failure for dynamic switching.
854
+ */
855
+ recordWorkerFailure(model, error) {
856
+ this.workerFailures.push({
857
+ model,
858
+ timestamp: Date.now(),
859
+ error,
860
+ });
861
+ // If 3 or more failures in the last 5 minutes, upgrade worker to orchestrator model
862
+ const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;
863
+ const recentFailures = this.workerFailures.filter(f => f.timestamp > fiveMinutesAgo);
864
+ if (recentFailures.length >= 3 && this.config.dynamicSwitching) {
865
+ this.workerUpgradeUntil = Date.now() + 10 * 60 * 1000; // Upgrade for 10 minutes
866
+ console.warn(`\x1b[33m\x1b[1m[DYNAMIC SWITCH]\x1b[0m Worker model has failed ${recentFailures.length} times in 5 minutes. Upgrading to orchestrator model for 10 minutes.`);
867
+ }
868
+ }
869
+ /**
870
+ * Check if the worker is currently upgraded to the orchestrator model.
871
+ */
872
+ isWorkerUpgraded() {
873
+ if (!this.config.dynamicSwitching)
874
+ return false;
875
+ if (this.workerUpgradeUntil <= 0)
876
+ return false;
877
+ if (Date.now() > this.workerUpgradeUntil) {
878
+ this.workerUpgradeUntil = 0;
879
+ // Reset effective model back to worker's configured model
880
+ this.effectiveModels.worker = this.config.roles.worker.model;
881
+ return false;
882
+ }
883
+ return true;
884
+ }
885
+ // ===================================================================
886
+ // Private: Helpers
887
+ // ===================================================================
888
+ /**
889
+ * Get the cheapest available model from the registry.
890
+ * Used for classification calls to minimize cost.
891
+ */
892
+ cheapestAvailableModel() {
893
+ // Prefer free models with tool support
894
+ const freeToolModels = Object.values(MODELS).filter(m => m.inputPrice === 0 && m.outputPrice === 0 && m.supportsTools);
895
+ if (freeToolModels.length > 0) {
896
+ // Sort by context window descending (larger is better for classification)
897
+ freeToolModels.sort((a, b) => b.contextWindow - a.contextWindow);
898
+ return freeToolModels[0].id;
899
+ }
900
+ // Fall back to any free model
901
+ const freeModels = Object.values(MODELS).filter(m => m.inputPrice === 0 && m.outputPrice === 0);
902
+ if (freeModels.length > 0) {
903
+ return freeModels[0].id;
904
+ }
905
+ // Last resort: use worker model
906
+ return this.config.roles.worker.model;
907
+ }
908
+ /**
909
+ * Get the ModelRole config for a given role name.
910
+ */
911
+ getRoleByName(name) {
912
+ switch (name) {
913
+ case 'orchestrator': return this.config.roles.orchestrator;
914
+ case 'worker': return this.config.roles.worker;
915
+ case 'evaluator': return this.config.roles.evaluator;
916
+ case 'reviewer': return this.config.roles.reviewer;
917
+ default: return undefined;
918
+ }
919
+ }
920
+ /**
921
+ * Get a system prompt appropriate for a role.
922
+ */
923
+ getSystemPromptForRole(roleName) {
924
+ switch (roleName) {
925
+ case 'orchestrator':
926
+ return 'You are an expert software architect and planner. Analyze tasks, break them into clear actionable steps, and provide detailed implementation plans. Be thorough and consider edge cases.';
927
+ case 'worker':
928
+ return 'You are an efficient code executor. Follow the provided plan precisely and implement the required changes. Focus on correctness, clean code, and minimal changes. Do not over-engineer.';
929
+ case 'evaluator':
930
+ return 'You are a quality assurance evaluator. Assess whether the execution output correctly and completely fulfills the original task. Be objective and thorough. Rate your confidence from 0.0 to 1.0.';
931
+ case 'reviewer':
932
+ return 'You are a senior code reviewer. Review the output for correctness, style, best practices, potential bugs, and security issues. Be constructive and specific.';
933
+ default:
934
+ return 'You are a helpful AI assistant.';
935
+ }
936
+ }
937
+ /**
938
+ * Get an appropriate temperature for a role.
939
+ */
940
+ getTemperatureForRole(roleName) {
941
+ switch (roleName) {
942
+ case 'orchestrator': return 0.4; // More creative planning
943
+ case 'worker': return 0.2; // Precise execution
944
+ case 'evaluator': return 0.1; // Objective evaluation
945
+ case 'reviewer': return 0.2; // Thorough but precise
946
+ default: return 0.5;
947
+ }
948
+ }
949
+ /**
950
+ * Create an empty ModelResponse for error/budget-exceeded cases.
951
+ */
952
+ emptyResponse(role) {
953
+ return {
954
+ content: '',
955
+ model: this.getEffectiveModel(role),
956
+ role,
957
+ usage: { inputTokens: 0, outputTokens: 0, cost: 0 },
958
+ toolCalls: [],
959
+ timestamp: Date.now(),
960
+ };
961
+ }
962
+ /**
963
+ * Sum the cost of completed phases.
964
+ */
965
+ sumPhaseCosts(phases) {
966
+ return phases.reduce((sum, p) => sum + p.cost, 0);
967
+ }
968
+ /**
969
+ * Sum the tokens of completed phases.
970
+ */
971
+ sumPhaseTokens(phases) {
972
+ return phases.reduce((sum, p) => sum + p.inputTokens + p.outputTokens, 0);
973
+ }
974
+ /**
975
+ * Rebuild the fallback chain when role configs change.
976
+ */
977
+ rebuildFallbackChain() {
978
+ const allFallbacks = new Set();
979
+ for (const role of Object.values(this.config.roles)) {
980
+ for (const fb of role.fallbackModels) {
981
+ allFallbacks.add(fb);
982
+ }
983
+ }
984
+ this.fallbackChain = new FallbackChain(this.client, {
985
+ models: [...allFallbacks],
986
+ maxRetries: 2,
987
+ retryDelayMs: 3000,
988
+ fallbackOnErrors: ['rate_limit', 'overloaded', 'context_length_exceeded', 'timeout', 'server_error'],
989
+ });
990
+ }
991
+ }
992
+ // ============================================================
993
+ // Factory: Create a pre-configured orchestrator
994
+ // ============================================================
995
+ /**
996
+ * Create a MultiModelOrchestrator with sensible defaults for free models.
997
+ */
998
+ export function createFreeOrchestrator(apiClient) {
999
+ return new MultiModelOrchestrator(DEFAULT_ORCHESTRATOR_CONFIG, apiClient);
1000
+ }
1001
+ /**
1002
+ * Create a MultiModelOrchestrator with premium models for maximum capability.
1003
+ */
1004
+ export function createPremiumOrchestrator(apiClient) {
1005
+ const premiumConfig = {
1006
+ roles: {
1007
+ orchestrator: {
1008
+ name: 'orchestrator',
1009
+ model: 'anthropic/claude-opus-4',
1010
+ fallbackModels: ['openai/o3', 'google/gemini-2.5-pro'],
1011
+ maxTokensPerRequest: 32000,
1012
+ description: 'Premium orchestrator for complex planning',
1013
+ },
1014
+ worker: {
1015
+ name: 'worker',
1016
+ model: 'anthropic/claude-sonnet-4',
1017
+ fallbackModels: ['google/gemini-2.5-flash', 'deepseek/deepseek-chat'],
1018
+ maxTokensPerRequest: 64000,
1019
+ description: 'Capable worker for execution',
1020
+ },
1021
+ evaluator: {
1022
+ name: 'evaluator',
1023
+ model: 'google/gemini-2.5-flash',
1024
+ fallbackModels: ['openai/gpt-4o-mini', 'deepseek/deepseek-chat'],
1025
+ maxTokensPerRequest: 65536,
1026
+ description: 'Fast evaluator for quality checks',
1027
+ },
1028
+ reviewer: {
1029
+ name: 'reviewer',
1030
+ model: 'anthropic/claude-sonnet-4',
1031
+ fallbackModels: ['google/gemini-2.5-pro', 'deepseek/deepseek-r1'],
1032
+ maxTokensPerRequest: 64000,
1033
+ description: 'Thorough reviewer for code review',
1034
+ },
1035
+ },
1036
+ costBudget: {
1037
+ maxPerSession: 10.0,
1038
+ maxPerTask: 2.0,
1039
+ warnThreshold: 75,
1040
+ },
1041
+ qualityGates: {
1042
+ enabled: true,
1043
+ evaluatorModel: 'google/gemini-2.5-flash',
1044
+ minConfidence: 0.7,
1045
+ },
1046
+ dynamicSwitching: true,
1047
+ };
1048
+ return new MultiModelOrchestrator(premiumConfig, apiClient);
1049
+ }
1050
+ /**
1051
+ * Create a MultiModelOrchestrator with a balanced mix of free and premium models.
1052
+ */
1053
+ export function createBalancedOrchestrator(apiClient) {
1054
+ const balancedConfig = {
1055
+ roles: {
1056
+ orchestrator: {
1057
+ name: 'orchestrator',
1058
+ model: 'anthropic/claude-sonnet-4',
1059
+ fallbackModels: ['qwen/qwen3-coder:free', 'google/gemini-2.5-pro'],
1060
+ maxTokensPerRequest: 64000,
1061
+ description: 'Balanced orchestrator for planning',
1062
+ },
1063
+ worker: {
1064
+ name: 'worker',
1065
+ model: 'qwen/qwen3-coder:free',
1066
+ fallbackModels: ['google/gemma-4-31b-it:free', 'deepseek/deepseek-chat'],
1067
+ maxTokensPerRequest: 262000,
1068
+ description: 'Free worker for execution',
1069
+ },
1070
+ evaluator: {
1071
+ name: 'evaluator',
1072
+ model: 'google/gemini-2.5-flash',
1073
+ fallbackModels: ['qwen/qwen3-coder:free', 'deepseek/deepseek-chat'],
1074
+ maxTokensPerRequest: 65536,
1075
+ description: 'Fast evaluator for quality checks',
1076
+ },
1077
+ reviewer: {
1078
+ name: 'reviewer',
1079
+ model: 'google/gemini-2.5-flash',
1080
+ fallbackModels: ['qwen/qwen3-coder:free', 'deepseek/deepseek-r1'],
1081
+ maxTokensPerRequest: 65536,
1082
+ description: 'Fast reviewer for code review',
1083
+ },
1084
+ },
1085
+ costBudget: {
1086
+ maxPerSession: 3.0,
1087
+ maxPerTask: 0.75,
1088
+ warnThreshold: 75,
1089
+ },
1090
+ qualityGates: {
1091
+ enabled: true,
1092
+ evaluatorModel: 'google/gemini-2.5-flash',
1093
+ minConfidence: 0.7,
1094
+ },
1095
+ dynamicSwitching: true,
1096
+ };
1097
+ return new MultiModelOrchestrator(balancedConfig, apiClient);
1098
+ }
1099
+ //# sourceMappingURL=multi-model.js.map