@ghyper9023/pi-dev-workflow 0.5.0 → 0.5.1
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/extensions/sub-agents.ts +59 -14
- package/extensions/workflow-engine.ts +24 -0
- package/package.json +1 -1
package/extensions/sub-agents.ts
CHANGED
|
@@ -34,7 +34,11 @@ const DEFAULT_TIMEOUT_MS = 180_000;
|
|
|
34
34
|
const PROGRESS_INTERVAL_MS = 1_500;
|
|
35
35
|
|
|
36
36
|
/** Hard limit on accumulated output per subagent (prevents OOM on runaway). */
|
|
37
|
-
|
|
37
|
+
// Keep up to 10MB of output — large enough to hold complete session event streams
|
|
38
|
+
// including cargo test compilation output (commonly 100-400 KB).
|
|
39
|
+
// We preserve the END of the buffer (last N bytes) so extractFinalOutput can
|
|
40
|
+
// find the final AI response even when the total output is large.
|
|
41
|
+
const MAX_BUFFER_BYTES = 10_000_000;
|
|
38
42
|
|
|
39
43
|
/**
|
|
40
44
|
* Locations to auto-load APPEND_SYSTEM.md / append.system.md from (checked in order).
|
|
@@ -477,13 +481,14 @@ export async function spawnSubagent(
|
|
|
477
481
|
|
|
478
482
|
// ── Stream stdout data + immediate progress callback ──
|
|
479
483
|
// Fix #3: Flush progress on every data chunk
|
|
484
|
+
// Fix #4: Preserve the END of the buffer (last N bytes) instead of the beginning,
|
|
485
|
+
// because extractFinalOutput needs the final AI response which is at the end.
|
|
480
486
|
proc.stdout.on("data", (data: Buffer) => {
|
|
481
487
|
const chunk = data.toString();
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
}
|
|
488
|
+
stdout += chunk;
|
|
489
|
+
if (stdout.length > MAX_BUFFER_BYTES) {
|
|
490
|
+
// Keep only the last MAX_BUFFER_BYTES bytes
|
|
491
|
+
stdout = stdout.slice(stdout.length - MAX_BUFFER_BYTES);
|
|
487
492
|
}
|
|
488
493
|
// Immediate progress on new data
|
|
489
494
|
if (!settled && onProgress) {
|
|
@@ -497,11 +502,9 @@ export async function spawnSubagent(
|
|
|
497
502
|
|
|
498
503
|
proc.stderr.on("data", (data: Buffer) => {
|
|
499
504
|
const chunk = data.toString();
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
stderr = stderr.slice(0, MAX_BUFFER_BYTES);
|
|
504
|
-
}
|
|
505
|
+
stderr += chunk;
|
|
506
|
+
if (stderr.length > MAX_BUFFER_BYTES) {
|
|
507
|
+
stderr = stderr.slice(stderr.length - MAX_BUFFER_BYTES);
|
|
505
508
|
}
|
|
506
509
|
});
|
|
507
510
|
|
|
@@ -573,11 +576,13 @@ export function extractFinalOutput(jsonOutput: string): string {
|
|
|
573
576
|
textEndSeen = true;
|
|
574
577
|
}
|
|
575
578
|
|
|
576
|
-
// Format 2:
|
|
577
|
-
//
|
|
579
|
+
// Format 2: pi agent event — message_end / message_stop / message_complete
|
|
580
|
+
// Only process assistant messages; toolResult and user messages contain
|
|
581
|
+
// raw tool output (e.g. ls -la) that would pollute the extracted summary.
|
|
578
582
|
if ((event.type === "message_stop" || event.type === "message_end" ||
|
|
579
583
|
event.type === "message_complete") &&
|
|
580
|
-
event.message?.content
|
|
584
|
+
event.message?.content &&
|
|
585
|
+
event.message?.role === "assistant") {
|
|
581
586
|
const parts = Array.isArray(event.message.content)
|
|
582
587
|
? event.message.content
|
|
583
588
|
: [event.message.content];
|
|
@@ -608,6 +613,46 @@ export function extractFinalOutput(jsonOutput: string): string {
|
|
|
608
613
|
if (event.type === "complete" && event.text) {
|
|
609
614
|
result = event.text;
|
|
610
615
|
}
|
|
616
|
+
|
|
617
|
+
// Format 5b: turn_end — extract text from the turn's assistant message.
|
|
618
|
+
// This provides an additional extraction point when message_end is lost
|
|
619
|
+
// due to buffer truncation (large tool results like cargo test output
|
|
620
|
+
// can push the final assistant message beyond the buffer limit).
|
|
621
|
+
if (event.type === "turn_end" &&
|
|
622
|
+
event.message?.content &&
|
|
623
|
+
event.message?.role === "assistant") {
|
|
624
|
+
const parts = Array.isArray(event.message.content)
|
|
625
|
+
? event.message.content
|
|
626
|
+
: [event.message.content];
|
|
627
|
+
for (const part of parts) {
|
|
628
|
+
if (typeof part === "string") result = part;
|
|
629
|
+
else if (part?.type === "text") result = part.text;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Format 6: agent_end — iterate messages backwards to find the last
|
|
634
|
+
// assistant message with text content. This catches edge cases where
|
|
635
|
+
// the final assistant response has no type:text block (e.g. only
|
|
636
|
+
// thinking/toolCall), which left result polluted by a prior tool result.
|
|
637
|
+
if (event.type === "agent_end" && Array.isArray(event.messages)) {
|
|
638
|
+
for (let i = event.messages.length - 1; i >= 0; i--) {
|
|
639
|
+
const msg = event.messages[i];
|
|
640
|
+
if (msg?.role === "assistant" && Array.isArray(msg.content)) {
|
|
641
|
+
for (let j = msg.content.length - 1; j >= 0; j--) {
|
|
642
|
+
const block = msg.content[j];
|
|
643
|
+
if (typeof block === "string") {
|
|
644
|
+
result = block;
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
if (block?.type === "text" && block.text) {
|
|
648
|
+
result = block.text;
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (result) break;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
611
656
|
} catch {
|
|
612
657
|
// If a line isn't JSON, it might be raw text output - collect it
|
|
613
658
|
if (!result) {
|
|
@@ -1378,6 +1378,14 @@ async function executeSingleStep(
|
|
|
1378
1378
|
} else {
|
|
1379
1379
|
updateChainContext(chainKey, `${agentName} 已完成执行,未检测到文件变更。`);
|
|
1380
1380
|
}
|
|
1381
|
+
|
|
1382
|
+
// ── Capture AI work summary as supplementary chain context ──
|
|
1383
|
+
const workSummary = extractFinalOutput(result.output);
|
|
1384
|
+
if (workSummary) {
|
|
1385
|
+
updateChainContext(`${agentName} 工作总结`,
|
|
1386
|
+
`${agentName} 已完成工作,以下是其工作总结:\n\n${workSummary}`
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1381
1389
|
}
|
|
1382
1390
|
|
|
1383
1391
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1471,6 +1479,14 @@ async function executeLoopGroup(
|
|
|
1471
1479
|
);
|
|
1472
1480
|
}
|
|
1473
1481
|
|
|
1482
|
+
// ── Capture loop agent work summary as supplementary chain context ──
|
|
1483
|
+
const loopFinalText = extractFinalOutput(agentResult.output);
|
|
1484
|
+
if (loopFinalText) {
|
|
1485
|
+
updateChainContext(`${step.loopAgentName} 工作总结`,
|
|
1486
|
+
`${step.loopAgentName} 已完成工作,以下是其工作总结:\n\n${loopFinalText}`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1474
1490
|
if (isTimeoutResult(agentResult)) {
|
|
1475
1491
|
if (mode === "full-auto") {
|
|
1476
1492
|
contextPrompt = `[TIMEOUT_WARNING] 上一个 ${step.loopAgentName} 执行超时。\n\n${buildReviewTask(prompt, planFileRelPath, _workflowCwd, _workflowId, buildChainContext())}`;
|
|
@@ -1526,6 +1542,14 @@ async function executeLoopGroup(
|
|
|
1526
1542
|
);
|
|
1527
1543
|
}
|
|
1528
1544
|
|
|
1545
|
+
// ── Capture reviewer work summary as supplementary chain context ──
|
|
1546
|
+
// Reuse already-parsed extractedOutput to avoid double parsing
|
|
1547
|
+
if (extractedOutput) {
|
|
1548
|
+
updateChainContext(`审查工作总结`,
|
|
1549
|
+
`审查者已完成审查,以下是其审查总结:\n\n${extractedOutput}`
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1529
1553
|
// ── Decide whether to loop ──
|
|
1530
1554
|
if (reviewSummary?.maxSeverity === "critical" && loopCount < maxLoops) {
|
|
1531
1555
|
const reviewReportHint = `\n完整审查报告在 .pi-dev-output/pi-review/md/(grep UUID "${_workflowId}" 查找)。`;
|