@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.
@@ -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(new Text(formatRunningToolPreview(activityPreview, theme), 2, 0));
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, 0));
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
- if (state.errorText)
179
- 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
+ }
180
181
  if (options.expanded)
181
- body.push(new Text(theme.fg("dim", state.taskPreview), 2, 0));
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, 0)] : [];
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
- 0,
202
+ margin,
201
203
  )
202
- : new Text(theme.fg("muted", "(no output)"), 0, 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 { 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";
19
- import { SENSITIVE_PATTERN } from "./progress-state.js";
21
+ import { renderToolActivity, SENSITIVE_PATTERN } from "./progress-format.js";
20
22
 
21
- export function hasSubagentFailed(result: SingleResult): boolean {
22
- return (
23
- result.exitCode !== 0 ||
24
- result.stopReason === "error" ||
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(({ messages, termination, ...result }) => ({
92
- ...result,
93
- stderr: includeMessages ? result.stderr : "",
94
- ...(includeMessages
95
- ? { messages: redactSensitiveDebugMessages(messages), termination }
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: Record<string, unknown> = {
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: _child, ...rest } = nextActivity;
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
- patchProgressState(
159
- requestId,
160
- patch as Parameters<typeof patchProgressState>[1],
161
- );
221
+ if (latestResult?.model?.trim()) {
222
+ patch["modelDisplay"] = latestResult.model;
223
+ }
224
+ patchProgressState(requestId, patch);
162
225
  }
163
226
 
164
- export function getSubagentText(result: SubagentToolResult): string {
227
+ function getSubagentText(result: SubagentToolResult): string {
165
228
  return (result.content[0] as { text?: string })?.text ?? "";
166
229
  }
167
230
 
@@ -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;
@@ -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
- export const MAX_SUBAGENT_DEPTH_CEILING = 10;
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 detectMessageError(messages: Message[]): boolean {
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.some((c) => c.type === "text" && c.text.trim().length > 0)
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
- lastAssistantIdx = i;
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
+ }