@narumitw/pi-subagents 0.15.1 → 0.16.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-subagents",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Pi extension for delegating work to specialized isolated subagents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/execution.ts CHANGED
@@ -6,17 +6,19 @@ import {
6
6
  type AgentScope,
7
7
  type SubagentThinkingLevel,
8
8
  } from "./agents.js";
9
+ import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
10
+ import type { SubagentParams } from "./params.js";
9
11
  import {
10
12
  buildFanInContext,
13
+ formatResultFailure,
11
14
  getResultFinalOutput,
15
+ isResultError,
12
16
  mapWithConcurrencyLimit,
13
- runSingleAgent,
14
17
  type OnUpdateCallback,
18
+ runSingleAgent,
15
19
  type SingleResult,
16
20
  type SubagentDetails,
17
21
  } from "./runner.js";
18
- import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
19
- import type { SubagentParams } from "./params.js";
20
22
  import { readSubagentSettings, resolveSubagentThinkingLevel } from "./settings.js";
21
23
 
22
24
  const MAX_PARALLEL_TASKS = 8;
@@ -225,10 +227,9 @@ export async function executeSubagent(
225
227
  );
226
228
  results.push(result);
227
229
 
228
- const isError =
229
- result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
230
+ const isError = isResultError(result);
230
231
  if (isError) {
231
- const errorMsg = result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
232
+ const errorMsg = formatResultFailure(result);
232
233
  return {
233
234
  content: [{ type: "text", text: `Chain stopped at step ${i + 1} (${step.agent}): ${errorMsg}` }],
234
235
  details: { ...makeDetails("chain")(results), isError: true },
@@ -372,14 +373,16 @@ export async function executeSubagent(
372
373
  );
373
374
  }
374
375
 
375
- const successCount = results.filter((r) => r.exitCode === 0).length;
376
- const summaries = results.map((r) => {
377
- const output = getResultFinalOutput(r);
378
- const error = r.errorMessage || r.stderr.trim();
379
- const summaryText = output || error;
380
- const preview = summaryText.slice(0, 160) + (summaryText.length > 160 ? "..." : "");
381
- return `[${r.agent}] ${r.exitCode === 0 ? "completed" : "failed"}: ${preview || "(no output)"}`;
376
+ const successCount = results.filter((result) => !isResultError(result)).length;
377
+ const summaries = results.map((result) => {
378
+ const failed = isResultError(result);
379
+ const output = getResultFinalOutput(result);
380
+ const error = result.errorMessage || result.stderr.trim();
381
+ const summaryText = failed ? formatResultFailure(result) : output || error;
382
+ const preview = truncateUtf8(summaryText, 160).text;
383
+ return `[${result.agent}] ${failed ? "failed" : "completed"}: ${preview || "(no output)"}`;
382
384
  });
385
+ const aggregatorFailed = aggregatorResult ? isResultError(aggregatorResult) : false;
383
386
  const aggregatorOutput = aggregatorResult ? getResultFinalOutput(aggregatorResult) : "";
384
387
  const aggregatorError = aggregatorResult?.errorMessage || aggregatorResult?.stderr.trim() || "";
385
388
  return {
@@ -387,23 +390,19 @@ export async function executeSubagent(
387
390
  {
388
391
  type: "text",
389
392
  text: aggregatorResult
390
- ? aggregatorOutput || aggregatorError || `(aggregator ${aggregatorResult.agent} produced no output)`
393
+ ? aggregatorFailed
394
+ ? formatResultFailure(aggregatorResult)
395
+ : aggregatorOutput ||
396
+ aggregatorError ||
397
+ `(aggregator ${aggregatorResult.agent} produced no output)`
391
398
  : `Parallel: ${successCount}/${results.length} succeeded\n\n${summaries.join("\n\n")}`,
392
399
  },
393
400
  ],
394
401
  details: {
395
402
  ...makeDetails("parallel")(results, aggregatorResult),
396
- isError: aggregatorResult
397
- ? aggregatorResult.exitCode !== 0 ||
398
- aggregatorResult.stopReason === "error" ||
399
- aggregatorResult.stopReason === "aborted"
400
- : false,
403
+ isError: aggregatorFailed,
401
404
  },
402
- isError: aggregatorResult
403
- ? aggregatorResult.exitCode !== 0 ||
404
- aggregatorResult.stopReason === "error" ||
405
- aggregatorResult.stopReason === "aborted"
406
- : undefined,
405
+ isError: aggregatorResult ? aggregatorFailed : undefined,
407
406
  };
408
407
  } finally {
409
408
  status.clear();
@@ -427,9 +426,9 @@ export async function executeSubagent(
427
426
  onUpdate,
428
427
  makeDetails("single"),
429
428
  );
430
- const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
429
+ const isError = isResultError(result);
431
430
  if (isError) {
432
- const errorMsg = result.errorMessage || result.stderr || getResultFinalOutput(result) || "(no output)";
431
+ const errorMsg = formatResultFailure(result);
433
432
  return {
434
433
  content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
435
434
  details: { ...makeDetails("single")([result]), isError: true },
package/src/render.ts CHANGED
@@ -12,6 +12,7 @@ import type { AgentScope, SubagentThinkingLevel } from "./agents.js";
12
12
  import type { SubagentParams } from "./params.js";
13
13
  import {
14
14
  getResultFinalOutput,
15
+ isResultError,
15
16
  type SingleResult,
16
17
  type SubagentDetails,
17
18
  } from "./runner.js";
@@ -221,7 +222,7 @@ export function renderSubagentResult(
221
222
 
222
223
  if (details.mode === "single" && details.results.length === 1) {
223
224
  const r = details.results[0];
224
- const isError = r.exitCode !== 0 || r.stopReason === "error" || r.stopReason === "aborted";
225
+ const isError = isResultError(r);
225
226
  const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
226
227
  const displayItems = getDisplayItems(r.messages);
227
228
  const finalOutput = getResultFinalOutput(r);
@@ -291,7 +292,7 @@ export function renderSubagentResult(
291
292
  };
292
293
 
293
294
  if (details.mode === "chain") {
294
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
295
+ const successCount = details.results.filter((result) => !isResultError(result)).length;
295
296
  const icon = successCount === details.results.length ? theme.fg("success", "✓") : theme.fg("error", "✗");
296
297
 
297
298
  if (expanded) {
@@ -308,7 +309,7 @@ export function renderSubagentResult(
308
309
  );
309
310
 
310
311
  for (const r of details.results) {
311
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
312
+ const rIcon = !isResultError(r) ? theme.fg("success", "✓") : theme.fg("error", "✗");
312
313
  const displayItems = getDisplayItems(r.messages);
313
314
  const finalOutput = getResultFinalOutput(r);
314
315
 
@@ -360,7 +361,7 @@ export function renderSubagentResult(
360
361
  theme.fg("toolTitle", theme.bold("chain ")) +
361
362
  theme.fg("accent", `${successCount}/${details.results.length} steps`);
362
363
  for (const r of details.results) {
363
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
364
+ const rIcon = !isResultError(r) ? theme.fg("success", "✓") : theme.fg("error", "✗");
364
365
  const displayItems = getDisplayItems(r.messages);
365
366
  text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
366
367
  if (displayItems.length === 0) text += `\n${theme.fg("muted", "(no output)")}`;
@@ -373,12 +374,18 @@ export function renderSubagentResult(
373
374
  }
374
375
 
375
376
  if (details.mode === "parallel") {
376
- const running = details.results.filter((r) => r.exitCode === -1).length;
377
- const successCount = details.results.filter((r) => r.exitCode === 0).length;
378
- const failCount = details.results.filter((r) => r.exitCode > 0).length;
377
+ const running = details.results.filter((result) => result.exitCode === -1).length;
378
+ const successCount = details.results.filter(
379
+ (result) => result.exitCode !== -1 && !isResultError(result),
380
+ ).length;
381
+ const failCount = details.results.filter(
382
+ (result) => result.exitCode !== -1 && isResultError(result),
383
+ ).length;
379
384
  const aggregator = details.aggregator;
380
385
  const aggregatorRunning = aggregator?.exitCode === -1;
381
- const aggregatorFailed = aggregator ? aggregator.exitCode > 0 || aggregator.stopReason === "error" : false;
386
+ const aggregatorFailed = aggregator
387
+ ? aggregator.exitCode !== -1 && isResultError(aggregator)
388
+ : false;
382
389
  const isRunning = running > 0 || aggregatorRunning;
383
390
  const icon = isRunning
384
391
  ? theme.fg("warning", "⏳")
@@ -404,7 +411,7 @@ export function renderSubagentResult(
404
411
  );
405
412
 
406
413
  for (const r of details.results) {
407
- const rIcon = r.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
414
+ const rIcon = !isResultError(r) ? theme.fg("success", "✓") : theme.fg("error", "✗");
408
415
  const displayItems = getDisplayItems(r.messages);
409
416
  const finalOutput = getResultFinalOutput(r);
410
417
 
@@ -438,7 +445,9 @@ export function renderSubagentResult(
438
445
  }
439
446
 
440
447
  if (aggregator) {
441
- const rIcon = aggregator.exitCode === 0 ? theme.fg("success", "✓") : theme.fg("error", "✗");
448
+ const rIcon = !isResultError(aggregator)
449
+ ? theme.fg("success", "✓")
450
+ : theme.fg("error", "✗");
442
451
  const displayItems = getDisplayItems(aggregator.messages);
443
452
  const finalOutput = getResultFinalOutput(aggregator);
444
453
 
@@ -485,7 +494,7 @@ export function renderSubagentResult(
485
494
  const rIcon =
486
495
  r.exitCode === -1
487
496
  ? theme.fg("warning", "⏳")
488
- : r.exitCode === 0
497
+ : !isResultError(r)
489
498
  ? theme.fg("success", "✓")
490
499
  : theme.fg("error", "✗");
491
500
  const displayItems = getDisplayItems(r.messages);
@@ -498,7 +507,7 @@ export function renderSubagentResult(
498
507
  const rIcon =
499
508
  aggregator.exitCode === -1
500
509
  ? theme.fg("warning", "⏳")
501
- : aggregator.exitCode === 0
510
+ : !isResultError(aggregator)
502
511
  ? theme.fg("success", "✓")
503
512
  : theme.fg("error", "✗");
504
513
  const displayItems = getDisplayItems(aggregator.messages);
package/src/runner.ts CHANGED
@@ -72,9 +72,11 @@ function getFinalOutput(messages: Message[]): string {
72
72
  for (let i = messages.length - 1; i >= 0; i--) {
73
73
  const msg = messages[i];
74
74
  if (msg.role === "assistant") {
75
- for (const part of msg.content) {
76
- if (part.type === "text") return part.text;
77
- }
75
+ const text = msg.content
76
+ .filter((part) => part.type === "text")
77
+ .map((part) => part.text)
78
+ .join("\n");
79
+ if (text) return text;
78
80
  }
79
81
  }
80
82
  return "";
@@ -84,6 +86,22 @@ export function getResultFinalOutput(result: SingleResult): string {
84
86
  return result.finalOutput ?? getFinalOutput(result.messages);
85
87
  }
86
88
 
89
+ export function isResultError(result: SingleResult): boolean {
90
+ return (
91
+ (result.exitCode !== 0 && result.exitCode !== -1) ||
92
+ result.stopReason === "error" ||
93
+ result.stopReason === "aborted"
94
+ );
95
+ }
96
+
97
+ export function formatResultFailure(result: SingleResult): string {
98
+ const error = result.errorMessage || result.stderr.trim();
99
+ const output = getResultFinalOutput(result);
100
+ const combined =
101
+ error && output ? `${error}\n\nPartial output:\n${output}` : error || output || "(no output)";
102
+ return truncateUtf8(combined, DEFAULT_MAX_CONTEXT_BYTES).text;
103
+ }
104
+
87
105
  function boundMessageText(message: Message, maxBytes: number): { message: Message; bytes: number; truncated: boolean } {
88
106
  const serializedBytes = Buffer.byteLength(JSON.stringify(message), "utf8");
89
107
  if (serializedBytes <= maxBytes) return { message, bytes: serializedBytes, truncated: false };
@@ -107,13 +125,19 @@ function boundMessageText(message: Message, maxBytes: number): { message: Messag
107
125
  export function buildFanInContext(results: SingleResult[], maxBytes = DEFAULT_MAX_CONTEXT_BYTES): string {
108
126
  const text = results
109
127
  .map((result, index) => {
110
- const status = result.exitCode === 0 ? "completed" : result.exitCode === -1 ? "running" : "failed";
128
+ const failed = isResultError(result);
129
+ const status = result.exitCode === -1 ? "running" : failed ? "failed" : "completed";
111
130
  const output = getResultFinalOutput(result);
112
131
  const error = result.errorMessage || result.stderr.trim();
132
+ const resultText = failed
133
+ ? `${error ? "Error" : output ? "Partial output" : "Error"}:\n${formatResultFailure(result)}`
134
+ : output
135
+ ? `Output:\n${output}`
136
+ : "Output: (no output)";
113
137
  return [
114
138
  `## Result ${index + 1}: ${result.agent} (${status})`,
115
139
  `Task: ${result.task}`,
116
- output ? `Output:\n${output}` : error ? `Error:\n${error}` : "Output: (no output)",
140
+ resultText,
117
141
  ].join("\n\n");
118
142
  })
119
143
  .join("\n\n---\n\n");
@@ -261,6 +285,9 @@ export async function runSingleAgent(
261
285
  let tmpPromptDir: string | null = null;
262
286
  let tmpPromptPath: string | null = null;
263
287
 
288
+ let latestAssistantOutput = "";
289
+ let terminalAssistantOutput: string | undefined;
290
+
264
291
  const currentResult: SingleResult = {
265
292
  agent: agentName,
266
293
  agentSource: agent.source,
@@ -274,9 +301,21 @@ export async function runSingleAgent(
274
301
  step,
275
302
  timeoutMs,
276
303
  };
304
+ const selectedAssistantOutput = () =>
305
+ terminalAssistantOutput !== undefined
306
+ ? terminalAssistantOutput
307
+ : latestAssistantOutput || getFinalOutput(currentResult.messages);
308
+ const setErrorMessage = (message: string) => {
309
+ const bounded = truncateUtf8(message, DEFAULT_MAX_STDERR_BYTES);
310
+ currentResult.errorMessage = bounded.text;
311
+ currentResult.truncated ||= bounded.truncated;
312
+ return bounded.text;
313
+ };
277
314
 
278
315
  const emitUpdate = () => {
279
- currentResult.finalOutput = getFinalOutput(currentResult.messages);
316
+ const latest = truncateUtf8(selectedAssistantOutput(), DEFAULT_MAX_OUTPUT_BYTES);
317
+ currentResult.finalOutput = latest.text;
318
+ currentResult.truncated ||= latest.truncated;
280
319
  if (onUpdate) {
281
320
  onUpdate({
282
321
  content: [{ type: "text", text: currentResult.finalOutput || "(running...)" }],
@@ -293,8 +332,7 @@ export async function runSingleAgent(
293
332
  currentResult.exitCode = 1;
294
333
  currentResult.stopReason = "error";
295
334
  const reason = error instanceof Error ? error.message : String(error);
296
- currentResult.errorMessage = `Invalid subagent cwd: ${effectiveCwd} (${reason})`;
297
- currentResult.stderr = currentResult.errorMessage;
335
+ currentResult.stderr = setErrorMessage(`Invalid subagent cwd: ${effectiveCwd} (${reason})`);
298
336
  return currentResult;
299
337
  }
300
338
 
@@ -302,7 +340,7 @@ export async function runSingleAgent(
302
340
  currentResult.exitCode = 130;
303
341
  currentResult.aborted = true;
304
342
  currentResult.stopReason = "aborted";
305
- currentResult.errorMessage = "Subagent was aborted before start";
343
+ setErrorMessage("Subagent was aborted before start");
306
344
  return currentResult;
307
345
  }
308
346
 
@@ -356,8 +394,7 @@ export async function runSingleAgent(
356
394
  },
357
395
  });
358
396
  } catch (error) {
359
- currentResult.errorMessage = error instanceof Error ? error.message : String(error);
360
- currentResult.stderr = currentResult.errorMessage;
397
+ currentResult.stderr = setErrorMessage(error instanceof Error ? error.message : String(error));
361
398
  finish(1);
362
399
  return;
363
400
  }
@@ -379,6 +416,14 @@ export async function runSingleAgent(
379
416
  const event = raw as { type?: string; message?: Message };
380
417
  if (event.type === "message_end" && event.message) {
381
418
  const msg = event.message;
419
+ if (msg.role === "assistant") {
420
+ const output = truncateUtf8(getFinalOutput([msg]), DEFAULT_MAX_OUTPUT_BYTES);
421
+ currentResult.truncated ||= output.truncated;
422
+ if (output.text) latestAssistantOutput = output.text;
423
+ if (msg.stopReason === "stop" || msg.stopReason === "length") {
424
+ terminalAssistantOutput = output.text;
425
+ }
426
+ }
382
427
  addMessage(msg);
383
428
  if (msg.role === "assistant") {
384
429
  currentResult.usage.turns++;
@@ -393,7 +438,7 @@ export async function runSingleAgent(
393
438
  }
394
439
  if (!currentResult.model && msg.model) currentResult.model = msg.model;
395
440
  if (msg.stopReason) currentResult.stopReason = msg.stopReason;
396
- if (msg.errorMessage) currentResult.errorMessage = msg.errorMessage;
441
+ if (msg.errorMessage) setErrorMessage(msg.errorMessage);
397
442
  }
398
443
  emitUpdate();
399
444
  } else if (event.type === "tool_result_end" && event.message) {
@@ -415,7 +460,7 @@ export async function runSingleAgent(
415
460
  timedOut = true;
416
461
  currentResult.timedOut = true;
417
462
  currentResult.stopReason = "timeout";
418
- currentResult.errorMessage = `Subagent timed out after ${timeoutMs}ms`;
463
+ setErrorMessage(`Subagent timed out after ${timeoutMs}ms`);
419
464
  const bounded = appendBounded(
420
465
  currentResult.stderr,
421
466
  `\nSubagent timed out after ${timeoutMs}ms.`,
@@ -439,10 +484,10 @@ export async function runSingleAgent(
439
484
  finish(timedOut ? 124 : wasAborted ? 130 : (code ?? 0));
440
485
  });
441
486
  proc.on("error", (error) => {
442
- currentResult.errorMessage = error.message;
487
+ const message = setErrorMessage(error.message);
443
488
  const bounded = appendBounded(
444
489
  currentResult.stderr,
445
- `${currentResult.stderr ? "\n" : ""}${error.message}`,
490
+ `${currentResult.stderr ? "\n" : ""}${message}`,
446
491
  DEFAULT_MAX_STDERR_BYTES,
447
492
  );
448
493
  currentResult.stderr = bounded.text;
@@ -456,7 +501,7 @@ export async function runSingleAgent(
456
501
  wasAborted = true;
457
502
  currentResult.aborted = true;
458
503
  currentResult.stopReason = "aborted";
459
- currentResult.errorMessage = "Subagent was aborted";
504
+ setErrorMessage("Subagent was aborted");
460
505
  cleanupTermination = terminateProcess(proc);
461
506
  };
462
507
  if (signal.aborted) abortHandler();
@@ -465,9 +510,18 @@ export async function runSingleAgent(
465
510
  });
466
511
 
467
512
  currentResult.exitCode = exitCode;
468
- const final = truncateUtf8(getFinalOutput(currentResult.messages), DEFAULT_MAX_OUTPUT_BYTES);
513
+ const final = truncateUtf8(selectedAssistantOutput(), DEFAULT_MAX_OUTPUT_BYTES);
469
514
  currentResult.finalOutput = final.text;
470
515
  currentResult.truncated ||= final.truncated;
516
+ if (
517
+ currentResult.exitCode === 0 &&
518
+ currentResult.stopReason !== "error" &&
519
+ (currentResult.stopReason === "toolUse" || !currentResult.finalOutput.trim())
520
+ ) {
521
+ currentResult.exitCode = 1;
522
+ currentResult.stopReason = "error";
523
+ setErrorMessage("Subagent completed without final text");
524
+ }
471
525
  currentResult.policy = {
472
526
  inherited: ["environment"],
473
527
  overridden: [