pi-subagentura 1.0.4 → 1.0.6
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 +59 -4
- package/helpers.ts +43 -7
- package/package.json +1 -1
- package/subagent.ts +36 -11
package/README.md
CHANGED
|
@@ -4,19 +4,24 @@
|
|
|
4
4
|
|
|
5
5
|
> **Note:** The `docs/` folder is managed by the [`pi-docs`](https://github.com/lmn451/pi-docs) package.
|
|
6
6
|
|
|
7
|
-
A public [Pi](https://pi.dev) package that adds
|
|
7
|
+
A public [Pi](https://pi.dev) package that adds in-process sub-agent tools:
|
|
8
8
|
|
|
9
9
|
- `subagent_with_context` — spawn a sub-agent that inherits the full conversation history
|
|
10
10
|
- `subagent_isolated` — spawn a sub-agent with a fresh, empty context window
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
- `get_subagent_status` — poll an async subagent job for live progress
|
|
12
|
+
- `get_subagent_result` — block until an async job completes and return the final output
|
|
13
|
+
- `cancel_subagent` — abort a running async job
|
|
14
|
+
- `prune_subagent_jobs` — remove all completed and failed jobs from the registry
|
|
15
|
+
The sub-agents run inside the current Pi process, stream live progress back to the UI, and inherit the active model by default. Async sub-agents run in the background — the main agent continues immediately while you poll for progress and collect results when ready.
|
|
13
16
|
|
|
14
17
|
## Why use it?
|
|
15
18
|
|
|
16
19
|
- Delegate focused side-tasks without leaving the current session
|
|
17
20
|
- Compare context-aware vs isolated reasoning
|
|
18
21
|
- Keep tool feedback lightweight with live status updates
|
|
19
|
-
-
|
|
22
|
+
- Run sub-agents in the background while continuing the main conversation
|
|
23
|
+
- Poll, collect, or cancel background jobs on demand
|
|
24
|
+
- Get live previews of running sub-agents (current turn, active tool, usage)
|
|
20
25
|
|
|
21
26
|

|
|
22
27
|
|
|
@@ -58,12 +63,16 @@ Parameters:
|
|
|
58
63
|
- `persona` — optional system-style persona
|
|
59
64
|
- `model` — optional model override like `anthropic/claude-sonnet-4-5`
|
|
60
65
|
- `cwd` — optional working directory override
|
|
66
|
+
- `async` — run in background; returns a jobId immediately instead of blocking
|
|
67
|
+
- `notifyOnComplete` — `"notify"` or `"inject"`; auto-deliver completion notification (async only)
|
|
68
|
+
- `maxAge` — optional TTL in ms for completed job retention (async only)
|
|
61
69
|
|
|
62
70
|
Best for:
|
|
63
71
|
|
|
64
72
|
- review tasks that depend on prior discussion
|
|
65
73
|
- continuing a line of reasoning in parallel
|
|
66
74
|
- focused implementation or research using the current context
|
|
75
|
+
- background side-quests that report results later
|
|
67
76
|
|
|
68
77
|
### `subagent_isolated`
|
|
69
78
|
|
|
@@ -75,18 +84,64 @@ Parameters:
|
|
|
75
84
|
- `persona` — optional system-style persona
|
|
76
85
|
- `model` — optional model override like `anthropic/claude-sonnet-4-5`
|
|
77
86
|
- `cwd` — optional working directory override
|
|
87
|
+
- `async` — run in background; returns a jobId immediately instead of blocking
|
|
88
|
+
- `notifyOnComplete` — `"notify"` or `"inject"`; auto-deliver completion notification (async only)
|
|
89
|
+
- `maxAge` — optional TTL in ms for completed job retention (async only)
|
|
78
90
|
|
|
79
91
|
Best for:
|
|
80
92
|
|
|
81
93
|
- second opinions
|
|
82
94
|
- clean-room summaries
|
|
83
95
|
- avoiding context contamination from the parent session
|
|
96
|
+
- background analysis without polluting the main conversation
|
|
97
|
+
|
|
98
|
+
### Async Workflow Tools
|
|
99
|
+
|
|
100
|
+
When you spawn a sub-agent with `async: true`, it returns a **jobId** immediately and runs in the background. Use these tools to manage async jobs:
|
|
101
|
+
|
|
102
|
+
#### `get_subagent_status`
|
|
103
|
+
|
|
104
|
+
Poll an async subagent job by jobId. Returns a live preview of the subagent's current turn, active tool, and partial output.
|
|
105
|
+
|
|
106
|
+
Parameters:
|
|
107
|
+
|
|
108
|
+
- `jobId` — required job ID returned by the async spawn
|
|
109
|
+
|
|
110
|
+
#### `get_subagent_result`
|
|
111
|
+
|
|
112
|
+
Block until an async subagent job completes, then return the final output and usage summary. If the job is already done, it returns immediately.
|
|
113
|
+
|
|
114
|
+
Parameters:
|
|
115
|
+
|
|
116
|
+
- `jobId` — required job ID returned by the async spawn
|
|
117
|
+
|
|
118
|
+
#### `cancel_subagent`
|
|
119
|
+
|
|
120
|
+
Abort a running async subagent job by jobId.
|
|
121
|
+
|
|
122
|
+
Parameters:
|
|
123
|
+
|
|
124
|
+
- `jobId` — required job ID returned by the async spawn
|
|
125
|
+
|
|
126
|
+
#### `prune_subagent_jobs`
|
|
127
|
+
|
|
128
|
+
Remove all completed and failed subagent jobs from the registry. Running and cancelled jobs are preserved.
|
|
129
|
+
|
|
130
|
+
### `list_available_models`
|
|
131
|
+
|
|
132
|
+
List all available AI models with auth status. Use this to validate model identifiers before passing them to subagent tools — prevents silent fallback to the parent session model.
|
|
133
|
+
|
|
134
|
+
Parameters:
|
|
84
135
|
|
|
136
|
+
- `filter` — optional substring filter for provider or model name
|
|
137
|
+
- `authOnly` — if true (default), only return models with configured auth
|
|
85
138
|
## Example prompts
|
|
86
139
|
|
|
87
140
|
- “Use a sub-agent to review this change and list risks.”
|
|
88
141
|
- “Use an isolated sub-agent to propose a README outline for this repo.”
|
|
89
142
|
- “Spawn a context-aware sub-agent to continue debugging while we keep planning here.”
|
|
143
|
+
- “Run a sub-agent in the background to run the test suite, then notify me when done.”
|
|
144
|
+
- “Spawn two isolated async sub-agents to review this code from different angles, then collect both results.”
|
|
90
145
|
|
|
91
146
|
## Development
|
|
92
147
|
|
package/helpers.ts
CHANGED
|
@@ -177,18 +177,35 @@ export function generateJobId(): string {
|
|
|
177
177
|
* The caller (LLM agent) is responsible for providing the correct model id.
|
|
178
178
|
* This function does NOT guess — it only does exact lookups:
|
|
179
179
|
* 1. undefined → defaultModel
|
|
180
|
-
* 2.
|
|
181
|
-
* 3.
|
|
182
|
-
* 4.
|
|
180
|
+
* 2. Use parent modelRegistry (has extension-added models like minimax)
|
|
181
|
+
* 3. "provider/id" format → exact getModel lookup (global static registry)
|
|
182
|
+
* 4. Bare id → exact getModel scan across all providers (global static registry)
|
|
183
|
+
* 5. Falls back to defaultModel when nothing matches
|
|
183
184
|
*/
|
|
184
185
|
export function resolveModel(
|
|
185
186
|
modelId: string | undefined,
|
|
186
|
-
// @ts-expect-error — Model<TApi> requires type arg; unknown is a safe placeholder
|
|
187
|
+
// @ts-expect-error — Model<TApi> requires type arg; unknown is a safe placeholder
|
|
187
188
|
defaultModel: Model | undefined,
|
|
189
|
+
parentModelRegistry?: ModelRegistry,
|
|
188
190
|
) {
|
|
189
191
|
if (!modelId) return defaultModel;
|
|
190
192
|
|
|
191
|
-
//
|
|
193
|
+
// Only exact matching — no fuzzy/substring guessing.
|
|
194
|
+
// The AI should call list_available_models and pick from the list.
|
|
195
|
+
if (parentModelRegistry) {
|
|
196
|
+
if (modelId.includes("/")) {
|
|
197
|
+
const [provider, id] = modelId.split("/", 2);
|
|
198
|
+
const exact = parentModelRegistry.find(provider, id);
|
|
199
|
+
if (exact) return exact as any;
|
|
200
|
+
} else {
|
|
201
|
+
// Bare id — search all models in parent registry
|
|
202
|
+
for (const m of parentModelRegistry.getAll()) {
|
|
203
|
+
if (m.id === modelId) return m as any;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Fall back to global static registry (built-in models only)
|
|
192
209
|
if (modelId.includes("/")) {
|
|
193
210
|
const [provider, id] = modelId.split("/", 2);
|
|
194
211
|
// @ts-expect-error — getModel requires KnownProvider union; we trust the caller
|
|
@@ -255,6 +272,8 @@ export interface StartSubagentJobParams {
|
|
|
255
272
|
// @ts-expect-error — Model<TApi> requires type arg
|
|
256
273
|
defaultModel: Model | undefined;
|
|
257
274
|
maxAge?: number;
|
|
275
|
+
/** Parent session's model registry for resolving extension-added models (e.g. minimax) */
|
|
276
|
+
parentModelRegistry?: ModelRegistry;
|
|
258
277
|
}
|
|
259
278
|
|
|
260
279
|
export interface StartSubagentJobResult {
|
|
@@ -263,6 +282,8 @@ export interface StartSubagentJobResult {
|
|
|
263
282
|
session: AgentSession;
|
|
264
283
|
liveStatus: SubagentLiveStatus;
|
|
265
284
|
modelLabel?: string;
|
|
285
|
+
/** Warning when modelOverride was specified but not found — lists available models */
|
|
286
|
+
modelWarning?: string;
|
|
266
287
|
}
|
|
267
288
|
|
|
268
289
|
/**
|
|
@@ -286,6 +307,7 @@ export async function startSubagentJob(
|
|
|
286
307
|
signal,
|
|
287
308
|
onUpdate,
|
|
288
309
|
defaultModel,
|
|
310
|
+
parentModelRegistry,
|
|
289
311
|
} = params;
|
|
290
312
|
|
|
291
313
|
// Enforce registry size cap before adding a new job
|
|
@@ -298,11 +320,25 @@ export async function startSubagentJob(
|
|
|
298
320
|
const modelRegistry = ModelRegistry.create(authStorage);
|
|
299
321
|
|
|
300
322
|
// Resolve model: exact match only, fallback to default
|
|
301
|
-
|
|
323
|
+
// Uses parent's modelRegistry to find extension-added models (e.g. minimax)
|
|
324
|
+
const targetModel = resolveModel(modelOverride, defaultModel, parentModelRegistry);
|
|
302
325
|
const modelLabel = targetModel
|
|
303
326
|
? `${targetModel.provider}/${targetModel.id}`
|
|
304
327
|
: undefined;
|
|
305
328
|
|
|
329
|
+
// Build model warning when override was specified (helps AI discover valid models)
|
|
330
|
+
let modelWarning: string | undefined;
|
|
331
|
+
if (modelOverride && parentModelRegistry) {
|
|
332
|
+
const available = parentModelRegistry.getAvailable();
|
|
333
|
+
const modelList = available
|
|
334
|
+
.map((m) => ` ${m.provider}/${m.id}${m.name ? ` (${m.name})` : ""}`)
|
|
335
|
+
.join("\n");
|
|
336
|
+
modelWarning =
|
|
337
|
+
`Requested model "${modelOverride}" resolved to ${modelLabel ?? "none"}. ` +
|
|
338
|
+
`Available models:\n${modelList || " (none)"}\n` +
|
|
339
|
+
`Use list_available_models to discover more.`;
|
|
340
|
+
}
|
|
341
|
+
|
|
306
342
|
let handleAbort: (() => void) | undefined;
|
|
307
343
|
let unsubscribe: (() => void) | undefined;
|
|
308
344
|
|
|
@@ -505,5 +541,5 @@ export async function startSubagentJob(
|
|
|
505
541
|
return result;
|
|
506
542
|
})();
|
|
507
543
|
|
|
508
|
-
return { jobId, jobPromise, session, liveStatus, modelLabel };
|
|
544
|
+
return { jobId, jobPromise, session, liveStatus, modelLabel, modelWarning };
|
|
509
545
|
}
|
package/package.json
CHANGED
package/subagent.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
type Theme,
|
|
24
24
|
convertToLlm,
|
|
25
25
|
serializeConversation,
|
|
26
|
+
ModelRegistry,
|
|
26
27
|
} from "@mariozechner/pi-coding-agent";
|
|
27
28
|
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
|
28
29
|
import type { Model } from "@mariozechner/pi-ai";
|
|
@@ -64,9 +65,10 @@ async function runSubagent(
|
|
|
64
65
|
onUpdate: ((partial: AgentToolResult) => void) | undefined,
|
|
65
66
|
// @ts-expect-error — Model<TApi> requires type arg; unknown is a safe placeholder
|
|
66
67
|
defaultModel: Model | undefined,
|
|
68
|
+
parentModelRegistry: ModelRegistry | undefined,
|
|
67
69
|
): Promise<SubagentResult> {
|
|
68
70
|
try {
|
|
69
|
-
const { jobPromise } = await startSubagentJob({
|
|
71
|
+
const { jobPromise, modelWarning } = await startSubagentJob({
|
|
70
72
|
task,
|
|
71
73
|
persona,
|
|
72
74
|
modelOverride,
|
|
@@ -75,8 +77,14 @@ async function runSubagent(
|
|
|
75
77
|
signal,
|
|
76
78
|
onUpdate,
|
|
77
79
|
defaultModel,
|
|
80
|
+
parentModelRegistry,
|
|
78
81
|
});
|
|
79
|
-
|
|
82
|
+
const result = await jobPromise;
|
|
83
|
+
// Surface model resolution info so the AI sees what model was used
|
|
84
|
+
if (modelWarning && !result.isError) {
|
|
85
|
+
result.output = `${modelWarning}\n---\n${result.output}`;
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
80
88
|
} catch (err) {
|
|
81
89
|
// Preserve original error formatting: if startSubagentJob throws
|
|
82
90
|
// (e.g., createAgentSession auth failure), return clean SubagentResult
|
|
@@ -475,8 +483,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
475
483
|
label: "Sub-Agent (with context)",
|
|
476
484
|
description: [
|
|
477
485
|
"Spawn an in-process sub-agent that inherits the full conversation history.",
|
|
486
|
+
"WARNING: Each call serializes the entire conversation into memory. Spawning many",
|
|
487
|
+
"subagents with context in parallel can cause heap exhaustion (OOM).",
|
|
488
|
+
"",
|
|
489
|
+
"MEMORY-SAVING ALTERNATIVES:",
|
|
490
|
+
"1. Use subagent_isolated for tasks that don't need full history",
|
|
491
|
+
"2. Run few parallel subagents (1-3 at a time) instead of batching many",
|
|
492
|
+
"3. Consider summarizing the context before passing to subagent",
|
|
493
|
+
"",
|
|
478
494
|
"The sub-agent sees everything discussed so far plus the new task.",
|
|
479
|
-
"Model is inherited by default.
|
|
495
|
+
"Model is inherited by default. Use the model param to override (e.g. 'minimax/MiniMax-M2.7').",
|
|
496
|
+
"Use list_available_models to see which models have configured auth before setting model.",
|
|
497
|
+
"Streams output in real-time when sync.",
|
|
480
498
|
"",
|
|
481
499
|
"Examples:",
|
|
482
500
|
' - task: "Review this PR for security issues", persona: "You are a senior security auditor"',
|
|
@@ -513,7 +531,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
513
531
|
const conversationText = serializeConversation(llmMessages);
|
|
514
532
|
const targetCwd = params.cwd ?? ctx.cwd;
|
|
515
533
|
|
|
516
|
-
const { jobId, jobPromise, session, liveStatus, modelLabel } =
|
|
534
|
+
const { jobId, jobPromise, session, liveStatus, modelLabel, modelWarning } =
|
|
517
535
|
await startSubagentJob({
|
|
518
536
|
task: params.task,
|
|
519
537
|
persona: params.persona,
|
|
@@ -524,6 +542,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
524
542
|
onUpdate: undefined,
|
|
525
543
|
defaultModel: ctx.model,
|
|
526
544
|
maxAge: params.maxAge,
|
|
545
|
+
parentModelRegistry: ctx.modelRegistry,
|
|
527
546
|
});
|
|
528
547
|
const jobState: JobState = {
|
|
529
548
|
id: jobId,
|
|
@@ -620,7 +639,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
620
639
|
content: [
|
|
621
640
|
{
|
|
622
641
|
type: "text",
|
|
623
|
-
text: `Job ${jobId} started. The main agent continues — use get_subagent_status to check progress and get_subagent_result to collect output when ready
|
|
642
|
+
text: `Job ${jobId} started. The main agent continues — use get_subagent_status to check progress and get_subagent_result to collect output when ready.` +
|
|
643
|
+
(modelWarning ? `\n\n${modelWarning}` : ""),
|
|
624
644
|
},
|
|
625
645
|
],
|
|
626
646
|
details: {
|
|
@@ -654,6 +674,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
654
674
|
signal,
|
|
655
675
|
onUpdate,
|
|
656
676
|
ctx.model,
|
|
677
|
+
ctx.modelRegistry,
|
|
657
678
|
);
|
|
658
679
|
|
|
659
680
|
const usageStr = formatUsage(result.usage, result.model);
|
|
@@ -694,7 +715,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
694
715
|
description: [
|
|
695
716
|
"Spawn an in-process sub-agent with a fresh, empty context window.",
|
|
696
717
|
"Only receives the task and optional persona. No conversation history.",
|
|
697
|
-
"Model is inherited by default.
|
|
718
|
+
"Model is inherited by default. Use the model param to override (e.g. 'minimax/MiniMax-M2.7').",
|
|
719
|
+
"Use list_available_models to see which models have configured auth before setting model.",
|
|
720
|
+
"Streams output in real-time when sync.",
|
|
698
721
|
"",
|
|
699
722
|
"Examples:",
|
|
700
723
|
' - task: "Propose a README outline for this repo", persona: "You are a technical writer"',
|
|
@@ -711,7 +734,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
711
734
|
if (params.async === true) {
|
|
712
735
|
const targetCwd = params.cwd ?? ctx.cwd;
|
|
713
736
|
|
|
714
|
-
const { jobId, jobPromise, session, liveStatus, modelLabel } =
|
|
737
|
+
const { jobId, jobPromise, session, liveStatus, modelLabel, modelWarning } =
|
|
715
738
|
await startSubagentJob({
|
|
716
739
|
task: params.task,
|
|
717
740
|
persona: params.persona,
|
|
@@ -722,6 +745,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
722
745
|
onUpdate: undefined,
|
|
723
746
|
defaultModel: ctx.model,
|
|
724
747
|
maxAge: params.maxAge,
|
|
748
|
+
parentModelRegistry: ctx.modelRegistry,
|
|
725
749
|
});
|
|
726
750
|
const jobState: JobState = {
|
|
727
751
|
id: jobId,
|
|
@@ -821,7 +845,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
821
845
|
content: [
|
|
822
846
|
{
|
|
823
847
|
type: "text",
|
|
824
|
-
text: `Job ${jobId} started. The main agent continues — use get_subagent_status to check progress and get_subagent_result to collect output when ready
|
|
848
|
+
text: `Job ${jobId} started. The main agent continues — use get_subagent_status to check progress and get_subagent_result to collect output when ready.` +
|
|
849
|
+
(modelWarning ? `\n\n${modelWarning}` : ""),
|
|
825
850
|
},
|
|
826
851
|
],
|
|
827
852
|
details: { jobId, status: "started" },
|
|
@@ -840,6 +865,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
840
865
|
signal,
|
|
841
866
|
onUpdate,
|
|
842
867
|
ctx.model,
|
|
868
|
+
ctx.modelRegistry,
|
|
843
869
|
);
|
|
844
870
|
|
|
845
871
|
const usageStr = formatUsage(result.usage, result.model);
|
|
@@ -876,8 +902,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
876
902
|
pi.registerTool({
|
|
877
903
|
name: "get_subagent_status",
|
|
878
904
|
label: "Get Subagent Status",
|
|
879
|
-
description:
|
|
880
|
-
"Poll an async subagent job by jobId. Returns live preview of the subagent's current turn, active tool, and output.",
|
|
905
|
+
description: "Poll an async subagent job by jobId. Returns live preview of the subagent's current turn, active tool, and output.",
|
|
881
906
|
parameters: StatusParams,
|
|
882
907
|
|
|
883
908
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
@@ -891,7 +916,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
891
916
|
text: `Job ${params.jobId} not found. It may have been cancelled.`,
|
|
892
917
|
},
|
|
893
918
|
],
|
|
894
|
-
|
|
919
|
+
details: { jobId: params.jobId, status: "not_found" },
|
|
895
920
|
isError: true,
|
|
896
921
|
};
|
|
897
922
|
}
|