@henryqw/pi-subagent 1.1.1 → 2.0.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 +38 -68
- package/extensions/subagent.ts +39 -135
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,18 +1,37 @@
|
|
|
1
|
-
#
|
|
1
|
+
# `@henryqw/pi-subagent`
|
|
2
2
|
|
|
3
|
-
Delegate one bounded task to one isolated Pi process. Main chooses role and model class per
|
|
4
|
-
|
|
5
|
-
Based on Pi's authoritative [`examples/extensions/subagent`](https://github.com/earendil-works/pi/tree/main/packages/coding-agent/examples/extensions/subagent): child processes use `pi --mode json -p --no-session`.
|
|
3
|
+
Delegate one bounded task to one isolated Pi process. Main chooses the role and model class per call.
|
|
6
4
|
|
|
7
5
|
## Install
|
|
8
6
|
|
|
9
7
|
```bash
|
|
8
|
+
pi install npm:@henryqw/pi-task-models
|
|
9
|
+
pi install npm:@henryqw/pi-multi-codex
|
|
10
10
|
pi install npm:@henryqw/pi-subagent
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## With
|
|
14
|
+
|
|
15
|
+
| Package | Why |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `@henryqw/pi-task-models` | Required. Shared `fast` / `balanced` / `frontier` routes. |
|
|
18
|
+
| `@henryqw/pi-multi-codex` | Required. Child uses Main's active Codex slot. |
|
|
19
|
+
|
|
20
|
+
## Use
|
|
21
|
+
|
|
22
|
+
| Surface | Type | Purpose |
|
|
23
|
+
| --- | --- | --- |
|
|
24
|
+
| `delegate_task` | tool | Start one isolated child for `role`, `task`, and optional `modelClass`. |
|
|
25
|
+
|
|
26
|
+
`modelClass` is `fast`, `balanced`, or `frontier`. Omitted class uses `balanced`. Primary route is resolved against current scoped text models; fallback is tried only before launch. If no route is usable, delegation rejects with `Run /task-models`. A started child is never retried.
|
|
27
|
+
|
|
28
|
+
Each call starts one isolated child (`pi --mode json -p --no-session`). Ambient extensions and skills are off; only role resources load. Child uses the delegated working directory and Main's project approval. Abort kills the child process group. Streaming output is capped at 50 KiB.
|
|
29
|
+
|
|
30
|
+
TUI shows one row per Subagent with role, route, task, tokens, and elapsed time. Terminal rows drop after one second.
|
|
14
31
|
|
|
15
|
-
|
|
32
|
+
## Config
|
|
33
|
+
|
|
34
|
+
Role files live in `~/.pi/agent/config/pi-subagent/*.md`. Model routes live in `~/.pi/agent/config/pi-task-models.json`.
|
|
16
35
|
|
|
17
36
|
```markdown
|
|
18
37
|
---
|
|
@@ -30,76 +49,27 @@ Review only requested change. Return ranked findings with file and line evidence
|
|
|
30
49
|
Do not edit files.
|
|
31
50
|
```
|
|
32
51
|
|
|
33
|
-
Fields:
|
|
34
|
-
|
|
35
52
|
| Field | Required | Meaning |
|
|
36
53
|
| --- | --- | --- |
|
|
37
54
|
| `name` | yes | Role selected by Main |
|
|
38
|
-
| `description` | yes | Tells Main when to use role |
|
|
39
|
-
| `tools` | no | Omit for Pi
|
|
40
|
-
| `extensions` | no | Absolute
|
|
41
|
-
| `skills` | no | Effective Pi
|
|
55
|
+
| `description` | yes | Tells Main when to use the role |
|
|
56
|
+
| `tools` | no | Omit for Pi `defaultTools`; a non-empty list is an exact allowlist; `[]` means none |
|
|
57
|
+
| `extensions` | no | Absolute/`~/` paths or package sources. Repository-relative paths are rejected. |
|
|
58
|
+
| `skills` | no | Effective Pi skill names, resolved from Main's registry |
|
|
42
59
|
| Markdown body | yes | Role system instructions |
|
|
43
60
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
String lists may also use comma-separated text, matching Pi's example role files. Repository-relative extension paths are rejected: child working directory is delegated project, so relative paths could load untrusted project code. Use absolute paths, `~/...`, or explicit package sources such as `npm:...`.
|
|
47
|
-
|
|
48
|
-
Skill entries use Pi Skill names, normally Skill directory names, not filesystem paths. At delegation time, package resolves names from Main's effective Pi Skill registry and passes matching files to child. Missing or unavailable Skills produce warning and are skipped; they do not block delegation. This preserves Main's trust and Skill collision decisions.
|
|
49
|
-
|
|
50
|
-
Pi's example `agents/` directory contains sample Role files, not another runtime mechanism. This package reuses that Markdown format but ships no presets: role capabilities and model routes stay explicit in user config. No nested `agents/` directory is needed because Roles remain Markdown files.
|
|
51
|
-
|
|
52
|
-
Reload Pi after adding or changing role files or manually editing model config. `/subagent` applies changes immediately.
|
|
53
|
-
|
|
54
|
-
## Execution
|
|
61
|
+
Missing skills warn and skip; they do not block delegation. No repo-controlled `.pi/agents` roles. No package-local model picker.
|
|
55
62
|
|
|
56
|
-
|
|
63
|
+
## Remove
|
|
57
64
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
- `modelClass`: optional `fast`, `balanced`, or `frontier`; defaults to configured `balanced`, then Main route
|
|
61
|
-
|
|
62
|
-
Run `/subagent` once per class. Select class, authenticated text model, and supported thinking level. Config lives in `~/.pi/agent/config/pi-subagent.json`:
|
|
63
|
-
|
|
64
|
-
```json
|
|
65
|
-
{
|
|
66
|
-
"models": {
|
|
67
|
-
"fast": { "model": "provider/fast-model", "thinkingLevel": "off" },
|
|
68
|
-
"balanced": { "model": "provider/balanced-model", "thinkingLevel": "medium" },
|
|
69
|
-
"frontier": { "model": "provider/frontier-model", "thinkingLevel": "max" }
|
|
70
|
-
}
|
|
71
|
-
}
|
|
65
|
+
```bash
|
|
66
|
+
pi remove npm:@henryqw/pi-subagent
|
|
72
67
|
```
|
|
73
68
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
Each call starts isolated child process. Ambient extensions and skills are disabled. Only role resources load. Child uses delegated working directory and normal Pi project context files, inheriting Main's project approval decision. Abort terminates child process group.
|
|
77
|
-
|
|
78
|
-
Configured model and thinking level must exist in Main model registry. Invalid role config or explicit model class fails before child starts.
|
|
79
|
-
|
|
80
|
-
Main-visible streaming updates, final output, and errors are capped at 50 KiB of UTF-8 text. Error collection stays bounded while child runs; malformed JSON events above 1 MiB fail delegation. Truncated output ends with exact omitted-byte count.
|
|
81
|
-
|
|
82
|
-
## Widget
|
|
69
|
+
## Development
|
|
83
70
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
✓ reviewer[sonnet:high] inspect auth diff 22.1k 14s
|
|
89
|
-
✗ worker[sonnet:high] fix token expiry 31.8k 27s
|
|
90
|
-
■ scout[haiku:low] map routes 6.2k 5s
|
|
71
|
+
```bash
|
|
72
|
+
npm test --workspace @henryqw/pi-subagent
|
|
73
|
+
npm run typecheck --workspace @henryqw/pi-subagent
|
|
74
|
+
npm run pack:check --workspace @henryqw/pi-subagent
|
|
91
75
|
```
|
|
92
|
-
|
|
93
|
-
Spinner means working; terminal icons mean success, failure, or abort. Token column sums `usage.totalTokens` across Subagent turns. Terminal rows auto-remove after one second; empty widget renders nothing. Role/route and task columns truncate to preserve right-aligned token and elapsed columns on narrow terminals.
|
|
94
|
-
|
|
95
|
-
## Scouting roles
|
|
96
|
-
|
|
97
|
-
Treat scouting result as index, not file dump. Put conclusions first, followed by exact file/line references, short snippets only, risks, and unexamined scope. Main can read cited files when more detail is needed. Split work when useful index cannot fit within output cap.
|
|
98
|
-
|
|
99
|
-
## Deliberate limits
|
|
100
|
-
|
|
101
|
-
- User roles only; no repo-controlled `.pi/agents` trust flow.
|
|
102
|
-
- One task per call; Main handles orchestration.
|
|
103
|
-
- No custom profile schema; role Markdown already groups instructions, tools, extensions, and skills.
|
|
104
|
-
- No persistent child sessions or interactive panes.
|
|
105
|
-
- No full-result artifact or retrieval protocol; split oversized work instead.
|
package/extensions/subagent.ts
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
3
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
|
-
import { basename,
|
|
6
|
-
import {
|
|
5
|
+
import { basename, isAbsolute, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
7
8
|
import { type ExtensionAPI, type ExtensionContext, getAgentDir, parseFrontmatter, type Theme } from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import { type Component, truncateToWidth, type TUI, visibleWidth } from "@earendil-works/pi-tui";
|
|
10
|
+
import {
|
|
11
|
+
modelReference,
|
|
12
|
+
orderedProfileRoutes,
|
|
13
|
+
PROFILE_NAMES,
|
|
14
|
+
readTaskModelsConfig,
|
|
15
|
+
resolveTaskModelRoute,
|
|
16
|
+
type ProfileName,
|
|
17
|
+
type ResolvedTaskRoute,
|
|
18
|
+
} from "@henryqw/pi-task-models";
|
|
9
19
|
import { Type } from "typebox";
|
|
10
20
|
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
21
|
+
const MODEL_CLASSES = PROFILE_NAMES;
|
|
22
|
+
const CODEX_ALIAS = /^openai-codex-(?:[2-9]|[1-9]\d+)$/;
|
|
23
|
+
const MULTI_CODEX_EXTENSION = fileURLToPath(import.meta.resolve("@henryqw/pi-multi-codex/extensions/multi-codex.ts"));
|
|
14
24
|
const MAX_OUTPUT_BYTES = 50 * 1024;
|
|
15
25
|
const MAX_JSON_EVENT_BYTES = 1024 * 1024;
|
|
16
26
|
const WIDGET_KEY = "subagent-status";
|
|
@@ -19,10 +29,7 @@ const TERMINAL_DISPLAY_MS = 1_000;
|
|
|
19
29
|
const MAX_WIDGET_ROWS = 8;
|
|
20
30
|
const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
21
31
|
|
|
22
|
-
type
|
|
23
|
-
type ModelClass = (typeof MODEL_CLASSES)[number];
|
|
24
|
-
type ConfiguredModel = { model: string; thinkingLevel: ThinkingLevel };
|
|
25
|
-
type Config = { models: Partial<Record<ModelClass, ConfiguredModel>> };
|
|
32
|
+
type ModelClass = ProfileName;
|
|
26
33
|
type Role = {
|
|
27
34
|
name: string;
|
|
28
35
|
description: string;
|
|
@@ -49,62 +56,27 @@ type WidgetItem = {
|
|
|
49
56
|
removeAt?: number;
|
|
50
57
|
};
|
|
51
58
|
|
|
52
|
-
const defaultConfig = (): Config => ({ models: {} });
|
|
53
59
|
const isModelClass = (value: unknown): value is ModelClass =>
|
|
54
60
|
typeof value === "string" && MODEL_CLASSES.includes(value as ModelClass);
|
|
55
|
-
const
|
|
56
|
-
typeof value === "string" && THINKING_LEVELS.includes(value as ThinkingLevel);
|
|
57
|
-
const isModelReference = (value: unknown): value is string => {
|
|
58
|
-
if (typeof value !== "string" || value !== value.trim() || value.includes("\0")) return false;
|
|
59
|
-
const separator = value.indexOf("/");
|
|
60
|
-
return separator > 0 && separator < value.length - 1;
|
|
61
|
-
};
|
|
61
|
+
const isNumberedCodexProvider = (provider: string): boolean => CODEX_ALIAS.test(provider);
|
|
62
62
|
|
|
63
|
-
function
|
|
63
|
+
function resolveTaskRoute(ctx: ExtensionContext, modelClass: ModelClass): ResolvedTaskRoute {
|
|
64
|
+
let config;
|
|
64
65
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (Object.keys(record).some((key) => key !== "models")) return { value: defaultConfig(), invalid: true };
|
|
69
|
-
if (record.models === undefined) return { value: defaultConfig(), invalid: false };
|
|
70
|
-
if (!record.models || typeof record.models !== "object" || Array.isArray(record.models)) {
|
|
71
|
-
return { value: defaultConfig(), invalid: true };
|
|
72
|
-
}
|
|
73
|
-
const modelRecord = record.models as Record<string, unknown>;
|
|
74
|
-
const models: Config["models"] = {};
|
|
75
|
-
let invalid = Object.keys(modelRecord).some((key) => !isModelClass(key));
|
|
76
|
-
for (const modelClass of MODEL_CLASSES) {
|
|
77
|
-
if (!Object.hasOwn(modelRecord, modelClass)) continue;
|
|
78
|
-
const candidate = modelRecord[modelClass];
|
|
79
|
-
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
80
|
-
invalid = true;
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
const route = candidate as Record<string, unknown>;
|
|
84
|
-
if (isModelReference(route.model) && isThinkingLevel(route.thinkingLevel)) {
|
|
85
|
-
models[modelClass] = { model: route.model, thinkingLevel: route.thinkingLevel };
|
|
86
|
-
} else invalid = true;
|
|
87
|
-
}
|
|
88
|
-
return { value: { models }, invalid };
|
|
89
|
-
} catch (error: unknown) {
|
|
90
|
-
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
91
|
-
return { value: defaultConfig(), invalid: false };
|
|
92
|
-
}
|
|
93
|
-
return { value: defaultConfig(), invalid: true };
|
|
66
|
+
config = readTaskModelsConfig();
|
|
67
|
+
} catch {
|
|
68
|
+
throw new Error("Couldn't read task model config. Run /task-models.");
|
|
94
69
|
}
|
|
95
|
-
}
|
|
96
70
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
71
|
+
const profile = config.profiles[modelClass];
|
|
72
|
+
if (!profile) throw new Error(`No ${modelClass} task model profile is configured. Run /task-models.`);
|
|
73
|
+
for (const route of orderedProfileRoutes(profile)) {
|
|
74
|
+
const resolved = resolveTaskModelRoute(ctx, route);
|
|
75
|
+
if (resolved) return resolved;
|
|
76
|
+
}
|
|
77
|
+
throw new Error(`No usable ${modelClass} task model route. Run /task-models.`);
|
|
101
78
|
}
|
|
102
79
|
|
|
103
|
-
const modelReference = (model: { provider: string; id: string }): string => `${model.provider}/${model.id}`;
|
|
104
|
-
const availableTextModels = (ctx: ExtensionContext) => ctx.modelRegistry
|
|
105
|
-
.getAvailable()
|
|
106
|
-
.filter((model) => model.input.includes("text"));
|
|
107
|
-
|
|
108
80
|
const cleanText = (value: unknown, field: string, file: string): string => {
|
|
109
81
|
if (typeof value !== "string" || !value.trim() || value.includes("\0")) {
|
|
110
82
|
throw new Error(`${file}: ${field} must be non-empty text.`);
|
|
@@ -466,7 +438,7 @@ const Parameters = Type.Object({
|
|
|
466
438
|
role: Type.String({ description: "Configured Subagent role name" }),
|
|
467
439
|
task: Type.String({ description: "One bounded task with needed context and expected result" }),
|
|
468
440
|
modelClass: Type.Optional(StringEnum(MODEL_CLASSES, {
|
|
469
|
-
description: "Classify task complexity: fast for narrow lookups or mechanical edits; balanced for normal bounded work; frontier for ambiguous, cross-cutting, or high-risk reasoning. Defaults to
|
|
441
|
+
description: "Classify task complexity: fast for narrow lookups or mechanical edits; balanced for normal bounded work; frontier for ambiguous, cross-cutting, or high-risk reasoning. Defaults to the shared balanced profile.",
|
|
470
442
|
})),
|
|
471
443
|
});
|
|
472
444
|
|
|
@@ -499,8 +471,6 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
499
471
|
let widgetTimer: ReturnType<typeof setInterval> | undefined;
|
|
500
472
|
let spinnerIndex = 0;
|
|
501
473
|
let activeTui: TUI | undefined;
|
|
502
|
-
let config = defaultConfig();
|
|
503
|
-
|
|
504
474
|
const stopWidgetTimer = () => {
|
|
505
475
|
if (!widgetTimer) return;
|
|
506
476
|
clearInterval(widgetTimer);
|
|
@@ -573,15 +543,8 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
573
543
|
requestWidgetRender();
|
|
574
544
|
};
|
|
575
545
|
|
|
576
|
-
const refreshConfig = (ctx: ExtensionContext) => {
|
|
577
|
-
const loaded = readConfig();
|
|
578
|
-
config = loaded.value;
|
|
579
|
-
if (loaded.invalid) ctx.ui.notify("Invalid pi-subagent config values were ignored.", "warning");
|
|
580
|
-
};
|
|
581
|
-
|
|
582
546
|
pi.on("session_start", (_event, ctx) => {
|
|
583
547
|
ensureWidget(ctx);
|
|
584
|
-
refreshConfig(ctx);
|
|
585
548
|
});
|
|
586
549
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
587
550
|
stopWidgetTimer();
|
|
@@ -591,48 +554,6 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
591
554
|
if (ctx.hasUI) ctx.ui.setWidget(WIDGET_KEY, undefined);
|
|
592
555
|
});
|
|
593
556
|
|
|
594
|
-
pi.registerCommand("subagent", {
|
|
595
|
-
description: "configure fast, balanced, and frontier Subagent model routes",
|
|
596
|
-
handler: async (args, ctx) => {
|
|
597
|
-
if (args.trim()) {
|
|
598
|
-
ctx.ui.notify("Usage: /subagent", "warning");
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
refreshConfig(ctx);
|
|
602
|
-
const modelClass = await ctx.ui.select("Subagent model class", [...MODEL_CLASSES]);
|
|
603
|
-
if (!isModelClass(modelClass)) return;
|
|
604
|
-
const models = availableTextModels(ctx);
|
|
605
|
-
if (!models.length) {
|
|
606
|
-
ctx.ui.notify("No authenticated text models are available.", "warning");
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
const saved = config.models[modelClass];
|
|
610
|
-
const references = models.map(modelReference).sort();
|
|
611
|
-
const selected = await ctx.ui.select(
|
|
612
|
-
`${modelClass} Subagent model · saved: ${saved?.model ?? "none"}`,
|
|
613
|
-
references,
|
|
614
|
-
);
|
|
615
|
-
const selectedModel = models.find((model) => modelReference(model) === selected);
|
|
616
|
-
if (!selectedModel) return;
|
|
617
|
-
const levels = getSupportedThinkingLevels(selectedModel);
|
|
618
|
-
const thinkingLevel = await ctx.ui.select(
|
|
619
|
-
`${modelClass} Subagent thinking level · saved: ${saved?.thinkingLevel ?? "none"}`,
|
|
620
|
-
levels,
|
|
621
|
-
);
|
|
622
|
-
if (!isThinkingLevel(thinkingLevel) || !levels.some((level) => level === thinkingLevel)) return;
|
|
623
|
-
try {
|
|
624
|
-
const latest = readConfig();
|
|
625
|
-
const route = { model: modelReference(selectedModel), thinkingLevel };
|
|
626
|
-
const next = { ...latest.value, models: { ...latest.value.models, [modelClass]: route } };
|
|
627
|
-
writeConfig(next);
|
|
628
|
-
config = next;
|
|
629
|
-
ctx.ui.notify(`${modelClass} Subagent set to ${route.model} with thinking ${thinkingLevel}.`, "info");
|
|
630
|
-
} catch {
|
|
631
|
-
ctx.ui.notify("Couldn't save pi-subagent model config.", "warning");
|
|
632
|
-
}
|
|
633
|
-
},
|
|
634
|
-
});
|
|
635
|
-
|
|
636
557
|
pi.registerTool({
|
|
637
558
|
name: "delegate_task",
|
|
638
559
|
label: "Subagent",
|
|
@@ -646,34 +567,14 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
646
567
|
throw new Error(`Unknown Subagent role: ${params.role}. Available roles: ${roles.map(({ name }) => name).join(", ") || "none"}.`);
|
|
647
568
|
}
|
|
648
569
|
|
|
649
|
-
refreshConfig(ctx);
|
|
650
|
-
if (!ctx.model) throw new Error("delegate_task requires an active Pi model.");
|
|
651
570
|
if (params.modelClass !== undefined && !isModelClass(params.modelClass)) {
|
|
652
571
|
throw new Error("delegate_task modelClass must be fast, balanced, or frontier.");
|
|
653
572
|
}
|
|
654
573
|
const modelClass = params.modelClass ?? "balanced";
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const selectedModelInfo = configuredModel && availableTextModels(ctx)
|
|
660
|
-
.find((model) => modelReference(model) === configuredModel.model);
|
|
661
|
-
if (params.modelClass && configuredModel && !selectedModelInfo) {
|
|
662
|
-
throw new Error(`Configured ${modelClass} Subagent model is unavailable; run /subagent.`);
|
|
663
|
-
}
|
|
664
|
-
const thinkingAvailable = Boolean(configuredModel && selectedModelInfo
|
|
665
|
-
&& getSupportedThinkingLevels(selectedModelInfo).some((level) => level === configuredModel.thinkingLevel));
|
|
666
|
-
if (params.modelClass && configuredModel && selectedModelInfo && !thinkingAvailable) {
|
|
667
|
-
throw new Error(`Configured ${modelClass} Subagent thinking level is unavailable; run /subagent.`);
|
|
668
|
-
}
|
|
669
|
-
const configuredRoute = thinkingAvailable ? configuredModel : undefined;
|
|
670
|
-
const modelReferenceValue = configuredRoute?.model ?? modelReference(ctx.model);
|
|
671
|
-
const model = configuredRoute ? selectedModelInfo : ctx.model;
|
|
672
|
-
if (!model) throw new Error("Subagent model metadata is unavailable.");
|
|
673
|
-
const thinkingLevel = configuredRoute?.thinkingLevel ?? ctx.thinkingLevel;
|
|
674
|
-
if (thinkingLevel && !getSupportedThinkingLevels(model).includes(thinkingLevel)) {
|
|
675
|
-
throw new Error(`Subagent thinking level ${thinkingLevel} is unavailable for ${modelReferenceValue}.`);
|
|
676
|
-
}
|
|
574
|
+
const resolvedRoute = resolveTaskRoute(ctx, modelClass);
|
|
575
|
+
const model = resolvedRoute.model;
|
|
576
|
+
const modelReferenceValue = modelReference(model);
|
|
577
|
+
const thinkingLevel = resolvedRoute.thinkingLevel;
|
|
677
578
|
|
|
678
579
|
const resolvedSkills = resolveSkillPaths(pi, role.skills);
|
|
679
580
|
if (resolvedSkills.missing.length) {
|
|
@@ -689,7 +590,10 @@ export default function subagentExtension(pi: ExtensionAPI): void {
|
|
|
689
590
|
try {
|
|
690
591
|
await writeFile(promptPath, role.systemPrompt, { encoding: "utf8", mode: 0o600 });
|
|
691
592
|
const args = ["--mode", "json", "-p", "--no-session", "--no-extensions", "--no-skills"];
|
|
692
|
-
|
|
593
|
+
const extensions = isNumberedCodexProvider(model.provider)
|
|
594
|
+
? [...role.extensions, MULTI_CODEX_EXTENSION]
|
|
595
|
+
: role.extensions;
|
|
596
|
+
for (const extension of new Set(extensions)) args.push("--extension", extension);
|
|
693
597
|
for (const skill of resolvedSkills.paths) args.push("--skill", skill);
|
|
694
598
|
if (role.tools !== undefined) {
|
|
695
599
|
if (role.tools.length) args.push("--tools", role.tools.join(","));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@henryqw/pi-subagent",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "Delegate one task to an isolated Pi role with explicit extensions and skills.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -44,5 +44,9 @@
|
|
|
44
44
|
"extensions": [
|
|
45
45
|
"./extensions/subagent.ts"
|
|
46
46
|
]
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"@henryqw/pi-multi-codex": "^0.3.8",
|
|
50
|
+
"@henryqw/pi-task-models": "^0.1.0"
|
|
47
51
|
}
|
|
48
52
|
}
|