kernelbot 1.0.28 → 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 (56) hide show
  1. package/.env.example +4 -0
  2. package/README.md +0 -0
  3. package/bin/kernel.js +13 -6
  4. package/config.example.yaml +14 -1
  5. package/package.json +1 -1
  6. package/src/agent.js +482 -27
  7. package/src/automation/automation-manager.js +0 -0
  8. package/src/automation/automation.js +0 -0
  9. package/src/automation/index.js +0 -0
  10. package/src/automation/scheduler.js +0 -0
  11. package/src/bot.js +340 -3
  12. package/src/claude-auth.js +93 -0
  13. package/src/coder.js +48 -6
  14. package/src/conversation.js +0 -0
  15. package/src/intents/detector.js +0 -0
  16. package/src/intents/index.js +0 -0
  17. package/src/intents/planner.js +0 -0
  18. package/src/persona.js +0 -0
  19. package/src/prompts/orchestrator.js +53 -5
  20. package/src/prompts/persona.md +0 -0
  21. package/src/prompts/system.js +0 -0
  22. package/src/prompts/workers.js +61 -2
  23. package/src/providers/anthropic.js +0 -0
  24. package/src/providers/base.js +0 -0
  25. package/src/providers/index.js +0 -0
  26. package/src/providers/models.js +0 -0
  27. package/src/providers/openai-compat.js +0 -0
  28. package/src/security/audit.js +0 -0
  29. package/src/security/auth.js +0 -0
  30. package/src/security/confirm.js +0 -0
  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 +0 -0
  35. package/src/skills/custom.js +0 -0
  36. package/src/swarm/job-manager.js +54 -7
  37. package/src/swarm/job.js +19 -1
  38. package/src/swarm/worker-registry.js +5 -0
  39. package/src/tools/browser.js +0 -0
  40. package/src/tools/categories.js +0 -0
  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 +0 -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 +76 -19
  50. package/src/tools/os.js +14 -1
  51. package/src/tools/persona.js +0 -0
  52. package/src/tools/process.js +0 -0
  53. package/src/utils/config.js +105 -2
  54. package/src/utils/display.js +0 -0
  55. package/src/utils/logger.js +0 -0
  56. package/src/worker.js +96 -5
package/src/agent.js CHANGED
@@ -7,29 +7,34 @@ import { getWorkerPrompt } from './prompts/workers.js';
7
7
  import { getUnifiedSkillById } from './skills/custom.js';
8
8
  import { WorkerAgent } from './worker.js';
9
9
  import { getLogger } from './utils/logger.js';
10
- 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';
11
12
 
12
13
  const MAX_RESULT_LENGTH = 3000;
13
14
  const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
14
15
 
15
16
  export class OrchestratorAgent {
16
- constructor({ config, conversationManager, personaManager, jobManager, automationManager }) {
17
+ constructor({ config, conversationManager, personaManager, selfManager, jobManager, automationManager }) {
17
18
  this.config = config;
18
19
  this.conversationManager = conversationManager;
19
20
  this.personaManager = personaManager;
21
+ this.selfManager = selfManager || null;
20
22
  this.jobManager = jobManager;
21
23
  this.automationManager = automationManager || null;
22
24
  this._pending = new Map(); // chatId -> pending state
23
25
  this._chatCallbacks = new Map(); // chatId -> { onUpdate, sendPhoto }
24
26
 
25
- // Orchestrator always uses Anthropic (30s timeout — lean dispatch/summarize calls)
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;
26
31
  this.orchestratorProvider = createProvider({
27
32
  brain: {
28
- provider: 'anthropic',
33
+ provider: orchProviderKey,
29
34
  model: config.orchestrator.model,
30
35
  max_tokens: config.orchestrator.max_tokens,
31
36
  temperature: config.orchestrator.temperature,
32
- api_key: config.orchestrator.api_key || process.env.ANTHROPIC_API_KEY,
37
+ api_key: orchApiKey,
33
38
  timeout: 30_000,
34
39
  },
35
40
  });
@@ -52,8 +57,13 @@ export class OrchestratorAgent {
52
57
  userPersona = this.personaManager.load(user.id, user.username);
53
58
  }
54
59
 
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);
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);
57
67
  }
58
68
 
59
69
  setSkill(chatId, skillId) {
@@ -79,6 +89,131 @@ export class OrchestratorAgent {
79
89
  return { provider, providerName, model, modelLabel };
80
90
  }
81
91
 
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) {
105
+ const logger = getLogger();
106
+ const providerDef = PROVIDERS[providerKey];
107
+ if (!providerDef) return `Unknown provider: ${providerKey}`;
108
+
109
+ const envKey = providerDef.envKey;
110
+ const apiKey = process.env[envKey];
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 };
170
+ }
171
+ }
172
+
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
+ }
181
+
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
+ }
190
+
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
+ }
205
+
206
+ return info;
207
+ }
208
+
209
+ /** Set Claude Code auth mode + credential at runtime. */
210
+ setClaudeCodeAuth(mode, value) {
211
+ const logger = getLogger();
212
+ saveClaudeCodeAuth(this.config, mode, value);
213
+ resetClaudeCodeSpawner();
214
+ logger.info(`Claude Code auth mode set to: ${mode}`);
215
+ }
216
+
82
217
  /** Switch worker brain provider/model at runtime. */
83
218
  async switchBrain(providerKey, modelId) {
84
219
  const logger = getLogger();
@@ -184,8 +319,9 @@ export class OrchestratorAgent {
184
319
 
185
320
  logger.info(`Orchestrator reply for chat ${chatId}: "${(reply || '').slice(0, 150)}"`);
186
321
 
187
- // Background persona extraction
322
+ // Background persona extraction + self-reflection
188
323
  this._extractPersonaBackground(userMessage, reply, user).catch(() => {});
324
+ this._reflectOnSelfBackground(userMessage, reply, user).catch(() => {});
189
325
 
190
326
  return reply;
191
327
  }
@@ -221,28 +357,44 @@ export class OrchestratorAgent {
221
357
  const workerDef = WORKER_TYPES[job.workerType] || {};
222
358
  const label = workerDef.label || job.workerType;
223
359
 
224
- logger.info(`[Orchestrator] Job completed event: ${job.id} [${job.workerType}] in chat ${chatId} (${job.duration}s) — result length: ${(job.result || '').length} chars`);
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}`);
225
361
 
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)
362
+ // 1. IMMEDIATELY notify user (guarantees they see something regardless of summary LLM)
234
363
  const notifyMsgId = await this._sendUpdate(chatId, `✅ ${label} finished! Preparing summary...`);
235
364
 
236
- // 3. Try to summarize (provider timeout protects against hangs)
365
+ // 2. Try to summarize, then store ONE message in history (summary or fallback not both)
237
366
  try {
238
367
  const summary = await this._summarizeJobResult(chatId, job);
239
368
  if (summary) {
240
369
  this.conversationManager.addMessage(chatId, 'assistant', summary);
241
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(() => {});
242
376
  }
243
377
  } catch (err) {
244
378
  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(() => {});
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
+ }
384
+ });
385
+
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
+ }
246
398
  }
247
399
  });
248
400
 
@@ -272,8 +424,9 @@ export class OrchestratorAgent {
272
424
 
273
425
  /**
274
426
  * 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.
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).
277
430
  * Returns the summary text, or null. Caller handles delivery.
278
431
  */
279
432
  async _summarizeJobResult(chatId, job) {
@@ -283,6 +436,29 @@ export class OrchestratorAgent {
283
436
 
284
437
  logger.info(`[Orchestrator] Summarizing job ${job.id} [${job.workerType}] result for user`);
285
438
 
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');
458
+ } else {
459
+ resultContext = (job.result || 'Done.').slice(0, 8000);
460
+ }
461
+
286
462
  const history = this.conversationManager.getSummarizedHistory(chatId);
287
463
 
288
464
  const response = await this.orchestratorProvider.chat({
@@ -291,7 +467,7 @@ export class OrchestratorAgent {
291
467
  ...history,
292
468
  {
293
469
  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.`,
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.`,
295
471
  },
296
472
  ],
297
473
  });
@@ -302,6 +478,143 @@ export class OrchestratorAgent {
302
478
  return summary || null;
303
479
  }
304
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
+ }
514
+ }
515
+
516
+ return parts.join('\n\n');
517
+ }
518
+
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
+ }
539
+
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
+ }
544
+
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}`);
556
+ }
557
+
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');
616
+ }
617
+
305
618
  /**
306
619
  * Spawn a worker for a job — called from dispatch_task handler.
307
620
  * Creates smart progress reporting via editable Telegram message.
@@ -361,7 +674,10 @@ export class OrchestratorAgent {
361
674
  // Get scoped tools and skill
362
675
  const tools = getToolsForWorker(job.workerType);
363
676
  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}`);
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'}`);
365
681
 
366
682
  const worker = new WorkerAgent({
367
683
  config: this.config,
@@ -369,17 +685,19 @@ export class OrchestratorAgent {
369
685
  jobId: job.id,
370
686
  tools,
371
687
  skillId,
688
+ workerContext,
372
689
  callbacks: {
373
690
  onProgress: (text) => addActivity(text),
691
+ onHeartbeat: (text) => job.addProgress(text),
374
692
  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)}"`);
693
+ onComplete: (result, parsedResult) => {
694
+ logger.info(`[Worker ${job.id}] Completed — structured=${!!parsedResult?.structured}, result: "${(result || '').slice(0, 150)}"`);
377
695
  // Final status message update
378
696
  if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
379
697
  if (statusMsgId && onUpdate) {
380
698
  onUpdate(buildStatusText('done'), { editMessageId: statusMsgId }).catch(() => {});
381
699
  }
382
- this.jobManager.completeJob(job.id, result);
700
+ this.jobManager.completeJob(job.id, result, parsedResult || null);
383
701
  },
384
702
  onError: (err) => {
385
703
  logger.error(`[Worker ${job.id}] Error — ${err.message || String(err)}`);
@@ -404,15 +722,78 @@ export class OrchestratorAgent {
404
722
  return worker.run(job.task);
405
723
  }
406
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}` : ''}`);
743
+ }
744
+
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(', ')}`);
750
+ }
751
+
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')}`;
780
+ }
781
+
407
782
  async _runLoop(chatId, messages, user, startDepth, maxDepth) {
408
783
  const logger = getLogger();
409
784
 
410
785
  for (let depth = startDepth; depth < maxDepth; depth++) {
411
786
  logger.info(`[Orchestrator] LLM call ${depth + 1}/${maxDepth} for chat ${chatId} — sending ${messages.length} messages`);
412
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
+
413
794
  const response = await this.orchestratorProvider.chat({
414
795
  system: this._getSystemPrompt(chatId, user),
415
- messages,
796
+ messages: workingMessages,
416
797
  tools: orchestratorToolDefinitions,
417
798
  });
418
799
 
@@ -446,6 +827,7 @@ export class OrchestratorAgent {
446
827
  config: this.config,
447
828
  spawnWorker: (job) => this._spawnWorker(job),
448
829
  automationManager: this.automationManager,
830
+ user,
449
831
  });
450
832
 
451
833
  logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
@@ -550,6 +932,79 @@ export class OrchestratorAgent {
550
932
  logger.debug(`Persona extraction skipped: ${err.message}`);
551
933
  }
552
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
+ }
553
1008
  }
554
1009
 
555
1010
  // Re-export as Agent for backward compatibility with bin/kernel.js import
File without changes
File without changes
File without changes
File without changes