@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,884 +1,911 @@
|
|
|
1
|
-
import { LongAgentManager } from "../orchestration/longagent-manager.mjs"
|
|
2
|
-
import { processTurnLoop } from "./loop.mjs"
|
|
3
|
-
import { markSessionStatus } from "./store.mjs"
|
|
4
|
-
import { EventBus } from "../core/events.mjs"
|
|
5
|
-
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
6
|
-
import { run4StageLongAgent } from "./longagent-4stage.mjs"
|
|
7
|
-
import { runHybridLongAgent } from "./longagent-hybrid.mjs"
|
|
8
|
-
import {
|
|
9
|
-
isComplete,
|
|
10
|
-
isLikelyActionableObjective,
|
|
11
|
-
mergeCappedFileChanges,
|
|
12
|
-
stageProgressStats,
|
|
13
|
-
summarizeGateFailures,
|
|
14
|
-
LONGAGENT_FILE_CHANGES_LIMIT,
|
|
15
|
-
createStuckTracker
|
|
16
|
-
} from "./longagent-utils.mjs"
|
|
17
|
-
import { saveCheckpoint, loadCheckpoint, cleanupCheckpoints } from "./checkpoint.mjs"
|
|
18
|
-
import {
|
|
19
|
-
runUsabilityGates,
|
|
20
|
-
hasGatePreferences,
|
|
21
|
-
getGatePreferences,
|
|
22
|
-
saveGatePreferences,
|
|
23
|
-
buildGatePromptText,
|
|
24
|
-
parseGateSelection
|
|
25
|
-
} from "./usability-gates.mjs"
|
|
26
|
-
import { runIntakeDialogue, buildStagePlan } from "./longagent-plan.mjs"
|
|
27
|
-
import { runStageBarrier } from "../orchestration/stage-scheduler.mjs"
|
|
28
|
-
import { runScaffoldPhase } from "./longagent-scaffold.mjs"
|
|
29
|
-
import { createValidator } from "./task-validator.mjs"
|
|
30
|
-
import * as git from "../util/git.mjs"
|
|
31
|
-
|
|
32
|
-
async function runParallelLongAgent({
|
|
33
|
-
prompt,
|
|
34
|
-
model,
|
|
35
|
-
providerType,
|
|
36
|
-
sessionId,
|
|
37
|
-
configState,
|
|
38
|
-
baseUrl = null,
|
|
39
|
-
apiKeyEnv = null,
|
|
40
|
-
agent = null,
|
|
41
|
-
maxIterations: maxIterationsParam = 0,
|
|
42
|
-
signal = null,
|
|
43
|
-
output = null,
|
|
44
|
-
allowQuestion = true,
|
|
45
|
-
toolContext = {}
|
|
46
|
-
}) {
|
|
47
|
-
const longagentConfig = configState.config.agent.longagent || {}
|
|
48
|
-
const maxIterations = Number(longagentConfig.max_iterations || maxIterationsParam)
|
|
49
|
-
const plannerConfig = longagentConfig.planner || {}
|
|
50
|
-
const intakeConfig = plannerConfig.intake_questions || {}
|
|
51
|
-
const parallelConfig = longagentConfig.parallel || {}
|
|
52
|
-
const noProgressLimit = Number(longagentConfig.no_progress_limit || 5)
|
|
53
|
-
const checkpointInterval = Number(longagentConfig.checkpoint_interval || 5)
|
|
54
|
-
const maxGateAttempts = Number(longagentConfig.max_gate_attempts || 5)
|
|
55
|
-
|
|
56
|
-
const gitConfig = longagentConfig.git || {}
|
|
57
|
-
const gitEnabled = gitConfig.enabled === true || gitConfig.enabled === "ask"
|
|
58
|
-
const gitAsk = gitConfig.enabled === "ask"
|
|
59
|
-
|
|
60
|
-
let iteration = 0
|
|
61
|
-
let recoveryCount = 0
|
|
62
|
-
let currentPhase = "L0"
|
|
63
|
-
let currentGate = "intake"
|
|
64
|
-
let gateStatus = {}
|
|
65
|
-
let lastGateFailures = []
|
|
66
|
-
let lastProgress = { percentage: 0, currentStep: 0, totalSteps: 0 }
|
|
67
|
-
let finalReply = ""
|
|
68
|
-
let stageIndex = 0
|
|
69
|
-
let planFrozen = false
|
|
70
|
-
let stagePlan = null
|
|
71
|
-
let taskProgress = {}
|
|
72
|
-
let fileChanges = []
|
|
73
|
-
const fileChangesLimit = Math.max(20, Number(longagentConfig.file_changes_limit || LONGAGENT_FILE_CHANGES_LIMIT))
|
|
74
|
-
const aggregateUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
75
|
-
const toolEvents = []
|
|
76
|
-
const startTime = Date.now()
|
|
77
|
-
let completionMarkerSeen = false
|
|
78
|
-
let gitBranch = null
|
|
79
|
-
let gitBaseBranch = null
|
|
80
|
-
let gitActive = false
|
|
81
|
-
|
|
82
|
-
async function setPhase(nextPhase, reason = "") {
|
|
83
|
-
if (currentPhase === nextPhase) return
|
|
84
|
-
const prevPhase = currentPhase
|
|
85
|
-
currentPhase = nextPhase
|
|
86
|
-
await EventBus.emit({
|
|
87
|
-
type: EVENT_TYPES.LONGAGENT_PHASE_CHANGED,
|
|
88
|
-
sessionId,
|
|
89
|
-
payload: { prevPhase, nextPhase, reason, iteration }
|
|
90
|
-
})
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function syncState(patch = {}) {
|
|
94
|
-
const stats = stageProgressStats(taskProgress)
|
|
95
|
-
const stageCount = stagePlan?.stages?.length || 0
|
|
96
|
-
const currentStage = stagePlan?.stages?.[stageIndex] || null
|
|
97
|
-
await LongAgentManager.update(sessionId, {
|
|
98
|
-
status: patch.status || "running",
|
|
99
|
-
phase: currentPhase,
|
|
100
|
-
gateStatus,
|
|
101
|
-
currentGate,
|
|
102
|
-
recoveryCount,
|
|
103
|
-
lastGateFailures,
|
|
104
|
-
iterations: iteration,
|
|
105
|
-
heartbeatAt: Date.now(),
|
|
106
|
-
noProgressCount: 0,
|
|
107
|
-
progress: lastProgress,
|
|
108
|
-
planFrozen,
|
|
109
|
-
currentStageId: currentStage?.stageId || null,
|
|
110
|
-
stageIndex,
|
|
111
|
-
stageCount,
|
|
112
|
-
stageStatus: patch.stageStatus || null,
|
|
113
|
-
taskProgress,
|
|
114
|
-
remainingFiles: stats.remainingFiles,
|
|
115
|
-
remainingFilesCount: stats.remainingFilesCount,
|
|
116
|
-
stageProgress: {
|
|
117
|
-
done: stats.done,
|
|
118
|
-
total: stats.total
|
|
119
|
-
},
|
|
120
|
-
...patch
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
await markSessionStatus(sessionId, "running-longagent")
|
|
125
|
-
await syncState({
|
|
126
|
-
status: "running",
|
|
127
|
-
lastMessage: "longagent parallel mode started",
|
|
128
|
-
stopRequested: false
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
if (!isLikelyActionableObjective(prompt)) {
|
|
132
|
-
const blocked = "LongAgent 需要明确的编码目标。请直接描述要实现/修复的内容、涉及文件或验收标准。"
|
|
133
|
-
await LongAgentManager.update(sessionId, {
|
|
134
|
-
status: "blocked",
|
|
135
|
-
phase: "L0",
|
|
136
|
-
currentGate: "intake",
|
|
137
|
-
gateStatus: {
|
|
138
|
-
intake: {
|
|
139
|
-
status: "blocked",
|
|
140
|
-
reason: "objective_not_actionable"
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
lastMessage: blocked
|
|
144
|
-
})
|
|
145
|
-
await markSessionStatus(sessionId, "active")
|
|
146
|
-
return {
|
|
147
|
-
sessionId,
|
|
148
|
-
turnId: `turn_long_${Date.now()}`,
|
|
149
|
-
reply: blocked,
|
|
150
|
-
usage: aggregateUsage,
|
|
151
|
-
toolEvents,
|
|
152
|
-
iterations: 0,
|
|
153
|
-
emittedText: false,
|
|
154
|
-
context: null,
|
|
155
|
-
status: "blocked",
|
|
156
|
-
phase: "L0",
|
|
157
|
-
gateStatus: { intake: { status: "blocked", reason: "objective_not_actionable" } },
|
|
158
|
-
currentGate: "intake",
|
|
159
|
-
lastGateFailures: [],
|
|
160
|
-
recoveryCount: 0,
|
|
161
|
-
progress: { percentage: 0, currentStep: 0, totalSteps: 0 },
|
|
162
|
-
elapsed: 0,
|
|
163
|
-
stageIndex: 0,
|
|
164
|
-
stageCount: 0,
|
|
165
|
-
currentStageId: null,
|
|
166
|
-
planFrozen: false,
|
|
167
|
-
taskProgress: {},
|
|
168
|
-
stageProgress: { done: 0, total: 0, remainingFiles: [], remainingFilesCount: 0 },
|
|
169
|
-
fileChanges: [],
|
|
170
|
-
remainingFilesCount: 0
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
await EventBus.emit({
|
|
175
|
-
type: EVENT_TYPES.LONGAGENT_INTAKE_STARTED,
|
|
176
|
-
sessionId,
|
|
177
|
-
payload: { objective: prompt }
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
const intakeEnabled = intakeConfig.enabled !== false
|
|
181
|
-
let intakeSummary = prompt
|
|
182
|
-
if (intakeEnabled) {
|
|
183
|
-
await setPhase("L0", "intake")
|
|
184
|
-
const intake = await runIntakeDialogue({
|
|
185
|
-
objective: prompt,
|
|
186
|
-
model,
|
|
187
|
-
providerType,
|
|
188
|
-
sessionId,
|
|
189
|
-
configState,
|
|
190
|
-
baseUrl,
|
|
191
|
-
apiKeyEnv,
|
|
192
|
-
agent,
|
|
193
|
-
signal,
|
|
194
|
-
maxRounds: Number(intakeConfig.max_rounds || 6)
|
|
195
|
-
})
|
|
196
|
-
intakeSummary = intake.summary || prompt
|
|
197
|
-
gateStatus.intake = {
|
|
198
|
-
status: "pass",
|
|
199
|
-
rounds: intake.transcript.length,
|
|
200
|
-
summary: intakeSummary.slice(0, 500)
|
|
201
|
-
}
|
|
202
|
-
await syncState({
|
|
203
|
-
lastMessage: `intake completed (${intake.transcript.length} qa pairs)`
|
|
204
|
-
})
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// --- Git branch creation (after intake, before planning) ---
|
|
208
|
-
const cwd = process.cwd()
|
|
209
|
-
const inGitRepo = gitEnabled && await git.isGitRepo(cwd)
|
|
210
|
-
if (inGitRepo) {
|
|
211
|
-
let userWantsGit = !gitAsk
|
|
212
|
-
if (gitAsk && allowQuestion) {
|
|
213
|
-
//
|
|
214
|
-
const askResult = await processTurnLoop({
|
|
215
|
-
prompt: [
|
|
216
|
-
"[SYSTEM] Git 分支管理已就绪。是否为本次 LongAgent 会话创建独立分支?",
|
|
217
|
-
"回复 yes/是 启用,no/否 跳过。",
|
|
218
|
-
"启用后:自动创建特性分支 → 每阶段自动提交 → 完成后合并回主分支。"
|
|
219
|
-
].join("\n"),
|
|
220
|
-
mode: "
|
|
221
|
-
baseUrl, apiKeyEnv, agent, signal, allowQuestion: true, toolContext
|
|
222
|
-
})
|
|
223
|
-
const answer = String(askResult.reply || "").toLowerCase().trim()
|
|
224
|
-
userWantsGit = ["yes", "是", "y", "ok", "好", "确认", "开启", "启用"].some(k => answer.includes(k))
|
|
225
|
-
aggregateUsage.input += askResult.usage.input || 0
|
|
226
|
-
aggregateUsage.output += askResult.usage.output || 0
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (userWantsGit) {
|
|
230
|
-
gitBaseBranch = await git.currentBranch(cwd)
|
|
231
|
-
const branchName = git.generateBranchName(sessionId, prompt)
|
|
232
|
-
const clean = await git.isClean(cwd)
|
|
233
|
-
let stashed = false
|
|
234
|
-
if (!clean) {
|
|
235
|
-
const stashResult = await git.stash("kkcode-auto-stash-before-branch", cwd)
|
|
236
|
-
stashed = stashResult.ok
|
|
237
|
-
}
|
|
238
|
-
try {
|
|
239
|
-
const created = await git.createBranch(branchName, cwd)
|
|
240
|
-
if (created.ok) {
|
|
241
|
-
gitBranch = branchName
|
|
242
|
-
gitActive = true
|
|
243
|
-
gateStatus.git = { status: "pass", branch: branchName, baseBranch: gitBaseBranch }
|
|
244
|
-
await EventBus.emit({
|
|
245
|
-
type: EVENT_TYPES.LONGAGENT_GIT_BRANCH_CREATED,
|
|
246
|
-
sessionId,
|
|
247
|
-
payload: { branch: branchName, baseBranch: gitBaseBranch }
|
|
248
|
-
})
|
|
249
|
-
await syncState({ lastMessage: `git branch created: ${branchName}` })
|
|
250
|
-
} else {
|
|
251
|
-
gateStatus.git = { status: "warn", reason: created.message }
|
|
252
|
-
}
|
|
253
|
-
} finally {
|
|
254
|
-
if (stashed) {
|
|
255
|
-
await git.stashPop(cwd).catch(() => {})
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
await setPhase("L1", "plan_frozen")
|
|
262
|
-
currentGate = "planning"
|
|
263
|
-
const planResult = await buildStagePlan({
|
|
264
|
-
objective: prompt,
|
|
265
|
-
intakeSummary,
|
|
266
|
-
model,
|
|
267
|
-
providerType,
|
|
268
|
-
sessionId,
|
|
269
|
-
configState,
|
|
270
|
-
baseUrl,
|
|
271
|
-
apiKeyEnv,
|
|
272
|
-
agent,
|
|
273
|
-
signal,
|
|
274
|
-
defaults: {
|
|
275
|
-
timeoutMs: Number(parallelConfig.task_timeout_ms || 600000),
|
|
276
|
-
maxRetries: Number(parallelConfig.task_max_retries ?? 2)
|
|
277
|
-
}
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
stagePlan = planResult.plan
|
|
281
|
-
planFrozen = true
|
|
282
|
-
gateStatus.plan = {
|
|
283
|
-
status: planResult.errors.length ? "warn" : "pass",
|
|
284
|
-
errors: planResult.errors
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
await EventBus.emit({
|
|
288
|
-
type: EVENT_TYPES.LONGAGENT_PLAN_FROZEN,
|
|
289
|
-
sessionId,
|
|
290
|
-
payload: {
|
|
291
|
-
planId: stagePlan.planId,
|
|
292
|
-
stageCount: stagePlan.stages.length,
|
|
293
|
-
errors: planResult.errors
|
|
294
|
-
}
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
await syncState({
|
|
298
|
-
stagePlan,
|
|
299
|
-
planFrozen: true,
|
|
300
|
-
lastMessage: `plan frozen with ${stagePlan.stages.length} stage(s)`
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
// --- L1.5: Scaffolding Phase ---
|
|
304
|
-
const scaffoldEnabled = longagentConfig.scaffold?.enabled !== false
|
|
305
|
-
if (scaffoldEnabled && stagePlan.stages.length > 0) {
|
|
306
|
-
await setPhase("L1.5", "scaffolding")
|
|
307
|
-
currentGate = "scaffold"
|
|
308
|
-
await syncState({ lastMessage: "creating stub files for parallel agents" })
|
|
309
|
-
|
|
310
|
-
const scaffoldResult = await runScaffoldPhase({
|
|
311
|
-
objective: prompt,
|
|
312
|
-
stagePlan,
|
|
313
|
-
model,
|
|
314
|
-
providerType,
|
|
315
|
-
sessionId,
|
|
316
|
-
configState,
|
|
317
|
-
baseUrl,
|
|
318
|
-
apiKeyEnv,
|
|
319
|
-
agent,
|
|
320
|
-
signal,
|
|
321
|
-
toolContext
|
|
322
|
-
})
|
|
323
|
-
|
|
324
|
-
gateStatus.scaffold = {
|
|
325
|
-
status: scaffoldResult.scaffolded ? "pass" : "skip",
|
|
326
|
-
fileCount: scaffoldResult.fileCount,
|
|
327
|
-
files: scaffoldResult.files || []
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (scaffoldResult.usage) {
|
|
331
|
-
aggregateUsage.input += scaffoldResult.usage.input || 0
|
|
332
|
-
aggregateUsage.output += scaffoldResult.usage.output || 0
|
|
333
|
-
aggregateUsage.cacheRead += scaffoldResult.usage.cacheRead || 0
|
|
334
|
-
aggregateUsage.cacheWrite += scaffoldResult.usage.cacheWrite || 0
|
|
335
|
-
}
|
|
336
|
-
if (scaffoldResult.toolEvents?.length) {
|
|
337
|
-
toolEvents.push(...scaffoldResult.toolEvents)
|
|
338
|
-
}
|
|
339
|
-
if (scaffoldResult.files?.length) {
|
|
340
|
-
fileChanges = mergeCappedFileChanges(
|
|
341
|
-
fileChanges,
|
|
342
|
-
scaffoldResult.files.map((f) => ({
|
|
343
|
-
path: f, addedLines: 0, removedLines: 0, stageId: "scaffold", taskId: "scaffold"
|
|
344
|
-
})),
|
|
345
|
-
fileChangesLimit
|
|
346
|
-
)
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
await syncState({ lastMessage: `scaffolded ${scaffoldResult.fileCount} file(s)` })
|
|
350
|
-
|
|
351
|
-
await EventBus.emit({
|
|
352
|
-
type: EVENT_TYPES.LONGAGENT_SCAFFOLD_COMPLETE,
|
|
353
|
-
sessionId,
|
|
354
|
-
payload: { fileCount: scaffoldResult.fileCount, files: scaffoldResult.files || [] }
|
|
355
|
-
})
|
|
356
|
-
}
|
|
357
|
-
// --- End L1.5 ---
|
|
358
|
-
|
|
359
|
-
let priorContext = ""
|
|
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
|
-
|
|
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
|
-
iteration
|
|
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
|
-
|
|
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
|
-
|
|
1
|
+
import { LongAgentManager } from "../orchestration/longagent-manager.mjs"
|
|
2
|
+
import { processTurnLoop } from "./loop.mjs"
|
|
3
|
+
import { markSessionStatus } from "./store.mjs"
|
|
4
|
+
import { EventBus } from "../core/events.mjs"
|
|
5
|
+
import { EVENT_TYPES } from "../core/constants.mjs"
|
|
6
|
+
import { run4StageLongAgent } from "./longagent-4stage.mjs"
|
|
7
|
+
import { runHybridLongAgent } from "./longagent-hybrid.mjs"
|
|
8
|
+
import {
|
|
9
|
+
isComplete,
|
|
10
|
+
isLikelyActionableObjective,
|
|
11
|
+
mergeCappedFileChanges,
|
|
12
|
+
stageProgressStats,
|
|
13
|
+
summarizeGateFailures,
|
|
14
|
+
LONGAGENT_FILE_CHANGES_LIMIT,
|
|
15
|
+
createStuckTracker
|
|
16
|
+
} from "./longagent-utils.mjs"
|
|
17
|
+
import { saveCheckpoint, loadCheckpoint, cleanupCheckpoints } from "./checkpoint.mjs"
|
|
18
|
+
import {
|
|
19
|
+
runUsabilityGates,
|
|
20
|
+
hasGatePreferences,
|
|
21
|
+
getGatePreferences,
|
|
22
|
+
saveGatePreferences,
|
|
23
|
+
buildGatePromptText,
|
|
24
|
+
parseGateSelection
|
|
25
|
+
} from "./usability-gates.mjs"
|
|
26
|
+
import { runIntakeDialogue, buildStagePlan } from "./longagent-plan.mjs"
|
|
27
|
+
import { runStageBarrier } from "../orchestration/stage-scheduler.mjs"
|
|
28
|
+
import { runScaffoldPhase } from "./longagent-scaffold.mjs"
|
|
29
|
+
import { createValidator } from "./task-validator.mjs"
|
|
30
|
+
import * as git from "../util/git.mjs"
|
|
31
|
+
|
|
32
|
+
async function runParallelLongAgent({
|
|
33
|
+
prompt,
|
|
34
|
+
model,
|
|
35
|
+
providerType,
|
|
36
|
+
sessionId,
|
|
37
|
+
configState,
|
|
38
|
+
baseUrl = null,
|
|
39
|
+
apiKeyEnv = null,
|
|
40
|
+
agent = null,
|
|
41
|
+
maxIterations: maxIterationsParam = 0,
|
|
42
|
+
signal = null,
|
|
43
|
+
output = null,
|
|
44
|
+
allowQuestion = true,
|
|
45
|
+
toolContext = {}
|
|
46
|
+
}) {
|
|
47
|
+
const longagentConfig = configState.config.agent.longagent || {}
|
|
48
|
+
const maxIterations = Number(longagentConfig.max_iterations || maxIterationsParam)
|
|
49
|
+
const plannerConfig = longagentConfig.planner || {}
|
|
50
|
+
const intakeConfig = plannerConfig.intake_questions || {}
|
|
51
|
+
const parallelConfig = longagentConfig.parallel || {}
|
|
52
|
+
const noProgressLimit = Number(longagentConfig.no_progress_limit || 5)
|
|
53
|
+
const checkpointInterval = Number(longagentConfig.checkpoint_interval || 5)
|
|
54
|
+
const maxGateAttempts = Number(longagentConfig.max_gate_attempts || 5)
|
|
55
|
+
|
|
56
|
+
const gitConfig = longagentConfig.git || {}
|
|
57
|
+
const gitEnabled = gitConfig.enabled === true || gitConfig.enabled === "ask"
|
|
58
|
+
const gitAsk = gitConfig.enabled === "ask"
|
|
59
|
+
|
|
60
|
+
let iteration = 0
|
|
61
|
+
let recoveryCount = 0
|
|
62
|
+
let currentPhase = "L0"
|
|
63
|
+
let currentGate = "intake"
|
|
64
|
+
let gateStatus = {}
|
|
65
|
+
let lastGateFailures = []
|
|
66
|
+
let lastProgress = { percentage: 0, currentStep: 0, totalSteps: 0 }
|
|
67
|
+
let finalReply = ""
|
|
68
|
+
let stageIndex = 0
|
|
69
|
+
let planFrozen = false
|
|
70
|
+
let stagePlan = null
|
|
71
|
+
let taskProgress = {}
|
|
72
|
+
let fileChanges = []
|
|
73
|
+
const fileChangesLimit = Math.max(20, Number(longagentConfig.file_changes_limit || LONGAGENT_FILE_CHANGES_LIMIT))
|
|
74
|
+
const aggregateUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
|
|
75
|
+
const toolEvents = []
|
|
76
|
+
const startTime = Date.now()
|
|
77
|
+
let completionMarkerSeen = false
|
|
78
|
+
let gitBranch = null
|
|
79
|
+
let gitBaseBranch = null
|
|
80
|
+
let gitActive = false
|
|
81
|
+
|
|
82
|
+
async function setPhase(nextPhase, reason = "") {
|
|
83
|
+
if (currentPhase === nextPhase) return
|
|
84
|
+
const prevPhase = currentPhase
|
|
85
|
+
currentPhase = nextPhase
|
|
86
|
+
await EventBus.emit({
|
|
87
|
+
type: EVENT_TYPES.LONGAGENT_PHASE_CHANGED,
|
|
88
|
+
sessionId,
|
|
89
|
+
payload: { prevPhase, nextPhase, reason, iteration }
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function syncState(patch = {}) {
|
|
94
|
+
const stats = stageProgressStats(taskProgress)
|
|
95
|
+
const stageCount = stagePlan?.stages?.length || 0
|
|
96
|
+
const currentStage = stagePlan?.stages?.[stageIndex] || null
|
|
97
|
+
await LongAgentManager.update(sessionId, {
|
|
98
|
+
status: patch.status || "running",
|
|
99
|
+
phase: currentPhase,
|
|
100
|
+
gateStatus,
|
|
101
|
+
currentGate,
|
|
102
|
+
recoveryCount,
|
|
103
|
+
lastGateFailures,
|
|
104
|
+
iterations: iteration,
|
|
105
|
+
heartbeatAt: Date.now(),
|
|
106
|
+
noProgressCount: 0,
|
|
107
|
+
progress: lastProgress,
|
|
108
|
+
planFrozen,
|
|
109
|
+
currentStageId: currentStage?.stageId || null,
|
|
110
|
+
stageIndex,
|
|
111
|
+
stageCount,
|
|
112
|
+
stageStatus: patch.stageStatus || null,
|
|
113
|
+
taskProgress,
|
|
114
|
+
remainingFiles: stats.remainingFiles,
|
|
115
|
+
remainingFilesCount: stats.remainingFilesCount,
|
|
116
|
+
stageProgress: {
|
|
117
|
+
done: stats.done,
|
|
118
|
+
total: stats.total
|
|
119
|
+
},
|
|
120
|
+
...patch
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await markSessionStatus(sessionId, "running-longagent")
|
|
125
|
+
await syncState({
|
|
126
|
+
status: "running",
|
|
127
|
+
lastMessage: "longagent parallel mode started",
|
|
128
|
+
stopRequested: false
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
if (!isLikelyActionableObjective(prompt)) {
|
|
132
|
+
const blocked = "LongAgent 需要明确的编码目标。请直接描述要实现/修复的内容、涉及文件或验收标准。"
|
|
133
|
+
await LongAgentManager.update(sessionId, {
|
|
134
|
+
status: "blocked",
|
|
135
|
+
phase: "L0",
|
|
136
|
+
currentGate: "intake",
|
|
137
|
+
gateStatus: {
|
|
138
|
+
intake: {
|
|
139
|
+
status: "blocked",
|
|
140
|
+
reason: "objective_not_actionable"
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
lastMessage: blocked
|
|
144
|
+
})
|
|
145
|
+
await markSessionStatus(sessionId, "active")
|
|
146
|
+
return {
|
|
147
|
+
sessionId,
|
|
148
|
+
turnId: `turn_long_${Date.now()}`,
|
|
149
|
+
reply: blocked,
|
|
150
|
+
usage: aggregateUsage,
|
|
151
|
+
toolEvents,
|
|
152
|
+
iterations: 0,
|
|
153
|
+
emittedText: false,
|
|
154
|
+
context: null,
|
|
155
|
+
status: "blocked",
|
|
156
|
+
phase: "L0",
|
|
157
|
+
gateStatus: { intake: { status: "blocked", reason: "objective_not_actionable" } },
|
|
158
|
+
currentGate: "intake",
|
|
159
|
+
lastGateFailures: [],
|
|
160
|
+
recoveryCount: 0,
|
|
161
|
+
progress: { percentage: 0, currentStep: 0, totalSteps: 0 },
|
|
162
|
+
elapsed: 0,
|
|
163
|
+
stageIndex: 0,
|
|
164
|
+
stageCount: 0,
|
|
165
|
+
currentStageId: null,
|
|
166
|
+
planFrozen: false,
|
|
167
|
+
taskProgress: {},
|
|
168
|
+
stageProgress: { done: 0, total: 0, remainingFiles: [], remainingFilesCount: 0 },
|
|
169
|
+
fileChanges: [],
|
|
170
|
+
remainingFilesCount: 0
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await EventBus.emit({
|
|
175
|
+
type: EVENT_TYPES.LONGAGENT_INTAKE_STARTED,
|
|
176
|
+
sessionId,
|
|
177
|
+
payload: { objective: prompt }
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const intakeEnabled = intakeConfig.enabled !== false
|
|
181
|
+
let intakeSummary = prompt
|
|
182
|
+
if (intakeEnabled) {
|
|
183
|
+
await setPhase("L0", "intake")
|
|
184
|
+
const intake = await runIntakeDialogue({
|
|
185
|
+
objective: prompt,
|
|
186
|
+
model,
|
|
187
|
+
providerType,
|
|
188
|
+
sessionId,
|
|
189
|
+
configState,
|
|
190
|
+
baseUrl,
|
|
191
|
+
apiKeyEnv,
|
|
192
|
+
agent,
|
|
193
|
+
signal,
|
|
194
|
+
maxRounds: Number(intakeConfig.max_rounds || 6)
|
|
195
|
+
})
|
|
196
|
+
intakeSummary = intake.summary || prompt
|
|
197
|
+
gateStatus.intake = {
|
|
198
|
+
status: "pass",
|
|
199
|
+
rounds: intake.transcript.length,
|
|
200
|
+
summary: intakeSummary.slice(0, 500)
|
|
201
|
+
}
|
|
202
|
+
await syncState({
|
|
203
|
+
lastMessage: `intake completed (${intake.transcript.length} qa pairs)`
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// --- Git branch creation (after intake, before planning) ---
|
|
208
|
+
const cwd = process.cwd()
|
|
209
|
+
const inGitRepo = gitEnabled && await git.isGitRepo(cwd)
|
|
210
|
+
if (inGitRepo) {
|
|
211
|
+
let userWantsGit = !gitAsk
|
|
212
|
+
if (gitAsk && allowQuestion) {
|
|
213
|
+
// Confirm via a lightweight turn
|
|
214
|
+
const askResult = await processTurnLoop({
|
|
215
|
+
prompt: [
|
|
216
|
+
"[SYSTEM] Git 分支管理已就绪。是否为本次 LongAgent 会话创建独立分支?",
|
|
217
|
+
"回复 yes/是 启用,no/否 跳过。",
|
|
218
|
+
"启用后:自动创建特性分支 → 每阶段自动提交 → 完成后合并回主分支。"
|
|
219
|
+
].join("\n"),
|
|
220
|
+
mode: "assistant", model, providerType, sessionId, configState,
|
|
221
|
+
baseUrl, apiKeyEnv, agent, signal, allowQuestion: true, toolContext
|
|
222
|
+
})
|
|
223
|
+
const answer = String(askResult.reply || "").toLowerCase().trim()
|
|
224
|
+
userWantsGit = ["yes", "是", "y", "ok", "好", "确认", "开启", "启用"].some(k => answer.includes(k))
|
|
225
|
+
aggregateUsage.input += askResult.usage.input || 0
|
|
226
|
+
aggregateUsage.output += askResult.usage.output || 0
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (userWantsGit) {
|
|
230
|
+
gitBaseBranch = await git.currentBranch(cwd)
|
|
231
|
+
const branchName = git.generateBranchName(sessionId, prompt)
|
|
232
|
+
const clean = await git.isClean(cwd)
|
|
233
|
+
let stashed = false
|
|
234
|
+
if (!clean) {
|
|
235
|
+
const stashResult = await git.stash("kkcode-auto-stash-before-branch", cwd)
|
|
236
|
+
stashed = stashResult.ok
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const created = await git.createBranch(branchName, cwd)
|
|
240
|
+
if (created.ok) {
|
|
241
|
+
gitBranch = branchName
|
|
242
|
+
gitActive = true
|
|
243
|
+
gateStatus.git = { status: "pass", branch: branchName, baseBranch: gitBaseBranch }
|
|
244
|
+
await EventBus.emit({
|
|
245
|
+
type: EVENT_TYPES.LONGAGENT_GIT_BRANCH_CREATED,
|
|
246
|
+
sessionId,
|
|
247
|
+
payload: { branch: branchName, baseBranch: gitBaseBranch }
|
|
248
|
+
})
|
|
249
|
+
await syncState({ lastMessage: `git branch created: ${branchName}` })
|
|
250
|
+
} else {
|
|
251
|
+
gateStatus.git = { status: "warn", reason: created.message }
|
|
252
|
+
}
|
|
253
|
+
} finally {
|
|
254
|
+
if (stashed) {
|
|
255
|
+
await git.stashPop(cwd).catch(() => {})
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
await setPhase("L1", "plan_frozen")
|
|
262
|
+
currentGate = "planning"
|
|
263
|
+
const planResult = await buildStagePlan({
|
|
264
|
+
objective: prompt,
|
|
265
|
+
intakeSummary,
|
|
266
|
+
model,
|
|
267
|
+
providerType,
|
|
268
|
+
sessionId,
|
|
269
|
+
configState,
|
|
270
|
+
baseUrl,
|
|
271
|
+
apiKeyEnv,
|
|
272
|
+
agent,
|
|
273
|
+
signal,
|
|
274
|
+
defaults: {
|
|
275
|
+
timeoutMs: Number(parallelConfig.task_timeout_ms || 600000),
|
|
276
|
+
maxRetries: Number(parallelConfig.task_max_retries ?? 2)
|
|
277
|
+
}
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
stagePlan = planResult.plan
|
|
281
|
+
planFrozen = true
|
|
282
|
+
gateStatus.plan = {
|
|
283
|
+
status: planResult.errors.length ? "warn" : "pass",
|
|
284
|
+
errors: planResult.errors
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
await EventBus.emit({
|
|
288
|
+
type: EVENT_TYPES.LONGAGENT_PLAN_FROZEN,
|
|
289
|
+
sessionId,
|
|
290
|
+
payload: {
|
|
291
|
+
planId: stagePlan.planId,
|
|
292
|
+
stageCount: stagePlan.stages.length,
|
|
293
|
+
errors: planResult.errors
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
await syncState({
|
|
298
|
+
stagePlan,
|
|
299
|
+
planFrozen: true,
|
|
300
|
+
lastMessage: `plan frozen with ${stagePlan.stages.length} stage(s)`
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
// --- L1.5: Scaffolding Phase ---
|
|
304
|
+
const scaffoldEnabled = longagentConfig.scaffold?.enabled !== false
|
|
305
|
+
if (scaffoldEnabled && stagePlan.stages.length > 0) {
|
|
306
|
+
await setPhase("L1.5", "scaffolding")
|
|
307
|
+
currentGate = "scaffold"
|
|
308
|
+
await syncState({ lastMessage: "creating stub files for parallel agents" })
|
|
309
|
+
|
|
310
|
+
const scaffoldResult = await runScaffoldPhase({
|
|
311
|
+
objective: prompt,
|
|
312
|
+
stagePlan,
|
|
313
|
+
model,
|
|
314
|
+
providerType,
|
|
315
|
+
sessionId,
|
|
316
|
+
configState,
|
|
317
|
+
baseUrl,
|
|
318
|
+
apiKeyEnv,
|
|
319
|
+
agent,
|
|
320
|
+
signal,
|
|
321
|
+
toolContext
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
gateStatus.scaffold = {
|
|
325
|
+
status: scaffoldResult.scaffolded ? "pass" : "skip",
|
|
326
|
+
fileCount: scaffoldResult.fileCount,
|
|
327
|
+
files: scaffoldResult.files || []
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (scaffoldResult.usage) {
|
|
331
|
+
aggregateUsage.input += scaffoldResult.usage.input || 0
|
|
332
|
+
aggregateUsage.output += scaffoldResult.usage.output || 0
|
|
333
|
+
aggregateUsage.cacheRead += scaffoldResult.usage.cacheRead || 0
|
|
334
|
+
aggregateUsage.cacheWrite += scaffoldResult.usage.cacheWrite || 0
|
|
335
|
+
}
|
|
336
|
+
if (scaffoldResult.toolEvents?.length) {
|
|
337
|
+
toolEvents.push(...scaffoldResult.toolEvents)
|
|
338
|
+
}
|
|
339
|
+
if (scaffoldResult.files?.length) {
|
|
340
|
+
fileChanges = mergeCappedFileChanges(
|
|
341
|
+
fileChanges,
|
|
342
|
+
scaffoldResult.files.map((f) => ({
|
|
343
|
+
path: f, addedLines: 0, removedLines: 0, stageId: "scaffold", taskId: "scaffold"
|
|
344
|
+
})),
|
|
345
|
+
fileChangesLimit
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
await syncState({ lastMessage: `scaffolded ${scaffoldResult.fileCount} file(s)` })
|
|
350
|
+
|
|
351
|
+
await EventBus.emit({
|
|
352
|
+
type: EVENT_TYPES.LONGAGENT_SCAFFOLD_COMPLETE,
|
|
353
|
+
sessionId,
|
|
354
|
+
payload: { fileCount: scaffoldResult.fileCount, files: scaffoldResult.files || [] }
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
// --- End L1.5 ---
|
|
358
|
+
|
|
359
|
+
let priorContext = ""
|
|
360
|
+
const seenFilePaths = new Set() // #3 去重:跨阶段文件路径去重,避免 priorContext 重复提及
|
|
361
|
+
|
|
362
|
+
while (stageIndex < stagePlan.stages.length) {
|
|
363
|
+
const state = await LongAgentManager.get(sessionId)
|
|
364
|
+
if (state?.retryStageId) {
|
|
365
|
+
const targetIdx = stagePlan.stages.findIndex((stage) => stage.stageId === state.retryStageId)
|
|
366
|
+
// Atomically clear retryStageId to prevent race with concurrent updates
|
|
367
|
+
await LongAgentManager.update(sessionId, { retryStageId: null })
|
|
368
|
+
if (targetIdx >= 0) {
|
|
369
|
+
stageIndex = targetIdx
|
|
370
|
+
// Clear progress for target stage AND all subsequent stages
|
|
371
|
+
for (let si = targetIdx; si < stagePlan.stages.length; si++) {
|
|
372
|
+
const stageTasks = new Set((stagePlan.stages[si].tasks || []).map((task) => task.taskId))
|
|
373
|
+
for (const taskId of Object.keys(taskProgress)) {
|
|
374
|
+
if (stageTasks.has(taskId)) delete taskProgress[taskId]
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (state?.stopRequested || signal?.aborted) {
|
|
380
|
+
await LongAgentManager.update(sessionId, {
|
|
381
|
+
status: "stopped",
|
|
382
|
+
phase: currentPhase,
|
|
383
|
+
currentGate,
|
|
384
|
+
gateStatus,
|
|
385
|
+
lastMessage: "stop requested by user"
|
|
386
|
+
})
|
|
387
|
+
await markSessionStatus(sessionId, "stopped")
|
|
388
|
+
break
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
iteration += 1
|
|
392
|
+
const stage = stagePlan.stages[stageIndex]
|
|
393
|
+
currentGate = `stage:${stage.stageId}`
|
|
394
|
+
await setPhase("L2", `stage_running:${stage.stageId}`)
|
|
395
|
+
|
|
396
|
+
if (maxIterations > 0 && iteration >= maxIterations && iteration % Math.max(1, maxIterations) === 0) {
|
|
397
|
+
await EventBus.emit({
|
|
398
|
+
type: EVENT_TYPES.LONGAGENT_GATE_CHECKED,
|
|
399
|
+
sessionId,
|
|
400
|
+
payload: { gate: "max_iterations", status: "warn", iteration, threshold: maxIterations }
|
|
401
|
+
})
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
await syncState({
|
|
405
|
+
stageStatus: "running",
|
|
406
|
+
lastMessage: `running ${stage.stageId} (${stageIndex + 1}/${stagePlan.stages.length})`
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
const seeded = Object.fromEntries(
|
|
410
|
+
stage.tasks
|
|
411
|
+
.map((task) => [task.taskId, taskProgress[task.taskId]])
|
|
412
|
+
.filter(([, value]) => Boolean(value))
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
// #4 计划锚点 — 每个阶段执行前重建,确保模型始终看到完整计划和当前进度
|
|
416
|
+
const stageStatuses = stagePlan.stages.map((s, i) => {
|
|
417
|
+
const marker = i < stageIndex ? "✓" : i === stageIndex ? "→" : " "
|
|
418
|
+
return `[${marker}] 阶段${i + 1}: ${s.name || s.stageId}`
|
|
419
|
+
}).join("\n")
|
|
420
|
+
const planAnchor = `## 计划锚点\n目标: ${stagePlan.objective || prompt}\n进度: ${stageIndex + 1}/${stagePlan.stages.length}\n${stageStatuses}\n\n`
|
|
421
|
+
|
|
422
|
+
const stageResult = await runStageBarrier({
|
|
423
|
+
stage,
|
|
424
|
+
sessionId,
|
|
425
|
+
config: configState.config,
|
|
426
|
+
model,
|
|
427
|
+
providerType,
|
|
428
|
+
seedTaskProgress: seeded,
|
|
429
|
+
objective: prompt,
|
|
430
|
+
stageIndex,
|
|
431
|
+
stageCount: stagePlan.stages.length,
|
|
432
|
+
priorContext: planAnchor + priorContext
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
for (const [taskId, progress] of Object.entries(stageResult.taskProgress || {})) {
|
|
436
|
+
taskProgress[taskId] = {
|
|
437
|
+
...taskProgress[taskId],
|
|
438
|
+
...progress
|
|
439
|
+
}
|
|
440
|
+
if (String(progress.lastReply || "").toLowerCase().includes("[task_complete]")) {
|
|
441
|
+
completionMarkerSeen = true
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (stageResult.completionMarkerSeen) completionMarkerSeen = true
|
|
445
|
+
if (Array.isArray(stageResult.fileChanges) && stageResult.fileChanges.length) {
|
|
446
|
+
fileChanges = mergeCappedFileChanges(fileChanges, stageResult.fileChanges, fileChangesLimit)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
gateStatus[stage.stageId] = {
|
|
450
|
+
status: stageResult.allSuccess ? "pass" : "fail",
|
|
451
|
+
successCount: stageResult.successCount,
|
|
452
|
+
failCount: stageResult.failCount,
|
|
453
|
+
retryCount: stageResult.retryCount,
|
|
454
|
+
remainingFiles: stageResult.remainingFiles
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// #1 阶段级压缩 + #3 去重 — 结构化阶段摘要,文件路径跨阶段去重
|
|
458
|
+
const taskSummaries = Object.values(stageResult.taskProgress || {})
|
|
459
|
+
.filter(t => t.lastReply)
|
|
460
|
+
.map(t => ` - [${t.taskId}] ${t.status}: ${t.lastReply.slice(0, 250)}`)
|
|
461
|
+
const stageFiles = (stageResult.fileChanges || [])
|
|
462
|
+
.map(f => (typeof f === "string" ? f : (f.path || f.file || "")))
|
|
463
|
+
.filter(Boolean)
|
|
464
|
+
const newFiles = stageFiles.filter(f => !seenFilePaths.has(f))
|
|
465
|
+
newFiles.forEach(f => seenFilePaths.add(f))
|
|
466
|
+
if (taskSummaries.length || newFiles.length) {
|
|
467
|
+
const fileNote = newFiles.length ? `\n 新增/修改文件: ${newFiles.join(", ")}` : ""
|
|
468
|
+
const failNote = !stageResult.allSuccess ? `\n 失败任务数: ${stageResult.failCount}` : ""
|
|
469
|
+
priorContext += `### 阶段${stageIndex + 1}: ${stage.name || stage.stageId} (${stageResult.allSuccess ? "PASS" : "FAIL"})${failNote}\n${taskSummaries.join("\n")}${fileNote}\n\n`
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
lastProgress = {
|
|
473
|
+
percentage: Math.round(((stageIndex + (stageResult.allSuccess ? 1 : 0)) / Math.max(1, stagePlan.stages.length)) * 100),
|
|
474
|
+
currentStep: stageIndex + (stageResult.allSuccess ? 1 : 0),
|
|
475
|
+
totalSteps: stagePlan.stages.length
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
await syncState({
|
|
479
|
+
stageStatus: stageResult.allSuccess ? "completed" : "failed",
|
|
480
|
+
lastMessage: stageResult.allSuccess
|
|
481
|
+
? `stage ${stage.stageId} completed`
|
|
482
|
+
: `stage ${stage.stageId} failed (${stageResult.failCount})`
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
// --- Git: auto-commit after successful stage ---
|
|
486
|
+
if (gitActive && stageResult.allSuccess && gitConfig.auto_commit_stages !== false) {
|
|
487
|
+
const commitMsg = `[kkcode] stage ${stage.stageId} completed (${stageIndex + 1}/${stagePlan.stages.length})`
|
|
488
|
+
const commitResult = await git.commitAll(commitMsg, cwd)
|
|
489
|
+
if (commitResult.ok && !commitResult.empty) {
|
|
490
|
+
await EventBus.emit({
|
|
491
|
+
type: EVENT_TYPES.LONGAGENT_GIT_STAGE_COMMITTED,
|
|
492
|
+
sessionId,
|
|
493
|
+
payload: { stageId: stage.stageId, message: commitMsg }
|
|
494
|
+
})
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
if (!stageResult.allSuccess) {
|
|
499
|
+
recoveryCount += 1
|
|
500
|
+
// Exponential backoff before retry
|
|
501
|
+
const backoffMs = Math.min(1000 * 2 ** (recoveryCount - 1), 30000)
|
|
502
|
+
await new Promise(r => setTimeout(r, backoffMs))
|
|
503
|
+
lastGateFailures = Object.values(stageResult.taskProgress || {})
|
|
504
|
+
.filter((item) => item.status !== "completed")
|
|
505
|
+
.map((item) => `${item.taskId}:${item.lastError || item.status}`)
|
|
506
|
+
|
|
507
|
+
await EventBus.emit({
|
|
508
|
+
type: EVENT_TYPES.LONGAGENT_RECOVERY_ENTERED,
|
|
509
|
+
sessionId,
|
|
510
|
+
payload: {
|
|
511
|
+
reason: `stage_failed:${stage.stageId}`,
|
|
512
|
+
stageId: stage.stageId,
|
|
513
|
+
recoveryCount,
|
|
514
|
+
iteration
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
await setPhase("L2.5", `stage_recover:${stage.stageId}`)
|
|
519
|
+
currentGate = "stage_recovery"
|
|
520
|
+
await syncState({
|
|
521
|
+
status: "recovering",
|
|
522
|
+
stageStatus: "recovering",
|
|
523
|
+
lastMessage: `recovering stage ${stage.stageId}`
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
if (recoveryCount >= noProgressLimit) {
|
|
527
|
+
await EventBus.emit({
|
|
528
|
+
type: EVENT_TYPES.LONGAGENT_ALERT,
|
|
529
|
+
sessionId,
|
|
530
|
+
payload: {
|
|
531
|
+
kind: "retry_storm",
|
|
532
|
+
message: `stage recovery count reached ${recoveryCount}`,
|
|
533
|
+
recoveryCount,
|
|
534
|
+
threshold: noProgressLimit,
|
|
535
|
+
iteration
|
|
536
|
+
}
|
|
537
|
+
})
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Circuit breaker: abort stage after max recovery attempts
|
|
541
|
+
const maxStageRecoveries = Number(longagentConfig.max_stage_recoveries ?? 3)
|
|
542
|
+
if (recoveryCount >= maxStageRecoveries) {
|
|
543
|
+
await setPhase("L2.5", `stage_abort:${stage.stageId}`)
|
|
544
|
+
await syncState({
|
|
545
|
+
status: "error",
|
|
546
|
+
stageStatus: "aborted",
|
|
547
|
+
lastMessage: `stage ${stage.stageId} aborted after ${recoveryCount} recovery attempts`
|
|
548
|
+
})
|
|
549
|
+
await EventBus.emit({
|
|
550
|
+
type: EVENT_TYPES.LONGAGENT_ALERT,
|
|
551
|
+
sessionId,
|
|
552
|
+
payload: {
|
|
553
|
+
kind: "stage_aborted",
|
|
554
|
+
message: `stage ${stage.stageId} aborted: max recoveries (${maxStageRecoveries}) exceeded`,
|
|
555
|
+
recoveryCount,
|
|
556
|
+
stageId: stage.stageId
|
|
557
|
+
}
|
|
558
|
+
})
|
|
559
|
+
break
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (longagentConfig.resume_incomplete_files !== false) {
|
|
563
|
+
// Reset failed tasks so runStageBarrier will re-dispatch them
|
|
564
|
+
for (const [taskId, tp] of Object.entries(taskProgress)) {
|
|
565
|
+
if (tp.status === "error") {
|
|
566
|
+
taskProgress[taskId] = { ...tp, status: "retrying", attempt: 0 }
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
continue
|
|
570
|
+
}
|
|
571
|
+
break
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
stageIndex += 1
|
|
575
|
+
recoveryCount = 0 // reset per-stage recovery counter after successful stage
|
|
576
|
+
// Always checkpoint after each stage for reliable recovery
|
|
577
|
+
await saveCheckpoint(sessionId, {
|
|
578
|
+
name: `stage_${stage.stageId}`,
|
|
579
|
+
iteration,
|
|
580
|
+
currentPhase,
|
|
581
|
+
currentGate,
|
|
582
|
+
recoveryCount,
|
|
583
|
+
gateStatus,
|
|
584
|
+
taskProgress,
|
|
585
|
+
stageIndex,
|
|
586
|
+
stagePlan,
|
|
587
|
+
planFrozen,
|
|
588
|
+
lastProgress
|
|
589
|
+
})
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (stagePlan && stageIndex >= stagePlan.stages.length) {
|
|
593
|
+
// --- Gate preference prompt (first run only) ---
|
|
594
|
+
const gatesConfig = longagentConfig.usability_gates || {}
|
|
595
|
+
const shouldPromptGates = gatesConfig.prompt_user === "first_run" || gatesConfig.prompt_user === "always"
|
|
596
|
+
if (shouldPromptGates && allowQuestion) {
|
|
597
|
+
const hasPrefs = await hasGatePreferences()
|
|
598
|
+
if (!hasPrefs || gatesConfig.prompt_user === "always") {
|
|
599
|
+
const gateAssistantResult = await processTurnLoop({
|
|
600
|
+
prompt: buildGatePromptText(),
|
|
601
|
+
mode: "assistant", model, providerType, sessionId, configState,
|
|
602
|
+
baseUrl, apiKeyEnv, agent, signal, allowQuestion: true, toolContext
|
|
603
|
+
})
|
|
604
|
+
const gatePrefs = parseGateSelection(gateAssistantResult.reply)
|
|
605
|
+
await saveGatePreferences(gatePrefs)
|
|
606
|
+
// Apply preferences to a shallow copy to avoid mutating shared configState
|
|
607
|
+
const gatesCopy = { ...configState.config.agent.longagent.usability_gates }
|
|
608
|
+
for (const [gate, enabled] of Object.entries(gatePrefs)) {
|
|
609
|
+
if (gatesCopy[gate]) {
|
|
610
|
+
gatesCopy[gate] = { ...gatesCopy[gate], enabled }
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
configState.config.agent.longagent = { ...configState.config.agent.longagent, usability_gates: gatesCopy }
|
|
614
|
+
aggregateUsage.input += gateAssistantResult.usage.input || 0
|
|
615
|
+
aggregateUsage.output += gateAssistantResult.usage.output || 0
|
|
616
|
+
} else {
|
|
617
|
+
// Apply saved preferences
|
|
618
|
+
const savedPrefs = await getGatePreferences()
|
|
619
|
+
if (savedPrefs) {
|
|
620
|
+
const gatesCopy = { ...configState.config.agent.longagent.usability_gates }
|
|
621
|
+
for (const [gate, enabled] of Object.entries(savedPrefs)) {
|
|
622
|
+
if (gatesCopy[gate]) {
|
|
623
|
+
gatesCopy[gate] = { ...gatesCopy[gate], enabled }
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
configState.config.agent.longagent = { ...configState.config.agent.longagent, usability_gates: gatesCopy }
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// --- Structured completion verification ---
|
|
632
|
+
const validationLevel = longagentConfig.validation_level || "standard"
|
|
633
|
+
let validationReport = null
|
|
634
|
+
try {
|
|
635
|
+
const validator = await createValidator({ cwd, configState })
|
|
636
|
+
validationReport = await validator.validate({ todoState: toolContext?._todoState, level: validationLevel })
|
|
637
|
+
gateStatus.validation = {
|
|
638
|
+
status: validationReport.verdict === "BLOCK" ? "fail" : "pass",
|
|
639
|
+
verdict: validationReport.verdict,
|
|
640
|
+
reason: validationReport.verdict === "APPROVE"
|
|
641
|
+
? "all checks passed"
|
|
642
|
+
: `${validationReport.results.filter(r => !r.passed).length} check(s) failed`
|
|
643
|
+
}
|
|
644
|
+
} catch (valErr) {
|
|
645
|
+
gateStatus.validation = { status: "warn", reason: `validation skipped: ${valErr.message}` }
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const validationContext = validationReport
|
|
649
|
+
? `\n\nVerification Report:\n${validationReport.message}`
|
|
650
|
+
: ""
|
|
651
|
+
|
|
652
|
+
if (!completionMarkerSeen) {
|
|
653
|
+
const markerTurn = await processTurnLoop({
|
|
654
|
+
prompt: [
|
|
655
|
+
`Objective: ${prompt}`,
|
|
656
|
+
"All planned stages are done.",
|
|
657
|
+
validationContext,
|
|
658
|
+
validationReport?.verdict === "BLOCK"
|
|
659
|
+
? "Verification found critical issues. Fix them, then include [TASK_COMPLETE]."
|
|
660
|
+
: "Validate if the task is truly complete. If complete, include [TASK_COMPLETE] exactly once."
|
|
661
|
+
].filter(Boolean).join("\n"),
|
|
662
|
+
mode: "agent",
|
|
663
|
+
model,
|
|
664
|
+
providerType,
|
|
665
|
+
sessionId,
|
|
666
|
+
configState,
|
|
667
|
+
baseUrl,
|
|
668
|
+
apiKeyEnv,
|
|
669
|
+
agent,
|
|
670
|
+
signal,
|
|
671
|
+
allowQuestion: plannerConfig.ask_user_after_plan_frozen === true && allowQuestion,
|
|
672
|
+
toolContext
|
|
673
|
+
})
|
|
674
|
+
finalReply = markerTurn.reply
|
|
675
|
+
aggregateUsage.input += markerTurn.usage.input || 0
|
|
676
|
+
aggregateUsage.output += markerTurn.usage.output || 0
|
|
677
|
+
aggregateUsage.cacheRead += markerTurn.usage.cacheRead || 0
|
|
678
|
+
aggregateUsage.cacheWrite += markerTurn.usage.cacheWrite || 0
|
|
679
|
+
toolEvents.push(...markerTurn.toolEvents)
|
|
680
|
+
completionMarkerSeen = isComplete(markerTurn.reply)
|
|
681
|
+
gateStatus.completionMarker = {
|
|
682
|
+
status: completionMarkerSeen ? "pass" : "warn",
|
|
683
|
+
reason: completionMarkerSeen ? "completion marker confirmed" : "marker missing"
|
|
684
|
+
}
|
|
685
|
+
} else {
|
|
686
|
+
gateStatus.completionMarker = {
|
|
687
|
+
status: "pass",
|
|
688
|
+
reason: "completion marker present in stage outputs"
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
let gateAttempt = 0
|
|
693
|
+
while (gateAttempt < maxGateAttempts) {
|
|
694
|
+
if (signal?.aborted) break
|
|
695
|
+
const preState = await LongAgentManager.get(sessionId)
|
|
696
|
+
if (preState?.stopRequested) break
|
|
697
|
+
|
|
698
|
+
gateAttempt += 1
|
|
699
|
+
currentGate = "usability_gates"
|
|
700
|
+
await setPhase("L3", "usability-gate-check")
|
|
701
|
+
const gateResult = await runUsabilityGates({
|
|
702
|
+
sessionId,
|
|
703
|
+
config: configState.config,
|
|
704
|
+
cwd: process.cwd(),
|
|
705
|
+
iteration
|
|
706
|
+
})
|
|
707
|
+
gateStatus.usability = gateResult.gates
|
|
708
|
+
|
|
709
|
+
if (gateResult.allPass && completionMarkerSeen) {
|
|
710
|
+
await LongAgentManager.update(sessionId, {
|
|
711
|
+
status: "completed",
|
|
712
|
+
phase: currentPhase,
|
|
713
|
+
currentGate,
|
|
714
|
+
gateStatus,
|
|
715
|
+
recoveryCount,
|
|
716
|
+
lastGateFailures: [],
|
|
717
|
+
iterations: iteration,
|
|
718
|
+
lastMessage: "parallel stages and usability gates passed"
|
|
719
|
+
})
|
|
720
|
+
await markSessionStatus(sessionId, "completed")
|
|
721
|
+
break
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
const failureSummary = summarizeGateFailures(gateResult.failures)
|
|
725
|
+
lastGateFailures = gateResult.failures.map((item) => `${item.gate}:${item.reason}`)
|
|
726
|
+
// Use gate-specific backoff (not shared recoveryCount) to avoid over-aggressive delays
|
|
727
|
+
const gateBackoffMs = Math.min(1000 * 2 ** (gateAttempt - 1), 30000)
|
|
728
|
+
await new Promise(r => setTimeout(r, gateBackoffMs))
|
|
729
|
+
|
|
730
|
+
await EventBus.emit({
|
|
731
|
+
type: EVENT_TYPES.LONGAGENT_RECOVERY_ENTERED,
|
|
732
|
+
sessionId,
|
|
733
|
+
payload: {
|
|
734
|
+
reason: `usability_gates_failed:${failureSummary || "unknown"}`,
|
|
735
|
+
gateAttempt,
|
|
736
|
+
recoveryCount,
|
|
737
|
+
iteration
|
|
738
|
+
}
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
await setPhase("L2.5", "gate_recovery")
|
|
742
|
+
currentGate = "gate_recovery"
|
|
743
|
+
await syncState({
|
|
744
|
+
status: "recovering",
|
|
745
|
+
stageStatus: "gate_recovery",
|
|
746
|
+
lastMessage: `gate recovery #${gateAttempt}: ${failureSummary || "unknown"}`
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
// Re-run validation to give remediation agent fresh context
|
|
750
|
+
let remediationContext = ""
|
|
751
|
+
try {
|
|
752
|
+
const reValidator = await createValidator({ cwd, configState })
|
|
753
|
+
const reReport = await reValidator.validate({ todoState: toolContext?._todoState, level: validationLevel })
|
|
754
|
+
remediationContext = `\n\nCurrent Verification:\n${reReport.message}`
|
|
755
|
+
} catch { /* skip */ }
|
|
756
|
+
|
|
757
|
+
const remediation = await processTurnLoop({
|
|
758
|
+
prompt: [
|
|
759
|
+
`Objective: ${prompt}`,
|
|
760
|
+
"Usability gates failed.",
|
|
761
|
+
`Failures: ${failureSummary || "unknown"}`,
|
|
762
|
+
remediationContext,
|
|
763
|
+
"Fix ALL failing checks, then include [TASK_COMPLETE] when fully usable."
|
|
764
|
+
].filter(Boolean).join("\n"),
|
|
765
|
+
mode: "agent",
|
|
766
|
+
model,
|
|
767
|
+
providerType,
|
|
768
|
+
sessionId,
|
|
769
|
+
configState,
|
|
770
|
+
baseUrl,
|
|
771
|
+
apiKeyEnv,
|
|
772
|
+
agent,
|
|
773
|
+
signal,
|
|
774
|
+
allowQuestion: false,
|
|
775
|
+
toolContext
|
|
776
|
+
})
|
|
777
|
+
finalReply = remediation.reply
|
|
778
|
+
aggregateUsage.input += remediation.usage.input || 0
|
|
779
|
+
aggregateUsage.output += remediation.usage.output || 0
|
|
780
|
+
aggregateUsage.cacheRead += remediation.usage.cacheRead || 0
|
|
781
|
+
aggregateUsage.cacheWrite += remediation.usage.cacheWrite || 0
|
|
782
|
+
toolEvents.push(...remediation.toolEvents)
|
|
783
|
+
if (isComplete(remediation.reply)) {
|
|
784
|
+
completionMarkerSeen = true
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// If gate loop exhausted without success, mark as failed
|
|
789
|
+
const postGateState = await LongAgentManager.get(sessionId)
|
|
790
|
+
if (postGateState?.status !== "completed" && gateAttempt >= maxGateAttempts) {
|
|
791
|
+
await LongAgentManager.update(sessionId, {
|
|
792
|
+
status: "failed",
|
|
793
|
+
phase: currentPhase,
|
|
794
|
+
currentGate,
|
|
795
|
+
gateStatus,
|
|
796
|
+
recoveryCount,
|
|
797
|
+
lastGateFailures,
|
|
798
|
+
iterations: iteration,
|
|
799
|
+
lastMessage: `max gate recovery attempts (${maxGateAttempts}) exceeded`
|
|
800
|
+
})
|
|
801
|
+
await markSessionStatus(sessionId, "failed")
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// --- Git: final commit + merge back to base branch ---
|
|
806
|
+
if (gitActive && gitBaseBranch && gitBranch) {
|
|
807
|
+
try {
|
|
808
|
+
await git.commitAll(`[kkcode] longagent session ${sessionId} completed`, cwd)
|
|
809
|
+
if (gitConfig.auto_merge !== false) {
|
|
810
|
+
// Hold state lock during read-status → merge to prevent TOCTOU race
|
|
811
|
+
await LongAgentManager.withLock(async () => {
|
|
812
|
+
const doneState = await LongAgentManager.get(sessionId)
|
|
813
|
+
if (doneState?.status !== "completed") return
|
|
814
|
+
await git.checkoutBranch(gitBaseBranch, cwd)
|
|
815
|
+
const mergeResult = await git.mergeBranch(gitBranch, cwd)
|
|
816
|
+
if (mergeResult.ok) {
|
|
817
|
+
await git.deleteBranch(gitBranch, cwd)
|
|
818
|
+
gateStatus.git = { ...gateStatus.git, merged: true, mergeMessage: mergeResult.message }
|
|
819
|
+
await EventBus.emit({
|
|
820
|
+
type: EVENT_TYPES.LONGAGENT_GIT_MERGED,
|
|
821
|
+
sessionId,
|
|
822
|
+
payload: { branch: gitBranch, baseBranch: gitBaseBranch, merged: true }
|
|
823
|
+
})
|
|
824
|
+
} else {
|
|
825
|
+
gateStatus.git = { ...gateStatus.git, merged: false, mergeError: mergeResult.message }
|
|
826
|
+
await EventBus.emit({
|
|
827
|
+
type: EVENT_TYPES.LONGAGENT_ALERT,
|
|
828
|
+
sessionId,
|
|
829
|
+
payload: {
|
|
830
|
+
kind: "git_merge_failed",
|
|
831
|
+
message: `Git merge failed: ${mergeResult.message}. Staying on branch "${gitBranch}" — resolve conflicts manually.`
|
|
832
|
+
}
|
|
833
|
+
})
|
|
834
|
+
const rollback = await git.checkoutBranch(gitBranch, cwd)
|
|
835
|
+
if (!rollback.ok) {
|
|
836
|
+
gateStatus.git = { ...gateStatus.git, rollbackFailed: true, rollbackError: rollback.message }
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}, cwd)
|
|
840
|
+
}
|
|
841
|
+
} catch (gitErr) {
|
|
842
|
+
gateStatus.git = { ...gateStatus.git, error: gitErr.message }
|
|
843
|
+
// Best-effort: try to return to feature branch
|
|
844
|
+
try { await git.checkoutBranch(gitBranch, cwd) } catch { /* already on it or unrecoverable */ }
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Checkpoint cleanup (same as hybrid mode)
|
|
849
|
+
try {
|
|
850
|
+
const cleanResult = await cleanupCheckpoints(sessionId, {
|
|
851
|
+
maxKeep: 10,
|
|
852
|
+
keepStageCheckpoints: true
|
|
853
|
+
})
|
|
854
|
+
} catch (cleanupErr) {
|
|
855
|
+
console.warn(`[kkcode] checkpoint cleanup failed for session ${sessionId}: ${cleanupErr.message}`)
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const done = await LongAgentManager.get(sessionId)
|
|
859
|
+
const totalElapsed = Math.round((Date.now() - startTime) / 1000)
|
|
860
|
+
const stats = stageProgressStats(taskProgress)
|
|
861
|
+
|
|
862
|
+
return {
|
|
863
|
+
sessionId,
|
|
864
|
+
turnId: `turn_long_${Date.now()}`,
|
|
865
|
+
reply: finalReply || done?.lastMessage || "longagent stopped",
|
|
866
|
+
usage: aggregateUsage,
|
|
867
|
+
toolEvents,
|
|
868
|
+
iterations: iteration,
|
|
869
|
+
recoveryCount,
|
|
870
|
+
phase: done?.phase || currentPhase,
|
|
871
|
+
gateStatus: done?.gateStatus || gateStatus,
|
|
872
|
+
currentGate: done?.currentGate || currentGate,
|
|
873
|
+
lastGateFailures: done?.lastGateFailures || lastGateFailures,
|
|
874
|
+
status: done?.status || "unknown",
|
|
875
|
+
progress: lastProgress,
|
|
876
|
+
elapsed: totalElapsed,
|
|
877
|
+
stageIndex,
|
|
878
|
+
stageCount: stagePlan?.stages?.length || 0,
|
|
879
|
+
currentStageId: stagePlan?.stages?.[Math.min(stageIndex, (stagePlan?.stages?.length || 1) - 1)]?.stageId || null,
|
|
880
|
+
planFrozen,
|
|
881
|
+
taskProgress,
|
|
882
|
+
fileChanges,
|
|
883
|
+
stageProgress: {
|
|
884
|
+
done: stats.done,
|
|
885
|
+
total: stats.total
|
|
886
|
+
},
|
|
887
|
+
remainingFilesCount: stats.remainingFilesCount
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
export async function runLongAgent(args) {
|
|
893
|
+
const longagentConfig = args?.configState?.config?.agent?.longagent || {}
|
|
894
|
+
// Runtime impl override (set via /longagent 4stage or /longagent hybrid)
|
|
895
|
+
if (args?.longagentImpl === "4stage") {
|
|
896
|
+
return run4StageLongAgent(args)
|
|
897
|
+
}
|
|
898
|
+
if (args?.longagentImpl === "hybrid") {
|
|
899
|
+
return runHybridLongAgent(args)
|
|
900
|
+
}
|
|
901
|
+
// Hybrid mode (default): Preview → Blueprint → Git → Scaffold → Coding(并行) → Debugging(回滚) → Gates → GitMerge
|
|
902
|
+
if (longagentConfig.hybrid?.enabled !== false) {
|
|
903
|
+
return runHybridLongAgent(args)
|
|
904
|
+
}
|
|
905
|
+
// 4-stage mode: Preview → Blueprint → Coding → Debugging (Mark 研究用)
|
|
906
|
+
if (longagentConfig.four_stage?.enabled === true) {
|
|
907
|
+
return run4StageLongAgent(args)
|
|
908
|
+
}
|
|
909
|
+
// Parallel mode: 降级策略
|
|
910
|
+
return runParallelLongAgent(args)
|
|
911
|
+
}
|