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/agent.js CHANGED
@@ -1,23 +1,75 @@
1
1
  import { createProvider, PROVIDERS } from './providers/index.js';
2
- import { toolDefinitions, executeTool, checkConfirmation } from './tools/index.js';
3
- import { selectToolsForMessage, expandToolsForUsed } from './tools/categories.js';
4
- import { getSystemPrompt } from './prompts/system.js';
2
+ import { orchestratorToolDefinitions, executeOrchestratorTool } from './tools/orchestrator-tools.js';
3
+ import { getToolsForWorker } from './swarm/worker-registry.js';
4
+ import { WORKER_TYPES } from './swarm/worker-registry.js';
5
+ import { getOrchestratorPrompt } from './prompts/orchestrator.js';
6
+ import { getWorkerPrompt } from './prompts/workers.js';
7
+ import { getUnifiedSkillById } from './skills/custom.js';
8
+ import { WorkerAgent } from './worker.js';
5
9
  import { getLogger } from './utils/logger.js';
6
10
  import { getMissingCredential, saveCredential, saveProviderToYaml } from './utils/config.js';
7
11
 
8
12
  const MAX_RESULT_LENGTH = 3000;
9
13
  const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
10
14
 
11
- export class Agent {
12
- constructor({ config, conversationManager }) {
15
+ export class OrchestratorAgent {
16
+ constructor({ config, conversationManager, personaManager, jobManager, automationManager }) {
13
17
  this.config = config;
14
18
  this.conversationManager = conversationManager;
15
- this.provider = createProvider(config);
16
- this.systemPrompt = getSystemPrompt(config);
19
+ this.personaManager = personaManager;
20
+ this.jobManager = jobManager;
21
+ this.automationManager = automationManager || null;
17
22
  this._pending = new Map(); // chatId -> pending state
23
+ this._chatCallbacks = new Map(); // chatId -> { onUpdate, sendPhoto }
24
+
25
+ // Orchestrator always uses Anthropic (30s timeout — lean dispatch/summarize calls)
26
+ this.orchestratorProvider = createProvider({
27
+ brain: {
28
+ provider: 'anthropic',
29
+ model: config.orchestrator.model,
30
+ max_tokens: config.orchestrator.max_tokens,
31
+ temperature: config.orchestrator.temperature,
32
+ api_key: config.orchestrator.api_key || process.env.ANTHROPIC_API_KEY,
33
+ timeout: 30_000,
34
+ },
35
+ });
36
+
37
+ // Worker provider uses user's chosen brain
38
+ this.workerProvider = createProvider(config);
39
+
40
+ // Set up job lifecycle event listeners
41
+ this._setupJobListeners();
42
+ }
43
+
44
+ /** Build the orchestrator system prompt. */
45
+ _getSystemPrompt(chatId, user) {
46
+ const logger = getLogger();
47
+ const skillId = this.conversationManager.getSkill(chatId);
48
+ const skillPrompt = skillId ? getUnifiedSkillById(skillId)?.systemPrompt : null;
49
+
50
+ let userPersona = null;
51
+ if (this.personaManager && user?.id) {
52
+ userPersona = this.personaManager.load(user.id, user.username);
53
+ }
54
+
55
+ logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'}`);
56
+ return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona);
57
+ }
58
+
59
+ setSkill(chatId, skillId) {
60
+ this.conversationManager.setSkill(chatId, skillId);
61
+ }
62
+
63
+ clearSkill(chatId) {
64
+ this.conversationManager.clearSkill(chatId);
18
65
  }
19
66
 
20
- /** Return current brain info for display. */
67
+ getActiveSkill(chatId) {
68
+ const skillId = this.conversationManager.getSkill(chatId);
69
+ return skillId ? getUnifiedSkillById(skillId) : null;
70
+ }
71
+
72
+ /** Return current worker brain info for display. */
21
73
  getBrainInfo() {
22
74
  const { provider, model } = this.config.brain;
23
75
  const providerDef = PROVIDERS[provider];
@@ -27,64 +79,65 @@ export class Agent {
27
79
  return { provider, providerName, model, modelLabel };
28
80
  }
29
81
 
30
- /**
31
- * Switch to a different provider/model at runtime.
32
- * Resolves the API key from process.env automatically.
33
- * Returns null on success, or an error string if the key is missing.
34
- */
35
- switchBrain(providerKey, modelId) {
82
+ /** Switch worker brain provider/model at runtime. */
83
+ async switchBrain(providerKey, modelId) {
36
84
  const logger = getLogger();
37
85
  const providerDef = PROVIDERS[providerKey];
38
86
  if (!providerDef) return `Unknown provider: ${providerKey}`;
39
87
 
40
88
  const envKey = providerDef.envKey;
41
89
  const apiKey = process.env[envKey];
42
- if (!apiKey) {
43
- return envKey; // caller handles prompting
90
+ if (!apiKey) return envKey;
91
+
92
+ try {
93
+ const testConfig = { ...this.config, brain: { ...this.config.brain, provider: providerKey, model: modelId, api_key: apiKey } };
94
+ const testProvider = createProvider(testConfig);
95
+ await testProvider.ping();
96
+
97
+ this.config.brain.provider = providerKey;
98
+ this.config.brain.model = modelId;
99
+ this.config.brain.api_key = apiKey;
100
+ this.workerProvider = testProvider;
101
+ saveProviderToYaml(providerKey, modelId);
102
+
103
+ logger.info(`Worker brain switched to ${providerDef.name} / ${modelId}`);
104
+ return null;
105
+ } catch (err) {
106
+ logger.error(`Brain switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
107
+ return { error: err.message };
44
108
  }
45
-
46
- this.config.brain.provider = providerKey;
47
- this.config.brain.model = modelId;
48
- this.config.brain.api_key = apiKey;
49
-
50
- // Recreate the provider instance
51
- this.provider = createProvider(this.config);
52
-
53
- // Persist to config.yaml
54
- saveProviderToYaml(providerKey, modelId);
55
-
56
- logger.info(`Brain switched to ${providerDef.name} / ${modelId}`);
57
- return null;
58
109
  }
59
110
 
60
- /**
61
- * Finalize brain switch after API key was provided via chat.
62
- */
63
- switchBrainWithKey(providerKey, modelId, apiKey) {
111
+ /** Finalize brain switch after API key was provided via chat. */
112
+ async switchBrainWithKey(providerKey, modelId, apiKey) {
64
113
  const logger = getLogger();
65
114
  const providerDef = PROVIDERS[providerKey];
66
115
 
67
- // Save the key
68
- saveCredential(this.config, providerDef.envKey, apiKey);
69
-
70
- this.config.brain.provider = providerKey;
71
- this.config.brain.model = modelId;
72
- this.config.brain.api_key = apiKey;
73
-
74
- this.provider = createProvider(this.config);
75
- saveProviderToYaml(providerKey, modelId);
76
-
77
- logger.info(`Brain switched to ${providerDef.name} / ${modelId} (new key saved)`);
116
+ try {
117
+ const testConfig = { ...this.config, brain: { ...this.config.brain, provider: providerKey, model: modelId, api_key: apiKey } };
118
+ const testProvider = createProvider(testConfig);
119
+ await testProvider.ping();
120
+
121
+ saveCredential(this.config, providerDef.envKey, apiKey);
122
+ this.config.brain.provider = providerKey;
123
+ this.config.brain.model = modelId;
124
+ this.config.brain.api_key = apiKey;
125
+ this.workerProvider = testProvider;
126
+ saveProviderToYaml(providerKey, modelId);
127
+
128
+ logger.info(`Worker brain switched to ${providerDef.name} / ${modelId} (new key saved)`);
129
+ return null;
130
+ } catch (err) {
131
+ logger.error(`Brain switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
132
+ return { error: err.message };
133
+ }
78
134
  }
79
135
 
80
- /**
81
- * Truncate a tool result to stay within token budget.
82
- */
136
+ /** Truncate a tool result. */
83
137
  _truncateResult(name, result) {
84
138
  let str = JSON.stringify(result);
85
139
  if (str.length <= MAX_RESULT_LENGTH) return str;
86
140
 
87
- // Try truncating known large fields first
88
141
  if (result && typeof result === 'object') {
89
142
  const truncated = { ...result };
90
143
  for (const field of LARGE_FIELDS) {
@@ -96,217 +149,278 @@ export class Agent {
96
149
  if (str.length <= MAX_RESULT_LENGTH) return str;
97
150
  }
98
151
 
99
- // Hard truncate
100
152
  return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
101
153
  }
102
154
 
103
155
  async processMessage(chatId, userMessage, user, onUpdate, sendPhoto) {
104
156
  const logger = getLogger();
105
157
 
106
- this._onUpdate = onUpdate || null;
107
- this._sendPhoto = sendPhoto || null;
158
+ logger.info(`Orchestrator processing message for chat ${chatId} from ${user?.username || user?.id || 'unknown'}: "${userMessage.slice(0, 120)}"`);
159
+
160
+ // Store callbacks so workers can use them later
161
+ this._chatCallbacks.set(chatId, { onUpdate, sendPhoto });
108
162
 
109
163
  // Handle pending responses (confirmation or credential)
110
164
  const pending = this._pending.get(chatId);
111
165
  if (pending) {
112
166
  this._pending.delete(chatId);
167
+ logger.debug(`Orchestrator handling pending ${pending.type} response for chat ${chatId}`);
113
168
 
114
169
  if (pending.type === 'credential') {
115
- return await this._handleCredentialResponse(chatId, userMessage, user, pending);
116
- }
117
-
118
- if (pending.type === 'confirmation') {
119
- return await this._handleConfirmationResponse(chatId, userMessage, user, pending);
170
+ return await this._handleCredentialResponse(chatId, userMessage, user, pending, onUpdate);
120
171
  }
121
172
  }
122
173
 
123
- const { max_tool_depth } = this.config.brain;
174
+ const { max_tool_depth } = this.config.orchestrator;
124
175
 
125
176
  // Add user message to persistent history
126
177
  this.conversationManager.addMessage(chatId, 'user', userMessage);
127
178
 
128
179
  // Build working messages from compressed history
129
180
  const messages = [...this.conversationManager.getSummarizedHistory(chatId)];
181
+ logger.debug(`Orchestrator conversation context: ${messages.length} messages, max_depth=${max_tool_depth}`);
130
182
 
131
- // Select relevant tools based on user message
132
- const tools = selectToolsForMessage(userMessage, toolDefinitions);
133
- logger.debug(`Selected ${tools.length}/${toolDefinitions.length} tools for message`);
183
+ const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth);
134
184
 
135
- return await this._runLoop(chatId, messages, user, 0, max_tool_depth, tools);
136
- }
185
+ logger.info(`Orchestrator reply for chat ${chatId}: "${(reply || '').slice(0, 150)}"`);
137
186
 
138
- _formatToolSummary(name, input) {
139
- const key = {
140
- execute_command: 'command',
141
- read_file: 'path',
142
- write_file: 'path',
143
- list_directory: 'path',
144
- git_clone: 'repo',
145
- git_checkout: 'branch',
146
- git_commit: 'message',
147
- git_push: 'dir',
148
- git_diff: 'dir',
149
- github_create_pr: 'title',
150
- github_create_repo: 'name',
151
- github_list_prs: 'repo',
152
- github_get_pr_diff: 'repo',
153
- github_post_review: 'repo',
154
- spawn_claude_code: 'prompt',
155
- kill_process: 'pid',
156
- docker_exec: 'container',
157
- docker_logs: 'container',
158
- docker_compose: 'action',
159
- curl_url: 'url',
160
- check_port: 'port',
161
- screenshot_website: 'url',
162
- send_image: 'file_path',
163
- browse_website: 'url',
164
- extract_content: 'url',
165
- interact_with_page: 'url',
166
- }[name];
167
- const val = key && input[key] ? String(input[key]).slice(0, 120) : JSON.stringify(input).slice(0, 120);
168
- return `${name}: ${val}`;
187
+ // Background persona extraction
188
+ this._extractPersonaBackground(userMessage, reply, user).catch(() => {});
189
+
190
+ return reply;
169
191
  }
170
192
 
171
- async _sendUpdate(text) {
172
- if (this._onUpdate) {
173
- try { await this._onUpdate(text); } catch {}
193
+ async _sendUpdate(chatId, text, opts) {
194
+ const callbacks = this._chatCallbacks.get(chatId);
195
+ if (callbacks?.onUpdate) {
196
+ try { return await callbacks.onUpdate(text, opts); } catch {}
174
197
  }
198
+ return null;
175
199
  }
176
200
 
177
- async _handleCredentialResponse(chatId, userMessage, user, pending) {
201
+ async _handleCredentialResponse(chatId, userMessage, user, pending, onUpdate) {
178
202
  const logger = getLogger();
179
203
  const value = userMessage.trim();
180
204
 
181
205
  if (value.toLowerCase() === 'skip' || value.toLowerCase() === 'cancel') {
182
206
  logger.info(`User skipped credential: ${pending.credential.envKey}`);
183
- pending.toolResults.push({
184
- type: 'tool_result',
185
- tool_use_id: pending.block.id,
186
- content: this._truncateResult(pending.block.name, { error: `${pending.credential.label} not provided. Operation skipped.` }),
187
- });
188
- return await this._resumeAfterPause(chatId, user, pending);
207
+ return 'Credential skipped. You can provide it later.';
189
208
  }
190
209
 
191
- // Save the credential
192
210
  saveCredential(this.config, pending.credential.envKey, value);
193
211
  logger.info(`Saved credential: ${pending.credential.envKey}`);
212
+ return `Saved ${pending.credential.label}. You can now try the task again.`;
213
+ }
194
214
 
195
- // Now execute the original tool
196
- const result = await executeTool(pending.block.name, pending.block.input, {
197
- config: this.config,
198
- user,
199
- onUpdate: this._onUpdate,
200
- sendPhoto: this._sendPhoto,
201
- });
215
+ /** Set up listeners for job lifecycle events. */
216
+ _setupJobListeners() {
217
+ const logger = getLogger();
218
+
219
+ this.jobManager.on('job:completed', async (job) => {
220
+ const chatId = job.chatId;
221
+ const workerDef = WORKER_TYPES[job.workerType] || {};
222
+ const label = workerDef.label || job.workerType;
223
+
224
+ logger.info(`[Orchestrator] Job completed event: ${job.id} [${job.workerType}] in chat ${chatId} (${job.duration}s) — result length: ${(job.result || '').length} chars`);
202
225
 
203
- pending.toolResults.push({
204
- type: 'tool_result',
205
- tool_use_id: pending.block.id,
206
- content: this._truncateResult(pending.block.name, result),
226
+ // 1. Store raw result in conversation history so orchestrator has full context
227
+ let resultText = job.result || 'Done.';
228
+ if (resultText.length > 3000) {
229
+ resultText = resultText.slice(0, 3000) + '\n\n... [result truncated]';
230
+ }
231
+ this.conversationManager.addMessage(chatId, 'user', `[Worker result: ${label} (${job.id}, ${job.duration}s)]\n\n${resultText}`);
232
+
233
+ // 2. IMMEDIATELY notify user (guarantees they see something regardless of summary LLM)
234
+ const notifyMsgId = await this._sendUpdate(chatId, `✅ ${label} finished! Preparing summary...`);
235
+
236
+ // 3. Try to summarize (provider timeout protects against hangs)
237
+ try {
238
+ const summary = await this._summarizeJobResult(chatId, job);
239
+ if (summary) {
240
+ this.conversationManager.addMessage(chatId, 'assistant', summary);
241
+ await this._sendUpdate(chatId, summary, { editMessageId: notifyMsgId });
242
+ }
243
+ } catch (err) {
244
+ logger.error(`[Orchestrator] Failed to summarize job ${job.id}: ${err.message}`);
245
+ await this._sendUpdate(chatId, `✅ ${label} finished (\`${job.id}\`, ${job.duration}s)! Ask me for the details.`, { editMessageId: notifyMsgId }).catch(() => {});
246
+ }
207
247
  });
208
248
 
209
- return await this._resumeAfterPause(chatId, user, pending);
210
- }
249
+ this.jobManager.on('job:failed', (job) => {
250
+ const chatId = job.chatId;
251
+ const workerDef = WORKER_TYPES[job.workerType] || {};
252
+ const label = workerDef.label || job.workerType;
211
253
 
212
- async _handleConfirmationResponse(chatId, userMessage, user, pending) {
213
- const logger = getLogger();
214
- const lower = userMessage.toLowerCase().trim();
254
+ logger.error(`[Orchestrator] Job failed event: ${job.id} [${job.workerType}] in chat ${chatId} — ${job.error}`);
215
255
 
216
- if (lower === 'yes' || lower === 'y' || lower === 'confirm') {
217
- logger.info(`User confirmed dangerous tool: ${pending.block.name}`);
218
- const result = await executeTool(pending.block.name, pending.block.input, { ...pending.context, onUpdate: this._onUpdate, sendPhoto: this._sendPhoto });
256
+ const msg = `❌ **${label} failed** (\`${job.id}\`): ${job.error}`;
257
+ this.conversationManager.addMessage(chatId, 'assistant', msg);
258
+ this._sendUpdate(chatId, msg);
259
+ });
219
260
 
220
- pending.toolResults.push({
221
- type: 'tool_result',
222
- tool_use_id: pending.block.id,
223
- content: this._truncateResult(pending.block.name, result),
224
- });
225
- } else {
226
- logger.info(`User denied dangerous tool: ${pending.block.name}`);
227
- pending.toolResults.push({
228
- type: 'tool_result',
229
- tool_use_id: pending.block.id,
230
- content: this._truncateResult(pending.block.name, { error: 'User denied this operation.' }),
231
- });
232
- }
261
+ this.jobManager.on('job:cancelled', (job) => {
262
+ const chatId = job.chatId;
263
+ const workerDef = WORKER_TYPES[job.workerType] || {};
264
+ const label = workerDef.label || job.workerType;
265
+
266
+ logger.info(`[Orchestrator] Job cancelled event: ${job.id} [${job.workerType}] in chat ${chatId}`);
233
267
 
234
- return await this._resumeAfterPause(chatId, user, pending);
268
+ const msg = `🚫 **${label} cancelled** (\`${job.id}\`)`;
269
+ this._sendUpdate(chatId, msg);
270
+ });
235
271
  }
236
272
 
237
- async _resumeAfterPause(chatId, user, pending) {
238
- // Process remaining blocks
239
- for (const block of pending.remainingBlocks) {
240
- if (block.type !== 'tool_use') continue;
273
+ /**
274
+ * Auto-summarize a completed job result via the orchestrator LLM.
275
+ * The orchestrator reads the worker's raw result and presents a clean summary.
276
+ * Protected by the provider's built-in timeout (30s) — no manual Promise.race needed.
277
+ * Returns the summary text, or null. Caller handles delivery.
278
+ */
279
+ async _summarizeJobResult(chatId, job) {
280
+ const logger = getLogger();
281
+ const workerDef = WORKER_TYPES[job.workerType] || {};
282
+ const label = workerDef.label || job.workerType;
283
+
284
+ logger.info(`[Orchestrator] Summarizing job ${job.id} [${job.workerType}] result for user`);
285
+
286
+ const history = this.conversationManager.getSummarizedHistory(chatId);
287
+
288
+ const response = await this.orchestratorProvider.chat({
289
+ system: this._getSystemPrompt(chatId, null),
290
+ messages: [
291
+ ...history,
292
+ {
293
+ role: 'user',
294
+ content: `The ${label} worker just finished job \`${job.id}\` (took ${job.duration}s). Present the results to the user in a clean, well-formatted way. Don't mention "worker" or technical job details — just present the findings naturally as if you did the work yourself.`,
295
+ },
296
+ ],
297
+ });
241
298
 
242
- const pauseMsg = await this._checkPause(chatId, block, user, pending.toolResults, pending.remainingBlocks.filter((b) => b !== block), pending.messages);
243
- if (pauseMsg) return pauseMsg;
299
+ const summary = response.text || '';
300
+ logger.info(`[Orchestrator] Job ${job.id} summary: "${summary.slice(0, 200)}"`);
244
301
 
245
- const r = await executeTool(block.name, block.input, { config: this.config, user, onUpdate: this._onUpdate, sendPhoto: this._sendPhoto });
246
- pending.toolResults.push({
247
- type: 'tool_result',
248
- tool_use_id: block.id,
249
- content: this._truncateResult(block.name, r),
250
- });
251
- }
252
-
253
- pending.messages.push({ role: 'user', content: pending.toolResults });
254
- const { max_tool_depth } = this.config.brain;
255
- return await this._runLoop(chatId, pending.messages, user, 0, max_tool_depth, pending.tools || toolDefinitions);
302
+ return summary || null;
256
303
  }
257
304
 
258
- _checkPause(chatId, block, user, toolResults, remainingBlocks, messages) {
305
+ /**
306
+ * Spawn a worker for a job — called from dispatch_task handler.
307
+ * Creates smart progress reporting via editable Telegram message.
308
+ */
309
+ async _spawnWorker(job) {
259
310
  const logger = getLogger();
311
+ const chatId = job.chatId;
312
+ const callbacks = this._chatCallbacks.get(chatId) || {};
313
+ const onUpdate = callbacks.onUpdate;
314
+ const sendPhoto = callbacks.sendPhoto;
315
+
316
+ logger.info(`[Orchestrator] Spawning worker for job ${job.id} [${job.workerType}] in chat ${chatId} — task: "${job.task.slice(0, 120)}"`);
317
+
318
+ const workerDef = WORKER_TYPES[job.workerType] || {};
319
+ const abortController = new AbortController();
320
+
321
+ // Smart progress: editable Telegram message (same pattern as coder.js)
322
+ let statusMsgId = null;
323
+ let activityLines = [];
324
+ let flushTimer = null;
325
+ const MAX_VISIBLE = 10;
326
+
327
+ const buildStatusText = (finalState = null) => {
328
+ const visible = activityLines.slice(-MAX_VISIBLE);
329
+ const countInfo = activityLines.length > MAX_VISIBLE
330
+ ? `\n_... ${activityLines.length} operations total_\n`
331
+ : '';
332
+ const header = `${workerDef.emoji || '⚙️'} *${workerDef.label || job.workerType}* (\`${job.id}\`)`;
333
+ if (finalState === 'done') return `${header} — Done\n${countInfo}\n${visible.join('\n')}`;
334
+ if (finalState === 'error') return `${header} — Failed\n${countInfo}\n${visible.join('\n')}`;
335
+ if (finalState === 'cancelled') return `${header} — Cancelled\n${countInfo}\n${visible.join('\n')}`;
336
+ return `${header} — Working...\n${countInfo}\n${visible.join('\n')}`;
337
+ };
338
+
339
+ const flushStatus = async () => {
340
+ flushTimer = null;
341
+ if (!onUpdate || activityLines.length === 0) return;
342
+ try {
343
+ if (statusMsgId) {
344
+ await onUpdate(buildStatusText(), { editMessageId: statusMsgId });
345
+ } else {
346
+ statusMsgId = await onUpdate(buildStatusText());
347
+ job.statusMessageId = statusMsgId;
348
+ }
349
+ } catch {}
350
+ };
351
+
352
+ const addActivity = (line) => {
353
+ activityLines.push(line);
354
+ if (!statusMsgId && !flushTimer) {
355
+ flushStatus();
356
+ } else if (!flushTimer) {
357
+ flushTimer = setTimeout(flushStatus, 1000);
358
+ }
359
+ };
260
360
 
261
- // Check missing credentials first
262
- const missing = getMissingCredential(block.name, this.config);
263
- if (missing) {
264
- logger.warn(`Missing credential for ${block.name}: ${missing.envKey}`);
265
- this._pending.set(chatId, {
266
- type: 'credential',
267
- block,
268
- credential: missing,
269
- context: { config: this.config, user },
270
- toolResults,
271
- remainingBlocks,
272
- messages,
273
- });
274
- return `🔑 **${missing.label}** is required for this action.\n\nPlease send your token now (it will be saved to \`~/.kernelbot/.env\`).\n\nOr reply **skip** to cancel.`;
275
- }
361
+ // Get scoped tools and skill
362
+ const tools = getToolsForWorker(job.workerType);
363
+ const skillId = this.conversationManager.getSkill(chatId);
364
+ logger.debug(`[Orchestrator] Worker ${job.id} config: ${tools.length} tools, skill=${skillId || 'none'}, brain=${this.config.brain.provider}/${this.config.brain.model}`);
276
365
 
277
- // Check dangerous operation confirmation
278
- const dangerLabel = checkConfirmation(block.name, block.input, this.config);
279
- if (dangerLabel) {
280
- logger.warn(`Dangerous tool detected: ${block.name} — ${dangerLabel}`);
281
- this._pending.set(chatId, {
282
- type: 'confirmation',
283
- block,
284
- context: { config: this.config, user },
285
- toolResults,
286
- remainingBlocks,
287
- messages,
288
- });
289
- return `⚠️ This action will **${dangerLabel}**.\n\n\`${block.name}\`: \`${JSON.stringify(block.input)}\`\n\nConfirm? (yes/no)`;
290
- }
366
+ const worker = new WorkerAgent({
367
+ config: this.config,
368
+ workerType: job.workerType,
369
+ jobId: job.id,
370
+ tools,
371
+ skillId,
372
+ callbacks: {
373
+ onProgress: (text) => addActivity(text),
374
+ onUpdate, // Real bot onUpdate for tools (coder.js smart output needs message_id)
375
+ onComplete: (result) => {
376
+ logger.info(`[Worker ${job.id}] Completed — result: "${(result || '').slice(0, 150)}"`);
377
+ // Final status message update
378
+ if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
379
+ if (statusMsgId && onUpdate) {
380
+ onUpdate(buildStatusText('done'), { editMessageId: statusMsgId }).catch(() => {});
381
+ }
382
+ this.jobManager.completeJob(job.id, result);
383
+ },
384
+ onError: (err) => {
385
+ logger.error(`[Worker ${job.id}] Error — ${err.message || String(err)}`);
386
+ if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
387
+ if (statusMsgId && onUpdate) {
388
+ onUpdate(buildStatusText('error'), { editMessageId: statusMsgId }).catch(() => {});
389
+ }
390
+ this.jobManager.failJob(job.id, err.message || String(err));
391
+ },
392
+ sendPhoto,
393
+ },
394
+ abortController,
395
+ });
291
396
 
292
- return null;
397
+ // Store worker ref on job for cancellation
398
+ job.worker = worker;
399
+
400
+ // Start the job
401
+ this.jobManager.startJob(job.id);
402
+
403
+ // Fire and forget — return the promise so .catch() in orchestrator-tools works
404
+ return worker.run(job.task);
293
405
  }
294
406
 
295
- async _runLoop(chatId, messages, user, startDepth, maxDepth, tools) {
407
+ async _runLoop(chatId, messages, user, startDepth, maxDepth) {
296
408
  const logger = getLogger();
297
- let currentTools = tools || toolDefinitions;
298
409
 
299
410
  for (let depth = startDepth; depth < maxDepth; depth++) {
300
- logger.debug(`Agent loop iteration ${depth + 1}/${maxDepth}`);
411
+ logger.info(`[Orchestrator] LLM call ${depth + 1}/${maxDepth} for chat ${chatId} — sending ${messages.length} messages`);
301
412
 
302
- const response = await this.provider.chat({
303
- system: this.systemPrompt,
413
+ const response = await this.orchestratorProvider.chat({
414
+ system: this._getSystemPrompt(chatId, user),
304
415
  messages,
305
- tools: currentTools,
416
+ tools: orchestratorToolDefinitions,
306
417
  });
307
418
 
419
+ logger.info(`[Orchestrator] LLM response: stopReason=${response.stopReason}, text=${(response.text || '').length} chars, toolCalls=${(response.toolCalls || []).length}`);
420
+
308
421
  if (response.stopReason === 'end_turn') {
309
422
  const reply = response.text || '';
423
+ logger.info(`[Orchestrator] End turn — final reply: "${reply.slice(0, 200)}"`);
310
424
  this.conversationManager.addMessage(chatId, 'assistant', reply);
311
425
  return reply;
312
426
  }
@@ -314,40 +428,27 @@ export class Agent {
314
428
  if (response.stopReason === 'tool_use') {
315
429
  messages.push({ role: 'assistant', content: response.rawContent });
316
430
 
317
- // Send thinking text to the user
318
431
  if (response.text && response.text.trim()) {
319
- logger.info(`Agent thinking: ${response.text.slice(0, 200)}`);
320
- await this._sendUpdate(`💭 ${response.text}`);
432
+ logger.info(`[Orchestrator] Thinking: "${response.text.slice(0, 200)}"`);
321
433
  }
322
434
 
323
435
  const toolResults = [];
324
- const usedToolNames = [];
325
-
326
- for (let i = 0; i < response.toolCalls.length; i++) {
327
- const block = response.toolCalls[i];
328
-
329
- // Build a block-like object for _checkPause (needs .type for remainingBlocks filter)
330
- const blockObj = { type: 'tool_use', id: block.id, name: block.name, input: block.input };
331
-
332
- // Check if we need to pause (missing cred or dangerous action)
333
- const remaining = response.toolCalls.slice(i + 1).map((tc) => ({
334
- type: 'tool_use', id: tc.id, name: tc.name, input: tc.input,
335
- }));
336
- const pauseMsg = this._checkPause(chatId, blockObj, user, toolResults, remaining, messages);
337
- if (pauseMsg) return pauseMsg;
338
436
 
437
+ for (const block of response.toolCalls) {
339
438
  const summary = this._formatToolSummary(block.name, block.input);
340
- logger.info(`Tool call: ${summary}`);
341
- await this._sendUpdate(`🔧 \`${summary}\``);
439
+ logger.info(`[Orchestrator] Calling tool: ${block.name} — ${summary}`);
440
+ logger.debug(`[Orchestrator] Tool input: ${JSON.stringify(block.input).slice(0, 300)}`);
441
+ await this._sendUpdate(chatId, `⚡ ${summary}`);
342
442
 
343
- const result = await executeTool(block.name, block.input, {
443
+ const result = await executeOrchestratorTool(block.name, block.input, {
444
+ chatId,
445
+ jobManager: this.jobManager,
344
446
  config: this.config,
345
- user,
346
- onUpdate: this._onUpdate,
347
- sendPhoto: this._sendPhoto,
447
+ spawnWorker: (job) => this._spawnWorker(job),
448
+ automationManager: this.automationManager,
348
449
  });
349
450
 
350
- usedToolNames.push(block.name);
451
+ logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
351
452
 
352
453
  toolResults.push({
353
454
  type: 'tool_result',
@@ -356,15 +457,12 @@ export class Agent {
356
457
  });
357
458
  }
358
459
 
359
- // Expand tools based on what was actually used
360
- currentTools = expandToolsForUsed(usedToolNames, currentTools, toolDefinitions);
361
-
362
460
  messages.push({ role: 'user', content: toolResults });
363
461
  continue;
364
462
  }
365
463
 
366
464
  // Unexpected stop reason
367
- logger.warn(`Unexpected stopReason: ${response.stopReason}`);
465
+ logger.warn(`[Orchestrator] Unexpected stopReason: ${response.stopReason}`);
368
466
  if (response.text) {
369
467
  this.conversationManager.addMessage(chatId, 'assistant', response.text);
370
468
  return response.text;
@@ -372,10 +470,87 @@ export class Agent {
372
470
  return 'Something went wrong — unexpected response from the model.';
373
471
  }
374
472
 
375
- const depthWarning =
376
- `Reached maximum tool depth (${maxDepth}). Stopping to prevent infinite loops. ` +
377
- `Please try again with a simpler request.`;
473
+ logger.warn(`[Orchestrator] Reached max depth (${maxDepth}) for chat ${chatId}`);
474
+ const depthWarning = `Reached maximum orchestrator depth (${maxDepth}).`;
378
475
  this.conversationManager.addMessage(chatId, 'assistant', depthWarning);
379
476
  return depthWarning;
380
477
  }
478
+
479
+ _formatToolSummary(name, input) {
480
+ switch (name) {
481
+ case 'dispatch_task': {
482
+ const workerDef = WORKER_TYPES[input.worker_type] || {};
483
+ return `Dispatching ${workerDef.emoji || '⚙️'} ${workerDef.label || input.worker_type}: ${(input.task || '').slice(0, 60)}`;
484
+ }
485
+ case 'list_jobs':
486
+ return 'Checking job status';
487
+ case 'cancel_job':
488
+ return `Cancelling job ${input.job_id}`;
489
+ case 'create_automation':
490
+ return `Creating automation: ${(input.name || '').slice(0, 40)}`;
491
+ case 'list_automations':
492
+ return 'Listing automations';
493
+ case 'update_automation':
494
+ return `Updating automation ${input.automation_id}`;
495
+ case 'delete_automation':
496
+ return `Deleting automation ${input.automation_id}`;
497
+ default:
498
+ return name;
499
+ }
500
+ }
501
+
502
+ /** Background persona extraction. */
503
+ async _extractPersonaBackground(userMessage, reply, user) {
504
+ const logger = getLogger();
505
+
506
+ if (!this.personaManager || !user?.id) return;
507
+ if (!userMessage || userMessage.trim().length < 3) return;
508
+
509
+ const currentPersona = this.personaManager.load(user.id, user.username);
510
+
511
+ const system = [
512
+ 'You are a user-profile extractor. Analyze the user\'s message and extract any NEW personal information.',
513
+ '',
514
+ 'Look for: name, location, timezone, language, technical skills, expertise level,',
515
+ 'projects they\'re working on, tool/framework preferences, job title, role, company,',
516
+ 'interests, hobbies, communication style, or any other personal details.',
517
+ '',
518
+ 'RULES:',
519
+ '- Only extract FACTUAL information explicitly stated or strongly implied',
520
+ '- Do NOT infer personality traits from a single message',
521
+ '- Do NOT add information already in the profile',
522
+ '- If there IS new info, return the COMPLETE updated profile in the EXACT same markdown format',
523
+ '- If there is NO new info, respond with exactly: NONE',
524
+ ].join('\n');
525
+
526
+ const userPrompt = [
527
+ 'Current profile:',
528
+ '```',
529
+ currentPersona,
530
+ '```',
531
+ '',
532
+ `User's message: "${userMessage}"`,
533
+ '',
534
+ 'Return the updated profile markdown or NONE.',
535
+ ].join('\n');
536
+
537
+ try {
538
+ const response = await this.orchestratorProvider.chat({
539
+ system,
540
+ messages: [{ role: 'user', content: userPrompt }],
541
+ });
542
+
543
+ const text = (response.text || '').trim();
544
+
545
+ if (text && text !== 'NONE' && text.includes('# User Profile')) {
546
+ this.personaManager.save(user.id, text);
547
+ logger.info(`Auto-extracted persona update for user ${user.id} (${user.username})`);
548
+ }
549
+ } catch (err) {
550
+ logger.debug(`Persona extraction skipped: ${err.message}`);
551
+ }
552
+ }
381
553
  }
554
+
555
+ // Re-export as Agent for backward compatibility with bin/kernel.js import
556
+ export { OrchestratorAgent as Agent };