pi-subagents 0.28.0 → 0.29.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 (36) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +18 -61
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +4 -35
  5. package/src/agents/agent-management.ts +10 -20
  6. package/src/agents/agent-selection.ts +2 -0
  7. package/src/agents/agent-serializer.ts +0 -10
  8. package/src/agents/agents.ts +304 -47
  9. package/src/agents/chain-serializer.ts +4 -9
  10. package/src/extension/doctor.ts +4 -3
  11. package/src/extension/fanout-child.ts +0 -2
  12. package/src/extension/index.ts +3 -8
  13. package/src/extension/schemas.ts +32 -22
  14. package/src/intercom/intercom-bridge.ts +11 -1
  15. package/src/intercom/result-intercom.ts +0 -5
  16. package/src/runs/background/async-execution.ts +20 -11
  17. package/src/runs/background/run-status.ts +1 -7
  18. package/src/runs/background/subagent-runner.ts +81 -211
  19. package/src/runs/foreground/chain-execution.ts +62 -58
  20. package/src/runs/foreground/execution.ts +38 -343
  21. package/src/runs/foreground/subagent-executor.ts +28 -99
  22. package/src/runs/shared/acceptance.ts +605 -22
  23. package/src/runs/shared/completion-guard.ts +3 -26
  24. package/src/runs/shared/model-fallback.ts +38 -0
  25. package/src/runs/shared/parallel-utils.ts +6 -10
  26. package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
  27. package/src/runs/shared/workflow-graph.ts +2 -6
  28. package/src/shared/atomic-json.ts +68 -11
  29. package/src/shared/settings.ts +1 -0
  30. package/src/shared/types.ts +10 -48
  31. package/src/shared/utils.ts +2 -8
  32. package/src/tui/render.ts +14 -29
  33. package/src/runs/shared/acceptance-contract.ts +0 -318
  34. package/src/runs/shared/acceptance-evaluation.ts +0 -221
  35. package/src/runs/shared/acceptance-finalization.ts +0 -173
  36. package/src/runs/shared/acceptance-reports.ts +0 -127
@@ -78,30 +78,42 @@ const AcceptanceReviewGateSchema = Type.Object({
78
78
  }, { additionalProperties: false });
79
79
 
80
80
  const AcceptanceOverride = Type.Unsafe({
81
- type: "object",
82
- properties: {
83
- criteria: {
84
- type: "array",
85
- items: {
86
- anyOf: [
87
- { type: "string" },
88
- AcceptanceGateSchema,
89
- ],
81
+ anyOf: [
82
+ { type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
83
+ { const: false },
84
+ {
85
+ type: "object",
86
+ properties: {
87
+ level: { type: "string", enum: ["auto", "none", "attested", "checked", "verified", "reviewed"] },
88
+ criteria: {
89
+ type: "array",
90
+ items: {
91
+ anyOf: [
92
+ { type: "string" },
93
+ AcceptanceGateSchema,
94
+ ],
95
+ },
96
+ },
97
+ evidence: { type: "array", items: AcceptanceEvidenceKind },
98
+ verify: { type: "array", items: AcceptanceVerifyCommandSchema },
99
+ review: {
100
+ anyOf: [
101
+ { const: false },
102
+ AcceptanceReviewGateSchema,
103
+ ],
104
+ },
105
+ stopRules: { type: "array", items: { type: "string" } },
106
+ reason: { type: "string" },
90
107
  },
108
+ additionalProperties: false,
91
109
  },
92
- evidence: { type: "array", items: AcceptanceEvidenceKind },
93
- verify: { type: "array", items: AcceptanceVerifyCommandSchema },
94
- review: AcceptanceReviewGateSchema,
95
- stopRules: { type: "array", items: { type: "string" } },
96
- maxFinalizationTurns: { type: "integer", minimum: 1, maximum: 10 },
97
- },
98
- additionalProperties: false,
99
- description: "Optional acceptance contract. Use this for goal-style requests and for implementation handoffs from plans, PRDs, specs, issues, or broad fixes. Put implementation instructions and plan paths in task; put the definition of done in criteria, proof in evidence/verify, constraints in stopRules, and the bounded loop budget in maxFinalizationTurns. Runtime validation still requires at least one of criteria, evidence, verify, review, or stopRules. When present, the child must complete a same-session self-review/repair loop before acceptance is evaluated.",
110
+ ],
111
+ description: "Optional acceptance policy. Omitted means auto-inferred; verified requires configured runtime commands.",
100
112
  });
101
113
 
102
114
  const TaskItem = Type.Object({
103
- agent: Type.String(),
104
- task: Type.String(),
115
+ agent: Type.String(),
116
+ task: Type.String(),
105
117
  cwd: Type.Optional(Type.String()),
106
118
  count: Type.Optional(Type.Integer({ minimum: 1, description: "Repeat this parallel task N times with the same settings." })),
107
119
  output: Type.Optional(OutputOverride),
@@ -250,12 +262,10 @@ export const SubagentParams = Type.Object({
250
262
  { type: "object", additionalProperties: true },
251
263
  { type: "string" },
252
264
  ],
253
- description: "Agent or chain config for create/update. Agent: name, package (optional namespace; runtime name becomes package.name), description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth, maxExecutionTimeMs, maxTokens. Chain: name, package, description, scope, steps (array of {agent, task?, output?, outputMode?, reads?, model?, skill?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
265
+ description: "Agent or chain config for create/update. Agent: name, package (optional namespace; runtime name becomes package.name), description, scope ('user'|'project', default 'user'), systemPrompt, systemPromptMode, inheritProjectContext, inheritSkills, defaultContext ('fresh'|'fork'), model, tools (comma-separated), extensions (comma-separated), skills (comma-separated), thinking, output, reads, progress, maxSubagentDepth. Chain: name, package, description, scope, steps (array of {agent, task?, output?, outputMode?, reads?, model?, skill?, progress?}). Presence of 'steps' creates a chain instead of an agent. String values must be valid JSON."
254
266
  })),
255
267
  tasks: Type.Optional(Type.Array(TaskItem, { description: "PARALLEL mode: [{agent, task, count?, output?, outputMode?, reads?, progress?}, ...]" })),
256
268
  concurrency: Type.Optional(Type.Integer({ minimum: 1, description: "Top-level PARALLEL mode only: max concurrent tasks. Defaults to config.parallel.concurrency or 4." })),
257
- timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Foreground execution wall-clock timeout in milliseconds. When it expires, running children are soft-interrupted and timed-out results are returned. Foreground only; async/background runs ignore this field." })),
258
- maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias for timeoutMs. Use only one unless both values are identical." })),
259
269
  worktree: Type.Optional(Type.Boolean({
260
270
  description: "Create isolated git worktrees for each parallel task. " +
261
271
  "Prevents filesystem conflicts. Requires clean git state. " +
@@ -17,6 +17,16 @@ function defaultIntercomExtensionDir(agentDir = defaultAgentDir()): string {
17
17
  return path.join(agentDir, "extensions", PI_INTERCOM_PACKAGE_NAME);
18
18
  }
19
19
 
20
+ export const INTERCOM_EXTENSION_DIR_ENV = "PI_INTERCOM_EXTENSION_DIR";
21
+
22
+ // Launcher-provided override for the pi-intercom package directory. Lets a hermetic
23
+ // wrapper point the subagent intercom bridge at a read-only install (e.g. a Nix-store
24
+ // path) instead of seeding the package into the writable agent dir.
25
+ function envIntercomExtensionDir(): string | undefined {
26
+ const dir = process.env[INTERCOM_EXTENSION_DIR_ENV]?.trim();
27
+ return dir ? dir : undefined;
28
+ }
29
+
20
30
  function defaultIntercomConfigPath(agentDir = defaultAgentDir()): string {
21
31
  return path.join(agentDir, "intercom", "config.json");
22
32
  }
@@ -224,7 +234,7 @@ function configuredPiIntercomPackageDir(input: ResolveIntercomBridgeInput, agent
224
234
  }
225
235
 
226
236
  function resolveIntercomExtensionDir(input: ResolveIntercomBridgeInput, agentDir: string): string {
227
- const legacyDir = path.resolve(input.extensionDir ?? defaultIntercomExtensionDir(agentDir));
237
+ const legacyDir = path.resolve(input.extensionDir ?? envIntercomExtensionDir() ?? defaultIntercomExtensionDir(agentDir));
228
238
  if (fs.existsSync(legacyDir)) return legacyDir;
229
239
  return configuredPiIntercomPackageDir(input, agentDir) ?? legacyDir;
230
240
  }
@@ -20,10 +20,8 @@ export function resolveSubagentResultStatus(input: {
20
20
  state?: string;
21
21
  interrupted?: boolean;
22
22
  detached?: boolean;
23
- timedOut?: boolean;
24
23
  }): SubagentResultStatus {
25
24
  if (input.detached) return "detached";
26
- if (input.timedOut || input.state === "timed-out") return "timed-out";
27
25
  if (input.interrupted || input.state === "paused") return "paused";
28
26
  if (typeof input.success === "boolean") return input.success ? "completed" : "failed";
29
27
  if (input.state === "complete") return "completed";
@@ -38,7 +36,6 @@ function countStatuses(children: SubagentResultIntercomChild[]): Record<Subagent
38
36
  failed: 0,
39
37
  paused: 0,
40
38
  detached: 0,
41
- "timed-out": 0,
42
39
  };
43
40
  for (const child of children) {
44
41
  counts[child.status] += 1;
@@ -52,7 +49,6 @@ function formatStatusCounts(counts: Record<SubagentResultStatus, number>): strin
52
49
  counts.failed ? `${counts.failed} failed` : undefined,
53
50
  counts.paused ? `${counts.paused} paused` : undefined,
54
51
  counts.detached ? `${counts.detached} detached` : undefined,
55
- counts["timed-out"] ? `${counts["timed-out"]} timed out` : undefined,
56
52
  ].filter((part): part is string => Boolean(part));
57
53
  return parts.length ? parts.join(", ") : "0 results";
58
54
  }
@@ -60,7 +56,6 @@ function formatStatusCounts(counts: Record<SubagentResultStatus, number>): strin
60
56
  function resolveGroupedStatus(children: SubagentResultIntercomChild[]): SubagentResultStatus {
61
57
  const counts = countStatuses(children);
62
58
  if (counts.failed > 0) return "failed";
63
- if (counts["timed-out"] > 0) return "timed-out";
64
59
  if (counts.paused > 0) return "paused";
65
60
  if (counts.completed > 0) return "completed";
66
61
  if (counts.detached > 0) return "detached";
@@ -17,7 +17,7 @@ import type { RunnerStep } from "../shared/parallel-utils.ts";
17
17
  import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
18
18
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
19
19
  import { resolveChildCwd } from "../../shared/utils.ts";
20
- import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "../shared/model-fallback.ts";
20
+ import { buildModelCandidates, resolveModelCandidate, resolveSubagentModelOverride, type AvailableModelInfo, type ParentModel } from "../shared/model-fallback.ts";
21
21
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
22
22
  import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
23
23
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
@@ -95,6 +95,7 @@ interface AsyncExecutionContext {
95
95
  cwd: string;
96
96
  currentSessionId: string;
97
97
  currentModelProvider?: string;
98
+ currentModel?: ParentModel;
98
99
  }
99
100
 
100
101
  interface AsyncChainParams {
@@ -342,7 +343,8 @@ export function executeAsyncChain(
342
343
  taskTemplate = taskTemplate.replace(/\{chain_dir\}/g, runnerCwd);
343
344
  const task = injectSingleOutputInstruction(`${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`, outputPath);
344
345
 
345
- const primaryModel = resolveModelCandidate(behavior.model ?? a.model, availableModels, ctx.currentModelProvider);
346
+ const requestedModel = behavior.model ?? a.model;
347
+ const primaryModel = resolveSubagentModelOverride(requestedModel, ctx.currentModel, availableModels, ctx.currentModelProvider);
346
348
  const model = applyThinkingSuffix(primaryModel, a.thinking);
347
349
  return {
348
350
  agent: s.agent,
@@ -354,7 +356,7 @@ export function executeAsyncChain(
354
356
  cwd: stepCwd,
355
357
  model,
356
358
  thinking: resolveEffectiveThinking(model, a.thinking),
357
- modelCandidates: buildModelCandidates(behavior.model ?? a.model, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
359
+ modelCandidates: buildModelCandidates(primaryModel, a.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
358
360
  applyThinkingSuffix(candidate, a.thinking),
359
361
  ),
360
362
  tools: a.tools,
@@ -370,8 +372,6 @@ export function executeAsyncChain(
370
372
  outputMode: behavior.outputMode,
371
373
  sessionFile,
372
374
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
373
- maxExecutionTimeMs: a.maxExecutionTimeMs,
374
- maxTokens: a.maxTokens,
375
375
  effectiveAcceptance: resolveEffectiveAcceptance({
376
376
  explicit: s.acceptance,
377
377
  agentName: s.agent,
@@ -438,6 +438,14 @@ export function executeAsyncChain(
438
438
  failFast: s.failFast,
439
439
  phase: s.phase,
440
440
  label: s.label,
441
+ effectiveAcceptance: resolveEffectiveAcceptance({
442
+ explicit: s.acceptance,
443
+ agentName: s.parallel.agent,
444
+ task: s.parallel.task,
445
+ mode: resultMode,
446
+ async: true,
447
+ dynamicGroup: true,
448
+ }),
441
449
  };
442
450
  }
443
451
  return buildSeqStep(s as SequentialStep, nextSessionFile());
@@ -659,10 +667,13 @@ export function executeAsyncSingle(
659
667
  const validationError = validateFileOnlyOutputMode(outputMode, outputPath, `Async single run (${agent})`);
660
668
  if (validationError) return formatAsyncStartError("single", validationError);
661
669
  const taskWithOutputInstruction = injectSingleOutputInstruction(task, outputPath);
662
- const model = applyThinkingSuffix(
663
- resolveModelCandidate(params.modelOverride ?? agentConfig.model, availableModels, ctx.currentModelProvider),
664
- agentConfig.thinking,
670
+ const primaryModel = resolveSubagentModelOverride(
671
+ params.modelOverride ?? agentConfig.model,
672
+ ctx.currentModel,
673
+ availableModels,
674
+ ctx.currentModelProvider,
665
675
  );
676
+ const model = applyThinkingSuffix(primaryModel, agentConfig.thinking);
666
677
  let spawnResult: { pid?: number; error?: string } = {};
667
678
  try {
668
679
  spawnResult = spawnRunner(
@@ -675,7 +686,7 @@ export function executeAsyncSingle(
675
686
  cwd: runnerCwd,
676
687
  model,
677
688
  thinking: resolveEffectiveThinking(model, agentConfig.thinking),
678
- modelCandidates: buildModelCandidates(params.modelOverride ?? agentConfig.model, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
689
+ modelCandidates: buildModelCandidates(primaryModel, agentConfig.fallbackModels, availableModels, ctx.currentModelProvider).map((candidate) =>
679
690
  applyThinkingSuffix(candidate, agentConfig.thinking),
680
691
  ),
681
692
  tools: agentConfig.tools,
@@ -691,8 +702,6 @@ export function executeAsyncSingle(
691
702
  outputMode,
692
703
  sessionFile,
693
704
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
694
- maxExecutionTimeMs: agentConfig.maxExecutionTimeMs,
695
- maxTokens: agentConfig.maxTokens,
696
705
  effectiveAcceptance: resolveEffectiveAcceptance({
697
706
  explicit: params.acceptance,
698
707
  agentName: agent,
@@ -49,11 +49,6 @@ function formatResumeGuidance(runId: string | undefined, children: Array<{ agent
49
49
  return "Resume: unavailable; no child session file was persisted.";
50
50
  }
51
51
 
52
- function formatAcceptanceFinalizationSummary(finalization: NonNullable<NonNullable<AsyncStatus["steps"]>[number]["acceptance"]>["finalization"] | undefined): string {
53
- if (!finalization) return "";
54
- return `, finalization: ${finalization.status} after ${finalization.turns.length}/${finalization.maxTurns} turns`;
55
- }
56
-
57
52
  function stepLineLabel(status: AsyncStatus, index: number): string {
58
53
  const steps = status.steps ?? [];
59
54
  if (status.mode === "parallel") return `Agent ${index + 1}/${steps.length || 1}`;
@@ -222,8 +217,7 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
222
217
  const modelThinking = formatModelThinking(step.model, step.thinking);
223
218
  const modelText = modelThinking ? ` (${modelThinking})` : "";
224
219
  const errorText = step.error ? `, error: ${step.error}` : "";
225
- const finalizationText = formatAcceptanceFinalizationSummary(step.acceptance?.finalization);
226
- const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}${finalizationText}` : "";
220
+ const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}` : "";
227
221
  const display = step.label ? `${step.label} (${step.agent})` : step.agent;
228
222
  const phase = step.phase ? `[${step.phase}] ` : "";
229
223
  lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${acceptanceText}${errorText}`);