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/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 instructions: string[] = [];
201
+ ): { prefix: string; suffix: string } {
202
+ const prefixParts: string[] = [];
203
+ const suffixParts: string[] = [];
203
204
 
204
- // Include previous step's summary if available (prose output from prior agent)
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)).join(", ");
212
- instructions.push(`Read these files: ${files}`);
207
+ const files = behavior.reads.map((f) => resolveChainPath(f, chainDir));
208
+ prefixParts.push(`[Read from: ${files.join(", ")}]`);
213
209
  }
214
210
 
215
- // Output (supports both absolute and relative paths)
211
+ // OUTPUT - prepend so agent knows where to write
216
212
  if (behavior.output) {
217
213
  const outputPath = resolveChainPath(behavior.output, chainDir);
218
- instructions.push(`Write your output to: ${outputPath}`);
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
- instructions.push(`Create and maintain: ${progressPath}`);
226
- instructions.push("Format: Status, Tasks (checkboxes), Files Changed, Notes");
221
+ suffixParts.push(`Create and maintain progress at: ${progressPath}`);
227
222
  } else {
228
- instructions.push(`Read and update: ${progressPath}`);
223
+ suffixParts.push(`Update progress at: ${progressPath}`);
229
224
  }
230
225
  }
231
226
 
232
- if (instructions.length === 0) return "";
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 = 1000;
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
- return JSON.parse(content) as AsyncStatus;
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 || !fs.existsSync(outputFile)) return [];
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 lines = content.split("\n").filter((l) => l.trim());
45
- return lines.slice(-maxLines).map((l) => l.slice(0, 120) + (l.length > 120 ? "..." : ""));
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 || !fs.existsSync(outputFile)) return "";
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
- name: f,
96
- path: path.join(sessionDir, f),
97
- mtime: fs.statSync(path.join(sessionDir, f)).mtimeMs,
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
  }