pi-subagents 0.13.4 → 0.14.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 +23 -4
- package/README.md +35 -14
- package/agent-management.ts +15 -6
- package/agent-manager-detail.ts +13 -3
- package/agent-manager-edit.ts +75 -23
- package/agent-manager-list.ts +12 -5
- package/agent-manager.ts +199 -11
- package/agents.ts +315 -20
- package/artifacts.ts +11 -5
- package/async-execution.ts +92 -73
- package/chain-clarify.ts +49 -160
- package/chain-execution.ts +38 -76
- package/execution.ts +53 -48
- package/index.ts +1 -1
- package/install.mjs +3 -3
- package/model-fallback.ts +8 -2
- package/package.json +1 -1
- package/parallel-utils.ts +5 -5
- package/prompt-template-bridge.ts +19 -8
- package/render.ts +23 -50
- package/schemas.ts +1 -1
- package/settings.ts +6 -4
- package/single-output.ts +2 -2
- package/skills.ts +165 -75
- package/subagent-executor.ts +52 -18
- package/subagent-runner.ts +171 -54
- package/types.ts +65 -14
- package/utils.ts +52 -21
package/utils.ts
CHANGED
|
@@ -6,13 +6,12 @@ import * as fs from "node:fs";
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import type { Message } from "@mariozechner/pi-ai";
|
|
9
|
-
import type { AsyncStatus, DisplayItem, ErrorInfo, SingleResult } from "./types.ts";
|
|
9
|
+
import type { AgentProgress, AsyncStatus, Details, DisplayItem, ErrorInfo, SingleResult } from "./types.ts";
|
|
10
10
|
|
|
11
11
|
// ============================================================================
|
|
12
12
|
// File System Utilities
|
|
13
13
|
// ============================================================================
|
|
14
14
|
|
|
15
|
-
// Cache for status file reads - avoid re-reading unchanged files
|
|
16
15
|
const statusCache = new Map<string, { mtime: number; status: AsyncStatus }>();
|
|
17
16
|
|
|
18
17
|
function getErrorMessage(error: unknown): string {
|
|
@@ -74,7 +73,6 @@ export function readStatus(asyncDir: string): AsyncStatus | null {
|
|
|
74
73
|
return status;
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
// Cache for output tail reads - avoid re-reading unchanged files
|
|
78
76
|
const outputTailCache = new Map<string, { mtime: number; size: number; lines: string[] }>();
|
|
79
77
|
|
|
80
78
|
/**
|
|
@@ -87,7 +85,6 @@ export function getOutputTail(outputFile: string | undefined, maxLines: number =
|
|
|
87
85
|
const stat = fs.statSync(outputFile);
|
|
88
86
|
if (stat.size === 0) return [];
|
|
89
87
|
|
|
90
|
-
// Check cache using both mtime and size (size changes more frequently during writes)
|
|
91
88
|
const cached = outputTailCache.get(outputFile);
|
|
92
89
|
if (cached && cached.mtime === stat.mtimeMs && cached.size === stat.size) {
|
|
93
90
|
return cached.lines;
|
|
@@ -102,9 +99,7 @@ export function getOutputTail(outputFile: string | undefined, maxLines: number =
|
|
|
102
99
|
const allLines = content.split("\n").filter((l) => l.trim());
|
|
103
100
|
const lines = allLines.slice(-maxLines).map((l) => l.slice(0, 120) + (l.length > 120 ? "..." : ""));
|
|
104
101
|
|
|
105
|
-
// Cache the result
|
|
106
102
|
outputTailCache.set(outputFile, { mtime: stat.mtimeMs, size: stat.size, lines });
|
|
107
|
-
// Limit cache size
|
|
108
103
|
if (outputTailCache.size > 20) {
|
|
109
104
|
const firstKey = outputTailCache.keys().next().value;
|
|
110
105
|
if (firstKey) outputTailCache.delete(firstKey);
|
|
@@ -112,12 +107,15 @@ export function getOutputTail(outputFile: string | undefined, maxLines: number =
|
|
|
112
107
|
|
|
113
108
|
return lines;
|
|
114
109
|
} catch {
|
|
110
|
+
// Output tails are UI-only hints; unreadable or missing files should render as no tail.
|
|
115
111
|
return [];
|
|
116
112
|
} finally {
|
|
117
113
|
if (fd !== null) {
|
|
118
114
|
try {
|
|
119
115
|
fs.closeSync(fd);
|
|
120
|
-
} catch {
|
|
116
|
+
} catch {
|
|
117
|
+
// Closing the best-effort tail file handle should not surface over the main status view.
|
|
118
|
+
}
|
|
121
119
|
}
|
|
122
120
|
}
|
|
123
121
|
}
|
|
@@ -125,16 +123,16 @@ export function getOutputTail(outputFile: string | undefined, maxLines: number =
|
|
|
125
123
|
/**
|
|
126
124
|
* Get human-readable last activity time for a file
|
|
127
125
|
*/
|
|
128
|
-
export function getLastActivity(outputFile: string | undefined): string {
|
|
126
|
+
export function getLastActivity(outputFile: string | undefined): string {
|
|
129
127
|
if (!outputFile) return "";
|
|
130
128
|
try {
|
|
131
|
-
// Single stat call - throws if file doesn't exist
|
|
132
129
|
const stat = fs.statSync(outputFile);
|
|
133
130
|
const ago = Date.now() - stat.mtimeMs;
|
|
134
131
|
if (ago < 1000) return "active now";
|
|
135
132
|
if (ago < 60000) return `active ${Math.floor(ago / 1000)}s ago`;
|
|
136
133
|
return `active ${Math.floor(ago / 60000)}m ago`;
|
|
137
134
|
} catch {
|
|
135
|
+
// Last-activity text is best effort; missing files should simply omit the hint.
|
|
138
136
|
return "";
|
|
139
137
|
}
|
|
140
138
|
}
|
|
@@ -201,13 +199,14 @@ export function getFinalOutput(messages: Message[]): string {
|
|
|
201
199
|
}
|
|
202
200
|
|
|
203
201
|
export function getSingleResultOutput(result: Pick<SingleResult, "finalOutput" | "messages">): string {
|
|
204
|
-
return result.finalOutput ?? getFinalOutput(result.messages);
|
|
202
|
+
return result.finalOutput ?? getFinalOutput(result.messages ?? []);
|
|
205
203
|
}
|
|
206
204
|
|
|
207
205
|
/**
|
|
208
206
|
* Extract display items (text and tool calls) from messages
|
|
209
207
|
*/
|
|
210
|
-
export function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
208
|
+
export function getDisplayItems(messages: Message[] | undefined): DisplayItem[] {
|
|
209
|
+
if (!messages || messages.length === 0) return [];
|
|
211
210
|
const items: DisplayItem[] = [];
|
|
212
211
|
for (const msg of messages) {
|
|
213
212
|
if (msg.role === "assistant") {
|
|
@@ -220,19 +219,53 @@ export function getDisplayItems(messages: Message[]): DisplayItem[] {
|
|
|
220
219
|
return items;
|
|
221
220
|
}
|
|
222
221
|
|
|
222
|
+
function compactCompletedProgress(progress: AgentProgress): AgentProgress {
|
|
223
|
+
if (progress.status === "running") return progress;
|
|
224
|
+
return {
|
|
225
|
+
index: progress.index,
|
|
226
|
+
agent: progress.agent,
|
|
227
|
+
status: progress.status,
|
|
228
|
+
task: progress.task,
|
|
229
|
+
skills: progress.skills,
|
|
230
|
+
toolCount: progress.toolCount,
|
|
231
|
+
tokens: progress.tokens,
|
|
232
|
+
durationMs: progress.durationMs,
|
|
233
|
+
error: progress.error,
|
|
234
|
+
failedTool: progress.failedTool,
|
|
235
|
+
recentTools: [],
|
|
236
|
+
recentOutput: [],
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function compactForegroundResult(result: SingleResult): SingleResult {
|
|
241
|
+
if (result.progress?.status === "running") return result;
|
|
242
|
+
return {
|
|
243
|
+
...result,
|
|
244
|
+
messages: undefined,
|
|
245
|
+
progress: undefined,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function compactForegroundDetails(details: Details): Details {
|
|
250
|
+
return {
|
|
251
|
+
...details,
|
|
252
|
+
results: details.results.map(compactForegroundResult),
|
|
253
|
+
progress: details.progress
|
|
254
|
+
? details.progress.map(compactCompletedProgress)
|
|
255
|
+
: undefined,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
223
259
|
/**
|
|
224
260
|
* Detect errors in subagent execution from messages (only errors with no subsequent success)
|
|
225
261
|
*/
|
|
226
262
|
export function detectSubagentError(messages: Message[]): ErrorInfo {
|
|
227
|
-
// Step 1: Find the last assistant message with text content.
|
|
228
|
-
// If the agent produced a text response after encountering errors,
|
|
229
|
-
// it had a chance to recover — only errors AFTER this point matter.
|
|
230
263
|
let lastAssistantTextIndex = -1;
|
|
231
264
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
232
265
|
const msg = messages[i];
|
|
233
266
|
if (msg.role === "assistant") {
|
|
234
267
|
const hasText = Array.isArray(msg.content) && msg.content.some(
|
|
235
|
-
(c) => c.type === "text" && "text" in c &&
|
|
268
|
+
(c) => c.type === "text" && "text" in c && typeof c.text === "string" && c.text.trim().length > 0,
|
|
236
269
|
);
|
|
237
270
|
if (hasText) {
|
|
238
271
|
lastAssistantTextIndex = i;
|
|
@@ -241,28 +274,26 @@ export function detectSubagentError(messages: Message[]): ErrorInfo {
|
|
|
241
274
|
}
|
|
242
275
|
}
|
|
243
276
|
|
|
244
|
-
// Step 2: Only scan tool results AFTER the last assistant text message.
|
|
245
|
-
// Errors before the agent's final response are implicitly recovered.
|
|
246
277
|
const scanStart = lastAssistantTextIndex >= 0 ? lastAssistantTextIndex + 1 : 0;
|
|
247
278
|
|
|
248
|
-
// Step 3: Check tool results in the post-response window
|
|
249
279
|
for (let i = messages.length - 1; i >= scanStart; i--) {
|
|
250
280
|
const msg = messages[i];
|
|
251
281
|
if (msg.role !== "toolResult") continue;
|
|
282
|
+
const toolName = "toolName" in msg && typeof msg.toolName === "string" ? msg.toolName : undefined;
|
|
283
|
+
const isError = "isError" in msg && msg.isError === true;
|
|
252
284
|
|
|
253
|
-
if (
|
|
285
|
+
if (isError) {
|
|
254
286
|
const text = msg.content.find((c) => c.type === "text");
|
|
255
287
|
const details = text && "text" in text ? text.text : undefined;
|
|
256
288
|
const exitMatch = details?.match(/exit(?:ed)?\s*(?:with\s*)?(?:code|status)?\s*[:\s]?\s*(\d+)/i);
|
|
257
289
|
return {
|
|
258
290
|
hasError: true,
|
|
259
291
|
exitCode: exitMatch ? parseInt(exitMatch[1], 10) : 1,
|
|
260
|
-
errorType:
|
|
292
|
+
errorType: toolName || "tool",
|
|
261
293
|
details: details?.slice(0, 200),
|
|
262
294
|
};
|
|
263
295
|
}
|
|
264
296
|
|
|
265
|
-
const toolName = (msg as any).toolName;
|
|
266
297
|
if (toolName !== "bash") continue;
|
|
267
298
|
|
|
268
299
|
const text = msg.content.find((c) => c.type === "text");
|