@kkelly-offical/kkcode 0.1.2 → 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.
Files changed (58) hide show
  1. package/README.md +120 -178
  2. package/package.json +46 -46
  3. package/src/agent/agent.mjs +41 -0
  4. package/src/agent/prompt/frontend-designer.txt +58 -0
  5. package/src/agent/prompt/longagent-blueprint-agent.txt +83 -0
  6. package/src/agent/prompt/longagent-coding-agent.txt +37 -0
  7. package/src/agent/prompt/longagent-debugging-agent.txt +46 -0
  8. package/src/agent/prompt/longagent-preview-agent.txt +63 -0
  9. package/src/config/defaults.mjs +260 -195
  10. package/src/config/schema.mjs +71 -6
  11. package/src/core/constants.mjs +91 -46
  12. package/src/index.mjs +1 -1
  13. package/src/knowledge/frontend-aesthetics.txt +39 -0
  14. package/src/knowledge/loader.mjs +2 -1
  15. package/src/knowledge/tailwind.txt +12 -3
  16. package/src/mcp/client-http.mjs +141 -157
  17. package/src/mcp/client-sse.mjs +288 -286
  18. package/src/mcp/client-stdio.mjs +533 -451
  19. package/src/mcp/constants.mjs +2 -0
  20. package/src/mcp/registry.mjs +479 -394
  21. package/src/mcp/stdio-framing.mjs +133 -127
  22. package/src/mcp/tool-result.mjs +24 -0
  23. package/src/observability/index.mjs +42 -0
  24. package/src/observability/metrics.mjs +137 -0
  25. package/src/observability/tracer.mjs +137 -0
  26. package/src/orchestration/background-manager.mjs +372 -358
  27. package/src/orchestration/background-worker.mjs +305 -245
  28. package/src/orchestration/longagent-manager.mjs +171 -116
  29. package/src/orchestration/stage-scheduler.mjs +728 -489
  30. package/src/permission/exec-policy.mjs +9 -11
  31. package/src/provider/anthropic.mjs +1 -0
  32. package/src/provider/openai.mjs +340 -339
  33. package/src/provider/retry-policy.mjs +68 -68
  34. package/src/provider/router.mjs +241 -228
  35. package/src/provider/sse.mjs +104 -91
  36. package/src/repl.mjs +1 -1
  37. package/src/session/checkpoint.mjs +66 -3
  38. package/src/session/engine.mjs +227 -225
  39. package/src/session/longagent-4stage.mjs +460 -0
  40. package/src/session/longagent-hybrid.mjs +1081 -0
  41. package/src/session/longagent-plan.mjs +365 -329
  42. package/src/session/longagent-project-memory.mjs +53 -0
  43. package/src/session/longagent-scaffold.mjs +291 -100
  44. package/src/session/longagent-task-bus.mjs +54 -0
  45. package/src/session/longagent-utils.mjs +472 -0
  46. package/src/session/longagent.mjs +884 -1462
  47. package/src/session/project-context.mjs +30 -0
  48. package/src/session/store.mjs +510 -503
  49. package/src/session/task-validator.mjs +4 -3
  50. package/src/skill/builtin/design.mjs +76 -0
  51. package/src/skill/builtin/frontend.mjs +8 -0
  52. package/src/skill/registry.mjs +390 -336
  53. package/src/storage/ghost-commit-store.mjs +18 -8
  54. package/src/tool/executor.mjs +11 -0
  55. package/src/tool/git-auto.mjs +0 -19
  56. package/src/tool/registry.mjs +71 -37
  57. package/src/ui/activity-renderer.mjs +664 -410
  58. 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
+ }