dual-brain 0.2.30 → 0.3.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/.dual-brain/docs/claude-code-extension-points.md +32 -0
- package/.dual-brain/docs/data-tools-capabilities.md +181 -0
- package/.dual-brain/docs/ecosystem-tools.md +91 -0
- package/.dual-brain/docs/panel-handoff.md +124 -0
- package/.dual-brain/docs/ruflo-analysis.md +48 -0
- package/bin/dual-brain.mjs +56 -56
- package/dist/mcp-server/index.d.ts +27 -0
- package/dist/mcp-server/index.js +359 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/src/agent-protocol.d.ts +163 -0
- package/dist/src/agent-protocol.js +368 -0
- package/dist/src/agent-protocol.js.map +1 -0
- package/dist/src/agents/registry.d.ts +52 -0
- package/dist/src/agents/registry.js +393 -0
- package/dist/src/agents/registry.js.map +1 -0
- package/dist/src/awareness.d.ts +93 -0
- package/dist/src/awareness.js +406 -0
- package/dist/src/awareness.js.map +1 -0
- package/dist/src/brief.d.ts +48 -0
- package/dist/src/brief.js +179 -0
- package/dist/src/brief.js.map +1 -0
- package/dist/src/calibration.d.ts +32 -0
- package/dist/src/calibration.js +133 -0
- package/dist/src/calibration.js.map +1 -0
- package/dist/src/checkpoint.d.ts +33 -0
- package/dist/src/checkpoint.js +99 -0
- package/dist/src/checkpoint.js.map +1 -0
- package/dist/src/ci-triage.d.ts +33 -0
- package/dist/src/ci-triage.js +193 -0
- package/dist/src/ci-triage.js.map +1 -0
- package/dist/src/cognitive-loop.d.ts +56 -0
- package/dist/src/cognitive-loop.js +495 -0
- package/dist/src/cognitive-loop.js.map +1 -0
- package/dist/src/collaboration.d.ts +147 -0
- package/dist/src/collaboration.js +438 -0
- package/dist/src/collaboration.js.map +1 -0
- package/dist/src/context-intel.d.ts +47 -0
- package/dist/src/context-intel.js +156 -0
- package/dist/src/context-intel.js.map +1 -0
- package/dist/src/context.d.ts +53 -0
- package/dist/src/context.js +332 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/continuity.d.ts +89 -0
- package/dist/src/continuity.js +230 -0
- package/dist/src/continuity.js.map +1 -0
- package/dist/src/cost-tracker.d.ts +47 -0
- package/dist/src/cost-tracker.js +170 -0
- package/dist/src/cost-tracker.js.map +1 -0
- package/dist/src/debrief.d.ts +53 -0
- package/dist/src/debrief.js +222 -0
- package/dist/src/debrief.js.map +1 -0
- package/dist/src/decide.d.ts +96 -0
- package/dist/src/decide.js +744 -0
- package/dist/src/decide.js.map +1 -0
- package/dist/src/decompose.d.ts +39 -0
- package/dist/src/decompose.js +218 -0
- package/dist/src/decompose.js.map +1 -0
- package/dist/src/detect.d.ts +91 -0
- package/dist/src/detect.js +544 -0
- package/dist/src/detect.js.map +1 -0
- package/dist/src/dispatch.d.ts +154 -0
- package/dist/src/dispatch.js +1306 -0
- package/dist/src/dispatch.js.map +1 -0
- package/dist/src/doctor.d.ts +421 -0
- package/dist/src/doctor.js +1689 -0
- package/dist/src/doctor.js.map +1 -0
- package/dist/src/engine.d.ts +70 -0
- package/dist/src/engine.js +155 -0
- package/dist/src/engine.js.map +1 -0
- package/dist/src/envelope.d.ts +36 -0
- package/dist/src/envelope.js +80 -0
- package/dist/src/envelope.js.map +1 -0
- package/dist/src/failure-memory.d.ts +55 -0
- package/dist/src/failure-memory.js +175 -0
- package/dist/src/failure-memory.js.map +1 -0
- package/dist/src/fx.d.ts +87 -0
- package/dist/src/fx.js +272 -0
- package/dist/src/fx.js.map +1 -0
- package/dist/src/governance.d.ts +93 -0
- package/dist/src/governance.js +261 -0
- package/dist/src/governance.js.map +1 -0
- package/dist/src/handoff.d.ts +11 -0
- package/dist/src/handoff.js +90 -0
- package/dist/src/handoff.js.map +1 -0
- package/dist/src/head-protocol.d.ts +76 -0
- package/dist/src/head-protocol.js +109 -0
- package/dist/src/head-protocol.js.map +1 -0
- package/dist/src/head.d.ts +222 -0
- package/dist/src/head.js +765 -0
- package/dist/src/head.js.map +1 -0
- package/dist/src/health.d.ts +132 -0
- package/dist/src/health.js +435 -0
- package/dist/src/health.js.map +1 -0
- package/dist/src/inbox.d.ts +70 -0
- package/dist/src/inbox.js +218 -0
- package/dist/src/inbox.js.map +1 -0
- package/dist/src/index.d.ts +33 -0
- package/dist/src/index.js +38 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/install-hooks.d.ts +13 -0
- package/dist/src/install-hooks.js +88 -0
- package/dist/src/install-hooks.js.map +1 -0
- package/dist/src/integrity.d.ts +59 -0
- package/dist/src/integrity.js +206 -0
- package/dist/src/integrity.js.map +1 -0
- package/dist/src/intelligence.d.ts +104 -0
- package/dist/src/intelligence.js +391 -0
- package/dist/src/intelligence.js.map +1 -0
- package/dist/src/ledger.d.ts +54 -0
- package/dist/src/ledger.js +179 -0
- package/dist/src/ledger.js.map +1 -0
- package/dist/src/living-docs.d.ts +14 -0
- package/dist/src/living-docs.js +197 -0
- package/dist/src/living-docs.js.map +1 -0
- package/dist/src/memory-tiers.d.ts +37 -0
- package/dist/src/memory-tiers.js +160 -0
- package/dist/src/memory-tiers.js.map +1 -0
- package/dist/src/model-profiles.d.ts +65 -0
- package/dist/src/model-profiles.js +568 -0
- package/dist/src/model-profiles.js.map +1 -0
- package/dist/src/models.d.ts +58 -0
- package/dist/src/models.js +327 -0
- package/dist/src/models.js.map +1 -0
- package/dist/src/narrative.d.ts +54 -0
- package/dist/src/narrative.js +163 -0
- package/dist/src/narrative.js.map +1 -0
- package/dist/src/nextstep.d.ts +16 -0
- package/dist/src/nextstep.js +103 -0
- package/dist/src/nextstep.js.map +1 -0
- package/dist/src/observer.d.ts +18 -0
- package/dist/src/observer.js +251 -0
- package/dist/src/observer.js.map +1 -0
- package/dist/src/outcome.d.ts +110 -0
- package/dist/src/outcome.js +377 -0
- package/dist/src/outcome.js.map +1 -0
- package/dist/src/pipeline.d.ts +167 -0
- package/dist/src/pipeline.js +1503 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/playbook.d.ts +59 -0
- package/dist/src/playbook.js +238 -0
- package/dist/src/playbook.js.map +1 -0
- package/dist/src/pr-agent.d.ts +97 -0
- package/dist/src/pr-agent.js +195 -0
- package/dist/src/pr-agent.js.map +1 -0
- package/dist/src/predictive.d.ts +57 -0
- package/dist/src/predictive.js +230 -0
- package/dist/src/predictive.js.map +1 -0
- package/dist/src/profile.d.ts +294 -0
- package/dist/src/profile.js +1347 -0
- package/dist/src/profile.js.map +1 -0
- package/dist/src/prompt-audit.d.ts +22 -0
- package/dist/src/prompt-audit.js +194 -0
- package/dist/src/prompt-audit.js.map +1 -0
- package/dist/src/prompt-intel.d.ts +12 -0
- package/dist/src/prompt-intel.js +321 -0
- package/dist/src/prompt-intel.js.map +1 -0
- package/dist/src/provider-context.d.ts +121 -0
- package/dist/src/provider-context.js +222 -0
- package/dist/src/provider-context.js.map +1 -0
- package/dist/src/provider-manager.d.ts +92 -0
- package/dist/src/provider-manager.js +428 -0
- package/dist/src/provider-manager.js.map +1 -0
- package/dist/src/receipt.d.ts +87 -0
- package/dist/src/receipt.js +326 -0
- package/dist/src/receipt.js.map +1 -0
- package/dist/src/recommendations.d.ts +13 -0
- package/dist/src/recommendations.js +291 -0
- package/dist/src/recommendations.js.map +1 -0
- package/dist/src/redact.d.ts +15 -0
- package/dist/src/redact.js +129 -0
- package/dist/src/redact.js.map +1 -0
- package/dist/src/replit.d.ts +397 -0
- package/dist/src/replit.js +1160 -0
- package/dist/src/replit.js.map +1 -0
- package/dist/src/repo.d.ts +149 -0
- package/dist/src/repo.js +416 -0
- package/dist/src/repo.js.map +1 -0
- package/dist/src/revert.d.ts +30 -0
- package/dist/src/revert.js +166 -0
- package/dist/src/revert.js.map +1 -0
- package/dist/src/room.d.ts +102 -0
- package/dist/src/room.js +212 -0
- package/dist/src/room.js.map +1 -0
- package/dist/src/routing-advisor.d.ts +57 -0
- package/dist/src/routing-advisor.js +221 -0
- package/dist/src/routing-advisor.js.map +1 -0
- package/dist/src/self-correct.d.ts +40 -0
- package/dist/src/self-correct.js +137 -0
- package/dist/src/self-correct.js.map +1 -0
- package/dist/src/session-lock.d.ts +35 -0
- package/dist/src/session-lock.js +134 -0
- package/dist/src/session-lock.js.map +1 -0
- package/dist/src/session.d.ts +267 -0
- package/dist/src/session.js +1660 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/settings-tui.d.ts +5 -0
- package/dist/src/settings-tui.js +422 -0
- package/dist/src/settings-tui.js.map +1 -0
- package/dist/src/setup-flow.d.ts +63 -0
- package/dist/src/setup-flow.js +233 -0
- package/dist/src/setup-flow.js.map +1 -0
- package/dist/src/signal.d.ts +19 -0
- package/dist/src/signal.js +122 -0
- package/dist/src/signal.js.map +1 -0
- package/dist/src/simmer.d.ts +85 -0
- package/dist/src/simmer.js +224 -0
- package/dist/src/simmer.js.map +1 -0
- package/dist/src/state-export.d.ts +129 -0
- package/dist/src/state-export.js +233 -0
- package/dist/src/state-export.js.map +1 -0
- package/dist/src/strategy.d.ts +54 -0
- package/dist/src/strategy.js +95 -0
- package/dist/src/strategy.js.map +1 -0
- package/dist/src/subscription.d.ts +40 -0
- package/dist/src/subscription.js +189 -0
- package/dist/src/subscription.js.map +1 -0
- package/dist/src/templates.d.ts +208 -0
- package/dist/src/templates.js +238 -0
- package/dist/src/templates.js.map +1 -0
- package/dist/src/test.d.ts +9 -0
- package/dist/src/test.js +1173 -0
- package/dist/src/test.js.map +1 -0
- package/dist/src/think-engine.d.ts +67 -0
- package/dist/src/think-engine.js +412 -0
- package/dist/src/think-engine.js.map +1 -0
- package/dist/src/tui.d.ts +71 -0
- package/dist/src/tui.js +242 -0
- package/dist/src/tui.js.map +1 -0
- package/dist/src/types.d.ts +177 -0
- package/dist/src/types.js +6 -0
- package/dist/src/types.js.map +1 -0
- package/dist/src/update-check.d.ts +7 -0
- package/dist/src/update-check.js +36 -0
- package/dist/src/update-check.js.map +1 -0
- package/dist/src/wave-planner.d.ts +30 -0
- package/dist/src/wave-planner.js +281 -0
- package/dist/src/wave-planner.js.map +1 -0
- package/hooks/head-guard.sh +41 -0
- package/hooks/task-classifier.mjs +328 -0
- package/hooks/vibe-router.mjs +387 -0
- package/package.json +29 -153
- package/src/agents/registry.mjs +0 -405
- package/src/awareness.mjs +0 -425
- package/src/brief.mjs +0 -266
- package/src/calibration.mjs +0 -148
- package/src/checkpoint.mjs +0 -109
- package/src/ci-triage.mjs +0 -191
- package/src/cognitive-loop.mjs +0 -562
- package/src/collaboration.mjs +0 -545
- package/src/context-intel.mjs +0 -158
- package/src/context.mjs +0 -389
- package/src/continuity.mjs +0 -298
- package/src/cost-tracker.mjs +0 -184
- package/src/debrief.mjs +0 -228
- package/src/decide.mjs +0 -1099
- package/src/decompose.mjs +0 -331
- package/src/detect.mjs +0 -702
- package/src/dispatch.mjs +0 -1447
- package/src/doctor.mjs +0 -1607
- package/src/envelope.mjs +0 -139
- package/src/failure-memory.mjs +0 -178
- package/src/fx.mjs +0 -276
- package/src/governance.mjs +0 -279
- package/src/handoff.mjs +0 -87
- package/src/head-protocol.mjs +0 -128
- package/src/head.mjs +0 -952
- package/src/health.mjs +0 -528
- package/src/inbox.mjs +0 -195
- package/src/index.mjs +0 -44
- package/src/install-hooks.mjs +0 -100
- package/src/integrity.mjs +0 -245
- package/src/intelligence.mjs +0 -447
- package/src/ledger.mjs +0 -196
- package/src/living-docs.mjs +0 -210
- package/src/memory-tiers.mjs +0 -193
- package/src/models.mjs +0 -363
- package/src/narrative.mjs +0 -169
- package/src/nextstep.mjs +0 -100
- package/src/observer.mjs +0 -241
- package/src/outcome.mjs +0 -400
- package/src/pipeline.mjs +0 -1711
- package/src/playbook.mjs +0 -257
- package/src/pr-agent.mjs +0 -214
- package/src/predictive.mjs +0 -250
- package/src/profile.mjs +0 -1411
- package/src/prompt-audit.mjs +0 -231
- package/src/prompt-intel.mjs +0 -325
- package/src/provider-context.mjs +0 -257
- package/src/receipt.mjs +0 -344
- package/src/recommendations.mjs +0 -296
- package/src/redact.mjs +0 -192
- package/src/replit.mjs +0 -1210
- package/src/repo.mjs +0 -445
- package/src/revert.mjs +0 -149
- package/src/routing-advisor.mjs +0 -204
- package/src/self-correct.mjs +0 -147
- package/src/session-lock.mjs +0 -160
- package/src/session.mjs +0 -1655
- package/src/settings-tui.mjs +0 -373
- package/src/setup-flow.mjs +0 -223
- package/src/signal.mjs +0 -115
- package/src/simmer.mjs +0 -241
- package/src/strategy.mjs +0 -235
- package/src/subscription.mjs +0 -212
- package/src/templates.mjs +0 -260
- package/src/think-engine.mjs +0 -428
- package/src/tui.mjs +0 -276
- package/src/update-check.mjs +0 -35
- package/src/wave-planner.mjs +0 -294
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* decide.ts — Routing decision module for the Dual-Brain Orchestrator.
|
|
4
|
+
*
|
|
5
|
+
* Given a task detection + user profile, decides which provider/model/effort/mode
|
|
6
|
+
* to use and explains why in one sentence.
|
|
7
|
+
*
|
|
8
|
+
* Exports: decideRoute, getModelCapabilities, getAvailableModels,
|
|
9
|
+
* WORK_STYLES, getWorkStyle, estimateBudgetPressure,
|
|
10
|
+
* shouldDualBrain, explainDecision, getFailoverOrder
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from 'fs';
|
|
13
|
+
import { join, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
// @ts-ignore — health.mjs not yet migrated
|
|
16
|
+
import { getProviderScore, checkCooldown } from './health.js';
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const WORKSPACE = join(__dirname, '..');
|
|
19
|
+
// ─── Model Registry (optional, lazy-loaded) ───────────────────────────────────
|
|
20
|
+
let modelRegistry = null;
|
|
21
|
+
let _registryLoadAttempted = false;
|
|
22
|
+
function _loadModelRegistry() {
|
|
23
|
+
if (_registryLoadAttempted)
|
|
24
|
+
return;
|
|
25
|
+
_registryLoadAttempted = true;
|
|
26
|
+
import('./models.js').then(mod => {
|
|
27
|
+
modelRegistry = mod;
|
|
28
|
+
}).catch(() => { });
|
|
29
|
+
}
|
|
30
|
+
_loadModelRegistry();
|
|
31
|
+
let routingAdvisor = null;
|
|
32
|
+
let _advisorLoadAttempted = false;
|
|
33
|
+
function _loadRoutingAdvisor() {
|
|
34
|
+
if (_advisorLoadAttempted)
|
|
35
|
+
return;
|
|
36
|
+
_advisorLoadAttempted = true;
|
|
37
|
+
// @ts-ignore — routing-advisor.mjs not yet migrated
|
|
38
|
+
import('./routing-advisor.js').then(mod => {
|
|
39
|
+
routingAdvisor = mod;
|
|
40
|
+
}).catch(() => { });
|
|
41
|
+
}
|
|
42
|
+
_loadRoutingAdvisor();
|
|
43
|
+
export const WORK_STYLES = {
|
|
44
|
+
fast: {
|
|
45
|
+
label: 'Fast',
|
|
46
|
+
defaultWorker: 'claude-sonnet-4-6',
|
|
47
|
+
complexWorker: 'claude-sonnet-4-6',
|
|
48
|
+
challengerPolicy: 'never',
|
|
49
|
+
checkpointPolicy: 'never',
|
|
50
|
+
reviewPolicy: 'skip',
|
|
51
|
+
description: 'Quick answers, single model, minimal reviews',
|
|
52
|
+
},
|
|
53
|
+
balanced: {
|
|
54
|
+
label: 'Balanced',
|
|
55
|
+
defaultWorker: 'claude-sonnet-4-6',
|
|
56
|
+
complexWorker: 'claude-opus-4-6',
|
|
57
|
+
challengerPolicy: 'high-risk',
|
|
58
|
+
checkpointPolicy: 'risky-ops',
|
|
59
|
+
reviewPolicy: 'important',
|
|
60
|
+
description: 'Smart routing, reviews on important changes',
|
|
61
|
+
},
|
|
62
|
+
fullpower: {
|
|
63
|
+
label: 'Full Power',
|
|
64
|
+
defaultWorker: 'claude-sonnet-4-6',
|
|
65
|
+
complexWorker: 'claude-opus-4-6',
|
|
66
|
+
challengerPolicy: 'medium-risk',
|
|
67
|
+
checkpointPolicy: 'all-edits',
|
|
68
|
+
reviewPolicy: 'non-trivial',
|
|
69
|
+
description: 'Deep reasoning, dual-brain on everything that matters',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
export function getWorkStyle(profile) {
|
|
73
|
+
const key = profile?.workStyle || profile?.work_style || 'balanced';
|
|
74
|
+
const style = WORK_STYLES[key] ?? WORK_STYLES.balanced;
|
|
75
|
+
return { ...style, key: WORK_STYLES[key] ? key : 'balanced' };
|
|
76
|
+
}
|
|
77
|
+
const MODEL_CAPABILITIES = {
|
|
78
|
+
haiku: {
|
|
79
|
+
provider: 'claude', tierFit: ['search'], contextWindow: 200_000,
|
|
80
|
+
strengths: ['search', 'format', 'lookup', 'classification', 'grep-analysis'],
|
|
81
|
+
weaknesses: ['complex-edits', 'architecture', 'security', 'multi-file-refactor'],
|
|
82
|
+
effortLevels: null, costTier: 'cheap',
|
|
83
|
+
},
|
|
84
|
+
sonnet: {
|
|
85
|
+
provider: 'claude', tierFit: ['execute', 'search'], contextWindow: 200_000,
|
|
86
|
+
strengths: ['edit', 'refactor', 'test', 'debug', 'code-generation', 'tool-use'],
|
|
87
|
+
weaknesses: ['deep-architecture', 'ambiguous-requirements', 'frontier-reasoning'],
|
|
88
|
+
effortLevels: ['low', 'medium', 'high', 'xhigh'], costTier: 'medium',
|
|
89
|
+
},
|
|
90
|
+
opus: {
|
|
91
|
+
provider: 'claude', tierFit: ['think', 'execute'], contextWindow: 200_000,
|
|
92
|
+
strengths: ['architecture', 'security', 'complex-debug', 'review', 'planning', 'threat-modeling'],
|
|
93
|
+
weaknesses: ['cost', 'overkill-for-simple-tasks'],
|
|
94
|
+
effortLevels: ['low', 'medium', 'high', 'xhigh'], costTier: 'expensive',
|
|
95
|
+
},
|
|
96
|
+
'gpt-4.1-mini': {
|
|
97
|
+
provider: 'openai', tierFit: ['search'], contextWindow: 1_047_576,
|
|
98
|
+
strengths: ['search', 'format', 'classification', 'fast-lookups'],
|
|
99
|
+
weaknesses: ['complex-refactors', 'architecture', 'multi-file-edits'],
|
|
100
|
+
effortLevels: ['low', 'medium', 'high'], costTier: 'cheap',
|
|
101
|
+
},
|
|
102
|
+
'gpt-4.1': {
|
|
103
|
+
provider: 'openai', tierFit: ['execute', 'search'], contextWindow: 1_047_576,
|
|
104
|
+
strengths: ['edit', 'code-generation', 'simple-refactor'],
|
|
105
|
+
weaknesses: ['architecture', 'security', 'complex-debug'],
|
|
106
|
+
effortLevels: ['low', 'medium', 'high'], costTier: 'medium',
|
|
107
|
+
},
|
|
108
|
+
'gpt-4o': {
|
|
109
|
+
provider: 'openai', tierFit: ['execute', 'think'], contextWindow: 128_000,
|
|
110
|
+
strengths: ['refactor', 'debug', 'code-generation', 'test', 'multimodal'],
|
|
111
|
+
weaknesses: ['cost vs mini'],
|
|
112
|
+
effortLevels: ['low', 'medium', 'high'], costTier: 'medium',
|
|
113
|
+
},
|
|
114
|
+
'gpt-4o-mini': {
|
|
115
|
+
provider: 'openai', tierFit: ['search'], contextWindow: 128_000, costTier: 'cheap',
|
|
116
|
+
strengths: ['quick-tasks', 'search', 'classification'],
|
|
117
|
+
weaknesses: ['complex-edits', 'architecture'],
|
|
118
|
+
effortLevels: null,
|
|
119
|
+
},
|
|
120
|
+
'o3': {
|
|
121
|
+
provider: 'openai', tierFit: ['think'], contextWindow: 200_000,
|
|
122
|
+
strengths: ['architecture', 'security', 'review', 'planning', 'complex-debug', 'deep-reasoning'],
|
|
123
|
+
weaknesses: ['cost', 'latency'],
|
|
124
|
+
effortLevels: ['low', 'medium', 'high'], costTier: 'expensive',
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
const WORK_MODELS = {
|
|
128
|
+
execute: 'claude-sonnet-4-6',
|
|
129
|
+
think: 'claude-opus-4-6',
|
|
130
|
+
search: 'claude-haiku-4-5-20251001',
|
|
131
|
+
challengerGpt: 'o3',
|
|
132
|
+
challengerGptFallback: 'gpt-4o',
|
|
133
|
+
searchGpt: 'gpt-4o-mini',
|
|
134
|
+
};
|
|
135
|
+
export function getModelCapabilities(model) {
|
|
136
|
+
return MODEL_CAPABILITIES[model] ?? null;
|
|
137
|
+
}
|
|
138
|
+
export function getAvailableModels(profile) {
|
|
139
|
+
const ALL_CLAUDE = ['haiku', 'sonnet', 'opus'];
|
|
140
|
+
const ALL_OPENAI = ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o4-mini', 'o3'];
|
|
141
|
+
const claudeModels = profile?.providers?.claude?.models;
|
|
142
|
+
const openaiModels = profile?.providers?.openai?.models;
|
|
143
|
+
return {
|
|
144
|
+
claude: Array.isArray(claudeModels) ? claudeModels : ALL_CLAUDE,
|
|
145
|
+
openai: Array.isArray(openaiModels) ? openaiModels : ALL_OPENAI,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
// ─── Internal helpers ────────────────────────────────────────────────────────
|
|
149
|
+
function pickChallengerModel(primaryProvider, available) {
|
|
150
|
+
if (primaryProvider === 'claude') {
|
|
151
|
+
if (available.openai.includes(WORK_MODELS.challengerGpt))
|
|
152
|
+
return WORK_MODELS.challengerGpt;
|
|
153
|
+
if (available.openai.includes(WORK_MODELS.challengerGptFallback))
|
|
154
|
+
return WORK_MODELS.challengerGptFallback;
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
if (available.claude.includes('opus'))
|
|
159
|
+
return WORK_MODELS.think;
|
|
160
|
+
if (available.claude.includes('sonnet'))
|
|
161
|
+
return WORK_MODELS.execute;
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function shouldTriggerChallenger(challengerPolicy, risk, hasBothProviders) {
|
|
166
|
+
if (challengerPolicy === 'never' || !hasBothProviders)
|
|
167
|
+
return false;
|
|
168
|
+
if (challengerPolicy === 'high-risk')
|
|
169
|
+
return ['high', 'critical'].includes(risk);
|
|
170
|
+
if (challengerPolicy === 'medium-risk')
|
|
171
|
+
return ['medium', 'high', 'critical'].includes(risk);
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
export function estimateBudgetPressure(_profile, _cwd) {
|
|
175
|
+
return { claude: 0, openai: 0 };
|
|
176
|
+
}
|
|
177
|
+
function getHealthScores(tier, cwd) {
|
|
178
|
+
const claudeClass = tier === 'search' ? 'haiku' : tier === 'think' ? 'opus' : 'sonnet';
|
|
179
|
+
const openaiClass = tier === 'search' ? 'gpt-4o-mini' : tier === 'think' ? 'o3' : 'gpt-4o';
|
|
180
|
+
checkCooldown('claude', claudeClass, cwd);
|
|
181
|
+
checkCooldown('openai', openaiClass, cwd);
|
|
182
|
+
return {
|
|
183
|
+
claude: getProviderScore('claude', claudeClass, cwd),
|
|
184
|
+
openai: getProviderScore('openai', openaiClass, cwd),
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
export function shouldDualBrain(detection, profile) {
|
|
188
|
+
const { intent = '', risk = 'low', complexity = 'simple', designImpact = false } = detection;
|
|
189
|
+
const dualEnabled = profile?.dual_brain_enabled !== false;
|
|
190
|
+
if (!dualEnabled)
|
|
191
|
+
return false;
|
|
192
|
+
const hasBothProviders = !!(profile?.providers?.claude?.enabled &&
|
|
193
|
+
profile?.providers?.claude?.plan &&
|
|
194
|
+
profile?.providers?.openai?.enabled &&
|
|
195
|
+
profile?.providers?.openai?.plan);
|
|
196
|
+
if (designImpact)
|
|
197
|
+
return true;
|
|
198
|
+
if (!hasBothProviders)
|
|
199
|
+
return false;
|
|
200
|
+
const criticalRisk = risk === 'critical';
|
|
201
|
+
const archOrSecurity = ['architecture', 'security'].includes(intent);
|
|
202
|
+
const complexHighRisk = complexity === 'complex' && risk === 'high';
|
|
203
|
+
return criticalRisk || archOrSecurity || complexHighRisk;
|
|
204
|
+
}
|
|
205
|
+
const THINK_INTENTS = ['architecture', 'security', 'review', 'planning', 'compare'];
|
|
206
|
+
const SEARCH_INTENTS = ['search', 'format', 'explain', 'lookup'];
|
|
207
|
+
function pickClaudeModel(detection, available) {
|
|
208
|
+
const { intent = '', risk = 'low', effort = 'medium' } = detection;
|
|
209
|
+
const needsOpus = THINK_INTENTS.includes(intent) || risk === 'critical' || effort === 'xhigh';
|
|
210
|
+
const needsHaiku = SEARCH_INTENTS.includes(intent) && !['high', 'critical'].includes(risk);
|
|
211
|
+
if (needsOpus && available.includes('opus'))
|
|
212
|
+
return 'opus';
|
|
213
|
+
if (needsHaiku && available.includes('haiku'))
|
|
214
|
+
return 'haiku';
|
|
215
|
+
return available.includes('sonnet') ? 'sonnet' : available[available.length - 1];
|
|
216
|
+
}
|
|
217
|
+
function pickOpenAIModel(detection, available) {
|
|
218
|
+
const { intent = '', risk = 'low', complexity = 'simple', effort = 'medium' } = detection;
|
|
219
|
+
const needsTop = THINK_INTENTS.includes(intent) || risk === 'critical' || effort === 'xhigh';
|
|
220
|
+
const needsMini = SEARCH_INTENTS.includes(intent) && effort === 'low';
|
|
221
|
+
const needsCodex = ['refactor', 'debug'].includes(intent) && complexity !== 'trivial';
|
|
222
|
+
const pref = needsTop ? 'o3' : needsMini ? 'gpt-4o-mini' : needsCodex ? 'gpt-4o' : 'gpt-4o';
|
|
223
|
+
const rank = ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o4-mini', 'o3'];
|
|
224
|
+
const idx = rank.indexOf(pref);
|
|
225
|
+
for (let i = idx; i >= 0; i--) {
|
|
226
|
+
if (available.includes(rank[i]))
|
|
227
|
+
return rank[i];
|
|
228
|
+
}
|
|
229
|
+
return available[0] ?? 'gpt-4o-mini';
|
|
230
|
+
}
|
|
231
|
+
function toShortName(model, provider) {
|
|
232
|
+
if (!model)
|
|
233
|
+
return model;
|
|
234
|
+
const m = model.toLowerCase();
|
|
235
|
+
if (provider === 'claude') {
|
|
236
|
+
if (m.includes('haiku'))
|
|
237
|
+
return 'haiku';
|
|
238
|
+
if (m.includes('opus'))
|
|
239
|
+
return 'opus';
|
|
240
|
+
if (m.includes('sonnet'))
|
|
241
|
+
return 'sonnet';
|
|
242
|
+
}
|
|
243
|
+
return model;
|
|
244
|
+
}
|
|
245
|
+
function toFullModelId(shortName, provider, tier) {
|
|
246
|
+
if (!modelRegistry)
|
|
247
|
+
return shortName;
|
|
248
|
+
const registryProvider = provider === 'claude' ? 'anthropic' : 'openai';
|
|
249
|
+
const taskType = tier === 'search' ? 'search' : tier === 'think' ? 'think' : 'execute';
|
|
250
|
+
const candidates = modelRegistry.getModelsForTask(taskType, registryProvider);
|
|
251
|
+
const match = candidates.find(m => m.id.toLowerCase().includes(shortName.toLowerCase()));
|
|
252
|
+
return match ? match.id : shortName;
|
|
253
|
+
}
|
|
254
|
+
function applyHealthDowngrade(model, score, provider, available, isHighStakes) {
|
|
255
|
+
if (score >= 50 || isHighStakes)
|
|
256
|
+
return model;
|
|
257
|
+
if (provider === 'claude') {
|
|
258
|
+
const claudeRank = ['haiku', 'sonnet', 'opus'];
|
|
259
|
+
const idx = claudeRank.indexOf(model);
|
|
260
|
+
const steps = score === 0 ? 2 : 1;
|
|
261
|
+
const downIdx = Math.max(0, idx - steps);
|
|
262
|
+
for (let i = downIdx; i <= idx; i++) {
|
|
263
|
+
if (available.includes(claudeRank[i]))
|
|
264
|
+
return claudeRank[i];
|
|
265
|
+
}
|
|
266
|
+
return available[0] ?? 'haiku';
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
const oaiRank = ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o4-mini', 'o3'];
|
|
270
|
+
const idx = oaiRank.indexOf(model);
|
|
271
|
+
const steps = score === 0 ? 2 : 1;
|
|
272
|
+
const downIdx = Math.max(0, idx - steps);
|
|
273
|
+
for (let i = downIdx; i <= idx; i++) {
|
|
274
|
+
if (available.includes(oaiRank[i]))
|
|
275
|
+
return oaiRank[i];
|
|
276
|
+
}
|
|
277
|
+
return available[0] ?? 'gpt-4o-mini';
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
function applyProfileBias(model, profile, provider, available, tier) {
|
|
281
|
+
const mode = (profile?.mode || profile?.profile || 'auto');
|
|
282
|
+
if (mode === 'cost-saver') {
|
|
283
|
+
const ranks = {
|
|
284
|
+
claude: ['haiku', 'sonnet', 'opus'],
|
|
285
|
+
openai: ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o4-mini', 'o3'],
|
|
286
|
+
};
|
|
287
|
+
for (const m of ranks[provider]) {
|
|
288
|
+
if (!available.includes(m))
|
|
289
|
+
continue;
|
|
290
|
+
const caps = MODEL_CAPABILITIES[m];
|
|
291
|
+
if (tier && caps && !caps.tierFit.includes(tier))
|
|
292
|
+
continue;
|
|
293
|
+
return m;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (mode === 'quality-first') {
|
|
297
|
+
const ranks = {
|
|
298
|
+
claude: ['opus', 'sonnet', 'haiku'],
|
|
299
|
+
openai: ['o3', 'o4-mini', 'gpt-4o', 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4o-mini'],
|
|
300
|
+
};
|
|
301
|
+
for (const m of ranks[provider]) {
|
|
302
|
+
if (available.includes(m))
|
|
303
|
+
return m;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const prefs = profile?.preferences || [];
|
|
307
|
+
for (const pref of prefs) {
|
|
308
|
+
if (pref.model && available.includes(pref.model) &&
|
|
309
|
+
pref.for && MODEL_CAPABILITIES[pref.model]?.strengths?.includes(pref.for)) {
|
|
310
|
+
return pref.model;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return model;
|
|
314
|
+
}
|
|
315
|
+
function pickEffort(model, detection) {
|
|
316
|
+
const caps = MODEL_CAPABILITIES[model];
|
|
317
|
+
if (!caps?.effortLevels)
|
|
318
|
+
return null;
|
|
319
|
+
const { risk = 'low', complexity = 'simple', effort } = detection;
|
|
320
|
+
if (effort && caps.effortLevels.includes(effort))
|
|
321
|
+
return effort;
|
|
322
|
+
if (risk === 'critical' || complexity === 'complex')
|
|
323
|
+
return 'xhigh';
|
|
324
|
+
if (risk === 'high' || complexity === 'moderate')
|
|
325
|
+
return 'high';
|
|
326
|
+
if (risk === 'low' && complexity === 'trivial')
|
|
327
|
+
return 'low';
|
|
328
|
+
return 'medium';
|
|
329
|
+
}
|
|
330
|
+
function pickModes(model, detection) {
|
|
331
|
+
const { intent = '', complexity = 'simple' } = detection;
|
|
332
|
+
const thinkingModels = ['sonnet', 'opus', 'o3', 'gpt-4o'];
|
|
333
|
+
const lightIntents = ['search', 'format', 'explain', 'lookup'];
|
|
334
|
+
return {
|
|
335
|
+
extendedThinking: thinkingModels.includes(model)
|
|
336
|
+
&& ['moderate', 'complex'].includes(complexity)
|
|
337
|
+
&& !lightIntents.includes(intent),
|
|
338
|
+
fastMode: model === 'opus',
|
|
339
|
+
extendedContext: ['sonnet', 'opus'].includes(model),
|
|
340
|
+
webSearch: ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o'].includes(model),
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function pickSandbox(model, detection) {
|
|
344
|
+
const { tier = 'execute' } = detection;
|
|
345
|
+
if (tier === 'search')
|
|
346
|
+
return 'read-only';
|
|
347
|
+
if (MODEL_CAPABILITIES[model]?.provider === 'openai')
|
|
348
|
+
return 'danger-full-access';
|
|
349
|
+
return 'workspace-write';
|
|
350
|
+
}
|
|
351
|
+
function chooseProvider(detection, profile, healthScores) {
|
|
352
|
+
const { tier = 'execute', intent = '' } = detection;
|
|
353
|
+
const claudeScore = healthScores.claude;
|
|
354
|
+
const openaiScore = healthScores.openai;
|
|
355
|
+
const providers = profile?.providers;
|
|
356
|
+
if (!providers?.openai?.enabled)
|
|
357
|
+
return 'claude';
|
|
358
|
+
if (claudeScore === 0 && openaiScore === 0) {
|
|
359
|
+
return claudeScore >= openaiScore ? 'claude' : 'openai';
|
|
360
|
+
}
|
|
361
|
+
if (THINK_INTENTS.includes(intent) && claudeScore > 0)
|
|
362
|
+
return 'claude';
|
|
363
|
+
if (claudeScore === 0 && openaiScore > 0)
|
|
364
|
+
return 'openai';
|
|
365
|
+
if (tier === 'execute' && !THINK_INTENTS.includes(intent)) {
|
|
366
|
+
if (claudeScore < 100 && openaiScore > claudeScore)
|
|
367
|
+
return 'openai';
|
|
368
|
+
}
|
|
369
|
+
return claudeScore >= openaiScore ? 'claude' : 'openai';
|
|
370
|
+
}
|
|
371
|
+
export function explainDecision(decision, detection, profile) {
|
|
372
|
+
const { provider, model, effort, dualBrain, workStyle, challengerModel } = decision;
|
|
373
|
+
const { intent = 'task', risk = 'low', complexity = 'simple', tier = 'execute' } = detection;
|
|
374
|
+
const healthScores = (decision._healthScores || {});
|
|
375
|
+
const mode = (profile?.mode || profile?.profile || 'auto');
|
|
376
|
+
const ws = (decision._workStyle ?? getWorkStyle(profile));
|
|
377
|
+
const wsLabel = ws.label ?? workStyle ?? 'Balanced';
|
|
378
|
+
const modelLabel = effort ? `${model} ${effort}` : model;
|
|
379
|
+
if (dualBrain && challengerModel) {
|
|
380
|
+
return `${wsLabel} mode: ${modelLabel} for ${intent}, ${challengerModel} challenger on ${risk}-risk changes.`;
|
|
381
|
+
}
|
|
382
|
+
if (dualBrain) {
|
|
383
|
+
return `${wsLabel} mode: ${modelLabel} with dual-brain review because this ${intent} change is ${risk} risk.`;
|
|
384
|
+
}
|
|
385
|
+
const claudeScore = healthScores.claude ?? 100;
|
|
386
|
+
const providerScore = healthScores[provider] ?? 100;
|
|
387
|
+
if (claudeScore === 0 && provider === 'openai') {
|
|
388
|
+
return `${wsLabel} mode: using ${modelLabel} because Claude is rate-limited and this is an isolated ${tier} task.`;
|
|
389
|
+
}
|
|
390
|
+
if (providerScore < 50) {
|
|
391
|
+
return `${wsLabel} mode: using ${modelLabel} (downgraded due to rate-limit cooldown) for this ${complexity} ${intent}.`;
|
|
392
|
+
}
|
|
393
|
+
if (mode === 'cost-saver') {
|
|
394
|
+
return `${wsLabel} mode: using ${modelLabel} (cost-saver bias) for ${risk}-risk ${intent}.`;
|
|
395
|
+
}
|
|
396
|
+
if (mode === 'quality-first') {
|
|
397
|
+
return `${wsLabel} mode: using ${modelLabel} (quality-first bias) for ${intent}.`;
|
|
398
|
+
}
|
|
399
|
+
if (THINK_INTENTS.includes(intent)) {
|
|
400
|
+
return `${wsLabel} mode: ${modelLabel} for ${intent} — deep reasoning needed.`;
|
|
401
|
+
}
|
|
402
|
+
if (tier === 'search' || SEARCH_INTENTS.includes(intent)) {
|
|
403
|
+
return `${wsLabel} mode: ${modelLabel} for lightweight ${intent} lookup.`;
|
|
404
|
+
}
|
|
405
|
+
return `${wsLabel} mode: ${modelLabel} for ${intent} (${risk} risk, ${provider} healthy).`;
|
|
406
|
+
}
|
|
407
|
+
export function parsePreferences(preferences) {
|
|
408
|
+
const active = (preferences || []).filter(p => p.enabled);
|
|
409
|
+
const signals = {
|
|
410
|
+
biasOverride: null, preferProvider: null, avoidProvider: null,
|
|
411
|
+
alwaysDualBrain: false, neverDualBrain: false, preferModel: null,
|
|
412
|
+
};
|
|
413
|
+
for (const pref of active) {
|
|
414
|
+
const t = pref.text.toLowerCase();
|
|
415
|
+
if (/cheap|save|budget|frugal|economical|cost/i.test(t))
|
|
416
|
+
signals.biasOverride = 'cost-saver';
|
|
417
|
+
if (/quality|best|thorough|careful|premium/i.test(t))
|
|
418
|
+
signals.biasOverride = 'quality-first';
|
|
419
|
+
if (/prefer claude|use claude|claude first/i.test(t))
|
|
420
|
+
signals.preferProvider = 'claude';
|
|
421
|
+
if (/prefer (openai|gpt|chatgpt)|use (openai|gpt)/i.test(t))
|
|
422
|
+
signals.preferProvider = 'openai';
|
|
423
|
+
if (/avoid claude|no claude/i.test(t))
|
|
424
|
+
signals.avoidProvider = 'claude';
|
|
425
|
+
if (/avoid (openai|gpt)|no (openai|gpt)/i.test(t))
|
|
426
|
+
signals.avoidProvider = 'openai';
|
|
427
|
+
if (/always/.test(t) && /(consensus|dual.brain|two.brain|dual)/i.test(t))
|
|
428
|
+
signals.alwaysDualBrain = true;
|
|
429
|
+
if (/never (consensus|dual)|skip (review|consensus)|solo/i.test(t))
|
|
430
|
+
signals.neverDualBrain = true;
|
|
431
|
+
if (/prefer opus|use opus/i.test(t))
|
|
432
|
+
signals.preferModel = 'opus';
|
|
433
|
+
if (/prefer sonnet|use sonnet/i.test(t))
|
|
434
|
+
signals.preferModel = 'sonnet';
|
|
435
|
+
if (/prefer haiku|use haiku/i.test(t))
|
|
436
|
+
signals.preferModel = 'haiku';
|
|
437
|
+
}
|
|
438
|
+
return signals;
|
|
439
|
+
}
|
|
440
|
+
function applyCriticalRiskFloor(model, provider, available, risk) {
|
|
441
|
+
if (risk !== 'critical')
|
|
442
|
+
return model;
|
|
443
|
+
const cheapModels = { claude: 'haiku', openai: 'gpt-4.1-mini' };
|
|
444
|
+
const floorModels = { claude: 'sonnet', openai: 'gpt-4.1' };
|
|
445
|
+
if (model === cheapModels[provider]) {
|
|
446
|
+
const floor = floorModels[provider];
|
|
447
|
+
const escalated = available.includes(floor) ? floor : available[available.length - 1] ?? model;
|
|
448
|
+
process.stderr.write(`[dual-brain] Warning: cost-saver selected ${model} for a critical-risk task. Escalating to ${escalated} (safety floor).\n`);
|
|
449
|
+
return escalated;
|
|
450
|
+
}
|
|
451
|
+
return model;
|
|
452
|
+
}
|
|
453
|
+
// ─── Exported: decideRoute ────────────────────────────────────────────────────
|
|
454
|
+
export function decideRoute({ profile = {}, detection = {}, cwd, thinkResult, sessionContext = null } = {}) {
|
|
455
|
+
const available = getAvailableModels(profile);
|
|
456
|
+
const workStyle = getWorkStyle(profile);
|
|
457
|
+
const prefSignals = parsePreferences(profile.preferences);
|
|
458
|
+
const profileWithEffectiveBias = prefSignals.biasOverride
|
|
459
|
+
? { ...profile, mode: prefSignals.biasOverride }
|
|
460
|
+
: profile;
|
|
461
|
+
const { tier = 'execute', risk = 'low', complexity = 'simple', effort: detectionEffort } = detection;
|
|
462
|
+
const isHighStakes = ['critical', 'high'].includes(risk);
|
|
463
|
+
const needsDeepReasoning = THINK_INTENTS.includes(detection.intent || '') ||
|
|
464
|
+
risk === 'critical' ||
|
|
465
|
+
(complexity === 'complex' && ['high', 'critical'].includes(risk)) ||
|
|
466
|
+
detectionEffort === 'xhigh';
|
|
467
|
+
const healthScores = getHealthScores(tier, cwd);
|
|
468
|
+
let provider = chooseProvider(detection, profileWithEffectiveBias, healthScores);
|
|
469
|
+
if (prefSignals.preferProvider) {
|
|
470
|
+
const preferred = prefSignals.preferProvider;
|
|
471
|
+
const prefEnabled = profile?.providers?.[preferred]?.enabled;
|
|
472
|
+
const prefScore = healthScores[preferred] ?? 0;
|
|
473
|
+
if (prefEnabled && prefScore > 0)
|
|
474
|
+
provider = preferred;
|
|
475
|
+
}
|
|
476
|
+
if (prefSignals.avoidProvider && provider === prefSignals.avoidProvider) {
|
|
477
|
+
const other = prefSignals.avoidProvider === 'claude' ? 'openai' : 'claude';
|
|
478
|
+
const otherEnabled = profile?.providers?.[other]?.enabled;
|
|
479
|
+
const otherScore = healthScores[other] ?? 0;
|
|
480
|
+
if (otherEnabled && otherScore > 0)
|
|
481
|
+
provider = other;
|
|
482
|
+
}
|
|
483
|
+
const _fallbackClaude = (() => {
|
|
484
|
+
const wantOpus = needsDeepReasoning && workStyle.key !== 'fast';
|
|
485
|
+
const fb = wantOpus && available.claude.includes('opus') ? 'opus' : 'sonnet';
|
|
486
|
+
return available.claude.includes(fb) ? fb : (available.claude[available.claude.length - 1] ?? 'sonnet');
|
|
487
|
+
})();
|
|
488
|
+
const _fallbackOpenAI = (() => {
|
|
489
|
+
const wantO3 = needsDeepReasoning && workStyle.key === 'fullpower';
|
|
490
|
+
const fb = wantO3 && available.openai.includes('o3') ? 'o3' : 'gpt-4o';
|
|
491
|
+
return available.openai.includes(fb) ? fb : (available.openai[available.openai.length - 1] ?? 'gpt-4o');
|
|
492
|
+
})();
|
|
493
|
+
let model;
|
|
494
|
+
if (modelRegistry) {
|
|
495
|
+
const registryProvider = provider === 'claude' ? 'anthropic' : 'openai';
|
|
496
|
+
const taskType = tier === 'search' ? 'search' : tier === 'think' ? 'think' : 'execute';
|
|
497
|
+
const constraints = {
|
|
498
|
+
provider: registryProvider,
|
|
499
|
+
...(tier === 'search' && { preferSpeed: true }),
|
|
500
|
+
...(tier === 'think' && { requireReasoning: true }),
|
|
501
|
+
...(!needsDeepReasoning && workStyle.key === 'fast' && { maxCost: 'medium' }),
|
|
502
|
+
};
|
|
503
|
+
const registryResult = modelRegistry.getBestModel(taskType, constraints);
|
|
504
|
+
if (registryResult) {
|
|
505
|
+
model = registryResult.id;
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
model = provider === 'claude' ? _fallbackClaude : _fallbackOpenAI;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
model = provider === 'claude' ? _fallbackClaude : _fallbackOpenAI;
|
|
513
|
+
}
|
|
514
|
+
model = toShortName(model, provider);
|
|
515
|
+
model = applyHealthDowngrade(model, healthScores[provider], provider, available[provider], isHighStakes);
|
|
516
|
+
model = applyProfileBias(model, profileWithEffectiveBias, provider, available[provider], detection.tier);
|
|
517
|
+
let thinkTier = null;
|
|
518
|
+
try {
|
|
519
|
+
if (thinkResult?.tier)
|
|
520
|
+
thinkTier = thinkResult.tier;
|
|
521
|
+
}
|
|
522
|
+
catch { }
|
|
523
|
+
if (thinkTier && !isHighStakes) {
|
|
524
|
+
const claudeRankAsc = ['haiku', 'sonnet', 'opus'];
|
|
525
|
+
const openaiRankAsc = ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o4-mini', 'o3'];
|
|
526
|
+
if (thinkTier === 'recall' && provider === 'claude') {
|
|
527
|
+
const target = 'haiku';
|
|
528
|
+
const currentIdx = claudeRankAsc.indexOf(model);
|
|
529
|
+
const targetIdx = claudeRankAsc.indexOf(target);
|
|
530
|
+
if (targetIdx !== -1 && targetIdx < currentIdx && available.claude.includes(target))
|
|
531
|
+
model = target;
|
|
532
|
+
}
|
|
533
|
+
else if (thinkTier === 'recall' && provider === 'openai') {
|
|
534
|
+
const target = 'gpt-4o-mini';
|
|
535
|
+
const currentIdx = openaiRankAsc.indexOf(model);
|
|
536
|
+
const targetIdx = openaiRankAsc.indexOf(target);
|
|
537
|
+
if (targetIdx !== -1 && targetIdx < currentIdx && available.openai.includes(target))
|
|
538
|
+
model = target;
|
|
539
|
+
}
|
|
540
|
+
else if (thinkTier === 'quick' && provider === 'claude') {
|
|
541
|
+
const target = 'sonnet';
|
|
542
|
+
const currentIdx = claudeRankAsc.indexOf(model);
|
|
543
|
+
const targetIdx = claudeRankAsc.indexOf(target);
|
|
544
|
+
if (targetIdx !== -1 && targetIdx < currentIdx && available.claude.includes(target))
|
|
545
|
+
model = target;
|
|
546
|
+
}
|
|
547
|
+
else if (thinkTier === 'quick' && provider === 'openai') {
|
|
548
|
+
const target = 'gpt-4o';
|
|
549
|
+
const currentIdx = openaiRankAsc.indexOf(model);
|
|
550
|
+
const targetIdx = openaiRankAsc.indexOf(target);
|
|
551
|
+
if (targetIdx !== -1 && targetIdx < currentIdx && available.openai.includes(target))
|
|
552
|
+
model = target;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
// Session context escalation (abbreviated for brevity — same logic as .mjs)
|
|
556
|
+
if (sessionContext) {
|
|
557
|
+
const sessionAttempts = Array.isArray(sessionContext.priorAttempts) ? sessionContext.priorAttempts : [];
|
|
558
|
+
const sessionFailures = sessionAttempts.filter(a => a && (a.failed || a.status === 'failed'));
|
|
559
|
+
const sessionSuccesses = sessionAttempts.filter(a => a && !a.failed && a.status !== 'failed');
|
|
560
|
+
if (sessionFailures.length >= 2 && !isHighStakes) {
|
|
561
|
+
if (provider === 'claude') {
|
|
562
|
+
const claudeRank = ['haiku', 'sonnet', 'opus'];
|
|
563
|
+
const currentIdx = claudeRank.indexOf(toShortName(model, 'claude'));
|
|
564
|
+
if (currentIdx !== -1 && currentIdx < claudeRank.length - 1) {
|
|
565
|
+
const escalated = claudeRank[currentIdx + 1];
|
|
566
|
+
if (available.claude.includes(escalated))
|
|
567
|
+
model = escalated;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
const oaiRank = ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o4-mini', 'o3'];
|
|
572
|
+
const currentIdx = oaiRank.indexOf(model);
|
|
573
|
+
if (currentIdx !== -1 && currentIdx < oaiRank.length - 1) {
|
|
574
|
+
const escalated = oaiRank[currentIdx + 1];
|
|
575
|
+
if (available.openai.includes(escalated))
|
|
576
|
+
model = escalated;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
if (sessionSuccesses.length > 0) {
|
|
581
|
+
const lastSuccess = sessionSuccesses[sessionSuccesses.length - 1];
|
|
582
|
+
if (lastSuccess.provider && lastSuccess.model && !isHighStakes) {
|
|
583
|
+
const successProvider = lastSuccess.provider;
|
|
584
|
+
const successModel = lastSuccess.model;
|
|
585
|
+
const providerEnabled = profile?.providers?.[successProvider]?.enabled;
|
|
586
|
+
const providerHealthy = (healthScores[successProvider] ?? 0) > 0;
|
|
587
|
+
if (providerEnabled && providerHealthy) {
|
|
588
|
+
const shortSuccess = toShortName(successModel, successProvider);
|
|
589
|
+
if (available[successProvider]?.includes(shortSuccess)) {
|
|
590
|
+
provider = successProvider;
|
|
591
|
+
model = shortSuccess;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
model = applyCriticalRiskFloor(model, provider, available[provider], detection.risk || 'low');
|
|
598
|
+
if (prefSignals.preferModel) {
|
|
599
|
+
const wantedModel = prefSignals.preferModel;
|
|
600
|
+
if ((available[provider])?.includes(wantedModel)) {
|
|
601
|
+
model = wantedModel;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
model = toFullModelId(model, provider, tier);
|
|
605
|
+
let _advisorOverride = null;
|
|
606
|
+
if (routingAdvisor && provider === 'claude') {
|
|
607
|
+
try {
|
|
608
|
+
const advice = routingAdvisor.adviseModel({ intent: detection.intent || '', tier, risk: detection.risk || 'low' }, cwd);
|
|
609
|
+
if (advice.confidence > 0.3 && advice.model) {
|
|
610
|
+
const advisorShort = advice.model;
|
|
611
|
+
const previousModel = toShortName(model, 'claude');
|
|
612
|
+
if (advisorShort !== previousModel && available.claude.includes(advisorShort)) {
|
|
613
|
+
const overrideFullId = toFullModelId(advisorShort, 'claude', tier);
|
|
614
|
+
_advisorOverride = { from: model, to: overrideFullId, reason: advice.reason, explored: advice.explored };
|
|
615
|
+
model = overrideFullId;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch { /* non-blocking */ }
|
|
620
|
+
}
|
|
621
|
+
const hasBothProviders = !!(profile?.providers?.claude?.enabled &&
|
|
622
|
+
profile?.providers?.openai?.enabled);
|
|
623
|
+
const challengerTriggered = shouldTriggerChallenger(workStyle.challengerPolicy, risk, hasBothProviders);
|
|
624
|
+
const legacyDualBrain = !!(detection.designImpact && profile?.dual_brain_enabled !== false);
|
|
625
|
+
let dual = challengerTriggered || legacyDualBrain || shouldDualBrain(detection, profile);
|
|
626
|
+
if (prefSignals.alwaysDualBrain)
|
|
627
|
+
dual = true;
|
|
628
|
+
if (prefSignals.neverDualBrain)
|
|
629
|
+
dual = false;
|
|
630
|
+
if (dual && !hasBothProviders && !legacyDualBrain)
|
|
631
|
+
dual = false;
|
|
632
|
+
const degradedDualBrain = !!(legacyDualBrain && !hasBothProviders);
|
|
633
|
+
const challengerModel = dual ? pickChallengerModel(provider, available) : null;
|
|
634
|
+
const effort = pickEffort(model, detection);
|
|
635
|
+
const modes = pickModes(model, detection);
|
|
636
|
+
const sandbox = pickSandbox(model, detection);
|
|
637
|
+
const decision = {
|
|
638
|
+
provider,
|
|
639
|
+
model,
|
|
640
|
+
effort,
|
|
641
|
+
tier,
|
|
642
|
+
dualBrain: dual,
|
|
643
|
+
...(degradedDualBrain && { degradedDualBrain: true }),
|
|
644
|
+
...(challengerModel && { challengerModel }),
|
|
645
|
+
workStyle: workStyle.key,
|
|
646
|
+
modes,
|
|
647
|
+
sandbox,
|
|
648
|
+
explanation: '',
|
|
649
|
+
_healthScores: healthScores,
|
|
650
|
+
_workStyle: workStyle,
|
|
651
|
+
...(_advisorOverride && { _advisorOverride }),
|
|
652
|
+
};
|
|
653
|
+
decision.explanation = explainDecision(decision, detection, profileWithEffectiveBias);
|
|
654
|
+
const { _healthScores, _workStyle, ...result } = decision;
|
|
655
|
+
return result;
|
|
656
|
+
}
|
|
657
|
+
// ─── Exported: getFailoverOrder ──────────────────────────────────────────────
|
|
658
|
+
export function getFailoverOrder(decision, profile) {
|
|
659
|
+
const { provider: failedProvider, model: failedModel, tier = 'execute' } = decision;
|
|
660
|
+
const available = getAvailableModels(profile);
|
|
661
|
+
const claudeRankByTier = {
|
|
662
|
+
think: ['opus', 'sonnet', 'haiku'],
|
|
663
|
+
execute: ['sonnet', 'opus', 'haiku'],
|
|
664
|
+
search: ['haiku', 'sonnet', 'opus'],
|
|
665
|
+
};
|
|
666
|
+
const openaiRankByTier = {
|
|
667
|
+
think: ['o3', 'gpt-4o', 'gpt-4.1', 'gpt-4.1-mini', 'gpt-4o-mini'],
|
|
668
|
+
execute: ['gpt-4o', 'gpt-4.1', 'o3', 'gpt-4.1-mini', 'gpt-4o-mini'],
|
|
669
|
+
search: ['gpt-4o-mini', 'gpt-4.1-mini', 'gpt-4.1', 'gpt-4o', 'o3'],
|
|
670
|
+
};
|
|
671
|
+
const claudeRank = claudeRankByTier[tier] ?? claudeRankByTier.execute;
|
|
672
|
+
const openaiRank = openaiRankByTier[tier] ?? openaiRankByTier.execute;
|
|
673
|
+
const claudeEnabled = !!(profile?.providers?.claude?.enabled);
|
|
674
|
+
const openaiEnabled = !!(profile?.providers?.openai?.enabled);
|
|
675
|
+
const fallbacks = [];
|
|
676
|
+
if (failedProvider === 'claude') {
|
|
677
|
+
for (const m of claudeRank) {
|
|
678
|
+
if (m === failedModel || !available.claude.includes(m))
|
|
679
|
+
continue;
|
|
680
|
+
fallbacks.push({ provider: 'claude', model: m, label: `Claude ${m}` });
|
|
681
|
+
}
|
|
682
|
+
if (openaiEnabled) {
|
|
683
|
+
for (const m of openaiRank) {
|
|
684
|
+
if (!available.openai.includes(m))
|
|
685
|
+
continue;
|
|
686
|
+
fallbacks.push({ provider: 'openai', model: m, label: `OpenAI ${m}` });
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
for (const m of openaiRank) {
|
|
692
|
+
if (m === failedModel || !available.openai.includes(m))
|
|
693
|
+
continue;
|
|
694
|
+
fallbacks.push({ provider: 'openai', model: m, label: `OpenAI ${m}` });
|
|
695
|
+
}
|
|
696
|
+
if (claudeEnabled) {
|
|
697
|
+
for (const m of claudeRank) {
|
|
698
|
+
if (!available.claude.includes(m))
|
|
699
|
+
continue;
|
|
700
|
+
fallbacks.push({ provider: 'claude', model: m, label: `Claude ${m}` });
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return fallbacks;
|
|
705
|
+
}
|
|
706
|
+
// ─── CLI ──────────────────────────────────────────────────────────────────────
|
|
707
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
|
|
708
|
+
const args = process.argv.slice(2);
|
|
709
|
+
let profilePath, detectionJson, cwd;
|
|
710
|
+
for (let i = 0; i < args.length; i++) {
|
|
711
|
+
if (args[i] === '--profile' && args[i + 1]) {
|
|
712
|
+
profilePath = args[++i];
|
|
713
|
+
}
|
|
714
|
+
if (args[i] === '--detection' && args[i + 1]) {
|
|
715
|
+
detectionJson = args[++i];
|
|
716
|
+
}
|
|
717
|
+
if (args[i] === '--cwd' && args[i + 1]) {
|
|
718
|
+
cwd = args[++i];
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
let profile = {};
|
|
722
|
+
let detection = {};
|
|
723
|
+
if (profilePath) {
|
|
724
|
+
try {
|
|
725
|
+
profile = JSON.parse(readFileSync(profilePath, 'utf8'));
|
|
726
|
+
}
|
|
727
|
+
catch (e) {
|
|
728
|
+
console.error(`Failed to load profile: ${e.message}`);
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (detectionJson) {
|
|
733
|
+
try {
|
|
734
|
+
detection = JSON.parse(detectionJson);
|
|
735
|
+
}
|
|
736
|
+
catch (e) {
|
|
737
|
+
console.error(`Failed to parse detection JSON: ${e.message}`);
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const result = decideRoute({ profile, detection, cwd });
|
|
742
|
+
console.log(JSON.stringify(result, null, 2));
|
|
743
|
+
}
|
|
744
|
+
//# sourceMappingURL=decide.js.map
|