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 +30 -9
- package/extensions/index.ts +217 -37
- package/package.json +1 -1
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
|
|
65
|
-
│ └── result.md
|
|
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
|
-
│
|
|
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
|
-
### 🎛️
|
|
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
|
-
### 🚩
|
|
107
|
+
### 🚩 CLI Flags
|
|
91
108
|
|
|
92
|
-
|
|
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
|
package/extensions/index.ts
CHANGED
|
@@ -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
|
-
"
|
|
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:
|
|
563
|
-
|
|
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 (
|
|
659
|
+
if (timeoutSecs > 0) {
|
|
606
660
|
timeoutTimer = setTimeout(() => {
|
|
607
661
|
timeoutController.abort();
|
|
608
|
-
},
|
|
662
|
+
}, timeoutSecs * 1000);
|
|
609
663
|
}
|
|
610
664
|
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
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
|
-
|
|
779
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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