@mystilleef/pi-subagent 0.9.0 → 0.10.2
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/README.md +68 -40
- package/package.json +8 -8
- package/src/agent/agent-cache.ts +1 -0
- package/src/agent/agents.ts +22 -1
- package/src/child/child-events.ts +2 -0
- package/src/child/process.ts +196 -84
- package/src/child/termination.ts +316 -1
- package/src/env.d.ts +2 -0
- package/src/notification/delivery.ts +231 -0
- package/src/notification/desktop-notification.ts +73 -0
- package/src/orchestration/run-command.ts +1 -1
- package/src/orchestration/subagent-orchestrator.ts +49 -79
- package/src/output/normalize.ts +2 -2
- package/src/output/ui.ts +17 -28
- package/src/progress/progress-format.ts +111 -0
- package/src/progress/progress-state.ts +20 -108
- package/src/progress/progress.ts +29 -27
- package/src/progress/result-details.ts +96 -33
- package/src/shared/types.ts +2 -0
- package/src/shared/utils.ts +27 -6
package/src/progress/progress.ts
CHANGED
|
@@ -21,17 +21,22 @@ import { Box, Text } from "@earendil-works/pi-tui";
|
|
|
21
21
|
import { formatSubagentTitle, type SubagentTheme } from "../output/ui.js";
|
|
22
22
|
import {
|
|
23
23
|
formatHeaderStats,
|
|
24
|
-
getProgressState,
|
|
25
|
-
type ProgressStatus,
|
|
26
24
|
renderToolActivityForDisplay,
|
|
25
|
+
} from "./progress-format.js";
|
|
26
|
+
import {
|
|
27
|
+
getProgressState,
|
|
27
28
|
STATUS_BG,
|
|
28
29
|
STATUS_COLOR,
|
|
29
30
|
STATUS_ICON,
|
|
30
31
|
type SubagentProgressState,
|
|
31
|
-
type ThemeBg,
|
|
32
32
|
} from "./progress-state.js";
|
|
33
33
|
|
|
34
34
|
export { makeToolPreview } from "../output/normalize.js";
|
|
35
|
+
export {
|
|
36
|
+
formatElapsed,
|
|
37
|
+
formatHeaderStats,
|
|
38
|
+
renderToolActivity,
|
|
39
|
+
} from "./progress-format.js";
|
|
35
40
|
export {
|
|
36
41
|
cancelProgressState,
|
|
37
42
|
clearProgressState,
|
|
@@ -39,20 +44,11 @@ export {
|
|
|
39
44
|
extractProgressFromDetails,
|
|
40
45
|
failProgressState,
|
|
41
46
|
finalizeProgressState,
|
|
42
|
-
formatContextPercent,
|
|
43
|
-
formatElapsed,
|
|
44
|
-
formatHeaderStats,
|
|
45
47
|
getProgressState,
|
|
46
48
|
makeTaskPreview,
|
|
47
|
-
type ProgressStatus,
|
|
48
49
|
patchProgressState,
|
|
49
|
-
renderToolActivity,
|
|
50
|
-
renderToolActivityForDisplay,
|
|
51
50
|
resetProgressStore,
|
|
52
|
-
STATUS_COLOR,
|
|
53
|
-
STATUS_ICON,
|
|
54
51
|
type SubagentProgressState,
|
|
55
|
-
type ThemeBg,
|
|
56
52
|
} from "./progress-state.js";
|
|
57
53
|
|
|
58
54
|
/**
|
|
@@ -111,10 +107,6 @@ class DynamicSubagentProgressText implements Component {
|
|
|
111
107
|
}
|
|
112
108
|
}
|
|
113
109
|
|
|
114
|
-
function getProgressBackground(status: ProgressStatus): ThemeBg {
|
|
115
|
-
return STATUS_BG[status];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
110
|
function renderProgressBox(
|
|
119
111
|
state: SubagentProgressState,
|
|
120
112
|
options: { expanded: boolean },
|
|
@@ -124,12 +116,12 @@ function renderProgressBox(
|
|
|
124
116
|
const status = state.status;
|
|
125
117
|
const title = formatSubagentTitle(state.agent, state.instanceName, theme);
|
|
126
118
|
const header = `${theme.fg(STATUS_COLOR[status], STATUS_ICON[status])} ${title} ${theme.fg("dim", `[${status}]`)} ${theme.fg("muted", formatHeaderStats(state))}`;
|
|
127
|
-
const box = new Box(1, 1, (line) =>
|
|
128
|
-
theme.bg(getProgressBackground(status), line),
|
|
129
|
-
);
|
|
119
|
+
const box = new Box(1, 1, (line) => theme.bg(STATUS_BG[status], line));
|
|
130
120
|
box.addChild(new Text(header, 0, 0));
|
|
131
121
|
const body = makeProgressBody(state, options, theme, width);
|
|
132
122
|
for (const line of body) box.addChild(line);
|
|
123
|
+
if (state.modelDisplay)
|
|
124
|
+
box.addChild(new Text(theme.fg("dim", state.modelDisplay), 0, 0));
|
|
133
125
|
return box;
|
|
134
126
|
}
|
|
135
127
|
|
|
@@ -149,6 +141,10 @@ function makeProgressBody(
|
|
|
149
141
|
return [];
|
|
150
142
|
}
|
|
151
143
|
|
|
144
|
+
function bodyMargin(hasFooter: boolean, expanded: boolean): number {
|
|
145
|
+
return expanded || !hasFooter ? 0 : 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
152
148
|
function makeRunningProgressBody(
|
|
153
149
|
state: SubagentProgressState,
|
|
154
150
|
options: { expanded: boolean },
|
|
@@ -161,11 +157,14 @@ function makeRunningProgressBody(
|
|
|
161
157
|
state.activeToolActivity,
|
|
162
158
|
activityBudget,
|
|
163
159
|
);
|
|
160
|
+
const margin = bodyMargin(!!state.modelDisplay, options.expanded);
|
|
164
161
|
if (activityPreview) {
|
|
165
|
-
body.push(
|
|
162
|
+
body.push(
|
|
163
|
+
new Text(formatRunningToolPreview(activityPreview, theme), 2, margin),
|
|
164
|
+
);
|
|
166
165
|
}
|
|
167
166
|
if (options.expanded)
|
|
168
|
-
body.push(new Text(theme.fg("dim", state.taskPreview), 2,
|
|
167
|
+
body.push(new Text(theme.fg("dim", state.taskPreview), 2, margin));
|
|
169
168
|
return body;
|
|
170
169
|
}
|
|
171
170
|
|
|
@@ -175,10 +174,12 @@ function makeStoppedProgressBody(
|
|
|
175
174
|
theme: SubagentTheme,
|
|
176
175
|
): Text[] {
|
|
177
176
|
const body: Text[] = [];
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
const margin = bodyMargin(!!state.modelDisplay, options.expanded);
|
|
178
|
+
if (state.errorText) {
|
|
179
|
+
body.push(new Text(theme.fg("error", state.errorText), 2, margin));
|
|
180
|
+
}
|
|
180
181
|
if (options.expanded)
|
|
181
|
-
body.push(new Text(theme.fg("dim", state.taskPreview), 2,
|
|
182
|
+
body.push(new Text(theme.fg("dim", state.taskPreview), 2, margin));
|
|
182
183
|
return body;
|
|
183
184
|
}
|
|
184
185
|
|
|
@@ -188,8 +189,9 @@ function makeSuccessProgressBody(
|
|
|
188
189
|
theme: SubagentTheme,
|
|
189
190
|
): Text[] {
|
|
190
191
|
const output = state.finalOutput?.trim().split("\n")[0] ?? "";
|
|
192
|
+
const margin = bodyMargin(!!state.modelDisplay, options.expanded);
|
|
191
193
|
if (!options.expanded) {
|
|
192
|
-
return output ? [new Text(theme.fg("toolOutput", output), 2,
|
|
194
|
+
return output ? [new Text(theme.fg("toolOutput", output), 2, margin)] : [];
|
|
193
195
|
}
|
|
194
196
|
const body = [new Text(theme.fg("dim", state.taskPreview), 2, 0)];
|
|
195
197
|
body.push(
|
|
@@ -197,9 +199,9 @@ function makeSuccessProgressBody(
|
|
|
197
199
|
? new Text(
|
|
198
200
|
`${theme.fg("muted", "─── Output ───")}\n${theme.fg("toolOutput", output)}`,
|
|
199
201
|
0,
|
|
200
|
-
|
|
202
|
+
margin,
|
|
201
203
|
)
|
|
202
|
-
: new Text(theme.fg("muted", "(no output)"), 0,
|
|
204
|
+
: new Text(theme.fg("muted", "(no output)"), 0, margin),
|
|
203
205
|
);
|
|
204
206
|
return body;
|
|
205
207
|
}
|
|
@@ -1,32 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
2
|
+
import type { TerminationMetadata } from "../child/termination.js";
|
|
2
3
|
import {
|
|
3
4
|
formatSubagentResultForParent,
|
|
4
5
|
summarizeFeedbackUiFinalOutput,
|
|
5
6
|
} from "../output/summary.js";
|
|
6
|
-
import
|
|
7
|
-
SingleResult,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import {
|
|
8
|
+
type SingleResult,
|
|
9
|
+
type StreamingProgress,
|
|
10
|
+
type SubagentDetails,
|
|
11
|
+
type SubagentToolResult,
|
|
12
|
+
TOOL_RESULT_FAILED_MESSAGE,
|
|
13
|
+
type ToolActivity,
|
|
11
14
|
} from "../shared/types.js";
|
|
12
|
-
import { detectMessageError } from "../shared/utils.js";
|
|
13
15
|
import {
|
|
14
16
|
extractProgressFromDetails,
|
|
15
17
|
getProgressState,
|
|
16
18
|
patchProgressState,
|
|
17
|
-
|
|
19
|
+
type SubagentProgressState,
|
|
18
20
|
} from "./progress.js";
|
|
19
|
-
import { SENSITIVE_PATTERN } from "./progress-
|
|
21
|
+
import { renderToolActivity, SENSITIVE_PATTERN } from "./progress-format.js";
|
|
20
22
|
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
result.stopReason === "aborted" ||
|
|
26
|
-
Boolean(result.errorMessage?.trim()) ||
|
|
27
|
-
detectMessageError(result.messages ?? [])
|
|
28
|
-
);
|
|
29
|
-
}
|
|
23
|
+
export type DetailsOptions = {
|
|
24
|
+
includeMessages?: boolean;
|
|
25
|
+
recentMessages?: Message[];
|
|
26
|
+
};
|
|
30
27
|
|
|
31
28
|
export function createSubagentError(result: SingleResult): Error {
|
|
32
29
|
const formatted = formatSubagentResultForParent(result);
|
|
@@ -82,19 +79,86 @@ function redactSensitiveDebugMessages(messages: unknown): unknown {
|
|
|
82
79
|
return messages.map(redactSensitiveDebugValue);
|
|
83
80
|
}
|
|
84
81
|
|
|
82
|
+
export function sanitizeResultDetails(
|
|
83
|
+
result: SingleResult,
|
|
84
|
+
includeDebugMessages: boolean,
|
|
85
|
+
options: DetailsOptions | undefined,
|
|
86
|
+
): SingleResult {
|
|
87
|
+
const includeMessages =
|
|
88
|
+
includeDebugMessages && (options?.includeMessages ?? true);
|
|
89
|
+
const { messages, termination, progress, stderr, usage, ...core } = result;
|
|
90
|
+
const { contextWindowTokens, ...usageBase } = usage;
|
|
91
|
+
let progressValue: StreamingProgress | undefined;
|
|
92
|
+
if (progress) {
|
|
93
|
+
const {
|
|
94
|
+
activityText,
|
|
95
|
+
activeToolActivity,
|
|
96
|
+
lastToolPreview,
|
|
97
|
+
toolResultCompleted,
|
|
98
|
+
...progBase
|
|
99
|
+
} = progress;
|
|
100
|
+
progressValue = {
|
|
101
|
+
toolCalls: progBase.toolCalls.map((tc) => ({
|
|
102
|
+
id: tc.id,
|
|
103
|
+
preview: tc.preview,
|
|
104
|
+
})),
|
|
105
|
+
...(activityText !== undefined && { activityText }),
|
|
106
|
+
...(activeToolActivity !== undefined && { activeToolActivity }),
|
|
107
|
+
...(lastToolPreview !== undefined && { lastToolPreview }),
|
|
108
|
+
...(toolResultCompleted !== undefined && { toolResultCompleted }),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
let terminationValue: TerminationMetadata | undefined;
|
|
112
|
+
if (includeMessages && includeDebugMessages && termination) {
|
|
113
|
+
const { cancelReason, terminationSignal, fallbackCause, ...termBase } =
|
|
114
|
+
termination;
|
|
115
|
+
terminationValue = {
|
|
116
|
+
...termBase,
|
|
117
|
+
...(cancelReason !== undefined && { cancelReason }),
|
|
118
|
+
...(terminationSignal !== undefined && { terminationSignal }),
|
|
119
|
+
...(fallbackCause !== undefined && { fallbackCause }),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const sanitized: SingleResult = {
|
|
123
|
+
...core,
|
|
124
|
+
stderr: includeDebugMessages ? stderr : "",
|
|
125
|
+
usage: {
|
|
126
|
+
...usageBase,
|
|
127
|
+
...(contextWindowTokens !== undefined && { contextWindowTokens }),
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
if (progressValue !== undefined) sanitized.progress = progressValue;
|
|
131
|
+
if (includeMessages) {
|
|
132
|
+
sanitized.messages = options?.recentMessages
|
|
133
|
+
? [...options.recentMessages]
|
|
134
|
+
: messages !== undefined
|
|
135
|
+
? [...messages]
|
|
136
|
+
: undefined;
|
|
137
|
+
}
|
|
138
|
+
if (terminationValue !== undefined) sanitized.termination = terminationValue;
|
|
139
|
+
return sanitized;
|
|
140
|
+
}
|
|
141
|
+
|
|
85
142
|
export function sanitizeDetailsForDisplay(
|
|
86
143
|
details: SubagentDetails,
|
|
87
144
|
includeMessages = false,
|
|
88
145
|
): SubagentDetails {
|
|
89
146
|
return {
|
|
90
147
|
...details,
|
|
91
|
-
results: details.results.map((
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
148
|
+
results: details.results.map((result) => {
|
|
149
|
+
const sanitized = sanitizeResultDetails(
|
|
150
|
+
result,
|
|
151
|
+
includeMessages,
|
|
152
|
+
undefined,
|
|
153
|
+
);
|
|
154
|
+
if (includeMessages && sanitized.messages) {
|
|
155
|
+
return {
|
|
156
|
+
...sanitized,
|
|
157
|
+
messages: redactSensitiveDebugMessages(sanitized.messages),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return sanitized;
|
|
161
|
+
}),
|
|
98
162
|
} as SubagentDetails;
|
|
99
163
|
}
|
|
100
164
|
|
|
@@ -119,7 +183,7 @@ export function patchProgressFromDetails(
|
|
|
119
183
|
} = extractProgressFromDetails(details, seenToolCallIds);
|
|
120
184
|
const current = getProgressState(requestId);
|
|
121
185
|
if (!current) return;
|
|
122
|
-
const patch:
|
|
186
|
+
const patch: Partial<SubagentProgressState> = {
|
|
123
187
|
toolCount: current.toolCount + newToolCallIds.length,
|
|
124
188
|
};
|
|
125
189
|
let nextActivity: ToolActivity | undefined;
|
|
@@ -133,7 +197,7 @@ export function patchProgressFromDetails(
|
|
|
133
197
|
nextActivity = current.activeToolActivity;
|
|
134
198
|
}
|
|
135
199
|
if (toolResultCompleted && nextActivity?.child) {
|
|
136
|
-
const { child
|
|
200
|
+
const { child, ...rest } = nextActivity;
|
|
137
201
|
nextActivity = rest;
|
|
138
202
|
} else if (toolResultCompleted) {
|
|
139
203
|
nextActivity = undefined;
|
|
@@ -148,20 +212,19 @@ export function patchProgressFromDetails(
|
|
|
148
212
|
if (toolResultCompleted) {
|
|
149
213
|
patch["toolResultCompleted"] = true;
|
|
150
214
|
}
|
|
151
|
-
// Token accounting always applies when usage data is available
|
|
152
215
|
if (latestResult?.usage) {
|
|
153
216
|
patch["inputTokens"] = latestResult.usage.input;
|
|
154
217
|
patch["outputTokens"] = latestResult.usage.output;
|
|
155
218
|
patch["contextTokens"] = latestResult.usage.contextTokens;
|
|
156
219
|
patch["contextWindowTokens"] = latestResult.usage.contextWindowTokens;
|
|
157
220
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
);
|
|
221
|
+
if (latestResult?.model?.trim()) {
|
|
222
|
+
patch["modelDisplay"] = latestResult.model;
|
|
223
|
+
}
|
|
224
|
+
patchProgressState(requestId, patch);
|
|
162
225
|
}
|
|
163
226
|
|
|
164
|
-
|
|
227
|
+
function getSubagentText(result: SubagentToolResult): string {
|
|
165
228
|
return (result.content[0] as { text?: string })?.text ?? "";
|
|
166
229
|
}
|
|
167
230
|
|
package/src/shared/types.ts
CHANGED
|
@@ -2,6 +2,8 @@ import type { Message } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import type { AgentScope } from "../agent/agents.js";
|
|
3
3
|
import type { TerminationMetadata } from "../child/termination.js";
|
|
4
4
|
|
|
5
|
+
export const TOOL_RESULT_FAILED_MESSAGE = "Subagent tool result failed.";
|
|
6
|
+
|
|
5
7
|
export interface UsageStats {
|
|
6
8
|
input: number;
|
|
7
9
|
output: number;
|
package/src/shared/utils.ts
CHANGED
|
@@ -6,13 +6,14 @@ import {
|
|
|
6
6
|
DefaultResourceLoader,
|
|
7
7
|
getAgentDir,
|
|
8
8
|
} from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type { SingleResult } from "./types.js";
|
|
9
10
|
|
|
10
11
|
export const DEFAULT_MAX_OUTPUT_BYTES = 50_000;
|
|
11
12
|
export const DEFAULT_MAX_OUTPUT_LINES = 500;
|
|
12
13
|
export const DEFAULT_AGENT_END_GRACE_MS = 250;
|
|
13
14
|
export const DEFAULT_MAX_STDERR_BYTES = 10_000;
|
|
14
15
|
export const DEFAULT_MAX_SUBAGENT_DEPTH = 3;
|
|
15
|
-
|
|
16
|
+
const MAX_SUBAGENT_DEPTH_CEILING = 10;
|
|
16
17
|
|
|
17
18
|
export interface SubagentOutputLimits {
|
|
18
19
|
maxBytes: number;
|
|
@@ -130,6 +131,7 @@ async function canonicalPath(filePath: string): Promise<string> {
|
|
|
130
131
|
try {
|
|
131
132
|
return await fs.promises.realpath(filePath);
|
|
132
133
|
} catch {
|
|
134
|
+
/* symlinks or missing paths fall back to absolute path resolution */
|
|
133
135
|
return path.resolve(filePath);
|
|
134
136
|
}
|
|
135
137
|
}
|
|
@@ -209,18 +211,27 @@ export function subagentDepthEnv(): Record<string, string> {
|
|
|
209
211
|
return { PI_SUBAGENT_DEPTH: String(getSubagentDepth() + 1) };
|
|
210
212
|
}
|
|
211
213
|
|
|
212
|
-
export function
|
|
213
|
-
let lastAssistantIdx = -1;
|
|
214
|
+
export function findLastAssistantTextMessage(messages: Message[]): number {
|
|
214
215
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
215
216
|
const msg = messages[i];
|
|
216
217
|
if (
|
|
217
218
|
msg?.role === "assistant" &&
|
|
218
|
-
msg.content
|
|
219
|
+
Array.isArray(msg.content) &&
|
|
220
|
+
msg.content.some(
|
|
221
|
+
(c) =>
|
|
222
|
+
c.type === "text" &&
|
|
223
|
+
typeof c.text === "string" &&
|
|
224
|
+
c.text.trim().length > 0,
|
|
225
|
+
)
|
|
219
226
|
) {
|
|
220
|
-
|
|
221
|
-
break;
|
|
227
|
+
return i;
|
|
222
228
|
}
|
|
223
229
|
}
|
|
230
|
+
return -1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function detectMessageError(messages: Message[]): boolean {
|
|
234
|
+
const lastAssistantIdx = findLastAssistantTextMessage(messages);
|
|
224
235
|
const from = lastAssistantIdx >= 0 ? lastAssistantIdx + 1 : 0;
|
|
225
236
|
for (let i = messages.length - 1; i >= from; i--) {
|
|
226
237
|
const msg = messages[i];
|
|
@@ -228,3 +239,13 @@ export function detectMessageError(messages: Message[]): boolean {
|
|
|
228
239
|
}
|
|
229
240
|
return false;
|
|
230
241
|
}
|
|
242
|
+
|
|
243
|
+
export function hasSubagentFailed(result: SingleResult): boolean {
|
|
244
|
+
return (
|
|
245
|
+
result.exitCode !== 0 ||
|
|
246
|
+
result.stopReason === "error" ||
|
|
247
|
+
result.stopReason === "aborted" ||
|
|
248
|
+
Boolean(result.errorMessage?.trim()) ||
|
|
249
|
+
detectMessageError(result.messages ?? [])
|
|
250
|
+
);
|
|
251
|
+
}
|