skyloom 1.4.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 (225) hide show
  1. package/.github/workflows/ci.yml +36 -0
  2. package/CONVERSION_PLAN.md +191 -0
  3. package/README.md +67 -0
  4. package/dist/agents/dew.d.ts +15 -0
  5. package/dist/agents/dew.d.ts.map +1 -0
  6. package/dist/agents/dew.js +74 -0
  7. package/dist/agents/dew.js.map +1 -0
  8. package/dist/agents/fair.d.ts +15 -0
  9. package/dist/agents/fair.d.ts.map +1 -0
  10. package/dist/agents/fair.js +106 -0
  11. package/dist/agents/fair.js.map +1 -0
  12. package/dist/agents/fog.d.ts +15 -0
  13. package/dist/agents/fog.d.ts.map +1 -0
  14. package/dist/agents/fog.js +52 -0
  15. package/dist/agents/fog.js.map +1 -0
  16. package/dist/agents/frost.d.ts +15 -0
  17. package/dist/agents/frost.d.ts.map +1 -0
  18. package/dist/agents/frost.js +54 -0
  19. package/dist/agents/frost.js.map +1 -0
  20. package/dist/agents/rain.d.ts +15 -0
  21. package/dist/agents/rain.d.ts.map +1 -0
  22. package/dist/agents/rain.js +54 -0
  23. package/dist/agents/rain.js.map +1 -0
  24. package/dist/agents/snow.d.ts +27 -0
  25. package/dist/agents/snow.d.ts.map +1 -0
  26. package/dist/agents/snow.js +226 -0
  27. package/dist/agents/snow.js.map +1 -0
  28. package/dist/cli/main.d.ts +7 -0
  29. package/dist/cli/main.d.ts.map +1 -0
  30. package/dist/cli/main.js +402 -0
  31. package/dist/cli/main.js.map +1 -0
  32. package/dist/cli/mode.d.ts +17 -0
  33. package/dist/cli/mode.d.ts.map +1 -0
  34. package/dist/cli/mode.js +56 -0
  35. package/dist/cli/mode.js.map +1 -0
  36. package/dist/core/agent.d.ts +174 -0
  37. package/dist/core/agent.d.ts.map +1 -0
  38. package/dist/core/agent.js +1332 -0
  39. package/dist/core/agent.js.map +1 -0
  40. package/dist/core/agent_helpers.d.ts +51 -0
  41. package/dist/core/agent_helpers.d.ts.map +1 -0
  42. package/dist/core/agent_helpers.js +477 -0
  43. package/dist/core/agent_helpers.js.map +1 -0
  44. package/dist/core/bus.d.ts +99 -0
  45. package/dist/core/bus.d.ts.map +1 -0
  46. package/dist/core/bus.js +191 -0
  47. package/dist/core/bus.js.map +1 -0
  48. package/dist/core/cache.d.ts +63 -0
  49. package/dist/core/cache.d.ts.map +1 -0
  50. package/dist/core/cache.js +121 -0
  51. package/dist/core/cache.js.map +1 -0
  52. package/dist/core/checkpoint.d.ts +19 -0
  53. package/dist/core/checkpoint.d.ts.map +1 -0
  54. package/dist/core/checkpoint.js +120 -0
  55. package/dist/core/checkpoint.js.map +1 -0
  56. package/dist/core/circuit_breaker.d.ts +46 -0
  57. package/dist/core/circuit_breaker.d.ts.map +1 -0
  58. package/dist/core/circuit_breaker.js +99 -0
  59. package/dist/core/circuit_breaker.js.map +1 -0
  60. package/dist/core/config.d.ts +97 -0
  61. package/dist/core/config.d.ts.map +1 -0
  62. package/dist/core/config.js +281 -0
  63. package/dist/core/config.js.map +1 -0
  64. package/dist/core/constants.d.ts +78 -0
  65. package/dist/core/constants.d.ts.map +1 -0
  66. package/dist/core/constants.js +84 -0
  67. package/dist/core/constants.js.map +1 -0
  68. package/dist/core/factory.d.ts +63 -0
  69. package/dist/core/factory.d.ts.map +1 -0
  70. package/dist/core/factory.js +537 -0
  71. package/dist/core/factory.js.map +1 -0
  72. package/dist/core/icons.d.ts +28 -0
  73. package/dist/core/icons.d.ts.map +1 -0
  74. package/dist/core/icons.js +86 -0
  75. package/dist/core/icons.js.map +1 -0
  76. package/dist/core/index.d.ts +29 -0
  77. package/dist/core/index.d.ts.map +1 -0
  78. package/dist/core/index.js +54 -0
  79. package/dist/core/index.js.map +1 -0
  80. package/dist/core/llm.d.ts +121 -0
  81. package/dist/core/llm.d.ts.map +1 -0
  82. package/dist/core/llm.js +532 -0
  83. package/dist/core/llm.js.map +1 -0
  84. package/dist/core/logger.d.ts +57 -0
  85. package/dist/core/logger.d.ts.map +1 -0
  86. package/dist/core/logger.js +122 -0
  87. package/dist/core/logger.js.map +1 -0
  88. package/dist/core/mcp.d.ts +190 -0
  89. package/dist/core/mcp.d.ts.map +1 -0
  90. package/dist/core/mcp.js +822 -0
  91. package/dist/core/mcp.js.map +1 -0
  92. package/dist/core/mcp_server.d.ts +26 -0
  93. package/dist/core/mcp_server.d.ts.map +1 -0
  94. package/dist/core/mcp_server.js +211 -0
  95. package/dist/core/mcp_server.js.map +1 -0
  96. package/dist/core/memory.d.ts +190 -0
  97. package/dist/core/memory.d.ts.map +1 -0
  98. package/dist/core/memory.js +988 -0
  99. package/dist/core/memory.js.map +1 -0
  100. package/dist/core/middleware.d.ts +114 -0
  101. package/dist/core/middleware.d.ts.map +1 -0
  102. package/dist/core/middleware.js +248 -0
  103. package/dist/core/middleware.js.map +1 -0
  104. package/dist/core/pipelines.d.ts +87 -0
  105. package/dist/core/pipelines.d.ts.map +1 -0
  106. package/dist/core/pipelines.js +301 -0
  107. package/dist/core/pipelines.js.map +1 -0
  108. package/dist/core/profile.d.ts +23 -0
  109. package/dist/core/profile.d.ts.map +1 -0
  110. package/dist/core/profile.js +289 -0
  111. package/dist/core/profile.js.map +1 -0
  112. package/dist/core/router.d.ts +24 -0
  113. package/dist/core/router.d.ts.map +1 -0
  114. package/dist/core/router.js +111 -0
  115. package/dist/core/router.js.map +1 -0
  116. package/dist/core/schemas.d.ts +82 -0
  117. package/dist/core/schemas.d.ts.map +1 -0
  118. package/dist/core/schemas.js +200 -0
  119. package/dist/core/schemas.js.map +1 -0
  120. package/dist/core/semantic.d.ts +92 -0
  121. package/dist/core/semantic.d.ts.map +1 -0
  122. package/dist/core/semantic.js +175 -0
  123. package/dist/core/semantic.js.map +1 -0
  124. package/dist/core/skill.d.ts +68 -0
  125. package/dist/core/skill.d.ts.map +1 -0
  126. package/dist/core/skill.js +350 -0
  127. package/dist/core/skill.js.map +1 -0
  128. package/dist/core/tool.d.ts +99 -0
  129. package/dist/core/tool.d.ts.map +1 -0
  130. package/dist/core/tool.js +341 -0
  131. package/dist/core/tool.js.map +1 -0
  132. package/dist/core/tool_router.d.ts +29 -0
  133. package/dist/core/tool_router.d.ts.map +1 -0
  134. package/dist/core/tool_router.js +172 -0
  135. package/dist/core/tool_router.js.map +1 -0
  136. package/dist/core/workspace.d.ts +48 -0
  137. package/dist/core/workspace.d.ts.map +1 -0
  138. package/dist/core/workspace.js +179 -0
  139. package/dist/core/workspace.js.map +1 -0
  140. package/dist/plugins/loader.d.ts +17 -0
  141. package/dist/plugins/loader.d.ts.map +1 -0
  142. package/dist/plugins/loader.js +96 -0
  143. package/dist/plugins/loader.js.map +1 -0
  144. package/dist/skills/loader.d.ts +9 -0
  145. package/dist/skills/loader.d.ts.map +1 -0
  146. package/dist/skills/loader.js +78 -0
  147. package/dist/skills/loader.js.map +1 -0
  148. package/dist/tools/builtin.d.ts +10 -0
  149. package/dist/tools/builtin.d.ts.map +1 -0
  150. package/dist/tools/builtin.js +414 -0
  151. package/dist/tools/builtin.js.map +1 -0
  152. package/dist/tools/computer.d.ts +12 -0
  153. package/dist/tools/computer.d.ts.map +1 -0
  154. package/dist/tools/computer.js +326 -0
  155. package/dist/tools/computer.js.map +1 -0
  156. package/dist/tools/delegate.d.ts +10 -0
  157. package/dist/tools/delegate.d.ts.map +1 -0
  158. package/dist/tools/delegate.js +45 -0
  159. package/dist/tools/delegate.js.map +1 -0
  160. package/dist/web/server.d.ts +5 -0
  161. package/dist/web/server.d.ts.map +1 -0
  162. package/dist/web/server.js +647 -0
  163. package/dist/web/server.js.map +1 -0
  164. package/dist/web/tts.d.ts +33 -0
  165. package/dist/web/tts.d.ts.map +1 -0
  166. package/dist/web/tts.js +69 -0
  167. package/dist/web/tts.js.map +1 -0
  168. package/package.json +60 -0
  169. package/scripts/install.js +48 -0
  170. package/scripts/link.js +10 -0
  171. package/setup.bat +79 -0
  172. package/skill-test-ty2fOA/test.md +10 -0
  173. package/src/agents/dew.ts +70 -0
  174. package/src/agents/fair.ts +102 -0
  175. package/src/agents/fog.ts +48 -0
  176. package/src/agents/frost.ts +50 -0
  177. package/src/agents/rain.ts +50 -0
  178. package/src/agents/snow.ts +239 -0
  179. package/src/cli/main.ts +405 -0
  180. package/src/cli/mode.ts +58 -0
  181. package/src/core/agent.ts +1506 -0
  182. package/src/core/agent_helpers.ts +461 -0
  183. package/src/core/bus.ts +221 -0
  184. package/src/core/cache.ts +153 -0
  185. package/src/core/checkpoint.ts +94 -0
  186. package/src/core/circuit_breaker.ts +119 -0
  187. package/src/core/config.ts +341 -0
  188. package/src/core/constants.ts +95 -0
  189. package/src/core/factory.ts +627 -0
  190. package/src/core/icons.ts +53 -0
  191. package/src/core/index.ts +31 -0
  192. package/src/core/llm.ts +724 -0
  193. package/src/core/logger.ts +144 -0
  194. package/src/core/mcp.ts +953 -0
  195. package/src/core/mcp_server.ts +176 -0
  196. package/src/core/memory.ts +1169 -0
  197. package/src/core/middleware.ts +350 -0
  198. package/src/core/pipelines.ts +424 -0
  199. package/src/core/profile.ts +255 -0
  200. package/src/core/router.ts +124 -0
  201. package/src/core/schemas.ts +282 -0
  202. package/src/core/semantic.ts +211 -0
  203. package/src/core/skill.ts +342 -0
  204. package/src/core/tool.ts +427 -0
  205. package/src/core/tool_router.ts +193 -0
  206. package/src/core/workspace.ts +150 -0
  207. package/src/plugins/loader.ts +66 -0
  208. package/src/skills/loader.ts +46 -0
  209. package/src/sql.js.d.ts +29 -0
  210. package/src/tools/builtin.ts +382 -0
  211. package/src/tools/computer.ts +269 -0
  212. package/src/tools/delegate.ts +49 -0
  213. package/src/web/server.ts +634 -0
  214. package/src/web/tts.ts +93 -0
  215. package/tests/bus.test.ts +121 -0
  216. package/tests/icons.test.ts +45 -0
  217. package/tests/router.test.ts +86 -0
  218. package/tests/schemas.test.ts +51 -0
  219. package/tests/semantic.test.ts +83 -0
  220. package/tests/setup.ts +10 -0
  221. package/tests/skill.test.ts +172 -0
  222. package/tests/tool.test.ts +108 -0
  223. package/tests/tool_router.test.ts +71 -0
  224. package/tsconfig.json +37 -0
  225. package/vitest.config.ts +17 -0
@@ -0,0 +1,424 @@
1
+ /**
2
+ * Pipeline templates — predefined multi-agent DAGs.
3
+ *
4
+ * Why: Snow's task-decomposition LLM call costs 2-3k tokens per orchestration.
5
+ * For common, recognizable workflows (code review, research-then-write, etc.),
6
+ * we already know the right shape — paying an LLM to re-derive it every time
7
+ * is pure waste. A pipeline match short-circuits Snow's planner entirely.
8
+ *
9
+ * The match function is rules-only (keyword + regex), runs in microseconds, and
10
+ * falls back gracefully: ``matchPipeline`` returns null when nothing fits, and
11
+ * ``factory.orchestrateTask`` then takes the original LLM-planning path.
12
+ */
13
+
14
+ /**
15
+ * One step in a pipeline. Mirrors Task so we can map 1:1.
16
+ */
17
+ export interface PipelineStep {
18
+ readonly id: string;
19
+ readonly agent: string;
20
+ readonly descriptionTemplate: string; // `{goal}` placeholder gets substituted at build time
21
+ readonly dependsOn: readonly string[];
22
+ }
23
+
24
+ /**
25
+ * A predefined collaboration template.
26
+ */
27
+ export interface Pipeline {
28
+ readonly name: string;
29
+ readonly triggers: readonly string[]; // case-insensitive substring tokens
30
+ readonly steps: readonly PipelineStep[];
31
+ readonly requireRegex: readonly string[]; // optional regex requirement
32
+ }
33
+
34
+ /**
35
+ * Define a pipeline helper function.
36
+ */
37
+ function createPipeline(
38
+ name: string,
39
+ triggers: string[],
40
+ steps: PipelineStep[],
41
+ requireRegex?: string[]
42
+ ): Pipeline {
43
+ return {
44
+ name,
45
+ triggers: Object.freeze(triggers),
46
+ steps: Object.freeze(steps),
47
+ requireRegex: Object.freeze(requireRegex || []),
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Helper to create pipeline steps.
53
+ */
54
+ function createStep(
55
+ id: string,
56
+ agent: string,
57
+ descriptionTemplate: string,
58
+ dependsOn?: string[]
59
+ ): PipelineStep {
60
+ return Object.freeze({
61
+ id,
62
+ agent,
63
+ descriptionTemplate,
64
+ dependsOn: Object.freeze(dependsOn || []),
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Predefined pipeline templates.
70
+ */
71
+ const PIPELINES: readonly Pipeline[] = Object.freeze([
72
+ createPipeline('code_review', ['审查代码', '代码审查', 'code review', 'review my code', '审计安全', '安全审计'], [
73
+ createStep('1', 'frost', '审查代码: {goal}'),
74
+ ]),
75
+
76
+ createPipeline(
77
+ 'research_then_write',
78
+ ['调研后写', 'research and write', '先调研再写', 'research then write', '调研后生成', '调研并撰写'],
79
+ [
80
+ createStep('1', 'fog', '调研: {goal}'),
81
+ createStep('2', 'rain', '基于第 1 步的调研结果撰写: {goal}', ['1']),
82
+ ]
83
+ ),
84
+
85
+ createPipeline(
86
+ 'research_review_write',
87
+ ['调研审查后写', 'research review write', '先调研审查再写', '调研并审查后生成'],
88
+ [
89
+ createStep('1', 'fog', '调研: {goal}'),
90
+ createStep('2', 'frost', '审查第 1 步的调研结果,确认可行性', ['1']),
91
+ createStep('3', 'rain', '基于第 1 步调研和第 2 步审查意见撰写: {goal}', ['2']),
92
+ ]
93
+ ),
94
+
95
+ createPipeline(
96
+ 'implement_and_review',
97
+ ['实现并审查', '写完后审查', 'implement and review', '写代码并 review', '实现并审计'],
98
+ [
99
+ createStep('1', 'rain', '实现: {goal}'),
100
+ createStep('2', 'frost', '审查第 1 步的实现', ['1']),
101
+ ]
102
+ ),
103
+
104
+ createPipeline(
105
+ 'fix_and_verify',
106
+ ['修复并验证', 'fix and verify', '修复bug并验证', '修复后审查', 'bugfix review'],
107
+ [
108
+ createStep('1', 'rain', '修复: {goal}'),
109
+ createStep('2', 'frost', '验证第 1 步的修复是否正确', ['1']),
110
+ ]
111
+ ),
112
+
113
+ createPipeline(
114
+ 'implement_test_deploy',
115
+ ['实现测试部署', '实现并部署', '写完测试再部署', 'implement test deploy'],
116
+ [
117
+ createStep('1', 'rain', '实现: {goal}'),
118
+ createStep('2', 'frost', '审查第 1 步的实现', ['1']),
119
+ createStep('3', 'dew', '部署第 1 步的实现', ['2']),
120
+ ]
121
+ ),
122
+
123
+ createPipeline(
124
+ 'design_implement_review_deploy',
125
+ ['设计实现审查部署', '设计开发测试上线', 'design implement review deploy', '完整开发流程'],
126
+ [
127
+ createStep('1', 'fog', '设计方案: {goal}'),
128
+ createStep('2', 'rain', '根据第 1 步的设计实现: {goal}', ['1']),
129
+ createStep('3', 'frost', '审查第 2 步的实现代码', ['2']),
130
+ createStep('4', 'dew', '部署第 2 步的实现到生产环境', ['3']),
131
+ ]
132
+ ),
133
+
134
+ createPipeline(
135
+ 'investigate_report',
136
+ ['安全审计', 'security audit', '漏洞扫描', 'vulnerability scan', '安全检测'],
137
+ [
138
+ createStep('1', 'fog', '安全调研: {goal}'),
139
+ createStep('2', 'frost', '基于第 1 步的调研生成安全报告', ['1']),
140
+ ]
141
+ ),
142
+
143
+ createPipeline(
144
+ 'debug_and_deploy',
145
+ ['调试部署', 'debug and deploy', '修复并上线', 'hotfix deploy'],
146
+ [
147
+ createStep('1', 'rain', '调试修复: {goal}'),
148
+ createStep('2', 'dew', '部署第 1 步的修复', ['1']),
149
+ ]
150
+ ),
151
+ ]);
152
+
153
+ /**
154
+ * Compiled regex cache for require_regex.
155
+ */
156
+ const REGEX_CACHE: Map<number, RegExp[]> = new Map();
157
+
158
+ /**
159
+ * Compile and cache regex patterns.
160
+ */
161
+ function getCompiledRegex(p: Pipeline): RegExp[] {
162
+ const key = p.requireRegex ? p.requireRegex.join('|').hashCode() : 0;
163
+
164
+ if (REGEX_CACHE.has(key)) {
165
+ return REGEX_CACHE.get(key)!;
166
+ }
167
+
168
+ const compiled = p.requireRegex.map(rx => new RegExp(rx, 'i'));
169
+ REGEX_CACHE.set(key, compiled);
170
+ return compiled;
171
+ }
172
+
173
+ /**
174
+ * Hash function for strings (simple implementation).
175
+ */
176
+ declare global {
177
+ interface String {
178
+ hashCode(): number;
179
+ }
180
+ }
181
+
182
+ String.prototype.hashCode = function (): number {
183
+ let hash = 0;
184
+ if (this.length === 0) return hash;
185
+ for (let i = 0; i < this.length; i++) {
186
+ const char = this.charCodeAt(i);
187
+ hash = (hash << 5) - hash + char;
188
+ hash = hash & hash; // Convert to 32bit integer
189
+ }
190
+ return Math.abs(hash);
191
+ };
192
+
193
+ /**
194
+ * Match a goal string to a pipeline.
195
+ *
196
+ * Matching is case-insensitive substring + optional regex. Fast (~10us).
197
+ * Returns the first matching pipeline, or null if no match.
198
+ */
199
+ export function matchPipeline(goal: string): Pipeline | null {
200
+ if (!goal) {
201
+ return null;
202
+ }
203
+
204
+ const lower = goal.toLowerCase();
205
+
206
+ for (const p of PIPELINES) {
207
+ // Check triggers
208
+ if (!p.triggers.some(tok => lower.includes(tok.toLowerCase()))) {
209
+ continue;
210
+ }
211
+
212
+ // Check regex if required
213
+ if (p.requireRegex.length > 0) {
214
+ const regexes = getCompiledRegex(p);
215
+ if (!regexes.some(rx => rx.test(lower))) {
216
+ continue;
217
+ }
218
+ }
219
+
220
+ return p;
221
+ }
222
+
223
+ return null;
224
+ }
225
+
226
+ /**
227
+ * Task interface (from agent.ts, used for pipeline materialization).
228
+ */
229
+ export interface Task {
230
+ id: string;
231
+ description: string;
232
+ assignedTo: string;
233
+ parentId: string | null;
234
+ dependsOn: string[];
235
+ metadata?: Record<string, any>;
236
+ createdAt?: Date;
237
+ status?: string;
238
+ result?: string;
239
+ }
240
+
241
+ /**
242
+ * Materialize a pipeline into runtime Task objects (full DAG).
243
+ */
244
+ export function buildTasksFromPipeline(pipeline: Pipeline, goal: string): Task[] {
245
+ const tasks: Task[] = [];
246
+
247
+ for (const step of pipeline.steps) {
248
+ const description = step.descriptionTemplate.replace('{goal}', goal);
249
+ const depends = Array.from(step.dependsOn);
250
+ const parentId = depends.length > 0 ? depends[0] : null;
251
+
252
+ tasks.push({
253
+ id: step.id,
254
+ description,
255
+ assignedTo: step.agent,
256
+ parentId,
257
+ dependsOn: depends,
258
+ metadata: {
259
+ goal,
260
+ pipeline: pipeline.name,
261
+ },
262
+ createdAt: new Date(),
263
+ status: 'pending',
264
+ });
265
+ }
266
+
267
+ return tasks;
268
+ }
269
+
270
+ /**
271
+ * List all available pipelines for CLI/debug introspection.
272
+ */
273
+ export function listPipelines(): Record<string, any>[] {
274
+ return PIPELINES.map(p => ({
275
+ name: p.name,
276
+ triggers: Array.from(p.triggers),
277
+ steps: p.steps.map(s => ({
278
+ id: s.id,
279
+ agent: s.agent,
280
+ dependsOn: Array.from(s.dependsOn),
281
+ })),
282
+ }));
283
+ }
284
+
285
+ /**
286
+ * Get a pipeline by name.
287
+ */
288
+ export function getPipelineByName(name: string): Pipeline | null {
289
+ return PIPELINES.find(p => p.name === name) || null;
290
+ }
291
+
292
+ /**
293
+ * Get matching pipelines for a goal (all matches, not just first).
294
+ */
295
+ export function matchAllPipelines(goal: string): Pipeline[] {
296
+ if (!goal) {
297
+ return [];
298
+ }
299
+
300
+ const lower = goal.toLowerCase();
301
+ const matches: Pipeline[] = [];
302
+
303
+ for (const p of PIPELINES) {
304
+ if (!p.triggers.some(tok => lower.includes(tok.toLowerCase()))) {
305
+ continue;
306
+ }
307
+
308
+ if (p.requireRegex.length > 0) {
309
+ const regexes = getCompiledRegex(p);
310
+ if (!regexes.some(rx => rx.test(lower))) {
311
+ continue;
312
+ }
313
+ }
314
+
315
+ matches.push(p);
316
+ }
317
+
318
+ return matches;
319
+ }
320
+
321
+ /**
322
+ * Validate a DAG for cycles and missing dependencies.
323
+ */
324
+ export function validateDAG(tasks: Task[]): { valid: boolean; errors: string[] } {
325
+ const errors: string[] = [];
326
+ const taskIds = new Set(tasks.map(t => t.id));
327
+
328
+ // Check for missing dependencies
329
+ for (const task of tasks) {
330
+ for (const depId of task.dependsOn) {
331
+ if (!taskIds.has(depId)) {
332
+ errors.push(`Task ${task.id} depends on non-existent task ${depId}`);
333
+ }
334
+ }
335
+ }
336
+
337
+ // Simple cycle detection (DFS)
338
+ const visited = new Set<string>();
339
+ const recursionStack = new Set<string>();
340
+
341
+ const hasCycle = (taskId: string): boolean => {
342
+ visited.add(taskId);
343
+ recursionStack.add(taskId);
344
+
345
+ const task = tasks.find(t => t.id === taskId);
346
+ if (task) {
347
+ for (const depId of task.dependsOn) {
348
+ if (!visited.has(depId)) {
349
+ if (hasCycle(depId)) {
350
+ return true;
351
+ }
352
+ } else if (recursionStack.has(depId)) {
353
+ return true;
354
+ }
355
+ }
356
+ }
357
+
358
+ recursionStack.delete(taskId);
359
+ return false;
360
+ };
361
+
362
+ for (const task of tasks) {
363
+ if (!visited.has(task.id)) {
364
+ if (hasCycle(task.id)) {
365
+ errors.push(`Cycle detected involving task ${task.id}`);
366
+ }
367
+ }
368
+ }
369
+
370
+ return {
371
+ valid: errors.length === 0,
372
+ errors,
373
+ };
374
+ }
375
+
376
+ /**
377
+ * Topological sort of tasks based on dependencies.
378
+ */
379
+ export function topologicalSort(tasks: Task[]): Task[] {
380
+ const inDegree = new Map<string, number>();
381
+ const adjList = new Map<string, string[]>();
382
+
383
+ // Initialize
384
+ for (const task of tasks) {
385
+ inDegree.set(task.id, task.dependsOn.length);
386
+ adjList.set(task.id, []);
387
+ }
388
+
389
+ // Build adjacency list
390
+ for (const task of tasks) {
391
+ for (const depId of task.dependsOn) {
392
+ if (adjList.has(depId)) {
393
+ adjList.get(depId)!.push(task.id);
394
+ }
395
+ }
396
+ }
397
+
398
+ // Kahn's algorithm
399
+ const queue: string[] = [];
400
+ for (const [taskId, degree] of inDegree.entries()) {
401
+ if (degree === 0) {
402
+ queue.push(taskId);
403
+ }
404
+ }
405
+
406
+ const sorted: Task[] = [];
407
+ while (queue.length > 0) {
408
+ const taskId = queue.shift()!;
409
+ const task = tasks.find(t => t.id === taskId);
410
+ if (task) {
411
+ sorted.push(task);
412
+ }
413
+
414
+ for (const neighbor of adjList.get(taskId) || []) {
415
+ const newDegree = (inDegree.get(neighbor) || 0) - 1;
416
+ inDegree.set(neighbor, newDegree);
417
+ if (newDegree === 0) {
418
+ queue.push(neighbor);
419
+ }
420
+ }
421
+ }
422
+
423
+ return sorted;
424
+ }
@@ -0,0 +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
+ }