@narumitw/pi-subagents 0.49.3 → 0.51.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 (81) hide show
  1. package/README.md +313 -53
  2. package/package.json +10 -7
  3. package/src/adaptive-scheduler.ts +196 -0
  4. package/src/admission-benchmark.ts +95 -0
  5. package/src/admission-policy.ts +78 -0
  6. package/src/agent-projection.ts +53 -0
  7. package/src/agents.ts +58 -1
  8. package/src/auto-transport.ts +114 -0
  9. package/src/blocking-status.ts +63 -0
  10. package/src/capabilities.ts +145 -0
  11. package/src/capability-grant.ts +115 -0
  12. package/src/capability-router.ts +107 -0
  13. package/src/completion-delivery.ts +257 -0
  14. package/src/config-status.ts +221 -0
  15. package/src/config-ui.ts +215 -236
  16. package/src/consult-resources.ts +4 -27
  17. package/src/consult.ts +9 -1
  18. package/src/create-stateful-transport.ts +55 -0
  19. package/src/delegation-contract.ts +417 -0
  20. package/src/execution-plan.ts +322 -0
  21. package/src/execution-profiles.ts +95 -0
  22. package/src/execution-ui.ts +320 -0
  23. package/src/execution.ts +848 -158
  24. package/src/in-process-transport.ts +269 -25
  25. package/src/inspect-render.ts +101 -1
  26. package/src/inspect.ts +296 -3
  27. package/src/integration-controller.ts +98 -0
  28. package/src/limits.ts +3 -0
  29. package/src/orchestration-metrics.ts +78 -0
  30. package/src/outcome.ts +61 -0
  31. package/src/panel-child-group.ts +35 -0
  32. package/src/panel-contract.ts +343 -0
  33. package/src/panel-evidence.ts +59 -0
  34. package/src/panel-execution.ts +772 -0
  35. package/src/panel-failure.ts +56 -0
  36. package/src/panel-planning.ts +175 -0
  37. package/src/panel-prompts.ts +132 -0
  38. package/src/panel-reconciliation.ts +57 -0
  39. package/src/panel-render.ts +103 -0
  40. package/src/parallel-limit-ui.ts +112 -0
  41. package/src/params.ts +172 -3
  42. package/src/persistence.ts +182 -32
  43. package/src/prompt-resources.ts +38 -0
  44. package/src/registry-types.ts +175 -0
  45. package/src/registry.ts +466 -143
  46. package/src/render.ts +72 -6
  47. package/src/result-contract.ts +416 -0
  48. package/src/retained-semantic-state.ts +100 -0
  49. package/src/rpc-timeout-finalization.ts +207 -0
  50. package/src/rpc-transport-metadata.ts +65 -0
  51. package/src/rpc-transport.ts +990 -0
  52. package/src/rpc-turn-capture.ts +142 -0
  53. package/src/runner-result.ts +55 -0
  54. package/src/runner-usage.ts +48 -0
  55. package/src/runner.ts +325 -73
  56. package/src/semantic-snapshot.ts +214 -0
  57. package/src/settings.ts +254 -35
  58. package/src/spawn-idempotency.ts +61 -0
  59. package/src/stateful-config.ts +13 -0
  60. package/src/stateful-guidance.ts +1 -0
  61. package/src/stateful-lifecycle.ts +45 -2
  62. package/src/stateful-limit-ui.ts +246 -0
  63. package/src/stateful-limits.ts +96 -0
  64. package/src/stateful-prompt.ts +11 -2
  65. package/src/stateful-render.ts +48 -3
  66. package/src/stateful.ts +467 -357
  67. package/src/subagents.ts +114 -46
  68. package/src/subprocess-transport.ts +64 -5
  69. package/src/supervision.ts +103 -0
  70. package/src/timeout-checkpoint.ts +305 -0
  71. package/src/timeout-finalization.ts +75 -0
  72. package/src/transport-types.ts +68 -0
  73. package/src/transport-ui.ts +169 -0
  74. package/src/transport.ts +16 -4
  75. package/src/turn-budget.ts +109 -0
  76. package/src/verification-policy.ts +17 -0
  77. package/src/work-item-ledger.ts +682 -0
  78. package/src/work-item-persistence.ts +218 -0
  79. package/src/workflow-planning.ts +150 -0
  80. package/src/workflow-ui.ts +61 -0
  81. package/src/workspace.ts +69 -12
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-subagents",
3
- "version": "0.49.3",
3
+ "version": "0.51.0",
4
4
  "description": "Pi extension for delegating work to specialized isolated subagents.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,6 +23,9 @@
23
23
  "./src/index.ts"
24
24
  ]
25
25
  },
26
+ "piExtension": {
27
+ "lifecycle": "stable"
28
+ },
26
29
  "scripts": {
27
30
  "check": "biome check . && npm run typecheck",
28
31
  "format": "biome check --write .",
@@ -37,11 +40,11 @@
37
40
  },
38
41
  "devDependencies": {
39
42
  "@biomejs/biome": "2.5.7",
40
- "@earendil-works/pi-agent-core": "0.83.0",
41
- "@earendil-works/pi-ai": "0.83.0",
42
- "@earendil-works/pi-coding-agent": "0.83.0",
43
- "@earendil-works/pi-tui": "0.83.0",
44
- "typebox": "1.3.10",
43
+ "@earendil-works/pi-agent-core": "0.84.1",
44
+ "@earendil-works/pi-ai": "0.84.1",
45
+ "@earendil-works/pi-coding-agent": "0.84.1",
46
+ "@earendil-works/pi-tui": "0.84.1",
47
+ "typebox": "1.3.11",
45
48
  "typescript": "7.0.2"
46
49
  },
47
50
  "dependencies": {
@@ -51,6 +54,6 @@
51
54
  "repository": {
52
55
  "type": "git",
53
56
  "url": "https://github.com/narumiruna/pi-extensions",
54
- "directory": "extensions/pi-subagents"
57
+ "directory": "packages/pi-subagents"
55
58
  }
56
59
  }
@@ -0,0 +1,196 @@
1
+ import * as path from "node:path";
2
+ import type { WorkItemLedgerSnapshot, WorkItemRecord } from "./work-item-ledger.js";
3
+
4
+ export const ADAPTIVE_SCHEDULER_POLICY = "dependency-aware-v1" as const;
5
+
6
+ export type SchedulingReason =
7
+ | "selected"
8
+ | "dependency-not-ready"
9
+ | "state-not-ready"
10
+ | "budget-exhausted"
11
+ | "capacity-exhausted"
12
+ | "scope-conflict";
13
+
14
+ export interface SchedulingDecisionItem {
15
+ id: string;
16
+ reason: SchedulingReason;
17
+ criticalPathDepth: number;
18
+ }
19
+
20
+ export interface SchedulingDecision {
21
+ policy: typeof ADAPTIVE_SCHEDULER_POLICY;
22
+ workflowId: string;
23
+ workflowGeneration: number;
24
+ effectiveConcurrency: number;
25
+ selected: string[];
26
+ decisions: SchedulingDecisionItem[];
27
+ }
28
+
29
+ export interface AdaptiveSchedulerOptions {
30
+ maxConcurrency: number;
31
+ activeCount: number;
32
+ transportCapacity: number;
33
+ remainingBudgetMs: number;
34
+ activeReadPaths?: string[];
35
+ activeWritePaths?: string[];
36
+ activeOwnershipKeys?: string[];
37
+ activeMutatingCount?: number;
38
+ maxMutatingConcurrency?: number;
39
+ }
40
+
41
+ export class AdaptiveScheduler {
42
+ decide(snapshot: WorkItemLedgerSnapshot, options: AdaptiveSchedulerOptions): SchedulingDecision {
43
+ validateOptions(options);
44
+ const depth = criticalPathDepths(snapshot.items);
45
+ const ready = snapshot.items
46
+ .filter((item) => item.state === "ready")
47
+ .sort(
48
+ (left, right) =>
49
+ (depth.get(right.id) ?? 0) - (depth.get(left.id) ?? 0) || left.id.localeCompare(right.id),
50
+ );
51
+ const availableSlots = Math.max(
52
+ 0,
53
+ Math.min(
54
+ options.maxConcurrency - options.activeCount,
55
+ options.transportCapacity - options.activeCount,
56
+ ready.length,
57
+ ),
58
+ );
59
+ const effectiveConcurrency = options.remainingBudgetMs > 0 ? availableSlots : 0;
60
+ const selected: string[] = [];
61
+ let mutatingCount = options.activeMutatingCount ?? 0;
62
+ const maxMutatingConcurrency = options.maxMutatingConcurrency ?? 2;
63
+ const selectedReadPaths = normalizedScopes(options.activeReadPaths ?? []);
64
+ const selectedWritePaths = normalizedScopes(options.activeWritePaths ?? []);
65
+ const selectedOwnership = new Set(options.activeOwnershipKeys ?? []);
66
+ const reasons = new Map<string, SchedulingReason>();
67
+ for (const item of ready) {
68
+ if (options.remainingBudgetMs <= 0) {
69
+ reasons.set(item.id, "budget-exhausted");
70
+ continue;
71
+ }
72
+ if (
73
+ selected.length >= effectiveConcurrency ||
74
+ (item.sideEffectPolicy !== "read-only" && mutatingCount >= maxMutatingConcurrency)
75
+ ) {
76
+ reasons.set(item.id, "capacity-exhausted");
77
+ continue;
78
+ }
79
+ if (hasConflict(item, selectedReadPaths, selectedWritePaths, selectedOwnership)) {
80
+ reasons.set(item.id, "scope-conflict");
81
+ continue;
82
+ }
83
+ selected.push(item.id);
84
+ if (item.sideEffectPolicy !== "read-only") mutatingCount++;
85
+ reasons.set(item.id, "selected");
86
+ for (const scope of normalizedScopes(item.readPaths)) selectedReadPaths.add(scope);
87
+ for (const scope of normalizedScopes(item.writePaths)) selectedWritePaths.add(scope);
88
+ for (const key of item.ownershipKeys) selectedOwnership.add(key);
89
+ }
90
+ const decisions = snapshot.items
91
+ .map((item) => ({
92
+ id: item.id,
93
+ reason:
94
+ reasons.get(item.id) ??
95
+ (item.state === "pending" ? "dependency-not-ready" : "state-not-ready"),
96
+ criticalPathDepth: depth.get(item.id) ?? 0,
97
+ }))
98
+ .sort((left, right) => left.id.localeCompare(right.id));
99
+ return {
100
+ policy: ADAPTIVE_SCHEDULER_POLICY,
101
+ workflowId: snapshot.workflowId,
102
+ workflowGeneration: snapshot.generation,
103
+ effectiveConcurrency,
104
+ selected,
105
+ decisions,
106
+ };
107
+ }
108
+ }
109
+
110
+ function hasConflict(
111
+ item: WorkItemRecord,
112
+ readPaths: ReadonlySet<string>,
113
+ writePaths: ReadonlySet<string>,
114
+ ownership: ReadonlySet<string>,
115
+ ): boolean {
116
+ const candidateReads = normalizedScopes(item.readPaths);
117
+ const candidateWrites = normalizedScopes(item.writePaths);
118
+ return (
119
+ [...candidateWrites].some(
120
+ (scope) => overlapsAny(scope, readPaths) || overlapsAny(scope, writePaths),
121
+ ) ||
122
+ [...candidateReads].some((scope) => overlapsAny(scope, writePaths)) ||
123
+ item.ownershipKeys.some((key) => ownership.has(key))
124
+ );
125
+ }
126
+
127
+ function normalizedScopes(values: readonly string[]): Set<string> {
128
+ return new Set(values.map(normalizeScope));
129
+ }
130
+
131
+ function normalizeScope(value: string): string {
132
+ const normalized = path.posix.normalize(value.trim().replace(/\\+/gu, "/"));
133
+ if (
134
+ normalized === "." ||
135
+ normalized === ".." ||
136
+ normalized.startsWith("../") ||
137
+ normalized.startsWith("/") ||
138
+ /^[A-Za-z]:\//u.test(normalized)
139
+ ) {
140
+ return "";
141
+ }
142
+ return normalized.replace(/^\.\//u, "").replace(/\/$/u, "");
143
+ }
144
+
145
+ function overlapsAny(scope: string, candidates: ReadonlySet<string>): boolean {
146
+ return [...candidates].some((candidate) => scopesOverlap(scope, candidate));
147
+ }
148
+
149
+ function scopesOverlap(left: string, right: string): boolean {
150
+ if (!left || !right || left === right) return true;
151
+ return left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
152
+ }
153
+
154
+ function criticalPathDepths(items: WorkItemRecord[]): Map<string, number> {
155
+ const byId = new Map(items.map((item) => [item.id, item]));
156
+ const memo = new Map<string, number>();
157
+ const visit = (id: string): number => {
158
+ const cached = memo.get(id);
159
+ if (cached !== undefined) return cached;
160
+ const item = byId.get(id);
161
+ if (!item || item.dependents.length === 0) {
162
+ memo.set(id, 0);
163
+ return 0;
164
+ }
165
+ const value = 1 + Math.max(...item.dependents.map(visit));
166
+ memo.set(id, value);
167
+ return value;
168
+ };
169
+ for (const item of items) visit(item.id);
170
+ return memo;
171
+ }
172
+
173
+ function validateOptions(options: AdaptiveSchedulerOptions): void {
174
+ for (const [name, value] of [
175
+ ["maxConcurrency", options.maxConcurrency],
176
+ ["activeCount", options.activeCount],
177
+ ["transportCapacity", options.transportCapacity],
178
+ ] as const) {
179
+ if (!Number.isSafeInteger(value) || value < (name === "activeCount" ? 0 : 1)) {
180
+ throw new Error(
181
+ `${name} must be a ${name === "activeCount" ? "non-negative" : "positive"} safe integer`,
182
+ );
183
+ }
184
+ }
185
+ for (const [name, value] of [
186
+ ["activeMutatingCount", options.activeMutatingCount ?? 0],
187
+ ["maxMutatingConcurrency", options.maxMutatingConcurrency ?? 2],
188
+ ] as const) {
189
+ if (!Number.isSafeInteger(value) || value < (name === "activeMutatingCount" ? 0 : 1)) {
190
+ throw new Error(`${name} has an invalid bound`);
191
+ }
192
+ }
193
+ if (!Number.isFinite(options.remainingBudgetMs) || options.remainingBudgetMs < 0) {
194
+ throw new Error("remainingBudgetMs must be a non-negative finite number");
195
+ }
196
+ }
@@ -0,0 +1,95 @@
1
+ export const ADMISSION_BENCHMARK_VERSION = "pi-subagents:admission-benchmark:v1" as const;
2
+
3
+ export const ADMISSION_BENCHMARK_ARMS = [
4
+ "strong-single",
5
+ "one-child",
6
+ "equal-budget-best-of-n",
7
+ "naive-parallel",
8
+ "fixed-two-child",
9
+ "admission-selected",
10
+ ] as const;
11
+
12
+ export type AdmissionBenchmarkArm = (typeof ADMISSION_BENCHMARK_ARMS)[number];
13
+
14
+ export interface AdmissionBenchmarkProtocol {
15
+ version: typeof ADMISSION_BENCHMARK_VERSION;
16
+ model: string;
17
+ evaluator: string;
18
+ taskIds: string[];
19
+ pairedSeeds: number[];
20
+ maxTokens: number;
21
+ maxCost: number;
22
+ maxWallClockMs: number;
23
+ maxRetries: number;
24
+ maxMutatingChildren: number;
25
+ maxRecursiveDepth: number;
26
+ arms: AdmissionBenchmarkArm[];
27
+ }
28
+
29
+ export interface AdmissionBenchmarkAdapter {
30
+ arm: AdmissionBenchmarkArm;
31
+ model: string;
32
+ evaluator: string;
33
+ maxTokens: number;
34
+ maxCost: number;
35
+ maxWallClockMs: number;
36
+ maxRetries: number;
37
+ mutatingChildren: number;
38
+ recursiveDepth: number;
39
+ }
40
+
41
+ export interface AdmissionBenchmarkDryRun {
42
+ version: typeof ADMISSION_BENCHMARK_VERSION;
43
+ pairedInstances: number;
44
+ arms: AdmissionBenchmarkArm[];
45
+ valid: true;
46
+ }
47
+
48
+ export function validateAdmissionBenchmark(
49
+ protocol: AdmissionBenchmarkProtocol,
50
+ adapters: AdmissionBenchmarkAdapter[],
51
+ ): AdmissionBenchmarkDryRun {
52
+ if (protocol.version !== ADMISSION_BENCHMARK_VERSION) {
53
+ throw new Error("Unsupported admission benchmark protocol");
54
+ }
55
+ if (protocol.taskIds.length < 1 || protocol.pairedSeeds.length < 2) {
56
+ throw new Error("Admission benchmark requires tasks and repeated paired seeds");
57
+ }
58
+ if (
59
+ protocol.maxMutatingChildren < 0 ||
60
+ protocol.maxMutatingChildren > 2 ||
61
+ protocol.maxRecursiveDepth !== 0
62
+ ) {
63
+ throw new Error("Admission benchmark permits at most two mutating children and no recursion");
64
+ }
65
+ const expectedArms = [...new Set(protocol.arms)].sort();
66
+ if (expectedArms.length !== ADMISSION_BENCHMARK_ARMS.length) {
67
+ throw new Error("Admission benchmark must declare every comparison arm");
68
+ }
69
+ for (const arm of ADMISSION_BENCHMARK_ARMS) {
70
+ const adapter = adapters.find((candidate) => candidate.arm === arm);
71
+ if (!adapter) throw new Error(`Missing admission benchmark adapter: ${arm}`);
72
+ if (
73
+ adapter.model !== protocol.model ||
74
+ adapter.evaluator !== protocol.evaluator ||
75
+ adapter.maxTokens !== protocol.maxTokens ||
76
+ adapter.maxCost !== protocol.maxCost ||
77
+ adapter.maxWallClockMs !== protocol.maxWallClockMs ||
78
+ adapter.maxRetries !== protocol.maxRetries
79
+ ) {
80
+ throw new Error(`Admission benchmark adapter ${arm} violates matched resources`);
81
+ }
82
+ if (
83
+ adapter.mutatingChildren > protocol.maxMutatingChildren ||
84
+ adapter.recursiveDepth > protocol.maxRecursiveDepth
85
+ ) {
86
+ throw new Error(`Admission benchmark adapter ${arm} exceeds width or depth`);
87
+ }
88
+ }
89
+ return {
90
+ version: ADMISSION_BENCHMARK_VERSION,
91
+ pairedInstances: protocol.taskIds.length * protocol.pairedSeeds.length,
92
+ arms: [...ADMISSION_BENCHMARK_ARMS],
93
+ valid: true,
94
+ };
95
+ }
@@ -0,0 +1,78 @@
1
+ export const DELEGATION_ADMISSION_VERSION = "pi-subagents:admission:v1" as const;
2
+
3
+ export type DelegationAdmissionRecommendation =
4
+ | "parent-owned-direct"
5
+ | "one-child"
6
+ | "one-child-plus-verification"
7
+ | "bounded-two-child"
8
+ | "abstain-insufficient-evidence";
9
+
10
+ export interface DelegationAdmissionInput {
11
+ contextPressure: "low" | "medium" | "high";
12
+ independentWorkItems: number;
13
+ coupling: "dense" | "sparse";
14
+ verificationRequired: boolean;
15
+ verificationAvailable: boolean;
16
+ capabilitiesSupported: boolean;
17
+ budgetAllowsChildren: boolean;
18
+ generationCurrent: boolean;
19
+ requirementsComplete: boolean;
20
+ }
21
+
22
+ export interface DelegationAdmissionDecision {
23
+ version: typeof DELEGATION_ADMISSION_VERSION;
24
+ recommendation: DelegationAdmissionRecommendation;
25
+ reasonCodes: string[];
26
+ benefitHypothesis: string;
27
+ auditOnly: true;
28
+ }
29
+
30
+ export function evaluateDelegationAdmission(
31
+ input: DelegationAdmissionInput,
32
+ ): DelegationAdmissionDecision {
33
+ if (!input.generationCurrent)
34
+ return decision("abstain-insufficient-evidence", ["stale-generation"]);
35
+ if (!input.requirementsComplete) {
36
+ return decision("abstain-insufficient-evidence", ["requirements-incomplete"]);
37
+ }
38
+ if (!input.capabilitiesSupported) {
39
+ return decision("abstain-insufficient-evidence", ["capability-unsupported"]);
40
+ }
41
+ if (!input.budgetAllowsChildren) {
42
+ return decision("parent-owned-direct", ["child-budget-insufficient"]);
43
+ }
44
+ if (input.independentWorkItems >= 2 && input.coupling === "sparse") {
45
+ return decision(
46
+ "bounded-two-child",
47
+ ["declared-independent-work"],
48
+ "parallel-critical-path-reduction",
49
+ );
50
+ }
51
+ if (input.verificationRequired) {
52
+ return input.verificationAvailable
53
+ ? decision(
54
+ "one-child-plus-verification",
55
+ ["independent-verification-required"],
56
+ "independent-error-detection",
57
+ )
58
+ : decision("abstain-insufficient-evidence", ["verification-unavailable"]);
59
+ }
60
+ if (input.contextPressure === "high") {
61
+ return decision("one-child", ["declared-context-pressure"], "context-isolation");
62
+ }
63
+ return decision("parent-owned-direct", ["delegation-benefit-not-established"]);
64
+ }
65
+
66
+ function decision(
67
+ recommendation: DelegationAdmissionRecommendation,
68
+ reasonCodes: string[],
69
+ benefitHypothesis = "none",
70
+ ): DelegationAdmissionDecision {
71
+ return {
72
+ version: DELEGATION_ADMISSION_VERSION,
73
+ recommendation,
74
+ reasonCodes,
75
+ benefitHypothesis,
76
+ auditOnly: true,
77
+ };
78
+ }
@@ -0,0 +1,53 @@
1
+ import type { ManagedAgent } from "./registry.js";
2
+
3
+ export interface AgentProjectionLimits {
4
+ maxAgents: number;
5
+ maxDepth?: number;
6
+ }
7
+
8
+ /**
9
+ * Select the newest restorable agent trees without orphaning child records.
10
+ *
11
+ * Complete ancestor chains are selected together, then returned in their original
12
+ * order so persistence and restore behavior stay deterministic.
13
+ */
14
+ export function projectAgentRecords(
15
+ agents: readonly ManagedAgent[],
16
+ limits: AgentProjectionLimits,
17
+ ): ManagedAgent[] {
18
+ const byId = new Map(agents.map((agent) => [agent.id, agent]));
19
+ const selected = new Set<string>();
20
+ const maxDepth = limits.maxDepth ?? Number.MAX_SAFE_INTEGER;
21
+ const newestFirst = agents
22
+ .map((agent, index) => ({ agent, index }))
23
+ .sort(
24
+ (left, right) => right.agent.updatedAt - left.agent.updatedAt || right.index - left.index,
25
+ );
26
+
27
+ for (const { agent } of newestFirst) {
28
+ const chain = ancestorChain(agent, byId);
29
+ if (!chain || chain.length - 1 > maxDepth) continue;
30
+ const missing = chain.filter((candidate) => !selected.has(candidate.id));
31
+ if (selected.size + missing.length > limits.maxAgents) continue;
32
+ for (const candidate of missing) selected.add(candidate.id);
33
+ }
34
+
35
+ return agents.filter((agent) => selected.has(agent.id));
36
+ }
37
+
38
+ function ancestorChain(
39
+ agent: ManagedAgent,
40
+ byId: ReadonlyMap<string, ManagedAgent>,
41
+ ): ManagedAgent[] | undefined {
42
+ const chain: ManagedAgent[] = [];
43
+ const seen = new Set<string>();
44
+ let current: ManagedAgent | undefined = agent;
45
+ while (current) {
46
+ if (seen.has(current.id)) return undefined;
47
+ seen.add(current.id);
48
+ chain.unshift(current);
49
+ if (!current.parentId) return chain;
50
+ current = byId.get(current.parentId);
51
+ }
52
+ return undefined;
53
+ }
package/src/agents.ts CHANGED
@@ -5,6 +5,11 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as path from "node:path";
7
7
  import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
8
+ import {
9
+ type AgentCapabilityManifest,
10
+ CAPABILITY_MANIFEST_VERSION,
11
+ normalizeCapabilityManifest,
12
+ } from "./capabilities.js";
8
13
 
9
14
  export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
10
15
 
@@ -18,6 +23,12 @@ export type AgentScope = "user" | "project" | "both";
18
23
 
19
24
  export type AgentSource = "built-in" | "user" | "project";
20
25
 
26
+ export const DEFAULT_PI_TOOL_NAMES = ["read", "bash", "edit", "write"] as const;
27
+
28
+ export function resolveAgentToolNames(tools: readonly string[] | undefined): string[] {
29
+ return [...new Set(tools ?? DEFAULT_PI_TOOL_NAMES)];
30
+ }
31
+
21
32
  export interface AgentConfig {
22
33
  name: string;
23
34
  description: string;
@@ -25,6 +36,7 @@ export interface AgentConfig {
25
36
  model?: string;
26
37
  thinkingLevel?: SubagentThinkingLevel;
27
38
  timeoutMs?: number;
39
+ capabilityManifest?: AgentCapabilityManifest;
28
40
  systemPrompt: string;
29
41
  source: AgentSource;
30
42
  filePath: string;
@@ -37,7 +49,7 @@ export interface SubagentAgentConfig {
37
49
  timeoutMs?: number | null;
38
50
  }
39
51
 
40
- export type SubagentTransportKind = "subprocess" | "in-process";
52
+ export type SubagentTransportKind = "subprocess" | "in-process" | "rpc" | "auto";
41
53
 
42
54
  export type CompletionDelivery = "next-turn" | "auto-resume";
43
55
 
@@ -66,6 +78,7 @@ export interface SubagentCwdPolicySettings {
66
78
 
67
79
  export interface SubagentBlockingSettings {
68
80
  enabled?: boolean;
81
+ maxParallelTasks?: number;
69
82
  }
70
83
 
71
84
  export interface SubagentRuntimeSettings {
@@ -97,6 +110,9 @@ const BUILT_IN_AGENTS: AgentConfig[] = [
97
110
  description:
98
111
  "Read-only codebase reconnaissance; returns concise findings with paths and evidence.",
99
112
  tools: ["read", "grep", "find", "ls", "bash"],
113
+ capabilityManifest: builtInManifest(["repository-search", "code-evidence"], "read", [
114
+ "evidence-gathering",
115
+ ]),
100
116
  source: "built-in",
101
117
  filePath: "built-in:scout",
102
118
  systemPrompt: [
@@ -109,6 +125,10 @@ const BUILT_IN_AGENTS: AgentConfig[] = [
109
125
  name: "planner",
110
126
  description: "Turns reconnaissance into a lean implementation or migration plan.",
111
127
  tools: ["read", "grep", "find", "ls"],
128
+ capabilityManifest: builtInManifest(
129
+ ["task-decomposition", "implementation-planning", "migration-planning"],
130
+ "read",
131
+ ),
112
132
  source: "built-in",
113
133
  filePath: "built-in:planner",
114
134
  systemPrompt: [
@@ -121,6 +141,11 @@ const BUILT_IN_AGENTS: AgentConfig[] = [
121
141
  name: "reviewer",
122
142
  description: "Independent code review agent that inspects existing verification evidence.",
123
143
  tools: ["read", "grep", "find", "ls", "bash"],
144
+ capabilityManifest: builtInManifest(
145
+ ["code-review", "evidence-review", "security-baseline"],
146
+ "read",
147
+ ["independent-review"],
148
+ ),
124
149
  source: "built-in",
125
150
  filePath: "built-in:reviewer",
126
151
  systemPrompt: [
@@ -133,6 +158,10 @@ const BUILT_IN_AGENTS: AgentConfig[] = [
133
158
  {
134
159
  name: "worker",
135
160
  description: "General-purpose implementation worker with the default Pi tool set.",
161
+ capabilityManifest: builtInManifest(
162
+ ["implementation", "command-execution", "repository-modification"],
163
+ "write",
164
+ ),
136
165
  source: "built-in",
137
166
  filePath: "built-in:worker",
138
167
  systemPrompt: workerSystemPrompt(),
@@ -140,6 +169,10 @@ const BUILT_IN_AGENTS: AgentConfig[] = [
140
169
  {
141
170
  name: "general",
142
171
  description: "Alias for worker; kept for model-generated subagent names.",
172
+ capabilityManifest: builtInManifest(
173
+ ["implementation", "command-execution", "repository-modification"],
174
+ "write",
175
+ ),
143
176
  source: "built-in",
144
177
  filePath: "built-in:general",
145
178
  systemPrompt: workerSystemPrompt(),
@@ -147,12 +180,35 @@ const BUILT_IN_AGENTS: AgentConfig[] = [
147
180
  {
148
181
  name: "general-purpose",
149
182
  description: "Alias for worker; compatible with common subagent naming conventions.",
183
+ capabilityManifest: builtInManifest(
184
+ ["implementation", "command-execution", "repository-modification"],
185
+ "write",
186
+ ),
150
187
  source: "built-in",
151
188
  filePath: "built-in:general-purpose",
152
189
  systemPrompt: workerSystemPrompt(),
153
190
  },
154
191
  ];
155
192
 
193
+ function builtInManifest(
194
+ capabilities: string[],
195
+ filesystem: "read" | "write",
196
+ verificationRoles: string[] = [],
197
+ ): AgentCapabilityManifest {
198
+ return {
199
+ version: CAPABILITY_MANIFEST_VERSION,
200
+ capabilities,
201
+ modalities: ["text"],
202
+ resultFormats: ["text", "structured-v1", "structured-v2"],
203
+ authority: { filesystem },
204
+ verificationRoles,
205
+ contextStrengths: ["repository"],
206
+ costHint: filesystem === "read" ? "low" : "medium",
207
+ latencyHint: filesystem === "read" ? "low" : "medium",
208
+ limitations: [],
209
+ };
210
+ }
211
+
156
212
  function workerSystemPrompt(): string {
157
213
  return [
158
214
  "You are a focused worker subagent running in an isolated Pi process.",
@@ -296,6 +352,7 @@ function loadAgentsFromDir(
296
352
  thinkingLevel: isThinkingLevel(frontmatter.thinkingLevel)
297
353
  ? frontmatter.thinkingLevel
298
354
  : undefined,
355
+ capabilityManifest: normalizeCapabilityManifest(frontmatter.capabilityManifest),
299
356
  systemPrompt: body,
300
357
  source,
301
358
  filePath,