@bf-meishijian/aip 0.0.3 → 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.
Files changed (46) hide show
  1. package/README.md +9 -1
  2. package/dist/agent-harness/index.d.ts +4 -0
  3. package/dist/agent-harness/index.d.ts.map +1 -0
  4. package/dist/agent-harness/index.js +4 -0
  5. package/dist/agent-harness/index.js.map +1 -0
  6. package/dist/agent-harness/pi-chat-model.d.ts +35 -0
  7. package/dist/agent-harness/pi-chat-model.d.ts.map +1 -0
  8. package/dist/agent-harness/pi-chat-model.js +285 -0
  9. package/dist/agent-harness/pi-chat-model.js.map +1 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +14 -15
  12. package/dist/cli.js.map +1 -1
  13. package/dist/coding-run-manager.d.ts +14 -4
  14. package/dist/coding-run-manager.d.ts.map +1 -1
  15. package/dist/coding-run-manager.js +565 -425
  16. package/dist/coding-run-manager.js.map +1 -1
  17. package/dist/delivery-files.d.ts +2 -10
  18. package/dist/delivery-files.d.ts.map +1 -1
  19. package/dist/delivery-files.js +2 -10
  20. package/dist/delivery-files.js.map +1 -1
  21. package/dist/harness.d.ts +2 -0
  22. package/dist/harness.d.ts.map +1 -0
  23. package/dist/harness.js +1 -0
  24. package/dist/harness.js.map +1 -0
  25. package/dist/live-employee.d.ts +1 -7
  26. package/dist/live-employee.d.ts.map +1 -1
  27. package/dist/live-employee.js +6 -10
  28. package/dist/live-employee.js.map +1 -1
  29. package/dist/model-stores.d.ts +20 -0
  30. package/dist/model-stores.d.ts.map +1 -0
  31. package/dist/model-stores.js +77 -0
  32. package/dist/model-stores.js.map +1 -0
  33. package/dist/pi-model-service.d.ts.map +1 -1
  34. package/dist/pi-model-service.js +47 -45
  35. package/dist/pi-model-service.js.map +1 -1
  36. package/dist/platform-client.d.ts +1 -1
  37. package/dist/platform-client.d.ts.map +1 -1
  38. package/dist/stage-delivery.d.ts +2 -0
  39. package/dist/stage-delivery.d.ts.map +1 -0
  40. package/dist/stage-delivery.js +75 -0
  41. package/dist/stage-delivery.js.map +1 -0
  42. package/dist/state.d.ts +3 -2
  43. package/dist/state.d.ts.map +1 -1
  44. package/dist/state.js +5 -2
  45. package/dist/state.js.map +1 -1
  46. package/package.json +7 -5
@@ -2,17 +2,19 @@ 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
7
  import { createAgentSession, DefaultResourceLoader, SessionManager, } from "@earendil-works/pi-coding-agent";
8
8
  import { Type } from "typebox";
9
9
  import { collectDeliveryFiles, deliveryDirectory, initializeDeliveryFiles, } from "./delivery-files.js";
10
- import { liveEmployee, TEAM_AGENTS } from "./live-employee.js";
10
+ import { liveEmployee } from "./live-employee.js";
11
11
  import { prepareMergeRequest, registerMergeRequest, } from "./merge-requests.js";
12
12
  import { paths } from "./paths.js";
13
13
  import { PlatformClientError } from "./platform-client.js";
14
14
  import { readJson, writeSecureJson } from "./secure-json.js";
15
+ import { DELIVERY_FILES, STAGE_DELIVERY_PROTOCOLS, stageEmployeeIds } from "./stage-delivery.js";
15
16
  const execFileAsync = promisify(execFile);
17
+ const EVENT_APPEND_MAX_ATTEMPTS = 8;
16
18
  export class CodingRunManager {
17
19
  pi;
18
20
  active = new Map();
@@ -65,14 +67,14 @@ export class CodingRunManager {
65
67
  active = undefined;
66
68
  }
67
69
  if (active) {
68
- if (images.length > 0 && !active.session.model?.input.includes("image"))
70
+ if (images.length > 0 && !active.supportsImages)
69
71
  throw new Error("MODEL_IMAGES_UNSUPPORTED");
72
+ active.observed.pendingSteerText = command.run.contentMarkdown ?? "";
70
73
  await active.emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
71
74
  if (images.length > 0)
72
75
  await active.session.steer(command.run.contentMarkdown ?? "", images);
73
76
  else
74
77
  await active.session.steer(command.run.contentMarkdown ?? "");
75
- await active.emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
76
78
  }
77
79
  else {
78
80
  if (!command.run.runInput)
@@ -88,6 +90,7 @@ export class CodingRunManager {
88
90
  active.stopping = true;
89
91
  active.observed.stopping = true;
90
92
  await active.session.abort();
93
+ await active.emit.flush();
91
94
  for (const [activityKey, agentId] of active.observed.activeTasks) {
92
95
  await active.emit("SUBAGENT_STOPPED", {
93
96
  activityKey,
@@ -126,10 +129,12 @@ export class CodingRunManager {
126
129
  const local = paths();
127
130
  const runRoot = join(local.runWorktrees, runId);
128
131
  await mkdir(runRoot, { recursive: true, mode: 0o700 });
129
- await mkdir(local.piSessions, { recursive: true, mode: 0o700 });
130
- await mkdir(local.piAgent, { recursive: true, mode: 0o700 });
131
132
  const stateFile = join(local.runState, `${runId}.json`);
132
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:请新建执行,历史文件和工作目录已保留。");
133
138
  const cwd = await prepareProjects(local.repositoryCache, runRoot, runId, input.projects, Boolean(storedState?.prepared));
134
139
  for (const project of input.projects)
135
140
  project.devBranch ||= `aip/${runId}/${project.id.slice(0, 8)}`;
@@ -158,7 +163,7 @@ export class CodingRunManager {
158
163
  : undefined;
159
164
  const phaseSessions = storedState?.phaseSessions ?? {};
160
165
  const sessionPhase = input.executionPurpose === "RELEASE" ? "DEVELOPMENT" : input.phase;
161
- let sessionFileRef = phaseSessions[sessionPhase];
166
+ let sessionRef = phaseSessions[sessionPhase];
162
167
  let lastEventSequence = storedState?.lastEventSequence ?? 0;
163
168
  const observed = {
164
169
  pendingQuestion: storedState?.phase === input.phase ? (storedState.pendingQuestion ?? "") : "",
@@ -178,7 +183,8 @@ export class CodingRunManager {
178
183
  },
179
184
  prepared: true,
180
185
  phaseSessions,
181
- sessionFile: sessionFileRef,
186
+ sessionRef,
187
+ harness: "pi",
182
188
  lastEventSequence,
183
189
  pendingQuestion: observed.pendingQuestion,
184
190
  childSessions,
@@ -208,12 +214,12 @@ export class CodingRunManager {
208
214
  });
209
215
  return;
210
216
  }
211
- const terminal = terminalExtension(client, host, runId, turnId, runRoot, input, observed, redact, async (batch) => {
217
+ const terminal = terminalTools(client, host, runId, turnId, runRoot, input, observed, redact, async (batch) => {
212
218
  pendingDelivery = batch;
213
219
  await persistState();
214
220
  }, () => pendingDelivery, (question) => emit("QUESTION_UPDATED", { question: redact(question) }), () => emit.flush());
215
221
  if (!this.pi.resolveRuntimeModel)
216
- throw new Error("PI_RUNTIME_UNAVAILABLE");
222
+ throw new Error("MODEL_RUNTIME_UNAVAILABLE");
217
223
  const resolved = await this.pi.resolveRuntimeModel(input.providerId, input.modelId, {
218
224
  runId,
219
225
  turnId,
@@ -228,125 +234,111 @@ export class CodingRunManager {
228
234
  turnId,
229
235
  member: "supervisor",
230
236
  };
231
- const availableAgents = Object.entries(TEAM_AGENTS)
232
- .filter(([member]) => input.team?.members[`${member}Id`])
233
- .map(([, name]) => name);
237
+ const availableAgents = stageEmployeeIds(input);
234
238
  const live = liveEmployee(client, runId, binding, await client.employeeConfig(runId, { ...binding, purpose: "LOAD" }), runRoot, availableAgents.length > 0, input.executionPurpose === "RELEASE", input.phase, () => observed.terminalReached);
235
239
  const childInstances = new Map();
236
240
  const childUpdates = new Map();
237
241
  const childTasks = new Map();
238
- const team = (api) => {
242
+ const memberExtension = (api) => {
243
+ if (!availableAgents.length)
244
+ return;
239
245
  api.registerTool({
240
246
  name: "subagent",
241
247
  label: "派发研发团队",
242
- description: `向实际绑定员工分派子任务,返回真实子会话结果。可用员工:${availableAgents.join("、")}。`,
248
+ description: `向实际绑定员工分派子任务,返回真实子会话结果。可用员工:${availableAgents
249
+ .map((id) => `${id}:${input.teamEmployees?.[id]?.name ?? id};${input.teamEmployees?.[id]?.description ?? ""}`)
250
+ .join("\n")}。`,
243
251
  parameters: Type.Object({
244
- agent: Type.Union(availableAgents.map((name) => Type.Literal(name))),
252
+ agent: availableAgents.length === 1
253
+ ? Type.Literal(availableAgents[0])
254
+ : Type.Union(availableAgents.map((name) => Type.Literal(name))),
245
255
  task: Type.String({ minLength: 1 }),
246
256
  }),
247
257
  async execute(callId, params, signal, onUpdate) {
248
- const member = Object.entries(TEAM_AGENTS).find(([, name]) => name === params.agent)?.[0];
258
+ const member = String(params.agent);
259
+ if (!availableAgents.includes(member))
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);
249
264
  childTasks.set(member, callId);
250
- const childBinding = { ...binding, member };
265
+ observed.memberResults.delete(member);
266
+ childUpdates.set(member, (text) => onUpdate?.({ content: [{ type: "text", text }], details: { agent: member } }));
267
+ const key = `${input.phase}:${member}`;
251
268
  let child = childInstances.get(member);
252
- observed.memberResults.delete(params.agent);
253
- childUpdates.set(member, (text) => onUpdate?.({ content: [{ type: "text", text }], details: { agent: params.agent } }));
254
269
  if (!child) {
255
- const childLive = liveEmployee(client, runId, childBinding, await client.employeeConfig(runId, { ...childBinding, purpose: "LOAD" }), runRoot, true, false, input.phase);
256
- const resultExtension = (pi) => pi.registerTool({
257
- name: "submit_member_result",
258
- label: "提交成员结果",
259
- description: "如实记录本次子任务的结果,未解决的问题必须 FAIL。仅向主管汇报,不确认最终交付。",
260
- parameters: Type.Object({
261
- status: Type.Union([Type.Literal("PASS"), Type.Literal("FAIL")]),
262
- summary: Type.String({ minLength: 1 }),
263
- }),
264
- async execute(_id, result) {
265
- const revisions = (await captureProjectRevisions(runRoot, input.projects, input.workItem.key, input.primaryProjectId)).filter((r) => Boolean(r));
266
- observed.memberResults.set(params.agent, {
267
- status: result.status,
268
- summary: result.summary,
269
- revisions,
270
- });
271
- return toolResult("结果已记录,向主管返回结论。");
272
- },
273
- });
274
- const childLoader = new DefaultResourceLoader({
275
- cwd,
276
- agentDir: local.piAgent,
277
- noExtensions: true,
278
- noSkills: true,
279
- noPromptTemplates: true,
280
- noThemes: true,
281
- noContextFiles: true,
282
- extensionFactories: [childLive.extension, resultExtension],
283
- systemPrompt: "你是研发团队中的独立数字员工,按本次固定职责工作。",
284
- });
285
- await childLoader.reload();
286
- const file = childSessions[`${input.phase}:${member}`];
287
- const childManager = file
288
- ? SessionManager.open(join(local.piSessions, file), local.piSessions, cwd)
289
- : SessionManager.create(cwd, local.piSessions);
290
- 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({
291
290
  cwd,
291
+ sessionDir: local.piSessions,
292
292
  agentDir: local.piAgent,
293
+ existingId: childSessions[key],
293
294
  modelRuntime: resolved.runtime,
294
295
  model: resolved.model,
295
296
  thinkingLevel: input.thinkingLevel,
296
- resourceLoader: childLoader,
297
- sessionManager: childManager,
297
+ extensions: [childLive.extension, resultExtension],
298
+ systemPrompt: "当前为团队子成员会话,员工配置由平台下发。",
299
+ runId,
298
300
  });
299
- child = created.session;
300
- childLive.attach(child);
301
+ childLive.attach(opened.session);
302
+ child = opened.session;
303
+ childSessions[key] = opened.sessionId;
301
304
  childInstances.set(member, child);
302
- const childFile = childManager.getSessionFile();
303
- if (!childFile)
304
- throw new Error("CHILD_SESSION_FILE_MISSING");
305
- childSessions[`${input.phase}:${member}`] = basename(childFile);
306
- await persistState();
307
- subscribeSession(child, async (kind, payload) => {
308
- if (kind === "MESSAGE_STARTED")
309
- return;
310
- if (kind === "TEXT_DELTA") {
305
+ subscribeSession(child, (kind, payload) => {
306
+ if (kind === "TEXT_DELTA")
311
307
  childUpdates.get(member)?.(String(payload.text ?? ""));
312
- return;
313
- }
314
- await emit(kind, {
308
+ return emit(kind, {
315
309
  ...payload,
316
- agentId: params.agent,
317
310
  phase: input.phase,
311
+ agentId: member,
318
312
  parentActivityKey: childTasks.get(member),
319
- sessionId: childSessions[`${input.phase}:${member}`],
313
+ sessionId: opened.sessionId,
320
314
  });
321
315
  }, observed, redact);
316
+ await persistState();
322
317
  }
323
318
  await emit("SUBAGENT_UPDATED", {
324
319
  phase: input.phase,
325
320
  task: params.task,
326
321
  activityKey: callId,
327
- agentId: params.agent,
328
- sessionFileRef: childSessions[`${input.phase}:${member}`],
322
+ agentId: member,
323
+ sessionRef: childSessions[key],
329
324
  summary: "独立子会话已就绪",
330
325
  });
331
326
  const abort = () => {
332
- void child?.abort();
327
+ child?.agent.abort();
333
328
  };
334
329
  signal?.addEventListener("abort", abort, { once: true });
335
330
  try {
336
- await child.prompt(`${initialPrompt(input, runRoot)}\n\n本次主管分派给 ${params.agent} 的任务:${params.task}\n只完成自身职责,最后调用 submit_member_result;主管负责其他成员协调和最终交付。`);
331
+ signal?.throwIfAborted();
332
+ await child.prompt(`${initialPrompt(input, runRoot, false)}\n\n本次主管分派给 ${member} 的任务:${params.task}\n只完成自身职责,最后调用 submit_member_result;主管负责其他成员协调和最终交付。`);
337
333
  await child.waitForIdle();
338
334
  if (signal?.aborted || observed.stopping)
339
335
  throw new Error("SUBAGENT_STOPPED");
340
- const result = observed.memberResults.get(params.agent);
336
+ const result = observed.memberResults.get(member);
341
337
  if (!result)
342
338
  throw new Error("SUBAGENT_RESULT_MISSING");
343
339
  return {
344
340
  content: [{ type: "text", text: JSON.stringify(result) }],
345
- details: {
346
- agent: params.agent,
347
- status: result.status,
348
- sessionFileRef: childSessions[`${input.phase}:${member}`],
349
- },
341
+ details: { agent: member, status: result.status },
350
342
  };
351
343
  }
352
344
  finally {
@@ -356,66 +348,49 @@ export class CodingRunManager {
356
348
  },
357
349
  });
358
350
  };
359
- const loader = new DefaultResourceLoader({
360
- cwd,
361
- agentDir: local.piAgent,
362
- extensionFactories: [...(availableAgents.length ? [team] : []), terminal, live.extension],
363
- noSkills: true,
364
- noExtensions: true,
365
- noPromptTemplates: true,
366
- noThemes: true,
367
- noContextFiles: true,
368
- systemPrompt: input.team
369
- ? "你是研发团队的主管,由平台提供固定团队身份和实时能力配置。"
370
- : "你是负责本工单实现、自审、测试与最终交付的数字员工。",
371
- });
372
- await loader.reload();
373
- const existingSession = phaseSessions[sessionPhase]
374
- ? join(local.piSessions, phaseSessions[sessionPhase])
375
- : null;
376
- let sessionManager;
377
- if (existingSession) {
378
- sessionManager = SessionManager.open(existingSession, local.piSessions, cwd);
379
- }
380
- else {
381
- sessionManager = SessionManager.create(cwd, local.piSessions);
382
- const allocatedSessionFile = sessionManager.getSessionFile();
383
- if (!allocatedSessionFile)
384
- throw new Error("PI_SESSION_FILE_MISSING");
385
- const placeholder = await open(allocatedSessionFile, "wx", 0o600);
386
- await placeholder.close();
387
- sessionManager.setSessionFile(allocatedSessionFile);
388
- sessionManager.appendCustomEntry("aip-run-binding", {
389
- runId,
390
- });
391
- }
392
- const sessionFile = sessionManager.getSessionFile();
393
- if (!sessionFile)
394
- throw new Error("PI_SESSION_FILE_MISSING");
395
- if (!(await exists(sessionFile)))
396
- throw new Error("PI_SESSION_FILE_NOT_PERSISTED");
397
- const { session } = await createAgentSession({
351
+ const opened = await openPiSession({
398
352
  cwd,
353
+ sessionDir: local.piSessions,
399
354
  agentDir: local.piAgent,
355
+ existingId: sessionRef,
400
356
  modelRuntime: resolved.runtime,
401
357
  model: resolved.model,
402
358
  thinkingLevel: input.thinkingLevel,
403
- resourceLoader: loader,
404
- sessionManager,
359
+ extensions: [...(availableAgents.length ? [memberExtension] : []), terminal, live.extension],
360
+ systemPrompt: input.team
361
+ ? "当前为团队主负责人会话,员工配置由平台下发。"
362
+ : "当前为工单主会话,员工配置由平台下发。",
363
+ runId,
405
364
  });
406
- live.attach(session);
407
- const activeRun = { session, input, turnId, emit, stopping: false, observed };
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,
379
+ });
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
+ };
408
390
  this.active.set(runId, activeRun);
409
- subscribeSession(session, (kind, payload) => emit(kind, {
410
- ...payload,
411
- phase: input.phase,
412
- agentId: payload.agentId ?? "supervisor",
413
- sessionId: sessionFileRef,
414
- }), observed, redact);
415
- sessionFileRef = basename(sessionFile);
416
- phaseSessions[sessionPhase] = sessionFileRef;
391
+ phaseSessions[sessionPhase] = sessionRef;
417
392
  await persistState();
418
- await emit("RUN_STARTED", { sessionFileRef, phase: input.phase });
393
+ await emit("RUN_STARTED", { sessionRef, phase: input.phase });
419
394
  if (resumePrompt || images.length > 0) {
420
395
  await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "QUEUED" });
421
396
  await emit("MESSAGE_QUEUE_UPDATED", { deliveryStatus: "READ" });
@@ -431,23 +406,29 @@ export class CodingRunManager {
431
406
  })
432
407
  .finally(async () => {
433
408
  await emit.flush();
409
+ for (const child of childInstances.values())
410
+ child.dispose();
411
+ session.dispose();
434
412
  if (this.active.get(runId) === activeRun)
435
413
  this.active.delete(runId);
436
414
  });
437
415
  }
438
416
  async synthetic(client, host, runId, turnId, input) {
439
417
  const emit = eventEmitter(client, host, runId, turnId);
440
- await emit("RUN_STARTED", { sessionFileRef: `${runId}.jsonl` });
441
- await emit("SUBAGENT_STARTED", {
442
- activityKey: "reviewer-1",
443
- agentId: "platform-reviewer",
444
- summary: "审查实现",
445
- });
446
- await emit("SUBAGENT_COMPLETED", {
447
- activityKey: "reviewer-1",
448
- agentId: "platform-reviewer",
449
- summary: "审查通过",
450
- });
418
+ await emit("RUN_STARTED", { sessionRef: `synthetic:${runId}` });
419
+ const member = stageEmployeeIds(input)[0];
420
+ if (member)
421
+ await emit("SUBAGENT_STARTED", {
422
+ activityKey: "member-1",
423
+ agentId: member,
424
+ summary: "审查实现",
425
+ });
426
+ if (member)
427
+ await emit("SUBAGENT_COMPLETED", {
428
+ activityKey: "member-1",
429
+ agentId: member,
430
+ summary: "审查通过",
431
+ });
451
432
  await emit("MESSAGE_COMPLETED", {
452
433
  contentMarkdown: "实现、测试与代码审查已经完成,等待确认交付。",
453
434
  });
@@ -480,29 +461,39 @@ function eventEmitter(client, host, runId, turnId, initialSequence = 0, persistS
480
461
  let sequence = initialSequence;
481
462
  let queue = Promise.resolve();
482
463
  const emit = (kind, payload) => {
483
- sequence += 1;
484
- const event = {
485
- eventId: crypto.randomUUID(),
486
- sequence,
487
- turnId,
488
- kind,
489
- payload: kind === "PREPARATION_COMPLETED" ? payload : redactPayload(payload, redact),
490
- createdAt: new Date().toISOString(),
491
- };
492
- queue = queue
493
- .then(() => client.appendCodingEvents(runId, {
494
- hostId: host.hostId,
495
- generation: host.generation,
496
- events: [event],
497
- }))
498
- .then(() => persistSequence?.(event.sequence))
499
- .then(() => undefined)
500
- .catch((error) => {
501
- if (error instanceof PlatformClientError &&
502
- error.status === 409 &&
503
- error.code === "CODING_EVENT_TURN_STALE")
504
- return;
505
- throw error;
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
+ }
506
497
  });
507
498
  return queue;
508
499
  };
@@ -520,227 +511,55 @@ function redactPayloadValue(value, redact) {
520
511
  return redactPayload(value, redact);
521
512
  return value;
522
513
  }
523
- async function driveToTerminal(session, initial, observed, client, host, runId, turnId, emit, release, phase, images = []) {
524
- let prompt = initial;
525
- for (let attempt = 0; attempt <= 2; attempt += 1) {
526
- if (!session.model?.input.includes("image") &&
527
- session.messages?.some((message) => "content" in message &&
528
- Array.isArray(message.content) &&
529
- message.content.some((block) => block.type === "image")))
530
- throw new Error("MODEL_IMAGES_UNSUPPORTED");
531
- if (attempt === 0 && images.length > 0)
532
- await session.prompt(prompt, { images });
533
- else
534
- await session.prompt(prompt);
535
- await session.waitForIdle();
536
- if (observed.stopping || observed.terminalReached || observed.pendingQuestion)
537
- return;
538
- if (attempt < 2) {
539
- prompt = release
540
- ? `请调用 submit_pull_requests 完成提 PR;最近错误:${observed.lastDeliveryError ?? "尚未提交"}。`
541
- : `你尚未成功交付。自主补齐必需产物后调用 ${phase === "DEVELOPMENT" ? "complete_development" : "submit_delivery"}。最近提交结果:${observed.lastDeliveryError ?? "尚未提交"}。缺文件就在当前会话补齐,传输故障不要重写报告。无法继续时说明真实原因,需要沟通可记录待回应问题。`;
542
- continue;
543
- }
544
- await client.reportCodingTerminal(runId, {
545
- hostId: host.hostId,
546
- generation: host.generation,
547
- turnId,
548
- kind: "FAILED",
549
- summary: observed.lastDeliveryError ?? "Agent 多次结束但没有形成明确终态",
550
- knownIssues: "",
551
- failureCode: observed.lastDeliveryError?.split("\n")[0]?.slice(0, 200) ?? "PI_TERMINAL_TOOL_NOT_CALLED",
552
- projectFacts: [],
553
- testFacts: [],
554
- });
555
- observed.terminalReached = true;
556
- 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);
557
524
  }
558
- }
559
- function terminalExtension(client, host, runId, turnId, runRoot, input, observed, redact, persistBatch, pendingBatch, emitQuestion, flushEvents) {
560
- return (pi) => {
561
- pi.registerTool({
562
- name: input.executionPurpose === "RELEASE"
563
- ? "submit_pull_requests"
564
- : input.phase === "DEVELOPMENT"
565
- ? "complete_development"
566
- : "submit_delivery",
567
- label: "提交交付",
568
- description: input.executionPurpose === "RELEASE"
569
- ? "核实上线清单并提交分支后,使用 Runtime GitLab 身份创建或复用 MR 并登记工单。"
570
- : "开发、review、测试完成后提交交付事实。",
571
- parameters: input.executionPurpose === "RELEASE"
572
- ? Type.Object({
573
- checklistSummary: Type.String({
574
- description: "核实依据、无需修改或修改摘要;缺口未解决时不要提交",
575
- }),
576
- projects: Type.Array(Type.Object({
577
- project: Type.String({
578
- description: `项目名称,只能选择:${input.projects.map((p) => p.name).join("、")}`,
579
- }),
580
- title: Type.String({ description: "包含工单编号及本项目变更摘要的 MR 标题" }),
581
- description: Type.String({
582
- description: "任务目的、本项目改动、测试与验收结论、发布注意、依赖、风险回滚及清单和报告链接",
583
- }),
584
- })),
585
- })
586
- : Type.Object({}),
587
- async execute(_toolCallId, rawParams) {
588
- try {
589
- if (input.executionPurpose === "RELEASE") {
590
- const params = rawParams;
591
- if (!params?.checklistSummary?.trim())
592
- throw new Error("AIP_RELEASE_CHECKLIST_SUMMARY_REQUIRED");
593
- const files = await collectDeliveryFiles(runRoot, input, ["RELEASE_CHECKLIST"]);
594
- const checklist = {
595
- status: "VERIFIED",
596
- summary: params.checklistSummary,
597
- markdown: files[0].content,
598
- };
599
- await client.submitPullRequests(runId, {
600
- hostId: host.hostId,
601
- generation: host.generation,
602
- turnId,
603
- workItemId: input.workItem.id,
604
- projects: [],
605
- checklist,
606
- });
607
- const context = {
608
- runId,
609
- turnId,
610
- workItem: input.workItem,
611
- projects: input.projects,
612
- primaryProjectId: input.primaryProjectId,
613
- };
614
- for (const project of input.projects) {
615
- const explanation = params.projects?.find((item) => item.project === project.name);
616
- if (!explanation)
617
- throw new Error(`AIP_MR_DESCRIPTION_REQUIRED:${project.name}`);
618
- const pendingFile = join(runRoot, `mr-pending-${safeName(project.id)}.json`);
619
- const result = await prepareMergeRequest({
620
- project,
621
- cwd: join(runRoot, safeName(project.name)),
622
- title: explanation.title,
623
- description: explanation.description,
624
- pendingFile,
625
- });
626
- await registerMergeRequest(client, context, host, result, pendingFile);
627
- }
628
- await client.submitPullRequests(runId, {
629
- hostId: host.hostId,
630
- generation: host.generation,
631
- turnId,
632
- workItemId: input.workItem.id,
633
- projects: [],
634
- completeRelease: true,
635
- });
636
- observed.terminalReached = true;
637
- return toolResult("上线清单已核实,MR 已提交并登记,等待合并;平台负责查询最新状态。");
638
- }
639
- const existing = pendingBatch();
640
- if (existing && existing.turnId === turnId) {
641
- await flushEvents();
642
- await uploadBatch(client, runId, existing);
643
- await persistBatch(undefined);
644
- observed.terminalReached = true;
645
- return toolResult("交付已提交,等待经办人确认。");
646
- }
647
- const files = await collectDeliveryFiles(runRoot, input, input.phase === "DEVELOPMENT"
648
- ? ["SUMMARY", "CODE_REVIEW", "IMPLEMENTATION_PLAN", "RELEASE_CHECKLIST"]
649
- : undefined);
650
- const projectFacts = await collectProjectFacts(runRoot, input.projects, input.workItem.key, input.primaryProjectId);
651
- const batch = {
652
- hostId: host.hostId,
653
- generation: host.generation,
654
- turnId,
655
- kind: input.phase === "DEVELOPMENT" ? "DEVELOPMENT_COMPLETED" : "DELIVERY",
656
- submissionId: crypto.randomUUID(),
657
- files,
658
- summary: files.find((file) => file.slot === "SUMMARY").content,
659
- knownIssues: "",
660
- projectFacts,
661
- testFacts: [],
662
- };
663
- await persistBatch(batch);
664
- await flushEvents();
665
- await uploadBatch(client, runId, batch);
666
- await persistBatch(undefined);
667
- observed.terminalReached = true;
668
- return toolResult("交付已提交,等待经办人确认。");
669
- }
670
- catch (error) {
671
- observed.lastDeliveryError =
672
- error instanceof Error ? error.message : "AIP_DELIVERY_FAILED";
673
- if (error instanceof PlatformClientError && error.status < 500 && error.status !== 429)
674
- await persistBatch(undefined);
675
- throw error;
676
- }
677
- },
678
- });
679
- pi.registerTool({
680
- name: "update_question",
681
- label: "更新待回应问题",
682
- description: "仅记录当前执行必须由人决定的业务问题,不把一般建议变为审批;真正解决后传空字符串清除。普通聊天继续使用 steer,不停止 Run。",
683
- parameters: Type.Object({ question: Type.String() }),
684
- async execute(_id, params) {
685
- // The caller binding supplies technical identity; the model only owns the question text.
686
- observed.pendingQuestion = params.question.trim();
687
- await emitQuestion(params.question);
688
- return toolResult(params.question);
689
- },
690
- });
691
- if (input.executionPurpose === "RELEASE")
692
- pi.registerTool({
693
- name: "request_code_changes",
694
- label: "返回开发修改",
695
- description: "用户明确要求修改应用代码时,将工单返回开发,再测试和人工确认;仅讨论或文档修改不要调用。",
696
- parameters: Type.Object({
697
- reason: Type.String({ description: "用户要求修改的内容及原因" }),
698
- }),
699
- async execute(_id, params) {
700
- await client.request(`/api/v1/coding-runs/${runId}/return-to-development`, {
701
- method: "POST",
702
- body: JSON.stringify({
703
- hostId: host.hostId,
704
- generation: host.generation,
705
- turnId,
706
- reason: params.reason,
707
- }),
708
- });
709
- observed.terminalReached = true;
710
- return toolResult("已返回开发,等待下一执行接手续改,完成后进入测试与人工确认。");
711
- },
712
- });
713
- pi.registerTool({
714
- name: "report_blocker",
715
- label: "报告阻塞",
716
- description: "仅在认证、权限、仓库、外部环境或输入冲突真实阻塞时报告。",
717
- parameters: Type.Object({
718
- category: Type.Union([
719
- Type.Literal("AUTHENTICATION"),
720
- Type.Literal("PERMISSION"),
721
- Type.Literal("REPOSITORY"),
722
- Type.Literal("EXTERNAL_ENVIRONMENT"),
723
- Type.Literal("INPUT_CONFLICT"),
724
- ]),
725
- summary: Type.String(),
726
- }),
727
- async execute(_id, params) {
728
- await client.reportCodingTerminal(runId, {
729
- hostId: host.hostId,
730
- generation: host.generation,
731
- turnId,
732
- kind: "BLOCKED",
733
- summary: redact(params.summary),
734
- knownIssues: "",
735
- blockerCategory: params.category,
736
- projectFacts: [],
737
- testFacts: [],
738
- });
739
- observed.terminalReached = true;
740
- return toolResult("阻塞已报告。");
741
- },
742
- });
743
- };
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 };
744
563
  }
745
564
  function subscribeSession(session, emit, observed, redact) {
746
565
  let messageId = crypto.randomUUID();
@@ -773,6 +592,22 @@ function subscribeSession(session, emit, observed, redact) {
773
592
  messageId = crypto.randomUUID();
774
593
  void emit("MESSAGE_STARTED", { messageId });
775
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
+ }
776
611
  else if (event.type === "message_update" &&
777
612
  event.assistantMessageEvent.type === "text_delta" &&
778
613
  event.assistantMessageEvent.delta) {
@@ -790,7 +625,7 @@ function subscribeSession(session, emit, observed, redact) {
790
625
  void emit(subagent ? "SUBAGENT_STARTED" : "TOOL_STARTED", subagent
791
626
  ? {
792
627
  activityKey: event.toolCallId,
793
- agentId: String(event.args?.agent ?? "platform-reviewer"),
628
+ agentId: String(event.args?.agent ?? ""),
794
629
  summary: String(event.args?.task ?? "团队子任务开始"),
795
630
  task: String(event.args?.task ?? ""),
796
631
  }
@@ -801,28 +636,26 @@ function subscribeSession(session, emit, observed, redact) {
801
636
  });
802
637
  }
803
638
  else if (event.type === "tool_execution_update") {
804
- pendingToolUpdates.set(event.toolCallId, event.toolName === "subagent"
805
- ? {
806
- kind: "SUBAGENT_UPDATED",
807
- payload: {
639
+ pendingToolUpdates.set(event.toolCallId, {
640
+ kind: event.toolName === "subagent" ? "SUBAGENT_UPDATED" : "TOOL_UPDATED",
641
+ payload: event.toolName === "subagent"
642
+ ? {
808
643
  activityKey: event.toolCallId,
809
- agentId: String(event.args?.agent ?? "platform-reviewer"),
644
+ agentId: String(event.args?.agent ?? observed.activeTasks.get(event.toolCallId) ?? ""),
810
645
  currentAction: redact(textFromResult(event.partialResult)),
811
- },
812
- }
813
- : {
814
- kind: "TOOL_UPDATED",
815
- payload: {
646
+ }
647
+ : {
816
648
  activityKey: event.toolCallId,
817
649
  toolName: event.toolName,
818
650
  currentAction: redact(textFromResult(event.partialResult)),
819
651
  },
820
- });
652
+ });
821
653
  if (!toolUpdateTimers.has(event.toolCallId))
822
654
  toolUpdateTimers.set(event.toolCallId, setTimeout(() => flushToolUpdate(event.toolCallId), 200));
823
655
  }
824
656
  else if (event.type === "tool_execution_end") {
825
657
  flushToolUpdate(event.toolCallId);
658
+ const member = observed.activeTasks.get(event.toolCallId);
826
659
  if (event.toolName === "subagent")
827
660
  observed.activeTasks.delete(event.toolCallId);
828
661
  void emit(event.toolName === "subagent"
@@ -835,7 +668,8 @@ function subscribeSession(session, emit, observed, redact) {
835
668
  ? {
836
669
  activityKey: event.toolCallId,
837
670
  agentId: String(event.result?.details?.agent ??
838
- "platform-reviewer"),
671
+ member ??
672
+ ""),
839
673
  summary: redact(textFromResult(event.result)),
840
674
  }
841
675
  : {
@@ -845,14 +679,330 @@ function subscribeSession(session, emit, observed, redact) {
845
679
  summary: redact(textFromResult(event.result)),
846
680
  });
847
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
+ }
848
713
  else if (event.type === "message_end" && event.message.role === "assistant") {
849
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);
850
719
  const content = textFromMessage(event.message.content);
851
- if (content)
852
- void emit("MESSAGE_COMPLETED", { contentMarkdown: redact(content), messageId });
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);
853
736
  }
854
737
  });
855
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
+ }
856
1006
  export async function prepareProjects(cacheRoot, runRoot, runId, projects, resume) {
857
1007
  await mkdir(cacheRoot, { recursive: true, mode: 0o700 });
858
1008
  for (const project of projects) {
@@ -957,14 +1107,22 @@ async function canonicalRepository(value) {
957
1107
  }
958
1108
  return value.replace(/\/$/u, "");
959
1109
  }
960
- export function initialPrompt(input, runRoot) {
1110
+ export function initialPrompt(input, runRoot, responsible = true) {
961
1111
  const directory = deliveryDirectory(runRoot, input);
962
1112
  if (input.executionPurpose === "RELEASE")
963
1113
  return `当前是人工验收通过后的上线准备,恢复原开发会话。工单 ${input.workItem.key}:${input.workItem.title}。项目分支:${input.projects.map((p) => `${p.name} ${p.devBranch} → ${p.baselineBranch}`).join(";")}。共享工作区已由测试会话处理,不能依赖旧会话记忆:先读取最新代码、${directory}/report.md、release_checklist.md、summary.md 和适用的 design.md。以项目源分支和目标分支为依据,不按 commit ID 或 tree hash 校验验收一致性。核实上线清单,明确无需修改或修改摘要;清单覆盖项目与分支、SQL、Apollo、定时任务、Kafka/RabbitMQ资源、监控告警、发布顺序、验证和回滚。以设计为预期、代码核实;没有设计则从代码提取。无法证明的环境事实标明待核实,不涉及说明原因;测试环境部署记录不能代替上线清单。用户明确要求修改应用代码时调用 request_code_changes,返回开发→测试→人工确认,不能在此阶段擅自修改后放行。清单有缺口时使用 update_question 说明,不能宣称核实完成。核实完成后提交清单文档改动,再调用 submit_pull_requests,传入核实摘要和各项目 MR 标题说明;工具使用 Runtime 的 glab 身份推送、创建或复用 MR 并结构化登记工单。说明覆盖任务目的、本项目改动、测试验收、发布注意、跨项目依赖、风险回滚及材料入口。MR仍打开时复用;关闭后仅在用户明确重提时创建新MR。平台查询状态,不负责创建。仅说明或更新文档不退回研发;不执行实际发布、知识整理、关单。`;
964
1114
  const projects = input.projects
965
1115
  .map((p) => `- ${p.name}: ${join(runRoot, safeName(p.name))}(开发分支 ${p.devBranch} → 基准分支 ${p.baselineBranch};测试分支 ${p.testBranch})${p.id === input.primaryProjectId ? "(主项目)" : ""}`)
966
1116
  .join("\n");
967
- return `${input.phase === "DEVELOPMENT" ? "当前是开发阶段:按员工与阶段配置完成工作,填写需求汇总 summary.md、plan.md、code_review.md、release_checklist.md 后调用 complete_development。由主管安排适合的成员协作。" : "当前是系统测试阶段:读取开发交接文件,随测试修复维护开发产出的 release_checklist.md,按需要安排成员协作。开始研发已授权将工单代码合并并 push 到项目已确认的 testBranch,触发测试环境自动部署;必须实际 push,只有本地合并不会触发部署。当前仓库使用共享 bare mirror,推送使用 git -c remote.origin.mirror=false push origin <已确认测试分支>,不要修改全局或共享缓存的 mirror 配置;操作后回到本工单分支。这是测试部署,不是生产上线;不得推送生产分支或执行生产发布。使用环境 Skill 观察部署目标和实际版本,将实际观察和验证结论写入测试报告;失败时修复、提交合并、等待部署、重测。人工打回复用当前环境。不默认重新审核,报告区分审核版本和后续修复。"} 需要业务判断时正常提问,使用 update_question 维护待回应问题;不要调用 report_blocker 表示普通提问。${input.inputConflict ? `正式输入已变化:先读取 ${directory}/prd.platform.md 和适用的 design.platform.md,对照合并到 prd.md/design.md;本地副本未被覆盖,对照文件不收集。` : ""}请完成工单 ${input.workItem.key}:${input.workItem.title}。项目:\n${projects}\n先用 read 读取 ${directory}/prd.md${input.technicalDesign ? ` 和 ${directory}/design.md` : ""}。\n在 ${directory} 完成 ${input.phase === "DEVELOPMENT" ? "summary.md、plan.md、code_review.md、release_checklist.md" : "summary.md、plan.md、report.md、code_review.md、release_checklist.md;test_plan.md 可选"}。summary.md 是本工单的问题、目标和范围汇总,不是实现摘要。release_checklist.md 是实际上线操作清单,覆盖项目与源/目标分支、SQL、Apollo、定时任务、Kafka/RabbitMQ资源、监控告警、发布顺序、验证及回滚;有设计以设计为预期并核对代码,没有设计从代码提取;未知环境事实标明待核实,不涉及写原因,不能用测试部署记录代替。全流程不以 commit ID 或 tree hash 判断是否需要补验。允许修改 PRD/Design,本轮将回收最终内容。报告包含各项目实际验证结果、版本、证据、未验证和遗留。只有上述固定槽位会被回收。\n${input.team ? "只向实际可用成员分派任务,无子成员时由主管自行完成并汇总。" : "自行完成当前阶段工作。"}团队自主选择开发、审核和测试方法,如实记录完成情况、未验证项及遗留问题,不编造结果。平台只检查必需产物,不要求指定成员、PASS组合或测试命令。由主管调用当前阶段的无参数完成工具,Runtime读取文件并上传。缺文件按工具错误补齐后再次提交。`;
1117
+ const phase = input.phase === "DEVELOPMENT" ? "DEVELOPMENT" : "INTEGRATION";
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}。`;
968
1126
  }
969
1127
  async function uploadBatch(client, runId, batch) {
970
1128
  for (let attempt = 0;; attempt++) {
@@ -981,24 +1139,6 @@ async function uploadBatch(client, runId, batch) {
981
1139
  }
982
1140
  }
983
1141
  }
984
- function textFromMessage(content) {
985
- if (typeof content === "string")
986
- return content;
987
- if (!Array.isArray(content))
988
- return "";
989
- return content
990
- .filter((part) => Boolean(part &&
991
- typeof part === "object" &&
992
- part.type === "text" &&
993
- typeof part.text === "string"))
994
- .map((part) => part.text)
995
- .join("\n");
996
- }
997
- function textFromResult(value) {
998
- if (!value || typeof value !== "object")
999
- return "";
1000
- return textFromMessage(value.content).slice(0, 4000);
1001
- }
1002
1142
  function toolResult(text) {
1003
1143
  return { content: [{ type: "text", text }], details: {} };
1004
1144
  }