@mystilleef/pi-subagent 0.5.0 → 0.7.0
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 +1 -1
- package/package.json +13 -11
- package/src/agent/agent-cache.ts +258 -0
- package/src/agent/agents.ts +232 -0
- package/src/child/child-events.ts +119 -0
- package/src/{process.ts → child/process.ts} +268 -122
- package/src/{prompt-contract.ts → child/prompt-contract.ts} +2 -0
- package/src/index.ts +43 -24
- package/src/{jobs-command.ts → orchestration/jobs-command.ts} +2 -3
- package/src/{run.ts → orchestration/run.ts} +2 -2
- package/src/{subagent-orchestrator.ts → orchestration/subagent-orchestrator.ts} +246 -124
- package/src/{normalize.ts → output/normalize.ts} +35 -49
- package/src/{summary.ts → output/summary.ts} +4 -2
- package/src/{ui.ts → output/ui.ts} +45 -48
- package/src/{progress-state.ts → progress/progress-state.ts} +164 -52
- package/src/{progress.ts → progress/progress.ts} +21 -21
- package/src/{result-details.ts → progress/result-details.ts} +55 -17
- package/src/shared/instance-name.ts +117 -0
- package/src/{types.ts → shared/types.ts} +13 -2
- package/src/{utils.ts → shared/utils.ts} +4 -6
- package/src/agent-cache.ts +0 -41
- package/src/agents.ts +0 -178
- package/src/child-events.ts +0 -32
- package/src/instance-name.ts +0 -164
- /package/src/{termination.ts → child/termination.ts} +0 -0
- /package/src/{cancel-command.ts → orchestration/cancel-command.ts} +0 -0
- /package/src/{run-command.ts → orchestration/run-command.ts} +0 -0
- /package/src/{run-registry.ts → orchestration/run-registry.ts} +0 -0
|
@@ -7,56 +7,37 @@ export function normalizeSummaryValue(value: string): string {
|
|
|
7
7
|
return normalized;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
const SECRET_KEY_RE = /secret|token|password|passwd|credential|auth/i;
|
|
11
|
+
const JWT_RE = /^eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
|
|
12
|
+
|
|
10
13
|
export function extractSemanticToolTarget(
|
|
11
|
-
toolName: string,
|
|
12
14
|
args: Record<string, unknown>,
|
|
13
15
|
forceJson = false,
|
|
14
16
|
): string {
|
|
15
17
|
if (forceJson) return JSON.stringify(args);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
const semanticKeys = [
|
|
19
|
+
"command",
|
|
20
|
+
"path",
|
|
21
|
+
"agent",
|
|
22
|
+
"query",
|
|
23
|
+
"url",
|
|
24
|
+
"action",
|
|
25
|
+
"name",
|
|
26
|
+
];
|
|
27
|
+
for (const key of semanticKeys) {
|
|
28
|
+
const value = args[key];
|
|
29
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
30
|
+
}
|
|
31
|
+
for (const key of Object.keys(args)) {
|
|
32
|
+
const value = args[key];
|
|
33
|
+
if (typeof value !== "string" || !value.trim()) continue;
|
|
34
|
+
if (SECRET_KEY_RE.test(key)) continue;
|
|
35
|
+
if (value.length > 60 || JWT_RE.test(value)) continue;
|
|
36
|
+
return value;
|
|
31
37
|
}
|
|
32
38
|
return "";
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
export function isTranscriptNoiseLine(line: string): boolean {
|
|
36
|
-
return /^(?:(?:hello|hi|hey)(?:[!,.?:;]+|\s|$)|reasoning:|raw log:|apolog(?:y|ies)|sorry\b)/i.test(
|
|
37
|
-
line,
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function isFailureDiagnosticLine(line: string): boolean {
|
|
42
|
-
return /^(?:at\s+|error:|failed:|failure:|exception:|traceback\b|caused by:)/i.test(
|
|
43
|
-
line,
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function filterOutputLines(output: string): string[] {
|
|
48
|
-
return output
|
|
49
|
-
.split(/\r?\n/)
|
|
50
|
-
.map((l) => l.trim())
|
|
51
|
-
.filter(
|
|
52
|
-
(l) => l && !isTranscriptNoiseLine(l) && !isFailureDiagnosticLine(l),
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function stripTerminalStatusPrefixes(value: string): string {
|
|
57
|
-
return value.replace(/^(?:(?:success|failure):\s*)+/i, "");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
41
|
export function truncateText(text: string, limit: number): string {
|
|
61
42
|
if (text.length <= limit) return text;
|
|
62
43
|
return `${text.slice(0, limit - 1)}…`;
|
|
@@ -72,7 +53,10 @@ export function normalizeTerminalSentence(
|
|
|
72
53
|
.replace(/^\s*`{1,3}([^`]+)`{1,3}\s*$/, "$1")
|
|
73
54
|
.replace(/^\s*\*\*([^*]+)\*\*\s*$/, "$1")
|
|
74
55
|
.replace(/^\s*__([^_]+)__\s*$/, "$1");
|
|
75
|
-
const withoutStatusPrefix =
|
|
56
|
+
const withoutStatusPrefix = unwrapped.replace(
|
|
57
|
+
/^(?:(?:success|failure):\s*)+/i,
|
|
58
|
+
"",
|
|
59
|
+
);
|
|
76
60
|
const withoutLabel = withoutStatusPrefix.replace(
|
|
77
61
|
/^\s*(?:status|summary|result|output|message|error|check|outcome|project summary):\s+/i,
|
|
78
62
|
"",
|
|
@@ -85,19 +69,21 @@ export function normalizeTerminalSentence(
|
|
|
85
69
|
|
|
86
70
|
export const TOOL_PREVIEW_MAX_CHARS = 120;
|
|
87
71
|
|
|
72
|
+
export function normalizeAndTruncate(
|
|
73
|
+
text: string,
|
|
74
|
+
limit = TOOL_PREVIEW_MAX_CHARS,
|
|
75
|
+
): string {
|
|
76
|
+
return truncateText(normalizeSummaryValue(text), limit);
|
|
77
|
+
}
|
|
78
|
+
|
|
88
79
|
export function makeToolPreview(
|
|
89
80
|
toolName: string,
|
|
90
81
|
args: Record<string, unknown> | undefined,
|
|
91
82
|
): string {
|
|
92
83
|
if (!args || Object.keys(args).length === 0) return toolName;
|
|
93
|
-
const target = normalizeSummaryValue(
|
|
94
|
-
extractSemanticToolTarget(toolName, args),
|
|
95
|
-
);
|
|
84
|
+
const target = normalizeSummaryValue(extractSemanticToolTarget(args));
|
|
96
85
|
if (!target) return toolName;
|
|
97
|
-
return
|
|
98
|
-
normalizeSummaryValue(`${toolName}: ${target}`),
|
|
99
|
-
TOOL_PREVIEW_MAX_CHARS,
|
|
100
|
-
);
|
|
86
|
+
return normalizeAndTruncate(`${toolName}: ${target}`);
|
|
101
87
|
}
|
|
102
88
|
|
|
103
89
|
export function isStatusOnlySuccess(value: string): boolean {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { SingleResult } from "../shared/types.js";
|
|
1
2
|
import { normalizeTerminalSentence } from "./normalize.js";
|
|
2
|
-
import type { SingleResult } from "./types.js";
|
|
3
3
|
|
|
4
4
|
export const FEEDBACK_UI_SUMMARY_MAX_CHARS = 120;
|
|
5
5
|
|
|
@@ -20,7 +20,9 @@ const FEEDBACK_UI_LABEL_PATTERN =
|
|
|
20
20
|
/^\s*(outcome|project summary|result|summary|status|output|message|error|check):\s*/i;
|
|
21
21
|
|
|
22
22
|
export function formatSubagentResultForParent(result: SingleResult): string {
|
|
23
|
-
return result.
|
|
23
|
+
return result.thinkingWarning
|
|
24
|
+
? `[thinking] ${result.thinkingWarning}\n\n${result.finalOutput}`
|
|
25
|
+
: result.finalOutput;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export function summarizeFeedbackUiFinalOutput(finalOutput: string): string {
|
|
@@ -7,11 +7,7 @@ import {
|
|
|
7
7
|
type MarkdownTheme,
|
|
8
8
|
Text,
|
|
9
9
|
} from "@earendil-works/pi-tui";
|
|
10
|
-
import type { AgentScope } from "
|
|
11
|
-
import {
|
|
12
|
-
extractSemanticToolTarget,
|
|
13
|
-
normalizeSummaryValue,
|
|
14
|
-
} from "./normalize.js";
|
|
10
|
+
import type { AgentScope } from "../agent/agents.js";
|
|
15
11
|
import {
|
|
16
12
|
formatContextPercent,
|
|
17
13
|
formatElapsed,
|
|
@@ -21,11 +17,13 @@ import {
|
|
|
21
17
|
STATUS_ICON,
|
|
22
18
|
type SubagentProgressState,
|
|
23
19
|
type ThemeBg,
|
|
24
|
-
} from "
|
|
25
|
-
import { hasSubagentFailed } from "
|
|
26
|
-
import type { SubagentDetails, UsageStats } from "
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
} from "../progress/progress-state.js";
|
|
21
|
+
import { hasSubagentFailed } from "../progress/result-details.js";
|
|
22
|
+
import type { SubagentDetails, UsageStats } from "../shared/types.js";
|
|
23
|
+
import {
|
|
24
|
+
extractSemanticToolTarget,
|
|
25
|
+
normalizeSummaryValue,
|
|
26
|
+
} from "./normalize.js";
|
|
29
27
|
|
|
30
28
|
/**
|
|
31
29
|
* Abstraction for theme-aware text formatting.
|
|
@@ -103,32 +101,16 @@ export function formatUsageStats(
|
|
|
103
101
|
return parts.join(" · ");
|
|
104
102
|
}
|
|
105
103
|
|
|
106
|
-
/**
|
|
107
|
-
* Formats millisecond durations into human-readable time strings.
|
|
108
|
-
*/
|
|
109
|
-
export function formatDuration(ms: number): string {
|
|
110
|
-
if (ms < 1000) return `${Math.floor(ms)}ms`;
|
|
111
|
-
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
112
|
-
const minutes = Math.floor(ms / 60000);
|
|
113
|
-
const seconds = Math.floor((ms % 60000) / 1000);
|
|
114
|
-
return `${minutes}m ${seconds.toString().padStart(2, "0")}s`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
104
|
/**
|
|
118
105
|
* Formats the footer for subagent result cards, including model, context, turns, and cost.
|
|
119
106
|
*/
|
|
120
|
-
export function formatResultFooter(
|
|
121
|
-
usage: UsageStats,
|
|
122
|
-
model?: string,
|
|
123
|
-
durationMs?: number,
|
|
124
|
-
): string {
|
|
107
|
+
export function formatResultFooter(usage: UsageStats, model?: string): string {
|
|
125
108
|
const parts: string[] = [];
|
|
126
109
|
if (model) parts.push(model);
|
|
127
|
-
if (usage.contextTokens && usage.contextTokens > 0)
|
|
128
|
-
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
129
110
|
if (usage.turns)
|
|
130
111
|
parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
|
|
131
|
-
if (
|
|
112
|
+
if (usage.contextTokens && usage.contextTokens > 0)
|
|
113
|
+
parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
|
|
132
114
|
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
133
115
|
return parts.join(" · ");
|
|
134
116
|
}
|
|
@@ -143,7 +125,7 @@ export function formatToolCall(
|
|
|
143
125
|
forceJson = false,
|
|
144
126
|
): string {
|
|
145
127
|
const target = normalizeSummaryValue(
|
|
146
|
-
extractSemanticToolTarget(
|
|
128
|
+
extractSemanticToolTarget(args, forceJson),
|
|
147
129
|
);
|
|
148
130
|
if (!target) return themeFg("accent", toolName);
|
|
149
131
|
return themeFg("accent", toolName) + themeFg("dim", ` ${target}`);
|
|
@@ -199,7 +181,8 @@ export function renderSubagentCall(
|
|
|
199
181
|
): Text {
|
|
200
182
|
const scope: AgentScope = args.agentScope ?? "both";
|
|
201
183
|
const agentName = args.agent || "...";
|
|
202
|
-
|
|
184
|
+
// Parser-owned preview suppresses task text: show agent + scope only
|
|
185
|
+
const target = args.agent ? `[${scope}]` : JSON.stringify(args);
|
|
203
186
|
let text =
|
|
204
187
|
theme.fg("toolTitle", theme.bold("subagent ")) +
|
|
205
188
|
theme.fg("accent", agentName) +
|
|
@@ -208,15 +191,17 @@ export function renderSubagentCall(
|
|
|
208
191
|
return new Text(text, 0, 0, (line) => theme.bg("toolPendingBg", line));
|
|
209
192
|
}
|
|
210
193
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
194
|
+
export function renderSubagentToolResult(
|
|
195
|
+
result: { content: { type: string; text?: string }[]; details?: unknown },
|
|
196
|
+
theme: SubagentTheme,
|
|
197
|
+
display?: { isPartial?: boolean },
|
|
198
|
+
): Component {
|
|
199
|
+
const details = result.details as SubagentDetails | undefined;
|
|
200
|
+
if (details?.renderedByMessage) return new Text("", 0, 0);
|
|
201
|
+
return renderSubagentResult(result, theme, display);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Invariants: Red background = failure, green = success. Trims redundant "Outcome:" lines. Shows usage stats + duration in footer.
|
|
220
205
|
export function renderSubagentResult(
|
|
221
206
|
result: { content: { type: string; text?: string }[]; details?: unknown },
|
|
222
207
|
theme: SubagentTheme,
|
|
@@ -242,13 +227,29 @@ export function renderSubagentResult(
|
|
|
242
227
|
: "success";
|
|
243
228
|
const finalOutput = r.finalOutput ?? getFinalOutput(r.messages ?? []);
|
|
244
229
|
const title = formatSubagentTitle(r.agent, r.instanceName, theme);
|
|
245
|
-
|
|
246
|
-
|
|
230
|
+
let effectiveBody = bodyOverride ?? finalOutput;
|
|
231
|
+
if (_display?.isPartial && !finalOutput?.trim() && !bodyOverride) {
|
|
232
|
+
effectiveBody =
|
|
233
|
+
result.content[0]?.text ||
|
|
234
|
+
r.progress?.activityText ||
|
|
235
|
+
r.progress?.lastToolPreview ||
|
|
236
|
+
"(running...)";
|
|
237
|
+
}
|
|
238
|
+
const bodyText = stripOutcomeLineForResultUi(effectiveBody);
|
|
239
|
+
const toolCount = r.progress?.toolCalls?.length ?? 0;
|
|
240
|
+
const toolLabel = `${toolCount} ${toolCount === 1 ? "tool" : "tools"}`;
|
|
241
|
+
const ctxPercent = formatContextPercent({
|
|
242
|
+
contextTokens: r.usage.contextTokens,
|
|
243
|
+
contextWindowTokens: r.usage.contextWindowTokens,
|
|
244
|
+
} as SubagentProgressState);
|
|
245
|
+
const metadata = `${toolLabel} · ${ctxPercent} ctx · ${formatElapsed(r.durationMs ?? 0)}`;
|
|
246
|
+
const usageStr = formatResultFooter(r.usage, r.model);
|
|
247
247
|
return renderStatusCard(
|
|
248
248
|
{
|
|
249
249
|
status: resultStatus,
|
|
250
250
|
title,
|
|
251
251
|
variant: "full",
|
|
252
|
+
metadata,
|
|
252
253
|
body: bodyText,
|
|
253
254
|
footer: usageStr,
|
|
254
255
|
},
|
|
@@ -347,7 +348,6 @@ function renderJobCard(
|
|
|
347
348
|
);
|
|
348
349
|
}
|
|
349
350
|
|
|
350
|
-
/** Sort states by startTime descending (newest first). */
|
|
351
351
|
function sortByStartTimeDesc(
|
|
352
352
|
a: SubagentProgressState,
|
|
353
353
|
b: SubagentProgressState,
|
|
@@ -363,11 +363,8 @@ const BOARD_SECTIONS: [string, ProgressStatus][] = [
|
|
|
363
363
|
["SUCCEEDED", "success"],
|
|
364
364
|
];
|
|
365
365
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
* Jobs render in status-specific sections, each sorted by `startTime` descending.
|
|
369
|
-
* Status icons preserve the existing /jobs contract for running and cancelled jobs.
|
|
370
|
-
*/
|
|
366
|
+
// Jobs render in status-specific sections, each sorted by `startTime` descending.
|
|
367
|
+
// Status icons preserve the existing /jobs contract for running and cancelled jobs.
|
|
371
368
|
export function renderRunsBoard(
|
|
372
369
|
states: SubagentProgressState[],
|
|
373
370
|
theme: SubagentTheme,
|
|
@@ -3,12 +3,18 @@ import {
|
|
|
3
3
|
isStatusOnlyFailure,
|
|
4
4
|
isStatusOnlySuccess,
|
|
5
5
|
makeToolPreview,
|
|
6
|
+
normalizeAndTruncate,
|
|
6
7
|
normalizeSummaryValue,
|
|
7
8
|
normalizeTerminalSentence,
|
|
8
|
-
TOOL_PREVIEW_MAX_CHARS,
|
|
9
9
|
truncateText,
|
|
10
|
-
} from "
|
|
11
|
-
import type {
|
|
10
|
+
} from "../output/normalize.js";
|
|
11
|
+
import type {
|
|
12
|
+
SingleResult,
|
|
13
|
+
SubagentDetails,
|
|
14
|
+
ToolActivity,
|
|
15
|
+
} from "../shared/types.js";
|
|
16
|
+
|
|
17
|
+
export const SENSITIVE_PATTERN = /secret|token|password/i;
|
|
12
18
|
|
|
13
19
|
export type ThemeBg = "toolPendingBg" | "toolSuccessBg" | "toolErrorBg";
|
|
14
20
|
|
|
@@ -43,7 +49,9 @@ export interface SubagentProgressState {
|
|
|
43
49
|
status: ProgressStatus;
|
|
44
50
|
startTime: number;
|
|
45
51
|
durationMs?: number;
|
|
52
|
+
activeToolActivity?: ToolActivity;
|
|
46
53
|
lastToolPreview?: string;
|
|
54
|
+
toolResultCompleted?: boolean;
|
|
47
55
|
toolCount: number;
|
|
48
56
|
inputTokens?: number;
|
|
49
57
|
outputTokens?: number;
|
|
@@ -59,7 +67,7 @@ export function createProgressState(
|
|
|
59
67
|
requestId: string,
|
|
60
68
|
agent: string,
|
|
61
69
|
task: string,
|
|
62
|
-
instanceName
|
|
70
|
+
instanceName?: string,
|
|
63
71
|
): void {
|
|
64
72
|
store.set(requestId, {
|
|
65
73
|
requestId,
|
|
@@ -91,7 +99,9 @@ export function patchProgressState(
|
|
|
91
99
|
store.set(requestId, {
|
|
92
100
|
...state,
|
|
93
101
|
...patch,
|
|
102
|
+
activeToolActivity: undefined,
|
|
94
103
|
lastToolPreview: undefined,
|
|
104
|
+
toolResultCompleted: undefined,
|
|
95
105
|
});
|
|
96
106
|
return;
|
|
97
107
|
}
|
|
@@ -115,7 +125,9 @@ export function finalizeProgressState(
|
|
|
115
125
|
storeTerminalProgressState(requestId, {
|
|
116
126
|
status: "success",
|
|
117
127
|
finalOutput: makeProgressFinalOutput(finalOutput),
|
|
128
|
+
activeToolActivity: undefined,
|
|
118
129
|
lastToolPreview: undefined,
|
|
130
|
+
toolResultCompleted: undefined,
|
|
119
131
|
});
|
|
120
132
|
}
|
|
121
133
|
|
|
@@ -124,14 +136,18 @@ export function failProgressState(requestId: string, errorText: string): void {
|
|
|
124
136
|
storeTerminalProgressState(requestId, {
|
|
125
137
|
status: "error",
|
|
126
138
|
errorText: sentence,
|
|
139
|
+
activeToolActivity: undefined,
|
|
127
140
|
lastToolPreview: undefined,
|
|
141
|
+
toolResultCompleted: undefined,
|
|
128
142
|
});
|
|
129
143
|
}
|
|
130
144
|
|
|
131
145
|
export function cancelProgressState(requestId: string, reason?: string): void {
|
|
132
146
|
storeTerminalProgressState(requestId, {
|
|
133
147
|
status: "cancelled",
|
|
148
|
+
activeToolActivity: undefined,
|
|
134
149
|
lastToolPreview: undefined,
|
|
150
|
+
toolResultCompleted: undefined,
|
|
135
151
|
...(reason !== undefined
|
|
136
152
|
? { errorText: normalizeTerminalSentence(reason) }
|
|
137
153
|
: {}),
|
|
@@ -198,53 +214,110 @@ function isMeaningfulProgressErrorLine(line: string): boolean {
|
|
|
198
214
|
|
|
199
215
|
export interface DetailsProgress {
|
|
200
216
|
lastToolPreview?: string;
|
|
217
|
+
activityText?: string;
|
|
218
|
+
activeToolActivity?: ToolActivity;
|
|
219
|
+
progressLastToolPreview?: string;
|
|
220
|
+
toolResultCompleted?: boolean;
|
|
201
221
|
newToolCallIds: string[];
|
|
202
222
|
}
|
|
203
223
|
|
|
224
|
+
function trackNewToolCall(
|
|
225
|
+
id: string,
|
|
226
|
+
preview: string,
|
|
227
|
+
seenToolCallIds: Set<string>,
|
|
228
|
+
state: DetailsProgress,
|
|
229
|
+
): void {
|
|
230
|
+
if (seenToolCallIds.has(id)) return;
|
|
231
|
+
seenToolCallIds.add(id);
|
|
232
|
+
state.newToolCallIds.push(id);
|
|
233
|
+
state.lastToolPreview = preview;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function extractProgressFromExistingProgress(
|
|
237
|
+
progress: {
|
|
238
|
+
activityText?: string;
|
|
239
|
+
activeToolActivity?: ToolActivity;
|
|
240
|
+
lastToolPreview?: string;
|
|
241
|
+
toolCalls: { id: string; preview: string }[];
|
|
242
|
+
toolResultCompleted?: boolean;
|
|
243
|
+
},
|
|
244
|
+
seenToolCallIds: Set<string>,
|
|
245
|
+
state: DetailsProgress,
|
|
246
|
+
): void {
|
|
247
|
+
if (
|
|
248
|
+
typeof progress.activityText === "string" &&
|
|
249
|
+
progress.activityText.trim()
|
|
250
|
+
) {
|
|
251
|
+
state.activityText = normalizeAndTruncate(progress.activityText);
|
|
252
|
+
}
|
|
253
|
+
if (progress.activeToolActivity) {
|
|
254
|
+
state.activeToolActivity = progress.activeToolActivity;
|
|
255
|
+
}
|
|
256
|
+
if (
|
|
257
|
+
typeof progress.lastToolPreview === "string" &&
|
|
258
|
+
progress.lastToolPreview.trim()
|
|
259
|
+
) {
|
|
260
|
+
state.progressLastToolPreview = normalizeAndTruncate(
|
|
261
|
+
progress.lastToolPreview,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if (progress.toolResultCompleted) {
|
|
265
|
+
state.toolResultCompleted = true;
|
|
266
|
+
}
|
|
267
|
+
for (const toolCall of progress.toolCalls) {
|
|
268
|
+
if (!isDerivedToolCall(toolCall)) continue;
|
|
269
|
+
const preview = normalizeAndTruncate(toolCall.preview);
|
|
270
|
+
trackNewToolCall(toolCall.id, preview, seenToolCallIds, state);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function extractProgressFromMessages(
|
|
275
|
+
messages: SingleResult["messages"] = [],
|
|
276
|
+
seenToolCallIds: Set<string>,
|
|
277
|
+
state: DetailsProgress,
|
|
278
|
+
): void {
|
|
279
|
+
for (const msg of messages) {
|
|
280
|
+
if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
281
|
+
for (const part of msg.content) {
|
|
282
|
+
if (isToolCallPart(part)) {
|
|
283
|
+
const preview = makeToolPreview(part.name, part.arguments);
|
|
284
|
+
trackNewToolCall(part.id, preview, seenToolCallIds, state);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
204
290
|
export function extractProgressFromDetails(
|
|
205
291
|
details: SubagentDetails,
|
|
206
292
|
seenToolCallIds: Set<string>,
|
|
207
293
|
): DetailsProgress {
|
|
208
|
-
const
|
|
209
|
-
let lastToolPreview: string | undefined;
|
|
294
|
+
const state: DetailsProgress = { newToolCallIds: [] };
|
|
210
295
|
const results = Array.isArray(details.results) ? details.results : [];
|
|
211
296
|
for (const result of results) {
|
|
212
297
|
if (result.progress) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
);
|
|
219
|
-
if (seenToolCallIds.has(toolCall.id)) continue;
|
|
220
|
-
seenToolCallIds.add(toolCall.id);
|
|
221
|
-
newToolCallIds.push(toolCall.id);
|
|
222
|
-
}
|
|
298
|
+
extractProgressFromExistingProgress(
|
|
299
|
+
result.progress,
|
|
300
|
+
seenToolCallIds,
|
|
301
|
+
state,
|
|
302
|
+
);
|
|
223
303
|
continue;
|
|
224
304
|
}
|
|
225
305
|
const messages = Array.isArray(result.messages) ? result.messages : [];
|
|
226
|
-
|
|
227
|
-
if (msg.role !== "assistant" || !Array.isArray(msg.content)) continue;
|
|
228
|
-
for (const part of msg.content) {
|
|
229
|
-
if (isToolCallPart(part)) {
|
|
230
|
-
lastToolPreview = makeToolPreview(part.name, part.arguments);
|
|
231
|
-
if (seenToolCallIds.has(part.id)) continue;
|
|
232
|
-
seenToolCallIds.add(part.id);
|
|
233
|
-
newToolCallIds.push(part.id);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
306
|
+
extractProgressFromMessages(messages, seenToolCallIds, state);
|
|
237
307
|
}
|
|
238
|
-
return
|
|
308
|
+
return state;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function isObjectWith(part: unknown): part is Record<string, unknown> {
|
|
312
|
+
return typeof part === "object" && part !== null;
|
|
239
313
|
}
|
|
240
314
|
|
|
241
315
|
function isDerivedToolCall(part: unknown): part is {
|
|
242
316
|
id: string;
|
|
243
317
|
preview: string;
|
|
244
318
|
} {
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
return typeof maybe.id === "string" && typeof maybe.preview === "string";
|
|
319
|
+
if (!isObjectWith(part)) return false;
|
|
320
|
+
return typeof part.id === "string" && typeof part.preview === "string";
|
|
248
321
|
}
|
|
249
322
|
|
|
250
323
|
export function isToolCallPart(part: unknown): part is {
|
|
@@ -253,12 +326,11 @@ export function isToolCallPart(part: unknown): part is {
|
|
|
253
326
|
name: string;
|
|
254
327
|
arguments?: Record<string, unknown>;
|
|
255
328
|
} {
|
|
256
|
-
if (
|
|
257
|
-
const maybe = part as { type?: unknown; id?: unknown; name?: unknown };
|
|
329
|
+
if (!isObjectWith(part)) return false;
|
|
258
330
|
return (
|
|
259
|
-
|
|
260
|
-
typeof
|
|
261
|
-
typeof
|
|
331
|
+
part.type === "toolCall" &&
|
|
332
|
+
typeof part.id === "string" &&
|
|
333
|
+
typeof part.name === "string"
|
|
262
334
|
);
|
|
263
335
|
}
|
|
264
336
|
|
|
@@ -274,22 +346,6 @@ export function formatElapsed(ms: number): string {
|
|
|
274
346
|
return `${mins}m ${secs}s`;
|
|
275
347
|
}
|
|
276
348
|
|
|
277
|
-
/**
|
|
278
|
-
* Format a raw token count for compact inline display.
|
|
279
|
-
* Values below 1000 rendered as-is. Larger counts use `k`
|
|
280
|
-
* or `M` suffixes with one decimal place, stripping trailing `.0`.
|
|
281
|
-
*/
|
|
282
|
-
export function formatTokenCount(count: number): string {
|
|
283
|
-
if (count < 1000) return String(count);
|
|
284
|
-
const unit = count >= 1_000_000 ? "M" : "k";
|
|
285
|
-
const divisor = count >= 1_000_000 ? 1_000_000 : 1000;
|
|
286
|
-
return `${trimTrailingZero((count / divisor).toFixed(1))}${unit}`;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function trimTrailingZero(value: string): string {
|
|
290
|
-
return value.endsWith(".0") ? value.slice(0, -2) : value;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
349
|
export function formatContextPercent(state: SubagentProgressState): string {
|
|
294
350
|
const d = state.contextWindowTokens;
|
|
295
351
|
if (!d || d <= 0 || !Number.isFinite(d)) return "--%";
|
|
@@ -311,3 +367,59 @@ export function formatHeaderStats(state: SubagentProgressState): string {
|
|
|
311
367
|
const toolLabel = state.toolCount === 1 ? "tool" : "tools";
|
|
312
368
|
return `${state.toolCount} ${toolLabel} · ${formatContextPercent(state)} ctx · ${formatElapsed(elapsedMs)}\n`;
|
|
313
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
|
+
}
|