@workermill/agent 0.7.7 → 0.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.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * AI SDK Text Generation with Tool Support
3
+ *
4
+ * Wraps the Vercel AI SDK to provide tool-enabled text generation for
5
+ * non-Anthropic providers (OpenAI, Google, Ollama). Anthropic planning
6
+ * still uses Claude CLI for tool access (battle-tested, OAuth auth).
7
+ *
8
+ * Tools: glob (file search), read_file (file reading), grep (content search).
9
+ * These match the tools Claude CLI exposes to analysts.
10
+ */
11
+ import type { AIProvider } from "./providers.js";
12
+ export interface GenerateWithToolsOptions {
13
+ provider: AIProvider;
14
+ model: string;
15
+ apiKey: string;
16
+ prompt: string;
17
+ systemPrompt?: string;
18
+ workingDir?: string;
19
+ maxTokens?: number;
20
+ temperature?: number;
21
+ timeoutMs?: number;
22
+ maxSteps?: number;
23
+ /** Enable tool use (glob, read_file, grep). Default: true */
24
+ enableTools?: boolean;
25
+ }
26
+ /**
27
+ * Generate text using the Vercel AI SDK with optional tool support.
28
+ *
29
+ * For providers that support tool calling (OpenAI, Google, Anthropic),
30
+ * the model can use glob/read_file/grep to explore a cloned repo.
31
+ * maxSteps controls how many tool call rounds are allowed.
32
+ */
33
+ export declare function generateTextWithTools(options: GenerateWithToolsOptions): Promise<string>;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * AI SDK Text Generation with Tool Support
3
+ *
4
+ * Wraps the Vercel AI SDK to provide tool-enabled text generation for
5
+ * non-Anthropic providers (OpenAI, Google, Ollama). Anthropic planning
6
+ * still uses Claude CLI for tool access (battle-tested, OAuth auth).
7
+ *
8
+ * Tools: glob (file search), read_file (file reading), grep (content search).
9
+ * These match the tools Claude CLI exposes to analysts.
10
+ */
11
+ import { generateText as aiGenerateText, tool, stepCountIs } from "ai";
12
+ import { createOpenAI } from "@ai-sdk/openai";
13
+ import { createAnthropic } from "@ai-sdk/anthropic";
14
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
15
+ import { z } from "zod";
16
+ import { execSync } from "child_process";
17
+ import { readFileSync, existsSync } from "fs";
18
+ /**
19
+ * Create the AI SDK model instance for a given provider.
20
+ */
21
+ function createModel(provider, model, apiKey) {
22
+ switch (provider) {
23
+ case "anthropic": {
24
+ const anthropic = createAnthropic({ apiKey });
25
+ return anthropic(model);
26
+ }
27
+ case "openai": {
28
+ const openai = createOpenAI({ apiKey });
29
+ return openai(model);
30
+ }
31
+ case "google": {
32
+ const google = createGoogleGenerativeAI({ apiKey });
33
+ return google(model);
34
+ }
35
+ case "ollama": {
36
+ // Ollama uses OpenAI-compatible API
37
+ const ollama = createOpenAI({
38
+ baseURL: apiKey || "http://localhost:11434/v1",
39
+ apiKey: "ollama", // Ollama doesn't need a real key
40
+ });
41
+ return ollama(model);
42
+ }
43
+ default:
44
+ throw new Error(`Unsupported AI provider: ${provider}`);
45
+ }
46
+ }
47
+ // Zod schemas for tool inputs
48
+ const globSchema = z.object({
49
+ pattern: z
50
+ .string()
51
+ .describe("Glob pattern like '**/*.ts', 'src/**/*.js', 'package.json'"),
52
+ });
53
+ const readFileSchema = z.object({
54
+ path: z.string().describe("File path relative to the working directory"),
55
+ limit: z
56
+ .number()
57
+ .optional()
58
+ .describe("Max number of lines to read (default: 500)"),
59
+ });
60
+ const grepSchema = z.object({
61
+ pattern: z.string().describe("Search pattern (regex supported)"),
62
+ glob: z
63
+ .string()
64
+ .optional()
65
+ .describe("File glob to filter (e.g. '*.ts', '*.py')"),
66
+ });
67
+ /**
68
+ * Build filesystem tools scoped to a working directory.
69
+ * These are the same tools Claude CLI exposes (Glob, Read, Grep).
70
+ */
71
+ function buildTools(workingDir) {
72
+ return {
73
+ glob: tool({
74
+ description: "Find files matching a glob pattern. Returns file paths relative to the working directory.",
75
+ inputSchema: globSchema,
76
+ execute: async (input) => {
77
+ try {
78
+ // Use find as a cross-platform glob (fast-glob not available)
79
+ const result = execSync(`find . -path './.git' -prune -o -path './node_modules' -prune -o -name '${input.pattern.replace(/\*\*/g, "*")}' -print 2>/dev/null | head -200`, { cwd: workingDir, encoding: "utf-8", timeout: 15000 }).trim();
80
+ if (!result) {
81
+ // Try with a broader approach for ** patterns
82
+ const broader = execSync(`find . -path './.git' -prune -o -path './node_modules' -prune -o -type f -print 2>/dev/null | head -500`, { cwd: workingDir, encoding: "utf-8", timeout: 15000 }).trim();
83
+ return broader || "No files found";
84
+ }
85
+ return result;
86
+ }
87
+ catch {
88
+ return "Error running glob search";
89
+ }
90
+ },
91
+ }),
92
+ read_file: tool({
93
+ description: "Read the contents of a file. Returns the file text.",
94
+ inputSchema: readFileSchema,
95
+ execute: async (input) => {
96
+ try {
97
+ const fullPath = `${workingDir}/${input.path}`.replace(/\/\//g, "/");
98
+ if (!existsSync(fullPath)) {
99
+ return `File not found: ${input.path}`;
100
+ }
101
+ const content = readFileSync(fullPath, "utf-8");
102
+ const lines = content.split("\n");
103
+ const maxLines = input.limit || 500;
104
+ if (lines.length > maxLines) {
105
+ return (lines.slice(0, maxLines).join("\n") +
106
+ `\n... (truncated, ${lines.length - maxLines} more lines)`);
107
+ }
108
+ return content;
109
+ }
110
+ catch (err) {
111
+ return `Error reading file: ${err instanceof Error ? err.message : String(err)}`;
112
+ }
113
+ },
114
+ }),
115
+ grep: tool({
116
+ description: "Search for a pattern in files. Returns matching lines with file paths and line numbers.",
117
+ inputSchema: grepSchema,
118
+ execute: async (input) => {
119
+ try {
120
+ const includeFlag = input.glob ? `--include='${input.glob}'` : "";
121
+ const result = execSync(`grep -rn ${includeFlag} --exclude-dir=node_modules --exclude-dir=.git '${input.pattern.replace(/'/g, "'\\''")}' . 2>/dev/null | head -100`, { cwd: workingDir, encoding: "utf-8", timeout: 15000 }).trim();
122
+ return result || "No matches found";
123
+ }
124
+ catch {
125
+ return "No matches found";
126
+ }
127
+ },
128
+ }),
129
+ };
130
+ }
131
+ /**
132
+ * Generate text using the Vercel AI SDK with optional tool support.
133
+ *
134
+ * For providers that support tool calling (OpenAI, Google, Anthropic),
135
+ * the model can use glob/read_file/grep to explore a cloned repo.
136
+ * maxSteps controls how many tool call rounds are allowed.
137
+ */
138
+ export async function generateTextWithTools(options) {
139
+ const { provider, model: modelName, apiKey, prompt, systemPrompt, workingDir, maxTokens = 16384, temperature = 0.7, timeoutMs = 600_000, maxSteps = 15, enableTools = true, } = options;
140
+ const sdkModel = createModel(provider, modelName, apiKey);
141
+ const tools = enableTools && workingDir ? buildTools(workingDir) : undefined;
142
+ const abortController = new AbortController();
143
+ const timeout = setTimeout(() => abortController.abort(), timeoutMs);
144
+ try {
145
+ const result = await aiGenerateText({
146
+ model: sdkModel,
147
+ prompt,
148
+ system: systemPrompt,
149
+ maxOutputTokens: maxTokens,
150
+ temperature,
151
+ tools,
152
+ stopWhen: tools ? stepCountIs(maxSteps) : undefined,
153
+ abortSignal: abortController.signal,
154
+ });
155
+ return result.text;
156
+ }
157
+ finally {
158
+ clearTimeout(timeout);
159
+ }
160
+ }
package/dist/planner.js CHANGED
@@ -15,11 +15,12 @@
15
15
  * sees the same planning progress as cloud mode.
16
16
  */
17
17
  import chalk from "chalk";
18
+ import ora from "ora";
18
19
  import { spawn, execSync } from "child_process";
19
20
  import { findClaudePath } from "./config.js";
20
21
  import { api } from "./api.js";
21
22
  import { parseExecutionPlan, applyFileCap, applyStoryCap, serializePlan, runCriticValidation, formatCriticFeedback, AUTO_APPROVAL_THRESHOLD, } from "./plan-validator.js";
22
- import { generateText } from "./providers.js";
23
+ import { generateTextWithTools } from "./ai-sdk-generate.js";
23
24
  /** Max Planner-Critic iterations before giving up */
24
25
  const MAX_ITERATIONS = 3;
25
26
  /** Timestamp prefix */
@@ -103,6 +104,23 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
103
104
  let stderrOutput = "";
104
105
  let charsReceived = 0;
105
106
  let toolCallCount = 0;
107
+ // Live spinner — shows elapsed time, phase, and chars generated
108
+ const spinner = ora({
109
+ text: `${taskLabel} Initializing planner...`,
110
+ prefixText: "",
111
+ spinner: "dots",
112
+ }).start();
113
+ function updateSpinner() {
114
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
115
+ const phaseIcon = currentPhase === "reading_repo" ? "📂" :
116
+ currentPhase === "analyzing" ? "🔍" :
117
+ currentPhase === "generating_plan" ? "📝" :
118
+ currentPhase === "validating" ? "✅" : "⏳";
119
+ const stats = chalk.dim(`${formatElapsed(elapsed)} · ${charsReceived} chars · ${toolCallCount} tools`);
120
+ spinner.text = `${taskLabel} ${phaseIcon} ${phaseLabel(currentPhase, elapsed)} ${stats}`;
121
+ }
122
+ // Update spinner every 500ms for smooth elapsed time display
123
+ const spinnerInterval = setInterval(updateSpinner, 500);
106
124
  // Buffered text streaming — flush complete lines to dashboard every 1s.
107
125
  // LLM deltas are tiny fragments; we accumulate until we see '\n', then
108
126
  // a 1s interval flushes all complete lines as log entries. On exit we
@@ -117,6 +135,11 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
117
135
  for (const line of parts) {
118
136
  if (line.trim()) {
119
137
  postLog(taskId, `${PREFIX} ${line}`, "output");
138
+ // Echo planner thoughts to local terminal
139
+ spinner.stop();
140
+ const truncated = line.trim().length > 160 ? line.trim().substring(0, 160) + "…" : line.trim();
141
+ console.log(`${ts()} ${taskLabel} ${chalk.dim("💭")} ${chalk.dim(truncated)}`);
142
+ spinner.start();
120
143
  }
121
144
  }
122
145
  textBuffer = incomplete;
@@ -133,7 +156,10 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
133
156
  const elapsed = Math.round((Date.now() - startTime) / 1000);
134
157
  const msg = phaseLabel(newPhase, elapsed);
135
158
  postLog(taskId, msg);
159
+ spinner.stop();
136
160
  console.log(`${ts()} ${taskLabel} ${chalk.dim(msg)}`);
161
+ spinner.start();
162
+ updateSpinner();
137
163
  }
138
164
  // Flush buffered LLM text to dashboard every 1s (complete lines only)
139
165
  const textFlushInterval = setInterval(() => flushTextBuffer(), 1_000);
@@ -159,7 +185,9 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
159
185
  lastProgressLogAt = elapsed;
160
186
  const msg = `${PREFIX} Planning in progress — analyzing requirements and decomposing into steps (${formatElapsed(elapsed)} elapsed)`;
161
187
  postLog(taskId, msg);
188
+ spinner.stop();
162
189
  console.log(`${ts()} ${taskLabel} ${chalk.dim(msg)}`);
190
+ spinner.start();
163
191
  }
164
192
  }, 5_000);
165
193
  // Parse streaming JSON lines from Claude CLI
@@ -248,20 +276,22 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
248
276
  proc.stderr.on("data", (chunk) => {
249
277
  stderrOutput += chunk.toString();
250
278
  });
251
- const timeout = setTimeout(() => {
279
+ function cleanupAll() {
252
280
  clearInterval(progressInterval);
253
281
  clearInterval(sseProgressInterval);
254
282
  clearInterval(textFlushInterval);
283
+ clearInterval(spinnerInterval);
255
284
  flushTextBuffer(true);
285
+ spinner.stop();
286
+ }
287
+ const timeout = setTimeout(() => {
288
+ cleanupAll();
256
289
  proc.kill("SIGTERM");
257
290
  reject(new Error("Claude CLI timed out after 20 minutes"));
258
291
  }, 1_200_000);
259
292
  proc.on("exit", (code) => {
260
293
  clearTimeout(timeout);
261
- clearInterval(progressInterval);
262
- clearInterval(sseProgressInterval);
263
- clearInterval(textFlushInterval);
264
- flushTextBuffer(true);
294
+ cleanupAll();
265
295
  // Emit final "validating" phase to dashboard
266
296
  const elapsedAtClose = Math.round((Date.now() - startTime) / 1000);
267
297
  postProgress(taskId, "validating", elapsedAtClose, "Validating plan...", charsReceived, toolCallCount);
@@ -275,10 +305,7 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
275
305
  });
276
306
  proc.on("error", (err) => {
277
307
  clearTimeout(timeout);
278
- clearInterval(progressInterval);
279
- clearInterval(sseProgressInterval);
280
- clearInterval(textFlushInterval);
281
- flushTextBuffer(true);
308
+ cleanupAll();
282
309
  reject(err);
283
310
  });
284
311
  });
@@ -351,10 +378,14 @@ async function cloneTargetRepo(repo, token, scmProvider, taskId) {
351
378
  * Run an analyst agent via Claude CLI with tool access to the cloned repo.
352
379
  * Returns the analyst's report text, or an empty string on failure.
353
380
  */
354
- function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs = 900_000) {
381
+ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs = 900_000, taskId) {
355
382
  const label = chalk.blue(`[${name}]`);
383
+ const modelLabel = chalk.yellow(model);
356
384
  return new Promise((resolve) => {
357
- console.log(`${ts()} ${label} Starting (${chalk.dim(model)})...`);
385
+ console.log(`${ts()} ${label} Starting analyst using ${modelLabel}...`);
386
+ if (taskId) {
387
+ postLog(taskId, `${PREFIX} [${name}] Starting analyst using ${model}...`);
388
+ }
358
389
  const proc = spawn(claudePath, [
359
390
  "--print",
360
391
  "--verbose",
@@ -376,12 +407,23 @@ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs =
376
407
  let toolCalls = 0;
377
408
  let timedOut = false;
378
409
  const startMs = Date.now();
410
+ // Live spinner for this analyst
411
+ const analystSpinner = ora({
412
+ text: `${label} Starting (${model})...`,
413
+ spinner: "dots",
414
+ }).start();
415
+ const analystSpinnerInterval = setInterval(() => {
416
+ const elapsed = Math.round((Date.now() - startMs) / 1000);
417
+ analystSpinner.text = `${label} ${chalk.dim(`${formatElapsed(elapsed)} · ${toolCalls} tools · ${fullText.length} chars`)}`;
418
+ }, 500);
379
419
  proc.stderr.on("data", (chunk) => {
380
420
  const text = chunk.toString();
381
421
  stderrOutput += text;
382
422
  // Show stderr in real-time so we can see what's happening
383
423
  for (const line of text.split("\n").filter((l) => l.trim())) {
424
+ analystSpinner.stop();
384
425
  console.log(`${ts()} ${label} ${chalk.red("stderr:")} ${line.trim()}`);
426
+ analystSpinner.start();
385
427
  }
386
428
  });
387
429
  proc.stdout.on("data", (data) => {
@@ -404,7 +446,11 @@ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs =
404
446
  // Log analyst reasoning (first line, truncated)
405
447
  const thought = block.text.trim().split("\n")[0].substring(0, 120);
406
448
  if (thought) {
449
+ analystSpinner.stop();
407
450
  console.log(`${ts()} ${label} ${chalk.dim("💭")} ${chalk.dim(thought)}`);
451
+ if (taskId)
452
+ postLog(taskId, `${PREFIX} [${name}] 💭 ${thought}`);
453
+ analystSpinner.start();
408
454
  }
409
455
  }
410
456
  else if (block.type === "tool_use") {
@@ -413,7 +459,11 @@ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs =
413
459
  // Show tool name + input preview (file path, pattern, etc.)
414
460
  const inputStr = block.input ? JSON.stringify(block.input) : "";
415
461
  const inputPreview = inputStr.length > 80 ? inputStr.substring(0, 80) + "…" : inputStr;
416
- console.log(`${ts()} ${label} ${chalk.dim(`Tool: ${toolName}`)}${inputPreview ? chalk.dim(` ${inputPreview}`) : ""} (${toolCalls} total)`);
462
+ analystSpinner.stop();
463
+ console.log(`${ts()} ${label} ${chalk.dim(`Tool: ${toolName}`)}${inputPreview ? chalk.dim(` ${inputPreview}`) : ""}`);
464
+ if (taskId)
465
+ postLog(taskId, `${PREFIX} [${name}] Tool: ${toolName} ${inputPreview}`);
466
+ analystSpinner.start();
417
467
  }
418
468
  }
419
469
  }
@@ -428,7 +478,11 @@ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs =
428
478
  else if (event.type === "content_block_start" && event.content_block?.type === "tool_use") {
429
479
  toolCalls++;
430
480
  const toolName = event.content_block?.name || "unknown";
431
- console.log(`${ts()} ${label} ${chalk.dim(`Tool: ${toolName}`)} (${toolCalls} total)`);
481
+ analystSpinner.stop();
482
+ console.log(`${ts()} ${label} ${chalk.dim(`Tool: ${toolName}`)}`);
483
+ if (taskId)
484
+ postLog(taskId, `${PREFIX} [${name}] Tool: ${toolName}`);
485
+ analystSpinner.start();
432
486
  }
433
487
  else if (event.type === "result" && event.result) {
434
488
  resultText =
@@ -442,35 +496,89 @@ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs =
442
496
  });
443
497
  const timeout = setTimeout(() => {
444
498
  timedOut = true;
499
+ clearInterval(analystSpinnerInterval);
500
+ analystSpinner.stop();
445
501
  proc.kill("SIGTERM");
446
502
  const elapsed = Math.round((Date.now() - startMs) / 1000);
447
- console.log(`${ts()} ${label} ${chalk.yellow("⚠ Timed out")} after ${elapsed}s (${toolCalls} tool calls, ${fullText.length} chars)`);
503
+ console.log(`${ts()} ${label} ${chalk.yellow("⚠ Timed out")} after ${formatElapsed(elapsed)} (${toolCalls} tool calls, ${fullText.length} chars)`);
504
+ if (taskId)
505
+ postLog(taskId, `${PREFIX} [${name}] ⚠ Timed out after ${formatElapsed(elapsed)}`);
448
506
  resolve(resultText || fullText || "");
449
507
  }, timeoutMs);
450
508
  proc.on("exit", (code) => {
451
509
  clearTimeout(timeout);
510
+ clearInterval(analystSpinnerInterval);
511
+ analystSpinner.stop();
452
512
  const elapsed = Math.round((Date.now() - startMs) / 1000);
453
513
  if (timedOut)
454
514
  return; // already resolved
455
515
  const output = resultText || fullText || "";
456
516
  if (code === 0 && output.length > 0) {
457
- console.log(`${ts()} ${label} ${chalk.green("✓ Done")} in ${elapsed}s (${toolCalls} tool calls, ${output.length} chars)`);
517
+ console.log(`${ts()} ${label} ${chalk.green("✓ Done")} in ${formatElapsed(elapsed)} (${toolCalls} tool calls, ${output.length} chars)`);
518
+ if (taskId)
519
+ postLog(taskId, `${PREFIX} [${name}] ✓ Done in ${formatElapsed(elapsed)} (${toolCalls} tool calls, ${output.length} chars)`);
458
520
  }
459
521
  else if (code !== 0) {
460
- console.log(`${ts()} ${label} ${chalk.red(`✗ Exited ${code}`)} after ${elapsed}s — ${stderrOutput.substring(0, 150) || "no stderr"}`);
522
+ console.log(`${ts()} ${label} ${chalk.red(`✗ Exited ${code}`)} after ${formatElapsed(elapsed)} — ${stderrOutput.substring(0, 150) || "no stderr"}`);
523
+ if (taskId)
524
+ postLog(taskId, `${PREFIX} [${name}] ✗ Exited ${code} after ${formatElapsed(elapsed)}`);
461
525
  }
462
526
  else {
463
- console.log(`${ts()} ${label} ${chalk.yellow("⚠ Empty output")} after ${elapsed}s (${toolCalls} tool calls)`);
527
+ console.log(`${ts()} ${label} ${chalk.yellow("⚠ Empty output")} after ${formatElapsed(elapsed)} (${toolCalls} tool calls)`);
528
+ if (taskId)
529
+ postLog(taskId, `${PREFIX} [${name}] ⚠ Empty output after ${formatElapsed(elapsed)}`);
464
530
  }
465
531
  resolve(output);
466
532
  });
467
533
  proc.on("error", (err) => {
468
534
  clearTimeout(timeout);
535
+ clearInterval(analystSpinnerInterval);
536
+ analystSpinner.stop();
469
537
  console.log(`${ts()} ${label} ${chalk.red("✗ Spawn failed:")} ${err.message}`);
470
538
  resolve("");
471
539
  });
472
540
  });
473
541
  }
542
+ /**
543
+ * Run an analyst agent via Vercel AI SDK with tool access to the cloned repo.
544
+ * Used for non-Anthropic providers (OpenAI, Google, Ollama) that can't use Claude CLI.
545
+ * Returns the analyst's report text, or an empty string on failure.
546
+ */
547
+ async function runAnalystWithSdk(name, provider, model, apiKey, prompt, repoPath, timeoutMs = 900_000, taskId) {
548
+ const label = chalk.blue(`[${name}]`);
549
+ const modelLabel = chalk.yellow(`${provider}/${model}`);
550
+ const startMs = Date.now();
551
+ console.log(`${ts()} ${label} Starting analyst using ${modelLabel} via AI SDK...`);
552
+ if (taskId)
553
+ postLog(taskId, `${PREFIX} [${name}] Starting analyst using ${provider}/${model} via AI SDK...`);
554
+ try {
555
+ const result = await generateTextWithTools({
556
+ provider,
557
+ model,
558
+ apiKey,
559
+ prompt,
560
+ workingDir: repoPath,
561
+ maxTokens: 16384,
562
+ temperature: 0.3,
563
+ timeoutMs,
564
+ maxSteps: 20, // Allow thorough exploration
565
+ enableTools: true,
566
+ });
567
+ const elapsed = Math.round((Date.now() - startMs) / 1000);
568
+ if (result && result.length > 0) {
569
+ console.log(`${ts()} ${label} ${chalk.green("✓ Done")} in ${elapsed}s (${result.length} chars)`);
570
+ return result;
571
+ }
572
+ console.log(`${ts()} ${label} ${chalk.yellow("⚠ Empty output")} after ${elapsed}s`);
573
+ return "";
574
+ }
575
+ catch (error) {
576
+ const elapsed = Math.round((Date.now() - startMs) / 1000);
577
+ const errMsg = error instanceof Error ? error.message : String(error);
578
+ console.log(`${ts()} ${label} ${chalk.red(`✗ Failed`)} after ${elapsed}s: ${errMsg.substring(0, 150)}`);
579
+ return "";
580
+ }
581
+ }
474
582
  /** Analyst prompt templates */
475
583
  const CODEBASE_ANALYST_PROMPT = `You are a codebase analyst. Your job is to explore this repository using tools and report what you find.
476
584
 
@@ -546,13 +654,25 @@ Keep your report under 1500 words. Only report facts you verified with tools.`;
546
654
  * This runs ONCE before the planner-critic loop — analyst prompts don't
547
655
  * include critic feedback, so re-running them on iteration 2+ is waste.
548
656
  */
549
- async function runTeamAnalysis(task, basePrompt, claudePath, model, env, repoPath, taskId, startTime) {
657
+ async function runTeamAnalysis(task, basePrompt, claudePath, model, env, repoPath, taskId, startTime, provider = "anthropic", providerApiKey) {
550
658
  const taskLabel = chalk.cyan(taskId.slice(0, 8));
551
659
  console.log(`${ts()} ${taskLabel} ${chalk.magenta("◆ Team planning")} — running 3 analysts in parallel...`);
552
660
  await postLog(taskId, `${PREFIX} Team planning: running codebase, requirements, and risk analysts in parallel...`);
553
661
  await postProgress(taskId, "reading_repo", Math.round((Date.now() - startTime) / 1000), "Running parallel analysis agents...", 0, 0);
554
662
  const analysisModel = model;
555
663
  const MAX_TEAM_RETRIES = 3;
664
+ const useCliAnalysts = provider === "anthropic";
665
+ // Helper: dispatch analyst to Claude CLI or AI SDK based on provider
666
+ const dispatchAnalyst = (name, prompt) => {
667
+ if (useCliAnalysts) {
668
+ return runAnalyst(name, claudePath, analysisModel, prompt, repoPath, env, 900_000, taskId);
669
+ }
670
+ if (!providerApiKey) {
671
+ console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} No API key for ${provider} analysts, skipping ${name}`);
672
+ return Promise.resolve("");
673
+ }
674
+ return runAnalystWithSdk(name, provider, analysisModel, providerApiKey, prompt, repoPath, 900_000, taskId);
675
+ };
556
676
  let codebaseReport = "";
557
677
  let requirementsReport = "";
558
678
  let riskReport = "";
@@ -562,9 +682,9 @@ async function runTeamAnalysis(task, basePrompt, claudePath, model, env, repoPat
562
682
  await postLog(taskId, `${PREFIX} Team analysis retry ${attempt}/${MAX_TEAM_RETRIES}...`);
563
683
  }
564
684
  const [codebaseResult, requirementsResult, riskResult] = await Promise.allSettled([
565
- codebaseReport ? Promise.resolve(codebaseReport) : runAnalyst("Codebase", claudePath, analysisModel, CODEBASE_ANALYST_PROMPT, repoPath, env),
566
- requirementsReport ? Promise.resolve(requirementsReport) : runAnalyst("Requirements", claudePath, analysisModel, makeRequirementsAnalystPrompt(task), repoPath, env),
567
- riskReport ? Promise.resolve(riskReport) : runAnalyst("Risk", claudePath, analysisModel, makeRiskAssessorPrompt(task), repoPath, env),
685
+ codebaseReport ? Promise.resolve(codebaseReport) : dispatchAnalyst("Codebase", CODEBASE_ANALYST_PROMPT),
686
+ requirementsReport ? Promise.resolve(requirementsReport) : dispatchAnalyst("Requirements", makeRequirementsAnalystPrompt(task)),
687
+ riskReport ? Promise.resolve(riskReport) : dispatchAnalyst("Risk", makeRiskAssessorPrompt(task)),
568
688
  ]);
569
689
  if (!codebaseReport && codebaseResult.status === "fulfilled") {
570
690
  codebaseReport = codebaseResult.value;
@@ -646,7 +766,7 @@ export async function planTask(task, config, credentials) {
646
766
  // on iteration 2+ wastes compute (they'd produce the same reports).
647
767
  let repoPath = null;
648
768
  let enhancedBasePrompt = basePrompt;
649
- if (isAnthropicPlanning && config.teamPlanningEnabled && task.githubRepo) {
769
+ if (config.teamPlanningEnabled && task.githubRepo) {
650
770
  const scmProvider = task.scmProvider || "github";
651
771
  const scmToken = scmProvider === "bitbucket"
652
772
  ? config.bitbucketToken
@@ -661,8 +781,9 @@ export async function planTask(task, config, credentials) {
661
781
  }
662
782
  if (repoPath) {
663
783
  const analystModel = config.analystModel || cliModel;
664
- console.log(`${ts()} ${taskLabel} Analysts using model: ${chalk.yellow(analystModel)} (planner: ${chalk.yellow(cliModel)})`);
665
- const analysisResult = await runTeamAnalysis(task, basePrompt, claudePath, analystModel, cleanEnv, repoPath, task.id, startTime);
784
+ const analystBackend = isAnthropicPlanning ? "Claude CLI" : `${provider} AI SDK`;
785
+ console.log(`${ts()} ${taskLabel} Analysts using model: ${chalk.yellow(analystModel)} via ${chalk.dim(analystBackend)} (planner: ${chalk.yellow(cliModel)})`);
786
+ const analysisResult = await runTeamAnalysis(task, basePrompt, claudePath, analystModel, cleanEnv, repoPath, task.id, startTime, provider, providerApiKey);
666
787
  if (analysisResult) {
667
788
  enhancedBasePrompt = analysisResult;
668
789
  }
@@ -701,8 +822,17 @@ export async function planTask(task, config, credentials) {
701
822
  throw new Error(`No API key available for provider "${provider}". Configure it in Settings > Integrations.`);
702
823
  }
703
824
  const genStart = Math.round((Date.now() - startTime) / 1000);
704
- await postProgress(task.id, "generating_plan", genStart, "Generating plan via API...", 0, 0);
705
- rawOutput = await generateText(provider, cliModel, currentPrompt, providerApiKey);
825
+ await postProgress(task.id, "generating_plan", genStart, "Generating plan via AI SDK...", 0, 0);
826
+ // Use AI SDK with tool access to cloned repo (if available)
827
+ rawOutput = await generateTextWithTools({
828
+ provider,
829
+ model: cliModel,
830
+ apiKey: providerApiKey,
831
+ prompt: currentPrompt,
832
+ workingDir: repoPath || undefined,
833
+ enableTools: !!repoPath, // Only enable tools if we have a cloned repo
834
+ maxSteps: 10,
835
+ });
706
836
  // Post "validating" phase so the dashboard progress bar transitions correctly
707
837
  const genEnd = Math.round((Date.now() - startTime) / 1000);
708
838
  await postProgress(task.id, "validating", genEnd, "Validating plan...", rawOutput.length, 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workermill/agent",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
4
4
  "description": "WorkerMill Remote Agent - Run AI workers locally with your Claude Max subscription",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -20,11 +20,16 @@
20
20
  "node": ">=20.0.0"
21
21
  },
22
22
  "dependencies": {
23
+ "@ai-sdk/anthropic": "^3.0.0",
24
+ "@ai-sdk/google": "^3.0.0",
25
+ "@ai-sdk/openai": "^3.0.0",
26
+ "ai": "^6.0.0",
23
27
  "axios": "^1.7.0",
24
28
  "chalk": "^5.3.0",
25
29
  "commander": "^12.0.0",
26
30
  "inquirer": "^9.2.0",
27
- "ora": "^8.0.0"
31
+ "ora": "^8.0.0",
32
+ "zod": "^3.23.0"
28
33
  },
29
34
  "devDependencies": {
30
35
  "@types/inquirer": "^9.0.9",