agent-sin 0.1.11 → 0.1.15
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/CHANGELOG.md +79 -0
- package/README.md +2 -1
- package/builtin-skills/_shared/_todo_lib.py +290 -0
- package/builtin-skills/even-g2-setup/main.ts +896 -0
- package/builtin-skills/even-g2-setup/skill.yaml +133 -0
- package/builtin-skills/memo-delete/main.py +28 -107
- package/builtin-skills/memo-delete/skill.yaml +10 -21
- package/builtin-skills/memo-index/main.py +96 -64
- package/builtin-skills/memo-index/skill.yaml +4 -10
- package/builtin-skills/memo-list/main.py +179 -0
- package/builtin-skills/memo-list/skill.yaml +51 -0
- package/builtin-skills/memo-save/main.py +191 -25
- package/builtin-skills/memo-save/skill.yaml +29 -5
- package/builtin-skills/memo-search/main.py +38 -18
- package/builtin-skills/memo-vector-search/main.py +11 -6
- package/builtin-skills/nightly-topic-knowledge/_feedback_lib.py +391 -0
- package/builtin-skills/nightly-topic-knowledge/_topics_lib.py +415 -0
- package/builtin-skills/nightly-topic-knowledge/main.py +403 -0
- package/builtin-skills/nightly-topic-knowledge/skill.yaml +88 -0
- package/builtin-skills/schedule-add/main.py +26 -0
- package/builtin-skills/service-restart/main.ts +249 -0
- package/builtin-skills/service-restart/skill.yaml +49 -0
- package/builtin-skills/todo-add/main.py +3 -1
- package/builtin-skills/todo-delete/main.py +3 -1
- package/builtin-skills/todo-done/main.py +3 -1
- package/builtin-skills/todo-list/main.py +4 -1
- package/builtin-skills/todo-tick/main.py +3 -1
- package/builtin-skills/topic-knowledge-read/main.py +118 -0
- package/builtin-skills/topic-knowledge-read/skill.yaml +49 -0
- package/dist/builder/build-action-classifier.d.ts +18 -0
- package/dist/builder/build-action-classifier.js +82 -1
- package/dist/builder/build-flow.d.ts +33 -4
- package/dist/builder/build-flow.js +251 -89
- package/dist/builder/builder-session.d.ts +1 -1
- package/dist/builder/builder-session.js +112 -7
- package/dist/builder/conversation-router.d.ts +4 -2
- package/dist/builder/conversation-router.js +19 -2
- package/dist/cli/index.js +323 -20
- package/dist/core/ai-provider.d.ts +1 -0
- package/dist/core/ai-provider.js +8 -3
- package/dist/core/chat-engine.d.ts +10 -3
- package/dist/core/chat-engine.js +1563 -197
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +82 -0
- package/dist/core/daily-memory-promotion.d.ts +7 -0
- package/dist/core/daily-memory-promotion.js +568 -14
- package/dist/core/image-attachments.d.ts +31 -0
- package/dist/core/image-attachments.js +237 -0
- package/dist/core/logger.d.ts +2 -1
- package/dist/core/logger.js +77 -1
- package/dist/core/memo-migration.d.ts +3 -0
- package/dist/core/memo-migration.js +422 -0
- package/dist/core/native-modules.d.ts +24 -0
- package/dist/core/native-modules.js +99 -0
- package/dist/core/notifier.d.ts +8 -3
- package/dist/core/notifier.js +191 -17
- package/dist/core/obsidian-vault.d.ts +19 -0
- package/dist/core/obsidian-vault.js +477 -0
- package/dist/core/operating-model.d.ts +2 -0
- package/dist/core/operating-model.js +15 -0
- package/dist/core/output-writer.d.ts +3 -2
- package/dist/core/output-writer.js +108 -7
- package/dist/core/profile-memory.js +22 -1
- package/dist/core/runtime.d.ts +2 -0
- package/dist/core/runtime.js +9 -1
- package/dist/core/secrets.d.ts +4 -0
- package/dist/core/secrets.js +34 -0
- package/dist/core/skill-history.d.ts +44 -0
- package/dist/core/skill-history.js +329 -0
- package/dist/core/skill-registry.d.ts +5 -0
- package/dist/core/skill-registry.js +11 -0
- package/dist/discord/bot.d.ts +13 -0
- package/dist/discord/bot.js +542 -10
- package/dist/even-g2/gateway.d.ts +15 -0
- package/dist/even-g2/gateway.js +868 -0
- package/dist/runtimes/codex-app-server.d.ts +5 -1
- package/dist/runtimes/codex-app-server.js +147 -8
- package/dist/runtimes/python-runner.js +82 -0
- package/dist/runtimes/typescript-runner.js +13 -1
- package/dist/skills-sdk/types.d.ts +19 -4
- package/dist/telegram/bot.d.ts +1 -0
- package/dist/telegram/bot.js +122 -31
- package/package.json +3 -1
- package/templates/even-g2-agent/README.md +83 -0
- package/templates/even-g2-agent/app.json +20 -0
- package/templates/even-g2-agent/index.html +31 -0
- package/templates/even-g2-agent/package-lock.json +1836 -0
- package/templates/even-g2-agent/package.json +22 -0
- package/templates/even-g2-agent/scripts/qr-auto.mjs +182 -0
- package/templates/even-g2-agent/src/embedded-config.ts +4 -0
- package/templates/even-g2-agent/src/main.ts +539 -0
- package/templates/even-g2-agent/src/style.css +70 -0
- package/templates/even-g2-agent/tsconfig.json +11 -0
- package/templates/skill-python/main.py +20 -2
- package/templates/skill-python/skill.yaml +9 -0
- package/templates/skill-typescript/main.ts +40 -5
- package/templates/skill-typescript/skill.yaml +9 -0
|
@@ -7,8 +7,17 @@ import { appendProfileMemory, profileMemoryPath, readProfileMemoryFiles, } from
|
|
|
7
7
|
import { l } from "./i18n.js";
|
|
8
8
|
const MAX_DAILY_CHARS = 24000;
|
|
9
9
|
const MAX_MEMORY_CHARS = 12000;
|
|
10
|
-
const MAX_PROMOTION_ITEMS =
|
|
10
|
+
const MAX_PROMOTION_ITEMS = 5;
|
|
11
11
|
const MAX_ITEM_CHARS = 500;
|
|
12
|
+
const RECENT_TOPIC_DAYS = 7;
|
|
13
|
+
const MAX_RECENT_TOPIC_ITEMS = 50;
|
|
14
|
+
const MAX_RECENT_TOPIC_CHARS = 180;
|
|
15
|
+
const MAX_RECENT_TOPIC_DAILY_CHARS = 5000;
|
|
16
|
+
const MAX_RECENT_TOPIC_INPUT_CHARS = 36000;
|
|
17
|
+
const MAX_MEMORY_DIFF_CHARS = 12000;
|
|
18
|
+
const MAX_RECENT_TOPIC_USER_ENTRY_CHARS = 360;
|
|
19
|
+
const MAX_RECENT_TOPIC_ASSISTANT_ENTRY_CHARS = 420;
|
|
20
|
+
const MAX_RECENT_TOPIC_TOOL_ENTRY_CHARS = 260;
|
|
12
21
|
export async function maybePromoteDailyMemory(config, options = {}) {
|
|
13
22
|
let result;
|
|
14
23
|
try {
|
|
@@ -68,9 +77,12 @@ export async function promoteDailyMemory(config, options = {}) {
|
|
|
68
77
|
};
|
|
69
78
|
}
|
|
70
79
|
const profile = await readProfileMemoryFiles(config);
|
|
80
|
+
const memoryFile = profileMemoryPath(config, "memory");
|
|
81
|
+
const memoryBefore = await readTextIfExists(memoryFile);
|
|
82
|
+
const promotionDaily = dailyMemoryPromotionSource(daily);
|
|
71
83
|
const response = await requestPromotionItems(config, {
|
|
72
84
|
date,
|
|
73
|
-
daily,
|
|
85
|
+
daily: promotionDaily,
|
|
74
86
|
existingMemory: profile.memory,
|
|
75
87
|
modelId: options.modelId || config.chat_model_id,
|
|
76
88
|
});
|
|
@@ -88,6 +100,13 @@ export async function promoteDailyMemory(config, options = {}) {
|
|
|
88
100
|
await appendProfileMemory(config, "memory", formatPromotedMemoryEntry(date, items), options.now || new Date());
|
|
89
101
|
await consolidateMemoryFile(config, options.modelId || config.chat_model_id, options.eventSource || "chat");
|
|
90
102
|
}
|
|
103
|
+
if (!options.dryRun) {
|
|
104
|
+
await refreshRecentTopicsSection(config, {
|
|
105
|
+
endDate: date,
|
|
106
|
+
modelId: options.modelId || config.chat_model_id,
|
|
107
|
+
eventSource: options.eventSource || "chat",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
91
110
|
if (!options.dryRun) {
|
|
92
111
|
state.dates[date] = {
|
|
93
112
|
hash,
|
|
@@ -97,6 +116,17 @@ export async function promoteDailyMemory(config, options = {}) {
|
|
|
97
116
|
};
|
|
98
117
|
await writePromotionState(config, state);
|
|
99
118
|
}
|
|
119
|
+
if (!options.dryRun) {
|
|
120
|
+
await appendMemoryPromotionDiffLog(config, {
|
|
121
|
+
date,
|
|
122
|
+
file: memoryFile,
|
|
123
|
+
before: memoryBefore,
|
|
124
|
+
after: await readTextIfExists(memoryFile),
|
|
125
|
+
items,
|
|
126
|
+
status: items.length > 0 ? "promoted" : "reviewed",
|
|
127
|
+
eventSource: options.eventSource || "chat",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
100
130
|
return {
|
|
101
131
|
status: items.length > 0 ? "promoted" : "reviewed",
|
|
102
132
|
date,
|
|
@@ -105,6 +135,116 @@ export async function promoteDailyMemory(config, options = {}) {
|
|
|
105
135
|
message: items.length > 0 ? `promoted ${items.length} item(s) from ${date}` : `no long-term memory items: ${date}`,
|
|
106
136
|
};
|
|
107
137
|
}
|
|
138
|
+
async function appendMemoryPromotionDiffLog(config, input) {
|
|
139
|
+
const diff = formatMemoryDiff(input.before, input.after);
|
|
140
|
+
await appendEventLog(config, {
|
|
141
|
+
level: "info",
|
|
142
|
+
source: input.eventSource,
|
|
143
|
+
event: "memory_promotion_diff",
|
|
144
|
+
message: diff
|
|
145
|
+
? `memory.md diff recorded for ${input.date}`
|
|
146
|
+
: `memory.md unchanged for ${input.date}`,
|
|
147
|
+
details: {
|
|
148
|
+
date: input.date,
|
|
149
|
+
status: input.status,
|
|
150
|
+
file: input.file,
|
|
151
|
+
promoted_items: input.items,
|
|
152
|
+
changed: diff.length > 0,
|
|
153
|
+
diff,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
function formatMemoryDiff(before, after) {
|
|
158
|
+
if (normalizeForCompare(before) === normalizeForCompare(after)) {
|
|
159
|
+
return "";
|
|
160
|
+
}
|
|
161
|
+
const raw = unifiedLineDiff(before, after);
|
|
162
|
+
return raw.length <= MAX_MEMORY_DIFF_CHARS
|
|
163
|
+
? raw
|
|
164
|
+
: `${raw.slice(0, MAX_MEMORY_DIFF_CHARS)}\n... diff truncated ...`;
|
|
165
|
+
}
|
|
166
|
+
function unifiedLineDiff(before, after) {
|
|
167
|
+
const beforeLines = before.split(/\r?\n/);
|
|
168
|
+
const afterLines = after.split(/\r?\n/);
|
|
169
|
+
const operations = lineDiffOperations(beforeLines, afterLines);
|
|
170
|
+
return [
|
|
171
|
+
"--- memory.md before",
|
|
172
|
+
"+++ memory.md after",
|
|
173
|
+
"@@",
|
|
174
|
+
...operations.map((op) => `${op.kind}${op.text}`),
|
|
175
|
+
].join("\n");
|
|
176
|
+
}
|
|
177
|
+
function lineDiffOperations(beforeLines, afterLines) {
|
|
178
|
+
const rows = beforeLines.length + 1;
|
|
179
|
+
const cols = afterLines.length + 1;
|
|
180
|
+
if (rows * cols > 20000) {
|
|
181
|
+
return [
|
|
182
|
+
{ kind: "-", text: `<${beforeLines.length} lines before>` },
|
|
183
|
+
{ kind: "+", text: `<${afterLines.length} lines after>` },
|
|
184
|
+
];
|
|
185
|
+
}
|
|
186
|
+
const dp = Array.from({ length: rows }, () => Array(cols).fill(0));
|
|
187
|
+
for (let i = beforeLines.length - 1; i >= 0; i -= 1) {
|
|
188
|
+
for (let j = afterLines.length - 1; j >= 0; j -= 1) {
|
|
189
|
+
dp[i][j] = beforeLines[i] === afterLines[j]
|
|
190
|
+
? dp[i + 1][j + 1] + 1
|
|
191
|
+
: Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const operations = [];
|
|
195
|
+
let i = 0;
|
|
196
|
+
let j = 0;
|
|
197
|
+
while (i < beforeLines.length && j < afterLines.length) {
|
|
198
|
+
if (beforeLines[i] === afterLines[j]) {
|
|
199
|
+
operations.push({ kind: " ", text: beforeLines[i] });
|
|
200
|
+
i += 1;
|
|
201
|
+
j += 1;
|
|
202
|
+
}
|
|
203
|
+
else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
204
|
+
operations.push({ kind: "-", text: beforeLines[i] });
|
|
205
|
+
i += 1;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
operations.push({ kind: "+", text: afterLines[j] });
|
|
209
|
+
j += 1;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
while (i < beforeLines.length) {
|
|
213
|
+
operations.push({ kind: "-", text: beforeLines[i] });
|
|
214
|
+
i += 1;
|
|
215
|
+
}
|
|
216
|
+
while (j < afterLines.length) {
|
|
217
|
+
operations.push({ kind: "+", text: afterLines[j] });
|
|
218
|
+
j += 1;
|
|
219
|
+
}
|
|
220
|
+
return compactDiffContext(operations);
|
|
221
|
+
}
|
|
222
|
+
function compactDiffContext(operations) {
|
|
223
|
+
const keep = new Set();
|
|
224
|
+
for (let i = 0; i < operations.length; i += 1) {
|
|
225
|
+
if (operations[i].kind === " ") {
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
for (let j = Math.max(0, i - 2); j <= Math.min(operations.length - 1, i + 2); j += 1) {
|
|
229
|
+
keep.add(j);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const compacted = [];
|
|
233
|
+
let skipped = false;
|
|
234
|
+
for (let i = 0; i < operations.length; i += 1) {
|
|
235
|
+
if (keep.has(i)) {
|
|
236
|
+
if (skipped) {
|
|
237
|
+
compacted.push({ kind: " ", text: "..." });
|
|
238
|
+
skipped = false;
|
|
239
|
+
}
|
|
240
|
+
compacted.push(operations[i]);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
skipped = true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return compacted;
|
|
247
|
+
}
|
|
108
248
|
export function parseDailyMemoryPromotionResponse(text) {
|
|
109
249
|
const jsonText = extractJson(text);
|
|
110
250
|
let parsed;
|
|
@@ -135,6 +275,26 @@ export function parseDailyMemoryPromotionResponse(text) {
|
|
|
135
275
|
.filter(Boolean)
|
|
136
276
|
.slice(0, MAX_PROMOTION_ITEMS);
|
|
137
277
|
}
|
|
278
|
+
function dailyMemoryPromotionSource(daily) {
|
|
279
|
+
const note = [
|
|
280
|
+
"<!-- Promotion note:",
|
|
281
|
+
"Assistant and tool entries are included for context.",
|
|
282
|
+
"Promote a long-term fact only when it is supported by the target-date exchange, preferably by direct user wording.",
|
|
283
|
+
"Do not promote details that appear only because the assistant recalled or summarized an earlier date.",
|
|
284
|
+
"-->",
|
|
285
|
+
].join("\n");
|
|
286
|
+
if (!daily.trim()) {
|
|
287
|
+
return daily;
|
|
288
|
+
}
|
|
289
|
+
const lines = daily.split(/\r?\n/);
|
|
290
|
+
const titleIndex = lines.findIndex((line) => /^#\s+\S/.test(line.trim()));
|
|
291
|
+
if (titleIndex < 0) {
|
|
292
|
+
return `${note}\n\n${daily}`;
|
|
293
|
+
}
|
|
294
|
+
const next = [...lines];
|
|
295
|
+
next.splice(titleIndex + 1, 0, "", note);
|
|
296
|
+
return next.join("\n");
|
|
297
|
+
}
|
|
138
298
|
async function requestPromotionItems(config, input) {
|
|
139
299
|
try {
|
|
140
300
|
const response = await getAiProvider()(config, {
|
|
@@ -145,7 +305,10 @@ async function requestPromotionItems(config, input) {
|
|
|
145
305
|
role: "system",
|
|
146
306
|
content: [
|
|
147
307
|
"You are Agent-Sin's long-term memory curator. memory.md is a long-term note used to understand the user deeply and keep conversations smooth.",
|
|
148
|
-
"Promote
|
|
308
|
+
"Promote observations from daily conversation memory that help understand the user across future conversations. Lean toward promoting when an observation is reasonably supported by user wording; skip only when truly unclear or noisy. Maximum 5 items, zero is fine.",
|
|
309
|
+
"Assistant responses may be used as context, but do not promote a fact whose only support is an assistant response, tool output, or system context.",
|
|
310
|
+
"A promoted fact must be grounded in the target-date exchange. Prefer direct user wording; assistant summaries are only supporting context.",
|
|
311
|
+
"If the user asks whether you remember a previous conversation, do not treat the assistant's recall as a new fact for the target date.",
|
|
149
312
|
"Write each item in the same language as the source daily-conversation.md content.",
|
|
150
313
|
"Keep only items directly about the user:",
|
|
151
314
|
"- The user's role, experience, interests, and personal context",
|
|
@@ -198,15 +361,382 @@ function formatPromotedMemoryEntry(date, items) {
|
|
|
198
361
|
...items.map((item) => `- ${item}`),
|
|
199
362
|
].join("\n");
|
|
200
363
|
}
|
|
364
|
+
export async function refreshRecentTopicsSection(config, input) {
|
|
365
|
+
try {
|
|
366
|
+
const targetDate = input.endDate;
|
|
367
|
+
const targetDaily = await readTextIfExists(dailyMemoryFileForDate(config, targetDate));
|
|
368
|
+
const dailyHasContent = !!targetDaily.replace(/^# .+$/m, "").trim();
|
|
369
|
+
const file = profileMemoryPath(config, "memory");
|
|
370
|
+
const original = await readTextIfExists(file);
|
|
371
|
+
const existingTopics = parseDatedTopicsFromSection(original, targetDate);
|
|
372
|
+
const prunedExisting = pruneStaleTopics(existingTopics, targetDate, RECENT_TOPIC_DAYS);
|
|
373
|
+
let mergedTopics = prunedExisting;
|
|
374
|
+
let appended = 0;
|
|
375
|
+
if (dailyHasContent) {
|
|
376
|
+
const compacted = compactDailyMemoryForRecentTopics(targetDaily);
|
|
377
|
+
const newTopics = await requestRecentTopicsForDate(config, {
|
|
378
|
+
targetDate,
|
|
379
|
+
dailyContent: compacted,
|
|
380
|
+
existingTopics: prunedExisting,
|
|
381
|
+
modelId: input.modelId,
|
|
382
|
+
});
|
|
383
|
+
appended = newTopics.length;
|
|
384
|
+
mergedTopics = mergeDatedTopics(prunedExisting, newTopics, targetDate);
|
|
385
|
+
}
|
|
386
|
+
if (mergedTopics.length === 0 && existingTopics.length === 0) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
const next = upsertRecentTopicsSection(original, targetDate, mergedTopics);
|
|
390
|
+
if (normalizeForCompare(next) === normalizeForCompare(original)) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
await writeFile(file, next, "utf8");
|
|
394
|
+
await appendEventLog(config, {
|
|
395
|
+
level: "info",
|
|
396
|
+
source: input.eventSource,
|
|
397
|
+
event: "recent_topics_refreshed",
|
|
398
|
+
message: `recent topics refreshed through ${targetDate}`,
|
|
399
|
+
details: {
|
|
400
|
+
file,
|
|
401
|
+
end_date: targetDate,
|
|
402
|
+
items: mergedTopics.length,
|
|
403
|
+
appended,
|
|
404
|
+
pruned: existingTopics.length - prunedExisting.length,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
catch (error) {
|
|
409
|
+
await appendEventLog(config, {
|
|
410
|
+
level: "warn",
|
|
411
|
+
source: input.eventSource,
|
|
412
|
+
event: "recent_topics_refresh_failed",
|
|
413
|
+
message: error instanceof Error ? error.message : String(error),
|
|
414
|
+
details: { end_date: input.endDate },
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function parseDatedTopicsFromSection(original, fallbackDate) {
|
|
419
|
+
const lines = original.split(/\r?\n/);
|
|
420
|
+
const start = lines.findIndex((line) => recentTopicsHeading(line));
|
|
421
|
+
if (start < 0) {
|
|
422
|
+
return [];
|
|
423
|
+
}
|
|
424
|
+
let end = start + 1;
|
|
425
|
+
while (end < lines.length && !/^#{1,6}\s+\S/.test(lines[end].trim())) {
|
|
426
|
+
end += 1;
|
|
427
|
+
}
|
|
428
|
+
const body = lines.slice(start + 1, end);
|
|
429
|
+
const topics = [];
|
|
430
|
+
const seen = new Set();
|
|
431
|
+
for (const raw of body) {
|
|
432
|
+
const line = raw.replace(/^[-*・]\s*/, "").trim();
|
|
433
|
+
if (!line || line.startsWith("<!--"))
|
|
434
|
+
continue;
|
|
435
|
+
const dated = line.match(/^(\d{4}-\d{2}-\d{2})\s*[::]\s*(.+)$/);
|
|
436
|
+
const date = dated ? dated[1] : fallbackDate;
|
|
437
|
+
const text = cleanRecentTopic(dated ? dated[2] : line);
|
|
438
|
+
if (!text)
|
|
439
|
+
continue;
|
|
440
|
+
const key = normalizeForDedupe(text);
|
|
441
|
+
if (!key || seen.has(key))
|
|
442
|
+
continue;
|
|
443
|
+
seen.add(key);
|
|
444
|
+
topics.push({ date, text });
|
|
445
|
+
}
|
|
446
|
+
return topics;
|
|
447
|
+
}
|
|
448
|
+
function pruneStaleTopics(topics, targetDate, days) {
|
|
449
|
+
const cutoff = addDays(dateFromLocalDateString(targetDate), -(days - 1));
|
|
450
|
+
const cutoffKey = localDateString(cutoff);
|
|
451
|
+
return topics.filter((topic) => topic.date >= cutoffKey);
|
|
452
|
+
}
|
|
453
|
+
function mergeDatedTopics(existing, incoming, targetDate) {
|
|
454
|
+
const byKey = new Map();
|
|
455
|
+
const order = [];
|
|
456
|
+
for (const topic of existing) {
|
|
457
|
+
const key = normalizeForDedupe(topic.text);
|
|
458
|
+
if (!key || byKey.has(key))
|
|
459
|
+
continue;
|
|
460
|
+
byKey.set(key, topic);
|
|
461
|
+
order.push(key);
|
|
462
|
+
}
|
|
463
|
+
for (const topic of incoming) {
|
|
464
|
+
const key = normalizeForDedupe(topic.text);
|
|
465
|
+
if (!key || byKey.has(key))
|
|
466
|
+
continue;
|
|
467
|
+
byKey.set(key, { date: topic.date || targetDate, text: topic.text });
|
|
468
|
+
order.push(key);
|
|
469
|
+
}
|
|
470
|
+
const merged = order.map((key) => byKey.get(key)).filter(Boolean);
|
|
471
|
+
merged.sort((a, b) => (a.date === b.date ? 0 : a.date < b.date ? 1 : -1));
|
|
472
|
+
return merged.slice(0, MAX_RECENT_TOPIC_ITEMS);
|
|
473
|
+
}
|
|
474
|
+
async function requestRecentTopicsForDate(config, input) {
|
|
475
|
+
const existingBlock = input.existingTopics.length > 0
|
|
476
|
+
? input.existingTopics.map((topic) => `- ${topic.date}: ${topic.text}`).join("\n")
|
|
477
|
+
: "(none)";
|
|
478
|
+
const response = await getAiProvider()(config, {
|
|
479
|
+
model_id: input.modelId,
|
|
480
|
+
temperature: 0,
|
|
481
|
+
messages: [
|
|
482
|
+
{
|
|
483
|
+
role: "system",
|
|
484
|
+
content: [
|
|
485
|
+
"You extract recent conversation topics for one day, to append into Agent-Sin's short-lived 'Recent 7-day topics' section inside memory.md.",
|
|
486
|
+
"Each output topic represents a topic that was active in the target-date conversation. Older topics from previous days are already kept by the system; do not restate them unless they were genuinely revisited today.",
|
|
487
|
+
"Capture what the user is working on, investigating, deciding, playing, tracking, or discussing — including work/project topics and notable personal, hobby, health, media, and life-context topics.",
|
|
488
|
+
"Keep concrete project, product, game, and skill names when useful. Merge duplicates within the day. Maximum 5 new topics.",
|
|
489
|
+
"Each topic: one concise sentence, in the same language as the source.",
|
|
490
|
+
"Do not include secrets, tokens, raw logs, private identifiers, URL lists, or sensitive facts about other people.",
|
|
491
|
+
"Avoid detailed private finance or family facts; if such a thread matters, keep it broad and non-identifying.",
|
|
492
|
+
"Do not restate stable preferences unless they were an active topic today.",
|
|
493
|
+
'Output JSON only: {"topics":[{"text":"..."}]}. If nothing qualifies, return {"topics":[]}.',
|
|
494
|
+
].join("\n"),
|
|
495
|
+
},
|
|
496
|
+
{
|
|
497
|
+
role: "user",
|
|
498
|
+
content: [
|
|
499
|
+
`Target date: ${input.targetDate}`,
|
|
500
|
+
"",
|
|
501
|
+
"<existing-recent-topics>",
|
|
502
|
+
existingBlock,
|
|
503
|
+
"</existing-recent-topics>",
|
|
504
|
+
"",
|
|
505
|
+
`<daily-conversation.md date="${input.targetDate}">`,
|
|
506
|
+
input.dailyContent,
|
|
507
|
+
`</daily-conversation.md>`,
|
|
508
|
+
].join("\n").slice(0, MAX_RECENT_TOPIC_INPUT_CHARS),
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
});
|
|
512
|
+
const texts = parseRecentTopicsResponse(response.text);
|
|
513
|
+
return texts.map((text) => ({ date: input.targetDate, text }));
|
|
514
|
+
}
|
|
515
|
+
function compactDailyMemoryForRecentTopics(raw) {
|
|
516
|
+
const parsed = parseDailyMemoryEntries(raw);
|
|
517
|
+
if (parsed.entries.length === 0) {
|
|
518
|
+
return clipAround(raw, MAX_RECENT_TOPIC_DAILY_CHARS);
|
|
519
|
+
}
|
|
520
|
+
const userEntries = parsed.entries
|
|
521
|
+
.filter((entry) => entry.role === "user")
|
|
522
|
+
.map((entry) => formatRecentTopicEntry(entry, MAX_RECENT_TOPIC_USER_ENTRY_CHARS))
|
|
523
|
+
.filter(Boolean);
|
|
524
|
+
const contextEntries = parsed.entries
|
|
525
|
+
.filter((entry) => entry.role === "assistant" || entry.role === "tool")
|
|
526
|
+
.map((entry) => formatRecentTopicEntry(entry, entry.role === "tool" ? MAX_RECENT_TOPIC_TOOL_ENTRY_CHARS : MAX_RECENT_TOPIC_ASSISTANT_ENTRY_CHARS))
|
|
527
|
+
.filter(Boolean);
|
|
528
|
+
const header = [
|
|
529
|
+
parsed.title,
|
|
530
|
+
"",
|
|
531
|
+
"<!-- Compacted for recent topic extraction. Direct user entries are prioritized; assistant/tool entries are included as context when budget allows. -->",
|
|
532
|
+
"",
|
|
533
|
+
].join("\n");
|
|
534
|
+
const directSection = userEntries.length > 0
|
|
535
|
+
? ["## Direct user entries", "", userEntries.join("\n\n")].join("\n")
|
|
536
|
+
: "## Direct user entries\n\n(no direct user entries)";
|
|
537
|
+
const contextSection = contextEntries.length > 0
|
|
538
|
+
? ["## Assistant/tool context", "", contextEntries.join("\n\n")].join("\n")
|
|
539
|
+
: "";
|
|
540
|
+
const directOnly = `${header}${directSection}`.trimEnd();
|
|
541
|
+
if (directOnly.length >= MAX_RECENT_TOPIC_DAILY_CHARS || !contextSection) {
|
|
542
|
+
return `${clipAround(directOnly, MAX_RECENT_TOPIC_DAILY_CHARS)}\n`;
|
|
543
|
+
}
|
|
544
|
+
const remaining = MAX_RECENT_TOPIC_DAILY_CHARS - directOnly.length - 2;
|
|
545
|
+
return `${directOnly}\n\n${clipAround(contextSection, Math.max(0, remaining))}\n`;
|
|
546
|
+
}
|
|
547
|
+
function parseDailyMemoryEntries(raw) {
|
|
548
|
+
const lines = raw.split(/\r?\n/);
|
|
549
|
+
const title = lines.find((line) => /^#\s+\S/.test(line.trim())) || "# Daily conversation memory";
|
|
550
|
+
const entries = [];
|
|
551
|
+
let current = null;
|
|
552
|
+
for (const line of lines) {
|
|
553
|
+
const heading = line.match(/^##\s+(.+?)\s*$/);
|
|
554
|
+
if (heading) {
|
|
555
|
+
if (current) {
|
|
556
|
+
entries.push({
|
|
557
|
+
heading: current.heading,
|
|
558
|
+
role: dailyMemoryHeadingRole(current.heading),
|
|
559
|
+
body: current.body.join("\n").trim(),
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
current = { heading: heading[1].trim(), body: [] };
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (current) {
|
|
566
|
+
current.body.push(line);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (current) {
|
|
570
|
+
entries.push({
|
|
571
|
+
heading: current.heading,
|
|
572
|
+
role: dailyMemoryHeadingRole(current.heading),
|
|
573
|
+
body: current.body.join("\n").trim(),
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
return { title, entries };
|
|
577
|
+
}
|
|
578
|
+
function dailyMemoryHeadingRole(heading) {
|
|
579
|
+
const match = heading.trim().match(/\b(system|user|assistant|tool)\s*$/);
|
|
580
|
+
return match?.[1] || "";
|
|
581
|
+
}
|
|
582
|
+
function formatRecentTopicEntry(entry, maxBodyChars) {
|
|
583
|
+
const body = normalizeRecentTopicEntryBody(entry.role, entry.body);
|
|
584
|
+
if (!body) {
|
|
585
|
+
return "";
|
|
586
|
+
}
|
|
587
|
+
return [`### ${entry.heading}`, clipAround(body, maxBodyChars)].join("\n\n");
|
|
588
|
+
}
|
|
589
|
+
function normalizeRecentTopicEntryBody(role, body) {
|
|
590
|
+
const trimmed = body.trim();
|
|
591
|
+
if (!trimmed) {
|
|
592
|
+
return "";
|
|
593
|
+
}
|
|
594
|
+
if (role === "assistant") {
|
|
595
|
+
const parsed = parseJsonObject(trimmed);
|
|
596
|
+
if (parsed) {
|
|
597
|
+
return normalizeAssistantEnvelope(parsed) || scrubRecentTopicText(trimmed);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
if (role === "tool") {
|
|
601
|
+
const fenced = trimmed.match(/```skill-result\s*([\s\S]*?)```/i);
|
|
602
|
+
const parsed = parseJsonObject(fenced?.[1]?.trim() || trimmed);
|
|
603
|
+
if (parsed) {
|
|
604
|
+
const summary = parsed.summary;
|
|
605
|
+
return scrubRecentTopicText(typeof summary === "string" ? summary : trimmed);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return scrubRecentTopicText(trimmed);
|
|
609
|
+
}
|
|
610
|
+
function normalizeAssistantEnvelope(value) {
|
|
611
|
+
const parts = [];
|
|
612
|
+
if (typeof value.reply === "string" && value.reply.trim()) {
|
|
613
|
+
parts.push(value.reply.trim());
|
|
614
|
+
}
|
|
615
|
+
const calls = Array.isArray(value.skill_calls) ? value.skill_calls : [];
|
|
616
|
+
const callTexts = calls
|
|
617
|
+
.map((call) => {
|
|
618
|
+
if (!call || typeof call !== "object")
|
|
619
|
+
return "";
|
|
620
|
+
const record = call;
|
|
621
|
+
const id = typeof record.id === "string" ? record.id : "";
|
|
622
|
+
const args = record.args && typeof record.args === "object" ? record.args : {};
|
|
623
|
+
const text = typeof args.text === "string" ? args.text : "";
|
|
624
|
+
return [id, text].filter(Boolean).join(": ");
|
|
625
|
+
})
|
|
626
|
+
.filter(Boolean);
|
|
627
|
+
if (callTexts.length > 0) {
|
|
628
|
+
parts.push(`Skill calls: ${callTexts.join("; ")}`);
|
|
629
|
+
}
|
|
630
|
+
return scrubRecentTopicText(parts.join("\n\n"));
|
|
631
|
+
}
|
|
632
|
+
function parseJsonObject(text) {
|
|
633
|
+
try {
|
|
634
|
+
const parsed = JSON.parse(text);
|
|
635
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
636
|
+
? parsed
|
|
637
|
+
: null;
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function scrubRecentTopicText(text) {
|
|
644
|
+
return text
|
|
645
|
+
.replace(/\/Users\/[^\s)]+/g, "[local path]")
|
|
646
|
+
.replace(/\s+\n/g, "\n")
|
|
647
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
648
|
+
.trim();
|
|
649
|
+
}
|
|
650
|
+
export function parseRecentTopicsResponse(text) {
|
|
651
|
+
const jsonText = extractJson(text);
|
|
652
|
+
let parsed;
|
|
653
|
+
try {
|
|
654
|
+
parsed = JSON.parse(jsonText);
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
return [];
|
|
658
|
+
}
|
|
659
|
+
const rawItems = Array.isArray(parsed)
|
|
660
|
+
? parsed
|
|
661
|
+
: parsed && typeof parsed === "object" && Array.isArray(parsed.topics)
|
|
662
|
+
? parsed.topics
|
|
663
|
+
: parsed && typeof parsed === "object" && Array.isArray(parsed.items)
|
|
664
|
+
? parsed.items
|
|
665
|
+
: [];
|
|
666
|
+
const seen = new Set();
|
|
667
|
+
const topics = [];
|
|
668
|
+
for (const item of rawItems) {
|
|
669
|
+
const textValue = topicText(item);
|
|
670
|
+
const cleaned = cleanRecentTopic(textValue);
|
|
671
|
+
const key = normalizeForDedupe(cleaned);
|
|
672
|
+
if (!cleaned || !key || seen.has(key)) {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
seen.add(key);
|
|
676
|
+
topics.push(cleaned);
|
|
677
|
+
if (topics.length >= MAX_RECENT_TOPIC_ITEMS) {
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return topics;
|
|
682
|
+
}
|
|
683
|
+
function topicText(item) {
|
|
684
|
+
if (typeof item === "string") {
|
|
685
|
+
return item;
|
|
686
|
+
}
|
|
687
|
+
if (item && typeof item === "object") {
|
|
688
|
+
const record = item;
|
|
689
|
+
const value = record.text || record.topic || record.summary || record.content;
|
|
690
|
+
return typeof value === "string" ? value : "";
|
|
691
|
+
}
|
|
692
|
+
return "";
|
|
693
|
+
}
|
|
694
|
+
function cleanRecentTopic(text) {
|
|
695
|
+
return text
|
|
696
|
+
.replace(/\s+/g, " ")
|
|
697
|
+
.replace(/^[-*・]\s*/, "")
|
|
698
|
+
.trim()
|
|
699
|
+
.slice(0, MAX_RECENT_TOPIC_CHARS);
|
|
700
|
+
}
|
|
701
|
+
function upsertRecentTopicsSection(original, endDate, topics) {
|
|
702
|
+
const base = removeRecentTopicsSection(original).replace(/\s+$/, "");
|
|
703
|
+
const section = formatRecentTopicsSection(endDate, topics);
|
|
704
|
+
return `${base}\n\n${section}\n`;
|
|
705
|
+
}
|
|
706
|
+
function formatRecentTopicsSection(endDate, topics) {
|
|
707
|
+
return [
|
|
708
|
+
l("## Recent 7-day topics", "## 直近1週間のトピック"),
|
|
709
|
+
"",
|
|
710
|
+
`<!-- ${l(`Short-lived context updated daily from the previous day's conversation memory through ${endDate}. Each line is dated; entries older than 7 days are evicted.`, `${endDate} までの前日の会話記録から日次で更新される短期トピック。各行は日付付き。7日より古い項目は自動で削除される。`)} -->`,
|
|
711
|
+
"",
|
|
712
|
+
...topics.map((topic) => `- ${topic.date}: ${topic.text}`),
|
|
713
|
+
].join("\n");
|
|
714
|
+
}
|
|
715
|
+
export function removeRecentTopicsSection(text) {
|
|
716
|
+
const lines = text.split(/\r?\n/);
|
|
717
|
+
const start = lines.findIndex((line) => recentTopicsHeading(line));
|
|
718
|
+
if (start < 0) {
|
|
719
|
+
return text;
|
|
720
|
+
}
|
|
721
|
+
let end = start + 1;
|
|
722
|
+
while (end < lines.length && !/^#{1,6}\s+\S/.test(lines[end].trim())) {
|
|
723
|
+
end += 1;
|
|
724
|
+
}
|
|
725
|
+
return [...lines.slice(0, start), ...lines.slice(end)].join("\n").replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
|
|
726
|
+
}
|
|
727
|
+
function recentTopicsHeading(line) {
|
|
728
|
+
return /^##\s+(?:Recent 7-day topics|直近1週間のトピック)\s*$/.test(line.trim());
|
|
729
|
+
}
|
|
201
730
|
const MEMORY_CONSOLIDATE_INPUT_MAX = 32000;
|
|
202
731
|
const MEMORY_HEADER_PATTERN = /^# memory\.md\s*$/im;
|
|
203
732
|
async function consolidateMemoryFile(config, modelId, eventSource) {
|
|
204
733
|
const file = profileMemoryPath(config, "memory");
|
|
205
734
|
const original = await readTextIfExists(file);
|
|
206
|
-
|
|
735
|
+
const originalWithoutRecentTopics = removeRecentTopicsSection(original);
|
|
736
|
+
if (!originalWithoutRecentTopics.trim())
|
|
207
737
|
return;
|
|
208
|
-
const headerBlock = extractHeaderBlock(
|
|
209
|
-
const body =
|
|
738
|
+
const headerBlock = extractHeaderBlock(originalWithoutRecentTopics);
|
|
739
|
+
const body = originalWithoutRecentTopics.slice(headerBlock.length);
|
|
210
740
|
if (!body.trim())
|
|
211
741
|
return;
|
|
212
742
|
try {
|
|
@@ -217,18 +747,20 @@ async function consolidateMemoryFile(config, modelId, eventSource) {
|
|
|
217
747
|
{
|
|
218
748
|
role: "system",
|
|
219
749
|
content: [
|
|
220
|
-
"You organize Agent-Sin long-term memory. memory.md is a long-term note for understanding the user. Read the body, remove duplicates and stale items,
|
|
750
|
+
"You organize Agent-Sin long-term memory. memory.md is a long-term note for understanding the user across many future conversations. Read the body, then actively reshape it: remove duplicates and stale items, edit/overwrite existing items with newer information, and reclassify content under better headings when the current structure no longer fits.",
|
|
751
|
+
"Treat this as a periodic inventory. You are encouraged to add new headings, rename headings, merge or split headings, move bullets across sections, and delete obsolete or low-signal lines. Do not let memory.md grow by pure appending, and do not freeze the existing structure just because it exists.",
|
|
221
752
|
"Output rules:",
|
|
222
753
|
"- Return only the Markdown body. No preface, afterword, or code fences.",
|
|
223
754
|
"- Write content in the same language as the original memory.md body.",
|
|
224
|
-
"-
|
|
755
|
+
"- Normalize content under headings to one fact per bullet line starting with '-'. Do not create date headers or 'Auto promotion: ...' / '自動昇格: ...' headings.",
|
|
225
756
|
"- Merge items with the same meaning. If new information updates old information, discard the old version and rewrite to the latest state.",
|
|
226
|
-
"-
|
|
227
|
-
"-
|
|
757
|
+
"- Prioritize keeping anything useful for understanding the user across future conversations: role, expertise, preferences, values, communication tendencies, decision habits, media preferences, recurring interests, stable dislikes, and long-running themes worth remembering even when they no longer come up daily.",
|
|
758
|
+
"- Overlap with the short-lived 'Recent 7-day topics' / '直近1週間のトピック' section is acceptable when the topic is also a durable long-term interest. The recent-topics section is managed separately, so focus this output on the long-term body only.",
|
|
759
|
+
"- Remove operating rules, schedules, skill behavior settings, specific sender names, filters, raw recent task work, transient progress, and casual chat that has no long-term signal.",
|
|
228
760
|
"- Do not keep sensitive finance/family information or information that identifies other people.",
|
|
229
761
|
"- Do not invent content. Do not add facts not present in the source.",
|
|
230
762
|
"- Each line must be a concise generic sentence or fact. Do not include dates or 'today I...' phrasing.",
|
|
231
|
-
"-
|
|
763
|
+
"- Aim for roughly 40 lines, and up to about 60 lines when there is genuinely durable signal worth preserving. Cut aggressively below this when items are weak or duplicative.",
|
|
232
764
|
"Even if the target is empty or does not need much cleanup, return the minimal bullet list that preserves the current state.",
|
|
233
765
|
].join("\n"),
|
|
234
766
|
},
|
|
@@ -242,9 +774,8 @@ async function consolidateMemoryFile(config, modelId, eventSource) {
|
|
|
242
774
|
if (!cleaned)
|
|
243
775
|
return;
|
|
244
776
|
const next = `${headerBlock.replace(/\s+$/, "")}\n\n${cleaned}\n`;
|
|
245
|
-
if (normalizeForCompare(next) === normalizeForCompare(
|
|
777
|
+
if (normalizeForCompare(next) === normalizeForCompare(originalWithoutRecentTopics))
|
|
246
778
|
return;
|
|
247
|
-
await writeFile(`${file}.bak`, original, "utf8");
|
|
248
779
|
await writeFile(file, next, "utf8");
|
|
249
780
|
await appendEventLog(config, {
|
|
250
781
|
level: "info",
|
|
@@ -291,7 +822,7 @@ function sanitizeConsolidatedBody(text) {
|
|
|
291
822
|
.split(/\r?\n/)
|
|
292
823
|
.map((line) => line.trimEnd())
|
|
293
824
|
.filter((line) => line.length > 0)
|
|
294
|
-
.map((line) => (line.startsWith("- ") || line
|
|
825
|
+
.map((line) => (line.startsWith("- ") || /^#{1,6}\s+\S/.test(line) ? line : line.replace(/^[-*・]?\s*/, "- ")));
|
|
295
826
|
return lines.join("\n").trim();
|
|
296
827
|
}
|
|
297
828
|
function isStructuredModelPayload(text) {
|
|
@@ -381,6 +912,15 @@ function localDateString(date) {
|
|
|
381
912
|
const dd = String(date.getDate()).padStart(2, "0");
|
|
382
913
|
return `${yyyy}-${MM}-${dd}`;
|
|
383
914
|
}
|
|
915
|
+
function dateFromLocalDateString(value) {
|
|
916
|
+
const [yyyy, MM, dd] = value.split("-").map((part) => Number.parseInt(part, 10));
|
|
917
|
+
return new Date(yyyy, MM - 1, dd);
|
|
918
|
+
}
|
|
919
|
+
function addDays(date, days) {
|
|
920
|
+
const next = new Date(date);
|
|
921
|
+
next.setDate(next.getDate() + days);
|
|
922
|
+
return next;
|
|
923
|
+
}
|
|
384
924
|
function dailyMemoryFileForDate(config, date) {
|
|
385
925
|
const [yyyy, MM, dd] = date.split("-").map((part) => Number.parseInt(part, 10));
|
|
386
926
|
return dailyConversationMemoryFile(config, new Date(yyyy, MM - 1, dd));
|
|
@@ -420,3 +960,17 @@ function clip(text, max) {
|
|
|
420
960
|
}
|
|
421
961
|
return `${text.slice(0, max)}\n\n... clipped ...`;
|
|
422
962
|
}
|
|
963
|
+
function clipAround(text, max) {
|
|
964
|
+
if (max <= 0) {
|
|
965
|
+
return "";
|
|
966
|
+
}
|
|
967
|
+
if (text.length <= max) {
|
|
968
|
+
return text;
|
|
969
|
+
}
|
|
970
|
+
if (max < 40) {
|
|
971
|
+
return text.slice(0, max);
|
|
972
|
+
}
|
|
973
|
+
const marker = "\n\n... clipped middle ...\n\n";
|
|
974
|
+
const side = Math.floor((max - marker.length) / 2);
|
|
975
|
+
return `${text.slice(0, side)}${marker}${text.slice(text.length - side)}`;
|
|
976
|
+
}
|