kernelbot 1.0.26 → 1.0.30
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/.env.example +4 -0
- package/README.md +198 -124
- package/bin/kernel.js +208 -4
- package/config.example.yaml +14 -1
- package/package.json +1 -1
- package/src/agent.js +839 -209
- package/src/automation/automation-manager.js +377 -0
- package/src/automation/automation.js +79 -0
- package/src/automation/index.js +2 -0
- package/src/automation/scheduler.js +141 -0
- package/src/bot.js +1001 -18
- package/src/claude-auth.js +93 -0
- package/src/coder.js +48 -6
- package/src/conversation.js +33 -0
- package/src/intents/detector.js +50 -0
- package/src/intents/index.js +2 -0
- package/src/intents/planner.js +58 -0
- package/src/persona.js +68 -0
- package/src/prompts/orchestrator.js +124 -0
- package/src/prompts/persona.md +21 -0
- package/src/prompts/system.js +59 -6
- package/src/prompts/workers.js +148 -0
- package/src/providers/anthropic.js +23 -16
- package/src/providers/base.js +76 -2
- package/src/providers/index.js +1 -0
- package/src/providers/models.js +2 -1
- package/src/providers/openai-compat.js +5 -3
- package/src/security/audit.js +0 -0
- package/src/security/auth.js +0 -0
- package/src/security/confirm.js +7 -2
- package/src/self.js +122 -0
- package/src/services/stt.js +139 -0
- package/src/services/tts.js +124 -0
- package/src/skills/catalog.js +506 -0
- package/src/skills/custom.js +128 -0
- package/src/swarm/job-manager.js +216 -0
- package/src/swarm/job.js +85 -0
- package/src/swarm/worker-registry.js +79 -0
- package/src/tools/browser.js +458 -335
- package/src/tools/categories.js +3 -3
- package/src/tools/coding.js +5 -0
- package/src/tools/docker.js +0 -0
- package/src/tools/git.js +0 -0
- package/src/tools/github.js +0 -0
- package/src/tools/index.js +3 -0
- package/src/tools/jira.js +0 -0
- package/src/tools/monitor.js +0 -0
- package/src/tools/network.js +0 -0
- package/src/tools/orchestrator-tools.js +428 -0
- package/src/tools/os.js +14 -1
- package/src/tools/persona.js +32 -0
- package/src/tools/process.js +0 -0
- package/src/utils/config.js +153 -15
- package/src/utils/display.js +0 -0
- package/src/utils/logger.js +0 -0
- package/src/worker.js +396 -0
- package/.agents/skills/interface-design/SKILL.md +0 -391
- package/.agents/skills/interface-design/references/critique.md +0 -67
- package/.agents/skills/interface-design/references/example.md +0 -86
- package/.agents/skills/interface-design/references/principles.md +0 -235
- package/.agents/skills/interface-design/references/validation.md +0 -48
package/src/utils/config.js
CHANGED
|
@@ -12,12 +12,23 @@ const DEFAULTS = {
|
|
|
12
12
|
name: 'KernelBot',
|
|
13
13
|
description: 'AI engineering agent with full OS control',
|
|
14
14
|
},
|
|
15
|
+
orchestrator: {
|
|
16
|
+
provider: 'anthropic',
|
|
17
|
+
model: 'claude-opus-4-6',
|
|
18
|
+
max_tokens: 2048,
|
|
19
|
+
temperature: 0.3,
|
|
20
|
+
max_tool_depth: 5,
|
|
21
|
+
},
|
|
15
22
|
brain: {
|
|
16
23
|
provider: 'anthropic',
|
|
17
24
|
model: 'claude-sonnet-4-20250514',
|
|
18
25
|
max_tokens: 4096,
|
|
19
26
|
temperature: 0.3,
|
|
20
|
-
|
|
27
|
+
},
|
|
28
|
+
swarm: {
|
|
29
|
+
max_concurrent_jobs: 3,
|
|
30
|
+
job_timeout_seconds: 300,
|
|
31
|
+
cleanup_interval_minutes: 30,
|
|
21
32
|
},
|
|
22
33
|
telegram: {
|
|
23
34
|
allowed_users: [],
|
|
@@ -28,6 +39,7 @@ const DEFAULTS = {
|
|
|
28
39
|
max_turns: 50,
|
|
29
40
|
timeout_seconds: 600,
|
|
30
41
|
workspace_dir: null, // defaults to ~/.kernelbot/workspaces
|
|
42
|
+
auth_mode: 'system', // system | api_key | oauth_token
|
|
31
43
|
},
|
|
32
44
|
github: {
|
|
33
45
|
default_branch: 'main',
|
|
@@ -180,34 +192,132 @@ export function saveProviderToYaml(providerKey, modelId) {
|
|
|
180
192
|
return configPath;
|
|
181
193
|
}
|
|
182
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Save orchestrator provider and model to config.yaml.
|
|
197
|
+
*/
|
|
198
|
+
export function saveOrchestratorToYaml(providerKey, modelId) {
|
|
199
|
+
const configDir = getConfigDir();
|
|
200
|
+
mkdirSync(configDir, { recursive: true });
|
|
201
|
+
const configPath = join(configDir, 'config.yaml');
|
|
202
|
+
|
|
203
|
+
let existing = {};
|
|
204
|
+
if (existsSync(configPath)) {
|
|
205
|
+
existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
existing.orchestrator = {
|
|
209
|
+
...(existing.orchestrator || {}),
|
|
210
|
+
provider: providerKey,
|
|
211
|
+
model: modelId,
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
|
|
215
|
+
return configPath;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Save Claude Code model to config.yaml.
|
|
220
|
+
*/
|
|
221
|
+
export function saveClaudeCodeModelToYaml(modelId) {
|
|
222
|
+
const configDir = getConfigDir();
|
|
223
|
+
mkdirSync(configDir, { recursive: true });
|
|
224
|
+
const configPath = join(configDir, 'config.yaml');
|
|
225
|
+
|
|
226
|
+
let existing = {};
|
|
227
|
+
if (existsSync(configPath)) {
|
|
228
|
+
existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
existing.claude_code = {
|
|
232
|
+
...(existing.claude_code || {}),
|
|
233
|
+
model: modelId,
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
|
|
237
|
+
return configPath;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Save Claude Code auth mode + credential to config.yaml and .env.
|
|
242
|
+
*/
|
|
243
|
+
export function saveClaudeCodeAuth(config, mode, value) {
|
|
244
|
+
const configDir = getConfigDir();
|
|
245
|
+
mkdirSync(configDir, { recursive: true });
|
|
246
|
+
const configPath = join(configDir, 'config.yaml');
|
|
247
|
+
|
|
248
|
+
let existing = {};
|
|
249
|
+
if (existsSync(configPath)) {
|
|
250
|
+
existing = yaml.load(readFileSync(configPath, 'utf-8')) || {};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
existing.claude_code = {
|
|
254
|
+
...(existing.claude_code || {}),
|
|
255
|
+
auth_mode: mode,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
writeFileSync(configPath, yaml.dump(existing, { lineWidth: -1 }));
|
|
259
|
+
|
|
260
|
+
// Update live config
|
|
261
|
+
config.claude_code.auth_mode = mode;
|
|
262
|
+
|
|
263
|
+
if (mode === 'api_key' && value) {
|
|
264
|
+
saveCredential(config, 'CLAUDE_CODE_API_KEY', value);
|
|
265
|
+
config.claude_code.api_key = value;
|
|
266
|
+
} else if (mode === 'oauth_token' && value) {
|
|
267
|
+
saveCredential(config, 'CLAUDE_CODE_OAUTH_TOKEN', value);
|
|
268
|
+
config.claude_code.oauth_token = value;
|
|
269
|
+
}
|
|
270
|
+
// mode === 'system' — no credentials to save
|
|
271
|
+
}
|
|
272
|
+
|
|
183
273
|
/**
|
|
184
274
|
* Full interactive flow: change brain model + optionally enter API key.
|
|
185
275
|
*/
|
|
186
276
|
export async function changeBrainModel(config, rl) {
|
|
277
|
+
const { createProvider } = await import('../providers/index.js');
|
|
187
278
|
const { providerKey, modelId } = await promptProviderSelection(rl);
|
|
188
279
|
|
|
189
280
|
const providerDef = PROVIDERS[providerKey];
|
|
281
|
+
|
|
282
|
+
// Resolve API key
|
|
283
|
+
const envKey = providerDef.envKey;
|
|
284
|
+
let apiKey = process.env[envKey];
|
|
285
|
+
if (!apiKey) {
|
|
286
|
+
const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key (${envKey}): `));
|
|
287
|
+
if (!key.trim()) {
|
|
288
|
+
console.log(chalk.yellow('\n No API key provided. Brain not changed.\n'));
|
|
289
|
+
return config;
|
|
290
|
+
}
|
|
291
|
+
apiKey = key.trim();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Validate the new provider before saving anything
|
|
295
|
+
console.log(chalk.dim(`\n Verifying ${providerDef.name} / ${modelId}...`));
|
|
296
|
+
const testConfig = { ...config, brain: { ...config.brain, provider: providerKey, model: modelId, api_key: apiKey } };
|
|
297
|
+
try {
|
|
298
|
+
const testProvider = createProvider(testConfig);
|
|
299
|
+
await testProvider.ping();
|
|
300
|
+
} catch (err) {
|
|
301
|
+
console.log(chalk.red(`\n ✖ Verification failed: ${err.message}`));
|
|
302
|
+
console.log(chalk.yellow(` Brain not changed. Keeping current model.\n`));
|
|
303
|
+
return config;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Validation passed — save everything
|
|
190
307
|
const savedPath = saveProviderToYaml(providerKey, modelId);
|
|
191
|
-
console.log(chalk.dim(
|
|
308
|
+
console.log(chalk.dim(` Saved to ${savedPath}`));
|
|
192
309
|
|
|
193
|
-
// Update live config
|
|
194
310
|
config.brain.provider = providerKey;
|
|
195
311
|
config.brain.model = modelId;
|
|
312
|
+
config.brain.api_key = apiKey;
|
|
196
313
|
|
|
197
|
-
//
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const key = await ask(rl, chalk.cyan(`\n ${providerDef.name} API key (${envKey}): `));
|
|
202
|
-
if (key.trim()) {
|
|
203
|
-
saveCredential(config, envKey, key.trim());
|
|
204
|
-
config.brain.api_key = key.trim();
|
|
205
|
-
console.log(chalk.dim(' Saved.\n'));
|
|
206
|
-
}
|
|
207
|
-
} else {
|
|
208
|
-
config.brain.api_key = currentKey;
|
|
314
|
+
// Save the key if it was newly entered
|
|
315
|
+
if (!process.env[envKey]) {
|
|
316
|
+
saveCredential(config, envKey, apiKey);
|
|
317
|
+
console.log(chalk.dim(' API key saved.\n'));
|
|
209
318
|
}
|
|
210
319
|
|
|
320
|
+
console.log(chalk.green(` ✔ Brain switched to ${providerDef.name} / ${modelId}\n`));
|
|
211
321
|
return config;
|
|
212
322
|
}
|
|
213
323
|
|
|
@@ -299,6 +409,16 @@ export function loadConfig() {
|
|
|
299
409
|
|
|
300
410
|
const config = deepMerge(DEFAULTS, fileConfig);
|
|
301
411
|
|
|
412
|
+
// Orchestrator — resolve API key based on configured provider
|
|
413
|
+
const orchProvider = PROVIDERS[config.orchestrator.provider];
|
|
414
|
+
if (orchProvider && process.env[orchProvider.envKey]) {
|
|
415
|
+
config.orchestrator.api_key = process.env[orchProvider.envKey];
|
|
416
|
+
}
|
|
417
|
+
// Legacy fallback: ANTHROPIC_API_KEY for anthropic orchestrator
|
|
418
|
+
if (config.orchestrator.provider === 'anthropic' && !config.orchestrator.api_key && process.env.ANTHROPIC_API_KEY) {
|
|
419
|
+
config.orchestrator.api_key = process.env.ANTHROPIC_API_KEY;
|
|
420
|
+
}
|
|
421
|
+
|
|
302
422
|
// Overlay env vars for brain API key based on provider
|
|
303
423
|
const providerDef = PROVIDERS[config.brain.provider];
|
|
304
424
|
if (providerDef && process.env[providerDef.envKey]) {
|
|
@@ -316,6 +436,16 @@ export function loadConfig() {
|
|
|
316
436
|
if (!config.github) config.github = {};
|
|
317
437
|
config.github.token = process.env.GITHUB_TOKEN;
|
|
318
438
|
}
|
|
439
|
+
// ElevenLabs voice credentials
|
|
440
|
+
if (process.env.ELEVENLABS_API_KEY) {
|
|
441
|
+
if (!config.elevenlabs) config.elevenlabs = {};
|
|
442
|
+
config.elevenlabs.api_key = process.env.ELEVENLABS_API_KEY;
|
|
443
|
+
}
|
|
444
|
+
if (process.env.ELEVENLABS_VOICE_ID) {
|
|
445
|
+
if (!config.elevenlabs) config.elevenlabs = {};
|
|
446
|
+
config.elevenlabs.voice_id = process.env.ELEVENLABS_VOICE_ID;
|
|
447
|
+
}
|
|
448
|
+
|
|
319
449
|
if (process.env.JIRA_BASE_URL || process.env.JIRA_EMAIL || process.env.JIRA_API_TOKEN) {
|
|
320
450
|
if (!config.jira) config.jira = {};
|
|
321
451
|
if (process.env.JIRA_BASE_URL) config.jira.base_url = process.env.JIRA_BASE_URL;
|
|
@@ -323,6 +453,14 @@ export function loadConfig() {
|
|
|
323
453
|
if (process.env.JIRA_API_TOKEN) config.jira.api_token = process.env.JIRA_API_TOKEN;
|
|
324
454
|
}
|
|
325
455
|
|
|
456
|
+
// Claude Code auth credentials from env
|
|
457
|
+
if (process.env.CLAUDE_CODE_API_KEY) {
|
|
458
|
+
config.claude_code.api_key = process.env.CLAUDE_CODE_API_KEY;
|
|
459
|
+
}
|
|
460
|
+
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
461
|
+
config.claude_code.oauth_token = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
462
|
+
}
|
|
463
|
+
|
|
326
464
|
return config;
|
|
327
465
|
}
|
|
328
466
|
|
package/src/utils/display.js
CHANGED
|
File without changes
|
package/src/utils/logger.js
CHANGED
|
File without changes
|
package/src/worker.js
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { createProvider } from './providers/index.js';
|
|
2
|
+
import { executeTool } from './tools/index.js';
|
|
3
|
+
import { closeSession } from './tools/browser.js';
|
|
4
|
+
import { getMissingCredential } from './utils/config.js';
|
|
5
|
+
import { getWorkerPrompt } from './prompts/workers.js';
|
|
6
|
+
import { getUnifiedSkillById } from './skills/custom.js';
|
|
7
|
+
import { getLogger } from './utils/logger.js';
|
|
8
|
+
|
|
9
|
+
const MAX_RESULT_LENGTH = 3000;
|
|
10
|
+
const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* WorkerAgent — runs a scoped agent loop in the background.
|
|
14
|
+
* Extracted from Agent._runLoop() with simplifications:
|
|
15
|
+
* - No conversation persistence
|
|
16
|
+
* - No intent detection or persona extraction
|
|
17
|
+
* - No completion gate
|
|
18
|
+
* - Checks cancellation before each iteration and tool execution
|
|
19
|
+
* - Reports progress via callbacks
|
|
20
|
+
*/
|
|
21
|
+
export class WorkerAgent {
|
|
22
|
+
/**
|
|
23
|
+
* @param {object} opts
|
|
24
|
+
* @param {object} opts.config - Full app config (opts.config.brain used for LLM)
|
|
25
|
+
* @param {string} opts.workerType - coding, browser, system, devops, research
|
|
26
|
+
* @param {string} opts.jobId - Job ID for logging
|
|
27
|
+
* @param {Array} opts.tools - Scoped tool definitions
|
|
28
|
+
* @param {string|null} opts.skillId - Active skill ID (for worker prompt)
|
|
29
|
+
* @param {string|null} opts.workerContext - Structured context (conversation history, persona, dependency results)
|
|
30
|
+
* @param {object} opts.callbacks - { onProgress, onComplete, onError }
|
|
31
|
+
* @param {AbortController} opts.abortController - For cancellation
|
|
32
|
+
*/
|
|
33
|
+
constructor({ config, workerType, jobId, tools, skillId, workerContext, callbacks, abortController }) {
|
|
34
|
+
this.config = config;
|
|
35
|
+
this.workerType = workerType;
|
|
36
|
+
this.jobId = jobId;
|
|
37
|
+
this.tools = tools;
|
|
38
|
+
this.skillId = skillId;
|
|
39
|
+
this.workerContext = workerContext || null;
|
|
40
|
+
this.callbacks = callbacks || {};
|
|
41
|
+
this.abortController = abortController || new AbortController();
|
|
42
|
+
this._cancelled = false;
|
|
43
|
+
this._toolCallCount = 0;
|
|
44
|
+
this._errors = [];
|
|
45
|
+
|
|
46
|
+
// Create provider from worker brain config
|
|
47
|
+
this.provider = createProvider(config);
|
|
48
|
+
|
|
49
|
+
// Build system prompt
|
|
50
|
+
const skillPrompt = skillId ? getUnifiedSkillById(skillId)?.systemPrompt : null;
|
|
51
|
+
this.systemPrompt = getWorkerPrompt(workerType, config, skillPrompt);
|
|
52
|
+
|
|
53
|
+
// Safety ceiling — not a real limit, just prevents infinite loops
|
|
54
|
+
// The real limit is the job timeout enforced by JobManager
|
|
55
|
+
this.maxIterations = 200;
|
|
56
|
+
|
|
57
|
+
const logger = getLogger();
|
|
58
|
+
logger.info(`[Worker ${jobId}] Created: type=${workerType}, provider=${config.brain.provider}/${config.brain.model}, tools=${tools.length}, skill=${skillId || 'none'}, context=${workerContext ? 'yes' : 'none'}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Cancel this worker. */
|
|
62
|
+
cancel() {
|
|
63
|
+
this._cancelled = true;
|
|
64
|
+
this.abortController.abort();
|
|
65
|
+
getLogger().info(`[Worker ${this.jobId}] Cancel signal sent — aborting ${this.workerType} worker`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Run the worker loop with the given task. */
|
|
69
|
+
async run(task) {
|
|
70
|
+
const logger = getLogger();
|
|
71
|
+
logger.info(`[Worker ${this.jobId}] Starting task: "${task.slice(0, 150)}"`);
|
|
72
|
+
|
|
73
|
+
// Build first message: context sections + task
|
|
74
|
+
let firstMessage = '';
|
|
75
|
+
if (this.workerContext) {
|
|
76
|
+
firstMessage += this.workerContext + '\n\n---\n\n';
|
|
77
|
+
}
|
|
78
|
+
firstMessage += task;
|
|
79
|
+
|
|
80
|
+
const messages = [{ role: 'user', content: firstMessage }];
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const result = await this._runLoop(messages);
|
|
84
|
+
if (this._cancelled) {
|
|
85
|
+
logger.info(`[Worker ${this.jobId}] Run completed but worker was cancelled — skipping callbacks`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const parsed = this._parseResult(result);
|
|
89
|
+
logger.info(`[Worker ${this.jobId}] Run finished successfully — structured=${!!parsed.structured}, result: "${(result || '').slice(0, 150)}"`);
|
|
90
|
+
if (this.callbacks.onComplete) this.callbacks.onComplete(result, parsed);
|
|
91
|
+
} catch (err) {
|
|
92
|
+
if (this._cancelled) {
|
|
93
|
+
logger.info(`[Worker ${this.jobId}] Run threw error but worker was cancelled — ignoring: ${err.message}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
logger.error(`[Worker ${this.jobId}] Run failed: ${err.message}`);
|
|
97
|
+
if (this.callbacks.onError) this.callbacks.onError(err);
|
|
98
|
+
} finally {
|
|
99
|
+
// Clean up browser session for this worker (frees the Puppeteer page)
|
|
100
|
+
closeSession(this.jobId).catch(() => {});
|
|
101
|
+
logger.info(`[Worker ${this.jobId}] Browser session cleaned up`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async _runLoop(messages) {
|
|
106
|
+
const logger = getLogger();
|
|
107
|
+
let consecutiveAllFailIterations = 0; // Track iterations where ALL tool calls fail
|
|
108
|
+
|
|
109
|
+
for (let depth = 0; depth < this.maxIterations; depth++) {
|
|
110
|
+
if (this._cancelled) {
|
|
111
|
+
logger.info(`[Worker ${this.jobId}] Cancelled before iteration ${depth + 1}`);
|
|
112
|
+
throw new Error('Worker cancelled');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
logger.info(`[Worker ${this.jobId}] LLM call ${depth + 1} — sending ${messages.length} messages`);
|
|
116
|
+
|
|
117
|
+
const response = await this.provider.chat({
|
|
118
|
+
system: this.systemPrompt,
|
|
119
|
+
messages,
|
|
120
|
+
tools: this.tools,
|
|
121
|
+
signal: this.abortController.signal,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
logger.info(`[Worker ${this.jobId}] LLM response: stopReason=${response.stopReason}, text=${(response.text || '').length} chars, toolCalls=${(response.toolCalls || []).length}`);
|
|
125
|
+
|
|
126
|
+
if (this._cancelled) {
|
|
127
|
+
logger.info(`[Worker ${this.jobId}] Cancelled after LLM response`);
|
|
128
|
+
throw new Error('Worker cancelled');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// End turn — return the text
|
|
132
|
+
if (response.stopReason === 'end_turn') {
|
|
133
|
+
logger.info(`[Worker ${this.jobId}] End turn — final response: "${(response.text || '').slice(0, 200)}"`);
|
|
134
|
+
return response.text || 'Task completed.';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Tool use
|
|
138
|
+
if (response.stopReason === 'tool_use') {
|
|
139
|
+
messages.push({ role: 'assistant', content: response.rawContent });
|
|
140
|
+
|
|
141
|
+
// Log thinking text
|
|
142
|
+
if (response.text && response.text.trim()) {
|
|
143
|
+
logger.info(`[Worker ${this.jobId}] Thinking: "${response.text.slice(0, 200)}"`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const toolResults = [];
|
|
147
|
+
|
|
148
|
+
for (const block of response.toolCalls) {
|
|
149
|
+
if (this._cancelled) {
|
|
150
|
+
logger.info(`[Worker ${this.jobId}] Cancelled before executing tool ${block.name}`);
|
|
151
|
+
throw new Error('Worker cancelled');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const summary = this._formatToolSummary(block.name, block.input);
|
|
155
|
+
logger.info(`[Worker ${this.jobId}] Executing tool: ${block.name} — ${summary}`);
|
|
156
|
+
logger.debug(`[Worker ${this.jobId}] Tool input: ${JSON.stringify(block.input).slice(0, 300)}`);
|
|
157
|
+
this._reportProgress(`🔧 ${summary}`);
|
|
158
|
+
|
|
159
|
+
this._toolCallCount++;
|
|
160
|
+
|
|
161
|
+
const result = await executeTool(block.name, block.input, {
|
|
162
|
+
config: this.config,
|
|
163
|
+
user: null, // workers don't have user context
|
|
164
|
+
personaManager: null,
|
|
165
|
+
onUpdate: this.callbacks.onUpdate || null, // Real bot onUpdate (returns message_id for coder.js smart output)
|
|
166
|
+
sendPhoto: this.callbacks.sendPhoto || null,
|
|
167
|
+
sessionId: this.jobId, // Per-worker browser session isolation
|
|
168
|
+
signal: this.abortController.signal, // For killing child processes on cancellation
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Track errors
|
|
172
|
+
if (result && typeof result === 'object' && result.error) {
|
|
173
|
+
this._errors.push({ tool: block.name, error: result.error });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const resultStr = this._truncateResult(block.name, result);
|
|
177
|
+
logger.info(`[Worker ${this.jobId}] Tool ${block.name} result: ${resultStr.slice(0, 200)}`);
|
|
178
|
+
|
|
179
|
+
toolResults.push({
|
|
180
|
+
type: 'tool_result',
|
|
181
|
+
tool_use_id: block.id,
|
|
182
|
+
content: resultStr,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Track consecutive all-fail iterations (circuit breaker)
|
|
187
|
+
const allFailed = toolResults.every(tr => {
|
|
188
|
+
try { const parsed = JSON.parse(tr.content); return !!parsed.error; } catch { return false; }
|
|
189
|
+
});
|
|
190
|
+
if (allFailed) {
|
|
191
|
+
consecutiveAllFailIterations++;
|
|
192
|
+
logger.warn(`[Worker ${this.jobId}] All ${toolResults.length} tool calls failed (streak: ${consecutiveAllFailIterations})`);
|
|
193
|
+
if (consecutiveAllFailIterations >= 3) {
|
|
194
|
+
logger.warn(`[Worker ${this.jobId}] Circuit breaker: 3 consecutive all-fail iterations — forcing stop`);
|
|
195
|
+
messages.push({ role: 'user', content: toolResults });
|
|
196
|
+
messages.push({
|
|
197
|
+
role: 'user',
|
|
198
|
+
content: 'STOP: All your tool calls have failed 3 times in a row. Do NOT call any more tools. Summarize whatever you have found so far, or explain what went wrong.',
|
|
199
|
+
});
|
|
200
|
+
const bailResponse = await this.provider.chat({
|
|
201
|
+
system: this.systemPrompt,
|
|
202
|
+
messages,
|
|
203
|
+
tools: [], // No tools — force text response
|
|
204
|
+
signal: this.abortController.signal,
|
|
205
|
+
});
|
|
206
|
+
return bailResponse.text || 'All tool calls failed repeatedly. Could not complete the task.';
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
consecutiveAllFailIterations = 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
messages.push({ role: 'user', content: toolResults });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Unexpected stop reason
|
|
217
|
+
logger.warn(`[Worker ${this.jobId}] Unexpected stopReason: ${response.stopReason}`);
|
|
218
|
+
return response.text || 'Worker finished with unexpected response.';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Safety ceiling hit (should basically never happen — job timeout is the real limit)
|
|
222
|
+
logger.warn(`[Worker ${this.jobId}] Hit safety ceiling (${this.maxIterations} iterations) — requesting final summary`);
|
|
223
|
+
this._reportProgress(`⏳ Summarizing results...`);
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
messages.push({
|
|
227
|
+
role: 'user',
|
|
228
|
+
content: 'You have reached the iteration limit. Summarize everything you have found and accomplished so far. Return a complete, detailed summary of all results, data, and findings.',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const summaryResponse = await this.provider.chat({
|
|
232
|
+
system: this.systemPrompt,
|
|
233
|
+
messages,
|
|
234
|
+
tools: [], // No tools — force text-only response
|
|
235
|
+
signal: this.abortController.signal,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const summary = summaryResponse.text || '';
|
|
239
|
+
logger.info(`[Worker ${this.jobId}] Final summary: "${summary.slice(0, 200)}"`);
|
|
240
|
+
|
|
241
|
+
if (summary.length > 10) {
|
|
242
|
+
return summary;
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
logger.warn(`[Worker ${this.jobId}] Summary call failed: ${err.message}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Fallback: extract any text the LLM produced during the loop
|
|
249
|
+
const lastAssistantText = this._extractLastAssistantText(messages);
|
|
250
|
+
if (lastAssistantText) {
|
|
251
|
+
logger.info(`[Worker ${this.jobId}] Falling back to last assistant text: "${lastAssistantText.slice(0, 200)}"`);
|
|
252
|
+
return lastAssistantText;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return 'Worker finished but could not produce a final summary.';
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Extract the last meaningful assistant text from message history. */
|
|
259
|
+
_extractLastAssistantText(messages) {
|
|
260
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
261
|
+
const msg = messages[i];
|
|
262
|
+
if (msg.role !== 'assistant') continue;
|
|
263
|
+
|
|
264
|
+
if (typeof msg.content === 'string' && msg.content.trim()) {
|
|
265
|
+
return msg.content.trim();
|
|
266
|
+
}
|
|
267
|
+
if (Array.isArray(msg.content)) {
|
|
268
|
+
const texts = msg.content
|
|
269
|
+
.filter(b => b.type === 'text' && b.text?.trim())
|
|
270
|
+
.map(b => b.text.trim());
|
|
271
|
+
if (texts.length > 0) return texts.join('\n');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Parse the worker's final text into a structured WorkerResult.
|
|
279
|
+
* Attempts JSON parse from ```json fences, falls back to wrapping raw text.
|
|
280
|
+
*/
|
|
281
|
+
_parseResult(text) {
|
|
282
|
+
if (!text) {
|
|
283
|
+
return {
|
|
284
|
+
structured: false,
|
|
285
|
+
summary: 'Task completed.',
|
|
286
|
+
status: 'success',
|
|
287
|
+
details: '',
|
|
288
|
+
artifacts: [],
|
|
289
|
+
followUp: null,
|
|
290
|
+
toolsUsed: this._toolCallCount,
|
|
291
|
+
errors: this._errors,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Try to extract JSON from ```json ... ``` fences
|
|
296
|
+
const fenceMatch = text.match(/```json\s*\n?([\s\S]*?)\n?\s*```/);
|
|
297
|
+
if (fenceMatch) {
|
|
298
|
+
try {
|
|
299
|
+
const parsed = JSON.parse(fenceMatch[1]);
|
|
300
|
+
if (parsed.summary && parsed.status) {
|
|
301
|
+
return {
|
|
302
|
+
structured: true,
|
|
303
|
+
summary: parsed.summary || '',
|
|
304
|
+
status: parsed.status || 'success',
|
|
305
|
+
details: parsed.details || '',
|
|
306
|
+
artifacts: Array.isArray(parsed.artifacts) ? parsed.artifacts : [],
|
|
307
|
+
followUp: parsed.followUp || null,
|
|
308
|
+
toolsUsed: this._toolCallCount,
|
|
309
|
+
errors: this._errors,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
} catch { /* fall through */ }
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Try raw JSON parse (no fences)
|
|
316
|
+
try {
|
|
317
|
+
const parsed = JSON.parse(text);
|
|
318
|
+
if (parsed.summary && parsed.status) {
|
|
319
|
+
return {
|
|
320
|
+
structured: true,
|
|
321
|
+
summary: parsed.summary || '',
|
|
322
|
+
status: parsed.status || 'success',
|
|
323
|
+
details: parsed.details || '',
|
|
324
|
+
artifacts: Array.isArray(parsed.artifacts) ? parsed.artifacts : [],
|
|
325
|
+
followUp: parsed.followUp || null,
|
|
326
|
+
toolsUsed: this._toolCallCount,
|
|
327
|
+
errors: this._errors,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
} catch { /* fall through */ }
|
|
331
|
+
|
|
332
|
+
// Fallback: wrap raw text
|
|
333
|
+
return {
|
|
334
|
+
structured: false,
|
|
335
|
+
summary: text.slice(0, 200),
|
|
336
|
+
status: 'success',
|
|
337
|
+
details: text,
|
|
338
|
+
artifacts: [],
|
|
339
|
+
followUp: null,
|
|
340
|
+
toolsUsed: this._toolCallCount,
|
|
341
|
+
errors: this._errors,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
_reportProgress(text) {
|
|
346
|
+
if (this.callbacks.onProgress) {
|
|
347
|
+
try { this.callbacks.onProgress(text); } catch {}
|
|
348
|
+
}
|
|
349
|
+
if (this.callbacks.onHeartbeat) {
|
|
350
|
+
try { this.callbacks.onHeartbeat(text); } catch {}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
_truncateResult(name, result) {
|
|
355
|
+
let str = JSON.stringify(result);
|
|
356
|
+
if (str.length <= MAX_RESULT_LENGTH) return str;
|
|
357
|
+
|
|
358
|
+
if (result && typeof result === 'object') {
|
|
359
|
+
const truncated = { ...result };
|
|
360
|
+
for (const field of LARGE_FIELDS) {
|
|
361
|
+
if (typeof truncated[field] === 'string' && truncated[field].length > 500) {
|
|
362
|
+
truncated[field] = truncated[field].slice(0, 500) + `\n... [truncated ${truncated[field].length - 500} chars]`;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
str = JSON.stringify(truncated);
|
|
366
|
+
if (str.length <= MAX_RESULT_LENGTH) return str;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
_formatToolSummary(name, input) {
|
|
373
|
+
const _short = (s, len = 80) => s && s.length > len ? s.slice(0, len) + '...' : s;
|
|
374
|
+
const _host = (url) => { try { return new URL(url).hostname; } catch { return url; } };
|
|
375
|
+
|
|
376
|
+
switch (name) {
|
|
377
|
+
case 'web_search': return `Searching: "${_short(input.query, 60)}"`;
|
|
378
|
+
case 'browse_website': return `Opening ${_host(input.url)}`;
|
|
379
|
+
case 'interact_with_page': return 'Interacting with page';
|
|
380
|
+
case 'extract_content': return 'Extracting content';
|
|
381
|
+
case 'screenshot_website': return `Screenshot of ${_host(input.url)}`;
|
|
382
|
+
case 'execute_command': return `Running: ${_short(input.command, 60)}`;
|
|
383
|
+
case 'read_file': return `Reading ${_short(input.path)}`;
|
|
384
|
+
case 'write_file': return `Writing ${_short(input.path)}`;
|
|
385
|
+
case 'git_clone': return `Cloning ${_short(input.repo)}`;
|
|
386
|
+
case 'git_checkout': return `Switching to ${input.branch}`;
|
|
387
|
+
case 'git_commit': return `Committing: "${_short(input.message, 50)}"`;
|
|
388
|
+
case 'git_push': return 'Pushing changes';
|
|
389
|
+
case 'github_create_pr': return `Creating PR: "${_short(input.title, 50)}"`;
|
|
390
|
+
case 'spawn_claude_code': return `Coding: ${_short(input.prompt, 60)}`;
|
|
391
|
+
case 'docker_exec': return `Running in ${_short(input.container)}`;
|
|
392
|
+
case 'docker_compose': return `Docker compose ${input.action}`;
|
|
393
|
+
default: return `${name}`;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|