wispy-cli 2.7.7 → 2.7.9
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/core/browser.mjs +327 -0
- package/core/engine.mjs +239 -0
- package/core/subagent-worker.mjs +325 -0
- package/core/subagents.mjs +642 -88
- package/core/task-decomposer.mjs +375 -0
- package/core/task-router.mjs +395 -0
- package/core/tools.mjs +59 -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", "design"],
|
|
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 += costScore(model) * 2; // 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
|
+
}
|
package/core/tools.mjs
CHANGED
|
@@ -275,6 +275,57 @@ export class ToolRegistry {
|
|
|
275
275
|
required: ["id", "message"],
|
|
276
276
|
},
|
|
277
277
|
},
|
|
278
|
+
// ── Browser tools ────────────────────────────────────────────────────────
|
|
279
|
+
{
|
|
280
|
+
name: "browser_status",
|
|
281
|
+
description: "Check browser bridge health and current session status",
|
|
282
|
+
parameters: { type: "object", properties: {} },
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: "browser_tabs",
|
|
286
|
+
description: "List all open browser tabs",
|
|
287
|
+
parameters: {
|
|
288
|
+
type: "object",
|
|
289
|
+
properties: {
|
|
290
|
+
browser: { type: "string", enum: ["safari", "chrome"] },
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
name: "browser_navigate",
|
|
296
|
+
description: "Navigate the active browser tab to a URL",
|
|
297
|
+
parameters: {
|
|
298
|
+
type: "object",
|
|
299
|
+
properties: { url: { type: "string" } },
|
|
300
|
+
required: ["url"],
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: "browser_screenshot",
|
|
305
|
+
description: "Take a screenshot of the active browser tab",
|
|
306
|
+
parameters: { type: "object", properties: {} },
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "browser_front_tab",
|
|
310
|
+
description: "Get info about the currently active browser tab (URL, title)",
|
|
311
|
+
parameters: { type: "object", properties: {} },
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
name: "browser_activate",
|
|
315
|
+
description: "Bring the browser tab to front / focus it",
|
|
316
|
+
parameters: { type: "object", properties: {} },
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: "browser_attach",
|
|
320
|
+
description: "Attach to a browser for control. Auto-selects the best available browser if no args given.",
|
|
321
|
+
parameters: {
|
|
322
|
+
type: "object",
|
|
323
|
+
properties: {
|
|
324
|
+
browser: { type: "string" },
|
|
325
|
+
mode: { type: "string" },
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
278
329
|
];
|
|
279
330
|
|
|
280
331
|
for (const def of builtins) {
|
|
@@ -594,6 +645,14 @@ export class ToolRegistry {
|
|
|
594
645
|
case "get_subagent_result":
|
|
595
646
|
case "kill_subagent":
|
|
596
647
|
case "steer_subagent":
|
|
648
|
+
// Browser tools — handled at engine level
|
|
649
|
+
case "browser_status":
|
|
650
|
+
case "browser_tabs":
|
|
651
|
+
case "browser_navigate":
|
|
652
|
+
case "browser_screenshot":
|
|
653
|
+
case "browser_front_tab":
|
|
654
|
+
case "browser_activate":
|
|
655
|
+
case "browser_attach":
|
|
597
656
|
return { success: false, error: `Tool "${name}" requires engine context. Call via WispyEngine.processMessage().` };
|
|
598
657
|
|
|
599
658
|
default:
|