pi-subagents 0.30.0 → 0.31.1

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 (43) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +189 -18
  3. package/agents/context-builder.md +3 -3
  4. package/agents/planner.md +1 -1
  5. package/agents/researcher.md +1 -1
  6. package/agents/scout.md +1 -1
  7. package/package.json +7 -7
  8. package/skills/pi-subagents/SKILL.md +5 -0
  9. package/src/agents/agent-management.ts +170 -6
  10. package/src/agents/agent-serializer.ts +31 -13
  11. package/src/agents/agents.ts +207 -23
  12. package/src/agents/frontmatter.ts +66 -2
  13. package/src/agents/skills.ts +117 -20
  14. package/src/extension/doctor.ts +20 -0
  15. package/src/extension/fanout-child.ts +1 -0
  16. package/src/extension/index.ts +58 -4
  17. package/src/extension/schemas.ts +10 -76
  18. package/src/intercom/intercom-bridge.ts +27 -4
  19. package/src/profiles/profiles.ts +637 -0
  20. package/src/runs/background/async-execution.ts +14 -4
  21. package/src/runs/background/async-job-tracker.ts +56 -11
  22. package/src/runs/background/async-resume.ts +11 -13
  23. package/src/runs/background/control-channel.ts +177 -0
  24. package/src/runs/background/result-watcher.ts +11 -2
  25. package/src/runs/background/stale-run-reconciler.ts +9 -4
  26. package/src/runs/background/subagent-runner.ts +86 -3
  27. package/src/runs/foreground/chain-execution.ts +26 -2
  28. package/src/runs/foreground/execution.ts +113 -8
  29. package/src/runs/foreground/subagent-executor.ts +356 -86
  30. package/src/runs/shared/acceptance.ts +285 -34
  31. package/src/runs/shared/completion-guard.ts +1 -1
  32. package/src/runs/shared/dynamic-fanout.ts +4 -2
  33. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
  34. package/src/runs/shared/parallel-utils.ts +6 -1
  35. package/src/runs/shared/pi-args.ts +9 -1
  36. package/src/runs/shared/single-output.ts +15 -1
  37. package/src/runs/shared/subagent-prompt-runtime.ts +1 -0
  38. package/src/shared/settings.ts +1 -0
  39. package/src/shared/types.ts +9 -2
  40. package/src/shared/utils.ts +19 -1
  41. package/src/slash/prompt-template-bridge.ts +26 -3
  42. package/src/slash/slash-commands.ts +642 -43
  43. package/src/tui/render.ts +265 -13
@@ -8,6 +8,7 @@ import {
8
8
  type AgentSource,
9
9
  type ChainConfig,
10
10
  type ChainStepConfig,
11
+ BUILTIN_AGENT_NAMES,
11
12
  defaultInheritProjectContext,
12
13
  defaultInheritSkills,
13
14
  defaultSystemPromptMode,
@@ -22,11 +23,15 @@ import { discoverAvailableSkills } from "./skills.ts";
22
23
  import {
23
24
  buildProactiveSkillSubagentRecommendationLines,
24
25
  } from "./proactive-skills.ts";
26
+ import { parseFrontmatter } from "./frontmatter.ts";
27
+ import { toModelInfo } from "../shared/model-info.ts";
28
+ import { resolveSubagentModelOverride, type ParentModel } from "../runs/shared/model-fallback.ts";
25
29
  import type { Details, ExtensionConfig } from "../shared/types.ts";
30
+ import { getProjectConfigDir } from "../shared/utils.ts";
26
31
 
27
- type ManagementAction = "list" | "get" | "create" | "update" | "delete";
32
+ type ManagementAction = "list" | "get" | "models" | "create" | "update" | "delete";
28
33
  type ManagementScope = "user" | "project";
29
- type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { config?: ExtensionConfig };
34
+ type ManagementContext = Pick<ExtensionContext, "cwd" | "modelRegistry"> & { model?: ExtensionContext["model"]; config?: ExtensionConfig };
30
35
 
31
36
  interface ManagementParams {
32
37
  action?: string;
@@ -166,6 +171,84 @@ function skillsWarning(cwd: string, skills: string[] | undefined): string | unde
166
171
  return missing.length ? `Warning: skills not found: ${missing.join(", ")}.` : undefined;
167
172
  }
168
173
 
174
+ function editableAgentConfig(agent: AgentConfig): AgentConfig {
175
+ const base = agent.override?.base;
176
+ if (!base) return { ...agent };
177
+
178
+ return {
179
+ ...agent,
180
+ model: base.model,
181
+ fallbackModels: base.fallbackModels ? [...base.fallbackModels] : undefined,
182
+ thinking: base.thinking,
183
+ systemPromptMode: base.systemPromptMode,
184
+ inheritProjectContext: base.inheritProjectContext,
185
+ inheritSkills: base.inheritSkills,
186
+ defaultContext: base.defaultContext,
187
+ disabled: base.disabled,
188
+ systemPrompt: base.systemPrompt,
189
+ skills: base.skills ? [...base.skills] : undefined,
190
+ tools: base.tools ? [...base.tools] : undefined,
191
+ mcpDirectTools: base.mcpDirectTools ? [...base.mcpDirectTools] : undefined,
192
+ subagentOnlyExtensions: base.subagentOnlyExtensions ? [...base.subagentOnlyExtensions] : undefined,
193
+ completionGuard: base.completionGuard,
194
+ override: undefined,
195
+ };
196
+ }
197
+
198
+ function readAgentFrontmatterFields(filePath: string): Set<string> {
199
+ try {
200
+ const { frontmatter } = parseFrontmatter(fs.readFileSync(filePath, "utf-8"));
201
+ return new Set(Object.keys(frontmatter));
202
+ } catch {
203
+ return new Set();
204
+ }
205
+ }
206
+
207
+ function preservedAgentFrontmatterFields(agent: AgentConfig, cfg: Record<string, unknown>): Set<string> {
208
+ const fields = readAgentFrontmatterFields(agent.filePath);
209
+ const changed = (...names: string[]) => {
210
+ for (const name of names) fields.delete(name);
211
+ };
212
+
213
+ if (hasKey(cfg, "name")) changed("name");
214
+ if (hasKey(cfg, "package")) changed("package");
215
+ if (hasKey(cfg, "description")) changed("description");
216
+ if (hasKey(cfg, "systemPrompt")) changed("systemPrompt");
217
+ if (hasKey(cfg, "model")) changed("model");
218
+ if (hasKey(cfg, "fallbackModels")) changed("fallbackModels");
219
+ if (hasKey(cfg, "tools")) changed("tools");
220
+ if (hasKey(cfg, "skills")) changed("skill", "skills");
221
+ if (hasKey(cfg, "extensions")) changed("extensions");
222
+ if (hasKey(cfg, "subagentOnlyExtensions")) changed("subagentOnlyExtensions");
223
+ if (hasKey(cfg, "thinking")) {
224
+ changed("thinking");
225
+ if (cfg.thinking === "off") fields.add("thinking");
226
+ }
227
+ if (hasKey(cfg, "systemPromptMode")) {
228
+ changed("systemPromptMode");
229
+ fields.add("systemPromptMode");
230
+ }
231
+ if (hasKey(cfg, "inheritProjectContext")) {
232
+ changed("inheritProjectContext");
233
+ fields.add("inheritProjectContext");
234
+ }
235
+ if (hasKey(cfg, "inheritSkills")) {
236
+ changed("inheritSkills");
237
+ fields.add("inheritSkills");
238
+ }
239
+ if (hasKey(cfg, "defaultContext")) changed("defaultContext");
240
+ if (hasKey(cfg, "output")) changed("output");
241
+ if (hasKey(cfg, "reads")) changed("defaultReads");
242
+ if (hasKey(cfg, "progress")) changed("defaultProgress");
243
+ if (hasKey(cfg, "maxSubagentDepth")) changed("maxSubagentDepth");
244
+ if (hasKey(cfg, "completionGuard")) {
245
+ changed("completionGuard");
246
+ if (cfg.completionGuard === true) fields.add("completionGuard");
247
+ }
248
+
249
+ return fields;
250
+ }
251
+
169
252
  function parseStepList(raw: unknown): { steps?: ChainStepConfig[]; error?: string } {
170
253
  if (!Array.isArray(raw)) return { error: "config.steps must be an array." };
171
254
  if (raw.length === 0) return { error: "config.steps must include at least one step." };
@@ -480,6 +563,84 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
480
563
  return result(lines.join("\n"));
481
564
  }
482
565
 
566
+ function formatModelSource(agent: AgentConfig, currentModel: ParentModel | undefined): string {
567
+ if (agent.override && agent.model !== agent.override.base.model) {
568
+ return `${agent.override.scope} override`;
569
+ }
570
+ if (agent.model) return "builtin agent config";
571
+ if (currentModel) return "inherits current session model";
572
+ return "inherit requested, but no current session model is available";
573
+ }
574
+
575
+ function handleModels(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
576
+ const requestedAgent = params.agent?.trim();
577
+ if (requestedAgent && !(BUILTIN_AGENT_NAMES as readonly string[]).includes(requestedAgent)) {
578
+ return result(`Builtin agent '${requestedAgent}' not found. Available: ${BUILTIN_AGENT_NAMES.join(", ")}.`, true);
579
+ }
580
+
581
+ const discovered = discoverAgentsAll(ctx.cwd);
582
+ const builtinByName = new Map(discovered.builtin.map((agent) => [agent.name, agent]));
583
+ const availableModels = ctx.modelRegistry.getAvailable().map(toModelInfo);
584
+ const currentModel = ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined;
585
+ const preferredProvider = ctx.model?.provider;
586
+ const names = requestedAgent ? [requestedAgent] : [...BUILTIN_AGENT_NAMES];
587
+
588
+ if (requestedAgent) {
589
+ const agent = builtinByName.get(requestedAgent);
590
+ if (!agent) return result(`Builtin agent '${requestedAgent}' not found.`, true);
591
+ const resolvedModel = resolveSubagentModelOverride(agent.model, currentModel, availableModels, preferredProvider);
592
+ const lines = [
593
+ "Builtin subagent model",
594
+ "",
595
+ `Agent: ${requestedAgent}`,
596
+ "Effective model:",
597
+ ` ${resolvedModel ?? "(unresolved)"}`,
598
+ `Source: ${formatModelSource(agent, currentModel)}`,
599
+ ];
600
+ if (agent.override) {
601
+ lines.push("Override file:");
602
+ lines.push(` ${agent.override.path}`);
603
+ }
604
+ if (agent.model && resolvedModel && agent.model !== resolvedModel) {
605
+ lines.push("Requested model setting:");
606
+ lines.push(` ${agent.model}`);
607
+ }
608
+ if (agent.disabled) lines.push("Disabled: true");
609
+ lines.push("Current session model:");
610
+ lines.push(` ${currentModel ? `${currentModel.provider}/${currentModel.id}` : "(unavailable)"}`);
611
+ return result(lines.join("\n"));
612
+ }
613
+
614
+ const lines = [
615
+ "Builtin subagent models",
616
+ "",
617
+ "Current session model:",
618
+ ` ${currentModel ? `${currentModel.provider}/${currentModel.id}` : "(unavailable)"}`,
619
+ "",
620
+ ];
621
+
622
+ for (const name of names) {
623
+ const agent = builtinByName.get(name);
624
+ if (!agent) {
625
+ lines.push(name);
626
+ lines.push(" model:");
627
+ lines.push(" (builtin definition not found)");
628
+ lines.push(" source: missing");
629
+ lines.push("");
630
+ continue;
631
+ }
632
+ const resolvedModel = resolveSubagentModelOverride(agent.model, currentModel, availableModels, preferredProvider);
633
+ const source = `${formatModelSource(agent, currentModel)}${agent.disabled ? "; disabled" : ""}`;
634
+ lines.push(name);
635
+ lines.push(" model:");
636
+ lines.push(` ${resolvedModel ?? "(unresolved)"}`);
637
+ lines.push(` source: ${source}`);
638
+ lines.push("");
639
+ }
640
+
641
+ return result(lines.join("\n"));
642
+ }
643
+
483
644
  function handleGet(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
484
645
  if (!params.agent && !params.chainName) return result("Specify 'agent' or 'chainName' for get.", true);
485
646
  const hasBoth = Boolean(params.agent && params.chainName);
@@ -527,9 +688,10 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
527
688
  const scope = scopeRaw as ManagementScope;
528
689
  const isChain = hasKey(cfg, "steps");
529
690
  const d = discoverAgentsAll(ctx.cwd);
691
+ const projectConfigDir = getProjectConfigDir(ctx.cwd);
530
692
  const targetDir = isChain
531
- ? scope === "user" ? d.userChainDir : d.projectChainDir ?? path.join(ctx.cwd, ".pi", "chains")
532
- : scope === "user" ? d.userDir : d.projectDir ?? path.join(ctx.cwd, ".pi", "agents");
693
+ ? scope === "user" ? d.userChainDir : d.projectChainDir ?? path.join(projectConfigDir, "chains")
694
+ : scope === "user" ? d.userDir : d.projectDir ?? path.join(projectConfigDir, "agents");
533
695
  fs.mkdirSync(targetDir, { recursive: true });
534
696
  if (nameExistsInScope(ctx.cwd, scope, runtimeName)) return result(`Name '${runtimeName}' already exists in ${scope} scope. Use update instead.`, true);
535
697
  const targetPath = path.join(targetDir, isChain ? `${runtimeName}.chain.md` : `${runtimeName}.md`);
@@ -583,7 +745,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
583
745
  const targetOrError = resolveTarget("agent", params.agent, findAgents(params.agent, ctx.cwd, scopeHint ?? "both"), ctx.cwd, params.agentScope);
584
746
  if ("content" in targetOrError) return targetOrError;
585
747
  const target = targetOrError;
586
- const updated: AgentConfig = { ...target };
748
+ const updated = editableAgentConfig(target);
587
749
  const oldName = target.name;
588
750
  if (hasKey(cfg, "name") && (typeof cfg.name !== "string" || !cfg.name.trim())) return result("config.name must be a non-empty string when provided.", true);
589
751
  if (hasKey(cfg, "description") && (typeof cfg.description !== "string" || !cfg.description.trim())) return result("config.description must be a non-empty string when provided.", true);
@@ -600,6 +762,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
600
762
  }
601
763
  const applyError = applyAgentConfig(updated, cfg);
602
764
  if (applyError) return result(applyError, true);
765
+ const preserveFrontmatterFields = preservedAgentFrontmatterFields(target, cfg);
603
766
  updated.localName = newLocalName;
604
767
  updated.packageName = newPackageName;
605
768
  updated.name = buildRuntimeName(newLocalName, newPackageName);
@@ -621,7 +784,7 @@ export function handleUpdate(params: ManagementParams, ctx: ManagementContext):
621
784
  if (renamed.error) return result(renamed.error, true);
622
785
  updated.filePath = renamed.filePath!;
623
786
  }
624
- fs.writeFileSync(updated.filePath, serializeAgent(updated), "utf-8");
787
+ fs.writeFileSync(updated.filePath, serializeAgent(updated, { preserveFrontmatterFields }), "utf-8");
625
788
  if (updated.name !== oldName) {
626
789
  const refs = discoverAgentsAll(ctx.cwd).chains.filter((c) => c.steps.some((s) => s.agent === oldName)).map((c) => `${c.name} (${c.source})`);
627
790
  if (refs.length) warnings.push(`Warning: chains still reference '${oldName}': ${refs.join(", ")}.`);
@@ -703,6 +866,7 @@ export function handleManagementAction(action: string, params: ManagementParams,
703
866
  switch (action as ManagementAction) {
704
867
  case "list": return handleList(params, ctx);
705
868
  case "get": return handleGet(params, ctx);
869
+ case "models": return handleModels(params, ctx);
706
870
  case "create": return handleCreate(params, ctx);
707
871
  case "update": return handleUpdate(params, ctx);
708
872
  case "delete": return handleDelete(params, ctx);
@@ -30,8 +30,14 @@ function joinComma(values: string[] | undefined): string | undefined {
30
30
  return values.join(", ");
31
31
  }
32
32
 
33
- export function serializeAgent(config: AgentConfig): string {
33
+ interface SerializeAgentOptions {
34
+ preserveFrontmatterFields?: ReadonlySet<string>;
35
+ }
36
+
37
+ export function serializeAgent(config: AgentConfig, options: SerializeAgentOptions = {}): string {
34
38
  const lines: string[] = [];
39
+ const preserve = (...fields: string[]) => fields.some((field) => options.preserveFrontmatterFields?.has(field));
40
+ const preservingExistingFrontmatter = options.preserveFrontmatterFields !== undefined;
35
41
  lines.push("---");
36
42
  lines.push(`name: ${frontmatterNameForConfig(config)}`);
37
43
  if (config.packageName) lines.push(`package: ${config.packageName}`);
@@ -42,25 +48,27 @@ export function serializeAgent(config: AgentConfig): string {
42
48
  ...(config.mcpDirectTools ?? []).map((tool) => `mcp:${tool}`),
43
49
  ];
44
50
  const toolsValue = joinComma(tools);
45
- if (toolsValue) lines.push(`tools: ${toolsValue}`);
51
+ if (toolsValue || preserve("tools")) lines.push(`tools: ${toolsValue ?? ""}`);
46
52
 
47
- if (config.model) lines.push(`model: ${config.model}`);
53
+ if (config.model || preserve("model")) lines.push(`model: ${config.model ?? ""}`);
48
54
  const fallbackModelsValue = joinComma(config.fallbackModels);
49
- if (fallbackModelsValue) lines.push(`fallbackModels: ${fallbackModelsValue}`);
50
- if (config.thinking && config.thinking !== "off") lines.push(`thinking: ${config.thinking}`);
51
- lines.push(`systemPromptMode: ${config.systemPromptMode}`);
52
- lines.push(`inheritProjectContext: ${config.inheritProjectContext ? "true" : "false"}`);
53
- lines.push(`inheritSkills: ${config.inheritSkills ? "true" : "false"}`);
54
- if (config.defaultContext) lines.push(`defaultContext: ${config.defaultContext}`);
55
+ if (fallbackModelsValue || preserve("fallbackModels")) lines.push(`fallbackModels: ${fallbackModelsValue ?? ""}`);
56
+ if ((config.thinking && (config.thinking !== "off" || preserve("thinking"))) || (!config.thinking && preserve("thinking"))) {
57
+ lines.push(`thinking: ${config.thinking ?? ""}`);
58
+ }
59
+ if (!preservingExistingFrontmatter || preserve("systemPromptMode")) lines.push(`systemPromptMode: ${config.systemPromptMode}`);
60
+ if (!preservingExistingFrontmatter || preserve("inheritProjectContext")) lines.push(`inheritProjectContext: ${config.inheritProjectContext ? "true" : "false"}`);
61
+ if (!preservingExistingFrontmatter || preserve("inheritSkills")) lines.push(`inheritSkills: ${config.inheritSkills ? "true" : "false"}`);
62
+ if (config.defaultContext || preserve("defaultContext")) lines.push(`defaultContext: ${config.defaultContext ?? ""}`);
55
63
 
56
64
  const skillsValue = joinComma(config.skills);
57
- if (skillsValue) lines.push(`skills: ${skillsValue}`);
65
+ if (skillsValue || preserve("skill", "skills")) lines.push(`skills: ${skillsValue ?? ""}`);
58
66
 
59
67
  if (config.extensions !== undefined) {
60
68
  const extensionsValue = joinComma(config.extensions);
61
69
  lines.push(`extensions: ${extensionsValue ?? ""}`);
62
70
  }
63
- if (config.subagentOnlyExtensions !== undefined) {
71
+ if (config.subagentOnlyExtensions !== undefined || preserve("subagentOnlyExtensions")) {
64
72
  const subagentOnlyExtensionsValue = joinComma(config.subagentOnlyExtensions);
65
73
  lines.push(`subagentOnlyExtensions: ${subagentOnlyExtensionsValue ?? ""}`);
66
74
  }
@@ -76,12 +84,22 @@ export function serializeAgent(config: AgentConfig): string {
76
84
  if (typeof maxSubagentDepth === "number" && Number.isInteger(maxSubagentDepth) && maxSubagentDepth >= 0) {
77
85
  lines.push(`maxSubagentDepth: ${maxSubagentDepth}`);
78
86
  }
79
- if (config.completionGuard === false) lines.push("completionGuard: false");
87
+ if (config.completionGuard === false || preserve("completionGuard")) {
88
+ lines.push(`completionGuard: ${config.completionGuard === undefined ? "" : config.completionGuard ? "true" : "false"}`);
89
+ }
80
90
 
81
91
  if (config.extraFields) {
82
92
  for (const [key, value] of Object.entries(config.extraFields)) {
83
93
  if (KNOWN_FIELDS.has(key)) continue;
84
- lines.push(`${key}: ${value}`);
94
+ if (typeof value === "string" && value.includes("\n")) {
95
+ // Multi-line block value (e.g. permission: nested YAML)
96
+ lines.push(`${key}:`);
97
+ for (const blockLine of value.split("\n")) {
98
+ lines.push(` ${blockLine}`);
99
+ }
100
+ } else {
101
+ lines.push(`${key}: ${value}`);
102
+ }
85
103
  }
86
104
  }
87
105