skyloom 1.13.5 → 1.13.7

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 (195) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +220 -159
  3. package/config/providers.yaml +39 -39
  4. package/config/skills/api_integrator/SKILL.md +15 -15
  5. package/config/skills/arch_designer/SKILL.md +13 -13
  6. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  7. package/config/skills/code_analysis/SKILL.md +13 -13
  8. package/config/skills/code_generator/SKILL.md +12 -12
  9. package/config/skills/code_reviewer/SKILL.md +13 -13
  10. package/config/skills/content_writer/SKILL.md +14 -14
  11. package/config/skills/data_transformer/SKILL.md +15 -15
  12. package/config/skills/document_analysis/SKILL.md +13 -13
  13. package/config/skills/emotional_companion/SKILL.md +15 -15
  14. package/config/skills/performance_checker/SKILL.md +14 -14
  15. package/config/skills/security_auditor/SKILL.md +14 -14
  16. package/config/skills/self_evolve/SKILL.md +13 -13
  17. package/config/skills/sys_operator/SKILL.md +15 -15
  18. package/config/skills/task_planner/SKILL.md +14 -14
  19. package/config/skills/web_research/SKILL.md +14 -14
  20. package/config/skills/workflow_designer/SKILL.md +13 -13
  21. package/dist/agents/dew.js +52 -52
  22. package/dist/agents/fair.js +84 -84
  23. package/dist/agents/fog.js +30 -30
  24. package/dist/agents/frost.js +32 -32
  25. package/dist/agents/rain.js +32 -32
  26. package/dist/agents/snow.js +68 -68
  27. package/dist/cli/commands_md.d.ts +41 -0
  28. package/dist/cli/commands_md.d.ts.map +1 -0
  29. package/dist/cli/commands_md.js +140 -0
  30. package/dist/cli/commands_md.js.map +1 -0
  31. package/dist/cli/input_macros.d.ts +28 -0
  32. package/dist/cli/input_macros.d.ts.map +1 -0
  33. package/dist/cli/input_macros.js +120 -0
  34. package/dist/cli/input_macros.js.map +1 -0
  35. package/dist/cli/loom.d.ts +220 -0
  36. package/dist/cli/loom.d.ts.map +1 -0
  37. package/dist/cli/loom.js +1094 -0
  38. package/dist/cli/loom.js.map +1 -0
  39. package/dist/cli/loom_chat.d.ts +20 -0
  40. package/dist/cli/loom_chat.d.ts.map +1 -0
  41. package/dist/cli/loom_chat.js +685 -0
  42. package/dist/cli/loom_chat.js.map +1 -0
  43. package/dist/cli/main.js +310 -14
  44. package/dist/cli/main.js.map +1 -1
  45. package/dist/cli/tui.d.ts.map +1 -1
  46. package/dist/cli/tui.js +7 -1
  47. package/dist/cli/tui.js.map +1 -1
  48. package/dist/core/agent/guard.d.ts +45 -0
  49. package/dist/core/agent/guard.d.ts.map +1 -0
  50. package/dist/core/agent/guard.js +113 -0
  51. package/dist/core/agent/guard.js.map +1 -0
  52. package/dist/core/agent.d.ts +17 -0
  53. package/dist/core/agent.d.ts.map +1 -1
  54. package/dist/core/agent.js +182 -93
  55. package/dist/core/agent.js.map +1 -1
  56. package/dist/core/factory.d.ts.map +1 -1
  57. package/dist/core/factory.js +34 -2
  58. package/dist/core/factory.js.map +1 -1
  59. package/dist/core/file_checkpoint.d.ts +57 -0
  60. package/dist/core/file_checkpoint.d.ts.map +1 -0
  61. package/dist/core/file_checkpoint.js +162 -0
  62. package/dist/core/file_checkpoint.js.map +1 -0
  63. package/dist/core/hooks.d.ts +43 -0
  64. package/dist/core/hooks.d.ts.map +1 -0
  65. package/dist/core/hooks.js +110 -0
  66. package/dist/core/hooks.js.map +1 -0
  67. package/dist/core/llm.d.ts.map +1 -1
  68. package/dist/core/llm.js +15 -9
  69. package/dist/core/llm.js.map +1 -1
  70. package/dist/core/longdoc.js +5 -5
  71. package/dist/core/mcp.d.ts +16 -0
  72. package/dist/core/mcp.d.ts.map +1 -1
  73. package/dist/core/mcp.js +55 -0
  74. package/dist/core/mcp.js.map +1 -1
  75. package/dist/core/model_config.d.ts +40 -0
  76. package/dist/core/model_config.d.ts.map +1 -0
  77. package/dist/core/model_config.js +191 -0
  78. package/dist/core/model_config.js.map +1 -0
  79. package/dist/core/skill.d.ts +7 -0
  80. package/dist/core/skill.d.ts.map +1 -1
  81. package/dist/core/skill.js +47 -0
  82. package/dist/core/skill.js.map +1 -1
  83. package/dist/core/skymd.d.ts +39 -0
  84. package/dist/core/skymd.d.ts.map +1 -0
  85. package/dist/core/skymd.js +177 -0
  86. package/dist/core/skymd.js.map +1 -0
  87. package/dist/core/tool.d.ts +12 -0
  88. package/dist/core/tool.d.ts.map +1 -1
  89. package/dist/core/tool.js +30 -0
  90. package/dist/core/tool.js.map +1 -1
  91. package/dist/core/verify.d.ts +27 -0
  92. package/dist/core/verify.d.ts.map +1 -0
  93. package/dist/core/verify.js +62 -0
  94. package/dist/core/verify.js.map +1 -0
  95. package/dist/skills/loader.d.ts +22 -2
  96. package/dist/skills/loader.d.ts.map +1 -1
  97. package/dist/skills/loader.js +45 -15
  98. package/dist/skills/loader.js.map +1 -1
  99. package/dist/tools/builtin.d.ts.map +1 -1
  100. package/dist/tools/builtin.js +13 -3
  101. package/dist/tools/builtin.js.map +1 -1
  102. package/dist/tools/model_tool.d.ts +11 -0
  103. package/dist/tools/model_tool.d.ts.map +1 -0
  104. package/dist/tools/model_tool.js +71 -0
  105. package/dist/tools/model_tool.js.map +1 -0
  106. package/dist/tools/todo.d.ts +30 -0
  107. package/dist/tools/todo.d.ts.map +1 -0
  108. package/dist/tools/todo.js +78 -0
  109. package/dist/tools/todo.js.map +1 -0
  110. package/docs/AESTHETIC_DESIGN.md +152 -144
  111. package/docs/OPTIMIZATION_PLAN.md +178 -178
  112. package/package.json +1 -1
  113. package/scripts/install.js +48 -48
  114. package/scripts/link.js +10 -10
  115. package/setup.bat +79 -79
  116. package/skill-test-ty2fOA/test.md +10 -10
  117. package/src/agents/dew.ts +70 -70
  118. package/src/agents/fair.ts +102 -102
  119. package/src/agents/fog.ts +48 -48
  120. package/src/agents/frost.ts +50 -50
  121. package/src/agents/rain.ts +50 -50
  122. package/src/agents/snow.ts +239 -239
  123. package/src/cli/commands_md.ts +112 -0
  124. package/src/cli/input_macros.ts +83 -0
  125. package/src/cli/loom.ts +982 -0
  126. package/src/cli/loom_chat.ts +598 -0
  127. package/src/cli/main.ts +255 -9
  128. package/src/cli/mode.ts +58 -58
  129. package/src/cli/tui.ts +228 -222
  130. package/src/core/agent/guard.ts +134 -0
  131. package/src/core/agent/task.ts +100 -100
  132. package/src/core/agent.ts +177 -95
  133. package/src/core/arbitrate.ts +162 -162
  134. package/src/core/catalog.ts +178 -178
  135. package/src/core/checkpoint.ts +94 -94
  136. package/src/core/estimate.ts +104 -104
  137. package/src/core/evolve.ts +191 -191
  138. package/src/core/factory.ts +31 -2
  139. package/src/core/file_checkpoint.ts +136 -0
  140. package/src/core/filter.ts +103 -103
  141. package/src/core/graph.ts +156 -156
  142. package/src/core/hooks.ts +126 -0
  143. package/src/core/icons.ts +53 -53
  144. package/src/core/index.ts +37 -37
  145. package/src/core/learn.ts +146 -146
  146. package/src/core/llm.ts +15 -9
  147. package/src/core/longdoc.ts +155 -155
  148. package/src/core/mcp.ts +48 -0
  149. package/src/core/mcp_server.ts +176 -176
  150. package/src/core/model_config.ts +157 -0
  151. package/src/core/profile.ts +255 -255
  152. package/src/core/router.ts +124 -124
  153. package/src/core/sandbox.ts +142 -142
  154. package/src/core/security.ts +243 -243
  155. package/src/core/skill.ts +42 -0
  156. package/src/core/skymd.ts +143 -0
  157. package/src/core/theme.ts +65 -65
  158. package/src/core/tool.ts +30 -0
  159. package/src/core/tool_router.ts +193 -193
  160. package/src/core/vector.ts +152 -152
  161. package/src/core/verify.ts +71 -0
  162. package/src/core/workspace.ts +150 -150
  163. package/src/plugins/loader.ts +66 -66
  164. package/src/skills/loader.ts +45 -16
  165. package/src/sql.js.d.ts +29 -29
  166. package/src/tools/builtin.ts +13 -3
  167. package/src/tools/computer.ts +269 -269
  168. package/src/tools/delegate.ts +49 -49
  169. package/src/tools/model_tool.ts +74 -0
  170. package/src/tools/todo.ts +76 -0
  171. package/src/web/tts.ts +93 -93
  172. package/tests/agent.test.ts +159 -159
  173. package/tests/agent_helpers.test.ts +48 -48
  174. package/tests/bus.test.ts +121 -121
  175. package/tests/catalog.test.ts +86 -86
  176. package/tests/checkpoint_commands.test.ts +124 -0
  177. package/tests/claude_compat.test.ts +110 -0
  178. package/tests/config.test.ts +41 -41
  179. package/tests/guard.test.ts +75 -0
  180. package/tests/icons.test.ts +45 -45
  181. package/tests/loom.test.ts +248 -0
  182. package/tests/memory.test.ts +170 -170
  183. package/tests/model_config.test.ts +109 -0
  184. package/tests/router.test.ts +86 -86
  185. package/tests/schemas.test.ts +51 -51
  186. package/tests/semantic.test.ts +83 -83
  187. package/tests/setup.ts +10 -10
  188. package/tests/skill.test.ts +172 -172
  189. package/tests/skymd.test.ts +146 -0
  190. package/tests/task.test.ts +60 -60
  191. package/tests/todo_toolstats.test.ts +94 -0
  192. package/tests/tool.test.ts +108 -108
  193. package/tests/tool_router.test.ts +71 -71
  194. package/tests/tui.test.ts +67 -67
  195. package/vitest.config.ts +17 -17
@@ -1,170 +1,170 @@
1
- import { describe, it, expect, afterEach } from "vitest";
2
- import * as os from "os";
3
- import * as path from "path";
4
- import * as fs from "fs";
5
- import { Memory } from "../src/core/memory";
6
-
7
- /** addMessage mutates shortTerm through an async mutex — let microtasks flush. */
8
- const flush = () => new Promise((r) => setTimeout(r, 15));
9
-
10
- let tmpDirs: string[] = [];
11
- function tmpConfig(shortTermLimit = 100) {
12
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sky-mem-"));
13
- tmpDirs.push(dir);
14
- return { dbPath: path.join(dir, "memory.db"), shortTermLimit, maxPersistedMessages: 2000 };
15
- }
16
-
17
- afterEach(() => {
18
- for (const d of tmpDirs) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* ignore */ } }
19
- tmpDirs = [];
20
- });
21
-
22
- describe("Memory · short-term (in-memory, no DB)", () => {
23
- it("makes a message visible SYNCHRONOUSLY (regression: first-turn crash)", () => {
24
- // Previously addMessage pushed inside an async mutex, so getMessages() in the
25
- // same tick missed the message — crashing chatImpl/chatStreamImpl on a fresh
26
- // session's first user message. The push must be synchronous.
27
- const mem = new Memory(tmpConfig(), "fog");
28
- mem.addMessage("user", "first message");
29
- const msgs = mem.getMessages(); // no flush!
30
- expect(msgs).toHaveLength(1);
31
- expect(msgs[0]).toMatchObject({ role: "user", content: "first message" });
32
- });
33
-
34
- it("records and returns messages in order", async () => {
35
- const mem = new Memory(tmpConfig(), "fog");
36
- mem.addMessage("user", "hello");
37
- mem.addMessage("assistant", "hi there");
38
- await flush();
39
- const msgs = mem.getMessages();
40
- expect(msgs.map((m) => m.role)).toEqual(["user", "assistant"]);
41
- expect(msgs[0].content).toBe("hello");
42
- });
43
-
44
- it("preserves tool-call metadata in getMessages", async () => {
45
- const mem = new Memory(tmpConfig(), "fog");
46
- mem.addMessage("assistant", "", { toolCalls: [{ id: "t1", function: { name: "x" } }] });
47
- mem.addMessage("tool", "result", { name: "x", toolCallId: "t1" });
48
- await flush();
49
- const msgs = mem.getMessages();
50
- const toolMsg = msgs.find((m) => m.role === "tool");
51
- expect(toolMsg?.tool_call_id).toBe("t1");
52
- expect(toolMsg?.name).toBe("x");
53
- });
54
-
55
- it("prunes past the short-term limit but keeps system messages", async () => {
56
- const mem = new Memory(tmpConfig(3), "fog");
57
- mem.addMessage("system", "persona");
58
- for (let i = 0; i < 5; i++) mem.addMessage("user", `m${i}`);
59
- await flush();
60
- const msgs = mem.getMessages();
61
- expect(msgs.length).toBeLessThanOrEqual(3);
62
- expect(msgs.some((m) => m.role === "system" && m.content === "persona")).toBe(true);
63
- // most recent user message survives
64
- expect(msgs[msgs.length - 1].content).toBe("m4");
65
- });
66
-
67
- it("clearShortTerm keeps system messages", async () => {
68
- const mem = new Memory(tmpConfig(), "fog");
69
- mem.addMessage("system", "persona");
70
- mem.addMessage("user", "hello");
71
- await flush();
72
- await mem.clearShortTerm();
73
- const msgs = mem.getMessages();
74
- expect(msgs).toHaveLength(1);
75
- expect(msgs[0].role).toBe("system");
76
- });
77
- });
78
-
79
- describe("Memory · context window estimation", () => {
80
- it("counts CJK as heavier than ascii", async () => {
81
- const mem = new Memory(tmpConfig(), "fog");
82
- mem.addMessage("user", "你好世界"); // 4 CJK chars
83
- await flush();
84
- const usage = mem.getContextWindowUsage();
85
- expect(usage.messageCount).toBe(1);
86
- expect(usage.totalChars).toBe(4);
87
- // CJK weight is 2/char => >= 8
88
- expect(usage.estimatedTokens).toBeGreaterThanOrEqual(8);
89
- });
90
- });
91
-
92
- describe("Memory · working memory", () => {
93
- it("set/get/clear round-trips", () => {
94
- const mem = new Memory(tmpConfig(), "fog");
95
- mem.setWorking("plan", { step: 1 });
96
- expect(mem.getWorking("plan")).toEqual({ step: 1 });
97
- expect(mem.getWorking("missing", "fallback")).toBe("fallback");
98
- mem.clearWorking();
99
- expect(mem.getWorking("plan")).toBeNull();
100
- });
101
- });
102
-
103
- describe("Memory · long-term (SQLite)", () => {
104
- it("remember / recall / forget round-trips", async () => {
105
- const mem = new Memory(tmpConfig(), "fog");
106
- await mem.initDb();
107
- try {
108
- await mem.remember("favorite_lang", "typescript", "pref");
109
- const hits = await mem.recall("favorite_lang");
110
- expect(hits).toHaveLength(1);
111
- expect(hits[0].value).toBe("typescript");
112
- expect(hits[0].category).toBe("pref");
113
-
114
- await mem.forget("favorite_lang");
115
- expect(await mem.recall("favorite_lang")).toHaveLength(0);
116
- } finally {
117
- await mem.close();
118
- }
119
- });
120
-
121
- it("recall filters by category", async () => {
122
- const mem = new Memory(tmpConfig(), "fog");
123
- await mem.initDb();
124
- try {
125
- await mem.remember("a", 1, "x");
126
- await mem.remember("b", 2, "y");
127
- const xs = await mem.recall(null, "x");
128
- expect(xs).toHaveLength(1);
129
- expect(xs[0].key).toBe("a");
130
- } finally {
131
- await mem.close();
132
- }
133
- });
134
-
135
- it("persists sessions + messages to disk and reloads across instances (regression)", async () => {
136
- // Previously persistDb() was never called, so nothing survived a restart and
137
- // session resume was impossible. close() must save; a fresh instance must reload.
138
- const cfg = tmpConfig(); // shared dbPath for both instances
139
- const a = new Memory(cfg, "fog");
140
- await a.initDb();
141
- const sid = await a.createSession("s1");
142
- a.addMessage("user", "the sky is blue");
143
- a.addMessage("assistant", "noted: sky is blue");
144
- await a.remember("fact1", "value1", "auto");
145
- await a.close(); // must flush to disk
146
-
147
- const b = new Memory(cfg, "fog");
148
- await b.initDb();
149
- const sessions = await b.listSessions();
150
- expect(sessions.some((s) => s.id === sid)).toBe(true);
151
- expect(await b.loadSession(sid)).toBe(true);
152
- const msgs = b.getMessages().filter((m) => m.role !== "system");
153
- expect(msgs.some((m) => String(m.content).includes("sky is blue"))).toBe(true);
154
- expect((await b.recall("fact1"))[0]?.value).toBe("value1"); // long-term memory survived too
155
- await b.close();
156
- });
157
-
158
- it("getMemoryStats returns a populated object", async () => {
159
- const mem = new Memory(tmpConfig(), "fog");
160
- await mem.initDb();
161
- try {
162
- await mem.remember("k", "v");
163
- const stats = await mem.getMemoryStats();
164
- expect(typeof stats).toBe("object");
165
- expect(stats).not.toBeNull();
166
- } finally {
167
- await mem.close();
168
- }
169
- });
170
- });
1
+ import { describe, it, expect, afterEach } from "vitest";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import * as fs from "fs";
5
+ import { Memory } from "../src/core/memory";
6
+
7
+ /** addMessage mutates shortTerm through an async mutex — let microtasks flush. */
8
+ const flush = () => new Promise((r) => setTimeout(r, 15));
9
+
10
+ let tmpDirs: string[] = [];
11
+ function tmpConfig(shortTermLimit = 100) {
12
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sky-mem-"));
13
+ tmpDirs.push(dir);
14
+ return { dbPath: path.join(dir, "memory.db"), shortTermLimit, maxPersistedMessages: 2000 };
15
+ }
16
+
17
+ afterEach(() => {
18
+ for (const d of tmpDirs) { try { fs.rmSync(d, { recursive: true, force: true }); } catch { /* ignore */ } }
19
+ tmpDirs = [];
20
+ });
21
+
22
+ describe("Memory · short-term (in-memory, no DB)", () => {
23
+ it("makes a message visible SYNCHRONOUSLY (regression: first-turn crash)", () => {
24
+ // Previously addMessage pushed inside an async mutex, so getMessages() in the
25
+ // same tick missed the message — crashing chatImpl/chatStreamImpl on a fresh
26
+ // session's first user message. The push must be synchronous.
27
+ const mem = new Memory(tmpConfig(), "fog");
28
+ mem.addMessage("user", "first message");
29
+ const msgs = mem.getMessages(); // no flush!
30
+ expect(msgs).toHaveLength(1);
31
+ expect(msgs[0]).toMatchObject({ role: "user", content: "first message" });
32
+ });
33
+
34
+ it("records and returns messages in order", async () => {
35
+ const mem = new Memory(tmpConfig(), "fog");
36
+ mem.addMessage("user", "hello");
37
+ mem.addMessage("assistant", "hi there");
38
+ await flush();
39
+ const msgs = mem.getMessages();
40
+ expect(msgs.map((m) => m.role)).toEqual(["user", "assistant"]);
41
+ expect(msgs[0].content).toBe("hello");
42
+ });
43
+
44
+ it("preserves tool-call metadata in getMessages", async () => {
45
+ const mem = new Memory(tmpConfig(), "fog");
46
+ mem.addMessage("assistant", "", { toolCalls: [{ id: "t1", function: { name: "x" } }] });
47
+ mem.addMessage("tool", "result", { name: "x", toolCallId: "t1" });
48
+ await flush();
49
+ const msgs = mem.getMessages();
50
+ const toolMsg = msgs.find((m) => m.role === "tool");
51
+ expect(toolMsg?.tool_call_id).toBe("t1");
52
+ expect(toolMsg?.name).toBe("x");
53
+ });
54
+
55
+ it("prunes past the short-term limit but keeps system messages", async () => {
56
+ const mem = new Memory(tmpConfig(3), "fog");
57
+ mem.addMessage("system", "persona");
58
+ for (let i = 0; i < 5; i++) mem.addMessage("user", `m${i}`);
59
+ await flush();
60
+ const msgs = mem.getMessages();
61
+ expect(msgs.length).toBeLessThanOrEqual(3);
62
+ expect(msgs.some((m) => m.role === "system" && m.content === "persona")).toBe(true);
63
+ // most recent user message survives
64
+ expect(msgs[msgs.length - 1].content).toBe("m4");
65
+ });
66
+
67
+ it("clearShortTerm keeps system messages", async () => {
68
+ const mem = new Memory(tmpConfig(), "fog");
69
+ mem.addMessage("system", "persona");
70
+ mem.addMessage("user", "hello");
71
+ await flush();
72
+ await mem.clearShortTerm();
73
+ const msgs = mem.getMessages();
74
+ expect(msgs).toHaveLength(1);
75
+ expect(msgs[0].role).toBe("system");
76
+ });
77
+ });
78
+
79
+ describe("Memory · context window estimation", () => {
80
+ it("counts CJK as heavier than ascii", async () => {
81
+ const mem = new Memory(tmpConfig(), "fog");
82
+ mem.addMessage("user", "你好世界"); // 4 CJK chars
83
+ await flush();
84
+ const usage = mem.getContextWindowUsage();
85
+ expect(usage.messageCount).toBe(1);
86
+ expect(usage.totalChars).toBe(4);
87
+ // CJK weight is 2/char => >= 8
88
+ expect(usage.estimatedTokens).toBeGreaterThanOrEqual(8);
89
+ });
90
+ });
91
+
92
+ describe("Memory · working memory", () => {
93
+ it("set/get/clear round-trips", () => {
94
+ const mem = new Memory(tmpConfig(), "fog");
95
+ mem.setWorking("plan", { step: 1 });
96
+ expect(mem.getWorking("plan")).toEqual({ step: 1 });
97
+ expect(mem.getWorking("missing", "fallback")).toBe("fallback");
98
+ mem.clearWorking();
99
+ expect(mem.getWorking("plan")).toBeNull();
100
+ });
101
+ });
102
+
103
+ describe("Memory · long-term (SQLite)", () => {
104
+ it("remember / recall / forget round-trips", async () => {
105
+ const mem = new Memory(tmpConfig(), "fog");
106
+ await mem.initDb();
107
+ try {
108
+ await mem.remember("favorite_lang", "typescript", "pref");
109
+ const hits = await mem.recall("favorite_lang");
110
+ expect(hits).toHaveLength(1);
111
+ expect(hits[0].value).toBe("typescript");
112
+ expect(hits[0].category).toBe("pref");
113
+
114
+ await mem.forget("favorite_lang");
115
+ expect(await mem.recall("favorite_lang")).toHaveLength(0);
116
+ } finally {
117
+ await mem.close();
118
+ }
119
+ });
120
+
121
+ it("recall filters by category", async () => {
122
+ const mem = new Memory(tmpConfig(), "fog");
123
+ await mem.initDb();
124
+ try {
125
+ await mem.remember("a", 1, "x");
126
+ await mem.remember("b", 2, "y");
127
+ const xs = await mem.recall(null, "x");
128
+ expect(xs).toHaveLength(1);
129
+ expect(xs[0].key).toBe("a");
130
+ } finally {
131
+ await mem.close();
132
+ }
133
+ });
134
+
135
+ it("persists sessions + messages to disk and reloads across instances (regression)", async () => {
136
+ // Previously persistDb() was never called, so nothing survived a restart and
137
+ // session resume was impossible. close() must save; a fresh instance must reload.
138
+ const cfg = tmpConfig(); // shared dbPath for both instances
139
+ const a = new Memory(cfg, "fog");
140
+ await a.initDb();
141
+ const sid = await a.createSession("s1");
142
+ a.addMessage("user", "the sky is blue");
143
+ a.addMessage("assistant", "noted: sky is blue");
144
+ await a.remember("fact1", "value1", "auto");
145
+ await a.close(); // must flush to disk
146
+
147
+ const b = new Memory(cfg, "fog");
148
+ await b.initDb();
149
+ const sessions = await b.listSessions();
150
+ expect(sessions.some((s) => s.id === sid)).toBe(true);
151
+ expect(await b.loadSession(sid)).toBe(true);
152
+ const msgs = b.getMessages().filter((m) => m.role !== "system");
153
+ expect(msgs.some((m) => String(m.content).includes("sky is blue"))).toBe(true);
154
+ expect((await b.recall("fact1"))[0]?.value).toBe("value1"); // long-term memory survived too
155
+ await b.close();
156
+ });
157
+
158
+ it("getMemoryStats returns a populated object", async () => {
159
+ const mem = new Memory(tmpConfig(), "fog");
160
+ await mem.initDb();
161
+ try {
162
+ await mem.remember("k", "v");
163
+ const stats = await mem.getMemoryStats();
164
+ expect(typeof stats).toBe("object");
165
+ expect(stats).not.toBeNull();
166
+ } finally {
167
+ await mem.close();
168
+ }
169
+ });
170
+ });
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import * as fs from "fs";
3
+ import * as os from "os";
4
+ import * as path from "path";
5
+ import * as yaml from "yaml";
6
+ import {
7
+ providerOfModel, setAgentModel, clearAgentModel, setUnifiedModel,
8
+ setAgentApiKey, clearAgentApiKey, describeAgentLLM,
9
+ } from "../src/core/model_config";
10
+ import { createModelTools } from "../src/tools/model_tool";
11
+
12
+ let tmp: string;
13
+ let cfg: any;
14
+ const savedEnv = { ...process.env };
15
+
16
+ beforeEach(() => {
17
+ tmp = fs.mkdtempSync(path.join(os.tmpdir(), "skymodel-"));
18
+ cfg = { agents: {}, default_model: "gpt-4o" };
19
+ delete process.env.DEEPSEEK_API_KEY;
20
+ delete process.env.OPENAI_API_KEY;
21
+ });
22
+ afterEach(() => {
23
+ fs.rmSync(tmp, { recursive: true, force: true });
24
+ process.env = { ...savedEnv };
25
+ });
26
+
27
+ const userYaml = () => yaml.parse(fs.readFileSync(path.join(tmp, "config.yaml"), "utf-8"));
28
+
29
+ describe("模型配置 — 统一 + 独立覆盖", () => {
30
+ it("providerOfModel resolves catalog models and prefixed ids", () => {
31
+ expect(providerOfModel("deepseek-chat")).toBe("deepseek");
32
+ expect(providerOfModel("anthropic/claude-sonnet-4-6")).toBe("anthropic");
33
+ expect(providerOfModel("no-such-model")).toBeNull();
34
+ });
35
+
36
+ it("setAgentModel mutates runtime config AND persists a narrow patch", () => {
37
+ const r = setAgentModel(cfg, "fog", "deepseek-chat", tmp);
38
+ expect(r.ok).toBe(true);
39
+ expect(r.provider).toBe("deepseek");
40
+ // 热生效:运行时对象即刻更新(LLMClient.getModel 走同一引用)
41
+ expect(cfg.agents.fog.model).toBe("deepseek-chat");
42
+ // 持久化:只写覆盖项,不把合并后的默认配置泄漏进用户文件
43
+ const u = userYaml();
44
+ expect(u.agents.fog.model).toBe("deepseek-chat");
45
+ expect(u.default_model).toBeUndefined();
46
+ });
47
+
48
+ it("rejects models not in the catalog, with suggestions", () => {
49
+ const r = setAgentModel(cfg, "fog", "gpt-99-ultra", tmp);
50
+ expect(r.ok).toBe(false);
51
+ expect(cfg.agents.fog?.model).toBeUndefined();
52
+ });
53
+
54
+ it("clearAgentModel falls back to the unified default", () => {
55
+ setAgentModel(cfg, "fog", "deepseek-chat", tmp);
56
+ clearAgentModel(cfg, "fog", tmp);
57
+ expect(cfg.agents.fog?.model).toBeUndefined();
58
+ expect(describeAgentLLM(cfg, "fog", tmp).model).toBe("gpt-4o");
59
+ expect(userYaml().agents?.fog).toBeUndefined();
60
+ });
61
+
62
+ it("setUnifiedModel changes the default for every non-overridden agent", () => {
63
+ setAgentModel(cfg, "rain", "deepseek-chat", tmp);
64
+ const r = setUnifiedModel(cfg, "gpt-4o-mini", tmp);
65
+ expect(r.ok).toBe(true);
66
+ expect(describeAgentLLM(cfg, "fog", tmp).model).toBe("gpt-4o-mini"); // 跟随统一
67
+ expect(describeAgentLLM(cfg, "rain", tmp).model).toBe("deepseek-chat"); // 保持独立
68
+ expect(userYaml().default_model).toBe("gpt-4o-mini");
69
+ });
70
+
71
+ it("per-agent api key: set/clear + keySource resolution", () => {
72
+ setAgentApiKey(cfg, "fog", "sk-fog-own", tmp);
73
+ expect(cfg.agents.fog.api_key).toBe("sk-fog-own");
74
+ expect(describeAgentLLM(cfg, "fog", tmp).keySource).toBe("agent");
75
+ expect(userYaml().agents.fog.api_key).toBe("sk-fog-own");
76
+
77
+ clearAgentApiKey(cfg, "fog", tmp);
78
+ expect(describeAgentLLM(cfg, "fog", tmp).keySource).toBe("missing");
79
+
80
+ process.env.OPENAI_API_KEY = "sk-env";
81
+ expect(describeAgentLLM(cfg, "fog", tmp).keySource).toBe("env");
82
+ });
83
+
84
+ it("describeAgentLLM reports source agent vs unified", () => {
85
+ expect(describeAgentLLM(cfg, "fog", tmp).source).toBe("unified");
86
+ setAgentModel(cfg, "fog", "deepseek-chat", tmp);
87
+ const d = describeAgentLLM(cfg, "fog", tmp);
88
+ expect(d.source).toBe("agent");
89
+ expect(d.provider).toBe("deepseek");
90
+ });
91
+ });
92
+
93
+ describe("agent 自助换模型工具", () => {
94
+ it("set_my_model rejects unknown ids without touching config", async () => {
95
+ const tools = createModelTools("fog", cfg);
96
+ const setModel = tools.find(t => t.name === "set_my_model")!;
97
+ const out = await setModel.handler({ model: "gpt-99-ultra" });
98
+ expect(String(out)).toContain("✗");
99
+ expect(cfg.agents.fog?.model).toBeUndefined();
100
+ });
101
+
102
+ it("list_models reports current model and catalog entries", async () => {
103
+ const tools = createModelTools("fog", cfg);
104
+ const list = tools.find(t => t.name === "list_models")!;
105
+ const out = String(await list.handler({}));
106
+ expect(out).toContain("Current: gpt-4o");
107
+ expect(out).toContain("deepseek-chat");
108
+ });
109
+ });
@@ -1,86 +1,86 @@
1
- /**
2
- * Tests for the complexity router.
3
- */
4
- import { describe, it, expect } from 'vitest';
5
- import { classify, pickAgentForGoal } from '../src/core/router';
6
-
7
- describe('classify', () => {
8
- it.each([
9
- '你好',
10
- 'hi',
11
- '在吗',
12
- '谢谢',
13
- '什么是 RAG?',
14
- '为什么天空是蓝的?',
15
- '1 + 1 = ?',
16
- '解释一下闭包',
17
- ])('returns "direct" for simple questions: %s', (goal) => {
18
- expect(classify(goal)).toBe('direct');
19
- });
20
-
21
- it.each([
22
- '帮我写一个二分查找函数',
23
- '搜一下今天的天气',
24
- '审查 src/foo.py 的安全问题',
25
- '把这段中文翻译成英文:我喜欢猫',
26
- ])('returns "single" for focused tasks: %s', (goal) => {
27
- expect(classify(goal)).toBe('single');
28
- });
29
-
30
- it.each([
31
- '先帮我分析这段代码,然后重构它,最后写测试',
32
- '首先调研一下市场上有哪些方案,其次对比性能,最后给出推荐',
33
- '1. 创建数据库迁移\n2. 写 API\n3. 加测试\n4. 部署',
34
- ])('returns "orchestrate" for multi-step: %s', (goal) => {
35
- expect(classify(goal)).toBe('orchestrate');
36
- });
37
-
38
- it('empty goal returns direct', () => {
39
- expect(classify('')).toBe('direct');
40
- expect(classify(' ')).toBe('direct');
41
- });
42
-
43
- it('inline enumerated list is orchestrate', () => {
44
- expect(classify('1. 拉数据 2. 分析 3. 出图')).toBe('orchestrate');
45
- expect(classify('先做 1. xxx 2. yyy 3. zzz 4. www')).toBe('orchestrate');
46
- });
47
-
48
- it('two inline items is not orchestrate', () => {
49
- expect(classify('1. 你好 2. 谢谢')).not.toBe('orchestrate');
50
- });
51
- });
52
-
53
- describe('pickAgentForGoal', () => {
54
- const allAgents = new Set(['fog', 'rain', 'frost', 'snow', 'dew', 'fair']);
55
-
56
- it('security keyword picks frost', () => {
57
- expect(pickAgentForGoal('帮我做安全审查', allAgents)).toBe('frost');
58
- });
59
-
60
- it('research keyword picks fog', () => {
61
- expect(pickAgentForGoal('搜一下最新的 React 文档', allAgents)).toBe('fog');
62
- });
63
-
64
- it('greeting picks fair', () => {
65
- expect(pickAgentForGoal('你好啊', allAgents)).toBe('fair');
66
- });
67
-
68
- it('falls back to rain', () => {
69
- expect(pickAgentForGoal('处理这个东西', allAgents)).toBe('rain');
70
- });
71
-
72
- it('binary search picks rain not fog', () => {
73
- expect(pickAgentForGoal('帮我写一个二分查找', allAgents)).toBe('rain');
74
- expect(pickAgentForGoal('实现一个排序函数', allAgents)).toBe('rain');
75
- });
76
-
77
- it('skips missing agents', () => {
78
- const available = new Set(['rain', 'snow']);
79
- const result = pickAgentForGoal('做安全审查', available);
80
- expect(available.has(result)).toBe(true);
81
- });
82
-
83
- it('single agent available', () => {
84
- expect(pickAgentForGoal('anything', new Set(['rain']))).toBe('rain');
85
- });
86
- });
1
+ /**
2
+ * Tests for the complexity router.
3
+ */
4
+ import { describe, it, expect } from 'vitest';
5
+ import { classify, pickAgentForGoal } from '../src/core/router';
6
+
7
+ describe('classify', () => {
8
+ it.each([
9
+ '你好',
10
+ 'hi',
11
+ '在吗',
12
+ '谢谢',
13
+ '什么是 RAG?',
14
+ '为什么天空是蓝的?',
15
+ '1 + 1 = ?',
16
+ '解释一下闭包',
17
+ ])('returns "direct" for simple questions: %s', (goal) => {
18
+ expect(classify(goal)).toBe('direct');
19
+ });
20
+
21
+ it.each([
22
+ '帮我写一个二分查找函数',
23
+ '搜一下今天的天气',
24
+ '审查 src/foo.py 的安全问题',
25
+ '把这段中文翻译成英文:我喜欢猫',
26
+ ])('returns "single" for focused tasks: %s', (goal) => {
27
+ expect(classify(goal)).toBe('single');
28
+ });
29
+
30
+ it.each([
31
+ '先帮我分析这段代码,然后重构它,最后写测试',
32
+ '首先调研一下市场上有哪些方案,其次对比性能,最后给出推荐',
33
+ '1. 创建数据库迁移\n2. 写 API\n3. 加测试\n4. 部署',
34
+ ])('returns "orchestrate" for multi-step: %s', (goal) => {
35
+ expect(classify(goal)).toBe('orchestrate');
36
+ });
37
+
38
+ it('empty goal returns direct', () => {
39
+ expect(classify('')).toBe('direct');
40
+ expect(classify(' ')).toBe('direct');
41
+ });
42
+
43
+ it('inline enumerated list is orchestrate', () => {
44
+ expect(classify('1. 拉数据 2. 分析 3. 出图')).toBe('orchestrate');
45
+ expect(classify('先做 1. xxx 2. yyy 3. zzz 4. www')).toBe('orchestrate');
46
+ });
47
+
48
+ it('two inline items is not orchestrate', () => {
49
+ expect(classify('1. 你好 2. 谢谢')).not.toBe('orchestrate');
50
+ });
51
+ });
52
+
53
+ describe('pickAgentForGoal', () => {
54
+ const allAgents = new Set(['fog', 'rain', 'frost', 'snow', 'dew', 'fair']);
55
+
56
+ it('security keyword picks frost', () => {
57
+ expect(pickAgentForGoal('帮我做安全审查', allAgents)).toBe('frost');
58
+ });
59
+
60
+ it('research keyword picks fog', () => {
61
+ expect(pickAgentForGoal('搜一下最新的 React 文档', allAgents)).toBe('fog');
62
+ });
63
+
64
+ it('greeting picks fair', () => {
65
+ expect(pickAgentForGoal('你好啊', allAgents)).toBe('fair');
66
+ });
67
+
68
+ it('falls back to rain', () => {
69
+ expect(pickAgentForGoal('处理这个东西', allAgents)).toBe('rain');
70
+ });
71
+
72
+ it('binary search picks rain not fog', () => {
73
+ expect(pickAgentForGoal('帮我写一个二分查找', allAgents)).toBe('rain');
74
+ expect(pickAgentForGoal('实现一个排序函数', allAgents)).toBe('rain');
75
+ });
76
+
77
+ it('skips missing agents', () => {
78
+ const available = new Set(['rain', 'snow']);
79
+ const result = pickAgentForGoal('做安全审查', available);
80
+ expect(available.has(result)).toBe(true);
81
+ });
82
+
83
+ it('single agent available', () => {
84
+ expect(pickAgentForGoal('anything', new Set(['rain']))).toBe('rain');
85
+ });
86
+ });