@johnnywu/pi-subagents 2.2.1 → 2.2.3
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/CHANGELOG.md +15 -0
- package/extensions/subagent-executor.ts +64 -21
- package/extensions/subagent-render.ts +5 -0
- package/extensions/subagent-tool.ts +79 -13
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
## [2.2.3](https://github.com/jwu/pi-subagents/compare/v2.2.2...v2.2.3) (2026-09-18)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* keep subagent warnings in tool details ([2168ab0](https://github.com/jwu/pi-subagents/commit/2168ab010109af3119780dc28a110dbc7baa12c9))
|
|
7
|
+
* throttle concurrent subagent progress updates ([46807f5](https://github.com/jwu/pi-subagents/commit/46807f5f3f2d15dab7f0d79fa4710ecf3f6cf572))
|
|
8
|
+
|
|
9
|
+
## [2.2.2](https://github.com/jwu/pi-subagents/compare/v2.2.1...v2.2.2) (2026-09-04)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* surface subagent assistant-turn failures ([b9a3013](https://github.com/jwu/pi-subagents/commit/b9a3013ef9577fea1f634d9fda7b82c4ad09074d))
|
|
15
|
+
|
|
1
16
|
## [2.2.1](https://github.com/jwu/pi-subagents/compare/v2.2.0...v2.2.1) (2026-08-24)
|
|
2
17
|
|
|
3
18
|
|
|
@@ -49,6 +49,8 @@ export interface AgentProgress {
|
|
|
49
49
|
elapsedMs: number;
|
|
50
50
|
model?: string;
|
|
51
51
|
session?: SubagentSessionInfo;
|
|
52
|
+
/** 运行准备阶段的非致命提示,由工具渲染器展示,不能直接写入宿主 TUI。 */
|
|
53
|
+
warnings?: string[];
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export interface AgentResult extends AgentProgress {
|
|
@@ -223,14 +225,27 @@ function textFromMessage(message: unknown): string | undefined {
|
|
|
223
225
|
const content = (message as { content?: unknown }).content;
|
|
224
226
|
if (!Array.isArray(content)) return undefined;
|
|
225
227
|
|
|
228
|
+
const texts: string[] = [];
|
|
226
229
|
for (const part of content) {
|
|
227
230
|
if (part && typeof part === 'object' && (part as { type?: unknown }).type === 'text') {
|
|
228
231
|
const text = (part as { text?: unknown }).text;
|
|
229
|
-
if (typeof text === 'string')
|
|
232
|
+
if (typeof text === 'string') texts.push(text);
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
235
|
|
|
233
|
-
return undefined;
|
|
236
|
+
return texts.length > 0 ? texts.join('\n') : undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function isAssistantMessage(message: unknown): boolean {
|
|
240
|
+
return (
|
|
241
|
+
!!message && typeof message === 'object' && (message as { role?: unknown }).role === 'assistant'
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function stringMessageProperty(message: unknown, property: 'stopReason' | 'errorMessage') {
|
|
246
|
+
if (!message || typeof message !== 'object') return undefined;
|
|
247
|
+
const value = (message as Record<string, unknown>)[property];
|
|
248
|
+
return typeof value === 'string' ? value : undefined;
|
|
234
249
|
}
|
|
235
250
|
|
|
236
251
|
type ContextWindowLookup = {
|
|
@@ -322,18 +337,18 @@ function usageFromMessages(
|
|
|
322
337
|
return sawUsage ? aggregate : undefined;
|
|
323
338
|
}
|
|
324
339
|
|
|
325
|
-
function
|
|
340
|
+
function lastAssistantMessage(messages: unknown): unknown | undefined {
|
|
326
341
|
if (!Array.isArray(messages)) return undefined;
|
|
327
342
|
for (let index = messages.length - 1; index >= 0; index--) {
|
|
328
|
-
|
|
329
|
-
if (!message || typeof message !== 'object') continue;
|
|
330
|
-
if ((message as { role?: unknown }).role !== 'assistant') continue;
|
|
331
|
-
const model = modelFromMessage(message);
|
|
332
|
-
if (model) return model;
|
|
343
|
+
if (isAssistantMessage(messages[index])) return messages[index];
|
|
333
344
|
}
|
|
334
345
|
return undefined;
|
|
335
346
|
}
|
|
336
347
|
|
|
348
|
+
function lastAssistantModel(messages: unknown): string | undefined {
|
|
349
|
+
return modelFromMessage(lastAssistantMessage(messages));
|
|
350
|
+
}
|
|
351
|
+
|
|
337
352
|
function buildTaskArgument(task: string, taskFilePath: string | undefined): string {
|
|
338
353
|
return taskFilePath ? `Task: @${taskFilePath}` : `Task: ${task}`;
|
|
339
354
|
}
|
|
@@ -453,6 +468,9 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
453
468
|
let stderr = '';
|
|
454
469
|
let model = options.agent.model;
|
|
455
470
|
let stdoutBuffer = '';
|
|
471
|
+
let finalAssistantStopReason: string | undefined;
|
|
472
|
+
let finalAssistantErrorMessage: string | undefined;
|
|
473
|
+
const warnings: string[] = [];
|
|
456
474
|
|
|
457
475
|
const progress = (status: AgentProgress['status']): AgentProgress => ({
|
|
458
476
|
agent: options.agent.name,
|
|
@@ -464,11 +482,19 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
464
482
|
elapsedMs: now() - startedAt,
|
|
465
483
|
model,
|
|
466
484
|
session: options.session ?? { requested: 'none', effective: 'none' },
|
|
485
|
+
...(warnings.length > 0 ? { warnings: [...warnings] } : {}),
|
|
467
486
|
});
|
|
468
487
|
|
|
469
488
|
const emit = (status: AgentProgress['status'] = 'running') =>
|
|
470
489
|
options.onProgress?.(progress(status));
|
|
471
490
|
|
|
491
|
+
const setFinalAssistantOutput = (message: unknown) => {
|
|
492
|
+
// 最后一条 assistant 消息才是权威的最终输出,不能让工具结果占位符或旧文本伪装为结果。
|
|
493
|
+
output = textFromMessage(message) ?? '';
|
|
494
|
+
finalAssistantStopReason = stringMessageProperty(message, 'stopReason');
|
|
495
|
+
finalAssistantErrorMessage = stringMessageProperty(message, 'errorMessage');
|
|
496
|
+
};
|
|
497
|
+
|
|
472
498
|
try {
|
|
473
499
|
const promptFilePath = path.join(tempDir, 'system-prompt.md');
|
|
474
500
|
|
|
@@ -477,15 +503,15 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
477
503
|
cwd: options.cwd,
|
|
478
504
|
agentDir: options.agentDir,
|
|
479
505
|
});
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
506
|
+
// 子代理可能在 fullscreen 工具视图中运行。直接 console.warn 会绕过工具渲染,
|
|
507
|
+
// 污染宿主的聊天输入区域;把非致命提示附到工具进度中交给渲染器显示。
|
|
508
|
+
warnings.push(
|
|
509
|
+
...promptResult.skippedSkillPackages.map(
|
|
510
|
+
(source) => `package not installed, skipping skills: ${source}`,
|
|
511
|
+
),
|
|
512
|
+
...promptResult.skillWarnings,
|
|
513
|
+
...promptResult.missingSkills.map((name) => `skill not found: ${name}`),
|
|
514
|
+
);
|
|
489
515
|
await fileSystem.writeFile(promptFilePath, promptResult.prompt);
|
|
490
516
|
|
|
491
517
|
let taskFilePath: string | undefined;
|
|
@@ -578,8 +604,9 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
578
604
|
}
|
|
579
605
|
|
|
580
606
|
if (event.type === 'message_end' && event.message) {
|
|
581
|
-
|
|
582
|
-
|
|
607
|
+
if (!isAssistantMessage(event.message)) return;
|
|
608
|
+
|
|
609
|
+
setFinalAssistantOutput(event.message);
|
|
583
610
|
updateUsage(usage, usageFromMessage(event.message, modelRegistry));
|
|
584
611
|
model = modelFromMessage(event.message) ?? model;
|
|
585
612
|
emit();
|
|
@@ -589,6 +616,8 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
589
616
|
if (event.type === 'agent_end') {
|
|
590
617
|
const aggregate = usageFromMessages(event.messages, modelRegistry);
|
|
591
618
|
if (aggregate) replaceUsage(usage, aggregate);
|
|
619
|
+
const finalAssistantMessage = lastAssistantMessage(event.messages);
|
|
620
|
+
if (finalAssistantMessage) setFinalAssistantOutput(finalAssistantMessage);
|
|
592
621
|
model = lastAssistantModel(event.messages) ?? model;
|
|
593
622
|
emit();
|
|
594
623
|
}
|
|
@@ -616,8 +645,22 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
616
645
|
);
|
|
617
646
|
|
|
618
647
|
if (stdoutBuffer.trim()) processLine(stdoutBuffer);
|
|
619
|
-
const
|
|
620
|
-
|
|
648
|
+
const failedStopReason =
|
|
649
|
+
finalAssistantStopReason === 'error' || finalAssistantStopReason === 'aborted';
|
|
650
|
+
const hasFinalOutput = output.trim().length > 0;
|
|
651
|
+
const isError = exit.exitCode !== 0 || failedStopReason || !hasFinalOutput;
|
|
652
|
+
|
|
653
|
+
if (failedStopReason && finalAssistantErrorMessage) {
|
|
654
|
+
output = finalAssistantErrorMessage;
|
|
655
|
+
} else if (isError && !hasFinalOutput) {
|
|
656
|
+
output =
|
|
657
|
+
stderr ||
|
|
658
|
+
(failedStopReason
|
|
659
|
+
? `Subagent stopped with reason: ${finalAssistantStopReason}`
|
|
660
|
+
: exit.exitCode !== 0
|
|
661
|
+
? `Subagent exited with code ${exit.exitCode}`
|
|
662
|
+
: 'Subagent produced no final text output.');
|
|
663
|
+
}
|
|
621
664
|
|
|
622
665
|
const truncated = truncateHeadContent(output, OUTPUT_MAX_BYTES, OUTPUT_MAX_LINES);
|
|
623
666
|
if (truncated !== undefined) {
|
|
@@ -208,6 +208,11 @@ export function formatSubagentResultLines(
|
|
|
208
208
|
...(progress.session?.warning
|
|
209
209
|
? [{ text: `session: ${progress.session.warning}`, kind: 'hint' as const, singleLine: true }]
|
|
210
210
|
: []),
|
|
211
|
+
...(progress.warnings ?? []).map((warning) => ({
|
|
212
|
+
text: `warning: ${warning}`,
|
|
213
|
+
kind: 'hint' as const,
|
|
214
|
+
singleLine: true,
|
|
215
|
+
})),
|
|
211
216
|
...toolLines,
|
|
212
217
|
];
|
|
213
218
|
|
|
@@ -88,6 +88,60 @@ function toProgressResult(progress: AgentProgress) {
|
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
const PROGRESS_UPDATE_INTERVAL_MS = 100;
|
|
92
|
+
|
|
93
|
+
type PendingProgressUpdate = {
|
|
94
|
+
progress: AgentProgress;
|
|
95
|
+
onUpdate: AgentToolUpdateCallback<AgentProgress>;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 多个独立 subagent 工具调用共享同一批进度更新窗口,避免每个子进程事件都触发 TUI 重绘。
|
|
100
|
+
* 每个工具调用只保留最新快照;终态更新不等待窗口,确保完成状态即时可见。
|
|
101
|
+
*/
|
|
102
|
+
class ProgressUpdateCoordinator {
|
|
103
|
+
private pending = new Map<string, PendingProgressUpdate>();
|
|
104
|
+
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
105
|
+
|
|
106
|
+
queue(
|
|
107
|
+
toolCallId: string,
|
|
108
|
+
progress: AgentProgress,
|
|
109
|
+
onUpdate?: AgentToolUpdateCallback<AgentProgress>,
|
|
110
|
+
) {
|
|
111
|
+
if (!onUpdate) return;
|
|
112
|
+
this.pending.set(toolCallId, { progress, onUpdate });
|
|
113
|
+
if (!this.timer) this.timer = setTimeout(() => this.flush(), PROGRESS_UPDATE_INTERVAL_MS);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
publishTerminal(
|
|
117
|
+
toolCallId: string,
|
|
118
|
+
progress: AgentProgress,
|
|
119
|
+
onUpdate?: AgentToolUpdateCallback<AgentProgress>,
|
|
120
|
+
) {
|
|
121
|
+
this.pending.delete(toolCallId);
|
|
122
|
+
this.clearTimerWhenIdle();
|
|
123
|
+
onUpdate?.(toProgressResult(progress));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
forget(toolCallId: string) {
|
|
127
|
+
this.pending.delete(toolCallId);
|
|
128
|
+
this.clearTimerWhenIdle();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private flush() {
|
|
132
|
+
this.timer = undefined;
|
|
133
|
+
const updates = [...this.pending.values()];
|
|
134
|
+
this.pending.clear();
|
|
135
|
+
for (const { progress, onUpdate } of updates) onUpdate(toProgressResult(progress));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private clearTimerWhenIdle() {
|
|
139
|
+
if (this.pending.size > 0 || !this.timer) return;
|
|
140
|
+
clearTimeout(this.timer);
|
|
141
|
+
this.timer = undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
91
145
|
function freshSessionInfo(requested: SubagentSessionMode, warning?: string): SubagentSessionInfo {
|
|
92
146
|
return {
|
|
93
147
|
requested,
|
|
@@ -308,6 +362,7 @@ export function registerSubagentTool(
|
|
|
308
362
|
? options.agents.filter((candidate) => allowed.has(candidate.name))
|
|
309
363
|
: options.agents;
|
|
310
364
|
const runner = options.run ?? runSubagent;
|
|
365
|
+
const progressCoordinator = new ProgressUpdateCoordinator();
|
|
311
366
|
|
|
312
367
|
const availableSubagents = agents.map((agent) => agent.name);
|
|
313
368
|
const agentNames = [...availableSubagents].sort().join(', ');
|
|
@@ -340,19 +395,30 @@ export function registerSubagentTool(
|
|
|
340
395
|
|
|
341
396
|
const childCwd = params.cwd ?? ctx.cwd;
|
|
342
397
|
const session = resolveSubagentSession(params.session, childCwd, ctx, options.agentDir);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
398
|
+
try {
|
|
399
|
+
const result = await runner({
|
|
400
|
+
agent,
|
|
401
|
+
task: params.task,
|
|
402
|
+
cwd: childCwd,
|
|
403
|
+
signal,
|
|
404
|
+
depth: Number(env.PI_SUBAGENT_DEPTH ?? '0') + 1,
|
|
405
|
+
availableAgents: availableSubagentsForAgent(agent, availableSubagents),
|
|
406
|
+
agentDir: options.agentDir,
|
|
407
|
+
session,
|
|
408
|
+
onProgress: (progress) => {
|
|
409
|
+
if (progress.status === 'running') {
|
|
410
|
+
progressCoordinator.queue(_toolCallId, progress, onUpdate);
|
|
411
|
+
} else {
|
|
412
|
+
progressCoordinator.publishTerminal(_toolCallId, progress, onUpdate);
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
return toToolResult(result);
|
|
418
|
+
} finally {
|
|
419
|
+
// Pi 会忽略工具 promise 结算后的 onUpdate;丢弃滞留快照以避免无效的延迟回调。
|
|
420
|
+
progressCoordinator.forget(_toolCallId);
|
|
421
|
+
}
|
|
356
422
|
},
|
|
357
423
|
|
|
358
424
|
renderCall(args, theme, context) {
|