@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.
- package/README.md +313 -53
- package/package.json +10 -7
- package/src/adaptive-scheduler.ts +196 -0
- package/src/admission-benchmark.ts +95 -0
- package/src/admission-policy.ts +78 -0
- package/src/agent-projection.ts +53 -0
- package/src/agents.ts +58 -1
- package/src/auto-transport.ts +114 -0
- package/src/blocking-status.ts +63 -0
- package/src/capabilities.ts +145 -0
- package/src/capability-grant.ts +115 -0
- package/src/capability-router.ts +107 -0
- package/src/completion-delivery.ts +257 -0
- package/src/config-status.ts +221 -0
- package/src/config-ui.ts +215 -236
- package/src/consult-resources.ts +4 -27
- package/src/consult.ts +9 -1
- package/src/create-stateful-transport.ts +55 -0
- package/src/delegation-contract.ts +417 -0
- package/src/execution-plan.ts +322 -0
- package/src/execution-profiles.ts +95 -0
- package/src/execution-ui.ts +320 -0
- package/src/execution.ts +848 -158
- package/src/in-process-transport.ts +269 -25
- package/src/inspect-render.ts +101 -1
- package/src/inspect.ts +296 -3
- package/src/integration-controller.ts +98 -0
- package/src/limits.ts +3 -0
- package/src/orchestration-metrics.ts +78 -0
- package/src/outcome.ts +61 -0
- package/src/panel-child-group.ts +35 -0
- package/src/panel-contract.ts +343 -0
- package/src/panel-evidence.ts +59 -0
- package/src/panel-execution.ts +772 -0
- package/src/panel-failure.ts +56 -0
- package/src/panel-planning.ts +175 -0
- package/src/panel-prompts.ts +132 -0
- package/src/panel-reconciliation.ts +57 -0
- package/src/panel-render.ts +103 -0
- package/src/parallel-limit-ui.ts +112 -0
- package/src/params.ts +172 -3
- package/src/persistence.ts +182 -32
- package/src/prompt-resources.ts +38 -0
- package/src/registry-types.ts +175 -0
- package/src/registry.ts +466 -143
- package/src/render.ts +72 -6
- package/src/result-contract.ts +416 -0
- package/src/retained-semantic-state.ts +100 -0
- package/src/rpc-timeout-finalization.ts +207 -0
- package/src/rpc-transport-metadata.ts +65 -0
- package/src/rpc-transport.ts +990 -0
- package/src/rpc-turn-capture.ts +142 -0
- package/src/runner-result.ts +55 -0
- package/src/runner-usage.ts +48 -0
- package/src/runner.ts +325 -73
- package/src/semantic-snapshot.ts +214 -0
- package/src/settings.ts +254 -35
- package/src/spawn-idempotency.ts +61 -0
- package/src/stateful-config.ts +13 -0
- package/src/stateful-guidance.ts +1 -0
- package/src/stateful-lifecycle.ts +45 -2
- package/src/stateful-limit-ui.ts +246 -0
- package/src/stateful-limits.ts +96 -0
- package/src/stateful-prompt.ts +11 -2
- package/src/stateful-render.ts +48 -3
- package/src/stateful.ts +467 -357
- package/src/subagents.ts +114 -46
- package/src/subprocess-transport.ts +64 -5
- package/src/supervision.ts +103 -0
- package/src/timeout-checkpoint.ts +305 -0
- package/src/timeout-finalization.ts +75 -0
- package/src/transport-types.ts +68 -0
- package/src/transport-ui.ts +169 -0
- package/src/transport.ts +16 -4
- package/src/turn-budget.ts +109 -0
- package/src/verification-policy.ts +17 -0
- package/src/work-item-ledger.ts +682 -0
- package/src/work-item-persistence.ts +218 -0
- package/src/workflow-planning.ts +150 -0
- package/src/workflow-ui.ts +61 -0
- 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.
|
|
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.
|
|
41
|
-
"@earendil-works/pi-ai": "0.
|
|
42
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
43
|
-
"@earendil-works/pi-tui": "0.
|
|
44
|
-
"typebox": "1.3.
|
|
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": "
|
|
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,
|