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 CHANGED
@@ -265,27 +265,102 @@ if (command === "handoff") {
265
265
  if (command === "model") {
266
266
  try {
267
267
  const { loadConfig, saveConfig, detectProvider, PROVIDERS } = await import(join(rootDir, "core/config.mjs"));
268
- const config = await loadConfig();
269
- const detected = await detectProvider();
270
268
 
271
- if (args[1]) {
272
- config.model = args[1];
273
- await saveConfig(config);
274
- console.log(`Model set to: ${args[1]}`);
275
- } else {
276
- console.log(`\n Provider: ${detected?.provider ?? "none"}`);
277
- console.log(` Model: ${detected?.model ?? "none"}`);
278
- console.log(`\n Override: wispy model <model-name>`);
279
- console.log(` Env var: WISPY_MODEL=<model-name>`);
280
-
281
- if (PROVIDERS) {
282
- console.log(`\n Available providers:`);
283
- for (const [key, p] of Object.entries(PROVIDERS)) {
284
- console.log(` ${key.padEnd(14)} ${p.defaultModel}`);
269
+ const sub = args[1];
270
+
271
+ // wispy model list → print all providers and their models
272
+ if (sub === "list") {
273
+ console.log("\n Available models by provider:\n");
274
+ for (const [key, p] of Object.entries(PROVIDERS)) {
275
+ console.log(` ${key}`);
276
+ const models = p.models ?? [p.defaultModel];
277
+ for (const m of models) {
278
+ const tag = m === p.defaultModel ? " (default)" : "";
279
+ console.log(` ${m}${tag}`);
285
280
  }
286
281
  }
287
282
  console.log("");
283
+ process.exit(0);
288
284
  }
285
+
286
+ // wispy model <name> → set directly
287
+ if (sub && sub !== "list") {
288
+ const config = await loadConfig();
289
+ config.model = sub;
290
+ await saveConfig(config);
291
+ console.log(`Model set to: ${sub}`);
292
+ process.exit(0);
293
+ }
294
+
295
+ // wispy model (no args) → show current + interactive picker
296
+ const { select } = await import("@inquirer/prompts");
297
+ const config = await loadConfig();
298
+ const detected = await detectProvider();
299
+
300
+ console.log(`\n Current provider: ${detected?.provider ?? "none"}`);
301
+ console.log(` Current model: ${detected?.model ?? "none"}\n`);
302
+
303
+ // Find providers with configured keys (or local providers)
304
+ const availableProviders = Object.entries(PROVIDERS).filter(([key, p]) => {
305
+ if (p.local) return true;
306
+ // Check env keys
307
+ for (const k of (p.envKeys ?? [])) {
308
+ if (process.env[k]) return true;
309
+ }
310
+ // Check config
311
+ if (config.provider === key && config.apiKey) return true;
312
+ if (config.providers?.[key]) return true;
313
+ return false;
314
+ });
315
+
316
+ let providerChoices = availableProviders.map(([key, p]) => ({
317
+ name: p.label ?? key,
318
+ value: key,
319
+ }));
320
+
321
+ // If no providers detected, show all
322
+ if (providerChoices.length === 0) {
323
+ providerChoices = Object.entries(PROVIDERS).map(([key, p]) => ({
324
+ name: p.label ?? key,
325
+ value: key,
326
+ }));
327
+ }
328
+
329
+ let selectedProvider;
330
+ try {
331
+ selectedProvider = await select({
332
+ message: "Select provider:",
333
+ choices: providerChoices,
334
+ default: detected?.provider,
335
+ });
336
+ } catch {
337
+ console.log(" Cancelled.");
338
+ process.exit(0);
339
+ }
340
+
341
+ const providerInfo = PROVIDERS[selectedProvider];
342
+ const modelList = providerInfo.models ?? [providerInfo.defaultModel];
343
+
344
+ let selectedModel;
345
+ try {
346
+ selectedModel = await select({
347
+ message: "Select model:",
348
+ choices: modelList.map((m) => ({
349
+ name: m === providerInfo.defaultModel ? `${m} (default)` : m,
350
+ value: m,
351
+ })),
352
+ default: providerInfo.defaultModel,
353
+ });
354
+ } catch {
355
+ console.log(" Cancelled.");
356
+ process.exit(0);
357
+ }
358
+
359
+ config.provider = selectedProvider;
360
+ config.model = selectedModel;
361
+ await saveConfig(config);
362
+ console.log(`\n Provider set to: ${selectedProvider}`);
363
+ console.log(` Model set to: ${selectedModel}\n`);
289
364
  } catch (err) {
290
365
  console.error("Model error:", err.message);
291
366
  process.exit(1);
package/core/config.mjs CHANGED
@@ -19,47 +19,74 @@ export const MEMORY_DIR = path.join(WISPY_DIR, "memory");
19
19
 
20
20
  export const PROVIDERS = {
21
21
  // ── Tier 0: Custom API providers ────────────────────────────────────────────
22
- google: { envKeys: ["GOOGLE_AI_KEY", "GOOGLE_GENERATIVE_AI_KEY", "GEMINI_API_KEY"], defaultModel: "gemini-2.5-flash", label: "Google AI (Gemini)", signupUrl: "https://aistudio.google.com/apikey" },
23
- anthropic: { envKeys: ["ANTHROPIC_API_KEY"], defaultModel: "claude-sonnet-4-20250514", label: "Anthropic (Claude)", signupUrl: "https://console.anthropic.com/settings/keys" },
22
+ google: { envKeys: ["GOOGLE_AI_KEY", "GOOGLE_GENERATIVE_AI_KEY", "GEMINI_API_KEY"], defaultModel: "gemini-2.5-flash", label: "Google AI (Gemini)", signupUrl: "https://aistudio.google.com/apikey",
23
+ models: ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash", "gemini-2.0-flash-lite", "gemini-1.5-pro", "gemini-1.5-flash"] },
24
+ anthropic: { envKeys: ["ANTHROPIC_API_KEY"], defaultModel: "claude-sonnet-4-20250514", label: "Anthropic (Claude)", signupUrl: "https://console.anthropic.com/settings/keys",
25
+ models: ["claude-opus-4-20250514", "claude-sonnet-4-20250514", "claude-3-5-sonnet-20241022", "claude-3-5-haiku-20241022", "claude-3-opus-20240229"] },
24
26
 
25
27
  // ── Tier 1: Popular OpenAI-compat ──────────────────────────────────────────
26
- openai: { envKeys: ["OPENAI_API_KEY"], defaultModel: "gpt-4o", label: "OpenAI", signupUrl: "https://platform.openai.com/api-keys" },
27
- xai: { envKeys: ["XAI_API_KEY"], defaultModel: "grok-3-mini", label: "xAI (Grok)", signupUrl: "https://console.x.ai/" },
28
- mistral: { envKeys: ["MISTRAL_API_KEY"], defaultModel: "mistral-large-latest", label: "Mistral", signupUrl: "https://console.mistral.ai/api-keys/" },
29
- together: { envKeys: ["TOGETHER_API_KEY"], defaultModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo", label: "Together AI", signupUrl: "https://api.together.xyz/settings/api-keys" },
30
- nvidia: { envKeys: ["NVIDIA_API_KEY"], defaultModel: "meta/llama-3.3-70b-instruct", label: "NVIDIA NIM", signupUrl: "https://build.nvidia.com/" },
28
+ openai: { envKeys: ["OPENAI_API_KEY"], defaultModel: "gpt-4o", label: "OpenAI", signupUrl: "https://platform.openai.com/api-keys",
29
+ models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "o1", "o1-mini", "o3", "o3-mini", "o4-mini"] },
30
+ xai: { envKeys: ["XAI_API_KEY"], defaultModel: "grok-3-mini", label: "xAI (Grok)", signupUrl: "https://console.x.ai/",
31
+ models: ["grok-3", "grok-3-mini", "grok-3-fast", "grok-2", "grok-2-mini"] },
32
+ mistral: { envKeys: ["MISTRAL_API_KEY"], defaultModel: "mistral-large-latest", label: "Mistral", signupUrl: "https://console.mistral.ai/api-keys/",
33
+ models: ["mistral-large-latest", "mistral-medium-latest", "mistral-small-latest", "codestral-latest", "open-mistral-nemo"] },
34
+ together: { envKeys: ["TOGETHER_API_KEY"], defaultModel: "meta-llama/Llama-3.3-70B-Instruct-Turbo", label: "Together AI", signupUrl: "https://api.together.xyz/settings/api-keys",
35
+ models: ["meta-llama/Llama-3.3-70B-Instruct-Turbo", "meta-llama/Llama-3.1-8B-Instruct-Turbo", "mistralai/Mixtral-8x22B-Instruct-v0.1", "Qwen/Qwen2.5-72B-Instruct-Turbo", "deepseek-ai/deepseek-r1"] },
36
+ nvidia: { envKeys: ["NVIDIA_API_KEY"], defaultModel: "meta/llama-3.3-70b-instruct", label: "NVIDIA NIM", signupUrl: "https://build.nvidia.com/",
37
+ models: ["meta/llama-3.3-70b-instruct", "meta/llama-3.1-8b-instruct", "mistralai/mistral-7b-instruct-v0.3", "google/gemma-2-9b-it"] },
31
38
 
32
39
  // ── Tier 1: Fast/free ───────────────────────────────────────────────────────
33
- groq: { envKeys: ["GROQ_API_KEY"], defaultModel: "llama-3.3-70b-versatile", label: "Groq (fast inference)", signupUrl: "https://console.groq.com/keys" },
34
- deepseek: { envKeys: ["DEEPSEEK_API_KEY"], defaultModel: "deepseek-chat", label: "DeepSeek", signupUrl: "https://platform.deepseek.com/api_keys" },
35
- chutes: { envKeys: ["CHUTES_API_KEY"], defaultModel: "deepseek-ai/DeepSeek-V3-0324", label: "Chutes", signupUrl: "https://chutes.ai/" },
40
+ groq: { envKeys: ["GROQ_API_KEY"], defaultModel: "llama-3.3-70b-versatile", label: "Groq (fast inference)", signupUrl: "https://console.groq.com/keys",
41
+ models: ["llama-3.3-70b-versatile", "llama-3.1-8b-instant", "mixtral-8x7b-32768", "gemma2-9b-it", "llama-3.3-70b-specdec"] },
42
+ deepseek: { envKeys: ["DEEPSEEK_API_KEY"], defaultModel: "deepseek-chat", label: "DeepSeek", signupUrl: "https://platform.deepseek.com/api_keys",
43
+ models: ["deepseek-chat", "deepseek-coder", "deepseek-reasoner"] },
44
+ chutes: { envKeys: ["CHUTES_API_KEY"], defaultModel: "deepseek-ai/DeepSeek-V3-0324", label: "Chutes", signupUrl: "https://chutes.ai/",
45
+ models: ["deepseek-ai/DeepSeek-V3-0324", "deepseek-ai/DeepSeek-R1", "Qwen/Qwen3-235B-A22B"] },
36
46
 
37
47
  // ── Tier 1: Aggregators ─────────────────────────────────────────────────────
38
- openrouter: { envKeys: ["OPENROUTER_API_KEY"], defaultModel: "anthropic/claude-sonnet-4-20250514", label: "OpenRouter (multi-model)", signupUrl: "https://openrouter.ai/keys" },
39
- vercelai: { envKeys: ["VERCEL_AI_TOKEN"], defaultModel: "anthropic/claude-3-5-sonnet", label: "Vercel AI Gateway", signupUrl: "https://vercel.com/docs/ai-gateway" },
48
+ openrouter: { envKeys: ["OPENROUTER_API_KEY"], defaultModel: "anthropic/claude-sonnet-4-20250514", label: "OpenRouter (multi-model)", signupUrl: "https://openrouter.ai/keys",
49
+ models: ["anthropic/claude-sonnet-4-20250514", "anthropic/claude-opus-4", "openai/gpt-4o", "openai/o3", "google/gemini-2.5-pro", "google/gemini-2.5-flash", "meta-llama/llama-3.3-70b-instruct", "deepseek/deepseek-r1"] },
50
+ vercelai: { envKeys: ["VERCEL_AI_TOKEN"], defaultModel: "anthropic/claude-3-5-sonnet", label: "Vercel AI Gateway", signupUrl: "https://vercel.com/docs/ai-gateway",
51
+ models: ["anthropic/claude-3-5-sonnet", "anthropic/claude-3-5-haiku", "openai/gpt-4o", "openai/gpt-4o-mini"] },
40
52
 
41
53
  // ── Tier 2: Useful ──────────────────────────────────────────────────────────
42
- kimi: { envKeys: ["MOONSHOT_API_KEY", "KIMI_API_KEY"], defaultModel: "moonshot-v1-8k", label: "Kimi/Moonshot", signupUrl: "https://platform.moonshot.cn/console/api-keys" },
43
- minimax: { envKeys: ["MINIMAX_API_KEY"], defaultModel: "MiniMax-Text-01", label: "MiniMax", signupUrl: "https://www.minimaxi.com/" },
44
- venice: { envKeys: ["VENICE_API_KEY"], defaultModel: "llama-3.3-70b", label: "Venice AI", signupUrl: "https://venice.ai/chat/api" },
45
- huggingface: { envKeys: ["HF_TOKEN", "HUGGINGFACE_API_KEY"], defaultModel: "meta-llama/Llama-3.3-70B-Instruct", label: "Hugging Face", signupUrl: "https://huggingface.co/settings/tokens", special: "hf_model_in_url" },
46
- cloudflare: { envKeys: ["CF_API_TOKEN"], defaultModel: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", label: "Cloudflare AI", signupUrl: "https://dash.cloudflare.com/", special: "cf_account_id" },
54
+ kimi: { envKeys: ["MOONSHOT_API_KEY", "KIMI_API_KEY"], defaultModel: "moonshot-v1-8k", label: "Kimi/Moonshot", signupUrl: "https://platform.moonshot.cn/console/api-keys",
55
+ models: ["moonshot-v1-8k", "moonshot-v1-32k", "moonshot-v1-128k"] },
56
+ minimax: { envKeys: ["MINIMAX_API_KEY"], defaultModel: "MiniMax-Text-01", label: "MiniMax", signupUrl: "https://www.minimaxi.com/",
57
+ models: ["MiniMax-Text-01", "abab6.5s-chat", "abab6.5g-chat"] },
58
+ venice: { envKeys: ["VENICE_API_KEY"], defaultModel: "llama-3.3-70b", label: "Venice AI", signupUrl: "https://venice.ai/chat/api",
59
+ models: ["llama-3.3-70b", "mistral-31-24b", "deepseek-r1-671b"] },
60
+ huggingface: { envKeys: ["HF_TOKEN", "HUGGINGFACE_API_KEY"], defaultModel: "meta-llama/Llama-3.3-70B-Instruct", label: "Hugging Face", signupUrl: "https://huggingface.co/settings/tokens", special: "hf_model_in_url",
61
+ models: ["meta-llama/Llama-3.3-70B-Instruct", "meta-llama/Llama-3.1-8B-Instruct", "mistralai/Mistral-7B-Instruct-v0.3", "Qwen/Qwen2.5-72B-Instruct"] },
62
+ cloudflare: { envKeys: ["CF_API_TOKEN"], defaultModel: "@cf/meta/llama-3.3-70b-instruct-fp8-fast", label: "Cloudflare AI", signupUrl: "https://dash.cloudflare.com/", special: "cf_account_id",
63
+ models: ["@cf/meta/llama-3.3-70b-instruct-fp8-fast", "@cf/mistral/mistral-7b-instruct-v0.2", "@cf/google/gemma-7b-it", "@cf/qwen/qwen1.5-14b-chat-awq"] },
47
64
 
48
65
  // ── Tier 3: Regional / niche ────────────────────────────────────────────────
49
- volcengine: { envKeys: ["VOLCENGINE_API_KEY", "ARK_API_KEY"], defaultModel: "doubao-pro-32k", label: "Volcengine (ByteDance)", signupUrl: "https://console.volcengine.com/ark" },
50
- byteplus: { envKeys: ["BYTEPLUS_API_KEY"], defaultModel: "doubao-pro-32k", label: "BytePlus", signupUrl: "https://console.byteplux.com/" },
51
- zai: { envKeys: ["ZAI_API_KEY", "GLM_API_KEY", "ZHIPU_API_KEY"], defaultModel: "glm-4-flash", label: "Z.AI / GLM", signupUrl: "https://open.bigmodel.cn/" },
52
- dashscope: { envKeys: ["DASHSCOPE_API_KEY"], defaultModel: "qwen-max", label: "DashScope (Alibaba)", signupUrl: "https://dashscope.aliyun.com/" },
53
- xiaomi: { envKeys: ["XIAOMI_API_KEY"], defaultModel: "MiMo-7B-RL", label: "Xiaomi AI", signupUrl: "https://ai.xiaomi.com/" },
66
+ volcengine: { envKeys: ["VOLCENGINE_API_KEY", "ARK_API_KEY"], defaultModel: "doubao-pro-32k", label: "Volcengine (ByteDance)", signupUrl: "https://console.volcengine.com/ark",
67
+ models: ["doubao-pro-32k", "doubao-pro-128k", "doubao-lite-32k"] },
68
+ byteplus: { envKeys: ["BYTEPLUS_API_KEY"], defaultModel: "doubao-pro-32k", label: "BytePlus", signupUrl: "https://console.byteplux.com/",
69
+ models: ["doubao-pro-32k", "doubao-lite-32k"] },
70
+ zai: { envKeys: ["ZAI_API_KEY", "GLM_API_KEY", "ZHIPU_API_KEY"], defaultModel: "glm-4-flash", label: "Z.AI / GLM", signupUrl: "https://open.bigmodel.cn/",
71
+ models: ["glm-4-flash", "glm-4-plus", "glm-4-long", "glm-z1-flash"] },
72
+ dashscope: { envKeys: ["DASHSCOPE_API_KEY"], defaultModel: "qwen-max", label: "DashScope (Alibaba)", signupUrl: "https://dashscope.aliyun.com/",
73
+ models: ["qwen-max", "qwen-plus", "qwen-turbo", "qwen-long", "qwen2.5-72b-instruct"] },
74
+ xiaomi: { envKeys: ["XIAOMI_API_KEY"], defaultModel: "MiMo-7B-RL", label: "Xiaomi AI", signupUrl: "https://ai.xiaomi.com/",
75
+ models: ["MiMo-7B-RL", "MiMo-7B-SFT"] },
54
76
 
55
77
  // ── OAuth / subscription-based ────────────────────────────────────────────
56
- "github-copilot": { envKeys: ["GITHUB_COPILOT_TOKEN"], defaultModel: "gpt-4o", label: "GitHub Copilot", signupUrl: "https://github.com/features/copilot", auth: "oauth" },
78
+ "github-copilot": { envKeys: ["GITHUB_COPILOT_TOKEN"], defaultModel: "gpt-4o", label: "GitHub Copilot", signupUrl: "https://github.com/features/copilot", auth: "oauth",
79
+ models: ["gpt-4o", "gpt-4o-mini", "claude-sonnet-4-20250514", "o3-mini"] },
57
80
 
58
81
  // ── Local / self-hosted ─────────────────────────────────────────────────────
59
- ollama: { envKeys: [], defaultModel: "llama3.2", label: "Ollama (local)", signupUrl: null, local: true },
60
- vllm: { envKeys: [], defaultModel: "meta-llama/Llama-3.3-70B-Instruct", label: "vLLM (self-hosted)", signupUrl: "https://docs.vllm.ai/", local: true },
61
- sglang: { envKeys: [], defaultModel: "meta-llama/Llama-3.3-70B-Instruct", label: "SGLang (self-hosted)", signupUrl: "https://docs.sglang.ai/", local: true },
62
- litellm: { envKeys: ["LITELLM_API_KEY"], defaultModel: "gpt-4o", label: "LiteLLM (proxy)", signupUrl: "https://docs.litellm.ai/", local: true },
82
+ ollama: { envKeys: [], defaultModel: "llama3.2", label: "Ollama (local)", signupUrl: null, local: true,
83
+ models: ["llama3.2", "llama3.1", "llama3.1:70b", "mistral", "mixtral", "deepseek-r1", "qwen2.5", "phi4", "gemma3"] },
84
+ vllm: { envKeys: [], defaultModel: "meta-llama/Llama-3.3-70B-Instruct", label: "vLLM (self-hosted)", signupUrl: "https://docs.vllm.ai/", local: true,
85
+ models: ["meta-llama/Llama-3.3-70B-Instruct", "meta-llama/Llama-3.1-8B-Instruct", "mistralai/Mistral-7B-Instruct-v0.3", "Qwen/Qwen2.5-72B-Instruct"] },
86
+ sglang: { envKeys: [], defaultModel: "meta-llama/Llama-3.3-70B-Instruct", label: "SGLang (self-hosted)", signupUrl: "https://docs.sglang.ai/", local: true,
87
+ models: ["meta-llama/Llama-3.3-70B-Instruct", "meta-llama/Llama-3.1-8B-Instruct", "deepseek-ai/DeepSeek-V3", "Qwen/Qwen2.5-72B-Instruct"] },
88
+ litellm: { envKeys: ["LITELLM_API_KEY"], defaultModel: "gpt-4o", label: "LiteLLM (proxy)", signupUrl: "https://docs.litellm.ai/", local: true,
89
+ models: ["gpt-4o", "claude-sonnet-4-20250514", "gemini/gemini-2.5-flash", "bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0"] },
63
90
  };
64
91
 
65
92
  async function tryKeychainKey(service) {
@@ -569,10 +569,31 @@ export class OnboardingWizard {
569
569
  const result = await validateApiKey(provKey, apiKey.trim());
570
570
 
571
571
  if (result.ok) {
572
- console.log(green(` ✅ Connected! (${result.model ?? getProviderDefaultModel(provKey)})`));
572
+ console.log(green(` ✅ Connected!`));
573
+
574
+ // Model selection step
575
+ const provInfo = PROVIDERS[provKey];
576
+ const modelList = provInfo?.models ?? [getProviderDefaultModel(provKey)];
577
+ let chosenModel = result.model ?? getProviderDefaultModel(provKey);
578
+ if (modelList.length > 1) {
579
+ try {
580
+ chosenModel = await select({
581
+ message: ` Select model for ${info?.label ?? provKey}:`,
582
+ choices: modelList.map((m) => ({
583
+ name: m === getProviderDefaultModel(provKey) ? `${m} (default)` : m,
584
+ value: m,
585
+ })),
586
+ default: chosenModel,
587
+ });
588
+ } catch {
589
+ // keep default
590
+ }
591
+ }
592
+ console.log(dim(` Using model: ${chosenModel}`));
593
+
573
594
  providers[provKey] = {
574
595
  apiKey: apiKey.trim(),
575
- model: result.model ?? getProviderDefaultModel(provKey),
596
+ model: chosenModel,
576
597
  };
577
598
  validated = true;
578
599
  } else {
@@ -0,0 +1,325 @@
1
+ /**
2
+ * core/subagent-worker.mjs — Worker thread code for sub-agent isolation
3
+ *
4
+ * Runs inside a worker_threads Worker. Receives task/config via workerData,
5
+ * communicates back to the main thread via parentPort.postMessage().
6
+ *
7
+ * Message protocol (worker → main):
8
+ * { type: 'progress', agentId, round, content }
9
+ * { type: 'tool_call', agentId, round, call: { name, args, id } }
10
+ * { type: 'tool_result', agentId, round, toolName, result }
11
+ * { type: 'completed', agentId, result }
12
+ * { type: 'failed', agentId, error }
13
+ *
14
+ * Message protocol (main → worker):
15
+ * { type: 'tool_result', callId, result }
16
+ * { type: 'steer', message }
17
+ * { type: 'kill' }
18
+ */
19
+
20
+ import { workerData, parentPort } from "node:worker_threads";
21
+
22
+ if (!parentPort) {
23
+ throw new Error("subagent-worker.mjs must be run as a Worker thread");
24
+ }
25
+
26
+ const { agentId, task, systemPrompt, model, timeout, providerConfig, toolDefs } = workerData;
27
+
28
+ const MAX_ROUNDS = 30;
29
+ const TOKEN_LIMIT = 128_000;
30
+ const COMPACT_THRESHOLD = 0.8; // compact at 80%
31
+
32
+ /** Estimate token count as chars/4 */
33
+ function estimateTokens(text) {
34
+ return Math.ceil((text?.length ?? 0) / 4);
35
+ }
36
+
37
+ function estimateMessages(msgs) {
38
+ return msgs.reduce((sum, m) => {
39
+ const content = m.content ?? JSON.stringify(m);
40
+ return sum + estimateTokens(content);
41
+ }, 0);
42
+ }
43
+
44
+ /** Pending tool call resolvers: callId → { resolve, reject } */
45
+ const pendingToolCalls = new Map();
46
+ /** Pending steer messages */
47
+ const steerMessages = [];
48
+ let killed = false;
49
+
50
+ // Listen for messages from the main thread
51
+ parentPort.on("message", (msg) => {
52
+ if (msg.type === "tool_result") {
53
+ const pending = pendingToolCalls.get(msg.callId);
54
+ if (pending) {
55
+ pendingToolCalls.delete(msg.callId);
56
+ if (msg.error) {
57
+ pending.reject(new Error(msg.error));
58
+ } else {
59
+ pending.resolve(msg.result);
60
+ }
61
+ }
62
+ } else if (msg.type === "steer") {
63
+ steerMessages.push(msg.message);
64
+ } else if (msg.type === "kill") {
65
+ killed = true;
66
+ }
67
+ });
68
+
69
+ /**
70
+ * Request a tool call from the main thread.
71
+ * Returns a Promise that resolves when the main thread sends back the result.
72
+ */
73
+ function requestToolCall(call, round) {
74
+ return new Promise((resolve, reject) => {
75
+ const callId = call.id ?? `${call.name}-${Date.now()}`;
76
+ pendingToolCalls.set(callId, { resolve, reject });
77
+
78
+ parentPort.postMessage({
79
+ type: "tool_call",
80
+ agentId,
81
+ round,
82
+ call: { name: call.name, args: call.args, id: callId },
83
+ });
84
+
85
+ // Tool timeout: 60s
86
+ setTimeout(() => {
87
+ if (pendingToolCalls.has(callId)) {
88
+ pendingToolCalls.delete(callId);
89
+ reject(new Error(`Tool '${call.name}' timed out in worker`));
90
+ }
91
+ }, 60_000);
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Compact messages when approaching token limit.
97
+ * Keep system prompt + last 3 rounds, summarize the middle.
98
+ *
99
+ * @param {Array} messages
100
+ * @returns {Array} compacted messages
101
+ */
102
+ function compactMessages(messages) {
103
+ const system = messages.filter(m => m.role === "system");
104
+ const nonSystem = messages.filter(m => m.role !== "system");
105
+
106
+ // Keep last 6 messages (≈3 rounds of user+assistant)
107
+ const keepTail = nonSystem.slice(-6);
108
+ const toSummarize = nonSystem.slice(0, -6);
109
+
110
+ if (toSummarize.length === 0) {
111
+ return messages; // Nothing to compact
112
+ }
113
+
114
+ const summaryContent = toSummarize
115
+ .filter(m => m.role === "user" || m.role === "assistant")
116
+ .map(m => `[${m.role}]: ${(m.content ?? "").slice(0, 300)}`)
117
+ .join("\n");
118
+
119
+ const summaryMsg = {
120
+ role: "user",
121
+ content: `[Context summary from earlier in this conversation]\n${summaryContent}\n[End of summary]`,
122
+ };
123
+
124
+ return [...system, summaryMsg, ...keepTail];
125
+ }
126
+
127
+ /**
128
+ * Make an HTTP call to the provider API.
129
+ * Uses the providerConfig passed from the main thread.
130
+ */
131
+ async function callProvider(messages) {
132
+ const { provider, apiKey, model: configModel, endpoint } = providerConfig;
133
+ const useModel = model ?? configModel;
134
+
135
+ // Build request based on provider
136
+ if (provider === "anthropic") {
137
+ const systemMsg = messages.find(m => m.role === "system")?.content ?? "";
138
+ const nonSystem = messages.filter(m => m.role !== "system");
139
+
140
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
141
+ method: "POST",
142
+ headers: {
143
+ "Content-Type": "application/json",
144
+ "x-api-key": apiKey,
145
+ "anthropic-version": "2023-06-01",
146
+ },
147
+ body: JSON.stringify({
148
+ model: useModel ?? "claude-3-5-haiku-20241022",
149
+ max_tokens: 4096,
150
+ system: systemMsg,
151
+ messages: nonSystem.map(m => ({
152
+ role: m.role === "tool_result" ? "user" : m.role,
153
+ content: m.role === "tool_result"
154
+ ? [{ type: "tool_result", tool_use_id: m.toolUseId, content: JSON.stringify(m.result) }]
155
+ : m.role === "assistant" && m.toolCalls?.length
156
+ ? m.toolCalls.map(tc => ({ type: "tool_use", id: tc.id, name: tc.name, input: tc.args ?? tc.input ?? {} }))
157
+ : [{ type: "text", text: m.content ?? "" }],
158
+ })),
159
+ tools: (toolDefs ?? []).map(t => ({
160
+ name: t.name,
161
+ description: t.description,
162
+ input_schema: t.parameters,
163
+ })),
164
+ }),
165
+ });
166
+
167
+ const data = await response.json();
168
+ if (!response.ok) throw new Error(data.error?.message ?? `API error ${response.status}`);
169
+
170
+ const textBlock = data.content?.find(b => b.type === "text");
171
+ const toolUseBlocks = data.content?.filter(b => b.type === "tool_use") ?? [];
172
+
173
+ if (toolUseBlocks.length > 0) {
174
+ return {
175
+ type: "tool_calls",
176
+ calls: toolUseBlocks.map(b => ({ id: b.id, name: b.name, args: b.input })),
177
+ };
178
+ }
179
+ return { type: "text", text: textBlock?.text ?? "" };
180
+ }
181
+
182
+ // Default: OpenAI-compat
183
+ const url = endpoint ?? "https://api.openai.com/v1/chat/completions";
184
+ const response = await fetch(url, {
185
+ method: "POST",
186
+ headers: {
187
+ "Content-Type": "application/json",
188
+ "Authorization": `Bearer ${apiKey}`,
189
+ },
190
+ body: JSON.stringify({
191
+ model: useModel ?? "gpt-4o-mini",
192
+ messages: messages.map(m => {
193
+ if (m.role === "tool_result") {
194
+ return { role: "tool", tool_call_id: m.toolUseId, content: JSON.stringify(m.result) };
195
+ }
196
+ if (m.role === "assistant" && m.toolCalls?.length) {
197
+ return {
198
+ role: "assistant",
199
+ content: m.content ?? null,
200
+ tool_calls: m.toolCalls.map(tc => ({
201
+ id: tc.id,
202
+ type: "function",
203
+ function: { name: tc.name, arguments: JSON.stringify(tc.args ?? {}) },
204
+ })),
205
+ };
206
+ }
207
+ return { role: m.role, content: m.content ?? "" };
208
+ }),
209
+ tools: (toolDefs ?? []).map(t => ({
210
+ type: "function",
211
+ function: { name: t.name, description: t.description, parameters: t.parameters },
212
+ })),
213
+ }),
214
+ });
215
+
216
+ const data = await response.json();
217
+ if (!response.ok) throw new Error(data.error?.message ?? `API error ${response.status}`);
218
+
219
+ const choice = data.choices?.[0];
220
+ const msg = choice?.message;
221
+
222
+ if (msg?.tool_calls?.length) {
223
+ return {
224
+ type: "tool_calls",
225
+ calls: msg.tool_calls.map(tc => ({
226
+ id: tc.id,
227
+ name: tc.function.name,
228
+ args: JSON.parse(tc.function.arguments ?? "{}"),
229
+ })),
230
+ };
231
+ }
232
+ return { type: "text", text: msg?.content ?? "" };
233
+ }
234
+
235
+ /** Main agent loop */
236
+ async function runLoop() {
237
+ const messages = [
238
+ { role: "system", content: systemPrompt },
239
+ { role: "user", content: task },
240
+ ];
241
+
242
+ for (let round = 0; round < MAX_ROUNDS; round++) {
243
+ if (killed) {
244
+ parentPort.postMessage({ type: "failed", agentId, error: "killed" });
245
+ return;
246
+ }
247
+
248
+ // Inject steer messages
249
+ while (steerMessages.length > 0) {
250
+ const steer = steerMessages.shift();
251
+ messages.push({ role: "user", content: `[Guidance from orchestrator]: ${steer}` });
252
+ }
253
+
254
+ // Context compaction
255
+ const totalTokens = estimateMessages(messages);
256
+ if (totalTokens > TOKEN_LIMIT * COMPACT_THRESHOLD) {
257
+ const compacted = compactMessages(messages);
258
+ messages.length = 0;
259
+ messages.push(...compacted);
260
+ }
261
+
262
+ // Emit progress
263
+ parentPort.postMessage({ type: "progress", agentId, round, content: `Starting round ${round + 1}` });
264
+
265
+ // Provider call with retry + fallback handled by main thread
266
+ // Here we just call via the providerConfig directly
267
+ let result;
268
+ try {
269
+ result = await callProvider(messages);
270
+ } catch (err) {
271
+ parentPort.postMessage({ type: "failed", agentId, error: err.message });
272
+ return;
273
+ }
274
+
275
+ if (result.type === "text") {
276
+ parentPort.postMessage({ type: "completed", agentId, result: result.text });
277
+ return;
278
+ }
279
+
280
+ // Handle tool calls
281
+ messages.push({ role: "assistant", toolCalls: result.calls, content: "" });
282
+
283
+ for (const call of result.calls) {
284
+ if (killed) {
285
+ parentPort.postMessage({ type: "failed", agentId, error: "killed" });
286
+ return;
287
+ }
288
+
289
+ let toolResult;
290
+ try {
291
+ toolResult = await requestToolCall(call, round);
292
+ } catch (err) {
293
+ toolResult = { error: err.message, success: false };
294
+ }
295
+
296
+ parentPort.postMessage({
297
+ type: "tool_result",
298
+ agentId,
299
+ round,
300
+ toolName: call.name,
301
+ result: toolResult,
302
+ });
303
+
304
+ messages.push({
305
+ role: "tool_result",
306
+ toolName: call.name,
307
+ toolUseId: call.id ?? call.name,
308
+ result: toolResult,
309
+ });
310
+ }
311
+ }
312
+
313
+ // Max rounds
314
+ parentPort.postMessage({ type: "completed", agentId, result: "(max rounds reached — partial work above)" });
315
+ }
316
+
317
+ // Start the loop with a timeout
318
+ const timeoutMs = timeout ?? 300_000;
319
+ const timeoutPromise = new Promise((_, reject) =>
320
+ setTimeout(() => reject(new Error("Sub-agent timed out")), timeoutMs)
321
+ );
322
+
323
+ Promise.race([runLoop(), timeoutPromise]).catch((err) => {
324
+ parentPort.postMessage({ type: "failed", agentId, error: err.message ?? String(err) });
325
+ });