@workermill/agent 0.7.6 → 0.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.
@@ -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/config.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface AgentConfig {
17
17
  gitlabToken: string;
18
18
  workerImage: string;
19
19
  teamPlanningEnabled: boolean;
20
- analystModel: string;
20
+ analystModel?: string;
21
21
  }
22
22
  export interface FileConfig {
23
23
  apiUrl: string;
package/dist/config.js CHANGED
@@ -76,7 +76,7 @@ export function loadConfigFromFile() {
76
76
  gitlabToken: fc.tokens?.gitlab || "",
77
77
  workerImage,
78
78
  teamPlanningEnabled: fc.teamPlanningEnabled ?? true,
79
- analystModel: fc.analystModel || "sonnet",
79
+ analystModel: fc.analystModel,
80
80
  };
81
81
  }
82
82
  /**
@@ -122,7 +122,7 @@ export function loadConfig() {
122
122
  gitlabToken: process.env.GITLAB_TOKEN || "",
123
123
  workerImage: process.env.WORKER_IMAGE || "workermill-worker:local",
124
124
  teamPlanningEnabled: process.env.TEAM_PLANNING_ENABLED !== "false",
125
- analystModel: process.env.ANALYST_MODEL || "sonnet",
125
+ analystModel: process.env.ANALYST_MODEL,
126
126
  };
127
127
  }
128
128
  /**
package/dist/planner.js CHANGED
@@ -19,7 +19,7 @@ import { spawn, execSync } from "child_process";
19
19
  import { findClaudePath } from "./config.js";
20
20
  import { api } from "./api.js";
21
21
  import { parseExecutionPlan, applyFileCap, applyStoryCap, serializePlan, runCriticValidation, formatCriticFeedback, AUTO_APPROVAL_THRESHOLD, } from "./plan-validator.js";
22
- import { generateText } from "./providers.js";
22
+ import { generateTextWithTools } from "./ai-sdk-generate.js";
23
23
  /** Max Planner-Critic iterations before giving up */
24
24
  const MAX_ITERATIONS = 3;
25
25
  /** Timestamp prefix */
@@ -471,6 +471,43 @@ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs =
471
471
  });
472
472
  });
473
473
  }
474
+ /**
475
+ * Run an analyst agent via Vercel AI SDK with tool access to the cloned repo.
476
+ * Used for non-Anthropic providers (OpenAI, Google, Ollama) that can't use Claude CLI.
477
+ * Returns the analyst's report text, or an empty string on failure.
478
+ */
479
+ async function runAnalystWithSdk(name, provider, model, apiKey, prompt, repoPath, timeoutMs = 900_000) {
480
+ const label = chalk.blue(`[${name}]`);
481
+ const startMs = Date.now();
482
+ console.log(`${ts()} ${label} Starting via AI SDK (${chalk.dim(`${provider}/${model}`)})...`);
483
+ try {
484
+ const result = await generateTextWithTools({
485
+ provider,
486
+ model,
487
+ apiKey,
488
+ prompt,
489
+ workingDir: repoPath,
490
+ maxTokens: 16384,
491
+ temperature: 0.3,
492
+ timeoutMs,
493
+ maxSteps: 20, // Allow thorough exploration
494
+ enableTools: true,
495
+ });
496
+ const elapsed = Math.round((Date.now() - startMs) / 1000);
497
+ if (result && result.length > 0) {
498
+ console.log(`${ts()} ${label} ${chalk.green("✓ Done")} in ${elapsed}s (${result.length} chars)`);
499
+ return result;
500
+ }
501
+ console.log(`${ts()} ${label} ${chalk.yellow("⚠ Empty output")} after ${elapsed}s`);
502
+ return "";
503
+ }
504
+ catch (error) {
505
+ const elapsed = Math.round((Date.now() - startMs) / 1000);
506
+ const errMsg = error instanceof Error ? error.message : String(error);
507
+ console.log(`${ts()} ${label} ${chalk.red(`✗ Failed`)} after ${elapsed}s: ${errMsg.substring(0, 150)}`);
508
+ return "";
509
+ }
510
+ }
474
511
  /** Analyst prompt templates */
475
512
  const CODEBASE_ANALYST_PROMPT = `You are a codebase analyst. Your job is to explore this repository using tools and report what you find.
476
513
 
@@ -546,13 +583,25 @@ Keep your report under 1500 words. Only report facts you verified with tools.`;
546
583
  * This runs ONCE before the planner-critic loop — analyst prompts don't
547
584
  * include critic feedback, so re-running them on iteration 2+ is waste.
548
585
  */
549
- async function runTeamAnalysis(task, basePrompt, claudePath, model, env, repoPath, taskId, startTime) {
586
+ async function runTeamAnalysis(task, basePrompt, claudePath, model, env, repoPath, taskId, startTime, provider = "anthropic", providerApiKey) {
550
587
  const taskLabel = chalk.cyan(taskId.slice(0, 8));
551
588
  console.log(`${ts()} ${taskLabel} ${chalk.magenta("◆ Team planning")} — running 3 analysts in parallel...`);
552
589
  await postLog(taskId, `${PREFIX} Team planning: running codebase, requirements, and risk analysts in parallel...`);
553
590
  await postProgress(taskId, "reading_repo", Math.round((Date.now() - startTime) / 1000), "Running parallel analysis agents...", 0, 0);
554
591
  const analysisModel = model;
555
592
  const MAX_TEAM_RETRIES = 3;
593
+ const useCliAnalysts = provider === "anthropic";
594
+ // Helper: dispatch analyst to Claude CLI or AI SDK based on provider
595
+ const dispatchAnalyst = (name, prompt) => {
596
+ if (useCliAnalysts) {
597
+ return runAnalyst(name, claudePath, analysisModel, prompt, repoPath, env);
598
+ }
599
+ if (!providerApiKey) {
600
+ console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} No API key for ${provider} analysts, skipping ${name}`);
601
+ return Promise.resolve("");
602
+ }
603
+ return runAnalystWithSdk(name, provider, analysisModel, providerApiKey, prompt, repoPath);
604
+ };
556
605
  let codebaseReport = "";
557
606
  let requirementsReport = "";
558
607
  let riskReport = "";
@@ -562,9 +611,9 @@ async function runTeamAnalysis(task, basePrompt, claudePath, model, env, repoPat
562
611
  await postLog(taskId, `${PREFIX} Team analysis retry ${attempt}/${MAX_TEAM_RETRIES}...`);
563
612
  }
564
613
  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),
614
+ codebaseReport ? Promise.resolve(codebaseReport) : dispatchAnalyst("Codebase", CODEBASE_ANALYST_PROMPT),
615
+ requirementsReport ? Promise.resolve(requirementsReport) : dispatchAnalyst("Requirements", makeRequirementsAnalystPrompt(task)),
616
+ riskReport ? Promise.resolve(riskReport) : dispatchAnalyst("Risk", makeRiskAssessorPrompt(task)),
568
617
  ]);
569
618
  if (!codebaseReport && codebaseResult.status === "fulfilled") {
570
619
  codebaseReport = codebaseResult.value;
@@ -630,7 +679,7 @@ export async function planTask(task, config, credentials) {
630
679
  });
631
680
  const { prompt: basePrompt, model, provider: planningProvider, maxStories: apiMaxStories } = promptResponse.data;
632
681
  const maxStories = typeof apiMaxStories === "number" ? apiMaxStories : 8;
633
- const cliModel = model || "sonnet";
682
+ const cliModel = model;
634
683
  const provider = (planningProvider || "anthropic");
635
684
  const isAnthropicPlanning = provider === "anthropic";
636
685
  const claudePath = process.env.CLAUDE_CLI_PATH || findClaudePath() || "claude";
@@ -646,7 +695,7 @@ export async function planTask(task, config, credentials) {
646
695
  // on iteration 2+ wastes compute (they'd produce the same reports).
647
696
  let repoPath = null;
648
697
  let enhancedBasePrompt = basePrompt;
649
- if (isAnthropicPlanning && config.teamPlanningEnabled && task.githubRepo) {
698
+ if (config.teamPlanningEnabled && task.githubRepo) {
650
699
  const scmProvider = task.scmProvider || "github";
651
700
  const scmToken = scmProvider === "bitbucket"
652
701
  ? config.bitbucketToken
@@ -660,9 +709,10 @@ export async function planTask(task, config, credentials) {
660
709
  console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} No SCM token for ${scmProvider}, skipping team planning`);
661
710
  }
662
711
  if (repoPath) {
663
- const analystModel = config.analystModel || "sonnet";
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);
712
+ const analystModel = config.analystModel || cliModel;
713
+ const analystBackend = isAnthropicPlanning ? "Claude CLI" : `${provider} AI SDK`;
714
+ console.log(`${ts()} ${taskLabel} Analysts using model: ${chalk.yellow(analystModel)} via ${chalk.dim(analystBackend)} (planner: ${chalk.yellow(cliModel)})`);
715
+ const analysisResult = await runTeamAnalysis(task, basePrompt, claudePath, analystModel, cleanEnv, repoPath, task.id, startTime, provider, providerApiKey);
666
716
  if (analysisResult) {
667
717
  enhancedBasePrompt = analysisResult;
668
718
  }
@@ -701,8 +751,17 @@ export async function planTask(task, config, credentials) {
701
751
  throw new Error(`No API key available for provider "${provider}". Configure it in Settings > Integrations.`);
702
752
  }
703
753
  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);
754
+ await postProgress(task.id, "generating_plan", genStart, "Generating plan via AI SDK...", 0, 0);
755
+ // Use AI SDK with tool access to cloned repo (if available)
756
+ rawOutput = await generateTextWithTools({
757
+ provider,
758
+ model: cliModel,
759
+ apiKey: providerApiKey,
760
+ prompt: currentPrompt,
761
+ workingDir: repoPath || undefined,
762
+ enableTools: !!repoPath, // Only enable tools if we have a cloned repo
763
+ maxSteps: 10,
764
+ });
706
765
  // Post "validating" phase so the dashboard progress bar transitions correctly
707
766
  const genEnd = Math.round((Date.now() - startTime) / 1000);
708
767
  await postProgress(task.id, "validating", genEnd, "Validating plan...", rawOutput.length, 0);
package/dist/spawner.js CHANGED
@@ -182,9 +182,9 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
182
182
  // Target repository
183
183
  TARGET_REPO: task.githubRepo || "",
184
184
  GITHUB_REPO: task.githubRepo || "",
185
- // Worker model (CLAUDE_MODEL is legacy compat for manager entrypoint)
186
- WORKER_MODEL: task.workerModel || String(orgConfig.defaultWorkerModel || "sonnet"),
187
- CLAUDE_MODEL: task.workerProvider === "anthropic" ? (task.workerModel || "sonnet") : "sonnet",
185
+ // Worker model comes from task or org settings, no hardcoded fallbacks
186
+ WORKER_MODEL: task.workerModel || String(orgConfig.defaultWorkerModel || ""),
187
+ CLAUDE_MODEL: task.workerModel || String(orgConfig.defaultWorkerModel || ""),
188
188
  // Jira credentials (from org Secrets Manager via /api/agent/claim)
189
189
  JIRA_BASE_URL: credentials?.jiraBaseUrl || "",
190
190
  JIRA_EMAIL: credentials?.jiraEmail || "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workermill/agent",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
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",