tiger-agent 0.2.0
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 +22 -0
- package/.env.secrets.example +14 -0
- package/LICENSE +22 -0
- package/README.md +284 -0
- package/bin/tiger.js +96 -0
- package/package.json +58 -0
- package/scripts/audit.sh +54 -0
- package/scripts/backup.sh +42 -0
- package/scripts/cryptoEnv.js +57 -0
- package/scripts/decrypt-env.js +34 -0
- package/scripts/encrypt-env.js +34 -0
- package/scripts/migrate-vector-db.js +44 -0
- package/scripts/onboard.js +319 -0
- package/scripts/scan-secrets.sh +87 -0
- package/scripts/setup.js +302 -0
- package/scripts/sqlite_memory.py +297 -0
- package/scripts/sqlite_vec_setup.py +112 -0
- package/src/agent/contextFiles.js +30 -0
- package/src/agent/db.js +349 -0
- package/src/agent/mainAgent.js +406 -0
- package/src/agent/reflectionAgent.js +193 -0
- package/src/agent/reflectionScheduler.js +21 -0
- package/src/agent/skills.js +169 -0
- package/src/agent/subAgent.js +39 -0
- package/src/agent/toolbox.js +291 -0
- package/src/apiProviders.js +217 -0
- package/src/cli.js +187 -0
- package/src/config.js +141 -0
- package/src/kimiClient.js +88 -0
- package/src/llmClient.js +147 -0
- package/src/telegram/bot.js +182 -0
- package/src/telegram/supervisor.js +84 -0
- package/src/tokenManager.js +223 -0
- package/src/utils.js +30 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { chatCompletion, embedText } = require('../llmClient');
|
|
4
|
+
const {
|
|
5
|
+
embeddingsEnabled,
|
|
6
|
+
allowShell,
|
|
7
|
+
ownSkillPath,
|
|
8
|
+
ownSkillUpdateHours,
|
|
9
|
+
soulPath,
|
|
10
|
+
soulUpdateHours,
|
|
11
|
+
memoryIngestEveryTurns,
|
|
12
|
+
memoryIngestMinChars
|
|
13
|
+
} = require('../config');
|
|
14
|
+
const { loadContextFiles } = require('./contextFiles');
|
|
15
|
+
const { tools, callTool } = require('./toolbox');
|
|
16
|
+
const {
|
|
17
|
+
ensureConversation,
|
|
18
|
+
addMessage,
|
|
19
|
+
getRecentMessages,
|
|
20
|
+
getMessagesForCompaction,
|
|
21
|
+
deleteMessagesUpTo,
|
|
22
|
+
addMemory,
|
|
23
|
+
getRelevantMemories,
|
|
24
|
+
getMeta,
|
|
25
|
+
setMeta,
|
|
26
|
+
recordSkillUsage
|
|
27
|
+
} = require('./db');
|
|
28
|
+
|
|
29
|
+
function safeJsonParse(text, fallback = {}) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(text);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
return fallback;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function renderContextFiles(files) {
|
|
38
|
+
return files
|
|
39
|
+
.map((f) => `## ${f.name}\n${f.content}`)
|
|
40
|
+
.join('\n\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function compactConversation(conversationIdValue) {
|
|
44
|
+
const rows = getMessagesForCompaction(conversationIdValue);
|
|
45
|
+
if (!rows.length) return;
|
|
46
|
+
|
|
47
|
+
const raw = rows.map((r) => `${r.role.toUpperCase()}: ${r.content}`).join('\n');
|
|
48
|
+
const summaryMessage = await chatCompletion([
|
|
49
|
+
{
|
|
50
|
+
role: 'system',
|
|
51
|
+
content:
|
|
52
|
+
'Summarize dialogue into durable memory: user profile, goals, preferences, commitments, decisions, unresolved items.'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
role: 'user',
|
|
56
|
+
content: raw
|
|
57
|
+
}
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const summary = String(summaryMessage.content || '').trim();
|
|
61
|
+
if (!summary) return;
|
|
62
|
+
|
|
63
|
+
let emb = [];
|
|
64
|
+
if (embeddingsEnabled) {
|
|
65
|
+
try {
|
|
66
|
+
emb = await embedText(summary);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
emb = [];
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (emb.length) {
|
|
72
|
+
addMemory(conversationIdValue, 'compaction', summary, emb);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const maxId = rows[rows.length - 1].id;
|
|
76
|
+
deleteMessagesUpTo(conversationIdValue, maxId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function maybeUpdateHumanFile(userText, assistantText) {
|
|
80
|
+
const files = loadContextFiles();
|
|
81
|
+
const human2 = files.find((f) => f.name === 'human2.md');
|
|
82
|
+
if (!human2) return;
|
|
83
|
+
|
|
84
|
+
const message = await chatCompletion([
|
|
85
|
+
{
|
|
86
|
+
role: 'system',
|
|
87
|
+
content:
|
|
88
|
+
'Extract long-term user profile updates. Return strict JSON: {"append": "..."} or {"append": ""}. Use short bullets.'
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
role: 'user',
|
|
92
|
+
content: `User message:\n${userText}\n\nAssistant message:\n${assistantText}`
|
|
93
|
+
}
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
const parsed = safeJsonParse(String(message.content || '{}'), {});
|
|
97
|
+
const append = String(parsed.append || '').trim();
|
|
98
|
+
if (!append) return;
|
|
99
|
+
|
|
100
|
+
const block = `\n## Update ${new Date().toISOString()}\n${append}\n`;
|
|
101
|
+
fs.appendFileSync(path.resolve(human2.full), block, 'utf8');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildSystemPrompt(contextText, memoriesText) {
|
|
105
|
+
const shellStatus = allowShell ? 'enabled' : 'disabled';
|
|
106
|
+
return [
|
|
107
|
+
'You are Tiger, a practical orchestration agent.',
|
|
108
|
+
'Use tools when needed to inspect files, execute tasks, load skills, and run sub-agents.',
|
|
109
|
+
'For OpenClaw skills, use clawhub_search and clawhub_install tools.',
|
|
110
|
+
'For greetings/small talk (e.g., "hi", "hello", "how are you"), reply directly and do not start tool/setup actions.',
|
|
111
|
+
'Only begin setup/install/search/execution steps when the user explicitly asks for those actions.',
|
|
112
|
+
`Shell tool status right now: ${shellStatus}.`,
|
|
113
|
+
'If a user asks to search the web, call run_shell first and use installed skills/commands.',
|
|
114
|
+
'Do not claim shell is disabled unless a run_shell tool call in this turn returns that error.',
|
|
115
|
+
'If tool output is incomplete, continue calling tools before final answer.',
|
|
116
|
+
'Keep answers concise and actionable.',
|
|
117
|
+
'',
|
|
118
|
+
'Always account for this identity and user profile context:',
|
|
119
|
+
contextText,
|
|
120
|
+
'',
|
|
121
|
+
memoriesText ? `Relevant compacted memory:\n${memoriesText}` : ''
|
|
122
|
+
]
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.join('\n');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function shouldRefreshFile(filePath, updateHours) {
|
|
128
|
+
try {
|
|
129
|
+
const stat = fs.statSync(filePath);
|
|
130
|
+
const maxAgeMs = updateHours * 60 * 60 * 1000;
|
|
131
|
+
return Date.now() - stat.mtimeMs >= maxAgeMs;
|
|
132
|
+
} catch (err) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function shouldRefreshOwnSkill() {
|
|
138
|
+
return shouldRefreshFile(ownSkillPath, ownSkillUpdateHours);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function maybeUpdateOwnSkillSummary(conversationIdValue) {
|
|
142
|
+
if (!shouldRefreshOwnSkill()) return;
|
|
143
|
+
|
|
144
|
+
const recent = getRecentMessages(conversationIdValue, 80);
|
|
145
|
+
const transcript = recent
|
|
146
|
+
.map((m) => `${String(m.role || '').toUpperCase()}: ${String(m.content || '')}`)
|
|
147
|
+
.join('\n');
|
|
148
|
+
const previous = fs.existsSync(ownSkillPath) ? fs.readFileSync(ownSkillPath, 'utf8') : '';
|
|
149
|
+
|
|
150
|
+
const message = await chatCompletion([
|
|
151
|
+
{
|
|
152
|
+
role: 'system',
|
|
153
|
+
content: [
|
|
154
|
+
'You maintain Tiger\'s own skill summary file.',
|
|
155
|
+
'Return concise markdown only.',
|
|
156
|
+
'Include sections:',
|
|
157
|
+
'# ownskill',
|
|
158
|
+
'## Updated',
|
|
159
|
+
'## Skills Learned',
|
|
160
|
+
'## Recent Work Summary',
|
|
161
|
+
'## Patterns Observed',
|
|
162
|
+
'## Failures & Lessons',
|
|
163
|
+
'## Successful Workflows',
|
|
164
|
+
'## Known Limits',
|
|
165
|
+
'## Next Improvements',
|
|
166
|
+
'Base updates on recent conversation work and keep it factual.'
|
|
167
|
+
].join('\n')
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
role: 'user',
|
|
171
|
+
content: `Previous ownskill.md:\n${previous || '(empty)'}\n\nRecent work transcript:\n${transcript || '(empty)'}`
|
|
172
|
+
}
|
|
173
|
+
]);
|
|
174
|
+
|
|
175
|
+
const next = String(message.content || '').trim();
|
|
176
|
+
if (!next) return;
|
|
177
|
+
fs.writeFileSync(path.resolve(ownSkillPath), `${next}\n`, 'utf8');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function maybeUpdateSoulSummary(conversationIdValue) {
|
|
181
|
+
if (!shouldRefreshFile(soulPath, soulUpdateHours)) return;
|
|
182
|
+
|
|
183
|
+
const recent = getRecentMessages(conversationIdValue, 80);
|
|
184
|
+
const transcript = recent
|
|
185
|
+
.map((m) => `${String(m.role || '').toUpperCase()}: ${String(m.content || '')}`)
|
|
186
|
+
.join('\n');
|
|
187
|
+
const previous = fs.existsSync(soulPath) ? fs.readFileSync(soulPath, 'utf8') : '';
|
|
188
|
+
const nowIso = new Date().toISOString();
|
|
189
|
+
|
|
190
|
+
const message = await chatCompletion([
|
|
191
|
+
{
|
|
192
|
+
role: 'system',
|
|
193
|
+
content: [
|
|
194
|
+
"You maintain Tiger's soul.md: identity, principles, operating rules, and stable preferences.",
|
|
195
|
+
'Return concise markdown only.',
|
|
196
|
+
'Always include a section:',
|
|
197
|
+
'## Self-Update',
|
|
198
|
+
`- cadence_hours: ${soulUpdateHours}`,
|
|
199
|
+
`- last_updated: ${nowIso}`,
|
|
200
|
+
'- note: This is a self-maintained summary (not model training).',
|
|
201
|
+
'Keep it factual; do not include secrets/tokens; do not paste API keys.'
|
|
202
|
+
].join('\n')
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
role: 'user',
|
|
206
|
+
content: `Previous soul.md:\n${previous || '(empty)'}\n\nRecent work transcript:\n${transcript || '(empty)'}`
|
|
207
|
+
}
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
const next = String(message.content || '').trim();
|
|
211
|
+
if (!next) return;
|
|
212
|
+
fs.writeFileSync(path.resolve(soulPath), `${next}\n`, 'utf8');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function maybeIngestTurnMemory(conversationIdValue, userText, assistantText) {
|
|
216
|
+
const recent = getRecentMessages(conversationIdValue, 120);
|
|
217
|
+
const messageCount = recent.length;
|
|
218
|
+
const key = `turn_ingest_last_count:${conversationIdValue}`;
|
|
219
|
+
const lastCount = Number(getMeta(key, 0) || 0);
|
|
220
|
+
const thresholdMessages = Math.max(2, memoryIngestEveryTurns * 2);
|
|
221
|
+
const combined = `${String(userText || '').trim()}\n${String(assistantText || '').trim()}`.trim();
|
|
222
|
+
const hasPreferenceSignal =
|
|
223
|
+
/(prefer|always|never|avoid|instead|workflow|habit|schedule|every|important|priority)/i.test(combined);
|
|
224
|
+
|
|
225
|
+
if (messageCount - lastCount < thresholdMessages && combined.length < memoryIngestMinChars && !hasPreferenceSignal) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const transcript = recent
|
|
230
|
+
.slice(-10)
|
|
231
|
+
.map((m) => `${String(m.role || '').toUpperCase()}: ${String(m.content || '')}`)
|
|
232
|
+
.join('\n');
|
|
233
|
+
|
|
234
|
+
const message = await chatCompletion([
|
|
235
|
+
{
|
|
236
|
+
role: 'system',
|
|
237
|
+
content: [
|
|
238
|
+
'Extract one durable memory item from the recent exchange.',
|
|
239
|
+
'Return plain text only, max 2 short bullets.',
|
|
240
|
+
'Focus on user preferences, recurring workflows, or stable decisions.',
|
|
241
|
+
'Return empty string if there is no durable memory.'
|
|
242
|
+
].join('\n')
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
role: 'user',
|
|
246
|
+
content: transcript || '(empty)'
|
|
247
|
+
}
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
const summary = String(message.content || '').trim();
|
|
251
|
+
if (!summary) return;
|
|
252
|
+
|
|
253
|
+
let emb = [];
|
|
254
|
+
if (embeddingsEnabled) {
|
|
255
|
+
try {
|
|
256
|
+
emb = await embedText(summary);
|
|
257
|
+
} catch (err) {
|
|
258
|
+
emb = [];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
addMemory(conversationIdValue, 'turn_ingest', summary, emb);
|
|
262
|
+
setMeta(key, messageCount);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function runWithTools(initialMessages) {
|
|
266
|
+
const messages = [...initialMessages];
|
|
267
|
+
const maxToolRounds = 8;
|
|
268
|
+
let lastToolError = null;
|
|
269
|
+
|
|
270
|
+
for (let i = 0; i < maxToolRounds; i += 1) {
|
|
271
|
+
const assistant = await chatCompletion(messages, { tools, tool_choice: 'auto' });
|
|
272
|
+
const rawToolCalls = Array.isArray(assistant.tool_calls) ? assistant.tool_calls : [];
|
|
273
|
+
const toolCalls = rawToolCalls.filter((tc) => tc && tc.id && tc.function && tc.function.name);
|
|
274
|
+
const assistantContent = assistant.content || '';
|
|
275
|
+
const reasoningContent = assistant.reasoning_content || '';
|
|
276
|
+
|
|
277
|
+
const assistantMsg = {
|
|
278
|
+
role: 'assistant',
|
|
279
|
+
content: assistantContent,
|
|
280
|
+
tool_calls: toolCalls
|
|
281
|
+
};
|
|
282
|
+
if (reasoningContent) {
|
|
283
|
+
assistantMsg.reasoning_content = reasoningContent;
|
|
284
|
+
}
|
|
285
|
+
messages.push(assistantMsg);
|
|
286
|
+
|
|
287
|
+
if (!toolCalls.length) {
|
|
288
|
+
if (assistantContent) return assistantContent;
|
|
289
|
+
if (lastToolError) {
|
|
290
|
+
return `⚠️ ${lastToolError.name} failed: ${lastToolError.error}`;
|
|
291
|
+
}
|
|
292
|
+
// Allow one more round if the model emitted a reasoning-only assistant turn.
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (const tc of toolCalls) {
|
|
297
|
+
const fn = tc.function || {};
|
|
298
|
+
const name = fn.name || '';
|
|
299
|
+
const args = safeJsonParse(fn.arguments || '{}', {});
|
|
300
|
+
let payload;
|
|
301
|
+
try {
|
|
302
|
+
payload = await callTool(name, args);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
payload = { ok: false, error: err.message };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const toolFailed =
|
|
308
|
+
payload?.ok === false ||
|
|
309
|
+
payload?.status === 'error' ||
|
|
310
|
+
(typeof payload?.error === 'string' && payload.error.length > 0);
|
|
311
|
+
if (toolFailed) {
|
|
312
|
+
lastToolError = {
|
|
313
|
+
name,
|
|
314
|
+
error: String(payload?.error || 'tool execution failed')
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
recordSkillUsage(name, 'tool');
|
|
319
|
+
if (name === 'load_skill') {
|
|
320
|
+
const skillName = String(args.name || '').trim();
|
|
321
|
+
if (skillName) recordSkillUsage(`skill:${skillName}`, 'skill');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
messages.push({
|
|
325
|
+
role: 'tool',
|
|
326
|
+
tool_call_id: tc.id,
|
|
327
|
+
content: JSON.stringify(payload)
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const fallback = await chatCompletion(messages, { tools: [], tool_choice: 'none' });
|
|
333
|
+
const fallbackText = String(fallback.content || '').trim();
|
|
334
|
+
if (fallbackText) return fallbackText;
|
|
335
|
+
if (lastToolError) {
|
|
336
|
+
return `⚠️ ${lastToolError.name} failed: ${lastToolError.error}`;
|
|
337
|
+
}
|
|
338
|
+
return 'I could not produce a final answer.';
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function handleMessage({ platform, userId, text }) {
|
|
342
|
+
const conversationIdValue = ensureConversation(platform, userId);
|
|
343
|
+
addMessage(conversationIdValue, 'user', text);
|
|
344
|
+
|
|
345
|
+
try {
|
|
346
|
+
await compactConversation(conversationIdValue);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
// Keep chat available even if compaction fails.
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const contextFiles = loadContextFiles();
|
|
352
|
+
const contextText = renderContextFiles(contextFiles);
|
|
353
|
+
const recent = getRecentMessages(conversationIdValue);
|
|
354
|
+
|
|
355
|
+
let memoryText = '';
|
|
356
|
+
if (embeddingsEnabled) {
|
|
357
|
+
try {
|
|
358
|
+
const qEmb = await embedText(text);
|
|
359
|
+
const relevant = getRelevantMemories(conversationIdValue, qEmb, 6);
|
|
360
|
+
memoryText = relevant
|
|
361
|
+
.map((m) => `- (${m.source}) ${m.content}`)
|
|
362
|
+
.join('\n');
|
|
363
|
+
} catch (err) {
|
|
364
|
+
memoryText = '';
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const system = buildSystemPrompt(contextText, memoryText);
|
|
369
|
+
const messages = [
|
|
370
|
+
{ role: 'system', content: system },
|
|
371
|
+
...recent.map((m) => ({ role: m.role, content: m.content }))
|
|
372
|
+
];
|
|
373
|
+
|
|
374
|
+
const reply = (await runWithTools(messages)).trim() || 'No response generated.';
|
|
375
|
+
addMessage(conversationIdValue, 'assistant', reply);
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
await maybeUpdateHumanFile(text, reply);
|
|
379
|
+
} catch (err) {
|
|
380
|
+
// Non-blocking profile update.
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
await maybeUpdateOwnSkillSummary(conversationIdValue);
|
|
385
|
+
} catch (err) {
|
|
386
|
+
// Non-blocking own-skill update.
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
try {
|
|
390
|
+
await maybeUpdateSoulSummary(conversationIdValue);
|
|
391
|
+
} catch (err) {
|
|
392
|
+
// Non-blocking soul update.
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
try {
|
|
396
|
+
await maybeIngestTurnMemory(conversationIdValue, text, reply);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
// Non-blocking ingestion update.
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return reply;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
module.exports = {
|
|
405
|
+
handleMessage
|
|
406
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { chatCompletion, embedText } = require('../llmClient');
|
|
4
|
+
const { dataDir, embeddingsEnabled } = require('../config');
|
|
5
|
+
const { addMemory, getMeta, setMeta, getMessagesSince, getRecentMessagesAll } = require('./db');
|
|
6
|
+
|
|
7
|
+
const REFLECTION_META_KEY = 'memory_reflection_last_run_ts';
|
|
8
|
+
const MAX_MESSAGE_SCAN = 600;
|
|
9
|
+
|
|
10
|
+
function nowTs() {
|
|
11
|
+
return Date.now();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function asIso(ts) {
|
|
15
|
+
return new Date(ts).toISOString();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseJsonMaybe(content) {
|
|
19
|
+
const text = String(content || '').trim();
|
|
20
|
+
if (!text) return null;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(text);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
// Continue with fenced JSON extraction fallback.
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const fenced = text.match(/```json\s*([\s\S]*?)```/i);
|
|
28
|
+
if (!fenced) return null;
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(String(fenced[1] || '').trim());
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function normalizeItems(input, limit = 6) {
|
|
37
|
+
if (!Array.isArray(input)) return [];
|
|
38
|
+
return input
|
|
39
|
+
.map((v) => String(v || '').trim())
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.slice(0, limit);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function buildTranscript(rows) {
|
|
45
|
+
return rows
|
|
46
|
+
.map((m) => {
|
|
47
|
+
const role = String(m.role || '').toUpperCase();
|
|
48
|
+
const conv = String(m.conversation_id || 'unknown');
|
|
49
|
+
const text = String(m.content || '').trim();
|
|
50
|
+
return `[${conv}] ${role}: ${text}`;
|
|
51
|
+
})
|
|
52
|
+
.join('\n');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ensureHeading(markdown, heading) {
|
|
56
|
+
if (new RegExp(`^##\\s+${heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm').test(markdown)) {
|
|
57
|
+
return markdown;
|
|
58
|
+
}
|
|
59
|
+
const suffix = markdown.endsWith('\n') ? '' : '\n';
|
|
60
|
+
return `${markdown}${suffix}\n## ${heading}\n`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function appendTimestampedBullets(filePath, heading, bullets, stamp) {
|
|
64
|
+
if (!bullets.length) return;
|
|
65
|
+
const full = path.resolve(filePath);
|
|
66
|
+
const existing = fs.existsSync(full) ? fs.readFileSync(full, 'utf8') : `# ${path.basename(filePath, '.md')}\n\n`;
|
|
67
|
+
const withHeading = ensureHeading(existing, heading);
|
|
68
|
+
const lines = bullets.map((line) => `- [${stamp}] ${line}`).join('\n');
|
|
69
|
+
const next = `${withHeading.trimEnd()}\n${lines}\n`;
|
|
70
|
+
fs.writeFileSync(full, next, 'utf8');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function appendHuman2Update(filePath, payload, stampIso) {
|
|
74
|
+
const full = path.resolve(filePath);
|
|
75
|
+
const existing = fs.existsSync(full) ? fs.readFileSync(full, 'utf8') : '# human2\n\n';
|
|
76
|
+
const lines = [];
|
|
77
|
+
if (payload.summary) lines.push(`- Summary: ${payload.summary}`);
|
|
78
|
+
for (const p of payload.patternsObserved) lines.push(`- Pattern: ${p}`);
|
|
79
|
+
for (const f of payload.failuresLessons) lines.push(`- Lesson: ${f}`);
|
|
80
|
+
for (const w of payload.successfulWorkflows) lines.push(`- Workflow: ${w}`);
|
|
81
|
+
if (!lines.length) return;
|
|
82
|
+
const block = `\n## Update ${stampIso}\n${lines.join('\n')}\n`;
|
|
83
|
+
fs.writeFileSync(full, `${existing.trimEnd()}\n${block}`, 'utf8');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function generateReflection(rows, sinceIso, untilIso) {
|
|
87
|
+
const transcript = buildTranscript(rows);
|
|
88
|
+
const response = await chatCompletion([
|
|
89
|
+
{
|
|
90
|
+
role: 'system',
|
|
91
|
+
content: [
|
|
92
|
+
'You are a reflection agent that improves an assistant memory system.',
|
|
93
|
+
'Return strict JSON with this schema only:',
|
|
94
|
+
'{"summary":"","patterns_observed":[],"failures_lessons":[],"successful_workflows":[],"preference_updates":[],"what_to_do_differently":[]}',
|
|
95
|
+
'Rules:',
|
|
96
|
+
'- Keep each bullet short and factual.',
|
|
97
|
+
'- If unknown, return empty arrays.',
|
|
98
|
+
'- Do not include secrets.'
|
|
99
|
+
].join('\n')
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
role: 'user',
|
|
103
|
+
content: [
|
|
104
|
+
`Analyze conversation data between ${sinceIso} and ${untilIso}.`,
|
|
105
|
+
'Extract actionable long-term memory updates.',
|
|
106
|
+
transcript || '(empty)'
|
|
107
|
+
].join('\n\n')
|
|
108
|
+
}
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const parsed = parseJsonMaybe(response.content || '');
|
|
112
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
summary: String(parsed.summary || '').trim(),
|
|
118
|
+
patternsObserved: normalizeItems(parsed.patterns_observed),
|
|
119
|
+
failuresLessons: normalizeItems(parsed.failures_lessons),
|
|
120
|
+
successfulWorkflows: normalizeItems(parsed.successful_workflows),
|
|
121
|
+
preferenceUpdates: normalizeItems(parsed.preference_updates),
|
|
122
|
+
whatToDoDifferently: normalizeItems(parsed.what_to_do_differently)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function maybeRunReflectionCycle({ force = false } = {}) {
|
|
127
|
+
const startedAt = nowTs();
|
|
128
|
+
const lastRunTs = Number(getMeta(REFLECTION_META_KEY, 0) || 0);
|
|
129
|
+
const rows = lastRunTs
|
|
130
|
+
? getMessagesSince(lastRunTs, MAX_MESSAGE_SCAN)
|
|
131
|
+
: getRecentMessagesAll(Math.min(MAX_MESSAGE_SCAN, 240));
|
|
132
|
+
if (!rows.length && !force) {
|
|
133
|
+
return { ok: true, skipped: true, reason: 'no_new_messages' };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const sinceIso = lastRunTs ? asIso(lastRunTs) : 'beginning';
|
|
137
|
+
const untilIso = asIso(startedAt);
|
|
138
|
+
const reflection = await generateReflection(rows, sinceIso, untilIso);
|
|
139
|
+
if (!reflection) {
|
|
140
|
+
return { ok: false, skipped: true, reason: 'invalid_reflection_json' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const stampIso = asIso(startedAt);
|
|
144
|
+
const stampDay = stampIso.slice(0, 10);
|
|
145
|
+
const ownSkillPath = path.resolve(dataDir, 'ownskill.md');
|
|
146
|
+
const humanPath = path.resolve(dataDir, 'human.md');
|
|
147
|
+
const human2Path = path.resolve(dataDir, 'human2.md');
|
|
148
|
+
const soulPath = path.resolve(dataDir, 'soul.md');
|
|
149
|
+
|
|
150
|
+
appendTimestampedBullets(ownSkillPath, 'Patterns Observed', reflection.patternsObserved, stampDay);
|
|
151
|
+
appendTimestampedBullets(ownSkillPath, 'Failures & Lessons', reflection.failuresLessons, stampDay);
|
|
152
|
+
appendTimestampedBullets(ownSkillPath, 'Successful Workflows', reflection.successfulWorkflows, stampDay);
|
|
153
|
+
|
|
154
|
+
appendTimestampedBullets(humanPath, 'Patterns Observed', reflection.patternsObserved, stampDay);
|
|
155
|
+
appendTimestampedBullets(humanPath, 'Successful Workflows', reflection.successfulWorkflows, stampDay);
|
|
156
|
+
|
|
157
|
+
appendTimestampedBullets(soulPath, 'Patterns Observed', reflection.patternsObserved, stampDay);
|
|
158
|
+
appendTimestampedBullets(soulPath, 'Failures & Lessons', reflection.failuresLessons, stampDay);
|
|
159
|
+
appendTimestampedBullets(soulPath, 'Successful Workflows', reflection.successfulWorkflows, stampDay);
|
|
160
|
+
appendTimestampedBullets(soulPath, 'Adaptations', reflection.whatToDoDifferently, stampDay);
|
|
161
|
+
|
|
162
|
+
appendHuman2Update(human2Path, reflection, stampIso);
|
|
163
|
+
|
|
164
|
+
const memoryPayload = [
|
|
165
|
+
reflection.summary ? `Summary: ${reflection.summary}` : '',
|
|
166
|
+
...reflection.patternsObserved.map((v) => `Pattern: ${v}`),
|
|
167
|
+
...reflection.failuresLessons.map((v) => `Lesson: ${v}`),
|
|
168
|
+
...reflection.successfulWorkflows.map((v) => `Workflow: ${v}`),
|
|
169
|
+
...reflection.preferenceUpdates.map((v) => `Preference: ${v}`),
|
|
170
|
+
...reflection.whatToDoDifferently.map((v) => `Adaptation: ${v}`)
|
|
171
|
+
]
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
.join('\n');
|
|
174
|
+
|
|
175
|
+
if (memoryPayload) {
|
|
176
|
+
let emb = [];
|
|
177
|
+
if (embeddingsEnabled) {
|
|
178
|
+
try {
|
|
179
|
+
emb = await embedText(memoryPayload);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
emb = [];
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
addMemory('global', 'self_reflection', memoryPayload, emb);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
setMeta(REFLECTION_META_KEY, startedAt);
|
|
188
|
+
return { ok: true, skipped: false, at: stampIso };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
maybeRunReflectionCycle
|
|
193
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const { reflectionUpdateHours } = require('../config');
|
|
2
|
+
const { maybeRunReflectionCycle } = require('./reflectionAgent');
|
|
3
|
+
|
|
4
|
+
let intervalHandle = null;
|
|
5
|
+
|
|
6
|
+
function startReflectionScheduler() {
|
|
7
|
+
if (intervalHandle) return;
|
|
8
|
+
|
|
9
|
+
const everyMs = Math.max(1, Number(reflectionUpdateHours || 12)) * 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
// Kick one asynchronous check on startup.
|
|
12
|
+
maybeRunReflectionCycle().catch(() => {});
|
|
13
|
+
|
|
14
|
+
intervalHandle = setInterval(() => {
|
|
15
|
+
maybeRunReflectionCycle().catch(() => {});
|
|
16
|
+
}, everyMs);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
startReflectionScheduler
|
|
21
|
+
};
|