@kkelly-offical/kkcode 0.1.7 → 0.2.1

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 (163) hide show
  1. package/LICENSE +674 -674
  2. package/README.md +452 -387
  3. package/package.json +50 -46
  4. package/src/agent/agent.mjs +228 -220
  5. package/src/agent/custom-agent-loader.mjs +6 -3
  6. package/src/agent/generator.mjs +2 -2
  7. package/src/agent/prompt/assistant.txt +12 -0
  8. package/src/agent/prompt/bug-hunter.txt +89 -89
  9. package/src/agent/prompt/frontend-designer.txt +58 -58
  10. package/src/agent/prompt/guide.txt +1 -1
  11. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
  12. package/src/agent/prompt/longagent-coding-agent.txt +37 -37
  13. package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
  14. package/src/agent/prompt/longagent-preview-agent.txt +63 -63
  15. package/src/command/custom-commands.mjs +2 -2
  16. package/src/commands/agent.mjs +1 -1
  17. package/src/commands/background.mjs +145 -4
  18. package/src/commands/chat.mjs +117 -76
  19. package/src/commands/config.mjs +148 -1
  20. package/src/commands/doctor.mjs +30 -6
  21. package/src/commands/init.mjs +32 -6
  22. package/src/commands/longagent.mjs +117 -0
  23. package/src/commands/mcp.mjs +275 -43
  24. package/src/commands/permission.mjs +1 -1
  25. package/src/commands/session.mjs +195 -140
  26. package/src/commands/skill.mjs +63 -0
  27. package/src/commands/theme.mjs +1 -1
  28. package/src/config/defaults.mjs +280 -260
  29. package/src/config/import-config.mjs +1 -1
  30. package/src/config/load-config.mjs +61 -4
  31. package/src/config/schema.mjs +591 -574
  32. package/src/context.mjs +4 -1
  33. package/src/core/constants.mjs +97 -91
  34. package/src/core/types.mjs +1 -1
  35. package/src/github/api.mjs +78 -78
  36. package/src/github/auth.mjs +294 -286
  37. package/src/github/flow.mjs +298 -298
  38. package/src/github/workspace.mjs +225 -212
  39. package/src/index.mjs +84 -82
  40. package/src/knowledge/frontend-aesthetics.txt +38 -38
  41. package/src/mcp/client-http.mjs +139 -141
  42. package/src/mcp/client-sse.mjs +297 -288
  43. package/src/mcp/client-stdio.mjs +534 -533
  44. package/src/mcp/constants.mjs +2 -2
  45. package/src/mcp/registry.mjs +498 -479
  46. package/src/mcp/stdio-framing.mjs +135 -133
  47. package/src/mcp/tool-result.mjs +24 -24
  48. package/src/observability/edit-diagnostics.mjs +449 -0
  49. package/src/observability/index.mjs +42 -42
  50. package/src/observability/metrics.mjs +165 -137
  51. package/src/observability/tracer.mjs +137 -137
  52. package/src/onboarding.mjs +209 -0
  53. package/src/orchestration/background-manager.mjs +567 -372
  54. package/src/orchestration/background-worker.mjs +419 -305
  55. package/src/orchestration/interruption-reason.mjs +21 -0
  56. package/src/orchestration/longagent-manager.mjs +197 -171
  57. package/src/orchestration/stage-scheduler.mjs +733 -728
  58. package/src/orchestration/subagent-router.mjs +7 -1
  59. package/src/orchestration/task-scheduler.mjs +219 -7
  60. package/src/permission/engine.mjs +1 -1
  61. package/src/permission/exec-policy.mjs +370 -370
  62. package/src/permission/file-edit-policy.mjs +108 -0
  63. package/src/permission/prompt.mjs +1 -1
  64. package/src/permission/rules.mjs +116 -7
  65. package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
  66. package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
  67. package/src/plugin/hook-bus.mjs +19 -5
  68. package/src/plugin/manifest-loader.mjs +222 -0
  69. package/src/provider/anthropic.mjs +396 -390
  70. package/src/provider/ollama.mjs +7 -1
  71. package/src/provider/openai.mjs +382 -340
  72. package/src/provider/retry-policy.mjs +74 -68
  73. package/src/provider/router.mjs +242 -241
  74. package/src/provider/sse.mjs +104 -104
  75. package/src/provider/wizard.mjs +556 -0
  76. package/src/repl/capability-facade.mjs +30 -0
  77. package/src/repl/command-surface.mjs +23 -0
  78. package/src/repl/controller-entry.mjs +40 -0
  79. package/src/repl/core-shell.mjs +208 -0
  80. package/src/repl/dialog-router.mjs +87 -0
  81. package/src/repl/input-engine.mjs +76 -0
  82. package/src/repl/keymap.mjs +7 -0
  83. package/src/repl/operator-surface.mjs +15 -0
  84. package/src/repl/permission-flow.mjs +49 -0
  85. package/src/repl/runtime-facade.mjs +36 -0
  86. package/src/repl/slash-router.mjs +62 -0
  87. package/src/repl/state-store.mjs +29 -0
  88. package/src/repl/turn-controller.mjs +58 -0
  89. package/src/repl/verification.mjs +23 -0
  90. package/src/repl.mjs +3368 -2981
  91. package/src/rules/load-rules.mjs +3 -3
  92. package/src/runtime.mjs +1 -1
  93. package/src/session/agent-transaction.mjs +86 -0
  94. package/src/session/checkpoint.mjs +302 -302
  95. package/src/session/compaction.mjs +298 -298
  96. package/src/session/engine.mjs +417 -232
  97. package/src/session/longagent-4stage.mjs +467 -460
  98. package/src/session/longagent-hybrid.mjs +1344 -1097
  99. package/src/session/longagent-plan.mjs +376 -365
  100. package/src/session/longagent-project-memory.mjs +53 -53
  101. package/src/session/longagent-scaffold.mjs +291 -291
  102. package/src/session/longagent-task-bus.mjs +138 -54
  103. package/src/session/longagent-utils.mjs +828 -472
  104. package/src/session/longagent.mjs +911 -900
  105. package/src/session/loop.mjs +1005 -930
  106. package/src/session/prompt/agent.txt +25 -25
  107. package/src/session/prompt/anthropic.txt +150 -150
  108. package/src/session/prompt/beast.txt +1 -1
  109. package/src/session/prompt/plan.txt +31 -31
  110. package/src/session/prompt/qwen.txt +46 -46
  111. package/src/session/recovery.mjs +21 -0
  112. package/src/session/rollback.mjs +196 -195
  113. package/src/session/routing-observability.mjs +72 -0
  114. package/src/session/runtime-state.mjs +47 -0
  115. package/src/session/store.mjs +523 -519
  116. package/src/session/system-prompt.mjs +308 -273
  117. package/src/session/task-validator.mjs +267 -267
  118. package/src/session/usability-gates.mjs +2 -2
  119. package/src/skill/builtin/commit.mjs +64 -64
  120. package/src/skill/builtin/design.mjs +76 -76
  121. package/src/skill/generator.mjs +18 -2
  122. package/src/skill/registry.mjs +642 -390
  123. package/src/storage/audit-store.mjs +18 -11
  124. package/src/storage/event-log.mjs +7 -1
  125. package/src/storage/ghost-commit-store.mjs +243 -245
  126. package/src/storage/paths.mjs +13 -0
  127. package/src/theme/default-theme.mjs +1 -1
  128. package/src/theme/markdown.mjs +4 -0
  129. package/src/theme/schema.mjs +1 -1
  130. package/src/theme/status-bar.mjs +162 -158
  131. package/src/tool/audit-wrapper.mjs +18 -2
  132. package/src/tool/edit-transaction.mjs +23 -0
  133. package/src/tool/executor.mjs +26 -1
  134. package/src/tool/file-read-state.mjs +65 -0
  135. package/src/tool/git-auto.mjs +526 -526
  136. package/src/tool/git-full-auto.mjs +487 -478
  137. package/src/tool/mutation-guard.mjs +54 -0
  138. package/src/tool/prompt/edit.txt +3 -3
  139. package/src/tool/prompt/multiedit.txt +1 -0
  140. package/src/tool/prompt/notebookedit.txt +2 -1
  141. package/src/tool/prompt/patch.txt +25 -24
  142. package/src/tool/prompt/read.txt +3 -3
  143. package/src/tool/prompt/sysinfo.txt +29 -0
  144. package/src/tool/prompt/task.txt +66 -4
  145. package/src/tool/prompt/write.txt +2 -2
  146. package/src/tool/question-prompt.mjs +99 -93
  147. package/src/tool/registry.mjs +1701 -1343
  148. package/src/tool/task-tool.mjs +14 -6
  149. package/src/ui/activity-renderer.mjs +667 -664
  150. package/src/ui/repl-background-panel.mjs +7 -0
  151. package/src/ui/repl-capability-panel.mjs +9 -0
  152. package/src/ui/repl-dashboard.mjs +54 -4
  153. package/src/ui/repl-help.mjs +110 -0
  154. package/src/ui/repl-operator-panel.mjs +12 -0
  155. package/src/ui/repl-route-feedback.mjs +35 -0
  156. package/src/ui/repl-status-view.mjs +76 -0
  157. package/src/ui/repl-task-panel.mjs +5 -0
  158. package/src/ui/repl-transcript-panel.mjs +56 -0
  159. package/src/ui/repl-turn-summary.mjs +135 -0
  160. package/src/usage/pricing.mjs +122 -121
  161. package/src/usage/usage-meter.mjs +1 -0
  162. package/src/util/git.mjs +562 -519
  163. package/src/util/template.mjs +6 -1
@@ -1,472 +1,828 @@
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
- }
1
+ import { detectAgentContinuationInput, extractPromptPathHints } from "./agent-transaction.mjs"
2
+
3
+ /**
4
+ * LongAgent 共享工具函数
5
+ * 被 longagent.mjs、longagent-hybrid.mjs 共同使用
6
+ */
7
+
8
+ export const LONGAGENT_FILE_CHANGES_LIMIT = 400
9
+
10
+ // ========== 共享 JSON 解析工具 ==========
11
+
12
+ export function stripFence(text = "") {
13
+ const raw = String(text || "").trim()
14
+ const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i)
15
+ return fenced ? fenced[1].trim() : raw
16
+ }
17
+
18
+ export function parseJsonLoose(text = "") {
19
+ const raw = stripFence(text)
20
+ // 1. 直接解析
21
+ try { return JSON.parse(raw) } catch { /* ignore */ }
22
+ // 2. 修复 trailing comma(LLM 常产出 {…, } […, ])
23
+ const repaired = raw.replace(/,\s*([}\]])/g, "$1")
24
+ if (repaired !== raw) {
25
+ try { return JSON.parse(repaired) } catch { /* ignore */ }
26
+ }
27
+ // 3. 提取最外层 {}
28
+ const start = raw.indexOf("{")
29
+ const end = raw.lastIndexOf("}")
30
+ if (start >= 0 && end > start) {
31
+ const slice = raw.slice(start, end + 1)
32
+ try { return JSON.parse(slice) } catch { /* ignore */ }
33
+ // 3b. 对提取的块也尝试 trailing comma 修复
34
+ const sliceRepaired = slice.replace(/,\s*([}\]])/g, "$1")
35
+ if (sliceRepaired !== slice) {
36
+ try { return JSON.parse(sliceRepaired) } catch { /* ignore */ }
37
+ }
38
+ }
39
+ return null
40
+ }
41
+
42
+ // ========== Phase 1: 错误分类 ==========
43
+
44
+ export const ERROR_CATEGORIES = {
45
+ TRANSIENT: "transient",
46
+ LOGIC: "logic",
47
+ PERMANENT: "permanent",
48
+ UNKNOWN: "unknown"
49
+ }
50
+
51
+ export function classifyError(errorText, bgStatus) {
52
+ const text = String(errorText || "").toLowerCase()
53
+
54
+ // transient: 网络/超时/限流/worker 消失
55
+ if (
56
+ text.includes("timeout") || text.includes("timed out") ||
57
+ text.includes("econnreset") || text.includes("econnrefused") ||
58
+ text.includes("enotfound") || text.includes("socket hang up") ||
59
+ text.includes("rate limit") || text.includes("429") ||
60
+ text.includes("503") || text.includes("502") ||
61
+ text.includes("worker disappeared") || text.includes("background worker disappeared") ||
62
+ bgStatus === "interrupted"
63
+ ) {
64
+ return ERROR_CATEGORIES.TRANSIENT
65
+ }
66
+
67
+ // permanent: 文件不存在/权限不足/配置缺失
68
+ if (
69
+ text.includes("enoent") || text.includes("no such file") ||
70
+ text.includes("eacces") || text.includes("eperm") || text.includes("permission denied") ||
71
+ text.includes("config missing") || text.includes("configuration not found") ||
72
+ text.includes("module not found") || text.includes("cannot find module") ||
73
+ bgStatus === "cancelled"
74
+ ) {
75
+ return ERROR_CATEGORIES.PERMANENT
76
+ }
77
+
78
+ // logic: 代码 bug/类型错误/语法错误
79
+ if (
80
+ text.includes("syntaxerror") || text.includes("syntax error") ||
81
+ text.includes("typeerror") || text.includes("type error") ||
82
+ text.includes("referenceerror") || text.includes("reference error") ||
83
+ text.includes("rangeerror") || text.includes("assertionerror") ||
84
+ text.includes("unexpected token") || text.includes("is not a function") ||
85
+ text.includes("is not defined") || text.includes("cannot read propert")
86
+ ) {
87
+ return ERROR_CATEGORIES.LOGIC
88
+ }
89
+
90
+ // 默认: 未知类别,不自动重试(避免在不可恢复错误上浪费资源)
91
+ return ERROR_CATEGORIES.UNKNOWN
92
+ }
93
+
94
+ export function isComplete(text) {
95
+ const lower = String(text || "").toLowerCase()
96
+ if (lower.includes("[task_complete]")) return true
97
+ // Only match "task complete" as a standalone phrase, not substring of other text
98
+ if (/\btask[\s_-]?complete\b/.test(lower)) return true
99
+ return false
100
+ }
101
+
102
+ export function isLikelyActionableObjective(prompt) {
103
+ const text = String(prompt || "").trim()
104
+ if (!text) return false
105
+ const lower = text.toLowerCase()
106
+ const greetings = [
107
+ "hi", "hello", "hey", "你好", "您好", "在吗", "yo", "嗨"
108
+ ]
109
+ const codingSignals = [
110
+ "fix", "build", "implement", "refactor", "debug", "test", "review", "write", "create", "add", "optimize", "migrate", "deploy",
111
+ "bug", "issue", "error", "code", "repo", "file", "function", "api",
112
+ "修复", "实现", "重构", "调试", "测试", "优化", "迁移", "部署", "代码", "仓库", "文件", "函数", "接口", "需求", "功能", "报错"
113
+ ]
114
+ if (codingSignals.some((kw) => lower.includes(kw))) return true
115
+ if (greetings.some((g) => lower === g || lower === `${g}!` || lower === `${g}!`)) return false
116
+ if (text.length <= 8 && !/[./\\:_-]/.test(text)) return false
117
+ return true
118
+ }
119
+
120
+ export function summarizeGateFailures(failures = []) {
121
+ if (!failures.length) return ""
122
+ return failures
123
+ .slice(0, 5)
124
+ .map((item) => `${item.gate}:${item.reason}`)
125
+ .join("; ")
126
+ }
127
+
128
+ export function stageProgressStats(taskProgress = {}) {
129
+ if (!taskProgress || typeof taskProgress !== "object") {
130
+ return { done: 0, total: 0, remainingFiles: [], remainingFilesCount: 0 }
131
+ }
132
+ const items = Object.values(taskProgress)
133
+ const done = items.filter((item) => item.status === "completed").length
134
+ const total = items.length
135
+ const remainingFiles = [...new Set(items.flatMap((item) => Array.isArray(item.remainingFiles) ? item.remainingFiles : []))]
136
+ return { done, total, remainingFiles, remainingFilesCount: remainingFiles.length }
137
+ }
138
+
139
+ export function normalizeFileChange(item = {}) {
140
+ const path = String(item.path || "").trim()
141
+ if (!path) return null
142
+ return {
143
+ path,
144
+ addedLines: Math.max(0, Number(item.addedLines || 0)),
145
+ removedLines: Math.max(0, Number(item.removedLines || 0)),
146
+ stageId: item.stageId ? String(item.stageId) : "",
147
+ taskId: item.taskId ? String(item.taskId) : ""
148
+ }
149
+ }
150
+
151
+ // ========== 防卡死机制 (ported from Mark's anti-stuck work) ==========
152
+
153
+ export const READ_ONLY_TOOLS = new Set(["read", "glob", "grep", "list", "webfetch", "websearch", "codesearch"])
154
+
155
+ export function isReadOnlyTool(name) {
156
+ return READ_ONLY_TOOLS.has(name)
157
+ }
158
+
159
+ /**
160
+ * 检测配置文件搜索循环(Qwen 等模型容易反复 glob 配置文件)
161
+ */
162
+ export function detectExplorationLoop(recentToolCalls) {
163
+ const recentGlobs = recentToolCalls.slice(-10).filter(sig => sig.startsWith("glob:"))
164
+ if (recentGlobs.length >= 6) {
165
+ const patterns = recentGlobs.map(sig => {
166
+ try { return JSON.parse(sig.slice(5)).pattern } catch { return null }
167
+ }).filter(Boolean)
168
+ const configPatterns = [/pyproject\.toml/, /setup\.py/, /Pipfile/, /Dockerfile/, /\.env/, /main\.py/, /package\.json/, /tsconfig\.json/]
169
+ const matched = patterns.filter(p => configPatterns.some(cp => cp.test(p)))
170
+ if (matched.length >= 4) return { isLoop: true, reason: "repeated_config_file_glob" }
171
+ }
172
+ return { isLoop: false }
173
+ }
174
+
175
+ /**
176
+ * 检测工具调用循环(同类工具重复 / 前后半段完全相同)
177
+ */
178
+ export function detectToolCycle(recentToolCalls) {
179
+ if (recentToolCalls.length < 6) return false
180
+ // 同类工具连续 6 次
181
+ const recentTypes = recentToolCalls.slice(-6).map(sig => sig.split(":")[0])
182
+ if (recentTypes.every(t => t === recentTypes[0]) && isReadOnlyTool(recentTypes[0])) return true
183
+ // 前后半段按序列比较(保留顺序信息,避免排序后误报)
184
+ const allReadOnly = recentToolCalls.every(sig => isReadOnlyTool(sig.split(":")[0]))
185
+ if (allReadOnly) {
186
+ const half = Math.floor(recentToolCalls.length / 2)
187
+ if (half >= 3) {
188
+ const first = recentToolCalls.slice(0, half).join(",")
189
+ const second = recentToolCalls.slice(half, half * 2).join(",")
190
+ if (first === second) return true
191
+ }
192
+ }
193
+ return false
194
+ }
195
+
196
+ /**
197
+ * 创建一个有状态的卡死追踪器,供各模式的主循环使用
198
+ */
199
+ // ========== Phase 4: 写操作循环检测 ==========
200
+
201
+ const WRITE_TOOLS = new Set(["write", "edit", "notebookedit"])
202
+
203
+ function isWriteTool(name) {
204
+ return WRITE_TOOLS.has(name)
205
+ }
206
+
207
+ function detectWriteLoop(recentWriteOps) {
208
+ if (recentWriteOps.length < 3) return { isLoop: false, reason: null }
209
+
210
+ // 检测同一文件被连续 edit 3+ 次
211
+ const last3 = recentWriteOps.slice(-3)
212
+ if (last3.every(op => op.path === last3[0].path && op.tool === "edit")) {
213
+ return { isLoop: true, reason: "write_loop_detected" }
214
+ }
215
+
216
+ // 检测 write→error→edit→error 循环(同一文件交替出现)
217
+ if (recentWriteOps.length >= 4) {
218
+ const last4 = recentWriteOps.slice(-4)
219
+ const samePath = last4.every(op => op.path === last4[0].path)
220
+ if (samePath) {
221
+ const tools = last4.map(op => op.tool)
222
+ const hasAlternation = (tools[0] === "write" && tools[2] === "edit") ||
223
+ (tools[0] === "edit" && tools[2] === "write")
224
+ if (hasAlternation) return { isLoop: true, reason: "edit_cycle_detected" }
225
+ }
226
+ }
227
+
228
+ return { isLoop: false, reason: null }
229
+ }
230
+
231
+ export function createStuckTracker(maxRecent = 10) {
232
+ const recentToolCalls = []
233
+ const recentWriteOps = []
234
+ let consecutiveReadOnlyCount = 0
235
+
236
+ return {
237
+ /** 记录本轮 tool events,返回 { isStuck, reason } */
238
+ track(toolEvents = []) {
239
+ const sigs = toolEvents.map(e => `${e.name}:${JSON.stringify(e.args || {})}`)
240
+ recentToolCalls.push(...sigs)
241
+ while (recentToolCalls.length > maxRecent) recentToolCalls.shift()
242
+
243
+ // Phase 4: 追踪写操作
244
+ for (const e of toolEvents) {
245
+ if (isWriteTool(e.name)) {
246
+ recentWriteOps.push({
247
+ tool: e.name,
248
+ path: String(e.args?.path || e.args?.file_path || "").trim(),
249
+ lineRange: e.args?.old_string ? e.args.old_string.slice(0, 50) : ""
250
+ })
251
+ while (recentWriteOps.length > maxRecent) recentWriteOps.shift()
252
+ }
253
+ }
254
+
255
+ const allReadOnly = toolEvents.length > 0 && toolEvents.every(e => isReadOnlyTool(e.name))
256
+ if (allReadOnly) consecutiveReadOnlyCount++
257
+ else consecutiveReadOnlyCount = 0
258
+
259
+ const loop = detectExplorationLoop(recentToolCalls)
260
+ if (loop.isLoop) return { isStuck: true, reason: loop.reason }
261
+ if (detectToolCycle(recentToolCalls)) return { isStuck: true, reason: "tool_cycle_detected" }
262
+ if (consecutiveReadOnlyCount >= 4) return { isStuck: true, reason: "excessive_read_only_exploration" }
263
+
264
+ // Phase 4: 写循环检测
265
+ const writeLoop = detectWriteLoop(recentWriteOps)
266
+ if (writeLoop.isLoop) return { isStuck: true, reason: writeLoop.reason }
267
+
268
+ return { isStuck: false, reason: null }
269
+ },
270
+ /** 重置连续只读计数(警告注入后调用) */
271
+ resetReadOnlyCount() { consecutiveReadOnlyCount = 0 },
272
+ get consecutiveReadOnly() { return consecutiveReadOnlyCount },
273
+ get writeOps() { return recentWriteOps }
274
+ }
275
+ }
276
+
277
+ export function mergeCappedFileChanges(current = [], incoming = [], limit = LONGAGENT_FILE_CHANGES_LIMIT) {
278
+ const maxEntries = Math.max(1, Number(limit || LONGAGENT_FILE_CHANGES_LIMIT))
279
+ const map = new Map()
280
+
281
+ const append = (entry) => {
282
+ const normalized = normalizeFileChange(entry)
283
+ if (!normalized) return
284
+ const key = `${normalized.path}::${normalized.stageId}::${normalized.taskId}`
285
+ const prev = map.get(key) || { ...normalized, addedLines: 0, removedLines: 0 }
286
+ prev.addedLines += normalized.addedLines
287
+ prev.removedLines += normalized.removedLines
288
+ map.delete(key)
289
+ map.set(key, prev)
290
+ }
291
+
292
+ for (const item of current) append(item)
293
+ for (const item of incoming) append(item)
294
+
295
+ const merged = [...map.values()]
296
+ if (merged.length > maxEntries) {
297
+ const truncated = merged.slice(merged.length - maxEntries)
298
+ truncated._truncatedFrom = merged.length
299
+ return truncated
300
+ }
301
+ return merged
302
+ }
303
+
304
+ // ========== Phase 5: 语义级错误检测 ==========
305
+
306
+ export function createSemanticErrorTracker(threshold = 3) {
307
+ const errorHistory = []
308
+
309
+ function extractErrorPattern(text) {
310
+ const str = String(text || "")
311
+ const patterns = []
312
+ const errorRegex = /(?:TypeError|ReferenceError|SyntaxError|RangeError|Error|AssertionError):\s*(.+?)(?:\n|$)/gi
313
+ let m
314
+ while ((m = errorRegex.exec(str)) !== null) {
315
+ patterns.push(m[0].trim().slice(0, 120))
316
+ }
317
+ return patterns
318
+ }
319
+
320
+ function isSimilar(a, b) {
321
+ if (a === b) return true
322
+ if (a.length < 10 || b.length < 10) return a === b
323
+ // Token-level Jaccard similarity — more robust than substring matching
324
+ const tokenize = (s) => new Set(s.toLowerCase().split(/[\s:.'"`()\[\]{}]+/).filter(t => t.length > 2))
325
+ const setA = tokenize(a)
326
+ const setB = tokenize(b)
327
+ if (!setA.size || !setB.size) return false
328
+ let intersection = 0
329
+ for (const t of setA) { if (setB.has(t)) intersection++ }
330
+ const union = setA.size + setB.size - intersection
331
+ return union > 0 && (intersection / union) >= 0.6
332
+ }
333
+
334
+ return {
335
+ track(replyText) {
336
+ const patterns = extractErrorPattern(replyText)
337
+ if (!patterns.length) {
338
+ errorHistory.push(null)
339
+ return { isRepeated: false, error: null, count: 0 }
340
+ }
341
+ const primary = patterns[0]
342
+ errorHistory.push(primary)
343
+
344
+ // 检查最近 threshold 次是否出现相同错误
345
+ if (errorHistory.length >= threshold) {
346
+ const recent = errorHistory.slice(-threshold).filter(Boolean)
347
+ if (recent.length === threshold && recent.every(e => isSimilar(e, primary))) {
348
+ return { isRepeated: true, error: primary, count: threshold }
349
+ }
350
+ }
351
+ return { isRepeated: false, error: primary, count: 1 }
352
+ },
353
+ reset() { errorHistory.length = 0 },
354
+ get history() { return errorHistory }
355
+ }
356
+ }
357
+
358
+ // ========== Phase 6: 渐进式降级策略 ==========
359
+
360
+ export function createDegradationChain(config = {}) {
361
+ const strategies = [
362
+ {
363
+ name: "switch_model",
364
+ apply(ctx) {
365
+ const fallback = config.fallback_model
366
+ if (!fallback || ctx.model === fallback) return false
367
+ ctx.previousModel = ctx.model
368
+ ctx.model = fallback
369
+ return true
370
+ }
371
+ },
372
+ {
373
+ name: "reduce_scope",
374
+ apply(ctx) {
375
+ if (!config.skip_non_critical || !ctx.taskProgress) return false
376
+ let skipped = 0
377
+ for (const [taskId, tp] of Object.entries(ctx.taskProgress)) {
378
+ if (tp.status === "error" || tp.status === "retrying") {
379
+ ctx.taskProgress[taskId] = { ...tp, status: "skipped", skipReason: "degradation_reduce_scope" }
380
+ skipped++
381
+ }
382
+ }
383
+ return skipped > 0
384
+ }
385
+ },
386
+ {
387
+ name: "serial_mode",
388
+ apply(ctx) {
389
+ if (!ctx.configState?.config?.agent?.longagent?.parallel) return false
390
+ ctx.configState.config.agent.longagent.parallel.max_concurrency = 1
391
+ return true
392
+ }
393
+ },
394
+ {
395
+ name: "graceful_stop",
396
+ apply(ctx) {
397
+ ctx.shouldStop = true
398
+ return true
399
+ }
400
+ }
401
+ ]
402
+
403
+ let currentLevel = 0
404
+
405
+ return {
406
+ canDegrade() { return currentLevel < strategies.length },
407
+ currentStrategy() { return strategies[currentLevel] || null },
408
+ nextStrategy() { return strategies[currentLevel] || null },
409
+ apply(ctx) {
410
+ if (currentLevel >= strategies.length) return { applied: false, strategy: null }
411
+ const strategy = strategies[currentLevel]
412
+ const applied = strategy.apply(ctx)
413
+ if (applied) currentLevel++
414
+ return { applied, strategy: strategy.name }
415
+ },
416
+ get level() { return currentLevel }
417
+ }
418
+ }
419
+
420
+ // ========== Task 1: 智能任务模式分类 ==========
421
+
422
+ const MODE_REASON_EXPLANATIONS = {
423
+ empty_input: "空输入,按问答处理",
424
+ question_with_explain_intent: "检测到问答 / 解释意图",
425
+ short_question: "检测到简短问答",
426
+ planning_or_design_intent: "检测到规划 / 设计意图",
427
+ long_complex_prompt: "检测到长而复杂的任务描述,可能需要 longagent",
428
+ short_local_task_protected: "检测到短小本地事务,避免升级到 longagent",
429
+ local_transaction_task: "检测到本地事务型任务,适合保持在轻量 agent 路径",
430
+ assistant_local_task: "检测到终端个人助手任务",
431
+ local_lookup_task: "检测到本地读取 / 总结类任务",
432
+ single_path_or_command_task: "检测到单路径或单命令任务,适合保持在轻量路径",
433
+ multi_file_or_system_task: "检测到跨文件 / 系统级任务",
434
+ broad_scope_multi_step: "检测到宽范围多步骤任务",
435
+ simple_action_task: "检测到单轮执行任务",
436
+ default_agent: "信号偏执行型,保持 agent",
437
+ default_assistant: "信号不足,按 assistant 处理",
438
+ low_confidence: "信号不足,保持当前模式",
439
+ plan_mode_exempt: "plan 模式不参与自动路由"
440
+ }
441
+
442
+ export function explainTaskModeReason(reason) {
443
+ return MODE_REASON_EXPLANATIONS[reason] || String(reason || "unknown")
444
+ }
445
+
446
+ function countPromptMatches(patterns, input) {
447
+ return patterns.reduce((count, pattern) => count + (pattern.test(input) ? 1 : 0), 0)
448
+ }
449
+
450
+ /**
451
+ * 分析 prompt,判断最适合的执行模式
452
+ * @returns {{ mode: 'assistant'|'plan'|'agent'|'longagent', confidence: 'high'|'medium'|'low', reason: string }}
453
+ */
454
+ export function classifyTaskMode(prompt, options = {}) {
455
+ const text = String(prompt || "").trim()
456
+ if (!text) return { mode: "assistant", confidence: "high", reason: "empty_input" }
457
+
458
+ const continuation = options?.continuation || null
459
+ const lower = text.toLowerCase()
460
+ const len = text.length
461
+ const pathHints = extractPromptPathHints(text)
462
+ const hasPathHint = pathHints.length > 0
463
+
464
+ const questionPatterns = [
465
+ /^(what|how|why|when|where|who|which|explain|tell me|describe|show me)\b/i,
466
+ /^(什么|为什么|怎么|如何|哪里|哪个|谁|能否|请解释|告诉我|描述|是什么|有什么|怎样)/,
467
+ /[??]\s*$/
468
+ ]
469
+ const pureAssistantKeywords = [
470
+ "explain", "what is", "what are", "how does", "why does", "describe", "tell me about",
471
+ "解释", "是什么", "为什么", "怎么理解", "什么意思", "有什么区别", "如何理解"
472
+ ]
473
+ const planPatterns = [
474
+ /\b(plan|design|architect|outline|blueprint|draft|propose|sketch)\b/i,
475
+ /\b(规划|设计|架构|方案|蓝图|草案|提案|计划一下|帮我想想)\b/i
476
+ ]
477
+ const explicitHeavyScopePatterns = [
478
+ /\b(multiple files?|across files?|entire (codebase|project|repo)|all files?|cross[- ]repo|跨文件|多个文件|整个项目|全量)\b/i,
479
+ /\b(refactor|rewrite|overhaul|redesign|migrate).{0,30}(system|service|architecture|repo|project|module|pipeline|codebase)\b/i
480
+ ]
481
+ const heavyDeliveryPatterns = [
482
+ /\b(implement|build|create|develop|add).{0,40}(system|subsystem|service|feature|component|framework|pipeline|architecture|架构|系统|模块|服务|功能|组件|框架|流水线)\b/i,
483
+ /\b(full|complete|comprehensive|end.to.end|完整实现|完全|端到端)\b/i,
484
+ /\b(multi.?stage|multi.?step|phases?|多阶段|多步骤|分阶段)\b/i
485
+ ]
486
+ const inspectPatterns = [
487
+ /\b(run|execute|check|inspect|look at|read|open|summari[sz]e|scan|search|find|list|grep|tail|cat|count|compare|verify|show)\b/i,
488
+ /\b(日志|目录|文件|配置|仓库|看一下|检查|查看|读取|总结|搜一下|列出|执行|运行|验证)\b/i
489
+ ]
490
+ const patchPatterns = [
491
+ /\b(fix|debug|patch|update|change|modify|rename|delete|remove|add|insert|append)\b/i,
492
+ /\b(修复|调试|修改|更新|删除|添加|插入|改一下|帮我改|帮我加)\b/i
493
+ ]
494
+ const singleCommandPatterns = [
495
+ /`[^`]+`/,
496
+ /\b(npm|pnpm|yarn|node|git|ls|cat|grep|rg|find|sed|awk|tail|head)\b/i
497
+ ]
498
+ const verifyPatterns = [
499
+ /\b(test|verify|validate|confirm|make sure|ensure|smoke)\b/i,
500
+ /\b(验证|确认|测试|确保|冒烟)\b/i
501
+ ]
502
+
503
+ const isQuestion = questionPatterns.some((re) => re.test(text))
504
+ const isPureAssistantRequest = pureAssistantKeywords.some((kw) => lower.includes(kw))
505
+ const isPlan = planPatterns.some((re) => re.test(lower))
506
+ const explicitHeavyScope = explicitHeavyScopePatterns.some((re) => re.test(lower))
507
+ const heavyDelivery = heavyDeliveryPatterns.some((re) => re.test(lower))
508
+ const hasAcrossScope = /\bacross\b|跨/.test(lower)
509
+ const isLocalTask = inspectPatterns.some((re) => re.test(lower))
510
+ const isPatchTask = patchPatterns.some((re) => re.test(lower))
511
+ const isVerifyTask = verifyPatterns.some((re) => re.test(lower))
512
+ const isAgentAction = isPatchTask || isVerifyTask
513
+ const isSingleCommandTask = singleCommandPatterns.some((re) => re.test(text))
514
+ const isVerificationTask = isVerifyTask
515
+ const hasContinuationSignal = Boolean(options?.continued || continuation?.objective || detectAgentContinuationInput(text, continuation))
516
+ const isLongAgent = explicitHeavyScope || heavyDelivery
517
+ const localSignalCount = [isLocalTask, isPatchTask, isVerifyTask, hasPathHint, isSingleCommandTask, hasContinuationSignal].filter(Boolean).length
518
+ const heavySignalCount = [explicitHeavyScope, heavyDelivery, hasAcrossScope].filter(Boolean).length
519
+ const smallBoundedPathSet = hasPathHint && pathHints.length <= 3
520
+ const isBoundedLocalTask = !isLongAgent && (isLocalTask || isAgentAction) && (hasPathHint || isSingleCommandTask || len < 320)
521
+ const isInspectPatchVerifyLoop = isLocalTask && isAgentAction && isVerificationTask
522
+ const evidence = []
523
+ if (hasPathHint) evidence.push(hasPathHint && pathHints.length === 1 ? "single_path" : "bounded_file_set")
524
+ if (isSingleCommandTask) evidence.push("single_command")
525
+ if (isLocalTask) evidence.push("inspect")
526
+ if (isPatchTask) evidence.push("patch")
527
+ if (isVerifyTask) evidence.push("verify")
528
+ if (isInspectPatchVerifyLoop) evidence.push("inspect_patch_verify")
529
+ if (isPlan && localSignalCount >= 2 && !explicitHeavyScope) evidence.push("embedded_planning_language")
530
+ if (hasContinuationSignal) evidence.push("continuation_context")
531
+ if (explicitHeavyScope || hasAcrossScope) evidence.push("cross_file_scope")
532
+ if (heavyDelivery) evidence.push("heavy_delivery")
533
+
534
+ let topology = "open_ended"
535
+ if (explicitHeavyScope || (heavyDelivery && heavySignalCount >= 2 && localSignalCount <= 2)) {
536
+ topology = "heavy_multi_file_delivery"
537
+ } else if (isInspectPatchVerifyLoop || (localSignalCount >= 3 && smallBoundedPathSet)) {
538
+ topology = "bounded_local_transaction"
539
+ } else if (hasContinuationSignal) {
540
+ topology = "continued_local_transaction"
541
+ } else if (isLocalTask || hasPathHint || isSingleCommandTask) {
542
+ topology = "bounded_lookup"
543
+ }
544
+
545
+ const scores = { assistant: 0, plan: 0, agent: 0, longagent: 0 }
546
+ const reasons = {
547
+ assistant: "assistant_local_task",
548
+ plan: "planning_or_design_intent",
549
+ agent: "default_agent",
550
+ longagent: "default_longagent"
551
+ }
552
+
553
+ if (isQuestion) {
554
+ evidence.push("question_intent")
555
+ scores.assistant += len < 120 ? 4 : 3
556
+ reasons.assistant = len < 80 ? "short_question" : "question_with_explain_intent"
557
+ }
558
+ if (isPureAssistantRequest) {
559
+ evidence.push("pure_explanation_request")
560
+ scores.assistant += 3
561
+ reasons.assistant = "question_with_explain_intent"
562
+ }
563
+
564
+ if (isPlan) {
565
+ evidence.push("planning_language")
566
+ scores.plan += 4
567
+ }
568
+
569
+ if (isLongAgent) {
570
+ evidence.push("heavy_scope_signal")
571
+ scores.longagent += 6
572
+ reasons.longagent = "multi_file_or_system_task"
573
+ }
574
+ if (heavyDelivery) {
575
+ scores.longagent += explicitHeavyScope ? 2 : 3
576
+ if (reasons.longagent === "default_longagent") reasons.longagent = "multi_file_or_system_task"
577
+ }
578
+ if (hasAcrossScope && heavyDelivery) {
579
+ scores.longagent += 3
580
+ reasons.longagent = "multi_file_or_system_task"
581
+ }
582
+
583
+ if (isLocalTask) {
584
+ evidence.push("local_task_signal")
585
+ scores.assistant += 4
586
+ reasons.assistant = "local_lookup_task"
587
+ }
588
+ if (isAgentAction) {
589
+ evidence.push("mutation_signal")
590
+ scores.agent += 3
591
+ if (reasons.agent === "default_agent") reasons.agent = "simple_action_task"
592
+ }
593
+ if (isVerifyTask) {
594
+ scores.agent += 2
595
+ if (reasons.agent === "default_agent") reasons.agent = "simple_action_task"
596
+ }
597
+ if (isInspectPatchVerifyLoop) {
598
+ scores.agent += 2
599
+ reasons.agent = "local_transaction_task"
600
+ }
601
+ if (hasContinuationSignal) {
602
+ scores.agent += 3
603
+ reasons.agent = "local_transaction_task"
604
+ }
605
+ if (isAgentAction && localSignalCount >= 2 && !explicitHeavyScope) {
606
+ scores.agent += 3
607
+ reasons.agent = "local_transaction_task"
608
+ }
609
+ if (hasPathHint || isSingleCommandTask) {
610
+ evidence.push(hasPathHint ? "path_hint" : "single_command")
611
+ scores.assistant += 2
612
+ if (reasons.assistant === "assistant_local_task") reasons.assistant = "single_path_or_command_task"
613
+ }
614
+ if (isVerificationTask) {
615
+ evidence.push("verification_signal")
616
+ }
617
+ if (isBoundedLocalTask) {
618
+ evidence.push("bounded_local_scope")
619
+ scores.assistant += 2
620
+ if (isAgentAction) scores.agent += 2
621
+ }
622
+ if (isInspectPatchVerifyLoop) {
623
+ evidence.push("inspect_patch_verify_loop")
624
+ scores.agent += 2
625
+ scores.longagent = Math.max(0, scores.longagent - 4)
626
+ reasons.agent = "short_local_task_protected"
627
+ }
628
+
629
+ if (len > 500 && !isQuestion && !isLocalTask && !isAgentAction && !hasPathHint && !isSingleCommandTask) {
630
+ evidence.push("long_prompt")
631
+ scores.longagent += 2
632
+ if (reasons.longagent === "default_longagent") reasons.longagent = "long_complex_prompt"
633
+ }
634
+
635
+ if ((len < 240 || isBoundedLocalTask) && (isLocalTask || isAgentAction || hasPathHint || isSingleCommandTask) && !isLongAgent) {
636
+ scores.longagent = Math.max(0, scores.longagent - 3)
637
+ if (isAgentAction && (reasons.agent === "local_transaction_task" || reasons.agent === "single_path_or_command_task")) {
638
+ reasons.agent = "short_local_task_protected"
639
+ }
640
+ }
641
+
642
+ if (!isQuestion && len > 50) {
643
+ scores.assistant += 1
644
+ if (isAgentAction) scores.agent += 1
645
+ }
646
+
647
+ if (scores.plan >= 4 && scores.plan >= scores.longagent + 2 && scores.plan >= scores.agent + 1 && len < 240) {
648
+ return {
649
+ mode: "plan",
650
+ confidence: scores.plan >= 5 ? "high" : "medium",
651
+ reason: reasons.plan,
652
+ evidence,
653
+ topology,
654
+ pathHints,
655
+ continuity: hasContinuationSignal ? "continue_current_transaction" : "new_transaction"
656
+ }
657
+ }
658
+
659
+ if (scores.longagent >= Math.max(scores.assistant, scores.agent) + 2 && scores.longagent > 0) {
660
+ return {
661
+ mode: "longagent",
662
+ confidence: scores.longagent >= 6 ? "high" : "medium",
663
+ reason: reasons.longagent === "default_longagent" ? "long_complex_prompt" : reasons.longagent,
664
+ evidence,
665
+ topology,
666
+ pathHints,
667
+ continuity: hasContinuationSignal ? "continue_current_transaction" : "new_transaction"
668
+ }
669
+ }
670
+
671
+ if (scores.agent >= scores.assistant && scores.agent > 0) {
672
+ return {
673
+ mode: "agent",
674
+ confidence: scores.agent >= 6 ? "high" : scores.agent >= 3 ? "medium" : "low",
675
+ reason: reasons.agent,
676
+ evidence,
677
+ topology,
678
+ pathHints,
679
+ continuity: hasContinuationSignal ? "continue_current_transaction" : "new_transaction"
680
+ }
681
+ }
682
+
683
+ if (scores.assistant > 0) {
684
+ return {
685
+ mode: "assistant",
686
+ confidence: scores.assistant >= 6 ? "high" : scores.assistant >= 3 ? "medium" : "low",
687
+ reason: reasons.assistant,
688
+ evidence,
689
+ topology,
690
+ pathHints,
691
+ continuity: hasContinuationSignal ? "continue_current_transaction" : "new_transaction"
692
+ }
693
+ }
694
+
695
+ return {
696
+ mode: "assistant",
697
+ confidence: scores.assistant >= 6 ? "high" : scores.assistant >= 3 ? "medium" : "low",
698
+ reason: reasons.assistant || "default_assistant",
699
+ evidence,
700
+ topology,
701
+ pathHints,
702
+ continuity: hasContinuationSignal ? "continue_current_transaction" : "new_transaction"
703
+ }
704
+ }
705
+
706
+ // ========== Task 4: 前端任务检测与设计风格提示词 ==========
707
+
708
+ /**
709
+ * 检测 prompt 是否涉及前端/UI 任务
710
+ */
711
+ export function detectFrontendTask(prompt) {
712
+ const lower = String(prompt || "").toLowerCase()
713
+ const frontendPatterns = [
714
+ /\b(react|vue|angular|svelte|next\.?js|nuxt|remix|astro|solid)\b/i,
715
+ /\b(html|css|scss|sass|less|tailwind|bootstrap|styled.components|emotion|chakra)\b/i,
716
+ /\b(ui|ux|frontend|front.end|web app|webpage|landing page|dashboard|component|widget)\b/i,
717
+ /\b(button|form|modal|navbar|sidebar|layout|grid|flex|animation|transition|responsive)\b/i,
718
+ /\b(前端|界面|页面|组件|样式|布局|动画|交互|响应式|移动端)\b/i
719
+ ]
720
+ return frontendPatterns.some(re => re.test(lower))
721
+ }
722
+
723
+ /**
724
+ * 生成前端设计风格提示词块
725
+ * @param {string} designStyle - 用户 profile 中的 design_style
726
+ */
727
+ export function buildFrontendDesignPrompt(designStyle = "") {
728
+ const lines = [
729
+ "## Frontend Design Guidelines",
730
+ "",
731
+ "Apply these principles to all UI/frontend code:",
732
+ "- Use semantic HTML5 elements (header, nav, main, section, article, footer)",
733
+ "- Responsive design with mobile-first approach (breakpoints: 640/768/1024/1280px)",
734
+ "- Accessibility: aria labels, keyboard navigation, color contrast ≥ 4.5:1",
735
+ "- CSS custom properties for theming; prefer CSS Grid/Flexbox for layouts",
736
+ "- Smooth transitions (150-300ms ease) for interactive elements",
737
+ "- Consistent spacing scale (4px base unit: 4/8/12/16/24/32/48/64px)",
738
+ "- Cross-browser compatibility (Chrome, Firefox, Safari, Edge)"
739
+ ]
740
+
741
+ if (designStyle) {
742
+ const s = designStyle.toLowerCase()
743
+ if (s.includes("minimal") || s.includes("clean")) {
744
+ lines.push("- Style: Minimal/Clean — generous whitespace, 2-3 color palette, flat design, no decorative elements")
745
+ } else if (s.includes("material")) {
746
+ lines.push("- Style: Material Design — elevation shadows, ripple effects, Material color system, 8dp grid")
747
+ } else if (s.includes("dark")) {
748
+ lines.push("- Style: Dark theme — backgrounds #121212/#1e1e1e, surface #2d2d2d, ensure contrast ≥ 4.5:1")
749
+ } else if (s.includes("glass") || s.includes("glassmorphism")) {
750
+ lines.push("- Style: Glassmorphism — backdrop-filter blur(10-20px), semi-transparent bg (rgba white/black 0.1-0.2), subtle 1px border")
751
+ } else if (s.includes("neumorphism") || s.includes("soft")) {
752
+ lines.push("- Style: Neumorphism — soft inset/outset shadows, monochromatic palette, subtle depth without harsh borders")
753
+ } else {
754
+ lines.push(`- Style: ${designStyle} — apply consistently across all components`)
755
+ }
756
+ }
757
+
758
+ return lines.join("\n")
759
+ }
760
+
761
+ // ========== Phase 11: 恢复建议生成 ==========
762
+
763
+ export function generateRecoverySuggestions({ status, taskProgress, gateStatus, phase, recoveryCount, fileChanges }) {
764
+ const suggestions = []
765
+
766
+ // 分析已完成和失败的 task
767
+ const completedTasks = []
768
+ const failedTasks = []
769
+ if (taskProgress && typeof taskProgress === "object") {
770
+ for (const [taskId, tp] of Object.entries(taskProgress)) {
771
+ if (tp.status === "completed") {
772
+ completedTasks.push(taskId)
773
+ } else if (tp.status === "error" || tp.status === "cancelled") {
774
+ const category = classifyError(tp.lastError)
775
+ failedTasks.push({
776
+ taskId,
777
+ error: (tp.lastError || "").slice(0, 200),
778
+ category,
779
+ suggestion: category === "permanent"
780
+ ? "此错误不可自动恢复,需要手动检查配置或文件路径"
781
+ : category === "logic"
782
+ ? "代码逻辑错误,建议检查相关文件的实现"
783
+ : "临时性错误,可尝试重新运行"
784
+ })
785
+ }
786
+ }
787
+ }
788
+
789
+ // 分析 gate 失败
790
+ const manualSteps = []
791
+ if (gateStatus) {
792
+ for (const [gate, info] of Object.entries(gateStatus)) {
793
+ if (info?.status === "fail" || info?.status === "fixing") {
794
+ manualSteps.push(`检查 ${gate} gate 的失败原因: ${info.failures || info.reason || "unknown"}`)
795
+ }
796
+ }
797
+ }
798
+
799
+ // 根据 phase 判断失败阶段
800
+ if (phase) {
801
+ if (phase.startsWith("H4")) suggestions.push("编码阶段未完成,可尝试缩小任务范围后重试")
802
+ if (phase.startsWith("H5")) suggestions.push("调试阶段未通过,建议手动检查测试输出")
803
+ if (phase.startsWith("H6")) suggestions.push("门控检查未通过,建议手动运行 build/test/lint 命令")
804
+ if (phase.startsWith("H7")) suggestions.push("Git 合并阶段出现问题,建议手动解决冲突")
805
+ }
806
+
807
+ const resumeHint = completedTasks.length > 0
808
+ ? `已完成 ${completedTasks.length} 个 task,可从 checkpoint 恢复继续`
809
+ : "无已完成的 task,建议重新开始"
810
+
811
+ const summary = [
812
+ `状态: ${status}`,
813
+ `阶段: ${phase || "unknown"}`,
814
+ `恢复次数: ${recoveryCount || 0}`,
815
+ `已完成: ${completedTasks.length} task(s)`,
816
+ `失败: ${failedTasks.length} task(s)`,
817
+ `文件变更: ${fileChanges?.length || 0}`
818
+ ].join(", ")
819
+
820
+ return {
821
+ suggestions,
822
+ completedTasks,
823
+ failedTasks,
824
+ manualSteps,
825
+ resumeHint,
826
+ summary
827
+ }
828
+ }