@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 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') return text;
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 lastAssistantModel(messages: unknown): string | undefined {
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
- const message = messages[index];
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
- for (const source of promptResult.skippedSkillPackages) {
481
- console.warn(`[pi-subagents] package not installed, skipping skills: ${source}`);
482
- }
483
- for (const warning of promptResult.skillWarnings) {
484
- console.warn(`[pi-subagents] ${warning}`);
485
- }
486
- for (const name of promptResult.missingSkills) {
487
- console.warn(`[pi-subagents] skill not found: ${name}`);
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
- const text = textFromMessage(event.message);
582
- if (text !== undefined) output = text;
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 isError = exit.exitCode !== 0;
620
- if (isError && !output) output = stderr || `Subagent exited with code ${exit.exitCode}`;
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
- const result = await runner({
344
- agent,
345
- task: params.task,
346
- cwd: childCwd,
347
- signal,
348
- depth: Number(env.PI_SUBAGENT_DEPTH ?? '0') + 1,
349
- availableAgents: availableSubagentsForAgent(agent, availableSubagents),
350
- agentDir: options.agentDir,
351
- session,
352
- onProgress: (progress) => onUpdate?.(toProgressResult(progress)),
353
- });
354
-
355
- return toToolResult(result);
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnnywu/pi-subagents",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "Sub-agents extension for pi coding agent.",
5
5
  "homepage": "https://github.com/jwu/pi-subagents#readme",
6
6
  "repository": {