skyloom 1.13.6 → 1.13.8
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.
- package/.github/workflows/ci.yml +36 -36
- package/README.md +220 -159
- package/config/providers.yaml +39 -39
- package/config/skills/api_integrator/SKILL.md +15 -15
- package/config/skills/arch_designer/SKILL.md +13 -13
- package/config/skills/ci_cd_manager/SKILL.md +14 -14
- package/config/skills/code_analysis/SKILL.md +13 -13
- package/config/skills/code_generator/SKILL.md +12 -12
- package/config/skills/code_reviewer/SKILL.md +13 -13
- package/config/skills/content_writer/SKILL.md +14 -14
- package/config/skills/data_transformer/SKILL.md +15 -15
- package/config/skills/document_analysis/SKILL.md +13 -13
- package/config/skills/emotional_companion/SKILL.md +15 -15
- package/config/skills/performance_checker/SKILL.md +14 -14
- package/config/skills/security_auditor/SKILL.md +14 -14
- package/config/skills/self_evolve/SKILL.md +13 -13
- package/config/skills/sys_operator/SKILL.md +15 -15
- package/config/skills/task_planner/SKILL.md +14 -14
- package/config/skills/web_research/SKILL.md +14 -14
- package/config/skills/workflow_designer/SKILL.md +13 -13
- package/dist/agents/dew.js +52 -52
- package/dist/agents/fair.js +84 -84
- package/dist/agents/fog.js +30 -30
- package/dist/agents/frost.js +32 -32
- package/dist/agents/rain.js +32 -32
- package/dist/agents/snow.js +68 -68
- package/dist/cli/commands_md.d.ts +41 -0
- package/dist/cli/commands_md.d.ts.map +1 -0
- package/dist/cli/commands_md.js +140 -0
- package/dist/cli/commands_md.js.map +1 -0
- package/dist/cli/input_macros.d.ts +28 -0
- package/dist/cli/input_macros.d.ts.map +1 -0
- package/dist/cli/input_macros.js +120 -0
- package/dist/cli/input_macros.js.map +1 -0
- package/dist/cli/loom.d.ts +220 -0
- package/dist/cli/loom.d.ts.map +1 -0
- package/dist/cli/loom.js +1094 -0
- package/dist/cli/loom.js.map +1 -0
- package/dist/cli/loom_chat.d.ts +20 -0
- package/dist/cli/loom_chat.d.ts.map +1 -0
- package/dist/cli/loom_chat.js +685 -0
- package/dist/cli/loom_chat.js.map +1 -0
- package/dist/cli/main.js +310 -14
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +7 -1
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent.d.ts +20 -0
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +199 -16
- package/dist/core/agent.js.map +1 -1
- package/dist/core/factory.d.ts.map +1 -1
- package/dist/core/factory.js +34 -2
- package/dist/core/factory.js.map +1 -1
- package/dist/core/file_checkpoint.d.ts +57 -0
- package/dist/core/file_checkpoint.d.ts.map +1 -0
- package/dist/core/file_checkpoint.js +162 -0
- package/dist/core/file_checkpoint.js.map +1 -0
- package/dist/core/hooks.d.ts +43 -0
- package/dist/core/hooks.d.ts.map +1 -0
- package/dist/core/hooks.js +110 -0
- package/dist/core/hooks.js.map +1 -0
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +15 -9
- package/dist/core/llm.js.map +1 -1
- package/dist/core/longdoc.js +5 -5
- package/dist/core/mcp.d.ts +16 -0
- package/dist/core/mcp.d.ts.map +1 -1
- package/dist/core/mcp.js +55 -0
- package/dist/core/mcp.js.map +1 -1
- package/dist/core/model_config.d.ts +40 -0
- package/dist/core/model_config.d.ts.map +1 -0
- package/dist/core/model_config.js +191 -0
- package/dist/core/model_config.js.map +1 -0
- package/dist/core/skill.d.ts +7 -0
- package/dist/core/skill.d.ts.map +1 -1
- package/dist/core/skill.js +47 -0
- package/dist/core/skill.js.map +1 -1
- package/dist/core/skymd.d.ts +39 -0
- package/dist/core/skymd.d.ts.map +1 -0
- package/dist/core/skymd.js +177 -0
- package/dist/core/skymd.js.map +1 -0
- package/dist/core/tool.d.ts +12 -0
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +30 -0
- package/dist/core/tool.js.map +1 -1
- package/dist/core/verify.d.ts +27 -0
- package/dist/core/verify.d.ts.map +1 -0
- package/dist/core/verify.js +62 -0
- package/dist/core/verify.js.map +1 -0
- package/dist/skills/loader.d.ts +22 -2
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +45 -15
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +13 -3
- package/dist/tools/builtin.js.map +1 -1
- package/dist/tools/model_tool.d.ts +11 -0
- package/dist/tools/model_tool.d.ts.map +1 -0
- package/dist/tools/model_tool.js +71 -0
- package/dist/tools/model_tool.js.map +1 -0
- package/dist/tools/todo.d.ts +30 -0
- package/dist/tools/todo.d.ts.map +1 -0
- package/dist/tools/todo.js +78 -0
- package/dist/tools/todo.js.map +1 -0
- package/docs/AESTHETIC_DESIGN.md +152 -144
- package/docs/OPTIMIZATION_PLAN.md +178 -178
- package/package.json +68 -68
- package/scripts/install.js +48 -48
- package/scripts/link.js +10 -10
- package/setup.bat +79 -79
- package/skill-test-ty2fOA/test.md +10 -10
- package/src/agents/dew.ts +70 -70
- package/src/agents/fair.ts +102 -102
- package/src/agents/fog.ts +48 -48
- package/src/agents/frost.ts +50 -50
- package/src/agents/rain.ts +50 -50
- package/src/agents/snow.ts +239 -239
- package/src/cli/commands_md.ts +112 -0
- package/src/cli/input_macros.ts +83 -0
- package/src/cli/loom.ts +982 -0
- package/src/cli/loom_chat.ts +598 -0
- package/src/cli/main.ts +255 -9
- package/src/cli/mode.ts +58 -58
- package/src/cli/tui.ts +228 -222
- package/src/core/agent/guard.ts +134 -134
- package/src/core/agent/task.ts +100 -100
- package/src/core/agent.ts +195 -16
- package/src/core/arbitrate.ts +162 -162
- package/src/core/catalog.ts +178 -178
- package/src/core/checkpoint.ts +94 -94
- package/src/core/estimate.ts +104 -104
- package/src/core/evolve.ts +191 -191
- package/src/core/factory.ts +31 -2
- package/src/core/file_checkpoint.ts +136 -0
- package/src/core/filter.ts +103 -103
- package/src/core/graph.ts +156 -156
- package/src/core/hooks.ts +126 -0
- package/src/core/icons.ts +53 -53
- package/src/core/index.ts +37 -37
- package/src/core/learn.ts +146 -146
- package/src/core/llm.ts +15 -9
- package/src/core/longdoc.ts +155 -155
- package/src/core/mcp.ts +48 -0
- package/src/core/mcp_server.ts +176 -176
- package/src/core/model_config.ts +157 -0
- package/src/core/profile.ts +255 -255
- package/src/core/router.ts +124 -124
- package/src/core/sandbox.ts +142 -142
- package/src/core/security.ts +243 -243
- package/src/core/skill.ts +42 -0
- package/src/core/skymd.ts +143 -0
- package/src/core/theme.ts +65 -65
- package/src/core/tool.ts +30 -0
- package/src/core/tool_router.ts +193 -193
- package/src/core/vector.ts +152 -152
- package/src/core/verify.ts +71 -0
- package/src/core/workspace.ts +150 -150
- package/src/plugins/loader.ts +66 -66
- package/src/skills/loader.ts +45 -16
- package/src/sql.js.d.ts +29 -29
- package/src/tools/builtin.ts +13 -3
- package/src/tools/computer.ts +269 -269
- package/src/tools/delegate.ts +49 -49
- package/src/tools/model_tool.ts +74 -0
- package/src/tools/todo.ts +76 -0
- package/src/web/tts.ts +93 -93
- package/tests/agent.test.ts +159 -159
- package/tests/agent_helpers.test.ts +48 -48
- package/tests/bus.test.ts +121 -121
- package/tests/catalog.test.ts +86 -86
- package/tests/checkpoint_commands.test.ts +124 -0
- package/tests/claude_compat.test.ts +110 -0
- package/tests/config.test.ts +41 -41
- package/tests/guard.test.ts +75 -75
- package/tests/icons.test.ts +45 -45
- package/tests/loom.test.ts +248 -0
- package/tests/memory.test.ts +170 -170
- package/tests/model_config.test.ts +109 -0
- package/tests/router.test.ts +86 -86
- package/tests/schemas.test.ts +51 -51
- package/tests/semantic.test.ts +83 -83
- package/tests/setup.ts +10 -10
- package/tests/skill.test.ts +172 -172
- package/tests/skymd.test.ts +146 -0
- package/tests/task.test.ts +60 -60
- package/tests/todo_toolstats.test.ts +94 -0
- package/tests/tool.test.ts +108 -108
- package/tests/tool_router.test.ts +71 -71
- package/tests/tui.test.ts +67 -67
- package/vitest.config.ts +17 -17
- package/=12 +0 -0
- package/=8 +0 -0
package/src/core/agent.ts
CHANGED
|
@@ -28,6 +28,27 @@ import { LoopGuard } from './agent/guard';
|
|
|
28
28
|
|
|
29
29
|
const log = getLogger('agent');
|
|
30
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
|
+
|
|
31
52
|
// Domain model lives in ./agent/task — re-exported here so importers of
|
|
32
53
|
// '../core/agent' are unaffected by the Phase 3 split.
|
|
33
54
|
import { AgentState, TaskState, Task, TaskResult } from './agent/task';
|
|
@@ -65,6 +86,11 @@ export class BaseAgent {
|
|
|
65
86
|
protected _pendingRequests: Map<string, { resolve: (value: string) => void; reject: (err: Error) => void }> = new Map();
|
|
66
87
|
protected _bgTasks: Set<Promise<void>> = new Set();
|
|
67
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;
|
|
68
94
|
protected _turnLock: Promise<void> = Promise.resolve();
|
|
69
95
|
private _turnLockCounter: number = 0;
|
|
70
96
|
private _turnLockResolve: (() => void) | null = null;
|
|
@@ -129,26 +155,41 @@ export class BaseAgent {
|
|
|
129
155
|
}
|
|
130
156
|
}
|
|
131
157
|
|
|
158
|
+
/** Always return the live current time — never stale. */
|
|
132
159
|
protected currentTimeTag(): string {
|
|
133
|
-
const now = Date.now() / 1000;
|
|
134
|
-
if (BaseAgent._timeTag !== null && now - BaseAgent._timeTagTs < 30) {
|
|
135
|
-
return BaseAgent._timeTag;
|
|
136
|
-
}
|
|
137
160
|
const date = new Date();
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return
|
|
161
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
162
|
+
const iso = date.toISOString();
|
|
163
|
+
const local = date.toLocaleString("zh-CN", { hour12: false, year: "numeric", month: "2-digit", day: "2-digit", weekday: "long", hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
164
|
+
return `Current time: ${iso.slice(0, 19).replace("T", " ")} UTC (${local} ${tz})`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Inject live time into messages before every LLM call. */
|
|
168
|
+
protected injectCurrentTime(): void {
|
|
169
|
+
const tag = `[${this.currentTimeTag()}]`;
|
|
170
|
+
// Replace any previous time tag in the most recent system message or append
|
|
171
|
+
for (let i = this.memory.shortTerm.length - 1; i >= 0; i--) {
|
|
172
|
+
const m = this.memory.shortTerm[i];
|
|
173
|
+
if (m.role === "system" && (m.content || "").startsWith("[Current time:")) {
|
|
174
|
+
m.content = tag; return;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// No existing time tag — insert one before the last user message
|
|
178
|
+
const lastUser = [...this.memory.shortTerm].reverse().findIndex(m => m.role === "user");
|
|
179
|
+
if (lastUser >= 0) {
|
|
180
|
+
const idx = this.memory.shortTerm.length - 1 - lastUser;
|
|
181
|
+
this.memory.shortTerm.splice(idx, 0, { role: "system", content: tag });
|
|
182
|
+
}
|
|
142
183
|
}
|
|
143
184
|
|
|
144
185
|
protected injectBehaviorRules(prompt: string): string {
|
|
145
186
|
const lang = (this.config as any).llm?.language || 'zh';
|
|
146
187
|
if (lang === 'en') {
|
|
147
188
|
return prompt +
|
|
148
|
-
`\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.`;
|
|
189
|
+
`\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.`;
|
|
149
190
|
}
|
|
150
191
|
return prompt +
|
|
151
|
-
`\n\n## 思考协议\n行动前快速判断:(1) 用户真实需求是什么?(2) 我有多大把握?低于80%标注 [不确定] 并主动询问。\n卡住时承认,给出部分答案或请求用户指导。绝不编造。\n\n## 行为守则\n- 直接行动,不预告。不说「我将要...」,直接调用工具\n- 不擅自扩大范围。用户要什么做什么,核心完成即止\n- 独立的工具调用一次发出,并行执行\n- 写入后回读验证,汇报已验证状态而非仅尝试\n- 任务涉及专业能力时(PPT/Excel/PDF/网页设计/代码审查等),先调 list_skills 查看可用技能,再用 use_skill 激活`;
|
|
192
|
+
`\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 激活`;
|
|
152
193
|
}
|
|
153
194
|
|
|
154
195
|
protected injectProgrammingWisdom(prompt: string): string {
|
|
@@ -159,16 +200,34 @@ export class BaseAgent {
|
|
|
159
200
|
return prompt + `\n\n## 工程能力\n顶级工程师:类型安全、真实的错误处理、按根因调试、按安全与性能审查。你可以阅读和修改 Skyloom 自身源码。`;
|
|
160
201
|
}
|
|
161
202
|
|
|
203
|
+
/** Layered SKY.md / CLAUDE.md / AGENTS.md project memory (see core/skymd). */
|
|
204
|
+
protected injectProjectMemory(prompt: string): string {
|
|
205
|
+
try {
|
|
206
|
+
const { loadProjectMemory } = require('./skymd');
|
|
207
|
+
const mem = loadProjectMemory();
|
|
208
|
+
if (!mem.text) return prompt;
|
|
209
|
+
return prompt + `\n\n## 项目记忆 (SKY.md)\n用户与项目维护的约定,优先级高于你的通用习惯:\n\n${mem.text}`;
|
|
210
|
+
} catch {
|
|
211
|
+
return prompt;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
162
215
|
reinitLanguage(): void {
|
|
163
216
|
this._baseSystemPrompt = '';
|
|
164
217
|
this._baseSystemPrompt = this.resolveSystemPrompt();
|
|
165
218
|
this._baseSystemPrompt = this.injectWorkspaceInfo(this._baseSystemPrompt);
|
|
166
219
|
this._baseSystemPrompt = this.injectBehaviorRules(this._baseSystemPrompt);
|
|
167
220
|
this._baseSystemPrompt = this.injectProgrammingWisdom(this._baseSystemPrompt);
|
|
221
|
+
this._baseSystemPrompt = this.injectProjectMemory(this._baseSystemPrompt);
|
|
168
222
|
this._baseSystemPrompt += '\n\n' + this.currentTimeTag();
|
|
169
223
|
this.rebuildSystemPrompt();
|
|
170
224
|
}
|
|
171
225
|
|
|
226
|
+
/** Re-read SKY.md layers into the system prompt (after `#` quick memory / edits). */
|
|
227
|
+
reloadProjectMemory(): void {
|
|
228
|
+
this.reinitLanguage();
|
|
229
|
+
}
|
|
230
|
+
|
|
172
231
|
async init(): Promise<void> {
|
|
173
232
|
if (this._baseSystemPrompt) return;
|
|
174
233
|
await this.memory.initDb();
|
|
@@ -186,6 +245,7 @@ export class BaseAgent {
|
|
|
186
245
|
this._baseSystemPrompt = this.injectWorkspaceInfo(this._baseSystemPrompt);
|
|
187
246
|
this._baseSystemPrompt = this.injectBehaviorRules(this._baseSystemPrompt);
|
|
188
247
|
this._baseSystemPrompt = this.injectProgrammingWisdom(this._baseSystemPrompt);
|
|
248
|
+
this._baseSystemPrompt = this.injectProjectMemory(this._baseSystemPrompt);
|
|
189
249
|
this._baseSystemPrompt += '\n\n' + this.currentTimeTag();
|
|
190
250
|
this.rebuildSystemPrompt();
|
|
191
251
|
this._tools = this.toolRegistry.getTools();
|
|
@@ -212,6 +272,13 @@ export class BaseAgent {
|
|
|
212
272
|
description: 'List all available skills with their names and descriptions. Use this first to discover what skills you can activate.',
|
|
213
273
|
parameters: [],
|
|
214
274
|
handler: async () => {
|
|
275
|
+
// live change detection: re-scan user/project skill folders so a
|
|
276
|
+
// SKILL.md edit or drop-in applies without restarting the session
|
|
277
|
+
try {
|
|
278
|
+
const { registerDynamicSkills } = require('../skills/loader');
|
|
279
|
+
registerDynamicSkills(self.skillRegistry);
|
|
280
|
+
self.loadSkills();
|
|
281
|
+
} catch { /* live reload is best-effort */ }
|
|
215
282
|
const skills = self.getAvailableSkills();
|
|
216
283
|
if (!skills.length) return 'No skills available.';
|
|
217
284
|
const maxName = Math.max(...skills.map(s => s.name.length), 1);
|
|
@@ -553,9 +620,37 @@ export class BaseAgent {
|
|
|
553
620
|
if (onStatus) onStatus(p.label);
|
|
554
621
|
await this.setState(AgentState.ACTING);
|
|
555
622
|
|
|
623
|
+
// File checkpoint: snapshot the target before any mutating file tool
|
|
624
|
+
// runs, so /rewind can restore the pre-turn state.
|
|
625
|
+
try {
|
|
626
|
+
const { getFileCheckpoints } = require('./file_checkpoint');
|
|
627
|
+
const cp = getFileCheckpoints();
|
|
628
|
+
const snapPath = cp.pathToSnapshot(p.toolName, p.toolArgs || {});
|
|
629
|
+
if (snapPath) cp.snapshot(snapPath);
|
|
630
|
+
} catch { /* checkpointing must never block execution */ }
|
|
631
|
+
|
|
632
|
+
// pre_tool hooks are enforced policy: a non-zero exit blocks the call.
|
|
633
|
+
const hooks = this.getHooks();
|
|
634
|
+
if (hooks.preTool.length > 0) {
|
|
635
|
+
try {
|
|
636
|
+
const { runPreToolHooks } = require('./hooks');
|
|
637
|
+
const pre = runPreToolHooks(hooks, p.toolName, p.toolArgs || {}, this.name);
|
|
638
|
+
if (!pre.allowed) {
|
|
639
|
+
return { idx, result: { tc: p.tc, result: `[blocked by pre_tool hook] ${pre.reason}`, success: false, toolName: p.toolName } };
|
|
640
|
+
}
|
|
641
|
+
} catch { /* hook machinery must never break tool execution */ }
|
|
642
|
+
}
|
|
643
|
+
|
|
556
644
|
try {
|
|
557
645
|
const toolResult = await this.toolRegistry.execute(p.toolName, p.toolArgs || {});
|
|
558
646
|
const resultStr = toolResult.result || toolResult.error || '(no output)';
|
|
647
|
+
if (toolResult.success && WRITE_TOOL_RE.test(p.toolName)) this._turnWroteFiles = true;
|
|
648
|
+
if (hooks.postTool.length > 0) {
|
|
649
|
+
try {
|
|
650
|
+
const { runPostToolHooks } = require('./hooks');
|
|
651
|
+
runPostToolHooks(hooks, p.toolName, p.toolArgs || {}, this.name);
|
|
652
|
+
} catch { /* best-effort */ }
|
|
653
|
+
}
|
|
559
654
|
return { idx, result: { tc: p.tc, result: resultStr, success: toolResult.success, toolName: p.toolName } };
|
|
560
655
|
} catch (e) {
|
|
561
656
|
return { idx, result: { tc: p.tc, result: `Tool '${p.toolName}' execution failed: ${e}`, success: false, toolName: p.toolName } };
|
|
@@ -578,7 +673,9 @@ export class BaseAgent {
|
|
|
578
673
|
}
|
|
579
674
|
}
|
|
580
675
|
|
|
581
|
-
// Phase D: Record results to memory
|
|
676
|
+
// Phase D: Record results to memory (clamped — one runaway read_file or
|
|
677
|
+
// http_get must not flood the context window)
|
|
678
|
+
const resultLimit = Number((this.config as any)?.llm?.tool_result_limit) || undefined;
|
|
582
679
|
for (const r of results) {
|
|
583
680
|
if (!r) continue;
|
|
584
681
|
|
|
@@ -586,7 +683,7 @@ export class BaseAgent {
|
|
|
586
683
|
if (suppressed) suppressed.add(r.toolName);
|
|
587
684
|
}
|
|
588
685
|
|
|
589
|
-
this.memory.addMessage('tool', r.result, {
|
|
686
|
+
this.memory.addMessage('tool', clampToolResult(r.result, resultLimit), {
|
|
590
687
|
name: r.toolName,
|
|
591
688
|
toolCallId: r.tc.id,
|
|
592
689
|
ephemeral,
|
|
@@ -651,7 +748,7 @@ export class BaseAgent {
|
|
|
651
748
|
if (options?.temperature != null) overrides.temperature = options.temperature;
|
|
652
749
|
if (options?.maxTokens != null) overrides.maxTokens = options.maxTokens;
|
|
653
750
|
|
|
654
|
-
const messages = [{ role: 'user', content: prompt }];
|
|
751
|
+
const messages = [{ role: 'system', content: `[${this.currentTimeTag()}]` }, { role: 'user', content: prompt }];
|
|
655
752
|
const response = await this.llm.complete(
|
|
656
753
|
messages,
|
|
657
754
|
this.name,
|
|
@@ -726,7 +823,15 @@ export class BaseAgent {
|
|
|
726
823
|
signal?: AbortSignal
|
|
727
824
|
): AsyncGenerator<Record<string, any>> {
|
|
728
825
|
await this.setState(AgentState.THINKING);
|
|
729
|
-
|
|
826
|
+
// Plan mode: the tag travels with the message so the model plans instead
|
|
827
|
+
// of acting, and the read-only tool filter below removes the temptation.
|
|
828
|
+
const userMessage = this.planMode
|
|
829
|
+
? `[计划模式] 只读调研,不要执行任何修改。请输出一份编号的执行计划(涉及哪些文件、每步做什么、风险点),等待用户批准后再实施。\n\n${message}`
|
|
830
|
+
: message;
|
|
831
|
+
this.memory.addMessage('user', userMessage);
|
|
832
|
+
try {
|
|
833
|
+
require('./file_checkpoint').getFileCheckpoints().beginTurn(message);
|
|
834
|
+
} catch { /* optional */ }
|
|
730
835
|
let assistantStored = false;
|
|
731
836
|
|
|
732
837
|
if (this.shouldAutoCompact()) {
|
|
@@ -750,9 +855,16 @@ export class BaseAgent {
|
|
|
750
855
|
let cacheKey: string | null = null;
|
|
751
856
|
|
|
752
857
|
const resolveToolNames = (): string[] => {
|
|
753
|
-
const key = JSON.stringify([[...suppressedTools].sort(), [...this._activeSkills].sort()]);
|
|
858
|
+
const key = JSON.stringify([[...suppressedTools].sort(), [...this._activeSkills].sort(), this.planMode]);
|
|
754
859
|
if (toolNamesCache !== null && cacheKey === key) return toolNamesCache;
|
|
755
860
|
let candidates = this.activeToolNames().filter(t => !suppressedTools.has(t));
|
|
861
|
+
if (this.planMode) {
|
|
862
|
+
candidates = candidates.filter(n => {
|
|
863
|
+
if (SIDE_EFFECT_TOOL_RE.test(n)) return false;
|
|
864
|
+
const t = this.toolRegistry.get(n);
|
|
865
|
+
return !(t as any)?.dangerous;
|
|
866
|
+
});
|
|
867
|
+
}
|
|
756
868
|
const must = new Set<string>();
|
|
757
869
|
for (const s of this._skills) {
|
|
758
870
|
if (this._activeSkills.has(s.name)) {
|
|
@@ -1029,6 +1141,25 @@ export class BaseAgent {
|
|
|
1029
1141
|
};
|
|
1030
1142
|
}
|
|
1031
1143
|
|
|
1144
|
+
/** Per-role token breakdown for the /context command. */
|
|
1145
|
+
contextDetail(): Record<string, any> {
|
|
1146
|
+
const byRole: Record<string, { tokens: number; count: number }> = {};
|
|
1147
|
+
for (const m of this.memory.shortTerm) {
|
|
1148
|
+
const extra = (m as any).toolCalls ? JSON.stringify((m as any).toolCalls).length : 0;
|
|
1149
|
+
const tokens = Math.ceil(((m.content || '').length + extra) / 4);
|
|
1150
|
+
const slot = byRole[m.role] || (byRole[m.role] = { tokens: 0, count: 0 });
|
|
1151
|
+
slot.tokens += tokens;
|
|
1152
|
+
slot.count += 1;
|
|
1153
|
+
}
|
|
1154
|
+
return {
|
|
1155
|
+
...this.contextUsage(),
|
|
1156
|
+
byRole,
|
|
1157
|
+
systemPromptTokens: Math.ceil(this._baseSystemPrompt.length / 4),
|
|
1158
|
+
toolCount: this.activeToolNames().length,
|
|
1159
|
+
activeSkills: [...this._activeSkills],
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1032
1163
|
protected shouldAutoCompact(): boolean {
|
|
1033
1164
|
const usage = this.memory.getContextWindowUsage();
|
|
1034
1165
|
// Compact before hitting the real window — leave ~20% headroom for the reply.
|
|
@@ -1122,6 +1253,8 @@ export class BaseAgent {
|
|
|
1122
1253
|
}
|
|
1123
1254
|
|
|
1124
1255
|
protected async messagesWithRecall(): Promise<Record<string, any>[]> {
|
|
1256
|
+
// Inject live time before every LLM call so the agent always knows the current time
|
|
1257
|
+
this.injectCurrentTime();
|
|
1125
1258
|
const messages = this.memory.getMessages();
|
|
1126
1259
|
if (!messages || process.env.WA_NO_RECALL === '1') return messages;
|
|
1127
1260
|
|
|
@@ -1259,9 +1392,43 @@ export class BaseAgent {
|
|
|
1259
1392
|
|
|
1260
1393
|
this.memory.addMessage('user', prompt);
|
|
1261
1394
|
const preLen = this.memory.shortTerm.length;
|
|
1395
|
+
this._turnWroteFiles = false;
|
|
1396
|
+
try {
|
|
1397
|
+
require('./file_checkpoint').getFileCheckpoints().beginTurn(`[task] ${task.description}`);
|
|
1398
|
+
} catch { /* optional */ }
|
|
1262
1399
|
|
|
1263
1400
|
try {
|
|
1264
|
-
|
|
1401
|
+
let response = await this.llmLoop({ onStatus, ephemeral: true });
|
|
1402
|
+
|
|
1403
|
+
// ── 验证闭环: if this task touched the filesystem and verify commands
|
|
1404
|
+
// are configured (config.verify or SKY.md "## Verify"), run them and
|
|
1405
|
+
// feed failures back for a bounded number of fix rounds. ──
|
|
1406
|
+
try {
|
|
1407
|
+
const { resolveVerifyConfig, runVerify } = require('./verify');
|
|
1408
|
+
const vc = resolveVerifyConfig(this.config);
|
|
1409
|
+
if (vc.commands.length > 0 && this._turnWroteFiles) {
|
|
1410
|
+
for (let round = 0; round <= vc.maxFixRounds; round++) {
|
|
1411
|
+
if (onStatus) onStatus(`verify: ${vc.commands.length} 条命令`);
|
|
1412
|
+
const vr = runVerify(vc);
|
|
1413
|
+
if (vr.ok) {
|
|
1414
|
+
response.content += `\n\n[verify ✓ 全部通过]\n${vr.report}`;
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1417
|
+
if (round === vc.maxFixRounds) {
|
|
1418
|
+
response.content += `\n\n[verify ✗ 经 ${vc.maxFixRounds} 轮修复仍未通过]\n${vr.report.slice(0, 1500)}`;
|
|
1419
|
+
break;
|
|
1420
|
+
}
|
|
1421
|
+
if (onStatus) onStatus(`verify 失败 — 修复第 ${round + 1}/${vc.maxFixRounds} 轮`);
|
|
1422
|
+
log.warn('verify_failed_fixing', { agent: this.name, round: round + 1 });
|
|
1423
|
+
this.memory.addMessage('user',
|
|
1424
|
+
`[自动验证失败] 以下验证命令未通过。请定位根因并修复,确保它们全部通过:\n\n${vr.report}`);
|
|
1425
|
+
response = await this.llmLoop({ onStatus, ephemeral: true });
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
} catch (e) {
|
|
1429
|
+
log.warn('verify_loop_error', { error: String(e) });
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1265
1432
|
const filePaths = extractFilePathsFromMessages(this.memory.shortTerm.slice(preLen));
|
|
1266
1433
|
const enriched = enrichResponseWithArtifacts(response.content, filePaths);
|
|
1267
1434
|
this.memory.addMessage('assistant', enriched, { toolCalls: response.toolCalls, reasoningContent: response.reasoningContent });
|
|
@@ -1285,6 +1452,18 @@ export class BaseAgent {
|
|
|
1285
1452
|
private _security: any = null;
|
|
1286
1453
|
get security(): any { if (!this._security) { try { const { getSecurity } = require('./security'); this._security = getSecurity(); } catch { this._security = {}; } } return this._security; }
|
|
1287
1454
|
|
|
1455
|
+
protected getHooks(): import('./hooks').Hooks {
|
|
1456
|
+
if (!this._hooks) {
|
|
1457
|
+
try {
|
|
1458
|
+
const { loadHooks } = require('./hooks');
|
|
1459
|
+
this._hooks = loadHooks(this.config);
|
|
1460
|
+
} catch {
|
|
1461
|
+
this._hooks = { sessionStart: [], preTool: [], postTool: [] };
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return this._hooks!;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1288
1467
|
protected async checkToolApproval(toolName: string, toolArgs: Record<string, any>): Promise<boolean> {
|
|
1289
1468
|
try {
|
|
1290
1469
|
const sec = this.security;
|