@bf-meishijian/aip 0.0.4 → 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/cli.d.ts.map +1 -1
- package/dist/cli.js +14 -15
- package/dist/cli.js.map +1 -1
- 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 +776 -544
- 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/harness.d.ts +2 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +1 -0
- package/dist/harness.js.map +1 -0
- package/dist/live-employee.d.ts +4 -2
- package/dist/live-employee.d.ts.map +1 -1
- package/dist/live-employee.js +49 -15
- package/dist/live-employee.js.map +1 -1
- package/dist/model-stores.d.ts +20 -0
- package/dist/model-stores.d.ts.map +1 -0
- package/dist/model-stores.js +77 -0
- package/dist/model-stores.js.map +1 -0
- 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/pi-model-service.js +47 -45
- package/dist/pi-model-service.js.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 +3 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +5 -2
- package/dist/state.js.map +1 -1
- package/package.json +13 -7
|
@@ -2,18 +2,22 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { mkdir, mkdtemp, open, realpath, rm, stat } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { basename, join, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
-
import { DELIVERY_FILES, STAGE_DELIVERY_PROTOCOLS, stageEmployeeIds } from "./stage-delivery.js";
|
|
8
7
|
import { createAgentSession, DefaultResourceLoader, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
9
8
|
import { Type } from "typebox";
|
|
10
|
-
import {
|
|
9
|
+
import { createCodingSubagents } from "./coding-subagents.js";
|
|
10
|
+
import { collectDeliveryFiles, deliveryDirectory, initializeDeliveryFiles, publishReviewMaterials, } from "./delivery-files.js";
|
|
11
11
|
import { liveEmployee } from "./live-employee.js";
|
|
12
12
|
import { prepareMergeRequest, registerMergeRequest, } from "./merge-requests.js";
|
|
13
13
|
import { paths } from "./paths.js";
|
|
14
|
+
import { PiMessageQueue } from "./pi-message-queue.js";
|
|
14
15
|
import { PlatformClientError } from "./platform-client.js";
|
|
16
|
+
import { materializeReviewReport } from "./review-report.js";
|
|
15
17
|
import { readJson, writeSecureJson } from "./secure-json.js";
|
|
18
|
+
import { DELIVERY_FILES, STAGE_DELIVERY_PROTOCOLS, stageEmployeeIds } from "./stage-delivery.js";
|
|
16
19
|
const execFileAsync = promisify(execFile);
|
|
20
|
+
const EVENT_APPEND_MAX_ATTEMPTS = 8;
|
|
17
21
|
export class CodingRunManager {
|
|
18
22
|
pi;
|
|
19
23
|
active = new Map();
|
|
@@ -26,6 +30,25 @@ export class CodingRunManager {
|
|
|
26
30
|
async execute(client, host, command) {
|
|
27
31
|
if (!command.run)
|
|
28
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
|
+
}
|
|
29
52
|
const imageRun = command.run;
|
|
30
53
|
let images;
|
|
31
54
|
try {
|
|
@@ -40,6 +63,7 @@ export class CodingRunManager {
|
|
|
40
63
|
if (active?.turnId === imageRun.turnId) {
|
|
41
64
|
active.stopping = true;
|
|
42
65
|
active.observed.stopping = true;
|
|
66
|
+
await active.subagents?.stop();
|
|
43
67
|
await active.session.abort();
|
|
44
68
|
await active.finished;
|
|
45
69
|
if (this.active.get(imageRun.runId) === active)
|
|
@@ -51,7 +75,18 @@ export class CodingRunManager {
|
|
|
51
75
|
const input = command.run.runInput;
|
|
52
76
|
await this.start(client, host, command.run.runId, command.run.turnId, input, false, command.run.contentMarkdown, images);
|
|
53
77
|
}
|
|
54
|
-
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") {
|
|
55
90
|
let active = this.active.get(command.run.runId);
|
|
56
91
|
if (!command.run.contentMarkdown && images.length === 0)
|
|
57
92
|
throw new Error("CODING_RUN_MESSAGE_MISSING");
|
|
@@ -60,25 +95,63 @@ export class CodingRunManager {
|
|
|
60
95
|
throw new Error("CODING_RUN_TURN_MISMATCH");
|
|
61
96
|
active.stopping = true;
|
|
62
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();
|
|
63
105
|
await active.session.abort();
|
|
64
106
|
if (this.active.get(command.run.runId) === active)
|
|
65
107
|
this.active.delete(command.run.runId);
|
|
66
108
|
active = undefined;
|
|
67
109
|
}
|
|
68
110
|
if (active) {
|
|
69
|
-
if (images.length > 0 && !active.
|
|
111
|
+
if (images.length > 0 && !active.supportsImages)
|
|
70
112
|
throw new Error("MODEL_IMAGES_UNSUPPORTED");
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
+
});
|
|
77
145
|
}
|
|
78
146
|
else {
|
|
79
147
|
if (!command.run.runInput)
|
|
80
148
|
throw new Error("CODING_RUN_INPUT_MISSING");
|
|
81
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
|
+
});
|
|
82
155
|
}
|
|
83
156
|
}
|
|
84
157
|
else if (command.run.action === "STOP") {
|
|
@@ -88,33 +161,55 @@ export class CodingRunManager {
|
|
|
88
161
|
throw new Error("CODING_RUN_TURN_MISMATCH");
|
|
89
162
|
active.stopping = true;
|
|
90
163
|
active.observed.stopping = true;
|
|
164
|
+
await active.subagents?.stop();
|
|
91
165
|
await active.session.abort();
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
active.
|
|
100
|
-
|
|
166
|
+
await active.emit("AUTO_CONTINUATION_CHANGED", {
|
|
167
|
+
phase: active.input.phase,
|
|
168
|
+
autoContinueEnabled: false,
|
|
169
|
+
reason: "USER_STOP",
|
|
170
|
+
source: "HOST_HANDLED",
|
|
171
|
+
});
|
|
172
|
+
await active.emit.flush();
|
|
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" });
|
|
101
180
|
}
|
|
102
181
|
else {
|
|
103
182
|
const stateFile = join(paths().runState, `${command.run.runId}.json`);
|
|
104
183
|
const state = await readJson(stateFile);
|
|
105
|
-
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
|
+
});
|
|
106
189
|
}
|
|
107
190
|
}
|
|
108
191
|
else if (command.run.action === "RESUME") {
|
|
109
192
|
await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
|
|
110
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
|
+
}
|
|
111
206
|
}
|
|
112
|
-
async start(client, host, runId, turnId, input, resume, resumePrompt, images = []) {
|
|
207
|
+
async start(client, host, runId, turnId, input, resume, resumePrompt, images = [], continuation) {
|
|
113
208
|
const active = this.active.get(runId);
|
|
114
209
|
if (active) {
|
|
115
210
|
if (active.turnId === turnId)
|
|
116
211
|
return;
|
|
117
|
-
if (!active.observed.terminalReached)
|
|
212
|
+
if (!active.observed.terminalReached && !active.stopping)
|
|
118
213
|
throw new Error("CODING_RUN_TURN_MISMATCH");
|
|
119
214
|
await active.finished;
|
|
120
215
|
if (this.active.get(runId) === active)
|
|
@@ -127,13 +222,25 @@ export class CodingRunManager {
|
|
|
127
222
|
const local = paths();
|
|
128
223
|
const runRoot = join(local.runWorktrees, runId);
|
|
129
224
|
await mkdir(runRoot, { recursive: true, mode: 0o700 });
|
|
130
|
-
await mkdir(local.piSessions, { recursive: true, mode: 0o700 });
|
|
131
|
-
await mkdir(local.piAgent, { recursive: true, mode: 0o700 });
|
|
132
225
|
const stateFile = join(local.runState, `${runId}.json`);
|
|
133
226
|
const storedState = await readJson(stateFile);
|
|
134
|
-
|
|
227
|
+
if (storedState?.phaseSessions &&
|
|
228
|
+
Object.keys(storedState.phaseSessions).length &&
|
|
229
|
+
storedState.harness !== "pi")
|
|
230
|
+
throw new Error("PRIOR_HARNESS_SESSION_UNSUPPORTED:请新建执行,历史文件和工作目录已保留。");
|
|
135
231
|
for (const project of input.projects)
|
|
136
|
-
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
|
+
}
|
|
137
244
|
if (!storedState?.prepared || input.inputConflict)
|
|
138
245
|
await initializeDeliveryFiles(runRoot, input, Boolean(storedState?.prepared));
|
|
139
246
|
const deliveryInputHash = createHash("sha256")
|
|
@@ -143,7 +250,7 @@ export class CodingRunManager {
|
|
|
143
250
|
input.technicalDesign?.content,
|
|
144
251
|
]))
|
|
145
252
|
.digest("hex");
|
|
146
|
-
const
|
|
253
|
+
const delegations = storedState?.delegations ?? {};
|
|
147
254
|
let pendingDelivery = storedState?.phase !== input.phase ||
|
|
148
255
|
resumePrompt ||
|
|
149
256
|
images.length > 0 ||
|
|
@@ -159,11 +266,10 @@ export class CodingRunManager {
|
|
|
159
266
|
: undefined;
|
|
160
267
|
const phaseSessions = storedState?.phaseSessions ?? {};
|
|
161
268
|
const sessionPhase = input.executionPurpose === "RELEASE" ? "DEVELOPMENT" : input.phase;
|
|
162
|
-
let
|
|
269
|
+
let sessionRef = phaseSessions[sessionPhase];
|
|
163
270
|
let lastEventSequence = storedState?.lastEventSequence ?? 0;
|
|
164
271
|
const observed = {
|
|
165
|
-
|
|
166
|
-
memberResults: new Map(),
|
|
272
|
+
userIntervened: input.autoContinueEnabled === false,
|
|
167
273
|
activeTasks: new Map(),
|
|
168
274
|
stopping: false,
|
|
169
275
|
terminalReached: false,
|
|
@@ -178,11 +284,12 @@ export class CodingRunManager {
|
|
|
178
284
|
primaryProjectId: input.primaryProjectId,
|
|
179
285
|
},
|
|
180
286
|
prepared: true,
|
|
287
|
+
preparationBaselines,
|
|
181
288
|
phaseSessions,
|
|
182
|
-
|
|
289
|
+
sessionRef,
|
|
290
|
+
harness: "pi",
|
|
183
291
|
lastEventSequence,
|
|
184
|
-
|
|
185
|
-
childSessions,
|
|
292
|
+
delegations,
|
|
186
293
|
pendingDelivery,
|
|
187
294
|
deliveryInputHash,
|
|
188
295
|
});
|
|
@@ -192,15 +299,15 @@ export class CodingRunManager {
|
|
|
192
299
|
return persistState();
|
|
193
300
|
}, redact);
|
|
194
301
|
if (input.phase === "PREPARATION") {
|
|
302
|
+
if (!storedState?.prepared)
|
|
303
|
+
await publishReviewMaterials(runRoot, input);
|
|
195
304
|
await persistState();
|
|
196
305
|
await emit("PREPARATION_COMPLETED", {
|
|
197
306
|
primaryProjectId: input.primaryProjectId,
|
|
198
307
|
projects: await Promise.all(input.projects.map(async (p) => ({
|
|
199
308
|
projectId: p.id,
|
|
200
309
|
projectName: p.name,
|
|
201
|
-
baselineCommit:
|
|
202
|
-
cwd: join(runRoot, safeName(p.name)),
|
|
203
|
-
})).stdout.trim(),
|
|
310
|
+
baselineCommit: preparationBaselines[p.id] ?? p.baselineCommit,
|
|
204
311
|
directory: join(runRoot, safeName(p.name)),
|
|
205
312
|
branch: (await execFileAsync("git", ["branch", "--show-current"], {
|
|
206
313
|
cwd: join(runRoot, safeName(p.name)),
|
|
@@ -209,12 +316,12 @@ export class CodingRunManager {
|
|
|
209
316
|
});
|
|
210
317
|
return;
|
|
211
318
|
}
|
|
212
|
-
const terminal =
|
|
319
|
+
const terminal = terminalTools(client, host, runId, turnId, runRoot, input, observed, async (batch) => {
|
|
213
320
|
pendingDelivery = batch;
|
|
214
321
|
await persistState();
|
|
215
|
-
}, () => pendingDelivery, (
|
|
322
|
+
}, () => pendingDelivery, () => emit.flush());
|
|
216
323
|
if (!this.pi.resolveRuntimeModel)
|
|
217
|
-
throw new Error("
|
|
324
|
+
throw new Error("MODEL_RUNTIME_UNAVAILABLE");
|
|
218
325
|
const resolved = await this.pi.resolveRuntimeModel(input.providerId, input.modelId, {
|
|
219
326
|
runId,
|
|
220
327
|
turnId,
|
|
@@ -230,215 +337,143 @@ export class CodingRunManager {
|
|
|
230
337
|
member: "supervisor",
|
|
231
338
|
};
|
|
232
339
|
const availableAgents = stageEmployeeIds(input);
|
|
233
|
-
const live = liveEmployee(client, runId, binding, await client.employeeConfig(runId, { ...binding, purpose: "LOAD" }), runRoot,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const revisions = (await captureProjectRevisions(runRoot, input.projects, input.workItem.key, input.primaryProjectId)).filter((r) => Boolean(r));
|
|
267
|
-
observed.memberResults.set(params.agent, {
|
|
268
|
-
status: result.status,
|
|
269
|
-
summary: result.summary,
|
|
270
|
-
revisions,
|
|
271
|
-
});
|
|
272
|
-
return toolResult("结果已记录,向主管返回结论。");
|
|
273
|
-
},
|
|
274
|
-
});
|
|
275
|
-
const childLoader = new DefaultResourceLoader({
|
|
276
|
-
cwd,
|
|
277
|
-
agentDir: local.piAgent,
|
|
278
|
-
noExtensions: true,
|
|
279
|
-
noSkills: true,
|
|
280
|
-
noPromptTemplates: true,
|
|
281
|
-
noThemes: true,
|
|
282
|
-
noContextFiles: true,
|
|
283
|
-
extensionFactories: [childLive.extension, resultExtension],
|
|
284
|
-
systemPrompt: "当前为团队子成员会话,员工配置由平台下发。",
|
|
285
|
-
});
|
|
286
|
-
await childLoader.reload();
|
|
287
|
-
const file = childSessions[`${input.phase}:${member}`];
|
|
288
|
-
const childManager = file
|
|
289
|
-
? SessionManager.open(join(local.piSessions, file), local.piSessions, cwd)
|
|
290
|
-
: SessionManager.create(cwd, local.piSessions);
|
|
291
|
-
const created = await createAgentSession({
|
|
292
|
-
cwd,
|
|
293
|
-
agentDir: local.piAgent,
|
|
294
|
-
modelRuntime: resolved.runtime,
|
|
295
|
-
model: resolved.model,
|
|
296
|
-
thinkingLevel: input.thinkingLevel,
|
|
297
|
-
resourceLoader: childLoader,
|
|
298
|
-
sessionManager: childManager,
|
|
299
|
-
});
|
|
300
|
-
child = created.session;
|
|
301
|
-
childLive.attach(child);
|
|
302
|
-
childInstances.set(member, child);
|
|
303
|
-
const childFile = childManager.getSessionFile();
|
|
304
|
-
if (!childFile)
|
|
305
|
-
throw new Error("CHILD_SESSION_FILE_MISSING");
|
|
306
|
-
childSessions[`${input.phase}:${member}`] = basename(childFile);
|
|
307
|
-
await persistState();
|
|
308
|
-
subscribeSession(child, async (kind, payload) => {
|
|
309
|
-
if (kind === "MESSAGE_STARTED")
|
|
310
|
-
return;
|
|
311
|
-
if (kind === "TEXT_DELTA") {
|
|
312
|
-
childUpdates.get(member)?.(String(payload.text ?? ""));
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
await emit(kind, {
|
|
316
|
-
...payload,
|
|
317
|
-
agentId: params.agent,
|
|
318
|
-
phase: input.phase,
|
|
319
|
-
parentActivityKey: childTasks.get(member),
|
|
320
|
-
sessionId: childSessions[`${input.phase}:${member}`],
|
|
321
|
-
});
|
|
322
|
-
}, observed, redact);
|
|
323
|
-
}
|
|
324
|
-
await emit("SUBAGENT_UPDATED", {
|
|
325
|
-
phase: input.phase,
|
|
326
|
-
task: params.task,
|
|
327
|
-
activityKey: callId,
|
|
328
|
-
agentId: params.agent,
|
|
329
|
-
sessionFileRef: childSessions[`${input.phase}:${member}`],
|
|
330
|
-
summary: "独立子会话已就绪",
|
|
331
|
-
});
|
|
332
|
-
const abort = () => {
|
|
333
|
-
void child?.abort();
|
|
334
|
-
};
|
|
335
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
336
|
-
try {
|
|
337
|
-
await child.prompt(`${initialPrompt(input, runRoot, false)}\n\n本次主管分派给 ${params.agent} 的任务:${params.task}\n只完成自身职责,最后调用 submit_member_result;主管负责其他成员协调和最终交付。`);
|
|
338
|
-
await child.waitForIdle();
|
|
339
|
-
if (signal?.aborted || observed.stopping)
|
|
340
|
-
throw new Error("SUBAGENT_STOPPED");
|
|
341
|
-
const result = observed.memberResults.get(params.agent);
|
|
342
|
-
if (!result)
|
|
343
|
-
throw new Error("SUBAGENT_RESULT_MISSING");
|
|
344
|
-
return {
|
|
345
|
-
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
346
|
-
details: {
|
|
347
|
-
agent: params.agent,
|
|
348
|
-
status: result.status,
|
|
349
|
-
sessionFileRef: childSessions[`${input.phase}:${member}`],
|
|
350
|
-
},
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
finally {
|
|
354
|
-
childUpdates.delete(member);
|
|
355
|
-
signal?.removeEventListener("abort", abort);
|
|
356
|
-
}
|
|
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);
|
|
357
373
|
},
|
|
358
374
|
});
|
|
359
|
-
|
|
360
|
-
const loader = new DefaultResourceLoader({
|
|
375
|
+
const opened = await openPiSession({
|
|
361
376
|
cwd,
|
|
377
|
+
sessionDir: local.piSessions,
|
|
362
378
|
agentDir: local.piAgent,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
noContextFiles: true,
|
|
379
|
+
existingId: sessionRef,
|
|
380
|
+
modelRuntime: resolved.runtime,
|
|
381
|
+
model: resolved.model,
|
|
382
|
+
thinkingLevel: input.thinkingLevel,
|
|
383
|
+
extensions: [...(delegation ? [delegation.extension] : []), terminal, live.extension],
|
|
369
384
|
systemPrompt: input.team
|
|
370
385
|
? "当前为团队主负责人会话,员工配置由平台下发。"
|
|
371
386
|
: "当前为工单主会话,员工配置由平台下发。",
|
|
387
|
+
runId,
|
|
372
388
|
});
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
sessionManager.setSessionFile(allocatedSessionFile);
|
|
389
|
-
sessionManager.appendCustomEntry("aip-run-binding", {
|
|
390
|
-
runId,
|
|
389
|
+
live.attach(opened.session);
|
|
390
|
+
const session = opened.session;
|
|
391
|
+
sessionRef = opened.sessionId;
|
|
392
|
+
observed.messageQueue = new PiMessageQueue(session);
|
|
393
|
+
subscribeSession(session, (kind, payload) => {
|
|
394
|
+
const activityKey = String(payload.activityKey ?? "");
|
|
395
|
+
if (kind === "SUBAGENT_STARTED")
|
|
396
|
+
observed.activeTasks.set(activityKey, String(payload.agentId));
|
|
397
|
+
if (["SUBAGENT_COMPLETED", "SUBAGENT_FAILED", "SUBAGENT_STOPPED"].includes(kind))
|
|
398
|
+
observed.activeTasks.delete(activityKey);
|
|
399
|
+
return emit(kind, {
|
|
400
|
+
...payload,
|
|
401
|
+
phase: input.phase,
|
|
402
|
+
agentId: payload.agentId ?? "supervisor",
|
|
403
|
+
sessionId: sessionRef,
|
|
391
404
|
});
|
|
392
|
-
}
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
thinkingLevel: input.thinkingLevel,
|
|
404
|
-
resourceLoader: loader,
|
|
405
|
-
sessionManager,
|
|
406
|
-
});
|
|
407
|
-
live.attach(session);
|
|
408
|
-
const activeRun = { session, input, turnId, emit, stopping: false, observed };
|
|
405
|
+
}, observed, redact);
|
|
406
|
+
const activeRun = {
|
|
407
|
+
session,
|
|
408
|
+
supportsImages: resolved.model.input?.includes("image") === true,
|
|
409
|
+
input,
|
|
410
|
+
turnId,
|
|
411
|
+
emit,
|
|
412
|
+
stopping: false,
|
|
413
|
+
observed,
|
|
414
|
+
...(delegation ? { subagents: delegation } : {}),
|
|
415
|
+
};
|
|
409
416
|
this.active.set(runId, activeRun);
|
|
410
|
-
|
|
411
|
-
...payload,
|
|
412
|
-
phase: input.phase,
|
|
413
|
-
agentId: payload.agentId ?? "supervisor",
|
|
414
|
-
sessionId: sessionFileRef,
|
|
415
|
-
}), observed, redact);
|
|
416
|
-
sessionFileRef = basename(sessionFile);
|
|
417
|
-
phaseSessions[sessionPhase] = sessionFileRef;
|
|
417
|
+
phaseSessions[sessionPhase] = sessionRef;
|
|
418
418
|
await persistState();
|
|
419
|
-
await emit("RUN_STARTED", {
|
|
420
|
-
if (
|
|
419
|
+
await emit("RUN_STARTED", { sessionRef, phase: input.phase });
|
|
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)) {
|
|
421
430
|
await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
|
|
422
431
|
await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
|
|
423
432
|
}
|
|
424
|
-
const
|
|
425
|
-
|
|
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)
|
|
426
442
|
.catch(async (error) => {
|
|
427
443
|
if (activeRun.stopping)
|
|
428
444
|
return;
|
|
445
|
+
await emit("EXECUTION_DECIDED", {
|
|
446
|
+
phase: input.phase,
|
|
447
|
+
decision: "SKIP",
|
|
448
|
+
reason: "MODEL_ERROR",
|
|
449
|
+
stopReason: "error",
|
|
450
|
+
});
|
|
429
451
|
await emit("RUN_FAILED", {
|
|
430
|
-
|
|
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)),
|
|
431
457
|
});
|
|
432
458
|
})
|
|
433
459
|
.finally(async () => {
|
|
434
460
|
await emit.flush();
|
|
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
|
+
}
|
|
435
470
|
if (this.active.get(runId) === activeRun)
|
|
436
471
|
this.active.delete(runId);
|
|
437
472
|
});
|
|
438
473
|
}
|
|
439
474
|
async synthetic(client, host, runId, turnId, input) {
|
|
440
475
|
const emit = eventEmitter(client, host, runId, turnId);
|
|
441
|
-
await emit("RUN_STARTED", {
|
|
476
|
+
await emit("RUN_STARTED", { sessionRef: `synthetic:${runId}` });
|
|
442
477
|
const member = stageEmployeeIds(input)[0];
|
|
443
478
|
if (member)
|
|
444
479
|
await emit("SUBAGENT_STARTED", {
|
|
@@ -484,29 +519,46 @@ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistS
|
|
|
484
519
|
let sequence = initialSequence;
|
|
485
520
|
let queue = Promise.resolve();
|
|
486
521
|
const emit = (kind, payload) => {
|
|
487
|
-
sequence += 1;
|
|
488
|
-
const event = {
|
|
489
|
-
eventId: crypto.randomUUID(),
|
|
490
|
-
sequence,
|
|
491
|
-
turnId,
|
|
492
|
-
kind,
|
|
493
|
-
payload: kind === "PREPARATION_COMPLETED" ? payload : redactPayload(payload, redact),
|
|
494
|
-
createdAt: new Date().toISOString(),
|
|
495
|
-
};
|
|
496
522
|
queue = queue
|
|
497
|
-
.
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
523
|
+
.catch(() => { })
|
|
524
|
+
.then(async () => {
|
|
525
|
+
const event = {
|
|
526
|
+
eventId: crypto.randomUUID(),
|
|
527
|
+
sequence: sequence + 1,
|
|
528
|
+
turnId,
|
|
529
|
+
kind,
|
|
530
|
+
payload: kind === "PREPARATION_COMPLETED" ? payload : redactPayload(payload, redact),
|
|
531
|
+
createdAt: new Date().toISOString(),
|
|
532
|
+
};
|
|
533
|
+
for (let attempt = 1; attempt <= EVENT_APPEND_MAX_ATTEMPTS; attempt++) {
|
|
534
|
+
try {
|
|
535
|
+
await client.appendCodingEvents(runId, {
|
|
536
|
+
hostId: host.hostId,
|
|
537
|
+
generation: host.generation,
|
|
538
|
+
events: [event],
|
|
539
|
+
});
|
|
540
|
+
sequence = event.sequence;
|
|
541
|
+
await persistSequence?.(event.sequence);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
catch (error) {
|
|
545
|
+
if (error instanceof PlatformClientError &&
|
|
546
|
+
error.status === 409 &&
|
|
547
|
+
error.code === "CODING_EVENT_TURN_STALE") {
|
|
548
|
+
if (["EXECUTION_STARTED", "EXECUTION_PAUSED", "CONVERSATION_COMPLETED"].includes(kind))
|
|
549
|
+
throw error;
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
process.stderr.write(`[local-coding-host] 事件上报失败:${redact(error instanceof Error ? error.message : String(error))}\n`);
|
|
553
|
+
if (attempt === EVENT_APPEND_MAX_ATTEMPTS) {
|
|
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;
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
510
562
|
});
|
|
511
563
|
return queue;
|
|
512
564
|
};
|
|
@@ -524,227 +576,56 @@ function redactPayloadValue(value, redact) {
|
|
|
524
576
|
return redactPayload(value, redact);
|
|
525
577
|
return value;
|
|
526
578
|
}
|
|
527
|
-
async function
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
else
|
|
538
|
-
await session.prompt(prompt);
|
|
539
|
-
await session.waitForIdle();
|
|
540
|
-
if (observed.stopping || observed.terminalReached || observed.pendingQuestion)
|
|
541
|
-
return;
|
|
542
|
-
if (attempt < 2) {
|
|
543
|
-
prompt = release
|
|
544
|
-
? `请调用 submit_pull_requests 完成提 PR;最近错误:${observed.lastDeliveryError ?? "尚未提交"}。`
|
|
545
|
-
: `你尚未成功交付。自主补齐必需产物后调用 ${phase === "DEVELOPMENT" ? "complete_development" : "submit_delivery"}。最近提交结果:${observed.lastDeliveryError ?? "尚未提交"}。缺文件就在当前会话补齐,传输故障不要重写报告。无法继续时说明真实原因,需要沟通可记录待回应问题。`;
|
|
546
|
-
continue;
|
|
547
|
-
}
|
|
548
|
-
await client.reportCodingTerminal(runId, {
|
|
549
|
-
hostId: host.hostId,
|
|
550
|
-
generation: host.generation,
|
|
551
|
-
turnId,
|
|
552
|
-
kind: "FAILED",
|
|
553
|
-
summary: observed.lastDeliveryError ?? "Agent 多次结束但没有形成明确终态",
|
|
554
|
-
knownIssues: "",
|
|
555
|
-
failureCode: observed.lastDeliveryError?.split("\n")[0]?.slice(0, 200) ?? "PI_TERMINAL_TOOL_NOT_CALLED",
|
|
556
|
-
projectFacts: [],
|
|
557
|
-
testFacts: [],
|
|
558
|
-
});
|
|
559
|
-
observed.terminalReached = true;
|
|
560
|
-
await emit("RUN_FAILED", { code: "PI_TERMINAL_TOOL_NOT_CALLED" });
|
|
579
|
+
async function openPiSession(options) {
|
|
580
|
+
await mkdir(options.sessionDir, { recursive: true, mode: 0o700 });
|
|
581
|
+
await mkdir(options.agentDir, { recursive: true, mode: 0o700 });
|
|
582
|
+
let sessionManager;
|
|
583
|
+
if (options.existingId) {
|
|
584
|
+
const listed = await SessionManager.list(options.cwd, options.sessionDir);
|
|
585
|
+
const match = listed.find((item) => item.id === options.existingId);
|
|
586
|
+
if (!match)
|
|
587
|
+
throw new Error("PI_SESSION_NOT_FOUND:请新建执行,历史文件已保留。");
|
|
588
|
+
sessionManager = SessionManager.open(match.path, options.sessionDir, options.cwd);
|
|
561
589
|
}
|
|
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
|
-
markdown: files[0].content,
|
|
602
|
-
};
|
|
603
|
-
await client.submitPullRequests(runId, {
|
|
604
|
-
hostId: host.hostId,
|
|
605
|
-
generation: host.generation,
|
|
606
|
-
turnId,
|
|
607
|
-
workItemId: input.workItem.id,
|
|
608
|
-
projects: [],
|
|
609
|
-
checklist,
|
|
610
|
-
});
|
|
611
|
-
const context = {
|
|
612
|
-
runId,
|
|
613
|
-
turnId,
|
|
614
|
-
workItem: input.workItem,
|
|
615
|
-
projects: input.projects,
|
|
616
|
-
primaryProjectId: input.primaryProjectId,
|
|
617
|
-
};
|
|
618
|
-
for (const project of input.projects) {
|
|
619
|
-
const explanation = params.projects?.find((item) => item.project === project.name);
|
|
620
|
-
if (!explanation)
|
|
621
|
-
throw new Error(`AIP_MR_DESCRIPTION_REQUIRED:${project.name}`);
|
|
622
|
-
const pendingFile = join(runRoot, `mr-pending-${safeName(project.id)}.json`);
|
|
623
|
-
const result = await prepareMergeRequest({
|
|
624
|
-
project,
|
|
625
|
-
cwd: join(runRoot, safeName(project.name)),
|
|
626
|
-
title: explanation.title,
|
|
627
|
-
description: explanation.description,
|
|
628
|
-
pendingFile,
|
|
629
|
-
});
|
|
630
|
-
await registerMergeRequest(client, context, host, result, pendingFile);
|
|
631
|
-
}
|
|
632
|
-
await client.submitPullRequests(runId, {
|
|
633
|
-
hostId: host.hostId,
|
|
634
|
-
generation: host.generation,
|
|
635
|
-
turnId,
|
|
636
|
-
workItemId: input.workItem.id,
|
|
637
|
-
projects: [],
|
|
638
|
-
completeRelease: true,
|
|
639
|
-
});
|
|
640
|
-
observed.terminalReached = true;
|
|
641
|
-
return toolResult("上线清单已核实,MR 已提交并登记,等待合并;平台负责查询最新状态。");
|
|
642
|
-
}
|
|
643
|
-
const existing = pendingBatch();
|
|
644
|
-
if (existing && existing.turnId === turnId) {
|
|
645
|
-
await flushEvents();
|
|
646
|
-
await uploadBatch(client, runId, existing);
|
|
647
|
-
await persistBatch(undefined);
|
|
648
|
-
observed.terminalReached = true;
|
|
649
|
-
return toolResult("交付已提交,等待经办人确认。");
|
|
650
|
-
}
|
|
651
|
-
const files = await collectDeliveryFiles(runRoot, input, input.phase === "DEVELOPMENT"
|
|
652
|
-
? Object.keys(DELIVERY_FILES).filter((slot) => STAGE_DELIVERY_PROTOCOLS.DEVELOPMENT.requiredFiles.includes(DELIVERY_FILES[slot]))
|
|
653
|
-
: undefined);
|
|
654
|
-
const projectFacts = await collectProjectFacts(runRoot, input.projects, input.workItem.key, input.primaryProjectId);
|
|
655
|
-
const batch = {
|
|
656
|
-
hostId: host.hostId,
|
|
657
|
-
generation: host.generation,
|
|
658
|
-
turnId,
|
|
659
|
-
kind: input.phase === "DEVELOPMENT" ? "DEVELOPMENT_COMPLETED" : "DELIVERY",
|
|
660
|
-
submissionId: crypto.randomUUID(),
|
|
661
|
-
files,
|
|
662
|
-
summary: files.find((file) => file.slot === "SUMMARY").content,
|
|
663
|
-
knownIssues: "",
|
|
664
|
-
projectFacts,
|
|
665
|
-
testFacts: [],
|
|
666
|
-
};
|
|
667
|
-
await persistBatch(batch);
|
|
668
|
-
await flushEvents();
|
|
669
|
-
await uploadBatch(client, runId, batch);
|
|
670
|
-
await persistBatch(undefined);
|
|
671
|
-
observed.terminalReached = true;
|
|
672
|
-
return toolResult("交付已提交,等待经办人确认。");
|
|
673
|
-
}
|
|
674
|
-
catch (error) {
|
|
675
|
-
observed.lastDeliveryError =
|
|
676
|
-
error instanceof Error ? error.message : "AIP_DELIVERY_FAILED";
|
|
677
|
-
if (error instanceof PlatformClientError && error.status < 500 && error.status !== 429)
|
|
678
|
-
await persistBatch(undefined);
|
|
679
|
-
throw error;
|
|
680
|
-
}
|
|
681
|
-
},
|
|
682
|
-
});
|
|
683
|
-
pi.registerTool({
|
|
684
|
-
name: "update_question",
|
|
685
|
-
label: "更新待回应问题",
|
|
686
|
-
description: "仅记录当前执行必须由人决定的业务问题,不把一般建议变为审批;真正解决后传空字符串清除。普通聊天继续使用 steer,不停止 Run。",
|
|
687
|
-
parameters: Type.Object({ question: Type.String() }),
|
|
688
|
-
async execute(_id, params) {
|
|
689
|
-
// The caller binding supplies technical identity; the model only owns the question text.
|
|
690
|
-
observed.pendingQuestion = params.question.trim();
|
|
691
|
-
await emitQuestion(params.question);
|
|
692
|
-
return toolResult(params.question);
|
|
693
|
-
},
|
|
694
|
-
});
|
|
695
|
-
if (input.executionPurpose === "RELEASE")
|
|
696
|
-
pi.registerTool({
|
|
697
|
-
name: "request_code_changes",
|
|
698
|
-
label: "返回开发修改",
|
|
699
|
-
description: "用户明确要求修改应用代码时,将工单返回开发,再测试和人工确认;仅讨论或文档修改不要调用。",
|
|
700
|
-
parameters: Type.Object({
|
|
701
|
-
reason: Type.String({ description: "用户要求修改的内容及原因" }),
|
|
702
|
-
}),
|
|
703
|
-
async execute(_id, params) {
|
|
704
|
-
await client.request(`/api/v1/coding-runs/${runId}/return-to-development`, {
|
|
705
|
-
method: "POST",
|
|
706
|
-
body: JSON.stringify({
|
|
707
|
-
hostId: host.hostId,
|
|
708
|
-
generation: host.generation,
|
|
709
|
-
turnId,
|
|
710
|
-
reason: params.reason,
|
|
711
|
-
}),
|
|
712
|
-
});
|
|
713
|
-
observed.terminalReached = true;
|
|
714
|
-
return toolResult("已返回开发,等待下一执行接手续改,完成后进入测试与人工确认。");
|
|
715
|
-
},
|
|
716
|
-
});
|
|
717
|
-
pi.registerTool({
|
|
718
|
-
name: "report_blocker",
|
|
719
|
-
label: "报告阻塞",
|
|
720
|
-
description: "仅在认证、权限、仓库、外部环境或输入冲突真实阻塞时报告。",
|
|
721
|
-
parameters: Type.Object({
|
|
722
|
-
category: Type.Union([
|
|
723
|
-
Type.Literal("AUTHENTICATION"),
|
|
724
|
-
Type.Literal("PERMISSION"),
|
|
725
|
-
Type.Literal("REPOSITORY"),
|
|
726
|
-
Type.Literal("EXTERNAL_ENVIRONMENT"),
|
|
727
|
-
Type.Literal("INPUT_CONFLICT"),
|
|
728
|
-
]),
|
|
729
|
-
summary: Type.String(),
|
|
730
|
-
}),
|
|
731
|
-
async execute(_id, params) {
|
|
732
|
-
await client.reportCodingTerminal(runId, {
|
|
733
|
-
hostId: host.hostId,
|
|
734
|
-
generation: host.generation,
|
|
735
|
-
turnId,
|
|
736
|
-
kind: "BLOCKED",
|
|
737
|
-
summary: redact(params.summary),
|
|
738
|
-
knownIssues: "",
|
|
739
|
-
blockerCategory: params.category,
|
|
740
|
-
projectFacts: [],
|
|
741
|
-
testFacts: [],
|
|
742
|
-
});
|
|
743
|
-
observed.terminalReached = true;
|
|
744
|
-
return toolResult("阻塞已报告。");
|
|
745
|
-
},
|
|
746
|
-
});
|
|
747
|
-
};
|
|
590
|
+
else {
|
|
591
|
+
sessionManager = SessionManager.create(options.cwd, options.sessionDir);
|
|
592
|
+
const allocated = sessionManager.getSessionFile();
|
|
593
|
+
if (!allocated)
|
|
594
|
+
throw new Error("PI_SESSION_FILE_MISSING");
|
|
595
|
+
await mkdir(dirname(allocated), { recursive: true, mode: 0o700 });
|
|
596
|
+
const placeholder = await open(allocated, "wx", 0o600);
|
|
597
|
+
await placeholder.close();
|
|
598
|
+
sessionManager.setSessionFile(allocated);
|
|
599
|
+
sessionManager.appendCustomEntry("aip-run-binding", { runId: options.runId });
|
|
600
|
+
}
|
|
601
|
+
const sessionId = sessionManager.getSessionId();
|
|
602
|
+
const sessionFile = sessionManager.getSessionFile();
|
|
603
|
+
if (!sessionFile || !(await exists(sessionFile)))
|
|
604
|
+
throw new Error("PI_SESSION_FILE_NOT_PERSISTED");
|
|
605
|
+
const loader = new DefaultResourceLoader({
|
|
606
|
+
cwd: options.cwd,
|
|
607
|
+
agentDir: options.agentDir,
|
|
608
|
+
extensionFactories: options.extensions,
|
|
609
|
+
noSkills: true,
|
|
610
|
+
noExtensions: true,
|
|
611
|
+
noPromptTemplates: true,
|
|
612
|
+
noThemes: true,
|
|
613
|
+
noContextFiles: true,
|
|
614
|
+
systemPrompt: options.systemPrompt,
|
|
615
|
+
});
|
|
616
|
+
await loader.reload();
|
|
617
|
+
const { session } = await createAgentSession({
|
|
618
|
+
cwd: options.cwd,
|
|
619
|
+
agentDir: options.agentDir,
|
|
620
|
+
modelRuntime: options.modelRuntime,
|
|
621
|
+
model: options.model,
|
|
622
|
+
thinkingLevel: options.thinkingLevel,
|
|
623
|
+
resourceLoader: loader,
|
|
624
|
+
sessionManager,
|
|
625
|
+
});
|
|
626
|
+
await session.bindExtensions({ mode: "print" });
|
|
627
|
+
session.setAutoCompactionEnabled(true);
|
|
628
|
+
return { session, sessionId };
|
|
748
629
|
}
|
|
749
630
|
function subscribeSession(session, emit, observed, redact) {
|
|
750
631
|
let messageId = crypto.randomUUID();
|
|
@@ -773,10 +654,31 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
773
654
|
void emit(update.kind, update.payload);
|
|
774
655
|
};
|
|
775
656
|
session.subscribe((event) => {
|
|
776
|
-
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") {
|
|
777
666
|
messageId = crypto.randomUUID();
|
|
778
667
|
void emit("MESSAGE_STARTED", { messageId });
|
|
779
668
|
}
|
|
669
|
+
else if (event.type === "message_start" && event.message.role === "user") {
|
|
670
|
+
const id = observed.messageQueue?.consumed(event.message);
|
|
671
|
+
if (id)
|
|
672
|
+
void emit("MESSAGE_QUEUE_UPDATED", { messageId: id, deliveryStatus: "READ" });
|
|
673
|
+
}
|
|
674
|
+
else if (event.type === "agent_end") {
|
|
675
|
+
observed.nativeWillRetry = event.willRetry === true;
|
|
676
|
+
}
|
|
677
|
+
else if (event.type === "auto_retry_end") {
|
|
678
|
+
observed.nativeWillRetry = false;
|
|
679
|
+
if (event.success === false && event.finalError)
|
|
680
|
+
observed.lastModelError = event.finalError;
|
|
681
|
+
}
|
|
780
682
|
else if (event.type === "message_update" &&
|
|
781
683
|
event.assistantMessageEvent.type === "text_delta" &&
|
|
782
684
|
event.assistantMessageEvent.delta) {
|
|
@@ -788,78 +690,385 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
788
690
|
}
|
|
789
691
|
else if (event.type === "tool_execution_start") {
|
|
790
692
|
flushText();
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
summary: String(event.args?.task ?? "团队子任务开始"),
|
|
799
|
-
task: String(event.args?.task ?? ""),
|
|
800
|
-
}
|
|
801
|
-
: {
|
|
802
|
-
activityKey: event.toolCallId,
|
|
803
|
-
toolName: event.toolName,
|
|
804
|
-
summary: `执行 ${event.toolName}`,
|
|
805
|
-
});
|
|
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
|
+
});
|
|
806
700
|
}
|
|
807
701
|
else if (event.type === "tool_execution_update") {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
kind: "TOOL_UPDATED",
|
|
819
|
-
payload: {
|
|
820
|
-
activityKey: event.toolCallId,
|
|
821
|
-
toolName: event.toolName,
|
|
822
|
-
currentAction: redact(textFromResult(event.partialResult)),
|
|
823
|
-
},
|
|
824
|
-
});
|
|
702
|
+
if (event.toolName === "subagent")
|
|
703
|
+
return;
|
|
704
|
+
pendingToolUpdates.set(event.toolCallId, {
|
|
705
|
+
kind: "TOOL_UPDATED",
|
|
706
|
+
payload: {
|
|
707
|
+
activityKey: event.toolCallId,
|
|
708
|
+
toolName: event.toolName,
|
|
709
|
+
currentAction: redact(textFromResult(event.partialResult)),
|
|
710
|
+
},
|
|
711
|
+
});
|
|
825
712
|
if (!toolUpdateTimers.has(event.toolCallId))
|
|
826
713
|
toolUpdateTimers.set(event.toolCallId, setTimeout(() => flushToolUpdate(event.toolCallId), 200));
|
|
827
714
|
}
|
|
828
715
|
else if (event.type === "tool_execution_end") {
|
|
829
|
-
flushToolUpdate(event.toolCallId);
|
|
830
|
-
const member = observed.activeTasks.get(event.toolCallId);
|
|
831
716
|
if (event.toolName === "subagent")
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
+
});
|
|
725
|
+
}
|
|
726
|
+
else if (event.type === "bash_execution_update" && event.delta) {
|
|
727
|
+
const activityKey = event.id ?? "bash";
|
|
728
|
+
pendingToolUpdates.set(activityKey, {
|
|
729
|
+
kind: "TOOL_UPDATED",
|
|
730
|
+
payload: {
|
|
731
|
+
activityKey,
|
|
732
|
+
toolName: "bash",
|
|
733
|
+
currentAction: redact(event.delta),
|
|
734
|
+
},
|
|
735
|
+
});
|
|
736
|
+
if (!toolUpdateTimers.has(activityKey))
|
|
737
|
+
toolUpdateTimers.set(activityKey, setTimeout(() => flushToolUpdate(activityKey), 200));
|
|
738
|
+
}
|
|
739
|
+
else if (event.type === "compaction_start") {
|
|
740
|
+
flushText();
|
|
741
|
+
void emit("TOOL_STARTED", {
|
|
742
|
+
activityKey: `compaction:${event.reason}`,
|
|
743
|
+
toolName: "compaction",
|
|
744
|
+
summary: `上下文压缩(${event.reason})`,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
else if (event.type === "compaction_end") {
|
|
748
|
+
void emit("TOOL_COMPLETED", {
|
|
749
|
+
activityKey: `compaction:${event.reason}`,
|
|
750
|
+
toolName: "compaction",
|
|
751
|
+
isError: event.aborted === true || Boolean(event.errorMessage),
|
|
752
|
+
summary: redact(event.result?.summary ??
|
|
753
|
+
event.errorMessage ??
|
|
754
|
+
(event.aborted ? "压缩已取消" : "压缩完成")),
|
|
755
|
+
});
|
|
853
756
|
}
|
|
854
757
|
else if (event.type === "message_end" && event.message.role === "assistant") {
|
|
855
758
|
flushText();
|
|
759
|
+
if ("stopReason" in event.message && event.message.stopReason)
|
|
760
|
+
observed.lastModelStopReason = String(event.message.stopReason);
|
|
761
|
+
if ("errorMessage" in event.message && event.message.errorMessage)
|
|
762
|
+
observed.lastModelError = String(event.message.errorMessage);
|
|
856
763
|
const content = textFromMessage(event.message.content);
|
|
857
|
-
|
|
858
|
-
|
|
764
|
+
const usage = event.message.usage;
|
|
765
|
+
const payload = {
|
|
766
|
+
contentMarkdown: redact(content),
|
|
767
|
+
messageId,
|
|
768
|
+
};
|
|
769
|
+
if (usage) {
|
|
770
|
+
payload.usage = {
|
|
771
|
+
input: usage.input,
|
|
772
|
+
output: usage.output,
|
|
773
|
+
cacheRead: usage.cacheRead,
|
|
774
|
+
cacheWrite: usage.cacheWrite,
|
|
775
|
+
totalTokens: usage.totalTokens,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
if (content.trim())
|
|
779
|
+
void emit("MESSAGE_COMPLETED", payload);
|
|
859
780
|
}
|
|
860
781
|
});
|
|
861
782
|
}
|
|
862
|
-
|
|
783
|
+
function textFromMessage(content) {
|
|
784
|
+
if (typeof content === "string")
|
|
785
|
+
return content;
|
|
786
|
+
if (!Array.isArray(content))
|
|
787
|
+
return "";
|
|
788
|
+
return content
|
|
789
|
+
.map((part) => part && typeof part === "object" && "text" in part ? String(part.text ?? "") : "")
|
|
790
|
+
.join("");
|
|
791
|
+
}
|
|
792
|
+
function textFromResult(value) {
|
|
793
|
+
if (!value || typeof value !== "object")
|
|
794
|
+
return "";
|
|
795
|
+
return textFromMessage(value.content);
|
|
796
|
+
}
|
|
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)
|
|
849
|
+
return;
|
|
850
|
+
await client.reportCodingTerminal(runId, {
|
|
851
|
+
...host,
|
|
852
|
+
turnId,
|
|
853
|
+
kind: "FAILED",
|
|
854
|
+
summary: observed.lastModelError ?? code,
|
|
855
|
+
knownIssues: "",
|
|
856
|
+
failureCode: code,
|
|
857
|
+
projectFacts: [],
|
|
858
|
+
testFacts: [],
|
|
859
|
+
});
|
|
860
|
+
observed.terminalReached = true;
|
|
861
|
+
await emit("RUN_FAILED", { phase, code, message: observed.lastModelError ?? code });
|
|
862
|
+
return;
|
|
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;
|
|
908
|
+
}
|
|
909
|
+
function terminalTools(client, host, runId, turnId, runRoot, input, observed, persistBatch, pendingBatch, flushEvents) {
|
|
910
|
+
const definitions = [];
|
|
911
|
+
const add = (definition) => {
|
|
912
|
+
definitions.push(definition);
|
|
913
|
+
};
|
|
914
|
+
add({
|
|
915
|
+
name: input.executionPurpose === "RELEASE"
|
|
916
|
+
? "submit_pull_requests"
|
|
917
|
+
: input.phase === "DEVELOPMENT"
|
|
918
|
+
? "complete_development"
|
|
919
|
+
: "submit_delivery",
|
|
920
|
+
label: "提交交付",
|
|
921
|
+
description: input.executionPurpose === "RELEASE"
|
|
922
|
+
? "核实上线清单并提交分支后,使用 Runtime GitLab 身份创建或复用 MR 并登记工单。"
|
|
923
|
+
: "读取并校验本阶段最终交付文件,上传成功后完成当前阶段。",
|
|
924
|
+
parameters: input.executionPurpose === "RELEASE"
|
|
925
|
+
? Type.Object({
|
|
926
|
+
checklistSummary: Type.String({
|
|
927
|
+
description: "核实依据、无需修改或修改摘要;缺口未解决时不要提交",
|
|
928
|
+
}),
|
|
929
|
+
projects: Type.Array(Type.Object({
|
|
930
|
+
project: Type.String({
|
|
931
|
+
description: `项目名称,只能选择:${input.projects.map((p) => p.name).join("、")}`,
|
|
932
|
+
}),
|
|
933
|
+
title: Type.String({ description: "包含工单编号及本项目变更摘要的 MR 标题" }),
|
|
934
|
+
description: Type.String({
|
|
935
|
+
description: "任务目的、本项目改动、测试与验收结论、发布注意、依赖、风险回滚及清单和报告链接",
|
|
936
|
+
}),
|
|
937
|
+
})),
|
|
938
|
+
})
|
|
939
|
+
: Type.Object({}),
|
|
940
|
+
async execute(_toolCallId, rawParams) {
|
|
941
|
+
try {
|
|
942
|
+
if (observed.activeTasks.size > 0)
|
|
943
|
+
throw new Error("SUBAGENT_TASKS_RUNNING:子任务尚未结束,请等待结果后再提交阶段交付。");
|
|
944
|
+
if (input.executionPurpose === "RELEASE") {
|
|
945
|
+
const params = rawParams;
|
|
946
|
+
if (!params?.checklistSummary?.trim())
|
|
947
|
+
throw new Error("AIP_RELEASE_CHECKLIST_SUMMARY_REQUIRED");
|
|
948
|
+
const files = await collectDeliveryFiles(runRoot, input, ["RELEASE_CHECKLIST"]);
|
|
949
|
+
const checklist = {
|
|
950
|
+
status: "VERIFIED",
|
|
951
|
+
summary: params.checklistSummary,
|
|
952
|
+
markdown: files[0].content,
|
|
953
|
+
};
|
|
954
|
+
await client.submitPullRequests(runId, {
|
|
955
|
+
hostId: host.hostId,
|
|
956
|
+
generation: host.generation,
|
|
957
|
+
turnId,
|
|
958
|
+
workItemId: input.workItem.id,
|
|
959
|
+
projects: [],
|
|
960
|
+
checklist,
|
|
961
|
+
});
|
|
962
|
+
const context = {
|
|
963
|
+
runId,
|
|
964
|
+
turnId,
|
|
965
|
+
workItem: input.workItem,
|
|
966
|
+
projects: input.projects,
|
|
967
|
+
primaryProjectId: input.primaryProjectId,
|
|
968
|
+
};
|
|
969
|
+
for (const project of input.projects) {
|
|
970
|
+
const explanation = params.projects?.find((item) => item.project === project.name);
|
|
971
|
+
if (!explanation)
|
|
972
|
+
throw new Error(`AIP_MR_DESCRIPTION_REQUIRED:${project.name}`);
|
|
973
|
+
const pendingFile = join(runRoot, `mr-pending-${safeName(project.id)}.json`);
|
|
974
|
+
const result = await prepareMergeRequest({
|
|
975
|
+
project,
|
|
976
|
+
cwd: join(runRoot, safeName(project.name)),
|
|
977
|
+
title: explanation.title,
|
|
978
|
+
description: explanation.description,
|
|
979
|
+
pendingFile,
|
|
980
|
+
});
|
|
981
|
+
await registerMergeRequest(client, context, host, result, pendingFile);
|
|
982
|
+
}
|
|
983
|
+
await client.submitPullRequests(runId, {
|
|
984
|
+
hostId: host.hostId,
|
|
985
|
+
generation: host.generation,
|
|
986
|
+
turnId,
|
|
987
|
+
workItemId: input.workItem.id,
|
|
988
|
+
projects: [],
|
|
989
|
+
completeRelease: true,
|
|
990
|
+
});
|
|
991
|
+
observed.terminalReached = true;
|
|
992
|
+
return toolResult("上线清单已核实,MR 已提交并登记,等待合并;平台负责查询最新状态。");
|
|
993
|
+
}
|
|
994
|
+
const existing = pendingBatch();
|
|
995
|
+
if (existing && existing.turnId === turnId) {
|
|
996
|
+
await flushEvents();
|
|
997
|
+
await uploadBatch(client, runId, existing);
|
|
998
|
+
await persistBatch(undefined);
|
|
999
|
+
observed.terminalReached = true;
|
|
1000
|
+
return toolResult("交付已提交,等待经办人确认。");
|
|
1001
|
+
}
|
|
1002
|
+
const files = await collectDeliveryFiles(runRoot, input, input.phase === "DEVELOPMENT"
|
|
1003
|
+
? Object.keys(DELIVERY_FILES).filter((slot) => STAGE_DELIVERY_PROTOCOLS.DEVELOPMENT.requiredFiles.includes(DELIVERY_FILES[slot]))
|
|
1004
|
+
: undefined);
|
|
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);
|
|
1008
|
+
const batch = {
|
|
1009
|
+
hostId: host.hostId,
|
|
1010
|
+
generation: host.generation,
|
|
1011
|
+
turnId,
|
|
1012
|
+
kind: input.phase === "DEVELOPMENT" ? "DEVELOPMENT_COMPLETED" : "DELIVERY",
|
|
1013
|
+
submissionId: crypto.randomUUID(),
|
|
1014
|
+
files,
|
|
1015
|
+
summary: files.find((file) => file.slot === "SUMMARY").content,
|
|
1016
|
+
knownIssues: "",
|
|
1017
|
+
projectFacts,
|
|
1018
|
+
testFacts: [],
|
|
1019
|
+
};
|
|
1020
|
+
await persistBatch(batch);
|
|
1021
|
+
await flushEvents();
|
|
1022
|
+
await uploadBatch(client, runId, batch);
|
|
1023
|
+
await persistBatch(undefined);
|
|
1024
|
+
observed.terminalReached = true;
|
|
1025
|
+
return toolResult("交付已提交,等待经办人确认。");
|
|
1026
|
+
}
|
|
1027
|
+
catch (error) {
|
|
1028
|
+
observed.lastDeliveryError = error instanceof Error ? error.message : "AIP_DELIVERY_FAILED";
|
|
1029
|
+
if (error instanceof PlatformClientError && error.status < 500 && error.status !== 429)
|
|
1030
|
+
await persistBatch(undefined);
|
|
1031
|
+
throw error;
|
|
1032
|
+
}
|
|
1033
|
+
},
|
|
1034
|
+
});
|
|
1035
|
+
if (input.executionPurpose === "RELEASE")
|
|
1036
|
+
add({
|
|
1037
|
+
name: "request_code_changes",
|
|
1038
|
+
label: "返回开发修改",
|
|
1039
|
+
description: "用户明确要求修改应用代码时,将工单返回开发,再测试和人工确认;仅讨论或文档修改不要调用。",
|
|
1040
|
+
parameters: Type.Object({
|
|
1041
|
+
reason: Type.String({ description: "用户要求修改的内容及原因" }),
|
|
1042
|
+
}),
|
|
1043
|
+
async execute(_id, params) {
|
|
1044
|
+
await client.request(`/api/v1/coding-runs/${runId}/return-to-development`, {
|
|
1045
|
+
method: "POST",
|
|
1046
|
+
body: JSON.stringify({
|
|
1047
|
+
hostId: host.hostId,
|
|
1048
|
+
generation: host.generation,
|
|
1049
|
+
turnId,
|
|
1050
|
+
reason: params.reason,
|
|
1051
|
+
}),
|
|
1052
|
+
});
|
|
1053
|
+
observed.terminalReached = true;
|
|
1054
|
+
return toolResult("已返回开发,等待下一执行接手续改,完成后进入测试与人工确认。");
|
|
1055
|
+
},
|
|
1056
|
+
});
|
|
1057
|
+
return (api) => {
|
|
1058
|
+
for (const definition of definitions)
|
|
1059
|
+
api.registerTool({
|
|
1060
|
+
name: definition.name,
|
|
1061
|
+
label: definition.label,
|
|
1062
|
+
description: definition.description,
|
|
1063
|
+
parameters: definition.parameters,
|
|
1064
|
+
async execute(id, args, signal) {
|
|
1065
|
+
signal?.throwIfAborted();
|
|
1066
|
+
return definition.execute(id, args);
|
|
1067
|
+
},
|
|
1068
|
+
});
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects, resume) {
|
|
863
1072
|
await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
|
|
864
1073
|
for (const project of projects) {
|
|
865
1074
|
const mirror = join(cacheRoot, `${project.id}.git`);
|
|
@@ -903,7 +1112,7 @@ export async function prepareProjects(cacheRoot, runRoot, runId, projects, resum
|
|
|
903
1112
|
});
|
|
904
1113
|
}
|
|
905
1114
|
const target = join(runRoot, safeName(project.name));
|
|
906
|
-
const expectedBranch = project.devBranch || `aip/${
|
|
1115
|
+
const expectedBranch = project.devBranch || `aip/${workItemKey}`;
|
|
907
1116
|
const existingWorktree = await exists(target);
|
|
908
1117
|
if (!existingWorktree) {
|
|
909
1118
|
if (resume)
|
|
@@ -963,22 +1172,40 @@ async function canonicalRepository(value) {
|
|
|
963
1172
|
}
|
|
964
1173
|
return value.replace(/\/$/u, "");
|
|
965
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
|
+
}
|
|
966
1189
|
export function initialPrompt(input, runRoot, responsible = true) {
|
|
967
1190
|
const directory = deliveryDirectory(runRoot, input);
|
|
968
1191
|
if (input.executionPurpose === "RELEASE")
|
|
969
|
-
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。平台查询状态,不负责创建。仅说明或更新文档不退回研发;不执行实际发布、知识整理、关单。`;
|
|
970
1193
|
const projects = input.projects
|
|
971
1194
|
.map((p) => `- ${p.name}: ${join(runRoot, safeName(p.name))}(开发分支 ${p.devBranch} → 基准分支 ${p.baselineBranch};测试分支 ${p.testBranch})${p.id === input.primaryProjectId ? "(主项目)" : ""}`)
|
|
972
1195
|
.join("\n");
|
|
973
1196
|
const phase = input.phase === "DEVELOPMENT" ? "DEVELOPMENT" : "INTEGRATION";
|
|
1197
|
+
const phaseLabel = phase === "DEVELOPMENT" ? "开发与自审" : "系统测试,当前已进入系统测试,不是开发阶段";
|
|
974
1198
|
const protocol = STAGE_DELIVERY_PROTOCOLS[phase];
|
|
975
1199
|
const delivery = input.stageDeliverySnapshots?.[phase];
|
|
976
1200
|
if (responsible && !delivery)
|
|
977
1201
|
throw new Error("STAGE_DELIVERY_CONFIGURATION_MISSING");
|
|
978
|
-
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;本地副本未被覆盖,对照文件不收集。` : ""}`;
|
|
979
1203
|
if (!responsible)
|
|
980
1204
|
return context;
|
|
981
|
-
|
|
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}`;
|
|
982
1209
|
}
|
|
983
1210
|
async function uploadBatch(client, runId, batch) {
|
|
984
1211
|
for (let attempt = 0;; attempt++) {
|
|
@@ -995,24 +1222,6 @@ async function uploadBatch(client, runId, batch) {
|
|
|
995
1222
|
}
|
|
996
1223
|
}
|
|
997
1224
|
}
|
|
998
|
-
function textFromMessage(content) {
|
|
999
|
-
if (typeof content === "string")
|
|
1000
|
-
return content;
|
|
1001
|
-
if (!Array.isArray(content))
|
|
1002
|
-
return "";
|
|
1003
|
-
return content
|
|
1004
|
-
.filter((part) => Boolean(part &&
|
|
1005
|
-
typeof part === "object" &&
|
|
1006
|
-
part.type === "text" &&
|
|
1007
|
-
typeof part.text === "string"))
|
|
1008
|
-
.map((part) => part.text)
|
|
1009
|
-
.join("\n");
|
|
1010
|
-
}
|
|
1011
|
-
function textFromResult(value) {
|
|
1012
|
-
if (!value || typeof value !== "object")
|
|
1013
|
-
return "";
|
|
1014
|
-
return textFromMessage(value.content).slice(0, 4000);
|
|
1015
|
-
}
|
|
1016
1225
|
function toolResult(text) {
|
|
1017
1226
|
return { content: [{ type: "text", text }], details: {} };
|
|
1018
1227
|
}
|
|
@@ -1047,6 +1256,29 @@ async function collectProjectFacts(runRoot, projects, key, primaryProjectId) {
|
|
|
1047
1256
|
};
|
|
1048
1257
|
}));
|
|
1049
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
|
+
}
|
|
1050
1282
|
async function captureProjectRevisions(runRoot, projects, key, primaryProjectId) {
|
|
1051
1283
|
return collectProjectFacts(runRoot, projects, key, primaryProjectId);
|
|
1052
1284
|
}
|