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.
- package/LICENSE +21 -0
- package/README.md +510 -0
- package/dist/advisor/advisor.d.ts +50 -0
- package/dist/advisor/advisor.js +178 -0
- package/dist/agents/base.d.ts +62 -0
- package/dist/agents/base.js +215 -0
- package/dist/agents/orchestrator.d.ts +46 -0
- package/dist/agents/orchestrator.js +192 -0
- package/dist/agents/team.d.ts +51 -0
- package/dist/agents/team.js +210 -0
- package/dist/api/models.d.ts +23 -0
- package/dist/api/models.js +514 -0
- package/dist/api/ollama.d.ts +153 -0
- package/dist/api/ollama.js +751 -0
- package/dist/api/openrouter.d.ts +55 -0
- package/dist/api/openrouter.js +223 -0
- package/dist/commands/commands.d.ts +43 -0
- package/dist/commands/commands.js +308 -0
- package/dist/config/config.d.ts +8 -0
- package/dist/config/config.js +311 -0
- package/dist/context/compaction.d.ts +54 -0
- package/dist/context/compaction.js +251 -0
- package/dist/context/custom-agents.d.ts +107 -0
- package/dist/context/custom-agents.js +397 -0
- package/dist/context/custom-tools.d.ts +120 -0
- package/dist/context/custom-tools.js +564 -0
- package/dist/context/git-checkpoint.d.ts +52 -0
- package/dist/context/git-checkpoint.js +240 -0
- package/dist/context/neuro-md.d.ts +48 -0
- package/dist/context/neuro-md.js +202 -0
- package/dist/context/neuroignore.d.ts +102 -0
- package/dist/context/neuroignore.js +441 -0
- package/dist/context/repo-map.d.ts +38 -0
- package/dist/context/repo-map.js +220 -0
- package/dist/context/skill-standard.d.ts +262 -0
- package/dist/context/skill-standard.js +1156 -0
- package/dist/context/skill-system.d.ts +75 -0
- package/dist/context/skill-system.js +578 -0
- package/dist/context/tree-sitter.d.ts +99 -0
- package/dist/context/tree-sitter.js +1956 -0
- package/dist/core/acp.d.ts +325 -0
- package/dist/core/acp.js +1498 -0
- package/dist/core/api-server.d.ts +143 -0
- package/dist/core/api-server.js +550 -0
- package/dist/core/approval.d.ts +81 -0
- package/dist/core/approval.js +432 -0
- package/dist/core/auto-compact.d.ts +127 -0
- package/dist/core/auto-compact.js +436 -0
- package/dist/core/auto-mode.d.ts +232 -0
- package/dist/core/auto-mode.js +831 -0
- package/dist/core/background-session.d.ts +166 -0
- package/dist/core/background-session.js +696 -0
- package/dist/core/cicd.d.ts +257 -0
- package/dist/core/cicd.js +1443 -0
- package/dist/core/cloud-sync.d.ts +156 -0
- package/dist/core/cloud-sync.js +582 -0
- package/dist/core/code-review.d.ts +132 -0
- package/dist/core/code-review.js +1191 -0
- package/dist/core/completion.d.ts +49 -0
- package/dist/core/completion.js +384 -0
- package/dist/core/context.d.ts +38 -0
- package/dist/core/context.js +144 -0
- package/dist/core/diff-preview.d.ts +35 -0
- package/dist/core/diff-preview.js +173 -0
- package/dist/core/doom-loop.d.ts +51 -0
- package/dist/core/doom-loop.js +179 -0
- package/dist/core/engine.d.ts +183 -0
- package/dist/core/engine.js +942 -0
- package/dist/core/extended-thinking.d.ts +103 -0
- package/dist/core/extended-thinking.js +269 -0
- package/dist/core/fallback.d.ts +54 -0
- package/dist/core/fallback.js +104 -0
- package/dist/core/git-worktree.d.ts +139 -0
- package/dist/core/git-worktree.js +614 -0
- package/dist/core/headless.d.ts +30 -0
- package/dist/core/headless.js +101 -0
- package/dist/core/i18n.d.ts +83 -0
- package/dist/core/i18n.js +739 -0
- package/dist/core/linting.d.ts +129 -0
- package/dist/core/linting.js +699 -0
- package/dist/core/model-router.d.ts +109 -0
- package/dist/core/model-router.js +581 -0
- package/dist/core/multi-model.d.ts +243 -0
- package/dist/core/multi-model.js +1099 -0
- package/dist/core/multi-session.d.ts +144 -0
- package/dist/core/multi-session.js +442 -0
- package/dist/core/multimodal.d.ts +125 -0
- package/dist/core/multimodal.js +286 -0
- package/dist/core/observability.d.ts +93 -0
- package/dist/core/observability.js +737 -0
- package/dist/core/os-sandbox.d.ts +122 -0
- package/dist/core/os-sandbox.js +1193 -0
- package/dist/core/outcome-grading.d.ts +228 -0
- package/dist/core/outcome-grading.js +1123 -0
- package/dist/core/output-styles.d.ts +57 -0
- package/dist/core/output-styles.js +382 -0
- package/dist/core/parallel-agents.d.ts +183 -0
- package/dist/core/parallel-agents.js +563 -0
- package/dist/core/plugin-bundle.d.ts +236 -0
- package/dist/core/plugin-bundle.js +887 -0
- package/dist/core/plugin-sdk.d.ts +139 -0
- package/dist/core/plugin-sdk.js +273 -0
- package/dist/core/prompt-cache.d.ts +163 -0
- package/dist/core/prompt-cache.js +599 -0
- package/dist/core/sandbox.d.ts +127 -0
- package/dist/core/sandbox.js +369 -0
- package/dist/core/scheduled-tasks.d.ts +151 -0
- package/dist/core/scheduled-tasks.js +502 -0
- package/dist/core/security-scanner.d.ts +160 -0
- package/dist/core/security-scanner.js +1494 -0
- package/dist/core/session.d.ts +83 -0
- package/dist/core/session.js +269 -0
- package/dist/core/shell-completion.d.ts +51 -0
- package/dist/core/shell-completion.js +674 -0
- package/dist/core/smart-monitor.d.ts +146 -0
- package/dist/core/smart-monitor.js +1199 -0
- package/dist/core/spec-driven.d.ts +233 -0
- package/dist/core/spec-driven.js +1485 -0
- package/dist/core/spending-warnings.d.ts +123 -0
- package/dist/core/spending-warnings.js +456 -0
- package/dist/core/sub-agent.d.ts +298 -0
- package/dist/core/sub-agent.js +1023 -0
- package/dist/core/telemetry.d.ts +157 -0
- package/dist/core/telemetry.js +412 -0
- package/dist/core/terminal-ux.d.ts +134 -0
- package/dist/core/terminal-ux.js +649 -0
- package/dist/core/testing.d.ts +146 -0
- package/dist/core/testing.js +1200 -0
- package/dist/core/types.d.ts +439 -0
- package/dist/core/types.js +6 -0
- package/dist/core/undo-redo.d.ts +112 -0
- package/dist/core/undo-redo.js +290 -0
- package/dist/core/updater.d.ts +159 -0
- package/dist/core/updater.js +608 -0
- package/dist/core/vim-mode.d.ts +151 -0
- package/dist/core/vim-mode.js +771 -0
- package/dist/core/voice.d.ts +137 -0
- package/dist/core/voice.js +538 -0
- package/dist/core/web-dashboard.d.ts +109 -0
- package/dist/core/web-dashboard.js +484 -0
- package/dist/hooks/hooks.d.ts +74 -0
- package/dist/hooks/hooks.js +160 -0
- package/dist/hooks/llm-evaluator.d.ts +165 -0
- package/dist/hooks/llm-evaluator.js +560 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1186 -0
- package/dist/lsp/lsp-manager.d.ts +63 -0
- package/dist/lsp/lsp-manager.js +351 -0
- package/dist/mcp/client.d.ts +133 -0
- package/dist/mcp/client.js +684 -0
- package/dist/mcp/mcp-apps.d.ts +70 -0
- package/dist/mcp/mcp-apps.js +1007 -0
- package/dist/tools/bash.d.ts +5 -0
- package/dist/tools/bash.js +195 -0
- package/dist/tools/browser.d.ts +92 -0
- package/dist/tools/browser.js +1570 -0
- package/dist/tools/extended.d.ts +6 -0
- package/dist/tools/extended.js +191 -0
- package/dist/tools/file.d.ts +10 -0
- package/dist/tools/file.js +382 -0
- package/dist/tools/github.d.ts +389 -0
- package/dist/tools/github.js +759 -0
- package/dist/tools/index.d.ts +9 -0
- package/dist/tools/index.js +40 -0
- package/dist/tools/memory.d.ts +6 -0
- package/dist/tools/memory.js +197 -0
- package/dist/tools/registry.d.ts +29 -0
- package/dist/tools/registry.js +64 -0
- package/dist/tools/web.d.ts +6 -0
- package/dist/tools/web.js +150 -0
- package/dist/ui/renderer.d.ts +97 -0
- package/dist/ui/renderer.js +279 -0
- package/dist/ui/theme.d.ts +27 -0
- package/dist/ui/theme.js +106 -0
- 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
|