@pi-archimedes/subagent 2.7.1 → 2.7.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/README.md CHANGED
@@ -44,7 +44,7 @@ The `agent` field is optional — omitting it is not a built-in default agent, a
44
44
  }
45
45
  ```
46
46
 
47
- **The dispatch waits.** A call — single or parallel — blocks until every task completes and returns the combined results, each optionally carrying the child's `childSessionId`. There is an `async` field in the schema; the implementation ignores it, so don't plan on fire-and-forget: batch the work into `tasks` and let the call block on all of it.
47
+ **The dispatch waits.** A call — single or parallel — blocks until every task completes and returns the combined results, each optionally carrying the child's `childSessionId`. There is an `async` field in the schema; the implementation ignores it, so don't plan on fire-and-forget: batch the work into `tasks` and let the call block on all of it. In parallel mode each result section carries the child's final output beneath its metrics line.
48
48
 
49
49
  ### Model and thinking resolution
50
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-archimedes/subagent",
3
- "version": "2.7.1",
3
+ "version": "2.7.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/danielcherubini/pi-archimedes.git"
@@ -15,8 +15,8 @@
15
15
  ],
16
16
  "main": "./src/index.ts",
17
17
  "dependencies": {
18
- "@pi-archimedes/core": "2.7.1",
19
- "@pi-archimedes/todo": "2.7.1"
18
+ "@pi-archimedes/core": "2.7.3",
19
+ "@pi-archimedes/todo": "2.7.3"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "@earendil-works/pi-ai": ">=0.1.0",
@@ -1,5 +1,21 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { truncLine } from "./format.js";
2
+ import { truncLine, formatParallelResults } from "./format.js";
3
+ import type { SubagentResult } from "./types.js";
4
+
5
+ function makeResult(overrides: Partial<SubagentResult> = {}): SubagentResult {
6
+ return {
7
+ agent: "test-agent",
8
+ task: "do something",
9
+ exitCode: 0,
10
+ usage: { input: 100, output: 50, cacheRead: 0, cacheWrite: 0, cost: 0.01, turns: 3 },
11
+ model: "gpt-4",
12
+ finalOutput: undefined,
13
+ error: undefined,
14
+ progress: undefined,
15
+ progressSummary: { toolCount: 5, tokens: 150, durationMs: 3000 },
16
+ ...overrides,
17
+ };
18
+ }
3
19
 
4
20
  // ── truncLine ───────────────────────────────────────────────────────────────
5
21
 
@@ -28,3 +44,76 @@ describe("truncLine", () => {
28
44
  expect(truncLine("\nhello", 10)).toBe("...");
29
45
  });
30
46
  });
47
+
48
+ describe("formatParallelResults", () => {
49
+ it("renders header + body per task with exactly one blank line between sections", () => {
50
+ const r1 = makeResult({
51
+ agent: "researcher",
52
+ progressSummary: { toolCount: 31, tokens: 12345, durationMs: 153000 },
53
+ finalOutput: "Found the bug.\n", // trailing newline: pins trimEnd
54
+ });
55
+ const r2 = makeResult({
56
+ agent: "reviewer",
57
+ progressSummary: { toolCount: 4, tokens: 2000, durationMs: 12000 },
58
+ finalOutput: "Looks good.",
59
+ });
60
+ expect(formatParallelResults([r1, r2])).toBe(
61
+ "✓ researcher 31 tools · 12k tok · 153s\nFound the bug.\n\n✓ reviewer 4 tools · 2k tok · 12s\nLooks good.",
62
+ );
63
+ });
64
+
65
+ it("rounds tokens and duration with Math.round", () => {
66
+ const r = makeResult({
67
+ progressSummary: { toolCount: 1, tokens: 12500, durationMs: 154000 },
68
+ finalOutput: "x",
69
+ });
70
+ expect(formatParallelResults([r])).toBe("✓ test-agent 1 tools · 13k tok · 154s\nx");
71
+ });
72
+
73
+ it("renders header only when progressSummary is absent (synthetic case)", () => {
74
+ const r = makeResult({ progressSummary: undefined });
75
+ expect(formatParallelResults([r])).toBe("✓ test-agent\ncompleted");
76
+ });
77
+
78
+ it("uses the ✗ glyph for non-zero exit codes", () => {
79
+ const r = makeResult({ exitCode: 1, finalOutput: "done" });
80
+ expect(formatParallelResults([r])).toBe("✗ test-agent 5 tools · 0k tok · 3s\ndone");
81
+ });
82
+
83
+ it("falls back to 'completed' when there is no finalOutput and no error", () => {
84
+ const r = makeResult();
85
+ expect(formatParallelResults([r])).toBe("✓ test-agent 5 tools · 0k tok · 3s\ncompleted");
86
+ });
87
+
88
+ it("uses the error text as the body when there is no finalOutput", () => {
89
+ const r = makeResult({ exitCode: 1, error: "spawn failed" });
90
+ expect(formatParallelResults([r])).toBe("✗ test-agent 5 tools · 0k tok · 3s\nspawn failed");
91
+ });
92
+
93
+ it("prefers finalOutput over error when both are set", () => {
94
+ const r = makeResult({ finalOutput: "out", error: "boom" });
95
+ expect(formatParallelResults([r])).toBe("✓ test-agent 5 tools · 0k tok · 3s\nout");
96
+ });
97
+
98
+ it("keeps internal newlines in a multi-line body and trims only the tail", () => {
99
+ const r1 = makeResult({
100
+ agent: "researcher",
101
+ finalOutput: "line one\nline two\n",
102
+ });
103
+ const r2 = makeResult({
104
+ agent: "reviewer",
105
+ finalOutput: "second",
106
+ });
107
+ expect(formatParallelResults([r1, r2])).toBe(
108
+ "✓ researcher 5 tools · 0k tok · 3s\nline one\nline two\n\n✓ reviewer 5 tools · 0k tok · 3s\nsecond",
109
+ );
110
+ });
111
+
112
+ it("keeps duplicate agent names distinguishable by position and body", () => {
113
+ const a = makeResult({ agent: "researcher", finalOutput: "first" });
114
+ const b = makeResult({ agent: "researcher", finalOutput: "second" });
115
+ expect(formatParallelResults([a, b])).toBe(
116
+ "✓ researcher 5 tools · 0k tok · 3s\nfirst\n\n✓ researcher 5 tools · 0k tok · 3s\nsecond",
117
+ );
118
+ });
119
+ });
package/src/format.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { SubagentResult } from "./types.js";
2
+
1
3
  export function formatTokens(n: number): string {
2
4
  if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M";
3
5
  if (n >= 1_000) return Math.round(n / 1_000) + "k";
@@ -41,6 +43,27 @@ export function truncLine(text: string, maxLen: number): string {
41
43
  return text.slice(0, maxLen - 3) + "...";
42
44
  }
43
45
 
46
+ /**
47
+ * Renders one section per parallel result. The header line is intentionally
48
+ * byte-identical to the pre-#55 metrics summary (a contract, not an
49
+ * accident); the body mirrors the single-mode fallback chain
50
+ * (finalOutput ?? error ?? "completed"), `trimEnd()`-ed so sections stay
51
+ * separated by exactly one blank line, and is deliberately untruncated by
52
+ * design (see docs/decisions/0020-subagent-parallel-full-output.md).
53
+ */
54
+ export function formatParallelResults(results: SubagentResult[]): string {
55
+ const sections = results.map((r) => {
56
+ const status = r.exitCode === 0 ? "✓" : "✗";
57
+ const summary = r.progressSummary
58
+ ? `${r.progressSummary.toolCount} tools · ${Math.round(r.progressSummary.tokens / 1000)}k tok · ${Math.round(r.progressSummary.durationMs / 1000)}s`
59
+ : "";
60
+ const header = `${status} ${r.agent}${summary ? " " + summary : ""}`;
61
+ const body = (r.finalOutput ?? r.error ?? "completed").trimEnd();
62
+ return `${header}\n${body}`;
63
+ });
64
+ return sections.join("\n\n");
65
+ }
66
+
44
67
  export interface StatsData {
45
68
  turns: number | undefined;
46
69
  toolCount: number | undefined;
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ import { Text, type TUI } from "@earendil-works/pi-tui";
3
3
  import { Type } from "typebox";
4
4
  import { SUBAGENT_PARAMS_SCHEMA } from "./tool-schema.js";
5
5
  import { executeSubagent, executeParallel } from "./execute.js";
6
+ import { formatParallelResults } from "./format.js";
6
7
  // agent-manager.js lazy-loaded below to keep subagent tool registration fast
7
8
  import { renderSubagentResult } from "./render.js";
8
9
  import { OVERLAY_CHROME } from "@pi-archimedes/core/overlay";
@@ -110,7 +111,7 @@ export function registerSubagent(pi: ExtensionAPI): void {
110
111
  });
111
112
 
112
113
  return {
113
- content: [{ type: "text", text: formatResultsSummary(results) }],
114
+ content: [{ type: "text", text: formatParallelResults(results) }],
114
115
  details: {
115
116
  mode: "parallel",
116
117
  results,
@@ -281,17 +282,6 @@ function formatProgressSummary(progress: SubagentProgress[]): string {
281
282
  return lines.join("\n");
282
283
  }
283
284
 
284
- function formatResultsSummary(results: SubagentResult[]): string {
285
- const lines = results.map((r) => {
286
- const status = r.exitCode === 0 ? "✓" : "✗";
287
- const summary = r.progressSummary
288
- ? `${r.progressSummary.toolCount} tools · ${Math.round(r.progressSummary.tokens / 1000)}k tok · ${Math.round(r.progressSummary.durationMs / 1000)}s`
289
- : "";
290
- return `${status} ${r.agent}${summary ? " " + summary : ""}`;
291
- });
292
- return lines.join("\n");
293
- }
294
-
295
285
  // ── Command registration ────────────────────────────────────────────────────
296
286
 
297
287
  export function registerAgentsCommand(pi: ExtensionAPI): void {