agent-sin 0.1.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/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/assets/logo.png +0 -0
- package/builtin-skills/_shared/_models_lib.py +227 -0
- package/builtin-skills/_shared/_profile_lib.py +98 -0
- package/builtin-skills/_shared/_schedules_lib.py +313 -0
- package/builtin-skills/_shared/_skill_settings_lib.py +153 -0
- package/builtin-skills/_shared/i18n.py +26 -0
- package/builtin-skills/memo-delete/main.py +155 -0
- package/builtin-skills/memo-delete/skill.yaml +57 -0
- package/builtin-skills/memo-index/main.py +178 -0
- package/builtin-skills/memo-index/skill.yaml +53 -0
- package/builtin-skills/memo-save/README.md +5 -0
- package/builtin-skills/memo-save/main.py +74 -0
- package/builtin-skills/memo-save/skill.yaml +52 -0
- package/builtin-skills/memo-search/README.md +10 -0
- package/builtin-skills/memo-search/main.py +97 -0
- package/builtin-skills/memo-search/skill.yaml +51 -0
- package/builtin-skills/memo-vector-search/main.py +121 -0
- package/builtin-skills/memo-vector-search/skill.yaml +53 -0
- package/builtin-skills/model-add/main.py +180 -0
- package/builtin-skills/model-add/skill.yaml +112 -0
- package/builtin-skills/model-list/main.py +93 -0
- package/builtin-skills/model-list/skill.yaml +48 -0
- package/builtin-skills/model-set/main.py +123 -0
- package/builtin-skills/model-set/skill.yaml +69 -0
- package/builtin-skills/profile-delete/_profile_lib.py +98 -0
- package/builtin-skills/profile-delete/main.py +98 -0
- package/builtin-skills/profile-delete/skill.yaml +64 -0
- package/builtin-skills/profile-edit/_profile_lib.py +98 -0
- package/builtin-skills/profile-edit/main.py +97 -0
- package/builtin-skills/profile-edit/skill.yaml +72 -0
- package/builtin-skills/profile-save/main.py +52 -0
- package/builtin-skills/profile-save/skill.yaml +69 -0
- package/builtin-skills/schedule-add/_schedules_lib.py +303 -0
- package/builtin-skills/schedule-add/main.py +137 -0
- package/builtin-skills/schedule-add/skill.yaml +94 -0
- package/builtin-skills/schedule-list/_schedules_lib.py +303 -0
- package/builtin-skills/schedule-list/main.py +86 -0
- package/builtin-skills/schedule-list/skill.yaml +45 -0
- package/builtin-skills/schedule-remove/_schedules_lib.py +303 -0
- package/builtin-skills/schedule-remove/main.py +69 -0
- package/builtin-skills/schedule-remove/skill.yaml +49 -0
- package/builtin-skills/schedule-toggle/_schedules_lib.py +303 -0
- package/builtin-skills/schedule-toggle/main.py +78 -0
- package/builtin-skills/schedule-toggle/skill.yaml +61 -0
- package/builtin-skills/skills-disable/main.py +63 -0
- package/builtin-skills/skills-disable/skill.yaml +52 -0
- package/builtin-skills/skills-enable/main.py +62 -0
- package/builtin-skills/skills-enable/skill.yaml +51 -0
- package/builtin-skills/todo-add/main.py +68 -0
- package/builtin-skills/todo-add/skill.yaml +53 -0
- package/builtin-skills/todo-delete/main.py +65 -0
- package/builtin-skills/todo-delete/skill.yaml +47 -0
- package/builtin-skills/todo-done/main.py +75 -0
- package/builtin-skills/todo-done/skill.yaml +47 -0
- package/builtin-skills/todo-list/main.py +91 -0
- package/builtin-skills/todo-list/skill.yaml +48 -0
- package/builtin-skills/todo-tick/main.py +125 -0
- package/builtin-skills/todo-tick/skill.yaml +48 -0
- package/dist/builder/build-action-classifier.d.ts +18 -0
- package/dist/builder/build-action-classifier.js +142 -0
- package/dist/builder/build-commands.d.ts +19 -0
- package/dist/builder/build-commands.js +133 -0
- package/dist/builder/build-flow.d.ts +72 -0
- package/dist/builder/build-flow.js +416 -0
- package/dist/builder/builder-session.d.ts +117 -0
- package/dist/builder/builder-session.js +1129 -0
- package/dist/builder/conversation-router.d.ts +22 -0
- package/dist/builder/conversation-router.js +69 -0
- package/dist/builder/intent-runtime-store.d.ts +7 -0
- package/dist/builder/intent-runtime-store.js +60 -0
- package/dist/builder/progress-format.d.ts +7 -0
- package/dist/builder/progress-format.js +46 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +2835 -0
- package/dist/cli/spinner.d.ts +30 -0
- package/dist/cli/spinner.js +164 -0
- package/dist/core/ai-provider.d.ts +75 -0
- package/dist/core/ai-provider.js +678 -0
- package/dist/core/builtin-skills.d.ts +27 -0
- package/dist/core/builtin-skills.js +120 -0
- package/dist/core/chat-engine.d.ts +70 -0
- package/dist/core/chat-engine.js +812 -0
- package/dist/core/config.d.ts +127 -0
- package/dist/core/config.js +1379 -0
- package/dist/core/daily-memory-promotion.d.ts +21 -0
- package/dist/core/daily-memory-promotion.js +422 -0
- package/dist/core/i18n.d.ts +23 -0
- package/dist/core/i18n.js +167 -0
- package/dist/core/info-lines.d.ts +5 -0
- package/dist/core/info-lines.js +39 -0
- package/dist/core/input-schema.d.ts +2 -0
- package/dist/core/input-schema.js +156 -0
- package/dist/core/intent-router.d.ts +27 -0
- package/dist/core/intent-router.js +160 -0
- package/dist/core/logger.d.ts +60 -0
- package/dist/core/logger.js +240 -0
- package/dist/core/memory.d.ts +10 -0
- package/dist/core/memory.js +72 -0
- package/dist/core/message-utils.d.ts +13 -0
- package/dist/core/message-utils.js +104 -0
- package/dist/core/notifier.d.ts +17 -0
- package/dist/core/notifier.js +424 -0
- package/dist/core/output-writer.d.ts +13 -0
- package/dist/core/output-writer.js +100 -0
- package/dist/core/plan-decision.d.ts +16 -0
- package/dist/core/plan-decision.js +88 -0
- package/dist/core/profile-memory.d.ts +17 -0
- package/dist/core/profile-memory.js +142 -0
- package/dist/core/runtime.d.ts +50 -0
- package/dist/core/runtime.js +187 -0
- package/dist/core/scheduler.d.ts +28 -0
- package/dist/core/scheduler.js +155 -0
- package/dist/core/secrets.d.ts +31 -0
- package/dist/core/secrets.js +214 -0
- package/dist/core/service.d.ts +35 -0
- package/dist/core/service.js +479 -0
- package/dist/core/skill-planner.d.ts +24 -0
- package/dist/core/skill-planner.js +100 -0
- package/dist/core/skill-registry.d.ts +98 -0
- package/dist/core/skill-registry.js +319 -0
- package/dist/core/skill-scaffold.d.ts +33 -0
- package/dist/core/skill-scaffold.js +256 -0
- package/dist/core/skill-settings.d.ts +11 -0
- package/dist/core/skill-settings.js +63 -0
- package/dist/core/transfer.d.ts +31 -0
- package/dist/core/transfer.js +270 -0
- package/dist/core/update-notifier.d.ts +2 -0
- package/dist/core/update-notifier.js +140 -0
- package/dist/discord/bot.d.ts +96 -0
- package/dist/discord/bot.js +2424 -0
- package/dist/runtimes/codex-app-server.d.ts +53 -0
- package/dist/runtimes/codex-app-server.js +305 -0
- package/dist/runtimes/python-runner.d.ts +7 -0
- package/dist/runtimes/python-runner.js +302 -0
- package/dist/runtimes/typescript-runner.d.ts +5 -0
- package/dist/runtimes/typescript-runner.js +172 -0
- package/dist/skills-sdk/types.d.ts +38 -0
- package/dist/skills-sdk/types.js +1 -0
- package/dist/telegram/bot.d.ts +94 -0
- package/dist/telegram/bot.js +1219 -0
- package/install.ps1 +132 -0
- package/install.sh +130 -0
- package/package.json +60 -0
- package/templates/skill-python/main.py +74 -0
- package/templates/skill-python/skill.yaml +48 -0
- package/templates/skill-typescript/main.ts +87 -0
- package/templates/skill-typescript/skill.yaml +42 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AppConfig } from "./config.js";
|
|
2
|
+
import { type EventLogSource } from "./logger.js";
|
|
3
|
+
export type DailyMemoryPromotionStatus = "promoted" | "reviewed" | "skipped" | "not_found" | "empty" | "model_failed" | "invalid_response";
|
|
4
|
+
export interface DailyMemoryPromotionOptions {
|
|
5
|
+
date?: string | Date;
|
|
6
|
+
now?: Date;
|
|
7
|
+
force?: boolean;
|
|
8
|
+
dryRun?: boolean;
|
|
9
|
+
modelId?: string;
|
|
10
|
+
eventSource?: EventLogSource;
|
|
11
|
+
}
|
|
12
|
+
export interface DailyMemoryPromotionResult {
|
|
13
|
+
status: DailyMemoryPromotionStatus;
|
|
14
|
+
date: string;
|
|
15
|
+
file: string;
|
|
16
|
+
items: string[];
|
|
17
|
+
message?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function maybePromoteDailyMemory(config: AppConfig, options?: DailyMemoryPromotionOptions): Promise<DailyMemoryPromotionResult>;
|
|
20
|
+
export declare function promoteDailyMemory(config: AppConfig, options?: DailyMemoryPromotionOptions): Promise<DailyMemoryPromotionResult>;
|
|
21
|
+
export declare function parseDailyMemoryPromotionResponse(text: string): string[];
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { getAiProvider } from "./ai-provider.js";
|
|
5
|
+
import { appendEventLog, dailyConversationMemoryFile } from "./logger.js";
|
|
6
|
+
import { appendProfileMemory, profileMemoryPath, readProfileMemoryFiles, } from "./profile-memory.js";
|
|
7
|
+
import { l } from "./i18n.js";
|
|
8
|
+
const MAX_DAILY_CHARS = 24000;
|
|
9
|
+
const MAX_MEMORY_CHARS = 12000;
|
|
10
|
+
const MAX_PROMOTION_ITEMS = 3;
|
|
11
|
+
const MAX_ITEM_CHARS = 500;
|
|
12
|
+
export async function maybePromoteDailyMemory(config, options = {}) {
|
|
13
|
+
let result;
|
|
14
|
+
try {
|
|
15
|
+
result = await promoteDailyMemory(config, options);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
const date = safeNormalizeTargetDate(options.date, options.now || new Date());
|
|
19
|
+
result = {
|
|
20
|
+
status: "model_failed",
|
|
21
|
+
date,
|
|
22
|
+
file: dailyMemoryFileForDate(config, date),
|
|
23
|
+
items: [],
|
|
24
|
+
message: error instanceof Error ? error.message : String(error),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (result.status === "promoted" || result.status === "reviewed" || result.status === "model_failed") {
|
|
28
|
+
try {
|
|
29
|
+
await appendEventLog(config, {
|
|
30
|
+
level: result.status === "model_failed" ? "warn" : "info",
|
|
31
|
+
source: options.eventSource || "chat",
|
|
32
|
+
event: "daily_memory_promotion",
|
|
33
|
+
message: result.message,
|
|
34
|
+
details: {
|
|
35
|
+
date: result.date,
|
|
36
|
+
status: result.status,
|
|
37
|
+
items: result.items.length,
|
|
38
|
+
file: result.file,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Promotion must never block the primary chat / builder / gateway flow.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
export async function promoteDailyMemory(config, options = {}) {
|
|
49
|
+
const date = normalizeTargetDate(options.date, options.now || new Date());
|
|
50
|
+
const file = dailyMemoryFileForDate(config, date);
|
|
51
|
+
const daily = await readTextIfExists(file);
|
|
52
|
+
if (!daily) {
|
|
53
|
+
return { status: "not_found", date, file, items: [], message: `daily memory not found: ${date}` };
|
|
54
|
+
}
|
|
55
|
+
if (!daily.replace(/^# .+$/m, "").trim()) {
|
|
56
|
+
return { status: "empty", date, file, items: [], message: `daily memory is empty: ${date}` };
|
|
57
|
+
}
|
|
58
|
+
const hash = sha256(daily);
|
|
59
|
+
const state = await readPromotionState(config);
|
|
60
|
+
const previous = state.dates[date];
|
|
61
|
+
if (!options.force && previous?.hash === hash) {
|
|
62
|
+
return {
|
|
63
|
+
status: "skipped",
|
|
64
|
+
date,
|
|
65
|
+
file,
|
|
66
|
+
items: [],
|
|
67
|
+
message: `daily memory already reviewed: ${date}`,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const profile = await readProfileMemoryFiles(config);
|
|
71
|
+
const response = await requestPromotionItems(config, {
|
|
72
|
+
date,
|
|
73
|
+
daily,
|
|
74
|
+
existingMemory: profile.memory,
|
|
75
|
+
modelId: options.modelId || config.chat_model_id,
|
|
76
|
+
});
|
|
77
|
+
if (response.status !== "ok") {
|
|
78
|
+
return {
|
|
79
|
+
status: response.status,
|
|
80
|
+
date,
|
|
81
|
+
file,
|
|
82
|
+
items: [],
|
|
83
|
+
message: response.message,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const items = dedupePromotionItems(response.items, profile.memory);
|
|
87
|
+
if (!options.dryRun && items.length > 0) {
|
|
88
|
+
await appendProfileMemory(config, "memory", formatPromotedMemoryEntry(date, items), options.now || new Date());
|
|
89
|
+
await consolidateMemoryFile(config, options.modelId || config.chat_model_id, options.eventSource || "chat");
|
|
90
|
+
}
|
|
91
|
+
if (!options.dryRun) {
|
|
92
|
+
state.dates[date] = {
|
|
93
|
+
hash,
|
|
94
|
+
status: items.length > 0 ? "promoted" : "reviewed",
|
|
95
|
+
promoted_at: (options.now || new Date()).toISOString(),
|
|
96
|
+
items: items.length,
|
|
97
|
+
};
|
|
98
|
+
await writePromotionState(config, state);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
status: items.length > 0 ? "promoted" : "reviewed",
|
|
102
|
+
date,
|
|
103
|
+
file,
|
|
104
|
+
items,
|
|
105
|
+
message: items.length > 0 ? `promoted ${items.length} item(s) from ${date}` : `no long-term memory items: ${date}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export function parseDailyMemoryPromotionResponse(text) {
|
|
109
|
+
const jsonText = extractJson(text);
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = JSON.parse(jsonText);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
+
throw new Error(l(`invalid promotion JSON: ${message}`, `昇格用JSONが不正です: ${message}`));
|
|
117
|
+
}
|
|
118
|
+
const rawItems = Array.isArray(parsed)
|
|
119
|
+
? parsed
|
|
120
|
+
: parsed && typeof parsed === "object" && Array.isArray(parsed.items)
|
|
121
|
+
? parsed.items
|
|
122
|
+
: [];
|
|
123
|
+
return rawItems
|
|
124
|
+
.map((item) => {
|
|
125
|
+
if (typeof item === "string")
|
|
126
|
+
return item;
|
|
127
|
+
if (item && typeof item === "object") {
|
|
128
|
+
const record = item;
|
|
129
|
+
const value = record.text || record.summary || record.content;
|
|
130
|
+
return typeof value === "string" ? value : "";
|
|
131
|
+
}
|
|
132
|
+
return "";
|
|
133
|
+
})
|
|
134
|
+
.map(cleanPromotionItem)
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.slice(0, MAX_PROMOTION_ITEMS);
|
|
137
|
+
}
|
|
138
|
+
async function requestPromotionItems(config, input) {
|
|
139
|
+
try {
|
|
140
|
+
const response = await getAiProvider()(config, {
|
|
141
|
+
model_id: input.modelId,
|
|
142
|
+
temperature: 0,
|
|
143
|
+
messages: [
|
|
144
|
+
{
|
|
145
|
+
role: "system",
|
|
146
|
+
content: [
|
|
147
|
+
"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 only observations from daily conversation memory that directly help understand the user. Be strict; if unsure, output nothing. Maximum 3 items, zero is fine.",
|
|
149
|
+
"Write each item in the same language as the source daily-conversation.md content.",
|
|
150
|
+
"Keep only items directly about the user:",
|
|
151
|
+
"- The user's role, experience, interests, and personal context",
|
|
152
|
+
"- Communication tendencies, preferred style, disliked expressions, and decision habits",
|
|
153
|
+
"- Values, beliefs, and things the user considers important",
|
|
154
|
+
"- Notification/channel preferences, subscribed media, preferred formats",
|
|
155
|
+
"- Stable interests or areas the user is not interested in",
|
|
156
|
+
"Never keep operating rules, skill settings, or work logs:",
|
|
157
|
+
"- Schedules, notification timing, notification destinations, filters, limits, or other skill behavior settings",
|
|
158
|
+
"- Action rules tied to individual emails, files, or usernames",
|
|
159
|
+
"- Recent task work, commits, fixes, or implementation steps",
|
|
160
|
+
"- Same-day chat, impressions, status updates, progress, mood, health, or weather",
|
|
161
|
+
"- Tool output, logs, code snippets, or URL lists",
|
|
162
|
+
"- Content with the same meaning as existing memory.md",
|
|
163
|
+
"- Secrets, API keys, tokens, sensitive finance/family information, or information identifying other people",
|
|
164
|
+
"Output rules: each item must be one generic sentence or fact. Do not include dates, ongoing wording, or 'today I...' phrasing.",
|
|
165
|
+
'Output JSON only: {"items":[{"text":"..."}]}. If nothing qualifies, return {"items":[]}.',
|
|
166
|
+
].join("\n"),
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
role: "user",
|
|
170
|
+
content: [
|
|
171
|
+
`Target date: ${input.date}`,
|
|
172
|
+
"",
|
|
173
|
+
"<existing-memory.md>",
|
|
174
|
+
clip(input.existingMemory, MAX_MEMORY_CHARS),
|
|
175
|
+
"</existing-memory.md>",
|
|
176
|
+
"",
|
|
177
|
+
"<daily-conversation.md>",
|
|
178
|
+
clip(input.daily, MAX_DAILY_CHARS),
|
|
179
|
+
"</daily-conversation.md>",
|
|
180
|
+
].join("\n"),
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
});
|
|
184
|
+
return { status: "ok", items: parseDailyMemoryPromotionResponse(response.text) };
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
188
|
+
if (message.startsWith("invalid promotion JSON:") || message.startsWith("昇格用JSONが不正です:")) {
|
|
189
|
+
return { status: "invalid_response", message };
|
|
190
|
+
}
|
|
191
|
+
return { status: "model_failed", message };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function formatPromotedMemoryEntry(date, items) {
|
|
195
|
+
return [
|
|
196
|
+
l(`Auto promotion: from daily conversation memory on ${date}`, `自動昇格: ${date} の日別会話記録から`),
|
|
197
|
+
"",
|
|
198
|
+
...items.map((item) => `- ${item}`),
|
|
199
|
+
].join("\n");
|
|
200
|
+
}
|
|
201
|
+
const MEMORY_CONSOLIDATE_INPUT_MAX = 32000;
|
|
202
|
+
const MEMORY_HEADER_PATTERN = /^# memory\.md\s*$/im;
|
|
203
|
+
async function consolidateMemoryFile(config, modelId, eventSource) {
|
|
204
|
+
const file = profileMemoryPath(config, "memory");
|
|
205
|
+
const original = await readTextIfExists(file);
|
|
206
|
+
if (!original.trim())
|
|
207
|
+
return;
|
|
208
|
+
const headerBlock = extractHeaderBlock(original);
|
|
209
|
+
const body = original.slice(headerBlock.length);
|
|
210
|
+
if (!body.trim())
|
|
211
|
+
return;
|
|
212
|
+
try {
|
|
213
|
+
const response = await getAiProvider()(config, {
|
|
214
|
+
model_id: modelId,
|
|
215
|
+
temperature: 0,
|
|
216
|
+
messages: [
|
|
217
|
+
{
|
|
218
|
+
role: "system",
|
|
219
|
+
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, and edit/overwrite existing items with newer information. Do not let it grow by appending only.",
|
|
221
|
+
"Output rules:",
|
|
222
|
+
"- Return only the Markdown body. No preface, afterword, or code fences.",
|
|
223
|
+
"- Write content in the same language as the original memory.md body.",
|
|
224
|
+
"- Preserve the existing heading structure when possible and normalize content under headings to one fact per bullet line starting with '-'. Do not create date headers or 'Auto promotion: ...' / '自動昇格: ...' headings.",
|
|
225
|
+
"- Merge items with the same meaning. If new information updates old information, discard the old version and rewrite to the latest state.",
|
|
226
|
+
"- Always keep observations directly tied to understanding the user: role, preferences, values, communication tendencies, media preferences, and interests.",
|
|
227
|
+
"- Remove operating rules, schedules, skill behavior settings, specific sender names, filters, recent task work, progress, and casual chat.",
|
|
228
|
+
"- Do not keep sensitive finance/family information or information that identifies other people.",
|
|
229
|
+
"- Do not invent content. Do not add facts not present in the source.",
|
|
230
|
+
"- Each line must be a concise generic sentence or fact. Do not include dates or 'today I...' phrasing.",
|
|
231
|
+
"- Keep the whole result to about 25 lines at most. Reduce further when possible.",
|
|
232
|
+
"Even if the target is empty or does not need much cleanup, return the minimal bullet list that preserves the current state.",
|
|
233
|
+
].join("\n"),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
role: "user",
|
|
237
|
+
content: ["<memory.md-body>", clip(body, MEMORY_CONSOLIDATE_INPUT_MAX), "</memory.md-body>"].join("\n"),
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
const cleaned = sanitizeConsolidatedBody(response.text);
|
|
242
|
+
if (!cleaned)
|
|
243
|
+
return;
|
|
244
|
+
const next = `${headerBlock.replace(/\s+$/, "")}\n\n${cleaned}\n`;
|
|
245
|
+
if (normalizeForCompare(next) === normalizeForCompare(original))
|
|
246
|
+
return;
|
|
247
|
+
await writeFile(`${file}.bak`, original, "utf8");
|
|
248
|
+
await writeFile(file, next, "utf8");
|
|
249
|
+
await appendEventLog(config, {
|
|
250
|
+
level: "info",
|
|
251
|
+
source: eventSource,
|
|
252
|
+
event: "memory_consolidated",
|
|
253
|
+
message: `memory.md consolidated (${body.length} -> ${cleaned.length} chars)`,
|
|
254
|
+
details: { file, before_chars: body.length, after_chars: cleaned.length },
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
await appendEventLog(config, {
|
|
259
|
+
level: "warn",
|
|
260
|
+
source: eventSource,
|
|
261
|
+
event: "memory_consolidate_failed",
|
|
262
|
+
message: error instanceof Error ? error.message : String(error),
|
|
263
|
+
details: { file },
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function extractHeaderBlock(raw) {
|
|
268
|
+
const match = MEMORY_HEADER_PATTERN.exec(raw);
|
|
269
|
+
if (!match)
|
|
270
|
+
return "";
|
|
271
|
+
const headerStart = match.index;
|
|
272
|
+
const afterHeader = raw.slice(headerStart);
|
|
273
|
+
const commentEnd = afterHeader.indexOf("-->");
|
|
274
|
+
if (commentEnd >= 0) {
|
|
275
|
+
return raw.slice(0, headerStart + commentEnd + "-->".length);
|
|
276
|
+
}
|
|
277
|
+
const newlineIdx = afterHeader.indexOf("\n");
|
|
278
|
+
if (newlineIdx < 0)
|
|
279
|
+
return raw.slice(0, headerStart + match[0].length);
|
|
280
|
+
return raw.slice(0, headerStart + newlineIdx);
|
|
281
|
+
}
|
|
282
|
+
function sanitizeConsolidatedBody(text) {
|
|
283
|
+
let body = text.trim();
|
|
284
|
+
const fenced = body.match(/```(?:markdown|md)?\s*([\s\S]*?)```/i);
|
|
285
|
+
if (fenced?.[1])
|
|
286
|
+
body = fenced[1].trim();
|
|
287
|
+
if (isStructuredModelPayload(body)) {
|
|
288
|
+
return "";
|
|
289
|
+
}
|
|
290
|
+
const lines = body
|
|
291
|
+
.split(/\r?\n/)
|
|
292
|
+
.map((line) => line.trimEnd())
|
|
293
|
+
.filter((line) => line.length > 0)
|
|
294
|
+
.map((line) => (line.startsWith("- ") || line.startsWith("# ") ? line : line.replace(/^[-*・]?\s*/, "- ")));
|
|
295
|
+
return lines.join("\n").trim();
|
|
296
|
+
}
|
|
297
|
+
function isStructuredModelPayload(text) {
|
|
298
|
+
const trimmed = text.trim();
|
|
299
|
+
if (!/^[{[]/.test(trimmed)) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
try {
|
|
303
|
+
JSON.parse(trimmed);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function normalizeForCompare(text) {
|
|
311
|
+
return text.replace(/\s+/g, " ").trim();
|
|
312
|
+
}
|
|
313
|
+
function dedupePromotionItems(items, existingMemory) {
|
|
314
|
+
const existing = normalizeForDedupe(existingMemory);
|
|
315
|
+
const seen = new Set();
|
|
316
|
+
const result = [];
|
|
317
|
+
for (const item of items) {
|
|
318
|
+
const cleaned = cleanPromotionItem(item);
|
|
319
|
+
const key = normalizeForDedupe(cleaned);
|
|
320
|
+
if (!cleaned || !key || seen.has(key) || existing.includes(key)) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
seen.add(key);
|
|
324
|
+
result.push(cleaned);
|
|
325
|
+
}
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
function cleanPromotionItem(text) {
|
|
329
|
+
return text
|
|
330
|
+
.replace(/\s+/g, " ")
|
|
331
|
+
.replace(/^[-*・]\s*/, "")
|
|
332
|
+
.trim()
|
|
333
|
+
.slice(0, MAX_ITEM_CHARS);
|
|
334
|
+
}
|
|
335
|
+
function normalizeForDedupe(text) {
|
|
336
|
+
return text.toLowerCase().replace(/\s+/g, "");
|
|
337
|
+
}
|
|
338
|
+
function extractJson(text) {
|
|
339
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
340
|
+
if (fenced?.[1]) {
|
|
341
|
+
return fenced[1].trim();
|
|
342
|
+
}
|
|
343
|
+
const objectStart = text.indexOf("{");
|
|
344
|
+
const objectEnd = text.lastIndexOf("}");
|
|
345
|
+
if (objectStart >= 0 && objectEnd > objectStart) {
|
|
346
|
+
return text.slice(objectStart, objectEnd + 1).trim();
|
|
347
|
+
}
|
|
348
|
+
const arrayStart = text.indexOf("[");
|
|
349
|
+
const arrayEnd = text.lastIndexOf("]");
|
|
350
|
+
if (arrayStart >= 0 && arrayEnd > arrayStart) {
|
|
351
|
+
return text.slice(arrayStart, arrayEnd + 1).trim();
|
|
352
|
+
}
|
|
353
|
+
return text.trim();
|
|
354
|
+
}
|
|
355
|
+
function normalizeTargetDate(value, now) {
|
|
356
|
+
if (value instanceof Date) {
|
|
357
|
+
return localDateString(value);
|
|
358
|
+
}
|
|
359
|
+
if (typeof value === "string" && value.trim()) {
|
|
360
|
+
const trimmed = value.trim();
|
|
361
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
|
|
362
|
+
throw new Error(l(`Invalid date: ${trimmed}. Use YYYY-MM-DD.`, `日付が不正です: ${trimmed}。YYYY-MM-DD を使ってください。`));
|
|
363
|
+
}
|
|
364
|
+
return trimmed;
|
|
365
|
+
}
|
|
366
|
+
const previous = new Date(now);
|
|
367
|
+
previous.setDate(previous.getDate() - 1);
|
|
368
|
+
return localDateString(previous);
|
|
369
|
+
}
|
|
370
|
+
function safeNormalizeTargetDate(value, now) {
|
|
371
|
+
try {
|
|
372
|
+
return normalizeTargetDate(value, now);
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return localDateString(now);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
function localDateString(date) {
|
|
379
|
+
const yyyy = String(date.getFullYear());
|
|
380
|
+
const MM = String(date.getMonth() + 1).padStart(2, "0");
|
|
381
|
+
const dd = String(date.getDate()).padStart(2, "0");
|
|
382
|
+
return `${yyyy}-${MM}-${dd}`;
|
|
383
|
+
}
|
|
384
|
+
function dailyMemoryFileForDate(config, date) {
|
|
385
|
+
const [yyyy, MM, dd] = date.split("-").map((part) => Number.parseInt(part, 10));
|
|
386
|
+
return dailyConversationMemoryFile(config, new Date(yyyy, MM - 1, dd));
|
|
387
|
+
}
|
|
388
|
+
function promotionStateFile(config) {
|
|
389
|
+
return path.join(config.memory_dir, "daily", ".promotion-state.json");
|
|
390
|
+
}
|
|
391
|
+
async function readPromotionState(config) {
|
|
392
|
+
try {
|
|
393
|
+
const raw = await readFile(promotionStateFile(config), "utf8");
|
|
394
|
+
const parsed = JSON.parse(raw);
|
|
395
|
+
return { dates: parsed.dates && typeof parsed.dates === "object" ? parsed.dates : {} };
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return { dates: {} };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
async function writePromotionState(config, state) {
|
|
402
|
+
const file = promotionStateFile(config);
|
|
403
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
404
|
+
await writeFile(file, `${JSON.stringify(state, null, 2)}\n`, "utf8");
|
|
405
|
+
}
|
|
406
|
+
async function readTextIfExists(file) {
|
|
407
|
+
try {
|
|
408
|
+
return await readFile(file, "utf8");
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return "";
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function sha256(text) {
|
|
415
|
+
return crypto.createHash("sha256").update(text).digest("hex");
|
|
416
|
+
}
|
|
417
|
+
function clip(text, max) {
|
|
418
|
+
if (text.length <= max) {
|
|
419
|
+
return text;
|
|
420
|
+
}
|
|
421
|
+
return `${text.slice(0, max)}\n\n... clipped ...`;
|
|
422
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny localization helper used for framework-emitted UI strings (spinner
|
|
3
|
+
* labels, chat narration, etc.) and packaged metadata such as builtin skill
|
|
4
|
+
* names/descriptions.
|
|
5
|
+
*
|
|
6
|
+
* Locale resolution (only `en` and `ja` supported):
|
|
7
|
+
* 1. AGENT_SIN_LOCALE env var (explicit override)
|
|
8
|
+
* 2. Per-turn locale inferred from the current user message
|
|
9
|
+
* 3. Configured locale via setLocale()
|
|
10
|
+
* 4. LC_ALL or LANG starting with `ja` → `ja` (Unix shells)
|
|
11
|
+
* 5. Intl.DateTimeFormat().resolvedOptions().locale starting with `ja`
|
|
12
|
+
* → `ja` (cross-platform OS locale, including Windows where step 2 is empty)
|
|
13
|
+
* 6. Default: `en`
|
|
14
|
+
*/
|
|
15
|
+
export type Locale = "en" | "ja";
|
|
16
|
+
export declare function detectLocale(): Locale;
|
|
17
|
+
export declare function setLocale(locale: Locale | null): void;
|
|
18
|
+
export declare function inferLocaleFromText(value: string | string[] | undefined): Locale | null;
|
|
19
|
+
export declare function withLocale<T>(locale: Locale | null | undefined, fn: () => T): T;
|
|
20
|
+
export declare function t(key: string, vars?: Record<string, string | number>): string;
|
|
21
|
+
export declare function l(en: string, ja: string, vars?: Record<string, string | number>): string;
|
|
22
|
+
export declare function lLines(en: string[], ja: string[]): string[];
|
|
23
|
+
export declare function localizeObject<T>(value: T, locale?: Locale): T;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const localeContext = new AsyncLocalStorage();
|
|
3
|
+
const STRINGS = {
|
|
4
|
+
"spinner.thinking": {
|
|
5
|
+
en: "Thinking",
|
|
6
|
+
ja: "考え中",
|
|
7
|
+
},
|
|
8
|
+
"spinner.skill_running": {
|
|
9
|
+
en: "Running {skill}",
|
|
10
|
+
ja: "{skill} を実行中",
|
|
11
|
+
},
|
|
12
|
+
"spinner.skill_repairing": {
|
|
13
|
+
en: "Repairing {skill}",
|
|
14
|
+
ja: "{skill} を修正中",
|
|
15
|
+
},
|
|
16
|
+
"chat.history_reset": {
|
|
17
|
+
en: "Chat history reset.",
|
|
18
|
+
ja: "会話履歴をリセットしました。",
|
|
19
|
+
},
|
|
20
|
+
"chat.tool_call_announce": {
|
|
21
|
+
en: "→ Calling {skill}",
|
|
22
|
+
ja: "→ {skill} を実行します",
|
|
23
|
+
},
|
|
24
|
+
"chat.skill_repair_started": {
|
|
25
|
+
en: "→ Repairing {skill} after a failed run",
|
|
26
|
+
ja: "→ {skill} が失敗したため修正します",
|
|
27
|
+
},
|
|
28
|
+
"chat.skill_repair_done": {
|
|
29
|
+
en: "Repaired it and ran it again.",
|
|
30
|
+
ja: "修正してもう一度実行しました。",
|
|
31
|
+
},
|
|
32
|
+
"chat.skill_repair_failed": {
|
|
33
|
+
en: "Automatic repair could not finish: {message}",
|
|
34
|
+
ja: "自動修正を完了できませんでした: {message}",
|
|
35
|
+
},
|
|
36
|
+
"chat.skill_repair_still_failed": {
|
|
37
|
+
en: "It still failed after repair: {message}",
|
|
38
|
+
ja: "修正後も実行できませんでした: {message}",
|
|
39
|
+
},
|
|
40
|
+
"chat.model_unreachable": {
|
|
41
|
+
en: "[chat model '{model}' is unreachable] {message}",
|
|
42
|
+
ja: "[chat-model '{model}' に接続できませんでした] {message}",
|
|
43
|
+
},
|
|
44
|
+
"skill.default_done": {
|
|
45
|
+
en: "Done",
|
|
46
|
+
ja: "完了",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
let active = null;
|
|
50
|
+
export function detectLocale() {
|
|
51
|
+
const explicit = (process.env.AGENT_SIN_LOCALE || "").trim().toLowerCase();
|
|
52
|
+
if (explicit === "ja" || explicit === "en") {
|
|
53
|
+
return explicit;
|
|
54
|
+
}
|
|
55
|
+
const scoped = localeContext.getStore();
|
|
56
|
+
if (scoped) {
|
|
57
|
+
return scoped;
|
|
58
|
+
}
|
|
59
|
+
if (active) {
|
|
60
|
+
return active;
|
|
61
|
+
}
|
|
62
|
+
// Respect an explicitly-set Unix shell locale even when it is not Japanese
|
|
63
|
+
// (a user who typed `export LANG=en_US.UTF-8` wants English).
|
|
64
|
+
const lang = (process.env.LC_ALL || process.env.LANG || "").trim();
|
|
65
|
+
if (lang) {
|
|
66
|
+
active = /^ja(_|$|-)/i.test(lang) ? "ja" : "en";
|
|
67
|
+
return active;
|
|
68
|
+
}
|
|
69
|
+
// No shell-level locale set (typical on Windows or stripped-down envs):
|
|
70
|
+
// fall back to the OS-reported locale via the Intl API.
|
|
71
|
+
try {
|
|
72
|
+
const intlLocale = (Intl.DateTimeFormat().resolvedOptions().locale || "").toLowerCase();
|
|
73
|
+
if (/^ja(-|$)/.test(intlLocale)) {
|
|
74
|
+
active = "ja";
|
|
75
|
+
return active;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Intl API can fail in stripped-down builds; fall through to default.
|
|
80
|
+
}
|
|
81
|
+
active = "en";
|
|
82
|
+
return active;
|
|
83
|
+
}
|
|
84
|
+
export function setLocale(locale) {
|
|
85
|
+
active = locale;
|
|
86
|
+
}
|
|
87
|
+
export function inferLocaleFromText(value) {
|
|
88
|
+
const text = Array.isArray(value) ? value.join("\n") : value || "";
|
|
89
|
+
if (/[\u3040-\u30ff\u3400-\u9fff]/.test(text)) {
|
|
90
|
+
return "ja";
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
export function withLocale(locale, fn) {
|
|
95
|
+
if (!locale) {
|
|
96
|
+
return fn();
|
|
97
|
+
}
|
|
98
|
+
return localeContext.run(locale, fn);
|
|
99
|
+
}
|
|
100
|
+
export function t(key, vars) {
|
|
101
|
+
const locale = detectLocale();
|
|
102
|
+
const entry = STRINGS[key];
|
|
103
|
+
if (!entry) {
|
|
104
|
+
return key;
|
|
105
|
+
}
|
|
106
|
+
let value = entry[locale] || entry.en || key;
|
|
107
|
+
if (vars) {
|
|
108
|
+
for (const [name, replacement] of Object.entries(vars)) {
|
|
109
|
+
value = value.replaceAll(`{${name}}`, String(replacement));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
export function l(en, ja, vars) {
|
|
115
|
+
let value = detectLocale() === "ja" ? ja : en;
|
|
116
|
+
if (vars) {
|
|
117
|
+
for (const [name, replacement] of Object.entries(vars)) {
|
|
118
|
+
value = value.replaceAll(`{${name}}`, String(replacement));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
export function lLines(en, ja) {
|
|
124
|
+
return detectLocale() === "ja" ? ja : en;
|
|
125
|
+
}
|
|
126
|
+
export function localizeObject(value, locale = detectLocale()) {
|
|
127
|
+
return resolveLocalizedFields(value, locale);
|
|
128
|
+
}
|
|
129
|
+
function resolveLocalizedFields(value, locale) {
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
return value.map((item) => resolveLocalizedFields(item, locale));
|
|
132
|
+
}
|
|
133
|
+
if (!isPlainRecord(value)) {
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
const out = {};
|
|
137
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
138
|
+
if (key.endsWith("_i18n")) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
out[key] = resolveLocalizedFields(entry, locale);
|
|
142
|
+
}
|
|
143
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
144
|
+
if (!key.endsWith("_i18n")) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const baseKey = key.slice(0, -"_i18n".length);
|
|
148
|
+
const localized = pickLocalizedEntry(entry, locale);
|
|
149
|
+
if (localized !== undefined) {
|
|
150
|
+
out[baseKey] = resolveLocalizedFields(localized, locale);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
function pickLocalizedEntry(value, locale) {
|
|
156
|
+
if (!isPlainRecord(value)) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
const localized = value;
|
|
160
|
+
if (localized[locale] !== undefined) {
|
|
161
|
+
return localized[locale];
|
|
162
|
+
}
|
|
163
|
+
return localized.en;
|
|
164
|
+
}
|
|
165
|
+
function isPlainRecord(value) {
|
|
166
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
167
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type AppConfig, type ModelConfig } from "./config.js";
|
|
2
|
+
export declare function skillsLines(config: AppConfig): Promise<string[]>;
|
|
3
|
+
export declare function modelsLines(config: AppConfig): Promise<string[]>;
|
|
4
|
+
export declare function formatModelRow(id: string, model: ModelConfig["models"][string], chatId: string, builderId: string): string;
|
|
5
|
+
export declare function modelSummary(id: string, model: ModelConfig["models"][string]): string;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { loadModels } from "./config.js";
|
|
2
|
+
import { listSkillManifests } from "./skill-registry.js";
|
|
3
|
+
import { l } from "./i18n.js";
|
|
4
|
+
export async function skillsLines(config) {
|
|
5
|
+
const skills = await listSkillManifests(config.skills_dir);
|
|
6
|
+
if (skills.length === 0) {
|
|
7
|
+
return [l("No skills registered.", "登録済みのスキルはありません。")];
|
|
8
|
+
}
|
|
9
|
+
return skills.map((skill) => {
|
|
10
|
+
const enabled = skill.enabled === false ? l("disabled", "無効") : l("enabled", "有効");
|
|
11
|
+
const source = skill.source === "builtin" ? l("builtin", "ビルトイン") : skill.override ? l("override", "上書き") : l("user", "ユーザー");
|
|
12
|
+
return `${skill.id}\t${skill.name}\t${enabled}\t${source}`;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export async function modelsLines(config) {
|
|
16
|
+
const models = await loadModels(config.workspace);
|
|
17
|
+
const chatId = config.chat_model_id;
|
|
18
|
+
const builderId = config.builder_model_id;
|
|
19
|
+
return Object.entries(models.models).map(([id, model]) => formatModelRow(id, model, chatId, builderId));
|
|
20
|
+
}
|
|
21
|
+
export function formatModelRow(id, model, chatId, builderId) {
|
|
22
|
+
const provider = model.provider || (model.type === "api" ? id : model.type);
|
|
23
|
+
const name = model.model || "-";
|
|
24
|
+
const effort = model.effort || "-";
|
|
25
|
+
const enabled = model.enabled ? l("enabled", "有効") : l("disabled", "無効");
|
|
26
|
+
const tags = [];
|
|
27
|
+
if (id === chatId)
|
|
28
|
+
tags.push("chat");
|
|
29
|
+
if (id === builderId)
|
|
30
|
+
tags.push("builder");
|
|
31
|
+
const tag = tags.length ? `← ${tags.join(",")}` : "";
|
|
32
|
+
return `${provider.padEnd(12)} ${name.padEnd(18)} ${effort.padEnd(8)} ${enabled.padEnd(8)} ${tag}`.trimEnd();
|
|
33
|
+
}
|
|
34
|
+
export function modelSummary(id, model) {
|
|
35
|
+
const provider = model.provider || (model.type === "api" ? id : model.type);
|
|
36
|
+
const name = model.model || "-";
|
|
37
|
+
const effort = model.effort && model.effort !== "-" ? ` ${model.effort}` : "";
|
|
38
|
+
return `${provider} ${name}${effort}`;
|
|
39
|
+
}
|