kcode-pi 0.1.27 → 0.1.31

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.
@@ -0,0 +1,430 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { basename, join } from "node:path";
5
+ import { Type, type Message } from "@earendil-works/pi-ai";
6
+ import { defineTool, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import {
8
+ buildChainedDelegationRequest,
9
+ buildDelegationCommandPrompt,
10
+ buildDelegationPrompt,
11
+ CHILD_AGENT_USER_TASK,
12
+ DEFAULT_REVIEW_TASK,
13
+ delegationBlockReason,
14
+ formatDelegationPreview,
15
+ isDelegationRole,
16
+ KD_DELEGATE_USAGE,
17
+ KD_DELEGATE_COMMAND_DESCRIPTION,
18
+ KD_REVIEW_COMMAND_DESCRIPTION,
19
+ KD_SUBAGENT_INVALID_PARAMS,
20
+ KD_SUBAGENT_PARALLEL_ROLE_ERROR,
21
+ KD_SUBAGENT_SCHEMA_DESCRIPTIONS,
22
+ KD_SUBAGENT_TOOL_DESCRIPTION,
23
+ isReadOnlyDelegationRole,
24
+ isSubagentChild,
25
+ parallelDelegationBlockReason,
26
+ parseDelegationArgs,
27
+ subagentAllowedTools,
28
+ type DelegationRequest,
29
+ type DelegationRole,
30
+ } from "../src/harness/delegation.ts";
31
+ import { readActiveRun } from "../src/harness/state.ts";
32
+
33
+ interface ChildAgentResult {
34
+ role: DelegationRole;
35
+ task: string;
36
+ exitCode: number;
37
+ output: string;
38
+ stderr: string;
39
+ model?: string;
40
+ turns: number;
41
+ step?: number;
42
+ }
43
+
44
+ type RawChildAgentResult = Omit<ChildAgentResult, "role" | "task" | "step">;
45
+
46
+ const DEFAULT_OUTPUT_LIMIT = 30_000;
47
+ const MAX_PARALLEL_TASKS = 8;
48
+ const MAX_CONCURRENCY = 4;
49
+ const MAX_CHAIN_TASKS = 8;
50
+
51
+ const kdSubagentTool = defineTool({
52
+ name: "kd_subagent",
53
+ label: "KD 子Agent",
54
+ description: KD_SUBAGENT_TOOL_DESCRIPTION,
55
+ parameters: Type.Object({
56
+ role: Type.Optional(Type.String({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.role })),
57
+ task: Type.Optional(Type.String({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.task })),
58
+ tasks: Type.Optional(
59
+ Type.Array(
60
+ Type.Object({
61
+ role: Type.String({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.taskItemRole }),
62
+ task: Type.String({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.taskItemTask }),
63
+ }),
64
+ { description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.tasks },
65
+ ),
66
+ ),
67
+ chain: Type.Optional(
68
+ Type.Array(
69
+ Type.Object({
70
+ role: Type.String({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.taskItemRole }),
71
+ task: Type.String({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.taskItemTask }),
72
+ }),
73
+ { description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.chain },
74
+ ),
75
+ ),
76
+ dryRun: Type.Optional(Type.Boolean({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.dryRun })),
77
+ maxOutputChars: Type.Optional(Type.Number({ description: KD_SUBAGENT_SCHEMA_DESCRIPTIONS.maxOutputChars })),
78
+ }),
79
+
80
+ async execute(_toolCallId, params, signal, _onUpdate, ctx) {
81
+ const mode = resolveMode(params);
82
+ if (!mode) {
83
+ return {
84
+ content: [{ type: "text", text: KD_SUBAGENT_INVALID_PARAMS }],
85
+ details: { error: "invalid-params" },
86
+ isError: true,
87
+ };
88
+ }
89
+
90
+ const run = readActiveRun(ctx.cwd);
91
+ if (params.dryRun) {
92
+ return {
93
+ content: [{ type: "text", text: formatModePreview(ctx.cwd, run, mode.requests) }],
94
+ details: { mode: mode.kind, dryRun: true, requests: mode.requests },
95
+ };
96
+ }
97
+
98
+ const modeBlockReason = mode.kind === "parallel" ? parallelDelegationBlockReason(mode.requests) : undefined;
99
+ const blockReason = modeBlockReason ?? mode.requests.map((request) => delegationBlockReason(request.role, run)).find(Boolean);
100
+ if (blockReason) {
101
+ return {
102
+ content: [{ type: "text", text: blockReason }],
103
+ details: { error: "blocked", mode: mode.kind, run },
104
+ isError: true,
105
+ };
106
+ }
107
+
108
+ const limit = normalizeOutputLimit(params.maxOutputChars);
109
+ const results =
110
+ mode.kind === "parallel"
111
+ ? await runParallelAgents(ctx, run, mode.requests, signal)
112
+ : mode.kind === "chain"
113
+ ? await runChainedAgents(ctx, run, mode.requests, signal)
114
+ : [await runDelegation(ctx, run, mode.requests[0], signal)];
115
+ const text = formatModeResult(mode.kind, results, limit);
116
+ return {
117
+ content: [{ type: "text", text }],
118
+ details: { mode: mode.kind, results },
119
+ isError: results.some((result) => result.exitCode !== 0),
120
+ };
121
+ },
122
+ });
123
+
124
+ export default function (pi: ExtensionAPI) {
125
+ if (isSubagentChild()) return;
126
+
127
+ pi.registerTool(kdSubagentTool);
128
+
129
+ pi.registerCommand("kd-delegate", {
130
+ description: KD_DELEGATE_COMMAND_DESCRIPTION,
131
+ handler: async (args, ctx) => {
132
+ const parsed = parseDelegationArgs(args);
133
+ if (!parsed) {
134
+ ctx.ui.notify(KD_DELEGATE_USAGE, "error");
135
+ return;
136
+ }
137
+ sendDelegationPrompt(pi, ctx, parsed.role, parsed.task, parsed.dryRun);
138
+ },
139
+ });
140
+
141
+ pi.registerCommand("kd-review", {
142
+ description: KD_REVIEW_COMMAND_DESCRIPTION,
143
+ handler: async (args, ctx) => {
144
+ const task = args.trim() || DEFAULT_REVIEW_TASK;
145
+ sendDelegationPrompt(pi, ctx, "review", task, false);
146
+ },
147
+ });
148
+ }
149
+
150
+ function sendDelegationPrompt(pi: ExtensionAPI, ctx: ExtensionContext, role: DelegationRole, task: string, dryRun: boolean): void {
151
+ const message = buildDelegationCommandPrompt({ role, task }, dryRun);
152
+ if (ctx.isIdle()) pi.sendUserMessage(message);
153
+ else pi.sendUserMessage(message, { deliverAs: "followUp" });
154
+ }
155
+
156
+ function resolveMode(params: {
157
+ role?: unknown;
158
+ task?: unknown;
159
+ tasks?: unknown;
160
+ chain?: unknown;
161
+ }): { kind: "single" | "parallel" | "chain"; requests: DelegationRequest[] } | undefined {
162
+ const single = normalizeRequest(params.role, params.task);
163
+ const tasks = Array.isArray(params.tasks) ? normalizeTaskList(params.tasks) : [];
164
+ const chain = Array.isArray(params.chain) ? normalizeTaskList(params.chain) : [];
165
+ if (tasks === undefined || chain === undefined) return undefined;
166
+ const modeCount = Number(Boolean(single)) + Number(tasks.length > 0) + Number(chain.length > 0);
167
+ if (modeCount !== 1) return undefined;
168
+ if (single) return { kind: "single", requests: [single] };
169
+ if (tasks.length > 0 && tasks.length <= MAX_PARALLEL_TASKS) return { kind: "parallel", requests: tasks };
170
+ if (chain.length > 0 && chain.length <= MAX_CHAIN_TASKS) return { kind: "chain", requests: chain };
171
+ return undefined;
172
+ }
173
+
174
+ function normalizeTaskList(values: unknown[]): DelegationRequest[] | undefined {
175
+ const requests = values.map(normalizeTaskItem);
176
+ if (requests.some((request) => !request)) return undefined;
177
+ return requests as DelegationRequest[];
178
+ }
179
+
180
+ function normalizeTaskItem(value: unknown): DelegationRequest | undefined {
181
+ if (!value || typeof value !== "object") return undefined;
182
+ const item = value as { role?: unknown; task?: unknown };
183
+ return normalizeRequest(item.role, item.task);
184
+ }
185
+
186
+ function normalizeRequest(roleValue: unknown, taskValue: unknown): DelegationRequest | undefined {
187
+ const role = typeof roleValue === "string" ? roleValue.toLowerCase() : "";
188
+ const task = typeof taskValue === "string" ? taskValue.trim() : "";
189
+ if (!isDelegationRole(role) || !task) return undefined;
190
+ return { role, task };
191
+ }
192
+
193
+ function formatModePreview(cwd: string, run: ReturnType<typeof readActiveRun>, requests: DelegationRequest[]): string {
194
+ return requests.map((request, index) => [`## Task ${index + 1}`, formatDelegationPreview(cwd, run, request)].join("\n\n")).join("\n\n---\n\n");
195
+ }
196
+
197
+ async function runDelegation(
198
+ ctx: ExtensionContext,
199
+ run: ReturnType<typeof readActiveRun>,
200
+ request: DelegationRequest,
201
+ signal: AbortSignal | undefined,
202
+ step?: number,
203
+ ): Promise<ChildAgentResult> {
204
+ const prompt = buildDelegationPrompt(ctx.cwd, run, request);
205
+ return await runChildAgent(ctx, request, prompt, signal, step);
206
+ }
207
+
208
+ async function runParallelAgents(
209
+ ctx: ExtensionContext,
210
+ run: ReturnType<typeof readActiveRun>,
211
+ requests: DelegationRequest[],
212
+ signal: AbortSignal | undefined,
213
+ ): Promise<ChildAgentResult[]> {
214
+ const results: ChildAgentResult[] = new Array(requests.length);
215
+ let next = 0;
216
+ const workerCount = Math.min(MAX_CONCURRENCY, requests.length);
217
+ await Promise.all(
218
+ Array.from({ length: workerCount }, async () => {
219
+ while (true) {
220
+ const index = next++;
221
+ if (index >= requests.length) return;
222
+ results[index] = await runDelegation(ctx, run, requests[index], signal, index + 1);
223
+ }
224
+ }),
225
+ );
226
+ return results;
227
+ }
228
+
229
+ async function runChainedAgents(
230
+ ctx: ExtensionContext,
231
+ run: ReturnType<typeof readActiveRun>,
232
+ requests: DelegationRequest[],
233
+ signal: AbortSignal | undefined,
234
+ ): Promise<ChildAgentResult[]> {
235
+ const results: ChildAgentResult[] = [];
236
+ let previousOutput = "";
237
+ for (let i = 0; i < requests.length; i++) {
238
+ const request = buildChainedDelegationRequest(requests[i], previousOutput);
239
+ const result = await runDelegation(ctx, run, request, signal, i + 1);
240
+ results.push(result);
241
+ previousOutput = result.output;
242
+ if (result.exitCode !== 0) break;
243
+ }
244
+ return results;
245
+ }
246
+
247
+ function formatModeResult(mode: "single" | "parallel" | "chain", results: ChildAgentResult[], limit: number): string {
248
+ const succeeded = results.filter((result) => result.exitCode === 0).length;
249
+ const header = `子 agent ${mode} 完成:${succeeded}/${results.length} succeeded`;
250
+ const sections = results.map((result, index) => {
251
+ const output = truncateOutput(result.output || result.stderr || "(子 agent 无输出)", limit);
252
+ return [
253
+ `## ${result.step ?? index + 1}. ${result.role}`,
254
+ `Exit:${result.exitCode}`,
255
+ result.model ? `Model:${result.model}` : undefined,
256
+ `Turns:${result.turns}`,
257
+ "",
258
+ output,
259
+ result.stderr.trim() ? `\nSTDERR:\n${truncateOutput(result.stderr.trim(), 4000)}` : undefined,
260
+ ]
261
+ .filter(Boolean)
262
+ .join("\n");
263
+ });
264
+ return [header, "", ...sections].join("\n");
265
+ }
266
+
267
+ function normalizeOutputLimit(value: unknown): number {
268
+ if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_OUTPUT_LIMIT;
269
+ return Math.max(1000, Math.min(100_000, Math.trunc(value)));
270
+ }
271
+
272
+ async function runChildAgent(
273
+ ctx: ExtensionContext,
274
+ request: DelegationRequest,
275
+ prompt: string,
276
+ signal: AbortSignal | undefined,
277
+ step?: number,
278
+ ): Promise<ChildAgentResult> {
279
+ const temp = writePromptFile(request.role, prompt);
280
+ const args = [
281
+ "--mode",
282
+ "json",
283
+ "-p",
284
+ "--no-session",
285
+ "--tools",
286
+ subagentAllowedTools(request.role).join(","),
287
+ "--append-system-prompt",
288
+ temp.file,
289
+ CHILD_AGENT_USER_TASK,
290
+ ];
291
+ const invocation = getPiInvocation(args);
292
+ try {
293
+ const result = await spawnJsonAgent(invocation.command, invocation.args, ctx.cwd, request.role, signal);
294
+ return { ...result, role: request.role, task: request.task, step };
295
+ } finally {
296
+ rmSync(temp.file, { force: true });
297
+ rmSync(temp.dir, { recursive: true, force: true });
298
+ }
299
+ }
300
+
301
+ function writePromptFile(role: DelegationRole, prompt: string): { dir: string; file: string } {
302
+ const dir = join(tmpdir(), `kcode-subagent-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
303
+ mkdirSync(dir, { recursive: true });
304
+ const file = join(dir, `${role}.md`);
305
+ writeFileSync(file, prompt, { encoding: "utf8", mode: 0o600 });
306
+ return { dir, file };
307
+ }
308
+
309
+ function getPiInvocation(args: string[]): { command: string; args: string[] } {
310
+ const currentScript = process.argv[1];
311
+ if (currentScript && existsSync(currentScript)) {
312
+ return { command: process.execPath, args: [currentScript, ...args] };
313
+ }
314
+
315
+ const execName = basename(process.execPath).toLowerCase();
316
+ const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
317
+ if (!isGenericRuntime) return { command: process.execPath, args };
318
+ return { command: "pi", args };
319
+ }
320
+
321
+ async function spawnJsonAgent(
322
+ command: string,
323
+ args: string[],
324
+ cwd: string,
325
+ role: DelegationRole,
326
+ signal: AbortSignal | undefined,
327
+ ): Promise<RawChildAgentResult> {
328
+ return await new Promise((resolve) => {
329
+ const messages: Message[] = [];
330
+ let stderr = "";
331
+ let buffer = "";
332
+ let turns = 0;
333
+ let model: string | undefined;
334
+ let completed = false;
335
+
336
+ const proc = spawn(command, args, {
337
+ cwd,
338
+ shell: false,
339
+ stdio: ["ignore", "pipe", "pipe"],
340
+ env: {
341
+ ...process.env,
342
+ KCODE_SUBAGENT_CHILD: "1",
343
+ KCODE_SUBAGENT_ROLE: role,
344
+ },
345
+ });
346
+
347
+ const finish = (exitCode: number) => {
348
+ if (completed) return;
349
+ completed = true;
350
+ resolve({
351
+ exitCode,
352
+ output: finalAssistantText(messages),
353
+ stderr,
354
+ model,
355
+ turns,
356
+ });
357
+ };
358
+
359
+ const processLine = (line: string) => {
360
+ if (!line.trim()) return;
361
+ let event: unknown;
362
+ try {
363
+ event = JSON.parse(line);
364
+ } catch {
365
+ return;
366
+ }
367
+ if (!isJsonEvent(event)) return;
368
+ if (event.type === "message_end" && event.message) {
369
+ messages.push(event.message);
370
+ if (event.message.role === "assistant") {
371
+ turns++;
372
+ if (event.message.model) model = event.message.model;
373
+ }
374
+ }
375
+ if (event.type === "tool_result_end" && event.message) messages.push(event.message);
376
+ };
377
+
378
+ proc.stdout.on("data", (chunk) => {
379
+ buffer += chunk.toString();
380
+ const lines = buffer.split(/\r?\n/);
381
+ buffer = lines.pop() ?? "";
382
+ for (const line of lines) processLine(line);
383
+ });
384
+
385
+ proc.stderr.on("data", (chunk) => {
386
+ stderr += chunk.toString();
387
+ });
388
+
389
+ proc.on("close", (code) => {
390
+ if (buffer.trim()) processLine(buffer);
391
+ finish(code ?? 0);
392
+ });
393
+ proc.on("error", (error) => {
394
+ stderr += error.message;
395
+ finish(1);
396
+ });
397
+
398
+ if (signal) {
399
+ const abort = () => {
400
+ stderr += "\n子 agent 已被中断。";
401
+ proc.kill("SIGTERM");
402
+ setTimeout(() => {
403
+ if (!proc.killed) proc.kill("SIGKILL");
404
+ }, 5000);
405
+ };
406
+ if (signal.aborted) abort();
407
+ else signal.addEventListener("abort", abort, { once: true });
408
+ }
409
+ });
410
+ }
411
+
412
+ function isJsonEvent(value: unknown): value is { type: string; message?: Message } {
413
+ return Boolean(value) && typeof value === "object" && typeof (value as { type?: unknown }).type === "string";
414
+ }
415
+
416
+ function finalAssistantText(messages: Message[]): string {
417
+ for (let i = messages.length - 1; i >= 0; i--) {
418
+ const message = messages[i];
419
+ if (message.role !== "assistant") continue;
420
+ for (const part of message.content) {
421
+ if (part.type === "text" && part.text.trim()) return part.text.trim();
422
+ }
423
+ }
424
+ return "";
425
+ }
426
+
427
+ function truncateOutput(output: string, maxChars: number): string {
428
+ if (output.length <= maxChars) return output;
429
+ return `${output.slice(0, maxChars)}\n\n[子 agent 输出已截断:剩余 ${output.length - maxChars} 字符]`;
430
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kcode-pi",
3
- "version": "0.1.27",
3
+ "version": "0.1.31",
4
4
  "description": "面向金蝶开发的 Pi Coding Agent 启动器、工具包和 Harness 工作流",
5
5
  "type": "module",
6
6
  "private": false,
@@ -32,6 +32,7 @@
32
32
  "extensions": [
33
33
  "./extensions/kingdee-header.ts",
34
34
  "./extensions/kingdee-harness.ts",
35
+ "./extensions/kingdee-subagents.ts",
35
36
  "./extensions/kingdee-tools.ts"
36
37
  ],
37
38
  "skills": [
@@ -4,7 +4,7 @@ description: 验证当前金蝶实现并收集证据。
4
4
 
5
5
  使用 `kd-verify` skill。
6
6
 
7
- 执行 `PLAN.md` 中的验证命令,收集 evidence,并更新 `VERIFY.md`。成功证据记录命令、`Exit: 0` 和输出摘要;命令无法运行时记录阻塞原因和残余风险。
7
+ 执行 `PLAN.md` 中的验证命令。验证命令完成后必须调用 `kd_verify_result` 记录 `command`、`exitCode` 和关键输出;不要手工绕过 Harness 的验证结果记录。
8
8
 
9
9
  用户补充说明:
10
10
 
@@ -13,12 +13,12 @@ Goal:
13
13
  - For Cangqiong/Xinghan/Flagship Java projects, run the planned Gradle command to catch syntax/compile errors, for example `.\gradlew.bat build`, `./gradlew build`, or a narrow `:module:build` task.
14
14
  - For C#/Enterprise projects, run `dotnet build` or `dotnet build <.sln/.csproj>` to catch syntax/compile errors.
15
15
  - Run `kd_check` when code is available.
16
- - Collect evidence into `.pi/kd/runs/<run-id>/VERIFY.md`.
16
+ - Record the completed validation command through `kd_verify_result` with the real command, exit code, and useful output summary.
17
17
  - Record failures, fixes, skipped checks, and residual risk.
18
18
 
19
19
  Rules:
20
20
 
21
21
  - Passing unit tests is not enough if acceptance criteria require workflow behavior.
22
22
  - If validation cannot run, state the exact blocker.
23
- - Passing evidence must show the real command, `Exit: 0`, and useful output summary.
23
+ - Do not hand-edit `VERIFY.md` as the primary result path; `kd_verify_result` owns pass/failure evidence and the repair loop.
24
24
  - Do not ship while verification evidence is missing.