kernelbot 1.0.25 → 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 -123
  2. package/bin/kernel.js +201 -4
  3. package/package.json +1 -1
  4. package/src/agent.js +447 -174
  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 +908 -69
  10. package/src/conversation.js +69 -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 +74 -35
  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 +101 -0
  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 +53 -16
  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,220 +1,426 @@
1
- import { createProvider } from './providers/index.js';
2
- import { toolDefinitions, executeTool, checkConfirmation } from './tools/index.js';
3
- import { getSystemPrompt } from './prompts/system.js';
1
+ import { createProvider, PROVIDERS } from './providers/index.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';
4
9
  import { getLogger } from './utils/logger.js';
5
- import { getMissingCredential, saveCredential } from './utils/config.js';
10
+ import { getMissingCredential, saveCredential, saveProviderToYaml } from './utils/config.js';
6
11
 
7
- export class Agent {
8
- constructor({ config, conversationManager }) {
12
+ const MAX_RESULT_LENGTH = 3000;
13
+ const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
14
+
15
+ export class OrchestratorAgent {
16
+ constructor({ config, conversationManager, personaManager, jobManager, automationManager }) {
9
17
  this.config = config;
10
18
  this.conversationManager = conversationManager;
11
- this.provider = createProvider(config);
12
- this.systemPrompt = getSystemPrompt(config);
19
+ this.personaManager = personaManager;
20
+ this.jobManager = jobManager;
21
+ this.automationManager = automationManager || null;
13
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);
65
+ }
66
+
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. */
73
+ getBrainInfo() {
74
+ const { provider, model } = this.config.brain;
75
+ const providerDef = PROVIDERS[provider];
76
+ const providerName = providerDef ? providerDef.name : provider;
77
+ const modelEntry = providerDef?.models.find((m) => m.id === model);
78
+ const modelLabel = modelEntry ? modelEntry.label : model;
79
+ return { provider, providerName, model, modelLabel };
80
+ }
81
+
82
+ /** Switch worker brain provider/model at runtime. */
83
+ async switchBrain(providerKey, modelId) {
84
+ const logger = getLogger();
85
+ const providerDef = PROVIDERS[providerKey];
86
+ if (!providerDef) return `Unknown provider: ${providerKey}`;
87
+
88
+ const envKey = providerDef.envKey;
89
+ const apiKey = process.env[envKey];
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 };
108
+ }
109
+ }
110
+
111
+ /** Finalize brain switch after API key was provided via chat. */
112
+ async switchBrainWithKey(providerKey, modelId, apiKey) {
113
+ const logger = getLogger();
114
+ const providerDef = PROVIDERS[providerKey];
115
+
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
+ }
134
+ }
135
+
136
+ /** Truncate a tool result. */
137
+ _truncateResult(name, result) {
138
+ let str = JSON.stringify(result);
139
+ if (str.length <= MAX_RESULT_LENGTH) return str;
140
+
141
+ if (result && typeof result === 'object') {
142
+ const truncated = { ...result };
143
+ for (const field of LARGE_FIELDS) {
144
+ if (typeof truncated[field] === 'string' && truncated[field].length > 500) {
145
+ truncated[field] = truncated[field].slice(0, 500) + `\n... [truncated ${truncated[field].length - 500} chars]`;
146
+ }
147
+ }
148
+ str = JSON.stringify(truncated);
149
+ if (str.length <= MAX_RESULT_LENGTH) return str;
150
+ }
151
+
152
+ return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
14
153
  }
15
154
 
16
155
  async processMessage(chatId, userMessage, user, onUpdate, sendPhoto) {
17
156
  const logger = getLogger();
18
157
 
19
- this._onUpdate = onUpdate || null;
20
- 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 });
21
162
 
22
163
  // Handle pending responses (confirmation or credential)
23
164
  const pending = this._pending.get(chatId);
24
165
  if (pending) {
25
166
  this._pending.delete(chatId);
167
+ logger.debug(`Orchestrator handling pending ${pending.type} response for chat ${chatId}`);
26
168
 
27
169
  if (pending.type === 'credential') {
28
- return await this._handleCredentialResponse(chatId, userMessage, user, pending);
29
- }
30
-
31
- if (pending.type === 'confirmation') {
32
- return await this._handleConfirmationResponse(chatId, userMessage, user, pending);
170
+ return await this._handleCredentialResponse(chatId, userMessage, user, pending, onUpdate);
33
171
  }
34
172
  }
35
173
 
36
- const { max_tool_depth } = this.config.brain;
174
+ const { max_tool_depth } = this.config.orchestrator;
37
175
 
38
176
  // Add user message to persistent history
39
177
  this.conversationManager.addMessage(chatId, 'user', userMessage);
40
178
 
41
- // Build working messages from history
42
- const messages = [...this.conversationManager.getHistory(chatId)];
179
+ // Build working messages from compressed history
180
+ const messages = [...this.conversationManager.getSummarizedHistory(chatId)];
181
+ logger.debug(`Orchestrator conversation context: ${messages.length} messages, max_depth=${max_tool_depth}`);
43
182
 
44
- return await this._runLoop(chatId, messages, user, 0, max_tool_depth);
45
- }
183
+ const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth);
46
184
 
47
- _formatToolSummary(name, input) {
48
- const key = {
49
- execute_command: 'command',
50
- read_file: 'path',
51
- write_file: 'path',
52
- list_directory: 'path',
53
- git_clone: 'repo',
54
- git_checkout: 'branch',
55
- git_commit: 'message',
56
- git_push: 'dir',
57
- git_diff: 'dir',
58
- github_create_pr: 'title',
59
- github_create_repo: 'name',
60
- github_list_prs: 'repo',
61
- github_get_pr_diff: 'repo',
62
- github_post_review: 'repo',
63
- spawn_claude_code: 'prompt',
64
- kill_process: 'pid',
65
- docker_exec: 'container',
66
- docker_logs: 'container',
67
- docker_compose: 'action',
68
- curl_url: 'url',
69
- check_port: 'port',
70
- screenshot_website: 'url',
71
- send_image: 'file_path',
72
- browse_website: 'url',
73
- extract_content: 'url',
74
- interact_with_page: 'url',
75
- }[name];
76
- const val = key && input[key] ? String(input[key]).slice(0, 120) : JSON.stringify(input).slice(0, 120);
77
- return `${name}: ${val}`;
185
+ logger.info(`Orchestrator reply for chat ${chatId}: "${(reply || '').slice(0, 150)}"`);
186
+
187
+ // Background persona extraction
188
+ this._extractPersonaBackground(userMessage, reply, user).catch(() => {});
189
+
190
+ return reply;
78
191
  }
79
192
 
80
- async _sendUpdate(text) {
81
- if (this._onUpdate) {
82
- 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 {}
83
197
  }
198
+ return null;
84
199
  }
85
200
 
86
- async _handleCredentialResponse(chatId, userMessage, user, pending) {
201
+ async _handleCredentialResponse(chatId, userMessage, user, pending, onUpdate) {
87
202
  const logger = getLogger();
88
203
  const value = userMessage.trim();
89
204
 
90
205
  if (value.toLowerCase() === 'skip' || value.toLowerCase() === 'cancel') {
91
206
  logger.info(`User skipped credential: ${pending.credential.envKey}`);
92
- pending.toolResults.push({
93
- type: 'tool_result',
94
- tool_use_id: pending.block.id,
95
- content: JSON.stringify({ error: `${pending.credential.label} not provided. Operation skipped.` }),
96
- });
97
- return await this._resumeAfterPause(chatId, user, pending);
207
+ return 'Credential skipped. You can provide it later.';
98
208
  }
99
209
 
100
- // Save the credential
101
210
  saveCredential(this.config, pending.credential.envKey, value);
102
211
  logger.info(`Saved credential: ${pending.credential.envKey}`);
212
+ return `Saved ${pending.credential.label}. You can now try the task again.`;
213
+ }
103
214
 
104
- // Now execute the original tool
105
- const result = await executeTool(pending.block.name, pending.block.input, {
106
- config: this.config,
107
- user,
108
- onUpdate: this._onUpdate,
109
- sendPhoto: this._sendPhoto,
110
- });
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`);
225
+
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}`);
111
232
 
112
- pending.toolResults.push({
113
- type: 'tool_result',
114
- tool_use_id: pending.block.id,
115
- content: JSON.stringify(result),
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
+ }
116
247
  });
117
248
 
118
- return await this._resumeAfterPause(chatId, user, pending);
119
- }
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;
120
253
 
121
- async _handleConfirmationResponse(chatId, userMessage, user, pending) {
122
- const logger = getLogger();
123
- const lower = userMessage.toLowerCase().trim();
254
+ logger.error(`[Orchestrator] Job failed event: ${job.id} [${job.workerType}] in chat ${chatId} — ${job.error}`);
124
255
 
125
- if (lower === 'yes' || lower === 'y' || lower === 'confirm') {
126
- logger.info(`User confirmed dangerous tool: ${pending.block.name}`);
127
- 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
+ });
128
260
 
129
- pending.toolResults.push({
130
- type: 'tool_result',
131
- tool_use_id: pending.block.id,
132
- content: JSON.stringify(result),
133
- });
134
- } else {
135
- logger.info(`User denied dangerous tool: ${pending.block.name}`);
136
- pending.toolResults.push({
137
- type: 'tool_result',
138
- tool_use_id: pending.block.id,
139
- content: JSON.stringify({ error: 'User denied this operation.' }),
140
- });
141
- }
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;
142
265
 
143
- return await this._resumeAfterPause(chatId, user, pending);
144
- }
266
+ logger.info(`[Orchestrator] Job cancelled event: ${job.id} [${job.workerType}] in chat ${chatId}`);
145
267
 
146
- async _resumeAfterPause(chatId, user, pending) {
147
- // Process remaining blocks
148
- for (const block of pending.remainingBlocks) {
149
- if (block.type !== 'tool_use') continue;
268
+ const msg = `🚫 **${label} cancelled** (\`${job.id}\`)`;
269
+ this._sendUpdate(chatId, msg);
270
+ });
271
+ }
150
272
 
151
- const pauseMsg = await this._checkPause(chatId, block, user, pending.toolResults, pending.remainingBlocks.filter((b) => b !== block), pending.messages);
152
- if (pauseMsg) return pauseMsg;
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
+ });
153
298
 
154
- const r = await executeTool(block.name, block.input, { config: this.config, user, onUpdate: this._onUpdate, sendPhoto: this._sendPhoto });
155
- pending.toolResults.push({
156
- type: 'tool_result',
157
- tool_use_id: block.id,
158
- content: JSON.stringify(r),
159
- });
160
- }
299
+ const summary = response.text || '';
300
+ logger.info(`[Orchestrator] Job ${job.id} summary: "${summary.slice(0, 200)}"`);
161
301
 
162
- pending.messages.push({ role: 'user', content: pending.toolResults });
163
- const { max_tool_depth } = this.config.brain;
164
- return await this._runLoop(chatId, pending.messages, user, 0, max_tool_depth);
302
+ return summary || null;
165
303
  }
166
304
 
167
- _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) {
168
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
+ };
169
360
 
170
- // Check missing credentials first
171
- const missing = getMissingCredential(block.name, this.config);
172
- if (missing) {
173
- logger.warn(`Missing credential for ${block.name}: ${missing.envKey}`);
174
- this._pending.set(chatId, {
175
- type: 'credential',
176
- block,
177
- credential: missing,
178
- context: { config: this.config, user },
179
- toolResults,
180
- remainingBlocks,
181
- messages,
182
- });
183
- 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.`;
184
- }
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}`);
185
365
 
186
- // Check dangerous operation confirmation
187
- const dangerLabel = checkConfirmation(block.name, block.input, this.config);
188
- if (dangerLabel) {
189
- logger.warn(`Dangerous tool detected: ${block.name} — ${dangerLabel}`);
190
- this._pending.set(chatId, {
191
- type: 'confirmation',
192
- block,
193
- context: { config: this.config, user },
194
- toolResults,
195
- remainingBlocks,
196
- messages,
197
- });
198
- return `⚠️ This action will **${dangerLabel}**.\n\n\`${block.name}\`: \`${JSON.stringify(block.input)}\`\n\nConfirm? (yes/no)`;
199
- }
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
+ });
200
396
 
201
- 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);
202
405
  }
203
406
 
204
407
  async _runLoop(chatId, messages, user, startDepth, maxDepth) {
205
408
  const logger = getLogger();
206
409
 
207
410
  for (let depth = startDepth; depth < maxDepth; depth++) {
208
- logger.debug(`Agent loop iteration ${depth + 1}/${maxDepth}`);
411
+ logger.info(`[Orchestrator] LLM call ${depth + 1}/${maxDepth} for chat ${chatId} — sending ${messages.length} messages`);
209
412
 
210
- const response = await this.provider.chat({
211
- system: this.systemPrompt,
413
+ const response = await this.orchestratorProvider.chat({
414
+ system: this._getSystemPrompt(chatId, user),
212
415
  messages,
213
- tools: toolDefinitions,
416
+ tools: orchestratorToolDefinitions,
214
417
  });
215
418
 
419
+ logger.info(`[Orchestrator] LLM response: stopReason=${response.stopReason}, text=${(response.text || '').length} chars, toolCalls=${(response.toolCalls || []).length}`);
420
+
216
421
  if (response.stopReason === 'end_turn') {
217
422
  const reply = response.text || '';
423
+ logger.info(`[Orchestrator] End turn — final reply: "${reply.slice(0, 200)}"`);
218
424
  this.conversationManager.addMessage(chatId, 'assistant', reply);
219
425
  return reply;
220
426
  }
@@ -222,42 +428,32 @@ export class Agent {
222
428
  if (response.stopReason === 'tool_use') {
223
429
  messages.push({ role: 'assistant', content: response.rawContent });
224
430
 
225
- // Send thinking text to the user
226
431
  if (response.text && response.text.trim()) {
227
- logger.info(`Agent thinking: ${response.text.slice(0, 200)}`);
228
- await this._sendUpdate(`💭 ${response.text}`);
432
+ logger.info(`[Orchestrator] Thinking: "${response.text.slice(0, 200)}"`);
229
433
  }
230
434
 
231
435
  const toolResults = [];
232
436
 
233
- for (let i = 0; i < response.toolCalls.length; i++) {
234
- const block = response.toolCalls[i];
235
-
236
- // Build a block-like object for _checkPause (needs .type for remainingBlocks filter)
237
- const blockObj = { type: 'tool_use', id: block.id, name: block.name, input: block.input };
238
-
239
- // Check if we need to pause (missing cred or dangerous action)
240
- const remaining = response.toolCalls.slice(i + 1).map((tc) => ({
241
- type: 'tool_use', id: tc.id, name: tc.name, input: tc.input,
242
- }));
243
- const pauseMsg = this._checkPause(chatId, blockObj, user, toolResults, remaining, messages);
244
- if (pauseMsg) return pauseMsg;
245
-
437
+ for (const block of response.toolCalls) {
246
438
  const summary = this._formatToolSummary(block.name, block.input);
247
- logger.info(`Tool call: ${summary}`);
248
- 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}`);
249
442
 
250
- const result = await executeTool(block.name, block.input, {
443
+ const result = await executeOrchestratorTool(block.name, block.input, {
444
+ chatId,
445
+ jobManager: this.jobManager,
251
446
  config: this.config,
252
- user,
253
- onUpdate: this._onUpdate,
254
- sendPhoto: this._sendPhoto,
447
+ spawnWorker: (job) => this._spawnWorker(job),
448
+ automationManager: this.automationManager,
255
449
  });
256
450
 
451
+ logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
452
+
257
453
  toolResults.push({
258
454
  type: 'tool_result',
259
455
  tool_use_id: block.id,
260
- content: JSON.stringify(result),
456
+ content: this._truncateResult(block.name, result),
261
457
  });
262
458
  }
263
459
 
@@ -266,7 +462,7 @@ export class Agent {
266
462
  }
267
463
 
268
464
  // Unexpected stop reason
269
- logger.warn(`Unexpected stopReason: ${response.stopReason}`);
465
+ logger.warn(`[Orchestrator] Unexpected stopReason: ${response.stopReason}`);
270
466
  if (response.text) {
271
467
  this.conversationManager.addMessage(chatId, 'assistant', response.text);
272
468
  return response.text;
@@ -274,10 +470,87 @@ export class Agent {
274
470
  return 'Something went wrong — unexpected response from the model.';
275
471
  }
276
472
 
277
- const depthWarning =
278
- `Reached maximum tool depth (${maxDepth}). Stopping to prevent infinite loops. ` +
279
- `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}).`;
280
475
  this.conversationManager.addMessage(chatId, 'assistant', depthWarning);
281
476
  return depthWarning;
282
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
+ }
283
553
  }
554
+
555
+ // Re-export as Agent for backward compatibility with bin/kernel.js import
556
+ export { OrchestratorAgent as Agent };