kernelbot 1.0.26 → 1.0.28
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/README.md +198 -124
- package/bin/kernel.js +201 -4
- package/package.json +1 -1
- package/src/agent.js +397 -222
- 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 +667 -21
- 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 +76 -0
- package/src/prompts/persona.md +21 -0
- package/src/prompts/system.js +59 -6
- package/src/prompts/workers.js +89 -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/confirm.js +7 -2
- package/src/skills/catalog.js +506 -0
- package/src/skills/custom.js +128 -0
- package/src/swarm/job-manager.js +169 -0
- package/src/swarm/job.js +67 -0
- package/src/swarm/worker-registry.js +74 -0
- package/src/tools/browser.js +458 -335
- package/src/tools/categories.js +3 -3
- package/src/tools/index.js +3 -0
- package/src/tools/orchestrator-tools.js +371 -0
- package/src/tools/persona.js +32 -0
- package/src/utils/config.js +50 -15
- package/src/worker.js +305 -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/worker.js
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
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 {object} opts.callbacks - { onProgress, onComplete, onError }
|
|
30
|
+
* @param {AbortController} opts.abortController - For cancellation
|
|
31
|
+
*/
|
|
32
|
+
constructor({ config, workerType, jobId, tools, skillId, callbacks, abortController }) {
|
|
33
|
+
this.config = config;
|
|
34
|
+
this.workerType = workerType;
|
|
35
|
+
this.jobId = jobId;
|
|
36
|
+
this.tools = tools;
|
|
37
|
+
this.skillId = skillId;
|
|
38
|
+
this.callbacks = callbacks || {};
|
|
39
|
+
this.abortController = abortController || new AbortController();
|
|
40
|
+
this._cancelled = false;
|
|
41
|
+
|
|
42
|
+
// Create provider from worker brain config
|
|
43
|
+
this.provider = createProvider(config);
|
|
44
|
+
|
|
45
|
+
// Build system prompt
|
|
46
|
+
const skillPrompt = skillId ? getUnifiedSkillById(skillId)?.systemPrompt : null;
|
|
47
|
+
this.systemPrompt = getWorkerPrompt(workerType, config, skillPrompt);
|
|
48
|
+
|
|
49
|
+
// Safety ceiling — not a real limit, just prevents infinite loops
|
|
50
|
+
// The real limit is the job timeout enforced by JobManager
|
|
51
|
+
this.maxIterations = 200;
|
|
52
|
+
|
|
53
|
+
const logger = getLogger();
|
|
54
|
+
logger.info(`[Worker ${jobId}] Created: type=${workerType}, provider=${config.brain.provider}/${config.brain.model}, tools=${tools.length}, skill=${skillId || 'none'}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Cancel this worker. */
|
|
58
|
+
cancel() {
|
|
59
|
+
this._cancelled = true;
|
|
60
|
+
this.abortController.abort();
|
|
61
|
+
getLogger().info(`[Worker ${this.jobId}] Cancel signal sent — aborting ${this.workerType} worker`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Run the worker loop with the given task. */
|
|
65
|
+
async run(task) {
|
|
66
|
+
const logger = getLogger();
|
|
67
|
+
logger.info(`[Worker ${this.jobId}] Starting task: "${task.slice(0, 150)}"`);
|
|
68
|
+
|
|
69
|
+
const messages = [{ role: 'user', content: task }];
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const result = await this._runLoop(messages);
|
|
73
|
+
if (this._cancelled) {
|
|
74
|
+
logger.info(`[Worker ${this.jobId}] Run completed but worker was cancelled — skipping callbacks`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
logger.info(`[Worker ${this.jobId}] Run finished successfully — result: "${(result || '').slice(0, 150)}"`);
|
|
78
|
+
if (this.callbacks.onComplete) this.callbacks.onComplete(result);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (this._cancelled) {
|
|
81
|
+
logger.info(`[Worker ${this.jobId}] Run threw error but worker was cancelled — ignoring: ${err.message}`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
logger.error(`[Worker ${this.jobId}] Run failed: ${err.message}`);
|
|
85
|
+
if (this.callbacks.onError) this.callbacks.onError(err);
|
|
86
|
+
} finally {
|
|
87
|
+
// Clean up browser session for this worker (frees the Puppeteer page)
|
|
88
|
+
closeSession(this.jobId).catch(() => {});
|
|
89
|
+
logger.info(`[Worker ${this.jobId}] Browser session cleaned up`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async _runLoop(messages) {
|
|
94
|
+
const logger = getLogger();
|
|
95
|
+
let consecutiveAllFailIterations = 0; // Track iterations where ALL tool calls fail
|
|
96
|
+
|
|
97
|
+
for (let depth = 0; depth < this.maxIterations; depth++) {
|
|
98
|
+
if (this._cancelled) {
|
|
99
|
+
logger.info(`[Worker ${this.jobId}] Cancelled before iteration ${depth + 1}`);
|
|
100
|
+
throw new Error('Worker cancelled');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
logger.info(`[Worker ${this.jobId}] LLM call ${depth + 1} — sending ${messages.length} messages`);
|
|
104
|
+
|
|
105
|
+
const response = await this.provider.chat({
|
|
106
|
+
system: this.systemPrompt,
|
|
107
|
+
messages,
|
|
108
|
+
tools: this.tools,
|
|
109
|
+
signal: this.abortController.signal,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
logger.info(`[Worker ${this.jobId}] LLM response: stopReason=${response.stopReason}, text=${(response.text || '').length} chars, toolCalls=${(response.toolCalls || []).length}`);
|
|
113
|
+
|
|
114
|
+
if (this._cancelled) {
|
|
115
|
+
logger.info(`[Worker ${this.jobId}] Cancelled after LLM response`);
|
|
116
|
+
throw new Error('Worker cancelled');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// End turn — return the text
|
|
120
|
+
if (response.stopReason === 'end_turn') {
|
|
121
|
+
logger.info(`[Worker ${this.jobId}] End turn — final response: "${(response.text || '').slice(0, 200)}"`);
|
|
122
|
+
return response.text || 'Task completed.';
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Tool use
|
|
126
|
+
if (response.stopReason === 'tool_use') {
|
|
127
|
+
messages.push({ role: 'assistant', content: response.rawContent });
|
|
128
|
+
|
|
129
|
+
// Log thinking text
|
|
130
|
+
if (response.text && response.text.trim()) {
|
|
131
|
+
logger.info(`[Worker ${this.jobId}] Thinking: "${response.text.slice(0, 200)}"`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const toolResults = [];
|
|
135
|
+
|
|
136
|
+
for (const block of response.toolCalls) {
|
|
137
|
+
if (this._cancelled) {
|
|
138
|
+
logger.info(`[Worker ${this.jobId}] Cancelled before executing tool ${block.name}`);
|
|
139
|
+
throw new Error('Worker cancelled');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const summary = this._formatToolSummary(block.name, block.input);
|
|
143
|
+
logger.info(`[Worker ${this.jobId}] Executing tool: ${block.name} — ${summary}`);
|
|
144
|
+
logger.debug(`[Worker ${this.jobId}] Tool input: ${JSON.stringify(block.input).slice(0, 300)}`);
|
|
145
|
+
this._reportProgress(`🔧 ${summary}`);
|
|
146
|
+
|
|
147
|
+
const result = await executeTool(block.name, block.input, {
|
|
148
|
+
config: this.config,
|
|
149
|
+
user: null, // workers don't have user context
|
|
150
|
+
personaManager: null,
|
|
151
|
+
onUpdate: this.callbacks.onUpdate || null, // Real bot onUpdate (returns message_id for coder.js smart output)
|
|
152
|
+
sendPhoto: this.callbacks.sendPhoto || null,
|
|
153
|
+
sessionId: this.jobId, // Per-worker browser session isolation
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const resultStr = this._truncateResult(block.name, result);
|
|
157
|
+
logger.info(`[Worker ${this.jobId}] Tool ${block.name} result: ${resultStr.slice(0, 200)}`);
|
|
158
|
+
|
|
159
|
+
toolResults.push({
|
|
160
|
+
type: 'tool_result',
|
|
161
|
+
tool_use_id: block.id,
|
|
162
|
+
content: resultStr,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Track consecutive all-fail iterations (circuit breaker)
|
|
167
|
+
const allFailed = toolResults.every(tr => {
|
|
168
|
+
try { const parsed = JSON.parse(tr.content); return !!parsed.error; } catch { return false; }
|
|
169
|
+
});
|
|
170
|
+
if (allFailed) {
|
|
171
|
+
consecutiveAllFailIterations++;
|
|
172
|
+
logger.warn(`[Worker ${this.jobId}] All ${toolResults.length} tool calls failed (streak: ${consecutiveAllFailIterations})`);
|
|
173
|
+
if (consecutiveAllFailIterations >= 3) {
|
|
174
|
+
logger.warn(`[Worker ${this.jobId}] Circuit breaker: 3 consecutive all-fail iterations — forcing stop`);
|
|
175
|
+
messages.push({ role: 'user', content: toolResults });
|
|
176
|
+
messages.push({
|
|
177
|
+
role: 'user',
|
|
178
|
+
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.',
|
|
179
|
+
});
|
|
180
|
+
const bailResponse = await this.provider.chat({
|
|
181
|
+
system: this.systemPrompt,
|
|
182
|
+
messages,
|
|
183
|
+
tools: [], // No tools — force text response
|
|
184
|
+
signal: this.abortController.signal,
|
|
185
|
+
});
|
|
186
|
+
return bailResponse.text || 'All tool calls failed repeatedly. Could not complete the task.';
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
consecutiveAllFailIterations = 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
messages.push({ role: 'user', content: toolResults });
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Unexpected stop reason
|
|
197
|
+
logger.warn(`[Worker ${this.jobId}] Unexpected stopReason: ${response.stopReason}`);
|
|
198
|
+
return response.text || 'Worker finished with unexpected response.';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Safety ceiling hit (should basically never happen — job timeout is the real limit)
|
|
202
|
+
logger.warn(`[Worker ${this.jobId}] Hit safety ceiling (${this.maxIterations} iterations) — requesting final summary`);
|
|
203
|
+
this._reportProgress(`⏳ Summarizing results...`);
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
messages.push({
|
|
207
|
+
role: 'user',
|
|
208
|
+
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.',
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const summaryResponse = await this.provider.chat({
|
|
212
|
+
system: this.systemPrompt,
|
|
213
|
+
messages,
|
|
214
|
+
tools: [], // No tools — force text-only response
|
|
215
|
+
signal: this.abortController.signal,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const summary = summaryResponse.text || '';
|
|
219
|
+
logger.info(`[Worker ${this.jobId}] Final summary: "${summary.slice(0, 200)}"`);
|
|
220
|
+
|
|
221
|
+
if (summary.length > 10) {
|
|
222
|
+
return summary;
|
|
223
|
+
}
|
|
224
|
+
} catch (err) {
|
|
225
|
+
logger.warn(`[Worker ${this.jobId}] Summary call failed: ${err.message}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Fallback: extract any text the LLM produced during the loop
|
|
229
|
+
const lastAssistantText = this._extractLastAssistantText(messages);
|
|
230
|
+
if (lastAssistantText) {
|
|
231
|
+
logger.info(`[Worker ${this.jobId}] Falling back to last assistant text: "${lastAssistantText.slice(0, 200)}"`);
|
|
232
|
+
return lastAssistantText;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return 'Worker finished but could not produce a final summary.';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Extract the last meaningful assistant text from message history. */
|
|
239
|
+
_extractLastAssistantText(messages) {
|
|
240
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
241
|
+
const msg = messages[i];
|
|
242
|
+
if (msg.role !== 'assistant') continue;
|
|
243
|
+
|
|
244
|
+
if (typeof msg.content === 'string' && msg.content.trim()) {
|
|
245
|
+
return msg.content.trim();
|
|
246
|
+
}
|
|
247
|
+
if (Array.isArray(msg.content)) {
|
|
248
|
+
const texts = msg.content
|
|
249
|
+
.filter(b => b.type === 'text' && b.text?.trim())
|
|
250
|
+
.map(b => b.text.trim());
|
|
251
|
+
if (texts.length > 0) return texts.join('\n');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
_reportProgress(text) {
|
|
258
|
+
if (this.callbacks.onProgress) {
|
|
259
|
+
try { this.callbacks.onProgress(text); } catch {}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
_truncateResult(name, result) {
|
|
264
|
+
let str = JSON.stringify(result);
|
|
265
|
+
if (str.length <= MAX_RESULT_LENGTH) return str;
|
|
266
|
+
|
|
267
|
+
if (result && typeof result === 'object') {
|
|
268
|
+
const truncated = { ...result };
|
|
269
|
+
for (const field of LARGE_FIELDS) {
|
|
270
|
+
if (typeof truncated[field] === 'string' && truncated[field].length > 500) {
|
|
271
|
+
truncated[field] = truncated[field].slice(0, 500) + `\n... [truncated ${truncated[field].length - 500} chars]`;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
str = JSON.stringify(truncated);
|
|
275
|
+
if (str.length <= MAX_RESULT_LENGTH) return str;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
_formatToolSummary(name, input) {
|
|
282
|
+
const _short = (s, len = 80) => s && s.length > len ? s.slice(0, len) + '...' : s;
|
|
283
|
+
const _host = (url) => { try { return new URL(url).hostname; } catch { return url; } };
|
|
284
|
+
|
|
285
|
+
switch (name) {
|
|
286
|
+
case 'web_search': return `Searching: "${_short(input.query, 60)}"`;
|
|
287
|
+
case 'browse_website': return `Opening ${_host(input.url)}`;
|
|
288
|
+
case 'interact_with_page': return 'Interacting with page';
|
|
289
|
+
case 'extract_content': return 'Extracting content';
|
|
290
|
+
case 'screenshot_website': return `Screenshot of ${_host(input.url)}`;
|
|
291
|
+
case 'execute_command': return `Running: ${_short(input.command, 60)}`;
|
|
292
|
+
case 'read_file': return `Reading ${_short(input.path)}`;
|
|
293
|
+
case 'write_file': return `Writing ${_short(input.path)}`;
|
|
294
|
+
case 'git_clone': return `Cloning ${_short(input.repo)}`;
|
|
295
|
+
case 'git_checkout': return `Switching to ${input.branch}`;
|
|
296
|
+
case 'git_commit': return `Committing: "${_short(input.message, 50)}"`;
|
|
297
|
+
case 'git_push': return 'Pushing changes';
|
|
298
|
+
case 'github_create_pr': return `Creating PR: "${_short(input.title, 50)}"`;
|
|
299
|
+
case 'spawn_claude_code': return `Coding: ${_short(input.prompt, 60)}`;
|
|
300
|
+
case 'docker_exec': return `Running in ${_short(input.container)}`;
|
|
301
|
+
case 'docker_compose': return `Docker compose ${input.action}`;
|
|
302
|
+
default: return `${name}`;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|