@mystilleef/pi-subagent 0.8.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.
package/README.md CHANGED
@@ -130,8 +130,9 @@ delegation.
130
130
  - `task`: task prompt for the child agent.
131
131
  - `agentScope`: optional lookup scope, one of `user`, `project`, or
132
132
  `both`.
133
- - `debug`: optional flag that includes full child messages in result
134
- details.
133
+ - `debug`: optional flag that requests child diagnostic details. Full child
134
+ messages and raw internals require `PI_SUBAGENT_DEBUG_ENABLED=1` in the
135
+ host environment.
135
136
 
136
137
  ## Security
137
138
 
@@ -143,6 +144,12 @@ executable automation.
143
144
 
144
145
  - Review project-local agents before running them.
145
146
  - Avoid delegating secrets unless the agent and tools need them.
147
+ - Treat child-agent prompts, tool arguments, stderr, and debug transcripts as
148
+ potentially sensitive.
149
+ - Enable debug details only for trusted investigations. `debug: true` or
150
+ `/run --debug` can expose child conversation transcripts, termination
151
+ internals, and stderr only when the host explicitly sets
152
+ `PI_SUBAGENT_DEBUG_ENABLED=1`.
146
153
  - Prefer trusted repositories for shared agent definitions.
147
154
  - Remember that child agents can call their configured tools.
148
155
 
@@ -150,12 +157,26 @@ executable automation.
150
157
 
151
158
  **Environment variables:**
152
159
 
153
- - `PI_SUBAGENT_DEPTH`: nested subagent depth guard. Nested calls stop at
154
- depth `3`.
160
+ - `PI_SUBAGENT_DEPTH`: current nested subagent depth counter set internally
161
+ for child processes.
162
+ - `PI_SUBAGENT_MAX_DEPTH`: max nested subagent depth. Default: `3`. Values
163
+ above `10` clamp to the internal ceiling `10`; deeper nesting increases
164
+ cost, latency, and runaway delegation risk.
165
+ - `PI_SUBAGENT_AGENT_END_GRACE_MS`: child process grace period after
166
+ `agent_end` before forced termination. Default: `250`.
167
+ - `PI_SUBAGENT_MAX_STDERR_BYTES`: max captured child stderr bytes. Default:
168
+ `10000`.
155
169
  - `PI_SUBAGENT_MAX_OUTPUT_BYTES`: max returned output bytes. Default:
156
170
  `50000`.
157
171
  - `PI_SUBAGENT_MAX_OUTPUT_LINES`: max returned output lines. Default:
158
172
  `500`.
173
+ - `PI_SUBAGENT_DEBUG_ENABLED`: debug detail authorization. Set to `1` to
174
+ allow `debug: true` or `/run --debug` to include sanitized child messages,
175
+ termination internals, and stderr; unset values keep non-debug detail
176
+ behavior.
177
+
178
+ Limit variables parse as positive integers. Empty, zero, negative, decimal,
179
+ `Infinity`, and non-numeric values fall back to defaults.
159
180
 
160
181
  ## Troubleshooting
161
182
 
@@ -173,7 +194,7 @@ executable automation.
173
194
 
174
195
  **Nested subagent blocked:**
175
196
 
176
- - Nested delegation hits the `PI_SUBAGENT_DEPTH` safety limit.
197
+ - Nested delegation hits the `PI_SUBAGENT_MAX_DEPTH` safety limit.
177
198
  - Run the child task directly from the parent session instead.
178
199
 
179
200
  **Truncated output:**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mystilleef/pi-subagent",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Pi subagent for the SPAE Framework",
5
5
  "author": "Lateef Alabi-Oki <mystilleef@gmail.com>",
6
6
  "license": "MIT",
@@ -50,7 +50,7 @@
50
50
  "typecheck": "tsc --noEmit",
51
51
  "lint": "biome check --write --unsafe --error-on-warnings .",
52
52
  "migrate": "biome migrate --write",
53
- "coverage": "bun test --coverage",
53
+ "coverage": "bun test --parallel --parallel-delay=0",
54
54
  "check": "bun lint && bun typecheck",
55
55
  "verify": "bun migrate && bun check && bun coverage",
56
56
  "pack:smoke": "bun scripts/pack-smoke.ts",
@@ -70,8 +70,8 @@
70
70
  "@earendil-works/pi-coding-agent": "^0.78.1",
71
71
  "@earendil-works/pi-tui": "^0.78.1",
72
72
  "@types/bun": "^1.3.14",
73
- "@types/node": "^25.9.1",
74
- "typebox": "^1.2.1",
73
+ "@types/node": "^25.9.2",
74
+ "typebox": "^1.2.2",
75
75
  "typescript": "^6.0.3"
76
76
  }
77
77
  }
@@ -20,9 +20,9 @@ export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
20
20
  export interface AgentConfig {
21
21
  name: string;
22
22
  description: string;
23
- tools?: string[];
24
- skills?: string[];
25
- thinking?: ThinkingLevel;
23
+ tools?: string[] | undefined;
24
+ skills?: string[] | undefined;
25
+ thinking?: ThinkingLevel | undefined;
26
26
  systemPrompt: string;
27
27
  source: AgentSource;
28
28
  filePath: string;
@@ -1,11 +1,10 @@
1
1
  import { makeToolPreview } from "../output/normalize.js";
2
2
  import type { ToolActivity } from "../shared/types.js";
3
3
 
4
- // Extracts results[0] from details; null-safe for malformed input.
5
4
  function tryFirstResult(details: unknown): Record<string, unknown> | null {
6
5
  try {
7
6
  if (typeof details !== "object" || details === null) return null;
8
- const results = (details as Record<string, unknown>).results;
7
+ const results = (details as Record<string, unknown>)["results"];
9
8
  if (!Array.isArray(results) || results.length === 0) return null;
10
9
  const nested = results[0];
11
10
  if (typeof nested !== "object" || nested === null) return null;
@@ -15,7 +14,6 @@ function tryFirstResult(details: unknown): Record<string, unknown> | null {
15
14
  }
16
15
  }
17
16
 
18
- // Malformed details (null result) falls back to { toolName }.
19
17
  function parseToolActivity(
20
18
  toolName: string,
21
19
  partialResult: { content?: unknown; details?: unknown },
@@ -24,23 +22,25 @@ function parseToolActivity(
24
22
  if (!nestedRecord) return { toolName, inputSummary: toolName };
25
23
  const isSubagent = toolName === "subagent";
26
24
  const activity: ToolActivity = { toolName };
27
- const agent = typeof nestedRecord.agent === "string" && nestedRecord.agent;
25
+ const agent =
26
+ typeof nestedRecord["agent"] === "string" && nestedRecord["agent"];
28
27
  activity.inputSummary =
29
28
  isSubagent && agent ? makeToolPreview(toolName, nestedRecord) : toolName;
30
29
  if (
31
- typeof nestedRecord.instanceName === "string" &&
32
- nestedRecord.instanceName
30
+ typeof nestedRecord["instanceName"] === "string" &&
31
+ nestedRecord["instanceName"]
33
32
  ) {
34
- activity.instanceName = nestedRecord.instanceName;
33
+ activity.instanceName = nestedRecord["instanceName"] as string;
35
34
  }
36
- const progress = nestedRecord.progress;
35
+ const progress = nestedRecord["progress"];
37
36
  if (typeof progress === "object" && progress !== null) {
38
- const activeToolActivity = (progress as Record<string, unknown>)
39
- .activeToolActivity;
37
+ const activeToolActivity = (progress as Record<string, unknown>)[
38
+ "activeToolActivity"
39
+ ];
40
40
  if (
41
41
  typeof activeToolActivity === "object" &&
42
42
  activeToolActivity !== null &&
43
- typeof (activeToolActivity as Record<string, unknown>).toolName ===
43
+ typeof (activeToolActivity as Record<string, unknown>)["toolName"] ===
44
44
  "string"
45
45
  ) {
46
46
  const childActivity = activeToolActivity as ToolActivity;
@@ -96,21 +96,21 @@ export function parseChildEventLine(line: string): ChildEventParseResult {
96
96
  typeof event === "object" &&
97
97
  event !== null &&
98
98
  "type" in event &&
99
- typeof (event as Record<string, unknown>).type === "string" &&
100
- KNOWN_TYPES.has((event as Record<string, unknown>).type as string)
99
+ typeof (event as Record<string, unknown>)["type"] === "string" &&
100
+ KNOWN_TYPES.has((event as Record<string, unknown>)["type"] as string)
101
101
  ) {
102
102
  const record = event as Record<string, unknown>;
103
- if (record.type === TOOL_EXECUTION_UPDATE_EVENT) {
103
+ if (record["type"] === TOOL_EXECUTION_UPDATE_EVENT) {
104
104
  if (
105
- typeof record.toolName !== "string" ||
106
- typeof record.partialResult !== "object" ||
107
- record.partialResult === null
105
+ typeof record["toolName"] !== "string" ||
106
+ typeof record["partialResult"] !== "object" ||
107
+ record["partialResult"] === null
108
108
  ) {
109
109
  return { kind: "unknown", event };
110
110
  }
111
- record.toolActivity = parseToolActivity(
112
- record.toolName as string,
113
- record.partialResult as { content?: unknown; details?: unknown },
111
+ record["toolActivity"] = parseToolActivity(
112
+ record["toolName"] as string,
113
+ record["partialResult"] as { content?: unknown; details?: unknown },
114
114
  );
115
115
  }
116
116
  return { kind: "known", event: event as ChildKnownEvent };
@@ -32,12 +32,14 @@ import {
32
32
  detectMessageError,
33
33
  getPiInvocation,
34
34
  getSubagentDepth,
35
+ getSubagentRuntimeLimits,
35
36
  resolveAgentSkillArgs,
36
37
  subagentDepthEnv,
37
38
  truncateOutput,
38
39
  writePromptToTempFile,
39
40
  } from "../shared/utils.js";
40
41
  import {
42
+ type ChildEventParseResult,
41
43
  type ChildKnownEvent,
42
44
  parseChildEventLine,
43
45
  TOOL_EXECUTION_UPDATE_EVENT,
@@ -48,8 +50,6 @@ import {
48
50
  terminateChildProcess,
49
51
  } from "./termination.js";
50
52
 
51
- const MAX_STDERR_BYTES = 10_000;
52
- const AGENT_END_GRACE_MS = 250;
53
53
  export function resolveThinkingLevel(
54
54
  requested: ThinkingLevel,
55
55
  provider: string,
@@ -73,15 +73,17 @@ export function resolveThinkingLevel(
73
73
  return { level: clamped, warning: mkWarning(clamped) };
74
74
  }
75
75
 
76
- const MAX_SUBAGENT_DEPTH = 3;
77
76
  export const TOOL_RESULT_FAILED_MESSAGE = "Subagent tool result failed.";
78
77
 
78
+ type RuntimeLimits = ReturnType<typeof getSubagentRuntimeLimits>;
79
79
  type RuntimeResult = SingleResult & { messages: Message[] };
80
80
 
81
81
  export class SubagentAbortError extends Error {
82
- constructor(public readonly result: SingleResult) {
82
+ readonly result: SingleResult;
83
+ constructor(result: SingleResult) {
83
84
  super("Subagent was aborted");
84
85
  this.name = "SubagentAbortError";
86
+ this.result = result;
85
87
  }
86
88
  }
87
89
 
@@ -91,6 +93,7 @@ type PromptSetupResult = { tmpPrompt: TempPrompt | null } | { error: unknown };
91
93
 
92
94
  interface SubagentState {
93
95
  result: RuntimeResult;
96
+ runtimeLimits: RuntimeLimits;
94
97
  spawnError?: Error;
95
98
  wasAborted: boolean;
96
99
  agentEndGraceTimer?: ReturnType<typeof setTimeout>;
@@ -99,25 +102,41 @@ interface SubagentState {
99
102
 
100
103
  function appendWithByteLimit(
101
104
  current: string,
102
- data: string,
105
+ data: string | Buffer,
103
106
  max: number,
104
107
  ): string {
105
- if (current.length >= max) return current;
106
- return current + data.slice(0, max - current.length);
108
+ const currentBytes = Buffer.from(current, "utf-8");
109
+ if (currentBytes.length >= max) return current;
110
+ const incomingBytes = Buffer.isBuffer(data)
111
+ ? data
112
+ : Buffer.from(data, "utf-8");
113
+ const combined = Buffer.concat([currentBytes, incomingBytes]);
114
+ if (combined.length <= max) return combined.toString("utf-8");
115
+ return truncateValidUtf8(combined, max);
116
+ }
117
+
118
+ function truncateValidUtf8(buffer: Buffer, max: number): string {
119
+ let end = Math.min(max, buffer.length);
120
+ while (end > 0) {
121
+ const candidate = buffer.subarray(0, end).toString("utf-8");
122
+ if (!candidate.endsWith("�")) return candidate;
123
+ end -= 1;
124
+ }
125
+ return "";
107
126
  }
108
127
 
109
128
  /**
110
- * Attempts to resolve the context window token limit for a given message's model.
111
129
  * Rationale: Subagent usage reporting needs context window awareness to provide
112
130
  * meaningful "context full" indicators to the parent.
113
131
  */
114
132
  function resolveContextWindowTokens(msg: Message): number | undefined {
115
133
  const m = msg as unknown as Record<string, unknown>;
116
- if (typeof m.provider !== "string" || typeof m.model !== "string") return;
134
+ if (typeof m["provider"] !== "string" || typeof m["model"] !== "string")
135
+ return;
117
136
  try {
118
137
  const contextWindow = getModel(
119
- m.provider as never,
120
- m.model as never,
138
+ m["provider"] as never,
139
+ m["model"] as never,
121
140
  )?.contextWindow;
122
141
  return Number.isFinite(contextWindow) && contextWindow > 0
123
142
  ? contextWindow
@@ -134,11 +153,6 @@ function getAbortReason(signal: AbortSignal): string {
134
153
  return "abort";
135
154
  }
136
155
 
137
- /**
138
- * Verifies if the agent produced any textual output or final response.
139
- * Precondition: Called after process exit to distinguish between clean completion
140
- * and silent failures where the process exited 0 but did nothing.
141
- */
142
156
  function hasCompletedAgentOutput(result: RuntimeResult): boolean {
143
157
  if (result.finalOutput.trim()) return true;
144
158
  return result.messages.some(
@@ -151,7 +165,6 @@ function hasCompletedAgentOutput(result: RuntimeResult): boolean {
151
165
  }
152
166
 
153
167
  /**
154
- * Determines the exit code for processes terminated via the agent_end timeout.
155
168
  * Rationale: `pi` processes in JSON mode might hang after finishing their task;
156
169
  * we force-kill them after a grace period and treat it as success (0) if they
157
170
  * actually produced output.
@@ -168,7 +181,6 @@ function getAgentEndTimeoutExitCode(
168
181
  }
169
182
 
170
183
  /**
171
- * Orchestrates the cleanup and exit code capture of a child process.
172
184
  * Safety: Implements a dual-timer strategy (idle and hard) to ensure streams
173
185
  * are destroyed and promises settled even if the process or its pipes hang.
174
186
  */
@@ -278,8 +290,9 @@ function accumulateUsage(result: RuntimeResult, msg: Message): void {
278
290
  result.usage.cacheWrite += usage.cacheWrite || 0;
279
291
  result.usage.cost += usage.cost?.total || 0;
280
292
  result.usage.contextTokens = usage.totalTokens || 0;
281
- result.usage.contextWindowTokens =
282
- resolveContextWindowTokens(msg) ?? result.usage.contextWindowTokens;
293
+ const ctxWindowTokens = resolveContextWindowTokens(msg);
294
+ if (ctxWindowTokens !== undefined)
295
+ result.usage.contextWindowTokens = ctxWindowTokens;
283
296
  }
284
297
 
285
298
  function addMessageToResult(result: RuntimeResult, msg: Message): void {
@@ -288,7 +301,7 @@ function addMessageToResult(result: RuntimeResult, msg: Message): void {
288
301
  if (msg.role === "toolResult" && msg.isError) {
289
302
  result.errorMessage ||= TOOL_RESULT_FAILED_MESSAGE;
290
303
  } else if (result.errorMessage === TOOL_RESULT_FAILED_MESSAGE) {
291
- result.errorMessage = undefined;
304
+ delete result.errorMessage;
292
305
  }
293
306
  if (msg.role === "assistant") {
294
307
  accumulateUsage(result, msg);
@@ -336,13 +349,14 @@ function errorForDepthLimit(
336
349
  source: "user" | "project" | "unknown",
337
350
  task: string,
338
351
  depth: number,
352
+ maxDepth: number,
339
353
  model?: string,
340
354
  ): SingleResult {
341
355
  return createErrorResult(
342
356
  agentName,
343
357
  source,
344
358
  task,
345
- `Subagent nesting limit reached (depth ${depth}/${MAX_SUBAGENT_DEPTH}).`,
359
+ `Subagent nesting limit reached (depth ${depth}/${maxDepth}).`,
346
360
  model,
347
361
  );
348
362
  }
@@ -387,12 +401,6 @@ function findRecentMessagesAnchor(messages: Message[]): number {
387
401
  return -1;
388
402
  }
389
403
 
390
- /**
391
- * Derives current execution progress from accumulated messages.
392
- * Maps tool calls to UI-safe previews for real-time feedback.
393
- * Builds activeToolActivity from the most recent tool call, providing
394
- * a compact parent summary for subagent tools before nested child data arrives.
395
- */
396
404
  function deriveStreamingProgress(messages: Message[]): StreamingProgress {
397
405
  const toolCalls: { id: string; preview: string }[] = [];
398
406
  let lastToolPreview: string | undefined;
@@ -410,18 +418,15 @@ function deriveStreamingProgress(messages: Message[]): StreamingProgress {
410
418
  activeToolActivity = { toolName: part.name, inputSummary: preview };
411
419
  }
412
420
  }
421
+ const activityText = renderToolActivity(activeToolActivity);
413
422
  return {
414
423
  activeToolActivity,
415
- activityText: renderToolActivity(activeToolActivity),
424
+ activityText,
416
425
  toolCalls,
417
426
  lastToolPreview,
418
427
  };
419
428
  }
420
429
 
421
- /**
422
- * Prevents leaking secrets in the CLI progress display.
423
- * Redacts values if the preview contains sensitive keywords.
424
- */
425
430
  function sanitizeProgressPreview(preview: string, toolName: string): string {
426
431
  return SENSITIVE_PATTERN.test(preview) ? toolName : preview;
427
432
  }
@@ -447,7 +452,9 @@ export function makeEmitUpdate(
447
452
  // so the parent retains nested context until newer activity arrives
448
453
  if (options?.toolResultCompleted && result.progress?.activeToolActivity) {
449
454
  progress.activeToolActivity = result.progress.activeToolActivity;
450
- progress.activityText = renderToolActivity(progress.activeToolActivity);
455
+ const renderedText = renderToolActivity(progress.activeToolActivity);
456
+ if (renderedText !== undefined) progress.activityText = renderedText;
457
+ else delete progress.activityText;
451
458
  }
452
459
  // Handle parsed tool activity from child events
453
460
  // Merge with parent activity if this is a nested update
@@ -474,7 +481,10 @@ export function makeEmitUpdate(
474
481
  } else {
475
482
  progress.activeToolActivity = options.toolActivity;
476
483
  }
477
- progress.activityText = renderToolActivity(progress.activeToolActivity);
484
+ const renderedActivity = renderToolActivity(progress.activeToolActivity);
485
+ if (renderedActivity !== undefined)
486
+ progress.activityText = renderedActivity;
487
+ else delete progress.activityText;
478
488
  }
479
489
  if (options?.toolResultCompleted) {
480
490
  progress.toolResultCompleted = true;
@@ -515,7 +525,7 @@ function makeRequestTerminator(
515
525
  function clearGraceTimer(state: SubagentState): void {
516
526
  if (!state.agentEndGraceTimer) return;
517
527
  clearTimeout(state.agentEndGraceTimer);
518
- state.agentEndGraceTimer = undefined;
528
+ delete state.agentEndGraceTimer;
519
529
  }
520
530
 
521
531
  function handleMessageEvent(
@@ -563,12 +573,25 @@ function handleAgentEndEvent(
563
573
  }
564
574
  if (state.agentEndGraceTimer || state.terminationPromise) return;
565
575
  state.agentEndGraceTimer = setTimeout(() => {
566
- state.agentEndGraceTimer = undefined;
576
+ delete state.agentEndGraceTimer;
567
577
  void requestTermination("agent_end_timeout");
568
- }, AGENT_END_GRACE_MS);
578
+ }, state.runtimeLimits.agentEndGraceMs);
569
579
  state.agentEndGraceTimer.unref?.();
570
580
  }
571
581
 
582
+ function formatUnknownEventDiagnostic(
583
+ line: string,
584
+ parseResult: Exclude<ChildEventParseResult, { kind: "known" }>,
585
+ ): string {
586
+ if (parseResult.kind === "invalid" && !line.trim()) {
587
+ return "[pi-subagent:unknown-event] blank";
588
+ }
589
+ if (parseResult.kind === "invalid") {
590
+ return `[pi-subagent:unknown-event] malformed: ${line}`;
591
+ }
592
+ return `[pi-subagent:unknown-event] unknown: ${JSON.stringify(parseResult.event)}`;
593
+ }
594
+
572
595
  function processEventLine(
573
596
  line: string,
574
597
  state: SubagentState,
@@ -577,9 +600,17 @@ function processEventLine(
577
600
  toolResultCompleted?: boolean;
578
601
  }) => void,
579
602
  requestTermination: (reason: string) => Promise<unknown>,
603
+ debugEventDiagnostics: boolean,
580
604
  ): void {
581
605
  const parseResult = parseChildEventLine(line);
582
- if (parseResult.kind !== "known") return;
606
+ if (parseResult.kind !== "known") {
607
+ if (debugEventDiagnostics) {
608
+ process.stderr.write(
609
+ `${formatUnknownEventDiagnostic(line, parseResult)}\n`,
610
+ );
611
+ }
612
+ return;
613
+ }
583
614
  const { event } = parseResult;
584
615
  handleMessageEvent(event, state, emitUpdate);
585
616
  handleToolExecutionUpdateEvent(event, emitUpdate);
@@ -639,28 +670,35 @@ function setupChildProcess(
639
670
  toolResultCompleted?: boolean;
640
671
  }) => void,
641
672
  requestTermination: (reason: string) => Promise<unknown>,
673
+ debugEventDiagnostics: boolean,
642
674
  ): void {
643
675
  proc.once("error", (error) => {
644
676
  state.spawnError = error;
645
677
  state.result.stderr = appendWithByteLimit(
646
678
  state.result.stderr,
647
679
  error.message,
648
- MAX_STDERR_BYTES,
680
+ state.runtimeLimits.maxStderrBytes,
649
681
  );
650
682
  });
651
683
  if (proc.stdout) {
652
684
  readline
653
685
  .createInterface({ input: proc.stdout })
654
686
  .on("line", (line) =>
655
- processEventLine(line, state, emitUpdate, requestTermination),
687
+ processEventLine(
688
+ line,
689
+ state,
690
+ emitUpdate,
691
+ requestTermination,
692
+ debugEventDiagnostics,
693
+ ),
656
694
  );
657
695
  }
658
696
  if (proc.stderr) {
659
- proc.stderr.on("data", (data) => {
697
+ proc.stderr.on("data", (data: Buffer) => {
660
698
  state.result.stderr = appendWithByteLimit(
661
699
  state.result.stderr,
662
- data.toString(),
663
- MAX_STDERR_BYTES,
700
+ data,
701
+ state.runtimeLimits.maxStderrBytes,
664
702
  );
665
703
  });
666
704
  }
@@ -689,8 +727,6 @@ async function finalizeResult(
689
727
  }
690
728
 
691
729
  /**
692
- * Executes a single subagent task.
693
- *
694
730
  * Rationale: Subagents run in isolated child processes to protect the parent's
695
731
  * context window and allow specialized system prompts/tools without polluting
696
732
  * the main conversation.
@@ -716,12 +752,20 @@ export async function runSingleAgent(
716
752
  ) => SubagentDetails,
717
753
  parentModel: { provider: string; id: string } | undefined,
718
754
  parentThinking: ThinkingLevel,
755
+ debugEventDiagnostics = false,
719
756
  ): Promise<SingleResult> {
720
757
  const agent = agents.find((a) => a.name === agentName);
721
758
  if (!agent) return errorForUnknownAgent(agentName, agents, task);
759
+ const runtimeLimits = getSubagentRuntimeLimits();
722
760
  const depth = getSubagentDepth();
723
- if (depth >= MAX_SUBAGENT_DEPTH) {
724
- return errorForDepthLimit(agentName, agent.source, task, depth);
761
+ if (depth >= runtimeLimits.maxDepth) {
762
+ return errorForDepthLimit(
763
+ agentName,
764
+ agent.source,
765
+ task,
766
+ depth,
767
+ runtimeLimits.maxDepth,
768
+ );
725
769
  }
726
770
  const requestedThinking = agent.thinking ?? parentThinking;
727
771
  const { level: thinking, warning: thinkingWarning } = parentModel
@@ -754,6 +798,7 @@ export async function runSingleAgent(
754
798
  const startedAt = Date.now();
755
799
  const state: SubagentState = {
756
800
  result: initRuntimeResult(agentName, agent.source, task, modelDisplay),
801
+ runtimeLimits,
757
802
  wasAborted: false,
758
803
  };
759
804
  if (thinkingWarning) state.result.thinkingWarning = thinkingWarning;
@@ -787,7 +832,13 @@ export async function runSingleAgent(
787
832
  terminateOptions,
788
833
  state,
789
834
  );
790
- setupChildProcess(proc, state, emitUpdate, requestTermination);
835
+ setupChildProcess(
836
+ proc,
837
+ state,
838
+ emitUpdate,
839
+ requestTermination,
840
+ debugEventDiagnostics,
841
+ );
791
842
  const onAbort = setupAbortHandler(
792
843
  signal,
793
844
  state,
@@ -4,12 +4,12 @@ export type TerminationSignal = "SIGTERM" | "SIGKILL";
4
4
 
5
5
  export type TerminationMetadata = {
6
6
  cancelRequestedAt: number;
7
- cancelReason?: string;
8
- terminationSignal?: TerminationSignal;
7
+ cancelReason?: string | undefined;
8
+ terminationSignal?: TerminationSignal | undefined;
9
9
  escalated: boolean;
10
10
  processTreeKilled: boolean;
11
11
  target: "direct" | "tree";
12
- fallbackCause?: string;
12
+ fallbackCause?: string | undefined;
13
13
  };
14
14
 
15
15
  type TimerHandle = unknown;
@@ -64,7 +64,7 @@ function settleState(state: TerminationState): void {
64
64
  if (state.settled) return;
65
65
  state.settled = true;
66
66
  if (state.timer) state.clearTimeout(state.timer);
67
- state.timer = undefined;
67
+ delete state.timer;
68
68
  state.resolve(state.metadata);
69
69
  }
70
70
 
package/src/env.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ declare namespace NodeJS {
2
+ interface ProcessEnv {
3
+ PATH?: string;
4
+ PI_CODING_AGENT_DIR?: string;
5
+ PI_SUBAGENT_DEPTH?: string;
6
+ PI_SUBAGENT_MAX_DEPTH?: string;
7
+ PI_SUBAGENT_MAX_OUTPUT_BYTES?: string;
8
+ PI_SUBAGENT_MAX_OUTPUT_LINES?: string;
9
+ PI_SUBAGENT_AGENT_END_GRACE_MS?: string;
10
+ PI_SUBAGENT_MAX_STDERR_BYTES?: string;
11
+ PI_SUBAGENT_DEBUG_ENABLED?: string;
12
+ }
13
+ }
@@ -48,7 +48,6 @@ export function cancelAllRunJobs(reason = "Cancelled"): number {
48
48
  return count;
49
49
  }
50
50
 
51
- export function clearRunJobsForTests(): void {
51
+ export function resetRunRegistry(): void {
52
52
  jobs.clear();
53
53
  }
54
- export const resetRunRegistry = clearRunJobsForTests;
@@ -77,6 +77,10 @@ type DetailsBuilder = (
77
77
  options?: DetailsOptions,
78
78
  ) => SubagentDetails;
79
79
 
80
+ function isDebugDetailsAuthorized(debugRequested: boolean): boolean {
81
+ return debugRequested && process.env.PI_SUBAGENT_DEBUG_ENABLED === "1";
82
+ }
83
+
80
84
  interface LifecycleContext {
81
85
  pi: ExtensionAPI;
82
86
  ctx: ExtensionContext;
@@ -90,7 +94,7 @@ interface LifecycleContext {
90
94
  task: string;
91
95
  parentModel: { provider: string; id: string } | undefined;
92
96
  parentThinking: ThinkingLevel;
93
- hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails>;
97
+ hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails> | undefined;
94
98
  }
95
99
 
96
100
  function createDetailsBuilder(
@@ -123,7 +127,7 @@ function sanitizeResultDetails(
123
127
  usage: { ...usageBase },
124
128
  };
125
129
  if (contextWindowTokens !== undefined) {
126
- (sanitized.usage as Record<string, unknown>).contextWindowTokens =
130
+ (sanitized["usage"] as Record<string, unknown>)["contextWindowTokens"] =
127
131
  contextWindowTokens;
128
132
  }
129
133
  if (progress !== undefined) {
@@ -134,7 +138,7 @@ function sanitizeResultDetails(
134
138
  toolResultCompleted,
135
139
  ...progBase
136
140
  } = progress;
137
- sanitized.progress = {
141
+ sanitized["progress"] = {
138
142
  toolCalls: progBase.toolCalls.map((tc) => ({
139
143
  id: tc.id,
140
144
  preview: tc.preview,
@@ -146,7 +150,7 @@ function sanitizeResultDetails(
146
150
  };
147
151
  }
148
152
  if (includeMessages) {
149
- sanitized.messages = options?.recentMessages
153
+ sanitized["messages"] = options?.recentMessages
150
154
  ? [...options.recentMessages]
151
155
  : messages !== undefined
152
156
  ? [...messages]
@@ -154,7 +158,7 @@ function sanitizeResultDetails(
154
158
  if (includeDebugMessages && termination !== undefined) {
155
159
  const { cancelReason, terminationSignal, fallbackCause, ...termBase } =
156
160
  termination;
157
- sanitized.termination = {
161
+ sanitized["termination"] = {
158
162
  ...termBase,
159
163
  ...(cancelReason !== undefined && { cancelReason }),
160
164
  ...(terminationSignal !== undefined && { terminationSignal }),
@@ -318,6 +322,7 @@ async function runSubagentLifecycle(
318
322
  lc.makeDetails,
319
323
  lc.parentModel,
320
324
  lc.parentThinking,
325
+ lc.debug,
321
326
  );
322
327
  return finishLifecycleResult(lc, result);
323
328
  } catch (error) {
@@ -358,7 +363,7 @@ type PrepareSubagentJobResult =
358
363
  lc: LifecycleContext;
359
364
  instanceName: string;
360
365
  requestProgressRender: () => void;
361
- hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails>;
366
+ hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails> | undefined;
362
367
  }
363
368
  | { kind: "not_found"; makeDetails: DetailsBuilder }
364
369
  | { kind: "cancelled"; makeDetails: DetailsBuilder }
@@ -409,7 +414,7 @@ async function prepareSubagentJob(
409
414
  const agentScope: AgentScope = params.agentScope ?? "both";
410
415
  const discovery = await getCachedAgentDiscovery(ctx.cwd, agentScope);
411
416
  const agents = discovery.agents;
412
- const debug = params.debug === true;
417
+ const debug = isDebugDetailsAuthorized(params.debug === true);
413
418
  const makeDetails = createDetailsBuilder(
414
419
  agentScope,
415
420
  discovery.projectAgentsDir,
@@ -44,21 +44,21 @@ export const STATUS_BG: Record<ProgressStatus, ThemeBg> = {
44
44
  export interface SubagentProgressState {
45
45
  requestId: string;
46
46
  agent: string;
47
- instanceName?: string;
47
+ instanceName?: string | undefined;
48
48
  taskPreview: string;
49
49
  status: ProgressStatus;
50
50
  startTime: number;
51
- durationMs?: number;
52
- activeToolActivity?: ToolActivity;
53
- lastToolPreview?: string;
54
- toolResultCompleted?: boolean;
51
+ durationMs?: number | undefined;
52
+ activeToolActivity?: ToolActivity | undefined;
53
+ lastToolPreview?: string | undefined;
54
+ toolResultCompleted?: boolean | undefined;
55
55
  toolCount: number;
56
- inputTokens?: number;
57
- outputTokens?: number;
58
- contextTokens?: number;
59
- contextWindowTokens?: number;
60
- finalOutput?: string;
61
- errorText?: string;
56
+ inputTokens?: number | undefined;
57
+ outputTokens?: number | undefined;
58
+ contextTokens?: number | undefined;
59
+ contextWindowTokens?: number | undefined;
60
+ finalOutput?: string | undefined;
61
+ errorText?: string | undefined;
62
62
  }
63
63
 
64
64
  const store = new Map<string, SubagentProgressState>();
@@ -89,6 +89,23 @@ export function getAllProgressStates(): SubagentProgressState[] {
89
89
  return [...store.values()].sort((a, b) => b.startTime - a.startTime);
90
90
  }
91
91
 
92
+ type ProgressTransientFields = Pick<
93
+ SubagentProgressState,
94
+ "activeToolActivity" | "lastToolPreview" | "toolResultCompleted"
95
+ >;
96
+
97
+ function stripTransientFields(
98
+ merged: SubagentProgressState,
99
+ ): Omit<SubagentProgressState, keyof ProgressTransientFields> {
100
+ const {
101
+ activeToolActivity: _a,
102
+ lastToolPreview: _l,
103
+ toolResultCompleted: _t,
104
+ ...base
105
+ } = merged;
106
+ return base;
107
+ }
108
+
92
109
  export function patchProgressState(
93
110
  requestId: string,
94
111
  patch: Partial<SubagentProgressState>,
@@ -96,13 +113,7 @@ export function patchProgressState(
96
113
  const state = store.get(requestId);
97
114
  if (!state) return;
98
115
  if (state.status !== "running") {
99
- store.set(requestId, {
100
- ...state,
101
- ...patch,
102
- activeToolActivity: undefined,
103
- lastToolPreview: undefined,
104
- toolResultCompleted: undefined,
105
- });
116
+ store.set(requestId, stripTransientFields({ ...state, ...patch }));
106
117
  return;
107
118
  }
108
119
  store.set(requestId, { ...state, ...patch });
@@ -115,7 +126,10 @@ function storeTerminalProgressState(
115
126
  const state = store.get(requestId);
116
127
  if (!state) return;
117
128
  const durationMs = state.durationMs ?? Date.now() - state.startTime;
118
- store.set(requestId, { ...state, ...patch, durationMs });
129
+ store.set(requestId, {
130
+ ...stripTransientFields({ ...state, ...patch }),
131
+ durationMs,
132
+ });
119
133
  }
120
134
 
121
135
  export function finalizeProgressState(
@@ -125,9 +139,6 @@ export function finalizeProgressState(
125
139
  storeTerminalProgressState(requestId, {
126
140
  status: "success",
127
141
  finalOutput: makeProgressFinalOutput(finalOutput),
128
- activeToolActivity: undefined,
129
- lastToolPreview: undefined,
130
- toolResultCompleted: undefined,
131
142
  });
132
143
  }
133
144
 
@@ -136,21 +147,13 @@ export function failProgressState(requestId: string, errorText: string): void {
136
147
  storeTerminalProgressState(requestId, {
137
148
  status: "error",
138
149
  errorText: sentence,
139
- activeToolActivity: undefined,
140
- lastToolPreview: undefined,
141
- toolResultCompleted: undefined,
142
150
  });
143
151
  }
144
152
 
145
153
  export function cancelProgressState(requestId: string, reason?: string): void {
146
154
  storeTerminalProgressState(requestId, {
147
155
  status: "cancelled",
148
- activeToolActivity: undefined,
149
- lastToolPreview: undefined,
150
- toolResultCompleted: undefined,
151
- ...(reason !== undefined
152
- ? { errorText: normalizeTerminalSentence(reason) }
153
- : {}),
156
+ errorText: reason ? normalizeTerminalSentence(reason) : undefined,
154
157
  });
155
158
  }
156
159
 
@@ -235,11 +238,11 @@ function trackNewToolCall(
235
238
 
236
239
  function extractProgressFromExistingProgress(
237
240
  progress: {
238
- activityText?: string;
239
- activeToolActivity?: ToolActivity;
240
- lastToolPreview?: string;
241
+ activityText?: string | undefined;
242
+ activeToolActivity?: ToolActivity | undefined;
243
+ lastToolPreview?: string | undefined;
241
244
  toolCalls: { id: string; preview: string }[];
242
- toolResultCompleted?: boolean;
245
+ toolResultCompleted?: boolean | undefined;
243
246
  },
244
247
  seenToolCallIds: Set<string>,
245
248
  state: DetailsProgress,
@@ -317,7 +320,7 @@ function isDerivedToolCall(part: unknown): part is {
317
320
  preview: string;
318
321
  } {
319
322
  if (!isObjectWith(part)) return false;
320
- return typeof part.id === "string" && typeof part.preview === "string";
323
+ return typeof part["id"] === "string" && typeof part["preview"] === "string";
321
324
  }
322
325
 
323
326
  export function isToolCallPart(part: unknown): part is {
@@ -328,9 +331,9 @@ export function isToolCallPart(part: unknown): part is {
328
331
  } {
329
332
  if (!isObjectWith(part)) return false;
330
333
  return (
331
- part.type === "toolCall" &&
332
- typeof part.id === "string" &&
333
- typeof part.name === "string"
334
+ part["type"] === "toolCall" &&
335
+ typeof part["id"] === "string" &&
336
+ typeof part["name"] === "string"
334
337
  );
335
338
  }
336
339
 
@@ -89,11 +89,18 @@ export function renderSubagentProgress(
89
89
  }
90
90
 
91
91
  class DynamicSubagentProgressText implements Component {
92
+ private readonly requestId: string;
93
+ private readonly options: { expanded: boolean };
94
+ private readonly theme: SubagentTheme;
92
95
  constructor(
93
- private readonly requestId: string,
94
- private readonly options: { expanded: boolean },
95
- private readonly theme: SubagentTheme,
96
- ) {}
96
+ requestId: string,
97
+ options: { expanded: boolean },
98
+ theme: SubagentTheme,
99
+ ) {
100
+ this.requestId = requestId;
101
+ this.options = options;
102
+ this.theme = theme;
103
+ }
97
104
  invalidate(): void {}
98
105
  render(width: number): string[] {
99
106
  const state = getProgressState(this.requestId);
@@ -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(),
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
  }