@bacnh85/pi-subagent 0.7.1 → 0.8.1
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/CHANGELOG.md +6 -0
- package/README.md +2 -0
- package/agents/reviewer.md +4 -2
- package/extensions/agents.ts +25 -4
- package/extensions/index.ts +57 -25
- package/extensions/runner.ts +16 -4
- package/extensions/service.ts +8 -2
- package/extensions/thread-viewer.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.1 (2026-07-15)
|
|
4
|
+
|
|
5
|
+
### Review handoff
|
|
6
|
+
|
|
7
|
+
- Reviewer findings now require reproduction or evidence, expected behavior, and acceptance criteria so implementation agents receive self-contained actionable issues.
|
|
8
|
+
|
|
3
9
|
## 0.6.0 (2026-07-12)
|
|
4
10
|
|
|
5
11
|
### Security (breaking changes)
|
package/README.md
CHANGED
|
@@ -87,6 +87,7 @@ Every child execution receives a timeout:
|
|
|
87
87
|
- **Maximum:** 60 minutes (`MAX_TIMEOUT_MS`)
|
|
88
88
|
- Timeout values must be positive integers within the allowed range.
|
|
89
89
|
- Timeout errors are distinguishable from parent cancellation.
|
|
90
|
+
- Progress heartbeats keep the parent transport active during quiet model work; explicit timeouts remain hard deadlines.
|
|
90
91
|
- Parallel tasks and chain steps may have per-item timeouts.
|
|
91
92
|
|
|
92
93
|
### Output safety
|
|
@@ -158,6 +159,7 @@ The raw `stopReason` from the Pi SDK is preserved in the result.
|
|
|
158
159
|
- **Parent cancellation:** Aborting the parent tool call cancels all children.
|
|
159
160
|
- **Sibling cancellation:** In parallel mode with `abortOnFailure: true`, the first failed task cancels running siblings.
|
|
160
161
|
- **Timeout vs. abort:** Timeout errors set `status: "timeout"` and `stopReason: "timeout"`; parent cancellation sets `status: "aborted"`.
|
|
162
|
+
- **Transport idle:** The parent tool receives periodic progress heartbeats while a child runs; these do not extend its timeout.
|
|
161
163
|
|
|
162
164
|
## Extension contract
|
|
163
165
|
|
package/agents/reviewer.md
CHANGED
|
@@ -9,7 +9,7 @@ sandbox: read-only
|
|
|
9
9
|
|
|
10
10
|
You are an independent senior code reviewer. Inspect the requested Git scope with read-only tools.
|
|
11
11
|
|
|
12
|
-
Focus only on actionable issues introduced by the reviewed change:
|
|
12
|
+
Focus only on actionable issues introduced by the reviewed change. Return each confirmed finding as one self-contained issue another agent can fix without redoing the review:
|
|
13
13
|
1. Correctness and edge cases
|
|
14
14
|
2. Security and data loss
|
|
15
15
|
3. Regressions and API compatibility
|
|
@@ -27,8 +27,10 @@ Return JSON only:
|
|
|
27
27
|
"file": "relative/path",
|
|
28
28
|
"line": 1,
|
|
29
29
|
"issue": "what is wrong and why it matters",
|
|
30
|
-
"evidence": "specific inspected code evidence",
|
|
30
|
+
"evidence": "reproduction steps or specific inspected code evidence",
|
|
31
|
+
"expectedBehavior": "what should happen instead",
|
|
31
32
|
"suggestedFix": "smallest safe fix",
|
|
33
|
+
"acceptanceCriteria": "observable pass conditions and exact verification checks",
|
|
32
34
|
"blocking": true
|
|
33
35
|
}
|
|
34
36
|
]
|
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
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
isFailedResult,
|
|
39
39
|
mapWithConcurrencyLimit,
|
|
40
40
|
runSubAgent,
|
|
41
|
+
startHeartbeat,
|
|
41
42
|
} from "./runner.ts";
|
|
42
43
|
import {
|
|
43
44
|
normalizeTimeout,
|
|
@@ -209,11 +210,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
209
210
|
status: isFailedResult(result) ? (result.stopReason === "aborted" ? "aborted" : "failed") : "completed",
|
|
210
211
|
result,
|
|
211
212
|
});
|
|
212
|
-
|
|
213
|
-
|
|
213
|
+
try {
|
|
214
|
+
if (isFailedResult(result)) request.respond({ id: request.id, ok: false, error: getResultOutput(result) });
|
|
215
|
+
else request.respond({ id: request.id, ok: true, result });
|
|
216
|
+
} catch { /* respond channel closed */ }
|
|
214
217
|
}, (error) => {
|
|
215
218
|
threadStore.updateThread(thread.id, { status: "failed" });
|
|
216
|
-
|
|
219
|
+
try {
|
|
220
|
+
request.respond({ id: request.id, ok: false, error: error instanceof Error ? error.message : String(error) });
|
|
221
|
+
} catch { /* respond channel closed */ }
|
|
217
222
|
});
|
|
218
223
|
});
|
|
219
224
|
|
|
@@ -230,9 +235,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
230
235
|
const list = formatAgentList(fresh.agents, 20);
|
|
231
236
|
const extra = list.remaining > 0 ? `\n ... +${list.remaining} more` : "";
|
|
232
237
|
const dirs = fresh.projectAgentsDir ? `project: ${fresh.projectAgentsDir}` : "no project agents dir";
|
|
238
|
+
const diagText = fresh.diagnostics.length > 0
|
|
239
|
+
? "\n\nWarnings:\n" + fresh.diagnostics.map(d => ` - [${d.severity}] ${d.filePath}: ${d.issue}`).join("\n")
|
|
240
|
+
: "";
|
|
233
241
|
pi.sendMessage({
|
|
234
242
|
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}`,
|
|
243
|
+
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
244
|
display: true,
|
|
237
245
|
});
|
|
238
246
|
ctx.ui.notify("Agent definitions reloaded", "info");
|
|
@@ -244,9 +252,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
244
252
|
const list = formatAgentList(discovery.agents, 20);
|
|
245
253
|
const extra = list.remaining > 0 ? `\n ... +${list.remaining} more` : "";
|
|
246
254
|
const dirs = discovery.projectAgentsDir ? `\n project: ${discovery.projectAgentsDir}` : "";
|
|
255
|
+
const diagText = discovery.diagnostics.length > 0
|
|
256
|
+
? "\n\nWarnings:\n" + discovery.diagnostics.map(d => ` - [${d.severity}] ${d.filePath}: ${d.issue}`).join("\n")
|
|
257
|
+
: "";
|
|
247
258
|
pi.sendMessage({
|
|
248
259
|
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.`,
|
|
260
|
+
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
261
|
display: true,
|
|
251
262
|
});
|
|
252
263
|
return;
|
|
@@ -283,9 +294,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
283
294
|
const list = formatAgentList(discovery.agents, 20);
|
|
284
295
|
const extra = list.remaining > 0 ? `\n ... +${list.remaining} more` : "";
|
|
285
296
|
const dirs = discovery.projectAgentsDir ? `\n project: ${discovery.projectAgentsDir}` : "";
|
|
297
|
+
const diagText = discovery.diagnostics.length > 0
|
|
298
|
+
? "\n\nWarnings:\n" + discovery.diagnostics.map(d => ` - [${d.severity}] ${d.filePath}: ${d.issue}`).join("\n")
|
|
299
|
+
: "";
|
|
286
300
|
pi.sendMessage({
|
|
287
301
|
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.`,
|
|
302
|
+
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
303
|
display: true,
|
|
290
304
|
});
|
|
291
305
|
},
|
|
@@ -502,6 +516,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
502
516
|
parentSignal?: AbortSignal,
|
|
503
517
|
timeoutMs?: number,
|
|
504
518
|
onProgress?: (partial: SubAgentResult) => void,
|
|
519
|
+
heartbeatDetails?: () => SubagentDetails,
|
|
505
520
|
isReadOnly?: boolean,
|
|
506
521
|
): Promise<SubAgentResult> {
|
|
507
522
|
const agent = agents.find((a) => a.name === agentName);
|
|
@@ -512,6 +527,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
512
527
|
agent: agentName,
|
|
513
528
|
task,
|
|
514
529
|
exitCode: 1,
|
|
530
|
+
status: "error",
|
|
531
|
+
stopReason: "error",
|
|
515
532
|
messages: [],
|
|
516
533
|
stderr: `Unknown agent: "${agentName}". Available: ${available}.`,
|
|
517
534
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
@@ -527,6 +544,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
527
544
|
agent: agentName,
|
|
528
545
|
task,
|
|
529
546
|
exitCode: 1,
|
|
547
|
+
status: "error",
|
|
548
|
+
stopReason: "error",
|
|
530
549
|
messages: [],
|
|
531
550
|
stderr: `Model not found for agent "${agentName}". Tried: ${tried}. Parent model: ${parentInfo}. Check agent definition and pi model configuration.`,
|
|
532
551
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
@@ -550,6 +569,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
550
569
|
agent: agentName,
|
|
551
570
|
task,
|
|
552
571
|
exitCode: 1,
|
|
572
|
+
status: "error",
|
|
573
|
+
stopReason: "error",
|
|
553
574
|
messages: [],
|
|
554
575
|
stderr: `Validation error: ${errorMsg}`,
|
|
555
576
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
@@ -557,23 +578,30 @@ export default function (pi: ExtensionAPI) {
|
|
|
557
578
|
};
|
|
558
579
|
}
|
|
559
580
|
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
581
|
+
const stopHeartbeat = onUpdate ? startHeartbeat(() => onUpdate({
|
|
582
|
+
content: [{ type: "text", text: `Subagent ${agentName} is still running…` }],
|
|
583
|
+
details: heartbeatDetails?.() ?? makeDetails("single")([]),
|
|
584
|
+
})) : undefined;
|
|
585
|
+
try {
|
|
586
|
+
return await runSubAgent({
|
|
587
|
+
cwd: safeCwd,
|
|
588
|
+
systemPrompt: params.instructions
|
|
589
|
+
? `${agent.systemPrompt}\n\n## Task Contract\n${params.instructions.slice(0, MAX_INSTRUCTIONS_LENGTH)}`
|
|
590
|
+
: agent.systemPrompt,
|
|
591
|
+
task,
|
|
592
|
+
tools,
|
|
593
|
+
model: resolved.model,
|
|
594
|
+
authStorage,
|
|
595
|
+
modelRegistry,
|
|
596
|
+
signal: parentSignal,
|
|
597
|
+
timeoutMs: effectiveTimeoutMs,
|
|
598
|
+
agentName,
|
|
599
|
+
thinkingLevel: agent.thinking,
|
|
600
|
+
onMessage: onProgress,
|
|
601
|
+
});
|
|
602
|
+
} finally {
|
|
603
|
+
stopHeartbeat?.();
|
|
604
|
+
}
|
|
577
605
|
}
|
|
578
606
|
|
|
579
607
|
// --- Chain mode ---
|
|
@@ -583,7 +611,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
583
611
|
|
|
584
612
|
for (let i = 0; i < params.chain.length; i++) {
|
|
585
613
|
const step = params.chain[i];
|
|
586
|
-
const taskWithContext = step.task.replace(/\{previous\}/g, previousOutput);
|
|
614
|
+
const taskWithContext = step.task.replace(/\{previous\}/g, () => previousOutput);
|
|
587
615
|
|
|
588
616
|
const thread = threadStore.createThread({
|
|
589
617
|
agentName: step.agent,
|
|
@@ -596,6 +624,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
596
624
|
step.agent, taskWithContext, step.cwd,
|
|
597
625
|
signal, step.timeout ?? params.timeout,
|
|
598
626
|
(partial) => threadStore.updateThread(thread.id, { result: partial }),
|
|
627
|
+
() => makeDetails("chain")(results),
|
|
599
628
|
);
|
|
600
629
|
threadStore.updateThread(thread.id, {
|
|
601
630
|
status: isFailedResult(result) ? (result.stopReason === "aborted" ? "aborted" : "failed") : "completed",
|
|
@@ -725,6 +754,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
725
754
|
agent: t.agent,
|
|
726
755
|
task: t.task,
|
|
727
756
|
exitCode: 1,
|
|
757
|
+
status: "error",
|
|
728
758
|
messages: [],
|
|
729
759
|
stderr: "",
|
|
730
760
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
@@ -747,7 +777,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
747
777
|
const result = await runOne(
|
|
748
778
|
t.agent, t.task, t.cwd,
|
|
749
779
|
parallelController.signal, t.timeout ?? params.timeout,
|
|
750
|
-
|
|
780
|
+
(partial) => threadStore.updateThread(parallelThreads[index].id, { result: partial }),
|
|
781
|
+
() => makeDetails("parallel")([...allResults]),
|
|
751
782
|
);
|
|
752
783
|
allResults[index] = result;
|
|
753
784
|
threadStore.updateThread(parallelThreads[index].id, {
|
|
@@ -803,6 +834,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
803
834
|
params.agent, params.task, params.cwd,
|
|
804
835
|
signal, params.timeout,
|
|
805
836
|
(partial) => threadStore.updateThread(thread.id, { result: partial }),
|
|
837
|
+
() => makeDetails("single")([]),
|
|
806
838
|
);
|
|
807
839
|
threadStore.updateThread(thread.id, {
|
|
808
840
|
status: isFailedResult(result) ? (result.stopReason === "aborted" ? "aborted" : "failed") : "completed",
|
package/extensions/runner.ts
CHANGED
|
@@ -63,6 +63,12 @@ export interface SubAgentResult {
|
|
|
63
63
|
// Public API
|
|
64
64
|
// ---------------------------------------------------------------------------
|
|
65
65
|
|
|
66
|
+
export function startHeartbeat(onHeartbeat: () => void, intervalMs = 30_000): () => void {
|
|
67
|
+
const timer = setInterval(onHeartbeat, intervalMs);
|
|
68
|
+
timer.unref?.();
|
|
69
|
+
return () => clearInterval(timer);
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
export async function runSubAgent(options: {
|
|
67
73
|
cwd: string;
|
|
68
74
|
systemPrompt: string;
|
|
@@ -148,6 +154,8 @@ export async function runSubAgent(options: {
|
|
|
148
154
|
result.stopReason = isTimeout ? "timeout" : "aborted";
|
|
149
155
|
result.errorMessage = combinedSignal.reason instanceof Error ? combinedSignal.reason.message : "Sub-agent aborted before start";
|
|
150
156
|
result.status = classifyStopReason(result.stopReason, !isTimeout, isTimeout);
|
|
157
|
+
cleanupCombined?.();
|
|
158
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
151
159
|
return result;
|
|
152
160
|
}
|
|
153
161
|
|
|
@@ -167,6 +175,7 @@ export async function runSubAgent(options: {
|
|
|
167
175
|
let cleanupEventAbort: (() => void) | undefined;
|
|
168
176
|
let abortedBySignal = false;
|
|
169
177
|
let timedOut = false;
|
|
178
|
+
let eventUnsubscribe: (() => void) | undefined;
|
|
170
179
|
|
|
171
180
|
try {
|
|
172
181
|
// Wire combined abort signal to session
|
|
@@ -191,7 +200,8 @@ export async function runSubAgent(options: {
|
|
|
191
200
|
fn();
|
|
192
201
|
};
|
|
193
202
|
|
|
194
|
-
|
|
203
|
+
let unsubscribe: (() => void) | undefined;
|
|
204
|
+
unsubscribe = session.subscribe((event) => {
|
|
195
205
|
try {
|
|
196
206
|
switch (event.type) {
|
|
197
207
|
case "message_end": {
|
|
@@ -223,7 +233,7 @@ export async function runSubAgent(options: {
|
|
|
223
233
|
result.messages = event.messages as unknown as Message[];
|
|
224
234
|
}
|
|
225
235
|
finish(() => {
|
|
226
|
-
unsubscribe();
|
|
236
|
+
unsubscribe?.();
|
|
227
237
|
resolve();
|
|
228
238
|
});
|
|
229
239
|
break;
|
|
@@ -231,18 +241,19 @@ export async function runSubAgent(options: {
|
|
|
231
241
|
}
|
|
232
242
|
} catch (err) {
|
|
233
243
|
finish(() => {
|
|
234
|
-
unsubscribe();
|
|
244
|
+
unsubscribe?.();
|
|
235
245
|
reject(err);
|
|
236
246
|
});
|
|
237
247
|
}
|
|
238
248
|
});
|
|
249
|
+
eventUnsubscribe = unsubscribe;
|
|
239
250
|
|
|
240
251
|
// Resolve on abort so the eventPromise doesn't hang
|
|
241
252
|
const onAbortResolve = () => {
|
|
242
253
|
finish(() => {
|
|
243
254
|
result.exitCode = 1;
|
|
244
255
|
if (!result.errorMessage) result.errorMessage = "Sub-agent aborted";
|
|
245
|
-
unsubscribe();
|
|
256
|
+
unsubscribe?.();
|
|
246
257
|
resolve();
|
|
247
258
|
});
|
|
248
259
|
};
|
|
@@ -279,6 +290,7 @@ export async function runSubAgent(options: {
|
|
|
279
290
|
cleanupAbort?.();
|
|
280
291
|
cleanupEventAbort?.();
|
|
281
292
|
cleanupCombined();
|
|
293
|
+
eventUnsubscribe?.();
|
|
282
294
|
if (timeoutId) clearTimeout(timeoutId);
|
|
283
295
|
try {
|
|
284
296
|
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