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.
Files changed (41) hide show
  1. package/README.md +198 -124
  2. package/bin/kernel.js +201 -4
  3. package/package.json +1 -1
  4. package/src/agent.js +397 -222
  5. package/src/automation/automation-manager.js +377 -0
  6. package/src/automation/automation.js +79 -0
  7. package/src/automation/index.js +2 -0
  8. package/src/automation/scheduler.js +141 -0
  9. package/src/bot.js +667 -21
  10. package/src/conversation.js +33 -0
  11. package/src/intents/detector.js +50 -0
  12. package/src/intents/index.js +2 -0
  13. package/src/intents/planner.js +58 -0
  14. package/src/persona.js +68 -0
  15. package/src/prompts/orchestrator.js +76 -0
  16. package/src/prompts/persona.md +21 -0
  17. package/src/prompts/system.js +59 -6
  18. package/src/prompts/workers.js +89 -0
  19. package/src/providers/anthropic.js +23 -16
  20. package/src/providers/base.js +76 -2
  21. package/src/providers/index.js +1 -0
  22. package/src/providers/models.js +2 -1
  23. package/src/providers/openai-compat.js +5 -3
  24. package/src/security/confirm.js +7 -2
  25. package/src/skills/catalog.js +506 -0
  26. package/src/skills/custom.js +128 -0
  27. package/src/swarm/job-manager.js +169 -0
  28. package/src/swarm/job.js +67 -0
  29. package/src/swarm/worker-registry.js +74 -0
  30. package/src/tools/browser.js +458 -335
  31. package/src/tools/categories.js +3 -3
  32. package/src/tools/index.js +3 -0
  33. package/src/tools/orchestrator-tools.js +371 -0
  34. package/src/tools/persona.js +32 -0
  35. package/src/utils/config.js +50 -15
  36. package/src/worker.js +305 -0
  37. package/.agents/skills/interface-design/SKILL.md +0 -391
  38. package/.agents/skills/interface-design/references/critique.md +0 -67
  39. package/.agents/skills/interface-design/references/example.md +0 -86
  40. package/.agents/skills/interface-design/references/principles.md +0 -235
  41. 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
+ }