skyloom 1.12.0 → 1.13.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.
Files changed (135) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +142 -46
  3. package/config/default.yaml +43 -47
  4. package/config/models.yaml +155 -155
  5. package/config/providers.yaml +39 -39
  6. package/config/skills/api_integrator/SKILL.md +15 -15
  7. package/config/skills/arch_designer/SKILL.md +13 -13
  8. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  9. package/config/skills/code_analysis/SKILL.md +13 -13
  10. package/config/skills/code_generator/SKILL.md +12 -12
  11. package/config/skills/code_reviewer/SKILL.md +13 -13
  12. package/config/skills/content_writer/SKILL.md +14 -14
  13. package/config/skills/data_transformer/SKILL.md +15 -15
  14. package/config/skills/document_analysis/SKILL.md +13 -13
  15. package/config/skills/emotional_companion/SKILL.md +15 -15
  16. package/config/skills/performance_checker/SKILL.md +14 -14
  17. package/config/skills/security_auditor/SKILL.md +14 -14
  18. package/config/skills/self_evolve/SKILL.md +13 -13
  19. package/config/skills/sys_operator/SKILL.md +15 -15
  20. package/config/skills/task_planner/SKILL.md +14 -14
  21. package/config/skills/web_research/SKILL.md +14 -14
  22. package/config/skills/workflow_designer/SKILL.md +13 -13
  23. package/dist/agents/dew.js +52 -52
  24. package/dist/agents/fair.js +84 -84
  25. package/dist/agents/fog.js +30 -30
  26. package/dist/agents/frost.js +32 -32
  27. package/dist/agents/rain.js +32 -32
  28. package/dist/agents/snow.js +68 -68
  29. package/dist/cli/main.js +103 -51
  30. package/dist/cli/main.js.map +1 -1
  31. package/dist/cli/tui.d.ts.map +1 -1
  32. package/dist/cli/tui.js +8 -1
  33. package/dist/cli/tui.js.map +1 -1
  34. package/dist/core/agent/task.d.ts +58 -0
  35. package/dist/core/agent/task.d.ts.map +1 -0
  36. package/dist/core/agent/task.js +83 -0
  37. package/dist/core/agent/task.js.map +1 -0
  38. package/dist/core/agent.d.ts +2 -45
  39. package/dist/core/agent.d.ts.map +1 -1
  40. package/dist/core/agent.js +61 -145
  41. package/dist/core/agent.js.map +1 -1
  42. package/dist/core/agent_helpers.d.ts +10 -0
  43. package/dist/core/agent_helpers.d.ts.map +1 -1
  44. package/dist/core/agent_helpers.js +39 -0
  45. package/dist/core/agent_helpers.js.map +1 -1
  46. package/dist/core/catalog.d.ts +71 -0
  47. package/dist/core/catalog.d.ts.map +1 -0
  48. package/dist/core/catalog.js +176 -0
  49. package/dist/core/catalog.js.map +1 -0
  50. package/dist/core/config.d.ts +8 -0
  51. package/dist/core/config.d.ts.map +1 -1
  52. package/dist/core/config.js +12 -4
  53. package/dist/core/config.js.map +1 -1
  54. package/dist/core/factory.js +16 -16
  55. package/dist/core/llm.d.ts +7 -0
  56. package/dist/core/llm.d.ts.map +1 -1
  57. package/dist/core/llm.js +139 -7
  58. package/dist/core/llm.js.map +1 -1
  59. package/dist/core/longdoc.js +5 -5
  60. package/dist/core/memory.d.ts.map +1 -1
  61. package/dist/core/memory.js +69 -62
  62. package/dist/core/memory.js.map +1 -1
  63. package/dist/core/theme.d.ts +46 -0
  64. package/dist/core/theme.d.ts.map +1 -0
  65. package/dist/core/theme.js +42 -0
  66. package/dist/core/theme.js.map +1 -0
  67. package/dist/web/server.js +542 -519
  68. package/dist/web/server.js.map +1 -1
  69. package/docs/AESTHETIC_DESIGN.md +144 -0
  70. package/docs/OPTIMIZATION_PLAN.md +178 -0
  71. package/package.json +60 -60
  72. package/scripts/install.js +48 -48
  73. package/scripts/link.js +10 -10
  74. package/setup.bat +79 -79
  75. package/skill-test-ty2fOA/test.md +10 -10
  76. package/src/agents/dew.ts +70 -70
  77. package/src/agents/fair.ts +102 -102
  78. package/src/agents/fog.ts +48 -48
  79. package/src/agents/frost.ts +50 -50
  80. package/src/agents/rain.ts +50 -50
  81. package/src/agents/snow.ts +239 -239
  82. package/src/cli/main.ts +425 -372
  83. package/src/cli/mode.ts +58 -58
  84. package/src/cli/tui.ts +272 -269
  85. package/src/core/agent/task.ts +100 -0
  86. package/src/core/agent.ts +1446 -1549
  87. package/src/core/agent_helpers.ts +496 -461
  88. package/src/core/arbitrate.ts +162 -162
  89. package/src/core/catalog.ts +178 -0
  90. package/src/core/checkpoint.ts +94 -94
  91. package/src/core/config.ts +20 -4
  92. package/src/core/estimate.ts +104 -104
  93. package/src/core/evolve.ts +191 -191
  94. package/src/core/factory.ts +627 -627
  95. package/src/core/filter.ts +103 -103
  96. package/src/core/graph.ts +156 -156
  97. package/src/core/icons.ts +53 -53
  98. package/src/core/index.ts +37 -37
  99. package/src/core/learn.ts +146 -146
  100. package/src/core/llm.ts +108 -5
  101. package/src/core/longdoc.ts +155 -155
  102. package/src/core/mcp_server.ts +176 -176
  103. package/src/core/memory.ts +1178 -1171
  104. package/src/core/profile.ts +255 -255
  105. package/src/core/router.ts +124 -124
  106. package/src/core/sandbox.ts +142 -142
  107. package/src/core/security.ts +243 -243
  108. package/src/core/skill.ts +342 -342
  109. package/src/core/theme.ts +65 -0
  110. package/src/core/tool_router.ts +193 -193
  111. package/src/core/vector.ts +152 -152
  112. package/src/core/workspace.ts +150 -150
  113. package/src/plugins/loader.ts +66 -66
  114. package/src/skills/loader.ts +46 -46
  115. package/src/sql.js.d.ts +29 -29
  116. package/src/tools/builtin.ts +380 -380
  117. package/src/tools/computer.ts +269 -269
  118. package/src/tools/delegate.ts +49 -49
  119. package/src/web/server.ts +660 -634
  120. package/src/web/tts.ts +93 -93
  121. package/tests/agent_helpers.test.ts +48 -0
  122. package/tests/bus.test.ts +121 -121
  123. package/tests/catalog.test.ts +86 -0
  124. package/tests/config.test.ts +41 -0
  125. package/tests/icons.test.ts +45 -45
  126. package/tests/memory.test.ts +147 -0
  127. package/tests/router.test.ts +86 -86
  128. package/tests/schemas.test.ts +51 -51
  129. package/tests/semantic.test.ts +83 -83
  130. package/tests/setup.ts +10 -10
  131. package/tests/skill.test.ts +172 -172
  132. package/tests/task.test.ts +60 -0
  133. package/tests/tool.test.ts +108 -108
  134. package/tests/tool_router.test.ts +71 -71
  135. package/vitest.config.ts +17 -17
@@ -1,255 +1,255 @@
1
- /**
2
- * Local user profile (用户画像) + per-agent custom personas.
3
- *
4
- * Everything here lives under ~/.skyloom/ and never leaves the machine:
5
- *
6
- * - profile.json — a free-form dict of facts about the user.
7
- * - memories.json — running narrative of moods, life events, things worth following up on.
8
- * - personas/<agent>.md — optional custom role for a specific agent.
9
- */
10
-
11
- import * as fs from 'fs';
12
- import * as path from 'path';
13
- import { USER_CONFIG_DIR, AGENT_NAMES } from './config';
14
-
15
- const VALID_AGENTS = new Set<string>(AGENT_NAMES);
16
-
17
- // Keep only the most recent N memories in the prompt + on disk.
18
- const MEMORY_CAP = 40;
19
- // When over cap, fold this many of the oldest notes into ONE digest entry.
20
- const FOLD_BATCH = 8;
21
-
22
- // Pluggable summarizer for memory folding.
23
- let _summarizer: ((notes: string[]) => string) | null = null;
24
-
25
- export function setMemorySummarizer(fn: (notes: string[]) => string): void {
26
- _summarizer = fn;
27
- }
28
-
29
- function profilePath(): string {
30
- return path.join(USER_CONFIG_DIR, 'profile.json');
31
- }
32
-
33
- function memoriesPath(): string {
34
- return path.join(USER_CONFIG_DIR, 'memories.json');
35
- }
36
-
37
- function personaPath(agent: string): string {
38
- return path.join(USER_CONFIG_DIR, 'personas', `${agent}.md`);
39
- }
40
-
41
- // ── User profile ──
42
-
43
- export function loadProfile(): Record<string, string> {
44
- const p = profilePath();
45
- if (!fs.existsSync(p)) return {};
46
- try {
47
- const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
48
- return typeof data === 'object' && data !== null ? data : {};
49
- } catch {
50
- return {};
51
- }
52
- }
53
-
54
- export function saveProfile(data: Record<string, string>): void {
55
- const p = profilePath();
56
- const dir = path.dirname(p);
57
- if (!fs.existsSync(dir)) {
58
- fs.mkdirSync(dir, { recursive: true });
59
- }
60
- const tmp = p + '.tmp';
61
- fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
62
- fs.renameSync(tmp, p);
63
- }
64
-
65
- export function setProfileField(key: string, value: string): void {
66
- key = (key || '').trim();
67
- if (!key) return;
68
- const data = loadProfile();
69
- data[key] = value;
70
- saveProfile(data);
71
- }
72
-
73
- export function clearProfileField(key?: string | null): void {
74
- if (key == null) {
75
- try {
76
- const p = profilePath();
77
- if (fs.existsSync(p)) fs.unlinkSync(p);
78
- } catch { /* ignore */ }
79
- return;
80
- }
81
- const data = loadProfile();
82
- delete data[key];
83
- saveProfile(data);
84
- }
85
-
86
- export function formatProfileForPrompt(lang: string = 'zh'): string {
87
- const data = loadProfile();
88
- const entries = Object.entries(data);
89
- if (entries.length === 0) return '';
90
-
91
- const lines = entries.map(([k, v]) => {
92
- return lang === 'en' ? `- ${k}: ${v}` : `- ${k}:${v}`;
93
- });
94
- const body = lines.join('\n');
95
-
96
- if (lang === 'en') {
97
- return '\n\n## About the user (remember this and use it naturally)\n' + body;
98
- }
99
- return '\n\n## 关于用户(记住,并在对话中自然运用,不要生硬复述)\n' + body;
100
- }
101
-
102
- // ── Emotional / narrative memory ──
103
-
104
- export function loadMemories(): Record<string, any>[] {
105
- const p = memoriesPath();
106
- if (!fs.existsSync(p)) return [];
107
- try {
108
- const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
109
- return Array.isArray(data) ? data : [];
110
- } catch {
111
- return [];
112
- }
113
- }
114
-
115
- function norm(text: string): string {
116
- return text.toLowerCase().split('').filter(c => !c.match(/\s/) && !',。,.、!!??~…「」"\',。、;:?!'.includes(c)).join('');
117
- }
118
-
119
- function writeMemories(items: Record<string, any>[]): void {
120
- const p = memoriesPath();
121
- const dir = path.dirname(p);
122
- if (!fs.existsSync(dir)) {
123
- fs.mkdirSync(dir, { recursive: true });
124
- }
125
- const tmp = p + '.tmp';
126
- fs.writeFileSync(tmp, JSON.stringify(items, null, 2), 'utf-8');
127
- fs.renameSync(tmp, p);
128
- }
129
-
130
- function digest(notes: string[]): string {
131
- const seen: string[] = [];
132
- for (let n of notes) {
133
- n = n.trim();
134
- if (n.startsWith('早些时候:')) {
135
- n = n.slice('早些时候:'.length);
136
- }
137
- for (const part of n.split(';')) {
138
- const p = part.trim();
139
- if (p && !seen.includes(p)) {
140
- seen.push(p);
141
- }
142
- }
143
- }
144
- let joined = seen.join(';');
145
- if (joined.length > 180) {
146
- joined = joined.slice(0, 179) + '…';
147
- }
148
- return '早些时候:' + joined;
149
- }
150
-
151
- function summarizeNotes(notes: string[]): string {
152
- if (_summarizer) {
153
- try {
154
- const out = _summarizer(notes);
155
- if (out && out.trim()) return out.trim();
156
- } catch { /* ignore */ }
157
- }
158
- return digest(notes);
159
- }
160
-
161
- function foldOldest(items: Record<string, any>[]): Record<string, any>[] {
162
- let foldN = (items.length - MEMORY_CAP) + FOLD_BATCH;
163
- foldN = Math.min(foldN, items.length - 1);
164
- if (foldN <= 0) return items.slice(-MEMORY_CAP);
165
-
166
- const old = items.slice(0, foldN);
167
- const rest = items.slice(foldN);
168
- const digestEntry: Record<string, any> = {
169
- ts: old[old.length - 1]?.ts || new Date().toISOString().slice(0, 10),
170
- note: summarizeNotes(old.map(m => String(m.note || ''))),
171
- summary: true,
172
- };
173
- return [digestEntry, ...rest];
174
- }
175
-
176
- export function appendMemory(note: string): boolean {
177
- note = (note || '').trim();
178
- if (!note) return false;
179
-
180
- const items = loadMemories();
181
- const key = norm(note);
182
-
183
- if (key) {
184
- for (const m of items) {
185
- if (norm(String(m.note || '')) === key) {
186
- m.ts = new Date().toISOString().slice(0, 10); // refresh recency
187
- if (note.length > String(m.note || '').length) {
188
- m.note = note; // keep the richer phrasing
189
- }
190
- writeMemories(items);
191
- return true;
192
- }
193
- }
194
- }
195
-
196
- items.push({ ts: new Date().toISOString().slice(0, 10), note });
197
- if (items.length > MEMORY_CAP) {
198
- writeMemories(foldOldest(items));
199
- } else {
200
- writeMemories(items);
201
- }
202
- return true;
203
- }
204
-
205
- export function clearMemories(): void {
206
- try {
207
- const p = memoriesPath();
208
- if (fs.existsSync(p)) fs.unlinkSync(p);
209
- } catch { /* ignore */ }
210
- }
211
-
212
- export function formatMemoriesForPrompt(lang: string = 'zh', limit: number = 12): string {
213
- const items = loadMemories();
214
- if (items.length === 0) return '';
215
-
216
- const recent = items.slice(-limit);
217
- const lines = recent.map((m: any) => `- [${m.ts || ''}] ${m.note || ''}`);
218
- const body = lines.join('\n');
219
-
220
- if (lang === 'en') {
221
- return '\n\n## What you remember about them (recent context — weave in naturally, never recite)\n' + body;
222
- }
223
- return '\n\n## 你记得关于 ta 的事(近期,自然带出,别生硬复述)\n' + body;
224
- }
225
-
226
- // ── Per-agent custom persona ──
227
-
228
- export function loadPersona(agent: string): string | null {
229
- const p = personaPath(agent);
230
- if (!fs.existsSync(p)) return null;
231
- try {
232
- const text = fs.readFileSync(p, 'utf-8').trim();
233
- return text || null;
234
- } catch {
235
- return null;
236
- }
237
- }
238
-
239
- export function savePersona(agent: string, text: string): boolean {
240
- if (!VALID_AGENTS.has(agent)) return false;
241
- const p = personaPath(agent);
242
- const dir = path.dirname(p);
243
- if (!fs.existsSync(dir)) {
244
- fs.mkdirSync(dir, { recursive: true });
245
- }
246
- fs.writeFileSync(p, text.trim() + '\n', 'utf-8');
247
- return true;
248
- }
249
-
250
- export function clearPersona(agent: string): void {
251
- try {
252
- const p = personaPath(agent);
253
- if (fs.existsSync(p)) fs.unlinkSync(p);
254
- } catch { /* ignore */ }
255
- }
1
+ /**
2
+ * Local user profile (用户画像) + per-agent custom personas.
3
+ *
4
+ * Everything here lives under ~/.skyloom/ and never leaves the machine:
5
+ *
6
+ * - profile.json — a free-form dict of facts about the user.
7
+ * - memories.json — running narrative of moods, life events, things worth following up on.
8
+ * - personas/<agent>.md — optional custom role for a specific agent.
9
+ */
10
+
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { USER_CONFIG_DIR, AGENT_NAMES } from './config';
14
+
15
+ const VALID_AGENTS = new Set<string>(AGENT_NAMES);
16
+
17
+ // Keep only the most recent N memories in the prompt + on disk.
18
+ const MEMORY_CAP = 40;
19
+ // When over cap, fold this many of the oldest notes into ONE digest entry.
20
+ const FOLD_BATCH = 8;
21
+
22
+ // Pluggable summarizer for memory folding.
23
+ let _summarizer: ((notes: string[]) => string) | null = null;
24
+
25
+ export function setMemorySummarizer(fn: (notes: string[]) => string): void {
26
+ _summarizer = fn;
27
+ }
28
+
29
+ function profilePath(): string {
30
+ return path.join(USER_CONFIG_DIR, 'profile.json');
31
+ }
32
+
33
+ function memoriesPath(): string {
34
+ return path.join(USER_CONFIG_DIR, 'memories.json');
35
+ }
36
+
37
+ function personaPath(agent: string): string {
38
+ return path.join(USER_CONFIG_DIR, 'personas', `${agent}.md`);
39
+ }
40
+
41
+ // ── User profile ──
42
+
43
+ export function loadProfile(): Record<string, string> {
44
+ const p = profilePath();
45
+ if (!fs.existsSync(p)) return {};
46
+ try {
47
+ const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
48
+ return typeof data === 'object' && data !== null ? data : {};
49
+ } catch {
50
+ return {};
51
+ }
52
+ }
53
+
54
+ export function saveProfile(data: Record<string, string>): void {
55
+ const p = profilePath();
56
+ const dir = path.dirname(p);
57
+ if (!fs.existsSync(dir)) {
58
+ fs.mkdirSync(dir, { recursive: true });
59
+ }
60
+ const tmp = p + '.tmp';
61
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2), 'utf-8');
62
+ fs.renameSync(tmp, p);
63
+ }
64
+
65
+ export function setProfileField(key: string, value: string): void {
66
+ key = (key || '').trim();
67
+ if (!key) return;
68
+ const data = loadProfile();
69
+ data[key] = value;
70
+ saveProfile(data);
71
+ }
72
+
73
+ export function clearProfileField(key?: string | null): void {
74
+ if (key == null) {
75
+ try {
76
+ const p = profilePath();
77
+ if (fs.existsSync(p)) fs.unlinkSync(p);
78
+ } catch { /* ignore */ }
79
+ return;
80
+ }
81
+ const data = loadProfile();
82
+ delete data[key];
83
+ saveProfile(data);
84
+ }
85
+
86
+ export function formatProfileForPrompt(lang: string = 'zh'): string {
87
+ const data = loadProfile();
88
+ const entries = Object.entries(data);
89
+ if (entries.length === 0) return '';
90
+
91
+ const lines = entries.map(([k, v]) => {
92
+ return lang === 'en' ? `- ${k}: ${v}` : `- ${k}:${v}`;
93
+ });
94
+ const body = lines.join('\n');
95
+
96
+ if (lang === 'en') {
97
+ return '\n\n## About the user (remember this and use it naturally)\n' + body;
98
+ }
99
+ return '\n\n## 关于用户(记住,并在对话中自然运用,不要生硬复述)\n' + body;
100
+ }
101
+
102
+ // ── Emotional / narrative memory ──
103
+
104
+ export function loadMemories(): Record<string, any>[] {
105
+ const p = memoriesPath();
106
+ if (!fs.existsSync(p)) return [];
107
+ try {
108
+ const data = JSON.parse(fs.readFileSync(p, 'utf-8'));
109
+ return Array.isArray(data) ? data : [];
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+
115
+ function norm(text: string): string {
116
+ return text.toLowerCase().split('').filter(c => !c.match(/\s/) && !',。,.、!!??~…「」"\',。、;:?!'.includes(c)).join('');
117
+ }
118
+
119
+ function writeMemories(items: Record<string, any>[]): void {
120
+ const p = memoriesPath();
121
+ const dir = path.dirname(p);
122
+ if (!fs.existsSync(dir)) {
123
+ fs.mkdirSync(dir, { recursive: true });
124
+ }
125
+ const tmp = p + '.tmp';
126
+ fs.writeFileSync(tmp, JSON.stringify(items, null, 2), 'utf-8');
127
+ fs.renameSync(tmp, p);
128
+ }
129
+
130
+ function digest(notes: string[]): string {
131
+ const seen: string[] = [];
132
+ for (let n of notes) {
133
+ n = n.trim();
134
+ if (n.startsWith('早些时候:')) {
135
+ n = n.slice('早些时候:'.length);
136
+ }
137
+ for (const part of n.split(';')) {
138
+ const p = part.trim();
139
+ if (p && !seen.includes(p)) {
140
+ seen.push(p);
141
+ }
142
+ }
143
+ }
144
+ let joined = seen.join(';');
145
+ if (joined.length > 180) {
146
+ joined = joined.slice(0, 179) + '…';
147
+ }
148
+ return '早些时候:' + joined;
149
+ }
150
+
151
+ function summarizeNotes(notes: string[]): string {
152
+ if (_summarizer) {
153
+ try {
154
+ const out = _summarizer(notes);
155
+ if (out && out.trim()) return out.trim();
156
+ } catch { /* ignore */ }
157
+ }
158
+ return digest(notes);
159
+ }
160
+
161
+ function foldOldest(items: Record<string, any>[]): Record<string, any>[] {
162
+ let foldN = (items.length - MEMORY_CAP) + FOLD_BATCH;
163
+ foldN = Math.min(foldN, items.length - 1);
164
+ if (foldN <= 0) return items.slice(-MEMORY_CAP);
165
+
166
+ const old = items.slice(0, foldN);
167
+ const rest = items.slice(foldN);
168
+ const digestEntry: Record<string, any> = {
169
+ ts: old[old.length - 1]?.ts || new Date().toISOString().slice(0, 10),
170
+ note: summarizeNotes(old.map(m => String(m.note || ''))),
171
+ summary: true,
172
+ };
173
+ return [digestEntry, ...rest];
174
+ }
175
+
176
+ export function appendMemory(note: string): boolean {
177
+ note = (note || '').trim();
178
+ if (!note) return false;
179
+
180
+ const items = loadMemories();
181
+ const key = norm(note);
182
+
183
+ if (key) {
184
+ for (const m of items) {
185
+ if (norm(String(m.note || '')) === key) {
186
+ m.ts = new Date().toISOString().slice(0, 10); // refresh recency
187
+ if (note.length > String(m.note || '').length) {
188
+ m.note = note; // keep the richer phrasing
189
+ }
190
+ writeMemories(items);
191
+ return true;
192
+ }
193
+ }
194
+ }
195
+
196
+ items.push({ ts: new Date().toISOString().slice(0, 10), note });
197
+ if (items.length > MEMORY_CAP) {
198
+ writeMemories(foldOldest(items));
199
+ } else {
200
+ writeMemories(items);
201
+ }
202
+ return true;
203
+ }
204
+
205
+ export function clearMemories(): void {
206
+ try {
207
+ const p = memoriesPath();
208
+ if (fs.existsSync(p)) fs.unlinkSync(p);
209
+ } catch { /* ignore */ }
210
+ }
211
+
212
+ export function formatMemoriesForPrompt(lang: string = 'zh', limit: number = 12): string {
213
+ const items = loadMemories();
214
+ if (items.length === 0) return '';
215
+
216
+ const recent = items.slice(-limit);
217
+ const lines = recent.map((m: any) => `- [${m.ts || ''}] ${m.note || ''}`);
218
+ const body = lines.join('\n');
219
+
220
+ if (lang === 'en') {
221
+ return '\n\n## What you remember about them (recent context — weave in naturally, never recite)\n' + body;
222
+ }
223
+ return '\n\n## 你记得关于 ta 的事(近期,自然带出,别生硬复述)\n' + body;
224
+ }
225
+
226
+ // ── Per-agent custom persona ──
227
+
228
+ export function loadPersona(agent: string): string | null {
229
+ const p = personaPath(agent);
230
+ if (!fs.existsSync(p)) return null;
231
+ try {
232
+ const text = fs.readFileSync(p, 'utf-8').trim();
233
+ return text || null;
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+
239
+ export function savePersona(agent: string, text: string): boolean {
240
+ if (!VALID_AGENTS.has(agent)) return false;
241
+ const p = personaPath(agent);
242
+ const dir = path.dirname(p);
243
+ if (!fs.existsSync(dir)) {
244
+ fs.mkdirSync(dir, { recursive: true });
245
+ }
246
+ fs.writeFileSync(p, text.trim() + '\n', 'utf-8');
247
+ return true;
248
+ }
249
+
250
+ export function clearPersona(agent: string): void {
251
+ try {
252
+ const p = personaPath(agent);
253
+ if (fs.existsSync(p)) fs.unlinkSync(p);
254
+ } catch { /* ignore */ }
255
+ }