@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.
- package/README.md +9 -1
- 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 +14 -4
- package/dist/coding-run-manager.d.ts.map +1 -1
- package/dist/coding-run-manager.js +565 -425
- package/dist/coding-run-manager.js.map +1 -1
- package/dist/delivery-files.d.ts +2 -10
- package/dist/delivery-files.d.ts.map +1 -1
- package/dist/delivery-files.js +2 -10
- package/dist/delivery-files.js.map +1 -1
- package/dist/harness.d.ts +2 -0
- package/dist/harness.d.ts.map +1 -0
- package/dist/harness.js +1 -0
- package/dist/harness.js.map +1 -0
- package/dist/live-employee.d.ts +1 -7
- package/dist/live-employee.d.ts.map +1 -1
- package/dist/live-employee.js +6 -10
- 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 +1 -1
- package/dist/platform-client.d.ts.map +1 -1
- package/dist/stage-delivery.d.ts +2 -0
- package/dist/stage-delivery.d.ts.map +1 -0
- package/dist/stage-delivery.js +75 -0
- package/dist/stage-delivery.js.map +1 -0
- 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 +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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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("
|
|
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 =
|
|
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
|
|
242
|
+
const memberExtension = (api) => {
|
|
243
|
+
if (!availableAgents.length)
|
|
244
|
+
return;
|
|
239
245
|
api.registerTool({
|
|
240
246
|
name: "subagent",
|
|
241
247
|
label: "派发研发团队",
|
|
242
|
-
description: `向实际绑定员工分派子任务,返回真实子会话结果。可用员工:${availableAgents
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
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
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
const
|
|
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
|
-
|
|
297
|
-
|
|
297
|
+
extensions: [childLive.extension, resultExtension],
|
|
298
|
+
systemPrompt: "当前为团队子成员会话,员工配置由平台下发。",
|
|
299
|
+
runId,
|
|
298
300
|
});
|
|
299
|
-
|
|
300
|
-
|
|
301
|
+
childLive.attach(opened.session);
|
|
302
|
+
child = opened.session;
|
|
303
|
+
childSessions[key] = opened.sessionId;
|
|
301
304
|
childInstances.set(member, child);
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
328
|
-
|
|
322
|
+
agentId: member,
|
|
323
|
+
sessionRef: childSessions[key],
|
|
329
324
|
summary: "独立子会话已就绪",
|
|
330
325
|
});
|
|
331
326
|
const abort = () => {
|
|
332
|
-
|
|
327
|
+
child?.agent.abort();
|
|
333
328
|
};
|
|
334
329
|
signal?.addEventListener("abort", abort, { once: true });
|
|
335
330
|
try {
|
|
336
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
404
|
-
|
|
359
|
+
extensions: [...(availableAgents.length ? [memberExtension] : []), terminal, live.extension],
|
|
360
|
+
systemPrompt: input.team
|
|
361
|
+
? "当前为团队主负责人会话,员工配置由平台下发。"
|
|
362
|
+
: "当前为工单主会话,员工配置由平台下发。",
|
|
363
|
+
runId,
|
|
405
364
|
});
|
|
406
|
-
live.attach(session);
|
|
407
|
-
const
|
|
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
|
-
|
|
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", {
|
|
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", {
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
error
|
|
504
|
-
|
|
505
|
-
|
|
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
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
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 ?? "
|
|
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,
|
|
805
|
-
?
|
|
806
|
-
|
|
807
|
-
|
|
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 ?? "
|
|
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
|
-
|
|
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
|
-
|
|
852
|
-
|
|
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
|
-
|
|
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
|
}
|