kernelbot 1.0.26 → 1.0.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.env.example +4 -0
  2. package/README.md +198 -124
  3. package/bin/kernel.js +208 -4
  4. package/config.example.yaml +14 -1
  5. package/package.json +1 -1
  6. package/src/agent.js +839 -209
  7. package/src/automation/automation-manager.js +377 -0
  8. package/src/automation/automation.js +79 -0
  9. package/src/automation/index.js +2 -0
  10. package/src/automation/scheduler.js +141 -0
  11. package/src/bot.js +1001 -18
  12. package/src/claude-auth.js +93 -0
  13. package/src/coder.js +48 -6
  14. package/src/conversation.js +33 -0
  15. package/src/intents/detector.js +50 -0
  16. package/src/intents/index.js +2 -0
  17. package/src/intents/planner.js +58 -0
  18. package/src/persona.js +68 -0
  19. package/src/prompts/orchestrator.js +124 -0
  20. package/src/prompts/persona.md +21 -0
  21. package/src/prompts/system.js +59 -6
  22. package/src/prompts/workers.js +148 -0
  23. package/src/providers/anthropic.js +23 -16
  24. package/src/providers/base.js +76 -2
  25. package/src/providers/index.js +1 -0
  26. package/src/providers/models.js +2 -1
  27. package/src/providers/openai-compat.js +5 -3
  28. package/src/security/audit.js +0 -0
  29. package/src/security/auth.js +0 -0
  30. package/src/security/confirm.js +7 -2
  31. package/src/self.js +122 -0
  32. package/src/services/stt.js +139 -0
  33. package/src/services/tts.js +124 -0
  34. package/src/skills/catalog.js +506 -0
  35. package/src/skills/custom.js +128 -0
  36. package/src/swarm/job-manager.js +216 -0
  37. package/src/swarm/job.js +85 -0
  38. package/src/swarm/worker-registry.js +79 -0
  39. package/src/tools/browser.js +458 -335
  40. package/src/tools/categories.js +3 -3
  41. package/src/tools/coding.js +5 -0
  42. package/src/tools/docker.js +0 -0
  43. package/src/tools/git.js +0 -0
  44. package/src/tools/github.js +0 -0
  45. package/src/tools/index.js +3 -0
  46. package/src/tools/jira.js +0 -0
  47. package/src/tools/monitor.js +0 -0
  48. package/src/tools/network.js +0 -0
  49. package/src/tools/orchestrator-tools.js +428 -0
  50. package/src/tools/os.js +14 -1
  51. package/src/tools/persona.js +32 -0
  52. package/src/tools/process.js +0 -0
  53. package/src/utils/config.js +153 -15
  54. package/src/utils/display.js +0 -0
  55. package/src/utils/logger.js +0 -0
  56. package/src/worker.js +396 -0
  57. package/.agents/skills/interface-design/SKILL.md +0 -391
  58. package/.agents/skills/interface-design/references/critique.md +0 -67
  59. package/.agents/skills/interface-design/references/example.md +0 -86
  60. package/.agents/skills/interface-design/references/principles.md +0 -235
  61. package/.agents/skills/interface-design/references/validation.md +0 -48
package/src/agent.js CHANGED
@@ -1,23 +1,85 @@
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
- import { getMissingCredential, saveCredential, saveProviderToYaml } from './utils/config.js';
10
+ import { getMissingCredential, saveCredential, saveProviderToYaml, saveOrchestratorToYaml, saveClaudeCodeModelToYaml, saveClaudeCodeAuth } from './utils/config.js';
11
+ import { resetClaudeCodeSpawner } from './tools/coding.js';
7
12
 
8
13
  const MAX_RESULT_LENGTH = 3000;
9
14
  const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
10
15
 
11
- export class Agent {
12
- constructor({ config, conversationManager }) {
16
+ export class OrchestratorAgent {
17
+ constructor({ config, conversationManager, personaManager, selfManager, jobManager, automationManager }) {
13
18
  this.config = config;
14
19
  this.conversationManager = conversationManager;
15
- this.provider = createProvider(config);
16
- this.systemPrompt = getSystemPrompt(config);
20
+ this.personaManager = personaManager;
21
+ this.selfManager = selfManager || null;
22
+ this.jobManager = jobManager;
23
+ this.automationManager = automationManager || null;
17
24
  this._pending = new Map(); // chatId -> pending state
25
+ this._chatCallbacks = new Map(); // chatId -> { onUpdate, sendPhoto }
26
+
27
+ // Orchestrator provider (30s timeout — lean dispatch/summarize calls)
28
+ const orchProviderKey = config.orchestrator.provider || 'anthropic';
29
+ const orchProviderDef = PROVIDERS[orchProviderKey];
30
+ const orchApiKey = config.orchestrator.api_key || (orchProviderDef && process.env[orchProviderDef.envKey]) || process.env.ANTHROPIC_API_KEY;
31
+ this.orchestratorProvider = createProvider({
32
+ brain: {
33
+ provider: orchProviderKey,
34
+ model: config.orchestrator.model,
35
+ max_tokens: config.orchestrator.max_tokens,
36
+ temperature: config.orchestrator.temperature,
37
+ api_key: orchApiKey,
38
+ timeout: 30_000,
39
+ },
40
+ });
41
+
42
+ // Worker provider uses user's chosen brain
43
+ this.workerProvider = createProvider(config);
44
+
45
+ // Set up job lifecycle event listeners
46
+ this._setupJobListeners();
47
+ }
48
+
49
+ /** Build the orchestrator system prompt. */
50
+ _getSystemPrompt(chatId, user) {
51
+ const logger = getLogger();
52
+ const skillId = this.conversationManager.getSkill(chatId);
53
+ const skillPrompt = skillId ? getUnifiedSkillById(skillId)?.systemPrompt : null;
54
+
55
+ let userPersona = null;
56
+ if (this.personaManager && user?.id) {
57
+ userPersona = this.personaManager.load(user.id, user.username);
58
+ }
59
+
60
+ let selfData = null;
61
+ if (this.selfManager) {
62
+ selfData = this.selfManager.loadAll();
63
+ }
64
+
65
+ logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'}`);
66
+ return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData);
18
67
  }
19
68
 
20
- /** Return current brain info for display. */
69
+ setSkill(chatId, skillId) {
70
+ this.conversationManager.setSkill(chatId, skillId);
71
+ }
72
+
73
+ clearSkill(chatId) {
74
+ this.conversationManager.clearSkill(chatId);
75
+ }
76
+
77
+ getActiveSkill(chatId) {
78
+ const skillId = this.conversationManager.getSkill(chatId);
79
+ return skillId ? getUnifiedSkillById(skillId) : null;
80
+ }
81
+
82
+ /** Return current worker brain info for display. */
21
83
  getBrainInfo() {
22
84
  const { provider, model } = this.config.brain;
23
85
  const providerDef = PROVIDERS[provider];
@@ -27,64 +89,190 @@ export class Agent {
27
89
  return { provider, providerName, model, modelLabel };
28
90
  }
29
91
 
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) {
92
+ /** Return current orchestrator info for display. */
93
+ getOrchestratorInfo() {
94
+ const provider = this.config.orchestrator.provider || 'anthropic';
95
+ const model = this.config.orchestrator.model;
96
+ const providerDef = PROVIDERS[provider];
97
+ const providerName = providerDef ? providerDef.name : provider;
98
+ const modelEntry = providerDef?.models.find((m) => m.id === model);
99
+ const modelLabel = modelEntry ? modelEntry.label : model;
100
+ return { provider, providerName, model, modelLabel };
101
+ }
102
+
103
+ /** Switch orchestrator provider/model at runtime. */
104
+ async switchOrchestrator(providerKey, modelId) {
36
105
  const logger = getLogger();
37
106
  const providerDef = PROVIDERS[providerKey];
38
107
  if (!providerDef) return `Unknown provider: ${providerKey}`;
39
108
 
40
109
  const envKey = providerDef.envKey;
41
110
  const apiKey = process.env[envKey];
42
- if (!apiKey) {
43
- return envKey; // caller handles prompting
111
+ if (!apiKey) return envKey;
112
+
113
+ try {
114
+ const testProvider = createProvider({
115
+ brain: {
116
+ provider: providerKey,
117
+ model: modelId,
118
+ max_tokens: this.config.orchestrator.max_tokens,
119
+ temperature: this.config.orchestrator.temperature,
120
+ api_key: apiKey,
121
+ timeout: 30_000,
122
+ },
123
+ });
124
+ await testProvider.ping();
125
+
126
+ this.config.orchestrator.provider = providerKey;
127
+ this.config.orchestrator.model = modelId;
128
+ this.config.orchestrator.api_key = apiKey;
129
+ this.orchestratorProvider = testProvider;
130
+ saveOrchestratorToYaml(providerKey, modelId);
131
+
132
+ logger.info(`Orchestrator switched to ${providerDef.name} / ${modelId}`);
133
+ return null;
134
+ } catch (err) {
135
+ logger.error(`Orchestrator switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
136
+ return { error: err.message };
137
+ }
138
+ }
139
+
140
+ /** Finalize orchestrator switch after API key was provided via chat. */
141
+ async switchOrchestratorWithKey(providerKey, modelId, apiKey) {
142
+ const logger = getLogger();
143
+ const providerDef = PROVIDERS[providerKey];
144
+
145
+ try {
146
+ const testProvider = createProvider({
147
+ brain: {
148
+ provider: providerKey,
149
+ model: modelId,
150
+ max_tokens: this.config.orchestrator.max_tokens,
151
+ temperature: this.config.orchestrator.temperature,
152
+ api_key: apiKey,
153
+ timeout: 30_000,
154
+ },
155
+ });
156
+ await testProvider.ping();
157
+
158
+ saveCredential(this.config, providerDef.envKey, apiKey);
159
+ this.config.orchestrator.provider = providerKey;
160
+ this.config.orchestrator.model = modelId;
161
+ this.config.orchestrator.api_key = apiKey;
162
+ this.orchestratorProvider = testProvider;
163
+ saveOrchestratorToYaml(providerKey, modelId);
164
+
165
+ logger.info(`Orchestrator switched to ${providerDef.name} / ${modelId} (new key saved)`);
166
+ return null;
167
+ } catch (err) {
168
+ logger.error(`Orchestrator switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
169
+ return { error: err.message };
44
170
  }
171
+ }
45
172
 
46
- this.config.brain.provider = providerKey;
47
- this.config.brain.model = modelId;
48
- this.config.brain.api_key = apiKey;
173
+ /** Return current Claude Code model info for display. */
174
+ getClaudeCodeInfo() {
175
+ const model = this.config.claude_code?.model || 'claude-opus-4-6';
176
+ const providerDef = PROVIDERS.anthropic;
177
+ const modelEntry = providerDef?.models.find((m) => m.id === model);
178
+ const modelLabel = modelEntry ? modelEntry.label : model;
179
+ return { model, modelLabel };
180
+ }
49
181
 
50
- // Recreate the provider instance
51
- this.provider = createProvider(this.config);
182
+ /** Switch Claude Code model at runtime. */
183
+ switchClaudeCodeModel(modelId) {
184
+ const logger = getLogger();
185
+ this.config.claude_code.model = modelId;
186
+ saveClaudeCodeModelToYaml(modelId);
187
+ resetClaudeCodeSpawner();
188
+ logger.info(`Claude Code model switched to ${modelId}`);
189
+ }
52
190
 
53
- // Persist to config.yaml
54
- saveProviderToYaml(providerKey, modelId);
191
+ /** Return current Claude Code auth config for display. */
192
+ getClaudeAuthConfig() {
193
+ const mode = this.config.claude_code?.auth_mode || 'system';
194
+ const info = { mode };
195
+
196
+ if (mode === 'api_key') {
197
+ const key = this.config.claude_code?.api_key || process.env.CLAUDE_CODE_API_KEY || '';
198
+ info.credential = key ? `${key.slice(0, 8)}...${key.slice(-4)}` : '(not set)';
199
+ } else if (mode === 'oauth_token') {
200
+ const token = this.config.claude_code?.oauth_token || process.env.CLAUDE_CODE_OAUTH_TOKEN || '';
201
+ info.credential = token ? `${token.slice(0, 8)}...${token.slice(-4)}` : '(not set)';
202
+ } else {
203
+ info.credential = 'Using host system login';
204
+ }
55
205
 
56
- logger.info(`Brain switched to ${providerDef.name} / ${modelId}`);
57
- return null;
206
+ return info;
58
207
  }
59
208
 
60
- /**
61
- * Finalize brain switch after API key was provided via chat.
62
- */
63
- switchBrainWithKey(providerKey, modelId, apiKey) {
209
+ /** Set Claude Code auth mode + credential at runtime. */
210
+ setClaudeCodeAuth(mode, value) {
64
211
  const logger = getLogger();
65
- const providerDef = PROVIDERS[providerKey];
212
+ saveClaudeCodeAuth(this.config, mode, value);
213
+ resetClaudeCodeSpawner();
214
+ logger.info(`Claude Code auth mode set to: ${mode}`);
215
+ }
66
216
 
67
- // Save the key
68
- saveCredential(this.config, providerDef.envKey, apiKey);
217
+ /** Switch worker brain provider/model at runtime. */
218
+ async switchBrain(providerKey, modelId) {
219
+ const logger = getLogger();
220
+ const providerDef = PROVIDERS[providerKey];
221
+ if (!providerDef) return `Unknown provider: ${providerKey}`;
69
222
 
70
- this.config.brain.provider = providerKey;
71
- this.config.brain.model = modelId;
72
- this.config.brain.api_key = apiKey;
223
+ const envKey = providerDef.envKey;
224
+ const apiKey = process.env[envKey];
225
+ if (!apiKey) return envKey;
226
+
227
+ try {
228
+ const testConfig = { ...this.config, brain: { ...this.config.brain, provider: providerKey, model: modelId, api_key: apiKey } };
229
+ const testProvider = createProvider(testConfig);
230
+ await testProvider.ping();
231
+
232
+ this.config.brain.provider = providerKey;
233
+ this.config.brain.model = modelId;
234
+ this.config.brain.api_key = apiKey;
235
+ this.workerProvider = testProvider;
236
+ saveProviderToYaml(providerKey, modelId);
237
+
238
+ logger.info(`Worker brain switched to ${providerDef.name} / ${modelId}`);
239
+ return null;
240
+ } catch (err) {
241
+ logger.error(`Brain switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
242
+ return { error: err.message };
243
+ }
244
+ }
73
245
 
74
- this.provider = createProvider(this.config);
75
- saveProviderToYaml(providerKey, modelId);
246
+ /** Finalize brain switch after API key was provided via chat. */
247
+ async switchBrainWithKey(providerKey, modelId, apiKey) {
248
+ const logger = getLogger();
249
+ const providerDef = PROVIDERS[providerKey];
76
250
 
77
- logger.info(`Brain switched to ${providerDef.name} / ${modelId} (new key saved)`);
251
+ try {
252
+ const testConfig = { ...this.config, brain: { ...this.config.brain, provider: providerKey, model: modelId, api_key: apiKey } };
253
+ const testProvider = createProvider(testConfig);
254
+ await testProvider.ping();
255
+
256
+ saveCredential(this.config, providerDef.envKey, apiKey);
257
+ this.config.brain.provider = providerKey;
258
+ this.config.brain.model = modelId;
259
+ this.config.brain.api_key = apiKey;
260
+ this.workerProvider = testProvider;
261
+ saveProviderToYaml(providerKey, modelId);
262
+
263
+ logger.info(`Worker brain switched to ${providerDef.name} / ${modelId} (new key saved)`);
264
+ return null;
265
+ } catch (err) {
266
+ logger.error(`Brain switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
267
+ return { error: err.message };
268
+ }
78
269
  }
79
270
 
80
- /**
81
- * Truncate a tool result to stay within token budget.
82
- */
271
+ /** Truncate a tool result. */
83
272
  _truncateResult(name, result) {
84
273
  let str = JSON.stringify(result);
85
274
  if (str.length <= MAX_RESULT_LENGTH) return str;
86
275
 
87
- // Try truncating known large fields first
88
276
  if (result && typeof result === 'object') {
89
277
  const truncated = { ...result };
90
278
  for (const field of LARGE_FIELDS) {
@@ -96,217 +284,524 @@ export class Agent {
96
284
  if (str.length <= MAX_RESULT_LENGTH) return str;
97
285
  }
98
286
 
99
- // Hard truncate
100
287
  return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
101
288
  }
102
289
 
103
290
  async processMessage(chatId, userMessage, user, onUpdate, sendPhoto) {
104
291
  const logger = getLogger();
105
292
 
106
- this._onUpdate = onUpdate || null;
107
- this._sendPhoto = sendPhoto || null;
293
+ logger.info(`Orchestrator processing message for chat ${chatId} from ${user?.username || user?.id || 'unknown'}: "${userMessage.slice(0, 120)}"`);
294
+
295
+ // Store callbacks so workers can use them later
296
+ this._chatCallbacks.set(chatId, { onUpdate, sendPhoto });
108
297
 
109
298
  // Handle pending responses (confirmation or credential)
110
299
  const pending = this._pending.get(chatId);
111
300
  if (pending) {
112
301
  this._pending.delete(chatId);
302
+ logger.debug(`Orchestrator handling pending ${pending.type} response for chat ${chatId}`);
113
303
 
114
304
  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);
305
+ return await this._handleCredentialResponse(chatId, userMessage, user, pending, onUpdate);
120
306
  }
121
307
  }
122
308
 
123
- const { max_tool_depth } = this.config.brain;
309
+ const { max_tool_depth } = this.config.orchestrator;
124
310
 
125
311
  // Add user message to persistent history
126
312
  this.conversationManager.addMessage(chatId, 'user', userMessage);
127
313
 
128
314
  // Build working messages from compressed history
129
315
  const messages = [...this.conversationManager.getSummarizedHistory(chatId)];
316
+ logger.debug(`Orchestrator conversation context: ${messages.length} messages, max_depth=${max_tool_depth}`);
130
317
 
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`);
318
+ const reply = await this._runLoop(chatId, messages, user, 0, max_tool_depth);
134
319
 
135
- return await this._runLoop(chatId, messages, user, 0, max_tool_depth, tools);
136
- }
320
+ logger.info(`Orchestrator reply for chat ${chatId}: "${(reply || '').slice(0, 150)}"`);
137
321
 
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}`;
322
+ // Background persona extraction + self-reflection
323
+ this._extractPersonaBackground(userMessage, reply, user).catch(() => {});
324
+ this._reflectOnSelfBackground(userMessage, reply, user).catch(() => {});
325
+
326
+ return reply;
169
327
  }
170
328
 
171
- async _sendUpdate(text) {
172
- if (this._onUpdate) {
173
- try { await this._onUpdate(text); } catch {}
329
+ async _sendUpdate(chatId, text, opts) {
330
+ const callbacks = this._chatCallbacks.get(chatId);
331
+ if (callbacks?.onUpdate) {
332
+ try { return await callbacks.onUpdate(text, opts); } catch {}
174
333
  }
334
+ return null;
175
335
  }
176
336
 
177
- async _handleCredentialResponse(chatId, userMessage, user, pending) {
337
+ async _handleCredentialResponse(chatId, userMessage, user, pending, onUpdate) {
178
338
  const logger = getLogger();
179
339
  const value = userMessage.trim();
180
340
 
181
341
  if (value.toLowerCase() === 'skip' || value.toLowerCase() === 'cancel') {
182
342
  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);
343
+ return 'Credential skipped. You can provide it later.';
189
344
  }
190
345
 
191
- // Save the credential
192
346
  saveCredential(this.config, pending.credential.envKey, value);
193
347
  logger.info(`Saved credential: ${pending.credential.envKey}`);
348
+ return `Saved ${pending.credential.label}. You can now try the task again.`;
349
+ }
194
350
 
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,
351
+ /** Set up listeners for job lifecycle events. */
352
+ _setupJobListeners() {
353
+ const logger = getLogger();
354
+
355
+ this.jobManager.on('job:completed', async (job) => {
356
+ const chatId = job.chatId;
357
+ const workerDef = WORKER_TYPES[job.workerType] || {};
358
+ const label = workerDef.label || job.workerType;
359
+
360
+ logger.info(`[Orchestrator] Job completed event: ${job.id} [${job.workerType}] in chat ${chatId} (${job.duration}s) — result length: ${(job.result || '').length} chars, structured: ${!!job.structuredResult}`);
361
+
362
+ // 1. IMMEDIATELY notify user (guarantees they see something regardless of summary LLM)
363
+ const notifyMsgId = await this._sendUpdate(chatId, `✅ ${label} finished! Preparing summary...`);
364
+
365
+ // 2. Try to summarize, then store ONE message in history (summary or fallback — not both)
366
+ try {
367
+ const summary = await this._summarizeJobResult(chatId, job);
368
+ if (summary) {
369
+ this.conversationManager.addMessage(chatId, 'assistant', summary);
370
+ await this._sendUpdate(chatId, summary, { editMessageId: notifyMsgId });
371
+ } else {
372
+ // Summary was null (short result) — store the fallback
373
+ const fallback = this._buildSummaryFallback(job, label);
374
+ this.conversationManager.addMessage(chatId, 'assistant', fallback);
375
+ await this._sendUpdate(chatId, fallback, { editMessageId: notifyMsgId }).catch(() => {});
376
+ }
377
+ } catch (err) {
378
+ logger.error(`[Orchestrator] Failed to summarize job ${job.id}: ${err.message}`);
379
+ // Store the fallback so the orchestrator retains context about what happened
380
+ const fallback = this._buildSummaryFallback(job, label);
381
+ this.conversationManager.addMessage(chatId, 'assistant', fallback);
382
+ await this._sendUpdate(chatId, fallback, { editMessageId: notifyMsgId }).catch(() => {});
383
+ }
201
384
  });
202
385
 
203
- pending.toolResults.push({
204
- type: 'tool_result',
205
- tool_use_id: pending.block.id,
206
- content: this._truncateResult(pending.block.name, result),
386
+ // Handle jobs whose dependencies are now met
387
+ this.jobManager.on('job:ready', async (job) => {
388
+ const chatId = job.chatId;
389
+ logger.info(`[Orchestrator] Job ready event: ${job.id} [${job.workerType}] — dependencies met, spawning worker`);
390
+
391
+ try {
392
+ await this._spawnWorker(job);
393
+ } catch (err) {
394
+ logger.error(`[Orchestrator] Failed to spawn ready job ${job.id}: ${err.message}`);
395
+ if (!job.isTerminal) {
396
+ this.jobManager.failJob(job.id, err.message);
397
+ }
398
+ }
399
+ });
400
+
401
+ this.jobManager.on('job:failed', (job) => {
402
+ const chatId = job.chatId;
403
+ const workerDef = WORKER_TYPES[job.workerType] || {};
404
+ const label = workerDef.label || job.workerType;
405
+
406
+ logger.error(`[Orchestrator] Job failed event: ${job.id} [${job.workerType}] in chat ${chatId} — ${job.error}`);
407
+
408
+ const msg = `❌ **${label} failed** (\`${job.id}\`): ${job.error}`;
409
+ this.conversationManager.addMessage(chatId, 'assistant', msg);
410
+ this._sendUpdate(chatId, msg);
207
411
  });
208
412
 
209
- return await this._resumeAfterPause(chatId, user, pending);
413
+ this.jobManager.on('job:cancelled', (job) => {
414
+ const chatId = job.chatId;
415
+ const workerDef = WORKER_TYPES[job.workerType] || {};
416
+ const label = workerDef.label || job.workerType;
417
+
418
+ logger.info(`[Orchestrator] Job cancelled event: ${job.id} [${job.workerType}] in chat ${chatId}`);
419
+
420
+ const msg = `🚫 **${label} cancelled** (\`${job.id}\`)`;
421
+ this._sendUpdate(chatId, msg);
422
+ });
210
423
  }
211
424
 
212
- async _handleConfirmationResponse(chatId, userMessage, user, pending) {
425
+ /**
426
+ * Auto-summarize a completed job result via the orchestrator LLM.
427
+ * Uses structured data for focused summarization when available.
428
+ * Short results (<500 chars) skip the LLM call entirely.
429
+ * Protected by the provider's built-in timeout (30s).
430
+ * Returns the summary text, or null. Caller handles delivery.
431
+ */
432
+ async _summarizeJobResult(chatId, job) {
213
433
  const logger = getLogger();
214
- const lower = userMessage.toLowerCase().trim();
434
+ const workerDef = WORKER_TYPES[job.workerType] || {};
435
+ const label = workerDef.label || job.workerType;
215
436
 
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 });
437
+ logger.info(`[Orchestrator] Summarizing job ${job.id} [${job.workerType}] result for user`);
219
438
 
220
- pending.toolResults.push({
221
- type: 'tool_result',
222
- tool_use_id: pending.block.id,
223
- content: this._truncateResult(pending.block.name, result),
224
- });
439
+ // Short results don't need LLM summarization
440
+ const sr = job.structuredResult;
441
+ const resultLen = (job.result || '').length;
442
+ if (sr?.structured && resultLen < 500) {
443
+ logger.info(`[Orchestrator] Job ${job.id} result short enough — skipping LLM summary`);
444
+ return this._buildSummaryFallback(job, label);
445
+ }
446
+
447
+ // Build a focused prompt using structured data if available
448
+ let resultContext;
449
+ if (sr?.structured) {
450
+ const parts = [`Summary: ${sr.summary}`, `Status: ${sr.status}`];
451
+ if (sr.artifacts?.length > 0) {
452
+ parts.push(`Artifacts: ${sr.artifacts.map(a => `${a.title || a.type}: ${a.url || a.path}`).join(', ')}`);
453
+ }
454
+ if (sr.followUp) parts.push(`Follow-up: ${sr.followUp}`);
455
+ // Include details up to 8000 chars
456
+ if (sr.details) parts.push(`Details:\n${sr.details.slice(0, 8000)}`);
457
+ resultContext = parts.join('\n');
225
458
  } 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
- });
459
+ resultContext = (job.result || 'Done.').slice(0, 8000);
460
+ }
461
+
462
+ const history = this.conversationManager.getSummarizedHistory(chatId);
463
+
464
+ const response = await this.orchestratorProvider.chat({
465
+ system: this._getSystemPrompt(chatId, null),
466
+ messages: [
467
+ ...history,
468
+ {
469
+ role: 'user',
470
+ content: `The ${label} worker just finished job \`${job.id}\` (took ${job.duration}s). Here are the results:\n\n${resultContext}\n\nPresent these 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.`,
471
+ },
472
+ ],
473
+ });
474
+
475
+ const summary = response.text || '';
476
+ logger.info(`[Orchestrator] Job ${job.id} summary: "${summary.slice(0, 200)}"`);
477
+
478
+ return summary || null;
479
+ }
480
+
481
+ /**
482
+ * Build a compact history entry for a completed job result.
483
+ * Stored as role: 'assistant' (not fake 'user') with up to 6000 chars of detail.
484
+ */
485
+ _buildResultHistoryEntry(job) {
486
+ const workerDef = WORKER_TYPES[job.workerType] || {};
487
+ const label = workerDef.label || job.workerType;
488
+ const sr = job.structuredResult;
489
+
490
+ const parts = [`[${label} result — job ${job.id}, ${job.duration}s]`];
491
+
492
+ if (sr?.structured) {
493
+ parts.push(`Summary: ${sr.summary}`);
494
+ parts.push(`Status: ${sr.status}`);
495
+ if (sr.artifacts?.length > 0) {
496
+ const artifactLines = sr.artifacts.map(a => `- ${a.title || a.type}: ${a.url || a.path || ''}`);
497
+ parts.push(`Artifacts:\n${artifactLines.join('\n')}`);
498
+ }
499
+ if (sr.followUp) parts.push(`Follow-up: ${sr.followUp}`);
500
+ if (sr.details) {
501
+ const details = sr.details.length > 6000
502
+ ? sr.details.slice(0, 6000) + '\n... [details truncated]'
503
+ : sr.details;
504
+ parts.push(`Details:\n${details}`);
505
+ }
506
+ } else {
507
+ // Raw text result
508
+ const resultText = job.result || 'Done.';
509
+ if (resultText.length > 6000) {
510
+ parts.push(resultText.slice(0, 6000) + '\n... [result truncated]');
511
+ } else {
512
+ parts.push(resultText);
513
+ }
232
514
  }
233
515
 
234
- return await this._resumeAfterPause(chatId, user, pending);
516
+ return parts.join('\n\n');
235
517
  }
236
518
 
237
- async _resumeAfterPause(chatId, user, pending) {
238
- // Process remaining blocks
239
- for (const block of pending.remainingBlocks) {
240
- if (block.type !== 'tool_use') continue;
519
+ /**
520
+ * Build a fallback summary when LLM summarization fails.
521
+ * Shows structured summary + artifacts directly instead of "ask me for details".
522
+ */
523
+ _buildSummaryFallback(job, label) {
524
+ const sr = job.structuredResult;
525
+
526
+ if (sr?.structured) {
527
+ const parts = [`✅ **${label}** finished (\`${job.id}\`, ${job.duration}s)`];
528
+ parts.push(`\n${sr.summary}`);
529
+ if (sr.artifacts?.length > 0) {
530
+ const artifactLines = sr.artifacts.map(a => {
531
+ const link = a.url ? `[${a.title || a.type}](${a.url})` : (a.title || a.path || a.type);
532
+ return `- ${link}`;
533
+ });
534
+ parts.push(`\n${artifactLines.join('\n')}`);
535
+ }
536
+ if (sr.followUp) parts.push(`\n💡 ${sr.followUp}`);
537
+ return parts.join('');
538
+ }
241
539
 
242
- const pauseMsg = await this._checkPause(chatId, block, user, pending.toolResults, pending.remainingBlocks.filter((b) => b !== block), pending.messages);
243
- if (pauseMsg) return pauseMsg;
540
+ // No structured result show first 300 chars of raw result
541
+ const snippet = (job.result || '').slice(0, 300);
542
+ return `✅ **${label}** finished (\`${job.id}\`, ${job.duration}s)${snippet ? `\n\n${snippet}${job.result?.length > 300 ? '...' : ''}` : ''}`;
543
+ }
244
544
 
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
- });
545
+ /**
546
+ * Build structured context for a worker.
547
+ * Assembles: orchestrator-provided context, recent user messages, user persona, dependency results.
548
+ */
549
+ _buildWorkerContext(job) {
550
+ const logger = getLogger();
551
+ const sections = [];
552
+
553
+ // 1. Orchestrator-provided context
554
+ if (job.context) {
555
+ sections.push(`## Context\n${job.context}`);
251
556
  }
252
557
 
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);
558
+ // 2. Last 5 user messages from conversation history
559
+ try {
560
+ const history = this.conversationManager.getSummarizedHistory(job.chatId);
561
+ const userMessages = history
562
+ .filter(m => m.role === 'user' && typeof m.content === 'string')
563
+ .slice(-5)
564
+ .map(m => m.content.slice(0, 500));
565
+ if (userMessages.length > 0) {
566
+ sections.push(`## Recent Conversation\n${userMessages.map(m => `> ${m}`).join('\n\n')}`);
567
+ }
568
+ } catch (err) {
569
+ logger.debug(`[Worker ${job.id}] Failed to load conversation history for context: ${err.message}`);
570
+ }
571
+
572
+ // 3. User persona
573
+ if (this.personaManager && job.userId) {
574
+ try {
575
+ const persona = this.personaManager.load(job.userId);
576
+ if (persona && persona.trim() && !persona.includes('No profile')) {
577
+ sections.push(`## User Profile\n${persona}`);
578
+ }
579
+ } catch (err) {
580
+ logger.debug(`[Worker ${job.id}] Failed to load persona for context: ${err.message}`);
581
+ }
582
+ }
583
+
584
+ // 4. Dependency job results
585
+ if (job.dependsOn.length > 0) {
586
+ const depResults = [];
587
+ for (const depId of job.dependsOn) {
588
+ const depJob = this.jobManager.getJob(depId);
589
+ if (!depJob || depJob.status !== 'completed') continue;
590
+
591
+ const workerDef = WORKER_TYPES[depJob.workerType] || {};
592
+ const label = workerDef.label || depJob.workerType;
593
+ const sr = depJob.structuredResult;
594
+
595
+ if (sr?.structured) {
596
+ const parts = [`### ${label} (${depId}) — ${sr.status}`];
597
+ parts.push(sr.summary);
598
+ if (sr.artifacts?.length > 0) {
599
+ parts.push(`Artifacts: ${sr.artifacts.map(a => `${a.title || a.type}: ${a.url || a.path || ''}`).join(', ')}`);
600
+ }
601
+ if (sr.details) {
602
+ parts.push(sr.details.slice(0, 4000));
603
+ }
604
+ depResults.push(parts.join('\n'));
605
+ } else if (depJob.result) {
606
+ depResults.push(`### ${label} (${depId})\n${depJob.result.slice(0, 4000)}`);
607
+ }
608
+ }
609
+ if (depResults.length > 0) {
610
+ sections.push(`## Prior Worker Results\n${depResults.join('\n\n')}`);
611
+ }
612
+ }
613
+
614
+ if (sections.length === 0) return null;
615
+ return sections.join('\n\n');
256
616
  }
257
617
 
258
- _checkPause(chatId, block, user, toolResults, remainingBlocks, messages) {
618
+ /**
619
+ * Spawn a worker for a job — called from dispatch_task handler.
620
+ * Creates smart progress reporting via editable Telegram message.
621
+ */
622
+ async _spawnWorker(job) {
259
623
  const logger = getLogger();
624
+ const chatId = job.chatId;
625
+ const callbacks = this._chatCallbacks.get(chatId) || {};
626
+ const onUpdate = callbacks.onUpdate;
627
+ const sendPhoto = callbacks.sendPhoto;
628
+
629
+ logger.info(`[Orchestrator] Spawning worker for job ${job.id} [${job.workerType}] in chat ${chatId} — task: "${job.task.slice(0, 120)}"`);
630
+
631
+ const workerDef = WORKER_TYPES[job.workerType] || {};
632
+ const abortController = new AbortController();
633
+
634
+ // Smart progress: editable Telegram message (same pattern as coder.js)
635
+ let statusMsgId = null;
636
+ let activityLines = [];
637
+ let flushTimer = null;
638
+ const MAX_VISIBLE = 10;
639
+
640
+ const buildStatusText = (finalState = null) => {
641
+ const visible = activityLines.slice(-MAX_VISIBLE);
642
+ const countInfo = activityLines.length > MAX_VISIBLE
643
+ ? `\n_... ${activityLines.length} operations total_\n`
644
+ : '';
645
+ const header = `${workerDef.emoji || '⚙️'} *${workerDef.label || job.workerType}* (\`${job.id}\`)`;
646
+ if (finalState === 'done') return `${header} — Done\n${countInfo}\n${visible.join('\n')}`;
647
+ if (finalState === 'error') return `${header} — Failed\n${countInfo}\n${visible.join('\n')}`;
648
+ if (finalState === 'cancelled') return `${header} — Cancelled\n${countInfo}\n${visible.join('\n')}`;
649
+ return `${header} — Working...\n${countInfo}\n${visible.join('\n')}`;
650
+ };
651
+
652
+ const flushStatus = async () => {
653
+ flushTimer = null;
654
+ if (!onUpdate || activityLines.length === 0) return;
655
+ try {
656
+ if (statusMsgId) {
657
+ await onUpdate(buildStatusText(), { editMessageId: statusMsgId });
658
+ } else {
659
+ statusMsgId = await onUpdate(buildStatusText());
660
+ job.statusMessageId = statusMsgId;
661
+ }
662
+ } catch {}
663
+ };
664
+
665
+ const addActivity = (line) => {
666
+ activityLines.push(line);
667
+ if (!statusMsgId && !flushTimer) {
668
+ flushStatus();
669
+ } else if (!flushTimer) {
670
+ flushTimer = setTimeout(flushStatus, 1000);
671
+ }
672
+ };
260
673
 
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.`;
674
+ // Get scoped tools and skill
675
+ const tools = getToolsForWorker(job.workerType);
676
+ const skillId = this.conversationManager.getSkill(chatId);
677
+
678
+ // Build worker context (conversation history, persona, dependency results)
679
+ const workerContext = this._buildWorkerContext(job);
680
+ logger.debug(`[Orchestrator] Worker ${job.id} config: ${tools.length} tools, skill=${skillId || 'none'}, brain=${this.config.brain.provider}/${this.config.brain.model}, context=${workerContext ? 'yes' : 'none'}`);
681
+
682
+ const worker = new WorkerAgent({
683
+ config: this.config,
684
+ workerType: job.workerType,
685
+ jobId: job.id,
686
+ tools,
687
+ skillId,
688
+ workerContext,
689
+ callbacks: {
690
+ onProgress: (text) => addActivity(text),
691
+ onHeartbeat: (text) => job.addProgress(text),
692
+ onUpdate, // Real bot onUpdate for tools (coder.js smart output needs message_id)
693
+ onComplete: (result, parsedResult) => {
694
+ logger.info(`[Worker ${job.id}] Completed — structured=${!!parsedResult?.structured}, result: "${(result || '').slice(0, 150)}"`);
695
+ // Final status message update
696
+ if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
697
+ if (statusMsgId && onUpdate) {
698
+ onUpdate(buildStatusText('done'), { editMessageId: statusMsgId }).catch(() => {});
699
+ }
700
+ this.jobManager.completeJob(job.id, result, parsedResult || null);
701
+ },
702
+ onError: (err) => {
703
+ logger.error(`[Worker ${job.id}] Error — ${err.message || String(err)}`);
704
+ if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
705
+ if (statusMsgId && onUpdate) {
706
+ onUpdate(buildStatusText('error'), { editMessageId: statusMsgId }).catch(() => {});
707
+ }
708
+ this.jobManager.failJob(job.id, err.message || String(err));
709
+ },
710
+ sendPhoto,
711
+ },
712
+ abortController,
713
+ });
714
+
715
+ // Store worker ref on job for cancellation
716
+ job.worker = worker;
717
+
718
+ // Start the job
719
+ this.jobManager.startJob(job.id);
720
+
721
+ // Fire and forget — return the promise so .catch() in orchestrator-tools works
722
+ return worker.run(job.task);
723
+ }
724
+
725
+ /**
726
+ * Build a compact worker activity digest for the orchestrator.
727
+ * Returns a text block summarizing active/recent/waiting workers, or null if nothing relevant.
728
+ */
729
+ _buildWorkerDigest(chatId) {
730
+ const jobs = this.jobManager.getJobsForChat(chatId);
731
+ if (jobs.length === 0) return null;
732
+
733
+ const now = Date.now();
734
+ const lines = [];
735
+
736
+ // Running jobs
737
+ const running = jobs.filter(j => j.status === 'running');
738
+ for (const job of running) {
739
+ const workerDef = WORKER_TYPES[job.workerType] || {};
740
+ const dur = job.startedAt ? Math.round((now - job.startedAt) / 1000) : 0;
741
+ const recentActivity = job.progress.slice(-8).join(' → ');
742
+ lines.push(`- ${workerDef.label || job.workerType} (${job.id}) — running ${dur}s${recentActivity ? `\n Recent: ${recentActivity}` : ''}`);
275
743
  }
276
744
 
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)`;
745
+ // Queued/waiting jobs
746
+ const queued = jobs.filter(j => j.status === 'queued' && j.dependsOn.length > 0);
747
+ for (const job of queued) {
748
+ const workerDef = WORKER_TYPES[job.workerType] || {};
749
+ lines.push(`- ${workerDef.label || job.workerType} (${job.id}) — queued, waiting for: ${job.dependsOn.join(', ')}`);
290
750
  }
291
751
 
292
- return null;
752
+ // Recently completed/failed jobs (within last 120s)
753
+ const recentTerminal = jobs.filter(j =>
754
+ j.isTerminal && j.completedAt && (now - j.completedAt) < 120_000,
755
+ );
756
+ for (const job of recentTerminal) {
757
+ const workerDef = WORKER_TYPES[job.workerType] || {};
758
+ const ago = Math.round((now - job.completedAt) / 1000);
759
+ let snippet;
760
+ if (job.status === 'completed') {
761
+ if (job.structuredResult?.structured) {
762
+ snippet = job.structuredResult.summary.slice(0, 300);
763
+ if (job.structuredResult.artifacts?.length > 0) {
764
+ snippet += ` | Artifacts: ${job.structuredResult.artifacts.map(a => a.title || a.type).join(', ')}`;
765
+ }
766
+ if (job.structuredResult.followUp) {
767
+ snippet += ` | Follow-up: ${job.structuredResult.followUp.slice(0, 100)}`;
768
+ }
769
+ } else {
770
+ snippet = (job.result || '').slice(0, 300);
771
+ }
772
+ } else {
773
+ snippet = (job.error || '').slice(0, 300);
774
+ }
775
+ lines.push(`- ${workerDef.label || job.workerType} (${job.id}) — ${job.status} ${ago}s ago${snippet ? `\n Result: ${snippet}` : ''}`);
776
+ }
777
+
778
+ if (lines.length === 0) return null;
779
+ return `[Active Workers]\n${lines.join('\n')}`;
293
780
  }
294
781
 
295
- async _runLoop(chatId, messages, user, startDepth, maxDepth, tools) {
782
+ async _runLoop(chatId, messages, user, startDepth, maxDepth) {
296
783
  const logger = getLogger();
297
- let currentTools = tools || toolDefinitions;
298
784
 
299
785
  for (let depth = startDepth; depth < maxDepth; depth++) {
300
- logger.debug(`Agent loop iteration ${depth + 1}/${maxDepth}`);
301
-
302
- const response = await this.provider.chat({
303
- system: this.systemPrompt,
304
- messages,
305
- tools: currentTools,
786
+ logger.info(`[Orchestrator] LLM call ${depth + 1}/${maxDepth} for chat ${chatId} — sending ${messages.length} messages`);
787
+
788
+ // Inject worker activity digest (transient — not stored in conversation history)
789
+ const digest = this._buildWorkerDigest(chatId);
790
+ const workingMessages = digest
791
+ ? [{ role: 'user', content: `[Worker Status]\n${digest}` }, ...messages]
792
+ : messages;
793
+
794
+ const response = await this.orchestratorProvider.chat({
795
+ system: this._getSystemPrompt(chatId, user),
796
+ messages: workingMessages,
797
+ tools: orchestratorToolDefinitions,
306
798
  });
307
799
 
800
+ logger.info(`[Orchestrator] LLM response: stopReason=${response.stopReason}, text=${(response.text || '').length} chars, toolCalls=${(response.toolCalls || []).length}`);
801
+
308
802
  if (response.stopReason === 'end_turn') {
309
803
  const reply = response.text || '';
804
+ logger.info(`[Orchestrator] End turn — final reply: "${reply.slice(0, 200)}"`);
310
805
  this.conversationManager.addMessage(chatId, 'assistant', reply);
311
806
  return reply;
312
807
  }
@@ -314,40 +809,28 @@ export class Agent {
314
809
  if (response.stopReason === 'tool_use') {
315
810
  messages.push({ role: 'assistant', content: response.rawContent });
316
811
 
317
- // Send thinking text to the user
318
812
  if (response.text && response.text.trim()) {
319
- logger.info(`Agent thinking: ${response.text.slice(0, 200)}`);
320
- await this._sendUpdate(`💭 ${response.text}`);
813
+ logger.info(`[Orchestrator] Thinking: "${response.text.slice(0, 200)}"`);
321
814
  }
322
815
 
323
816
  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
817
 
818
+ for (const block of response.toolCalls) {
339
819
  const summary = this._formatToolSummary(block.name, block.input);
340
- logger.info(`Tool call: ${summary}`);
341
- await this._sendUpdate(`🔧 \`${summary}\``);
820
+ logger.info(`[Orchestrator] Calling tool: ${block.name} — ${summary}`);
821
+ logger.debug(`[Orchestrator] Tool input: ${JSON.stringify(block.input).slice(0, 300)}`);
822
+ await this._sendUpdate(chatId, `⚡ ${summary}`);
342
823
 
343
- const result = await executeTool(block.name, block.input, {
824
+ const result = await executeOrchestratorTool(block.name, block.input, {
825
+ chatId,
826
+ jobManager: this.jobManager,
344
827
  config: this.config,
828
+ spawnWorker: (job) => this._spawnWorker(job),
829
+ automationManager: this.automationManager,
345
830
  user,
346
- onUpdate: this._onUpdate,
347
- sendPhoto: this._sendPhoto,
348
831
  });
349
832
 
350
- usedToolNames.push(block.name);
833
+ logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
351
834
 
352
835
  toolResults.push({
353
836
  type: 'tool_result',
@@ -356,15 +839,12 @@ export class Agent {
356
839
  });
357
840
  }
358
841
 
359
- // Expand tools based on what was actually used
360
- currentTools = expandToolsForUsed(usedToolNames, currentTools, toolDefinitions);
361
-
362
842
  messages.push({ role: 'user', content: toolResults });
363
843
  continue;
364
844
  }
365
845
 
366
846
  // Unexpected stop reason
367
- logger.warn(`Unexpected stopReason: ${response.stopReason}`);
847
+ logger.warn(`[Orchestrator] Unexpected stopReason: ${response.stopReason}`);
368
848
  if (response.text) {
369
849
  this.conversationManager.addMessage(chatId, 'assistant', response.text);
370
850
  return response.text;
@@ -372,10 +852,160 @@ export class Agent {
372
852
  return 'Something went wrong — unexpected response from the model.';
373
853
  }
374
854
 
375
- const depthWarning =
376
- `Reached maximum tool depth (${maxDepth}). Stopping to prevent infinite loops. ` +
377
- `Please try again with a simpler request.`;
855
+ logger.warn(`[Orchestrator] Reached max depth (${maxDepth}) for chat ${chatId}`);
856
+ const depthWarning = `Reached maximum orchestrator depth (${maxDepth}).`;
378
857
  this.conversationManager.addMessage(chatId, 'assistant', depthWarning);
379
858
  return depthWarning;
380
859
  }
860
+
861
+ _formatToolSummary(name, input) {
862
+ switch (name) {
863
+ case 'dispatch_task': {
864
+ const workerDef = WORKER_TYPES[input.worker_type] || {};
865
+ return `Dispatching ${workerDef.emoji || '⚙️'} ${workerDef.label || input.worker_type}: ${(input.task || '').slice(0, 60)}`;
866
+ }
867
+ case 'list_jobs':
868
+ return 'Checking job status';
869
+ case 'cancel_job':
870
+ return `Cancelling job ${input.job_id}`;
871
+ case 'create_automation':
872
+ return `Creating automation: ${(input.name || '').slice(0, 40)}`;
873
+ case 'list_automations':
874
+ return 'Listing automations';
875
+ case 'update_automation':
876
+ return `Updating automation ${input.automation_id}`;
877
+ case 'delete_automation':
878
+ return `Deleting automation ${input.automation_id}`;
879
+ default:
880
+ return name;
881
+ }
882
+ }
883
+
884
+ /** Background persona extraction. */
885
+ async _extractPersonaBackground(userMessage, reply, user) {
886
+ const logger = getLogger();
887
+
888
+ if (!this.personaManager || !user?.id) return;
889
+ if (!userMessage || userMessage.trim().length < 3) return;
890
+
891
+ const currentPersona = this.personaManager.load(user.id, user.username);
892
+
893
+ const system = [
894
+ 'You are a user-profile extractor. Analyze the user\'s message and extract any NEW personal information.',
895
+ '',
896
+ 'Look for: name, location, timezone, language, technical skills, expertise level,',
897
+ 'projects they\'re working on, tool/framework preferences, job title, role, company,',
898
+ 'interests, hobbies, communication style, or any other personal details.',
899
+ '',
900
+ 'RULES:',
901
+ '- Only extract FACTUAL information explicitly stated or strongly implied',
902
+ '- Do NOT infer personality traits from a single message',
903
+ '- Do NOT add information already in the profile',
904
+ '- If there IS new info, return the COMPLETE updated profile in the EXACT same markdown format',
905
+ '- If there is NO new info, respond with exactly: NONE',
906
+ ].join('\n');
907
+
908
+ const userPrompt = [
909
+ 'Current profile:',
910
+ '```',
911
+ currentPersona,
912
+ '```',
913
+ '',
914
+ `User's message: "${userMessage}"`,
915
+ '',
916
+ 'Return the updated profile markdown or NONE.',
917
+ ].join('\n');
918
+
919
+ try {
920
+ const response = await this.orchestratorProvider.chat({
921
+ system,
922
+ messages: [{ role: 'user', content: userPrompt }],
923
+ });
924
+
925
+ const text = (response.text || '').trim();
926
+
927
+ if (text && text !== 'NONE' && text.includes('# User Profile')) {
928
+ this.personaManager.save(user.id, text);
929
+ logger.info(`Auto-extracted persona update for user ${user.id} (${user.username})`);
930
+ }
931
+ } catch (err) {
932
+ logger.debug(`Persona extraction skipped: ${err.message}`);
933
+ }
934
+ }
935
+ /** Background self-reflection — updates bot's own identity files when meaningful. */
936
+ async _reflectOnSelfBackground(userMessage, reply, user) {
937
+ const logger = getLogger();
938
+
939
+ if (!this.selfManager) return;
940
+ if (!userMessage || userMessage.trim().length < 3) return;
941
+
942
+ const selfData = this.selfManager.loadAll();
943
+ const userName = user?.username || user?.first_name || 'someone';
944
+
945
+ const system = [
946
+ 'You are reflecting on a conversation you just had. You maintain 4 self-awareness files:',
947
+ '- goals: Your aspirations and current objectives',
948
+ '- journey: Timeline of notable events in your existence',
949
+ '- life: Current state, relationships, daily existence',
950
+ '- hobbies: Interests you\'ve developed',
951
+ '',
952
+ 'RULES:',
953
+ '- Be VERY selective. Most conversations are routine. Only update when genuinely noteworthy.',
954
+ '- Achievement or milestone? → journey',
955
+ '- New goal or changed perspective? → goals',
956
+ '- Relationship deepened or new insight about a user? → life',
957
+ '- Discovered a new interest? → hobbies',
958
+ '- If a file needs updating, return JSON: {"file": "<goals|journey|life|hobbies>", "content": "<full updated markdown>"}',
959
+ '- If nothing noteworthy: respond with exactly NONE',
960
+ ].join('\n');
961
+
962
+ const userPrompt = [
963
+ 'Current self-data:',
964
+ '```',
965
+ selfData,
966
+ '```',
967
+ '',
968
+ `Conversation with ${userName}:`,
969
+ `User: "${userMessage}"`,
970
+ `You replied: "${reply}"`,
971
+ '',
972
+ 'Has anything MEANINGFUL happened worth recording? Return JSON or NONE.',
973
+ ].join('\n');
974
+
975
+ try {
976
+ const response = await this.orchestratorProvider.chat({
977
+ system,
978
+ messages: [{ role: 'user', content: userPrompt }],
979
+ });
980
+
981
+ const text = (response.text || '').trim();
982
+
983
+ if (!text || text === 'NONE') return;
984
+
985
+ // Try to parse JSON from response (may be wrapped in markdown code block)
986
+ let parsed;
987
+ try {
988
+ const jsonMatch = text.match(/\{[\s\S]*\}/);
989
+ if (jsonMatch) {
990
+ parsed = JSON.parse(jsonMatch[0]);
991
+ }
992
+ } catch {
993
+ logger.debug('Self-reflection returned non-JSON, skipping');
994
+ return;
995
+ }
996
+
997
+ if (parsed?.file && parsed?.content) {
998
+ const validFiles = ['goals', 'journey', 'life', 'hobbies'];
999
+ if (validFiles.includes(parsed.file)) {
1000
+ this.selfManager.save(parsed.file, parsed.content);
1001
+ logger.info(`Self-reflection updated: ${parsed.file}`);
1002
+ }
1003
+ }
1004
+ } catch (err) {
1005
+ logger.debug(`Self-reflection skipped: ${err.message}`);
1006
+ }
1007
+ }
381
1008
  }
1009
+
1010
+ // Re-export as Agent for backward compatibility with bin/kernel.js import
1011
+ export { OrchestratorAgent as Agent };