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,159 +1,159 @@
1
- import { describe, it, expect } from "vitest";
2
- import { FogAgent } from "../src/agents/fog";
3
- import { MessageBus } from "../src/core/bus";
4
- import { ToolRegistry } from "../src/core/tool";
5
- import { SkillRegistry } from "../src/core/skill";
6
-
7
- /**
8
- * Characterization tests for the agent chat/tool loop, driven by a scripted
9
- * mock LLM (no network). These lock in the behavior of the ~275-line hot path
10
- * (chatStreamImpl / llmLoop / tool execution / anti-loop guard) so it can be
11
- * refactored safely (Phase 3) — and they guard against regressions like the
12
- * first-message crash.
13
- */
14
-
15
- interface Turn { content?: string; toolCalls?: { name: string; args?: any }[]; reasoning?: string }
16
-
17
- class MockLLM {
18
- calls = 0;
19
- constructor(private turns: Turn[]) {}
20
- private turn(): Turn { const t = this.turns[Math.min(this.calls, this.turns.length - 1)]; this.calls++; return t || {}; }
21
- private toolCallObjs(t: Turn) {
22
- return (t.toolCalls || []).map((tc, i) => ({
23
- id: `call_${this.calls}_${i}`, type: "function",
24
- function: { name: tc.name, arguments: JSON.stringify(tc.args || {}) },
25
- }));
26
- }
27
- async *streamWithTools(): AsyncGenerator<any> {
28
- const t = this.turn();
29
- if (t.reasoning) yield { type: "reasoning", text: t.reasoning };
30
- if (t.content) yield { type: "content", text: t.content };
31
- for (const tc of this.toolCallObjs(t)) yield { type: "tool_call", toolCall: tc };
32
- yield { type: "done", usage: { promptTokens: 1, completionTokens: 1 } };
33
- }
34
- async complete(): Promise<any> {
35
- const t = this.turn();
36
- return { content: t.content || "", toolCalls: this.toolCallObjs(t), model: "mock", usage: { promptTokens: 1, completionTokens: 1 }, cost: 0, truncated: false };
37
- }
38
- getTotalCost() { return 0; }
39
- getModel() { return "mock"; }
40
- setLogger() { /* noop */ }
41
- }
42
-
43
- function makeAgent(turns: Turn[], tools: { name: string; handler: (a: any) => Promise<string> }[] = []) {
44
- const reg = new ToolRegistry();
45
- for (const t of tools) reg.register({ name: t.name, description: t.name, handler: t.handler });
46
- const config = { agents: { fog: {} }, llm: { language: "zh" }, memory: { shortTermLimit: 100, dbPath: "/tmp/sky-test" } };
47
- const agent = new FogAgent(config as any, new MockLLM(turns) as any, new MessageBus(), reg, new SkillRegistry());
48
- return agent;
49
- }
50
-
51
- async function collect(gen: AsyncGenerator<any>, cap = 500): Promise<any[]> {
52
- const evs: any[] = [];
53
- for await (const ev of gen) { evs.push(ev); if (evs.length > cap) break; }
54
- return evs;
55
- }
56
-
57
- describe("agent · chat loop (mock LLM)", () => {
58
- it("streams a simple reply and records both messages", async () => {
59
- const agent = makeAgent([{ content: "你好,我是雾。" }]);
60
- const evs = await collect(agent.chatStream("你好"));
61
- const text = evs.filter((e) => e.type === "content").map((e) => e.text).join("");
62
- expect(text).toContain("你好,我是雾。");
63
-
64
- const msgs = agent.memory.getMessages();
65
- expect(msgs[0]).toMatchObject({ role: "user", content: "你好" }); // regression: user msg present
66
- expect(msgs.some((m) => m.role === "assistant" && String(m.content).includes("雾"))).toBe(true);
67
- });
68
-
69
- it("blocking chat() returns the reply", async () => {
70
- const agent = makeAgent([{ content: "答案是 42" }]);
71
- const reply = await agent.chat("问题?");
72
- expect(reply).toContain("42");
73
- });
74
-
75
- it("streams reasoning before content", async () => {
76
- const agent = makeAgent([{ reasoning: "先想一下…", content: "结论。" }]);
77
- const evs = await collect(agent.chatStream("?"));
78
- expect(evs.some((e) => e.type === "reasoning")).toBe(true);
79
- expect(evs.filter((e) => e.type === "content").map((e) => e.text).join("")).toContain("结论。");
80
- });
81
-
82
- it("executes a tool call then produces the final answer", async () => {
83
- let received: any = null;
84
- const agent = makeAgent(
85
- [{ toolCalls: [{ name: "echo", args: { text: "hi" } }] }, { content: "工具回显: hi" }],
86
- [{ name: "echo", handler: async (a) => { received = a; return `echo:${a.text}`; } }],
87
- );
88
- const evs = await collect(agent.chatStream("用 echo 工具"));
89
- expect(received).toEqual({ text: "hi" }); // tool actually ran with parsed args
90
- expect(evs.some((e) => e.type === "tool_status" && e.tool_name === "echo")).toBe(true);
91
- expect(evs.some((e) => e.type === "tool_done" && e.tool_name === "echo" && e.success)).toBe(true);
92
- expect(evs.filter((e) => e.type === "content").map((e) => e.text).join("")).toContain("工具回显");
93
- // tool result recorded to memory
94
- expect(agent.memory.getMessages().some((m) => m.role === "tool" && String(m.content).includes("echo:hi"))).toBe(true);
95
- });
96
-
97
- it("terminates (does not loop forever) when the model repeats the same tool call", async () => {
98
- // Script the same tool call far beyond the round cap; the anti-loop guard must stop it.
99
- const turns: Turn[] = Array.from({ length: 60 }, () => ({ toolCalls: [{ name: "spin", args: { n: 1 } }] }));
100
- const llm = new MockLLM(turns);
101
- const reg = new ToolRegistry();
102
- reg.register({ name: "spin", description: "spin", handler: async () => "still spinning" });
103
- const config = { agents: { fog: {} }, llm: {}, memory: { shortTermLimit: 200, dbPath: "/tmp/sky-test" } };
104
- const agent = new FogAgent(config as any, llm as any, new MessageBus(), reg, new SkillRegistry());
105
-
106
- const evs = await collect(agent.chatStream("loop please"), 2000);
107
- // It must finish (the generator returns), not hang, and not call the model unboundedly.
108
- expect(evs.some((e) => e.type === "done")).toBe(true);
109
- expect(llm.calls).toBeLessThan(50); // bounded by the round cap / guard, not 60+
110
- }, 15000);
111
- });
112
-
113
- describe("agent · context window (catalog-aware compaction)", () => {
114
- it("contextUsage uses the active model's real window from the catalog", () => {
115
- const agent = makeAgent([{ content: "x" }]);
116
- (agent as any).config.agents.fog.model = "mixtral-8x7b"; // 32768
117
- expect(agent.contextUsage().maxTokens).toBe(32768);
118
- expect(agent.contextUsage().model).toBe("mixtral-8x7b");
119
- (agent as any).config.agents.fog.model = "gemini-2.5-pro"; // 1048576
120
- expect(agent.contextUsage().maxTokens).toBe(1048576);
121
- });
122
-
123
- it("auto-compaction triggers for a small window but not a large one (same history)", () => {
124
- const agent = makeAgent([{ content: "x" }]);
125
- const big = "字".repeat(800); // CJK ~2 tokens/char
126
- for (let i = 0; i < 20; i++) agent.memory.addMessage("user", big); // ~32k tokens
127
-
128
- (agent as any).config.agents.fog.model = "mixtral-8x7b"; // 32768 window -> over budget
129
- expect((agent as any).shouldAutoCompact()).toBe(true);
130
-
131
- (agent as any).config.agents.fog.model = "gemini-2.5-pro"; // 1M window -> fine
132
- expect((agent as any).shouldAutoCompact()).toBe(false);
133
- });
134
- });
135
-
136
- describe("agent · interrupt (Ctrl-C)", () => {
137
- it("stops between rounds on abort and preserves partial output", async () => {
138
- const controller = new AbortController();
139
- // Round 1 streams some content + a tool call; the tool aborts the signal.
140
- // Round 2 must never run.
141
- const turns: Turn[] = [
142
- { content: "部分内容已生成…", toolCalls: [{ name: "spin", args: {} }] },
143
- { content: "不应出现的第二轮" },
144
- ];
145
- const reg = new ToolRegistry();
146
- reg.register({ name: "spin", description: "spin", handler: async () => { controller.abort(); return "spun"; } });
147
- const config = { agents: { fog: {} }, llm: {}, memory: { shortTermLimit: 200, dbPath: "/tmp/sky-test" } };
148
- const agent = new FogAgent(config as any, new MockLLM(turns) as any, new MessageBus(), reg, new SkillRegistry());
149
-
150
- const evs = await collect(agent.chatStream("go", controller.signal));
151
- const text = evs.filter((e) => e.type === "content").map((e) => e.text).join("");
152
-
153
- expect(evs.some((e) => e.type === "interrupted")).toBe(true);
154
- expect(text).toContain("部分内容已生成"); // partial output kept
155
- expect(text).not.toContain("第二轮"); // round 2 never streamed
156
- // partial assistant content is in memory
157
- expect(agent.memory.getMessages().some((m) => m.role === "assistant" && String(m.content).includes("部分内容"))).toBe(true);
158
- });
159
- });
1
+ import { describe, it, expect } from "vitest";
2
+ import { FogAgent } from "../src/agents/fog";
3
+ import { MessageBus } from "../src/core/bus";
4
+ import { ToolRegistry } from "../src/core/tool";
5
+ import { SkillRegistry } from "../src/core/skill";
6
+
7
+ /**
8
+ * Characterization tests for the agent chat/tool loop, driven by a scripted
9
+ * mock LLM (no network). These lock in the behavior of the ~275-line hot path
10
+ * (chatStreamImpl / llmLoop / tool execution / anti-loop guard) so it can be
11
+ * refactored safely (Phase 3) — and they guard against regressions like the
12
+ * first-message crash.
13
+ */
14
+
15
+ interface Turn { content?: string; toolCalls?: { name: string; args?: any }[]; reasoning?: string }
16
+
17
+ class MockLLM {
18
+ calls = 0;
19
+ constructor(private turns: Turn[]) {}
20
+ private turn(): Turn { const t = this.turns[Math.min(this.calls, this.turns.length - 1)]; this.calls++; return t || {}; }
21
+ private toolCallObjs(t: Turn) {
22
+ return (t.toolCalls || []).map((tc, i) => ({
23
+ id: `call_${this.calls}_${i}`, type: "function",
24
+ function: { name: tc.name, arguments: JSON.stringify(tc.args || {}) },
25
+ }));
26
+ }
27
+ async *streamWithTools(): AsyncGenerator<any> {
28
+ const t = this.turn();
29
+ if (t.reasoning) yield { type: "reasoning", text: t.reasoning };
30
+ if (t.content) yield { type: "content", text: t.content };
31
+ for (const tc of this.toolCallObjs(t)) yield { type: "tool_call", toolCall: tc };
32
+ yield { type: "done", usage: { promptTokens: 1, completionTokens: 1 } };
33
+ }
34
+ async complete(): Promise<any> {
35
+ const t = this.turn();
36
+ return { content: t.content || "", toolCalls: this.toolCallObjs(t), model: "mock", usage: { promptTokens: 1, completionTokens: 1 }, cost: 0, truncated: false };
37
+ }
38
+ getTotalCost() { return 0; }
39
+ getModel() { return "mock"; }
40
+ setLogger() { /* noop */ }
41
+ }
42
+
43
+ function makeAgent(turns: Turn[], tools: { name: string; handler: (a: any) => Promise<string> }[] = []) {
44
+ const reg = new ToolRegistry();
45
+ for (const t of tools) reg.register({ name: t.name, description: t.name, handler: t.handler });
46
+ const config = { agents: { fog: {} }, llm: { language: "zh" }, memory: { shortTermLimit: 100, dbPath: "/tmp/sky-test" } };
47
+ const agent = new FogAgent(config as any, new MockLLM(turns) as any, new MessageBus(), reg, new SkillRegistry());
48
+ return agent;
49
+ }
50
+
51
+ async function collect(gen: AsyncGenerator<any>, cap = 500): Promise<any[]> {
52
+ const evs: any[] = [];
53
+ for await (const ev of gen) { evs.push(ev); if (evs.length > cap) break; }
54
+ return evs;
55
+ }
56
+
57
+ describe("agent · chat loop (mock LLM)", () => {
58
+ it("streams a simple reply and records both messages", async () => {
59
+ const agent = makeAgent([{ content: "你好,我是雾。" }]);
60
+ const evs = await collect(agent.chatStream("你好"));
61
+ const text = evs.filter((e) => e.type === "content").map((e) => e.text).join("");
62
+ expect(text).toContain("你好,我是雾。");
63
+
64
+ const msgs = agent.memory.getMessages();
65
+ expect(msgs[0]).toMatchObject({ role: "user", content: "你好" }); // regression: user msg present
66
+ expect(msgs.some((m) => m.role === "assistant" && String(m.content).includes("雾"))).toBe(true);
67
+ });
68
+
69
+ it("blocking chat() returns the reply", async () => {
70
+ const agent = makeAgent([{ content: "答案是 42" }]);
71
+ const reply = await agent.chat("问题?");
72
+ expect(reply).toContain("42");
73
+ });
74
+
75
+ it("streams reasoning before content", async () => {
76
+ const agent = makeAgent([{ reasoning: "先想一下…", content: "结论。" }]);
77
+ const evs = await collect(agent.chatStream("?"));
78
+ expect(evs.some((e) => e.type === "reasoning")).toBe(true);
79
+ expect(evs.filter((e) => e.type === "content").map((e) => e.text).join("")).toContain("结论。");
80
+ });
81
+
82
+ it("executes a tool call then produces the final answer", async () => {
83
+ let received: any = null;
84
+ const agent = makeAgent(
85
+ [{ toolCalls: [{ name: "echo", args: { text: "hi" } }] }, { content: "工具回显: hi" }],
86
+ [{ name: "echo", handler: async (a) => { received = a; return `echo:${a.text}`; } }],
87
+ );
88
+ const evs = await collect(agent.chatStream("用 echo 工具"));
89
+ expect(received).toEqual({ text: "hi" }); // tool actually ran with parsed args
90
+ expect(evs.some((e) => e.type === "tool_status" && e.tool_name === "echo")).toBe(true);
91
+ expect(evs.some((e) => e.type === "tool_done" && e.tool_name === "echo" && e.success)).toBe(true);
92
+ expect(evs.filter((e) => e.type === "content").map((e) => e.text).join("")).toContain("工具回显");
93
+ // tool result recorded to memory
94
+ expect(agent.memory.getMessages().some((m) => m.role === "tool" && String(m.content).includes("echo:hi"))).toBe(true);
95
+ });
96
+
97
+ it("terminates (does not loop forever) when the model repeats the same tool call", async () => {
98
+ // Script the same tool call far beyond the round cap; the anti-loop guard must stop it.
99
+ const turns: Turn[] = Array.from({ length: 60 }, () => ({ toolCalls: [{ name: "spin", args: { n: 1 } }] }));
100
+ const llm = new MockLLM(turns);
101
+ const reg = new ToolRegistry();
102
+ reg.register({ name: "spin", description: "spin", handler: async () => "still spinning" });
103
+ const config = { agents: { fog: {} }, llm: {}, memory: { shortTermLimit: 200, dbPath: "/tmp/sky-test" } };
104
+ const agent = new FogAgent(config as any, llm as any, new MessageBus(), reg, new SkillRegistry());
105
+
106
+ const evs = await collect(agent.chatStream("loop please"), 2000);
107
+ // It must finish (the generator returns), not hang, and not call the model unboundedly.
108
+ expect(evs.some((e) => e.type === "done")).toBe(true);
109
+ expect(llm.calls).toBeLessThan(50); // bounded by the round cap / guard, not 60+
110
+ }, 15000);
111
+ });
112
+
113
+ describe("agent · context window (catalog-aware compaction)", () => {
114
+ it("contextUsage uses the active model's real window from the catalog", () => {
115
+ const agent = makeAgent([{ content: "x" }]);
116
+ (agent as any).config.agents.fog.model = "mixtral-8x7b"; // 32768
117
+ expect(agent.contextUsage().maxTokens).toBe(32768);
118
+ expect(agent.contextUsage().model).toBe("mixtral-8x7b");
119
+ (agent as any).config.agents.fog.model = "gemini-2.5-pro"; // 1048576
120
+ expect(agent.contextUsage().maxTokens).toBe(1048576);
121
+ });
122
+
123
+ it("auto-compaction triggers for a small window but not a large one (same history)", () => {
124
+ const agent = makeAgent([{ content: "x" }]);
125
+ const big = "字".repeat(800); // CJK ~2 tokens/char
126
+ for (let i = 0; i < 20; i++) agent.memory.addMessage("user", big); // ~32k tokens
127
+
128
+ (agent as any).config.agents.fog.model = "mixtral-8x7b"; // 32768 window -> over budget
129
+ expect((agent as any).shouldAutoCompact()).toBe(true);
130
+
131
+ (agent as any).config.agents.fog.model = "gemini-2.5-pro"; // 1M window -> fine
132
+ expect((agent as any).shouldAutoCompact()).toBe(false);
133
+ });
134
+ });
135
+
136
+ describe("agent · interrupt (Ctrl-C)", () => {
137
+ it("stops between rounds on abort and preserves partial output", async () => {
138
+ const controller = new AbortController();
139
+ // Round 1 streams some content + a tool call; the tool aborts the signal.
140
+ // Round 2 must never run.
141
+ const turns: Turn[] = [
142
+ { content: "部分内容已生成…", toolCalls: [{ name: "spin", args: {} }] },
143
+ { content: "不应出现的第二轮" },
144
+ ];
145
+ const reg = new ToolRegistry();
146
+ reg.register({ name: "spin", description: "spin", handler: async () => { controller.abort(); return "spun"; } });
147
+ const config = { agents: { fog: {} }, llm: {}, memory: { shortTermLimit: 200, dbPath: "/tmp/sky-test" } };
148
+ const agent = new FogAgent(config as any, new MockLLM(turns) as any, new MessageBus(), reg, new SkillRegistry());
149
+
150
+ const evs = await collect(agent.chatStream("go", controller.signal));
151
+ const text = evs.filter((e) => e.type === "content").map((e) => e.text).join("");
152
+
153
+ expect(evs.some((e) => e.type === "interrupted")).toBe(true);
154
+ expect(text).toContain("部分内容已生成"); // partial output kept
155
+ expect(text).not.toContain("第二轮"); // round 2 never streamed
156
+ // partial assistant content is in memory
157
+ expect(agent.memory.getMessages().some((m) => m.role === "assistant" && String(m.content).includes("部分内容"))).toBe(true);
158
+ });
159
+ });
@@ -1,48 +1,48 @@
1
- import { describe, it, expect } from "vitest";
2
- import { parseExtractedFacts, synthesizeDelegationSummary } from "../src/core/agent_helpers";
3
-
4
- describe("parseExtractedFacts", () => {
5
- it("parses a raw JSON array", () => {
6
- const out = parseExtractedFacts('[{"key":"lang","value":"ts"}]');
7
- expect(out).toEqual([{ key: "lang", value: "ts" }]);
8
- });
9
-
10
- it("parses JSON inside a markdown fence", () => {
11
- const out = parseExtractedFacts('```json\n[{"key":"a","value":1}]\n```');
12
- expect(out).toEqual([{ key: "a", value: 1 }]);
13
- });
14
-
15
- it("extracts a JSON array embedded in prose", () => {
16
- const out = parseExtractedFacts('Sure! Here are the facts: [{"key":"x","value":true}] done.');
17
- expect(out).toEqual([{ key: "x", value: true }]);
18
- });
19
-
20
- it("returns [] for empty or non-JSON input", () => {
21
- expect(parseExtractedFacts("")).toEqual([]);
22
- expect(parseExtractedFacts("no json here")).toEqual([]);
23
- expect(parseExtractedFacts(" ")).toEqual([]);
24
- });
25
-
26
- it("returns [] when JSON is an object, not an array", () => {
27
- expect(parseExtractedFacts('{"key":"x"}')).toEqual([]);
28
- });
29
-
30
- it("filters out non-object array members", () => {
31
- const out = parseExtractedFacts('[{"key":"a","value":1}, "junk", 42, null]');
32
- // null is typeof 'object' in JS, so it survives the filter — assert the real members
33
- expect(out).toContainEqual({ key: "a", value: 1 });
34
- expect(out).not.toContain("junk");
35
- expect(out).not.toContain(42);
36
- });
37
- });
38
-
39
- describe("synthesizeDelegationSummary", () => {
40
- it("summarizes successes and failures", () => {
41
- expect(synthesizeDelegationSummary([["fog", true], ["rain", false]])).toBe(
42
- "[Delegated: fog | Failed: rain]"
43
- );
44
- });
45
- it("returns empty string when no delegations", () => {
46
- expect(synthesizeDelegationSummary([])).toBe("");
47
- });
48
- });
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseExtractedFacts, synthesizeDelegationSummary } from "../src/core/agent_helpers";
3
+
4
+ describe("parseExtractedFacts", () => {
5
+ it("parses a raw JSON array", () => {
6
+ const out = parseExtractedFacts('[{"key":"lang","value":"ts"}]');
7
+ expect(out).toEqual([{ key: "lang", value: "ts" }]);
8
+ });
9
+
10
+ it("parses JSON inside a markdown fence", () => {
11
+ const out = parseExtractedFacts('```json\n[{"key":"a","value":1}]\n```');
12
+ expect(out).toEqual([{ key: "a", value: 1 }]);
13
+ });
14
+
15
+ it("extracts a JSON array embedded in prose", () => {
16
+ const out = parseExtractedFacts('Sure! Here are the facts: [{"key":"x","value":true}] done.');
17
+ expect(out).toEqual([{ key: "x", value: true }]);
18
+ });
19
+
20
+ it("returns [] for empty or non-JSON input", () => {
21
+ expect(parseExtractedFacts("")).toEqual([]);
22
+ expect(parseExtractedFacts("no json here")).toEqual([]);
23
+ expect(parseExtractedFacts(" ")).toEqual([]);
24
+ });
25
+
26
+ it("returns [] when JSON is an object, not an array", () => {
27
+ expect(parseExtractedFacts('{"key":"x"}')).toEqual([]);
28
+ });
29
+
30
+ it("filters out non-object array members", () => {
31
+ const out = parseExtractedFacts('[{"key":"a","value":1}, "junk", 42, null]');
32
+ // null is typeof 'object' in JS, so it survives the filter — assert the real members
33
+ expect(out).toContainEqual({ key: "a", value: 1 });
34
+ expect(out).not.toContain("junk");
35
+ expect(out).not.toContain(42);
36
+ });
37
+ });
38
+
39
+ describe("synthesizeDelegationSummary", () => {
40
+ it("summarizes successes and failures", () => {
41
+ expect(synthesizeDelegationSummary([["fog", true], ["rain", false]])).toBe(
42
+ "[Delegated: fog | Failed: rain]"
43
+ );
44
+ });
45
+ it("returns empty string when no delegations", () => {
46
+ expect(synthesizeDelegationSummary([])).toBe("");
47
+ });
48
+ });