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 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
- // Effort is shown when available on metadata; Pi does not always expose it.
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, cwd,
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: "Model as provider/id (default: inherit foreground model)." })),
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: "Model as provider/id (default: inherit foreground model)." })),
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-subagents",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Pi extension for detached, sandboxed subagent runs that keep the foreground session free.",
5
5
  "license": "MIT",
6
6
  "type": "module",
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
+ }