@mystilleef/pi-subagent 0.8.0 → 0.10.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/README.md +76 -27
- package/package.json +9 -9
- package/src/agent/agent-cache.ts +1 -0
- package/src/agent/agents.ts +25 -4
- package/src/child/child-events.ts +22 -20
- package/src/child/process.ts +288 -125
- package/src/child/termination.ts +320 -5
- package/src/env.d.ts +15 -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/run-registry.ts +1 -2
- package/src/orchestration/subagent-orchestrator.ts +57 -82
- 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 +55 -140
- package/src/progress/progress.ts +40 -31
- package/src/progress/result-details.ts +146 -39
- package/src/shared/types.ts +18 -16
- package/src/shared/utils.ts +60 -11
- package/tsconfig.json +9 -11
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
normalizeAndTruncate,
|
|
7
7
|
normalizeSummaryValue,
|
|
8
8
|
normalizeTerminalSentence,
|
|
9
|
-
truncateText,
|
|
10
9
|
} from "../output/normalize.js";
|
|
11
10
|
import type {
|
|
12
11
|
SingleResult,
|
|
@@ -14,8 +13,6 @@ import type {
|
|
|
14
13
|
ToolActivity,
|
|
15
14
|
} from "../shared/types.js";
|
|
16
15
|
|
|
17
|
-
export const SENSITIVE_PATTERN = /secret|token|password/i;
|
|
18
|
-
|
|
19
16
|
export type ThemeBg = "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
20
17
|
|
|
21
18
|
export type ProgressStatus = "running" | "success" | "error" | "cancelled";
|
|
@@ -44,21 +41,22 @@ export const STATUS_BG: Record<ProgressStatus, ThemeBg> = {
|
|
|
44
41
|
export interface SubagentProgressState {
|
|
45
42
|
requestId: string;
|
|
46
43
|
agent: string;
|
|
47
|
-
instanceName?: string;
|
|
44
|
+
instanceName?: string | undefined;
|
|
48
45
|
taskPreview: string;
|
|
49
46
|
status: ProgressStatus;
|
|
50
47
|
startTime: number;
|
|
51
|
-
durationMs?: number;
|
|
52
|
-
activeToolActivity?: ToolActivity;
|
|
53
|
-
lastToolPreview?: string;
|
|
54
|
-
toolResultCompleted?: boolean;
|
|
48
|
+
durationMs?: number | undefined;
|
|
49
|
+
activeToolActivity?: ToolActivity | undefined;
|
|
50
|
+
lastToolPreview?: string | undefined;
|
|
51
|
+
toolResultCompleted?: boolean | undefined;
|
|
55
52
|
toolCount: number;
|
|
56
|
-
inputTokens?: number;
|
|
57
|
-
outputTokens?: number;
|
|
58
|
-
contextTokens?: number;
|
|
59
|
-
contextWindowTokens?: number;
|
|
60
|
-
|
|
61
|
-
|
|
53
|
+
inputTokens?: number | undefined;
|
|
54
|
+
outputTokens?: number | undefined;
|
|
55
|
+
contextTokens?: number | undefined;
|
|
56
|
+
contextWindowTokens?: number | undefined;
|
|
57
|
+
modelDisplay?: string | undefined;
|
|
58
|
+
finalOutput?: string | undefined;
|
|
59
|
+
errorText?: string | undefined;
|
|
62
60
|
}
|
|
63
61
|
|
|
64
62
|
const store = new Map<string, SubagentProgressState>();
|
|
@@ -89,23 +87,34 @@ export function getAllProgressStates(): SubagentProgressState[] {
|
|
|
89
87
|
return [...store.values()].sort((a, b) => b.startTime - a.startTime);
|
|
90
88
|
}
|
|
91
89
|
|
|
90
|
+
type ProgressTransientFields = Pick<
|
|
91
|
+
SubagentProgressState,
|
|
92
|
+
"activeToolActivity" | "lastToolPreview" | "toolResultCompleted"
|
|
93
|
+
>;
|
|
94
|
+
|
|
95
|
+
function stripTransientFields(
|
|
96
|
+
merged: SubagentProgressState,
|
|
97
|
+
): Omit<SubagentProgressState, keyof ProgressTransientFields> {
|
|
98
|
+
const { activeToolActivity, lastToolPreview, toolResultCompleted, ...base } =
|
|
99
|
+
merged;
|
|
100
|
+
return base;
|
|
101
|
+
}
|
|
102
|
+
|
|
92
103
|
export function patchProgressState(
|
|
93
104
|
requestId: string,
|
|
94
105
|
patch: Partial<SubagentProgressState>,
|
|
95
106
|
): void {
|
|
96
107
|
const state = store.get(requestId);
|
|
97
108
|
if (!state) return;
|
|
109
|
+
const merged: SubagentProgressState = { ...state, ...patch };
|
|
110
|
+
if (merged.modelDisplay === undefined || merged.modelDisplay === "") {
|
|
111
|
+
delete merged.modelDisplay;
|
|
112
|
+
}
|
|
98
113
|
if (state.status !== "running") {
|
|
99
|
-
store.set(requestId,
|
|
100
|
-
...state,
|
|
101
|
-
...patch,
|
|
102
|
-
activeToolActivity: undefined,
|
|
103
|
-
lastToolPreview: undefined,
|
|
104
|
-
toolResultCompleted: undefined,
|
|
105
|
-
});
|
|
114
|
+
store.set(requestId, stripTransientFields(merged));
|
|
106
115
|
return;
|
|
107
116
|
}
|
|
108
|
-
store.set(requestId,
|
|
117
|
+
store.set(requestId, merged);
|
|
109
118
|
}
|
|
110
119
|
|
|
111
120
|
function storeTerminalProgressState(
|
|
@@ -115,7 +124,10 @@ function storeTerminalProgressState(
|
|
|
115
124
|
const state = store.get(requestId);
|
|
116
125
|
if (!state) return;
|
|
117
126
|
const durationMs = state.durationMs ?? Date.now() - state.startTime;
|
|
118
|
-
store.set(requestId, {
|
|
127
|
+
store.set(requestId, {
|
|
128
|
+
...stripTransientFields({ ...state, ...patch }),
|
|
129
|
+
durationMs,
|
|
130
|
+
});
|
|
119
131
|
}
|
|
120
132
|
|
|
121
133
|
export function finalizeProgressState(
|
|
@@ -125,9 +137,6 @@ export function finalizeProgressState(
|
|
|
125
137
|
storeTerminalProgressState(requestId, {
|
|
126
138
|
status: "success",
|
|
127
139
|
finalOutput: makeProgressFinalOutput(finalOutput),
|
|
128
|
-
activeToolActivity: undefined,
|
|
129
|
-
lastToolPreview: undefined,
|
|
130
|
-
toolResultCompleted: undefined,
|
|
131
140
|
});
|
|
132
141
|
}
|
|
133
142
|
|
|
@@ -136,21 +145,13 @@ export function failProgressState(requestId: string, errorText: string): void {
|
|
|
136
145
|
storeTerminalProgressState(requestId, {
|
|
137
146
|
status: "error",
|
|
138
147
|
errorText: sentence,
|
|
139
|
-
activeToolActivity: undefined,
|
|
140
|
-
lastToolPreview: undefined,
|
|
141
|
-
toolResultCompleted: undefined,
|
|
142
148
|
});
|
|
143
149
|
}
|
|
144
150
|
|
|
145
151
|
export function cancelProgressState(requestId: string, reason?: string): void {
|
|
146
152
|
storeTerminalProgressState(requestId, {
|
|
147
153
|
status: "cancelled",
|
|
148
|
-
|
|
149
|
-
lastToolPreview: undefined,
|
|
150
|
-
toolResultCompleted: undefined,
|
|
151
|
-
...(reason !== undefined
|
|
152
|
-
? { errorText: normalizeTerminalSentence(reason) }
|
|
153
|
-
: {}),
|
|
154
|
+
errorText: reason ? normalizeTerminalSentence(reason) : undefined,
|
|
154
155
|
});
|
|
155
156
|
}
|
|
156
157
|
|
|
@@ -235,11 +236,11 @@ function trackNewToolCall(
|
|
|
235
236
|
|
|
236
237
|
function extractProgressFromExistingProgress(
|
|
237
238
|
progress: {
|
|
238
|
-
activityText?: string;
|
|
239
|
-
activeToolActivity?: ToolActivity;
|
|
240
|
-
lastToolPreview?: string;
|
|
239
|
+
activityText?: string | undefined;
|
|
240
|
+
activeToolActivity?: ToolActivity | undefined;
|
|
241
|
+
lastToolPreview?: string | undefined;
|
|
241
242
|
toolCalls: { id: string; preview: string }[];
|
|
242
|
-
toolResultCompleted?: boolean;
|
|
243
|
+
toolResultCompleted?: boolean | undefined;
|
|
243
244
|
},
|
|
244
245
|
seenToolCallIds: Set<string>,
|
|
245
246
|
state: DetailsProgress,
|
|
@@ -253,22 +254,26 @@ function extractProgressFromExistingProgress(
|
|
|
253
254
|
if (progress.activeToolActivity) {
|
|
254
255
|
state.activeToolActivity = progress.activeToolActivity;
|
|
255
256
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
257
|
+
const previewValue = progress.lastToolPreview;
|
|
258
|
+
const truncatedPreview =
|
|
259
|
+
typeof previewValue === "string" && previewValue.trim()
|
|
260
|
+
? normalizeAndTruncate(previewValue)
|
|
261
|
+
: undefined;
|
|
262
|
+
if (truncatedPreview) {
|
|
263
|
+
state.progressLastToolPreview = truncatedPreview;
|
|
263
264
|
}
|
|
264
265
|
if (progress.toolResultCompleted) {
|
|
265
266
|
state.toolResultCompleted = true;
|
|
266
267
|
}
|
|
268
|
+
const hasToolCalls = progress.toolCalls.some(isDerivedToolCall);
|
|
267
269
|
for (const toolCall of progress.toolCalls) {
|
|
268
270
|
if (!isDerivedToolCall(toolCall)) continue;
|
|
269
271
|
const preview = normalizeAndTruncate(toolCall.preview);
|
|
270
272
|
trackNewToolCall(toolCall.id, preview, seenToolCallIds, state);
|
|
271
273
|
}
|
|
274
|
+
if (truncatedPreview && !hasToolCalls) {
|
|
275
|
+
state.lastToolPreview = truncatedPreview;
|
|
276
|
+
}
|
|
272
277
|
}
|
|
273
278
|
|
|
274
279
|
function extractProgressFromMessages(
|
|
@@ -317,7 +322,7 @@ function isDerivedToolCall(part: unknown): part is {
|
|
|
317
322
|
preview: string;
|
|
318
323
|
} {
|
|
319
324
|
if (!isObjectWith(part)) return false;
|
|
320
|
-
return typeof part
|
|
325
|
+
return typeof part["id"] === "string" && typeof part["preview"] === "string";
|
|
321
326
|
}
|
|
322
327
|
|
|
323
328
|
export function isToolCallPart(part: unknown): part is {
|
|
@@ -328,98 +333,8 @@ export function isToolCallPart(part: unknown): part is {
|
|
|
328
333
|
} {
|
|
329
334
|
if (!isObjectWith(part)) return false;
|
|
330
335
|
return (
|
|
331
|
-
part
|
|
332
|
-
typeof part
|
|
333
|
-
typeof part
|
|
336
|
+
part["type"] === "toolCall" &&
|
|
337
|
+
typeof part["id"] === "string" &&
|
|
338
|
+
typeof part["name"] === "string"
|
|
334
339
|
);
|
|
335
340
|
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Format a millisecond duration for compact display.
|
|
339
|
-
* Renders sub-minute durations as decimal seconds (`45.2s`),
|
|
340
|
-
* longer durations as minutes and whole seconds (`2m 15s`).
|
|
341
|
-
*/
|
|
342
|
-
export function formatElapsed(ms: number): string {
|
|
343
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
344
|
-
const mins = Math.floor(ms / 60000);
|
|
345
|
-
const secs = Math.floor((ms % 60000) / 1000);
|
|
346
|
-
return `${mins}m ${secs}s`;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
export function formatContextPercent(state: SubagentProgressState): string {
|
|
350
|
-
const d = state.contextWindowTokens;
|
|
351
|
-
if (!d || d <= 0 || !Number.isFinite(d)) return "--%";
|
|
352
|
-
const n = state.contextTokens;
|
|
353
|
-
if (!n || n <= 0 || !Number.isFinite(n)) return "0%";
|
|
354
|
-
return `${Math.round((n / d) * 100)}%`;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* Format the one-line statistics header for a subagent progress display.
|
|
359
|
-
* Includes tool count, context window usage, and elapsed time.
|
|
360
|
-
* When the subagent is still running (`durationMs` unset), elapsed is
|
|
361
|
-
* computed live from `startTime`.
|
|
362
|
-
*
|
|
363
|
-
* @returns Single line ending in `\n`, e.g. `"3 tools · 45% ctx · 12.3s\n"`
|
|
364
|
-
*/
|
|
365
|
-
export function formatHeaderStats(state: SubagentProgressState): string {
|
|
366
|
-
const elapsedMs = state.durationMs ?? Date.now() - state.startTime;
|
|
367
|
-
const toolLabel = state.toolCount === 1 ? "tool" : "tools";
|
|
368
|
-
return `${state.toolCount} ${toolLabel} · ${formatContextPercent(state)} ctx · ${formatElapsed(elapsedMs)}\n`;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const REDACTED_PLACEHOLDER = "(running...)";
|
|
372
|
-
const REDACTED_PLACEHOLDER_LENGTH = REDACTED_PLACEHOLDER.length;
|
|
373
|
-
|
|
374
|
-
function redactOrTruncate(text: string, maxChars: number): string {
|
|
375
|
-
if (SENSITIVE_PATTERN.test(text))
|
|
376
|
-
return maxChars >= REDACTED_PLACEHOLDER_LENGTH ? REDACTED_PLACEHOLDER : "";
|
|
377
|
-
return truncateText(text, maxChars);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function walkActivityTree(activity: ToolActivity): string[] {
|
|
381
|
-
const parts: string[] = [];
|
|
382
|
-
let current: ToolActivity | undefined = activity;
|
|
383
|
-
while (current) {
|
|
384
|
-
if (current.inputSummary) {
|
|
385
|
-
const annotated = current.instanceName
|
|
386
|
-
? `${current.inputSummary} [${current.instanceName}]`
|
|
387
|
-
: current.inputSummary;
|
|
388
|
-
parts.push(annotated);
|
|
389
|
-
}
|
|
390
|
-
current = current.child;
|
|
391
|
-
}
|
|
392
|
-
return parts;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
/**
|
|
396
|
-
* Renders a ToolActivity tree for storage. Each segment is
|
|
397
|
-
* independently normalized and truncated to TOOL_PREVIEW_MAX_CHARS (120).
|
|
398
|
-
*/
|
|
399
|
-
export function renderToolActivity(
|
|
400
|
-
activity: ToolActivity | undefined,
|
|
401
|
-
): string | undefined {
|
|
402
|
-
if (!activity) return undefined;
|
|
403
|
-
const parts = walkActivityTree(activity);
|
|
404
|
-
if (parts.length === 0) return activity.toolName;
|
|
405
|
-
const result = parts.map((p) => normalizeAndTruncate(p)).join(" - ");
|
|
406
|
-
if (SENSITIVE_PATTERN.test(result)) return REDACTED_PLACEHOLDER;
|
|
407
|
-
return result;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Renders a ToolActivity tree for display with a caller-provided truncation
|
|
412
|
-
* budget. Segments are normalized without individual truncation so the
|
|
413
|
-
* joined result shares one post-join display budget.
|
|
414
|
-
*/
|
|
415
|
-
export function renderToolActivityForDisplay(
|
|
416
|
-
activity: ToolActivity | undefined,
|
|
417
|
-
maxChars: number,
|
|
418
|
-
): string | undefined {
|
|
419
|
-
if (!activity) return undefined;
|
|
420
|
-
if (maxChars <= 0) return "";
|
|
421
|
-
const parts = walkActivityTree(activity);
|
|
422
|
-
if (parts.length === 0) return redactOrTruncate(activity.toolName, maxChars);
|
|
423
|
-
const joined = parts.map((p) => normalizeSummaryValue(p)).join(" - ");
|
|
424
|
-
return redactOrTruncate(joined, maxChars);
|
|
425
|
-
}
|
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
|
/**
|
|
@@ -89,11 +85,18 @@ export function renderSubagentProgress(
|
|
|
89
85
|
}
|
|
90
86
|
|
|
91
87
|
class DynamicSubagentProgressText implements Component {
|
|
88
|
+
private readonly requestId: string;
|
|
89
|
+
private readonly options: { expanded: boolean };
|
|
90
|
+
private readonly theme: SubagentTheme;
|
|
92
91
|
constructor(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
) {
|
|
92
|
+
requestId: string,
|
|
93
|
+
options: { expanded: boolean },
|
|
94
|
+
theme: SubagentTheme,
|
|
95
|
+
) {
|
|
96
|
+
this.requestId = requestId;
|
|
97
|
+
this.options = options;
|
|
98
|
+
this.theme = theme;
|
|
99
|
+
}
|
|
97
100
|
invalidate(): void {}
|
|
98
101
|
render(width: number): string[] {
|
|
99
102
|
const state = getProgressState(this.requestId);
|
|
@@ -104,10 +107,6 @@ class DynamicSubagentProgressText implements Component {
|
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
function getProgressBackground(status: ProgressStatus): ThemeBg {
|
|
108
|
-
return STATUS_BG[status];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
110
|
function renderProgressBox(
|
|
112
111
|
state: SubagentProgressState,
|
|
113
112
|
options: { expanded: boolean },
|
|
@@ -117,12 +116,12 @@ function renderProgressBox(
|
|
|
117
116
|
const status = state.status;
|
|
118
117
|
const title = formatSubagentTitle(state.agent, state.instanceName, theme);
|
|
119
118
|
const header = `${theme.fg(STATUS_COLOR[status], STATUS_ICON[status])} ${title} ${theme.fg("dim", `[${status}]`)} ${theme.fg("muted", formatHeaderStats(state))}`;
|
|
120
|
-
const box = new Box(1, 1, (line) =>
|
|
121
|
-
theme.bg(getProgressBackground(status), line),
|
|
122
|
-
);
|
|
119
|
+
const box = new Box(1, 1, (line) => theme.bg(STATUS_BG[status], line));
|
|
123
120
|
box.addChild(new Text(header, 0, 0));
|
|
124
121
|
const body = makeProgressBody(state, options, theme, width);
|
|
125
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));
|
|
126
125
|
return box;
|
|
127
126
|
}
|
|
128
127
|
|
|
@@ -142,6 +141,10 @@ function makeProgressBody(
|
|
|
142
141
|
return [];
|
|
143
142
|
}
|
|
144
143
|
|
|
144
|
+
function bodyMargin(hasFooter: boolean, expanded: boolean): number {
|
|
145
|
+
return expanded || !hasFooter ? 0 : 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
145
148
|
function makeRunningProgressBody(
|
|
146
149
|
state: SubagentProgressState,
|
|
147
150
|
options: { expanded: boolean },
|
|
@@ -154,11 +157,14 @@ function makeRunningProgressBody(
|
|
|
154
157
|
state.activeToolActivity,
|
|
155
158
|
activityBudget,
|
|
156
159
|
);
|
|
160
|
+
const margin = bodyMargin(!!state.modelDisplay, options.expanded);
|
|
157
161
|
if (activityPreview) {
|
|
158
|
-
body.push(
|
|
162
|
+
body.push(
|
|
163
|
+
new Text(formatRunningToolPreview(activityPreview, theme), 2, margin),
|
|
164
|
+
);
|
|
159
165
|
}
|
|
160
166
|
if (options.expanded)
|
|
161
|
-
body.push(new Text(theme.fg("dim", state.taskPreview), 2,
|
|
167
|
+
body.push(new Text(theme.fg("dim", state.taskPreview), 2, margin));
|
|
162
168
|
return body;
|
|
163
169
|
}
|
|
164
170
|
|
|
@@ -168,10 +174,12 @@ function makeStoppedProgressBody(
|
|
|
168
174
|
theme: SubagentTheme,
|
|
169
175
|
): Text[] {
|
|
170
176
|
const body: Text[] = [];
|
|
171
|
-
|
|
172
|
-
|
|
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
|
+
}
|
|
173
181
|
if (options.expanded)
|
|
174
|
-
body.push(new Text(theme.fg("dim", state.taskPreview), 2,
|
|
182
|
+
body.push(new Text(theme.fg("dim", state.taskPreview), 2, margin));
|
|
175
183
|
return body;
|
|
176
184
|
}
|
|
177
185
|
|
|
@@ -181,8 +189,9 @@ function makeSuccessProgressBody(
|
|
|
181
189
|
theme: SubagentTheme,
|
|
182
190
|
): Text[] {
|
|
183
191
|
const output = state.finalOutput?.trim().split("\n")[0] ?? "";
|
|
192
|
+
const margin = bodyMargin(!!state.modelDisplay, options.expanded);
|
|
184
193
|
if (!options.expanded) {
|
|
185
|
-
return output ? [new Text(theme.fg("toolOutput", output), 2,
|
|
194
|
+
return output ? [new Text(theme.fg("toolOutput", output), 2, margin)] : [];
|
|
186
195
|
}
|
|
187
196
|
const body = [new Text(theme.fg("dim", state.taskPreview), 2, 0)];
|
|
188
197
|
body.push(
|
|
@@ -190,9 +199,9 @@ function makeSuccessProgressBody(
|
|
|
190
199
|
? new Text(
|
|
191
200
|
`${theme.fg("muted", "─── Output ───")}\n${theme.fg("toolOutput", output)}`,
|
|
192
201
|
0,
|
|
193
|
-
|
|
202
|
+
margin,
|
|
194
203
|
)
|
|
195
|
-
: new Text(theme.fg("muted", "(no output)"), 0,
|
|
204
|
+
: new Text(theme.fg("muted", "(no output)"), 0, margin),
|
|
196
205
|
);
|
|
197
206
|
return body;
|
|
198
207
|
}
|
|
@@ -1,31 +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";
|
|
21
|
+
import { renderToolActivity, SENSITIVE_PATTERN } from "./progress-format.js";
|
|
19
22
|
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
result.stopReason === "aborted" ||
|
|
25
|
-
Boolean(result.errorMessage?.trim()) ||
|
|
26
|
-
detectMessageError(result.messages ?? [])
|
|
27
|
-
);
|
|
28
|
-
}
|
|
23
|
+
export type DetailsOptions = {
|
|
24
|
+
includeMessages?: boolean;
|
|
25
|
+
recentMessages?: Message[];
|
|
26
|
+
};
|
|
29
27
|
|
|
30
28
|
export function createSubagentError(result: SingleResult): Error {
|
|
31
29
|
const formatted = formatSubagentResultForParent(result);
|
|
@@ -41,18 +39,127 @@ export function createSubagentError(result: SingleResult): Error {
|
|
|
41
39
|
return new Error(`Agent ${result.stopReason || "failed"}: ${msg}`);
|
|
42
40
|
}
|
|
43
41
|
|
|
42
|
+
const DEBUG_REDACTED_PLACEHOLDER = "[redacted]";
|
|
43
|
+
const SENSITIVE_ASSIGNMENT_PATTERN =
|
|
44
|
+
/\b(?:secret|password|[A-Za-z0-9_-]*token)(?:\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi;
|
|
45
|
+
const SENSITIVE_TERM_PATTERN = new RegExp(SENSITIVE_PATTERN.source, "gi");
|
|
46
|
+
const TOKEN_COUNT_KEY_PATTERN = /tokens$/i;
|
|
47
|
+
|
|
48
|
+
function redactSensitiveDebugString(text: string): string {
|
|
49
|
+
return text
|
|
50
|
+
.replace(SENSITIVE_ASSIGNMENT_PATTERN, DEBUG_REDACTED_PLACEHOLDER)
|
|
51
|
+
.replace(SENSITIVE_TERM_PATTERN, DEBUG_REDACTED_PLACEHOLDER);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isSensitiveDebugKey(key: string): boolean {
|
|
55
|
+
const lowerKey = key.toLowerCase();
|
|
56
|
+
if (TOKEN_COUNT_KEY_PATTERN.test(lowerKey)) return false;
|
|
57
|
+
return (
|
|
58
|
+
lowerKey.includes("secret") ||
|
|
59
|
+
lowerKey.includes("password") ||
|
|
60
|
+
lowerKey.endsWith("token")
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function redactSensitiveDebugValue(value: unknown): unknown {
|
|
65
|
+
if (typeof value === "string") return redactSensitiveDebugString(value);
|
|
66
|
+
if (Array.isArray(value)) return value.map(redactSensitiveDebugValue);
|
|
67
|
+
if (typeof value !== "object" || value === null) return value;
|
|
68
|
+
const redacted: Record<string, unknown> = {};
|
|
69
|
+
for (const [key, child] of Object.entries(value)) {
|
|
70
|
+
redacted[key] = isSensitiveDebugKey(key)
|
|
71
|
+
? DEBUG_REDACTED_PLACEHOLDER
|
|
72
|
+
: redactSensitiveDebugValue(child);
|
|
73
|
+
}
|
|
74
|
+
return redacted;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function redactSensitiveDebugMessages(messages: unknown): unknown {
|
|
78
|
+
if (!Array.isArray(messages)) return messages;
|
|
79
|
+
return messages.map(redactSensitiveDebugValue);
|
|
80
|
+
}
|
|
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
|
+
|
|
44
142
|
export function sanitizeDetailsForDisplay(
|
|
45
143
|
details: SubagentDetails,
|
|
46
144
|
includeMessages = false,
|
|
47
145
|
): SubagentDetails {
|
|
48
146
|
return {
|
|
49
147
|
...details,
|
|
50
|
-
results: details.results.map((
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
+
}),
|
|
162
|
+
} as SubagentDetails;
|
|
56
163
|
}
|
|
57
164
|
|
|
58
165
|
export function getLatestResult(
|
|
@@ -76,7 +183,7 @@ export function patchProgressFromDetails(
|
|
|
76
183
|
} = extractProgressFromDetails(details, seenToolCallIds);
|
|
77
184
|
const current = getProgressState(requestId);
|
|
78
185
|
if (!current) return;
|
|
79
|
-
const patch:
|
|
186
|
+
const patch: Partial<SubagentProgressState> = {
|
|
80
187
|
toolCount: current.toolCount + newToolCallIds.length,
|
|
81
188
|
};
|
|
82
189
|
let nextActivity: ToolActivity | undefined;
|
|
@@ -90,34 +197,34 @@ export function patchProgressFromDetails(
|
|
|
90
197
|
nextActivity = current.activeToolActivity;
|
|
91
198
|
}
|
|
92
199
|
if (toolResultCompleted && nextActivity?.child) {
|
|
93
|
-
|
|
200
|
+
const { child, ...rest } = nextActivity;
|
|
201
|
+
nextActivity = rest;
|
|
94
202
|
} else if (toolResultCompleted) {
|
|
95
203
|
nextActivity = undefined;
|
|
96
204
|
}
|
|
97
|
-
patch
|
|
205
|
+
patch["activeToolActivity"] = nextActivity;
|
|
98
206
|
const renderedPreview = renderToolActivity(nextActivity);
|
|
99
207
|
if (renderedPreview) {
|
|
100
|
-
patch
|
|
208
|
+
patch["lastToolPreview"] = renderedPreview;
|
|
101
209
|
} else if (toolResultCompleted && !nextActivity) {
|
|
102
|
-
patch
|
|
210
|
+
patch["lastToolPreview"] = undefined;
|
|
103
211
|
}
|
|
104
212
|
if (toolResultCompleted) {
|
|
105
|
-
patch
|
|
213
|
+
patch["toolResultCompleted"] = true;
|
|
106
214
|
}
|
|
107
|
-
// Token accounting always applies when usage data is available
|
|
108
215
|
if (latestResult?.usage) {
|
|
109
|
-
patch
|
|
110
|
-
patch
|
|
111
|
-
patch
|
|
112
|
-
patch
|
|
216
|
+
patch["inputTokens"] = latestResult.usage.input;
|
|
217
|
+
patch["outputTokens"] = latestResult.usage.output;
|
|
218
|
+
patch["contextTokens"] = latestResult.usage.contextTokens;
|
|
219
|
+
patch["contextWindowTokens"] = latestResult.usage.contextWindowTokens;
|
|
113
220
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
);
|
|
221
|
+
if (latestResult?.model?.trim()) {
|
|
222
|
+
patch["modelDisplay"] = latestResult.model;
|
|
223
|
+
}
|
|
224
|
+
patchProgressState(requestId, patch);
|
|
118
225
|
}
|
|
119
226
|
|
|
120
|
-
|
|
227
|
+
function getSubagentText(result: SubagentToolResult): string {
|
|
121
228
|
return (result.content[0] as { text?: string })?.text ?? "";
|
|
122
229
|
}
|
|
123
230
|
|