@workermill/agent 0.7.17 → 0.7.18
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/planner.js +78 -4
- package/dist/spawner.js +11 -0
- package/package.json +1 -1
package/dist/planner.js
CHANGED
|
@@ -20,6 +20,50 @@ import { findClaudePath } from "./config.js";
|
|
|
20
20
|
import { api } from "./api.js";
|
|
21
21
|
import { parseExecutionPlan, applyFileCap, applyStoryCap, resolveFileOverlaps, serializePlan, runCriticValidation, formatCriticFeedback, AUTO_APPROVAL_THRESHOLD, } from "./plan-validator.js";
|
|
22
22
|
import { generateTextWithTools } from "./ai-sdk-generate.js";
|
|
23
|
+
/**
|
|
24
|
+
* Extract token usage from a stream-json event.
|
|
25
|
+
* Claude reports cumulative tokens, so we use Math.max to track the highest values.
|
|
26
|
+
*/
|
|
27
|
+
function extractTokenUsage(event, usage) {
|
|
28
|
+
const paths = [
|
|
29
|
+
event.usage,
|
|
30
|
+
event.message?.usage,
|
|
31
|
+
event.result?.usage,
|
|
32
|
+
];
|
|
33
|
+
for (const u of paths) {
|
|
34
|
+
if (u && typeof u === "object") {
|
|
35
|
+
const d = u;
|
|
36
|
+
if (typeof d.input_tokens === "number")
|
|
37
|
+
usage.inputTokens = Math.max(usage.inputTokens, d.input_tokens);
|
|
38
|
+
if (typeof d.output_tokens === "number")
|
|
39
|
+
usage.outputTokens = Math.max(usage.outputTokens, d.output_tokens);
|
|
40
|
+
if (typeof d.cache_creation_input_tokens === "number")
|
|
41
|
+
usage.cacheCreationTokens = Math.max(usage.cacheCreationTokens, d.cache_creation_input_tokens);
|
|
42
|
+
if (typeof d.cache_read_input_tokens === "number")
|
|
43
|
+
usage.cacheReadTokens = Math.max(usage.cacheReadTokens, d.cache_read_input_tokens);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Report partial token usage to the cloud API.
|
|
49
|
+
*/
|
|
50
|
+
async function reportPlanningUsage(taskId, usage, model, mode) {
|
|
51
|
+
if (usage.inputTokens === 0 && usage.outputTokens === 0)
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
await api.post(`/api/tasks/${taskId}/usage/partial`, {
|
|
55
|
+
inputTokens: usage.inputTokens,
|
|
56
|
+
outputTokens: usage.outputTokens,
|
|
57
|
+
cacheCreationTokens: usage.cacheCreationTokens,
|
|
58
|
+
cacheReadTokens: usage.cacheReadTokens,
|
|
59
|
+
model,
|
|
60
|
+
mode,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Fire and forget
|
|
65
|
+
}
|
|
66
|
+
}
|
|
23
67
|
/** Max Planner-Critic iterations before giving up */
|
|
24
68
|
const MAX_ITERATIONS = 3;
|
|
25
69
|
/** Timestamp prefix */
|
|
@@ -83,16 +127,22 @@ function phaseLabel(phase, elapsed) {
|
|
|
83
127
|
* Run Claude CLI with stream-json output, posting real-time phase milestones
|
|
84
128
|
* to the cloud dashboard — identical terminal experience to cloud planning.
|
|
85
129
|
*/
|
|
86
|
-
function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
|
|
130
|
+
function runClaudeCli(claudePath, model, prompt, env, taskId, startTime, disableTools = false) {
|
|
87
131
|
const taskLabel = chalk.cyan(taskId.slice(0, 8));
|
|
88
132
|
return new Promise((resolve, reject) => {
|
|
89
|
-
const
|
|
133
|
+
const cliArgs = [
|
|
90
134
|
"--print",
|
|
91
135
|
"--verbose",
|
|
92
136
|
"--output-format", "stream-json",
|
|
93
137
|
"--model", model,
|
|
94
138
|
"--permission-mode", "bypassPermissions",
|
|
95
|
-
]
|
|
139
|
+
];
|
|
140
|
+
// When analysts already explored the repo, strip tools so the planner
|
|
141
|
+
// doesn't waste turns re-exploring — it has all context in the prompt.
|
|
142
|
+
if (disableTools) {
|
|
143
|
+
cliArgs.push("--allowedTools", "");
|
|
144
|
+
}
|
|
145
|
+
const proc = spawn(claudePath, cliArgs, {
|
|
96
146
|
env,
|
|
97
147
|
stdio: ["pipe", "pipe", "pipe"],
|
|
98
148
|
});
|
|
@@ -103,6 +153,9 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
|
|
|
103
153
|
let stderrOutput = "";
|
|
104
154
|
let charsReceived = 0;
|
|
105
155
|
let toolCallCount = 0;
|
|
156
|
+
// Token usage accumulator — extract from stream events using Math.max
|
|
157
|
+
const tokenUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
|
|
158
|
+
let resultModel = model;
|
|
106
159
|
// Buffered text streaming — flush complete lines to dashboard every 1s.
|
|
107
160
|
// LLM deltas are tiny fragments; we accumulate until we see '\n', then
|
|
108
161
|
// a 1s interval flushes all complete lines as log entries. On exit we
|
|
@@ -240,6 +293,16 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
|
|
|
240
293
|
else if (event.type === "result" && event.result) {
|
|
241
294
|
resultText = typeof event.result === "string" ? event.result : "";
|
|
242
295
|
}
|
|
296
|
+
// Extract token usage from any event that carries it
|
|
297
|
+
extractTokenUsage(event, tokenUsage);
|
|
298
|
+
if (event.type === "result" && event.total_cost_usd !== undefined) {
|
|
299
|
+
// Result event also carries model info
|
|
300
|
+
if (event.modelUsage && typeof event.modelUsage === "object") {
|
|
301
|
+
const models = Object.keys(event.modelUsage);
|
|
302
|
+
if (models.length > 0)
|
|
303
|
+
resultModel = models[0];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
243
306
|
}
|
|
244
307
|
catch {
|
|
245
308
|
// Not valid JSON — raw text, accumulate
|
|
@@ -251,10 +314,17 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
|
|
|
251
314
|
proc.stderr.on("data", (chunk) => {
|
|
252
315
|
stderrOutput += chunk.toString();
|
|
253
316
|
});
|
|
317
|
+
// Report partial token usage every 30s during planning
|
|
318
|
+
const usageReportInterval = setInterval(() => {
|
|
319
|
+
if (tokenUsage.inputTokens > 0 || tokenUsage.outputTokens > 0) {
|
|
320
|
+
reportPlanningUsage(taskId, tokenUsage, resultModel, "greatest").catch(() => { });
|
|
321
|
+
}
|
|
322
|
+
}, 30_000);
|
|
254
323
|
function cleanupAll() {
|
|
255
324
|
clearInterval(progressInterval);
|
|
256
325
|
clearInterval(sseProgressInterval);
|
|
257
326
|
clearInterval(textFlushInterval);
|
|
327
|
+
clearInterval(usageReportInterval);
|
|
258
328
|
flushTextBuffer(true);
|
|
259
329
|
}
|
|
260
330
|
const timeout = setTimeout(() => {
|
|
@@ -268,6 +338,8 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
|
|
|
268
338
|
// Emit final "validating" phase to dashboard
|
|
269
339
|
const elapsedAtClose = Math.round((Date.now() - startTime) / 1000);
|
|
270
340
|
postProgress(taskId, "validating", elapsedAtClose, "Validating plan...", charsReceived, toolCallCount);
|
|
341
|
+
// Final usage report
|
|
342
|
+
reportPlanningUsage(taskId, tokenUsage, resultModel, "greatest").catch(() => { });
|
|
271
343
|
if (code !== 0) {
|
|
272
344
|
reject(new Error(`Claude CLI failed (exit ${code}): ${stderrOutput.substring(0, 300)}`));
|
|
273
345
|
}
|
|
@@ -766,7 +838,9 @@ export async function planTask(task, config, credentials) {
|
|
|
766
838
|
let rawOutput;
|
|
767
839
|
try {
|
|
768
840
|
if (isAnthropicPlanning) {
|
|
769
|
-
|
|
841
|
+
// Disable tools when analysts already provided repo context
|
|
842
|
+
const hasAnalystContext = enhancedBasePrompt !== basePrompt;
|
|
843
|
+
rawOutput = await runClaudeCli(claudePath, cliModel, currentPrompt, cleanEnv, task.id, startTime, hasAnalystContext);
|
|
770
844
|
}
|
|
771
845
|
else {
|
|
772
846
|
if (!providerApiKey) {
|
package/dist/spawner.js
CHANGED
|
@@ -140,6 +140,17 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
|
|
|
140
140
|
return;
|
|
141
141
|
}
|
|
142
142
|
if (claudeConfigDir) {
|
|
143
|
+
// Ensure credentials file is readable AND writable inside container.
|
|
144
|
+
// Claude CLI creates .credentials.json with 600 permissions, but the container
|
|
145
|
+
// runs as UID 1001 (worker) while the host user is UID 1000. Without this chmod,
|
|
146
|
+
// the mounted file is unreadable inside the container → "Invalid API key" errors.
|
|
147
|
+
const credFile = path.join(claudeConfigDir, ".credentials.json");
|
|
148
|
+
try {
|
|
149
|
+
fs.chmodSync(credFile, 0o666);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Ignore - file may not exist yet
|
|
153
|
+
}
|
|
143
154
|
const dockerClaudeDir = toDockerPath(claudeConfigDir);
|
|
144
155
|
dockerArgs.push("-v", `${dockerClaudeDir}:/home/worker/.claude`);
|
|
145
156
|
}
|