@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.
- package/dist/ai-sdk-generate.d.ts +33 -0
- package/dist/ai-sdk-generate.js +160 -0
- package/dist/planner.js +157 -27
- package/package.json +7 -2
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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) :
|
|
566
|
-
requirementsReport ? Promise.resolve(requirementsReport) :
|
|
567
|
-
riskReport ? Promise.resolve(riskReport) :
|
|
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 (
|
|
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
|
-
|
|
665
|
-
|
|
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
|
|
705
|
-
|
|
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.
|
|
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",
|