@bacnh85/pi-subagent 0.7.1 → 0.8.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/extensions/agents.ts +25 -4
- package/extensions/index.ts +27 -7
- package/extensions/runner.ts +10 -4
- package/extensions/service.ts +8 -2
- package/extensions/thread-viewer.ts +1 -0
- package/package.json +1 -1
package/extensions/agents.ts
CHANGED
|
@@ -48,6 +48,7 @@ interface AgentCache {
|
|
|
48
48
|
scope: AgentScope;
|
|
49
49
|
agents: AgentConfig[];
|
|
50
50
|
projectAgentsDir: string | null;
|
|
51
|
+
diagnostics: AgentDiscoveryDiagnostic[];
|
|
51
52
|
/** File-level signature per directory (name:mtime:size for each .md file) */
|
|
52
53
|
dirSignatures: Map<string, string>;
|
|
53
54
|
}
|
|
@@ -104,7 +105,22 @@ function loadAgentsFromDir(
|
|
|
104
105
|
continue;
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
let frontmatter: Record<string, unknown>;
|
|
111
|
+
let body: string;
|
|
112
|
+
try {
|
|
113
|
+
const parsed = parseFrontmatter<Record<string, unknown>>(content);
|
|
114
|
+
frontmatter = parsed.frontmatter;
|
|
115
|
+
body = parsed.body;
|
|
116
|
+
} catch (err) {
|
|
117
|
+
diagnostics.push({
|
|
118
|
+
filePath,
|
|
119
|
+
issue: `Failed to parse YAML frontmatter: ${err instanceof Error ? err.message : String(err)}`,
|
|
120
|
+
severity: "error",
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
108
124
|
|
|
109
125
|
if (typeof frontmatter.name !== "string" || typeof frontmatter.description !== "string") {
|
|
110
126
|
if (typeof frontmatter.name !== "string" && typeof frontmatter.description !== "string") {
|
|
@@ -225,8 +241,12 @@ function dirSignature(dir: string): string {
|
|
|
225
241
|
.filter((e) => e.name.endsWith(".md") && (e.isFile() || e.isSymbolicLink()))
|
|
226
242
|
.map((e) => {
|
|
227
243
|
const file = path.join(dir, e.name);
|
|
228
|
-
|
|
229
|
-
|
|
244
|
+
try {
|
|
245
|
+
const st = fs.statSync(file);
|
|
246
|
+
return `${e.name}:${st.mtimeMs}:${st.size}`;
|
|
247
|
+
} catch {
|
|
248
|
+
return `${e.name}:broken`;
|
|
249
|
+
}
|
|
230
250
|
})
|
|
231
251
|
.sort();
|
|
232
252
|
return `exists:${entries.join("|")}`;
|
|
@@ -277,7 +297,7 @@ export function discoverAgents(
|
|
|
277
297
|
}
|
|
278
298
|
}
|
|
279
299
|
if (!stale) {
|
|
280
|
-
return { agents: _cache.agents, projectAgentsDir: _cache.projectAgentsDir, diagnostics:
|
|
300
|
+
return { agents: _cache.agents, projectAgentsDir: _cache.projectAgentsDir, diagnostics: _cache.diagnostics };
|
|
281
301
|
}
|
|
282
302
|
// Cache is stale — rebuild below
|
|
283
303
|
_cache = null;
|
|
@@ -316,6 +336,7 @@ export function discoverAgents(
|
|
|
316
336
|
scope,
|
|
317
337
|
agents,
|
|
318
338
|
projectAgentsDir,
|
|
339
|
+
diagnostics,
|
|
319
340
|
dirSignatures,
|
|
320
341
|
};
|
|
321
342
|
|
package/extensions/index.ts
CHANGED
|
@@ -209,11 +209,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
209
209
|
status: isFailedResult(result) ? (result.stopReason === "aborted" ? "aborted" : "failed") : "completed",
|
|
210
210
|
result,
|
|
211
211
|
});
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
try {
|
|
213
|
+
if (isFailedResult(result)) request.respond({ id: request.id, ok: false, error: getResultOutput(result) });
|
|
214
|
+
else request.respond({ id: request.id, ok: true, result });
|
|
215
|
+
} catch { /* respond channel closed */ }
|
|
214
216
|
}, (error) => {
|
|
215
217
|
threadStore.updateThread(thread.id, { status: "failed" });
|
|
216
|
-
|
|
218
|
+
try {
|
|
219
|
+
request.respond({ id: request.id, ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
220
|
+
} catch { /* respond channel closed */ }
|
|
217
221
|
});
|
|
218
222
|
});
|
|
219
223
|
|
|
@@ -230,9 +234,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
230
234
|
const list = formatAgentList(fresh.agents, 20);
|
|
231
235
|
const extra = list.remaining > 0 ? `\n ... +${list.remaining} more` : "";
|
|
232
236
|
const dirs = fresh.projectAgentsDir ? `project: ${fresh.projectAgentsDir}` : "no project agents dir";
|
|
237
|
+
const diagText = fresh.diagnostics.length > 0
|
|
238
|
+
? "\n\nWarnings:\n" + fresh.diagnostics.map(d => ` - [${d.severity}] ${d.filePath}: ${d.issue}`).join("\n")
|
|
239
|
+
: "";
|
|
233
240
|
pi.sendMessage({
|
|
234
241
|
customType: "pi-subagent",
|
|
235
|
-
content: `Agent definitions reloaded.\n\nAvailable agents (${fresh.agents.length}):\n ${list.text}${extra}\n\nDirectories searched:\n user: ${path.join(getAgentDir(), "agents")}\n ${dirs}\n bundled: ${bundledAgentsDir}`,
|
|
242
|
+
content: `Agent definitions reloaded.\n\nAvailable agents (${fresh.agents.length}):\n ${list.text}${extra}${diagText}\n\nDirectories searched:\n user: ${path.join(getAgentDir(), "agents")}\n ${dirs}\n bundled: ${bundledAgentsDir}`,
|
|
236
243
|
display: true,
|
|
237
244
|
});
|
|
238
245
|
ctx.ui.notify("Agent definitions reloaded", "info");
|
|
@@ -244,9 +251,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
244
251
|
const list = formatAgentList(discovery.agents, 20);
|
|
245
252
|
const extra = list.remaining > 0 ? `\n ... +${list.remaining} more` : "";
|
|
246
253
|
const dirs = discovery.projectAgentsDir ? `\n project: ${discovery.projectAgentsDir}` : "";
|
|
254
|
+
const diagText = discovery.diagnostics.length > 0
|
|
255
|
+
? "\n\nWarnings:\n" + discovery.diagnostics.map(d => ` - [${d.severity}] ${d.filePath}: ${d.issue}`).join("\n")
|
|
256
|
+
: "";
|
|
247
257
|
pi.sendMessage({
|
|
248
258
|
customType: "pi-subagent",
|
|
249
|
-
content: `Available agents (${discovery.agents.length}):\n ${list.text}${extra}\n\nScopes searched:\n user: ${path.join(getAgentDir(), "agents")}${dirs}\n bundled: ${bundledAgentsDir}\n\nUse /subagent <name> for agent details, /subagent reload to refresh.`,
|
|
259
|
+
content: `Available agents (${discovery.agents.length}):\n ${list.text}${extra}${diagText}\n\nScopes searched:\n user: ${path.join(getAgentDir(), "agents")}${dirs}\n bundled: ${bundledAgentsDir}\n\nUse /subagent <name> for agent details, /subagent reload to refresh.`,
|
|
250
260
|
display: true,
|
|
251
261
|
});
|
|
252
262
|
return;
|
|
@@ -283,9 +293,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
283
293
|
const list = formatAgentList(discovery.agents, 20);
|
|
284
294
|
const extra = list.remaining > 0 ? `\n ... +${list.remaining} more` : "";
|
|
285
295
|
const dirs = discovery.projectAgentsDir ? `\n project: ${discovery.projectAgentsDir}` : "";
|
|
296
|
+
const diagText = discovery.diagnostics.length > 0
|
|
297
|
+
? "\n\nWarnings:\n" + discovery.diagnostics.map(d => ` - [${d.severity}] ${d.filePath}: ${d.issue}`).join("\n")
|
|
298
|
+
: "";
|
|
286
299
|
pi.sendMessage({
|
|
287
300
|
customType: "pi-subagent",
|
|
288
|
-
content: `Available agents (${discovery.agents.length}):\n ${list.text}${extra}\n\nScopes searched:\n user: ${path.join(getAgentDir(), "agents")}${dirs}\n bundled: ${bundledAgentsDir}\n\nUse /subagent <name> for agent details, /subagent reload to refresh.`,
|
|
301
|
+
content: `Available agents (${discovery.agents.length}):\n ${list.text}${extra}${diagText}\n\nScopes searched:\n user: ${path.join(getAgentDir(), "agents")}${dirs}\n bundled: ${bundledAgentsDir}\n\nUse /subagent <name> for agent details, /subagent reload to refresh.`,
|
|
289
302
|
display: true,
|
|
290
303
|
});
|
|
291
304
|
},
|
|
@@ -512,6 +525,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
512
525
|
agent: agentName,
|
|
513
526
|
task,
|
|
514
527
|
exitCode: 1,
|
|
528
|
+
status: "error",
|
|
529
|
+
stopReason: "error",
|
|
515
530
|
messages: [],
|
|
516
531
|
stderr: `Unknown agent: "${agentName}". Available: ${available}.`,
|
|
517
532
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
@@ -527,6 +542,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
527
542
|
agent: agentName,
|
|
528
543
|
task,
|
|
529
544
|
exitCode: 1,
|
|
545
|
+
status: "error",
|
|
546
|
+
stopReason: "error",
|
|
530
547
|
messages: [],
|
|
531
548
|
stderr: `Model not found for agent "${agentName}". Tried: ${tried}. Parent model: ${parentInfo}. Check agent definition and pi model configuration.`,
|
|
532
549
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
@@ -550,6 +567,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
550
567
|
agent: agentName,
|
|
551
568
|
task,
|
|
552
569
|
exitCode: 1,
|
|
570
|
+
status: "error",
|
|
571
|
+
stopReason: "error",
|
|
553
572
|
messages: [],
|
|
554
573
|
stderr: `Validation error: ${errorMsg}`,
|
|
555
574
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
@@ -583,7 +602,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
583
602
|
|
|
584
603
|
for (let i = 0; i < params.chain.length; i++) {
|
|
585
604
|
const step = params.chain[i];
|
|
586
|
-
const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
|
|
605
|
+
const taskWithContext = step.task.replace(/\{previous\}/g, () => previousOutput);
|
|
587
606
|
|
|
588
607
|
const thread = threadStore.createThread({
|
|
589
608
|
agentName: step.agent,
|
|
@@ -725,6 +744,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
725
744
|
agent: t.agent,
|
|
726
745
|
task: t.task,
|
|
727
746
|
exitCode: 1,
|
|
747
|
+
status: "error",
|
|
728
748
|
messages: [],
|
|
729
749
|
stderr: "",
|
|
730
750
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
package/extensions/runner.ts
CHANGED
|
@@ -148,6 +148,8 @@ export async function runSubAgent(options: {
|
|
|
148
148
|
result.stopReason = isTimeout ? "timeout" : "aborted";
|
|
149
149
|
result.errorMessage = combinedSignal.reason instanceof Error ? combinedSignal.reason.message : "Sub-agent aborted before start";
|
|
150
150
|
result.status = classifyStopReason(result.stopReason, !isTimeout, isTimeout);
|
|
151
|
+
cleanupCombined?.();
|
|
152
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
151
153
|
return result;
|
|
152
154
|
}
|
|
153
155
|
|
|
@@ -167,6 +169,7 @@ export async function runSubAgent(options: {
|
|
|
167
169
|
let cleanupEventAbort: (() => void) | undefined;
|
|
168
170
|
let abortedBySignal = false;
|
|
169
171
|
let timedOut = false;
|
|
172
|
+
let eventUnsubscribe: (() => void) | undefined;
|
|
170
173
|
|
|
171
174
|
try {
|
|
172
175
|
// Wire combined abort signal to session
|
|
@@ -191,7 +194,8 @@ export async function runSubAgent(options: {
|
|
|
191
194
|
fn();
|
|
192
195
|
};
|
|
193
196
|
|
|
194
|
-
|
|
197
|
+
let unsubscribe: (() => void) | undefined;
|
|
198
|
+
unsubscribe = session.subscribe((event) => {
|
|
195
199
|
try {
|
|
196
200
|
switch (event.type) {
|
|
197
201
|
case "message_end": {
|
|
@@ -223,7 +227,7 @@ export async function runSubAgent(options: {
|
|
|
223
227
|
result.messages = event.messages as unknown as Message[];
|
|
224
228
|
}
|
|
225
229
|
finish(() => {
|
|
226
|
-
unsubscribe();
|
|
230
|
+
unsubscribe?.();
|
|
227
231
|
resolve();
|
|
228
232
|
});
|
|
229
233
|
break;
|
|
@@ -231,18 +235,19 @@ export async function runSubAgent(options: {
|
|
|
231
235
|
}
|
|
232
236
|
} catch (err) {
|
|
233
237
|
finish(() => {
|
|
234
|
-
unsubscribe();
|
|
238
|
+
unsubscribe?.();
|
|
235
239
|
reject(err);
|
|
236
240
|
});
|
|
237
241
|
}
|
|
238
242
|
});
|
|
243
|
+
eventUnsubscribe = unsubscribe;
|
|
239
244
|
|
|
240
245
|
// Resolve on abort so the eventPromise doesn't hang
|
|
241
246
|
const onAbortResolve = () => {
|
|
242
247
|
finish(() => {
|
|
243
248
|
result.exitCode = 1;
|
|
244
249
|
if (!result.errorMessage) result.errorMessage = "Sub-agent aborted";
|
|
245
|
-
unsubscribe();
|
|
250
|
+
unsubscribe?.();
|
|
246
251
|
resolve();
|
|
247
252
|
});
|
|
248
253
|
};
|
|
@@ -279,6 +284,7 @@ export async function runSubAgent(options: {
|
|
|
279
284
|
cleanupAbort?.();
|
|
280
285
|
cleanupEventAbort?.();
|
|
281
286
|
cleanupCombined();
|
|
287
|
+
eventUnsubscribe?.();
|
|
282
288
|
if (timeoutId) clearTimeout(timeoutId);
|
|
283
289
|
try {
|
|
284
290
|
session.dispose();
|
package/extensions/service.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
normalizeTimeout,
|
|
8
8
|
resolveSafeCwd,
|
|
9
9
|
MAX_INSTRUCTIONS_LENGTH,
|
|
10
|
+
READ_ONLY_TOOLS,
|
|
10
11
|
} from "./security.ts";
|
|
11
12
|
|
|
12
13
|
export const SUBAGENT_REQUEST_EVENT = "pi-subagent:run";
|
|
@@ -53,8 +54,13 @@ export async function runNamedAgent(options: {
|
|
|
53
54
|
const effectiveTimeoutMs = normalizeTimeout({ requested: options.timeout }).timeoutMs;
|
|
54
55
|
|
|
55
56
|
// Security: validate tools against allowlist.
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
let rawTools = options.agent.tools ?? ["read", "bash", "edit", "write", "grep", "find", "ls"];
|
|
58
|
+
// Enforce read-only sandbox: strip mutating and execution tools
|
|
59
|
+
if (options.agent.sandbox === "read-only") {
|
|
60
|
+
rawTools = rawTools.filter(t => READ_ONLY_TOOLS.includes(t));
|
|
61
|
+
if (rawTools.length === 0) rawTools = [...READ_ONLY_TOOLS];
|
|
62
|
+
}
|
|
63
|
+
const toolValidation = validateAgentTools({ tools: rawTools, readOnly: options.agent.sandbox === "read-only" });
|
|
58
64
|
if (toolValidation.errors.length > 0) {
|
|
59
65
|
throw new Error(`Tool validation errors for agent "${options.agent.name}": ${toolValidation.errors.join("; ")}`);
|
|
60
66
|
}
|
|
@@ -226,6 +226,7 @@ export class ThreadViewer {
|
|
|
226
226
|
? Math.max(0, total - (maxVisible - 1))
|
|
227
227
|
: 0;
|
|
228
228
|
const offset = Math.max(0, Math.min(this.scrollOffset, maxOffset));
|
|
229
|
+
this.scrollOffset = offset;
|
|
229
230
|
|
|
230
231
|
// Reserve space for scroll indicators
|
|
231
232
|
const aboveShown = offset > 0;
|
package/package.json
CHANGED