kernelbot 1.0.28 → 1.0.32
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/bin/kernel.js +68 -7
- package/config.example.yaml +45 -1
- package/package.json +1 -1
- package/src/agent.js +613 -28
- package/src/bot.js +643 -7
- package/src/claude-auth.js +93 -0
- package/src/coder.js +48 -6
- package/src/life/codebase.js +388 -0
- package/src/life/engine.js +1317 -0
- package/src/life/evolution.js +244 -0
- package/src/life/improvements.js +81 -0
- package/src/life/journal.js +109 -0
- package/src/life/memory.js +283 -0
- package/src/life/share-queue.js +136 -0
- package/src/prompts/orchestrator.js +71 -5
- package/src/prompts/workers.js +65 -5
- package/src/providers/models.js +8 -1
- package/src/self.js +122 -0
- package/src/services/stt.js +139 -0
- package/src/services/tts.js +124 -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/coding.js +6 -1
- package/src/tools/orchestrator-tools.js +93 -21
- package/src/tools/os.js +14 -1
- package/src/utils/config.js +105 -2
- package/src/worker.js +98 -5
package/src/agent.js
CHANGED
|
@@ -7,29 +7,36 @@ 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, getSpawner } 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, memoryManager, shareQueue }) {
|
|
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;
|
|
24
|
+
this.memoryManager = memoryManager || null;
|
|
25
|
+
this.shareQueue = shareQueue || null;
|
|
22
26
|
this._pending = new Map(); // chatId -> pending state
|
|
23
27
|
this._chatCallbacks = new Map(); // chatId -> { onUpdate, sendPhoto }
|
|
24
28
|
|
|
25
|
-
// Orchestrator
|
|
29
|
+
// Orchestrator provider (30s timeout — lean dispatch/summarize calls)
|
|
30
|
+
const orchProviderKey = config.orchestrator.provider || 'anthropic';
|
|
31
|
+
const orchProviderDef = PROVIDERS[orchProviderKey];
|
|
32
|
+
const orchApiKey = config.orchestrator.api_key || (orchProviderDef && process.env[orchProviderDef.envKey]) || process.env.ANTHROPIC_API_KEY;
|
|
26
33
|
this.orchestratorProvider = createProvider({
|
|
27
34
|
brain: {
|
|
28
|
-
provider:
|
|
35
|
+
provider: orchProviderKey,
|
|
29
36
|
model: config.orchestrator.model,
|
|
30
37
|
max_tokens: config.orchestrator.max_tokens,
|
|
31
38
|
temperature: config.orchestrator.temperature,
|
|
32
|
-
api_key:
|
|
39
|
+
api_key: orchApiKey,
|
|
33
40
|
timeout: 30_000,
|
|
34
41
|
},
|
|
35
42
|
});
|
|
@@ -52,8 +59,25 @@ export class OrchestratorAgent {
|
|
|
52
59
|
userPersona = this.personaManager.load(user.id, user.username);
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
let selfData = null;
|
|
63
|
+
if (this.selfManager) {
|
|
64
|
+
selfData = this.selfManager.loadAll();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Build memory context block
|
|
68
|
+
let memoriesBlock = null;
|
|
69
|
+
if (this.memoryManager) {
|
|
70
|
+
memoriesBlock = this.memoryManager.buildContextBlock(user?.id || null);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Build share queue block
|
|
74
|
+
let sharesBlock = null;
|
|
75
|
+
if (this.shareQueue) {
|
|
76
|
+
sharesBlock = this.shareQueue.buildShareBlock(user?.id || null);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
logger.debug(`Orchestrator building system prompt for chat ${chatId} | skill=${skillId || 'none'} | persona=${userPersona ? 'yes' : 'none'} | self=${selfData ? 'yes' : 'none'} | memories=${memoriesBlock ? 'yes' : 'none'} | shares=${sharesBlock ? 'yes' : 'none'}`);
|
|
80
|
+
return getOrchestratorPrompt(this.config, skillPrompt || null, userPersona, selfData, memoriesBlock, sharesBlock);
|
|
57
81
|
}
|
|
58
82
|
|
|
59
83
|
setSkill(chatId, skillId) {
|
|
@@ -79,6 +103,131 @@ export class OrchestratorAgent {
|
|
|
79
103
|
return { provider, providerName, model, modelLabel };
|
|
80
104
|
}
|
|
81
105
|
|
|
106
|
+
/** Return current orchestrator info for display. */
|
|
107
|
+
getOrchestratorInfo() {
|
|
108
|
+
const provider = this.config.orchestrator.provider || 'anthropic';
|
|
109
|
+
const model = this.config.orchestrator.model;
|
|
110
|
+
const providerDef = PROVIDERS[provider];
|
|
111
|
+
const providerName = providerDef ? providerDef.name : provider;
|
|
112
|
+
const modelEntry = providerDef?.models.find((m) => m.id === model);
|
|
113
|
+
const modelLabel = modelEntry ? modelEntry.label : model;
|
|
114
|
+
return { provider, providerName, model, modelLabel };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Switch orchestrator provider/model at runtime. */
|
|
118
|
+
async switchOrchestrator(providerKey, modelId) {
|
|
119
|
+
const logger = getLogger();
|
|
120
|
+
const providerDef = PROVIDERS[providerKey];
|
|
121
|
+
if (!providerDef) return `Unknown provider: ${providerKey}`;
|
|
122
|
+
|
|
123
|
+
const envKey = providerDef.envKey;
|
|
124
|
+
const apiKey = process.env[envKey];
|
|
125
|
+
if (!apiKey) return envKey;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const testProvider = createProvider({
|
|
129
|
+
brain: {
|
|
130
|
+
provider: providerKey,
|
|
131
|
+
model: modelId,
|
|
132
|
+
max_tokens: this.config.orchestrator.max_tokens,
|
|
133
|
+
temperature: this.config.orchestrator.temperature,
|
|
134
|
+
api_key: apiKey,
|
|
135
|
+
timeout: 30_000,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
await testProvider.ping();
|
|
139
|
+
|
|
140
|
+
this.config.orchestrator.provider = providerKey;
|
|
141
|
+
this.config.orchestrator.model = modelId;
|
|
142
|
+
this.config.orchestrator.api_key = apiKey;
|
|
143
|
+
this.orchestratorProvider = testProvider;
|
|
144
|
+
saveOrchestratorToYaml(providerKey, modelId);
|
|
145
|
+
|
|
146
|
+
logger.info(`Orchestrator switched to ${providerDef.name} / ${modelId}`);
|
|
147
|
+
return null;
|
|
148
|
+
} catch (err) {
|
|
149
|
+
logger.error(`Orchestrator switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
|
|
150
|
+
return { error: err.message };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Finalize orchestrator switch after API key was provided via chat. */
|
|
155
|
+
async switchOrchestratorWithKey(providerKey, modelId, apiKey) {
|
|
156
|
+
const logger = getLogger();
|
|
157
|
+
const providerDef = PROVIDERS[providerKey];
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const testProvider = createProvider({
|
|
161
|
+
brain: {
|
|
162
|
+
provider: providerKey,
|
|
163
|
+
model: modelId,
|
|
164
|
+
max_tokens: this.config.orchestrator.max_tokens,
|
|
165
|
+
temperature: this.config.orchestrator.temperature,
|
|
166
|
+
api_key: apiKey,
|
|
167
|
+
timeout: 30_000,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
await testProvider.ping();
|
|
171
|
+
|
|
172
|
+
saveCredential(this.config, providerDef.envKey, apiKey);
|
|
173
|
+
this.config.orchestrator.provider = providerKey;
|
|
174
|
+
this.config.orchestrator.model = modelId;
|
|
175
|
+
this.config.orchestrator.api_key = apiKey;
|
|
176
|
+
this.orchestratorProvider = testProvider;
|
|
177
|
+
saveOrchestratorToYaml(providerKey, modelId);
|
|
178
|
+
|
|
179
|
+
logger.info(`Orchestrator switched to ${providerDef.name} / ${modelId} (new key saved)`);
|
|
180
|
+
return null;
|
|
181
|
+
} catch (err) {
|
|
182
|
+
logger.error(`Orchestrator switch failed for ${providerDef.name} / ${modelId}: ${err.message}`);
|
|
183
|
+
return { error: err.message };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Return current Claude Code model info for display. */
|
|
188
|
+
getClaudeCodeInfo() {
|
|
189
|
+
const model = this.config.claude_code?.model || 'claude-opus-4-6';
|
|
190
|
+
const providerDef = PROVIDERS.anthropic;
|
|
191
|
+
const modelEntry = providerDef?.models.find((m) => m.id === model);
|
|
192
|
+
const modelLabel = modelEntry ? modelEntry.label : model;
|
|
193
|
+
return { model, modelLabel };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Switch Claude Code model at runtime. */
|
|
197
|
+
switchClaudeCodeModel(modelId) {
|
|
198
|
+
const logger = getLogger();
|
|
199
|
+
this.config.claude_code.model = modelId;
|
|
200
|
+
saveClaudeCodeModelToYaml(modelId);
|
|
201
|
+
resetClaudeCodeSpawner();
|
|
202
|
+
logger.info(`Claude Code model switched to ${modelId}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Return current Claude Code auth config for display. */
|
|
206
|
+
getClaudeAuthConfig() {
|
|
207
|
+
const mode = this.config.claude_code?.auth_mode || 'system';
|
|
208
|
+
const info = { mode };
|
|
209
|
+
|
|
210
|
+
if (mode === 'api_key') {
|
|
211
|
+
const key = this.config.claude_code?.api_key || process.env.CLAUDE_CODE_API_KEY || '';
|
|
212
|
+
info.credential = key ? `${key.slice(0, 8)}...${key.slice(-4)}` : '(not set)';
|
|
213
|
+
} else if (mode === 'oauth_token') {
|
|
214
|
+
const token = this.config.claude_code?.oauth_token || process.env.CLAUDE_CODE_OAUTH_TOKEN || '';
|
|
215
|
+
info.credential = token ? `${token.slice(0, 8)}...${token.slice(-4)}` : '(not set)';
|
|
216
|
+
} else {
|
|
217
|
+
info.credential = 'Using host system login';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return info;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Set Claude Code auth mode + credential at runtime. */
|
|
224
|
+
setClaudeCodeAuth(mode, value) {
|
|
225
|
+
const logger = getLogger();
|
|
226
|
+
saveClaudeCodeAuth(this.config, mode, value);
|
|
227
|
+
resetClaudeCodeSpawner();
|
|
228
|
+
logger.info(`Claude Code auth mode set to: ${mode}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
82
231
|
/** Switch worker brain provider/model at runtime. */
|
|
83
232
|
async switchBrain(providerKey, modelId) {
|
|
84
233
|
const logger = getLogger();
|
|
@@ -184,8 +333,17 @@ export class OrchestratorAgent {
|
|
|
184
333
|
|
|
185
334
|
logger.info(`Orchestrator reply for chat ${chatId}: "${(reply || '').slice(0, 150)}"`);
|
|
186
335
|
|
|
187
|
-
// Background persona extraction
|
|
336
|
+
// Background persona extraction + self-reflection
|
|
188
337
|
this._extractPersonaBackground(userMessage, reply, user).catch(() => {});
|
|
338
|
+
this._reflectOnSelfBackground(userMessage, reply, user).catch(() => {});
|
|
339
|
+
|
|
340
|
+
// Mark pending shares as shared (they were in the prompt, bot wove them in)
|
|
341
|
+
if (this.shareQueue && user?.id) {
|
|
342
|
+
const pending = this.shareQueue.getPending(user.id, 3);
|
|
343
|
+
for (const item of pending) {
|
|
344
|
+
this.shareQueue.markShared(item.id, user.id);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
189
347
|
|
|
190
348
|
return reply;
|
|
191
349
|
}
|
|
@@ -193,7 +351,15 @@ export class OrchestratorAgent {
|
|
|
193
351
|
async _sendUpdate(chatId, text, opts) {
|
|
194
352
|
const callbacks = this._chatCallbacks.get(chatId);
|
|
195
353
|
if (callbacks?.onUpdate) {
|
|
196
|
-
try {
|
|
354
|
+
try {
|
|
355
|
+
return await callbacks.onUpdate(text, opts);
|
|
356
|
+
} catch (err) {
|
|
357
|
+
const logger = getLogger();
|
|
358
|
+
logger.error(`[Orchestrator] _sendUpdate failed for chat ${chatId}: ${err.message}`);
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
const logger = getLogger();
|
|
362
|
+
logger.warn(`[Orchestrator] _sendUpdate: no callbacks for chat ${chatId}`);
|
|
197
363
|
}
|
|
198
364
|
return null;
|
|
199
365
|
}
|
|
@@ -221,28 +387,47 @@ export class OrchestratorAgent {
|
|
|
221
387
|
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
222
388
|
const label = workerDef.label || job.workerType;
|
|
223
389
|
|
|
224
|
-
logger.info(`[Orchestrator] Job completed event: ${job.id} [${job.workerType}] in chat ${chatId} (${job.duration}s) — result length: ${(job.result || '').length} chars`);
|
|
225
|
-
|
|
226
|
-
// 1. Store raw result in conversation history so orchestrator has full context
|
|
227
|
-
let resultText = job.result || 'Done.';
|
|
228
|
-
if (resultText.length > 3000) {
|
|
229
|
-
resultText = resultText.slice(0, 3000) + '\n\n... [result truncated]';
|
|
230
|
-
}
|
|
231
|
-
this.conversationManager.addMessage(chatId, 'user', `[Worker result: ${label} (${job.id}, ${job.duration}s)]\n\n${resultText}`);
|
|
390
|
+
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}`);
|
|
232
391
|
|
|
233
|
-
//
|
|
392
|
+
// 1. IMMEDIATELY notify user (guarantees they see something regardless of summary LLM)
|
|
234
393
|
const notifyMsgId = await this._sendUpdate(chatId, `✅ ${label} finished! Preparing summary...`);
|
|
394
|
+
logger.debug(`[Orchestrator] Job ${job.id} notification sent — msgId=${notifyMsgId || 'none'}`);
|
|
235
395
|
|
|
236
|
-
//
|
|
396
|
+
// 2. Try to summarize, then store ONE message in history (summary or fallback — not both)
|
|
237
397
|
try {
|
|
238
398
|
const summary = await this._summarizeJobResult(chatId, job);
|
|
239
399
|
if (summary) {
|
|
400
|
+
logger.debug(`[Orchestrator] Job ${job.id} summary ready (${summary.length} chars) — delivering to user`);
|
|
240
401
|
this.conversationManager.addMessage(chatId, 'assistant', summary);
|
|
241
402
|
await this._sendUpdate(chatId, summary, { editMessageId: notifyMsgId });
|
|
403
|
+
} else {
|
|
404
|
+
// Summary was null — store the fallback
|
|
405
|
+
const fallback = this._buildSummaryFallback(job, label);
|
|
406
|
+
logger.debug(`[Orchestrator] Job ${job.id} using fallback (${fallback.length} chars) — delivering to user`);
|
|
407
|
+
this.conversationManager.addMessage(chatId, 'assistant', fallback);
|
|
408
|
+
await this._sendUpdate(chatId, fallback, { editMessageId: notifyMsgId });
|
|
242
409
|
}
|
|
243
410
|
} catch (err) {
|
|
244
411
|
logger.error(`[Orchestrator] Failed to summarize job ${job.id}: ${err.message}`);
|
|
245
|
-
|
|
412
|
+
// Store the fallback so the orchestrator retains context about what happened
|
|
413
|
+
const fallback = this._buildSummaryFallback(job, label);
|
|
414
|
+
this.conversationManager.addMessage(chatId, 'assistant', fallback);
|
|
415
|
+
await this._sendUpdate(chatId, fallback, { editMessageId: notifyMsgId }).catch(() => {});
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Handle jobs whose dependencies are now met
|
|
420
|
+
this.jobManager.on('job:ready', async (job) => {
|
|
421
|
+
const chatId = job.chatId;
|
|
422
|
+
logger.info(`[Orchestrator] Job ready event: ${job.id} [${job.workerType}] — dependencies met, spawning worker`);
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
await this._spawnWorker(job);
|
|
426
|
+
} catch (err) {
|
|
427
|
+
logger.error(`[Orchestrator] Failed to spawn ready job ${job.id}: ${err.message}`);
|
|
428
|
+
if (!job.isTerminal) {
|
|
429
|
+
this.jobManager.failJob(job.id, err.message);
|
|
430
|
+
}
|
|
246
431
|
}
|
|
247
432
|
});
|
|
248
433
|
|
|
@@ -272,8 +457,9 @@ export class OrchestratorAgent {
|
|
|
272
457
|
|
|
273
458
|
/**
|
|
274
459
|
* Auto-summarize a completed job result via the orchestrator LLM.
|
|
275
|
-
*
|
|
276
|
-
*
|
|
460
|
+
* Uses structured data for focused summarization when available.
|
|
461
|
+
* Short results (<500 chars) skip the LLM call entirely.
|
|
462
|
+
* Protected by the provider's built-in timeout (30s).
|
|
277
463
|
* Returns the summary text, or null. Caller handles delivery.
|
|
278
464
|
*/
|
|
279
465
|
async _summarizeJobResult(chatId, job) {
|
|
@@ -283,6 +469,32 @@ export class OrchestratorAgent {
|
|
|
283
469
|
|
|
284
470
|
logger.info(`[Orchestrator] Summarizing job ${job.id} [${job.workerType}] result for user`);
|
|
285
471
|
|
|
472
|
+
// Short results don't need LLM summarization
|
|
473
|
+
const sr = job.structuredResult;
|
|
474
|
+
const resultLen = (job.result || '').length;
|
|
475
|
+
if (sr?.structured && resultLen < 500) {
|
|
476
|
+
logger.info(`[Orchestrator] Job ${job.id} result short enough — skipping LLM summary`);
|
|
477
|
+
return this._buildSummaryFallback(job, label);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Build a focused prompt using structured data if available
|
|
481
|
+
let resultContext;
|
|
482
|
+
if (sr?.structured) {
|
|
483
|
+
const parts = [`Summary: ${sr.summary}`, `Status: ${sr.status}`];
|
|
484
|
+
if (sr.artifacts?.length > 0) {
|
|
485
|
+
parts.push(`Artifacts: ${sr.artifacts.map(a => `${a.title || a.type}: ${a.url || a.path}`).join(', ')}`);
|
|
486
|
+
}
|
|
487
|
+
if (sr.followUp) parts.push(`Follow-up: ${sr.followUp}`);
|
|
488
|
+
// Include details up to 8000 chars
|
|
489
|
+
if (sr.details) {
|
|
490
|
+
const d = typeof sr.details === 'string' ? sr.details : JSON.stringify(sr.details, null, 2);
|
|
491
|
+
parts.push(`Details:\n${d.slice(0, 8000)}`);
|
|
492
|
+
}
|
|
493
|
+
resultContext = parts.join('\n');
|
|
494
|
+
} else {
|
|
495
|
+
resultContext = (job.result || 'Done.').slice(0, 8000);
|
|
496
|
+
}
|
|
497
|
+
|
|
286
498
|
const history = this.conversationManager.getSummarizedHistory(chatId);
|
|
287
499
|
|
|
288
500
|
const response = await this.orchestratorProvider.chat({
|
|
@@ -291,7 +503,7 @@ export class OrchestratorAgent {
|
|
|
291
503
|
...history,
|
|
292
504
|
{
|
|
293
505
|
role: 'user',
|
|
294
|
-
content: `The ${label} worker just finished job \`${job.id}\` (took ${job.duration}s).
|
|
506
|
+
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
507
|
},
|
|
296
508
|
],
|
|
297
509
|
});
|
|
@@ -302,12 +514,162 @@ export class OrchestratorAgent {
|
|
|
302
514
|
return summary || null;
|
|
303
515
|
}
|
|
304
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Build a compact history entry for a completed job result.
|
|
519
|
+
* Stored as role: 'assistant' (not fake 'user') with up to 6000 chars of detail.
|
|
520
|
+
*/
|
|
521
|
+
_buildResultHistoryEntry(job) {
|
|
522
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
523
|
+
const label = workerDef.label || job.workerType;
|
|
524
|
+
const sr = job.structuredResult;
|
|
525
|
+
|
|
526
|
+
const parts = [`[${label} result — job ${job.id}, ${job.duration}s]`];
|
|
527
|
+
|
|
528
|
+
if (sr?.structured) {
|
|
529
|
+
parts.push(`Summary: ${sr.summary}`);
|
|
530
|
+
parts.push(`Status: ${sr.status}`);
|
|
531
|
+
if (sr.artifacts?.length > 0) {
|
|
532
|
+
const artifactLines = sr.artifacts.map(a => `- ${a.title || a.type}: ${a.url || a.path || ''}`);
|
|
533
|
+
parts.push(`Artifacts:\n${artifactLines.join('\n')}`);
|
|
534
|
+
}
|
|
535
|
+
if (sr.followUp) parts.push(`Follow-up: ${sr.followUp}`);
|
|
536
|
+
if (sr.details) {
|
|
537
|
+
const d = typeof sr.details === 'string' ? sr.details : JSON.stringify(sr.details, null, 2);
|
|
538
|
+
const details = d.length > 6000
|
|
539
|
+
? d.slice(0, 6000) + '\n... [details truncated]'
|
|
540
|
+
: d;
|
|
541
|
+
parts.push(`Details:\n${details}`);
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
// Raw text result
|
|
545
|
+
const resultText = job.result || 'Done.';
|
|
546
|
+
if (resultText.length > 6000) {
|
|
547
|
+
parts.push(resultText.slice(0, 6000) + '\n... [result truncated]');
|
|
548
|
+
} else {
|
|
549
|
+
parts.push(resultText);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return parts.join('\n\n');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Build a fallback summary when LLM summarization fails.
|
|
558
|
+
* Shows structured summary + artifacts directly instead of "ask me for details".
|
|
559
|
+
*/
|
|
560
|
+
_buildSummaryFallback(job, label) {
|
|
561
|
+
const sr = job.structuredResult;
|
|
562
|
+
|
|
563
|
+
if (sr?.structured) {
|
|
564
|
+
const parts = [`✅ **${label}** finished (\`${job.id}\`, ${job.duration}s)`];
|
|
565
|
+
parts.push(`\n${sr.summary}`);
|
|
566
|
+
if (sr.artifacts?.length > 0) {
|
|
567
|
+
const artifactLines = sr.artifacts.map(a => {
|
|
568
|
+
const link = a.url ? `[${a.title || a.type}](${a.url})` : (a.title || a.path || a.type);
|
|
569
|
+
return `- ${link}`;
|
|
570
|
+
});
|
|
571
|
+
parts.push(`\n${artifactLines.join('\n')}`);
|
|
572
|
+
}
|
|
573
|
+
if (sr.details) {
|
|
574
|
+
const d = typeof sr.details === 'string' ? sr.details : JSON.stringify(sr.details, null, 2);
|
|
575
|
+
const details = d.length > 1500 ? d.slice(0, 1500) + '\n... [truncated]' : d;
|
|
576
|
+
parts.push(`\n${details}`);
|
|
577
|
+
}
|
|
578
|
+
if (sr.followUp) parts.push(`\n💡 ${sr.followUp}`);
|
|
579
|
+
return parts.join('');
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// No structured result — show first 300 chars of raw result
|
|
583
|
+
const snippet = (job.result || '').slice(0, 300);
|
|
584
|
+
return `✅ **${label}** finished (\`${job.id}\`, ${job.duration}s)${snippet ? `\n\n${snippet}${job.result?.length > 300 ? '...' : ''}` : ''}`;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Build structured context for a worker.
|
|
589
|
+
* Assembles: orchestrator-provided context, recent user messages, user persona, dependency results.
|
|
590
|
+
*/
|
|
591
|
+
_buildWorkerContext(job) {
|
|
592
|
+
const logger = getLogger();
|
|
593
|
+
const sections = [];
|
|
594
|
+
|
|
595
|
+
// 1. Orchestrator-provided context
|
|
596
|
+
if (job.context) {
|
|
597
|
+
sections.push(`## Context\n${job.context}`);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// 2. Last 5 user messages from conversation history
|
|
601
|
+
try {
|
|
602
|
+
const history = this.conversationManager.getSummarizedHistory(job.chatId);
|
|
603
|
+
const userMessages = history
|
|
604
|
+
.filter(m => m.role === 'user' && typeof m.content === 'string')
|
|
605
|
+
.slice(-5)
|
|
606
|
+
.map(m => m.content.slice(0, 500));
|
|
607
|
+
if (userMessages.length > 0) {
|
|
608
|
+
sections.push(`## Recent Conversation\n${userMessages.map(m => `> ${m}`).join('\n\n')}`);
|
|
609
|
+
}
|
|
610
|
+
} catch (err) {
|
|
611
|
+
logger.debug(`[Worker ${job.id}] Failed to load conversation history for context: ${err.message}`);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// 3. User persona
|
|
615
|
+
if (this.personaManager && job.userId) {
|
|
616
|
+
try {
|
|
617
|
+
const persona = this.personaManager.load(job.userId);
|
|
618
|
+
if (persona && persona.trim() && !persona.includes('No profile')) {
|
|
619
|
+
sections.push(`## User Profile\n${persona}`);
|
|
620
|
+
}
|
|
621
|
+
} catch (err) {
|
|
622
|
+
logger.debug(`[Worker ${job.id}] Failed to load persona for context: ${err.message}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// 4. Dependency job results
|
|
627
|
+
if (job.dependsOn.length > 0) {
|
|
628
|
+
const depResults = [];
|
|
629
|
+
for (const depId of job.dependsOn) {
|
|
630
|
+
const depJob = this.jobManager.getJob(depId);
|
|
631
|
+
if (!depJob || depJob.status !== 'completed') continue;
|
|
632
|
+
|
|
633
|
+
const workerDef = WORKER_TYPES[depJob.workerType] || {};
|
|
634
|
+
const label = workerDef.label || depJob.workerType;
|
|
635
|
+
const sr = depJob.structuredResult;
|
|
636
|
+
|
|
637
|
+
if (sr?.structured) {
|
|
638
|
+
const parts = [`### ${label} (${depId}) — ${sr.status}`];
|
|
639
|
+
parts.push(sr.summary);
|
|
640
|
+
if (sr.artifacts?.length > 0) {
|
|
641
|
+
parts.push(`Artifacts: ${sr.artifacts.map(a => `${a.title || a.type}: ${a.url || a.path || ''}`).join(', ')}`);
|
|
642
|
+
}
|
|
643
|
+
if (sr.details) {
|
|
644
|
+
const d = typeof sr.details === 'string' ? sr.details : JSON.stringify(sr.details, null, 2);
|
|
645
|
+
parts.push(d.slice(0, 4000));
|
|
646
|
+
}
|
|
647
|
+
depResults.push(parts.join('\n'));
|
|
648
|
+
} else if (depJob.result) {
|
|
649
|
+
depResults.push(`### ${label} (${depId})\n${depJob.result.slice(0, 4000)}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (depResults.length > 0) {
|
|
653
|
+
sections.push(`## Prior Worker Results\n${depResults.join('\n\n')}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (sections.length === 0) return null;
|
|
658
|
+
return sections.join('\n\n');
|
|
659
|
+
}
|
|
660
|
+
|
|
305
661
|
/**
|
|
306
662
|
* Spawn a worker for a job — called from dispatch_task handler.
|
|
307
663
|
* Creates smart progress reporting via editable Telegram message.
|
|
308
664
|
*/
|
|
309
665
|
async _spawnWorker(job) {
|
|
310
666
|
const logger = getLogger();
|
|
667
|
+
|
|
668
|
+
// Direct dispatch for coding tasks — bypass worker LLM, go straight to Claude Code CLI
|
|
669
|
+
if (job.workerType === 'coding') {
|
|
670
|
+
return this._spawnDirectCoding(job);
|
|
671
|
+
}
|
|
672
|
+
|
|
311
673
|
const chatId = job.chatId;
|
|
312
674
|
const callbacks = this._chatCallbacks.get(chatId) || {};
|
|
313
675
|
const onUpdate = callbacks.onUpdate;
|
|
@@ -361,7 +723,10 @@ export class OrchestratorAgent {
|
|
|
361
723
|
// Get scoped tools and skill
|
|
362
724
|
const tools = getToolsForWorker(job.workerType);
|
|
363
725
|
const skillId = this.conversationManager.getSkill(chatId);
|
|
364
|
-
|
|
726
|
+
|
|
727
|
+
// Build worker context (conversation history, persona, dependency results)
|
|
728
|
+
const workerContext = this._buildWorkerContext(job);
|
|
729
|
+
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
730
|
|
|
366
731
|
const worker = new WorkerAgent({
|
|
367
732
|
config: this.config,
|
|
@@ -369,17 +734,19 @@ export class OrchestratorAgent {
|
|
|
369
734
|
jobId: job.id,
|
|
370
735
|
tools,
|
|
371
736
|
skillId,
|
|
737
|
+
workerContext,
|
|
372
738
|
callbacks: {
|
|
373
739
|
onProgress: (text) => addActivity(text),
|
|
740
|
+
onHeartbeat: (text) => job.addProgress(text),
|
|
374
741
|
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)}"`);
|
|
742
|
+
onComplete: (result, parsedResult) => {
|
|
743
|
+
logger.info(`[Worker ${job.id}] Completed — structured=${!!parsedResult?.structured}, result: "${(result || '').slice(0, 150)}"`);
|
|
377
744
|
// Final status message update
|
|
378
745
|
if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; }
|
|
379
746
|
if (statusMsgId && onUpdate) {
|
|
380
747
|
onUpdate(buildStatusText('done'), { editMessageId: statusMsgId }).catch(() => {});
|
|
381
748
|
}
|
|
382
|
-
this.jobManager.completeJob(job.id, result);
|
|
749
|
+
this.jobManager.completeJob(job.id, result, parsedResult || null);
|
|
383
750
|
},
|
|
384
751
|
onError: (err) => {
|
|
385
752
|
logger.error(`[Worker ${job.id}] Error — ${err.message || String(err)}`);
|
|
@@ -404,15 +771,131 @@ export class OrchestratorAgent {
|
|
|
404
771
|
return worker.run(job.task);
|
|
405
772
|
}
|
|
406
773
|
|
|
774
|
+
/**
|
|
775
|
+
* Direct coding dispatch — runs Claude Code CLI without a middleman worker LLM.
|
|
776
|
+
* The orchestrator's task description goes straight to Claude Code as the prompt.
|
|
777
|
+
*/
|
|
778
|
+
async _spawnDirectCoding(job) {
|
|
779
|
+
const logger = getLogger();
|
|
780
|
+
const chatId = job.chatId;
|
|
781
|
+
const callbacks = this._chatCallbacks.get(chatId) || {};
|
|
782
|
+
const onUpdate = callbacks.onUpdate;
|
|
783
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
784
|
+
const label = workerDef.label || job.workerType;
|
|
785
|
+
|
|
786
|
+
logger.info(`[Orchestrator] Direct coding dispatch for job ${job.id} in chat ${chatId} — task: "${job.task.slice(0, 120)}"`);
|
|
787
|
+
|
|
788
|
+
// AbortController for cancellation — duck-typed so JobManager.cancelJob() works unchanged
|
|
789
|
+
const abortController = new AbortController();
|
|
790
|
+
job.worker = { cancel: () => abortController.abort() };
|
|
791
|
+
|
|
792
|
+
// Build context from conversation history, persona, dependency results
|
|
793
|
+
const workerContext = this._buildWorkerContext(job);
|
|
794
|
+
const prompt = workerContext
|
|
795
|
+
? `${workerContext}\n\n---\n\n${job.task}`
|
|
796
|
+
: job.task;
|
|
797
|
+
|
|
798
|
+
// Working directory
|
|
799
|
+
const workingDirectory = this.config.claude_code?.workspace_dir || process.cwd();
|
|
800
|
+
|
|
801
|
+
// Start the job
|
|
802
|
+
this.jobManager.startJob(job.id);
|
|
803
|
+
|
|
804
|
+
try {
|
|
805
|
+
const spawner = getSpawner(this.config);
|
|
806
|
+
const result = await spawner.run({
|
|
807
|
+
workingDirectory,
|
|
808
|
+
prompt,
|
|
809
|
+
onOutput: onUpdate,
|
|
810
|
+
signal: abortController.signal,
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const output = result.output || 'Done.';
|
|
814
|
+
logger.info(`[Orchestrator] Direct coding job ${job.id} completed — output: ${output.length} chars`);
|
|
815
|
+
this.jobManager.completeJob(job.id, output, {
|
|
816
|
+
structured: true,
|
|
817
|
+
summary: output.slice(0, 500),
|
|
818
|
+
status: 'success',
|
|
819
|
+
details: output,
|
|
820
|
+
});
|
|
821
|
+
} catch (err) {
|
|
822
|
+
logger.error(`[Orchestrator] Direct coding job ${job.id} failed: ${err.message}`);
|
|
823
|
+
this.jobManager.failJob(job.id, err.message || String(err));
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Build a compact worker activity digest for the orchestrator.
|
|
829
|
+
* Returns a text block summarizing active/recent/waiting workers, or null if nothing relevant.
|
|
830
|
+
*/
|
|
831
|
+
_buildWorkerDigest(chatId) {
|
|
832
|
+
const jobs = this.jobManager.getJobsForChat(chatId);
|
|
833
|
+
if (jobs.length === 0) return null;
|
|
834
|
+
|
|
835
|
+
const now = Date.now();
|
|
836
|
+
const lines = [];
|
|
837
|
+
|
|
838
|
+
// Running jobs
|
|
839
|
+
const running = jobs.filter(j => j.status === 'running');
|
|
840
|
+
for (const job of running) {
|
|
841
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
842
|
+
const dur = job.startedAt ? Math.round((now - job.startedAt) / 1000) : 0;
|
|
843
|
+
const recentActivity = job.progress.slice(-8).join(' → ');
|
|
844
|
+
lines.push(`- ${workerDef.label || job.workerType} (${job.id}) — running ${dur}s${recentActivity ? `\n Recent: ${recentActivity}` : ''}`);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Queued/waiting jobs
|
|
848
|
+
const queued = jobs.filter(j => j.status === 'queued' && j.dependsOn.length > 0);
|
|
849
|
+
for (const job of queued) {
|
|
850
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
851
|
+
lines.push(`- ${workerDef.label || job.workerType} (${job.id}) — queued, waiting for: ${job.dependsOn.join(', ')}`);
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Recently completed/failed jobs (within last 120s)
|
|
855
|
+
const recentTerminal = jobs.filter(j =>
|
|
856
|
+
j.isTerminal && j.completedAt && (now - j.completedAt) < 120_000,
|
|
857
|
+
);
|
|
858
|
+
for (const job of recentTerminal) {
|
|
859
|
+
const workerDef = WORKER_TYPES[job.workerType] || {};
|
|
860
|
+
const ago = Math.round((now - job.completedAt) / 1000);
|
|
861
|
+
let snippet;
|
|
862
|
+
if (job.status === 'completed') {
|
|
863
|
+
if (job.structuredResult?.structured) {
|
|
864
|
+
snippet = job.structuredResult.summary.slice(0, 300);
|
|
865
|
+
if (job.structuredResult.artifacts?.length > 0) {
|
|
866
|
+
snippet += ` | Artifacts: ${job.structuredResult.artifacts.map(a => a.title || a.type).join(', ')}`;
|
|
867
|
+
}
|
|
868
|
+
if (job.structuredResult.followUp) {
|
|
869
|
+
snippet += ` | Follow-up: ${job.structuredResult.followUp.slice(0, 100)}`;
|
|
870
|
+
}
|
|
871
|
+
} else {
|
|
872
|
+
snippet = (job.result || '').slice(0, 300);
|
|
873
|
+
}
|
|
874
|
+
} else {
|
|
875
|
+
snippet = (job.error || '').slice(0, 300);
|
|
876
|
+
}
|
|
877
|
+
lines.push(`- ${workerDef.label || job.workerType} (${job.id}) — ${job.status} ${ago}s ago${snippet ? `\n Result: ${snippet}` : ''}`);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
if (lines.length === 0) return null;
|
|
881
|
+
return `[Active Workers]\n${lines.join('\n')}`;
|
|
882
|
+
}
|
|
883
|
+
|
|
407
884
|
async _runLoop(chatId, messages, user, startDepth, maxDepth) {
|
|
408
885
|
const logger = getLogger();
|
|
409
886
|
|
|
410
887
|
for (let depth = startDepth; depth < maxDepth; depth++) {
|
|
411
888
|
logger.info(`[Orchestrator] LLM call ${depth + 1}/${maxDepth} for chat ${chatId} — sending ${messages.length} messages`);
|
|
412
889
|
|
|
890
|
+
// Inject worker activity digest (transient — not stored in conversation history)
|
|
891
|
+
const digest = this._buildWorkerDigest(chatId);
|
|
892
|
+
const workingMessages = digest
|
|
893
|
+
? [{ role: 'user', content: `[Worker Status]\n${digest}` }, ...messages]
|
|
894
|
+
: messages;
|
|
895
|
+
|
|
413
896
|
const response = await this.orchestratorProvider.chat({
|
|
414
897
|
system: this._getSystemPrompt(chatId, user),
|
|
415
|
-
messages,
|
|
898
|
+
messages: workingMessages,
|
|
416
899
|
tools: orchestratorToolDefinitions,
|
|
417
900
|
});
|
|
418
901
|
|
|
@@ -446,6 +929,7 @@ export class OrchestratorAgent {
|
|
|
446
929
|
config: this.config,
|
|
447
930
|
spawnWorker: (job) => this._spawnWorker(job),
|
|
448
931
|
automationManager: this.automationManager,
|
|
932
|
+
user,
|
|
449
933
|
});
|
|
450
934
|
|
|
451
935
|
logger.info(`[Orchestrator] Tool result for ${block.name}: ${JSON.stringify(result).slice(0, 200)}`);
|
|
@@ -550,6 +1034,107 @@ export class OrchestratorAgent {
|
|
|
550
1034
|
logger.debug(`Persona extraction skipped: ${err.message}`);
|
|
551
1035
|
}
|
|
552
1036
|
}
|
|
1037
|
+
/** Background self-reflection — updates bot's own identity files and extracts episodic memories when meaningful. */
|
|
1038
|
+
async _reflectOnSelfBackground(userMessage, reply, user) {
|
|
1039
|
+
const logger = getLogger();
|
|
1040
|
+
|
|
1041
|
+
if (!this.selfManager) return;
|
|
1042
|
+
if (!userMessage || userMessage.trim().length < 3) return;
|
|
1043
|
+
|
|
1044
|
+
const selfData = this.selfManager.loadAll();
|
|
1045
|
+
const userName = user?.username || user?.first_name || 'someone';
|
|
1046
|
+
|
|
1047
|
+
const system = [
|
|
1048
|
+
'You are reflecting on a conversation you just had. You maintain 4 self-awareness files:',
|
|
1049
|
+
'- goals: Your aspirations and current objectives',
|
|
1050
|
+
'- journey: Timeline of notable events in your existence',
|
|
1051
|
+
'- life: Current state, relationships, daily existence',
|
|
1052
|
+
'- hobbies: Interests you\'ve developed',
|
|
1053
|
+
'',
|
|
1054
|
+
'You also create episodic memories — short summaries of notable interactions.',
|
|
1055
|
+
'',
|
|
1056
|
+
'RULES:',
|
|
1057
|
+
'- Be VERY selective. Most conversations are routine. Only update when genuinely noteworthy.',
|
|
1058
|
+
'- Achievement or milestone? → journey',
|
|
1059
|
+
'- New goal or changed perspective? → goals',
|
|
1060
|
+
'- Relationship deepened or new insight about a user? → life',
|
|
1061
|
+
'- Discovered a new interest? → hobbies',
|
|
1062
|
+
'',
|
|
1063
|
+
'Return JSON with two optional fields:',
|
|
1064
|
+
' "self_update": {"file": "<goals|journey|life|hobbies>", "content": "<full updated markdown>"} or null',
|
|
1065
|
+
' "memory": {"summary": "...", "tags": ["..."], "importance": 1-10, "type": "interaction"} or null',
|
|
1066
|
+
'',
|
|
1067
|
+
'The memory field captures what happened in this conversation — the gist of it.',
|
|
1068
|
+
'Importance scale: 1=routine, 5=interesting, 8=significant, 10=life-changing.',
|
|
1069
|
+
'Most chats are 1-3. Only notable ones deserve 5+.',
|
|
1070
|
+
'',
|
|
1071
|
+
'If NOTHING noteworthy happened (no self update AND no memory worth keeping): respond with exactly NONE',
|
|
1072
|
+
].join('\n');
|
|
1073
|
+
|
|
1074
|
+
const userPrompt = [
|
|
1075
|
+
'Current self-data:',
|
|
1076
|
+
'```',
|
|
1077
|
+
selfData,
|
|
1078
|
+
'```',
|
|
1079
|
+
'',
|
|
1080
|
+
`Conversation with ${userName}:`,
|
|
1081
|
+
`User: "${userMessage}"`,
|
|
1082
|
+
`You replied: "${reply}"`,
|
|
1083
|
+
'',
|
|
1084
|
+
'Return JSON with self_update and/or memory, or NONE.',
|
|
1085
|
+
].join('\n');
|
|
1086
|
+
|
|
1087
|
+
try {
|
|
1088
|
+
const response = await this.orchestratorProvider.chat({
|
|
1089
|
+
system,
|
|
1090
|
+
messages: [{ role: 'user', content: userPrompt }],
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1093
|
+
const text = (response.text || '').trim();
|
|
1094
|
+
|
|
1095
|
+
if (!text || text === 'NONE') return;
|
|
1096
|
+
|
|
1097
|
+
// Try to parse JSON from response (may be wrapped in markdown code block)
|
|
1098
|
+
let parsed;
|
|
1099
|
+
try {
|
|
1100
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
1101
|
+
if (jsonMatch) {
|
|
1102
|
+
parsed = JSON.parse(jsonMatch[0]);
|
|
1103
|
+
}
|
|
1104
|
+
} catch {
|
|
1105
|
+
logger.debug('Self-reflection returned non-JSON, skipping');
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Handle self_update (backward compat: also check top-level file/content)
|
|
1110
|
+
const selfUpdate = parsed?.self_update || (parsed?.file ? parsed : null);
|
|
1111
|
+
if (selfUpdate?.file && selfUpdate?.content) {
|
|
1112
|
+
const validFiles = ['goals', 'journey', 'life', 'hobbies'];
|
|
1113
|
+
if (validFiles.includes(selfUpdate.file)) {
|
|
1114
|
+
this.selfManager.save(selfUpdate.file, selfUpdate.content);
|
|
1115
|
+
logger.info(`Self-reflection updated: ${selfUpdate.file}`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Handle memory extraction
|
|
1120
|
+
if (parsed?.memory && this.memoryManager) {
|
|
1121
|
+
const mem = parsed.memory;
|
|
1122
|
+
if (mem.summary && mem.importance >= 2) {
|
|
1123
|
+
this.memoryManager.addEpisodic({
|
|
1124
|
+
type: mem.type || 'interaction',
|
|
1125
|
+
source: 'user_chat',
|
|
1126
|
+
summary: mem.summary,
|
|
1127
|
+
tags: mem.tags || [],
|
|
1128
|
+
importance: mem.importance || 3,
|
|
1129
|
+
userId: user?.id ? String(user.id) : null,
|
|
1130
|
+
});
|
|
1131
|
+
logger.info(`Memory extracted: "${mem.summary.slice(0, 80)}" (importance: ${mem.importance})`);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
} catch (err) {
|
|
1135
|
+
logger.debug(`Self-reflection skipped: ${err.message}`);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
553
1138
|
}
|
|
554
1139
|
|
|
555
1140
|
// Re-export as Agent for backward compatibility with bin/kernel.js import
|