@bf-meishijian/aip 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/coding-run-manager.d.ts +8 -2
- package/dist/coding-run-manager.d.ts.map +1 -1
- package/dist/coding-run-manager.js +406 -300
- package/dist/coding-run-manager.js.map +1 -1
- package/dist/coding-subagent-child.d.ts +22 -0
- package/dist/coding-subagent-child.d.ts.map +1 -0
- package/dist/coding-subagent-child.js +90 -0
- package/dist/coding-subagent-child.js.map +1 -0
- package/dist/coding-subagents.d.ts +63 -0
- package/dist/coding-subagents.d.ts.map +1 -0
- package/dist/coding-subagents.js +537 -0
- package/dist/coding-subagents.js.map +1 -0
- package/dist/command-wakeup.d.ts +19 -0
- package/dist/command-wakeup.d.ts.map +1 -0
- package/dist/command-wakeup.js +109 -0
- package/dist/command-wakeup.js.map +1 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +68 -43
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-files.d.ts +12 -0
- package/dist/delivery-files.d.ts.map +1 -1
- package/dist/delivery-files.js +80 -0
- package/dist/delivery-files.js.map +1 -1
- package/dist/live-employee.d.ts +4 -2
- package/dist/live-employee.d.ts.map +1 -1
- package/dist/live-employee.js +47 -13
- package/dist/live-employee.js.map +1 -1
- package/dist/pi-message-queue.d.ts +15 -0
- package/dist/pi-message-queue.d.ts.map +1 -0
- package/dist/pi-message-queue.js +57 -0
- package/dist/pi-message-queue.js.map +1 -0
- package/dist/pi-model-service.d.ts.map +1 -1
- package/dist/platform-client.d.ts +16 -0
- package/dist/platform-client.d.ts.map +1 -1
- package/dist/platform-client.js +21 -0
- package/dist/platform-client.js.map +1 -1
- package/dist/review-report.d.ts +10 -0
- package/dist/review-report.d.ts.map +1 -0
- package/dist/review-report.js +59 -0
- package/dist/review-report.js.map +1 -0
- package/dist/stage-delivery.js +3 -2
- package/dist/state.d.ts +2 -2
- package/dist/state.js +2 -2
- package/package.json +11 -7
- package/dist/agent-harness/index.d.ts +0 -4
- package/dist/agent-harness/index.d.ts.map +0 -1
- package/dist/agent-harness/index.js +0 -4
- package/dist/agent-harness/index.js.map +0 -1
- package/dist/agent-harness/pi-chat-model.d.ts +0 -35
- package/dist/agent-harness/pi-chat-model.d.ts.map +0 -1
- package/dist/agent-harness/pi-chat-model.js +0 -285
- package/dist/agent-harness/pi-chat-model.js.map +0 -1
|
@@ -6,11 +6,14 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { createAgentSession, DefaultResourceLoader, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { Type } from "typebox";
|
|
9
|
-
import {
|
|
9
|
+
import { createCodingSubagents } from "./coding-subagents.js";
|
|
10
|
+
import { collectDeliveryFiles, deliveryDirectory, initializeDeliveryFiles, publishReviewMaterials, } from "./delivery-files.js";
|
|
10
11
|
import { liveEmployee } from "./live-employee.js";
|
|
11
12
|
import { prepareMergeRequest, registerMergeRequest, } from "./merge-requests.js";
|
|
12
13
|
import { paths } from "./paths.js";
|
|
14
|
+
import { PiMessageQueue } from "./pi-message-queue.js";
|
|
13
15
|
import { PlatformClientError } from "./platform-client.js";
|
|
16
|
+
import { materializeReviewReport } from "./review-report.js";
|
|
14
17
|
import { readJson, writeSecureJson } from "./secure-json.js";
|
|
15
18
|
import { DELIVERY_FILES, STAGE_DELIVERY_PROTOCOLS, stageEmployeeIds } from "./stage-delivery.js";
|
|
16
19
|
const execFileAsync = promisify(execFile);
|
|
@@ -27,6 +30,25 @@ export class CodingRunManager {
|
|
|
27
30
|
async execute(client, host, command) {
|
|
28
31
|
if (!command.run)
|
|
29
32
|
throw new Error("CODING_RUN_COMMAND_INVALID");
|
|
33
|
+
if (command.run.action === "REVIEW_REPORT") {
|
|
34
|
+
const report = command.run.reviewReport;
|
|
35
|
+
if (!report || !command.run.runInput)
|
|
36
|
+
throw new Error("REVIEW_REPORT_INPUT_MISSING");
|
|
37
|
+
const files = await materializeReviewReport(join(paths().runWorktrees, command.run.runId), command.run.runInput, report.files);
|
|
38
|
+
await client.acknowledgeReviewReport(command.run.runId, {
|
|
39
|
+
hostId: host.hostId,
|
|
40
|
+
generation: host.generation,
|
|
41
|
+
turnId: command.run.turnId,
|
|
42
|
+
roundId: report.roundId,
|
|
43
|
+
files,
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (command.run.action === "RECALL_QUEUE") {
|
|
48
|
+
return {
|
|
49
|
+
recalledMessageIds: this.active.get(command.run.runId)?.observed.messageQueue?.recall() ?? [],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
30
52
|
const imageRun = command.run;
|
|
31
53
|
let images;
|
|
32
54
|
try {
|
|
@@ -41,6 +63,7 @@ export class CodingRunManager {
|
|
|
41
63
|
if (active?.turnId === imageRun.turnId) {
|
|
42
64
|
active.stopping = true;
|
|
43
65
|
active.observed.stopping = true;
|
|
66
|
+
await active.subagents?.stop();
|
|
44
67
|
await active.session.abort();
|
|
45
68
|
await active.finished;
|
|
46
69
|
if (this.active.get(imageRun.runId) === active)
|
|
@@ -52,7 +75,18 @@ export class CodingRunManager {
|
|
|
52
75
|
const input = command.run.runInput;
|
|
53
76
|
await this.start(client, host, command.run.runId, command.run.turnId, input, false, command.run.contentMarkdown, images);
|
|
54
77
|
}
|
|
55
|
-
else if (command.run.action === "
|
|
78
|
+
else if (command.run.action === "CONTINUE") {
|
|
79
|
+
const active = this.active.get(command.run.runId);
|
|
80
|
+
if (active) {
|
|
81
|
+
await active.finished;
|
|
82
|
+
if (active.stopping || active.observed.userIntervened)
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (!command.run.runInput || !command.run.contentMarkdown || !command.run.continuation)
|
|
86
|
+
throw new Error("CODING_RUN_CONTINUATION_MISSING");
|
|
87
|
+
await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, [], command.run.continuation);
|
|
88
|
+
}
|
|
89
|
+
else if (command.run.action === "STEER" || command.run.action === "FOLLOW_UP") {
|
|
56
90
|
let active = this.active.get(command.run.runId);
|
|
57
91
|
if (!command.run.contentMarkdown && images.length === 0)
|
|
58
92
|
throw new Error("CODING_RUN_MESSAGE_MISSING");
|
|
@@ -61,6 +95,13 @@ export class CodingRunManager {
|
|
|
61
95
|
throw new Error("CODING_RUN_TURN_MISMATCH");
|
|
62
96
|
active.stopping = true;
|
|
63
97
|
active.observed.stopping = true;
|
|
98
|
+
await active.emit("EXECUTION_DECIDED", {
|
|
99
|
+
phase: active.input.phase,
|
|
100
|
+
decision: "SKIP",
|
|
101
|
+
reason: "SUPERSEDED",
|
|
102
|
+
stopReason: "SUPERSEDED",
|
|
103
|
+
});
|
|
104
|
+
await active.subagents?.stop();
|
|
64
105
|
await active.session.abort();
|
|
65
106
|
if (this.active.get(command.run.runId) === active)
|
|
66
107
|
this.active.delete(command.run.runId);
|
|
@@ -69,17 +110,48 @@ export class CodingRunManager {
|
|
|
69
110
|
if (active) {
|
|
70
111
|
if (images.length > 0 && !active.supportsImages)
|
|
71
112
|
throw new Error("MODEL_IMAGES_UNSUPPORTED");
|
|
72
|
-
active.observed.
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
113
|
+
active.observed.userIntervened = true;
|
|
114
|
+
const handoff = active.observed.awaitingDecision || active.session.isIdle;
|
|
115
|
+
if (handoff)
|
|
116
|
+
active.observed.handingOffInput = true;
|
|
117
|
+
await active.emit("AUTO_CONTINUATION_CHANGED", {
|
|
118
|
+
phase: active.input.phase,
|
|
119
|
+
autoContinueEnabled: false,
|
|
120
|
+
reason: "USER_MESSAGE",
|
|
121
|
+
source: "HOST_HANDLED",
|
|
122
|
+
});
|
|
123
|
+
if (handoff || active.session.isIdle || active.observed.awaitingDecision) {
|
|
124
|
+
active.observed.handingOffInput = true;
|
|
125
|
+
// Pi is idle: steer only queues input and cannot start a new model turn.
|
|
126
|
+
// Finish the old judge operation before opening the user turn.
|
|
127
|
+
await active.finished;
|
|
128
|
+
if (!command.run.runInput)
|
|
129
|
+
throw new Error("CODING_RUN_INPUT_MISSING");
|
|
130
|
+
await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
|
|
131
|
+
if (command.run.messageId)
|
|
132
|
+
await this.active.get(command.run.runId)?.emit("MESSAGE_QUEUE_UPDATED", {
|
|
133
|
+
messageId: command.run.messageId,
|
|
134
|
+
deliveryStatus: "READ",
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (!command.run.messageId)
|
|
139
|
+
throw new Error("CODING_RUN_MESSAGE_ID_MISSING");
|
|
140
|
+
await active.observed.messageQueue.submit(command.run.messageId, command.run.action, command.run.contentMarkdown ?? "", images.length ? images : undefined);
|
|
141
|
+
await active.emit("MESSAGE_QUEUE_UPDATED", {
|
|
142
|
+
messageId: command.run.messageId,
|
|
143
|
+
deliveryStatus: "QUEUED",
|
|
144
|
+
});
|
|
78
145
|
}
|
|
79
146
|
else {
|
|
80
147
|
if (!command.run.runInput)
|
|
81
148
|
throw new Error("CODING_RUN_INPUT_MISSING");
|
|
82
149
|
await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
|
|
150
|
+
if (command.run.messageId)
|
|
151
|
+
await this.active.get(command.run.runId)?.emit("MESSAGE_QUEUE_UPDATED", {
|
|
152
|
+
messageId: command.run.messageId,
|
|
153
|
+
deliveryStatus: "READ",
|
|
154
|
+
});
|
|
83
155
|
}
|
|
84
156
|
}
|
|
85
157
|
else if (command.run.action === "STOP") {
|
|
@@ -89,34 +161,55 @@ export class CodingRunManager {
|
|
|
89
161
|
throw new Error("CODING_RUN_TURN_MISMATCH");
|
|
90
162
|
active.stopping = true;
|
|
91
163
|
active.observed.stopping = true;
|
|
164
|
+
await active.subagents?.stop();
|
|
92
165
|
await active.session.abort();
|
|
166
|
+
await active.emit("AUTO_CONTINUATION_CHANGED", {
|
|
167
|
+
phase: active.input.phase,
|
|
168
|
+
autoContinueEnabled: false,
|
|
169
|
+
reason: "USER_STOP",
|
|
170
|
+
source: "HOST_HANDLED",
|
|
171
|
+
});
|
|
93
172
|
await active.emit.flush();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
active.observed.activeTasks.clear();
|
|
102
|
-
await active.emit("RUN_STOPPED", {});
|
|
173
|
+
await active.emit("EXECUTION_DECIDED", {
|
|
174
|
+
phase: active.input.phase,
|
|
175
|
+
decision: "SKIP",
|
|
176
|
+
reason: "STOP_REQUESTED",
|
|
177
|
+
stopReason: "USER_STOPPED",
|
|
178
|
+
});
|
|
179
|
+
await active.emit("RUN_STOPPED", { phase: active.input.phase, reason: "USER_STOPPED" });
|
|
103
180
|
}
|
|
104
181
|
else {
|
|
105
182
|
const stateFile = join(paths().runState, `${command.run.runId}.json`);
|
|
106
183
|
const state = await readJson(stateFile);
|
|
107
|
-
await eventEmitter(client, host, command.run.runId, command.run.turnId, state?.lastEventSequence ?? 0, (sequence) => writeSecureJson(stateFile, { ...state, lastEventSequence: sequence }), (value) => redactHostText(value, [paths().root]))("RUN_STOPPED", {
|
|
184
|
+
await eventEmitter(client, host, command.run.runId, command.run.turnId, state?.lastEventSequence ?? 0, (sequence) => writeSecureJson(stateFile, { ...state, lastEventSequence: sequence }), (value) => redactHostText(value, [paths().root]))("RUN_STOPPED", {
|
|
185
|
+
phase: state?.phase,
|
|
186
|
+
reason: "USER_STOPPED",
|
|
187
|
+
reminderDecision: "NO_REMINDER",
|
|
188
|
+
});
|
|
108
189
|
}
|
|
109
190
|
}
|
|
110
191
|
else if (command.run.action === "RESUME") {
|
|
111
192
|
await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
|
|
112
193
|
}
|
|
194
|
+
if (command.run.messageId &&
|
|
195
|
+
["START", "RESUME", "STEER", "FOLLOW_UP"].includes(command.run.action)) {
|
|
196
|
+
const active = this.active.get(command.run.runId);
|
|
197
|
+
if (active &&
|
|
198
|
+
(command.run.action === "RESUME" ||
|
|
199
|
+
command.run.action === "START" ||
|
|
200
|
+
!active.observed.messageQueue))
|
|
201
|
+
await active.emit("MESSAGE_QUEUE_UPDATED", {
|
|
202
|
+
messageId: command.run.messageId,
|
|
203
|
+
deliveryStatus: "READ",
|
|
204
|
+
});
|
|
205
|
+
}
|
|
113
206
|
}
|
|
114
|
-
async start(client, host, runId, turnId, input, resume, resumePrompt, images = []) {
|
|
207
|
+
async start(client, host, runId, turnId, input, resume, resumePrompt, images = [], continuation) {
|
|
115
208
|
const active = this.active.get(runId);
|
|
116
209
|
if (active) {
|
|
117
210
|
if (active.turnId === turnId)
|
|
118
211
|
return;
|
|
119
|
-
if (!active.observed.terminalReached)
|
|
212
|
+
if (!active.observed.terminalReached && !active.stopping)
|
|
120
213
|
throw new Error("CODING_RUN_TURN_MISMATCH");
|
|
121
214
|
await active.finished;
|
|
122
215
|
if (this.active.get(runId) === active)
|
|
@@ -135,9 +228,19 @@ export class CodingRunManager {
|
|
|
135
228
|
Object.keys(storedState.phaseSessions).length &&
|
|
136
229
|
storedState.harness !== "pi")
|
|
137
230
|
throw new Error("PRIOR_HARNESS_SESSION_UNSUPPORTED:请新建执行,历史文件和工作目录已保留。");
|
|
138
|
-
const cwd = await prepareProjects(local.repositoryCache, runRoot, runId, input.projects, Boolean(storedState?.prepared));
|
|
139
231
|
for (const project of input.projects)
|
|
140
|
-
project.devBranch ||=
|
|
232
|
+
project.devBranch ||=
|
|
233
|
+
storedState?.mrContext?.projects.find((p) => p.id === project.id)?.devBranch ||
|
|
234
|
+
`aip/${input.workItem.key}`;
|
|
235
|
+
const cwd = await prepareProjects(local.repositoryCache, runRoot, input.workItem.key, input.projects, Boolean(storedState?.prepared));
|
|
236
|
+
const preparationBaselines = storedState?.preparationBaselines ?? {};
|
|
237
|
+
if (input.phase === "PREPARATION" && !storedState?.prepared) {
|
|
238
|
+
for (const project of input.projects)
|
|
239
|
+
preparationBaselines[project.id] ??= (await execFileAsync("git", ["rev-parse", "HEAD"], {
|
|
240
|
+
cwd: join(runRoot, safeName(project.name)),
|
|
241
|
+
})).stdout.trim();
|
|
242
|
+
await writeSecureJson(stateFile, { ...storedState, preparationBaselines });
|
|
243
|
+
}
|
|
141
244
|
if (!storedState?.prepared || input.inputConflict)
|
|
142
245
|
await initializeDeliveryFiles(runRoot, input, Boolean(storedState?.prepared));
|
|
143
246
|
const deliveryInputHash = createHash("sha256")
|
|
@@ -147,7 +250,7 @@ export class CodingRunManager {
|
|
|
147
250
|
input.technicalDesign?.content,
|
|
148
251
|
]))
|
|
149
252
|
.digest("hex");
|
|
150
|
-
const
|
|
253
|
+
const delegations = storedState?.delegations ?? {};
|
|
151
254
|
let pendingDelivery = storedState?.phase !== input.phase ||
|
|
152
255
|
resumePrompt ||
|
|
153
256
|
images.length > 0 ||
|
|
@@ -166,8 +269,7 @@ export class CodingRunManager {
|
|
|
166
269
|
let sessionRef = phaseSessions[sessionPhase];
|
|
167
270
|
let lastEventSequence = storedState?.lastEventSequence ?? 0;
|
|
168
271
|
const observed = {
|
|
169
|
-
|
|
170
|
-
memberResults: new Map(),
|
|
272
|
+
userIntervened: input.autoContinueEnabled === false,
|
|
171
273
|
activeTasks: new Map(),
|
|
172
274
|
stopping: false,
|
|
173
275
|
terminalReached: false,
|
|
@@ -182,12 +284,12 @@ export class CodingRunManager {
|
|
|
182
284
|
primaryProjectId: input.primaryProjectId,
|
|
183
285
|
},
|
|
184
286
|
prepared: true,
|
|
287
|
+
preparationBaselines,
|
|
185
288
|
phaseSessions,
|
|
186
289
|
sessionRef,
|
|
187
290
|
harness: "pi",
|
|
188
291
|
lastEventSequence,
|
|
189
|
-
|
|
190
|
-
childSessions,
|
|
292
|
+
delegations,
|
|
191
293
|
pendingDelivery,
|
|
192
294
|
deliveryInputHash,
|
|
193
295
|
});
|
|
@@ -197,15 +299,15 @@ export class CodingRunManager {
|
|
|
197
299
|
return persistState();
|
|
198
300
|
}, redact);
|
|
199
301
|
if (input.phase === "PREPARATION") {
|
|
302
|
+
if (!storedState?.prepared)
|
|
303
|
+
await publishReviewMaterials(runRoot, input);
|
|
200
304
|
await persistState();
|
|
201
305
|
await emit("PREPARATION_COMPLETED", {
|
|
202
306
|
primaryProjectId: input.primaryProjectId,
|
|
203
307
|
projects: await Promise.all(input.projects.map(async (p) => ({
|
|
204
308
|
projectId: p.id,
|
|
205
309
|
projectName: p.name,
|
|
206
|
-
baselineCommit:
|
|
207
|
-
cwd: join(runRoot, safeName(p.name)),
|
|
208
|
-
})).stdout.trim(),
|
|
310
|
+
baselineCommit: preparationBaselines[p.id] ?? p.baselineCommit,
|
|
209
311
|
directory: join(runRoot, safeName(p.name)),
|
|
210
312
|
branch: (await execFileAsync("git", ["branch", "--show-current"], {
|
|
211
313
|
cwd: join(runRoot, safeName(p.name)),
|
|
@@ -214,10 +316,10 @@ export class CodingRunManager {
|
|
|
214
316
|
});
|
|
215
317
|
return;
|
|
216
318
|
}
|
|
217
|
-
const terminal = terminalTools(client, host, runId, turnId, runRoot, input, observed,
|
|
319
|
+
const terminal = terminalTools(client, host, runId, turnId, runRoot, input, observed, async (batch) => {
|
|
218
320
|
pendingDelivery = batch;
|
|
219
321
|
await persistState();
|
|
220
|
-
}, () => pendingDelivery, (
|
|
322
|
+
}, () => pendingDelivery, () => emit.flush());
|
|
221
323
|
if (!this.pi.resolveRuntimeModel)
|
|
222
324
|
throw new Error("MODEL_RUNTIME_UNAVAILABLE");
|
|
223
325
|
const resolved = await this.pi.resolveRuntimeModel(input.providerId, input.modelId, {
|
|
@@ -235,119 +337,41 @@ export class CodingRunManager {
|
|
|
235
337
|
member: "supervisor",
|
|
236
338
|
};
|
|
237
339
|
const availableAgents = stageEmployeeIds(input);
|
|
238
|
-
const live = liveEmployee(client, runId, binding, await client.employeeConfig(runId, { ...binding, purpose: "LOAD" }), runRoot,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
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
|
-
const childLive = liveEmployee(client, runId, childBinding, await client.employeeConfig(runId, { ...childBinding, purpose: "LOAD" }), runRoot, true, false, input.phase, () => observed.memberResults.has(member));
|
|
272
|
-
const resultExtension = (pi) => {
|
|
273
|
-
pi.registerTool({
|
|
274
|
-
name: "submit_member_result",
|
|
275
|
-
label: "提交成员结果",
|
|
276
|
-
description: "如实记录本次子任务结果;未解决的问题必须 FAIL。仅向主管汇报,不确认最终交付。",
|
|
277
|
-
parameters: Type.Object({
|
|
278
|
-
status: Type.Union([Type.Literal("PASS"), Type.Literal("FAIL")]),
|
|
279
|
-
summary: Type.String({ minLength: 1 }),
|
|
280
|
-
}),
|
|
281
|
-
async execute(_id, result) {
|
|
282
|
-
const revisions = await captureProjectRevisions(runRoot, input.projects, input.workItem.key, input.primaryProjectId);
|
|
283
|
-
observed.memberResults.set(member, { ...result, revisions });
|
|
284
|
-
queueMicrotask(() => child?.agent.abort());
|
|
285
|
-
return toolResult("结果已记录,向主管返回结论。");
|
|
286
|
-
},
|
|
287
|
-
});
|
|
288
|
-
};
|
|
289
|
-
const opened = await openPiSession({
|
|
290
|
-
cwd,
|
|
291
|
-
sessionDir: local.piSessions,
|
|
292
|
-
agentDir: local.piAgent,
|
|
293
|
-
existingId: childSessions[key],
|
|
294
|
-
modelRuntime: resolved.runtime,
|
|
295
|
-
model: resolved.model,
|
|
296
|
-
thinkingLevel: input.thinkingLevel,
|
|
297
|
-
extensions: [childLive.extension, resultExtension],
|
|
298
|
-
systemPrompt: "当前为团队子成员会话,员工配置由平台下发。",
|
|
299
|
-
runId,
|
|
300
|
-
});
|
|
301
|
-
childLive.attach(opened.session);
|
|
302
|
-
child = opened.session;
|
|
303
|
-
childSessions[key] = opened.sessionId;
|
|
304
|
-
childInstances.set(member, child);
|
|
305
|
-
subscribeSession(child, (kind, payload) => {
|
|
306
|
-
if (kind === "TEXT_DELTA")
|
|
307
|
-
childUpdates.get(member)?.(String(payload.text ?? ""));
|
|
308
|
-
return emit(kind, {
|
|
309
|
-
...payload,
|
|
310
|
-
phase: input.phase,
|
|
311
|
-
agentId: member,
|
|
312
|
-
parentActivityKey: childTasks.get(member),
|
|
313
|
-
sessionId: opened.sessionId,
|
|
314
|
-
});
|
|
315
|
-
}, observed, redact);
|
|
316
|
-
await persistState();
|
|
317
|
-
}
|
|
318
|
-
await emit("SUBAGENT_UPDATED", {
|
|
319
|
-
phase: input.phase,
|
|
320
|
-
task: params.task,
|
|
321
|
-
activityKey: callId,
|
|
322
|
-
agentId: member,
|
|
323
|
-
sessionRef: childSessions[key],
|
|
324
|
-
summary: "独立子会话已就绪",
|
|
325
|
-
});
|
|
326
|
-
const abort = () => {
|
|
327
|
-
child?.agent.abort();
|
|
328
|
-
};
|
|
329
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
330
|
-
try {
|
|
331
|
-
signal?.throwIfAborted();
|
|
332
|
-
await child.prompt(`${initialPrompt(input, runRoot, false)}\n\n本次主管分派给 ${member} 的任务:${params.task}\n只完成自身职责,最后调用 submit_member_result;主管负责其他成员协调和最终交付。`);
|
|
333
|
-
await child.waitForIdle();
|
|
334
|
-
if (signal?.aborted || observed.stopping)
|
|
335
|
-
throw new Error("SUBAGENT_STOPPED");
|
|
336
|
-
const result = observed.memberResults.get(member);
|
|
337
|
-
if (!result)
|
|
338
|
-
throw new Error("SUBAGENT_RESULT_MISSING");
|
|
339
|
-
return {
|
|
340
|
-
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
341
|
-
details: { agent: member, status: result.status },
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
finally {
|
|
345
|
-
childUpdates.delete(member);
|
|
346
|
-
signal?.removeEventListener("abort", abort);
|
|
347
|
-
}
|
|
340
|
+
const live = liveEmployee(client, runId, binding, await client.employeeConfig(runId, { ...binding, purpose: "LOAD" }), runRoot, Boolean(input.team), input.executionPurpose === "RELEASE", input.phase, () => observed.terminalReached, executionInstructions, () => {
|
|
341
|
+
observed.terminalClosed = true;
|
|
342
|
+
});
|
|
343
|
+
const delegation = input.executionPurpose === "RELEASE"
|
|
344
|
+
? undefined
|
|
345
|
+
: await createCodingSubagents({
|
|
346
|
+
client,
|
|
347
|
+
config: {
|
|
348
|
+
runId,
|
|
349
|
+
turnId,
|
|
350
|
+
hostId: host.hostId,
|
|
351
|
+
generation: host.generation,
|
|
352
|
+
runRoot,
|
|
353
|
+
providerId: input.providerId,
|
|
354
|
+
modelId: input.modelId,
|
|
355
|
+
phase: input.phase,
|
|
356
|
+
},
|
|
357
|
+
cwd,
|
|
358
|
+
directory: join(local.runState, runId, "subagents", input.phase),
|
|
359
|
+
team: Boolean(input.team),
|
|
360
|
+
agents: availableAgents.map((id) => ({
|
|
361
|
+
id,
|
|
362
|
+
name: input.teamEmployees?.[id]?.name ?? id,
|
|
363
|
+
description: input.teamEmployees?.[id]?.description ?? "",
|
|
364
|
+
})),
|
|
365
|
+
records: delegations,
|
|
366
|
+
emit,
|
|
367
|
+
persist: persistState,
|
|
368
|
+
started(activityKey, agentId) {
|
|
369
|
+
observed.activeTasks.set(activityKey, agentId);
|
|
370
|
+
},
|
|
371
|
+
async completed(activityKey) {
|
|
372
|
+
observed.activeTasks.delete(activityKey);
|
|
348
373
|
},
|
|
349
374
|
});
|
|
350
|
-
};
|
|
351
375
|
const opened = await openPiSession({
|
|
352
376
|
cwd,
|
|
353
377
|
sessionDir: local.piSessions,
|
|
@@ -356,7 +380,7 @@ export class CodingRunManager {
|
|
|
356
380
|
modelRuntime: resolved.runtime,
|
|
357
381
|
model: resolved.model,
|
|
358
382
|
thinkingLevel: input.thinkingLevel,
|
|
359
|
-
extensions: [...(
|
|
383
|
+
extensions: [...(delegation ? [delegation.extension] : []), terminal, live.extension],
|
|
360
384
|
systemPrompt: input.team
|
|
361
385
|
? "当前为团队主负责人会话,员工配置由平台下发。"
|
|
362
386
|
: "当前为工单主会话,员工配置由平台下发。",
|
|
@@ -365,6 +389,7 @@ export class CodingRunManager {
|
|
|
365
389
|
live.attach(opened.session);
|
|
366
390
|
const session = opened.session;
|
|
367
391
|
sessionRef = opened.sessionId;
|
|
392
|
+
observed.messageQueue = new PiMessageQueue(session);
|
|
368
393
|
subscribeSession(session, (kind, payload) => {
|
|
369
394
|
const activityKey = String(payload.activityKey ?? "");
|
|
370
395
|
if (kind === "SUBAGENT_STARTED")
|
|
@@ -386,29 +411,62 @@ export class CodingRunManager {
|
|
|
386
411
|
emit,
|
|
387
412
|
stopping: false,
|
|
388
413
|
observed,
|
|
414
|
+
...(delegation ? { subagents: delegation } : {}),
|
|
389
415
|
};
|
|
390
416
|
this.active.set(runId, activeRun);
|
|
391
417
|
phaseSessions[sessionPhase] = sessionRef;
|
|
392
418
|
await persistState();
|
|
393
419
|
await emit("RUN_STARTED", { sessionRef, phase: input.phase });
|
|
394
|
-
if (
|
|
420
|
+
if (!continuation &&
|
|
421
|
+
(input.autoContinueEnabled === false || (!resume && input.autoContinueEnabled === true))) {
|
|
422
|
+
await emit("AUTO_CONTINUATION_CHANGED", {
|
|
423
|
+
phase: input.phase,
|
|
424
|
+
autoContinueEnabled: input.autoContinueEnabled === true,
|
|
425
|
+
reason: input.autoContinueEnabled === true ? "SYSTEM_STAGE" : "USER_MESSAGE",
|
|
426
|
+
source: "HOST_HANDLED",
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
if (!continuation && (resumePrompt || images.length > 0)) {
|
|
395
430
|
await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
|
|
396
431
|
await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
|
|
397
432
|
}
|
|
398
|
-
const
|
|
399
|
-
|
|
433
|
+
const conversationFingerprint = await workspaceFingerprint(runRoot, input);
|
|
434
|
+
const prompt = continuation
|
|
435
|
+
? resumePrompt
|
|
436
|
+
: `${initialPrompt(input, runRoot)}\n\n${executionInstructions()}${resumePrompt ? `\n\n用户本次消息:${resumePrompt}` : resume && !resumePrompt ? "\n\n继续当前会话未完成的工作,先读取最新文件和交接结果。" : ""}`;
|
|
437
|
+
activeRun.finished = driveToTerminal(session, prompt, observed, client, host, runId, turnId, emit, input.phase, images, async () => {
|
|
438
|
+
await emit("CONVERSATION_COMPLETED", {
|
|
439
|
+
workspaceChanged: conversationFingerprint !== (await workspaceFingerprint(runRoot, input)),
|
|
440
|
+
});
|
|
441
|
+
}, continuation)
|
|
400
442
|
.catch(async (error) => {
|
|
401
443
|
if (activeRun.stopping)
|
|
402
444
|
return;
|
|
445
|
+
await emit("EXECUTION_DECIDED", {
|
|
446
|
+
phase: input.phase,
|
|
447
|
+
decision: "SKIP",
|
|
448
|
+
reason: "MODEL_ERROR",
|
|
449
|
+
stopReason: "error",
|
|
450
|
+
});
|
|
403
451
|
await emit("RUN_FAILED", {
|
|
404
|
-
|
|
452
|
+
phase: input.phase,
|
|
453
|
+
code: error instanceof Error && /^[A-Z][A-Z0-9_]+$/u.test(error.message)
|
|
454
|
+
? error.message.slice(0, 200)
|
|
455
|
+
: "PI_RUN_FAILED",
|
|
456
|
+
message: redact(error instanceof Error ? error.message : String(error)),
|
|
405
457
|
});
|
|
406
458
|
})
|
|
407
459
|
.finally(async () => {
|
|
408
460
|
await emit.flush();
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
461
|
+
try {
|
|
462
|
+
await delegation?.stop();
|
|
463
|
+
await delegation?.flush();
|
|
464
|
+
}
|
|
465
|
+
finally {
|
|
466
|
+
await delegation?.dispose();
|
|
467
|
+
await session.extensionRunner?.emit({ type: "session_shutdown", reason: "quit" });
|
|
468
|
+
session.dispose();
|
|
469
|
+
}
|
|
412
470
|
if (this.active.get(runId) === activeRun)
|
|
413
471
|
this.active.delete(runId);
|
|
414
472
|
});
|
|
@@ -461,7 +519,9 @@ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistS
|
|
|
461
519
|
let sequence = initialSequence;
|
|
462
520
|
let queue = Promise.resolve();
|
|
463
521
|
const emit = (kind, payload) => {
|
|
464
|
-
queue = queue
|
|
522
|
+
queue = queue
|
|
523
|
+
.catch(() => { })
|
|
524
|
+
.then(async () => {
|
|
465
525
|
const event = {
|
|
466
526
|
eventId: crypto.randomUUID(),
|
|
467
527
|
sequence: sequence + 1,
|
|
@@ -484,11 +544,16 @@ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistS
|
|
|
484
544
|
catch (error) {
|
|
485
545
|
if (error instanceof PlatformClientError &&
|
|
486
546
|
error.status === 409 &&
|
|
487
|
-
error.code === "CODING_EVENT_TURN_STALE")
|
|
547
|
+
error.code === "CODING_EVENT_TURN_STALE") {
|
|
548
|
+
if (["EXECUTION_STARTED", "EXECUTION_PAUSED", "CONVERSATION_COMPLETED"].includes(kind))
|
|
549
|
+
throw error;
|
|
488
550
|
return;
|
|
551
|
+
}
|
|
489
552
|
process.stderr.write(`[local-coding-host] 事件上报失败:${redact(error instanceof Error ? error.message : String(error))}\n`);
|
|
490
553
|
if (attempt === EVENT_APPEND_MAX_ATTEMPTS) {
|
|
491
554
|
process.stderr.write(`[local-coding-host] 事件上报放弃:${event.kind} sequence=${event.sequence} attempts=${attempt}\n`);
|
|
555
|
+
if (["EXECUTION_STARTED", "EXECUTION_PAUSED", "CONVERSATION_COMPLETED"].includes(kind))
|
|
556
|
+
throw error;
|
|
492
557
|
return;
|
|
493
558
|
}
|
|
494
559
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
@@ -558,6 +623,7 @@ async function openPiSession(options) {
|
|
|
558
623
|
resourceLoader: loader,
|
|
559
624
|
sessionManager,
|
|
560
625
|
});
|
|
626
|
+
await session.bindExtensions({ mode: "print" });
|
|
561
627
|
session.setAutoCompactionEnabled(true);
|
|
562
628
|
return { session, sessionId };
|
|
563
629
|
}
|
|
@@ -588,17 +654,22 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
588
654
|
void emit(update.kind, update.payload);
|
|
589
655
|
};
|
|
590
656
|
session.subscribe((event) => {
|
|
591
|
-
if (event.type === "
|
|
657
|
+
if (event.type === "agent_start") {
|
|
658
|
+
if (observed.messageQueue)
|
|
659
|
+
void emit("MESSAGE_QUEUE_UPDATED", { mainAgentRunning: true });
|
|
660
|
+
}
|
|
661
|
+
else if (event.type === "agent_settled") {
|
|
662
|
+
if (observed.messageQueue)
|
|
663
|
+
void emit("MESSAGE_QUEUE_UPDATED", { mainAgentRunning: false });
|
|
664
|
+
}
|
|
665
|
+
else if (event.type === "message_start" && event.message.role === "assistant") {
|
|
592
666
|
messageId = crypto.randomUUID();
|
|
593
667
|
void emit("MESSAGE_STARTED", { messageId });
|
|
594
668
|
}
|
|
595
669
|
else if (event.type === "message_start" && event.message.role === "user") {
|
|
596
|
-
const
|
|
597
|
-
if (
|
|
598
|
-
(
|
|
599
|
-
delete observed.pendingSteerText;
|
|
600
|
-
void emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
|
|
601
|
-
}
|
|
670
|
+
const id = observed.messageQueue?.consumed(event.message);
|
|
671
|
+
if (id)
|
|
672
|
+
void emit("MESSAGE_QUEUE_UPDATED", { messageId: id, deliveryStatus: "READ" });
|
|
602
673
|
}
|
|
603
674
|
else if (event.type === "agent_end") {
|
|
604
675
|
observed.nativeWillRetry = event.willRetry === true;
|
|
@@ -619,65 +690,38 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
619
690
|
}
|
|
620
691
|
else if (event.type === "tool_execution_start") {
|
|
621
692
|
flushText();
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
summary: String(event.args?.task ?? "团队子任务开始"),
|
|
630
|
-
task: String(event.args?.task ?? ""),
|
|
631
|
-
}
|
|
632
|
-
: {
|
|
633
|
-
activityKey: event.toolCallId,
|
|
634
|
-
toolName: event.toolName,
|
|
635
|
-
summary: `执行 ${event.toolName}`,
|
|
636
|
-
});
|
|
693
|
+
if (event.toolName === "subagent")
|
|
694
|
+
return;
|
|
695
|
+
void emit("TOOL_STARTED", {
|
|
696
|
+
activityKey: event.toolCallId,
|
|
697
|
+
toolName: event.toolName,
|
|
698
|
+
summary: `执行 ${event.toolName}`,
|
|
699
|
+
});
|
|
637
700
|
}
|
|
638
701
|
else if (event.type === "tool_execution_update") {
|
|
702
|
+
if (event.toolName === "subagent")
|
|
703
|
+
return;
|
|
639
704
|
pendingToolUpdates.set(event.toolCallId, {
|
|
640
|
-
kind:
|
|
641
|
-
payload:
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
}
|
|
647
|
-
: {
|
|
648
|
-
activityKey: event.toolCallId,
|
|
649
|
-
toolName: event.toolName,
|
|
650
|
-
currentAction: redact(textFromResult(event.partialResult)),
|
|
651
|
-
},
|
|
705
|
+
kind: "TOOL_UPDATED",
|
|
706
|
+
payload: {
|
|
707
|
+
activityKey: event.toolCallId,
|
|
708
|
+
toolName: event.toolName,
|
|
709
|
+
currentAction: redact(textFromResult(event.partialResult)),
|
|
710
|
+
},
|
|
652
711
|
});
|
|
653
712
|
if (!toolUpdateTimers.has(event.toolCallId))
|
|
654
713
|
toolUpdateTimers.set(event.toolCallId, setTimeout(() => flushToolUpdate(event.toolCallId), 200));
|
|
655
714
|
}
|
|
656
715
|
else if (event.type === "tool_execution_end") {
|
|
657
|
-
flushToolUpdate(event.toolCallId);
|
|
658
|
-
const member = observed.activeTasks.get(event.toolCallId);
|
|
659
716
|
if (event.toolName === "subagent")
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
? {
|
|
669
|
-
activityKey: event.toolCallId,
|
|
670
|
-
agentId: String(event.result?.details?.agent ??
|
|
671
|
-
member ??
|
|
672
|
-
""),
|
|
673
|
-
summary: redact(textFromResult(event.result)),
|
|
674
|
-
}
|
|
675
|
-
: {
|
|
676
|
-
activityKey: event.toolCallId,
|
|
677
|
-
toolName: event.toolName,
|
|
678
|
-
isError: event.isError === true,
|
|
679
|
-
summary: redact(textFromResult(event.result)),
|
|
680
|
-
});
|
|
717
|
+
return;
|
|
718
|
+
flushToolUpdate(event.toolCallId);
|
|
719
|
+
void emit("TOOL_COMPLETED", {
|
|
720
|
+
activityKey: event.toolCallId,
|
|
721
|
+
toolName: event.toolName,
|
|
722
|
+
isError: event.isError === true,
|
|
723
|
+
summary: redact(textFromResult(event.result)),
|
|
724
|
+
});
|
|
681
725
|
}
|
|
682
726
|
else if (event.type === "bash_execution_update" && event.delta) {
|
|
683
727
|
const activityKey = event.id ?? "bash";
|
|
@@ -750,60 +794,119 @@ function textFromResult(value) {
|
|
|
750
794
|
return "";
|
|
751
795
|
return textFromMessage(value.content);
|
|
752
796
|
}
|
|
753
|
-
async function driveToTerminal(session, initial, observed, client, host, runId, turnId, emit,
|
|
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
|
-
|
|
797
|
+
async function driveToTerminal(session, initial, observed, client, host, runId, turnId, emit, phase, images = [], completeConversation = async () => { }, continuation) {
|
|
798
|
+
if (!session.model?.input.includes("image") &&
|
|
799
|
+
session.messages?.some((message) => "content" in message &&
|
|
800
|
+
Array.isArray(message.content) &&
|
|
801
|
+
message.content.some((block) => block.type === "image")))
|
|
802
|
+
throw new Error("MODEL_IMAGES_UNSUPPORTED");
|
|
803
|
+
if (observed.stopping)
|
|
804
|
+
return;
|
|
805
|
+
if (continuation && observed.userIntervened)
|
|
806
|
+
return;
|
|
807
|
+
const execution = images.length > 0 ? session.prompt(initial, { images }) : session.prompt(initial);
|
|
808
|
+
// Observe a synchronous rejection while the invocation fact is persisted.
|
|
809
|
+
const outcome = execution.then(() => ({ ok: true }), (error) => ({ ok: false, error }));
|
|
810
|
+
if (continuation) {
|
|
811
|
+
await emit("EXECUTION_CONTINUED", {
|
|
812
|
+
phase,
|
|
813
|
+
...continuation,
|
|
814
|
+
prompt: initial,
|
|
815
|
+
deliveryStatus: "SUBMITTED",
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
const result = await outcome;
|
|
819
|
+
if (!result.ok)
|
|
820
|
+
throw result.error;
|
|
821
|
+
await session.waitForIdle();
|
|
822
|
+
observed.awaitingDecision = true;
|
|
823
|
+
if (observed.stopping)
|
|
824
|
+
return;
|
|
825
|
+
const stopReason = observed.terminalClosed
|
|
826
|
+
? "TERMINAL_CLOSED"
|
|
827
|
+
: (observed.lastModelStopReason ?? "idle");
|
|
828
|
+
const endFact = { phase, stopReason, nativeStopReason: observed.lastModelStopReason ?? null };
|
|
829
|
+
if (observed.terminalReached) {
|
|
830
|
+
await emit("EXECUTION_DECIDED", {
|
|
831
|
+
...endFact,
|
|
832
|
+
decision: "SKIP",
|
|
833
|
+
reason: "TERMINAL_ACCEPTED",
|
|
834
|
+
model: null,
|
|
835
|
+
});
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (observed.lastModelStopReason === "aborted" ||
|
|
839
|
+
(observed.lastModelStopReason === "error" && observed.nativeWillRetry !== true)) {
|
|
840
|
+
const code = observed.lastModelError?.split("\n")[0]?.slice(0, 200) ||
|
|
841
|
+
(observed.lastModelStopReason === "aborted" ? "PI_MODEL_ABORTED" : "MODEL_REQUEST_FAILED");
|
|
842
|
+
await emit("EXECUTION_DECIDED", {
|
|
843
|
+
...endFact,
|
|
844
|
+
decision: "SKIP",
|
|
845
|
+
reason: "MODEL_ERROR",
|
|
846
|
+
model: null,
|
|
847
|
+
});
|
|
848
|
+
if (observed.stopping)
|
|
783
849
|
return;
|
|
784
|
-
}
|
|
785
|
-
if (attempt < 2) {
|
|
786
|
-
prompt = release
|
|
787
|
-
? `请调用 submit_pull_requests 完成提 PR;最近错误:${observed.lastDeliveryError ?? "尚未提交"}。`
|
|
788
|
-
: `你尚未成功交付。自主补齐必需产物后调用 ${phase === "DEVELOPMENT" ? "complete_development" : "submit_delivery"}。最近提交结果:${observed.lastDeliveryError ?? "尚未提交"}。缺文件就在当前会话补齐,传输故障不要重写报告。无法继续时说明真实原因,需要沟通可记录待回应问题。`;
|
|
789
|
-
continue;
|
|
790
|
-
}
|
|
791
850
|
await client.reportCodingTerminal(runId, {
|
|
792
|
-
|
|
793
|
-
generation: host.generation,
|
|
851
|
+
...host,
|
|
794
852
|
turnId,
|
|
795
853
|
kind: "FAILED",
|
|
796
|
-
summary: observed.
|
|
854
|
+
summary: observed.lastModelError ?? code,
|
|
797
855
|
knownIssues: "",
|
|
798
|
-
failureCode:
|
|
856
|
+
failureCode: code,
|
|
799
857
|
projectFacts: [],
|
|
800
858
|
testFacts: [],
|
|
801
859
|
});
|
|
802
860
|
observed.terminalReached = true;
|
|
803
|
-
await emit("RUN_FAILED", { code:
|
|
861
|
+
await emit("RUN_FAILED", { phase, code, message: observed.lastModelError ?? code });
|
|
862
|
+
return;
|
|
804
863
|
}
|
|
864
|
+
// Judge reads durable messages. It queues automatic work through the same command
|
|
865
|
+
// claim boundary that gives user messages and stop priority.
|
|
866
|
+
await emit.flush();
|
|
867
|
+
if (observed.stopping)
|
|
868
|
+
return;
|
|
869
|
+
let decision;
|
|
870
|
+
try {
|
|
871
|
+
decision = await client.evaluateContinuation(runId, {
|
|
872
|
+
hostId: host.hostId,
|
|
873
|
+
generation: host.generation,
|
|
874
|
+
turnId,
|
|
875
|
+
stopReason,
|
|
876
|
+
...(observed.lastDeliveryError
|
|
877
|
+
? { lastDeliveryError: observed.lastDeliveryError.slice(0, 2000) }
|
|
878
|
+
: {}),
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
if (observed.stopping)
|
|
883
|
+
return;
|
|
884
|
+
await emit("EXECUTION_DECIDED", {
|
|
885
|
+
...endFact,
|
|
886
|
+
decision: "SKIP",
|
|
887
|
+
reason: "JUDGE_REQUEST_FAILED",
|
|
888
|
+
model: null,
|
|
889
|
+
explanation: error instanceof Error ? error.message : String(error),
|
|
890
|
+
});
|
|
891
|
+
if (observed.handingOffInput)
|
|
892
|
+
return;
|
|
893
|
+
await completeConversation();
|
|
894
|
+
observed.terminalReached = true;
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
if (observed.stopping)
|
|
898
|
+
return;
|
|
899
|
+
await emit("EXECUTION_DECIDED", { ...endFact, ...decision });
|
|
900
|
+
// The platform has queued the continuation; never execute the returned prompt
|
|
901
|
+
// directly, because a user can cancel it before the host claims the command.
|
|
902
|
+
if (decision.decision === "CONTINUE")
|
|
903
|
+
return;
|
|
904
|
+
if (observed.stopping || observed.handingOffInput)
|
|
905
|
+
return;
|
|
906
|
+
await completeConversation();
|
|
907
|
+
observed.terminalReached = true;
|
|
805
908
|
}
|
|
806
|
-
function terminalTools(client, host, runId, turnId, runRoot, input, observed,
|
|
909
|
+
function terminalTools(client, host, runId, turnId, runRoot, input, observed, persistBatch, pendingBatch, flushEvents) {
|
|
807
910
|
const definitions = [];
|
|
808
911
|
const add = (definition) => {
|
|
809
912
|
definitions.push(definition);
|
|
@@ -836,6 +939,8 @@ function terminalTools(client, host, runId, turnId, runRoot, input, observed, re
|
|
|
836
939
|
: Type.Object({}),
|
|
837
940
|
async execute(_toolCallId, rawParams) {
|
|
838
941
|
try {
|
|
942
|
+
if (observed.activeTasks.size > 0)
|
|
943
|
+
throw new Error("SUBAGENT_TASKS_RUNNING:子任务尚未结束,请等待结果后再提交阶段交付。");
|
|
839
944
|
if (input.executionPurpose === "RELEASE") {
|
|
840
945
|
const params = rawParams;
|
|
841
946
|
if (!params?.checklistSummary?.trim())
|
|
@@ -898,6 +1003,8 @@ function terminalTools(client, host, runId, turnId, runRoot, input, observed, re
|
|
|
898
1003
|
? Object.keys(DELIVERY_FILES).filter((slot) => STAGE_DELIVERY_PROTOCOLS.DEVELOPMENT.requiredFiles.includes(DELIVERY_FILES[slot]))
|
|
899
1004
|
: undefined);
|
|
900
1005
|
const projectFacts = await collectProjectFacts(runRoot, input.projects, input.workItem.key, input.primaryProjectId);
|
|
1006
|
+
if (input.phase === "DEVELOPMENT" && input.qualityReview?.enabled)
|
|
1007
|
+
await verifyReviewBranches(runRoot, input.projects, projectFacts);
|
|
901
1008
|
const batch = {
|
|
902
1009
|
hostId: host.hostId,
|
|
903
1010
|
generation: host.generation,
|
|
@@ -925,18 +1032,6 @@ function terminalTools(client, host, runId, turnId, runRoot, input, observed, re
|
|
|
925
1032
|
}
|
|
926
1033
|
},
|
|
927
1034
|
});
|
|
928
|
-
add({
|
|
929
|
-
name: "update_question",
|
|
930
|
-
label: "更新待回应问题",
|
|
931
|
-
description: "仅记录当前执行必须由人决定的业务问题,不把一般建议变为审批;真正解决后传空字符串清除。普通聊天继续使用 steer,不停止 Run。",
|
|
932
|
-
parameters: Type.Object({ question: Type.String() }),
|
|
933
|
-
async execute(_id, params) {
|
|
934
|
-
// The caller binding supplies technical identity; the model only owns the question text.
|
|
935
|
-
observed.pendingQuestion = params.question.trim();
|
|
936
|
-
await emitQuestion(params.question);
|
|
937
|
-
return toolResult(params.question);
|
|
938
|
-
},
|
|
939
|
-
});
|
|
940
1035
|
if (input.executionPurpose === "RELEASE")
|
|
941
1036
|
add({
|
|
942
1037
|
name: "request_code_changes",
|
|
@@ -959,36 +1054,6 @@ function terminalTools(client, host, runId, turnId, runRoot, input, observed, re
|
|
|
959
1054
|
return toolResult("已返回开发,等待下一执行接手续改,完成后进入测试与人工确认。");
|
|
960
1055
|
},
|
|
961
1056
|
});
|
|
962
|
-
add({
|
|
963
|
-
name: "report_blocker",
|
|
964
|
-
label: "报告阻塞",
|
|
965
|
-
description: "仅在认证、权限、仓库、外部环境或输入冲突真实阻塞时报告。",
|
|
966
|
-
parameters: Type.Object({
|
|
967
|
-
category: Type.Union([
|
|
968
|
-
Type.Literal("AUTHENTICATION"),
|
|
969
|
-
Type.Literal("PERMISSION"),
|
|
970
|
-
Type.Literal("REPOSITORY"),
|
|
971
|
-
Type.Literal("EXTERNAL_ENVIRONMENT"),
|
|
972
|
-
Type.Literal("INPUT_CONFLICT"),
|
|
973
|
-
]),
|
|
974
|
-
summary: Type.String(),
|
|
975
|
-
}),
|
|
976
|
-
async execute(_id, params) {
|
|
977
|
-
await client.reportCodingTerminal(runId, {
|
|
978
|
-
hostId: host.hostId,
|
|
979
|
-
generation: host.generation,
|
|
980
|
-
turnId,
|
|
981
|
-
kind: "BLOCKED",
|
|
982
|
-
summary: redact(params.summary),
|
|
983
|
-
knownIssues: "",
|
|
984
|
-
blockerCategory: params.category,
|
|
985
|
-
projectFacts: [],
|
|
986
|
-
testFacts: [],
|
|
987
|
-
});
|
|
988
|
-
observed.terminalReached = true;
|
|
989
|
-
return toolResult("阻塞已报告。");
|
|
990
|
-
},
|
|
991
|
-
});
|
|
992
1057
|
return (api) => {
|
|
993
1058
|
for (const definition of definitions)
|
|
994
1059
|
api.registerTool({
|
|
@@ -1003,7 +1068,7 @@ function terminalTools(client, host, runId, turnId, runRoot, input, observed, re
|
|
|
1003
1068
|
});
|
|
1004
1069
|
};
|
|
1005
1070
|
}
|
|
1006
|
-
export async function prepareProjects(cacheRoot, runRoot,
|
|
1071
|
+
export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects, resume) {
|
|
1007
1072
|
await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
|
|
1008
1073
|
for (const project of projects) {
|
|
1009
1074
|
const mirror = join(cacheRoot, `${project.id}.git`);
|
|
@@ -1047,7 +1112,7 @@ export async function prepareProjects(cacheRoot, runRoot, runId, projects, resum
|
|
|
1047
1112
|
});
|
|
1048
1113
|
}
|
|
1049
1114
|
const target = join(runRoot, safeName(project.name));
|
|
1050
|
-
const expectedBranch = project.devBranch || `aip/${
|
|
1115
|
+
const expectedBranch = project.devBranch || `aip/${workItemKey}`;
|
|
1051
1116
|
const existingWorktree = await exists(target);
|
|
1052
1117
|
if (!existingWorktree) {
|
|
1053
1118
|
if (resume)
|
|
@@ -1107,22 +1172,40 @@ async function canonicalRepository(value) {
|
|
|
1107
1172
|
}
|
|
1108
1173
|
return value.replace(/\/$/u, "");
|
|
1109
1174
|
}
|
|
1175
|
+
function executionInstructions() {
|
|
1176
|
+
return "按当前任务和用户最新要求工作:自主选择工具,读取、修改、编译、测试和验证,完成阶段目标后调用提交工具。咨询、解释和有限操作按用户范围处理,不自动扩张为旧阶段任务。需要用户决定或外部信息时,用自然语言说明具体问题和已完成工作,不虚构答案、测试或交付成功。错误依据真实反馈修正;结果未知的有副作用操作先核实,不盲目重放。普通回复不等于交付,提交校验失败需要检查和修正,业务门禁仍须遵守。";
|
|
1177
|
+
}
|
|
1178
|
+
async function workspaceFingerprint(runRoot, input) {
|
|
1179
|
+
return JSON.stringify(await Promise.all(input.projects.map(async (project) => {
|
|
1180
|
+
const cwd = join(runRoot, safeName(project.name));
|
|
1181
|
+
const [head, branch, tree] = await Promise.all([
|
|
1182
|
+
execFileAsync("git", ["rev-parse", "HEAD"], { cwd }),
|
|
1183
|
+
execFileAsync("git", ["symbolic-ref", "--short", "-q", "HEAD"], { cwd }),
|
|
1184
|
+
worktreeTreeHash(cwd),
|
|
1185
|
+
]);
|
|
1186
|
+
return [project.id, head.stdout.trim(), branch.stdout.trim(), tree];
|
|
1187
|
+
})));
|
|
1188
|
+
}
|
|
1110
1189
|
export function initialPrompt(input, runRoot, responsible = true) {
|
|
1111
1190
|
const directory = deliveryDirectory(runRoot, input);
|
|
1112
1191
|
if (input.executionPurpose === "RELEASE")
|
|
1113
|
-
return `当前是人工验收通过后的上线准备,恢复原开发会话。工单 ${input.workItem.key}:${input.workItem.title}。项目分支:${input.projects.map((p) => `${p.name} ${p.devBranch} → ${p.baselineBranch}`).join(";")}。共享工作区已由测试会话处理,不能依赖旧会话记忆:先读取最新代码、${directory}/report.md、release_checklist.md、summary.md 和适用的 design.md。以项目源分支和目标分支为依据,不按 commit ID 或 tree hash 校验验收一致性。核实上线清单,明确无需修改或修改摘要;清单覆盖项目与分支、SQL、Apollo、定时任务、Kafka/RabbitMQ资源、监控告警、发布顺序、验证和回滚。以设计为预期、代码核实;没有设计则从代码提取。无法证明的环境事实标明待核实,不涉及说明原因;测试环境部署记录不能代替上线清单。用户明确要求修改应用代码时调用 request_code_changes
|
|
1192
|
+
return `当前是人工验收通过后的上线准备,恢复原开发会话。工单 ${input.workItem.key}:${input.workItem.title}。项目分支:${input.projects.map((p) => `${p.name} ${p.devBranch} → ${p.baselineBranch}`).join(";")}。共享工作区已由测试会话处理,不能依赖旧会话记忆:先读取最新代码、${directory}/report.md、release_checklist.md、summary.md 和适用的 design.md。以项目源分支和目标分支为依据,不按 commit ID 或 tree hash 校验验收一致性。核实上线清单,明确无需修改或修改摘要;清单覆盖项目与分支、SQL、Apollo、定时任务、Kafka/RabbitMQ资源、监控告警、发布顺序、验证和回滚。以设计为预期、代码核实;没有设计则从代码提取。无法证明的环境事实标明待核实,不涉及说明原因;测试环境部署记录不能代替上线清单。用户明确要求修改应用代码时调用 request_code_changes,返回开发→测试→人工确认,不能在此阶段擅自修改后放行。清单有缺口时自然语言说明具体问题,需要用户补充时直接提问,不能宣称核实完成。核实完成后提交清单文档改动,再调用 submit_pull_requests,传入核实摘要和各项目 MR 标题说明;工具使用 Runtime 的 glab 身份推送、创建或复用 MR 并结构化登记工单。说明覆盖任务目的、本项目改动、测试验收、发布注意、跨项目依赖、风险回滚及材料入口。MR仍打开时复用;关闭后仅在用户明确重提时创建新MR。平台查询状态,不负责创建。仅说明或更新文档不退回研发;不执行实际发布、知识整理、关单。`;
|
|
1114
1193
|
const projects = input.projects
|
|
1115
1194
|
.map((p) => `- ${p.name}: ${join(runRoot, safeName(p.name))}(开发分支 ${p.devBranch} → 基准分支 ${p.baselineBranch};测试分支 ${p.testBranch})${p.id === input.primaryProjectId ? "(主项目)" : ""}`)
|
|
1116
1195
|
.join("\n");
|
|
1117
1196
|
const phase = input.phase === "DEVELOPMENT" ? "DEVELOPMENT" : "INTEGRATION";
|
|
1197
|
+
const phaseLabel = phase === "DEVELOPMENT" ? "开发与自审" : "系统测试,当前已进入系统测试,不是开发阶段";
|
|
1118
1198
|
const protocol = STAGE_DELIVERY_PROTOCOLS[phase];
|
|
1119
1199
|
const delivery = input.stageDeliverySnapshots?.[phase];
|
|
1120
1200
|
if (responsible && !delivery)
|
|
1121
1201
|
throw new Error("STAGE_DELIVERY_CONFIGURATION_MISSING");
|
|
1122
|
-
const context = `当前阶段:${phase}
|
|
1202
|
+
const context = `当前阶段:${phase}(${phaseLabel})。工单 ${input.workItem.key}:${input.workItem.title}。项目:\n${projects}\n输入材料:${directory}/prd.md${input.technicalDesign ? `、${directory}/design.md` : ""}。先读取输入材料;项目如有 AGENTS.md 或 CLAUDE.md,读取这些项目指令,按其中引用按需读取本项目框架指令和 Skill。项目材料不授予额外工具或资源权限。${input.inputConflict ? `正式输入已变化:读取 ${directory}/prd.platform.md 和适用的 design.platform.md,对照合并到 prd.md/design.md;本地副本未被覆盖,对照文件不收集。` : ""}`;
|
|
1123
1203
|
if (!responsible)
|
|
1124
1204
|
return context;
|
|
1125
|
-
|
|
1205
|
+
const reviewGitRequirement = phase === "DEVELOPMENT" && input.qualityReview?.enabled
|
|
1206
|
+
? `\n本轮启用平台代码质量审查。调用 complete_development 前,必须提交每个项目的全部本轮改动,并在对应项目目录使用普通 Git 将当前 HEAD 推送到平台已分配的开发分支(${input.projects.map((project) => `${project.name}: ${project.devBranch}`).join(";")})。只允许推送这些开发分支;不得推送 main、test 或基准分支,不得 force push,不创建 MR。推送后再调用 complete_development;该工具只读核对远端精确分支 SHA 与上报 HEAD 一致,不会代替你推送。`
|
|
1207
|
+
: "";
|
|
1208
|
+
return `${context}\n阶段交付要求(${delivery.source} revision ${delivery.revision}):\n${delivery.prompt}\n平台固定协议:\n${protocol.instructions}\n必交文件:${protocol.requiredFiles.join("、")}。条件文件:${input.technicalDesign ? protocol.conditionalFiles.join("、") || "无" : "无"}。可选文件:${protocol.optionalFiles.join("、") || "无"}。\n最终收件目录:${directory}。当前完成工具:${protocol.tool}。${reviewGitRequirement}`;
|
|
1126
1209
|
}
|
|
1127
1210
|
async function uploadBatch(client, runId, batch) {
|
|
1128
1211
|
for (let attempt = 0;; attempt++) {
|
|
@@ -1173,6 +1256,29 @@ async function collectProjectFacts(runRoot, projects, key, primaryProjectId) {
|
|
|
1173
1256
|
};
|
|
1174
1257
|
}));
|
|
1175
1258
|
}
|
|
1259
|
+
async function verifyReviewBranches(runRoot, projects, projectFacts) {
|
|
1260
|
+
for (const project of projects) {
|
|
1261
|
+
const fact = projectFacts.find((item) => item.projectId === project.id);
|
|
1262
|
+
if (!fact || fact.branch !== project.devBranch)
|
|
1263
|
+
throw new Error(`AIP_REVIEW_BRANCH_MISMATCH:${project.name}:请切换到已分配开发分支 ${project.devBranch} 后重新提交。`);
|
|
1264
|
+
const ref = `refs/heads/${project.devBranch}`;
|
|
1265
|
+
let remoteHead;
|
|
1266
|
+
try {
|
|
1267
|
+
const { stdout } = await execFileAsync("git", ["ls-remote", "--exit-code", "origin", ref], {
|
|
1268
|
+
cwd: join(runRoot, safeName(project.name)),
|
|
1269
|
+
});
|
|
1270
|
+
remoteHead = stdout.trim().split(/\s+/u)[0];
|
|
1271
|
+
}
|
|
1272
|
+
catch (error) {
|
|
1273
|
+
if (!(error && typeof error === "object" && "code" in error && error.code === 2))
|
|
1274
|
+
throw new Error(`AIP_REVIEW_REMOTE_CHECK_FAILED:${project.name}:无法核验远端,请检查网络或权限后重试。`);
|
|
1275
|
+
}
|
|
1276
|
+
if (!remoteHead)
|
|
1277
|
+
throw new Error(`AIP_REVIEW_REMOTE_BRANCH_MISSING:${project.name}:请将当前 HEAD 推送到 origin 的 ${project.devBranch} 后重试。`);
|
|
1278
|
+
if (remoteHead !== fact.commitSha)
|
|
1279
|
+
throw new Error(`AIP_REVIEW_REMOTE_HEAD_MISMATCH:${project.name}:远端 ${project.devBranch} 与当前 HEAD 不一致,请推送当前 HEAD 后重试。`);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1176
1282
|
async function captureProjectRevisions(runRoot, projects, key, primaryProjectId) {
|
|
1177
1283
|
return collectProjectFacts(runRoot, projects, key, primaryProjectId);
|
|
1178
1284
|
}
|