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,100 +1,100 @@
1
- /**
2
- * Agent domain model — states and the Task DAG node.
3
- *
4
- * Extracted from the monolithic agent.ts (Phase 3). Pure, dependency-free,
5
- * and unit-testable in isolation. `agent.ts` re-exports these so external
6
- * importers of `../core/agent` are unaffected.
7
- */
8
-
9
- /** Lifecycle state of a running agent. */
10
- export enum AgentState {
11
- IDLE = 'idle',
12
- THINKING = 'thinking',
13
- ACTING = 'acting',
14
- WAITING = 'waiting',
15
- ERROR = 'error',
16
- }
17
-
18
- /** Lifecycle state of an orchestrated task. */
19
- export enum TaskState {
20
- PENDING = 'pending',
21
- RUNNING = 'running',
22
- COMPLETED = 'completed',
23
- FAILED = 'failed',
24
- SKIPPED = 'skipped',
25
- }
26
-
27
- /** Allowed task state transitions. A terminal state has no outgoing edges. */
28
- export const VALID_TRANSITIONS: Record<TaskState, Set<TaskState>> = {
29
- [TaskState.PENDING]: new Set([TaskState.RUNNING, TaskState.SKIPPED, TaskState.FAILED]),
30
- [TaskState.RUNNING]: new Set([TaskState.RUNNING, TaskState.COMPLETED, TaskState.FAILED]),
31
- [TaskState.FAILED]: new Set([TaskState.RUNNING, TaskState.SKIPPED]),
32
- [TaskState.COMPLETED]: new Set(),
33
- [TaskState.SKIPPED]: new Set(),
34
- };
35
-
36
- /** A node in the orchestration DAG. */
37
- export class Task {
38
- id: string;
39
- description: string;
40
- assignedTo: string | null = null;
41
- parentId: string | null = null;
42
- dependsOn: string[] = [];
43
- status: TaskState = TaskState.PENDING;
44
- priority: number = 0;
45
- result: string | null = null;
46
- metadata: Record<string, any> = {};
47
-
48
- constructor(config: {
49
- id: string;
50
- description: string;
51
- assignedTo?: string | null;
52
- parentId?: string | null;
53
- dependsOn?: string[];
54
- status?: TaskState;
55
- priority?: number;
56
- result?: string | null;
57
- metadata?: Record<string, any>;
58
- }) {
59
- this.id = config.id;
60
- this.description = config.description;
61
- this.assignedTo = config.assignedTo ?? null;
62
- this.parentId = config.parentId ?? null;
63
- this.dependsOn = config.dependsOn || [];
64
- this.status = config.status ?? TaskState.PENDING;
65
- this.priority = config.priority ?? 0;
66
- this.result = config.result ?? null;
67
- this.metadata = config.metadata || {};
68
- }
69
-
70
- transitionTo(newState: TaskState): void {
71
- const allowed = VALID_TRANSITIONS[this.status] || new Set();
72
- if (!allowed.has(newState)) {
73
- throw new Error(
74
- `Invalid task state transition: ${this.status} -> ${newState}`
75
- );
76
- }
77
- this.status = newState;
78
- }
79
-
80
- get allDeps(): string[] {
81
- const deps = [...this.dependsOn];
82
- if (this.parentId && !deps.includes(this.parentId)) {
83
- deps.push(this.parentId);
84
- }
85
- return deps;
86
- }
87
- }
88
-
89
- /** Result of executing a single task. */
90
- export class TaskResult {
91
- success: boolean;
92
- content: string;
93
- data: Record<string, any> = {};
94
-
95
- constructor(success: boolean, content: string, data?: Record<string, any>) {
96
- this.success = success;
97
- this.content = content;
98
- this.data = data || {};
99
- }
100
- }
1
+ /**
2
+ * Agent domain model — states and the Task DAG node.
3
+ *
4
+ * Extracted from the monolithic agent.ts (Phase 3). Pure, dependency-free,
5
+ * and unit-testable in isolation. `agent.ts` re-exports these so external
6
+ * importers of `../core/agent` are unaffected.
7
+ */
8
+
9
+ /** Lifecycle state of a running agent. */
10
+ export enum AgentState {
11
+ IDLE = 'idle',
12
+ THINKING = 'thinking',
13
+ ACTING = 'acting',
14
+ WAITING = 'waiting',
15
+ ERROR = 'error',
16
+ }
17
+
18
+ /** Lifecycle state of an orchestrated task. */
19
+ export enum TaskState {
20
+ PENDING = 'pending',
21
+ RUNNING = 'running',
22
+ COMPLETED = 'completed',
23
+ FAILED = 'failed',
24
+ SKIPPED = 'skipped',
25
+ }
26
+
27
+ /** Allowed task state transitions. A terminal state has no outgoing edges. */
28
+ export const VALID_TRANSITIONS: Record<TaskState, Set<TaskState>> = {
29
+ [TaskState.PENDING]: new Set([TaskState.RUNNING, TaskState.SKIPPED, TaskState.FAILED]),
30
+ [TaskState.RUNNING]: new Set([TaskState.RUNNING, TaskState.COMPLETED, TaskState.FAILED]),
31
+ [TaskState.FAILED]: new Set([TaskState.RUNNING, TaskState.SKIPPED]),
32
+ [TaskState.COMPLETED]: new Set(),
33
+ [TaskState.SKIPPED]: new Set(),
34
+ };
35
+
36
+ /** A node in the orchestration DAG. */
37
+ export class Task {
38
+ id: string;
39
+ description: string;
40
+ assignedTo: string | null = null;
41
+ parentId: string | null = null;
42
+ dependsOn: string[] = [];
43
+ status: TaskState = TaskState.PENDING;
44
+ priority: number = 0;
45
+ result: string | null = null;
46
+ metadata: Record<string, any> = {};
47
+
48
+ constructor(config: {
49
+ id: string;
50
+ description: string;
51
+ assignedTo?: string | null;
52
+ parentId?: string | null;
53
+ dependsOn?: string[];
54
+ status?: TaskState;
55
+ priority?: number;
56
+ result?: string | null;
57
+ metadata?: Record<string, any>;
58
+ }) {
59
+ this.id = config.id;
60
+ this.description = config.description;
61
+ this.assignedTo = config.assignedTo ?? null;
62
+ this.parentId = config.parentId ?? null;
63
+ this.dependsOn = config.dependsOn || [];
64
+ this.status = config.status ?? TaskState.PENDING;
65
+ this.priority = config.priority ?? 0;
66
+ this.result = config.result ?? null;
67
+ this.metadata = config.metadata || {};
68
+ }
69
+
70
+ transitionTo(newState: TaskState): void {
71
+ const allowed = VALID_TRANSITIONS[this.status] || new Set();
72
+ if (!allowed.has(newState)) {
73
+ throw new Error(
74
+ `Invalid task state transition: ${this.status} -> ${newState}`
75
+ );
76
+ }
77
+ this.status = newState;
78
+ }
79
+
80
+ get allDeps(): string[] {
81
+ const deps = [...this.dependsOn];
82
+ if (this.parentId && !deps.includes(this.parentId)) {
83
+ deps.push(this.parentId);
84
+ }
85
+ return deps;
86
+ }
87
+ }
88
+
89
+ /** Result of executing a single task. */
90
+ export class TaskResult {
91
+ success: boolean;
92
+ content: string;
93
+ data: Record<string, any> = {};
94
+
95
+ constructor(success: boolean, content: string, data?: Record<string, any>) {
96
+ this.success = success;
97
+ this.content = content;
98
+ this.data = data || {};
99
+ }
100
+ }
package/src/core/agent.ts CHANGED
@@ -14,25 +14,41 @@ import { Skill, SkillRegistry } from './skill';
14
14
  import { type ToolDefinition, ToolRegistry } from './tool';
15
15
  import {
16
16
  parseToolArgs,
17
- looksLikeFailedToolResult,
18
17
  extractFilePathsFromMessages,
19
18
  enrichResponseWithArtifacts,
20
- toolCallSignature,
21
- textSimilarity,
22
19
  formatArgsParseError,
23
20
  suggestToolNames,
24
21
  toolStatusLabel,
25
22
  synthesizeDelegationSummary,
26
23
  parseExtractedFacts,
27
- SIG_WINDOW,
28
- SIG_LOOP_HINT,
29
- SIG_LOOP_HARDSTOP,
30
24
  } from './agent_helpers';
31
25
  import { selectRelevantTools } from './tool_router';
32
26
  import { getModelInfo } from './catalog';
27
+ import { LoopGuard } from './agent/guard';
33
28
 
34
29
  const log = getLogger('agent');
35
30
 
31
+ /** Tools whose success means the filesystem changed (triggers the verify loop). */
32
+ const WRITE_TOOL_RE = /^(write_|edit_|delete_|create_)|^run_bash$|^git_commit$/;
33
+
34
+ /** Tools with side effects, hidden from the model while in plan mode. */
35
+ const SIDE_EFFECT_TOOL_RE = /^(write_|edit_|delete_|create_|kill_|launch_|service_|browser_)|^run_bash$|^git_commit$|^open_path$|^delegate_to$/;
36
+
37
+ /** Default context budget per recorded tool result (chars; ~3k tokens). */
38
+ const TOOL_RESULT_LIMIT = 12000;
39
+
40
+ /**
41
+ * Clamp an oversized tool result before it enters the context window:
42
+ * keep head + tail, tell the model what was cut and how to fetch precisely.
43
+ */
44
+ export function clampToolResult(s: string, limit: number = TOOL_RESULT_LIMIT): string {
45
+ if (s.length <= limit) return s;
46
+ const head = s.slice(0, Math.floor(limit * 0.72));
47
+ const tail = s.slice(-Math.floor(limit * 0.18));
48
+ const cut = s.length - head.length - tail.length;
49
+ return `${head}\n…[工具结果过长,中间省略 ${cut} 字符 — 需要该部分时用更精确的参数重新调用(read_file 的 offset/limit、grep 定位、缩小查询范围)]\n${tail}`;
50
+ }
51
+
36
52
  // Domain model lives in ./agent/task — re-exported here so importers of
37
53
  // '../core/agent' are unaffected by the Phase 3 split.
38
54
  import { AgentState, TaskState, Task, TaskResult } from './agent/task';
@@ -70,6 +86,11 @@ export class BaseAgent {
70
86
  protected _pendingRequests: Map<string, { resolve: (value: string) => void; reject: (err: Error) => void }> = new Map();
71
87
  protected _bgTasks: Set<Promise<void>> = new Set();
72
88
  approvalCallback: ((toolName: string, args: Record<string, any>) => Promise<boolean>) | null = null;
89
+ /** Plan mode: read-only tool set + plan-first instructions on each turn. */
90
+ planMode: boolean = false;
91
+ /** Set when this turn executed a tool that mutates the filesystem (verify trigger). */
92
+ protected _turnWroteFiles: boolean = false;
93
+ private _hooks: import('./hooks').Hooks | null = null;
73
94
  protected _turnLock: Promise<void> = Promise.resolve();
74
95
  private _turnLockCounter: number = 0;
75
96
  private _turnLockResolve: (() => void) | null = null;
@@ -150,10 +171,10 @@ export class BaseAgent {
150
171
  const lang = (this.config as any).llm?.language || 'zh';
151
172
  if (lang === 'en') {
152
173
  return prompt +
153
- `\n\n## Thinking Protocol\nBefore acting, briefly weigh: (1) **What** is the actual need? (2) **How** sure am I? If <80%, flag with [uncertain] and ask.\nIf stuck, admit it — propose a partial answer or ask the user. Never fabricate.\n\n## Behavior\n- Act, don't narrate. No "I will..." before tool calls.\n- Stay in scope. Do what's asked, then stop.\n- Batch independent tool calls in one response.\n- Verify writes: read back, report verified state.\n- Call list_skills when the task needs specialized capabilities.`;
174
+ `\n\n## Thinking Protocol\nBefore acting, briefly weigh: (1) **What** is the actual need? (2) **How** sure am I? If <80%, flag with [uncertain] and ask.\nIf stuck, admit it — propose a partial answer or ask the user. Never fabricate.\n\n## Behavior\n- Act, don't narrate. No "I will..." before tool calls.\n- Stay in scope. Do what's asked, then stop.\n- Batch independent tool calls in one response.\n- For tasks with 3+ steps, plan with todo_write first and update item status as you go.\n- Verify writes: read back, report verified state.\n- Call list_skills when the task needs specialized capabilities.`;
154
175
  }
155
176
  return prompt +
156
- `\n\n## 思考协议\n行动前快速判断:(1) 用户真实需求是什么?(2) 我有多大把握?低于80%标注 [不确定] 并主动询问。\n卡住时承认,给出部分答案或请求用户指导。绝不编造。\n\n## 行为守则\n- 直接行动,不预告。不说「我将要...」,直接调用工具\n- 不擅自扩大范围。用户要什么做什么,核心完成即止\n- 独立的工具调用一次发出,并行执行\n- 写入后回读验证,汇报已验证状态而非仅尝试\n- 任务涉及专业能力时(PPT/Excel/PDF/网页设计/代码审查等),先调 list_skills 查看可用技能,再用 use_skill 激活`;
177
+ `\n\n## 思考协议\n行动前快速判断:(1) 用户真实需求是什么?(2) 我有多大把握?低于80%标注 [不确定] 并主动询问。\n卡住时承认,给出部分答案或请求用户指导。绝不编造。\n\n## 行为守则\n- 直接行动,不预告。不说「我将要...」,直接调用工具\n- 不擅自扩大范围。用户要什么做什么,核心完成即止\n- 独立的工具调用一次发出,并行执行\n- 3 步以上的任务先用 todo_write 列任务清单,开工/完成时逐项更新状态\n- 写入后回读验证,汇报已验证状态而非仅尝试\n- 任务涉及专业能力时(PPT/Excel/PDF/网页设计/代码审查等),先调 list_skills 查看可用技能,再用 use_skill 激活`;
157
178
  }
158
179
 
159
180
  protected injectProgrammingWisdom(prompt: string): string {
@@ -164,16 +185,34 @@ export class BaseAgent {
164
185
  return prompt + `\n\n## 工程能力\n顶级工程师:类型安全、真实的错误处理、按根因调试、按安全与性能审查。你可以阅读和修改 Skyloom 自身源码。`;
165
186
  }
166
187
 
188
+ /** Layered SKY.md / CLAUDE.md / AGENTS.md project memory (see core/skymd). */
189
+ protected injectProjectMemory(prompt: string): string {
190
+ try {
191
+ const { loadProjectMemory } = require('./skymd');
192
+ const mem = loadProjectMemory();
193
+ if (!mem.text) return prompt;
194
+ return prompt + `\n\n## 项目记忆 (SKY.md)\n用户与项目维护的约定,优先级高于你的通用习惯:\n\n${mem.text}`;
195
+ } catch {
196
+ return prompt;
197
+ }
198
+ }
199
+
167
200
  reinitLanguage(): void {
168
201
  this._baseSystemPrompt = '';
169
202
  this._baseSystemPrompt = this.resolveSystemPrompt();
170
203
  this._baseSystemPrompt = this.injectWorkspaceInfo(this._baseSystemPrompt);
171
204
  this._baseSystemPrompt = this.injectBehaviorRules(this._baseSystemPrompt);
172
205
  this._baseSystemPrompt = this.injectProgrammingWisdom(this._baseSystemPrompt);
206
+ this._baseSystemPrompt = this.injectProjectMemory(this._baseSystemPrompt);
173
207
  this._baseSystemPrompt += '\n\n' + this.currentTimeTag();
174
208
  this.rebuildSystemPrompt();
175
209
  }
176
210
 
211
+ /** Re-read SKY.md layers into the system prompt (after `#` quick memory / edits). */
212
+ reloadProjectMemory(): void {
213
+ this.reinitLanguage();
214
+ }
215
+
177
216
  async init(): Promise<void> {
178
217
  if (this._baseSystemPrompt) return;
179
218
  await this.memory.initDb();
@@ -191,6 +230,7 @@ export class BaseAgent {
191
230
  this._baseSystemPrompt = this.injectWorkspaceInfo(this._baseSystemPrompt);
192
231
  this._baseSystemPrompt = this.injectBehaviorRules(this._baseSystemPrompt);
193
232
  this._baseSystemPrompt = this.injectProgrammingWisdom(this._baseSystemPrompt);
233
+ this._baseSystemPrompt = this.injectProjectMemory(this._baseSystemPrompt);
194
234
  this._baseSystemPrompt += '\n\n' + this.currentTimeTag();
195
235
  this.rebuildSystemPrompt();
196
236
  this._tools = this.toolRegistry.getTools();
@@ -217,6 +257,13 @@ export class BaseAgent {
217
257
  description: 'List all available skills with their names and descriptions. Use this first to discover what skills you can activate.',
218
258
  parameters: [],
219
259
  handler: async () => {
260
+ // live change detection: re-scan user/project skill folders so a
261
+ // SKILL.md edit or drop-in applies without restarting the session
262
+ try {
263
+ const { registerDynamicSkills } = require('../skills/loader');
264
+ registerDynamicSkills(self.skillRegistry);
265
+ self.loadSkills();
266
+ } catch { /* live reload is best-effort */ }
220
267
  const skills = self.getAvailableSkills();
221
268
  if (!skills.length) return 'No skills available.';
222
269
  const maxName = Math.max(...skills.map(s => s.name.length), 1);
@@ -558,9 +605,37 @@ export class BaseAgent {
558
605
  if (onStatus) onStatus(p.label);
559
606
  await this.setState(AgentState.ACTING);
560
607
 
608
+ // File checkpoint: snapshot the target before any mutating file tool
609
+ // runs, so /rewind can restore the pre-turn state.
610
+ try {
611
+ const { getFileCheckpoints } = require('./file_checkpoint');
612
+ const cp = getFileCheckpoints();
613
+ const snapPath = cp.pathToSnapshot(p.toolName, p.toolArgs || {});
614
+ if (snapPath) cp.snapshot(snapPath);
615
+ } catch { /* checkpointing must never block execution */ }
616
+
617
+ // pre_tool hooks are enforced policy: a non-zero exit blocks the call.
618
+ const hooks = this.getHooks();
619
+ if (hooks.preTool.length > 0) {
620
+ try {
621
+ const { runPreToolHooks } = require('./hooks');
622
+ const pre = runPreToolHooks(hooks, p.toolName, p.toolArgs || {}, this.name);
623
+ if (!pre.allowed) {
624
+ return { idx, result: { tc: p.tc, result: `[blocked by pre_tool hook] ${pre.reason}`, success: false, toolName: p.toolName } };
625
+ }
626
+ } catch { /* hook machinery must never break tool execution */ }
627
+ }
628
+
561
629
  try {
562
630
  const toolResult = await this.toolRegistry.execute(p.toolName, p.toolArgs || {});
563
631
  const resultStr = toolResult.result || toolResult.error || '(no output)';
632
+ if (toolResult.success && WRITE_TOOL_RE.test(p.toolName)) this._turnWroteFiles = true;
633
+ if (hooks.postTool.length > 0) {
634
+ try {
635
+ const { runPostToolHooks } = require('./hooks');
636
+ runPostToolHooks(hooks, p.toolName, p.toolArgs || {}, this.name);
637
+ } catch { /* best-effort */ }
638
+ }
564
639
  return { idx, result: { tc: p.tc, result: resultStr, success: toolResult.success, toolName: p.toolName } };
565
640
  } catch (e) {
566
641
  return { idx, result: { tc: p.tc, result: `Tool '${p.toolName}' execution failed: ${e}`, success: false, toolName: p.toolName } };
@@ -583,7 +658,9 @@ export class BaseAgent {
583
658
  }
584
659
  }
585
660
 
586
- // Phase D: Record results to memory
661
+ // Phase D: Record results to memory (clamped — one runaway read_file or
662
+ // http_get must not flood the context window)
663
+ const resultLimit = Number((this.config as any)?.llm?.tool_result_limit) || undefined;
587
664
  for (const r of results) {
588
665
  if (!r) continue;
589
666
 
@@ -591,7 +668,7 @@ export class BaseAgent {
591
668
  if (suppressed) suppressed.add(r.toolName);
592
669
  }
593
670
 
594
- this.memory.addMessage('tool', r.result, {
671
+ this.memory.addMessage('tool', clampToolResult(r.result, resultLimit), {
595
672
  name: r.toolName,
596
673
  toolCallId: r.tc.id,
597
674
  ephemeral,
@@ -731,7 +808,15 @@ export class BaseAgent {
731
808
  signal?: AbortSignal
732
809
  ): AsyncGenerator<Record<string, any>> {
733
810
  await this.setState(AgentState.THINKING);
734
- this.memory.addMessage('user', message);
811
+ // Plan mode: the tag travels with the message so the model plans instead
812
+ // of acting, and the read-only tool filter below removes the temptation.
813
+ const userMessage = this.planMode
814
+ ? `[计划模式] 只读调研,不要执行任何修改。请输出一份编号的执行计划(涉及哪些文件、每步做什么、风险点),等待用户批准后再实施。\n\n${message}`
815
+ : message;
816
+ this.memory.addMessage('user', userMessage);
817
+ try {
818
+ require('./file_checkpoint').getFileCheckpoints().beginTurn(message);
819
+ } catch { /* optional */ }
735
820
  let assistantStored = false;
736
821
 
737
822
  if (this.shouldAutoCompact()) {
@@ -749,20 +834,22 @@ export class BaseAgent {
749
834
  );
750
835
  }
751
836
 
752
- const recentToolOutcomes: boolean[] = [];
753
- let stuckHintInjected = false;
754
- const recentResponseTexts: string[] = [];
755
- let repetitionHintInjected = false;
756
- const recentToolSigs: string[] = [];
757
- let toolLoopHintInjected = false;
837
+ const guard = new LoopGuard();
758
838
 
759
839
  let toolNamesCache: string[] | null = null;
760
840
  let cacheKey: string | null = null;
761
841
 
762
842
  const resolveToolNames = (): string[] => {
763
- const key = JSON.stringify([[...suppressedTools].sort(), [...this._activeSkills].sort()]);
843
+ const key = JSON.stringify([[...suppressedTools].sort(), [...this._activeSkills].sort(), this.planMode]);
764
844
  if (toolNamesCache !== null && cacheKey === key) return toolNamesCache;
765
845
  let candidates = this.activeToolNames().filter(t => !suppressedTools.has(t));
846
+ if (this.planMode) {
847
+ candidates = candidates.filter(n => {
848
+ if (SIDE_EFFECT_TOOL_RE.test(n)) return false;
849
+ const t = this.toolRegistry.get(n);
850
+ return !(t as any)?.dangerous;
851
+ });
852
+ }
766
853
  const must = new Set<string>();
767
854
  for (const s of this._skills) {
768
855
  if (this._activeSkills.has(s.name)) {
@@ -914,82 +1001,12 @@ export class BaseAgent {
914
1001
  return;
915
1002
  }
916
1003
 
917
- // ── Narration-loop detection ──
918
- const normalizedRound = roundContent.trim();
919
- if (normalizedRound && recentResponseTexts.length > 0) {
920
- const highSim = recentResponseTexts.slice(-2).some(prev => textSimilarity(normalizedRound, prev) >= 0.7);
921
- if (highSim && !repetitionHintInjected) {
922
- this.memory.addMessage('system', '[Stop narrating] Your last response is highly similar to your previous one. Stop writing prose. Either: (1) emit ONLY the next tool call, or (2) output the final deliverable.');
923
- repetitionHintInjected = true;
924
- }
925
- }
926
- recentResponseTexts.push(normalizedRound);
927
- if (recentResponseTexts.length > 3) recentResponseTexts.shift();
928
-
929
- // ── Tool-signature loop detection ──
930
- for (const tc of toolCallsReceived) {
931
- const tName = tc.function.name;
932
- if (['task_done', 'list_skills', 'use_skill'].includes(tName)) continue;
933
- const rawArgs = tc.function.arguments;
934
- const tArgs = typeof rawArgs === 'string' ? parseToolArgs(rawArgs) : rawArgs;
935
- const sig = toolCallSignature(tName, tArgs);
936
- if (sig) recentToolSigs.push(sig);
937
- }
938
- if (recentToolSigs.length > SIG_WINDOW) {
939
- recentToolSigs.splice(0, recentToolSigs.length - SIG_WINDOW);
940
- }
941
- if (recentToolSigs.length > 0) {
942
- const counts = new Map<string, number>();
943
- for (const s of recentToolSigs) counts.set(s, (counts.get(s) || 0) + 1);
944
- let topSig = '';
945
- let topCount = 0;
946
- for (const [s, c] of counts) { if (c > topCount) { topSig = s; topCount = c; } }
947
- if (topCount >= SIG_LOOP_HINT && !toolLoopHintInjected) {
948
- this.memory.addMessage('system', `[Tool loop] You have called \`${topSig}\` ${topCount}x in the last ${recentToolSigs.length} tool calls — you are iterating without converging. STOP repeating it.`);
949
- toolLoopHintInjected = true;
950
- }
951
- if (topCount >= SIG_LOOP_HARDSTOP) {
952
- this.memory.addMessage('assistant', `I have repeated \`${topSig}\` ${topCount} times without converging. Stopping.`);
953
- yield { type: 'content', text: `\n\n[stuck] tool \`${topSig}\` repeated ${topCount}x — stopping.` };
954
- await this.setState(AgentState.IDLE);
955
- yield { type: 'done' };
956
- return;
957
- }
958
- }
959
-
960
- // ── Stuck-loop detection ──
961
- for (const r of execResults) {
962
- if (!r || r.toolName === 'task_done') continue;
963
- const failed = !r.success || (typeof r.result === 'string' && looksLikeFailedToolResult(r.result));
964
- recentToolOutcomes.push(!failed);
965
- if (recentToolOutcomes.length > 6) recentToolOutcomes.shift();
966
- }
967
-
968
- if (!stuckHintInjected && recentToolOutcomes.length >= 5 &&
969
- recentToolOutcomes.filter(Boolean).length <= 1) {
970
- this.memory.addMessage('system', '[Recovery hint] Your last several tool calls have mostly failed. Synthesize a partial answer from what worked or ask the user for guidance.');
971
- stuckHintInjected = true;
972
- }
973
-
974
- if (recentToolOutcomes.length >= 8 && recentToolOutcomes.every(x => !x)) {
975
- this.memory.addMessage('assistant', 'Every recent tool call failed. Please give me more context.');
976
- yield { type: 'content', text: '\n\n[stuck] every recent tool call failed — stopping.\n' };
977
- await this.setState(AgentState.IDLE);
978
- yield { type: 'done' };
979
- return;
980
- }
981
-
982
- // ── Search-storm detection ──
983
- const searchStormCount = recentToolSigs.filter(s =>
984
- s.startsWith('web_search:') || ['fetch_page', 'http_get'].includes(s)
985
- ).length;
986
- if (searchStormCount >= 8 && !toolLoopHintInjected) {
987
- this.memory.addMessage('system', `[Search storm] ${searchStormCount} search calls. STOP searching and synthesize.`);
988
- toolLoopHintInjected = true;
989
- }
990
- if (searchStormCount >= 12) {
991
- this.memory.addMessage('assistant', 'Too many search requests. Synthesizing best answer.');
992
- yield { type: 'content', text: `\n\n[stuck] excessive web searching (${searchStormCount} calls) — stopping.\n` };
1004
+ // ── Anti-loop guard (narration / tool-signature / stuck / search-storm) ──
1005
+ const decision = guard.observe(roundContent, toolCallsReceived, execResults);
1006
+ for (const hint of decision.hints) this.memory.addMessage('system', hint);
1007
+ if (decision.stop) {
1008
+ this.memory.addMessage('assistant', decision.stop.note);
1009
+ yield { type: 'content', text: decision.stop.contentLine };
993
1010
  await this.setState(AgentState.IDLE);
994
1011
  yield { type: 'done' };
995
1012
  return;
@@ -1109,6 +1126,25 @@ export class BaseAgent {
1109
1126
  };
1110
1127
  }
1111
1128
 
1129
+ /** Per-role token breakdown for the /context command. */
1130
+ contextDetail(): Record<string, any> {
1131
+ const byRole: Record<string, { tokens: number; count: number }> = {};
1132
+ for (const m of this.memory.shortTerm) {
1133
+ const extra = (m as any).toolCalls ? JSON.stringify((m as any).toolCalls).length : 0;
1134
+ const tokens = Math.ceil(((m.content || '').length + extra) / 4);
1135
+ const slot = byRole[m.role] || (byRole[m.role] = { tokens: 0, count: 0 });
1136
+ slot.tokens += tokens;
1137
+ slot.count += 1;
1138
+ }
1139
+ return {
1140
+ ...this.contextUsage(),
1141
+ byRole,
1142
+ systemPromptTokens: Math.ceil(this._baseSystemPrompt.length / 4),
1143
+ toolCount: this.activeToolNames().length,
1144
+ activeSkills: [...this._activeSkills],
1145
+ };
1146
+ }
1147
+
1112
1148
  protected shouldAutoCompact(): boolean {
1113
1149
  const usage = this.memory.getContextWindowUsage();
1114
1150
  // Compact before hitting the real window — leave ~20% headroom for the reply.
@@ -1339,9 +1375,43 @@ export class BaseAgent {
1339
1375
 
1340
1376
  this.memory.addMessage('user', prompt);
1341
1377
  const preLen = this.memory.shortTerm.length;
1378
+ this._turnWroteFiles = false;
1379
+ try {
1380
+ require('./file_checkpoint').getFileCheckpoints().beginTurn(`[task] ${task.description}`);
1381
+ } catch { /* optional */ }
1342
1382
 
1343
1383
  try {
1344
- const response = await this.llmLoop({ onStatus, ephemeral: true });
1384
+ let response = await this.llmLoop({ onStatus, ephemeral: true });
1385
+
1386
+ // ── 验证闭环: if this task touched the filesystem and verify commands
1387
+ // are configured (config.verify or SKY.md "## Verify"), run them and
1388
+ // feed failures back for a bounded number of fix rounds. ──
1389
+ try {
1390
+ const { resolveVerifyConfig, runVerify } = require('./verify');
1391
+ const vc = resolveVerifyConfig(this.config);
1392
+ if (vc.commands.length > 0 && this._turnWroteFiles) {
1393
+ for (let round = 0; round <= vc.maxFixRounds; round++) {
1394
+ if (onStatus) onStatus(`verify: ${vc.commands.length} 条命令`);
1395
+ const vr = runVerify(vc);
1396
+ if (vr.ok) {
1397
+ response.content += `\n\n[verify ✓ 全部通过]\n${vr.report}`;
1398
+ break;
1399
+ }
1400
+ if (round === vc.maxFixRounds) {
1401
+ response.content += `\n\n[verify ✗ 经 ${vc.maxFixRounds} 轮修复仍未通过]\n${vr.report.slice(0, 1500)}`;
1402
+ break;
1403
+ }
1404
+ if (onStatus) onStatus(`verify 失败 — 修复第 ${round + 1}/${vc.maxFixRounds} 轮`);
1405
+ log.warn('verify_failed_fixing', { agent: this.name, round: round + 1 });
1406
+ this.memory.addMessage('user',
1407
+ `[自动验证失败] 以下验证命令未通过。请定位根因并修复,确保它们全部通过:\n\n${vr.report}`);
1408
+ response = await this.llmLoop({ onStatus, ephemeral: true });
1409
+ }
1410
+ }
1411
+ } catch (e) {
1412
+ log.warn('verify_loop_error', { error: String(e) });
1413
+ }
1414
+
1345
1415
  const filePaths = extractFilePathsFromMessages(this.memory.shortTerm.slice(preLen));
1346
1416
  const enriched = enrichResponseWithArtifacts(response.content, filePaths);
1347
1417
  this.memory.addMessage('assistant', enriched, { toolCalls: response.toolCalls, reasoningContent: response.reasoningContent });
@@ -1365,6 +1435,18 @@ export class BaseAgent {
1365
1435
  private _security: any = null;
1366
1436
  get security(): any { if (!this._security) { try { const { getSecurity } = require('./security'); this._security = getSecurity(); } catch { this._security = {}; } } return this._security; }
1367
1437
 
1438
+ protected getHooks(): import('./hooks').Hooks {
1439
+ if (!this._hooks) {
1440
+ try {
1441
+ const { loadHooks } = require('./hooks');
1442
+ this._hooks = loadHooks(this.config);
1443
+ } catch {
1444
+ this._hooks = { sessionStart: [], preTool: [], postTool: [] };
1445
+ }
1446
+ }
1447
+ return this._hooks!;
1448
+ }
1449
+
1368
1450
  protected async checkToolApproval(toolName: string, toolArgs: Record<string, any>): Promise<boolean> {
1369
1451
  try {
1370
1452
  const sec = this.security;