pi-better-subagents 0.1.12 → 0.1.14
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/batch.mjs +3 -0
- package/index.ts +16 -15
- package/package.json +1 -1
- package/registry.ts +2 -0
- package/thinking.ts +28 -0
package/batch.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { BUILTIN_TOOLS } from "./extensions.mjs";
|
|
11
11
|
import { SAFE_CLEAN_TOOLS } from "./config.ts";
|
|
12
|
+
import { assertThinkingLevel } from "./thinking.ts";
|
|
12
13
|
|
|
13
14
|
const VALID_CAPACITY_MODES = new Set(["reject", "launch-available"]);
|
|
14
15
|
|
|
@@ -21,6 +22,7 @@ export function mergeJobOptions(shared, job) {
|
|
|
21
22
|
prompt: job.prompt,
|
|
22
23
|
name: job.name ?? shared?.name,
|
|
23
24
|
model: job.model ?? shared?.model,
|
|
25
|
+
thinking: job.thinking ?? shared?.thinking,
|
|
24
26
|
tools: job.tools ?? shared?.tools,
|
|
25
27
|
exclude_tools: job.exclude_tools ?? shared?.exclude_tools,
|
|
26
28
|
clean: job.clean ?? shared?.clean,
|
|
@@ -74,6 +76,7 @@ export function validateBatchPlan({ shared, jobs, onCapacity, config }) {
|
|
|
74
76
|
seenPrompts.add(job.prompt);
|
|
75
77
|
|
|
76
78
|
const merged = mergeJobOptions(shared, job);
|
|
79
|
+
assertThinkingLevel(merged.thinking, `job ${i + 1} thinking`);
|
|
77
80
|
if (merged.clean === true) {
|
|
78
81
|
// Validate the effective tool allowlist, which resolves in this order:
|
|
79
82
|
// per-job tools → shared tools → config.defaultTools → clean-safe built-ins.
|
package/index.ts
CHANGED
|
@@ -106,6 +106,7 @@ import {
|
|
|
106
106
|
buildNavigatorDetail,
|
|
107
107
|
} from "./navigator.ts";
|
|
108
108
|
import { enforceRegistrySizeCapOnce, runDailyCleanupOnce } from "./cleanup.ts";
|
|
109
|
+
import { assertThinkingLevel, parseModelThinking, type ThinkingLevel } from "./thinking.ts";
|
|
109
110
|
|
|
110
111
|
/** The tools this extension registers — excluded from children by default so a
|
|
111
112
|
* subagent cannot recursively spawn more subagents unless explicitly allowed. */
|
|
@@ -613,11 +614,7 @@ function navigatorRows(visible?: RunMeta[], at?: number) {
|
|
|
613
614
|
const snap = spendFor(m.id, now);
|
|
614
615
|
return snap.tool ?? "";
|
|
615
616
|
},
|
|
616
|
-
|
|
617
|
-
effortFor: (m: RunMeta) => {
|
|
618
|
-
const any = m as RunMeta & { effort?: string; modelEffort?: string };
|
|
619
|
-
return any.effort ?? any.modelEffort;
|
|
620
|
-
},
|
|
617
|
+
effortFor: (m: RunMeta) => m.effort,
|
|
621
618
|
healthFor: (m: RunMeta) => observeNavigatorHealth(m, now),
|
|
622
619
|
});
|
|
623
620
|
}
|
|
@@ -668,10 +665,7 @@ function navigatorDetail(id: string, now: number = Date.now()) {
|
|
|
668
665
|
fmtElapsed,
|
|
669
666
|
fmtSpend,
|
|
670
667
|
now,
|
|
671
|
-
effortFor: (m: RunMeta) =>
|
|
672
|
-
const any = m as RunMeta & { effort?: string; modelEffort?: string };
|
|
673
|
-
return any.effort ?? any.modelEffort;
|
|
674
|
-
},
|
|
668
|
+
effortFor: (m: RunMeta) => m.effort,
|
|
675
669
|
healthFor: (m: RunMeta) => observeNavigatorHealth(m, now),
|
|
676
670
|
});
|
|
677
671
|
}
|
|
@@ -917,7 +911,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
917
911
|
ensureSubagentProvider();
|
|
918
912
|
|
|
919
913
|
type SpawnParams = {
|
|
920
|
-
prompt: string; name?: string; model?: string; tools?: string;
|
|
914
|
+
prompt: string; name?: string; model?: string; thinking?: ThinkingLevel; tools?: string;
|
|
921
915
|
exclude_tools?: string; clean?: boolean; sandbox?: boolean;
|
|
922
916
|
sandbox_dir?: string; callback?: boolean; cwd?: string;
|
|
923
917
|
git_clone_workspace?: boolean; approve?: boolean; allow_nested?: boolean;
|
|
@@ -941,7 +935,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
941
935
|
warn: string;
|
|
942
936
|
sandboxDir?: string;
|
|
943
937
|
}> {
|
|
938
|
+
assertThinkingLevel(p.thinking);
|
|
944
939
|
const cfg = loadConfig();
|
|
940
|
+
const requestedModel = p.model ?? cfg.defaultModel ?? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
|
|
941
|
+
const { model, thinking } = parseModelThinking(requestedModel, p.thinking);
|
|
945
942
|
// Best-effort daily hygiene for durable tmp state. The marker makes
|
|
946
943
|
// this effectively free after the first subagent launch each day.
|
|
947
944
|
runDailyCleanupOnce({ config: cfg });
|
|
@@ -978,7 +975,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
978
975
|
});
|
|
979
976
|
const cwd = workspace.cwd;
|
|
980
977
|
const requestedSandboxDir = workspace.requestedSandboxDir;
|
|
981
|
-
const model = p.model ?? cfg.defaultModel ?? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
|
|
982
978
|
|
|
983
979
|
if (requestedSandboxDir) mkdirSync(requestedSandboxDir, { recursive: true });
|
|
984
980
|
writeFileSync(promptPathFor(id), p.prompt);
|
|
@@ -1017,6 +1013,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1017
1013
|
"--session-id", id,
|
|
1018
1014
|
...extArgs,
|
|
1019
1015
|
...(model ? ["--model", model] : []),
|
|
1016
|
+
...(thinking ? ["--thinking", thinking] : []),
|
|
1020
1017
|
...(allow ? ["--tools", allow] : []),
|
|
1021
1018
|
...(excludes.size ? ["--exclude-tools", [...excludes].join(",")] : []),
|
|
1022
1019
|
...(p.approve ? ["--approve"] : []),
|
|
@@ -1043,7 +1040,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1043
1040
|
|
|
1044
1041
|
const meta: RunMeta = {
|
|
1045
1042
|
id, name: p.name, status: "running",
|
|
1046
|
-
pid: spawned.pid, spawnPid: process.pid, spawnPidStartTime: parentStartToken(), model,
|
|
1043
|
+
pid: spawned.pid, spawnPid: process.pid, spawnPidStartTime: parentStartToken(), model,
|
|
1044
|
+
effort: thinking, cwd,
|
|
1047
1045
|
...identity,
|
|
1048
1046
|
promptPreview: p.prompt.slice(0, 200),
|
|
1049
1047
|
startedAt: Date.now(), logPath: logPathFor(id), sessionId: id,
|
|
@@ -1088,14 +1086,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1088
1086
|
"Use subagent_spawn for independent work the user should not have to wait on. It returns at once with a run id; that return IS the deliverable — report the id to the user and continue.",
|
|
1089
1087
|
"After subagent_spawn, do NOT call subagent_output or subagent_result in a loop to wait for the result, and do NOT sleep. The run completes on its own and reports back on the next turn.",
|
|
1090
1088
|
"Only call subagent_result / subagent_output when the user explicitly asks how a run is going or for its result.",
|
|
1091
|
-
"The tools param is both the tool allowlist AND what determines which extensions load in the child (e.g. tools='read,bash,web_fetch' loads only the web-tools package). Ask for the tools the task needs and nothing more; clean:true gives a built-ins-only child. Pick a model with the model param (e.g. 'xai/grok-4.5').",
|
|
1089
|
+
"The tools param is both the tool allowlist AND what determines which extensions load in the child (e.g. tools='read,bash,web_fetch' loads only the web-tools package). Ask for the tools the task needs and nothing more; clean:true gives a built-ins-only child. Pick a model with the model param (e.g. 'xai/grok-4.5@high'); providerless model patterns are resolved by Pi, while provider/model is deterministic and loads mapped provider extensions.",
|
|
1092
1090
|
"By default the subagent is sandboxed (writes confined to its working dir, reads and network open) and triggers completion here on finish. Set callback:false to finish quietly — then read the result on demand via subagent_result.",
|
|
1093
1091
|
"Use git_clone_workspace:true when the subagent will mutate Git in a sandbox. The parent prepares a disposable, self-contained clone with a real .git/ directory inside the sandbox root, so linked-worktree metadata outside the sandbox cannot stall the child.",
|
|
1094
1092
|
],
|
|
1095
1093
|
parameters: Type.Object({
|
|
1096
1094
|
prompt: Type.String({ description: "The task for the subagent. This is the only context it gets — be self-contained." }),
|
|
1097
1095
|
name: Type.Optional(Type.String({ description: "Short label for the run (e.g. 'reviewer')." })),
|
|
1098
|
-
model: Type.Optional(Type.String({ description: "
|
|
1096
|
+
model: Type.Optional(Type.String({ description: "Pi model pattern, preferably provider/id, optionally suffixed with @effort (for example openai/gpt-5.5@high). Providerless patterns are resolved by Pi. Default: inherit foreground model." })),
|
|
1097
|
+
thinking: Type.Optional(Type.String({ description: "Reasoning effort for the child: off, minimal, low, medium, high, xhigh, or max (default: Pi/model default)." })),
|
|
1099
1098
|
tools: Type.Optional(Type.String({ description: "Tool allowlist: comma-separated names the child may use (e.g. 'read,bash,web_fetch'). This ALSO selects which extensions load — only packages backing a requested tool are loaded. Defaults to the configured safe set." })),
|
|
1100
1099
|
exclude_tools: Type.Optional(Type.String({ description: "Comma-separated tool denylist, applied on top of the allowlist." })),
|
|
1101
1100
|
clean: Type.Optional(Type.Boolean({ description: "Run a hermetic child with NO extensions at all (only built-ins: read, bash, edit, write). Default false — the extensions backing the requested tools load, so web_fetch and model auth (e.g. xai) work." })),
|
|
@@ -1160,7 +1159,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1160
1159
|
parameters: Type.Object({
|
|
1161
1160
|
batchName: Type.Optional(Type.String({ description: "Optional display label for the batch." })),
|
|
1162
1161
|
shared: Type.Optional(Type.Object({
|
|
1163
|
-
model: Type.Optional(Type.String({ description: "
|
|
1162
|
+
model: Type.Optional(Type.String({ description: "Pi model pattern, preferably provider/id, optionally suffixed with @effort (default: inherit foreground model)." })),
|
|
1163
|
+
thinking: Type.Optional(Type.String({ description: "Reasoning effort applied to every job: off, minimal, low, medium, high, xhigh, or max." })),
|
|
1164
1164
|
tools: Type.Optional(Type.String({ description: "Tool allowlist applied to every job." })),
|
|
1165
1165
|
exclude_tools: Type.Optional(Type.String({ description: "Comma-separated tool denylist applied to every job." })),
|
|
1166
1166
|
sandbox: Type.Optional(Type.Boolean({ description: "Default TRUE: kernel-confine writes to the working dir." })),
|
|
@@ -1176,6 +1176,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1176
1176
|
prompt: Type.String({ description: "The task for this job." }),
|
|
1177
1177
|
name: Type.Optional(Type.String({ description: "Short label for this job." })),
|
|
1178
1178
|
model: Type.Optional(Type.String()),
|
|
1179
|
+
thinking: Type.Optional(Type.String()),
|
|
1179
1180
|
tools: Type.Optional(Type.String()),
|
|
1180
1181
|
exclude_tools: Type.Optional(Type.String()),
|
|
1181
1182
|
sandbox: Type.Optional(Type.Boolean()),
|
package/package.json
CHANGED
package/registry.ts
CHANGED
|
@@ -85,6 +85,8 @@ export interface RunMeta {
|
|
|
85
85
|
*/
|
|
86
86
|
adoptedFromLostParentAt?: number;
|
|
87
87
|
model?: string;
|
|
88
|
+
/** Reasoning effort passed to the child via Pi's --thinking option. */
|
|
89
|
+
effort?: string;
|
|
88
90
|
cwd: string;
|
|
89
91
|
/** First ~200 chars of the task prompt, for listings. */
|
|
90
92
|
promptPreview: string;
|
package/thinking.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
2
|
+
|
|
3
|
+
export type ThinkingLevel = typeof THINKING_LEVELS[number];
|
|
4
|
+
|
|
5
|
+
const VALID_THINKING_LEVELS = new Set<string>(THINKING_LEVELS);
|
|
6
|
+
|
|
7
|
+
export function assertThinkingLevel(value: unknown, label: string = "thinking"): asserts value is ThinkingLevel | undefined {
|
|
8
|
+
if (value === undefined || VALID_THINKING_LEVELS.has(String(value))) return;
|
|
9
|
+
throw new Error(`${label} must be one of: ${THINKING_LEVELS.join(", ")}; got ${JSON.stringify(value)}.`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Resolve the convenient `model@effort` shorthand used by subagent callers. */
|
|
13
|
+
export function parseModelThinking(
|
|
14
|
+
model: string | undefined,
|
|
15
|
+
thinking?: ThinkingLevel,
|
|
16
|
+
): { model: string | undefined; thinking: ThinkingLevel | undefined } {
|
|
17
|
+
if (!model) return { model, thinking };
|
|
18
|
+
|
|
19
|
+
const separator = model.lastIndexOf("@");
|
|
20
|
+
if (separator <= 0 || separator === model.length - 1) return { model, thinking };
|
|
21
|
+
|
|
22
|
+
const suffix = model.slice(separator + 1);
|
|
23
|
+
assertThinkingLevel(suffix, "model thinking suffix");
|
|
24
|
+
return {
|
|
25
|
+
model: model.slice(0, separator),
|
|
26
|
+
thinking: thinking ?? suffix,
|
|
27
|
+
};
|
|
28
|
+
}
|