@bf-meishijian/aip 0.0.5 → 0.0.7
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 +114 -3
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +57 -23
- package/dist/cli.js.map +1 -1
- package/dist/coding-run-manager.d.ts +83 -17
- package/dist/coding-run-manager.d.ts.map +1 -1
- package/dist/coding-run-manager.js +861 -644
- 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 +83 -0
- package/dist/coding-subagent-child.js.map +1 -0
- package/dist/coding-subagents.d.ts +65 -0
- package/dist/coding-subagents.d.ts.map +1 -0
- package/dist/coding-subagents.js +521 -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 +4 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +189 -79
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-files.d.ts +24 -9
- package/dist/delivery-files.d.ts.map +1 -1
- package/dist/delivery-files.js +171 -45
- package/dist/delivery-files.js.map +1 -1
- package/dist/delivery-tools.d.ts +28 -0
- package/dist/delivery-tools.d.ts.map +1 -0
- package/dist/delivery-tools.js +167 -0
- package/dist/delivery-tools.js.map +1 -0
- package/dist/diagnostics.d.ts +1 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +48 -14
- package/dist/diagnostics.js.map +1 -1
- package/dist/installer.d.ts +5 -8
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +155 -65
- package/dist/installer.js.map +1 -1
- package/dist/linux-service.d.ts +14 -0
- package/dist/linux-service.d.ts.map +1 -0
- package/dist/linux-service.js +63 -0
- package/dist/linux-service.js.map +1 -0
- package/dist/live-employee.d.ts +6 -3
- package/dist/live-employee.d.ts.map +1 -1
- package/dist/live-employee.js +245 -49
- package/dist/live-employee.js.map +1 -1
- package/dist/local-handoff-workspace.d.ts +20 -0
- package/dist/local-handoff-workspace.d.ts.map +1 -0
- package/dist/local-handoff-workspace.js +80 -0
- package/dist/local-handoff-workspace.js.map +1 -0
- package/dist/local-model-usage.d.ts +6 -0
- package/dist/local-model-usage.d.ts.map +1 -0
- package/dist/local-model-usage.js +40 -0
- package/dist/local-model-usage.js.map +1 -0
- package/dist/login-url.d.ts +3 -0
- package/dist/login-url.d.ts.map +1 -0
- package/dist/login-url.js +30 -0
- package/dist/login-url.js.map +1 -0
- package/dist/paths.d.ts +9 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +27 -3
- package/dist/paths.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 +11 -0
- package/dist/pi-model-service.d.ts.map +1 -1
- package/dist/pi-model-service.js +28 -11
- package/dist/pi-model-service.js.map +1 -1
- package/dist/platform-client.d.ts +230 -6
- package/dist/platform-client.d.ts.map +1 -1
- package/dist/platform-client.js +87 -0
- package/dist/platform-client.js.map +1 -1
- package/dist/profile.d.ts +9 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +43 -0
- package/dist/profile.js.map +1 -0
- package/dist/project-facts.d.ts +19 -0
- package/dist/project-facts.d.ts.map +1 -0
- package/dist/project-facts.js +91 -0
- package/dist/project-facts.js.map +1 -0
- package/dist/provider-auth-error.d.ts +3 -0
- package/dist/provider-auth-error.d.ts.map +1 -0
- package/dist/provider-auth-error.js +12 -0
- package/dist/provider-auth-error.js.map +1 -0
- package/dist/qa-contract.d.ts +2 -0
- package/dist/qa-contract.d.ts.map +1 -0
- package/dist/qa-contract.js +165 -0
- package/dist/qa-contract.js.map +1 -0
- package/dist/qa-http-validation.js +145 -0
- package/dist/qa-project-checkout.d.ts +12 -0
- package/dist/qa-project-checkout.d.ts.map +1 -0
- package/dist/qa-project-checkout.js +68 -0
- package/dist/qa-project-checkout.js.map +1 -0
- package/dist/qa-session-manager.d.ts +101 -0
- package/dist/qa-session-manager.d.ts.map +1 -0
- package/dist/qa-session-manager.js +581 -0
- package/dist/qa-session-manager.js.map +1 -0
- package/dist/review-report.d.ts +9 -0
- package/dist/review-report.d.ts.map +1 -0
- package/dist/review-report.js +61 -0
- package/dist/review-report.js.map +1 -0
- package/dist/secure-json.d.ts.map +1 -1
- package/dist/secure-json.js +64 -2
- package/dist/secure-json.js.map +1 -1
- package/dist/stage-delivery.js +43 -25
- package/dist/state.d.ts +2 -2
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +6 -4
- package/dist/state.js.map +1 -1
- package/dist/test-cli.d.ts +2 -0
- package/dist/test-cli.d.ts.map +1 -0
- package/dist/test-cli.js +50 -0
- package/dist/test-cli.js.map +1 -0
- package/dist/test-executor.d.ts +44 -0
- package/dist/test-executor.d.ts.map +1 -0
- package/dist/test-executor.js +474 -0
- package/dist/test-executor.js.map +1 -0
- package/dist/test-reports.d.ts +13 -0
- package/dist/test-reports.d.ts.map +1 -0
- package/dist/test-reports.js +129 -0
- package/dist/test-reports.js.map +1 -0
- package/dist/test-run-manager.d.ts +24 -0
- package/dist/test-run-manager.d.ts.map +1 -0
- package/dist/test-run-manager.js +121 -0
- package/dist/test-run-manager.js.map +1 -0
- package/dist/tool-dispatch.d.ts +14 -0
- package/dist/tool-dispatch.d.ts.map +1 -0
- package/dist/tool-dispatch.js +56 -0
- package/dist/tool-dispatch.js.map +1 -0
- package/dist/windows-runtime.d.ts +9 -0
- package/dist/windows-runtime.d.ts.map +1 -0
- package/dist/windows-runtime.js +64 -0
- package/dist/windows-runtime.js.map +1 -0
- package/dist/windows-service.d.ts +20 -0
- package/dist/windows-service.d.ts.map +1 -0
- package/dist/windows-service.js +96 -0
- package/dist/windows-service.js.map +1 -0
- package/package.json +17 -6
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { mkdir,
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
3
|
+
import { mkdir, open, realpath, stat } from "node:fs/promises";
|
|
5
4
|
import { basename, dirname, join, resolve } from "node:path";
|
|
6
5
|
import { promisify } from "node:util";
|
|
7
6
|
import { createAgentSession, DefaultResourceLoader, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { deliveryTools } from "./delivery-tools.js";
|
|
8
|
+
import { inspectHandoffWorkspace, synchronizeLocalIntake, synchronizeRemoteWorkspace, } from "./local-handoff-workspace.js";
|
|
9
|
+
import { localModelUsage } from "./local-model-usage.js";
|
|
10
|
+
import { collectProjectFacts, worktreeTreeHash } from "./project-facts.js";
|
|
11
|
+
import { isProviderLoginRequired } from "./provider-auth-error.js";
|
|
12
|
+
export { worktreeTreeHash } from "./project-facts.js";
|
|
13
|
+
import { createCodingSubagents } from "./coding-subagents.js";
|
|
14
|
+
import { commandMaterialReader, deliveryDirectory, initializeDeliveryFiles, publishReviewMaterials, } from "./delivery-files.js";
|
|
10
15
|
import { liveEmployee } from "./live-employee.js";
|
|
11
|
-
import { prepareMergeRequest, registerMergeRequest, } from "./merge-requests.js";
|
|
12
16
|
import { paths } from "./paths.js";
|
|
17
|
+
import { PiMessageQueue } from "./pi-message-queue.js";
|
|
13
18
|
import { PlatformClientError } from "./platform-client.js";
|
|
19
|
+
import { materializeReviewReport } from "./review-report.js";
|
|
14
20
|
import { readJson, writeSecureJson } from "./secure-json.js";
|
|
15
|
-
import {
|
|
21
|
+
import { stageEmployeeIds } from "./stage-delivery.js";
|
|
16
22
|
const execFileAsync = promisify(execFile);
|
|
17
23
|
const EVENT_APPEND_MAX_ATTEMPTS = 8;
|
|
18
24
|
export class CodingRunManager {
|
|
@@ -25,8 +31,63 @@ export class CodingRunManager {
|
|
|
25
31
|
return this.active.size > 0;
|
|
26
32
|
}
|
|
27
33
|
async execute(client, host, command) {
|
|
34
|
+
const readMaterial = commandMaterialReader((source) => client.commandMaterial(command.id, {
|
|
35
|
+
hostId: host.hostId,
|
|
36
|
+
generation: host.generation,
|
|
37
|
+
contentHash: source.contentHash,
|
|
38
|
+
}));
|
|
28
39
|
if (!command.run)
|
|
29
40
|
throw new Error("CODING_RUN_COMMAND_INVALID");
|
|
41
|
+
if (command.run.action === "STOP" && command.run.runInput?.discussionTurn) {
|
|
42
|
+
const discussion = this.active.get(`${command.run.runId}:discussion`);
|
|
43
|
+
if (discussion && discussion.turnId === command.run.turnId) {
|
|
44
|
+
discussion.stopping = true;
|
|
45
|
+
discussion.observed.stopping = true;
|
|
46
|
+
await discussion.subagents?.stop();
|
|
47
|
+
await discussion.session.abort();
|
|
48
|
+
await discussion.emit("RUN_STOPPED", { phase: "DEVELOPMENT" });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const stateFile = join(paths().runState, `${command.run.runId}.json`);
|
|
52
|
+
const state = await readJson(stateFile);
|
|
53
|
+
await eventEmitter(client, host, command.run.runId, command.run.turnId, state?.lastEventSequence ?? 0, async (sequence) => {
|
|
54
|
+
const current = await readJson(stateFile);
|
|
55
|
+
if (current)
|
|
56
|
+
await writeSecureJson(stateFile, { ...current, lastEventSequence: sequence });
|
|
57
|
+
}, undefined, true)("RUN_STOPPED", { phase: "DEVELOPMENT" });
|
|
58
|
+
}
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (command.run.action === "REVIEW_REPORT") {
|
|
62
|
+
const report = command.run.reviewReport;
|
|
63
|
+
if (!report || !command.run.runInput)
|
|
64
|
+
throw new Error("REVIEW_REPORT_INPUT_MISSING");
|
|
65
|
+
const input = command.run.runInput;
|
|
66
|
+
const runRoot = join(paths().runWorktrees, command.run.runId);
|
|
67
|
+
if (input.transferContinuation) {
|
|
68
|
+
const notes = await inspectPriorTransferWorkspaces(paths().runWorktrees, input.transferWorkspaces?.priorRunIds ?? [], input.projects);
|
|
69
|
+
if (notes.length) {
|
|
70
|
+
const state = await readJson(join(paths().runState, `${command.run.runId}.json`));
|
|
71
|
+
await eventEmitter(client, host, command.run.runId, command.run.turnId, state?.lastEventSequence ?? 0)("MESSAGE_COMPLETED", { contentMarkdown: `旧工作区检查:\n${notes.join("\n")}` });
|
|
72
|
+
}
|
|
73
|
+
await mkdir(runRoot, { recursive: true, mode: 0o700 });
|
|
74
|
+
await prepareProjects(paths().repositoryCache, runRoot, input.workItem.key, input.projects, false, true, input.transferWorkspaces?.uninitializedProjectIds);
|
|
75
|
+
}
|
|
76
|
+
const files = await materializeReviewReport(runRoot, input, report.files, readMaterial);
|
|
77
|
+
await client.acknowledgeReviewReport(command.run.runId, {
|
|
78
|
+
hostId: host.hostId,
|
|
79
|
+
generation: host.generation,
|
|
80
|
+
turnId: command.run.turnId,
|
|
81
|
+
roundId: report.roundId,
|
|
82
|
+
files,
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (command.run.action === "RECALL_QUEUE") {
|
|
87
|
+
return {
|
|
88
|
+
recalledMessageIds: this.active.get(command.run.runId)?.observed.messageQueue?.recall() ?? [],
|
|
89
|
+
};
|
|
90
|
+
}
|
|
30
91
|
const imageRun = command.run;
|
|
31
92
|
let images;
|
|
32
93
|
try {
|
|
@@ -41,6 +102,7 @@ export class CodingRunManager {
|
|
|
41
102
|
if (active?.turnId === imageRun.turnId) {
|
|
42
103
|
active.stopping = true;
|
|
43
104
|
active.observed.stopping = true;
|
|
105
|
+
await active.subagents?.stop();
|
|
44
106
|
await active.session.abort();
|
|
45
107
|
await active.finished;
|
|
46
108
|
if (this.active.get(imageRun.runId) === active)
|
|
@@ -48,11 +110,32 @@ export class CodingRunManager {
|
|
|
48
110
|
}
|
|
49
111
|
throw error;
|
|
50
112
|
}
|
|
113
|
+
if (command.run.runInput?.discussionTurn) {
|
|
114
|
+
await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
|
|
115
|
+
const discussion = this.active.get(`${command.run.runId}:discussion`);
|
|
116
|
+
if (discussion && command.run.messageId)
|
|
117
|
+
await discussion.emit("MESSAGE_QUEUE_UPDATED", {
|
|
118
|
+
messageId: command.run.messageId,
|
|
119
|
+
deliveryStatus: "READ",
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
51
123
|
if (command.run.action === "START") {
|
|
52
124
|
const input = command.run.runInput;
|
|
53
|
-
await this.start(client, host, command.run.runId, command.run.turnId, input, false, command.run.contentMarkdown, images);
|
|
125
|
+
await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, input, false, command.run.contentMarkdown, images);
|
|
126
|
+
}
|
|
127
|
+
else if (command.run.action === "CONTINUE") {
|
|
128
|
+
const active = this.active.get(command.run.runId);
|
|
129
|
+
if (active) {
|
|
130
|
+
await active.finished;
|
|
131
|
+
if (active.stopping || active.observed.userIntervened)
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (!command.run.runInput || !command.run.contentMarkdown || !command.run.continuation)
|
|
135
|
+
throw new Error("CODING_RUN_CONTINUATION_MISSING");
|
|
136
|
+
await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, [], command.run.continuation);
|
|
54
137
|
}
|
|
55
|
-
else if (command.run.action === "STEER") {
|
|
138
|
+
else if (command.run.action === "STEER" || command.run.action === "FOLLOW_UP") {
|
|
56
139
|
let active = this.active.get(command.run.runId);
|
|
57
140
|
if (!command.run.contentMarkdown && images.length === 0)
|
|
58
141
|
throw new Error("CODING_RUN_MESSAGE_MISSING");
|
|
@@ -61,25 +144,65 @@ export class CodingRunManager {
|
|
|
61
144
|
throw new Error("CODING_RUN_TURN_MISMATCH");
|
|
62
145
|
active.stopping = true;
|
|
63
146
|
active.observed.stopping = true;
|
|
147
|
+
await active.emit("EXECUTION_DECIDED", {
|
|
148
|
+
phase: active.input.phase,
|
|
149
|
+
decision: "SKIP",
|
|
150
|
+
reason: "SUPERSEDED",
|
|
151
|
+
stopReason: "SUPERSEDED",
|
|
152
|
+
});
|
|
153
|
+
await active.subagents?.stop();
|
|
64
154
|
await active.session.abort();
|
|
65
155
|
if (this.active.get(command.run.runId) === active)
|
|
66
156
|
this.active.delete(command.run.runId);
|
|
67
157
|
active = undefined;
|
|
68
158
|
}
|
|
69
159
|
if (active) {
|
|
160
|
+
if (command.run.runInput)
|
|
161
|
+
active.input.handoff = command.run.runInput.handoff ?? null;
|
|
70
162
|
if (images.length > 0 && !active.supportsImages)
|
|
71
163
|
throw new Error("MODEL_IMAGES_UNSUPPORTED");
|
|
72
|
-
active.observed.
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
164
|
+
active.observed.userIntervened = true;
|
|
165
|
+
const handoff = active.observed.awaitingDecision || active.session.isIdle;
|
|
166
|
+
if (handoff)
|
|
167
|
+
active.observed.handingOffInput = true;
|
|
168
|
+
await active.emit("AUTO_CONTINUATION_CHANGED", {
|
|
169
|
+
phase: active.input.phase,
|
|
170
|
+
autoContinueEnabled: false,
|
|
171
|
+
reason: "USER_MESSAGE",
|
|
172
|
+
source: "HOST_HANDLED",
|
|
173
|
+
});
|
|
174
|
+
if (handoff || active.session.isIdle || active.observed.awaitingDecision) {
|
|
175
|
+
active.observed.handingOffInput = true;
|
|
176
|
+
// Pi is idle: steer only queues input and cannot start a new model turn.
|
|
177
|
+
// Finish the old judge operation before opening the user turn.
|
|
178
|
+
await active.finished;
|
|
179
|
+
if (!command.run.runInput)
|
|
180
|
+
throw new Error("CODING_RUN_INPUT_MISSING");
|
|
181
|
+
await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
|
|
182
|
+
if (command.run.messageId)
|
|
183
|
+
await this.active.get(command.run.runId)?.emit("MESSAGE_QUEUE_UPDATED", {
|
|
184
|
+
messageId: command.run.messageId,
|
|
185
|
+
deliveryStatus: "READ",
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (!command.run.messageId)
|
|
190
|
+
throw new Error("CODING_RUN_MESSAGE_ID_MISSING");
|
|
191
|
+
await active.observed.messageQueue.submit(command.run.messageId, command.run.action, command.run.contentMarkdown ?? "", images.length ? images : undefined);
|
|
192
|
+
await active.emit("MESSAGE_QUEUE_UPDATED", {
|
|
193
|
+
messageId: command.run.messageId,
|
|
194
|
+
deliveryStatus: "QUEUED",
|
|
195
|
+
});
|
|
78
196
|
}
|
|
79
197
|
else {
|
|
80
198
|
if (!command.run.runInput)
|
|
81
199
|
throw new Error("CODING_RUN_INPUT_MISSING");
|
|
82
|
-
await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
|
|
200
|
+
await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
|
|
201
|
+
if (command.run.messageId)
|
|
202
|
+
await this.active.get(command.run.runId)?.emit("MESSAGE_QUEUE_UPDATED", {
|
|
203
|
+
messageId: command.run.messageId,
|
|
204
|
+
deliveryStatus: "READ",
|
|
205
|
+
});
|
|
83
206
|
}
|
|
84
207
|
}
|
|
85
208
|
else if (command.run.action === "STOP") {
|
|
@@ -89,45 +212,95 @@ export class CodingRunManager {
|
|
|
89
212
|
throw new Error("CODING_RUN_TURN_MISMATCH");
|
|
90
213
|
active.stopping = true;
|
|
91
214
|
active.observed.stopping = true;
|
|
215
|
+
await active.subagents?.stop();
|
|
92
216
|
await active.session.abort();
|
|
217
|
+
await active.emit("AUTO_CONTINUATION_CHANGED", {
|
|
218
|
+
phase: active.input.phase,
|
|
219
|
+
autoContinueEnabled: false,
|
|
220
|
+
reason: "USER_STOP",
|
|
221
|
+
source: "HOST_HANDLED",
|
|
222
|
+
});
|
|
93
223
|
await active.emit.flush();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
224
|
+
await active.emit("EXECUTION_DECIDED", {
|
|
225
|
+
phase: active.input.phase,
|
|
226
|
+
decision: "SKIP",
|
|
227
|
+
reason: "STOP_REQUESTED",
|
|
228
|
+
stopReason: "USER_STOPPED",
|
|
229
|
+
});
|
|
230
|
+
await active.emit("RUN_STOPPED", {
|
|
231
|
+
phase: active.input.phase,
|
|
232
|
+
reason: "USER_STOPPED",
|
|
233
|
+
workspaceChanges: await Promise.all(active.input.projects.map(async (project) => ({
|
|
234
|
+
projectId: project.id,
|
|
235
|
+
...(await inspectHandoffWorkspace(join(active.runRoot, safeName(project.name)))),
|
|
236
|
+
}))),
|
|
237
|
+
});
|
|
103
238
|
}
|
|
104
239
|
else {
|
|
105
240
|
const stateFile = join(paths().runState, `${command.run.runId}.json`);
|
|
106
241
|
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", {
|
|
242
|
+
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", {
|
|
243
|
+
phase: state?.phase,
|
|
244
|
+
reason: "USER_STOPPED",
|
|
245
|
+
reminderDecision: "NO_REMINDER",
|
|
246
|
+
workspaceChanges: state?.mrContext?.projects
|
|
247
|
+
? await Promise.all(state.mrContext.projects.map(async (project) => ({
|
|
248
|
+
projectId: project.id,
|
|
249
|
+
...(await inspectHandoffWorkspace(join(paths().runWorktrees, command.run.runId, safeName(project.name)))),
|
|
250
|
+
})))
|
|
251
|
+
: [],
|
|
252
|
+
});
|
|
108
253
|
}
|
|
109
254
|
}
|
|
110
255
|
else if (command.run.action === "RESUME") {
|
|
111
|
-
await this.start(client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
|
|
256
|
+
await this.start(readMaterial, client, host, command.run.runId, command.run.turnId, command.run.runInput, true, command.run.contentMarkdown, images);
|
|
257
|
+
}
|
|
258
|
+
if (command.run.messageId &&
|
|
259
|
+
["START", "RESUME", "STEER", "FOLLOW_UP"].includes(command.run.action)) {
|
|
260
|
+
const active = this.active.get(command.run.runId);
|
|
261
|
+
if (active &&
|
|
262
|
+
(command.run.action === "RESUME" ||
|
|
263
|
+
command.run.action === "START" ||
|
|
264
|
+
!active.observed.messageQueue))
|
|
265
|
+
await active.emit("MESSAGE_QUEUE_UPDATED", {
|
|
266
|
+
messageId: command.run.messageId,
|
|
267
|
+
deliveryStatus: "READ",
|
|
268
|
+
});
|
|
112
269
|
}
|
|
113
270
|
}
|
|
114
|
-
async start(client, host, runId, turnId, input, resume, resumePrompt, images = []) {
|
|
115
|
-
|
|
271
|
+
async start(readMaterial, client, host, runId, turnId, input, resume, resumePrompt, images = [], continuation) {
|
|
272
|
+
if (!input.discussionTurn && input.phase === "DEVELOPMENT") {
|
|
273
|
+
const discussion = this.active.get(`${runId}:discussion`);
|
|
274
|
+
if (discussion) {
|
|
275
|
+
discussion.stopping = true;
|
|
276
|
+
discussion.observed.stopping = true;
|
|
277
|
+
await discussion.subagents?.stop();
|
|
278
|
+
await discussion.session.abort();
|
|
279
|
+
await discussion.emit("RUN_STOPPED", { phase: "DEVELOPMENT" });
|
|
280
|
+
await discussion.finished;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const activeKey = input.discussionTurn ? `${runId}:discussion` : runId;
|
|
284
|
+
const active = this.active.get(activeKey);
|
|
116
285
|
if (active) {
|
|
117
286
|
if (active.turnId === turnId)
|
|
118
287
|
return;
|
|
119
|
-
if (!active.observed.terminalReached)
|
|
288
|
+
if (!active.observed.terminalReached && !active.stopping)
|
|
120
289
|
throw new Error("CODING_RUN_TURN_MISMATCH");
|
|
121
290
|
await active.finished;
|
|
122
|
-
if (this.active.get(
|
|
123
|
-
this.active.delete(
|
|
291
|
+
if (this.active.get(activeKey) === active)
|
|
292
|
+
this.active.delete(activeKey);
|
|
124
293
|
}
|
|
125
|
-
if (process.env.AIP_TEST_PI === "1") {
|
|
294
|
+
if (process.env.AIP_TEST_PI === "1" && !input.discussionTurn) {
|
|
126
295
|
await this.synthetic(client, host, runId, turnId, input);
|
|
127
296
|
return;
|
|
128
297
|
}
|
|
129
298
|
const local = paths();
|
|
130
|
-
const
|
|
299
|
+
const developmentRoot = join(local.runWorktrees, runId);
|
|
300
|
+
const deploymentSession = input.phase === "DEPLOYMENT" && input.executionPurpose !== "RELEASE";
|
|
301
|
+
const runRoot = deploymentSession
|
|
302
|
+
? join(local.root, "workspaces", "deployments", runId)
|
|
303
|
+
: developmentRoot;
|
|
131
304
|
await mkdir(runRoot, { recursive: true, mode: 0o700 });
|
|
132
305
|
const stateFile = join(local.runState, `${runId}.json`);
|
|
133
306
|
const storedState = await readJson(stateFile);
|
|
@@ -135,20 +308,56 @@ export class CodingRunManager {
|
|
|
135
308
|
Object.keys(storedState.phaseSessions).length &&
|
|
136
309
|
storedState.harness !== "pi")
|
|
137
310
|
throw new Error("PRIOR_HARNESS_SESSION_UNSUPPORTED:请新建执行,历史文件和工作目录已保留。");
|
|
138
|
-
const cwd = await prepareProjects(local.repositoryCache, runRoot, runId, input.projects, Boolean(storedState?.prepared));
|
|
139
311
|
for (const project of input.projects)
|
|
140
|
-
project.devBranch ||=
|
|
141
|
-
|
|
142
|
-
|
|
312
|
+
project.devBranch ||=
|
|
313
|
+
storedState?.mrContext?.projects.find((p) => p.id === project.id)?.devBranch ||
|
|
314
|
+
`aip/${input.workItem.key}`;
|
|
315
|
+
const transferNotes = input.transferContinuation && !storedState?.prepared
|
|
316
|
+
? await inspectPriorTransferWorkspaces(local.runWorktrees, input.transferWorkspaces?.priorRunIds ?? [], input.projects)
|
|
317
|
+
: [];
|
|
318
|
+
const cwd = input.discussionTurn
|
|
319
|
+
? input.projects.length === 1
|
|
320
|
+
? join(runRoot, safeName(input.projects[0].name))
|
|
321
|
+
: runRoot
|
|
322
|
+
: deploymentSession
|
|
323
|
+
? await prepareDeploymentProjects(runRoot, input.projects, input.deploymentConfiguration)
|
|
324
|
+
: await prepareProjects(local.repositoryCache, runRoot, input.workItem.key, input.projects, Boolean(storedState?.prepared), input.transferContinuation === true, input.transferWorkspaces?.uninitializedProjectIds);
|
|
325
|
+
let appliedLocalIntake = storedState?.appliedLocalIntake;
|
|
326
|
+
if (!input.discussionTurn && input.localIntake && appliedLocalIntake !== input.localIntake.id) {
|
|
327
|
+
for (const project of input.projects) {
|
|
328
|
+
const accepted = input.localIntake.projects.find((fact) => fact.projectId === project.id);
|
|
329
|
+
if (!accepted || accepted.branch !== project.devBranch)
|
|
330
|
+
throw new Error("LOCAL_INTAKE_PROJECT_MISMATCH");
|
|
331
|
+
await synchronizeLocalIntake(join(developmentRoot, safeName(project.name)), accepted.commitSha);
|
|
332
|
+
}
|
|
333
|
+
appliedLocalIntake = input.localIntake.id;
|
|
334
|
+
await writeSecureJson(stateFile, { ...storedState, appliedLocalIntake });
|
|
335
|
+
}
|
|
336
|
+
const preparationBaselines = storedState?.preparationBaselines ?? {};
|
|
337
|
+
if (input.phase === "PREPARATION" && !storedState?.prepared) {
|
|
338
|
+
for (const project of input.projects)
|
|
339
|
+
preparationBaselines[project.id] ??= (await execFileAsync("git", ["rev-parse", "HEAD"], {
|
|
340
|
+
cwd: join(runRoot, safeName(project.name)),
|
|
341
|
+
})).stdout.trim();
|
|
342
|
+
await writeSecureJson(stateFile, { ...storedState, preparationBaselines });
|
|
343
|
+
}
|
|
344
|
+
await initializeDeliveryFiles(runRoot, input, Boolean(storedState?.prepared), readMaterial);
|
|
143
345
|
const deliveryInputHash = createHash("sha256")
|
|
144
346
|
.update(JSON.stringify([
|
|
145
347
|
input.primaryProjectId,
|
|
146
|
-
input.requirement.
|
|
147
|
-
input.technicalDesign?.
|
|
348
|
+
input.requirement.source.contentHash,
|
|
349
|
+
input.technicalDesign?.source.contentHash,
|
|
148
350
|
]))
|
|
149
351
|
.digest("hex");
|
|
150
|
-
const
|
|
151
|
-
|
|
352
|
+
const newDeploymentRequest = deploymentSession &&
|
|
353
|
+
Boolean(input.deploymentRequestId) &&
|
|
354
|
+
input.deploymentRequestId !== storedState?.deploymentRequestId;
|
|
355
|
+
const deploymentRequestId = deploymentSession
|
|
356
|
+
? input.deploymentRequestId
|
|
357
|
+
: storedState?.deploymentRequestId;
|
|
358
|
+
const delegations = storedState?.delegations ?? {};
|
|
359
|
+
let pendingDelivery = newDeploymentRequest ||
|
|
360
|
+
storedState?.phase !== input.phase ||
|
|
152
361
|
resumePrompt ||
|
|
153
362
|
images.length > 0 ||
|
|
154
363
|
(storedState?.deliveryInputHash && storedState.deliveryInputHash !== deliveryInputHash)
|
|
@@ -163,39 +372,54 @@ export class CodingRunManager {
|
|
|
163
372
|
: undefined;
|
|
164
373
|
const phaseSessions = storedState?.phaseSessions ?? {};
|
|
165
374
|
const sessionPhase = input.executionPurpose === "RELEASE" ? "DEVELOPMENT" : input.phase;
|
|
166
|
-
let sessionRef = phaseSessions[sessionPhase];
|
|
375
|
+
let sessionRef = newDeploymentRequest ? undefined : phaseSessions[sessionPhase];
|
|
167
376
|
let lastEventSequence = storedState?.lastEventSequence ?? 0;
|
|
168
377
|
const observed = {
|
|
169
|
-
|
|
170
|
-
memberResults: new Map(),
|
|
378
|
+
userIntervened: input.autoContinueEnabled === false,
|
|
171
379
|
activeTasks: new Map(),
|
|
172
380
|
stopping: false,
|
|
173
381
|
terminalReached: false,
|
|
174
382
|
};
|
|
175
|
-
const persistState = () =>
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
383
|
+
const persistState = () => input.discussionTurn
|
|
384
|
+
? Promise.resolve()
|
|
385
|
+
: writeSecureJson(stateFile, {
|
|
386
|
+
phase: input.phase,
|
|
387
|
+
appliedLocalIntake,
|
|
388
|
+
mrContext: {
|
|
389
|
+
runId,
|
|
390
|
+
turnId,
|
|
391
|
+
workItem: input.workItem,
|
|
392
|
+
projects: input.projects,
|
|
393
|
+
primaryProjectId: input.primaryProjectId,
|
|
394
|
+
},
|
|
395
|
+
prepared: true,
|
|
396
|
+
preparationBaselines,
|
|
397
|
+
phaseSessions,
|
|
398
|
+
deploymentRequestId,
|
|
399
|
+
sessionRef,
|
|
400
|
+
harness: "pi",
|
|
401
|
+
lastEventSequence,
|
|
402
|
+
delegations,
|
|
403
|
+
pendingDelivery,
|
|
404
|
+
deliveryInputHash,
|
|
405
|
+
});
|
|
194
406
|
const redact = (value) => redactHostText(value, [runRoot, local.root]);
|
|
195
|
-
const emit = eventEmitter(client, host, runId, turnId, lastEventSequence,
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
407
|
+
const emit = eventEmitter(client, host, runId, turnId, lastEventSequence, input.discussionTurn
|
|
408
|
+
? async (sequence) => {
|
|
409
|
+
const current = await readJson(stateFile);
|
|
410
|
+
if (current)
|
|
411
|
+
await writeSecureJson(stateFile, { ...current, lastEventSequence: sequence });
|
|
412
|
+
}
|
|
413
|
+
: (sequence) => {
|
|
414
|
+
lastEventSequence = sequence;
|
|
415
|
+
return persistState();
|
|
416
|
+
}, redact, Boolean(input.discussionTurn));
|
|
417
|
+
if (transferNotes.length)
|
|
418
|
+
await emit("MESSAGE_COMPLETED", {
|
|
419
|
+
contentMarkdown: `旧工作区检查:\n${transferNotes.join("\n")}`,
|
|
420
|
+
});
|
|
421
|
+
if (!storedState?.prepared)
|
|
422
|
+
await publishReviewMaterials(runRoot, input, readMaterial);
|
|
199
423
|
if (input.phase === "PREPARATION") {
|
|
200
424
|
await persistState();
|
|
201
425
|
await emit("PREPARATION_COMPLETED", {
|
|
@@ -203,9 +427,7 @@ export class CodingRunManager {
|
|
|
203
427
|
projects: await Promise.all(input.projects.map(async (p) => ({
|
|
204
428
|
projectId: p.id,
|
|
205
429
|
projectName: p.name,
|
|
206
|
-
baselineCommit:
|
|
207
|
-
cwd: join(runRoot, safeName(p.name)),
|
|
208
|
-
})).stdout.trim(),
|
|
430
|
+
baselineCommit: preparationBaselines[p.id] ?? p.baselineCommit,
|
|
209
431
|
directory: join(runRoot, safeName(p.name)),
|
|
210
432
|
branch: (await execFileAsync("git", ["branch", "--show-current"], {
|
|
211
433
|
cwd: join(runRoot, safeName(p.name)),
|
|
@@ -214,10 +436,20 @@ export class CodingRunManager {
|
|
|
214
436
|
});
|
|
215
437
|
return;
|
|
216
438
|
}
|
|
217
|
-
const
|
|
439
|
+
const binding = {
|
|
440
|
+
hostId: host.hostId,
|
|
441
|
+
generation: host.generation,
|
|
442
|
+
turnId,
|
|
443
|
+
member: "supervisor",
|
|
444
|
+
};
|
|
445
|
+
const initialEmployeeConfig = await client.employeeConfig(runId, {
|
|
446
|
+
...binding,
|
|
447
|
+
purpose: "LOAD",
|
|
448
|
+
});
|
|
449
|
+
const terminal = deliveryTools(client, host, runId, turnId, developmentRoot, input, observed, async (batch) => {
|
|
218
450
|
pendingDelivery = batch;
|
|
219
451
|
await persistState();
|
|
220
|
-
}, () => pendingDelivery, (
|
|
452
|
+
}, () => pendingDelivery, () => emit.flush(), initialEmployeeConfig.toolDefinitions);
|
|
221
453
|
if (!this.pi.resolveRuntimeModel)
|
|
222
454
|
throw new Error("MODEL_RUNTIME_UNAVAILABLE");
|
|
223
455
|
const resolved = await this.pi.resolveRuntimeModel(input.providerId, input.modelId, {
|
|
@@ -228,126 +460,43 @@ export class CodingRunManager {
|
|
|
228
460
|
});
|
|
229
461
|
if (images.length > 0 && !resolved.model.input?.includes("image"))
|
|
230
462
|
throw new Error("MODEL_IMAGES_UNSUPPORTED");
|
|
231
|
-
const binding = {
|
|
232
|
-
hostId: host.hostId,
|
|
233
|
-
generation: host.generation,
|
|
234
|
-
turnId,
|
|
235
|
-
member: "supervisor",
|
|
236
|
-
};
|
|
237
463
|
const availableAgents = stageEmployeeIds(input);
|
|
238
|
-
const live = liveEmployee(client, runId, binding,
|
|
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
|
-
|
|
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
|
-
}
|
|
464
|
+
const live = liveEmployee(client, runId, binding, initialEmployeeConfig, runRoot, Boolean(input.team), input.executionPurpose === "RELEASE", input.phase, () => observed.terminalReached, () => executionInstructions(input, runRoot), () => {
|
|
465
|
+
observed.terminalClosed = true;
|
|
466
|
+
});
|
|
467
|
+
const delegation = input.executionPurpose === "RELEASE"
|
|
468
|
+
? undefined
|
|
469
|
+
: await createCodingSubagents({
|
|
470
|
+
client,
|
|
471
|
+
toolDefinitions: initialEmployeeConfig.toolDefinitions,
|
|
472
|
+
config: {
|
|
473
|
+
runId,
|
|
474
|
+
turnId,
|
|
475
|
+
hostId: host.hostId,
|
|
476
|
+
generation: host.generation,
|
|
477
|
+
runRoot,
|
|
478
|
+
providerId: input.providerId,
|
|
479
|
+
modelId: input.modelId,
|
|
480
|
+
phase: input.phase,
|
|
481
|
+
},
|
|
482
|
+
cwd,
|
|
483
|
+
directory: join(local.runState, runId, "subagents", input.phase),
|
|
484
|
+
team: Boolean(input.team),
|
|
485
|
+
agents: availableAgents.map((id) => ({
|
|
486
|
+
id,
|
|
487
|
+
name: input.teamEmployees?.[id]?.name ?? id,
|
|
488
|
+
description: input.teamEmployees?.[id]?.description ?? "",
|
|
489
|
+
})),
|
|
490
|
+
records: delegations,
|
|
491
|
+
emit,
|
|
492
|
+
persist: persistState,
|
|
493
|
+
started(activityKey, agentId) {
|
|
494
|
+
observed.activeTasks.set(activityKey, agentId);
|
|
495
|
+
},
|
|
496
|
+
async completed(activityKey) {
|
|
497
|
+
observed.activeTasks.delete(activityKey);
|
|
348
498
|
},
|
|
349
499
|
});
|
|
350
|
-
};
|
|
351
500
|
const opened = await openPiSession({
|
|
352
501
|
cwd,
|
|
353
502
|
sessionDir: local.piSessions,
|
|
@@ -356,7 +505,7 @@ export class CodingRunManager {
|
|
|
356
505
|
modelRuntime: resolved.runtime,
|
|
357
506
|
model: resolved.model,
|
|
358
507
|
thinkingLevel: input.thinkingLevel,
|
|
359
|
-
extensions: [...(
|
|
508
|
+
extensions: [...(delegation ? [delegation.extension] : []), terminal, live.extension],
|
|
360
509
|
systemPrompt: input.team
|
|
361
510
|
? "当前为团队主负责人会话,员工配置由平台下发。"
|
|
362
511
|
: "当前为工单主会话,员工配置由平台下发。",
|
|
@@ -365,6 +514,7 @@ export class CodingRunManager {
|
|
|
365
514
|
live.attach(opened.session);
|
|
366
515
|
const session = opened.session;
|
|
367
516
|
sessionRef = opened.sessionId;
|
|
517
|
+
observed.messageQueue = new PiMessageQueue(session);
|
|
368
518
|
subscribeSession(session, (kind, payload) => {
|
|
369
519
|
const activityKey = String(payload.activityKey ?? "");
|
|
370
520
|
if (kind === "SUBAGENT_STARTED")
|
|
@@ -379,6 +529,7 @@ export class CodingRunManager {
|
|
|
379
529
|
});
|
|
380
530
|
}, observed, redact);
|
|
381
531
|
const activeRun = {
|
|
532
|
+
runRoot,
|
|
382
533
|
session,
|
|
383
534
|
supportsImages: resolved.model.input?.includes("image") === true,
|
|
384
535
|
input,
|
|
@@ -386,31 +537,96 @@ export class CodingRunManager {
|
|
|
386
537
|
emit,
|
|
387
538
|
stopping: false,
|
|
388
539
|
observed,
|
|
540
|
+
...(delegation ? { subagents: delegation } : {}),
|
|
389
541
|
};
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
542
|
+
const disposeRun = async () => {
|
|
543
|
+
try {
|
|
544
|
+
try {
|
|
545
|
+
await delegation?.stop();
|
|
546
|
+
await delegation?.flush();
|
|
547
|
+
}
|
|
548
|
+
finally {
|
|
549
|
+
await delegation?.dispose();
|
|
550
|
+
await session.extensionRunner?.emit({ type: "session_shutdown", reason: "quit" });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
finally {
|
|
554
|
+
session.dispose();
|
|
555
|
+
if (this.active.get(activeKey) === activeRun)
|
|
556
|
+
this.active.delete(activeKey);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
this.active.set(activeKey, activeRun);
|
|
560
|
+
let conversationFingerprint;
|
|
561
|
+
let prompt;
|
|
562
|
+
try {
|
|
563
|
+
phaseSessions[sessionPhase] = sessionRef;
|
|
564
|
+
await persistState();
|
|
565
|
+
await emit("RUN_STARTED", { sessionRef, phase: input.phase });
|
|
566
|
+
if (!input.discussionTurn &&
|
|
567
|
+
!continuation &&
|
|
568
|
+
(input.autoContinueEnabled === false || (!resume && input.autoContinueEnabled === true))) {
|
|
569
|
+
await emit("AUTO_CONTINUATION_CHANGED", {
|
|
570
|
+
phase: input.phase,
|
|
571
|
+
autoContinueEnabled: input.autoContinueEnabled === true,
|
|
572
|
+
reason: input.autoContinueEnabled === true ? "SYSTEM_STAGE" : "USER_MESSAGE",
|
|
573
|
+
source: "HOST_HANDLED",
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (!input.discussionTurn && !continuation && (resumePrompt || images.length > 0)) {
|
|
577
|
+
await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
|
|
578
|
+
await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
|
|
579
|
+
}
|
|
580
|
+
conversationFingerprint = await workspaceFingerprint(runRoot, input);
|
|
581
|
+
prompt = continuation
|
|
582
|
+
? resumePrompt
|
|
583
|
+
: `${initialPrompt(input, runRoot, true, developmentRoot)}${transferNotes.length ? `\n\n旧工作区检查:\n${transferNotes.join("\n")}` : ""}${resumePrompt ? `\n\n用户本次消息:${resumePrompt}` : resume && !resumePrompt ? "\n\n继续当前会话未完成的工作,先读取最新文件和交接结果。" : ""}`;
|
|
584
|
+
}
|
|
585
|
+
catch (error) {
|
|
586
|
+
await disposeRun();
|
|
587
|
+
throw error;
|
|
397
588
|
}
|
|
398
|
-
const
|
|
399
|
-
|
|
589
|
+
const discussion = async () => {
|
|
590
|
+
await session.prompt(prompt, images.length ? { images } : undefined);
|
|
591
|
+
await session.waitForIdle();
|
|
592
|
+
if (!observed.stopping)
|
|
593
|
+
await emit("CONVERSATION_COMPLETED", {
|
|
594
|
+
phase: "DEVELOPMENT",
|
|
595
|
+
workspaceChanged: conversationFingerprint !== (await workspaceFingerprint(runRoot, input)),
|
|
596
|
+
});
|
|
597
|
+
};
|
|
598
|
+
activeRun.finished = (input.discussionTurn
|
|
599
|
+
? discussion()
|
|
600
|
+
: driveToTerminal(session, prompt, observed, client, host, runId, turnId, emit, input.phase, images, async () => {
|
|
601
|
+
await emit("CONVERSATION_COMPLETED", {
|
|
602
|
+
workspaceChanged: conversationFingerprint !== (await workspaceFingerprint(runRoot, input)),
|
|
603
|
+
});
|
|
604
|
+
}, continuation))
|
|
400
605
|
.catch(async (error) => {
|
|
401
606
|
if (activeRun.stopping)
|
|
402
607
|
return;
|
|
608
|
+
if (!input.discussionTurn)
|
|
609
|
+
await emit("EXECUTION_DECIDED", {
|
|
610
|
+
phase: input.phase,
|
|
611
|
+
decision: "SKIP",
|
|
612
|
+
reason: "MODEL_ERROR",
|
|
613
|
+
stopReason: "error",
|
|
614
|
+
});
|
|
403
615
|
await emit("RUN_FAILED", {
|
|
404
|
-
|
|
616
|
+
phase: input.phase,
|
|
617
|
+
code: error instanceof Error && /^[A-Z][A-Z0-9_]+$/u.test(error.message)
|
|
618
|
+
? error.message.slice(0, 200)
|
|
619
|
+
: "PI_RUN_FAILED",
|
|
620
|
+
message: redact(error instanceof Error ? error.message : String(error)),
|
|
405
621
|
});
|
|
406
622
|
})
|
|
407
623
|
.finally(async () => {
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
624
|
+
try {
|
|
625
|
+
await emit.flush();
|
|
626
|
+
}
|
|
627
|
+
finally {
|
|
628
|
+
await disposeRun();
|
|
629
|
+
}
|
|
414
630
|
});
|
|
415
631
|
}
|
|
416
632
|
async synthetic(client, host, runId, turnId, input) {
|
|
@@ -446,25 +662,33 @@ export class CodingRunManager {
|
|
|
446
662
|
treeHash: "a".repeat(40),
|
|
447
663
|
branch: project.testBranch,
|
|
448
664
|
})),
|
|
449
|
-
testFacts:
|
|
450
|
-
projectId: project.id,
|
|
451
|
-
command: "pnpm test",
|
|
452
|
-
exitCode: 0,
|
|
453
|
-
commitSha: project.baselineCommit,
|
|
454
|
-
treeHash: "a".repeat(40),
|
|
455
|
-
finishedAt: new Date().toISOString(),
|
|
456
|
-
})),
|
|
665
|
+
testFacts: [],
|
|
457
666
|
});
|
|
458
667
|
}
|
|
459
668
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
let
|
|
669
|
+
const eventStreams = new WeakMap();
|
|
670
|
+
function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistSequence, redact = (value) => redactHostText(value), preservePersist = false) {
|
|
671
|
+
let streams = eventStreams.get(client);
|
|
672
|
+
if (!streams) {
|
|
673
|
+
streams = new Map();
|
|
674
|
+
eventStreams.set(client, streams);
|
|
675
|
+
}
|
|
676
|
+
let stream = streams.get(runId);
|
|
677
|
+
if (!stream) {
|
|
678
|
+
stream = { sequence: initialSequence, queue: Promise.resolve() };
|
|
679
|
+
streams.set(runId, stream);
|
|
680
|
+
}
|
|
681
|
+
stream.sequence = Math.max(stream.sequence, initialSequence);
|
|
682
|
+
if (persistSequence && (!preservePersist || !stream.persist))
|
|
683
|
+
stream.persist = persistSequence;
|
|
684
|
+
const state = stream;
|
|
463
685
|
const emit = (kind, payload) => {
|
|
464
|
-
queue = queue
|
|
686
|
+
state.queue = state.queue
|
|
687
|
+
.catch(() => { })
|
|
688
|
+
.then(async () => {
|
|
465
689
|
const event = {
|
|
466
690
|
eventId: crypto.randomUUID(),
|
|
467
|
-
sequence: sequence + 1,
|
|
691
|
+
sequence: state.sequence + 1,
|
|
468
692
|
turnId,
|
|
469
693
|
kind,
|
|
470
694
|
payload: kind === "PREPARATION_COMPLETED" ? payload : redactPayload(payload, redact),
|
|
@@ -477,27 +701,32 @@ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistS
|
|
|
477
701
|
generation: host.generation,
|
|
478
702
|
events: [event],
|
|
479
703
|
});
|
|
480
|
-
sequence = event.sequence;
|
|
481
|
-
await
|
|
704
|
+
state.sequence = event.sequence;
|
|
705
|
+
await state.persist?.(event.sequence);
|
|
482
706
|
return;
|
|
483
707
|
}
|
|
484
708
|
catch (error) {
|
|
485
709
|
if (error instanceof PlatformClientError &&
|
|
486
710
|
error.status === 409 &&
|
|
487
|
-
error.code === "CODING_EVENT_TURN_STALE")
|
|
711
|
+
error.code === "CODING_EVENT_TURN_STALE") {
|
|
712
|
+
if (["EXECUTION_STARTED", "EXECUTION_PAUSED", "CONVERSATION_COMPLETED"].includes(kind))
|
|
713
|
+
throw error;
|
|
488
714
|
return;
|
|
715
|
+
}
|
|
489
716
|
process.stderr.write(`[local-coding-host] 事件上报失败:${redact(error instanceof Error ? error.message : String(error))}\n`);
|
|
490
717
|
if (attempt === EVENT_APPEND_MAX_ATTEMPTS) {
|
|
491
718
|
process.stderr.write(`[local-coding-host] 事件上报放弃:${event.kind} sequence=${event.sequence} attempts=${attempt}\n`);
|
|
719
|
+
if (["EXECUTION_STARTED", "EXECUTION_PAUSED", "CONVERSATION_COMPLETED"].includes(kind))
|
|
720
|
+
throw error;
|
|
492
721
|
return;
|
|
493
722
|
}
|
|
494
723
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
495
724
|
}
|
|
496
725
|
}
|
|
497
726
|
});
|
|
498
|
-
return queue;
|
|
727
|
+
return state.queue;
|
|
499
728
|
};
|
|
500
|
-
return Object.assign(emit, { flush: () => queue });
|
|
729
|
+
return Object.assign(emit, { flush: () => state.queue });
|
|
501
730
|
}
|
|
502
731
|
function redactPayload(value, redact) {
|
|
503
732
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactPayloadValue(item, redact)]));
|
|
@@ -511,7 +740,7 @@ function redactPayloadValue(value, redact) {
|
|
|
511
740
|
return redactPayload(value, redact);
|
|
512
741
|
return value;
|
|
513
742
|
}
|
|
514
|
-
async function openPiSession(options) {
|
|
743
|
+
export async function openPiSession(options) {
|
|
515
744
|
await mkdir(options.sessionDir, { recursive: true, mode: 0o700 });
|
|
516
745
|
await mkdir(options.agentDir, { recursive: true, mode: 0o700 });
|
|
517
746
|
let sessionManager;
|
|
@@ -558,10 +787,49 @@ async function openPiSession(options) {
|
|
|
558
787
|
resourceLoader: loader,
|
|
559
788
|
sessionManager,
|
|
560
789
|
});
|
|
790
|
+
const savedModel = sessionManager.buildSessionContext().model;
|
|
791
|
+
if (options.model &&
|
|
792
|
+
(savedModel?.provider !== options.model.provider || savedModel?.modelId !== options.model.id)) {
|
|
793
|
+
try {
|
|
794
|
+
// SDK 的显式 model 决定本轮调用,已有会话的模型变更仍通过原生接口落盘。
|
|
795
|
+
await session.setModel(options.model);
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
session.dispose();
|
|
799
|
+
throw error;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
// SDK 的显式 thinkingLevel 不更新已有会话元数据,补记本轮实际档位。
|
|
803
|
+
if (sessionManager.buildSessionContext().thinkingLevel !== session.thinkingLevel) {
|
|
804
|
+
sessionManager.appendThinkingLevelChange(session.thinkingLevel);
|
|
805
|
+
}
|
|
806
|
+
await session.bindExtensions({ mode: "print" });
|
|
561
807
|
session.setAutoCompactionEnabled(true);
|
|
562
808
|
return { session, sessionId };
|
|
563
809
|
}
|
|
564
810
|
function subscribeSession(session, emit, observed, redact) {
|
|
811
|
+
const publishContext = (compacting = false) => {
|
|
812
|
+
const model = session.model;
|
|
813
|
+
if (!model)
|
|
814
|
+
return;
|
|
815
|
+
const usage = session.getContextUsage();
|
|
816
|
+
if (!usage)
|
|
817
|
+
return;
|
|
818
|
+
const settings = session.settingsManager.getCompactionSettings(model);
|
|
819
|
+
void emit("CONTEXT_UPDATED", {
|
|
820
|
+
contextUsage: {
|
|
821
|
+
modelId: `${model.provider}/${model.id}`,
|
|
822
|
+
contextWindow: usage.contextWindow,
|
|
823
|
+
tokens: compacting ? null : usage.tokens,
|
|
824
|
+
status: compacting ? "COMPACTING" : usage.tokens === null ? "UNKNOWN" : "READY",
|
|
825
|
+
source: "ESTIMATE",
|
|
826
|
+
compressionThreshold: settings.enabled
|
|
827
|
+
? Math.max(1, usage.contextWindow - settings.reserveTokens)
|
|
828
|
+
: null,
|
|
829
|
+
},
|
|
830
|
+
});
|
|
831
|
+
};
|
|
832
|
+
publishContext();
|
|
565
833
|
let messageId = crypto.randomUUID();
|
|
566
834
|
let pendingText = "";
|
|
567
835
|
let textFlushTimer;
|
|
@@ -588,17 +856,24 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
588
856
|
void emit(update.kind, update.payload);
|
|
589
857
|
};
|
|
590
858
|
session.subscribe((event) => {
|
|
591
|
-
if (event.type === "
|
|
859
|
+
if (event.type === "agent_start") {
|
|
860
|
+
publishContext();
|
|
861
|
+
if (observed.messageQueue)
|
|
862
|
+
void emit("MESSAGE_QUEUE_UPDATED", { mainAgentRunning: true });
|
|
863
|
+
}
|
|
864
|
+
else if (event.type === "agent_settled") {
|
|
865
|
+
publishContext();
|
|
866
|
+
if (observed.messageQueue)
|
|
867
|
+
void emit("MESSAGE_QUEUE_UPDATED", { mainAgentRunning: false });
|
|
868
|
+
}
|
|
869
|
+
else if (event.type === "message_start" && event.message.role === "assistant") {
|
|
592
870
|
messageId = crypto.randomUUID();
|
|
593
871
|
void emit("MESSAGE_STARTED", { messageId });
|
|
594
872
|
}
|
|
595
873
|
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
|
-
}
|
|
874
|
+
const id = observed.messageQueue?.consumed(event.message);
|
|
875
|
+
if (id)
|
|
876
|
+
void emit("MESSAGE_QUEUE_UPDATED", { messageId: id, deliveryStatus: "READ" });
|
|
602
877
|
}
|
|
603
878
|
else if (event.type === "agent_end") {
|
|
604
879
|
observed.nativeWillRetry = event.willRetry === true;
|
|
@@ -619,65 +894,39 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
619
894
|
}
|
|
620
895
|
else if (event.type === "tool_execution_start") {
|
|
621
896
|
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
|
-
});
|
|
897
|
+
if (event.toolName === "subagent")
|
|
898
|
+
return;
|
|
899
|
+
void emit("TOOL_STARTED", {
|
|
900
|
+
activityKey: event.toolCallId,
|
|
901
|
+
toolName: event.toolName,
|
|
902
|
+
summary: `执行 ${event.toolName}`,
|
|
903
|
+
});
|
|
637
904
|
}
|
|
638
905
|
else if (event.type === "tool_execution_update") {
|
|
906
|
+
if (event.toolName === "subagent")
|
|
907
|
+
return;
|
|
639
908
|
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
|
-
},
|
|
909
|
+
kind: "TOOL_UPDATED",
|
|
910
|
+
payload: {
|
|
911
|
+
activityKey: event.toolCallId,
|
|
912
|
+
toolName: event.toolName,
|
|
913
|
+
currentAction: redact(textFromResult(event.partialResult)),
|
|
914
|
+
},
|
|
652
915
|
});
|
|
653
916
|
if (!toolUpdateTimers.has(event.toolCallId))
|
|
654
917
|
toolUpdateTimers.set(event.toolCallId, setTimeout(() => flushToolUpdate(event.toolCallId), 200));
|
|
655
918
|
}
|
|
656
919
|
else if (event.type === "tool_execution_end") {
|
|
657
|
-
|
|
658
|
-
const member = observed.activeTasks.get(event.toolCallId);
|
|
920
|
+
publishContext();
|
|
659
921
|
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
|
-
});
|
|
922
|
+
return;
|
|
923
|
+
flushToolUpdate(event.toolCallId);
|
|
924
|
+
void emit("TOOL_COMPLETED", {
|
|
925
|
+
activityKey: event.toolCallId,
|
|
926
|
+
toolName: event.toolName,
|
|
927
|
+
isError: event.isError === true,
|
|
928
|
+
summary: redact(textFromResult(event.result)),
|
|
929
|
+
});
|
|
681
930
|
}
|
|
682
931
|
else if (event.type === "bash_execution_update" && event.delta) {
|
|
683
932
|
const activityKey = event.id ?? "bash";
|
|
@@ -693,6 +942,7 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
693
942
|
toolUpdateTimers.set(activityKey, setTimeout(() => flushToolUpdate(activityKey), 200));
|
|
694
943
|
}
|
|
695
944
|
else if (event.type === "compaction_start") {
|
|
945
|
+
publishContext(true);
|
|
696
946
|
flushText();
|
|
697
947
|
void emit("TOOL_STARTED", {
|
|
698
948
|
activityKey: `compaction:${event.reason}`,
|
|
@@ -701,6 +951,7 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
701
951
|
});
|
|
702
952
|
}
|
|
703
953
|
else if (event.type === "compaction_end") {
|
|
954
|
+
publishContext();
|
|
704
955
|
void emit("TOOL_COMPLETED", {
|
|
705
956
|
activityKey: `compaction:${event.reason}`,
|
|
706
957
|
toolName: "compaction",
|
|
@@ -711,6 +962,7 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
711
962
|
});
|
|
712
963
|
}
|
|
713
964
|
else if (event.type === "message_end" && event.message.role === "assistant") {
|
|
965
|
+
publishContext();
|
|
714
966
|
flushText();
|
|
715
967
|
if ("stopReason" in event.message && event.message.stopReason)
|
|
716
968
|
observed.lastModelStopReason = String(event.message.stopReason);
|
|
@@ -722,16 +974,8 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
722
974
|
contentMarkdown: redact(content),
|
|
723
975
|
messageId,
|
|
724
976
|
};
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
input: usage.input,
|
|
728
|
-
output: usage.output,
|
|
729
|
-
cacheRead: usage.cacheRead,
|
|
730
|
-
cacheWrite: usage.cacheWrite,
|
|
731
|
-
totalTokens: usage.totalTokens,
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
if (content.trim())
|
|
977
|
+
Object.assign(payload, localModelUsage(event.message, session.model));
|
|
978
|
+
if (content.trim() || usage)
|
|
735
979
|
void emit("MESSAGE_COMPLETED", payload);
|
|
736
980
|
}
|
|
737
981
|
});
|
|
@@ -750,260 +994,154 @@ function textFromResult(value) {
|
|
|
750
994
|
return "";
|
|
751
995
|
return textFromMessage(value.content);
|
|
752
996
|
}
|
|
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
|
-
|
|
997
|
+
async function driveToTerminal(session, initial, observed, client, host, runId, turnId, emit, phase, images = [], completeConversation = async () => { }, continuation) {
|
|
998
|
+
if (observed.stopping)
|
|
999
|
+
return;
|
|
1000
|
+
if (continuation && observed.userIntervened)
|
|
1001
|
+
return;
|
|
1002
|
+
const execution = images.length > 0 ? session.prompt(initial, { images }) : session.prompt(initial);
|
|
1003
|
+
// Observe a synchronous rejection while the invocation fact is persisted.
|
|
1004
|
+
const outcome = execution.then(() => ({ ok: true }), (error) => ({ ok: false, error }));
|
|
1005
|
+
if (continuation) {
|
|
1006
|
+
await emit("EXECUTION_CONTINUED", {
|
|
1007
|
+
phase,
|
|
1008
|
+
...continuation,
|
|
1009
|
+
prompt: initial,
|
|
1010
|
+
deliveryStatus: "SUBMITTED",
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
const result = await outcome;
|
|
1014
|
+
if (!result.ok)
|
|
1015
|
+
throw result.error;
|
|
1016
|
+
await session.waitForIdle();
|
|
1017
|
+
observed.awaitingDecision = true;
|
|
1018
|
+
if (observed.stopping)
|
|
1019
|
+
return;
|
|
1020
|
+
const stopReason = observed.terminalClosed
|
|
1021
|
+
? "TERMINAL_CLOSED"
|
|
1022
|
+
: (observed.lastModelStopReason ?? "idle");
|
|
1023
|
+
const endFact = { phase, stopReason, nativeStopReason: observed.lastModelStopReason ?? null };
|
|
1024
|
+
if (observed.terminalReached) {
|
|
1025
|
+
await emit("EXECUTION_DECIDED", {
|
|
1026
|
+
...endFact,
|
|
1027
|
+
decision: "SKIP",
|
|
1028
|
+
reason: observed.deploymentCheckPaused ? "DEPLOYMENT_CHECK_PAUSED" : "TERMINAL_ACCEPTED",
|
|
1029
|
+
model: null,
|
|
1030
|
+
});
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (observed.lastModelStopReason === "aborted" ||
|
|
1034
|
+
(observed.lastModelStopReason === "error" && observed.nativeWillRetry !== true)) {
|
|
1035
|
+
const loginRequired = isProviderLoginRequired(observed.lastModelError);
|
|
1036
|
+
const code = loginRequired
|
|
1037
|
+
? "PROVIDER_LOGIN_REQUIRED"
|
|
1038
|
+
: observed.lastModelError?.split("\n")[0]?.slice(0, 200) ||
|
|
1039
|
+
(observed.lastModelStopReason === "aborted" ? "PI_MODEL_ABORTED" : "MODEL_REQUEST_FAILED");
|
|
1040
|
+
const message = loginRequired
|
|
1041
|
+
? "模型服务登录已失效,请到本地开发环境重新登录对应订阅后重试。"
|
|
1042
|
+
: (observed.lastModelError ?? code);
|
|
1043
|
+
await emit("EXECUTION_DECIDED", {
|
|
1044
|
+
...endFact,
|
|
1045
|
+
decision: "SKIP",
|
|
1046
|
+
reason: "MODEL_ERROR",
|
|
1047
|
+
model: null,
|
|
1048
|
+
});
|
|
1049
|
+
if (observed.stopping)
|
|
783
1050
|
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
1051
|
await client.reportCodingTerminal(runId, {
|
|
792
1052
|
hostId: host.hostId,
|
|
793
1053
|
generation: host.generation,
|
|
794
1054
|
turnId,
|
|
795
1055
|
kind: "FAILED",
|
|
796
|
-
summary:
|
|
1056
|
+
summary: message,
|
|
797
1057
|
knownIssues: "",
|
|
798
|
-
failureCode:
|
|
1058
|
+
failureCode: code,
|
|
799
1059
|
projectFacts: [],
|
|
800
1060
|
testFacts: [],
|
|
801
1061
|
});
|
|
802
1062
|
observed.terminalReached = true;
|
|
803
|
-
await emit("RUN_FAILED", { code
|
|
1063
|
+
await emit("RUN_FAILED", { phase, code, message });
|
|
1064
|
+
return;
|
|
804
1065
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
:
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
: "读取并校验本阶段最终交付文件,上传成功后完成当前阶段。",
|
|
821
|
-
parameters: input.executionPurpose === "RELEASE"
|
|
822
|
-
? Type.Object({
|
|
823
|
-
checklistSummary: Type.String({
|
|
824
|
-
description: "核实依据、无需修改或修改摘要;缺口未解决时不要提交",
|
|
825
|
-
}),
|
|
826
|
-
projects: Type.Array(Type.Object({
|
|
827
|
-
project: Type.String({
|
|
828
|
-
description: `项目名称,只能选择:${input.projects.map((p) => p.name).join("、")}`,
|
|
829
|
-
}),
|
|
830
|
-
title: Type.String({ description: "包含工单编号及本项目变更摘要的 MR 标题" }),
|
|
831
|
-
description: Type.String({
|
|
832
|
-
description: "任务目的、本项目改动、测试与验收结论、发布注意、依赖、风险回滚及清单和报告链接",
|
|
833
|
-
}),
|
|
834
|
-
})),
|
|
835
|
-
})
|
|
836
|
-
: Type.Object({}),
|
|
837
|
-
async execute(_toolCallId, rawParams) {
|
|
838
|
-
try {
|
|
839
|
-
if (input.executionPurpose === "RELEASE") {
|
|
840
|
-
const params = rawParams;
|
|
841
|
-
if (!params?.checklistSummary?.trim())
|
|
842
|
-
throw new Error("AIP_RELEASE_CHECKLIST_SUMMARY_REQUIRED");
|
|
843
|
-
const files = await collectDeliveryFiles(runRoot, input, ["RELEASE_CHECKLIST"]);
|
|
844
|
-
const checklist = {
|
|
845
|
-
status: "VERIFIED",
|
|
846
|
-
summary: params.checklistSummary,
|
|
847
|
-
markdown: files[0].content,
|
|
848
|
-
};
|
|
849
|
-
await client.submitPullRequests(runId, {
|
|
850
|
-
hostId: host.hostId,
|
|
851
|
-
generation: host.generation,
|
|
852
|
-
turnId,
|
|
853
|
-
workItemId: input.workItem.id,
|
|
854
|
-
projects: [],
|
|
855
|
-
checklist,
|
|
856
|
-
});
|
|
857
|
-
const context = {
|
|
858
|
-
runId,
|
|
859
|
-
turnId,
|
|
860
|
-
workItem: input.workItem,
|
|
861
|
-
projects: input.projects,
|
|
862
|
-
primaryProjectId: input.primaryProjectId,
|
|
863
|
-
};
|
|
864
|
-
for (const project of input.projects) {
|
|
865
|
-
const explanation = params.projects?.find((item) => item.project === project.name);
|
|
866
|
-
if (!explanation)
|
|
867
|
-
throw new Error(`AIP_MR_DESCRIPTION_REQUIRED:${project.name}`);
|
|
868
|
-
const pendingFile = join(runRoot, `mr-pending-${safeName(project.id)}.json`);
|
|
869
|
-
const result = await prepareMergeRequest({
|
|
870
|
-
project,
|
|
871
|
-
cwd: join(runRoot, safeName(project.name)),
|
|
872
|
-
title: explanation.title,
|
|
873
|
-
description: explanation.description,
|
|
874
|
-
pendingFile,
|
|
875
|
-
});
|
|
876
|
-
await registerMergeRequest(client, context, host, result, pendingFile);
|
|
877
|
-
}
|
|
878
|
-
await client.submitPullRequests(runId, {
|
|
879
|
-
hostId: host.hostId,
|
|
880
|
-
generation: host.generation,
|
|
881
|
-
turnId,
|
|
882
|
-
workItemId: input.workItem.id,
|
|
883
|
-
projects: [],
|
|
884
|
-
completeRelease: true,
|
|
885
|
-
});
|
|
886
|
-
observed.terminalReached = true;
|
|
887
|
-
return toolResult("上线清单已核实,MR 已提交并登记,等待合并;平台负责查询最新状态。");
|
|
888
|
-
}
|
|
889
|
-
const existing = pendingBatch();
|
|
890
|
-
if (existing && existing.turnId === turnId) {
|
|
891
|
-
await flushEvents();
|
|
892
|
-
await uploadBatch(client, runId, existing);
|
|
893
|
-
await persistBatch(undefined);
|
|
894
|
-
observed.terminalReached = true;
|
|
895
|
-
return toolResult("交付已提交,等待经办人确认。");
|
|
896
|
-
}
|
|
897
|
-
const files = await collectDeliveryFiles(runRoot, input, input.phase === "DEVELOPMENT"
|
|
898
|
-
? Object.keys(DELIVERY_FILES).filter((slot) => STAGE_DELIVERY_PROTOCOLS.DEVELOPMENT.requiredFiles.includes(DELIVERY_FILES[slot]))
|
|
899
|
-
: undefined);
|
|
900
|
-
const projectFacts = await collectProjectFacts(runRoot, input.projects, input.workItem.key, input.primaryProjectId);
|
|
901
|
-
const batch = {
|
|
902
|
-
hostId: host.hostId,
|
|
903
|
-
generation: host.generation,
|
|
904
|
-
turnId,
|
|
905
|
-
kind: input.phase === "DEVELOPMENT" ? "DEVELOPMENT_COMPLETED" : "DELIVERY",
|
|
906
|
-
submissionId: crypto.randomUUID(),
|
|
907
|
-
files,
|
|
908
|
-
summary: files.find((file) => file.slot === "SUMMARY").content,
|
|
909
|
-
knownIssues: "",
|
|
910
|
-
projectFacts,
|
|
911
|
-
testFacts: [],
|
|
912
|
-
};
|
|
913
|
-
await persistBatch(batch);
|
|
914
|
-
await flushEvents();
|
|
915
|
-
await uploadBatch(client, runId, batch);
|
|
916
|
-
await persistBatch(undefined);
|
|
917
|
-
observed.terminalReached = true;
|
|
918
|
-
return toolResult("交付已提交,等待经办人确认。");
|
|
919
|
-
}
|
|
920
|
-
catch (error) {
|
|
921
|
-
observed.lastDeliveryError = error instanceof Error ? error.message : "AIP_DELIVERY_FAILED";
|
|
922
|
-
if (error instanceof PlatformClientError && error.status < 500 && error.status !== 429)
|
|
923
|
-
await persistBatch(undefined);
|
|
924
|
-
throw error;
|
|
925
|
-
}
|
|
926
|
-
},
|
|
927
|
-
});
|
|
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
|
-
if (input.executionPurpose === "RELEASE")
|
|
941
|
-
add({
|
|
942
|
-
name: "request_code_changes",
|
|
943
|
-
label: "返回开发修改",
|
|
944
|
-
description: "用户明确要求修改应用代码时,将工单返回开发,再测试和人工确认;仅讨论或文档修改不要调用。",
|
|
945
|
-
parameters: Type.Object({
|
|
946
|
-
reason: Type.String({ description: "用户要求修改的内容及原因" }),
|
|
947
|
-
}),
|
|
948
|
-
async execute(_id, params) {
|
|
949
|
-
await client.request(`/api/v1/coding-runs/${runId}/return-to-development`, {
|
|
950
|
-
method: "POST",
|
|
951
|
-
body: JSON.stringify({
|
|
952
|
-
hostId: host.hostId,
|
|
953
|
-
generation: host.generation,
|
|
954
|
-
turnId,
|
|
955
|
-
reason: params.reason,
|
|
956
|
-
}),
|
|
957
|
-
});
|
|
958
|
-
observed.terminalReached = true;
|
|
959
|
-
return toolResult("已返回开发,等待下一执行接手续改,完成后进入测试与人工确认。");
|
|
960
|
-
},
|
|
1066
|
+
// Judge reads durable messages. It queues automatic work through the same command
|
|
1067
|
+
// claim boundary that gives user messages and stop priority.
|
|
1068
|
+
await emit.flush();
|
|
1069
|
+
if (observed.stopping)
|
|
1070
|
+
return;
|
|
1071
|
+
let decision;
|
|
1072
|
+
try {
|
|
1073
|
+
decision = await client.evaluateContinuation(runId, {
|
|
1074
|
+
hostId: host.hostId,
|
|
1075
|
+
generation: host.generation,
|
|
1076
|
+
turnId,
|
|
1077
|
+
stopReason,
|
|
1078
|
+
...(observed.lastDeliveryError
|
|
1079
|
+
? { lastDeliveryError: observed.lastDeliveryError.slice(0, 2000) }
|
|
1080
|
+
: {}),
|
|
961
1081
|
});
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
if (observed.stopping)
|
|
1085
|
+
return;
|
|
1086
|
+
await emit("EXECUTION_DECIDED", {
|
|
1087
|
+
...endFact,
|
|
1088
|
+
decision: "SKIP",
|
|
1089
|
+
reason: "JUDGE_REQUEST_FAILED",
|
|
1090
|
+
model: null,
|
|
1091
|
+
explanation: error instanceof Error ? error.message : String(error),
|
|
1092
|
+
});
|
|
1093
|
+
if (observed.handingOffInput)
|
|
1094
|
+
return;
|
|
1095
|
+
await completeConversation();
|
|
1096
|
+
observed.terminalReached = true;
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (observed.stopping)
|
|
1100
|
+
return;
|
|
1101
|
+
await emit("EXECUTION_DECIDED", { ...endFact, ...decision });
|
|
1102
|
+
// The platform has queued the continuation; never execute the returned prompt
|
|
1103
|
+
// directly, because a user can cancel it before the host claims the command.
|
|
1104
|
+
if (decision.decision === "CONTINUE")
|
|
1105
|
+
return;
|
|
1106
|
+
if (observed.stopping || observed.handingOffInput)
|
|
1107
|
+
return;
|
|
1108
|
+
await completeConversation();
|
|
1109
|
+
observed.terminalReached = true;
|
|
1110
|
+
}
|
|
1111
|
+
export async function prepareDeploymentProjects(runRoot, projects, configuration) {
|
|
1112
|
+
await mkdir(runRoot, { recursive: true, mode: 0o700 });
|
|
1113
|
+
for (const project of projects) {
|
|
1114
|
+
const target = join(runRoot, safeName(project.name));
|
|
1115
|
+
const branch = configuration?.projects.find((p) => p.projectId === project.id)?.testBranch ??
|
|
1116
|
+
project.testBranch;
|
|
1117
|
+
if (!branch)
|
|
1118
|
+
throw new Error("DEPLOYMENT_TEST_BRANCH_REQUIRED");
|
|
1119
|
+
if (!(await exists(target))) {
|
|
1120
|
+
await execFileAsync("git", [
|
|
1121
|
+
"clone",
|
|
1122
|
+
"--branch",
|
|
1123
|
+
branch,
|
|
1124
|
+
"--",
|
|
1125
|
+
project.repositoryUrl,
|
|
1126
|
+
target,
|
|
1127
|
+
]).catch(() => {
|
|
1128
|
+
throw new Error("DEPLOYMENT_REPOSITORY_PREPARE_FAILED");
|
|
1003
1129
|
});
|
|
1004
|
-
|
|
1130
|
+
}
|
|
1131
|
+
// Reuse the interrupted deployment as-is; the employee resolves its Git state.
|
|
1132
|
+
const [{ stdout: origin }, { stdout: common }] = await Promise.all([
|
|
1133
|
+
execFileAsync("git", ["config", "--get", "remote.origin.url"], { cwd: target }),
|
|
1134
|
+
execFileAsync("git", ["rev-parse", "--git-common-dir"], { cwd: target }),
|
|
1135
|
+
]);
|
|
1136
|
+
if ((await canonicalRepository(origin.trim())) !==
|
|
1137
|
+
(await canonicalRepository(project.repositoryUrl)))
|
|
1138
|
+
throw new Error("CODING_REPOSITORY_ORIGIN_MISMATCH");
|
|
1139
|
+
if ((await realpath(resolve(target, common.trim()))) !== (await realpath(join(target, ".git"))))
|
|
1140
|
+
throw new Error("DEPLOYMENT_REPOSITORY_NOT_ISOLATED");
|
|
1141
|
+
}
|
|
1142
|
+
return projects.length === 1 ? join(runRoot, safeName(projects[0].name)) : runRoot;
|
|
1005
1143
|
}
|
|
1006
|
-
export async function prepareProjects(cacheRoot, runRoot,
|
|
1144
|
+
export async function prepareProjects(cacheRoot, runRoot, workItemKey, projects, resume, verifyRemote = false, uninitializedProjectIds = []) {
|
|
1007
1145
|
await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
|
|
1008
1146
|
for (const project of projects) {
|
|
1009
1147
|
const mirror = join(cacheRoot, `${project.id}.git`);
|
|
@@ -1023,8 +1161,8 @@ export async function prepareProjects(cacheRoot, runRoot, runId, projects, resum
|
|
|
1023
1161
|
(await canonicalRepository(project.repositoryUrl)))
|
|
1024
1162
|
throw new Error("CODING_REPOSITORY_ORIGIN_MISMATCH");
|
|
1025
1163
|
}
|
|
1026
|
-
if (!resume) {
|
|
1027
|
-
//
|
|
1164
|
+
if (!resume || verifyRemote) {
|
|
1165
|
+
// Never fetch into local branch refs: stopped worktrees may contain unpublished commits.
|
|
1028
1166
|
await execFileAsync("git", [
|
|
1029
1167
|
"--git-dir",
|
|
1030
1168
|
mirror,
|
|
@@ -1033,44 +1171,81 @@ export async function prepareProjects(cacheRoot, runRoot, runId, projects, resum
|
|
|
1033
1171
|
"remote.origin.fetch",
|
|
1034
1172
|
"+refs/heads/*:refs/remotes/origin/*",
|
|
1035
1173
|
]);
|
|
1036
|
-
await execFileAsync("git", ["--git-dir", mirror, "fetch", "origin"]).catch(() => {
|
|
1174
|
+
await execFileAsync("git", ["--git-dir", mirror, "fetch", "--prune", "origin"]).catch(() => {
|
|
1037
1175
|
throw new Error("CODING_REPOSITORY_ACCESS_FAILED");
|
|
1038
1176
|
});
|
|
1039
|
-
await execFileAsync("git", [
|
|
1040
|
-
"--git-dir",
|
|
1041
|
-
mirror,
|
|
1042
|
-
"cat-file",
|
|
1043
|
-
"-e",
|
|
1044
|
-
`refs/remotes/origin/${project.baselineBranch}`,
|
|
1045
|
-
]).catch(() => {
|
|
1046
|
-
throw new Error("CODING_BASELINE_BRANCH_UNAVAILABLE");
|
|
1047
|
-
});
|
|
1048
1177
|
}
|
|
1049
1178
|
const target = join(runRoot, safeName(project.name));
|
|
1050
|
-
const expectedBranch = project.devBranch || `aip/${
|
|
1179
|
+
const expectedBranch = project.devBranch || `aip/${workItemKey}`;
|
|
1051
1180
|
const existingWorktree = await exists(target);
|
|
1052
1181
|
if (!existingWorktree) {
|
|
1053
1182
|
if (resume)
|
|
1054
1183
|
throw new Error("CODING_RUN_WORKTREE_MISSING");
|
|
1055
|
-
await execFileAsync("git", [
|
|
1184
|
+
const remoteExists = await execFileAsync("git", [
|
|
1056
1185
|
"--git-dir",
|
|
1057
1186
|
mirror,
|
|
1058
|
-
"
|
|
1059
|
-
"
|
|
1060
|
-
"
|
|
1061
|
-
expectedBranch
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1187
|
+
"show-ref",
|
|
1188
|
+
"--verify",
|
|
1189
|
+
"--quiet",
|
|
1190
|
+
`refs/remotes/origin/${expectedBranch}`,
|
|
1191
|
+
]).then(() => true, () => false);
|
|
1192
|
+
const localExists = await execFileAsync("git", [
|
|
1193
|
+
"--git-dir",
|
|
1194
|
+
mirror,
|
|
1195
|
+
"show-ref",
|
|
1196
|
+
"--verify",
|
|
1197
|
+
"--quiet",
|
|
1198
|
+
`refs/heads/${expectedBranch}`,
|
|
1199
|
+
]).then(() => true, () => false);
|
|
1200
|
+
if (remoteExists) {
|
|
1201
|
+
// A new execution owns its branch ref; an old execution keeps its worktree untouched.
|
|
1202
|
+
await execFileAsync("git", [
|
|
1203
|
+
"clone",
|
|
1204
|
+
"--single-branch",
|
|
1205
|
+
"--branch",
|
|
1206
|
+
expectedBranch,
|
|
1207
|
+
project.repositoryUrl,
|
|
1208
|
+
target,
|
|
1209
|
+
]).catch(() => {
|
|
1210
|
+
throw new Error("CODING_REPOSITORY_ACCESS_FAILED");
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
else {
|
|
1214
|
+
if (localExists || (verifyRemote && !uninitializedProjectIds.includes(project.id)))
|
|
1215
|
+
throw new Error(localExists
|
|
1216
|
+
? "CODING_BRANCH_NOT_PUBLISHED:本机工单分支尚未推送,请检查并同步后再继续。"
|
|
1217
|
+
: "CODING_BRANCH_INITIALIZATION_UNCONFIRMED:远端尚无工单分支,原工作区初始化状态未确认,请检查后再继续。");
|
|
1218
|
+
await execFileAsync("git", [
|
|
1219
|
+
"--git-dir",
|
|
1220
|
+
mirror,
|
|
1221
|
+
"cat-file",
|
|
1222
|
+
"-e",
|
|
1223
|
+
`refs/remotes/origin/${project.baselineBranch}`,
|
|
1224
|
+
]).catch(() => {
|
|
1225
|
+
throw new Error("CODING_BASELINE_BRANCH_UNAVAILABLE");
|
|
1226
|
+
});
|
|
1227
|
+
await execFileAsync("git", [
|
|
1228
|
+
"--git-dir",
|
|
1229
|
+
mirror,
|
|
1230
|
+
"worktree",
|
|
1231
|
+
"add",
|
|
1232
|
+
"-b",
|
|
1233
|
+
expectedBranch,
|
|
1234
|
+
target,
|
|
1235
|
+
`refs/remotes/origin/${project.baselineBranch}`,
|
|
1236
|
+
]).catch(() => {
|
|
1237
|
+
throw new Error("CODING_WORKTREE_CREATE_FAILED");
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1067
1240
|
}
|
|
1068
|
-
await validateWorktree(mirror, target, expectedBranch, resume || existingWorktree);
|
|
1241
|
+
await validateWorktree(mirror, target, expectedBranch, resume || existingWorktree, project.repositoryUrl);
|
|
1242
|
+
if (existingWorktree && verifyRemote)
|
|
1243
|
+
await synchronizeRemoteWorkspace(target, expectedBranch);
|
|
1069
1244
|
}
|
|
1070
1245
|
const onlyProject = projects.length === 1 ? projects[0] : null;
|
|
1071
1246
|
return onlyProject ? join(runRoot, safeName(onlyProject.name)) : runRoot;
|
|
1072
1247
|
}
|
|
1073
|
-
async function validateWorktree(mirror, target, expectedBranch, resume) {
|
|
1248
|
+
async function validateWorktree(mirror, target, expectedBranch, resume, repositoryUrl) {
|
|
1074
1249
|
const facts = await Promise.all([
|
|
1075
1250
|
execFileAsync("git", ["rev-parse", "--is-inside-work-tree"], { cwd: target }),
|
|
1076
1251
|
execFileAsync("git", ["rev-parse", "--git-common-dir"], { cwd: target }),
|
|
@@ -1086,8 +1261,15 @@ async function validateWorktree(mirror, target, expectedBranch, resume) {
|
|
|
1086
1261
|
realpath(commonPath.startsWith("/") ? commonPath : resolve(target, commonPath)),
|
|
1087
1262
|
realpath(mirror),
|
|
1088
1263
|
]);
|
|
1089
|
-
if (actualCommon !== expectedCommon)
|
|
1090
|
-
|
|
1264
|
+
if (actualCommon !== expectedCommon) {
|
|
1265
|
+
if (actualCommon !== (await realpath(join(target, ".git"))))
|
|
1266
|
+
throw new Error("CODING_WORKTREE_OWNERSHIP_INVALID");
|
|
1267
|
+
const { stdout: origin } = await execFileAsync("git", ["remote", "get-url", "origin"], {
|
|
1268
|
+
cwd: target,
|
|
1269
|
+
});
|
|
1270
|
+
if ((await canonicalRepository(origin.trim())) !== (await canonicalRepository(repositoryUrl)))
|
|
1271
|
+
throw new Error("CODING_REPOSITORY_ORIGIN_MISMATCH");
|
|
1272
|
+
}
|
|
1091
1273
|
if (branch.trim() !== expectedBranch)
|
|
1092
1274
|
throw new Error("CODING_WORKTREE_BRANCH_MISMATCH");
|
|
1093
1275
|
if (resume)
|
|
@@ -1107,40 +1289,50 @@ async function canonicalRepository(value) {
|
|
|
1107
1289
|
}
|
|
1108
1290
|
return value.replace(/\/$/u, "");
|
|
1109
1291
|
}
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1292
|
+
function executionInstructions(input, runRoot) {
|
|
1293
|
+
// 环境事实与授权单独呈现,业务方法由平台 nodeRules 提供。
|
|
1294
|
+
return `本轮执行上下文:\n${JSON.stringify({
|
|
1295
|
+
phase: input.phase,
|
|
1296
|
+
executionPurpose: input.executionPurpose ?? null,
|
|
1297
|
+
discussionOnly: input.conversationContext?.discussionOnly ?? false,
|
|
1298
|
+
deploymentConfiguration: input.deploymentConfiguration ?? null,
|
|
1299
|
+
testHandoff: input.testHandoff
|
|
1300
|
+
? {
|
|
1301
|
+
summary: input.testHandoff.summary,
|
|
1302
|
+
...(input.testHandoff.files
|
|
1303
|
+
? {
|
|
1304
|
+
files: input.testHandoff.files.map(({ source, baseSource, materialPath, baseMaterialPath, ...metadata }) => ({
|
|
1305
|
+
...metadata,
|
|
1306
|
+
materialPath: join(deliveryDirectory(runRoot, input), materialPath),
|
|
1307
|
+
...(baseMaterialPath
|
|
1308
|
+
? {
|
|
1309
|
+
baseMaterialPath: join(deliveryDirectory(runRoot, input), baseMaterialPath),
|
|
1310
|
+
}
|
|
1311
|
+
: {}),
|
|
1312
|
+
})),
|
|
1313
|
+
}
|
|
1314
|
+
: {}),
|
|
1315
|
+
}
|
|
1316
|
+
: null,
|
|
1317
|
+
}, null, 2)}`;
|
|
1318
|
+
}
|
|
1319
|
+
async function workspaceFingerprint(runRoot, input) {
|
|
1320
|
+
return JSON.stringify(await Promise.all(input.projects.map(async (project) => {
|
|
1321
|
+
const cwd = join(runRoot, safeName(project.name));
|
|
1322
|
+
const [head, branch, tree] = await Promise.all([
|
|
1323
|
+
execFileAsync("git", ["rev-parse", "HEAD"], { cwd }),
|
|
1324
|
+
execFileAsync("git", ["symbolic-ref", "--short", "-q", "HEAD"], { cwd }),
|
|
1325
|
+
worktreeTreeHash(cwd),
|
|
1326
|
+
]);
|
|
1327
|
+
return [project.id, head.stdout.trim(), branch.stdout.trim(), tree];
|
|
1328
|
+
})));
|
|
1329
|
+
}
|
|
1330
|
+
export function initialPrompt(input, runRoot, _responsible = true, materialRoot = runRoot) {
|
|
1331
|
+
const directory = deliveryDirectory(materialRoot, input);
|
|
1114
1332
|
const projects = input.projects
|
|
1115
1333
|
.map((p) => `- ${p.name}: ${join(runRoot, safeName(p.name))}(开发分支 ${p.devBranch} → 基准分支 ${p.baselineBranch};测试分支 ${p.testBranch})${p.id === input.primaryProjectId ? "(主项目)" : ""}`)
|
|
1116
1334
|
.join("\n");
|
|
1117
|
-
|
|
1118
|
-
const protocol = STAGE_DELIVERY_PROTOCOLS[phase];
|
|
1119
|
-
const delivery = input.stageDeliverySnapshots?.[phase];
|
|
1120
|
-
if (responsible && !delivery)
|
|
1121
|
-
throw new Error("STAGE_DELIVERY_CONFIGURATION_MISSING");
|
|
1122
|
-
const context = `当前阶段:${phase}。工单 ${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
|
-
if (!responsible)
|
|
1124
|
-
return context;
|
|
1125
|
-
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}。`;
|
|
1126
|
-
}
|
|
1127
|
-
async function uploadBatch(client, runId, batch) {
|
|
1128
|
-
for (let attempt = 0;; attempt++) {
|
|
1129
|
-
try {
|
|
1130
|
-
await client.reportCodingTerminal(runId, batch);
|
|
1131
|
-
return;
|
|
1132
|
-
}
|
|
1133
|
-
catch (error) {
|
|
1134
|
-
if (error instanceof PlatformClientError && error.status < 500 && error.status !== 429)
|
|
1135
|
-
throw error;
|
|
1136
|
-
if (attempt >= 2)
|
|
1137
|
-
throw new Error(`AIP_DELIVERY_UPLOAD_FAILED:同批材料尚未提交成功,可恢复重试;${error instanceof Error ? error.message : "网络错误"}`);
|
|
1138
|
-
await new Promise((resolve) => setTimeout(resolve, 300 * (attempt + 1)));
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
function toolResult(text) {
|
|
1143
|
-
return { content: [{ type: "text", text }], details: {} };
|
|
1335
|
+
return `工单:${input.workItem.key} ${input.workItem.title}。\n当前阶段:${input.executionPurpose === "RELEASE" ? "RELEASE" : input.phase}。\n项目:\n${projects}\n输入材料:${input.materialPreparation.inputPaths.map((path) => join(directory, path)).join("、")}。\n交付目录:${directory}。${input.inputConflict ? "正式输入材料已更新。" : ""}\n${executionInstructions(input, materialRoot)}`;
|
|
1144
1336
|
}
|
|
1145
1337
|
function safeName(value) {
|
|
1146
1338
|
return basename(value).replace(/[^a-zA-Z0-9._-]/gu, "-") || "project";
|
|
@@ -1154,64 +1346,9 @@ async function exists(path) {
|
|
|
1154
1346
|
return false;
|
|
1155
1347
|
}
|
|
1156
1348
|
}
|
|
1157
|
-
async function collectProjectFacts(runRoot, projects, key, primaryProjectId) {
|
|
1158
|
-
return Promise.all(projects.map(async (project) => {
|
|
1159
|
-
const cwd = join(runRoot, safeName(project.name));
|
|
1160
|
-
const excludeKey = project.id === primaryProjectId ? key : undefined;
|
|
1161
|
-
const [{ stdout: head }, tree, { stdout: branch }] = await Promise.all([
|
|
1162
|
-
execFileAsync("git", ["rev-parse", "HEAD"], { cwd }),
|
|
1163
|
-
worktreeTreeHash(cwd, excludeKey),
|
|
1164
|
-
execFileAsync("git", ["branch", "--show-current"], { cwd }),
|
|
1165
|
-
]);
|
|
1166
|
-
const commitSha = head.trim();
|
|
1167
|
-
return {
|
|
1168
|
-
projectId: project.id,
|
|
1169
|
-
projectName: project.name,
|
|
1170
|
-
commitSha,
|
|
1171
|
-
treeHash: tree,
|
|
1172
|
-
branch: branch.trim(),
|
|
1173
|
-
};
|
|
1174
|
-
}));
|
|
1175
|
-
}
|
|
1176
1349
|
async function captureProjectRevisions(runRoot, projects, key, primaryProjectId) {
|
|
1177
1350
|
return collectProjectFacts(runRoot, projects, key, primaryProjectId);
|
|
1178
1351
|
}
|
|
1179
|
-
export async function worktreeTreeHash(cwd, key, committed = false, ref = "HEAD") {
|
|
1180
|
-
const temporary = await mkdtemp(join(tmpdir(), "aip-tested-tree-"));
|
|
1181
|
-
const index = join(temporary, "index");
|
|
1182
|
-
const options = { cwd, env: { ...process.env, GIT_INDEX_FILE: index } };
|
|
1183
|
-
try {
|
|
1184
|
-
await execFileAsync("git", ["read-tree", ref], options);
|
|
1185
|
-
if (!committed)
|
|
1186
|
-
await execFileAsync("git", ["add", "-A"], options);
|
|
1187
|
-
if (key) {
|
|
1188
|
-
const names = [
|
|
1189
|
-
"summary.md",
|
|
1190
|
-
"prd.md",
|
|
1191
|
-
"design.md",
|
|
1192
|
-
"plan.md",
|
|
1193
|
-
"test_plan.md",
|
|
1194
|
-
"report.md",
|
|
1195
|
-
"code_review.md",
|
|
1196
|
-
"release_checklist.md",
|
|
1197
|
-
"prd.platform.md",
|
|
1198
|
-
"design.platform.md",
|
|
1199
|
-
];
|
|
1200
|
-
await execFileAsync("git", [
|
|
1201
|
-
"rm",
|
|
1202
|
-
"--cached",
|
|
1203
|
-
"--force", // This removes entries only from the dedicated temporary index.
|
|
1204
|
-
"--ignore-unmatch",
|
|
1205
|
-
"--",
|
|
1206
|
-
...names.map((name) => `docs/aip/${key}/${name}`),
|
|
1207
|
-
], options);
|
|
1208
|
-
}
|
|
1209
|
-
return (await execFileAsync("git", ["write-tree"], options)).stdout.trim();
|
|
1210
|
-
}
|
|
1211
|
-
finally {
|
|
1212
|
-
await rm(temporary, { recursive: true, force: true });
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
1352
|
export function redactHostText(value, roots = []) {
|
|
1216
1353
|
let redacted = value;
|
|
1217
1354
|
for (const root of [...roots].filter(Boolean).sort((a, b) => b.length - a.length))
|
|
@@ -1227,4 +1364,84 @@ export function redactHostText(value, roots = []) {
|
|
|
1227
1364
|
.replace(/\b[A-Za-z]:\\(?:[^\s'"`<>]+\\?)+/gu, "[本地路径]");
|
|
1228
1365
|
return redacted.slice(0, 100_000);
|
|
1229
1366
|
}
|
|
1367
|
+
/** 检查失败命令自己的已知工作区,不从空准备结果推测初始化状态。 */
|
|
1368
|
+
export async function inspectWorkspaceInitialization(runId, input) {
|
|
1369
|
+
const local = paths();
|
|
1370
|
+
return Promise.all(input.projects.map(async (project) => {
|
|
1371
|
+
let status = "UNKNOWN";
|
|
1372
|
+
try {
|
|
1373
|
+
const target = join(local.runWorktrees, runId, safeName(project.name));
|
|
1374
|
+
const mirror = join(local.repositoryCache, `${project.id}.git`);
|
|
1375
|
+
const present = async (path) => {
|
|
1376
|
+
try {
|
|
1377
|
+
await stat(path);
|
|
1378
|
+
return true;
|
|
1379
|
+
}
|
|
1380
|
+
catch (error) {
|
|
1381
|
+
if (error.code === "ENOENT")
|
|
1382
|
+
return false;
|
|
1383
|
+
throw error;
|
|
1384
|
+
}
|
|
1385
|
+
};
|
|
1386
|
+
if (await present(target)) {
|
|
1387
|
+
const branch = (await execFileAsync("git", ["branch", "--show-current"], { cwd: target })).stdout.trim();
|
|
1388
|
+
await execFileAsync("git", ["rev-parse", "--verify", "HEAD^{commit}"], { cwd: target });
|
|
1389
|
+
if (branch === (project.devBranch || `aip/${input.workItem.key}`))
|
|
1390
|
+
status = "INITIALIZED";
|
|
1391
|
+
}
|
|
1392
|
+
else if (!(await present(mirror)))
|
|
1393
|
+
status = "NOT_INITIALIZED";
|
|
1394
|
+
else {
|
|
1395
|
+
const refs = await execFileAsync("git", [
|
|
1396
|
+
"--git-dir",
|
|
1397
|
+
mirror,
|
|
1398
|
+
"for-each-ref",
|
|
1399
|
+
"--format=%(refname)",
|
|
1400
|
+
`refs/heads/${project.devBranch || `aip/${input.workItem.key}`}`,
|
|
1401
|
+
]);
|
|
1402
|
+
status = refs.stdout.trim() ? "INITIALIZED" : "NOT_INITIALIZED";
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
catch {
|
|
1406
|
+
/* 不可访问或损坏的缓存不能证明没有旧成果。 */
|
|
1407
|
+
}
|
|
1408
|
+
return { projectId: project.id, status };
|
|
1409
|
+
}));
|
|
1410
|
+
}
|
|
1411
|
+
export async function inspectPriorTransferWorkspaces(root, runIds, projects) {
|
|
1412
|
+
const notes = [];
|
|
1413
|
+
for (const id of runIds) {
|
|
1414
|
+
if (!/^[0-9a-f-]{36}$/iu.test(id))
|
|
1415
|
+
throw new Error("CODING_PRIOR_RUN_ID_INVALID");
|
|
1416
|
+
for (const project of projects) {
|
|
1417
|
+
const cwd = join(root, id, safeName(project.name));
|
|
1418
|
+
try {
|
|
1419
|
+
try {
|
|
1420
|
+
await stat(cwd);
|
|
1421
|
+
}
|
|
1422
|
+
catch (error) {
|
|
1423
|
+
if (error.code === "ENOENT")
|
|
1424
|
+
continue;
|
|
1425
|
+
throw error;
|
|
1426
|
+
}
|
|
1427
|
+
const origin = (await execFileAsync("git", ["config", "--get", "remote.origin.url"], { cwd })).stdout.trim();
|
|
1428
|
+
const branch = (await execFileAsync("git", ["branch", "--show-current"], { cwd })).stdout.trim();
|
|
1429
|
+
if ((await canonicalRepository(origin)) !==
|
|
1430
|
+
(await canonicalRepository(project.repositoryUrl)) ||
|
|
1431
|
+
branch !== project.devBranch)
|
|
1432
|
+
throw new Error("WORKSPACE_IDENTITY_MISMATCH");
|
|
1433
|
+
await execFileAsync("git", ["fetch", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`], { cwd });
|
|
1434
|
+
const counts = (await execFileAsync("git", ["rev-list", "--left-right", "--count", `HEAD...refs/remotes/origin/${branch}`], { cwd })).stdout
|
|
1435
|
+
.trim()
|
|
1436
|
+
.split(/\s+/u);
|
|
1437
|
+
const dirty = Boolean((await execFileAsync("git", ["status", "--porcelain"], { cwd })).stdout.trim());
|
|
1438
|
+
notes.push(`${project.name},旧执行 ${id}:${dirty ? "有未提交修改" : "无未提交修改"},本地领先 ${counts[0]}、远端领先 ${counts[1]}${Number(counts[0]) && Number(counts[1]) ? "(已分叉)" : ""}。旧现场保留在 ${cwd};本次使用独立工作区,请核对需要保留的修改。`);
|
|
1439
|
+
}
|
|
1440
|
+
catch {
|
|
1441
|
+
notes.push(`${project.name},旧执行 ${id}:无法确认旧现场与远端差异,旧现场未改动,请检查 ${cwd}。`);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
return notes;
|
|
1446
|
+
}
|
|
1230
1447
|
//# sourceMappingURL=coding-run-manager.js.map
|