@bf-meishijian/aip 0.0.4 → 0.0.5
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/agent-harness/index.d.ts +4 -0
- package/dist/agent-harness/index.d.ts.map +1 -0
- package/dist/agent-harness/index.js +4 -0
- package/dist/agent-harness/index.js.map +1 -0
- package/dist/agent-harness/pi-chat-model.d.ts +35 -0
- package/dist/agent-harness/pi-chat-model.d.ts.map +1 -0
- package/dist/agent-harness/pi-chat-model.js +285 -0
- package/dist/agent-harness/pi-chat-model.js.map +1 -0
- 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.map +1 -1
- package/dist/coding-run-manager.js +533 -407
- package/dist/coding-run-manager.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.map +1 -1
- package/dist/live-employee.js +2 -2
- 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-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.map +1 -1
- 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 +6 -4
|
@@ -2,9 +2,8 @@ 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
9
|
import { collectDeliveryFiles, deliveryDirectory, initializeDeliveryFiles, } from "./delivery-files.js";
|
|
@@ -13,7 +12,9 @@ import { prepareMergeRequest, registerMergeRequest, } from "./merge-requests.js"
|
|
|
13
12
|
import { paths } from "./paths.js";
|
|
14
13
|
import { PlatformClientError } from "./platform-client.js";
|
|
15
14
|
import { readJson, writeSecureJson } from "./secure-json.js";
|
|
15
|
+
import { DELIVERY_FILES, STAGE_DELIVERY_PROTOCOLS, stageEmployeeIds } from "./stage-delivery.js";
|
|
16
16
|
const execFileAsync = promisify(execFile);
|
|
17
|
+
const EVENT_APPEND_MAX_ATTEMPTS = 8;
|
|
17
18
|
export class CodingRunManager {
|
|
18
19
|
pi;
|
|
19
20
|
active = new Map();
|
|
@@ -66,14 +67,14 @@ export class CodingRunManager {
|
|
|
66
67
|
active = undefined;
|
|
67
68
|
}
|
|
68
69
|
if (active) {
|
|
69
|
-
if (images.length > 0 && !active.
|
|
70
|
+
if (images.length > 0 && !active.supportsImages)
|
|
70
71
|
throw new Error("MODEL_IMAGES_UNSUPPORTED");
|
|
72
|
+
active.observed.pendingSteerText = command.run.contentMarkdown ?? "";
|
|
71
73
|
await active.emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
|
|
72
74
|
if (images.length > 0)
|
|
73
75
|
await active.session.steer(command.run.contentMarkdown ?? "", images);
|
|
74
76
|
else
|
|
75
77
|
await active.session.steer(command.run.contentMarkdown ?? "");
|
|
76
|
-
await active.emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
|
|
77
78
|
}
|
|
78
79
|
else {
|
|
79
80
|
if (!command.run.runInput)
|
|
@@ -89,6 +90,7 @@ export class CodingRunManager {
|
|
|
89
90
|
active.stopping = true;
|
|
90
91
|
active.observed.stopping = true;
|
|
91
92
|
await active.session.abort();
|
|
93
|
+
await active.emit.flush();
|
|
92
94
|
for (const [activityKey, agentId] of active.observed.activeTasks) {
|
|
93
95
|
await active.emit("SUBAGENT_STOPPED", {
|
|
94
96
|
activityKey,
|
|
@@ -127,10 +129,12 @@ export class CodingRunManager {
|
|
|
127
129
|
const local = paths();
|
|
128
130
|
const runRoot = join(local.runWorktrees, runId);
|
|
129
131
|
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
132
|
const stateFile = join(local.runState, `${runId}.json`);
|
|
133
133
|
const storedState = await readJson(stateFile);
|
|
134
|
+
if (storedState?.phaseSessions &&
|
|
135
|
+
Object.keys(storedState.phaseSessions).length &&
|
|
136
|
+
storedState.harness !== "pi")
|
|
137
|
+
throw new Error("PRIOR_HARNESS_SESSION_UNSUPPORTED:请新建执行,历史文件和工作目录已保留。");
|
|
134
138
|
const cwd = await prepareProjects(local.repositoryCache, runRoot, runId, input.projects, Boolean(storedState?.prepared));
|
|
135
139
|
for (const project of input.projects)
|
|
136
140
|
project.devBranch ||= `aip/${runId}/${project.id.slice(0, 8)}`;
|
|
@@ -159,7 +163,7 @@ export class CodingRunManager {
|
|
|
159
163
|
: undefined;
|
|
160
164
|
const phaseSessions = storedState?.phaseSessions ?? {};
|
|
161
165
|
const sessionPhase = input.executionPurpose === "RELEASE" ? "DEVELOPMENT" : input.phase;
|
|
162
|
-
let
|
|
166
|
+
let sessionRef = phaseSessions[sessionPhase];
|
|
163
167
|
let lastEventSequence = storedState?.lastEventSequence ?? 0;
|
|
164
168
|
const observed = {
|
|
165
169
|
pendingQuestion: storedState?.phase === input.phase ? (storedState.pendingQuestion ?? "") : "",
|
|
@@ -179,7 +183,8 @@ export class CodingRunManager {
|
|
|
179
183
|
},
|
|
180
184
|
prepared: true,
|
|
181
185
|
phaseSessions,
|
|
182
|
-
|
|
186
|
+
sessionRef,
|
|
187
|
+
harness: "pi",
|
|
183
188
|
lastEventSequence,
|
|
184
189
|
pendingQuestion: observed.pendingQuestion,
|
|
185
190
|
childSessions,
|
|
@@ -209,12 +214,12 @@ export class CodingRunManager {
|
|
|
209
214
|
});
|
|
210
215
|
return;
|
|
211
216
|
}
|
|
212
|
-
const terminal =
|
|
217
|
+
const terminal = terminalTools(client, host, runId, turnId, runRoot, input, observed, redact, async (batch) => {
|
|
213
218
|
pendingDelivery = batch;
|
|
214
219
|
await persistState();
|
|
215
220
|
}, () => pendingDelivery, (question) => emit("QUESTION_UPDATED", { question: redact(question) }), () => emit.flush());
|
|
216
221
|
if (!this.pi.resolveRuntimeModel)
|
|
217
|
-
throw new Error("
|
|
222
|
+
throw new Error("MODEL_RUNTIME_UNAVAILABLE");
|
|
218
223
|
const resolved = await this.pi.resolveRuntimeModel(input.providerId, input.modelId, {
|
|
219
224
|
runId,
|
|
220
225
|
turnId,
|
|
@@ -234,120 +239,106 @@ export class CodingRunManager {
|
|
|
234
239
|
const childInstances = new Map();
|
|
235
240
|
const childUpdates = new Map();
|
|
236
241
|
const childTasks = new Map();
|
|
237
|
-
const
|
|
242
|
+
const memberExtension = (api) => {
|
|
243
|
+
if (!availableAgents.length)
|
|
244
|
+
return;
|
|
238
245
|
api.registerTool({
|
|
239
246
|
name: "subagent",
|
|
240
247
|
label: "派发研发团队",
|
|
241
|
-
description: `向实际绑定员工分派子任务,返回真实子会话结果。可用员工:${availableAgents
|
|
248
|
+
description: `向实际绑定员工分派子任务,返回真实子会话结果。可用员工:${availableAgents
|
|
249
|
+
.map((id) => `${id}:${input.teamEmployees?.[id]?.name ?? id};${input.teamEmployees?.[id]?.description ?? ""}`)
|
|
250
|
+
.join("\n")}。`,
|
|
242
251
|
parameters: Type.Object({
|
|
243
|
-
agent:
|
|
252
|
+
agent: availableAgents.length === 1
|
|
253
|
+
? Type.Literal(availableAgents[0])
|
|
254
|
+
: Type.Union(availableAgents.map((name) => Type.Literal(name))),
|
|
244
255
|
task: Type.String({ minLength: 1 }),
|
|
245
256
|
}),
|
|
246
257
|
async execute(callId, params, signal, onUpdate) {
|
|
247
|
-
const member = params.agent;
|
|
258
|
+
const member = String(params.agent);
|
|
248
259
|
if (!availableAgents.includes(member))
|
|
249
260
|
throw new Error("DEVELOPMENT_TEAM_MEMBER_INVALID");
|
|
261
|
+
if (observed.stopping || signal?.aborted)
|
|
262
|
+
throw new Error("SUBAGENT_STOPPED");
|
|
263
|
+
observed.activeTasks.set(callId, member);
|
|
250
264
|
childTasks.set(member, callId);
|
|
251
|
-
|
|
265
|
+
observed.memberResults.delete(member);
|
|
266
|
+
childUpdates.set(member, (text) => onUpdate?.({ content: [{ type: "text", text }], details: { agent: member } }));
|
|
267
|
+
const key = `${input.phase}:${member}`;
|
|
252
268
|
let child = childInstances.get(member);
|
|
253
|
-
observed.memberResults.delete(params.agent);
|
|
254
|
-
childUpdates.set(member, (text) => onUpdate?.({ content: [{ type: "text", text }], details: { agent: params.agent } }));
|
|
255
269
|
if (!child) {
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
const
|
|
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({
|
|
270
|
+
const childBinding = { ...binding, member };
|
|
271
|
+
const childLive = liveEmployee(client, runId, childBinding, await client.employeeConfig(runId, { ...childBinding, purpose: "LOAD" }), runRoot, true, false, input.phase, () => observed.memberResults.has(member));
|
|
272
|
+
const resultExtension = (pi) => {
|
|
273
|
+
pi.registerTool({
|
|
274
|
+
name: "submit_member_result",
|
|
275
|
+
label: "提交成员结果",
|
|
276
|
+
description: "如实记录本次子任务结果;未解决的问题必须 FAIL。仅向主管汇报,不确认最终交付。",
|
|
277
|
+
parameters: Type.Object({
|
|
278
|
+
status: Type.Union([Type.Literal("PASS"), Type.Literal("FAIL")]),
|
|
279
|
+
summary: Type.String({ minLength: 1 }),
|
|
280
|
+
}),
|
|
281
|
+
async execute(_id, result) {
|
|
282
|
+
const revisions = await captureProjectRevisions(runRoot, input.projects, input.workItem.key, input.primaryProjectId);
|
|
283
|
+
observed.memberResults.set(member, { ...result, revisions });
|
|
284
|
+
queueMicrotask(() => child?.agent.abort());
|
|
285
|
+
return toolResult("结果已记录,向主管返回结论。");
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
};
|
|
289
|
+
const opened = await openPiSession({
|
|
292
290
|
cwd,
|
|
291
|
+
sessionDir: local.piSessions,
|
|
293
292
|
agentDir: local.piAgent,
|
|
293
|
+
existingId: childSessions[key],
|
|
294
294
|
modelRuntime: resolved.runtime,
|
|
295
295
|
model: resolved.model,
|
|
296
296
|
thinkingLevel: input.thinkingLevel,
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
extensions: [childLive.extension, resultExtension],
|
|
298
|
+
systemPrompt: "当前为团队子成员会话,员工配置由平台下发。",
|
|
299
|
+
runId,
|
|
299
300
|
});
|
|
300
|
-
|
|
301
|
-
|
|
301
|
+
childLive.attach(opened.session);
|
|
302
|
+
child = opened.session;
|
|
303
|
+
childSessions[key] = opened.sessionId;
|
|
302
304
|
childInstances.set(member, child);
|
|
303
|
-
|
|
304
|
-
|
|
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") {
|
|
305
|
+
subscribeSession(child, (kind, payload) => {
|
|
306
|
+
if (kind === "TEXT_DELTA")
|
|
312
307
|
childUpdates.get(member)?.(String(payload.text ?? ""));
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
await emit(kind, {
|
|
308
|
+
return emit(kind, {
|
|
316
309
|
...payload,
|
|
317
|
-
agentId: params.agent,
|
|
318
310
|
phase: input.phase,
|
|
311
|
+
agentId: member,
|
|
319
312
|
parentActivityKey: childTasks.get(member),
|
|
320
|
-
sessionId:
|
|
313
|
+
sessionId: opened.sessionId,
|
|
321
314
|
});
|
|
322
315
|
}, observed, redact);
|
|
316
|
+
await persistState();
|
|
323
317
|
}
|
|
324
318
|
await emit("SUBAGENT_UPDATED", {
|
|
325
319
|
phase: input.phase,
|
|
326
320
|
task: params.task,
|
|
327
321
|
activityKey: callId,
|
|
328
|
-
agentId:
|
|
329
|
-
|
|
322
|
+
agentId: member,
|
|
323
|
+
sessionRef: childSessions[key],
|
|
330
324
|
summary: "独立子会话已就绪",
|
|
331
325
|
});
|
|
332
326
|
const abort = () => {
|
|
333
|
-
|
|
327
|
+
child?.agent.abort();
|
|
334
328
|
};
|
|
335
329
|
signal?.addEventListener("abort", abort, { once: true });
|
|
336
330
|
try {
|
|
337
|
-
|
|
331
|
+
signal?.throwIfAborted();
|
|
332
|
+
await child.prompt(`${initialPrompt(input, runRoot, false)}\n\n本次主管分派给 ${member} 的任务:${params.task}\n只完成自身职责,最后调用 submit_member_result;主管负责其他成员协调和最终交付。`);
|
|
338
333
|
await child.waitForIdle();
|
|
339
334
|
if (signal?.aborted || observed.stopping)
|
|
340
335
|
throw new Error("SUBAGENT_STOPPED");
|
|
341
|
-
const result = observed.memberResults.get(
|
|
336
|
+
const result = observed.memberResults.get(member);
|
|
342
337
|
if (!result)
|
|
343
338
|
throw new Error("SUBAGENT_RESULT_MISSING");
|
|
344
339
|
return {
|
|
345
340
|
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
346
|
-
details: {
|
|
347
|
-
agent: params.agent,
|
|
348
|
-
status: result.status,
|
|
349
|
-
sessionFileRef: childSessions[`${input.phase}:${member}`],
|
|
350
|
-
},
|
|
341
|
+
details: { agent: member, status: result.status },
|
|
351
342
|
};
|
|
352
343
|
}
|
|
353
344
|
finally {
|
|
@@ -357,66 +348,49 @@ export class CodingRunManager {
|
|
|
357
348
|
},
|
|
358
349
|
});
|
|
359
350
|
};
|
|
360
|
-
const
|
|
351
|
+
const opened = await openPiSession({
|
|
361
352
|
cwd,
|
|
353
|
+
sessionDir: local.piSessions,
|
|
362
354
|
agentDir: local.piAgent,
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
noContextFiles: true,
|
|
355
|
+
existingId: sessionRef,
|
|
356
|
+
modelRuntime: resolved.runtime,
|
|
357
|
+
model: resolved.model,
|
|
358
|
+
thinkingLevel: input.thinkingLevel,
|
|
359
|
+
extensions: [...(availableAgents.length ? [memberExtension] : []), terminal, live.extension],
|
|
369
360
|
systemPrompt: input.team
|
|
370
361
|
? "当前为团队主负责人会话,员工配置由平台下发。"
|
|
371
362
|
: "当前为工单主会话,员工配置由平台下发。",
|
|
363
|
+
runId,
|
|
372
364
|
});
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
await placeholder.close();
|
|
388
|
-
sessionManager.setSessionFile(allocatedSessionFile);
|
|
389
|
-
sessionManager.appendCustomEntry("aip-run-binding", {
|
|
390
|
-
runId,
|
|
365
|
+
live.attach(opened.session);
|
|
366
|
+
const session = opened.session;
|
|
367
|
+
sessionRef = opened.sessionId;
|
|
368
|
+
subscribeSession(session, (kind, payload) => {
|
|
369
|
+
const activityKey = String(payload.activityKey ?? "");
|
|
370
|
+
if (kind === "SUBAGENT_STARTED")
|
|
371
|
+
observed.activeTasks.set(activityKey, String(payload.agentId));
|
|
372
|
+
if (["SUBAGENT_COMPLETED", "SUBAGENT_FAILED", "SUBAGENT_STOPPED"].includes(kind))
|
|
373
|
+
observed.activeTasks.delete(activityKey);
|
|
374
|
+
return emit(kind, {
|
|
375
|
+
...payload,
|
|
376
|
+
phase: input.phase,
|
|
377
|
+
agentId: payload.agentId ?? "supervisor",
|
|
378
|
+
sessionId: sessionRef,
|
|
391
379
|
});
|
|
392
|
-
}
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
model: resolved.model,
|
|
403
|
-
thinkingLevel: input.thinkingLevel,
|
|
404
|
-
resourceLoader: loader,
|
|
405
|
-
sessionManager,
|
|
406
|
-
});
|
|
407
|
-
live.attach(session);
|
|
408
|
-
const activeRun = { session, input, turnId, emit, stopping: false, observed };
|
|
380
|
+
}, observed, redact);
|
|
381
|
+
const activeRun = {
|
|
382
|
+
session,
|
|
383
|
+
supportsImages: resolved.model.input?.includes("image") === true,
|
|
384
|
+
input,
|
|
385
|
+
turnId,
|
|
386
|
+
emit,
|
|
387
|
+
stopping: false,
|
|
388
|
+
observed,
|
|
389
|
+
};
|
|
409
390
|
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;
|
|
391
|
+
phaseSessions[sessionPhase] = sessionRef;
|
|
418
392
|
await persistState();
|
|
419
|
-
await emit("RUN_STARTED", {
|
|
393
|
+
await emit("RUN_STARTED", { sessionRef, phase: input.phase });
|
|
420
394
|
if (resumePrompt || images.length > 0) {
|
|
421
395
|
await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
|
|
422
396
|
await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
|
|
@@ -432,13 +406,16 @@ export class CodingRunManager {
|
|
|
432
406
|
})
|
|
433
407
|
.finally(async () => {
|
|
434
408
|
await emit.flush();
|
|
409
|
+
for (const child of childInstances.values())
|
|
410
|
+
child.dispose();
|
|
411
|
+
session.dispose();
|
|
435
412
|
if (this.active.get(runId) === activeRun)
|
|
436
413
|
this.active.delete(runId);
|
|
437
414
|
});
|
|
438
415
|
}
|
|
439
416
|
async synthetic(client, host, runId, turnId, input) {
|
|
440
417
|
const emit = eventEmitter(client, host, runId, turnId);
|
|
441
|
-
await emit("RUN_STARTED", {
|
|
418
|
+
await emit("RUN_STARTED", { sessionRef: `synthetic:${runId}` });
|
|
442
419
|
const member = stageEmployeeIds(input)[0];
|
|
443
420
|
if (member)
|
|
444
421
|
await emit("SUBAGENT_STARTED", {
|
|
@@ -484,29 +461,39 @@ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistS
|
|
|
484
461
|
let sequence = initialSequence;
|
|
485
462
|
let queue = Promise.resolve();
|
|
486
463
|
const emit = (kind, payload) => {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
error
|
|
508
|
-
|
|
509
|
-
|
|
464
|
+
queue = queue.then(async () => {
|
|
465
|
+
const event = {
|
|
466
|
+
eventId: crypto.randomUUID(),
|
|
467
|
+
sequence: sequence + 1,
|
|
468
|
+
turnId,
|
|
469
|
+
kind,
|
|
470
|
+
payload: kind === "PREPARATION_COMPLETED" ? payload : redactPayload(payload, redact),
|
|
471
|
+
createdAt: new Date().toISOString(),
|
|
472
|
+
};
|
|
473
|
+
for (let attempt = 1; attempt <= EVENT_APPEND_MAX_ATTEMPTS; attempt++) {
|
|
474
|
+
try {
|
|
475
|
+
await client.appendCodingEvents(runId, {
|
|
476
|
+
hostId: host.hostId,
|
|
477
|
+
generation: host.generation,
|
|
478
|
+
events: [event],
|
|
479
|
+
});
|
|
480
|
+
sequence = event.sequence;
|
|
481
|
+
await persistSequence?.(event.sequence);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
catch (error) {
|
|
485
|
+
if (error instanceof PlatformClientError &&
|
|
486
|
+
error.status === 409 &&
|
|
487
|
+
error.code === "CODING_EVENT_TURN_STALE")
|
|
488
|
+
return;
|
|
489
|
+
process.stderr.write(`[local-coding-host] 事件上报失败:${redact(error instanceof Error ? error.message : String(error))}\n`);
|
|
490
|
+
if (attempt === EVENT_APPEND_MAX_ATTEMPTS) {
|
|
491
|
+
process.stderr.write(`[local-coding-host] 事件上报放弃:${event.kind} sequence=${event.sequence} attempts=${attempt}\n`);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
495
|
+
}
|
|
496
|
+
}
|
|
510
497
|
});
|
|
511
498
|
return queue;
|
|
512
499
|
};
|
|
@@ -524,227 +511,55 @@ function redactPayloadValue(value, redact) {
|
|
|
524
511
|
return redactPayload(value, redact);
|
|
525
512
|
return value;
|
|
526
513
|
}
|
|
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" });
|
|
514
|
+
async function openPiSession(options) {
|
|
515
|
+
await mkdir(options.sessionDir, { recursive: true, mode: 0o700 });
|
|
516
|
+
await mkdir(options.agentDir, { recursive: true, mode: 0o700 });
|
|
517
|
+
let sessionManager;
|
|
518
|
+
if (options.existingId) {
|
|
519
|
+
const listed = await SessionManager.list(options.cwd, options.sessionDir);
|
|
520
|
+
const match = listed.find((item) => item.id === options.existingId);
|
|
521
|
+
if (!match)
|
|
522
|
+
throw new Error("PI_SESSION_NOT_FOUND:请新建执行,历史文件已保留。");
|
|
523
|
+
sessionManager = SessionManager.open(match.path, options.sessionDir, options.cwd);
|
|
561
524
|
}
|
|
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
|
-
summary: params.checklistSummary,
|
|
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
|
-
};
|
|
525
|
+
else {
|
|
526
|
+
sessionManager = SessionManager.create(options.cwd, options.sessionDir);
|
|
527
|
+
const allocated = sessionManager.getSessionFile();
|
|
528
|
+
if (!allocated)
|
|
529
|
+
throw new Error("PI_SESSION_FILE_MISSING");
|
|
530
|
+
await mkdir(dirname(allocated), { recursive: true, mode: 0o700 });
|
|
531
|
+
const placeholder = await open(allocated, "wx", 0o600);
|
|
532
|
+
await placeholder.close();
|
|
533
|
+
sessionManager.setSessionFile(allocated);
|
|
534
|
+
sessionManager.appendCustomEntry("aip-run-binding", { runId: options.runId });
|
|
535
|
+
}
|
|
536
|
+
const sessionId = sessionManager.getSessionId();
|
|
537
|
+
const sessionFile = sessionManager.getSessionFile();
|
|
538
|
+
if (!sessionFile || !(await exists(sessionFile)))
|
|
539
|
+
throw new Error("PI_SESSION_FILE_NOT_PERSISTED");
|
|
540
|
+
const loader = new DefaultResourceLoader({
|
|
541
|
+
cwd: options.cwd,
|
|
542
|
+
agentDir: options.agentDir,
|
|
543
|
+
extensionFactories: options.extensions,
|
|
544
|
+
noSkills: true,
|
|
545
|
+
noExtensions: true,
|
|
546
|
+
noPromptTemplates: true,
|
|
547
|
+
noThemes: true,
|
|
548
|
+
noContextFiles: true,
|
|
549
|
+
systemPrompt: options.systemPrompt,
|
|
550
|
+
});
|
|
551
|
+
await loader.reload();
|
|
552
|
+
const { session } = await createAgentSession({
|
|
553
|
+
cwd: options.cwd,
|
|
554
|
+
agentDir: options.agentDir,
|
|
555
|
+
modelRuntime: options.modelRuntime,
|
|
556
|
+
model: options.model,
|
|
557
|
+
thinkingLevel: options.thinkingLevel,
|
|
558
|
+
resourceLoader: loader,
|
|
559
|
+
sessionManager,
|
|
560
|
+
});
|
|
561
|
+
session.setAutoCompactionEnabled(true);
|
|
562
|
+
return { session, sessionId };
|
|
748
563
|
}
|
|
749
564
|
function subscribeSession(session, emit, observed, redact) {
|
|
750
565
|
let messageId = crypto.randomUUID();
|
|
@@ -777,6 +592,22 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
777
592
|
messageId = crypto.randomUUID();
|
|
778
593
|
void emit("MESSAGE_STARTED", { messageId });
|
|
779
594
|
}
|
|
595
|
+
else if (event.type === "message_start" && event.message.role === "user") {
|
|
596
|
+
const text = textFromMessage(event.message.content);
|
|
597
|
+
if (observed.pendingSteerText !== undefined &&
|
|
598
|
+
(!observed.pendingSteerText || text.includes(observed.pendingSteerText))) {
|
|
599
|
+
delete observed.pendingSteerText;
|
|
600
|
+
void emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
else if (event.type === "agent_end") {
|
|
604
|
+
observed.nativeWillRetry = event.willRetry === true;
|
|
605
|
+
}
|
|
606
|
+
else if (event.type === "auto_retry_end") {
|
|
607
|
+
observed.nativeWillRetry = false;
|
|
608
|
+
if (event.success === false && event.finalError)
|
|
609
|
+
observed.lastModelError = event.finalError;
|
|
610
|
+
}
|
|
780
611
|
else if (event.type === "message_update" &&
|
|
781
612
|
event.assistantMessageEvent.type === "text_delta" &&
|
|
782
613
|
event.assistantMessageEvent.delta) {
|
|
@@ -794,7 +625,7 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
794
625
|
void emit(subagent ? "SUBAGENT_STARTED" : "TOOL_STARTED", subagent
|
|
795
626
|
? {
|
|
796
627
|
activityKey: event.toolCallId,
|
|
797
|
-
agentId: String(event.args?.agent ??
|
|
628
|
+
agentId: String(event.args?.agent ?? ""),
|
|
798
629
|
summary: String(event.args?.task ?? "团队子任务开始"),
|
|
799
630
|
task: String(event.args?.task ?? ""),
|
|
800
631
|
}
|
|
@@ -805,23 +636,20 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
805
636
|
});
|
|
806
637
|
}
|
|
807
638
|
else if (event.type === "tool_execution_update") {
|
|
808
|
-
pendingToolUpdates.set(event.toolCallId,
|
|
809
|
-
?
|
|
810
|
-
|
|
811
|
-
|
|
639
|
+
pendingToolUpdates.set(event.toolCallId, {
|
|
640
|
+
kind: event.toolName === "subagent" ? "SUBAGENT_UPDATED" : "TOOL_UPDATED",
|
|
641
|
+
payload: event.toolName === "subagent"
|
|
642
|
+
? {
|
|
812
643
|
activityKey: event.toolCallId,
|
|
813
644
|
agentId: String(event.args?.agent ?? observed.activeTasks.get(event.toolCallId) ?? ""),
|
|
814
645
|
currentAction: redact(textFromResult(event.partialResult)),
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
: {
|
|
818
|
-
kind: "TOOL_UPDATED",
|
|
819
|
-
payload: {
|
|
646
|
+
}
|
|
647
|
+
: {
|
|
820
648
|
activityKey: event.toolCallId,
|
|
821
649
|
toolName: event.toolName,
|
|
822
650
|
currentAction: redact(textFromResult(event.partialResult)),
|
|
823
651
|
},
|
|
824
|
-
|
|
652
|
+
});
|
|
825
653
|
if (!toolUpdateTimers.has(event.toolCallId))
|
|
826
654
|
toolUpdateTimers.set(event.toolCallId, setTimeout(() => flushToolUpdate(event.toolCallId), 200));
|
|
827
655
|
}
|
|
@@ -851,14 +679,330 @@ function subscribeSession(session, emit, observed, redact) {
|
|
|
851
679
|
summary: redact(textFromResult(event.result)),
|
|
852
680
|
});
|
|
853
681
|
}
|
|
682
|
+
else if (event.type === "bash_execution_update" && event.delta) {
|
|
683
|
+
const activityKey = event.id ?? "bash";
|
|
684
|
+
pendingToolUpdates.set(activityKey, {
|
|
685
|
+
kind: "TOOL_UPDATED",
|
|
686
|
+
payload: {
|
|
687
|
+
activityKey,
|
|
688
|
+
toolName: "bash",
|
|
689
|
+
currentAction: redact(event.delta),
|
|
690
|
+
},
|
|
691
|
+
});
|
|
692
|
+
if (!toolUpdateTimers.has(activityKey))
|
|
693
|
+
toolUpdateTimers.set(activityKey, setTimeout(() => flushToolUpdate(activityKey), 200));
|
|
694
|
+
}
|
|
695
|
+
else if (event.type === "compaction_start") {
|
|
696
|
+
flushText();
|
|
697
|
+
void emit("TOOL_STARTED", {
|
|
698
|
+
activityKey: `compaction:${event.reason}`,
|
|
699
|
+
toolName: "compaction",
|
|
700
|
+
summary: `上下文压缩(${event.reason})`,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
else if (event.type === "compaction_end") {
|
|
704
|
+
void emit("TOOL_COMPLETED", {
|
|
705
|
+
activityKey: `compaction:${event.reason}`,
|
|
706
|
+
toolName: "compaction",
|
|
707
|
+
isError: event.aborted === true || Boolean(event.errorMessage),
|
|
708
|
+
summary: redact(event.result?.summary ??
|
|
709
|
+
event.errorMessage ??
|
|
710
|
+
(event.aborted ? "压缩已取消" : "压缩完成")),
|
|
711
|
+
});
|
|
712
|
+
}
|
|
854
713
|
else if (event.type === "message_end" && event.message.role === "assistant") {
|
|
855
714
|
flushText();
|
|
715
|
+
if ("stopReason" in event.message && event.message.stopReason)
|
|
716
|
+
observed.lastModelStopReason = String(event.message.stopReason);
|
|
717
|
+
if ("errorMessage" in event.message && event.message.errorMessage)
|
|
718
|
+
observed.lastModelError = String(event.message.errorMessage);
|
|
856
719
|
const content = textFromMessage(event.message.content);
|
|
857
|
-
|
|
858
|
-
|
|
720
|
+
const usage = event.message.usage;
|
|
721
|
+
const payload = {
|
|
722
|
+
contentMarkdown: redact(content),
|
|
723
|
+
messageId,
|
|
724
|
+
};
|
|
725
|
+
if (usage) {
|
|
726
|
+
payload.usage = {
|
|
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())
|
|
735
|
+
void emit("MESSAGE_COMPLETED", payload);
|
|
859
736
|
}
|
|
860
737
|
});
|
|
861
738
|
}
|
|
739
|
+
function textFromMessage(content) {
|
|
740
|
+
if (typeof content === "string")
|
|
741
|
+
return content;
|
|
742
|
+
if (!Array.isArray(content))
|
|
743
|
+
return "";
|
|
744
|
+
return content
|
|
745
|
+
.map((part) => part && typeof part === "object" && "text" in part ? String(part.text ?? "") : "")
|
|
746
|
+
.join("");
|
|
747
|
+
}
|
|
748
|
+
function textFromResult(value) {
|
|
749
|
+
if (!value || typeof value !== "object")
|
|
750
|
+
return "";
|
|
751
|
+
return textFromMessage(value.content);
|
|
752
|
+
}
|
|
753
|
+
async function driveToTerminal(session, initial, observed, client, host, runId, turnId, emit, release, phase, images = []) {
|
|
754
|
+
let prompt = initial;
|
|
755
|
+
for (let attempt = 0; attempt <= 2; attempt += 1) {
|
|
756
|
+
if (!session.model?.input.includes("image") &&
|
|
757
|
+
session.messages?.some((message) => "content" in message &&
|
|
758
|
+
Array.isArray(message.content) &&
|
|
759
|
+
message.content.some((block) => block.type === "image")))
|
|
760
|
+
throw new Error("MODEL_IMAGES_UNSUPPORTED");
|
|
761
|
+
if (attempt === 0 && images.length > 0)
|
|
762
|
+
await session.prompt(prompt, { images });
|
|
763
|
+
else
|
|
764
|
+
await session.prompt(prompt);
|
|
765
|
+
await session.waitForIdle();
|
|
766
|
+
if (observed.stopping || observed.terminalReached || observed.pendingQuestion)
|
|
767
|
+
return;
|
|
768
|
+
if (observed.lastModelStopReason === "error" && observed.nativeWillRetry !== true) {
|
|
769
|
+
const code = observed.lastModelError?.split("\n")[0]?.slice(0, 200) || "MODEL_REQUEST_FAILED";
|
|
770
|
+
await client.reportCodingTerminal(runId, {
|
|
771
|
+
hostId: host.hostId,
|
|
772
|
+
generation: host.generation,
|
|
773
|
+
turnId,
|
|
774
|
+
kind: "FAILED",
|
|
775
|
+
summary: observed.lastModelError ?? code,
|
|
776
|
+
knownIssues: "",
|
|
777
|
+
failureCode: code,
|
|
778
|
+
projectFacts: [],
|
|
779
|
+
testFacts: [],
|
|
780
|
+
});
|
|
781
|
+
observed.terminalReached = true;
|
|
782
|
+
await emit("RUN_FAILED", { code });
|
|
783
|
+
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
|
+
await client.reportCodingTerminal(runId, {
|
|
792
|
+
hostId: host.hostId,
|
|
793
|
+
generation: host.generation,
|
|
794
|
+
turnId,
|
|
795
|
+
kind: "FAILED",
|
|
796
|
+
summary: observed.lastDeliveryError ?? "Agent 多次结束但没有形成明确终态",
|
|
797
|
+
knownIssues: "",
|
|
798
|
+
failureCode: observed.lastDeliveryError?.split("\n")[0]?.slice(0, 200) ?? "PI_TERMINAL_TOOL_NOT_CALLED",
|
|
799
|
+
projectFacts: [],
|
|
800
|
+
testFacts: [],
|
|
801
|
+
});
|
|
802
|
+
observed.terminalReached = true;
|
|
803
|
+
await emit("RUN_FAILED", { code: "PI_TERMINAL_TOOL_NOT_CALLED" });
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function terminalTools(client, host, runId, turnId, runRoot, input, observed, redact, persistBatch, pendingBatch, emitQuestion, flushEvents) {
|
|
807
|
+
const definitions = [];
|
|
808
|
+
const add = (definition) => {
|
|
809
|
+
definitions.push(definition);
|
|
810
|
+
};
|
|
811
|
+
add({
|
|
812
|
+
name: input.executionPurpose === "RELEASE"
|
|
813
|
+
? "submit_pull_requests"
|
|
814
|
+
: input.phase === "DEVELOPMENT"
|
|
815
|
+
? "complete_development"
|
|
816
|
+
: "submit_delivery",
|
|
817
|
+
label: "提交交付",
|
|
818
|
+
description: input.executionPurpose === "RELEASE"
|
|
819
|
+
? "核实上线清单并提交分支后,使用 Runtime GitLab 身份创建或复用 MR 并登记工单。"
|
|
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
|
+
},
|
|
961
|
+
});
|
|
962
|
+
add({
|
|
963
|
+
name: "report_blocker",
|
|
964
|
+
label: "报告阻塞",
|
|
965
|
+
description: "仅在认证、权限、仓库、外部环境或输入冲突真实阻塞时报告。",
|
|
966
|
+
parameters: Type.Object({
|
|
967
|
+
category: Type.Union([
|
|
968
|
+
Type.Literal("AUTHENTICATION"),
|
|
969
|
+
Type.Literal("PERMISSION"),
|
|
970
|
+
Type.Literal("REPOSITORY"),
|
|
971
|
+
Type.Literal("EXTERNAL_ENVIRONMENT"),
|
|
972
|
+
Type.Literal("INPUT_CONFLICT"),
|
|
973
|
+
]),
|
|
974
|
+
summary: Type.String(),
|
|
975
|
+
}),
|
|
976
|
+
async execute(_id, params) {
|
|
977
|
+
await client.reportCodingTerminal(runId, {
|
|
978
|
+
hostId: host.hostId,
|
|
979
|
+
generation: host.generation,
|
|
980
|
+
turnId,
|
|
981
|
+
kind: "BLOCKED",
|
|
982
|
+
summary: redact(params.summary),
|
|
983
|
+
knownIssues: "",
|
|
984
|
+
blockerCategory: params.category,
|
|
985
|
+
projectFacts: [],
|
|
986
|
+
testFacts: [],
|
|
987
|
+
});
|
|
988
|
+
observed.terminalReached = true;
|
|
989
|
+
return toolResult("阻塞已报告。");
|
|
990
|
+
},
|
|
991
|
+
});
|
|
992
|
+
return (api) => {
|
|
993
|
+
for (const definition of definitions)
|
|
994
|
+
api.registerTool({
|
|
995
|
+
name: definition.name,
|
|
996
|
+
label: definition.label,
|
|
997
|
+
description: definition.description,
|
|
998
|
+
parameters: definition.parameters,
|
|
999
|
+
async execute(id, args, signal) {
|
|
1000
|
+
signal?.throwIfAborted();
|
|
1001
|
+
return definition.execute(id, args);
|
|
1002
|
+
},
|
|
1003
|
+
});
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
862
1006
|
export async function prepareProjects(cacheRoot, runRoot, runId, projects, resume) {
|
|
863
1007
|
await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
|
|
864
1008
|
for (const project of projects) {
|
|
@@ -995,24 +1139,6 @@ async function uploadBatch(client, runId, batch) {
|
|
|
995
1139
|
}
|
|
996
1140
|
}
|
|
997
1141
|
}
|
|
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
1142
|
function toolResult(text) {
|
|
1017
1143
|
return { content: [{ type: "text", text }], details: {} };
|
|
1018
1144
|
}
|