@kkelly-offical/kkcode 0.1.6 → 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.
- package/LICENSE +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +19 -2
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +84 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +2 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3368 -2929
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +36 -14
- package/src/session/engine.mjs +417 -227
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1081
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -884
- package/src/session/loop.mjs +1005 -905
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +28 -6
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +197 -0
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -510
- package/src/session/system-prompt.mjs +56 -8
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +13 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +17 -4
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
|
@@ -1,1081 +1,1344 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LongAgent Hybrid 模式
|
|
3
|
-
* 融合 4-Stage 的只读探索/规划/调试回滚 + Parallel 的脚手架/并行执行/门控
|
|
4
|
-
*
|
|
5
|
-
* 流程: H0:Intake → H1:Preview → H2:Blueprint → H2.5:Git → H3:Scaffold → H4:Coding(并行) → H5:Debugging(回滚) → H5.5:Validation → H6:Gates → H7:GitMerge
|
|
6
|
-
*/
|
|
7
|
-
import { LongAgentManager } from "../orchestration/longagent-manager.mjs"
|
|
8
|
-
import { processTurnLoop } from "./loop.mjs"
|
|
9
|
-
import { markSessionStatus } from "./store.mjs"
|
|
10
|
-
import { EventBus } from "../core/events.mjs"
|
|
11
|
-
import { EVENT_TYPES, LONGAGENT_4STAGE_STAGES } from "../core/constants.mjs"
|
|
12
|
-
import { saveCheckpoint, loadCheckpoint, saveTaskCheckpoint, loadTaskCheckpoints, cleanupCheckpoints } from "./checkpoint.mjs"
|
|
13
|
-
import { getAgent } from "../agent/agent.mjs"
|
|
14
|
-
import { runStageBarrier } from "../orchestration/stage-scheduler.mjs"
|
|
15
|
-
import { runScaffoldPhase } from "./longagent-scaffold.mjs"
|
|
16
|
-
import {
|
|
17
|
-
runUsabilityGates,
|
|
18
|
-
hasGatePreferences,
|
|
19
|
-
getGatePreferences,
|
|
20
|
-
saveGatePreferences,
|
|
21
|
-
buildGatePromptText,
|
|
22
|
-
parseGateSelection
|
|
23
|
-
} from "./usability-gates.mjs"
|
|
24
|
-
import { runIntakeDialogue, validateAndNormalizeStagePlan, defaultStagePlan } from "./longagent-plan.mjs"
|
|
25
|
-
import { createValidator } from "./task-validator.mjs"
|
|
26
|
-
import { detectStageComplete, detectReturnToCoding, buildStageWrapper } from "./longagent-4stage.mjs"
|
|
27
|
-
import {
|
|
28
|
-
isComplete,
|
|
29
|
-
isLikelyActionableObjective,
|
|
30
|
-
mergeCappedFileChanges,
|
|
31
|
-
stageProgressStats,
|
|
32
|
-
summarizeGateFailures,
|
|
33
|
-
LONGAGENT_FILE_CHANGES_LIMIT,
|
|
34
|
-
createStuckTracker,
|
|
35
|
-
classifyError,
|
|
36
|
-
ERROR_CATEGORIES,
|
|
37
|
-
createSemanticErrorTracker,
|
|
38
|
-
createDegradationChain,
|
|
39
|
-
generateRecoverySuggestions,
|
|
40
|
-
stripFence,
|
|
41
|
-
parseJsonLoose
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
import
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (cp.stageIndex
|
|
54
|
-
return
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return { agent: "
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
"-
|
|
77
|
-
"-
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"",
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// #
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (hybridConfig.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
// ==========
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
.
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
const
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
//
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
await syncState({ lastMessage:
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* LongAgent Hybrid 模式
|
|
3
|
+
* 融合 4-Stage 的只读探索/规划/调试回滚 + Parallel 的脚手架/并行执行/门控
|
|
4
|
+
*
|
|
5
|
+
* 流程: H0:Intake → H1:Preview → H2:Blueprint → H2.5:Git → H3:Scaffold → H4:Coding(并行) → H5:Debugging(回滚) → H5.5:Validation → H6:Gates → H7:GitMerge
|
|
6
|
+
*/
|
|
7
|
+
import { LongAgentManager } from "../orchestration/longagent-manager.mjs"
|
|
8
|
+
import { processTurnLoop } from "./loop.mjs"
|
|
9
|
+
import { markSessionStatus } from "./store.mjs"
|
|
10
|
+
import { EventBus } from "../core/events.mjs"
|
|
11
|
+
import { EVENT_TYPES, LONGAGENT_4STAGE_STAGES } from "../core/constants.mjs"
|
|
12
|
+
import { saveCheckpoint, loadCheckpoint, saveTaskCheckpoint, loadTaskCheckpoints, cleanupCheckpoints } from "./checkpoint.mjs"
|
|
13
|
+
import { getAgent } from "../agent/agent.mjs"
|
|
14
|
+
import { runStageBarrier } from "../orchestration/stage-scheduler.mjs"
|
|
15
|
+
import { runScaffoldPhase } from "./longagent-scaffold.mjs"
|
|
16
|
+
import {
|
|
17
|
+
runUsabilityGates,
|
|
18
|
+
hasGatePreferences,
|
|
19
|
+
getGatePreferences,
|
|
20
|
+
saveGatePreferences,
|
|
21
|
+
buildGatePromptText,
|
|
22
|
+
parseGateSelection
|
|
23
|
+
} from "./usability-gates.mjs"
|
|
24
|
+
import { runIntakeDialogue, validateAndNormalizeStagePlan, defaultStagePlan } from "./longagent-plan.mjs"
|
|
25
|
+
import { createValidator } from "./task-validator.mjs"
|
|
26
|
+
import { detectStageComplete, detectReturnToCoding, buildStageWrapper } from "./longagent-4stage.mjs"
|
|
27
|
+
import {
|
|
28
|
+
isComplete,
|
|
29
|
+
isLikelyActionableObjective,
|
|
30
|
+
mergeCappedFileChanges,
|
|
31
|
+
stageProgressStats,
|
|
32
|
+
summarizeGateFailures,
|
|
33
|
+
LONGAGENT_FILE_CHANGES_LIMIT,
|
|
34
|
+
createStuckTracker,
|
|
35
|
+
classifyError,
|
|
36
|
+
ERROR_CATEGORIES,
|
|
37
|
+
createSemanticErrorTracker,
|
|
38
|
+
createDegradationChain,
|
|
39
|
+
generateRecoverySuggestions,
|
|
40
|
+
stripFence,
|
|
41
|
+
parseJsonLoose,
|
|
42
|
+
detectFrontendTask,
|
|
43
|
+
buildFrontendDesignPrompt
|
|
44
|
+
} from "./longagent-utils.mjs"
|
|
45
|
+
import { TaskBus } from "./longagent-task-bus.mjs"
|
|
46
|
+
import { loadProjectMemory, saveProjectMemory, memoryToContext, parseMemoryFromPreview } from "./longagent-project-memory.mjs"
|
|
47
|
+
import YAML from "yaml"
|
|
48
|
+
import * as git from "../util/git.mjs"
|
|
49
|
+
|
|
50
|
+
// Checkpoint 结构校验
|
|
51
|
+
function validateCheckpoint(cp) {
|
|
52
|
+
if (!cp || !cp.stagePlan || !Array.isArray(cp.stagePlan.stages)) return false
|
|
53
|
+
if (typeof cp.stageIndex !== "number" || cp.stageIndex < 0) return false
|
|
54
|
+
if (cp.stageIndex > cp.stagePlan.stages.length) return false
|
|
55
|
+
// Verify the previous stage exists for task checkpoint loading
|
|
56
|
+
if (cp.stageIndex > 0 && !cp.stagePlan.stages[cp.stageIndex - 1]) return false
|
|
57
|
+
return true
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Gate 修复策略路由 (Phase 8)
|
|
61
|
+
function getGateFixStrategy(failures) {
|
|
62
|
+
const gateTypes = (failures || []).map(f => f.gate).filter(Boolean)
|
|
63
|
+
if (gateTypes.includes("test")) return { agent: "debugging-agent", prefix: "Analyze test failures and fix:" }
|
|
64
|
+
if (gateTypes.every(g => g === "build")) return { agent: "coding-agent", prefix: "Fix build errors:" }
|
|
65
|
+
if (gateTypes.every(g => g === "lint")) return { autoFix: "npx eslint --fix .", agent: "coding-agent", prefix: "Fix lint errors:" }
|
|
66
|
+
return { agent: "coding-agent", prefix: "Fix gate failures:" }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// #13 上下文压缩
|
|
70
|
+
async function compressContext(text, limit, { model, providerType, sessionId, configState, baseUrl, apiKeyEnv, signal, toolContext }) {
|
|
71
|
+
if (text.length <= limit) return text
|
|
72
|
+
const out = await processTurnLoop({
|
|
73
|
+
prompt: [
|
|
74
|
+
`Compress the following engineering context to max ${Math.round(limit * 0.6)} characters.`,
|
|
75
|
+
"Preserve ONLY:",
|
|
76
|
+
"- Concrete decisions made (technology choices, architecture patterns, API contracts)",
|
|
77
|
+
"- File paths and function signatures that were created or modified",
|
|
78
|
+
"- Error messages and their resolutions",
|
|
79
|
+
"- Cross-task dependencies and integration points",
|
|
80
|
+
"- Test results (pass/fail with specific failure reasons)",
|
|
81
|
+
"Discard: exploration logs, verbose tool output, repeated information, reasoning chains.",
|
|
82
|
+
"Output the compressed context directly — no preamble or explanation.",
|
|
83
|
+
"",
|
|
84
|
+
text.slice(0, limit * 2)
|
|
85
|
+
].join("\n"),
|
|
86
|
+
mode: "assistant", model, providerType, sessionId, configState, baseUrl, apiKeyEnv, signal, allowQuestion: false, toolContext
|
|
87
|
+
})
|
|
88
|
+
return (out.reply || text.slice(0, limit)).slice(0, limit)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// #3 动态计划修订解析
|
|
92
|
+
function parseReplanMarker(text) {
|
|
93
|
+
const match = String(text || "").match(/\[REPLAN:\s*([\s\S]*?)\]/i)
|
|
94
|
+
if (!match) return null
|
|
95
|
+
try { return JSON.parse(match[1]) } catch { return null }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// #1 细粒度回滚:从 debugging 输出中提取失败的 taskId
|
|
99
|
+
function extractFailedTaskIds(text) {
|
|
100
|
+
const ids = []
|
|
101
|
+
const pattern = /\[FAILED_TASK:\s*(\S+)\]/gi
|
|
102
|
+
let m
|
|
103
|
+
while ((m = pattern.exec(text)) !== null) ids.push(m[1])
|
|
104
|
+
return ids
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildConflictResolutionPrompt(conflictFiles) {
|
|
108
|
+
return [
|
|
109
|
+
"## Git Merge Conflict Resolution",
|
|
110
|
+
"",
|
|
111
|
+
"The following files have merge conflicts that must be resolved:",
|
|
112
|
+
...conflictFiles.map(f => `- ${f}`),
|
|
113
|
+
"",
|
|
114
|
+
"## Resolution Protocol",
|
|
115
|
+
"1. Read each conflicted file and locate ALL conflict markers (<<<<<<< ======= >>>>>>>)",
|
|
116
|
+
"2. For each conflict block:",
|
|
117
|
+
" - Understand what BOTH sides intended (ours = feature branch, theirs = base branch)",
|
|
118
|
+
" - Keep the feature branch changes (our work) unless they break base branch functionality",
|
|
119
|
+
" - If both sides modified the same logic, merge them intelligently (not just pick one)",
|
|
120
|
+
" - Remove ALL conflict markers — no <<<<<<< or ======= or >>>>>>> should remain",
|
|
121
|
+
"3. After resolving, run syntax check on each file (node --check / python -m py_compile)",
|
|
122
|
+
"4. Verify imports still resolve correctly across resolved files"
|
|
123
|
+
].join("\n")
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
function parseBlueprintOutput(reply, objective, defaults) {
|
|
128
|
+
const parseErrors = []
|
|
129
|
+
|
|
130
|
+
// 1. 尝试提取 ```stage_plan_json ... ``` 块
|
|
131
|
+
const jsonMatch = reply.match(/```stage_plan_json\s*([\s\S]*?)```/)
|
|
132
|
+
if (jsonMatch) {
|
|
133
|
+
const parsed = parseJsonLoose(jsonMatch[1])
|
|
134
|
+
if (parsed?.stages) {
|
|
135
|
+
const { plan, errors } = validateAndNormalizeStagePlan(parsed, { objective, defaults })
|
|
136
|
+
if (!errors.length) {
|
|
137
|
+
return { architectureText: reply.replace(/```stage_plan_json[\s\S]*?```/g, "").trim(), stagePlan: plan, parseErrors: [] }
|
|
138
|
+
}
|
|
139
|
+
parseErrors.push(`stage_plan_json block validation: ${errors.join("; ")}`)
|
|
140
|
+
} else {
|
|
141
|
+
parseErrors.push("stage_plan_json block found but no stages field")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 2. 回退:尝试任意 JSON 围栏块(排除已处理的 stage_plan_json)
|
|
146
|
+
const anyJson = reply.match(/```(?:json)?\s*([\s\S]*?)```/g)
|
|
147
|
+
if (anyJson) {
|
|
148
|
+
for (const block of anyJson) {
|
|
149
|
+
if (/```stage_plan_json/.test(block)) continue
|
|
150
|
+
const inner = block.replace(/```(?:json)?\s*/g, "").replace(/```/g, "").trim()
|
|
151
|
+
const parsed = parseJsonLoose(inner)
|
|
152
|
+
if (parsed?.stages) {
|
|
153
|
+
const { plan, errors } = validateAndNormalizeStagePlan(parsed, { objective, defaults })
|
|
154
|
+
if (!errors.length) return { architectureText: reply, stagePlan: plan, parseErrors: [] }
|
|
155
|
+
parseErrors.push(`json block validation: ${errors.join("; ")}`)
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 3. 回退:裸 JSON — 定位含 "stages" 的最外层 {} 块
|
|
161
|
+
const stripped = reply.replace(/```[\s\S]*?```/g, "")
|
|
162
|
+
let braceDepth = 0, objStart = -1
|
|
163
|
+
for (let i = 0; i < stripped.length; i++) {
|
|
164
|
+
if (stripped[i] === "{") { if (braceDepth === 0) objStart = i; braceDepth++ }
|
|
165
|
+
else if (stripped[i] === "}") {
|
|
166
|
+
braceDepth--
|
|
167
|
+
if (braceDepth === 0 && objStart >= 0) {
|
|
168
|
+
const candidate = stripped.slice(objStart, i + 1)
|
|
169
|
+
if (candidate.includes('"stages"')) {
|
|
170
|
+
const parsed = parseJsonLoose(candidate)
|
|
171
|
+
if (parsed?.stages) {
|
|
172
|
+
const { plan, errors } = validateAndNormalizeStagePlan(parsed, { objective, defaults })
|
|
173
|
+
if (!errors.length) return { architectureText: reply, stagePlan: plan, parseErrors: [] }
|
|
174
|
+
parseErrors.push(`bare JSON validation: ${errors.join("; ")}`)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
objStart = -1
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 4. 回退:YAML 围栏块(```yaml ... ```)
|
|
183
|
+
const yamlBlocks = reply.match(/```ya?ml\s*([\s\S]*?)```/g)
|
|
184
|
+
if (yamlBlocks) {
|
|
185
|
+
for (const block of yamlBlocks) {
|
|
186
|
+
const inner = block.replace(/```ya?ml?\s*/g, "").replace(/```/g, "").trim()
|
|
187
|
+
try {
|
|
188
|
+
const parsed = YAML.parse(inner)
|
|
189
|
+
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) continue
|
|
190
|
+
if (parsed?.stages) {
|
|
191
|
+
const { plan, errors } = validateAndNormalizeStagePlan(parsed, { objective, defaults })
|
|
192
|
+
if (!errors.length) return { architectureText: reply, stagePlan: plan, parseErrors: [] }
|
|
193
|
+
parseErrors.push(`yaml block validation: ${errors.join("; ")}`)
|
|
194
|
+
}
|
|
195
|
+
} catch (e) {
|
|
196
|
+
parseErrors.push(`yaml parse error: ${e.message}`)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 5. 最终回退:单任务默认计划
|
|
202
|
+
if (!parseErrors.length) parseErrors.push("no JSON/YAML with stages field found in reply")
|
|
203
|
+
return { architectureText: reply, stagePlan: defaultStagePlan(objective, defaults), parseErrors }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export async function runHybridLongAgent({
|
|
207
|
+
prompt, model, providerType, sessionId, configState,
|
|
208
|
+
baseUrl = null, apiKeyEnv = null, agent = null,
|
|
209
|
+
maxIterations = 0, signal = null, output = null,
|
|
210
|
+
allowQuestion = true, toolContext = {}
|
|
211
|
+
}) {
|
|
212
|
+
const longagentConfig = configState.config.agent.longagent || {}
|
|
213
|
+
const hybridConfig = longagentConfig.hybrid || {}
|
|
214
|
+
const parallelConfig = longagentConfig.parallel || {}
|
|
215
|
+
const gitConfig = longagentConfig.git || {}
|
|
216
|
+
const noProgressLimit = Number(longagentConfig.no_progress_limit || 5)
|
|
217
|
+
const maxGateAttempts = Number(longagentConfig.max_gate_attempts || 5)
|
|
218
|
+
const fileChangesLimit = Math.max(20, Number(longagentConfig.file_changes_limit || LONGAGENT_FILE_CHANGES_LIMIT))
|
|
219
|
+
|
|
220
|
+
// 每阶段模型选择
|
|
221
|
+
const separateModels = hybridConfig.separate_models || {}
|
|
222
|
+
const useSeparateModels = separateModels.enabled === true
|
|
223
|
+
const adaptiveModels = hybridConfig.adaptive_models || {}
|
|
224
|
+
const useAdaptiveModels = adaptiveModels.enabled === true
|
|
225
|
+
function getModelForStage(stage) {
|
|
226
|
+
if (!useSeparateModels) return { model, providerType }
|
|
227
|
+
const m = { preview: separateModels.preview_model, blueprint: separateModels.blueprint_model, debugging: separateModels.debugging_model }
|
|
228
|
+
return m[stage] ? { model: m[stage], providerType } : { model, providerType }
|
|
229
|
+
}
|
|
230
|
+
// #8 自适应模型路由:根据 task complexity 选择模型
|
|
231
|
+
function getModelForTask(task) {
|
|
232
|
+
if (!useAdaptiveModels) return model
|
|
233
|
+
const tier = task?.complexity || "medium"
|
|
234
|
+
return adaptiveModels[tier] || model
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let iteration = 0, recoveryCount = 0, stageIndex = 0
|
|
238
|
+
let currentPhase = "H0", currentGate = "init"
|
|
239
|
+
let gateStatus = {}, lastGateFailures = []
|
|
240
|
+
let lastProgress = { percentage: 0, currentStep: 0, totalSteps: 0 }
|
|
241
|
+
let finalReply = "", planFrozen = false, stagePlan = null
|
|
242
|
+
let taskProgress = {}, fileChanges = []
|
|
243
|
+
let completionMarkerSeen = false
|
|
244
|
+
let gitBranch = null, gitBaseBranch = null, gitActive = false
|
|
245
|
+
const aggregateUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
246
|
+
const toolEvents = []
|
|
247
|
+
const startTime = Date.now()
|
|
248
|
+
const stuckTracker = createStuckTracker()
|
|
249
|
+
// Phase 6: 降级链
|
|
250
|
+
const degradationChain = createDegradationChain(hybridConfig.degradation || {})
|
|
251
|
+
// Phase 2: 阶段超时配置
|
|
252
|
+
const codingPhaseTimeoutMs = Number(hybridConfig.coding_phase_timeout_ms || 1800000)
|
|
253
|
+
const debuggingPhaseTimeoutMs = Number(hybridConfig.debugging_phase_timeout_ms || 600000)
|
|
254
|
+
// #4 TaskBus
|
|
255
|
+
const taskBus = hybridConfig.task_bus !== false ? new TaskBus() : null
|
|
256
|
+
// #5 Project Memory
|
|
257
|
+
const cwd = process.cwd()
|
|
258
|
+
let projectMemory = null
|
|
259
|
+
if (hybridConfig.project_memory !== false) {
|
|
260
|
+
try { projectMemory = await loadProjectMemory(cwd) } catch { projectMemory = null }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function accumulateUsage(turn) {
|
|
264
|
+
aggregateUsage.input += turn.usage?.input || 0
|
|
265
|
+
aggregateUsage.output += turn.usage?.output || 0
|
|
266
|
+
aggregateUsage.cacheRead += turn.usage?.cacheRead || 0
|
|
267
|
+
aggregateUsage.cacheWrite += turn.usage?.cacheWrite || 0
|
|
268
|
+
if (turn.toolEvents?.length) toolEvents.push(...turn.toolEvents)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function setPhase(next, reason = "") {
|
|
272
|
+
if (currentPhase === next) return
|
|
273
|
+
const prev = currentPhase
|
|
274
|
+
currentPhase = next
|
|
275
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_PHASE_CHANGED, sessionId, payload: { prevPhase: prev, nextPhase: next, reason, iteration } })
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function syncState(patch = {}) {
|
|
279
|
+
const stats = stageProgressStats(taskProgress)
|
|
280
|
+
await LongAgentManager.update(sessionId, {
|
|
281
|
+
status: patch.status || "running", phase: currentPhase, gateStatus, currentGate,
|
|
282
|
+
recoveryCount, lastGateFailures, iterations: iteration, heartbeatAt: Date.now(),
|
|
283
|
+
progress: lastProgress, planFrozen, stageIndex,
|
|
284
|
+
stageCount: stagePlan?.stages?.length || 0,
|
|
285
|
+
taskProgress, stageProgress: { done: stats.done, total: stats.total },
|
|
286
|
+
remainingFilesCount: stats.remainingFilesCount,
|
|
287
|
+
...patch
|
|
288
|
+
})
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Phase 2: 事件驱动 stop 检测 — 用内存标志替代磁盘轮询
|
|
292
|
+
let stopFlag = false
|
|
293
|
+
const unsubscribeStop = EventBus.subscribe((evt) => {
|
|
294
|
+
if (evt.type === EVENT_TYPES.LONGAGENT_STOP_REQUESTED && evt.sessionId === sessionId) {
|
|
295
|
+
stopFlag = true
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
await markSessionStatus(sessionId, "running-longagent")
|
|
300
|
+
await syncState({ status: "running", lastMessage: "hybrid mode started" })
|
|
301
|
+
|
|
302
|
+
// 前置检查
|
|
303
|
+
if (!isLikelyActionableObjective(prompt)) {
|
|
304
|
+
const blocked = "LongAgent 需要明确的编码目标。请直接描述要实现/修复的内容。"
|
|
305
|
+
await LongAgentManager.update(sessionId, { status: "blocked", phase: "H0", lastMessage: blocked })
|
|
306
|
+
await markSessionStatus(sessionId, "active")
|
|
307
|
+
unsubscribeStop()
|
|
308
|
+
return { sessionId, turnId: `turn_long_${Date.now()}`, reply: blocked, usage: aggregateUsage, toolEvents, iterations: 0, status: "blocked", phase: "H0", gateStatus: {}, currentGate: "init", lastGateFailures: [], recoveryCount: 0, progress: lastProgress, elapsed: 0, stageIndex: 0, stageCount: 0, planFrozen: false, taskProgress: {}, fileChanges: [], stageProgress: { done: 0, total: 0 }, remainingFilesCount: 0 }
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// #15 Checkpoint 恢复:如果有之前的检查点,跳过已完成阶段
|
|
312
|
+
// #22: 增强为 task 级粒度恢复
|
|
313
|
+
if (hybridConfig.checkpoint_resume !== false) {
|
|
314
|
+
try {
|
|
315
|
+
const cp = await loadCheckpoint(sessionId)
|
|
316
|
+
if (cp?.stageIndex > 0 && cp?.stagePlan) {
|
|
317
|
+
if (!validateCheckpoint(cp)) {
|
|
318
|
+
// Invalid checkpoint structure — discard and start fresh
|
|
319
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_CHECKPOINT_INVALID, sessionId, payload: { reason: "structure_validation_failed" } })
|
|
320
|
+
} else {
|
|
321
|
+
stagePlan = cp.stagePlan; stageIndex = cp.stageIndex; planFrozen = true
|
|
322
|
+
taskProgress = cp.taskProgress || {}; lastProgress = cp.lastProgress || lastProgress
|
|
323
|
+
iteration = cp.iteration || 0
|
|
324
|
+
// #22: Load task-level checkpoints to recover intra-stage progress
|
|
325
|
+
if (stageIndex > 0) {
|
|
326
|
+
const prevStage = cp.stagePlan.stages[stageIndex - 1]
|
|
327
|
+
if (prevStage) {
|
|
328
|
+
const taskCps = await loadTaskCheckpoints(sessionId, prevStage.stageId)
|
|
329
|
+
for (const [tid, tData] of Object.entries(taskCps)) {
|
|
330
|
+
if (!taskProgress[tid] || taskProgress[tid].status !== "completed") {
|
|
331
|
+
taskProgress[tid] = { ...taskProgress[tid], ...tData }
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_CHECKPOINT_RESUMED, sessionId, payload: { stageIndex, iteration } })
|
|
337
|
+
await syncState({ lastMessage: `resumed from checkpoint at stage ${stageIndex}` })
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
} catch { /* no checkpoint, start fresh */ }
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// #5 Memory 事件
|
|
344
|
+
if (projectMemory?.techStack?.length) {
|
|
345
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_MEMORY_LOADED, sessionId, payload: { techStack: projectMemory.techStack } })
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ========== H0: INTAKE (需求澄清) ==========
|
|
349
|
+
let intakeSummary = prompt
|
|
350
|
+
if (hybridConfig.intake !== false && !planFrozen) {
|
|
351
|
+
await setPhase("H0", "intake")
|
|
352
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_INTAKE_STARTED, sessionId, payload: { objective: prompt } })
|
|
353
|
+
await syncState({ lastMessage: "H0: intake dialogue — clarifying requirements" })
|
|
354
|
+
|
|
355
|
+
const plannerConfig = longagentConfig.planner || {}
|
|
356
|
+
const intakeConfig = plannerConfig.intake_questions || {}
|
|
357
|
+
const intake = await runIntakeDialogue({
|
|
358
|
+
objective: prompt,
|
|
359
|
+
model, providerType, sessionId, configState,
|
|
360
|
+
baseUrl, apiKeyEnv, agent, signal,
|
|
361
|
+
maxRounds: Number(intakeConfig.max_rounds || 6)
|
|
362
|
+
})
|
|
363
|
+
intakeSummary = intake.summary || prompt
|
|
364
|
+
accumulateUsage(intake)
|
|
365
|
+
gateStatus.intake = { status: "pass", rounds: intake.transcript.length, summary: intakeSummary.slice(0, 500) }
|
|
366
|
+
await syncState({ lastMessage: `H0: intake complete (${intake.transcript.length} qa pairs)` })
|
|
367
|
+
|
|
368
|
+
// Task 2: 用户可见的需求确认 — 展示 intake 摘要,请用户确认或补充
|
|
369
|
+
if (allowQuestion && hybridConfig.intake_user_confirm !== false) {
|
|
370
|
+
const confirmPrompt = [
|
|
371
|
+
"[SYSTEM] H0 需求分析完成。以下是对任务的理解摘要:",
|
|
372
|
+
"",
|
|
373
|
+
intakeSummary.slice(0, 1200),
|
|
374
|
+
"",
|
|
375
|
+
"请使用 question 工具询问用户:",
|
|
376
|
+
"1. 以上需求理解是否准确?",
|
|
377
|
+
"2. 是否有需要补充或修改的地方?",
|
|
378
|
+
"3. 如果没有补充,回复 [确认] 或 [继续] 即可开始执行。",
|
|
379
|
+
"",
|
|
380
|
+
"根据用户的回复更新 intakeSummary(如有补充则合并到需求中)。"
|
|
381
|
+
].join("\n")
|
|
382
|
+
const confirmOut = await processTurnLoop({
|
|
383
|
+
prompt: confirmPrompt,
|
|
384
|
+
mode: "assistant", model, providerType, sessionId, configState,
|
|
385
|
+
baseUrl, apiKeyEnv, agent, signal, allowQuestion: true, toolContext, output
|
|
386
|
+
})
|
|
387
|
+
accumulateUsage(confirmOut)
|
|
388
|
+
// 如果用户提供了补充,将其合并到 intakeSummary
|
|
389
|
+
const userAddition = String(confirmOut.reply || "").trim()
|
|
390
|
+
const cancelKeywords = ["abort", "cancel", "取消", "中止", "停止"]
|
|
391
|
+
if (cancelKeywords.some(k => userAddition.toLowerCase().includes(k))) {
|
|
392
|
+
await LongAgentManager.update(sessionId, { status: "aborted", lastMessage: "user cancelled at intake confirmation" })
|
|
393
|
+
await markSessionStatus(sessionId, "active")
|
|
394
|
+
unsubscribeStop()
|
|
395
|
+
return { sessionId, turnId: `turn_long_${Date.now()}`, reply: "用户在需求确认阶段取消了任务。", usage: aggregateUsage, toolEvents, iterations: iteration, status: "aborted", phase: "H0", gateStatus, currentGate, lastGateFailures: [], recoveryCount: 0, progress: lastProgress, elapsed: Math.round((Date.now() - startTime) / 1000), stageIndex: 0, stageCount: 0, planFrozen: false, taskProgress: {}, fileChanges: [], stageProgress: { done: 0, total: 0 }, remainingFilesCount: 0 }
|
|
396
|
+
}
|
|
397
|
+
if (userAddition && !["确认", "继续", "ok", "yes", "是", "好", "没有", "no addition"].some(k => userAddition.toLowerCase().includes(k))) {
|
|
398
|
+
intakeSummary = `${intakeSummary}\n\n[用户补充]\n${userAddition}`
|
|
399
|
+
gateStatus.intake = { ...gateStatus.intake, userConfirmed: true, userAddition: userAddition.slice(0, 200) }
|
|
400
|
+
} else {
|
|
401
|
+
gateStatus.intake = { ...gateStatus.intake, userConfirmed: true }
|
|
402
|
+
}
|
|
403
|
+
await syncState({ lastMessage: "H0: user confirmed requirements" })
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ========== H1: PREVIEW (只读探索) ==========
|
|
408
|
+
await setPhase("H1", "preview")
|
|
409
|
+
currentGate = "preview"
|
|
410
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_PREVIEW_START, sessionId, payload: { objective: prompt } })
|
|
411
|
+
await syncState({ lastMessage: "H1: preview agent exploring codebase" })
|
|
412
|
+
|
|
413
|
+
const previewModel = getModelForStage("preview")
|
|
414
|
+
// #5 注入 project memory 到 preview prompt
|
|
415
|
+
const memCtx = projectMemory ? memoryToContext(projectMemory) : ""
|
|
416
|
+
const previewPrompt = buildStageWrapper(LONGAGENT_4STAGE_STAGES.PREVIEW, { preview: null, blueprint: null, coding: null }, memCtx ? `${memCtx}\n\n${intakeSummary}` : intakeSummary)
|
|
417
|
+
const previewOut = await processTurnLoop({
|
|
418
|
+
prompt: previewPrompt, mode: "agent", agent: getAgent("preview-agent"),
|
|
419
|
+
model: previewModel.model, providerType: previewModel.providerType,
|
|
420
|
+
sessionId, configState, baseUrl, apiKeyEnv, signal, output, allowQuestion, toolContext
|
|
421
|
+
})
|
|
422
|
+
accumulateUsage(previewOut)
|
|
423
|
+
const previewFindings = previewOut.reply || ""
|
|
424
|
+
|
|
425
|
+
gateStatus.preview = { status: "pass", findingsLength: previewFindings.length }
|
|
426
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_PREVIEW_COMPLETE, sessionId, payload: { findingsLength: previewFindings.length } })
|
|
427
|
+
await syncState({ lastMessage: `H1: preview complete (${previewFindings.length} chars)` })
|
|
428
|
+
|
|
429
|
+
// ========== H2: BLUEPRINT (只读规划 + 结构化 stagePlan) ==========
|
|
430
|
+
await setPhase("H2", "blueprint")
|
|
431
|
+
currentGate = "blueprint"
|
|
432
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_START, sessionId, payload: {} })
|
|
433
|
+
await syncState({ lastMessage: "H2: blueprint agent designing architecture" })
|
|
434
|
+
|
|
435
|
+
const blueprintModel = getModelForStage("blueprint")
|
|
436
|
+
// Task 4: 检测前端任务,注入设计风格提示词
|
|
437
|
+
const isFrontend = detectFrontendTask(prompt)
|
|
438
|
+
const frontendBlock = isFrontend
|
|
439
|
+
? "\n\n" + buildFrontendDesignPrompt(configState.config.agent?.design_style || "")
|
|
440
|
+
: ""
|
|
441
|
+
const blueprintPrompt = buildStageWrapper(LONGAGENT_4STAGE_STAGES.BLUEPRINT, { preview: previewFindings, blueprint: null, coding: null }, prompt)
|
|
442
|
+
+ frontendBlock
|
|
443
|
+
+ [
|
|
444
|
+
"\n\n## HYBRID MODE: STRUCTURED EXECUTION PLAN (REQUIRED)",
|
|
445
|
+
"In addition to your architecture design, you MUST output a machine-parseable stage plan.",
|
|
446
|
+
"",
|
|
447
|
+
"Wrap it in a ```stage_plan_json ... ``` fenced block. Schema:",
|
|
448
|
+
'{"planId":"...","objective":"...","stages":[{"stageId":"...","name":"...","passRule":"all_success","tasks":[{"taskId":"...","prompt":"detailed task prompt for sub-agent","plannedFiles":["file1.mjs","file2.mjs"],"acceptance":["node --check file1.mjs","node --test test/file1.test.mjs"],"timeoutMs":600000,"maxRetries":2,"complexity":"low|medium|high"}]}]}',
|
|
449
|
+
"",
|
|
450
|
+
"Rules for the stage plan:",
|
|
451
|
+
"- Each task prompt must be SELF-CONTAINED: the sub-agent has NO access to your blueprint text",
|
|
452
|
+
"- plannedFiles must list EVERY file the task will create or modify (no file in multiple tasks)",
|
|
453
|
+
"- acceptance must be machine-verifiable commands (not subjective criteria)",
|
|
454
|
+
"- Files that import each other MUST be in the same task",
|
|
455
|
+
"- A module and its test file MUST be in the same task",
|
|
456
|
+
"- Order stages by dependency: shared types → core logic → integration → validation"
|
|
457
|
+
].join("\n")
|
|
458
|
+
const blueprintOut = await processTurnLoop({
|
|
459
|
+
prompt: blueprintPrompt, mode: "agent", agent: getAgent("blueprint-agent"),
|
|
460
|
+
model: blueprintModel.model, providerType: blueprintModel.providerType,
|
|
461
|
+
sessionId, configState, baseUrl, apiKeyEnv, signal, output, allowQuestion, toolContext
|
|
462
|
+
})
|
|
463
|
+
accumulateUsage(blueprintOut)
|
|
464
|
+
|
|
465
|
+
const planDefaults = { timeoutMs: Number(parallelConfig.task_timeout_ms || 600000), maxRetries: Number(parallelConfig.task_max_retries ?? 2) }
|
|
466
|
+
let { architectureText, stagePlan: parsedPlan, parseErrors } = parseBlueprintOutput(blueprintOut.reply || "", prompt, planDefaults)
|
|
467
|
+
|
|
468
|
+
// Blueprint 解析失败重试:用 repair prompt 要求 LLM 只输出合法 JSON
|
|
469
|
+
const maxBlueprintRetries = Number(hybridConfig.blueprint_parse_retries || 1)
|
|
470
|
+
if (parseErrors.length > 0 && maxBlueprintRetries > 0) {
|
|
471
|
+
for (let retryIdx = 0; retryIdx < maxBlueprintRetries; retryIdx++) {
|
|
472
|
+
await EventBus.emit({
|
|
473
|
+
type: EVENT_TYPES.LONGAGENT_ALERT, sessionId,
|
|
474
|
+
payload: { kind: "blueprint_parse_retry", attempt: retryIdx + 1, errors: parseErrors }
|
|
475
|
+
})
|
|
476
|
+
const repairPrompt = [
|
|
477
|
+
"Your previous blueprint output could not be parsed into a valid stage plan.",
|
|
478
|
+
`Parse errors: ${parseErrors.join("; ")}`,
|
|
479
|
+
"",
|
|
480
|
+
"Output ONLY a valid JSON object (no markdown, no explanation) with this schema:",
|
|
481
|
+
'{"planId":"...","objective":"...","stages":[{"stageId":"...","name":"...","tasks":[{"taskId":"...","prompt":"...","plannedFiles":["..."],"acceptance":["..."],"timeoutMs":600000,"maxRetries":2,"complexity":"medium"}]}]}',
|
|
482
|
+
"",
|
|
483
|
+
`Objective: ${prompt}`
|
|
484
|
+
].join("\n")
|
|
485
|
+
const repairOut = await processTurnLoop({
|
|
486
|
+
prompt: repairPrompt, mode: "assistant",
|
|
487
|
+
model: blueprintModel.model, providerType: blueprintModel.providerType,
|
|
488
|
+
sessionId, configState, baseUrl, apiKeyEnv, signal,
|
|
489
|
+
output: { write: () => {} }, allowQuestion: false
|
|
490
|
+
})
|
|
491
|
+
accumulateUsage(repairOut)
|
|
492
|
+
const retry = parseBlueprintOutput(repairOut.reply || "", prompt, planDefaults)
|
|
493
|
+
if (retry.parseErrors.length === 0) {
|
|
494
|
+
architectureText = architectureText || retry.architectureText
|
|
495
|
+
parsedPlan = retry.stagePlan
|
|
496
|
+
parseErrors = []
|
|
497
|
+
break
|
|
498
|
+
}
|
|
499
|
+
parseErrors = retry.parseErrors
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
stagePlan = parsedPlan
|
|
504
|
+
planFrozen = true
|
|
505
|
+
|
|
506
|
+
const blueprintFellBack = parseErrors.length > 0
|
|
507
|
+
gateStatus.blueprint = {
|
|
508
|
+
status: blueprintFellBack ? "warn" : "pass",
|
|
509
|
+
hasArchitecture: architectureText.length > 100,
|
|
510
|
+
stageCount: stagePlan.stages.length,
|
|
511
|
+
parseErrors: blueprintFellBack ? parseErrors : undefined
|
|
512
|
+
}
|
|
513
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_COMPLETE, sessionId, payload: { planId: stagePlan.planId, stageCount: stagePlan.stages.length } })
|
|
514
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_PLAN_FROZEN, sessionId, payload: { planId: stagePlan.planId, stageCount: stagePlan.stages.length, errors: [] } })
|
|
515
|
+
await syncState({ planFrozen: true, lastMessage: `H2: blueprint complete, ${stagePlan.stages.length} stage(s)` })
|
|
516
|
+
|
|
517
|
+
// #9 Blueprint 语义验证
|
|
518
|
+
if (hybridConfig.blueprint_validation !== false && stagePlan.stages.length > 0) {
|
|
519
|
+
const totalTasks = stagePlan.stages.reduce((s, st) => s + (st.tasks?.length || 0), 0)
|
|
520
|
+
const totalFiles = new Set(stagePlan.stages.flatMap(st => (st.tasks || []).flatMap(t => t.plannedFiles || []))).size
|
|
521
|
+
const valid = totalTasks > 0 && totalFiles > 0
|
|
522
|
+
gateStatus.blueprintValidation = { status: valid ? "pass" : "warn", totalTasks, totalFiles }
|
|
523
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_VALIDATED, sessionId, payload: { totalTasks, totalFiles, valid } })
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// #2 人工审查检查点(Task 2: 默认 ON,用户可见的 Blueprint 确认)
|
|
527
|
+
if (hybridConfig.blueprint_review !== false && allowQuestion) {
|
|
528
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_BLUEPRINT_REVIEW, sessionId, payload: { planId: stagePlan.planId } })
|
|
529
|
+
const stageList = stagePlan.stages.map((s, i) => ` ${i + 1}. ${s.name || s.stageId} (${(s.tasks || []).length} 个任务)`).join("\n")
|
|
530
|
+
const reviewOut = await processTurnLoop({
|
|
531
|
+
prompt: [
|
|
532
|
+
`[SYSTEM] H2 Blueprint 已生成,包含 ${stagePlan.stages.length} 个执行阶段:`,
|
|
533
|
+
stageList,
|
|
534
|
+
"",
|
|
535
|
+
"架构摘要:",
|
|
536
|
+
architectureText.slice(0, 1200),
|
|
537
|
+
"",
|
|
538
|
+
"请使用 question 工具询问用户:",
|
|
539
|
+
"1. 以上执行计划是否符合预期?",
|
|
540
|
+
"2. 是否有需要调整的阶段或任务?",
|
|
541
|
+
"3. 确认后将开始执行,输入 [确认]/[继续]/yes 开始,输入 [取消]/abort 中止。",
|
|
542
|
+
"",
|
|
543
|
+
"根据用户回复决定是否继续执行。"
|
|
544
|
+
].join("\n"),
|
|
545
|
+
mode: "assistant", model, providerType, sessionId, configState, baseUrl, apiKeyEnv, agent, signal, allowQuestion: true, toolContext, output
|
|
546
|
+
})
|
|
547
|
+
accumulateUsage(reviewOut)
|
|
548
|
+
const answer = String(reviewOut.reply || "").toLowerCase().trim()
|
|
549
|
+
if (["no", "否", "n", "取消", "abort", "cancel", "中止", "停止"].some(k => answer.includes(k))) {
|
|
550
|
+
await LongAgentManager.update(sessionId, { status: "aborted", lastMessage: "user rejected blueprint" })
|
|
551
|
+
await markSessionStatus(sessionId, "active")
|
|
552
|
+
return { sessionId, turnId: `turn_long_${Date.now()}`, reply: "用户中止了 Blueprint 审查。", usage: aggregateUsage, toolEvents, iterations: iteration, status: "aborted", phase: "H2", gateStatus, currentGate, lastGateFailures: [], recoveryCount: 0, progress: lastProgress, elapsed: Math.round((Date.now() - startTime) / 1000), stageIndex: 0, stageCount: stagePlan.stages.length, planFrozen, taskProgress: {}, fileChanges: [], stageProgress: { done: 0, total: 0 }, remainingFilesCount: 0 }
|
|
553
|
+
}
|
|
554
|
+
gateStatus.blueprintReview = { status: "pass", userConfirmed: true }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ========== H2.5: GIT BRANCH (可选) ==========
|
|
558
|
+
const gitEnabled = gitConfig.enabled === true || gitConfig.enabled === "ask"
|
|
559
|
+
const gitAsk = gitConfig.enabled === "ask"
|
|
560
|
+
const inGitRepo = gitEnabled && await git.isGitRepo(cwd)
|
|
561
|
+
|
|
562
|
+
if (inGitRepo) {
|
|
563
|
+
await setPhase("H2.5", "git_branch")
|
|
564
|
+
let userWantsGit = !gitAsk
|
|
565
|
+
if (gitAsk && allowQuestion) {
|
|
566
|
+
const askResult = await processTurnLoop({
|
|
567
|
+
prompt: "[SYSTEM] 是否为本次 Hybrid LongAgent 创建独立 Git 分支?回复 yes/是 启用,no/否 跳过。",
|
|
568
|
+
mode: "assistant", model, providerType, sessionId, configState, baseUrl, apiKeyEnv, agent, signal, allowQuestion: true, toolContext
|
|
569
|
+
})
|
|
570
|
+
const answer = String(askResult.reply || "").toLowerCase().trim()
|
|
571
|
+
userWantsGit = ["yes", "是", "y", "ok", "好", "确认"].some(k => answer.includes(k))
|
|
572
|
+
accumulateUsage(askResult)
|
|
573
|
+
}
|
|
574
|
+
if (userWantsGit) {
|
|
575
|
+
gitBaseBranch = await git.currentBranch(cwd)
|
|
576
|
+
// Guard: skip git flow if branch is empty or HEAD detached
|
|
577
|
+
if (!gitBaseBranch || gitBaseBranch === "HEAD") {
|
|
578
|
+
gateStatus.git = { status: "warn", reason: "detached HEAD or no branch" }
|
|
579
|
+
} else {
|
|
580
|
+
const branchName = git.generateBranchName(sessionId, prompt)
|
|
581
|
+
const clean = await git.isClean(cwd)
|
|
582
|
+
let stashed = false
|
|
583
|
+
try {
|
|
584
|
+
if (!clean) {
|
|
585
|
+
const sr = await git.stash("kkcode-auto-stash", cwd)
|
|
586
|
+
stashed = sr.ok
|
|
587
|
+
if (!stashed) {
|
|
588
|
+
// Stash failed — skip branch creation
|
|
589
|
+
gateStatus.git = { status: "warn", reason: "git stash failed" }
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (!stashed && !clean) {
|
|
593
|
+
// stash failed, skip branch creation (already set gateStatus above)
|
|
594
|
+
} else {
|
|
595
|
+
const created = await git.createBranch(branchName, cwd)
|
|
596
|
+
if (created.ok) {
|
|
597
|
+
gitBranch = branchName; gitActive = true
|
|
598
|
+
gateStatus.git = { status: "pass", branch: branchName, baseBranch: gitBaseBranch }
|
|
599
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_GIT_BRANCH_CREATED, sessionId, payload: { branch: branchName, baseBranch: gitBaseBranch } })
|
|
600
|
+
} else {
|
|
601
|
+
gateStatus.git = { status: "warn", reason: created.message }
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
} finally {
|
|
605
|
+
// Always restore stash on any exit path
|
|
606
|
+
if (stashed) await git.stashPop(cwd).catch(() => {})
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ========== H3: SCAFFOLD (脚手架) ==========
|
|
613
|
+
const scaffoldEnabled = longagentConfig.scaffold?.enabled !== false
|
|
614
|
+
if (scaffoldEnabled && stagePlan.stages.length > 0) {
|
|
615
|
+
await setPhase("H3", "scaffolding")
|
|
616
|
+
currentGate = "scaffold"
|
|
617
|
+
await syncState({ lastMessage: "H3: creating stub files" })
|
|
618
|
+
|
|
619
|
+
const scaffoldResult = await runScaffoldPhase({
|
|
620
|
+
objective: `${prompt}\n\n=== BLUEPRINT ARCHITECTURE ===\n${architectureText.slice(0, 4000)}`,
|
|
621
|
+
stagePlan, model, providerType, sessionId, configState,
|
|
622
|
+
baseUrl, apiKeyEnv, agent, signal, toolContext,
|
|
623
|
+
tddMode: hybridConfig.tdd_mode === true
|
|
624
|
+
})
|
|
625
|
+
|
|
626
|
+
gateStatus.scaffold = { status: scaffoldResult.scaffolded ? "pass" : "skip", fileCount: scaffoldResult.fileCount }
|
|
627
|
+
if (scaffoldResult.usage) accumulateUsage(scaffoldResult)
|
|
628
|
+
if (scaffoldResult.files?.length) {
|
|
629
|
+
fileChanges = mergeCappedFileChanges(fileChanges,
|
|
630
|
+
scaffoldResult.files.map(f => ({ path: f, addedLines: 0, removedLines: 0, stageId: "scaffold", taskId: "scaffold" })),
|
|
631
|
+
fileChangesLimit)
|
|
632
|
+
}
|
|
633
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_SCAFFOLD_COMPLETE, sessionId, payload: { fileCount: scaffoldResult.fileCount, files: scaffoldResult.files || [] } })
|
|
634
|
+
await syncState({ lastMessage: `H3: scaffolded ${scaffoldResult.fileCount} file(s)` })
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ========== H4+H5: CODING(并行) + DEBUGGING(回滚) 循环 ==========
|
|
638
|
+
const gatesConfig = longagentConfig.usability_gates || {}
|
|
639
|
+
let priorContext = [
|
|
640
|
+
"### Preview Findings", previewFindings.slice(0, 2000), "",
|
|
641
|
+
"### Blueprint Architecture", architectureText.slice(0, 3000)
|
|
642
|
+
].join("\n")
|
|
643
|
+
const seenFilePaths = new Set() // #3 去重:跨阶段文件路径去重
|
|
644
|
+
|
|
645
|
+
let codingRollbackCount = 0
|
|
646
|
+
const maxCodingRollbacks = Number(hybridConfig.max_coding_rollbacks || 2)
|
|
647
|
+
const maxDebugIterations = Number(hybridConfig.debugging_max_iterations || 20)
|
|
648
|
+
let rerunCoding = true
|
|
649
|
+
|
|
650
|
+
while (rerunCoding && codingRollbackCount <= maxCodingRollbacks) {
|
|
651
|
+
rerunCoding = false
|
|
652
|
+
|
|
653
|
+
// --- H4: CODING (并行 stage 执行) ---
|
|
654
|
+
await setPhase("H4", "coding")
|
|
655
|
+
currentGate = "coding"
|
|
656
|
+
stageIndex = 0
|
|
657
|
+
const codingPhaseStart = Date.now()
|
|
658
|
+
|
|
659
|
+
while (stageIndex < stagePlan.stages.length) {
|
|
660
|
+
if (stopFlag || signal?.aborted) break
|
|
661
|
+
|
|
662
|
+
// Phase 2: 阶段超时检测
|
|
663
|
+
if (Date.now() - codingPhaseStart > codingPhaseTimeoutMs) {
|
|
664
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_PHASE_TIMEOUT, sessionId, payload: { phase: "H4", elapsed: Date.now() - codingPhaseStart } })
|
|
665
|
+
if (degradationChain.canDegrade()) {
|
|
666
|
+
const degCtx = { model, taskProgress, configState, shouldStop: false }
|
|
667
|
+
const deg = degradationChain.apply(degCtx)
|
|
668
|
+
if (degCtx.model !== model) model = degCtx.model
|
|
669
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_DEGRADATION_APPLIED, sessionId, payload: { strategy: deg.strategy, phase: "H4" } })
|
|
670
|
+
if (deg.applied && deg.strategy === "graceful_stop") break
|
|
671
|
+
} else {
|
|
672
|
+
break
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
iteration++
|
|
677
|
+
const stage = stagePlan.stages[stageIndex]
|
|
678
|
+
currentGate = `stage:${stage.stageId}`
|
|
679
|
+
await syncState({ stageStatus: "running", lastMessage: `H4: running ${stage.stageId} (${stageIndex + 1}/${stagePlan.stages.length})` })
|
|
680
|
+
|
|
681
|
+
const seeded = Object.fromEntries(
|
|
682
|
+
stage.tasks.map(t => [t.taskId, taskProgress[t.taskId]]).filter(([, v]) => Boolean(v))
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
// #4 计划锚点 — 每阶段动态构建,不存入 priorContext 避免被压缩掉
|
|
686
|
+
const stageStatuses = stagePlan.stages.map((s, i) => {
|
|
687
|
+
const marker = i < stageIndex ? "✓" : i === stageIndex ? "→" : " "
|
|
688
|
+
return `[${marker}] 阶段${i + 1}: ${s.name || s.stageId}`
|
|
689
|
+
}).join("\n")
|
|
690
|
+
const planAnchor = `## 计划锚点\n目标: ${stagePlan.objective || prompt}\n进度: ${stageIndex + 1}/${stagePlan.stages.length}\n${stageStatuses}\n\n`
|
|
691
|
+
|
|
692
|
+
const stageResult = await runStageBarrier({
|
|
693
|
+
stage, sessionId, config: configState.config, model, providerType,
|
|
694
|
+
seedTaskProgress: seeded, objective: prompt,
|
|
695
|
+
stageIndex, stageCount: stagePlan.stages.length, priorContext: planAnchor + priorContext,
|
|
696
|
+
stuckTracker,
|
|
697
|
+
onTaskComplete: async (taskData) => {
|
|
698
|
+
await saveTaskCheckpoint(sessionId, taskData.stageId, taskData.taskId, taskData)
|
|
699
|
+
},
|
|
700
|
+
taskBus
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
// 合并结果
|
|
704
|
+
for (const [taskId, progress] of Object.entries(stageResult.taskProgress || {})) {
|
|
705
|
+
taskProgress[taskId] = { ...taskProgress[taskId], ...progress }
|
|
706
|
+
if (String(progress.lastReply || "").toLowerCase().includes("[task_complete]")) completionMarkerSeen = true
|
|
707
|
+
// #4 TaskBus: 解析 task 输出中的广播消息
|
|
708
|
+
if (taskBus && progress.lastReply) taskBus.parseTaskOutput(taskId, progress.lastReply)
|
|
709
|
+
// #3 动态重规划: 检测 [REPLAN:...] 标记
|
|
710
|
+
const replan = parseReplanMarker(progress.lastReply)
|
|
711
|
+
if (replan?.stages) {
|
|
712
|
+
const { plan, errors } = validateAndNormalizeStagePlan(replan, { objective: prompt, defaults: planDefaults })
|
|
713
|
+
if (!errors.length) {
|
|
714
|
+
stagePlan = plan
|
|
715
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_REPLAN, sessionId, payload: { newStageCount: plan.stages.length } })
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (stageResult.completionMarkerSeen) completionMarkerSeen = true
|
|
720
|
+
if (stageResult.fileChanges?.length) {
|
|
721
|
+
fileChanges = mergeCappedFileChanges(fileChanges, stageResult.fileChanges, fileChangesLimit)
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
gateStatus[stage.stageId] = {
|
|
725
|
+
status: stageResult.allSuccess ? "pass" : "fail",
|
|
726
|
+
successCount: stageResult.successCount, failCount: stageResult.failCount
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// #1 阶段级压缩 + #3 文件去重 — 结构化摘要,跨阶段去重文件路径
|
|
730
|
+
const taskSummaries = Object.values(stageResult.taskProgress || {})
|
|
731
|
+
.filter(t => t.lastReply)
|
|
732
|
+
.map(t => ` - [${t.taskId}] ${t.status}: ${t.lastReply.slice(0, 250)}`)
|
|
733
|
+
const stageFiles = (stageResult.fileChanges || [])
|
|
734
|
+
.map(f => (typeof f === "string" ? f : (f.path || f.file || "")))
|
|
735
|
+
.filter(Boolean)
|
|
736
|
+
const newFiles = stageFiles.filter(f => !seenFilePaths.has(f))
|
|
737
|
+
newFiles.forEach(f => seenFilePaths.add(f))
|
|
738
|
+
if (taskSummaries.length || newFiles.length) {
|
|
739
|
+
const fileNote = newFiles.length ? `\n 新增/修改文件: ${newFiles.join(", ")}` : ""
|
|
740
|
+
const failNote = !stageResult.allSuccess ? ` 失败任务数: ${stageResult.failCount}` : ""
|
|
741
|
+
priorContext += `\n### 阶段${stageIndex + 1}: ${stage.name || stage.stageId} (${stageResult.allSuccess ? "PASS" : "FAIL"}${failNote})\n${taskSummaries.join("\n")}${fileNote}\n`
|
|
742
|
+
}
|
|
743
|
+
// #4 TaskBus 增量注入到 priorContext(只包含本阶段新消息)
|
|
744
|
+
if (taskBus) {
|
|
745
|
+
const busCtx = taskBus.toDeltaString()
|
|
746
|
+
if (busCtx) priorContext += `\n${busCtx}\n`
|
|
747
|
+
}
|
|
748
|
+
// #13 上下文压缩
|
|
749
|
+
const pressureLimit = Number(hybridConfig.context_pressure_limit || 8000)
|
|
750
|
+
if (priorContext.length > pressureLimit) {
|
|
751
|
+
priorContext = await compressContext(priorContext, pressureLimit, { model, providerType, sessionId, configState, baseUrl, apiKeyEnv, signal, toolContext })
|
|
752
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_CONTEXT_COMPRESSED, sessionId, payload: { newLength: priorContext.length } })
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
lastProgress = {
|
|
756
|
+
percentage: Math.round(((stageIndex + (stageResult.allSuccess ? 1 : 0)) / Math.max(1, stagePlan.stages.length)) * 100),
|
|
757
|
+
currentStep: stageIndex + (stageResult.allSuccess ? 1 : 0),
|
|
758
|
+
totalSteps: stagePlan.stages.length
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Git: 每 stage 自动 commit
|
|
762
|
+
if (gitActive && stageResult.allSuccess && gitConfig.auto_commit_stages !== false) {
|
|
763
|
+
const msg = `[kkcode-hybrid] stage ${stage.stageId} completed (${stageIndex + 1}/${stagePlan.stages.length})`
|
|
764
|
+
await git.commitAll(msg, cwd)
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// #10 增量门控:每个 stage 完成后运行轻量检查
|
|
768
|
+
if (hybridConfig.incremental_gates !== false && stageResult.allSuccess && stageIndex < stagePlan.stages.length - 1) {
|
|
769
|
+
const stageFiles = (stageResult.fileChanges || []).map(f => f.path).filter(Boolean)
|
|
770
|
+
if (stageFiles.length > 0) {
|
|
771
|
+
const miniGate = await runUsabilityGates({
|
|
772
|
+
sessionId, configState, model, providerType, baseUrl, apiKeyEnv, signal, toolContext,
|
|
773
|
+
objective: `Verify stage ${stage.stageId}: ${stage.name || ""}`, fileChanges: stageResult.fileChanges || [],
|
|
774
|
+
gatesConfig: { ...gatesConfig, lint: true, typecheck: true, test: false, security: false, build: false }, allowQuestion: false
|
|
775
|
+
})
|
|
776
|
+
if (miniGate.usage) accumulateUsage(miniGate)
|
|
777
|
+
gateStatus[`gate_${stage.stageId}`] = { status: miniGate.allPassed ? "pass" : "warn" }
|
|
778
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_INCREMENTAL_GATE, sessionId, payload: { stageId: stage.stageId, passed: miniGate.allPassed } })
|
|
779
|
+
// #18: Feed gate results into priorContext so subsequent stages see lint/typecheck feedback
|
|
780
|
+
if (!miniGate.allPassed && miniGate.failures?.length) {
|
|
781
|
+
const gateFeedback = miniGate.failures.slice(0, 3).map(f => `${f.gate}: ${(f.reason || "").slice(0, 150)}`).join("; ")
|
|
782
|
+
priorContext += `\n### Incremental Gate Warning (${stage.stageId})\n${gateFeedback}\n`
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// #14 预算感知:检查 token 消耗是否超限
|
|
788
|
+
// #21: 增加基于历史平均值的预算预测
|
|
789
|
+
if (hybridConfig.budget_awareness !== false) {
|
|
790
|
+
const totalTokens = aggregateUsage.input + aggregateUsage.output
|
|
791
|
+
const budgetLimit = Number(longagentConfig.token_budget || 2000000)
|
|
792
|
+
|
|
793
|
+
// #21: Predict remaining budget based on average per-stage cost
|
|
794
|
+
const completedStages = stageIndex + (stageResult.allSuccess ? 1 : 0)
|
|
795
|
+
const remainingStages = stagePlan.stages.length - completedStages
|
|
796
|
+
if (completedStages > 0 && remainingStages > 0) {
|
|
797
|
+
const avgPerStage = totalTokens / completedStages
|
|
798
|
+
const predicted = totalTokens + avgPerStage * remainingStages
|
|
799
|
+
if (predicted > budgetLimit && totalTokens <= budgetLimit * 0.9) {
|
|
800
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_BUDGET_WARNING, sessionId, payload: { totalTokens, budgetLimit, predicted: Math.round(predicted), percentage: Math.round(totalTokens / budgetLimit * 100), forecast: true } })
|
|
801
|
+
await syncState({ lastMessage: `H4: budget forecast — predicted ${Math.round(predicted / 1000)}k tokens (limit ${Math.round(budgetLimit / 1000)}k)` })
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (totalTokens > budgetLimit * 0.9) {
|
|
806
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_BUDGET_WARNING, sessionId, payload: { totalTokens, budgetLimit, percentage: Math.round(totalTokens / budgetLimit * 100) } })
|
|
807
|
+
await syncState({ lastMessage: `H4: budget warning — ${Math.round(totalTokens / budgetLimit * 100)}% used` })
|
|
808
|
+
}
|
|
809
|
+
if (totalTokens > budgetLimit) {
|
|
810
|
+
// Phase 6: 尝试降级而非直接 break
|
|
811
|
+
if (degradationChain.canDegrade()) {
|
|
812
|
+
const degCtx2 = { model, taskProgress, configState, shouldStop: false }
|
|
813
|
+
const deg = degradationChain.apply(degCtx2)
|
|
814
|
+
if (degCtx2.model !== model) model = degCtx2.model
|
|
815
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_DEGRADATION_APPLIED, sessionId, payload: { strategy: deg.strategy, phase: "H4", reason: "budget_exceeded" } })
|
|
816
|
+
if (deg.applied && deg.strategy === "graceful_stop") {
|
|
817
|
+
await syncState({ status: "budget_exceeded", lastMessage: `H4: budget exceeded, graceful stop` })
|
|
818
|
+
break
|
|
819
|
+
}
|
|
820
|
+
} else {
|
|
821
|
+
await syncState({ status: "budget_exceeded", lastMessage: `H4: budget exceeded (${totalTokens}/${budgetLimit})` })
|
|
822
|
+
break
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (!stageResult.allSuccess) {
|
|
828
|
+
recoveryCount++
|
|
829
|
+
const backoffMs = Math.min(1000 * 2 ** (recoveryCount - 1), 30000)
|
|
830
|
+
await new Promise(r => setTimeout(r, backoffMs))
|
|
831
|
+
const maxStageRecoveries = Number(longagentConfig.max_stage_recoveries ?? 3)
|
|
832
|
+
if (recoveryCount >= maxStageRecoveries) {
|
|
833
|
+
// Phase 6: 尝试降级而非直接 abort
|
|
834
|
+
if (degradationChain.canDegrade()) {
|
|
835
|
+
const degCtx3 = { model, taskProgress, configState, shouldStop: false }
|
|
836
|
+
const deg = degradationChain.apply(degCtx3)
|
|
837
|
+
if (degCtx3.model !== model) model = degCtx3.model
|
|
838
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_DEGRADATION_APPLIED, sessionId, payload: { strategy: deg.strategy, phase: "H4", reason: "max_recoveries" } })
|
|
839
|
+
if (deg.applied && deg.strategy === "graceful_stop") {
|
|
840
|
+
await syncState({ status: "error", lastMessage: `stage ${stage.stageId} aborted after degradation` })
|
|
841
|
+
break
|
|
842
|
+
}
|
|
843
|
+
// 降级成功但非 graceful_stop,重置 recoveryCount 继续
|
|
844
|
+
recoveryCount = 0
|
|
845
|
+
} else {
|
|
846
|
+
await syncState({ status: "error", lastMessage: `stage ${stage.stageId} aborted after ${recoveryCount} recoveries` })
|
|
847
|
+
break
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
// Phase 1: 根据错误类别决定是否重试
|
|
851
|
+
for (const [taskId, tp] of Object.entries(taskProgress)) {
|
|
852
|
+
if (tp.status === "error") {
|
|
853
|
+
const category = classifyError(tp.lastError)
|
|
854
|
+
if (category === ERROR_CATEGORIES.PERMANENT || category === ERROR_CATEGORIES.UNKNOWN) {
|
|
855
|
+
taskProgress[taskId] = { ...tp, status: "error", skipReason: `${category} error` }
|
|
856
|
+
} else {
|
|
857
|
+
taskProgress[taskId] = { ...tp, status: "retrying", attempt: 0 }
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
continue
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
stageIndex++
|
|
865
|
+
recoveryCount = 0 // reset per-stage recovery counter after successful stage
|
|
866
|
+
await saveCheckpoint(sessionId, { name: `hybrid_stage_${stage.stageId}`, iteration, currentPhase, stageIndex, stagePlan, taskProgress, planFrozen, lastProgress })
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// #11 Cross-review + H5 ghost commit 并行化
|
|
870
|
+
// Phase 2 改进: ghost commit 不依赖 cross-review 结果,提前启动并行执行
|
|
871
|
+
const ghostCommitPromise = gitActive
|
|
872
|
+
? git.createGhostCommit(cwd, `[kkcode] pre-debug savepoint session ${sessionId}`).catch(() => null)
|
|
873
|
+
: Promise.resolve(null)
|
|
874
|
+
|
|
875
|
+
if (hybridConfig.cross_review !== false && fileChanges.length > 0) {
|
|
876
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_CROSS_REVIEW, sessionId, payload: { fileCount: fileChanges.length } })
|
|
877
|
+
const reviewFiles = fileChanges.slice(0, 20).map(f => f.path).join(", ")
|
|
878
|
+
const reviewOut = await processTurnLoop({
|
|
879
|
+
prompt: [
|
|
880
|
+
"You are the CROSS-REVIEW agent. Multiple parallel sub-agents just completed their coding tasks independently.",
|
|
881
|
+
"Your job: verify that their outputs are compatible, correct, and integrate properly.",
|
|
882
|
+
"",
|
|
883
|
+
"## Files to review:",
|
|
884
|
+
reviewFiles,
|
|
885
|
+
"",
|
|
886
|
+
"## Review Checklist",
|
|
887
|
+
"1. IMPORT RESOLUTION: Do all cross-file imports resolve? Are exported symbols correct?",
|
|
888
|
+
"2. INTERFACE COMPATIBILITY: Do function signatures match what callers expect?",
|
|
889
|
+
"3. ERROR HANDLING: Are errors properly caught, propagated, or thrown? No silent failures?",
|
|
890
|
+
"4. RESOURCE CLEANUP: Are timers cleared, listeners removed, handles closed in all code paths?",
|
|
891
|
+
"5. EDGE CASES: Null/undefined checks, empty arrays, concurrent access guards?",
|
|
892
|
+
"6. CONSISTENCY: Same naming conventions, error patterns, async style across files?",
|
|
893
|
+
"",
|
|
894
|
+
`## Original Objective: ${prompt}`,
|
|
895
|
+
"",
|
|
896
|
+
"## Output Format",
|
|
897
|
+
"For each issue found, output: [FAILED_TASK: taskId] with a description of the problem.",
|
|
898
|
+
"If no issues found, state that the cross-review passed.",
|
|
899
|
+
"Focus on REAL bugs that would cause runtime failures — not style preferences."
|
|
900
|
+
].join("\n"),
|
|
901
|
+
mode: "agent", agent: getAgent("debugging-agent"),
|
|
902
|
+
model, providerType, sessionId, configState, baseUrl, apiKeyEnv, signal, output, allowQuestion: false, toolContext
|
|
903
|
+
})
|
|
904
|
+
accumulateUsage(reviewOut)
|
|
905
|
+
if (reviewOut.reply) priorContext += `\n### Cross-Review Findings\n${reviewOut.reply.slice(0, 1500)}\n`
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// --- H5: DEBUGGING (回滚检测) ---
|
|
909
|
+
await setPhase("H5", "debugging")
|
|
910
|
+
currentGate = "debugging"
|
|
911
|
+
|
|
912
|
+
// 等待并行启动的 ghost commit 完成
|
|
913
|
+
const gcResult = await ghostCommitPromise
|
|
914
|
+
const debugSavepoint = gcResult?.ok ? (gcResult.ghostCommit?.commitHash || null) : null
|
|
915
|
+
|
|
916
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_DEBUGGING_START, sessionId, payload: { codingRollbackCount, debugSavepoint } })
|
|
917
|
+
await syncState({ lastMessage: "H5: debugging agent verifying implementation" })
|
|
918
|
+
|
|
919
|
+
const debugModel = getModelForStage("debugging")
|
|
920
|
+
const debugPrompt = buildStageWrapper(LONGAGENT_4STAGE_STAGES.DEBUGGING, {
|
|
921
|
+
preview: previewFindings.slice(0, 2000),
|
|
922
|
+
blueprint: architectureText.slice(0, 3000),
|
|
923
|
+
coding: priorContext.slice(0, 4000)
|
|
924
|
+
}, prompt)
|
|
925
|
+
|
|
926
|
+
let debugIter = 0
|
|
927
|
+
let debugDone = false
|
|
928
|
+
let debugRecoveryHint = "" // Phase 2 改进: stuck 恢复提示注入
|
|
929
|
+
const semanticTracker = createSemanticErrorTracker(3)
|
|
930
|
+
const debugPhaseStart = Date.now()
|
|
931
|
+
|
|
932
|
+
while (!debugDone && debugIter < maxDebugIterations) {
|
|
933
|
+
debugIter++
|
|
934
|
+
iteration++
|
|
935
|
+
if (stopFlag || signal?.aborted) break
|
|
936
|
+
|
|
937
|
+
// Phase 2: debugging 阶段超时检测
|
|
938
|
+
if (Date.now() - debugPhaseStart > debuggingPhaseTimeoutMs) {
|
|
939
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_PHASE_TIMEOUT, sessionId, payload: { phase: "H5", elapsed: Date.now() - debugPhaseStart } })
|
|
940
|
+
if (degradationChain.canDegrade()) {
|
|
941
|
+
const degCtx4 = { model, taskProgress, configState, shouldStop: false }
|
|
942
|
+
const deg = degradationChain.apply(degCtx4)
|
|
943
|
+
if (degCtx4.model !== model) model = degCtx4.model
|
|
944
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_DEGRADATION_APPLIED, sessionId, payload: { strategy: deg.strategy, phase: "H5" } })
|
|
945
|
+
if (deg.applied && deg.strategy === "graceful_stop") break
|
|
946
|
+
} else {
|
|
947
|
+
break
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const effectiveDebugPrompt = debugRecoveryHint ? `${debugRecoveryHint}\n\n${debugPrompt}` : debugPrompt
|
|
952
|
+
const debugOut = await processTurnLoop({
|
|
953
|
+
prompt: effectiveDebugPrompt, mode: "agent", agent: getAgent("debugging-agent"),
|
|
954
|
+
model: debugModel.model, providerType: debugModel.providerType,
|
|
955
|
+
sessionId, configState, baseUrl, apiKeyEnv, signal, output, allowQuestion, toolContext
|
|
956
|
+
})
|
|
957
|
+
accumulateUsage(debugOut)
|
|
958
|
+
finalReply = debugOut.reply || ""
|
|
959
|
+
debugRecoveryHint = "" // 每次迭代后清空恢复提示
|
|
960
|
+
|
|
961
|
+
// 防卡死检测
|
|
962
|
+
if (debugOut.toolEvents?.length) {
|
|
963
|
+
const stuckResult = stuckTracker.track(debugOut.toolEvents)
|
|
964
|
+
if (stuckResult.isStuck) {
|
|
965
|
+
stuckTracker.resetReadOnlyCount()
|
|
966
|
+
await EventBus.emit({
|
|
967
|
+
type: EVENT_TYPES.LONGAGENT_ALERT, sessionId,
|
|
968
|
+
payload: { kind: "stuck_warning", stage: "H5:debugging", reason: stuckResult.reason, debugIter }
|
|
969
|
+
})
|
|
970
|
+
await syncState({ lastMessage: `H5: stuck detected (${stuckResult.reason}), iter ${debugIter}` })
|
|
971
|
+
// Phase 2 改进: 注入恢复提示,引导 agent 换策略
|
|
972
|
+
debugRecoveryHint = [
|
|
973
|
+
"## Recovery Hint — Stuck Pattern Detected",
|
|
974
|
+
`Previous iteration was stuck: ${stuckResult.reason}.`,
|
|
975
|
+
"You MUST change your approach. Try one of these strategies:",
|
|
976
|
+
"1. If reading the same files repeatedly — stop reading and start making changes",
|
|
977
|
+
"2. If the same test keeps failing — re-read the error, check a different root cause",
|
|
978
|
+
"3. If edits are not taking effect — verify the file path and check for syntax errors",
|
|
979
|
+
"4. Consider reverting recent changes and trying a fundamentally different fix"
|
|
980
|
+
].join("\n")
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// Phase 5: 语义级错误检测
|
|
985
|
+
const semResult = semanticTracker.track(finalReply)
|
|
986
|
+
if (semResult.isRepeated) {
|
|
987
|
+
await EventBus.emit({
|
|
988
|
+
type: EVENT_TYPES.LONGAGENT_SEMANTIC_ERROR_REPEATED, sessionId,
|
|
989
|
+
payload: { error: semResult.error, count: semResult.count, debugIter }
|
|
990
|
+
})
|
|
991
|
+
await syncState({ lastMessage: `H5: repeated error detected (${semResult.count}x): ${(semResult.error || "").slice(0, 80)}` })
|
|
992
|
+
// Phase 2 改进: 语义重复错误超阈值强制退出,防止无限循环
|
|
993
|
+
const maxSemanticRepeats = 5
|
|
994
|
+
if (semResult.count >= maxSemanticRepeats) {
|
|
995
|
+
debugDone = true
|
|
996
|
+
gateStatus.debugging = { status: "force_exit", reason: "semantic_repeat_limit", error: (semResult.error || "").slice(0, 200), iterations: debugIter }
|
|
997
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_ALERT, sessionId, payload: { kind: "semantic_force_exit", count: semResult.count, error: semResult.error, debugIter } })
|
|
998
|
+
await syncState({ lastMessage: `H5: force exit — same error repeated ${semResult.count} times` })
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
if (detectStageComplete(finalReply, LONGAGENT_4STAGE_STAGES.DEBUGGING)) {
|
|
1003
|
+
debugDone = true
|
|
1004
|
+
gateStatus.debugging = { status: "pass", iterations: debugIter }
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (detectReturnToCoding(finalReply)) {
|
|
1008
|
+
codingRollbackCount++
|
|
1009
|
+
rerunCoding = true
|
|
1010
|
+
// #1 细粒度回滚:优先只重置被标记的失败 task
|
|
1011
|
+
const failedIds = extractFailedTaskIds(finalReply)
|
|
1012
|
+
if (failedIds.length > 0) {
|
|
1013
|
+
for (const fid of failedIds) {
|
|
1014
|
+
if (taskProgress[fid]) taskProgress[fid] = { ...taskProgress[fid], status: "retrying", attempt: 0 }
|
|
1015
|
+
}
|
|
1016
|
+
} else {
|
|
1017
|
+
// 回退:重置所有 error 状态的 task
|
|
1018
|
+
for (const [taskId, tp] of Object.entries(taskProgress)) {
|
|
1019
|
+
if (tp.status === "error") taskProgress[taskId] = { ...tp, status: "retrying", attempt: 0 }
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
gateStatus.debugging = { status: "rollback", iterations: debugIter, rollbackCount: codingRollbackCount, failedTaskIds: failedIds }
|
|
1023
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_RETURN_TO_CODING, sessionId, payload: { rollbackCount: codingRollbackCount, failedTaskIds: failedIds } })
|
|
1024
|
+
break
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (/\[TASK_COMPLETE\]/i.test(finalReply)) { completionMarkerSeen = true; debugDone = true }
|
|
1028
|
+
await syncState({ lastMessage: `H5: debugging iteration ${debugIter}/${maxDebugIterations}` })
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (!debugDone && !rerunCoding) {
|
|
1032
|
+
gateStatus.debugging = { status: "timeout", iterations: debugIter }
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_DEBUGGING_COMPLETE, sessionId, payload: { debugIter, rollback: rerunCoding } })
|
|
1036
|
+
await syncState({ lastMessage: rerunCoding ? `H5: rollback to coding (attempt ${codingRollbackCount})` : `H5: debugging complete` })
|
|
1037
|
+
} // end while(rerunCoding)
|
|
1038
|
+
|
|
1039
|
+
// ========== H5.5: COMPLETION VALIDATION ==========
|
|
1040
|
+
if (hybridConfig.completion_validation !== false) {
|
|
1041
|
+
await setPhase("H5.5", "completion_validation")
|
|
1042
|
+
await syncState({ lastMessage: "H5.5: validating completion" })
|
|
1043
|
+
|
|
1044
|
+
const cwd = process.cwd()
|
|
1045
|
+
try {
|
|
1046
|
+
const validator = await createValidator({ cwd, configState })
|
|
1047
|
+
const report = await validator.validate({ todoState: toolContext?._todoState, level: "standard" })
|
|
1048
|
+
gateStatus.completionValidation = {
|
|
1049
|
+
status: report.verdict === "BLOCK" ? "fail" : "pass",
|
|
1050
|
+
verdict: report.verdict,
|
|
1051
|
+
failedChecks: report.results?.filter(r => !r.passed).length || 0
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (report.verdict === "BLOCK" && !completionMarkerSeen) {
|
|
1055
|
+
const fixPrompt = [
|
|
1056
|
+
"## Completion Validation Failed — Fix Required",
|
|
1057
|
+
"",
|
|
1058
|
+
`Original objective: ${prompt}`,
|
|
1059
|
+
"",
|
|
1060
|
+
"## Validation Issues Found:",
|
|
1061
|
+
report.message,
|
|
1062
|
+
"",
|
|
1063
|
+
"## Fix Instructions",
|
|
1064
|
+
"1. Read each failing check and identify the root cause",
|
|
1065
|
+
"2. Fix the issue in the source code (not by suppressing the check)",
|
|
1066
|
+
"3. Re-run the relevant verification command to confirm the fix",
|
|
1067
|
+
"4. If a fix requires changes to multiple files, ensure cross-file consistency",
|
|
1068
|
+
"",
|
|
1069
|
+
"When ALL issues are resolved and verified, include [TASK_COMPLETE] in your response."
|
|
1070
|
+
].join("\n")
|
|
1071
|
+
const fixOut = await processTurnLoop({
|
|
1072
|
+
prompt: fixPrompt, mode: "agent", agent: getAgent("coding-agent"),
|
|
1073
|
+
model, providerType, sessionId, configState,
|
|
1074
|
+
baseUrl, apiKeyEnv, signal, output, allowQuestion, toolContext
|
|
1075
|
+
})
|
|
1076
|
+
accumulateUsage(fixOut)
|
|
1077
|
+
iteration++
|
|
1078
|
+
if (/\[TASK_COMPLETE\]/i.test(fixOut.reply || "")) completionMarkerSeen = true
|
|
1079
|
+
finalReply = fixOut.reply || finalReply
|
|
1080
|
+
}
|
|
1081
|
+
} catch (valErr) {
|
|
1082
|
+
gateStatus.completionValidation = { status: "warn", reason: `skipped: ${valErr.message}` }
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// ========== H6: USABILITY GATES ==========
|
|
1087
|
+
await setPhase("H6", "gates")
|
|
1088
|
+
currentGate = "gates"
|
|
1089
|
+
await syncState({ lastMessage: "H6: running usability gates" })
|
|
1090
|
+
|
|
1091
|
+
// Gate 偏好提示(首次运行时询问用户)
|
|
1092
|
+
const shouldPromptGates = gatesConfig.prompt_user === "first_run" || gatesConfig.prompt_user === "always"
|
|
1093
|
+
if (shouldPromptGates && allowQuestion) {
|
|
1094
|
+
const hasPrefs = await hasGatePreferences()
|
|
1095
|
+
if (!hasPrefs || gatesConfig.prompt_user === "always") {
|
|
1096
|
+
const gateAssistantResult = await processTurnLoop({
|
|
1097
|
+
prompt: buildGatePromptText(),
|
|
1098
|
+
mode: "assistant", model, providerType, sessionId, configState,
|
|
1099
|
+
baseUrl, apiKeyEnv, agent, signal, allowQuestion: true, toolContext
|
|
1100
|
+
})
|
|
1101
|
+
accumulateUsage(gateAssistantResult)
|
|
1102
|
+
const gatePrefs = parseGateSelection(gateAssistantResult.reply)
|
|
1103
|
+
await saveGatePreferences(gatePrefs)
|
|
1104
|
+
for (const [gate, enabled] of Object.entries(gatePrefs)) {
|
|
1105
|
+
if (configState.config.agent.longagent.usability_gates[gate]) {
|
|
1106
|
+
configState.config.agent.longagent.usability_gates[gate].enabled = enabled
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
} else {
|
|
1110
|
+
const savedPrefs = await getGatePreferences()
|
|
1111
|
+
if (savedPrefs) {
|
|
1112
|
+
for (const [gate, enabled] of Object.entries(savedPrefs)) {
|
|
1113
|
+
if (configState.config.agent.longagent.usability_gates[gate]) {
|
|
1114
|
+
configState.config.agent.longagent.usability_gates[gate].enabled = enabled
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
let gateAttempt = 0
|
|
1122
|
+
|
|
1123
|
+
while (gateAttempt < maxGateAttempts) {
|
|
1124
|
+
gateAttempt++
|
|
1125
|
+
if (stopFlag || signal?.aborted) break
|
|
1126
|
+
|
|
1127
|
+
const gateResult = await runUsabilityGates({
|
|
1128
|
+
sessionId, configState, model, providerType,
|
|
1129
|
+
baseUrl, apiKeyEnv, signal, toolContext,
|
|
1130
|
+
objective: prompt, fileChanges,
|
|
1131
|
+
gatesConfig, allowQuestion
|
|
1132
|
+
})
|
|
1133
|
+
if (gateResult.usage) accumulateUsage(gateResult)
|
|
1134
|
+
|
|
1135
|
+
if (gateResult.allPassed) {
|
|
1136
|
+
gateStatus.usabilityGates = { status: "pass", attempt: gateAttempt }
|
|
1137
|
+
break
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
lastGateFailures = gateResult.failures || []
|
|
1141
|
+
gateStatus.usabilityGates = { status: "fixing", attempt: gateAttempt, failures: summarizeGateFailures(lastGateFailures) }
|
|
1142
|
+
await syncState({ lastMessage: `H6: gate failures (attempt ${gateAttempt}/${maxGateAttempts}), fixing...` })
|
|
1143
|
+
|
|
1144
|
+
// 修复循环:根据 gate 类型选择修复策略 (Phase 8)
|
|
1145
|
+
const strategy = getGateFixStrategy(lastGateFailures)
|
|
1146
|
+
|
|
1147
|
+
// lint 失败时先尝试自动修复
|
|
1148
|
+
if (strategy.autoFix) {
|
|
1149
|
+
try {
|
|
1150
|
+
const { execSync } = await import("node:child_process")
|
|
1151
|
+
execSync(strategy.autoFix, { cwd: process.cwd(), timeout: 30000, stdio: "ignore" })
|
|
1152
|
+
} catch { /* autofix failed, fall through to agent */ }
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const gateFailureSummary = summarizeGateFailures(lastGateFailures)
|
|
1156
|
+
const fixPrompt = [
|
|
1157
|
+
`## Quality Gate Failures — Attempt ${gateAttempt}/${maxGateAttempts}`,
|
|
1158
|
+
"",
|
|
1159
|
+
`${strategy.prefix || "Fix the following quality gate failures:"}`,
|
|
1160
|
+
"",
|
|
1161
|
+
gateFailureSummary,
|
|
1162
|
+
"",
|
|
1163
|
+
"## Fix Protocol",
|
|
1164
|
+
"1. Read the error output carefully — identify the ROOT CAUSE, not just the symptom",
|
|
1165
|
+
"2. Fix the source code (do NOT disable or skip the gate check)",
|
|
1166
|
+
"3. Re-run the failing command to verify the fix works",
|
|
1167
|
+
"4. If the fix touches shared code, verify no regressions in other modules",
|
|
1168
|
+
"",
|
|
1169
|
+
`Original objective: ${prompt}`
|
|
1170
|
+
].join("\n")
|
|
1171
|
+
const fixOut = await processTurnLoop({
|
|
1172
|
+
prompt: fixPrompt, mode: "agent", agent: getAgent(strategy.agent || "coding-agent"),
|
|
1173
|
+
model, providerType, sessionId, configState,
|
|
1174
|
+
baseUrl, apiKeyEnv, signal, output, allowQuestion, toolContext
|
|
1175
|
+
})
|
|
1176
|
+
accumulateUsage(fixOut)
|
|
1177
|
+
iteration++
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
if (gateAttempt >= maxGateAttempts && lastGateFailures.length) {
|
|
1181
|
+
gateStatus.usabilityGates = { status: "fail", attempt: gateAttempt, failures: summarizeGateFailures(lastGateFailures) }
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// ========== H7: GIT MERGE (原子性保护) ==========
|
|
1185
|
+
if (gitActive && gitBaseBranch && gitBranch) {
|
|
1186
|
+
await setPhase("H7", "git_merge")
|
|
1187
|
+
try {
|
|
1188
|
+
if (gitConfig.auto_merge !== false) {
|
|
1189
|
+
await LongAgentManager.withLock(async () => {
|
|
1190
|
+
const doneState = await LongAgentManager.get(sessionId)
|
|
1191
|
+
if (doneState?.status === "failed") return
|
|
1192
|
+
|
|
1193
|
+
// Step 1: 提交 feature branch 上的最终变更(锁内执行,防止并发)
|
|
1194
|
+
const finalCommit = await git.commitAll(`[kkcode-hybrid] session ${sessionId} completed`, cwd)
|
|
1195
|
+
if (!finalCommit.ok && !finalCommit.empty) {
|
|
1196
|
+
gateStatus.gitMerge = { status: "warn", reason: `final commit failed: ${finalCommit.message}` }
|
|
1197
|
+
return
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// Step 2: 保存 savepoint — 记录 feature branch HEAD 用于回滚
|
|
1201
|
+
const featureHead = await git.getHeadHash(cwd)
|
|
1202
|
+
|
|
1203
|
+
// Step 3: checkout base branch
|
|
1204
|
+
const checkoutResult = await git.checkoutBranch(gitBaseBranch, cwd)
|
|
1205
|
+
if (!checkoutResult.ok) {
|
|
1206
|
+
await git.checkoutBranch(gitBranch, cwd).catch(() => {})
|
|
1207
|
+
gateStatus.gitMerge = { status: "warn", reason: `checkout base failed: ${checkoutResult.message}` }
|
|
1208
|
+
return
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Step 4: 保存 base branch HEAD(merge 失败时回滚到此)
|
|
1212
|
+
const baseHead = await git.getHeadHash(cwd)
|
|
1213
|
+
|
|
1214
|
+
// Step 5: 执行 merge
|
|
1215
|
+
const mergeResult = await git.mergeBranch(gitBranch, cwd)
|
|
1216
|
+
if (mergeResult.ok) {
|
|
1217
|
+
// Step 6: post-merge 验证 — 确认 HEAD 包含 feature 分支的变更
|
|
1218
|
+
const mergedHead = await git.getHeadHash(cwd)
|
|
1219
|
+
if (!mergedHead || mergedHead === baseHead) {
|
|
1220
|
+
// merge 声称成功但 HEAD 未变化,回滚
|
|
1221
|
+
if (baseHead) await git.resetTo(baseHead, cwd).catch(() => {})
|
|
1222
|
+
await git.checkoutBranch(gitBranch, cwd).catch(() => {})
|
|
1223
|
+
gateStatus.gitMerge = { status: "warn", reason: "merge reported success but HEAD unchanged" }
|
|
1224
|
+
return
|
|
1225
|
+
}
|
|
1226
|
+
await git.deleteBranch(gitBranch, cwd)
|
|
1227
|
+
gateStatus.gitMerge = { status: "pass", branch: gitBranch, baseBranch: gitBaseBranch }
|
|
1228
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_GIT_MERGED, sessionId, payload: { branch: gitBranch, baseBranch: gitBaseBranch } })
|
|
1229
|
+
return
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Step 7: merge 失败 — 检查是否为冲突
|
|
1233
|
+
const conflictFiles = await git.getConflictFiles(cwd)
|
|
1234
|
+
if (conflictFiles.length === 0) {
|
|
1235
|
+
// 非冲突类 merge 失败 — 回滚到 base HEAD,回到 feature branch
|
|
1236
|
+
if (baseHead) await git.resetTo(baseHead, cwd).catch(() => {})
|
|
1237
|
+
await git.checkoutBranch(gitBranch, cwd).catch(() => {})
|
|
1238
|
+
gateStatus.gitMerge = { status: "warn", reason: `merge failed: ${mergeResult.message}` }
|
|
1239
|
+
return
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Step 8: 冲突自愈 — 原子化处理
|
|
1243
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_GIT_CONFLICT_RESOLUTION, sessionId, payload: { files: conflictFiles } })
|
|
1244
|
+
try {
|
|
1245
|
+
const conflictPrompt = buildConflictResolutionPrompt(conflictFiles)
|
|
1246
|
+
const conflictOut = await processTurnLoop({
|
|
1247
|
+
prompt: conflictPrompt, mode: "agent", agent: getAgent("coding-agent"),
|
|
1248
|
+
model, providerType, sessionId, configState,
|
|
1249
|
+
baseUrl, apiKeyEnv, signal, output, allowQuestion: false, toolContext
|
|
1250
|
+
})
|
|
1251
|
+
accumulateUsage(conflictOut)
|
|
1252
|
+
|
|
1253
|
+
// Step 9: 验证冲突标记已全部清除
|
|
1254
|
+
const markersRemain = await git.hasConflictMarkers(cwd)
|
|
1255
|
+
if (markersRemain) {
|
|
1256
|
+
throw new Error("conflict markers still present after resolution")
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const commitResult = await git.commitAll(`[kkcode-hybrid] resolved merge conflicts`, cwd)
|
|
1260
|
+
if (commitResult.ok) {
|
|
1261
|
+
await git.deleteBranch(gitBranch, cwd)
|
|
1262
|
+
gateStatus.gitMerge = { status: "pass", branch: gitBranch, baseBranch: gitBaseBranch, conflictsResolved: true }
|
|
1263
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_GIT_MERGED, sessionId, payload: { branch: gitBranch, baseBranch: gitBaseBranch } })
|
|
1264
|
+
return
|
|
1265
|
+
}
|
|
1266
|
+
throw new Error(`commit after conflict resolution failed: ${commitResult.message}`)
|
|
1267
|
+
} catch (resolveErr) {
|
|
1268
|
+
// Step 10: 冲突解决失败 — 精确回滚
|
|
1269
|
+
await git.mergeAbort(cwd).catch(() => {})
|
|
1270
|
+
if (baseHead) await git.resetTo(baseHead, cwd).catch(() => {})
|
|
1271
|
+
await git.checkoutBranch(gitBranch, cwd).catch(() => {})
|
|
1272
|
+
if (featureHead) await git.resetTo(featureHead, cwd).catch(() => {})
|
|
1273
|
+
gateStatus.gitMerge = { status: "warn", reason: `conflict resolution failed: ${resolveErr.message}` }
|
|
1274
|
+
}
|
|
1275
|
+
}, cwd)
|
|
1276
|
+
}
|
|
1277
|
+
} catch (h7Err) {
|
|
1278
|
+
// 记录错误而非静默吞掉
|
|
1279
|
+
gateStatus.gitMerge = gateStatus.gitMerge || { status: "warn", reason: `H7 error: ${h7Err.message}` }
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// #5 保存 project memory
|
|
1284
|
+
if (hybridConfig.project_memory !== false && previewFindings) {
|
|
1285
|
+
try {
|
|
1286
|
+
const newMemory = parseMemoryFromPreview(previewFindings)
|
|
1287
|
+
if (newMemory.techStack.length) {
|
|
1288
|
+
const merged = { ...projectMemory, techStack: [...new Set([...(projectMemory?.techStack || []), ...newMemory.techStack])].slice(0, 20), patterns: [...new Set([...(projectMemory?.patterns || []), ...newMemory.patterns])].slice(0, 20), conventions: projectMemory?.conventions || [] }
|
|
1289
|
+
await saveProjectMemory(cwd, merged)
|
|
1290
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_HYBRID_MEMORY_SAVED, sessionId, payload: { techStackCount: merged.techStack.length } })
|
|
1291
|
+
}
|
|
1292
|
+
} catch { /* ignore memory save errors */ }
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Phase 10: Checkpoint 清理
|
|
1296
|
+
if (hybridConfig.checkpoint_cleanup !== false) {
|
|
1297
|
+
try {
|
|
1298
|
+
const cleanResult = await cleanupCheckpoints(sessionId, {
|
|
1299
|
+
maxKeep: Number(hybridConfig.checkpoint_max_keep || 10),
|
|
1300
|
+
keepStageCheckpoints: true
|
|
1301
|
+
})
|
|
1302
|
+
if (cleanResult.removed > 0) {
|
|
1303
|
+
await EventBus.emit({ type: EVENT_TYPES.LONGAGENT_CHECKPOINT_CLEANED, sessionId, payload: { removed: cleanResult.removed } })
|
|
1304
|
+
}
|
|
1305
|
+
} catch { /* ignore cleanup errors */ }
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// ========== 完成 ==========
|
|
1309
|
+
unsubscribeStop()
|
|
1310
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000)
|
|
1311
|
+
const finalStatus = completionMarkerSeen ? "completed" : "done"
|
|
1312
|
+
await LongAgentManager.update(sessionId, { status: finalStatus, lastMessage: "hybrid longagent complete", elapsed })
|
|
1313
|
+
await markSessionStatus(sessionId, finalStatus === "completed" ? "completed" : "active")
|
|
1314
|
+
|
|
1315
|
+
const stats = stageProgressStats(taskProgress)
|
|
1316
|
+
|
|
1317
|
+
// Phase 11: 恢复建议生成
|
|
1318
|
+
let recoverySuggestions = null
|
|
1319
|
+
if (finalStatus !== "completed") {
|
|
1320
|
+
recoverySuggestions = generateRecoverySuggestions({
|
|
1321
|
+
status: finalStatus,
|
|
1322
|
+
taskProgress,
|
|
1323
|
+
gateStatus,
|
|
1324
|
+
phase: currentPhase,
|
|
1325
|
+
recoveryCount,
|
|
1326
|
+
fileChanges
|
|
1327
|
+
})
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
return {
|
|
1331
|
+
sessionId, turnId: `turn_long_${Date.now()}`,
|
|
1332
|
+
reply: finalReply || "hybrid longagent complete",
|
|
1333
|
+
usage: aggregateUsage, toolEvents, iterations: iteration,
|
|
1334
|
+
status: finalStatus, phase: currentPhase,
|
|
1335
|
+
gateStatus, currentGate, lastGateFailures, recoveryCount,
|
|
1336
|
+
progress: lastProgress, elapsed,
|
|
1337
|
+
stageIndex, stageCount: stagePlan?.stages?.length || 0,
|
|
1338
|
+
planFrozen, taskProgress, fileChanges,
|
|
1339
|
+
stageProgress: { done: stats.done, total: stats.total },
|
|
1340
|
+
remainingFilesCount: stats.remainingFilesCount,
|
|
1341
|
+
gitBranch, gitBaseBranch,
|
|
1342
|
+
recoverySuggestions
|
|
1343
|
+
}
|
|
1344
|
+
}
|