pi-subagents 0.39.0 → 0.40.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/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.40.0] - 2026-08-01
6
+
7
+ ### Added
8
+ - Documented an optional recommended model-tiering setup in the README: fast workhorse, standard well-scoped, deep-but-bounded, and taste/intent tiers, with cross-provider `fallbackModels` guidance for usage-limit resilience.
9
+ - Added `description` to `subagents.agentOverrides` so deployments can replace the discovered description for builtin and custom agents in list output. Thanks to @chronoAP for #724.
10
+
11
+ ### Changed
12
+ - Refreshed the bundled `pi-subagents` skill for the 0.39 surface: Fleet inspector live controls (`s` steer, `D` stop), the recommended model-tiering recipe, `agentOverrides.description`, `projectRootResolution: "git-root"`, running-card live-detail/model badges, and the newer extension RPC capability projections (`fleetStatus`, `launchResolvedExtensions`, `runtimeAcknowledgedExtensions`, `(runId, index)` correlation). Corrected the stale README "inspection-only" fleet inspector wording.
13
+
14
+ ### Fixed
15
+ - Grouped intercom results now report child process status separately from provenance-aware output availability, including salvage guidance when a failed process produced output. Thanks to @youlikemodernart for #727.
16
+ - Collapsed running foreground subagent rows now show the model and thinking level: single-result cards include the effective thinking suffix and parallel/chain rows show the per-child model badge, matching the async widget.
17
+
5
18
  ## [0.39.0] - 2026-08-01
6
19
 
7
20
  ### Added
package/README.md CHANGED
@@ -155,6 +155,31 @@ For a persistent override, edit settings. This example pins the reviewer everywh
155
155
  }
156
156
  ```
157
157
 
158
+ ### Recommended model tiering (optional)
159
+
160
+ A setup that works well in practice is routing agents by task shape instead of running everything on one model. Four tiers:
161
+
162
+ 1. **Fast workhorse** — the cheapest capable model at low thinking, for recon, lookups, and mechanical edits. Example: `openai-codex/gpt-5.6-luna:low` on `scout`.
163
+ 2. **Standard well-scoped** — a mid-tier model at medium thinking, for most delegations: routine multi-file edits, focused reviews, straightforward implementation. Example: `openai-codex/gpt-5.6-terra:medium` on `worker`, `reviewer`, and a lightweight `delegate` agent.
164
+ 3. **Deep but bounded** — a top reasoning model at high thinking, only for hard tasks that arrive with explicit goals and completion criteria. These models tend to loop on vague goals, so keep them off open-ended work. Example: `openai-codex/gpt-5.6-sol:high` on `planner` and oracle-style agents.
165
+ 4. **Taste and intent** — a model that reads human intent well and makes judgment calls without looping, for ambiguous work: UX and design decisions, product tradeoffs, planning from vague requirements, writing quality. Example: `anthropic/claude-fable-5` at `low` for lighter passes and `medium` for harder ones.
166
+
167
+ The routing rule: use the capability tiers (1–3) when the task is well-scoped, and the intent tier (4) when scoping or judging is the task itself.
168
+
169
+ Give tier-4 agents cross-provider `fallbackModels` so subscription usage limits degrade gracefully instead of failing the run — fallback triggers on rate-limit and overload errors automatically:
170
+
171
+ ```yaml
172
+ ---
173
+ name: shaper
174
+ description: Open-ended design/UX/product/planning agent for ambiguous tasks
175
+ model: anthropic/claude-fable-5
176
+ thinking: medium
177
+ fallbackModels: openai-codex/gpt-5.5:high
178
+ ---
179
+ ```
180
+
181
+ One more interaction worth knowing for tier 4: forked context over an Anthropic parent transcript with signed thinking blocks forces the child's thinking off, so intent-tier agents work best with fresh context.
182
+
158
183
  Use `~/.pi/agent/settings.json` for a user override or the project config settings file (`.pi/settings.json` in standard Pi) for a project override. `subagents.defaultModel` applies to builtin, package, user, and project agents that do not set `model` in frontmatter. Per-run model overrides and `agentOverrides.<name>.model` still win, and explicit agent frontmatter still wins over the global default. The same `agentOverrides` block can change `tools`, `skills`, inherited context, prompt text, or disable a builtin. Matching user and project agents also receive override fields that their frontmatter leaves unset, so a shared project config agent can keep the persona while local settings choose the model.
159
184
 
160
185
  By default, project settings resolve from the nearest parent directory that contains `.pi` or `.agents`, preserving existing nested-project behavior. In monorepos or git worktrees where an incidental nested `.pi` directory should not shadow the repository-level config, set this in the repository root `.pi/settings.json`:
@@ -319,7 +344,7 @@ Foreground runs stream progress in the conversation while they run. They default
319
344
 
320
345
  Background runs keep working after control returns to you. Inspect active runs with `subagent({ action: "status" })`, or a specific run with `subagent({ action: "status", id: "..." })`. In the TUI, a persistent FleetView below the editor by default shows `main` plus active children with task, elapsed time, and token totals. Set `fleetViewPlacement` to `"aboveEditor"` to move it above the editor. When the focused editor is empty, press `↓` or `←` to activate FleetView, then use `↑`/`↓` or `j`/`k` to select a child and `Enter` to inspect it; printable navigation keys are never intercepted before activation.
321
346
 
322
- `/subagents-fleet` opens the live, inspection-only fleet inspector with current-session foreground work, recent async children, structured Markdown/tool transcripts, and completed output/session paths. Use `↑`/`↓` or `j`/`k` to select a child, `Shift+K`/`Shift+J` to scroll one line, `PgUp`/`PgDn` to scroll one page, `x`/`Ctrl+O` to toggle tool details, `r` to refresh, and `Esc` to close. `Ctrl+Alt+F` opens the same inspector even while a foreground turn is active and slash input is queued. Without a TUI, `/subagents-fleet` retains the textual `subagent({ action: "status", view: "fleet" })` fallback. Mutations stay in explicit commands: run `/subagents-stop` and pick from the selector, or use `/subagents-stop <run-id>` / `subagent({ action: "stop", id: "..." })` when you already know the id. Use `/subagents-detach [run-id]` only for an active foreground single-subagent run you want to leave running without terminating; the eventual result remains available through status/wait. To inspect one background child in text, use `subagent({ action: "status", id: "...", view: "transcript" })`; add `index` for a specific child in a parallel or chain run.
347
+ `/subagents-fleet` opens the live fleet inspector with current-session foreground work, recent async children, structured Markdown/tool transcripts, and completed output/session paths. Use `↑`/`↓` or `j`/`k` to select a child, `Shift+K`/`Shift+J` to scroll one line, `PgUp`/`PgDn` to scroll one page, `x`/`Ctrl+O` to toggle tool details, `r` to refresh, and `Esc` to close. For a selected live async child, `s` sends an acknowledged steer message and `D` stops its top-level async run after confirmation. `Ctrl+Alt+F` opens the same inspector even while a foreground turn is active and slash input is queued. Without a TUI, `/subagents-fleet` retains the textual `subagent({ action: "status", view: "fleet" })` fallback, and mutations use explicit commands: run `/subagents-stop` and pick from the selector, or use `/subagents-stop <run-id>` / `subagent({ action: "stop", id: "..." })` when you already know the id. Use `/subagents-detach [run-id]` only for an active foreground single-subagent run you want to leave running without terminating; the eventual result remains available through status/wait. To inspect one background child in text, use `subagent({ action: "status", id: "...", view: "transcript" })`; add `index` for a specific child in a parallel or chain run.
323
348
 
324
349
  FleetView replaces the legacy above-editor async widget by default, while completion notifications remain enabled. Parallel runs show every active child independently. Chains with parallel groups keep their grouped shape in progress and results, so failed or paused agents stay visible next to completed ones. When a child is explicitly allowed to fan out with `tools: subagent`, its nested runs appear under that parent child in the main status tree instead of being hidden inside the child process.
325
350
 
@@ -736,6 +761,7 @@ Example:
736
761
  "subagents": {
737
762
  "agentOverrides": {
738
763
  "reviewer": {
764
+ "description": "Independent review tier",
739
765
  "inheritProjectContext": false
740
766
  }
741
767
  }
@@ -743,7 +769,7 @@ Example:
743
769
  }
744
770
  ```
745
771
 
746
- Supported override fields are `model`, `fallbackModels`, `thinking`, `systemPromptMode`, `inheritProjectContext`, `inheritSkills`, `defaultContext`, `acceptanceRole`, `disabled`, `skills`, `tools`, and `systemPrompt`. Use `defaultContext: false` or `acceptanceRole: false` to clear an inherited override. Project overrides beat user overrides.
772
+ Supported override fields are `description`, `model`, `fallbackModels`, `thinking`, `systemPromptMode`, `inheritProjectContext`, `inheritSkills`, `defaultContext`, `acceptanceRole`, `disabled`, `skills`, `tools`, and `systemPrompt`. `description` replaces the discovered description for builtin and custom agents, which lets list output show deployment-specific routing or model metadata. Use `defaultContext: false` or `acceptanceRole: false` to clear an inherited override. Project overrides beat user overrides.
747
773
 
748
774
  Set `subagents.defaultModel` to give all subagents without an explicit model their own default model, separate from the parent session model. Per-agent model overrides and agent frontmatter still win.
749
775
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -20,6 +20,8 @@ Precedence is by parsed runtime name:
20
20
  2. user scope
21
21
  3. builtin agents
22
22
 
23
+ Project settings resolve from the nearest parent directory containing `.pi` or `.agents` by default. In monorepos or git worktrees where an incidental nested `.pi` directory should not shadow the repository config, set `subagents.projectRootResolution: "git-root"` in the repository root `.pi/settings.json`; a nested project can opt back with `"nearest"` in its own settings.
24
+
23
25
  ## Running Subagents
24
26
 
25
27
  ### Single agent
@@ -149,6 +151,8 @@ const run = subagent({
149
151
  // Continue local inspection, then later call status with the returned id.
150
152
  ```
151
153
 
154
+ While children run, the persistent FleetView and the collapsed foreground tool-result card show live per-child detail: resolved model and thinking level, `[fresh]`/`[fork]` context, tool/token/elapsed counters, and current activity. The collapsed running card also prints the configured expand-key hint ("Press … for live detail"); expanding it shows nested children, recent tools, and recent output. Model badges appear once the child's model resolves at first attempt start. `/subagents-fleet` opens the live fleet inspector, which also has per-child controls (`s` steer, `D` stop with confirmation).
155
+
152
156
  Inspect async runs with `subagent({ action: "status", id: "..." })` or `subagent({ action: "status" })` for active runs. Use `subagent({ action: "status", view: "fleet" })` when supervising several active foreground/background runs and `subagent({ action: "status", id: "...", view: "transcript", index: 0 })` when you need the latest child output without digging through artifacts. If a delegated fanout child launches nested runs, the parent status view shows them as a tree and you can target a nested run directly with its nested id.
153
157
 
154
158
  Stop a current-session top-level async run with `stop` (or `/subagents-stop`). Stopped runs finish as `stopped`/cancelled and are not resumable. For an active foreground single-subagent run, `/subagents-detach [run-id]` leaves the child running without terminating it and returns the eventual result through status/wait. Append one more step to the tail of a still-running async chain with `append-step` (`chain` must contain exactly one step). Use checkpoint steps for planned human gates; they pause without launching a child and are approved or rejected through current-session control actions:
@@ -141,4 +141,4 @@ Additional user prompt templates can delegate into `pi-subagents` through the na
141
141
 
142
142
  Other Pi extensions can call `pi-subagents` through the in-process event bus. The stable v1 channels are `subagents:rpc:v1:ready`, `subagents:rpc:v1:request`, and per-request replies at `subagents:rpc:v1:reply:<requestId>`. Envelopes use `{ version: 1, requestId, method, params }`, and replies use `{ version: 1, requestId, success, data | error }`. `ping` advertises the exact process-local async completion event as `events.asyncComplete` for RPC-spawn consumers.
143
143
 
144
- Methods: `ping`, `status`, `spawn`, `steer`, `interrupt`, `resume`, and `stop`. `spawn` is async-only and rejects management actions, `async: false`, or `clarify: true`; it reuses the normal executor, so discovery, validation, session attribution, configured spawn caps, child-safety depth, artifacts, and async status are shared with the `subagent` tool. `status`, acknowledged async `steer`, and `interrupt` map to the normal control actions. RPC steer disables pause-and-revive recovery and advertises `capabilities.nonRecoveringSteer`, preserving the caller's authority over the exact spawned child. `resume` requires a target plus non-empty message and delegates to the package-owned revival path; it may set a caller-owned `file-only` output path but cannot override the persisted child model, tools, budgets, session ownership, or exclusive session lease. `stop` targets running async runs through the existing timeout control channel. `pi.events` is process-local, so separate Pi processes and child subagents need lifecycle artifact files or `pi-intercom` instead.
144
+ Methods: `ping`, `status`, `spawn`, `steer`, `interrupt`, `resume`, and `stop`. `ping` capability metadata advertises optional projections: `capabilities.fleetStatus: { version: 1 }` adds bounded current-session `data.fleet` records (opaque reconciliation `key`, resolved `agent`, optional `role`, `model`, `effort`, caller-facing `goal`, `startedAt`, split `{ input, output, total }` tokens, plus `totalActive`/`omitted` overflow counts) to successful `status` replies; `capabilities.launchResolvedExtensions` advertises parent-resolved opaque launch-extension identifiers in status details; `capabilities.runtimeAcknowledgedExtensions` advertises the best-effort child-runtime acknowledgement projection fed by cooperating extensions emitting `subagent:acknowledge-extension`. Foreground `details.results[]` rows carry a stable numeric `index`; correlate children by `(runId, index)` rather than row position. Consumers should read status/result artifacts and RPC projections instead of scraping terminal output and must ignore unknown fields. `spawn` is async-only and rejects management actions, `async: false`, or `clarify: true`; it reuses the normal executor, so discovery, validation, session attribution, configured spawn caps, child-safety depth, artifacts, and async status are shared with the `subagent` tool. `status`, acknowledged async `steer`, and `interrupt` map to the normal control actions. RPC steer disables pause-and-revive recovery and advertises `capabilities.nonRecoveringSteer`, preserving the caller's authority over the exact spawned child. `resume` requires a target plus non-empty message and delegates to the package-owned revival path; it may set a caller-owned `file-only` output path but cannot override the persisted child model, tools, budgets, session ownership, or exclusive session lease. `stop` targets running async runs through the existing timeout control channel. `pi.events` is process-local, so separate Pi processes and child subagents need lifecycle artifact files or `pi-intercom` instead.
@@ -31,7 +31,7 @@ Humans often use the slash-command layer instead:
31
31
  - `/subagents-stop [run-id]` — stop a current-session top-level async run; opens a selector when no id is given
32
32
  - `/subagents-detach [run-id]` — detach an active foreground single-subagent run without terminating its child
33
33
  - `/subagent-cost` — show parent plus child token usage and cost for the session
34
- - `/subagents-fleet` — open the live, inspection-only foreground/async fleet; `Ctrl+Alt+F` opens it during an active foreground turn, `↑↓`/`jk` selects children, and `PgUp`/`PgDn` scrolls transcript detail
34
+ - `/subagents-fleet` — open the live fleet inspector with per-child controls; `Ctrl+Alt+F` opens it during an active foreground turn, `↑↓`/`jk` selects children, `PgUp`/`PgDn` scrolls transcript detail, `s` steers the selected live async child, and `D` stops its top-level async run after confirmation
35
35
  - `/subagents-watchdog` — inspect or configure the opt-in adversarial change watchdog (model, on/off, recommend-model, check)
36
36
  - `/subagents-doctor` — diagnose setup, discovery, async paths, and intercom bridge state
37
37
  - `/subagents-models [agent]` — show the live runtime-loaded builtin model mapping
@@ -253,12 +253,25 @@ Direct settings example:
253
253
  }
254
254
  ```
255
255
 
256
- Useful override fields: `model`, `fallbackModels`, `thinking`,
256
+ Useful override fields: `description`, `model`, `fallbackModels`, `thinking`,
257
257
  `systemPromptMode`, `inheritProjectContext`, `inheritSkills`, `defaultContext`,
258
258
  `acceptanceRole`, `disabled`, `skills`, `tools`, `extensions`, and `systemPrompt`.
259
+ `description` replaces the discovered description for builtin and custom agents
260
+ in `list` output, which is useful for deployment-specific routing notes.
259
261
  Use `acceptanceRole: false` to clear an override. Create a user or project
260
262
  agent with the same name only when you want a substantially different agent.
261
263
 
264
+ ### Recommended model tiering (optional)
265
+
266
+ When several providers are available, route agents by task shape instead of one model for everything:
267
+
268
+ 1. **Fast workhorse** — cheapest capable model at low thinking for recon, lookups, and mechanical edits (for example on `scout`).
269
+ 2. **Standard well-scoped** — mid-tier model at medium thinking for most delegations: routine multi-file edits, focused reviews, straightforward implementation (for example on `worker`, `reviewer`, `delegate`).
270
+ 3. **Deep but bounded** — top reasoning model at high thinking only for hard tasks that arrive with explicit goals and completion criteria; these models loop on vague goals (for example on `planner` and oracle-style agents).
271
+ 4. **Taste and intent** — a model that reads human intent well for ambiguous work: UX/design judgment, product tradeoffs, planning from vague requirements, writing quality.
272
+
273
+ Routing rule: use tiers 1–3 when the task is well-scoped; use tier 4 when scoping or judging is the task itself. Give tier-4 agents cross-provider `fallbackModels` so subscription usage limits degrade gracefully; fallback triggers automatically on rate-limit and overload errors. Note that forked context over an Anthropic parent transcript with signed thinking blocks forces the child's thinking off, so intent-tier agents work best with fresh context.
274
+
262
275
  If a provider rejects model IDs with thinking suffixes, use
263
276
  `subagents.disableThinking: true` in user or project settings to clear bundled
264
277
  builtin thinking defaults globally. A higher-precedence per-agent `thinking`
@@ -59,6 +59,7 @@ export function defaultInheritSkills(): boolean {
59
59
  }
60
60
 
61
61
  export interface BuiltinAgentOverrideBase {
62
+ description?: string;
62
63
  model?: string;
63
64
  fallbackModels?: string[];
64
65
  thinking?: string | false;
@@ -80,6 +81,7 @@ export interface BuiltinAgentOverrideBase {
80
81
  }
81
82
 
82
83
  interface BuiltinAgentOverrideConfig {
84
+ description?: string;
83
85
  model?: string | false;
84
86
  fallbackModels?: string[] | false;
85
87
  thinking?: string | false;
@@ -545,6 +547,7 @@ function arraysEqual(a: string[] | undefined, b: string[] | undefined): boolean
545
547
 
546
548
  function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
547
549
  return {
550
+ description: agent.description,
548
551
  model: agent.model,
549
552
  fallbackModels: agent.fallbackModels ? [...agent.fallbackModels] : undefined,
550
553
  thinking: agent.thinking,
@@ -568,6 +571,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
568
571
 
569
572
  function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentOverrideConfig {
570
573
  return {
574
+ ...(override.description !== undefined ? { description: override.description } : {}),
571
575
  ...(override.model !== undefined ? { model: override.model } : {}),
572
576
  ...(override.fallbackModels !== undefined
573
577
  ? { fallbackModels: override.fallbackModels === false ? false : [...override.fallbackModels] }
@@ -720,6 +724,14 @@ function parseBuiltinOverrideEntry(
720
724
  const input = value as Record<string, unknown>;
721
725
  const override: BuiltinAgentOverrideConfig = {};
722
726
 
727
+ if ("description" in input) {
728
+ if (typeof input.description === "string" && input.description.trim()) {
729
+ override.description = input.description.trim();
730
+ } else {
731
+ throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'description'; expected a non-empty string.`);
732
+ }
733
+ }
734
+
723
735
  if ("model" in input) {
724
736
  if (typeof input.model === "string" || input.model === false) override.model = input.model;
725
737
  else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'model'; expected a string or false.`);
@@ -967,6 +979,7 @@ function applyBuiltinOverride(
967
979
  override: { ...meta, base: cloneOverrideBase(agent) },
968
980
  };
969
981
 
982
+ if (override.description !== undefined) next.description = override.description;
970
983
  if (override.model !== undefined) next.model = override.model === false ? undefined : override.model;
971
984
  if (override.fallbackModels !== undefined) {
972
985
  next.fallbackModels = override.fallbackModels === false ? undefined : [...override.fallbackModels];
@@ -1087,6 +1100,10 @@ function applyCustomAgentOverride(
1087
1100
  anyFilled = true;
1088
1101
  };
1089
1102
 
1103
+ if (override.description !== undefined) {
1104
+ mutable().description = override.description;
1105
+ anyFilled = true;
1106
+ }
1090
1107
  if (override.model !== undefined) {
1091
1108
  fill("model", ["model"], override.model === false ? undefined : override.model);
1092
1109
  }
@@ -1177,10 +1194,14 @@ function applyCustomAgentOverrides(
1177
1194
 
1178
1195
  export function buildBuiltinOverrideConfig(
1179
1196
  base: BuiltinAgentOverrideBase,
1180
- draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget">,
1197
+ draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "defaultContext" | "acceptanceRole" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools" | "extensions" | "subagentOnlyExtensions" | "completionGuard" | "toolBudget"> & Partial<Pick<AgentConfig, "description">>,
1181
1198
  ): BuiltinAgentOverrideConfig | undefined {
1182
1199
  const override: BuiltinAgentOverrideConfig = {};
1183
1200
 
1201
+ if (draft.description !== undefined) {
1202
+ const description = draft.description.trim();
1203
+ if (description && description !== base.description) override.description = description;
1204
+ }
1184
1205
  if (draft.model !== base.model) override.model = draft.model ?? false;
1185
1206
  if (!arraysEqual(draft.fallbackModels, base.fallbackModels)) override.fallbackModels = draft.fallbackModels ? [...draft.fallbackModels] : false;
1186
1207
  if (draft.thinking !== base.thinking) override.thinking = draft.thinking ?? false;
@@ -11,6 +11,7 @@ import {
11
11
  type SubagentResultIntercomChild,
12
12
  type SubagentResultIntercomPayload,
13
13
  type SubagentResultStatus,
14
+ type SubagentOutputState,
14
15
  type SubagentRunMode,
15
16
  SUBAGENT_RESULT_INTERCOM_DELIVERY_EVENT,
16
17
  SUBAGENT_RESULT_INTERCOM_EVENT,
@@ -64,6 +65,21 @@ function formatStatusCounts(counts: Record<SubagentResultStatus, number>): strin
64
65
  return parts.length ? parts.join(", ") : "0 results";
65
66
  }
66
67
 
68
+ function countOutputStates(children: SubagentResultIntercomChild[]): Record<SubagentOutputState, number> {
69
+ const counts: Record<SubagentOutputState, number> = { present: 0, absent: 0, unknown: 0 };
70
+ for (const child of children) counts[child.outputState ?? "unknown"] += 1;
71
+ return counts;
72
+ }
73
+
74
+ function formatOutputCounts(counts: Record<SubagentOutputState, number>): string {
75
+ const parts = [
76
+ counts.present ? `${counts.present} present` : undefined,
77
+ counts.absent ? `${counts.absent} absent` : undefined,
78
+ counts.unknown ? `${counts.unknown} unknown` : undefined,
79
+ ].filter((part): part is string => Boolean(part));
80
+ return parts.length ? parts.join(", ") : "0 outputs";
81
+ }
82
+
67
83
  function resolveGroupedStatus(children: SubagentResultIntercomChild[]): SubagentResultStatus {
68
84
  const counts = countStatuses(children);
69
85
  if (counts.failed > 0) return "failed";
@@ -219,14 +235,19 @@ function formatSubagentResultIntercomMessage(input: {
219
235
  parallelHandoff?: ParallelHandoffReference;
220
236
  }): string {
221
237
  const counts = countStatuses(input.children);
238
+ const outputCounts = countOutputStates(input.children);
222
239
  const lines: string[] = [
223
240
  "subagent results",
224
241
  "",
225
242
  `Run: ${input.runId}`,
226
243
  `Mode: ${input.mode}`,
227
- `Status: ${input.status}`,
244
+ `Process status: ${input.status}`,
228
245
  `Children: ${formatStatusCounts(counts)}`,
246
+ `Outputs: ${formatOutputCounts(outputCounts)} (semantic adequacy unassessed)`,
229
247
  ];
248
+ if (input.children.some((child) => child.status === "failed" && child.outputState === "present")) {
249
+ lines.push("Recovery: At least one failed process produced output. Inspect that output before retrying; output presence does not establish task completion.");
250
+ }
230
251
  if (input.mode === "chain" && typeof input.chainSteps === "number") {
231
252
  lines.push(`Chain steps: ${input.chainSteps}`);
232
253
  }
@@ -245,7 +266,7 @@ function formatSubagentResultIntercomMessage(input: {
245
266
  for (let index = 0; index < input.children.length; index++) {
246
267
  const child = input.children[index]!;
247
268
  lines.push("");
248
- lines.push(`${index + 1}. ${child.agent} — ${child.status}`);
269
+ lines.push(`${index + 1}. ${child.agent} — process ${child.status} · output ${child.outputState ?? "unknown"}`);
249
270
  if (child.intercomTarget) lines.push(`${input.source === "async" ? "Previous intercom target" : "Run intercom target"}: ${child.intercomTarget}`);
250
271
  if (child.artifactPath) lines.push(`Output artifact: ${child.artifactPath}`);
251
272
  if (child.sessionPath) lines.push(`Session: ${child.sessionPath}`);
@@ -260,6 +281,7 @@ function formatSubagentResultIntercomMessage(input: {
260
281
  export function buildSubagentResultIntercomPayload(input: GroupedResultIntercomMessageInput): SubagentResultIntercomPayload {
261
282
  const children = input.children.map((child) => ({
262
283
  ...child,
284
+ outputState: child.outputState ?? "unknown",
263
285
  summary: child.summary.trim() || "(no output)",
264
286
  children: compactNestedResultChildren(child.children),
265
287
  }));
@@ -8,6 +8,7 @@ import {
8
8
  type NestedRunSummary,
9
9
  type ParallelHandoffReference,
10
10
  type SubagentResultIntercomChild,
11
+ type SubagentOutputState,
11
12
  type SubagentState,
12
13
  } from "../../shared/types.ts";
13
14
  import {
@@ -45,6 +46,7 @@ type ResultWatcherDeps = {
45
46
  type ResultFileChild = {
46
47
  agent?: string;
47
48
  output?: string;
49
+ outputState?: SubagentOutputState;
48
50
  error?: string;
49
51
  success?: boolean;
50
52
  state?: string;
@@ -178,9 +180,9 @@ export function createResultWatcher(
178
180
  const hasResultChildren = Array.isArray(data.results) && data.results.length > 0;
179
181
  const resultChildren: ResultFileChild[] = hasResultChildren
180
182
  ? data.results!
181
- : [{ agent: data.agent ?? undefined, output: data.summary, success: data.success }];
183
+ : [{ agent: data.agent ?? undefined, output: data.summary, outputState: "unknown", success: data.success }];
182
184
  const normalizedChildren = attachNestedChildrenToResultChildren(runId, resultChildren.map((result = {}, index): SubagentResultIntercomChild => {
183
- const baseOutput = result.output ?? data.summary;
185
+ const baseOutput = hasResultChildren ? result.output : result.output ?? data.summary;
184
186
  const hasRealOutput = typeof baseOutput === "string" && baseOutput.trim().length > 0;
185
187
  const output = hasRealOutput ? baseOutput : "(no output)";
186
188
  const summary = result.success === false && result.error
@@ -206,6 +208,9 @@ export function createResultWatcher(
206
208
  turnBudgetExceeded: result.turnBudgetExceeded,
207
209
  processSignal: result.processSignal,
208
210
  }),
211
+ outputState: result.outputState === "present" || result.outputState === "absent" || result.outputState === "unknown"
212
+ ? result.outputState
213
+ : "unknown",
209
214
  summary,
210
215
  index,
211
216
  artifactPath: result.artifactPaths?.outputPath,
@@ -27,6 +27,7 @@ import {
27
27
  type ResolvedTurnBudget,
28
28
  type ResolvedToolBudget,
29
29
  type SubagentRunMode,
30
+ type SubagentOutputState,
30
31
  type UsageBudgetConfig,
31
32
  type ToolBudgetState,
32
33
  type TurnBudgetState,
@@ -184,6 +185,7 @@ interface StepResult {
184
185
  launchResolvedExtensions?: LaunchResolvedChildExtensionsV1;
185
186
  runtimeAcknowledgedExtensions?: RuntimeAcknowledgedChildExtensionsV1;
186
187
  output: string;
188
+ outputState?: SubagentOutputState;
187
189
  error?: string;
188
190
  protocolError?: ProtocolOutputLimit;
189
191
  success: boolean;
@@ -422,6 +424,7 @@ interface RunPiStreamingResult {
422
424
  error?: string;
423
425
  protocolError?: ProtocolOutputLimit;
424
426
  finalOutput: string;
427
+ outputState: SubagentOutputState;
425
428
  interrupted?: boolean;
426
429
  timedOut?: boolean;
427
430
  stopped?: boolean;
@@ -837,6 +840,7 @@ function runPiStreaming(
837
840
  error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError ?? signalError,
838
841
  protocolError,
839
842
  finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput,
843
+ outputState: finalOutput.trim() ? "present" : "absent",
840
844
  interrupted,
841
845
  timedOut,
842
846
  stopped,
@@ -870,7 +874,7 @@ function runPiStreaming(
870
874
  const stderr = stderrTail.text();
871
875
  const finalOutput = getFinalOutput(messages) || rawStdoutTail.text().trim();
872
876
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
873
- resolve({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId });
877
+ resolve({ stderr, exitCode: 1, messages, usage, toolCount, durationMs: Date.now() - startedAt, model, error: stopped ? (stopMessage ?? "Subagent stopped by user.") : timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, protocolError, finalOutput: (timedOut || stopped) && !finalOutput.trim() ? (stopped ? stopMessage ?? "Subagent stopped by user." : timeoutMessage ?? "Subagent timed out.") : finalOutput, outputState: finalOutput.trim() ? "present" : "absent", timedOut, stopped, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudget?.outcome === "termination-deferred" || turnBudgetExceeded || undefined, observedMutationAttempt, watchdog: childWatchdogState, processInstanceId });
874
878
  });
875
879
  });
876
880
  }
@@ -1496,6 +1500,13 @@ async function runSingleStep(
1496
1500
  const childWrittenOutput = step.outputPath
1497
1501
  ? extractChildWrittenOutput(finalResult?.messages, step.outputPath, step.cwd ?? ctx.cwd)
1498
1502
  : undefined;
1503
+ const outputState: SubagentOutputState = finalResult?.outputState === "present"
1504
+ || (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput !== undefined
1505
+ || Boolean(childWrittenOutput?.trim())
1506
+ ? "present"
1507
+ : resolvedOutput.savedPath
1508
+ ? "unknown"
1509
+ : finalResult?.outputState ?? "unknown";
1499
1510
  const finalizedOutput = finalizeSingleOutput({
1500
1511
  fullOutput: outputForSummary,
1501
1512
  outputPath: step.outputPath,
@@ -1586,6 +1597,7 @@ async function runSingleStep(
1586
1597
  ...(step.agentContract ? { agentContract: step.agentContract } : {}),
1587
1598
  launchContractDigest: actualLaunchContractDigest,
1588
1599
  output: outputForSummary,
1600
+ outputState,
1589
1601
  exitCode: effectiveFinalExitCode,
1590
1602
  error: effectiveFinalError,
1591
1603
  protocolError: finalResult?.protocolError,
@@ -3376,6 +3388,7 @@ async function runSubagent(
3376
3388
  launchResolvedExtensions: pr.launchResolvedExtensions,
3377
3389
  runtimeAcknowledgedExtensions: pr.runtimeAcknowledgedExtensions,
3378
3390
  output: pr.output,
3391
+ outputState: pr.outputState,
3379
3392
  error: pr.error,
3380
3393
  protocolError: pr.protocolError,
3381
3394
  success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
@@ -3784,6 +3797,7 @@ async function runSubagent(
3784
3797
  launchContractDigest: pr.launchContractDigest,
3785
3798
  launchResolvedExtensions: pr.launchResolvedExtensions,
3786
3799
  output: pr.output,
3800
+ outputState: pr.outputState,
3787
3801
  error: pr.error,
3788
3802
  protocolError: pr.protocolError,
3789
3803
  success: pr.stopped !== true && pr.interrupted !== true && pr.exitCode === 0,
@@ -3970,6 +3984,7 @@ async function runSubagent(
3970
3984
  launchResolvedExtensions: singleResult.launchResolvedExtensions,
3971
3985
  runtimeAcknowledgedExtensions: singleResult.runtimeAcknowledgedExtensions,
3972
3986
  output: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
3987
+ outputState: singleResult.outputState,
3973
3988
  error: stopped || childStopped ? stopMessage : timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
3974
3989
  protocolError: singleResult.protocolError,
3975
3990
  success: !stopped && !childStopped && !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
@@ -4297,6 +4312,7 @@ async function runSubagent(
4297
4312
  agent: r.agent,
4298
4313
  context: r.context,
4299
4314
  output: r.output,
4315
+ outputState: r.outputState,
4300
4316
  error: r.error,
4301
4317
  protocolError: r.protocolError,
4302
4318
  success: r.success,
@@ -379,6 +379,7 @@ async function runSingleAttempt(
379
379
  launchContractDigest,
380
380
  launchResolvedExtensions,
381
381
  exitCode: 0,
382
+ outputState: "absent",
382
383
  messages: [],
383
384
  usage: emptyUsage(),
384
385
  model: modelArg,
@@ -1228,6 +1229,7 @@ async function runSingleAttempt(
1228
1229
 
1229
1230
  const acceptanceOutput = getFinalOutput(result.messages ?? []);
1230
1231
  let fullOutput = stripAcceptanceReport(acceptanceOutput);
1232
+ result.outputState = fullOutput.trim() || result.structuredOutput !== undefined ? "present" : "absent";
1231
1233
  if (result.timedOut) {
1232
1234
  const timeoutMessage = formatTimeoutMessage(options.timeoutMs ?? 0);
1233
1235
  fullOutput = fullOutput.trim()
@@ -1287,6 +1289,7 @@ async function runSingleAttempt(
1287
1289
  result.outputSaveError = resolvedOutput.saveError;
1288
1290
  if (resolvedOutput.savedPath) {
1289
1291
  result.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
1292
+ if (result.outputState === "absent") result.outputState = "unknown";
1290
1293
  }
1291
1294
  }
1292
1295
  artifactOutputByResult.set(result, fullOutput);
@@ -443,6 +443,7 @@ function rememberForegroundRun(state: SubagentState, input: { runId: string; mod
443
443
  ...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}),
444
444
  ...(result.error ? { error: result.error } : {}),
445
445
  ...(result.finalOutput ? { finalOutput: result.finalOutput } : {}),
446
+ ...(result.outputState ? { outputState: result.outputState } : {}),
446
447
  ...(result.outputMode ? { outputMode: result.outputMode } : {}),
447
448
  ...(result.savedOutputPath ? { savedOutputPath: result.savedOutputPath } : {}),
448
449
  ...(result.outputSaveError ? { outputSaveError: result.outputSaveError } : {}),
@@ -521,6 +522,7 @@ function updateRememberedForegroundChild(state: SubagentState, input: { runId: s
521
522
  ...(input.result.exitCode !== undefined ? { exitCode: input.result.exitCode } : {}),
522
523
  ...(input.result.error ? { error: input.result.error } : {}),
523
524
  ...(input.result.finalOutput ? { finalOutput: input.result.finalOutput } : {}),
525
+ outputState: input.result.outputState,
524
526
  outputMode: input.result.outputMode,
525
527
  savedOutputPath: input.result.savedOutputPath,
526
528
  outputSaveError: input.result.outputSaveError,
@@ -1487,6 +1489,7 @@ async function emitForegroundResultIntercom(input: {
1487
1489
  stopped: result.stopped,
1488
1490
  turnBudgetExceeded: result.turnBudgetExceeded,
1489
1491
  }),
1492
+ outputState: result.outputState ?? "unknown",
1490
1493
  summary: resultSummaryForIntercom(result),
1491
1494
  index,
1492
1495
  artifactPath: result.artifactPaths?.outputPath,
@@ -225,6 +225,7 @@ export interface ControlEvent {
225
225
  }
226
226
 
227
227
  export type SubagentResultStatus = "completed" | "failed" | "paused" | "stopped" | "detached";
228
+ export type SubagentOutputState = "present" | "absent" | "unknown";
228
229
  export type SubagentRunMode = "single" | "parallel" | "chain";
229
230
 
230
231
  export interface ParallelHandoffPatch {
@@ -517,7 +518,10 @@ export type PublicNestedRunSummary = Pick<
517
518
 
518
519
  export interface SubagentResultIntercomChild {
519
520
  agent: string;
521
+ /** Process/lifecycle status. It does not establish semantic task completion. */
520
522
  status: SubagentResultStatus;
523
+ /** Whether the child produced substantive output before its process ended. */
524
+ outputState?: SubagentOutputState;
521
525
  summary: string;
522
526
  index?: number;
523
527
  artifactPath?: string;
@@ -862,6 +866,8 @@ export interface SingleResult {
862
866
  artifactPaths?: ArtifactPaths;
863
867
  truncation?: TruncationResult;
864
868
  finalOutput?: string;
869
+ /** Provenance-aware state for substantive child output, excluding synthetic lifecycle messages. */
870
+ outputState?: SubagentOutputState;
865
871
  outputMode?: OutputMode;
866
872
  savedOutputPath?: string;
867
873
  outputReference?: SavedOutputReference;
@@ -1319,6 +1325,7 @@ export interface ForegroundResumeChild {
1319
1325
  exitCode?: number;
1320
1326
  error?: string;
1321
1327
  finalOutput?: string;
1328
+ outputState?: SubagentOutputState;
1322
1329
  outputMode?: OutputMode;
1323
1330
  savedOutputPath?: string;
1324
1331
  outputSaveError?: string;
@@ -1298,7 +1298,7 @@ export function registerSlashCommands(
1298
1298
  });
1299
1299
 
1300
1300
  pi.registerCommand("subagents-fleet", {
1301
- description: "Open the live, inspection-only subagent fleet",
1301
+ description: "Open the live subagent fleet inspector",
1302
1302
  handler: async (_args, ctx) => showFleet(ctx),
1303
1303
  });
1304
1304
 
package/src/tui/render.ts CHANGED
@@ -1318,7 +1318,7 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
1318
1318
  ]);
1319
1319
  const c = new Container();
1320
1320
  const width = getTermWidth() - 4;
1321
- const modelDisplay = modelThinkingBadge(theme, r.model);
1321
+ const modelDisplay = modelThinkingBadge(theme, r.model ?? r.progress?.model, r.thinking ?? r.progress?.thinking);
1322
1322
  c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning, undefined, frame)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
1323
1323
 
1324
1324
  if (isRunning && r.progress) {
@@ -1428,7 +1428,9 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1428
1428
  const glyph = rPending ? theme.fg("dim", "◦") : resultGlyph(r, output, theme, rRunning, progressRunningSeed(rProg), frame);
1429
1429
  const pendingLabel = rPending ? ` ${theme.fg("dim", "· pending")}` : "";
1430
1430
  const stepLabel = entry.rowLabel ?? resultRowLabel(multiLabel, i, stepNumber);
1431
- const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${contextModeBadge(theme, r.context)}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
1431
+ const rowProgressModel = rProg && "status" in rProg ? rProg : undefined;
1432
+ const rowModelDisplay = modelThinkingBadge(theme, r.model ?? rowProgressModel?.model, r.thinking ?? rowProgressModel?.thinking);
1433
+ const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${contextModeBadge(theme, r.context)}${rowModelDisplay}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
1432
1434
  c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
1433
1435
  if (rRunning && rProg && "status" in rProg) {
1434
1436
  const activity = compactCurrentActivity(rProg);