@mystilleef/pi-subagent 0.7.0 → 0.9.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 +26 -5
- package/package.json +10 -11
- package/src/agent/agent-cache.ts +48 -20
- package/src/agent/agents.ts +14 -9
- package/src/child/child-events.ts +20 -20
- package/src/child/process.ts +100 -49
- package/src/child/termination.ts +4 -4
- package/src/env.d.ts +13 -0
- package/src/orchestration/run-registry.ts +1 -2
- package/src/orchestration/subagent-orchestrator.ts +12 -7
- package/src/progress/progress-state.ts +42 -39
- package/src/progress/progress.ts +11 -4
- package/src/progress/result-details.ts +55 -11
- package/src/shared/types.ts +16 -16
- package/src/shared/utils.ts +80 -13
- package/tsconfig.json +9 -11
package/README.md
CHANGED
|
@@ -130,8 +130,9 @@ delegation.
|
|
|
130
130
|
- `task`: task prompt for the child agent.
|
|
131
131
|
- `agentScope`: optional lookup scope, one of `user`, `project`, or
|
|
132
132
|
`both`.
|
|
133
|
-
- `debug`: optional flag that
|
|
134
|
-
|
|
133
|
+
- `debug`: optional flag that requests child diagnostic details. Full child
|
|
134
|
+
messages and raw internals require `PI_SUBAGENT_DEBUG_ENABLED=1` in the
|
|
135
|
+
host environment.
|
|
135
136
|
|
|
136
137
|
## Security
|
|
137
138
|
|
|
@@ -143,6 +144,12 @@ executable automation.
|
|
|
143
144
|
|
|
144
145
|
- Review project-local agents before running them.
|
|
145
146
|
- Avoid delegating secrets unless the agent and tools need them.
|
|
147
|
+
- Treat child-agent prompts, tool arguments, stderr, and debug transcripts as
|
|
148
|
+
potentially sensitive.
|
|
149
|
+
- Enable debug details only for trusted investigations. `debug: true` or
|
|
150
|
+
`/run --debug` can expose child conversation transcripts, termination
|
|
151
|
+
internals, and stderr only when the host explicitly sets
|
|
152
|
+
`PI_SUBAGENT_DEBUG_ENABLED=1`.
|
|
146
153
|
- Prefer trusted repositories for shared agent definitions.
|
|
147
154
|
- Remember that child agents can call their configured tools.
|
|
148
155
|
|
|
@@ -150,12 +157,26 @@ executable automation.
|
|
|
150
157
|
|
|
151
158
|
**Environment variables:**
|
|
152
159
|
|
|
153
|
-
- `PI_SUBAGENT_DEPTH`: nested subagent depth
|
|
154
|
-
|
|
160
|
+
- `PI_SUBAGENT_DEPTH`: current nested subagent depth counter set internally
|
|
161
|
+
for child processes.
|
|
162
|
+
- `PI_SUBAGENT_MAX_DEPTH`: max nested subagent depth. Default: `3`. Values
|
|
163
|
+
above `10` clamp to the internal ceiling `10`; deeper nesting increases
|
|
164
|
+
cost, latency, and runaway delegation risk.
|
|
165
|
+
- `PI_SUBAGENT_AGENT_END_GRACE_MS`: child process grace period after
|
|
166
|
+
`agent_end` before forced termination. Default: `250`.
|
|
167
|
+
- `PI_SUBAGENT_MAX_STDERR_BYTES`: max captured child stderr bytes. Default:
|
|
168
|
+
`10000`.
|
|
155
169
|
- `PI_SUBAGENT_MAX_OUTPUT_BYTES`: max returned output bytes. Default:
|
|
156
170
|
`50000`.
|
|
157
171
|
- `PI_SUBAGENT_MAX_OUTPUT_LINES`: max returned output lines. Default:
|
|
158
172
|
`500`.
|
|
173
|
+
- `PI_SUBAGENT_DEBUG_ENABLED`: debug detail authorization. Set to `1` to
|
|
174
|
+
allow `debug: true` or `/run --debug` to include sanitized child messages,
|
|
175
|
+
termination internals, and stderr; unset values keep non-debug detail
|
|
176
|
+
behavior.
|
|
177
|
+
|
|
178
|
+
Limit variables parse as positive integers. Empty, zero, negative, decimal,
|
|
179
|
+
`Infinity`, and non-numeric values fall back to defaults.
|
|
159
180
|
|
|
160
181
|
## Troubleshooting
|
|
161
182
|
|
|
@@ -173,7 +194,7 @@ executable automation.
|
|
|
173
194
|
|
|
174
195
|
**Nested subagent blocked:**
|
|
175
196
|
|
|
176
|
-
- Nested delegation hits the `
|
|
197
|
+
- Nested delegation hits the `PI_SUBAGENT_MAX_DEPTH` safety limit.
|
|
177
198
|
- Run the child task directly from the parent session instead.
|
|
178
199
|
|
|
179
200
|
**Truncated output:**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mystilleef/pi-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Pi subagent for the SPAE Framework",
|
|
5
5
|
"author": "Lateef Alabi-Oki <mystilleef@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,12 +48,11 @@
|
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"typecheck": "tsc --noEmit",
|
|
51
|
-
"lint": "biome check --error-on-warnings .",
|
|
52
|
-
"fix": "biome check --write --unsafe .",
|
|
51
|
+
"lint": "biome check --write --unsafe --error-on-warnings .",
|
|
53
52
|
"migrate": "biome migrate --write",
|
|
54
|
-
"coverage": "bun test --
|
|
53
|
+
"coverage": "bun test --parallel --parallel-delay=0",
|
|
55
54
|
"check": "bun lint && bun typecheck",
|
|
56
|
-
"verify": "bun migrate && bun
|
|
55
|
+
"verify": "bun migrate && bun check && bun coverage",
|
|
57
56
|
"pack:smoke": "bun scripts/pack-smoke.ts",
|
|
58
57
|
"release": "sh -c 'npm version \"$1\" -m \"chore(release): %s\" && git push --follow-tags' --"
|
|
59
58
|
},
|
|
@@ -66,13 +65,13 @@
|
|
|
66
65
|
},
|
|
67
66
|
"devDependencies": {
|
|
68
67
|
"@biomejs/biome": "^2.4.16",
|
|
69
|
-
"@earendil-works/pi-agent-core": "^0.78.
|
|
70
|
-
"@earendil-works/pi-ai": "^0.78.
|
|
71
|
-
"@earendil-works/pi-coding-agent": "^0.78.
|
|
72
|
-
"@earendil-works/pi-tui": "^0.78.
|
|
68
|
+
"@earendil-works/pi-agent-core": "^0.78.1",
|
|
69
|
+
"@earendil-works/pi-ai": "^0.78.1",
|
|
70
|
+
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
71
|
+
"@earendil-works/pi-tui": "^0.78.1",
|
|
73
72
|
"@types/bun": "^1.3.14",
|
|
74
|
-
"@types/node": "^25.9.
|
|
75
|
-
"typebox": "^1.
|
|
73
|
+
"@types/node": "^25.9.2",
|
|
74
|
+
"typebox": "^1.2.2",
|
|
76
75
|
"typescript": "^6.0.3"
|
|
77
76
|
}
|
|
78
77
|
}
|
package/src/agent/agent-cache.ts
CHANGED
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
discoverAgentsAsync,
|
|
10
10
|
emptyScopeResult,
|
|
11
11
|
getUserAgentsDir,
|
|
12
|
-
|
|
12
|
+
readMarkdownDirWithStatusAsync,
|
|
13
13
|
} from "./agents.js";
|
|
14
14
|
|
|
15
15
|
interface AgentDiscoveryScopeSnapshot {
|
|
16
16
|
markdownFiles: string[];
|
|
17
17
|
fileHashes: Record<string, string | null>;
|
|
18
|
+
listingTrusted: boolean;
|
|
19
|
+
directory: string | null;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
type AgentDiscoverySnapshots = Record<AgentSource, AgentDiscoveryScopeSnapshot>;
|
|
@@ -52,7 +54,12 @@ function getFreshCacheEntry(
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function emptySnapshot(): AgentDiscoveryScopeSnapshot {
|
|
55
|
-
return {
|
|
57
|
+
return {
|
|
58
|
+
markdownFiles: [],
|
|
59
|
+
fileHashes: {},
|
|
60
|
+
listingTrusted: false,
|
|
61
|
+
directory: null,
|
|
62
|
+
};
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
function cloneScopeSnapshot(
|
|
@@ -62,6 +69,8 @@ function cloneScopeSnapshot(
|
|
|
62
69
|
return {
|
|
63
70
|
markdownFiles: [...snapshot.markdownFiles],
|
|
64
71
|
fileHashes: { ...snapshot.fileHashes },
|
|
72
|
+
listingTrusted: snapshot.listingTrusted,
|
|
73
|
+
directory: snapshot.directory,
|
|
65
74
|
};
|
|
66
75
|
}
|
|
67
76
|
|
|
@@ -71,14 +80,17 @@ function equalStringSets(a: string[], b: string[]): boolean {
|
|
|
71
80
|
return b.every((value) => set.has(value));
|
|
72
81
|
}
|
|
73
82
|
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
right: AgentDiscoveryScopeSnapshot,
|
|
83
|
+
function snapshotHasRequiredMetadata(
|
|
84
|
+
snapshot: AgentDiscoveryScopeSnapshot,
|
|
77
85
|
): boolean {
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
if (typeof snapshot.listingTrusted !== "boolean") return false;
|
|
87
|
+
if (snapshot.directory !== null && typeof snapshot.directory !== "string")
|
|
88
|
+
return false;
|
|
89
|
+
if (
|
|
90
|
+
!equalStringSets(snapshot.markdownFiles, Object.keys(snapshot.fileHashes))
|
|
91
|
+
)
|
|
92
|
+
return false;
|
|
93
|
+
return true;
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
function scopeAgentsMatchListing(
|
|
@@ -118,16 +130,20 @@ async function hashMarkdownFileAsync(
|
|
|
118
130
|
async function buildScopeSnapshotAsync(
|
|
119
131
|
dir: string | null,
|
|
120
132
|
): Promise<AgentDiscoveryScopeSnapshot> {
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
const markdownFiles = entries.map((entry) => entry.name);
|
|
133
|
+
const listing = await readMarkdownDirWithStatusAsync(dir);
|
|
134
|
+
const markdownFiles = listing.entries.map((entry) => entry.name);
|
|
124
135
|
const hashPairs = await Promise.all(
|
|
125
136
|
markdownFiles.map(
|
|
126
137
|
async (fileName) =>
|
|
127
|
-
[fileName, await hashMarkdownFileAsync(dir, fileName)] as const,
|
|
138
|
+
[fileName, await hashMarkdownFileAsync(dir ?? "", fileName)] as const,
|
|
128
139
|
),
|
|
129
140
|
);
|
|
130
|
-
return {
|
|
141
|
+
return {
|
|
142
|
+
markdownFiles,
|
|
143
|
+
fileHashes: Object.fromEntries(hashPairs),
|
|
144
|
+
listingTrusted: listing.ok,
|
|
145
|
+
directory: dir ? path.resolve(dir) : null,
|
|
146
|
+
};
|
|
131
147
|
}
|
|
132
148
|
|
|
133
149
|
async function buildCacheSnapshotsAsync(
|
|
@@ -146,13 +162,22 @@ async function canTrustDerivedScopeAsync(
|
|
|
146
162
|
): Promise<boolean> {
|
|
147
163
|
const dir =
|
|
148
164
|
source === "user" ? getUserAgentsDir() : bothEntry.projectAgentsDir;
|
|
165
|
+
const resolvedDir = dir ? path.resolve(dir) : null;
|
|
149
166
|
const scopeResult = bothEntry.scopes[source];
|
|
150
167
|
const cachedSnapshot = bothEntry.snapshots?.[source];
|
|
151
168
|
if (!cachedSnapshot) return false;
|
|
169
|
+
if (!snapshotHasRequiredMetadata(cachedSnapshot)) return false;
|
|
170
|
+
if (!cachedSnapshot.listingTrusted) return false;
|
|
171
|
+
if (cachedSnapshot.directory !== resolvedDir) return false;
|
|
152
172
|
if (!scopeAgentsMatchListing(scopeResult, source, dir)) return false;
|
|
153
173
|
if (!equalStringSets(scopeResult.markdownFiles, cachedSnapshot.markdownFiles))
|
|
154
174
|
return false;
|
|
155
|
-
|
|
175
|
+
const listing = await readMarkdownDirWithStatusAsync(dir);
|
|
176
|
+
if (!listing.ok) return false;
|
|
177
|
+
return equalStringSets(
|
|
178
|
+
cachedSnapshot.markdownFiles,
|
|
179
|
+
listing.entries.map((entry) => entry.name),
|
|
180
|
+
);
|
|
156
181
|
}
|
|
157
182
|
|
|
158
183
|
function buildSourceRecord<T>(
|
|
@@ -220,11 +245,14 @@ async function primeScopedCacheEntriesAsync(
|
|
|
220
245
|
ctx: CacheOperationContext,
|
|
221
246
|
bothEntry: AgentDiscoveryCacheEntry,
|
|
222
247
|
): Promise<void> {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
248
|
+
const missingSources = AGENT_SOURCES.filter(
|
|
249
|
+
(source) => !getFreshCacheEntry(ctx, cacheKey(ctx.cwd, source)),
|
|
250
|
+
);
|
|
251
|
+
await Promise.all(
|
|
252
|
+
missingSources.map((source) =>
|
|
253
|
+
deriveOrDiscoverScopedEntryAsync(ctx, source, bothEntry),
|
|
254
|
+
),
|
|
255
|
+
);
|
|
228
256
|
}
|
|
229
257
|
|
|
230
258
|
export async function getCachedAgentDiscovery(
|
package/src/agent/agents.ts
CHANGED
|
@@ -20,9 +20,9 @@ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
|
|
|
20
20
|
export interface AgentConfig {
|
|
21
21
|
name: string;
|
|
22
22
|
description: string;
|
|
23
|
-
tools?: string[];
|
|
24
|
-
skills?: string[];
|
|
25
|
-
thinking?: ThinkingLevel;
|
|
23
|
+
tools?: string[] | undefined;
|
|
24
|
+
skills?: string[] | undefined;
|
|
25
|
+
thinking?: ThinkingLevel | undefined;
|
|
26
26
|
systemPrompt: string;
|
|
27
27
|
source: AgentSource;
|
|
28
28
|
filePath: string;
|
|
@@ -143,7 +143,7 @@ async function loadAgentsFromDirAsync(
|
|
|
143
143
|
dir: string,
|
|
144
144
|
source: AgentSource,
|
|
145
145
|
): Promise<AgentDiscoveryScopeResult> {
|
|
146
|
-
const markdownEntries = await
|
|
146
|
+
const markdownEntries = (await readMarkdownDirWithStatusAsync(dir)).entries;
|
|
147
147
|
const markdownFiles = markdownEntries.map((entry) => entry.name);
|
|
148
148
|
const parsedAgents = await Promise.all(
|
|
149
149
|
markdownEntries.map((entry) =>
|
|
@@ -162,15 +162,20 @@ export function isMarkdownDirent(entry: Dirent): boolean {
|
|
|
162
162
|
);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
export
|
|
165
|
+
export interface MarkdownDirListing {
|
|
166
|
+
entries: Dirent[];
|
|
167
|
+
ok: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function readMarkdownDirWithStatusAsync(
|
|
166
171
|
dir: string | null,
|
|
167
|
-
): Promise<
|
|
168
|
-
if (!dir) return [];
|
|
172
|
+
): Promise<MarkdownDirListing> {
|
|
173
|
+
if (!dir) return { entries: [], ok: true };
|
|
169
174
|
try {
|
|
170
175
|
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
171
|
-
return entries.filter(isMarkdownDirent);
|
|
176
|
+
return { entries: entries.filter(isMarkdownDirent), ok: true };
|
|
172
177
|
} catch {
|
|
173
|
-
return [];
|
|
178
|
+
return { entries: [], ok: false };
|
|
174
179
|
}
|
|
175
180
|
}
|
|
176
181
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { makeToolPreview } from "../output/normalize.js";
|
|
2
2
|
import type { ToolActivity } from "../shared/types.js";
|
|
3
3
|
|
|
4
|
-
// Extracts results[0] from details; null-safe for malformed input.
|
|
5
4
|
function tryFirstResult(details: unknown): Record<string, unknown> | null {
|
|
6
5
|
try {
|
|
7
6
|
if (typeof details !== "object" || details === null) return null;
|
|
8
|
-
const results = (details as Record<string, unknown>)
|
|
7
|
+
const results = (details as Record<string, unknown>)["results"];
|
|
9
8
|
if (!Array.isArray(results) || results.length === 0) return null;
|
|
10
9
|
const nested = results[0];
|
|
11
10
|
if (typeof nested !== "object" || nested === null) return null;
|
|
@@ -15,7 +14,6 @@ function tryFirstResult(details: unknown): Record<string, unknown> | null {
|
|
|
15
14
|
}
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
// Malformed details (null result) falls back to { toolName }.
|
|
19
17
|
function parseToolActivity(
|
|
20
18
|
toolName: string,
|
|
21
19
|
partialResult: { content?: unknown; details?: unknown },
|
|
@@ -24,23 +22,25 @@ function parseToolActivity(
|
|
|
24
22
|
if (!nestedRecord) return { toolName, inputSummary: toolName };
|
|
25
23
|
const isSubagent = toolName === "subagent";
|
|
26
24
|
const activity: ToolActivity = { toolName };
|
|
27
|
-
const agent =
|
|
25
|
+
const agent =
|
|
26
|
+
typeof nestedRecord["agent"] === "string" && nestedRecord["agent"];
|
|
28
27
|
activity.inputSummary =
|
|
29
28
|
isSubagent && agent ? makeToolPreview(toolName, nestedRecord) : toolName;
|
|
30
29
|
if (
|
|
31
|
-
typeof nestedRecord
|
|
32
|
-
nestedRecord
|
|
30
|
+
typeof nestedRecord["instanceName"] === "string" &&
|
|
31
|
+
nestedRecord["instanceName"]
|
|
33
32
|
) {
|
|
34
|
-
activity.instanceName = nestedRecord
|
|
33
|
+
activity.instanceName = nestedRecord["instanceName"] as string;
|
|
35
34
|
}
|
|
36
|
-
const progress = nestedRecord
|
|
35
|
+
const progress = nestedRecord["progress"];
|
|
37
36
|
if (typeof progress === "object" && progress !== null) {
|
|
38
|
-
const activeToolActivity = (progress as Record<string, unknown>)
|
|
39
|
-
|
|
37
|
+
const activeToolActivity = (progress as Record<string, unknown>)[
|
|
38
|
+
"activeToolActivity"
|
|
39
|
+
];
|
|
40
40
|
if (
|
|
41
41
|
typeof activeToolActivity === "object" &&
|
|
42
42
|
activeToolActivity !== null &&
|
|
43
|
-
typeof (activeToolActivity as Record<string, unknown>)
|
|
43
|
+
typeof (activeToolActivity as Record<string, unknown>)["toolName"] ===
|
|
44
44
|
"string"
|
|
45
45
|
) {
|
|
46
46
|
const childActivity = activeToolActivity as ToolActivity;
|
|
@@ -96,21 +96,21 @@ export function parseChildEventLine(line: string): ChildEventParseResult {
|
|
|
96
96
|
typeof event === "object" &&
|
|
97
97
|
event !== null &&
|
|
98
98
|
"type" in event &&
|
|
99
|
-
typeof (event as Record<string, unknown>)
|
|
100
|
-
KNOWN_TYPES.has((event as Record<string, unknown>)
|
|
99
|
+
typeof (event as Record<string, unknown>)["type"] === "string" &&
|
|
100
|
+
KNOWN_TYPES.has((event as Record<string, unknown>)["type"] as string)
|
|
101
101
|
) {
|
|
102
102
|
const record = event as Record<string, unknown>;
|
|
103
|
-
if (record
|
|
103
|
+
if (record["type"] === TOOL_EXECUTION_UPDATE_EVENT) {
|
|
104
104
|
if (
|
|
105
|
-
typeof record
|
|
106
|
-
typeof record
|
|
107
|
-
record
|
|
105
|
+
typeof record["toolName"] !== "string" ||
|
|
106
|
+
typeof record["partialResult"] !== "object" ||
|
|
107
|
+
record["partialResult"] === null
|
|
108
108
|
) {
|
|
109
109
|
return { kind: "unknown", event };
|
|
110
110
|
}
|
|
111
|
-
record
|
|
112
|
-
record
|
|
113
|
-
record
|
|
111
|
+
record["toolActivity"] = parseToolActivity(
|
|
112
|
+
record["toolName"] as string,
|
|
113
|
+
record["partialResult"] as { content?: unknown; details?: unknown },
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
return { kind: "known", event: event as ChildKnownEvent };
|