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.
- package/.env.example +4 -0
- package/README.md +0 -0
- package/bin/kernel.js +13 -6
- package/config.example.yaml +14 -1
- package/package.json +1 -1
- package/src/agent.js +482 -27
- package/src/automation/automation-manager.js +0 -0
- package/src/automation/automation.js +0 -0
- package/src/automation/index.js +0 -0
- package/src/automation/scheduler.js +0 -0
- package/src/bot.js +340 -3
- package/src/claude-auth.js +93 -0
- package/src/coder.js +48 -6
- package/src/conversation.js +0 -0
- package/src/intents/detector.js +0 -0
- package/src/intents/index.js +0 -0
- package/src/intents/planner.js +0 -0
- package/src/persona.js +0 -0
- package/src/prompts/orchestrator.js +53 -5
- package/src/prompts/persona.md +0 -0
- package/src/prompts/system.js +0 -0
- package/src/prompts/workers.js +61 -2
- package/src/providers/anthropic.js +0 -0
- package/src/providers/base.js +0 -0
- package/src/providers/index.js +0 -0
- package/src/providers/models.js +0 -0
- package/src/providers/openai-compat.js +0 -0
- package/src/security/audit.js +0 -0
- package/src/security/auth.js +0 -0
- package/src/security/confirm.js +0 -0
- package/src/self.js +122 -0
- package/src/services/stt.js +139 -0
- package/src/services/tts.js +124 -0
- package/src/skills/catalog.js +0 -0
- package/src/skills/custom.js +0 -0
- package/src/swarm/job-manager.js +54 -7
- package/src/swarm/job.js +19 -1
- package/src/swarm/worker-registry.js +5 -0
- package/src/tools/browser.js +0 -0
- package/src/tools/categories.js +0 -0
- package/src/tools/coding.js +5 -0
- package/src/tools/docker.js +0 -0
- package/src/tools/git.js +0 -0
- package/src/tools/github.js +0 -0
- package/src/tools/index.js +0 -0
- package/src/tools/jira.js +0 -0
- package/src/tools/monitor.js +0 -0
- package/src/tools/network.js +0 -0
- package/src/tools/orchestrator-tools.js +76 -19
- package/src/tools/os.js +14 -1
- package/src/tools/persona.js +0 -0
- package/src/tools/process.js +0 -0
- package/src/utils/config.js +105 -2
- package/src/utils/display.js +0 -0
- package/src/utils/logger.js +0 -0
- 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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
56
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
*
|
|
276
|
-
*
|
|
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).
|
|
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
|
-
|
|
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
|
package/src/automation/index.js
CHANGED
|
File without changes
|
|
File without changes
|