pi-subagents 0.59.0 → 0.61.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/docs/agents.md +2 -2
  3. package/docs/configuration.md +9 -5
  4. package/docs/extension-api.md +14 -7
  5. package/docs/models.md +1 -1
  6. package/docs/observability.md +1 -1
  7. package/docs/tool-reference.md +15 -3
  8. package/docs/workflows.md +15 -14
  9. package/install.mjs +2 -1
  10. package/package.json +1 -1
  11. package/skills/council-mode/SKILL.md +48 -243
  12. package/skills/council-mode/references/pass-contracts.md +150 -0
  13. package/skills/pi-subagents/SKILL.md +89 -37
  14. package/skills/pi-subagents/references/constraints-and-recipes.md +30 -234
  15. package/skills/pi-subagents/references/execution-controls.md +79 -7
  16. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
  17. package/skills/pi-subagents/references/multi-lane-orchestration.md +13 -1
  18. package/skills/pi-subagents/references/prompting-and-roles.md +35 -28
  19. package/skills/pi-subagents/references/review-and-validation.md +73 -0
  20. package/src/agents/agent-management.ts +256 -88
  21. package/src/agents/agents.ts +527 -221
  22. package/src/api/background-work.ts +7 -2
  23. package/src/api/external-runs.ts +67 -4
  24. package/src/api/preflight.ts +13 -8
  25. package/src/api/shared-types.ts +3 -0
  26. package/src/extension/index.ts +7 -4
  27. package/src/extension/public-execution.ts +48 -4
  28. package/src/extension/rpc.ts +62 -4
  29. package/src/extension/schemas.ts +12 -7
  30. package/src/extension/tool-description.ts +16 -16
  31. package/src/runs/background/async-execution.ts +53 -37
  32. package/src/runs/background/async-job-tracker.ts +65 -3
  33. package/src/runs/background/async-resume.ts +3 -1
  34. package/src/runs/background/async-status.ts +104 -11
  35. package/src/runs/background/auto-drain.ts +1 -1
  36. package/src/runs/background/control-channel.ts +3 -2
  37. package/src/runs/background/fleet-view.ts +1 -1
  38. package/src/runs/background/result-watcher.ts +1 -1
  39. package/src/runs/background/resume-guidance.ts +1 -1
  40. package/src/runs/background/run-status.ts +15 -4
  41. package/src/runs/background/subagent-runner.ts +6 -3
  42. package/src/runs/background/subagent-wait.ts +30 -23
  43. package/src/runs/background/wait-completions.ts +4 -1
  44. package/src/runs/background/wait-tool.ts +24 -18
  45. package/src/runs/foreground/execution.ts +72 -4
  46. package/src/runs/foreground/subagent-executor.ts +276 -59
  47. package/src/runs/shared/acceptance.ts +43 -18
  48. package/src/runs/shared/async-status-projection.ts +138 -4
  49. package/src/runs/shared/background-process-options.ts +9 -0
  50. package/src/runs/shared/host-step-status.ts +1 -0
  51. package/src/runs/shared/mcp-direct-tool-grant.ts +2 -5
  52. package/src/runs/shared/model-fallback.ts +61 -17
  53. package/src/runs/shared/mutation-evidence.ts +52 -3
  54. package/src/runs/shared/permissions.ts +1 -1
  55. package/src/runs/shared/pi-args.ts +47 -1
  56. package/src/runs/shared/single-output.ts +45 -18
  57. package/src/runs/shared/subagent-prompt-runtime.ts +20 -2
  58. package/src/runs/shared/tool-timeout.ts +1 -1
  59. package/src/runs/shared/workflow-graph.ts +16 -0
  60. package/src/shared/types.ts +74 -4
  61. package/src/shared/workflow-child-permit.ts +91 -0
  62. package/src/slash/prompt-template-bridge.ts +37 -1
  63. package/src/slash/slash-commands.ts +18 -26
  64. package/src/tui/fleet-status.ts +11 -3
  65. package/src/tui/render-helpers.ts +31 -0
  66. package/src/tui/render.ts +673 -145
  67. package/src/watchdog/change-signature.ts +40 -1
  68. package/src/workflows/host-command.ts +6 -1
  69. package/src/workflows/scripted-workflow.ts +206 -6
  70. package/src/workflows/workflow-child-summary.ts +1 -1
  71. package/src/workflows/workflow-receipt.ts +41 -4
  72. package/src/workflows/workflow-resources.ts +150 -0
@@ -3,7 +3,7 @@ import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
  import { keyText, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
5
5
  import { Key, matchesKey, truncateToWidth, type Component, type KeyId, type TUI } from "@earendil-works/pi-tui";
6
- import { BUILTIN_AGENT_NAMES, discoverAgents, discoverAgentsAll, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentDiscoveryDiagnostic, type AgentScope, type UnknownAgentDiagnosticContext } from "../agents/agents.ts";
6
+ import { discoverAgentSnapshot, discoverAgents, findBlockingAgentDiagnostic, formatUnknownAgentError, resolveAgentName, unknownAgentDiagnosticContext, type AgentConfig, type AgentDiscoveryDiagnostic, type AgentScope, type UnknownAgentDiagnosticContext } from "../agents/agents.ts";
7
7
  import { listRuntimeAgentConfigs, mergeRuntimeAgents } from "../agents/runtime-agent-registry.ts";
8
8
  import { resolveExistingReadPaths } from "../shared/settings.ts";
9
9
  import {
@@ -113,9 +113,13 @@ const extractExecutionFlags = (rawArgs: string): { args: string; bg: boolean; fo
113
113
  };
114
114
 
115
115
  function discoverSlashAgents(pi: ExtensionAPI, cwd: string, scope: AgentScope): { agents: AgentConfig[]; agentDiagnostics?: AgentDiscoveryDiagnostic[]; unknownAgentDiagnosticContext: UnknownAgentDiagnosticContext } {
116
- const discovered = discoverAgents(cwd, scope);
117
- if (listRuntimeAgentConfigs(pi).length === 0) return { ...discovered, unknownAgentDiagnosticContext: unknownAgentDiagnosticContext(discovered) };
118
- const all = discoverAgentsAll(cwd);
116
+ if (listRuntimeAgentConfigs(pi).length === 0) {
117
+ const discovered = discoverAgents(cwd, scope);
118
+ return { ...discovered, unknownAgentDiagnosticContext: unknownAgentDiagnosticContext(discovered) };
119
+ }
120
+ const snapshot = discoverAgentSnapshot(cwd, scope, undefined, { includeChains: false });
121
+ const discovered = snapshot.effective;
122
+ const all = snapshot.all;
119
123
  const configuredAgents: AgentConfig[] = [
120
124
  ...all.builtin,
121
125
  ...all.package,
@@ -133,13 +137,6 @@ const makeAgentCompletions = (pi: ExtensionAPI, state: SubagentState) => (prefix
133
137
  .map((agent) => ({ value: agent.name, label: agent.name }));
134
138
  };
135
139
 
136
- const makeBuiltinAgentNameCompletions = () => (prefix: string) => {
137
- if (prefix.includes(" ")) return null;
138
- return BUILTIN_AGENT_NAMES
139
- .filter((name) => name.startsWith(prefix))
140
- .map((name) => ({ value: name, label: name }));
141
- };
142
-
143
140
  const makeProviderCompletions = (state: SubagentState) => (prefix: string) => {
144
141
  if (prefix.includes(" ")) return null;
145
142
  const available = state.lastUiContext?.modelRegistry?.getAvailable?.();
@@ -420,7 +417,7 @@ function detailsFromSessionEntry(entry: unknown): Details | undefined {
420
417
  }
421
418
  if (record.type !== "message" || !record.message || typeof record.message !== "object") return undefined;
422
419
  const message = record.message as { role?: unknown; toolName?: unknown; details?: unknown };
423
- if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "subagent_wait")) return undefined;
420
+ if (message.role !== "toolResult" || (message.toolName !== "subagent" && message.toolName !== "bg_wait" && message.toolName !== "subagent_wait")) return undefined;
424
421
  return isSubagentDetails(message.details) ? message.details : undefined;
425
422
  }
426
423
 
@@ -1005,7 +1002,7 @@ export function registerSlashCommands(
1005
1002
  ctx.ui.notify(`Foreground run ${control.runId} is not currently detachable.`, "info");
1006
1003
  return;
1007
1004
  }
1008
- sendSlashText(pi, `Detached foreground run ${control.runId} without terminating its child. Use subagent({ action: "status", id: ${JSON.stringify(control.runId)} }) or subagent_wait({ id: ${JSON.stringify(control.runId)} }) to recover the eventual result. This does not daemonize the process or guarantee survival across Pi reload/restart.`);
1005
+ sendSlashText(pi, `Detached foreground run ${control.runId} without terminating its child. Use subagent({ action: "status", id: ${JSON.stringify(control.runId)} }) or bg_wait({ id: ${JSON.stringify(control.runId)} }) to recover the eventual result. This does not daemonize the process or guarantee survival across Pi reload/restart.`);
1009
1006
  };
1010
1007
 
1011
1008
  pi.registerCommand("subagents-detach", {
@@ -1140,25 +1137,20 @@ export function registerSlashCommands(
1140
1137
  });
1141
1138
 
1142
1139
  pi.registerCommand("subagents-models", {
1143
- description: "Show runtime-loaded builtin subagent models",
1144
- getArgumentCompletions: makeBuiltinAgentNameCompletions(),
1145
- handler: async (args, ctx) => {
1146
- const trimmed = args.trim();
1147
- if (!trimmed) {
1148
- await runCommand(ctx, { action: "models" });
1140
+ description: "Show model mappings for discovered subagents",
1141
+ getArgumentCompletions: makeAgentCompletions(pi, state),
1142
+ handler: async (args, ctx) => {
1143
+ const trimmed = args.trim();
1144
+ if (!trimmed) {
1145
+ await runCommand(ctx, { action: "models" });
1149
1146
  return;
1150
1147
  }
1151
1148
  const parts = trimmed.split(/\s+/).filter(Boolean);
1152
1149
  if (parts.length !== 1) {
1153
- ctx.ui.notify("Usage: /subagents-models [builtin-agent-name]", "error");
1154
- return;
1155
- }
1156
- const agent = parts[0]!;
1157
- if (!(BUILTIN_AGENT_NAMES as readonly string[]).includes(agent)) {
1158
- ctx.ui.notify(`Unknown builtin agent: ${agent}`, "error");
1150
+ ctx.ui.notify("Usage: /subagents-models [agent-name]", "error");
1159
1151
  return;
1160
1152
  }
1161
- await runCommand(ctx, { action: "models", agent });
1153
+ await runCommand(ctx, { action: "models", agent: parts[0]! });
1162
1154
  },
1163
1155
  });
1164
1156
 
@@ -4,6 +4,7 @@ import { snapshotExternalRuns } from "../api/external-runs.ts";
4
4
  import { formatModelThinking } from "../shared/formatters.ts";
5
5
  import type { AsyncJobState, AsyncJobStep, FleetViewPlacement, HerdrProjectPaneSnapshot, HostStepState, HostStepVerdict, NestedRunSummary, NestedStepSummary, SubagentState } from "../shared/types.ts";
6
6
  import { projectAsyncWorkflowRows, type AsyncStatusWorkflowRow } from "../runs/shared/async-status-projection.ts";
7
+ import { contextModeLabel } from "../runs/shared/context-mode.ts";
7
8
  import { formatWorkflowJsonPreview } from "../workflows/scripted-workflow.ts";
8
9
  import { hostStepReportName, hostStepVerdictLabel } from "../runs/shared/host-step-status.ts";
9
10
  import { isStaleExtensionContextError } from "../shared/extension-context.ts";
@@ -362,7 +363,7 @@ export function collectFleetStatusEntries(state: SubagentState): FleetStatusEntr
362
363
  if (job.mode === "workflow") {
363
364
  const latestEmit = job.workflow?.emits?.length ? formatWorkflowJsonPreview(job.workflow.emits.at(-1), 120) : undefined;
364
365
  const workflowSteps = workflowStepsWithoutMaterializedChildren(job.steps, materializedChildrenByWorkflow.get(`async:${job.asyncId}`));
365
- const workflowRows = projectAsyncWorkflowRows(workflowSteps, job.hostSteps, job.preflight);
366
+ const workflowRows = projectAsyncWorkflowRows(workflowSteps, job.workflowGraph ?? job.hostSteps, job.preflight);
366
367
  entries.push({
367
368
  key: `async:${job.asyncId}`,
368
369
  ...(linkedParentKey ? { parentKey: linkedParentKey } : {}),
@@ -550,7 +551,12 @@ export class SubagentFleetStatus {
550
551
  this.lastRenderKey = renderKey;
551
552
  return;
552
553
  }
553
- if (renderKey === this.lastRenderKey) return;
554
+ if (renderKey === this.lastRenderKey) {
555
+ // Repaint anyway while anything is running so the wall-clock
556
+ // spinner animates between state changes (500ms tick).
557
+ if (this.entries.some((entry) => entry.state === "running")) this.tui?.requestRender();
558
+ return;
559
+ }
554
560
  this.lastRenderKey = renderKey;
555
561
  this.tui?.requestRender();
556
562
  }
@@ -728,6 +734,7 @@ export class SubagentFleetStatus {
728
734
  const marker = last ? "└─" : "├─";
729
735
  const indent = " ";
730
736
  if (row.overflow !== undefined) return truncateToWidth(`${indent}${marker} ${theme.fg("dim", `+${row.overflow} hidden workflow steps`)}`, width);
737
+ const context = contextModeLabel(row.context);
731
738
  const modelThinking = row.modelThinking ? ` (${row.modelThinking})` : "";
732
739
  const activity = row.activity ? ` · ${row.activity}` : "";
733
740
  const kind = row.kind ? `${row.kind}: ` : "";
@@ -738,7 +745,7 @@ export class SubagentFleetStatus {
738
745
  row.preflight.expectedOutput ? `expected:${row.preflight.expectedOutput}` : undefined,
739
746
  row.preflight.independence ? `independence:${row.preflight.independence}` : undefined,
740
747
  ].filter((value): value is string => Boolean(value)).join(" · ") : "";
741
- const left = `${indent}${marker} ${this.workflowRowGlyph(row, theme)} ${theme.fg("muted", `${kind}${row.name}${modelThinking}`)} · ${this.workflowRowStateLabel(row, theme)}${activity}${hints ? ` · ${hints}` : ""}`;
748
+ const left = `${indent}${marker} ${this.workflowRowGlyph(row, theme)} ${theme.fg("muted", `${kind}${row.name}${context ? ` ${context}` : ""}${modelThinking}`)} · ${this.workflowRowStateLabel(row, theme)}${activity}${hints ? ` · ${hints}` : ""}`;
742
749
  const details = [
743
750
  row.startedAt !== undefined ? formatFleetElapsed(Date.now() - row.startedAt) : undefined,
744
751
  row.tokens !== undefined ? formatFleetTokens(row.tokens, row.window) : undefined,
@@ -806,6 +813,7 @@ export class SubagentFleetStatus {
806
813
  row.kind,
807
814
  row.name,
808
815
  row.state,
816
+ row.context,
809
817
  row.modelThinking,
810
818
  row.activity,
811
819
  row.startedAt,
@@ -30,6 +30,37 @@ export function fuzzyFilter<T extends { name: string; description: string; model
30
30
  .map((x) => x.item);
31
31
  }
32
32
 
33
+ /** Remove a repeated job name from a child label when a safe separator follows it. */
34
+ export function stripRepeatedAgentPrefix(label: string, jobName: string | undefined): string {
35
+ const value = label.trim();
36
+ const prefix = jobName?.trim();
37
+ if (!prefix || !value.startsWith(prefix)) return value;
38
+
39
+ const suffix = value.slice(prefix.length);
40
+ if (!suffix || !/^(?::|·|\s)/u.test(suffix)) return value;
41
+ return suffix.replace(/^\s*(?::|·)?\s*/u, "").trim();
42
+ }
43
+
44
+ /** Whether a status label may omit the one logical step marker. */
45
+ export function shouldSuppressSingleStep(chainStepCount?: number, stepsTotal?: number): boolean {
46
+ return (chainStepCount ?? stepsTotal) === 1;
47
+ }
48
+
49
+ /** Add an Agent fraction only when visible candidates collide. */
50
+ export function withDuplicateLabelDiscriminators<T extends { index: number; displayName: string }>(
51
+ rows: readonly T[],
52
+ total: number,
53
+ ): Array<T & { rowLabel: string }> {
54
+ const counts = new Map<string, number>();
55
+ for (const row of rows) counts.set(row.displayName, (counts.get(row.displayName) ?? 0) + 1);
56
+ return rows.map((row) => ({
57
+ ...row,
58
+ rowLabel: (counts.get(row.displayName) ?? 0) > 1
59
+ ? `Agent ${row.index + 1}/${total}: ${row.displayName}`
60
+ : row.displayName,
61
+ }));
62
+ }
63
+
33
64
  export function pad(s: string, len: number): string {
34
65
  const vis = visibleWidth(s);
35
66
  return s + " ".repeat(Math.max(0, len - vis));