pi-subagents 0.3.1 → 0.3.3
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 +69 -0
- package/README.md +15 -2
- package/chain-clarify.ts +480 -19
- package/chain-execution.ts +109 -24
- package/execution.ts +72 -38
- package/index.ts +34 -4
- package/package.json +1 -1
- package/render.ts +31 -4
- package/schemas.ts +11 -9
- package/settings.ts +24 -20
- package/types.ts +3 -1
- package/utils.ts +51 -13
package/settings.ts
CHANGED
|
@@ -198,42 +198,46 @@ export function buildChainInstructions(
|
|
|
198
198
|
chainDir: string,
|
|
199
199
|
isFirstProgressAgent: boolean,
|
|
200
200
|
previousSummary?: string,
|
|
201
|
-
): string {
|
|
202
|
-
const
|
|
201
|
+
): { prefix: string; suffix: string } {
|
|
202
|
+
const prefixParts: string[] = [];
|
|
203
|
+
const suffixParts: string[] = [];
|
|
203
204
|
|
|
204
|
-
//
|
|
205
|
-
if (previousSummary && previousSummary.trim()) {
|
|
206
|
-
instructions.push(`Previous step summary:\n\n${previousSummary.trim()}`);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Reads (supports both absolute and relative paths)
|
|
205
|
+
// READS - prepend to override any hardcoded filenames in task text
|
|
210
206
|
if (behavior.reads && behavior.reads.length > 0) {
|
|
211
|
-
const files = behavior.reads.map((f) => resolveChainPath(f, chainDir))
|
|
212
|
-
|
|
207
|
+
const files = behavior.reads.map((f) => resolveChainPath(f, chainDir));
|
|
208
|
+
prefixParts.push(`[Read from: ${files.join(", ")}]`);
|
|
213
209
|
}
|
|
214
210
|
|
|
215
|
-
//
|
|
211
|
+
// OUTPUT - prepend so agent knows where to write
|
|
216
212
|
if (behavior.output) {
|
|
217
213
|
const outputPath = resolveChainPath(behavior.output, chainDir);
|
|
218
|
-
|
|
214
|
+
prefixParts.push(`[Write to: ${outputPath}]`);
|
|
219
215
|
}
|
|
220
216
|
|
|
221
|
-
// Progress
|
|
217
|
+
// Progress instructions in suffix (less critical)
|
|
222
218
|
if (behavior.progress) {
|
|
223
219
|
const progressPath = `${chainDir}/progress.md`;
|
|
224
220
|
if (isFirstProgressAgent) {
|
|
225
|
-
|
|
226
|
-
instructions.push("Format: Status, Tasks (checkboxes), Files Changed, Notes");
|
|
221
|
+
suffixParts.push(`Create and maintain progress at: ${progressPath}`);
|
|
227
222
|
} else {
|
|
228
|
-
|
|
223
|
+
suffixParts.push(`Update progress at: ${progressPath}`);
|
|
229
224
|
}
|
|
230
225
|
}
|
|
231
226
|
|
|
232
|
-
|
|
227
|
+
// Include previous step's summary in suffix if available
|
|
228
|
+
if (previousSummary && previousSummary.trim()) {
|
|
229
|
+
suffixParts.push(`Previous step output:\n${previousSummary.trim()}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const prefix = prefixParts.length > 0
|
|
233
|
+
? prefixParts.join("\n") + "\n\n"
|
|
234
|
+
: "";
|
|
235
|
+
|
|
236
|
+
const suffix = suffixParts.length > 0
|
|
237
|
+
? "\n\n---\n" + suffixParts.join("\n")
|
|
238
|
+
: "";
|
|
233
239
|
|
|
234
|
-
return
|
|
235
|
-
"\n\n---\n**Chain Instructions:**\n" + instructions.map((i) => `- ${i}`).join("\n")
|
|
236
|
-
);
|
|
240
|
+
return { prefix, suffix };
|
|
237
241
|
}
|
|
238
242
|
|
|
239
243
|
// =============================================================================
|
package/types.ts
CHANGED
|
@@ -193,6 +193,8 @@ export interface RunSyncOptions {
|
|
|
193
193
|
index?: number;
|
|
194
194
|
sessionDir?: string;
|
|
195
195
|
share?: boolean;
|
|
196
|
+
/** Override the agent's default model (format: "provider/id" or just "id") */
|
|
197
|
+
modelOverride?: string;
|
|
196
198
|
}
|
|
197
199
|
|
|
198
200
|
export interface ExtensionConfig {
|
|
@@ -222,7 +224,7 @@ export const MAX_CONCURRENCY = 4;
|
|
|
222
224
|
export const RESULTS_DIR = "/tmp/pi-async-subagent-results";
|
|
223
225
|
export const ASYNC_DIR = "/tmp/pi-async-subagent-runs";
|
|
224
226
|
export const WIDGET_KEY = "subagent-async";
|
|
225
|
-
export const POLL_INTERVAL_MS =
|
|
227
|
+
export const POLL_INTERVAL_MS = 250;
|
|
226
228
|
export const MAX_WIDGET_JOBS = 4;
|
|
227
229
|
|
|
228
230
|
// ============================================================================
|
package/utils.ts
CHANGED
|
@@ -12,37 +12,71 @@ import type { AsyncStatus, DisplayItem, ErrorInfo } from "./types.js";
|
|
|
12
12
|
// File System Utilities
|
|
13
13
|
// ============================================================================
|
|
14
14
|
|
|
15
|
+
// Cache for status file reads - avoid re-reading unchanged files
|
|
16
|
+
const statusCache = new Map<string, { mtime: number; status: AsyncStatus }>();
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
|
-
* Read async job status from disk
|
|
19
|
+
* Read async job status from disk (with mtime-based caching)
|
|
17
20
|
*/
|
|
18
21
|
export function readStatus(asyncDir: string): AsyncStatus | null {
|
|
19
22
|
const statusPath = path.join(asyncDir, "status.json");
|
|
20
|
-
if (!fs.existsSync(statusPath)) return null;
|
|
21
23
|
try {
|
|
24
|
+
const stat = fs.statSync(statusPath);
|
|
25
|
+
const cached = statusCache.get(statusPath);
|
|
26
|
+
if (cached && cached.mtime === stat.mtimeMs) {
|
|
27
|
+
return cached.status;
|
|
28
|
+
}
|
|
22
29
|
const content = fs.readFileSync(statusPath, "utf-8");
|
|
23
|
-
|
|
30
|
+
const status = JSON.parse(content) as AsyncStatus;
|
|
31
|
+
statusCache.set(statusPath, { mtime: stat.mtimeMs, status });
|
|
32
|
+
// Limit cache size to prevent memory leaks
|
|
33
|
+
if (statusCache.size > 50) {
|
|
34
|
+
const firstKey = statusCache.keys().next().value;
|
|
35
|
+
if (firstKey) statusCache.delete(firstKey);
|
|
36
|
+
}
|
|
37
|
+
return status;
|
|
24
38
|
} catch {
|
|
25
39
|
return null;
|
|
26
40
|
}
|
|
27
41
|
}
|
|
28
42
|
|
|
43
|
+
// Cache for output tail reads - avoid re-reading unchanged files
|
|
44
|
+
const outputTailCache = new Map<string, { mtime: number; size: number; lines: string[] }>();
|
|
45
|
+
|
|
29
46
|
/**
|
|
30
|
-
* Get the last N lines from an output file
|
|
47
|
+
* Get the last N lines from an output file (with mtime/size-based caching)
|
|
31
48
|
*/
|
|
32
49
|
export function getOutputTail(outputFile: string | undefined, maxLines: number = 3): string[] {
|
|
33
|
-
if (!outputFile
|
|
50
|
+
if (!outputFile) return [];
|
|
34
51
|
let fd: number | null = null;
|
|
35
52
|
try {
|
|
36
53
|
const stat = fs.statSync(outputFile);
|
|
37
54
|
if (stat.size === 0) return [];
|
|
55
|
+
|
|
56
|
+
// Check cache using both mtime and size (size changes more frequently during writes)
|
|
57
|
+
const cached = outputTailCache.get(outputFile);
|
|
58
|
+
if (cached && cached.mtime === stat.mtimeMs && cached.size === stat.size) {
|
|
59
|
+
return cached.lines;
|
|
60
|
+
}
|
|
61
|
+
|
|
38
62
|
const tailBytes = 4096;
|
|
39
63
|
const start = Math.max(0, stat.size - tailBytes);
|
|
40
64
|
fd = fs.openSync(outputFile, "r");
|
|
41
65
|
const buffer = Buffer.alloc(Math.min(tailBytes, stat.size));
|
|
42
66
|
fs.readSync(fd, buffer, 0, buffer.length, start);
|
|
43
67
|
const content = buffer.toString("utf-8");
|
|
44
|
-
const
|
|
45
|
-
|
|
68
|
+
const allLines = content.split("\n").filter((l) => l.trim());
|
|
69
|
+
const lines = allLines.slice(-maxLines).map((l) => l.slice(0, 120) + (l.length > 120 ? "..." : ""));
|
|
70
|
+
|
|
71
|
+
// Cache the result
|
|
72
|
+
outputTailCache.set(outputFile, { mtime: stat.mtimeMs, size: stat.size, lines });
|
|
73
|
+
// Limit cache size
|
|
74
|
+
if (outputTailCache.size > 20) {
|
|
75
|
+
const firstKey = outputTailCache.keys().next().value;
|
|
76
|
+
if (firstKey) outputTailCache.delete(firstKey);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return lines;
|
|
46
80
|
} catch {
|
|
47
81
|
return [];
|
|
48
82
|
} finally {
|
|
@@ -58,8 +92,9 @@ export function getOutputTail(outputFile: string | undefined, maxLines: number =
|
|
|
58
92
|
* Get human-readable last activity time for a file
|
|
59
93
|
*/
|
|
60
94
|
export function getLastActivity(outputFile: string | undefined): string {
|
|
61
|
-
if (!outputFile
|
|
95
|
+
if (!outputFile) return "";
|
|
62
96
|
try {
|
|
97
|
+
// Single stat call - throws if file doesn't exist
|
|
63
98
|
const stat = fs.statSync(outputFile);
|
|
64
99
|
const ago = Date.now() - stat.mtimeMs;
|
|
65
100
|
if (ago < 1000) return "active now";
|
|
@@ -91,11 +126,14 @@ export function findLatestSessionFile(sessionDir: string): string | null {
|
|
|
91
126
|
if (!fs.existsSync(sessionDir)) return null;
|
|
92
127
|
const files = fs.readdirSync(sessionDir)
|
|
93
128
|
.filter((f) => f.endsWith(".jsonl"))
|
|
94
|
-
.map((f) =>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
129
|
+
.map((f) => {
|
|
130
|
+
const filePath = path.join(sessionDir, f);
|
|
131
|
+
return {
|
|
132
|
+
name: f,
|
|
133
|
+
path: filePath,
|
|
134
|
+
mtime: fs.statSync(filePath).mtimeMs,
|
|
135
|
+
};
|
|
136
|
+
})
|
|
99
137
|
.sort((a, b) => b.mtime - a.mtime);
|
|
100
138
|
return files.length > 0 ? files[0].path : null;
|
|
101
139
|
}
|