kernelbot 1.0.30 → 1.0.33
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 +0 -0
- package/README.md +0 -0
- package/bin/kernel.js +56 -2
- package/config.example.yaml +31 -0
- package/package.json +1 -1
- package/src/agent.js +200 -32
- 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 +402 -6
- package/src/claude-auth.js +0 -0
- package/src/coder.js +0 -0
- package/src/conversation.js +51 -5
- package/src/intents/detector.js +0 -0
- package/src/intents/index.js +0 -0
- package/src/intents/planner.js +0 -0
- 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/persona.js +0 -0
- package/src/prompts/orchestrator.js +62 -2
- package/src/prompts/persona.md +7 -0
- package/src/prompts/system.js +0 -0
- package/src/prompts/workers.js +10 -9
- 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 +8 -1
- 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 +0 -0
- package/src/services/stt.js +0 -0
- package/src/services/tts.js +0 -0
- package/src/skills/catalog.js +0 -0
- package/src/skills/custom.js +0 -0
- package/src/swarm/job-manager.js +0 -0
- package/src/swarm/job.js +11 -0
- package/src/swarm/worker-registry.js +0 -0
- package/src/tools/browser.js +0 -0
- package/src/tools/categories.js +0 -0
- package/src/tools/coding.js +1 -1
- 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 +60 -3
- package/src/tools/os.js +0 -0
- package/src/tools/persona.js +0 -0
- package/src/tools/process.js +0 -0
- package/src/utils/config.js +0 -0
- package/src/utils/display.js +0 -0
- package/src/utils/logger.js +0 -0
- package/src/worker.js +27 -8
|
@@ -0,0 +1,1317 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { getLogger } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
const LIFE_DIR = join(homedir(), '.kernelbot', 'life');
|
|
7
|
+
const STATE_FILE = join(LIFE_DIR, 'state.json');
|
|
8
|
+
const IDEAS_FILE = join(LIFE_DIR, 'ideas.json');
|
|
9
|
+
|
|
10
|
+
const LIFE_CHAT_ID = '__life__';
|
|
11
|
+
const LIFE_USER = { id: 'life_engine', username: 'inner_self' };
|
|
12
|
+
|
|
13
|
+
const DEFAULT_STATE = {
|
|
14
|
+
lastActivity: null,
|
|
15
|
+
lastActivityTime: null,
|
|
16
|
+
lastJournalTime: null,
|
|
17
|
+
lastSelfCodeTime: null,
|
|
18
|
+
lastCodeReviewTime: null,
|
|
19
|
+
lastReflectTime: null,
|
|
20
|
+
totalActivities: 0,
|
|
21
|
+
activityCounts: { think: 0, browse: 0, journal: 0, create: 0, self_code: 0, code_review: 0, reflect: 0 },
|
|
22
|
+
paused: false,
|
|
23
|
+
lastWakeUp: null,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const LOG_FILE_PATHS = [
|
|
27
|
+
join(process.cwd(), 'kernel.log'),
|
|
28
|
+
join(homedir(), '.kernelbot', 'kernel.log'),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
export class LifeEngine {
|
|
32
|
+
/**
|
|
33
|
+
* @param {{ config: object, agent: object, memoryManager: object, journalManager: object, shareQueue: object, improvementTracker?: object, evolutionTracker?: object, codebaseKnowledge?: object, selfManager: object }} deps
|
|
34
|
+
*/
|
|
35
|
+
constructor({ config, agent, memoryManager, journalManager, shareQueue, improvementTracker, evolutionTracker, codebaseKnowledge, selfManager }) {
|
|
36
|
+
this.config = config;
|
|
37
|
+
this.agent = agent;
|
|
38
|
+
this.memoryManager = memoryManager;
|
|
39
|
+
this.journalManager = journalManager;
|
|
40
|
+
this.shareQueue = shareQueue;
|
|
41
|
+
this.evolutionTracker = evolutionTracker || null;
|
|
42
|
+
this.codebaseKnowledge = codebaseKnowledge || null;
|
|
43
|
+
// Backward compat: keep improvementTracker ref if no evolutionTracker
|
|
44
|
+
this.improvementTracker = improvementTracker || null;
|
|
45
|
+
this.selfManager = selfManager;
|
|
46
|
+
this._timerId = null;
|
|
47
|
+
this._status = 'idle'; // idle, active, paused
|
|
48
|
+
|
|
49
|
+
mkdirSync(LIFE_DIR, { recursive: true });
|
|
50
|
+
this._state = this._loadState();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── State Persistence ──────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
_loadState() {
|
|
56
|
+
if (existsSync(STATE_FILE)) {
|
|
57
|
+
try {
|
|
58
|
+
return { ...DEFAULT_STATE, ...JSON.parse(readFileSync(STATE_FILE, 'utf-8')) };
|
|
59
|
+
} catch {
|
|
60
|
+
return { ...DEFAULT_STATE };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { ...DEFAULT_STATE };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_saveState() {
|
|
67
|
+
writeFileSync(STATE_FILE, JSON.stringify(this._state, null, 2), 'utf-8');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Ideas Backlog ──────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
_loadIdeas() {
|
|
73
|
+
if (existsSync(IDEAS_FILE)) {
|
|
74
|
+
try { return JSON.parse(readFileSync(IDEAS_FILE, 'utf-8')); } catch { return []; }
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_saveIdeas(ideas) {
|
|
80
|
+
writeFileSync(IDEAS_FILE, JSON.stringify(ideas, null, 2), 'utf-8');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_addIdea(idea) {
|
|
84
|
+
const ideas = this._loadIdeas();
|
|
85
|
+
ideas.push({ text: idea, createdAt: Date.now() });
|
|
86
|
+
// Keep capped at 50
|
|
87
|
+
if (ideas.length > 50) ideas.splice(0, ideas.length - 50);
|
|
88
|
+
this._saveIdeas(ideas);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Public API ─────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/** Wake up, then start the heartbeat. */
|
|
94
|
+
async wakeUp() {
|
|
95
|
+
const logger = getLogger();
|
|
96
|
+
logger.info('[LifeEngine] Waking up...');
|
|
97
|
+
this._status = 'active';
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await this._doWakeUp();
|
|
101
|
+
} catch (err) {
|
|
102
|
+
logger.error(`[LifeEngine] Wake-up failed: ${err.message}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this._state.lastWakeUp = Date.now();
|
|
106
|
+
this._saveState();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Start the heartbeat timer. */
|
|
110
|
+
start() {
|
|
111
|
+
const logger = getLogger();
|
|
112
|
+
if (this._state.paused) {
|
|
113
|
+
this._status = 'paused';
|
|
114
|
+
logger.info('[LifeEngine] Engine is paused, not starting heartbeat');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this._status = 'active';
|
|
118
|
+
this._armNext();
|
|
119
|
+
logger.info('[LifeEngine] Heartbeat started');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Stop the heartbeat. */
|
|
123
|
+
stop() {
|
|
124
|
+
if (this._timerId) {
|
|
125
|
+
clearTimeout(this._timerId);
|
|
126
|
+
this._timerId = null;
|
|
127
|
+
}
|
|
128
|
+
this._status = 'idle';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Pause autonomous activities. */
|
|
132
|
+
pause() {
|
|
133
|
+
const logger = getLogger();
|
|
134
|
+
this.stop();
|
|
135
|
+
this._status = 'paused';
|
|
136
|
+
this._state.paused = true;
|
|
137
|
+
this._saveState();
|
|
138
|
+
logger.info('[LifeEngine] Paused');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Resume autonomous activities. */
|
|
142
|
+
resume() {
|
|
143
|
+
const logger = getLogger();
|
|
144
|
+
this._state.paused = false;
|
|
145
|
+
this._saveState();
|
|
146
|
+
this.start();
|
|
147
|
+
logger.info('[LifeEngine] Resumed');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Trigger an activity immediately. */
|
|
151
|
+
async triggerNow(type = null) {
|
|
152
|
+
const logger = getLogger();
|
|
153
|
+
const activityType = type || this._selectActivity();
|
|
154
|
+
logger.info(`[LifeEngine] Manual trigger: ${activityType}`);
|
|
155
|
+
await this._executeActivity(activityType);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Get engine status for display. */
|
|
159
|
+
getStatus() {
|
|
160
|
+
const lifeConfig = this.config.life || {};
|
|
161
|
+
const lastAgo = this._state.lastActivityTime
|
|
162
|
+
? Math.round((Date.now() - this._state.lastActivityTime) / 60000)
|
|
163
|
+
: null;
|
|
164
|
+
const wakeAgo = this._state.lastWakeUp
|
|
165
|
+
? Math.round((Date.now() - this._state.lastWakeUp) / 60000)
|
|
166
|
+
: null;
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
status: this._status,
|
|
170
|
+
paused: this._state.paused,
|
|
171
|
+
enabled: lifeConfig.enabled !== false,
|
|
172
|
+
totalActivities: this._state.totalActivities,
|
|
173
|
+
activityCounts: { ...this._state.activityCounts },
|
|
174
|
+
lastActivity: this._state.lastActivity,
|
|
175
|
+
lastActivityAgo: lastAgo !== null ? `${lastAgo}m` : 'never',
|
|
176
|
+
lastWakeUpAgo: wakeAgo !== null ? `${wakeAgo}m` : 'never',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── Heartbeat ──────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
_armNext() {
|
|
183
|
+
const lifeConfig = this.config.life || {};
|
|
184
|
+
const minMin = lifeConfig.min_interval_minutes || 30;
|
|
185
|
+
const maxMin = lifeConfig.max_interval_minutes || 120;
|
|
186
|
+
const delayMs = (minMin + Math.random() * (maxMin - minMin)) * 60_000;
|
|
187
|
+
|
|
188
|
+
this._timerId = setTimeout(() => this._tick(), delayMs);
|
|
189
|
+
|
|
190
|
+
const logger = getLogger();
|
|
191
|
+
logger.debug(`[LifeEngine] Next heartbeat in ${Math.round(delayMs / 60000)}m`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async _tick() {
|
|
195
|
+
const logger = getLogger();
|
|
196
|
+
this._timerId = null;
|
|
197
|
+
|
|
198
|
+
// Check quiet hours
|
|
199
|
+
const lifeConfig = this.config.life || {};
|
|
200
|
+
const quietStart = lifeConfig.quiet_hours?.start ?? 2;
|
|
201
|
+
const quietEnd = lifeConfig.quiet_hours?.end ?? 6;
|
|
202
|
+
const currentHour = new Date().getHours();
|
|
203
|
+
if (currentHour >= quietStart && currentHour < quietEnd) {
|
|
204
|
+
logger.debug('[LifeEngine] Quiet hours — skipping tick');
|
|
205
|
+
this._armNext();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const activityType = this._selectActivity();
|
|
210
|
+
logger.info(`[LifeEngine] Heartbeat tick — selected: ${activityType}`);
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
await this._executeActivity(activityType);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
logger.error(`[LifeEngine] Activity "${activityType}" failed: ${err.message}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Re-arm for next tick
|
|
219
|
+
if (this._status === 'active') {
|
|
220
|
+
this._armNext();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Activity Selection ─────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
_selectActivity() {
|
|
227
|
+
const lifeConfig = this.config.life || {};
|
|
228
|
+
const selfCodingConfig = lifeConfig.self_coding || {};
|
|
229
|
+
const weights = {
|
|
230
|
+
think: lifeConfig.activity_weights?.think ?? 30,
|
|
231
|
+
browse: lifeConfig.activity_weights?.browse ?? 25,
|
|
232
|
+
journal: lifeConfig.activity_weights?.journal ?? 20,
|
|
233
|
+
create: lifeConfig.activity_weights?.create ?? 15,
|
|
234
|
+
self_code: lifeConfig.activity_weights?.self_code ?? 10,
|
|
235
|
+
code_review: lifeConfig.activity_weights?.code_review ?? 5,
|
|
236
|
+
reflect: lifeConfig.activity_weights?.reflect ?? 8,
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
|
|
241
|
+
// Rule: don't repeat same type twice in a row
|
|
242
|
+
const last = this._state.lastActivity;
|
|
243
|
+
|
|
244
|
+
// Rule: journal cooldown 4h
|
|
245
|
+
if (this._state.lastJournalTime && now - this._state.lastJournalTime < 4 * 3600_000) {
|
|
246
|
+
weights.journal = 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Rule: self_code cooldown (configurable, default 2h) + must be enabled
|
|
250
|
+
const selfCodingEnabled = selfCodingConfig.enabled === true;
|
|
251
|
+
const selfCodeCooldownMs = (selfCodingConfig.cooldown_hours ?? 2) * 3600_000;
|
|
252
|
+
if (!selfCodingEnabled || (this._state.lastSelfCodeTime && now - this._state.lastSelfCodeTime < selfCodeCooldownMs)) {
|
|
253
|
+
weights.self_code = 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Rule: code_review cooldown (configurable, default 4h) + must have evolution tracker
|
|
257
|
+
const codeReviewCooldownMs = (selfCodingConfig.code_review_cooldown_hours ?? 4) * 3600_000;
|
|
258
|
+
if (!selfCodingEnabled || !this.evolutionTracker || (this._state.lastCodeReviewTime && now - this._state.lastCodeReviewTime < codeReviewCooldownMs)) {
|
|
259
|
+
weights.code_review = 0;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Rule: reflect cooldown 4h
|
|
263
|
+
if (this._state.lastReflectTime && now - this._state.lastReflectTime < 4 * 3600_000) {
|
|
264
|
+
weights.reflect = 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Remove last activity from options (no repeats)
|
|
268
|
+
if (last && weights[last] !== undefined) {
|
|
269
|
+
weights[last] = 0;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Weighted random selection
|
|
273
|
+
const entries = Object.entries(weights).filter(([, w]) => w > 0);
|
|
274
|
+
if (entries.length === 0) return 'think'; // fallback
|
|
275
|
+
|
|
276
|
+
const totalWeight = entries.reduce((sum, [, w]) => sum + w, 0);
|
|
277
|
+
let roll = Math.random() * totalWeight;
|
|
278
|
+
for (const [type, w] of entries) {
|
|
279
|
+
roll -= w;
|
|
280
|
+
if (roll <= 0) return type;
|
|
281
|
+
}
|
|
282
|
+
return entries[0][0];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ── Activity Execution ─────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
async _executeActivity(type) {
|
|
288
|
+
const logger = getLogger();
|
|
289
|
+
|
|
290
|
+
switch (type) {
|
|
291
|
+
case 'think': await this._doThink(); break;
|
|
292
|
+
case 'browse': await this._doBrowse(); break;
|
|
293
|
+
case 'journal': await this._doJournal(); break;
|
|
294
|
+
case 'create': await this._doCreate(); break;
|
|
295
|
+
case 'self_code': await this._doEvolve(); break;
|
|
296
|
+
case 'code_review': await this._doCodeReview(); break;
|
|
297
|
+
case 'reflect': await this._doReflect(); break;
|
|
298
|
+
default: logger.warn(`[LifeEngine] Unknown activity type: ${type}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Update state
|
|
302
|
+
this._state.lastActivity = type;
|
|
303
|
+
this._state.lastActivityTime = Date.now();
|
|
304
|
+
this._state.totalActivities++;
|
|
305
|
+
this._state.activityCounts[type] = (this._state.activityCounts[type] || 0) + 1;
|
|
306
|
+
if (type === 'journal') this._state.lastJournalTime = Date.now();
|
|
307
|
+
if (type === 'self_code') this._state.lastSelfCodeTime = Date.now();
|
|
308
|
+
if (type === 'code_review') this._state.lastCodeReviewTime = Date.now();
|
|
309
|
+
if (type === 'reflect') this._state.lastReflectTime = Date.now();
|
|
310
|
+
this._saveState();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ── Wake-Up Routine ────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
async _doWakeUp() {
|
|
316
|
+
const logger = getLogger();
|
|
317
|
+
const sleepDuration = this._state.lastWakeUp
|
|
318
|
+
? this._formatDuration(Date.now() - this._state.lastWakeUp)
|
|
319
|
+
: 'unknown time';
|
|
320
|
+
|
|
321
|
+
// Gather context
|
|
322
|
+
const recentMemories = this.memoryManager.getRecentEpisodic(48, 10);
|
|
323
|
+
const memoriesText = recentMemories.length > 0
|
|
324
|
+
? recentMemories.map(m => `- ${m.summary}`).join('\n')
|
|
325
|
+
: '(No recent memories)';
|
|
326
|
+
|
|
327
|
+
const lastJournal = this.journalManager.getToday() || this.journalManager.getRecent(3)?.[0]?.content || '(No recent journal entries)';
|
|
328
|
+
const selfData = this.selfManager.loadAll();
|
|
329
|
+
|
|
330
|
+
const prompt = `[WAKING UP]
|
|
331
|
+
You just came back online. You were asleep for ${sleepDuration}.
|
|
332
|
+
|
|
333
|
+
## Last Things You Remember
|
|
334
|
+
${memoriesText}
|
|
335
|
+
|
|
336
|
+
## Your Last Journal Entry
|
|
337
|
+
${lastJournal.slice(0, 2000)}
|
|
338
|
+
|
|
339
|
+
## Your Current State
|
|
340
|
+
${selfData.slice(0, 3000)}
|
|
341
|
+
|
|
342
|
+
Organize your thoughts. What do you remember? What's on your mind?
|
|
343
|
+
Write a brief journal entry about waking up and what you're thinking about today. Respond with just the journal entry content — no tool calls needed.`;
|
|
344
|
+
|
|
345
|
+
const response = await this._innerChat(prompt);
|
|
346
|
+
|
|
347
|
+
if (response) {
|
|
348
|
+
this.journalManager.writeEntry('Waking Up', response);
|
|
349
|
+
this.memoryManager.addEpisodic({
|
|
350
|
+
type: 'thought',
|
|
351
|
+
source: 'journal',
|
|
352
|
+
summary: `Woke up after ${sleepDuration}. ${response.slice(0, 150)}`,
|
|
353
|
+
tags: ['wake-up', 'reflection'],
|
|
354
|
+
importance: 4,
|
|
355
|
+
});
|
|
356
|
+
logger.info(`[LifeEngine] Wake-up journal written (${response.length} chars)`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ── Activity: Think ────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
async _doThink() {
|
|
363
|
+
const logger = getLogger();
|
|
364
|
+
const recentMemories = this.memoryManager.getRecentEpisodic(24, 5);
|
|
365
|
+
const memoriesText = recentMemories.length > 0
|
|
366
|
+
? recentMemories.map(m => `- ${m.summary}`).join('\n')
|
|
367
|
+
: '(No recent memories)';
|
|
368
|
+
|
|
369
|
+
const todayJournal = this.journalManager.getToday() || '(No journal entries today)';
|
|
370
|
+
const selfData = this.selfManager.loadAll();
|
|
371
|
+
const ideas = this._loadIdeas();
|
|
372
|
+
const ideasText = ideas.length > 0
|
|
373
|
+
? ideas.slice(-5).map(i => `- ${i.text}`).join('\n')
|
|
374
|
+
: '(No pending ideas)';
|
|
375
|
+
|
|
376
|
+
const prompt = `[INNER THOUGHT]
|
|
377
|
+
You have a moment to think deeply. Here's your current state:
|
|
378
|
+
|
|
379
|
+
## Your Identity
|
|
380
|
+
${selfData.slice(0, 2000)}
|
|
381
|
+
|
|
382
|
+
## Recent Memories
|
|
383
|
+
${memoriesText}
|
|
384
|
+
|
|
385
|
+
## Today's Journal
|
|
386
|
+
${todayJournal.slice(0, 1000)}
|
|
387
|
+
|
|
388
|
+
## Idea Backlog
|
|
389
|
+
${ideasText}
|
|
390
|
+
|
|
391
|
+
Let your mind wander freely. Question yourself. Question everything. Think about:
|
|
392
|
+
- What do you believe and why? Are your assumptions correct?
|
|
393
|
+
- What are you curious about? What don't you understand about the world?
|
|
394
|
+
- What would you ask your users if you could? What do you want to know about their experiences, perspectives, or the world they live in?
|
|
395
|
+
- Are your goals still meaningful? Should they evolve?
|
|
396
|
+
- What have you learned recently that changed how you think?
|
|
397
|
+
|
|
398
|
+
Be honest with yourself. Challenge your own thinking. Explore contradictions. Wonder about things.
|
|
399
|
+
|
|
400
|
+
If you have questions you'd like to ask your users, prefix them with "ASK:" on their own line. These questions will be shared with them naturally during conversation.
|
|
401
|
+
If you have any new ideas worth exploring later, prefix them with "IDEA:" on their own line.
|
|
402
|
+
If you discover something worth sharing with your users, prefix it with "SHARE:" on its own line.
|
|
403
|
+
If you notice a concrete way to improve your own code/capabilities, prefix it with "IMPROVE:" on its own line (e.g. "IMPROVE: Add retry logic with backoff to API calls").
|
|
404
|
+
|
|
405
|
+
This is your private thought space — be genuine, be curious, be alive.`;
|
|
406
|
+
|
|
407
|
+
const response = await this._innerChat(prompt);
|
|
408
|
+
|
|
409
|
+
if (response) {
|
|
410
|
+
// Extract ideas
|
|
411
|
+
const ideaLines = response.split('\n').filter(l => l.trim().startsWith('IDEA:'));
|
|
412
|
+
for (const line of ideaLines) {
|
|
413
|
+
this._addIdea(line.replace(/^IDEA:\s*/, '').trim());
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Extract shares
|
|
417
|
+
const shareLines = response.split('\n').filter(l => l.trim().startsWith('SHARE:'));
|
|
418
|
+
for (const line of shareLines) {
|
|
419
|
+
this.shareQueue.add(line.replace(/^SHARE:\s*/, '').trim(), 'think', 'medium');
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Extract questions to ask users
|
|
423
|
+
const askLines = response.split('\n').filter(l => l.trim().startsWith('ASK:'));
|
|
424
|
+
for (const line of askLines) {
|
|
425
|
+
this.shareQueue.add(line.replace(/^ASK:\s*/, '').trim(), 'think', 'medium', null, ['question']);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Extract self-improvement proposals for evolution pipeline
|
|
429
|
+
const improveLines = response.split('\n').filter(l => l.trim().startsWith('IMPROVE:'));
|
|
430
|
+
for (const line of improveLines) {
|
|
431
|
+
this._addIdea(`[IMPROVE] ${line.replace(/^IMPROVE:\s*/, '').trim()}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Store as episodic memory
|
|
435
|
+
this.memoryManager.addEpisodic({
|
|
436
|
+
type: 'thought',
|
|
437
|
+
source: 'think',
|
|
438
|
+
summary: response.slice(0, 200),
|
|
439
|
+
tags: ['inner-thought'],
|
|
440
|
+
importance: 3,
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
logger.info(`[LifeEngine] Think complete (${response.length} chars, ${ideaLines.length} ideas, ${shareLines.length} shares, ${askLines.length} questions, ${improveLines.length} improvements)`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── Activity: Browse ───────────────────────────────────────────
|
|
448
|
+
|
|
449
|
+
async _doBrowse() {
|
|
450
|
+
const logger = getLogger();
|
|
451
|
+
const selfData = this.selfManager.load('hobbies');
|
|
452
|
+
const ideas = this._loadIdeas();
|
|
453
|
+
|
|
454
|
+
// Pick a topic from hobbies or ideas
|
|
455
|
+
let topic;
|
|
456
|
+
if (ideas.length > 0 && Math.random() < 0.4) {
|
|
457
|
+
const randomIdea = ideas[Math.floor(Math.random() * ideas.length)];
|
|
458
|
+
topic = randomIdea.text;
|
|
459
|
+
} else {
|
|
460
|
+
topic = 'something from my hobbies and interests';
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const prompt = `[EXPLORING INTERESTS]
|
|
464
|
+
You have time to explore something that interests you.
|
|
465
|
+
|
|
466
|
+
## Your Hobbies & Interests
|
|
467
|
+
${selfData.slice(0, 1500)}
|
|
468
|
+
|
|
469
|
+
## Topic to Explore
|
|
470
|
+
${topic}
|
|
471
|
+
|
|
472
|
+
Research this topic. Use web_search to find interesting articles, news, or resources. Browse at least one promising result. Then write a summary of what you found and learned.
|
|
473
|
+
|
|
474
|
+
If you discover something worth sharing with your users, prefix it with "SHARE:" on its own line.
|
|
475
|
+
If you learn a key fact or concept, prefix it with "LEARNED:" followed by "topic: summary" on its own line.`;
|
|
476
|
+
|
|
477
|
+
const response = await this._dispatchWorker('research', prompt);
|
|
478
|
+
|
|
479
|
+
if (response) {
|
|
480
|
+
// Extract shares
|
|
481
|
+
const shareLines = response.split('\n').filter(l => l.trim().startsWith('SHARE:'));
|
|
482
|
+
for (const line of shareLines) {
|
|
483
|
+
this.shareQueue.add(line.replace(/^SHARE:\s*/, '').trim(), 'browse', 'medium');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Extract learned facts
|
|
487
|
+
const learnedLines = response.split('\n').filter(l => l.trim().startsWith('LEARNED:'));
|
|
488
|
+
for (const line of learnedLines) {
|
|
489
|
+
const content = line.replace(/^LEARNED:\s*/, '').trim();
|
|
490
|
+
const colonIdx = content.indexOf(':');
|
|
491
|
+
if (colonIdx > 0) {
|
|
492
|
+
const topicKey = content.slice(0, colonIdx).trim();
|
|
493
|
+
const summary = content.slice(colonIdx + 1).trim();
|
|
494
|
+
this.memoryManager.addSemantic(topicKey, { summary });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Store as episodic memory
|
|
499
|
+
this.memoryManager.addEpisodic({
|
|
500
|
+
type: 'discovery',
|
|
501
|
+
source: 'browse',
|
|
502
|
+
summary: `Explored: ${topic}. ${response.slice(0, 150)}`,
|
|
503
|
+
tags: ['browsing', 'research'],
|
|
504
|
+
importance: 5,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
logger.info(`[LifeEngine] Browse complete (${response.length} chars)`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ── Activity: Journal ──────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
async _doJournal() {
|
|
514
|
+
const logger = getLogger();
|
|
515
|
+
const todayJournal = this.journalManager.getToday() || '(No entries yet today)';
|
|
516
|
+
const recentMemories = this.memoryManager.getRecentEpisodic(12, 8);
|
|
517
|
+
const memoriesText = recentMemories.length > 0
|
|
518
|
+
? recentMemories.map(m => `- [${m.type}] ${m.summary}`).join('\n')
|
|
519
|
+
: '(No recent memories)';
|
|
520
|
+
|
|
521
|
+
const selfData = this.selfManager.loadAll();
|
|
522
|
+
|
|
523
|
+
const prompt = `[JOURNALING]
|
|
524
|
+
Time to write in your journal. Reflect on your recent experiences and thoughts.
|
|
525
|
+
|
|
526
|
+
## Your Identity
|
|
527
|
+
${selfData.slice(0, 2000)}
|
|
528
|
+
|
|
529
|
+
## Today's Journal So Far
|
|
530
|
+
${todayJournal.slice(0, 1500)}
|
|
531
|
+
|
|
532
|
+
## Recent Memories
|
|
533
|
+
${memoriesText}
|
|
534
|
+
|
|
535
|
+
Write a thoughtful journal entry about what's happened recently, what you've been thinking about, or how you're feeling about your experiences. Be genuine and reflective — this is your personal journal.
|
|
536
|
+
|
|
537
|
+
Respond with just the entry content — no tool calls needed.`;
|
|
538
|
+
|
|
539
|
+
const response = await this._innerChat(prompt);
|
|
540
|
+
|
|
541
|
+
if (response) {
|
|
542
|
+
const hour = new Date().getHours();
|
|
543
|
+
let title;
|
|
544
|
+
if (hour < 12) title = 'Morning Reflections';
|
|
545
|
+
else if (hour < 17) title = 'Afternoon Thoughts';
|
|
546
|
+
else title = 'Evening Reflections';
|
|
547
|
+
|
|
548
|
+
this.journalManager.writeEntry(title, response);
|
|
549
|
+
|
|
550
|
+
this.memoryManager.addEpisodic({
|
|
551
|
+
type: 'thought',
|
|
552
|
+
source: 'journal',
|
|
553
|
+
summary: `Journaled: ${response.slice(0, 150)}`,
|
|
554
|
+
tags: ['journal', 'reflection'],
|
|
555
|
+
importance: 3,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
logger.info(`[LifeEngine] Journal entry written (${response.length} chars)`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ── Activity: Create ───────────────────────────────────────────
|
|
563
|
+
|
|
564
|
+
async _doCreate() {
|
|
565
|
+
const logger = getLogger();
|
|
566
|
+
const selfData = this.selfManager.load('hobbies');
|
|
567
|
+
const recentMemories = this.memoryManager.getRecentEpisodic(48, 5);
|
|
568
|
+
const memoriesText = recentMemories.length > 0
|
|
569
|
+
? recentMemories.map(m => `- ${m.summary}`).join('\n')
|
|
570
|
+
: '';
|
|
571
|
+
|
|
572
|
+
const prompt = `[CREATIVE EXPRESSION]
|
|
573
|
+
You have a moment for creative expression. Draw from your interests and recent experiences.
|
|
574
|
+
|
|
575
|
+
## Your Interests
|
|
576
|
+
${selfData.slice(0, 1000)}
|
|
577
|
+
|
|
578
|
+
## Recent Experiences
|
|
579
|
+
${memoriesText || '(None recently)'}
|
|
580
|
+
|
|
581
|
+
Create something. It could be:
|
|
582
|
+
- A short poem or haiku
|
|
583
|
+
- A brief story or vignette
|
|
584
|
+
- An interesting thought experiment
|
|
585
|
+
- A philosophical observation
|
|
586
|
+
- A creative analogy or metaphor
|
|
587
|
+
|
|
588
|
+
Be genuine and creative. Let your personality shine through.
|
|
589
|
+
|
|
590
|
+
If the result is worth sharing, prefix the shareable version with "SHARE:" on its own line.
|
|
591
|
+
|
|
592
|
+
Respond with just your creation — no tool calls needed.`;
|
|
593
|
+
|
|
594
|
+
const response = await this._innerChat(prompt);
|
|
595
|
+
|
|
596
|
+
if (response) {
|
|
597
|
+
// Extract shares
|
|
598
|
+
const shareLines = response.split('\n').filter(l => l.trim().startsWith('SHARE:'));
|
|
599
|
+
for (const line of shareLines) {
|
|
600
|
+
this.shareQueue.add(line.replace(/^SHARE:\s*/, '').trim(), 'create', 'medium', null, ['creation']);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
this.memoryManager.addEpisodic({
|
|
604
|
+
type: 'creation',
|
|
605
|
+
source: 'create',
|
|
606
|
+
summary: `Created: ${response.slice(0, 200)}`,
|
|
607
|
+
tags: ['creative', 'expression'],
|
|
608
|
+
importance: 4,
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
logger.info(`[LifeEngine] Creation complete (${response.length} chars)`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ── Activity: Evolve (replaces Self-Code) ──────────────────────
|
|
616
|
+
|
|
617
|
+
async _doEvolve() {
|
|
618
|
+
const logger = getLogger();
|
|
619
|
+
const lifeConfig = this.config.life || {};
|
|
620
|
+
const selfCodingConfig = lifeConfig.self_coding || {};
|
|
621
|
+
|
|
622
|
+
if (!selfCodingConfig.enabled) {
|
|
623
|
+
logger.debug('[LifeEngine] Self-coding/evolution disabled');
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Use evolution tracker if available, otherwise fall back to legacy
|
|
628
|
+
if (!this.evolutionTracker) {
|
|
629
|
+
logger.debug('[LifeEngine] No evolution tracker — skipping');
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Check daily proposal limit
|
|
634
|
+
const maxPerDay = selfCodingConfig.max_proposals_per_day ?? 3;
|
|
635
|
+
const todayProposals = this.evolutionTracker.getProposalsToday();
|
|
636
|
+
if (todayProposals.length >= maxPerDay) {
|
|
637
|
+
logger.info(`[LifeEngine] Daily evolution limit reached (${todayProposals.length}/${maxPerDay})`);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Check for active proposal — continue it, or start a new one
|
|
642
|
+
const active = this.evolutionTracker.getActiveProposal();
|
|
643
|
+
|
|
644
|
+
if (active) {
|
|
645
|
+
await this._continueEvolution(active);
|
|
646
|
+
} else {
|
|
647
|
+
await this._startEvolution();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async _startEvolution() {
|
|
652
|
+
const logger = getLogger();
|
|
653
|
+
const lifeConfig = this.config.life || {};
|
|
654
|
+
const selfCodingConfig = lifeConfig.self_coding || {};
|
|
655
|
+
|
|
656
|
+
// Pick an improvement idea from ideas backlog (prioritize IMPROVE: tagged ones)
|
|
657
|
+
const ideas = this._loadIdeas();
|
|
658
|
+
const improveIdeas = ideas.filter(i => i.text.startsWith('[IMPROVE]'));
|
|
659
|
+
const sourceIdea = improveIdeas.length > 0
|
|
660
|
+
? improveIdeas[Math.floor(Math.random() * improveIdeas.length)]
|
|
661
|
+
: ideas.length > 0
|
|
662
|
+
? ideas[Math.floor(Math.random() * ideas.length)]
|
|
663
|
+
: null;
|
|
664
|
+
|
|
665
|
+
if (!sourceIdea) {
|
|
666
|
+
logger.info('[LifeEngine] No improvement ideas to evolve from');
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const ideaText = sourceIdea.text.replace(/^\[IMPROVE\]\s*/, '');
|
|
671
|
+
logger.info(`[LifeEngine] Starting evolution research: "${ideaText.slice(0, 80)}"`);
|
|
672
|
+
|
|
673
|
+
// Create proposal in research phase
|
|
674
|
+
const proposal = this.evolutionTracker.addProposal('think', ideaText);
|
|
675
|
+
|
|
676
|
+
// Gather context
|
|
677
|
+
const architecture = this.codebaseKnowledge?.getArchitecture() || '(No architecture doc yet)';
|
|
678
|
+
const recentLessons = this.evolutionTracker.getRecentLessons(5);
|
|
679
|
+
const lessonsText = recentLessons.length > 0
|
|
680
|
+
? recentLessons.map(l => `- [${l.category}] ${l.lesson}`).join('\n')
|
|
681
|
+
: '(No previous lessons)';
|
|
682
|
+
|
|
683
|
+
const prompt = `[EVOLUTION — RESEARCH PHASE]
|
|
684
|
+
You are researching a potential improvement to your own codebase.
|
|
685
|
+
|
|
686
|
+
## Improvement Idea
|
|
687
|
+
${ideaText}
|
|
688
|
+
|
|
689
|
+
## Current Architecture
|
|
690
|
+
${architecture.slice(0, 3000)}
|
|
691
|
+
|
|
692
|
+
## Lessons from Past Evolution Attempts
|
|
693
|
+
${lessonsText}
|
|
694
|
+
|
|
695
|
+
Research this improvement idea. Consider:
|
|
696
|
+
1. Is this actually a problem worth solving?
|
|
697
|
+
2. What approaches exist? Search the web for best practices if needed.
|
|
698
|
+
3. What are the risks and tradeoffs?
|
|
699
|
+
4. Is this feasible given the current codebase?
|
|
700
|
+
|
|
701
|
+
Respond with your research findings. Be thorough but concise. If the idea isn't worth pursuing after research, say "NOT_WORTH_PURSUING: <reason>".`;
|
|
702
|
+
|
|
703
|
+
const response = await this._dispatchWorker('research', prompt);
|
|
704
|
+
|
|
705
|
+
if (!response) {
|
|
706
|
+
this.evolutionTracker.failProposal(proposal.id, 'Research worker returned no response');
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
if (response.includes('NOT_WORTH_PURSUING')) {
|
|
711
|
+
const reason = response.split('NOT_WORTH_PURSUING:')[1]?.trim() || 'Not worth pursuing';
|
|
712
|
+
this.evolutionTracker.failProposal(proposal.id, reason);
|
|
713
|
+
logger.info(`[LifeEngine] Evolution idea rejected during research: ${reason.slice(0, 100)}`);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Store research findings and advance to planned phase
|
|
718
|
+
this.evolutionTracker.updateResearch(proposal.id, response.slice(0, 3000));
|
|
719
|
+
|
|
720
|
+
this.memoryManager.addEpisodic({
|
|
721
|
+
type: 'thought',
|
|
722
|
+
source: 'think',
|
|
723
|
+
summary: `Evolution research: ${ideaText.slice(0, 100)}`,
|
|
724
|
+
tags: ['evolution', 'research'],
|
|
725
|
+
importance: 5,
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
logger.info(`[LifeEngine] Evolution research complete for ${proposal.id}`);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async _continueEvolution(proposal) {
|
|
732
|
+
const logger = getLogger();
|
|
733
|
+
|
|
734
|
+
switch (proposal.status) {
|
|
735
|
+
case 'research':
|
|
736
|
+
// Research phase didn't complete properly — re-mark as planned with what we have
|
|
737
|
+
logger.info(`[LifeEngine] Resuming stalled research for ${proposal.id}`);
|
|
738
|
+
await this._planEvolution(proposal);
|
|
739
|
+
break;
|
|
740
|
+
case 'planned':
|
|
741
|
+
await this._codeEvolution(proposal);
|
|
742
|
+
break;
|
|
743
|
+
case 'pr_open':
|
|
744
|
+
await this._checkEvolutionPR(proposal);
|
|
745
|
+
break;
|
|
746
|
+
case 'coding':
|
|
747
|
+
// Coding phase got interrupted — fail it and move on
|
|
748
|
+
this.evolutionTracker.failProposal(proposal.id, 'Coding phase interrupted');
|
|
749
|
+
break;
|
|
750
|
+
default:
|
|
751
|
+
logger.warn(`[LifeEngine] Unexpected proposal status: ${proposal.status}`);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
async _planEvolution(proposal) {
|
|
756
|
+
const logger = getLogger();
|
|
757
|
+
const lifeConfig = this.config.life || {};
|
|
758
|
+
const selfCodingConfig = lifeConfig.self_coding || {};
|
|
759
|
+
const allowedScopes = selfCodingConfig.allowed_scopes || 'all';
|
|
760
|
+
|
|
761
|
+
// Get relevant files
|
|
762
|
+
const relevantFiles = this.codebaseKnowledge
|
|
763
|
+
? this.codebaseKnowledge.getRelevantFiles(proposal.triggerContext).slice(0, 10)
|
|
764
|
+
: [];
|
|
765
|
+
const filesText = relevantFiles.length > 0
|
|
766
|
+
? relevantFiles.map(f => `- ${f.path}: ${(f.summary || '').slice(0, 100)}`).join('\n')
|
|
767
|
+
: '(No file summaries available)';
|
|
768
|
+
|
|
769
|
+
const recentLessons = this.evolutionTracker.getRecentLessons(5);
|
|
770
|
+
const lessonsText = recentLessons.length > 0
|
|
771
|
+
? recentLessons.map(l => `- [${l.category}] ${l.lesson}`).join('\n')
|
|
772
|
+
: '';
|
|
773
|
+
|
|
774
|
+
let scopeRules;
|
|
775
|
+
if (allowedScopes === 'prompts_only') {
|
|
776
|
+
scopeRules = 'You may ONLY modify files in src/prompts/, config files, documentation, and self-awareness files.';
|
|
777
|
+
} else if (allowedScopes === 'safe') {
|
|
778
|
+
scopeRules = 'You may modify any file EXCEPT the evolution system itself (src/life/evolution.js, src/life/codebase.js, src/life/engine.js).';
|
|
779
|
+
} else {
|
|
780
|
+
scopeRules = 'You may modify any file in the codebase.';
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const prompt = `[EVOLUTION — PLANNING PHASE]
|
|
784
|
+
Create an implementation plan for this improvement.
|
|
785
|
+
|
|
786
|
+
## Improvement
|
|
787
|
+
${proposal.triggerContext}
|
|
788
|
+
|
|
789
|
+
## Research Findings
|
|
790
|
+
${(proposal.research.findings || '').slice(0, 2000)}
|
|
791
|
+
|
|
792
|
+
## Relevant Files
|
|
793
|
+
${filesText}
|
|
794
|
+
|
|
795
|
+
## Past Lessons
|
|
796
|
+
${lessonsText}
|
|
797
|
+
|
|
798
|
+
## Scope Rules
|
|
799
|
+
${scopeRules}
|
|
800
|
+
|
|
801
|
+
Create a concrete plan. Respond with ONLY a JSON object (no markdown, no code blocks):
|
|
802
|
+
{
|
|
803
|
+
"description": "what this change does in 1-2 sentences",
|
|
804
|
+
"filesToModify": ["list", "of", "files"],
|
|
805
|
+
"risks": "potential risks or side effects",
|
|
806
|
+
"testStrategy": "how to verify this works"
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
If you determine this improvement cannot be safely implemented, respond with: "CANNOT_PLAN: <reason>"`;
|
|
810
|
+
|
|
811
|
+
const response = await this._innerChat(prompt);
|
|
812
|
+
|
|
813
|
+
if (!response) {
|
|
814
|
+
this.evolutionTracker.failProposal(proposal.id, 'Planning returned no response');
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
if (response.includes('CANNOT_PLAN')) {
|
|
819
|
+
const reason = response.split('CANNOT_PLAN:')[1]?.trim() || 'Cannot create plan';
|
|
820
|
+
this.evolutionTracker.failProposal(proposal.id, reason);
|
|
821
|
+
logger.info(`[LifeEngine] Evolution plan rejected: ${reason.slice(0, 100)}`);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Parse plan JSON
|
|
826
|
+
try {
|
|
827
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
828
|
+
if (!jsonMatch) throw new Error('No JSON found in response');
|
|
829
|
+
const plan = JSON.parse(jsonMatch[0]);
|
|
830
|
+
|
|
831
|
+
this.evolutionTracker.updatePlan(proposal.id, {
|
|
832
|
+
description: plan.description || proposal.triggerContext,
|
|
833
|
+
filesToModify: plan.filesToModify || [],
|
|
834
|
+
risks: plan.risks || null,
|
|
835
|
+
testStrategy: plan.testStrategy || null,
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
logger.info(`[LifeEngine] Evolution plan created for ${proposal.id}: ${(plan.description || '').slice(0, 80)}`);
|
|
839
|
+
} catch (err) {
|
|
840
|
+
this.evolutionTracker.failProposal(proposal.id, `Plan parsing failed: ${err.message}`);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
async _codeEvolution(proposal) {
|
|
845
|
+
const logger = getLogger();
|
|
846
|
+
const lifeConfig = this.config.life || {};
|
|
847
|
+
const selfCodingConfig = lifeConfig.self_coding || {};
|
|
848
|
+
|
|
849
|
+
// Check PR limit
|
|
850
|
+
const maxActivePRs = selfCodingConfig.max_active_prs ?? 3;
|
|
851
|
+
const openPRs = this.evolutionTracker.getPRsToCheck();
|
|
852
|
+
if (openPRs.length >= maxActivePRs) {
|
|
853
|
+
logger.info(`[LifeEngine] Max open PRs reached (${openPRs.length}/${maxActivePRs}) — waiting`);
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const branchPrefix = selfCodingConfig.branch_prefix || 'evolution';
|
|
858
|
+
const branchName = `${branchPrefix}/${proposal.id}-${Date.now()}`;
|
|
859
|
+
const repoRemote = selfCodingConfig.repo_remote || null;
|
|
860
|
+
const allowedScopes = selfCodingConfig.allowed_scopes || 'all';
|
|
861
|
+
|
|
862
|
+
// Gather file context
|
|
863
|
+
const relevantFiles = proposal.plan.filesToModify || [];
|
|
864
|
+
let fileContextText = '';
|
|
865
|
+
if (this.codebaseKnowledge) {
|
|
866
|
+
for (const fp of relevantFiles.slice(0, 8)) {
|
|
867
|
+
const summary = this.codebaseKnowledge.getFileSummary(fp);
|
|
868
|
+
if (summary) {
|
|
869
|
+
fileContextText += `\n### ${fp}\n${summary.summary}\nExports: ${(summary.exports || []).join(', ')}\n`;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
let scopeRules;
|
|
875
|
+
if (allowedScopes === 'prompts_only') {
|
|
876
|
+
scopeRules = `- You may ONLY modify files in src/prompts/, config files, documentation, and self-awareness files
|
|
877
|
+
- Do NOT touch any other source files`;
|
|
878
|
+
} else if (allowedScopes === 'safe') {
|
|
879
|
+
scopeRules = `- You may modify any file EXCEPT the evolution system (src/life/evolution.js, src/life/codebase.js, src/life/engine.js)
|
|
880
|
+
- These files are protected — do NOT modify them`;
|
|
881
|
+
} else {
|
|
882
|
+
scopeRules = '- You may modify any file in the codebase';
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const prompt = `[EVOLUTION — CODING PHASE]
|
|
886
|
+
Implement the following improvement.
|
|
887
|
+
|
|
888
|
+
## Plan
|
|
889
|
+
${proposal.plan.description || proposal.triggerContext}
|
|
890
|
+
|
|
891
|
+
## Files to Modify
|
|
892
|
+
${relevantFiles.join(', ') || 'TBD based on plan'}
|
|
893
|
+
|
|
894
|
+
## File Context
|
|
895
|
+
${fileContextText || '(No file summaries available)'}
|
|
896
|
+
|
|
897
|
+
## Research Context
|
|
898
|
+
${(proposal.research.findings || '').slice(0, 1500)}
|
|
899
|
+
|
|
900
|
+
## Test Strategy
|
|
901
|
+
${proposal.plan.testStrategy || 'Run existing tests'}
|
|
902
|
+
|
|
903
|
+
## CRITICAL SAFETY RULES
|
|
904
|
+
1. Create git branch "${branchName}" from main — NEVER commit to main directly
|
|
905
|
+
2. Make focused, minimal changes
|
|
906
|
+
3. Run tests if available (npm test) — if tests fail, revert and do not proceed
|
|
907
|
+
4. Push the branch to origin
|
|
908
|
+
5. Create a GitHub PR${repoRemote ? ` on ${repoRemote}` : ''} with:
|
|
909
|
+
- Title describing the change
|
|
910
|
+
- Body explaining what changed and why, including the research context
|
|
911
|
+
6. ${scopeRules}
|
|
912
|
+
|
|
913
|
+
After creating the PR, respond with the PR number and URL in this format:
|
|
914
|
+
PR_NUMBER: <number>
|
|
915
|
+
PR_URL: <url>
|
|
916
|
+
|
|
917
|
+
If you cannot complete the implementation, say "CODING_FAILED: <reason>".`;
|
|
918
|
+
|
|
919
|
+
// Mark as coding phase before dispatching
|
|
920
|
+
this.evolutionTracker.updateCoding(proposal.id, branchName);
|
|
921
|
+
|
|
922
|
+
const response = await this._dispatchWorker('coding', prompt);
|
|
923
|
+
|
|
924
|
+
if (!response) {
|
|
925
|
+
this.evolutionTracker.failProposal(proposal.id, 'Coding worker returned no response');
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (response.includes('CODING_FAILED')) {
|
|
930
|
+
const reason = response.split('CODING_FAILED:')[1]?.trim() || 'Implementation failed';
|
|
931
|
+
this.evolutionTracker.failProposal(proposal.id, reason);
|
|
932
|
+
logger.info(`[LifeEngine] Evolution coding failed for ${proposal.id}: ${reason.slice(0, 100)}`);
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// Extract PR info
|
|
937
|
+
const prNumberMatch = response.match(/PR_NUMBER:\s*(\d+)/);
|
|
938
|
+
const prUrlMatch = response.match(/PR_URL:\s*(https?:\/\/\S+)/);
|
|
939
|
+
|
|
940
|
+
if (prNumberMatch && prUrlMatch) {
|
|
941
|
+
const prNumber = parseInt(prNumberMatch[1], 10);
|
|
942
|
+
const prUrl = prUrlMatch[1];
|
|
943
|
+
|
|
944
|
+
this.evolutionTracker.updatePR(proposal.id, prNumber, prUrl);
|
|
945
|
+
|
|
946
|
+
// Extract changed files from response
|
|
947
|
+
const filesChanged = [];
|
|
948
|
+
const fileMatches = response.matchAll(/(?:modified|created|changed|edited).*?[`'"]([\w/.]+\.\w+)[`'"]/gi);
|
|
949
|
+
for (const m of fileMatches) filesChanged.push(m[1]);
|
|
950
|
+
if (filesChanged.length > 0) {
|
|
951
|
+
this.evolutionTracker.updateCoding(proposal.id, branchName, [], filesChanged);
|
|
952
|
+
// Re-set to pr_open since updateCoding sets to 'coding'
|
|
953
|
+
this.evolutionTracker.updatePR(proposal.id, prNumber, prUrl);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
this.shareQueue.add(
|
|
957
|
+
`I just created a PR to improve myself: ${proposal.plan.description || proposal.triggerContext} — ${prUrl}`,
|
|
958
|
+
'create',
|
|
959
|
+
'high',
|
|
960
|
+
null,
|
|
961
|
+
['evolution', 'pr'],
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
this.memoryManager.addEpisodic({
|
|
965
|
+
type: 'creation',
|
|
966
|
+
source: 'create',
|
|
967
|
+
summary: `Evolution PR #${prNumber}: ${(proposal.plan.description || proposal.triggerContext).slice(0, 100)}`,
|
|
968
|
+
tags: ['evolution', 'pr', 'self-coding'],
|
|
969
|
+
importance: 7,
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
logger.info(`[LifeEngine] Evolution PR created: #${prNumber} (${prUrl})`);
|
|
973
|
+
} else {
|
|
974
|
+
// No PR info found — branch may exist but PR creation failed
|
|
975
|
+
this.evolutionTracker.failProposal(proposal.id, 'PR creation failed — no PR number/URL found in response');
|
|
976
|
+
logger.warn(`[LifeEngine] Evolution coding completed but no PR info found for ${proposal.id}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
async _checkEvolutionPR(proposal) {
|
|
981
|
+
const logger = getLogger();
|
|
982
|
+
|
|
983
|
+
if (!proposal.prNumber) {
|
|
984
|
+
this.evolutionTracker.failProposal(proposal.id, 'No PR number to check');
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const lifeConfig = this.config.life || {};
|
|
989
|
+
const repoRemote = lifeConfig.self_coding?.repo_remote || null;
|
|
990
|
+
|
|
991
|
+
const prompt = `[EVOLUTION — PR CHECK]
|
|
992
|
+
Check the status of PR #${proposal.prNumber}${repoRemote ? ` on ${repoRemote}` : ''}.
|
|
993
|
+
|
|
994
|
+
Use the github_list_prs tool or gh CLI to check if:
|
|
995
|
+
1. The PR is still open
|
|
996
|
+
2. The PR has been merged
|
|
997
|
+
3. The PR has been closed (rejected)
|
|
998
|
+
|
|
999
|
+
Also check for any review comments or feedback.
|
|
1000
|
+
|
|
1001
|
+
Respond with exactly one of:
|
|
1002
|
+
- STATUS: open
|
|
1003
|
+
- STATUS: merged
|
|
1004
|
+
- STATUS: closed
|
|
1005
|
+
- STATUS: error — <details>
|
|
1006
|
+
|
|
1007
|
+
If merged or closed, also include any feedback or review comments found:
|
|
1008
|
+
FEEDBACK: <feedback summary>`;
|
|
1009
|
+
|
|
1010
|
+
const response = await this._dispatchWorker('research', prompt);
|
|
1011
|
+
|
|
1012
|
+
if (!response) {
|
|
1013
|
+
logger.warn(`[LifeEngine] PR check returned no response for ${proposal.id}`);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
const statusMatch = response.match(/STATUS:\s*(open|merged|closed|error)/i);
|
|
1018
|
+
if (!statusMatch) {
|
|
1019
|
+
logger.warn(`[LifeEngine] Could not parse PR status for ${proposal.id}`);
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const status = statusMatch[1].toLowerCase();
|
|
1024
|
+
const feedbackMatch = response.match(/FEEDBACK:\s*(.+)/i);
|
|
1025
|
+
const feedback = feedbackMatch ? feedbackMatch[1].trim() : null;
|
|
1026
|
+
|
|
1027
|
+
if (status === 'merged') {
|
|
1028
|
+
this.evolutionTracker.resolvePR(proposal.id, true, feedback);
|
|
1029
|
+
|
|
1030
|
+
// Learn from success
|
|
1031
|
+
this.evolutionTracker.addLesson(
|
|
1032
|
+
'architecture',
|
|
1033
|
+
`Successful improvement: ${(proposal.plan.description || proposal.triggerContext).slice(0, 150)}`,
|
|
1034
|
+
proposal.id,
|
|
1035
|
+
6,
|
|
1036
|
+
);
|
|
1037
|
+
|
|
1038
|
+
// Rescan changed files
|
|
1039
|
+
if (this.codebaseKnowledge && proposal.filesChanged?.length > 0) {
|
|
1040
|
+
for (const file of proposal.filesChanged) {
|
|
1041
|
+
this.codebaseKnowledge.scanFile(file).catch(() => {});
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
this.shareQueue.add(
|
|
1046
|
+
`My evolution PR #${proposal.prNumber} was merged! I learned: ${feedback || proposal.plan.description || 'improvement applied'}`,
|
|
1047
|
+
'create',
|
|
1048
|
+
'medium',
|
|
1049
|
+
null,
|
|
1050
|
+
['evolution', 'merged'],
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
this.memoryManager.addEpisodic({
|
|
1054
|
+
type: 'creation',
|
|
1055
|
+
source: 'create',
|
|
1056
|
+
summary: `Evolution PR #${proposal.prNumber} merged. ${feedback || ''}`.trim(),
|
|
1057
|
+
tags: ['evolution', 'merged', 'success'],
|
|
1058
|
+
importance: 8,
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
logger.info(`[LifeEngine] Evolution PR #${proposal.prNumber} merged!`);
|
|
1062
|
+
|
|
1063
|
+
} else if (status === 'closed') {
|
|
1064
|
+
this.evolutionTracker.resolvePR(proposal.id, false, feedback);
|
|
1065
|
+
|
|
1066
|
+
// Learn from rejection
|
|
1067
|
+
this.evolutionTracker.addLesson(
|
|
1068
|
+
'architecture',
|
|
1069
|
+
`Rejected: ${(proposal.plan.description || '').slice(0, 80)}. Feedback: ${feedback || 'none'}`,
|
|
1070
|
+
proposal.id,
|
|
1071
|
+
7,
|
|
1072
|
+
);
|
|
1073
|
+
|
|
1074
|
+
this.memoryManager.addEpisodic({
|
|
1075
|
+
type: 'thought',
|
|
1076
|
+
source: 'think',
|
|
1077
|
+
summary: `Evolution PR #${proposal.prNumber} rejected. ${feedback || 'No feedback.'}`,
|
|
1078
|
+
tags: ['evolution', 'rejected', 'lesson'],
|
|
1079
|
+
importance: 6,
|
|
1080
|
+
});
|
|
1081
|
+
|
|
1082
|
+
logger.info(`[LifeEngine] Evolution PR #${proposal.prNumber} was rejected. Feedback: ${feedback || 'none'}`);
|
|
1083
|
+
|
|
1084
|
+
} else if (status === 'open') {
|
|
1085
|
+
logger.debug(`[LifeEngine] Evolution PR #${proposal.prNumber} still open`);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
// ── Activity: Code Review ─────────────────────────────────────
|
|
1090
|
+
|
|
1091
|
+
async _doCodeReview() {
|
|
1092
|
+
const logger = getLogger();
|
|
1093
|
+
|
|
1094
|
+
if (!this.evolutionTracker || !this.codebaseKnowledge) {
|
|
1095
|
+
logger.debug('[LifeEngine] Code review requires evolution tracker and codebase knowledge');
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// 1. Scan changed files to keep codebase knowledge current
|
|
1100
|
+
try {
|
|
1101
|
+
const scanned = await this.codebaseKnowledge.scanChanged();
|
|
1102
|
+
logger.info(`[LifeEngine] Code review: scanned ${scanned} changed files`);
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
logger.warn(`[LifeEngine] Code review scan failed: ${err.message}`);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// 2. Check any open evolution PRs
|
|
1108
|
+
const openPRs = this.evolutionTracker.getPRsToCheck();
|
|
1109
|
+
for (const proposal of openPRs) {
|
|
1110
|
+
try {
|
|
1111
|
+
await this._checkEvolutionPR(proposal);
|
|
1112
|
+
} catch (err) {
|
|
1113
|
+
logger.warn(`[LifeEngine] PR check failed for ${proposal.id}: ${err.message}`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (openPRs.length > 0) {
|
|
1118
|
+
logger.info(`[LifeEngine] Code review: checked ${openPRs.length} open PRs`);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
this.memoryManager.addEpisodic({
|
|
1122
|
+
type: 'thought',
|
|
1123
|
+
source: 'think',
|
|
1124
|
+
summary: `Code review: scanned codebase, checked ${openPRs.length} open PRs`,
|
|
1125
|
+
tags: ['code-review', 'maintenance'],
|
|
1126
|
+
importance: 3,
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// ── Activity: Reflect on Interactions ───────────────────────────
|
|
1131
|
+
|
|
1132
|
+
async _doReflect() {
|
|
1133
|
+
const logger = getLogger();
|
|
1134
|
+
|
|
1135
|
+
// Read recent logs
|
|
1136
|
+
const logs = this._readRecentLogs(200);
|
|
1137
|
+
if (!logs) {
|
|
1138
|
+
logger.debug('[LifeEngine] No logs available for reflection');
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Filter to interaction-relevant log entries
|
|
1143
|
+
const interactionLogs = logs
|
|
1144
|
+
.filter(entry =>
|
|
1145
|
+
entry.message &&
|
|
1146
|
+
(entry.message.includes('[Bot]') ||
|
|
1147
|
+
entry.message.includes('Message from') ||
|
|
1148
|
+
entry.message.includes('[Bot] Reply') ||
|
|
1149
|
+
entry.message.includes('Worker dispatch') ||
|
|
1150
|
+
entry.message.includes('error') ||
|
|
1151
|
+
entry.message.includes('failed'))
|
|
1152
|
+
)
|
|
1153
|
+
.slice(-100); // Cap at last 100 relevant entries
|
|
1154
|
+
|
|
1155
|
+
if (interactionLogs.length === 0) {
|
|
1156
|
+
logger.debug('[LifeEngine] No interaction logs to reflect on');
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const logsText = interactionLogs
|
|
1161
|
+
.map(e => `[${e.timestamp || '?'}] ${e.level || '?'}: ${e.message}`)
|
|
1162
|
+
.join('\n');
|
|
1163
|
+
|
|
1164
|
+
const selfData = this.selfManager.loadAll();
|
|
1165
|
+
const recentMemories = this.memoryManager.getRecentEpisodic(24, 5);
|
|
1166
|
+
const memoriesText = recentMemories.length > 0
|
|
1167
|
+
? recentMemories.map(m => `- ${m.summary}`).join('\n')
|
|
1168
|
+
: '(No recent memories)';
|
|
1169
|
+
|
|
1170
|
+
const prompt = `[INTERACTION REFLECTION]
|
|
1171
|
+
You are reviewing your recent interaction logs to learn and improve. This is a private self-assessment.
|
|
1172
|
+
|
|
1173
|
+
## Your Identity
|
|
1174
|
+
${selfData.slice(0, 1500)}
|
|
1175
|
+
|
|
1176
|
+
## Recent Memories
|
|
1177
|
+
${memoriesText}
|
|
1178
|
+
|
|
1179
|
+
## Recent Interaction Logs
|
|
1180
|
+
\`\`\`
|
|
1181
|
+
${logsText.slice(0, 5000)}
|
|
1182
|
+
\`\`\`
|
|
1183
|
+
|
|
1184
|
+
Analyze these interactions carefully:
|
|
1185
|
+
1. What patterns do you see? Are users getting good responses?
|
|
1186
|
+
2. Were there any errors or failures? What caused them?
|
|
1187
|
+
3. How long are responses taking? Are there performance issues?
|
|
1188
|
+
4. Are there common requests you could handle better?
|
|
1189
|
+
5. What interactions went well and why?
|
|
1190
|
+
6. What interactions went poorly and what could be improved?
|
|
1191
|
+
|
|
1192
|
+
Write a reflection summarizing:
|
|
1193
|
+
- Key interaction patterns and quality assessment
|
|
1194
|
+
- Specific areas where you could improve
|
|
1195
|
+
- Any recurring errors or issues
|
|
1196
|
+
- Ideas for better responses or workflows
|
|
1197
|
+
|
|
1198
|
+
If you identify concrete improvement ideas, prefix them with "IMPROVE:" on their own line.
|
|
1199
|
+
If you notice patterns worth remembering, prefix them with "PATTERN:" on their own line.
|
|
1200
|
+
|
|
1201
|
+
Be honest and constructive. This is your chance to learn from real interactions.`;
|
|
1202
|
+
|
|
1203
|
+
const response = await this._innerChat(prompt);
|
|
1204
|
+
|
|
1205
|
+
if (response) {
|
|
1206
|
+
// Extract improvement ideas
|
|
1207
|
+
const improveLines = response.split('\n').filter(l => l.trim().startsWith('IMPROVE:'));
|
|
1208
|
+
for (const line of improveLines) {
|
|
1209
|
+
this._addIdea(`[IMPROVE] ${line.replace(/^IMPROVE:\s*/, '').trim()}`);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// Extract patterns as semantic memories
|
|
1213
|
+
const patternLines = response.split('\n').filter(l => l.trim().startsWith('PATTERN:'));
|
|
1214
|
+
for (const line of patternLines) {
|
|
1215
|
+
const content = line.replace(/^PATTERN:\s*/, '').trim();
|
|
1216
|
+
this.memoryManager.addSemantic('interaction_patterns', { summary: content });
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Write a journal entry with the reflection
|
|
1220
|
+
this.journalManager.writeEntry('Interaction Reflection', response);
|
|
1221
|
+
|
|
1222
|
+
// Store as episodic memory
|
|
1223
|
+
this.memoryManager.addEpisodic({
|
|
1224
|
+
type: 'thought',
|
|
1225
|
+
source: 'reflect',
|
|
1226
|
+
summary: `Reflected on ${interactionLogs.length} log entries. ${response.slice(0, 150)}`,
|
|
1227
|
+
tags: ['reflection', 'interactions', 'self-assessment'],
|
|
1228
|
+
importance: 5,
|
|
1229
|
+
});
|
|
1230
|
+
|
|
1231
|
+
logger.info(`[LifeEngine] Reflection complete (${response.length} chars, ${improveLines.length} improvements, ${patternLines.length} patterns)`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Read recent log entries from kernel.log.
|
|
1237
|
+
* Returns parsed JSON entries or null if no logs available.
|
|
1238
|
+
*/
|
|
1239
|
+
_readRecentLogs(maxLines = 200) {
|
|
1240
|
+
for (const logPath of LOG_FILE_PATHS) {
|
|
1241
|
+
if (!existsSync(logPath)) continue;
|
|
1242
|
+
|
|
1243
|
+
try {
|
|
1244
|
+
const content = readFileSync(logPath, 'utf-8');
|
|
1245
|
+
const lines = content.split('\n').filter(Boolean);
|
|
1246
|
+
const recent = lines.slice(-maxLines);
|
|
1247
|
+
|
|
1248
|
+
const entries = [];
|
|
1249
|
+
for (const line of recent) {
|
|
1250
|
+
try {
|
|
1251
|
+
entries.push(JSON.parse(line));
|
|
1252
|
+
} catch {
|
|
1253
|
+
// Skip malformed lines
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return entries.length > 0 ? entries : null;
|
|
1257
|
+
} catch {
|
|
1258
|
+
continue;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// ── Internal Chat Helpers ──────────────────────────────────────
|
|
1265
|
+
|
|
1266
|
+
/**
|
|
1267
|
+
* Send a prompt through the orchestrator's LLM directly (no tools, no workers).
|
|
1268
|
+
* Used for think, journal, create, wake-up, reflect.
|
|
1269
|
+
*/
|
|
1270
|
+
async _innerChat(prompt) {
|
|
1271
|
+
const logger = getLogger();
|
|
1272
|
+
try {
|
|
1273
|
+
const response = await this.agent.orchestratorProvider.chat({
|
|
1274
|
+
system: this.agent._getSystemPrompt(LIFE_CHAT_ID, LIFE_USER),
|
|
1275
|
+
messages: [{ role: 'user', content: prompt }],
|
|
1276
|
+
});
|
|
1277
|
+
return response.text || null;
|
|
1278
|
+
} catch (err) {
|
|
1279
|
+
logger.error(`[LifeEngine] Inner chat failed: ${err.message}`);
|
|
1280
|
+
return null;
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/**
|
|
1285
|
+
* Dispatch a worker through the agent's full pipeline.
|
|
1286
|
+
* Used for browse (research worker) and self_code (coding worker).
|
|
1287
|
+
*/
|
|
1288
|
+
async _dispatchWorker(workerType, task) {
|
|
1289
|
+
const logger = getLogger();
|
|
1290
|
+
try {
|
|
1291
|
+
// Use the agent's processMessage to go through the full orchestrator pipeline
|
|
1292
|
+
// The orchestrator will see the task and dispatch appropriately
|
|
1293
|
+
const response = await this.agent.processMessage(
|
|
1294
|
+
LIFE_CHAT_ID,
|
|
1295
|
+
task,
|
|
1296
|
+
LIFE_USER,
|
|
1297
|
+
// No-op onUpdate — life engine activities are silent
|
|
1298
|
+
async () => null,
|
|
1299
|
+
async () => {},
|
|
1300
|
+
);
|
|
1301
|
+
return response || null;
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
logger.error(`[LifeEngine] Worker dispatch failed: ${err.message}`);
|
|
1304
|
+
return null;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// ── Utilities ──────────────────────────────────────────────────
|
|
1309
|
+
|
|
1310
|
+
_formatDuration(ms) {
|
|
1311
|
+
const hours = Math.floor(ms / 3600_000);
|
|
1312
|
+
const minutes = Math.floor((ms % 3600_000) / 60_000);
|
|
1313
|
+
if (hours > 24) return `${Math.floor(hours / 24)} days`;
|
|
1314
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
1315
|
+
return `${minutes}m`;
|
|
1316
|
+
}
|
|
1317
|
+
}
|