wispy-cli 2.7.6 → 2.7.8
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/bin/wispy.mjs +91 -16
- package/core/config.mjs +54 -27
- package/core/onboarding.mjs +23 -2
- package/core/subagent-worker.mjs +325 -0
- package/core/subagents.mjs +618 -87
- package/core/task-router.mjs +395 -0
- package/package.json +1 -1
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/task-router.mjs — Smart Model Router for Wispy
|
|
3
|
+
*
|
|
4
|
+
* Decides WHICH model handles WHICH task based on task classification,
|
|
5
|
+
* model capabilities, cost/speed tradeoffs, and available providers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { PROVIDERS, detectProvider } from "./config.mjs";
|
|
9
|
+
|
|
10
|
+
// ── Model capability registry ────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export const MODEL_CAPABILITIES = {
|
|
13
|
+
// Coding specialists
|
|
14
|
+
"gpt-5.4": {
|
|
15
|
+
strengths: ["coding", "debugging", "refactoring"],
|
|
16
|
+
speed: "medium",
|
|
17
|
+
cost: "high",
|
|
18
|
+
contextWindow: 128000,
|
|
19
|
+
provider: "openai",
|
|
20
|
+
},
|
|
21
|
+
"gpt-4o": {
|
|
22
|
+
strengths: ["coding", "analysis", "general"],
|
|
23
|
+
speed: "fast",
|
|
24
|
+
cost: "medium",
|
|
25
|
+
contextWindow: 128000,
|
|
26
|
+
provider: "openai",
|
|
27
|
+
},
|
|
28
|
+
"gpt-4o-mini": {
|
|
29
|
+
strengths: ["coding", "summarization"],
|
|
30
|
+
speed: "very-fast",
|
|
31
|
+
cost: "low",
|
|
32
|
+
contextWindow: 128000,
|
|
33
|
+
provider: "openai",
|
|
34
|
+
},
|
|
35
|
+
"o3-mini": {
|
|
36
|
+
strengths: ["reasoning", "math", "coding"],
|
|
37
|
+
speed: "slow",
|
|
38
|
+
cost: "medium",
|
|
39
|
+
contextWindow: 128000,
|
|
40
|
+
provider: "openai",
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// Claude family
|
|
44
|
+
"claude-opus-4-20250514": {
|
|
45
|
+
strengths: ["architecture", "reasoning", "writing", "analysis"],
|
|
46
|
+
speed: "slow",
|
|
47
|
+
cost: "very-high",
|
|
48
|
+
contextWindow: 200000,
|
|
49
|
+
provider: "anthropic",
|
|
50
|
+
},
|
|
51
|
+
"claude-sonnet-4-20250514": {
|
|
52
|
+
strengths: ["coding", "analysis", "review"],
|
|
53
|
+
speed: "medium",
|
|
54
|
+
cost: "medium",
|
|
55
|
+
contextWindow: 200000,
|
|
56
|
+
provider: "anthropic",
|
|
57
|
+
},
|
|
58
|
+
"claude-3-5-haiku-20241022": {
|
|
59
|
+
strengths: ["summarization", "formatting", "quick-tasks"],
|
|
60
|
+
speed: "very-fast",
|
|
61
|
+
cost: "low",
|
|
62
|
+
contextWindow: 200000,
|
|
63
|
+
provider: "anthropic",
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Gemini family
|
|
67
|
+
"gemini-2.5-pro": {
|
|
68
|
+
strengths: ["research", "analysis", "long-context", "planning"],
|
|
69
|
+
speed: "medium",
|
|
70
|
+
cost: "medium",
|
|
71
|
+
contextWindow: 1000000,
|
|
72
|
+
provider: "google",
|
|
73
|
+
},
|
|
74
|
+
"gemini-2.5-flash": {
|
|
75
|
+
strengths: ["summarization", "quick-tasks", "formatting"],
|
|
76
|
+
speed: "very-fast",
|
|
77
|
+
cost: "very-low",
|
|
78
|
+
contextWindow: 1000000,
|
|
79
|
+
provider: "google",
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Speed / free tier
|
|
83
|
+
"llama-3.3-70b-versatile": {
|
|
84
|
+
strengths: ["general", "summarization"],
|
|
85
|
+
speed: "very-fast",
|
|
86
|
+
cost: "free",
|
|
87
|
+
contextWindow: 32768,
|
|
88
|
+
provider: "groq",
|
|
89
|
+
},
|
|
90
|
+
"deepseek-chat": {
|
|
91
|
+
strengths: ["coding", "reasoning"],
|
|
92
|
+
speed: "fast",
|
|
93
|
+
cost: "very-low",
|
|
94
|
+
contextWindow: 64000,
|
|
95
|
+
provider: "deepseek",
|
|
96
|
+
},
|
|
97
|
+
"deepseek-reasoner": {
|
|
98
|
+
strengths: ["reasoning", "math", "analysis"],
|
|
99
|
+
speed: "slow",
|
|
100
|
+
cost: "low",
|
|
101
|
+
contextWindow: 64000,
|
|
102
|
+
provider: "deepseek",
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// ── Provider → model map (for detecting available models) ────────────────────
|
|
107
|
+
|
|
108
|
+
const PROVIDER_ENV_KEYS = {
|
|
109
|
+
openai: ["OPENAI_API_KEY"],
|
|
110
|
+
anthropic: ["ANTHROPIC_API_KEY"],
|
|
111
|
+
google: ["GOOGLE_AI_KEY", "GOOGLE_GENERATIVE_AI_KEY", "GEMINI_API_KEY"],
|
|
112
|
+
groq: ["GROQ_API_KEY"],
|
|
113
|
+
deepseek: ["DEEPSEEK_API_KEY"],
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
// ── Keyword→task type maps ────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
const TYPE_KEYWORDS = {
|
|
119
|
+
coding: [
|
|
120
|
+
"code", "function", "implement", "bug", "fix", "debug", "refactor", "write a", "create a",
|
|
121
|
+
"class", "method", "variable", "typescript", "javascript", "python", "rust", "go", "java",
|
|
122
|
+
".ts", ".js", ".py", ".rs", ".go", ".java", ".cpp", ".c", "```", "error:", "syntax",
|
|
123
|
+
"compile", "build", "test", "unit test", "integration test",
|
|
124
|
+
],
|
|
125
|
+
research: [
|
|
126
|
+
"research", "find", "search", "look up", "what is", "explain", "describe", "tell me about",
|
|
127
|
+
"history of", "why does", "how does", "what are", "latest", "news", "papers", "sources",
|
|
128
|
+
],
|
|
129
|
+
analysis: [
|
|
130
|
+
"analyze", "analysis", "evaluate", "compare", "contrast", "pros and cons", "review",
|
|
131
|
+
"assess", "benchmark", "performance", "metrics", "statistics", "data", "trend",
|
|
132
|
+
],
|
|
133
|
+
design: [
|
|
134
|
+
"design", "architecture", "system design", "schema", "diagram", "structure", "layout",
|
|
135
|
+
"plan", "blueprint", "mockup", "wireframe",
|
|
136
|
+
],
|
|
137
|
+
review: [
|
|
138
|
+
"review", "check", "verify", "validate", "audit", "security", "issues", "problems",
|
|
139
|
+
"vulnerabilities", "code review", "pull request", "pr review",
|
|
140
|
+
],
|
|
141
|
+
summarize: [
|
|
142
|
+
"summarize", "summary", "tldr", "tl;dr", "recap", "brief", "overview", "key points",
|
|
143
|
+
"shorten", "condense",
|
|
144
|
+
],
|
|
145
|
+
format: [
|
|
146
|
+
"format", "reformat", "prettify", "lint", "clean up", "style", "markdown", "json",
|
|
147
|
+
"yaml", "csv", "table", "list",
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const COMPLEXITY_THRESHOLDS = {
|
|
152
|
+
simple: 150, // < 150 chars → simple
|
|
153
|
+
medium: 600, // < 600 chars → medium
|
|
154
|
+
// above → complex
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// ── Task classification ──────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Classify a task into type, complexity, estimated tokens, and parallelizability.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} taskText
|
|
163
|
+
* @returns {{ type: string, complexity: string, estimatedTokens: number, parallelizable: boolean }}
|
|
164
|
+
*/
|
|
165
|
+
export function classifyTask(taskText) {
|
|
166
|
+
const lower = taskText.toLowerCase();
|
|
167
|
+
|
|
168
|
+
// Determine type via keyword matching
|
|
169
|
+
let bestType = "general";
|
|
170
|
+
let bestScore = 0;
|
|
171
|
+
|
|
172
|
+
for (const [type, keywords] of Object.entries(TYPE_KEYWORDS)) {
|
|
173
|
+
let score = 0;
|
|
174
|
+
for (const kw of keywords) {
|
|
175
|
+
if (lower.includes(kw)) score++;
|
|
176
|
+
}
|
|
177
|
+
if (score > bestScore) {
|
|
178
|
+
bestScore = score;
|
|
179
|
+
bestType = type;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Heuristic overrides
|
|
184
|
+
const hasCodeBlock = taskText.includes("```");
|
|
185
|
+
const hasFileExtension = /\.(ts|js|py|rs|go|java|cpp|c|rb|php|swift|kt)\b/i.test(taskText);
|
|
186
|
+
if (hasCodeBlock || hasFileExtension) {
|
|
187
|
+
bestType = "coding";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Complexity based on text length + heuristics
|
|
191
|
+
let complexity;
|
|
192
|
+
const len = taskText.length;
|
|
193
|
+
const wordCount = taskText.split(/\s+/).length;
|
|
194
|
+
if (len < COMPLEXITY_THRESHOLDS.simple && wordCount < 30) {
|
|
195
|
+
complexity = "simple";
|
|
196
|
+
} else if (len < COMPLEXITY_THRESHOLDS.medium && wordCount < 100) {
|
|
197
|
+
complexity = "medium";
|
|
198
|
+
} else {
|
|
199
|
+
complexity = "complex";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// More complexity signals
|
|
203
|
+
const complexSignals = [
|
|
204
|
+
"multiple", "all", "entire", "full", "complete", "end-to-end", "from scratch",
|
|
205
|
+
"and also", "in addition", "furthermore", "step by step",
|
|
206
|
+
];
|
|
207
|
+
if (complexSignals.some(s => lower.includes(s))) {
|
|
208
|
+
if (complexity === "simple") complexity = "medium";
|
|
209
|
+
else if (complexity === "medium") complexity = "complex";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Estimate tokens (rough: 1 token ≈ 4 chars, plus response overhead)
|
|
213
|
+
const estimatedTokens = Math.ceil(len / 4) + (complexity === "complex" ? 2000 : complexity === "medium" ? 800 : 300);
|
|
214
|
+
|
|
215
|
+
// Parallelizable: tasks that can be split across multiple independent workers
|
|
216
|
+
const parallelSignals = [
|
|
217
|
+
"and", "also", "multiple", "each", "for each", "list of", "all the", "several",
|
|
218
|
+
];
|
|
219
|
+
const parallelizable = complexity === "complex" && parallelSignals.some(s => lower.includes(s));
|
|
220
|
+
|
|
221
|
+
return { type: bestType, complexity, estimatedTokens, parallelizable };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Provider availability detection ─────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Return the set of provider IDs that actually have API keys in env.
|
|
228
|
+
* @returns {Set<string>}
|
|
229
|
+
*/
|
|
230
|
+
export function getAvailableProviders() {
|
|
231
|
+
const available = new Set();
|
|
232
|
+
for (const [provider, envKeys] of Object.entries(PROVIDER_ENV_KEYS)) {
|
|
233
|
+
if (envKeys.some(k => process.env[k])) {
|
|
234
|
+
available.add(provider);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return available;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Filter model list to only those whose provider is available.
|
|
242
|
+
* @param {string[]} modelNames
|
|
243
|
+
* @returns {string[]}
|
|
244
|
+
*/
|
|
245
|
+
export function filterAvailableModels(modelNames) {
|
|
246
|
+
const available = getAvailableProviders();
|
|
247
|
+
return modelNames.filter(m => {
|
|
248
|
+
const cap = MODEL_CAPABILITIES[m];
|
|
249
|
+
return cap && available.has(cap.provider);
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ── Cost/speed ordering helpers ──────────────────────────────────────────────
|
|
254
|
+
|
|
255
|
+
const COST_ORDER = { free: 0, "very-low": 1, low: 2, medium: 3, high: 4, "very-high": 5 };
|
|
256
|
+
const SPEED_ORDER = { "very-fast": 0, fast: 1, medium: 2, slow: 3 };
|
|
257
|
+
|
|
258
|
+
function costScore(model) {
|
|
259
|
+
return COST_ORDER[MODEL_CAPABILITIES[model]?.cost] ?? 3;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function speedScore(model) {
|
|
263
|
+
return SPEED_ORDER[MODEL_CAPABILITIES[model]?.speed] ?? 2;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ── Core routing logic ────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Route a task to the best available model.
|
|
270
|
+
*
|
|
271
|
+
* @param {string|object} task — task text or { type, complexity, estimatedTokens, parallelizable }
|
|
272
|
+
* @param {string[]} [availableModels] — explicit list; if omitted, auto-detected from env
|
|
273
|
+
* @param {object} [opts]
|
|
274
|
+
* @param {string} [opts.costPreference] — "minimize" | "balanced" | "maximize-quality"
|
|
275
|
+
* @param {string} [opts.defaultModel] — fallback model
|
|
276
|
+
* @returns {{ model: string, provider: string, reason: string }}
|
|
277
|
+
*/
|
|
278
|
+
export function routeTask(task, availableModels, opts = {}) {
|
|
279
|
+
const taskText = typeof task === "string" ? task : task.task ?? "";
|
|
280
|
+
const classification = typeof task === "object" && task.type
|
|
281
|
+
? task
|
|
282
|
+
: classifyTask(taskText);
|
|
283
|
+
|
|
284
|
+
const { type, complexity, estimatedTokens } = classification;
|
|
285
|
+
const costPreference = opts.costPreference ?? "balanced";
|
|
286
|
+
|
|
287
|
+
// Determine candidate models
|
|
288
|
+
let candidates = availableModels;
|
|
289
|
+
if (!candidates || candidates.length === 0) {
|
|
290
|
+
candidates = filterAvailableModels(Object.keys(MODEL_CAPABILITIES));
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Filter to models that have valid capabilities
|
|
294
|
+
candidates = candidates.filter(m => MODEL_CAPABILITIES[m]);
|
|
295
|
+
|
|
296
|
+
if (candidates.length === 0) {
|
|
297
|
+
// No models available — fall back to default
|
|
298
|
+
const fallback = opts.defaultModel ?? "gemini-2.5-flash";
|
|
299
|
+
const cap = MODEL_CAPABILITIES[fallback];
|
|
300
|
+
return {
|
|
301
|
+
model: fallback,
|
|
302
|
+
provider: cap?.provider ?? "google",
|
|
303
|
+
reason: `No models available; using default (${fallback})`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Score each candidate
|
|
308
|
+
const scored = candidates.map(model => {
|
|
309
|
+
const cap = MODEL_CAPABILITIES[model];
|
|
310
|
+
if (!cap) return { model, score: -Infinity };
|
|
311
|
+
|
|
312
|
+
let score = 0;
|
|
313
|
+
|
|
314
|
+
// Strength match
|
|
315
|
+
const strengthScore = cap.strengths.includes(type) ? 10 : 0;
|
|
316
|
+
score += strengthScore;
|
|
317
|
+
|
|
318
|
+
// Partial strength match (related types)
|
|
319
|
+
const related = { coding: ["analysis", "review"], research: ["analysis"], design: ["analysis", "planning"] };
|
|
320
|
+
const relatedStrengths = related[type] ?? [];
|
|
321
|
+
for (const rs of relatedStrengths) {
|
|
322
|
+
if (cap.strengths.includes(rs)) score += 3;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Complexity × cost/speed tradeoff
|
|
326
|
+
if (costPreference === "minimize") {
|
|
327
|
+
score -= costScore(model) * 2;
|
|
328
|
+
score -= speedScore(model);
|
|
329
|
+
} else if (costPreference === "maximize-quality") {
|
|
330
|
+
score += (5 - costScore(model)); // prefer expensive (high quality)
|
|
331
|
+
score -= speedScore(model) * 0.5;
|
|
332
|
+
} else {
|
|
333
|
+
// balanced: for complex tasks lean toward quality, simple tasks lean toward speed+cost
|
|
334
|
+
if (complexity === "complex") {
|
|
335
|
+
score -= costScore(model);
|
|
336
|
+
score -= speedScore(model) * 0.5;
|
|
337
|
+
} else if (complexity === "simple") {
|
|
338
|
+
score -= costScore(model) * 2;
|
|
339
|
+
score += (3 - speedScore(model)) * 1.5;
|
|
340
|
+
} else {
|
|
341
|
+
score -= costScore(model);
|
|
342
|
+
score -= speedScore(model);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Context window bonus for large tasks
|
|
347
|
+
if (estimatedTokens > 50000 && cap.contextWindow >= 200000) {
|
|
348
|
+
score += 3;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return { model, score, cap };
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
scored.sort((a, b) => b.score - a.score);
|
|
355
|
+
const best = scored[0];
|
|
356
|
+
const cap = best.cap ?? MODEL_CAPABILITIES[best.model];
|
|
357
|
+
|
|
358
|
+
const strengthMatch = cap?.strengths.includes(type)
|
|
359
|
+
? `strengths match '${type}'`
|
|
360
|
+
: `best available for '${type}'`;
|
|
361
|
+
|
|
362
|
+
const reason = `${best.model} selected: ${strengthMatch}, ${cap?.speed ?? "?"} speed, ${cap?.cost ?? "?"} cost (complexity: ${complexity}, preference: ${costPreference})`;
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
model: best.model,
|
|
366
|
+
provider: cap?.provider ?? "unknown",
|
|
367
|
+
reason,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Given a provider name, return the best cheap/fast decomposition model available.
|
|
373
|
+
* Used by the task decomposer.
|
|
374
|
+
* @returns {{ model: string, provider: string }}
|
|
375
|
+
*/
|
|
376
|
+
export function getCheapDecomposerModel() {
|
|
377
|
+
const available = getAvailableProviders();
|
|
378
|
+
const cheapModels = [
|
|
379
|
+
{ model: "gemini-2.5-flash", provider: "google" },
|
|
380
|
+
{ model: "claude-3-5-haiku-20241022", provider: "anthropic" },
|
|
381
|
+
{ model: "gpt-4o-mini", provider: "openai" },
|
|
382
|
+
{ model: "llama-3.3-70b-versatile", provider: "groq" },
|
|
383
|
+
{ model: "deepseek-chat", provider: "deepseek" },
|
|
384
|
+
];
|
|
385
|
+
for (const entry of cheapModels) {
|
|
386
|
+
if (available.has(entry.provider)) return entry;
|
|
387
|
+
}
|
|
388
|
+
// Final fallback — return first available model
|
|
389
|
+
const allCandidates = filterAvailableModels(Object.keys(MODEL_CAPABILITIES));
|
|
390
|
+
if (allCandidates.length > 0) {
|
|
391
|
+
const m = allCandidates[0];
|
|
392
|
+
return { model: m, provider: MODEL_CAPABILITIES[m].provider };
|
|
393
|
+
}
|
|
394
|
+
return { model: "gemini-2.5-flash", provider: "google" };
|
|
395
|
+
}
|