pi-subagent-in-memory 0.2.1 → 0.2.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.
package/README.md CHANGED
@@ -23,11 +23,25 @@ Spawns an in-process subagent session using pi's `createAgentSession` SDK. The s
23
23
  | `provider` | string | | LLM provider (e.g. `anthropic`, `google`, `openai`) |
24
24
  | `model` | string | | Model ID. Supports `provider/model` format (e.g. `openai/gpt-4o-mini`) |
25
25
  | `cwd` | string | | Working directory for the subagent |
26
- | `timeout` | number | | Timeout in seconds. Aborts the subagent if exceeded |
26
+ | `timeout` | number | | Timeout in seconds. Aborts the subagent if exceeded. Defaults to the configured default timeout (300s, see `/saim-timeout`) |
27
27
  | `columnWidthPercent` | number | | Card width as % of terminal (33–100). Controls card grid layout |
28
28
 
29
29
  If `provider` and `model` are omitted, the subagent inherits the main agent's model.
30
30
 
31
+ ### ⏱️ Timeouts That Don't Destroy Child Work
32
+
33
+ Every subagent has a timeout (default **300s**, configurable via `--saim-timeout` / `/saim-timeout`, `0` = unlimited). When a parent agent is aborted or times out while a child subagent is still running, the child is **not killed**. Instead it detaches:
34
+
35
+ - The parent's tool call returns immediately with a note that the child continues in the background
36
+ - The child keeps running, still bounded by its **own** timeout
37
+ - When it finishes, its output is written to `result.md` (or `error.md` on failure) as usual, so the work can be harvested later
38
+
39
+ On any failure (timeout, error), whatever text the subagent had already produced is salvaged to `partial-result.md` alongside `error.md` — nothing is silently discarded.
40
+
41
+ ### 🛑 Nesting Depth Limit
42
+
43
+ Subagents may spawn their own subagents, but only up to a configured max depth (default **2**: main agent → subagent → sub-subagent). Subagents at the max depth don't receive the `subagent_create` tool at all, so a runaway fork cascade is structurally impossible. Configure via `--saim-max-depth` / `/saim-max-depth` (1–10).
44
+
31
45
  ### 📊 Live TUI Card Widgets
32
46
 
33
47
  Each running subagent is displayed as a colored card widget above the editor:
@@ -61,11 +75,12 @@ Every subagent session is logged to disk for debugging and auditing:
61
75
  ```
62
76
  .pi/subagent-in-memory/<mainSessionId>/
63
77
  ├── subagent_1/
64
- │ ├── events.jsonl # Full event stream (text, tool calls, results)
65
- │ └── result.md # Final subagent output (or error.md on failure)
78
+ │ ├── events.jsonl # Full event stream (text, tool calls, results)
79
+ │ └── result.md # Final subagent output
66
80
  ├── subagent_2/
67
81
  │ ├── events.jsonl
68
- └── result.md
82
+ ├── error.md # On failure: what went wrong
83
+ │ └── partial-result.md # On failure: any text produced before the failure
69
84
  └── ...
70
85
  ```
71
86
 
@@ -77,19 +92,25 @@ The JSONL log includes:
77
92
 
78
93
  ### 🔄 Nested Subagent Support
79
94
 
80
- Subagents can spawn their own subagents. All nested cards render in the main agent's widget — they share the same module-level state regardless of nesting depth. This is achieved by passing the `subagent_create` tool directly as an `AgentTool` to child sessions.
95
+ Subagents can spawn their own subagents, up to the configured max depth (see above). All nested cards render in the main agent's widget — they share the same module-level state regardless of nesting depth. This is achieved by passing the `subagent_create` tool directly as an `AgentTool` to child sessions.
81
96
 
82
- ### 🎛️ TUI Overlay Slash Commands
97
+ ### 🎛️ Slash Commands
83
98
 
84
99
  | Command | Description |
85
100
  |---------|-------------|
101
+ | `/saim-max-depth [n]` | Show or set the max subagent nesting depth (1–10, default 2). Applies to newly created subagents. |
102
+ | `/saim-timeout [seconds]` | Show or set the default subagent timeout in seconds (default 300, `0` = unlimited). A per-call `timeout` parameter overrides it. |
86
103
  | `/saim-toggle-overlay [on\|off\|toggle]` | Enable, disable, or toggle the subagent TUI overlay. When disabled, **no card widget is mounted even while subagents are actively running** — they continue executing silently in the background. |
87
104
  | `/saim-set-max-tui-overlays <N>` | Set the maximum number of cards displayed at once (1–9, default 3). Older cards remain accessible via **Ctrl+Alt+←/→**. |
88
105
  | `/saim-clear-tui-overlay` | Clear all subagent cards from the TUI and close any open detail overlay. |
89
106
 
90
- ### 🚩 `--saim-no-tui` CLI Flag
107
+ ### 🚩 CLI Flags
91
108
 
92
- Start `pi` with `--saim-no-tui` to launch with the subagent overlay disabled (equivalent to running `/saim-toggle-overlay off` immediately on startup). Subagents still run normally — only the TUI cards are hidden.
109
+ | Flag | Description |
110
+ |------|-------------|
111
+ | `--saim-max-depth <n>` | Max subagent nesting depth (1–10, default 2). Same as `/saim-max-depth`. |
112
+ | `--saim-timeout <seconds>` | Default subagent timeout in seconds (default 300, `0` = unlimited). Same as `/saim-timeout`. |
113
+ | `--saim-no-tui` | Start with the subagent overlay disabled (equivalent to running `/saim-toggle-overlay off` immediately on startup). Subagents still run normally — only the TUI cards are hidden. |
93
114
 
94
115
  ## Install
95
116
 
@@ -108,7 +129,7 @@ pi remove npm:pi-subagent-in-memory
108
129
  After installing, start pi and check:
109
130
 
110
131
  1. The `subagent_create` tool should appear in the tool list
111
- 2. The `/saim-toggle-overlay`, `/saim-set-max-tui-overlays`, and `/saim-clear-tui-overlay` commands should be available (type `/` to see commands)
132
+ 2. The `/saim-max-depth`, `/saim-timeout`, `/saim-toggle-overlay`, `/saim-set-max-tui-overlays`, and `/saim-clear-tui-overlay` commands should be available (type `/` to see commands)
112
133
  3. Ask the agent to "run a subagent to list files" — you should see a card widget appear
113
134
 
114
135
  ## Usage Examples
@@ -17,6 +17,13 @@
17
17
  * /saim-toggle-overlay [on|off] — enable/disable rendering
18
18
  * /saim-set-max-tui-overlays <N> — limit visible cards (1-9)
19
19
  * /saim-clear-tui-overlay — clear all cards & close any overlay
20
+ * - Runtime limits (each also available as a CLI flag):
21
+ * /saim-max-depth [n] — max subagent nesting depth (default 2)
22
+ * /saim-timeout [seconds] — default subagent timeout (default 300s,
23
+ * 0 = unlimited)
24
+ * - Parent aborts/timeouts do NOT kill running children: the child detaches,
25
+ * keeps running under its own timeout, and still writes result.md /
26
+ * error.md (plus partial-result.md salvaging any text produced so far).
20
27
  * - Multi-provider support (Anthropic, OpenAI, Google, etc.)
21
28
  * - Ctrl+<N> to inspect subagent prompt & live messages
22
29
  * - Ctrl+Alt+Left / Ctrl+Alt+Right to page through cards when there are
@@ -143,6 +150,33 @@ let widgetRenderVersion = 0;
143
150
  let activeDetailTui: any = null;
144
151
  let activeDetailDone: ((result: void) => void) | null = null;
145
152
 
153
+ // ── Runtime limits (commands & flags) ───────────────────────────
154
+ const DEFAULT_MAX_DEPTH = 2;
155
+ const MAX_DEPTH_HARD_LIMIT = 10;
156
+ const DEFAULT_TIMEOUT_SECS = 300;
157
+ // Max nesting depth: 1 = only the main agent may spawn subagents,
158
+ // 2 = subagents may spawn one further level, etc. Prevents runaway forks.
159
+ let maxSubagentDepth = DEFAULT_MAX_DEPTH;
160
+ // Default per-subagent timeout in seconds, used when a subagent_create call
161
+ // doesn't pass `timeout` explicitly. 0 = unlimited.
162
+ let defaultTimeoutSecs = DEFAULT_TIMEOUT_SECS;
163
+
164
+ /** Thrown into the tool-call promise when the PARENT aborts, so the child can
165
+ * detach and keep running instead of being killed mid-flight. */
166
+ class ParentAbortedError extends Error {
167
+ constructor() {
168
+ super("Subagent was aborted by parent");
169
+ }
170
+ }
171
+
172
+ function parseIntSetting(raw: unknown): number | undefined {
173
+ if (typeof raw !== "string") return undefined;
174
+ const trimmed = raw.trim();
175
+ if (trimmed === "") return undefined;
176
+ const n = Number(trimmed);
177
+ return Number.isInteger(n) ? n : Number.NaN;
178
+ }
179
+
146
180
  // ── TUI overlay controls (commands & flag) ──────────────────────
147
181
  const DEFAULT_MAX_VISIBLE_OVERLAYS = 3;
148
182
  const MAX_OVERLAYS_HARD_LIMIT = 9;
@@ -477,8 +511,9 @@ const SubagentParams = Type.Object({
477
511
  timeout: Type.Optional(
478
512
  Type.Number({
479
513
  description:
480
- "Timeout in seconds for the subagent execution. If exceeded, the subagent is aborted. " +
481
- "Defaults to unlimited (no timeout).",
514
+ "Timeout in seconds for the subagent execution. If exceeded, the subagent is aborted " +
515
+ "(its own in-flight nested subagents keep running and still write their results). " +
516
+ "Defaults to the configured default timeout (300s unless changed via --saim-timeout or /saim-timeout).",
482
517
  minimum: 1,
483
518
  })
484
519
  ),
@@ -504,7 +539,18 @@ async function executeSubagent(
504
539
  fallbackProvider?: string,
505
540
  fallbackModel?: string,
506
541
  fallbackCwd?: string,
542
+ depth: number = 1,
507
543
  ): Promise<AgentToolResult<any>> {
544
+ if (depth > maxSubagentDepth) {
545
+ throw new Error(
546
+ `Subagent nesting depth limit reached (max depth ${maxSubagentDepth}, see /saim-max-depth). ` +
547
+ "Perform the task directly instead of delegating to another subagent.",
548
+ );
549
+ }
550
+ if (signal?.aborted) {
551
+ throw new Error("Subagent was aborted before it started");
552
+ }
553
+
508
554
  subagentCount++;
509
555
  const subagentNum = subagents.length + 1; // display number based on currently visible cards
510
556
  const outDir = join(".pi", "subagent-in-memory", mainSessionId, `subagent_${subagentCount}`);
@@ -550,6 +596,9 @@ async function executeSubagent(
550
596
  throw new Error(`Could not find model ${providerName}/${modelId}. ${details}`.trim());
551
597
  }
552
598
 
599
+ // Subagents at the max depth don't get the subagent_create tool at all, so
600
+ // the LLM can't even attempt a deeper fork.
601
+ const canNest = depth < maxSubagentDepth;
553
602
  const { session } = await createAgentSessionFromServices({
554
603
  services,
555
604
  sessionManager: SessionManager.inMemory(),
@@ -559,8 +608,10 @@ async function executeSubagent(
559
608
  // tools plus nested subagent support. Package extensions are still loaded
560
609
  // above so provider registrations are available, but their tools are not
561
610
  // activated unless explicitly listed here.
562
- tools: ["read", "bash", "edit", "write", "grep", "find", "ls", "subagent_create"],
563
- customTools: [createSubagentAgentTool(providerName, modelId, cwd)],
611
+ tools: canNest
612
+ ? ["read", "bash", "edit", "write", "grep", "find", "ls", "subagent_create"]
613
+ : ["read", "bash", "edit", "write", "grep", "find", "ls"],
614
+ customTools: canNest ? [createSubagentAgentTool(providerName, modelId, cwd, depth + 1)] : [],
564
615
  });
565
616
 
566
617
  // Set up JSONL event log
@@ -576,6 +627,7 @@ async function executeSubagent(
576
627
  model: modelId,
577
628
  task: params.task,
578
629
  title: params.title,
630
+ depth,
579
631
  });
580
632
  let lastEventId = session.sessionId;
581
633
 
@@ -599,26 +651,21 @@ async function executeSubagent(
599
651
  details: { sessionId: session.sessionId, status: "created" },
600
652
  });
601
653
 
602
- // Timeout handling
654
+ // Timeout handling — explicit params.timeout wins, otherwise the configured
655
+ // default applies. 0 means unlimited.
656
+ const timeoutSecs = params.timeout ?? defaultTimeoutSecs;
603
657
  let timeoutTimer: ReturnType<typeof setTimeout> | undefined;
604
658
  const timeoutController = new AbortController();
605
- if (params.timeout) {
659
+ if (timeoutSecs > 0) {
606
660
  timeoutTimer = setTimeout(() => {
607
661
  timeoutController.abort();
608
- }, params.timeout * 1000);
662
+ }, timeoutSecs * 1000);
609
663
  }
610
664
 
611
- const combinedAbort = () => {
612
- session.abort();
613
- card.status = "error";
614
- card.endedAt = Date.now();
615
- appendMessage(card, "[aborted]");
616
- requestSubagentRender();
617
- };
665
+ // Hoisted out of the run promise so error paths can salvage partial output.
666
+ let finalText = "";
618
667
 
619
- try {
620
- const result = await new Promise<string>((resolve, reject) => {
621
- let finalText = "";
668
+ const runPromise = new Promise<string>((resolve, reject) => {
622
669
  let textDeltaBuffer = "";
623
670
  let toolcallDeltaBuffer = "";
624
671
  let lastPartialUpdateAt = 0;
@@ -768,15 +815,17 @@ async function executeSubagent(
768
815
  }
769
816
  });
770
817
 
771
- if (signal) {
772
- signal.addEventListener("abort", () => {
773
- combinedAbort();
774
- reject(new Error("Subagent was aborted"));
775
- });
776
- }
777
818
  timeoutController.signal.addEventListener("abort", () => {
778
- combinedAbort();
779
- reject(new Error(`Subagent timed out after ${params.timeout}s`));
819
+ // Abort only this subagent's own session. Its in-flight nested
820
+ // subagents receive the abort as a PARENT abort and detach instead of
821
+ // dying (see ParentAbortedError handling below), so their work is
822
+ // still written to disk.
823
+ session.abort();
824
+ card.status = "error";
825
+ card.endedAt = Date.now();
826
+ appendMessage(card, `[timed out after ${timeoutSecs}s]`);
827
+ requestSubagentRender();
828
+ reject(new Error(`Subagent timed out after ${timeoutSecs}s`));
780
829
  });
781
830
 
782
831
  session.prompt(params.task).catch((err) => {
@@ -786,30 +835,85 @@ async function executeSubagent(
786
835
  requestSubagentRender();
787
836
  reject(err);
788
837
  });
789
- });
838
+ });
790
839
 
840
+ const finalizeSuccess = async (result: string): Promise<string> => {
791
841
  if (timeoutTimer) clearTimeout(timeoutTimer);
792
842
  session.dispose();
793
843
  await flushJsonl(jsonlPath);
794
-
795
844
  const resultPath = join(outDir, "result.md");
796
845
  writeFileSync(resultPath, result, "utf-8");
846
+ return resultPath;
847
+ };
797
848
 
798
- return {
799
- content: [{ type: "text", text: `Execution succeeded. Result is in \`${resultPath}\`` }],
800
- details: { sessionId: session.sessionId, status: "completed", outputDir: outDir },
801
- };
802
- } catch (err: any) {
849
+ const finalizeError = async (err: any): Promise<{ errorPath: string; partialPath?: string }> => {
803
850
  if (timeoutTimer) clearTimeout(timeoutTimer);
804
851
  session.dispose();
805
852
  await flushJsonl(jsonlPath);
806
-
807
853
  const errorMsg = err?.message ?? String(err);
808
854
  const errorPath = join(outDir, "error.md");
809
- writeFileSync(errorPath, `# Subagent Error\n\n${errorMsg}\n`, "utf-8");
855
+ let body = `# Subagent Error\n\n${errorMsg}\n`;
856
+ let partialPath: string | undefined;
857
+ if (finalText.trim()) {
858
+ partialPath = join(outDir, "partial-result.md");
859
+ writeFileSync(partialPath, finalText, "utf-8");
860
+ body += `\nPartial output produced before the failure was saved to \`partial-result.md\`.\n`;
861
+ }
862
+ writeFileSync(errorPath, body, "utf-8");
863
+ return { errorPath, partialPath };
864
+ };
810
865
 
866
+ // A parent abort races against the run instead of killing the session, so
867
+ // a timed-out/aborted parent can no longer destroy useful child work.
868
+ let parentAbortPromise: Promise<never> | undefined;
869
+ if (signal) {
870
+ parentAbortPromise = new Promise<never>((_, reject) => {
871
+ signal.addEventListener("abort", () => reject(new ParentAbortedError()), { once: true });
872
+ });
873
+ }
874
+
875
+ try {
876
+ const result = await (parentAbortPromise ? Promise.race([runPromise, parentAbortPromise]) : runPromise);
877
+ const resultPath = await finalizeSuccess(result);
811
878
  return {
812
- content: [{ type: "text", text: `Execution failed. Detail is in \`${errorPath}\`` }],
879
+ content: [{ type: "text", text: `Execution succeeded. Result is in \`${resultPath}\`` }],
880
+ details: { sessionId: session.sessionId, status: "completed", outputDir: outDir },
881
+ };
882
+ } catch (err: any) {
883
+ if (err instanceof ParentAbortedError) {
884
+ // The parent aborted (typically because it hit its own timeout) while
885
+ // this subagent was still working. Don't kill it — detach: let it run
886
+ // to completion, still bounded by its own timeout, and write result.md
887
+ // / error.md as usual so the work can be harvested later.
888
+ appendMessage(card, "[parent aborted — continuing detached]");
889
+ requestSubagentRender();
890
+ runPromise
891
+ .then((result) => finalizeSuccess(result))
892
+ .catch((e) => finalizeError(e))
893
+ .catch(() => {});
894
+ return {
895
+ content: [
896
+ {
897
+ type: "text",
898
+ text:
899
+ "Subagent was aborted by its parent but continues running detached (its own timeout still applies). " +
900
+ `Its result will be written to \`${outDir}\` when it finishes.`,
901
+ },
902
+ ],
903
+ details: { sessionId: session.sessionId, status: "detached", outputDir: outDir },
904
+ };
905
+ }
906
+
907
+ const { errorPath, partialPath } = await finalizeError(err);
908
+ return {
909
+ content: [
910
+ {
911
+ type: "text",
912
+ text:
913
+ `Execution failed. Detail is in \`${errorPath}\`` +
914
+ (partialPath ? ` (partial output in \`${partialPath}\`)` : ""),
915
+ },
916
+ ],
813
917
  details: { sessionId: session.sessionId, status: "error", outputDir: outDir },
814
918
  };
815
919
  }
@@ -820,13 +924,15 @@ function createSubagentAgentTool(
820
924
  parentProvider: string,
821
925
  parentModel: string,
822
926
  parentCwd: string,
927
+ childDepth: number,
823
928
  ): ToolDefinition<typeof SubagentParams> {
824
929
  return {
825
930
  name: "subagent_create",
826
931
  label: "Subagent",
827
932
  description:
828
933
  "Create a subagent to perform a task. The subagent runs in-process with its own session. " +
829
- "Progress is streamed back as execution updates. Returns the final result when the subagent finishes.",
934
+ "Progress is streamed back as execution updates. Returns the final result when the subagent finishes. " +
935
+ "Nesting is limited to a configured max depth — if the limit is reached, do the work directly instead.",
830
936
  parameters: SubagentParams,
831
937
  async execute(
832
938
  toolCallId: string,
@@ -843,6 +949,7 @@ function createSubagentAgentTool(
843
949
  parentProvider,
844
950
  parentModel,
845
951
  parentCwd,
952
+ childDepth,
846
953
  );
847
954
  },
848
955
  };
@@ -856,6 +963,16 @@ export default function (pi: ExtensionAPI) {
856
963
  default: false,
857
964
  });
858
965
 
966
+ pi.registerFlag("saim-max-depth", {
967
+ description: `Max subagent nesting depth, 1-${MAX_DEPTH_HARD_LIMIT} (default ${DEFAULT_MAX_DEPTH}). Prevents runaway subagent forks.`,
968
+ type: "string",
969
+ });
970
+
971
+ pi.registerFlag("saim-timeout", {
972
+ description: `Default subagent timeout in seconds, 0 = unlimited (default ${DEFAULT_TIMEOUT_SECS}). Per-call \`timeout\` parameter overrides it.`,
973
+ type: "string",
974
+ });
975
+
859
976
  pi.on("session_start", async (_event, ctx) => {
860
977
  currentCtx = ctx;
861
978
  mainSessionId = ctx.sessionManager.getSessionId?.() ?? `session-${Date.now()}`;
@@ -874,9 +991,70 @@ export default function (pi: ExtensionAPI) {
874
991
  overlayEnabled = false;
875
992
  }
876
993
 
994
+ // Apply --saim-max-depth / --saim-timeout flags if present
995
+ const depthFlag = parseIntSetting(pi.getFlag("saim-max-depth"));
996
+ if (depthFlag !== undefined) {
997
+ if (Number.isInteger(depthFlag) && depthFlag >= 1 && depthFlag <= MAX_DEPTH_HARD_LIMIT) {
998
+ maxSubagentDepth = depthFlag;
999
+ } else {
1000
+ ctx.ui.notify(
1001
+ `Invalid --saim-max-depth (expected 1-${MAX_DEPTH_HARD_LIMIT}); keeping ${maxSubagentDepth}`,
1002
+ "warning",
1003
+ );
1004
+ }
1005
+ }
1006
+ const timeoutFlag = parseIntSetting(pi.getFlag("saim-timeout"));
1007
+ if (timeoutFlag !== undefined) {
1008
+ if (Number.isInteger(timeoutFlag) && timeoutFlag >= 0) {
1009
+ defaultTimeoutSecs = timeoutFlag;
1010
+ } else {
1011
+ ctx.ui.notify(
1012
+ `Invalid --saim-timeout (expected seconds >= 0); keeping ${defaultTimeoutSecs}`,
1013
+ "warning",
1014
+ );
1015
+ }
1016
+ }
1017
+
877
1018
  requestSubagentRender();
878
1019
  });
879
1020
 
1021
+ pi.registerCommand("saim-max-depth", {
1022
+ description: `Show or set max subagent nesting depth (1-${MAX_DEPTH_HARD_LIMIT}, default ${DEFAULT_MAX_DEPTH}). Usage: /saim-max-depth [n]`,
1023
+ handler: async (args, ctx) => {
1024
+ const arg = (args ?? "").trim();
1025
+ if (arg === "") {
1026
+ ctx.ui.notify(`Max subagent nesting depth: ${maxSubagentDepth}`, "info");
1027
+ return;
1028
+ }
1029
+ const n = Number(arg);
1030
+ if (!Number.isInteger(n) || n < 1 || n > MAX_DEPTH_HARD_LIMIT) {
1031
+ ctx.ui.notify(`Provide an integer between 1 and ${MAX_DEPTH_HARD_LIMIT}.`, "warning");
1032
+ return;
1033
+ }
1034
+ maxSubagentDepth = n;
1035
+ ctx.ui.notify(`Max subagent nesting depth set to ${n} (applies to newly created subagents)`, "info");
1036
+ },
1037
+ });
1038
+
1039
+ pi.registerCommand("saim-timeout", {
1040
+ description: `Show or set the default subagent timeout in seconds (0 = unlimited, default ${DEFAULT_TIMEOUT_SECS}). Usage: /saim-timeout [seconds]`,
1041
+ handler: async (args, ctx) => {
1042
+ const arg = (args ?? "").trim();
1043
+ const fmt = (secs: number) => (secs === 0 ? "unlimited" : `${secs}s`);
1044
+ if (arg === "") {
1045
+ ctx.ui.notify(`Default subagent timeout: ${fmt(defaultTimeoutSecs)}`, "info");
1046
+ return;
1047
+ }
1048
+ const n = Number(arg);
1049
+ if (!Number.isInteger(n) || n < 0) {
1050
+ ctx.ui.notify("Provide a non-negative integer number of seconds (0 = unlimited).", "warning");
1051
+ return;
1052
+ }
1053
+ defaultTimeoutSecs = n;
1054
+ ctx.ui.notify(`Default subagent timeout set to ${fmt(n)} (applies to newly created subagents)`, "info");
1055
+ },
1056
+ });
1057
+
880
1058
  pi.registerCommand("saim-clear-tui-overlay", {
881
1059
  description: "Clear subagent TUI cards and close any open detail overlay",
882
1060
  handler: async (_args, ctx) => {
@@ -1012,7 +1190,8 @@ export default function (pi: ExtensionAPI) {
1012
1190
  label: "Subagent",
1013
1191
  description:
1014
1192
  "Create a subagent to perform a task. The subagent runs in-process with its own session. " +
1015
- "Progress is streamed back as execution updates. Returns the final result when the subagent finishes.",
1193
+ "Progress is streamed back as execution updates. Returns the final result when the subagent finishes. " +
1194
+ "Nesting is limited to a configured max depth — if the limit is reached, do the work directly instead.",
1016
1195
  parameters: SubagentParams,
1017
1196
 
1018
1197
  async execute(toolCallId, params, signal, onUpdate, ctx) {
@@ -1029,6 +1208,7 @@ export default function (pi: ExtensionAPI) {
1029
1208
  providerName,
1030
1209
  modelId,
1031
1210
  cwd,
1211
+ 1,
1032
1212
  );
1033
1213
  },
1034
1214
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagent-in-memory",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "In-process subagent tool for pi with live TUI card widgets, JSONL session logging, and zero system-prompt overhead.",
5
5
  "repository": {
6
6
  "type": "git",