@mystilleef/pi-subagent 0.7.0 → 0.9.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.
@@ -16,6 +16,7 @@ import {
16
16
  patchProgressState,
17
17
  renderToolActivity,
18
18
  } from "./progress.js";
19
+ import { SENSITIVE_PATTERN } from "./progress-state.js";
19
20
 
20
21
  export function hasSubagentFailed(result: SingleResult): boolean {
21
22
  return (
@@ -41,6 +42,46 @@ export function createSubagentError(result: SingleResult): Error {
41
42
  return new Error(`Agent ${result.stopReason || "failed"}: ${msg}`);
42
43
  }
43
44
 
45
+ const DEBUG_REDACTED_PLACEHOLDER = "[redacted]";
46
+ const SENSITIVE_ASSIGNMENT_PATTERN =
47
+ /\b(?:secret|password|[A-Za-z0-9_-]*token)(?:\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi;
48
+ const SENSITIVE_TERM_PATTERN = new RegExp(SENSITIVE_PATTERN.source, "gi");
49
+ const TOKEN_COUNT_KEY_PATTERN = /tokens$/i;
50
+
51
+ function redactSensitiveDebugString(text: string): string {
52
+ return text
53
+ .replace(SENSITIVE_ASSIGNMENT_PATTERN, DEBUG_REDACTED_PLACEHOLDER)
54
+ .replace(SENSITIVE_TERM_PATTERN, DEBUG_REDACTED_PLACEHOLDER);
55
+ }
56
+
57
+ function isSensitiveDebugKey(key: string): boolean {
58
+ const lowerKey = key.toLowerCase();
59
+ if (TOKEN_COUNT_KEY_PATTERN.test(lowerKey)) return false;
60
+ return (
61
+ lowerKey.includes("secret") ||
62
+ lowerKey.includes("password") ||
63
+ lowerKey.endsWith("token")
64
+ );
65
+ }
66
+
67
+ function redactSensitiveDebugValue(value: unknown): unknown {
68
+ if (typeof value === "string") return redactSensitiveDebugString(value);
69
+ if (Array.isArray(value)) return value.map(redactSensitiveDebugValue);
70
+ if (typeof value !== "object" || value === null) return value;
71
+ const redacted: Record<string, unknown> = {};
72
+ for (const [key, child] of Object.entries(value)) {
73
+ redacted[key] = isSensitiveDebugKey(key)
74
+ ? DEBUG_REDACTED_PLACEHOLDER
75
+ : redactSensitiveDebugValue(child);
76
+ }
77
+ return redacted;
78
+ }
79
+
80
+ function redactSensitiveDebugMessages(messages: unknown): unknown {
81
+ if (!Array.isArray(messages)) return messages;
82
+ return messages.map(redactSensitiveDebugValue);
83
+ }
84
+
44
85
  export function sanitizeDetailsForDisplay(
45
86
  details: SubagentDetails,
46
87
  includeMessages = false,
@@ -50,9 +91,11 @@ export function sanitizeDetailsForDisplay(
50
91
  results: details.results.map(({ messages, termination, ...result }) => ({
51
92
  ...result,
52
93
  stderr: includeMessages ? result.stderr : "",
53
- ...(includeMessages ? { messages, termination } : {}),
94
+ ...(includeMessages
95
+ ? { messages: redactSensitiveDebugMessages(messages), termination }
96
+ : {}),
54
97
  })),
55
- };
98
+ } as SubagentDetails;
56
99
  }
57
100
 
58
101
  export function getLatestResult(
@@ -90,26 +133,27 @@ export function patchProgressFromDetails(
90
133
  nextActivity = current.activeToolActivity;
91
134
  }
92
135
  if (toolResultCompleted && nextActivity?.child) {
93
- nextActivity = { ...nextActivity, child: undefined };
136
+ const { child: _child, ...rest } = nextActivity;
137
+ nextActivity = rest;
94
138
  } else if (toolResultCompleted) {
95
139
  nextActivity = undefined;
96
140
  }
97
- patch.activeToolActivity = nextActivity;
141
+ patch["activeToolActivity"] = nextActivity;
98
142
  const renderedPreview = renderToolActivity(nextActivity);
99
143
  if (renderedPreview) {
100
- patch.lastToolPreview = renderedPreview;
144
+ patch["lastToolPreview"] = renderedPreview;
101
145
  } else if (toolResultCompleted && !nextActivity) {
102
- patch.lastToolPreview = undefined;
146
+ patch["lastToolPreview"] = undefined;
103
147
  }
104
148
  if (toolResultCompleted) {
105
- patch.toolResultCompleted = true;
149
+ patch["toolResultCompleted"] = true;
106
150
  }
107
151
  // Token accounting always applies when usage data is available
108
152
  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;
153
+ patch["inputTokens"] = latestResult.usage.input;
154
+ patch["outputTokens"] = latestResult.usage.output;
155
+ patch["contextTokens"] = latestResult.usage.contextTokens;
156
+ patch["contextWindowTokens"] = latestResult.usage.contextWindowTokens;
113
157
  }
114
158
  patchProgressState(
115
159
  requestId,
@@ -15,9 +15,9 @@ export interface UsageStats {
15
15
 
16
16
  export interface ToolActivity {
17
17
  toolName: string;
18
- inputSummary?: string;
19
- instanceName?: string;
20
- child?: ToolActivity;
18
+ inputSummary?: string | undefined;
19
+ instanceName?: string | undefined;
20
+ child?: ToolActivity | undefined;
21
21
  }
22
22
 
23
23
  export interface StreamingProgressToolCall {
@@ -26,30 +26,30 @@ export interface StreamingProgressToolCall {
26
26
  }
27
27
 
28
28
  export interface StreamingProgress {
29
- activityText?: string;
30
- activeToolActivity?: ToolActivity;
29
+ activityText?: string | undefined;
30
+ activeToolActivity?: ToolActivity | undefined;
31
31
  toolCalls: StreamingProgressToolCall[];
32
- lastToolPreview?: string;
33
- toolResultCompleted?: boolean;
32
+ lastToolPreview?: string | undefined;
33
+ toolResultCompleted?: boolean | undefined;
34
34
  }
35
35
 
36
36
  export interface SingleResult {
37
37
  agent: string;
38
- instanceName?: string;
38
+ instanceName?: string | undefined;
39
39
  agentSource: "user" | "project" | "unknown";
40
40
  task: string;
41
41
  exitCode: number;
42
42
  finalOutput: string;
43
43
  stderr: string;
44
44
  usage: UsageStats;
45
- model?: string;
46
- stopReason?: string;
47
- errorMessage?: string;
48
- durationMs?: number;
49
- progress?: StreamingProgress;
50
- messages?: Message[];
51
- termination?: TerminationMetadata;
52
- thinkingWarning?: string;
45
+ model?: string | undefined;
46
+ stopReason?: string | undefined;
47
+ errorMessage?: string | undefined;
48
+ durationMs?: number | undefined;
49
+ progress?: StreamingProgress | undefined;
50
+ messages?: Message[] | undefined;
51
+ termination?: TerminationMetadata | undefined;
52
+ thinkingWarning?: string | undefined;
53
53
  }
54
54
 
55
55
  export interface SubagentDetails {
@@ -9,35 +9,63 @@ import {
9
9
 
10
10
  export const DEFAULT_MAX_OUTPUT_BYTES = 50_000;
11
11
  export const DEFAULT_MAX_OUTPUT_LINES = 500;
12
+ export const DEFAULT_AGENT_END_GRACE_MS = 250;
13
+ export const DEFAULT_MAX_STDERR_BYTES = 10_000;
14
+ export const DEFAULT_MAX_SUBAGENT_DEPTH = 3;
15
+ export const MAX_SUBAGENT_DEPTH_CEILING = 10;
12
16
 
13
17
  export interface SubagentOutputLimits {
14
18
  maxBytes: number;
15
19
  maxLines: number;
16
20
  }
17
21
 
18
- type OutputLimitConfig = Partial<Record<string, string | number | undefined>>;
22
+ export interface SubagentRuntimeLimits {
23
+ agentEndGraceMs: number;
24
+ maxStderrBytes: number;
25
+ maxDepth: number;
26
+ }
27
+
28
+ type EnvLimitConfig = Partial<Record<string, string | number | undefined>>;
19
29
 
20
30
  function parsePositiveInteger(
21
31
  value: string | number | undefined,
22
32
  ): number | undefined {
23
33
  const parsed = typeof value === "number" ? value : Number(value);
24
- if (!Number.isFinite(parsed) || parsed < 1) return undefined;
25
- return Math.floor(parsed);
34
+ if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1)
35
+ return undefined;
36
+ return parsed;
26
37
  }
27
38
 
28
39
  export function getSubagentOutputLimits(
29
- config: OutputLimitConfig = process.env,
40
+ config: EnvLimitConfig = process.env,
30
41
  ): SubagentOutputLimits {
31
42
  return {
32
43
  maxBytes:
33
- parsePositiveInteger(config.PI_SUBAGENT_MAX_OUTPUT_BYTES) ??
44
+ parsePositiveInteger(config["PI_SUBAGENT_MAX_OUTPUT_BYTES"]) ??
34
45
  DEFAULT_MAX_OUTPUT_BYTES,
35
46
  maxLines:
36
- parsePositiveInteger(config.PI_SUBAGENT_MAX_OUTPUT_LINES) ??
47
+ parsePositiveInteger(config["PI_SUBAGENT_MAX_OUTPUT_LINES"]) ??
37
48
  DEFAULT_MAX_OUTPUT_LINES,
38
49
  };
39
50
  }
40
51
 
52
+ export function getSubagentRuntimeLimits(
53
+ config: EnvLimitConfig = process.env,
54
+ ): SubagentRuntimeLimits {
55
+ const maxDepth =
56
+ parsePositiveInteger(config["PI_SUBAGENT_MAX_DEPTH"]) ??
57
+ DEFAULT_MAX_SUBAGENT_DEPTH;
58
+ return {
59
+ agentEndGraceMs:
60
+ parsePositiveInteger(config["PI_SUBAGENT_AGENT_END_GRACE_MS"]) ??
61
+ DEFAULT_AGENT_END_GRACE_MS,
62
+ maxStderrBytes:
63
+ parsePositiveInteger(config["PI_SUBAGENT_MAX_STDERR_BYTES"]) ??
64
+ DEFAULT_MAX_STDERR_BYTES,
65
+ maxDepth: Math.min(maxDepth, MAX_SUBAGENT_DEPTH_CEILING),
66
+ };
67
+ }
68
+
41
69
  export function truncateOutput(
42
70
  text: string,
43
71
  limits: SubagentOutputLimits = getSubagentOutputLimits(),
@@ -89,15 +117,54 @@ export function getPiInvocation(args: string[]): {
89
117
  return { command: "pi", args };
90
118
  }
91
119
 
120
+ const SKILL_DISCOVERY_CACHE_TTL_MS = 300_000;
121
+
122
+ type ResolvedSkillArgsCacheEntry = {
123
+ skillPaths: Map<string, string>;
124
+ ts: number;
125
+ };
126
+
127
+ const resolvedSkillArgsCache = new Map<string, ResolvedSkillArgsCacheEntry>();
128
+
129
+ async function canonicalPath(filePath: string): Promise<string> {
130
+ try {
131
+ return await fs.promises.realpath(filePath);
132
+ } catch {
133
+ return path.resolve(filePath);
134
+ }
135
+ }
136
+
137
+ function buildSkillArgs(
138
+ requested: string[],
139
+ skillPaths: Map<string, string>,
140
+ ): string[] {
141
+ return requested.flatMap((name) => ["--skill", skillPaths.get(name) ?? name]);
142
+ }
143
+
144
+ export function resetResolvedAgentSkillArgsCache(): void {
145
+ resolvedSkillArgsCache.clear();
146
+ }
147
+
92
148
  export async function resolveAgentSkillArgs(
93
149
  cwd: string,
94
150
  skillNames: string[],
95
151
  ): Promise<{ args: string[] } | { error: string }> {
96
152
  const requested = Array.from(new Set(skillNames));
97
153
  if (requested.length === 0) return { args: [] };
154
+ const cacheIdentitySkills = [...requested].sort();
155
+ const agentDir = getAgentDir();
156
+ const cacheKey = JSON.stringify({
157
+ cwd: await canonicalPath(cwd),
158
+ agentDir: await canonicalPath(agentDir),
159
+ skills: cacheIdentitySkills,
160
+ });
161
+ const cached = resolvedSkillArgsCache.get(cacheKey);
162
+ if (cached && Date.now() - cached.ts <= SKILL_DISCOVERY_CACHE_TTL_MS) {
163
+ return { args: buildSkillArgs(requested, cached.skillPaths) };
164
+ }
98
165
  const loader = new DefaultResourceLoader({
99
166
  cwd,
100
- agentDir: getAgentDir(),
167
+ agentDir,
101
168
  noContextFiles: true,
102
169
  noPromptTemplates: true,
103
170
  noThemes: true,
@@ -124,12 +191,12 @@ export async function resolveAgentSkillArgs(
124
191
  .join(", ")}. Available skills: ${available}.`,
125
192
  };
126
193
  }
127
- return {
128
- args: requested.flatMap((name) => [
129
- "--skill",
130
- skillMap.get(name)?.filePath ?? name,
131
- ]),
132
- };
194
+ const skillPaths = new Map(
195
+ requested.map((name) => [name, skillMap.get(name)?.filePath ?? name]),
196
+ );
197
+ const args = buildSkillArgs(requested, skillPaths);
198
+ resolvedSkillArgsCache.set(cacheKey, { skillPaths, ts: Date.now() });
199
+ return { args };
133
200
  }
134
201
 
135
202
  export function getSubagentDepth(): number {
package/tsconfig.json CHANGED
@@ -1,30 +1,28 @@
1
1
  {
2
2
  "compilerOptions": {
3
- // Environment setup & latest features
4
3
  "lib": ["ESNext"],
5
4
  "target": "ESNext",
6
5
  "module": "Preserve",
7
6
  "moduleDetection": "force",
8
7
  "jsx": "react-jsx",
9
- "allowJs": true,
10
8
  "types": ["bun"],
11
-
12
- // Bundler mode
13
9
  "moduleResolution": "bundler",
14
10
  "allowImportingTsExtensions": true,
15
11
  "verbatimModuleSyntax": true,
16
12
  "noEmit": true,
17
-
18
- // Best practices
19
13
  "strict": true,
20
14
  "skipLibCheck": true,
21
15
  "noFallthroughCasesInSwitch": true,
22
16
  "noUncheckedIndexedAccess": true,
23
17
  "noImplicitOverride": true,
24
-
25
- // Some stricter flags (disabled by default)
26
- "noUnusedLocals": false,
27
- "noUnusedParameters": false,
28
- "noPropertyAccessFromIndexSignature": false
18
+ "noImplicitReturns": true,
19
+ "erasableSyntaxOnly": true,
20
+ "forceConsistentCasingInFileNames": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "noPropertyAccessFromIndexSignature": true,
24
+ "exactOptionalPropertyTypes": true,
25
+ "allowUnreachableCode": false,
26
+ "allowUnusedLabels": false
29
27
  }
30
28
  }