@kkelly-offical/kkcode 0.1.3 → 0.1.6
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/README.md +110 -172
- package/package.json +46 -46
- package/src/agent/agent.mjs +41 -0
- package/src/agent/prompt/frontend-designer.txt +58 -0
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
- package/src/agent/prompt/longagent-coding-agent.txt +37 -0
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
- package/src/agent/prompt/longagent-preview-agent.txt +63 -0
- package/src/config/defaults.mjs +260 -195
- package/src/config/schema.mjs +71 -6
- package/src/core/constants.mjs +91 -46
- package/src/index.mjs +1 -1
- package/src/knowledge/frontend-aesthetics.txt +39 -0
- package/src/knowledge/loader.mjs +2 -1
- package/src/knowledge/tailwind.txt +12 -3
- package/src/mcp/client-http.mjs +141 -157
- package/src/mcp/client-sse.mjs +288 -286
- package/src/mcp/client-stdio.mjs +533 -451
- package/src/mcp/constants.mjs +2 -0
- package/src/mcp/registry.mjs +479 -394
- package/src/mcp/stdio-framing.mjs +133 -127
- package/src/mcp/tool-result.mjs +24 -0
- package/src/observability/index.mjs +42 -0
- package/src/observability/metrics.mjs +137 -0
- package/src/observability/tracer.mjs +137 -0
- package/src/orchestration/background-manager.mjs +372 -358
- package/src/orchestration/background-worker.mjs +305 -245
- package/src/orchestration/longagent-manager.mjs +171 -116
- package/src/orchestration/stage-scheduler.mjs +728 -489
- package/src/permission/exec-policy.mjs +9 -11
- package/src/provider/anthropic.mjs +1 -0
- package/src/provider/openai.mjs +340 -339
- package/src/provider/retry-policy.mjs +68 -68
- package/src/provider/router.mjs +241 -228
- package/src/provider/sse.mjs +104 -91
- package/src/repl.mjs +1 -1
- package/src/session/checkpoint.mjs +66 -3
- package/src/session/engine.mjs +227 -225
- package/src/session/longagent-4stage.mjs +460 -0
- package/src/session/longagent-hybrid.mjs +1081 -0
- package/src/session/longagent-plan.mjs +365 -329
- package/src/session/longagent-project-memory.mjs +53 -0
- package/src/session/longagent-scaffold.mjs +291 -100
- package/src/session/longagent-task-bus.mjs +54 -0
- package/src/session/longagent-utils.mjs +472 -0
- package/src/session/longagent.mjs +884 -1462
- package/src/session/project-context.mjs +30 -0
- package/src/session/store.mjs +510 -503
- package/src/session/task-validator.mjs +4 -3
- package/src/skill/builtin/design.mjs +76 -0
- package/src/skill/builtin/frontend.mjs +8 -0
- package/src/skill/registry.mjs +390 -336
- package/src/storage/ghost-commit-store.mjs +18 -8
- package/src/tool/executor.mjs +11 -0
- package/src/tool/git-auto.mjs +0 -19
- package/src/tool/registry.mjs +71 -37
- package/src/ui/activity-renderer.mjs +664 -410
- package/src/util/git.mjs +23 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LongAgent 共享工具函数
|
|
3
|
+
* 被 longagent.mjs、longagent-hybrid.mjs 共同使用
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const LONGAGENT_FILE_CHANGES_LIMIT = 400
|
|
7
|
+
|
|
8
|
+
// ========== 共享 JSON 解析工具 ==========
|
|
9
|
+
|
|
10
|
+
export function stripFence(text = "") {
|
|
11
|
+
const raw = String(text || "").trim()
|
|
12
|
+
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)
|
|
13
|
+
return fenced ? fenced[1].trim() : raw
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseJsonLoose(text = "") {
|
|
17
|
+
const raw = stripFence(text)
|
|
18
|
+
try { return JSON.parse(raw) } catch { /* ignore */ }
|
|
19
|
+
const start = raw.indexOf("{")
|
|
20
|
+
const end = raw.lastIndexOf("}")
|
|
21
|
+
if (start >= 0 && end > start) {
|
|
22
|
+
try { return JSON.parse(raw.slice(start, end + 1)) } catch { /* ignore */ }
|
|
23
|
+
}
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ========== Phase 1: 错误分类 ==========
|
|
28
|
+
|
|
29
|
+
export const ERROR_CATEGORIES = {
|
|
30
|
+
TRANSIENT: "transient",
|
|
31
|
+
LOGIC: "logic",
|
|
32
|
+
PERMANENT: "permanent",
|
|
33
|
+
UNKNOWN: "unknown"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function classifyError(errorText, bgStatus) {
|
|
37
|
+
const text = String(errorText || "").toLowerCase()
|
|
38
|
+
|
|
39
|
+
// transient: 网络/超时/限流/worker 消失
|
|
40
|
+
if (
|
|
41
|
+
text.includes("timeout") || text.includes("timed out") ||
|
|
42
|
+
text.includes("econnreset") || text.includes("econnrefused") ||
|
|
43
|
+
text.includes("enotfound") || text.includes("socket hang up") ||
|
|
44
|
+
text.includes("rate limit") || text.includes("429") ||
|
|
45
|
+
text.includes("503") || text.includes("502") ||
|
|
46
|
+
text.includes("worker disappeared") || text.includes("background worker disappeared") ||
|
|
47
|
+
bgStatus === "interrupted"
|
|
48
|
+
) {
|
|
49
|
+
return ERROR_CATEGORIES.TRANSIENT
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// permanent: 文件不存在/权限不足/配置缺失
|
|
53
|
+
if (
|
|
54
|
+
text.includes("enoent") || text.includes("no such file") ||
|
|
55
|
+
text.includes("eacces") || text.includes("eperm") || text.includes("permission denied") ||
|
|
56
|
+
text.includes("config missing") || text.includes("configuration not found") ||
|
|
57
|
+
text.includes("module not found") || text.includes("cannot find module") ||
|
|
58
|
+
bgStatus === "cancelled"
|
|
59
|
+
) {
|
|
60
|
+
return ERROR_CATEGORIES.PERMANENT
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// logic: 代码 bug/类型错误/语法错误
|
|
64
|
+
if (
|
|
65
|
+
text.includes("syntaxerror") || text.includes("syntax error") ||
|
|
66
|
+
text.includes("typeerror") || text.includes("type error") ||
|
|
67
|
+
text.includes("referenceerror") || text.includes("reference error") ||
|
|
68
|
+
text.includes("rangeerror") || text.includes("assertionerror") ||
|
|
69
|
+
text.includes("unexpected token") || text.includes("is not a function") ||
|
|
70
|
+
text.includes("is not defined") || text.includes("cannot read propert")
|
|
71
|
+
) {
|
|
72
|
+
return ERROR_CATEGORIES.LOGIC
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 默认: 未知类别,不自动重试(避免在不可恢复错误上浪费资源)
|
|
76
|
+
return ERROR_CATEGORIES.UNKNOWN
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function isComplete(text) {
|
|
80
|
+
const lower = String(text || "").toLowerCase()
|
|
81
|
+
if (lower.includes("[task_complete]")) return true
|
|
82
|
+
// Only match "task complete" as a standalone phrase, not substring of other text
|
|
83
|
+
if (/\btask[\s_-]?complete\b/.test(lower)) return true
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isLikelyActionableObjective(prompt) {
|
|
88
|
+
const text = String(prompt || "").trim()
|
|
89
|
+
if (!text) return false
|
|
90
|
+
const lower = text.toLowerCase()
|
|
91
|
+
const greetings = [
|
|
92
|
+
"hi", "hello", "hey", "你好", "您好", "在吗", "yo", "嗨"
|
|
93
|
+
]
|
|
94
|
+
const codingSignals = [
|
|
95
|
+
"fix", "build", "implement", "refactor", "debug", "test", "review", "write", "create", "add", "optimize", "migrate", "deploy",
|
|
96
|
+
"bug", "issue", "error", "code", "repo", "file", "function", "api",
|
|
97
|
+
"修复", "实现", "重构", "调试", "测试", "优化", "迁移", "部署", "代码", "仓库", "文件", "函数", "接口", "需求", "功能", "报错"
|
|
98
|
+
]
|
|
99
|
+
if (codingSignals.some((kw) => lower.includes(kw))) return true
|
|
100
|
+
if (greetings.some((g) => lower === g || lower === `${g}!` || lower === `${g}!`)) return false
|
|
101
|
+
if (text.length <= 8 && !/[./\\:_-]/.test(text)) return false
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function summarizeGateFailures(failures = []) {
|
|
106
|
+
if (!failures.length) return ""
|
|
107
|
+
return failures
|
|
108
|
+
.slice(0, 5)
|
|
109
|
+
.map((item) => `${item.gate}:${item.reason}`)
|
|
110
|
+
.join("; ")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function stageProgressStats(taskProgress = {}) {
|
|
114
|
+
if (!taskProgress || typeof taskProgress !== "object") {
|
|
115
|
+
return { done: 0, total: 0, remainingFiles: [], remainingFilesCount: 0 }
|
|
116
|
+
}
|
|
117
|
+
const items = Object.values(taskProgress)
|
|
118
|
+
const done = items.filter((item) => item.status === "completed").length
|
|
119
|
+
const total = items.length
|
|
120
|
+
const remainingFiles = [...new Set(items.flatMap((item) => Array.isArray(item.remainingFiles) ? item.remainingFiles : []))]
|
|
121
|
+
return { done, total, remainingFiles, remainingFilesCount: remainingFiles.length }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function normalizeFileChange(item = {}) {
|
|
125
|
+
const path = String(item.path || "").trim()
|
|
126
|
+
if (!path) return null
|
|
127
|
+
return {
|
|
128
|
+
path,
|
|
129
|
+
addedLines: Math.max(0, Number(item.addedLines || 0)),
|
|
130
|
+
removedLines: Math.max(0, Number(item.removedLines || 0)),
|
|
131
|
+
stageId: item.stageId ? String(item.stageId) : "",
|
|
132
|
+
taskId: item.taskId ? String(item.taskId) : ""
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ========== 防卡死机制 (ported from Mark's anti-stuck work) ==========
|
|
137
|
+
|
|
138
|
+
export const READ_ONLY_TOOLS = new Set(["read", "glob", "grep", "list", "webfetch", "websearch", "codesearch"])
|
|
139
|
+
|
|
140
|
+
export function isReadOnlyTool(name) {
|
|
141
|
+
return READ_ONLY_TOOLS.has(name)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 检测配置文件搜索循环(Qwen 等模型容易反复 glob 配置文件)
|
|
146
|
+
*/
|
|
147
|
+
export function detectExplorationLoop(recentToolCalls) {
|
|
148
|
+
const recentGlobs = recentToolCalls.slice(-10).filter(sig => sig.startsWith("glob:"))
|
|
149
|
+
if (recentGlobs.length >= 6) {
|
|
150
|
+
const patterns = recentGlobs.map(sig => {
|
|
151
|
+
try { return JSON.parse(sig.slice(5)).pattern } catch { return null }
|
|
152
|
+
}).filter(Boolean)
|
|
153
|
+
const configPatterns = [/pyproject\.toml/, /setup\.py/, /Pipfile/, /Dockerfile/, /\.env/, /main\.py/, /package\.json/, /tsconfig\.json/]
|
|
154
|
+
const matched = patterns.filter(p => configPatterns.some(cp => cp.test(p)))
|
|
155
|
+
if (matched.length >= 4) return { isLoop: true, reason: "repeated_config_file_glob" }
|
|
156
|
+
}
|
|
157
|
+
return { isLoop: false }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 检测工具调用循环(同类工具重复 / 前后半段完全相同)
|
|
162
|
+
*/
|
|
163
|
+
export function detectToolCycle(recentToolCalls) {
|
|
164
|
+
if (recentToolCalls.length < 6) return false
|
|
165
|
+
// 同类工具连续 6 次
|
|
166
|
+
const recentTypes = recentToolCalls.slice(-6).map(sig => sig.split(":")[0])
|
|
167
|
+
if (recentTypes.every(t => t === recentTypes[0]) && isReadOnlyTool(recentTypes[0])) return true
|
|
168
|
+
// 前后半段完全相同
|
|
169
|
+
const allReadOnly = recentToolCalls.every(sig => isReadOnlyTool(sig.split(":")[0]))
|
|
170
|
+
if (allReadOnly) {
|
|
171
|
+
const half = Math.floor(recentToolCalls.length / 2)
|
|
172
|
+
if (half >= 3) {
|
|
173
|
+
const first = recentToolCalls.slice(0, half).sort().join(",")
|
|
174
|
+
const second = recentToolCalls.slice(half).sort().join(",")
|
|
175
|
+
if (first === second) return true
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 创建一个有状态的卡死追踪器,供各模式的主循环使用
|
|
183
|
+
*/
|
|
184
|
+
// ========== Phase 4: 写操作循环检测 ==========
|
|
185
|
+
|
|
186
|
+
const WRITE_TOOLS = new Set(["write", "edit", "notebookedit"])
|
|
187
|
+
|
|
188
|
+
function isWriteTool(name) {
|
|
189
|
+
return WRITE_TOOLS.has(name)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function detectWriteLoop(recentWriteOps) {
|
|
193
|
+
if (recentWriteOps.length < 3) return { isLoop: false, reason: null }
|
|
194
|
+
|
|
195
|
+
// 检测同一文件被连续 edit 3+ 次
|
|
196
|
+
const last3 = recentWriteOps.slice(-3)
|
|
197
|
+
if (last3.every(op => op.path === last3[0].path && op.tool === "edit")) {
|
|
198
|
+
return { isLoop: true, reason: "write_loop_detected" }
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 检测 write→error→edit→error 循环(同一文件交替出现)
|
|
202
|
+
if (recentWriteOps.length >= 4) {
|
|
203
|
+
const last4 = recentWriteOps.slice(-4)
|
|
204
|
+
const samePath = last4.every(op => op.path === last4[0].path)
|
|
205
|
+
if (samePath) {
|
|
206
|
+
const tools = last4.map(op => op.tool)
|
|
207
|
+
const hasAlternation = (tools[0] === "write" && tools[2] === "edit") ||
|
|
208
|
+
(tools[0] === "edit" && tools[2] === "write")
|
|
209
|
+
if (hasAlternation) return { isLoop: true, reason: "edit_cycle_detected" }
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { isLoop: false, reason: null }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function createStuckTracker(maxRecent = 10) {
|
|
217
|
+
const recentToolCalls = []
|
|
218
|
+
const recentWriteOps = []
|
|
219
|
+
let consecutiveReadOnlyCount = 0
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
/** 记录本轮 tool events,返回 { isStuck, reason } */
|
|
223
|
+
track(toolEvents = []) {
|
|
224
|
+
const sigs = toolEvents.map(e => `${e.name}:${JSON.stringify(e.args || {})}`)
|
|
225
|
+
recentToolCalls.push(...sigs)
|
|
226
|
+
while (recentToolCalls.length > maxRecent) recentToolCalls.shift()
|
|
227
|
+
|
|
228
|
+
// Phase 4: 追踪写操作
|
|
229
|
+
for (const e of toolEvents) {
|
|
230
|
+
if (isWriteTool(e.name)) {
|
|
231
|
+
recentWriteOps.push({
|
|
232
|
+
tool: e.name,
|
|
233
|
+
path: String(e.args?.path || e.args?.file_path || "").trim(),
|
|
234
|
+
lineRange: e.args?.old_string ? e.args.old_string.slice(0, 50) : ""
|
|
235
|
+
})
|
|
236
|
+
while (recentWriteOps.length > maxRecent) recentWriteOps.shift()
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const allReadOnly = toolEvents.length > 0 && toolEvents.every(e => isReadOnlyTool(e.name))
|
|
241
|
+
if (allReadOnly) consecutiveReadOnlyCount++
|
|
242
|
+
else consecutiveReadOnlyCount = 0
|
|
243
|
+
|
|
244
|
+
const loop = detectExplorationLoop(recentToolCalls)
|
|
245
|
+
if (loop.isLoop) return { isStuck: true, reason: loop.reason }
|
|
246
|
+
if (detectToolCycle(recentToolCalls)) return { isStuck: true, reason: "tool_cycle_detected" }
|
|
247
|
+
if (consecutiveReadOnlyCount >= 4) return { isStuck: true, reason: "excessive_read_only_exploration" }
|
|
248
|
+
|
|
249
|
+
// Phase 4: 写循环检测
|
|
250
|
+
const writeLoop = detectWriteLoop(recentWriteOps)
|
|
251
|
+
if (writeLoop.isLoop) return { isStuck: true, reason: writeLoop.reason }
|
|
252
|
+
|
|
253
|
+
return { isStuck: false, reason: null }
|
|
254
|
+
},
|
|
255
|
+
/** 重置连续只读计数(警告注入后调用) */
|
|
256
|
+
resetReadOnlyCount() { consecutiveReadOnlyCount = 0 },
|
|
257
|
+
get consecutiveReadOnly() { return consecutiveReadOnlyCount },
|
|
258
|
+
get writeOps() { return recentWriteOps }
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function mergeCappedFileChanges(current = [], incoming = [], limit = LONGAGENT_FILE_CHANGES_LIMIT) {
|
|
263
|
+
const maxEntries = Math.max(1, Number(limit || LONGAGENT_FILE_CHANGES_LIMIT))
|
|
264
|
+
const map = new Map()
|
|
265
|
+
|
|
266
|
+
const append = (entry) => {
|
|
267
|
+
const normalized = normalizeFileChange(entry)
|
|
268
|
+
if (!normalized) return
|
|
269
|
+
const key = `${normalized.path}::${normalized.stageId}::${normalized.taskId}`
|
|
270
|
+
const prev = map.get(key) || { ...normalized, addedLines: 0, removedLines: 0 }
|
|
271
|
+
prev.addedLines += normalized.addedLines
|
|
272
|
+
prev.removedLines += normalized.removedLines
|
|
273
|
+
map.delete(key)
|
|
274
|
+
map.set(key, prev)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const item of current) append(item)
|
|
278
|
+
for (const item of incoming) append(item)
|
|
279
|
+
|
|
280
|
+
const merged = [...map.values()]
|
|
281
|
+
if (merged.length > maxEntries) {
|
|
282
|
+
const truncated = merged.slice(merged.length - maxEntries)
|
|
283
|
+
truncated._truncatedFrom = merged.length
|
|
284
|
+
return truncated
|
|
285
|
+
}
|
|
286
|
+
return merged
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ========== Phase 5: 语义级错误检测 ==========
|
|
290
|
+
|
|
291
|
+
export function createSemanticErrorTracker(threshold = 3) {
|
|
292
|
+
const errorHistory = []
|
|
293
|
+
|
|
294
|
+
function extractErrorPattern(text) {
|
|
295
|
+
const str = String(text || "")
|
|
296
|
+
const patterns = []
|
|
297
|
+
const errorRegex = /(?:TypeError|ReferenceError|SyntaxError|RangeError|Error|AssertionError):\s*(.+?)(?:\n|$)/gi
|
|
298
|
+
let m
|
|
299
|
+
while ((m = errorRegex.exec(str)) !== null) {
|
|
300
|
+
patterns.push(m[0].trim().slice(0, 120))
|
|
301
|
+
}
|
|
302
|
+
return patterns
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function isSimilar(a, b) {
|
|
306
|
+
if (a === b) return true
|
|
307
|
+
if (a.length < 10 || b.length < 10) return a === b
|
|
308
|
+
// Token-level Jaccard similarity — more robust than substring matching
|
|
309
|
+
const tokenize = (s) => new Set(s.toLowerCase().split(/[\s:.'"`()\[\]{}]+/).filter(t => t.length > 2))
|
|
310
|
+
const setA = tokenize(a)
|
|
311
|
+
const setB = tokenize(b)
|
|
312
|
+
if (!setA.size || !setB.size) return false
|
|
313
|
+
let intersection = 0
|
|
314
|
+
for (const t of setA) { if (setB.has(t)) intersection++ }
|
|
315
|
+
const union = setA.size + setB.size - intersection
|
|
316
|
+
return union > 0 && (intersection / union) >= 0.6
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
track(replyText) {
|
|
321
|
+
const patterns = extractErrorPattern(replyText)
|
|
322
|
+
if (!patterns.length) {
|
|
323
|
+
errorHistory.push(null)
|
|
324
|
+
return { isRepeated: false, error: null, count: 0 }
|
|
325
|
+
}
|
|
326
|
+
const primary = patterns[0]
|
|
327
|
+
errorHistory.push(primary)
|
|
328
|
+
|
|
329
|
+
// 检查最近 threshold 次是否出现相同错误
|
|
330
|
+
if (errorHistory.length >= threshold) {
|
|
331
|
+
const recent = errorHistory.slice(-threshold).filter(Boolean)
|
|
332
|
+
if (recent.length === threshold && recent.every(e => isSimilar(e, primary))) {
|
|
333
|
+
return { isRepeated: true, error: primary, count: threshold }
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return { isRepeated: false, error: primary, count: 1 }
|
|
337
|
+
},
|
|
338
|
+
reset() { errorHistory.length = 0 },
|
|
339
|
+
get history() { return errorHistory }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ========== Phase 6: 渐进式降级策略 ==========
|
|
344
|
+
|
|
345
|
+
export function createDegradationChain(config = {}) {
|
|
346
|
+
const strategies = [
|
|
347
|
+
{
|
|
348
|
+
name: "switch_model",
|
|
349
|
+
apply(ctx) {
|
|
350
|
+
const fallback = config.fallback_model
|
|
351
|
+
if (!fallback || ctx.model === fallback) return false
|
|
352
|
+
ctx.previousModel = ctx.model
|
|
353
|
+
ctx.model = fallback
|
|
354
|
+
return true
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "reduce_scope",
|
|
359
|
+
apply(ctx) {
|
|
360
|
+
if (!config.skip_non_critical || !ctx.taskProgress) return false
|
|
361
|
+
let skipped = 0
|
|
362
|
+
for (const [taskId, tp] of Object.entries(ctx.taskProgress)) {
|
|
363
|
+
if (tp.status === "error" || tp.status === "retrying") {
|
|
364
|
+
ctx.taskProgress[taskId] = { ...tp, status: "skipped", skipReason: "degradation_reduce_scope" }
|
|
365
|
+
skipped++
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return skipped > 0
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "serial_mode",
|
|
373
|
+
apply(ctx) {
|
|
374
|
+
if (!ctx.configState?.config?.agent?.longagent?.parallel) return false
|
|
375
|
+
ctx.configState.config.agent.longagent.parallel.max_concurrency = 1
|
|
376
|
+
return true
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
name: "graceful_stop",
|
|
381
|
+
apply(ctx) {
|
|
382
|
+
ctx.shouldStop = true
|
|
383
|
+
return true
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
]
|
|
387
|
+
|
|
388
|
+
let currentLevel = 0
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
canDegrade() { return currentLevel < strategies.length },
|
|
392
|
+
currentStrategy() { return strategies[currentLevel] || null },
|
|
393
|
+
nextStrategy() { return strategies[currentLevel] || null },
|
|
394
|
+
apply(ctx) {
|
|
395
|
+
if (currentLevel >= strategies.length) return { applied: false, strategy: null }
|
|
396
|
+
const strategy = strategies[currentLevel]
|
|
397
|
+
const applied = strategy.apply(ctx)
|
|
398
|
+
if (applied) currentLevel++
|
|
399
|
+
return { applied, strategy: strategy.name }
|
|
400
|
+
},
|
|
401
|
+
get level() { return currentLevel }
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ========== Phase 11: 恢复建议生成 ==========
|
|
406
|
+
|
|
407
|
+
export function generateRecoverySuggestions({ status, taskProgress, gateStatus, phase, recoveryCount, fileChanges }) {
|
|
408
|
+
const suggestions = []
|
|
409
|
+
|
|
410
|
+
// 分析已完成和失败的 task
|
|
411
|
+
const completedTasks = []
|
|
412
|
+
const failedTasks = []
|
|
413
|
+
if (taskProgress && typeof taskProgress === "object") {
|
|
414
|
+
for (const [taskId, tp] of Object.entries(taskProgress)) {
|
|
415
|
+
if (tp.status === "completed") {
|
|
416
|
+
completedTasks.push(taskId)
|
|
417
|
+
} else if (tp.status === "error" || tp.status === "cancelled") {
|
|
418
|
+
const category = classifyError(tp.lastError)
|
|
419
|
+
failedTasks.push({
|
|
420
|
+
taskId,
|
|
421
|
+
error: (tp.lastError || "").slice(0, 200),
|
|
422
|
+
category,
|
|
423
|
+
suggestion: category === "permanent"
|
|
424
|
+
? "此错误不可自动恢复,需要手动检查配置或文件路径"
|
|
425
|
+
: category === "logic"
|
|
426
|
+
? "代码逻辑错误,建议检查相关文件的实现"
|
|
427
|
+
: "临时性错误,可尝试重新运行"
|
|
428
|
+
})
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// 分析 gate 失败
|
|
434
|
+
const manualSteps = []
|
|
435
|
+
if (gateStatus) {
|
|
436
|
+
for (const [gate, info] of Object.entries(gateStatus)) {
|
|
437
|
+
if (info?.status === "fail" || info?.status === "fixing") {
|
|
438
|
+
manualSteps.push(`检查 ${gate} gate 的失败原因: ${info.failures || info.reason || "unknown"}`)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 根据 phase 判断失败阶段
|
|
444
|
+
if (phase) {
|
|
445
|
+
if (phase.startsWith("H4")) suggestions.push("编码阶段未完成,可尝试缩小任务范围后重试")
|
|
446
|
+
if (phase.startsWith("H5")) suggestions.push("调试阶段未通过,建议手动检查测试输出")
|
|
447
|
+
if (phase.startsWith("H6")) suggestions.push("门控检查未通过,建议手动运行 build/test/lint 命令")
|
|
448
|
+
if (phase.startsWith("H7")) suggestions.push("Git 合并阶段出现问题,建议手动解决冲突")
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const resumeHint = completedTasks.length > 0
|
|
452
|
+
? `已完成 ${completedTasks.length} 个 task,可从 checkpoint 恢复继续`
|
|
453
|
+
: "无已完成的 task,建议重新开始"
|
|
454
|
+
|
|
455
|
+
const summary = [
|
|
456
|
+
`状态: ${status}`,
|
|
457
|
+
`阶段: ${phase || "unknown"}`,
|
|
458
|
+
`恢复次数: ${recoveryCount || 0}`,
|
|
459
|
+
`已完成: ${completedTasks.length} task(s)`,
|
|
460
|
+
`失败: ${failedTasks.length} task(s)`,
|
|
461
|
+
`文件变更: ${fileChanges?.length || 0}`
|
|
462
|
+
].join(", ")
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
suggestions,
|
|
466
|
+
completedTasks,
|
|
467
|
+
failedTasks,
|
|
468
|
+
manualSteps,
|
|
469
|
+
resumeHint,
|
|
470
|
+
summary
|
|
471
|
+
}
|
|
472
|
+
}
|