@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.
@@ -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
- finalOutput?: string;
61
- errorText?: string;
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, { ...state, ...patch });
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, { ...state, ...patch, durationMs });
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
- activeToolActivity: undefined,
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
- if (
257
- typeof progress.lastToolPreview === "string" &&
258
- progress.lastToolPreview.trim()
259
- ) {
260
- state.progressLastToolPreview = normalizeAndTruncate(
261
- progress.lastToolPreview,
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.id === "string" && typeof part.preview === "string";
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.type === "toolCall" &&
332
- typeof part.id === "string" &&
333
- typeof part.name === "string"
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
- }
@@ -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
- private readonly requestId: string,
94
- private readonly options: { expanded: boolean },
95
- private readonly theme: SubagentTheme,
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(new Text(formatRunningToolPreview(activityPreview, theme), 2, 0));
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, 0));
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
- if (state.errorText)
172
- body.push(new Text(theme.fg("error", state.errorText), 2, 0));
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, 0));
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, 0)] : [];
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
- 0,
202
+ margin,
194
203
  )
195
- : new Text(theme.fg("muted", "(no output)"), 0, 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 { TOOL_RESULT_FAILED_MESSAGE } from "../child/process.js";
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 type {
7
- SingleResult,
8
- SubagentDetails,
9
- SubagentToolResult,
10
- ToolActivity,
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
- renderToolActivity,
19
+ type SubagentProgressState,
18
20
  } from "./progress.js";
21
+ import { renderToolActivity, SENSITIVE_PATTERN } from "./progress-format.js";
19
22
 
20
- export function hasSubagentFailed(result: SingleResult): boolean {
21
- return (
22
- result.exitCode !== 0 ||
23
- result.stopReason === "error" ||
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(({ messages, termination, ...result }) => ({
51
- ...result,
52
- stderr: includeMessages ? result.stderr : "",
53
- ...(includeMessages ? { messages, termination } : {}),
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: Record<string, unknown> = {
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
- nextActivity = { ...nextActivity, child: undefined };
200
+ const { child, ...rest } = nextActivity;
201
+ nextActivity = rest;
94
202
  } else if (toolResultCompleted) {
95
203
  nextActivity = undefined;
96
204
  }
97
- patch.activeToolActivity = nextActivity;
205
+ patch["activeToolActivity"] = nextActivity;
98
206
  const renderedPreview = renderToolActivity(nextActivity);
99
207
  if (renderedPreview) {
100
- patch.lastToolPreview = renderedPreview;
208
+ patch["lastToolPreview"] = renderedPreview;
101
209
  } else if (toolResultCompleted && !nextActivity) {
102
- patch.lastToolPreview = undefined;
210
+ patch["lastToolPreview"] = undefined;
103
211
  }
104
212
  if (toolResultCompleted) {
105
- patch.toolResultCompleted = true;
213
+ patch["toolResultCompleted"] = true;
106
214
  }
107
- // Token accounting always applies when usage data is available
108
215
  if (latestResult?.usage) {
109
- patch.inputTokens = latestResult.usage.input;
110
- patch.outputTokens = latestResult.usage.output;
111
- patch.contextTokens = latestResult.usage.contextTokens;
112
- patch.contextWindowTokens = latestResult.usage.contextWindowTokens;
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
- patchProgressState(
115
- requestId,
116
- patch as Parameters<typeof patchProgressState>[1],
117
- );
221
+ if (latestResult?.model?.trim()) {
222
+ patch["modelDisplay"] = latestResult.model;
223
+ }
224
+ patchProgressState(requestId, patch);
118
225
  }
119
226
 
120
- export function getSubagentText(result: SubagentToolResult): string {
227
+ function getSubagentText(result: SubagentToolResult): string {
121
228
  return (result.content[0] as { text?: string })?.text ?? "";
122
229
  }
123
230