pi-better-subagents 0.1.10 → 0.1.13

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
@@ -30,7 +30,7 @@ import {
30
30
  type BackgroundWorkRow,
31
31
  } from "./shared-navigator.ts";
32
32
  import { spawnDetached, type SpawnResult } from "./spawn.ts";
33
- import { parseRun, resetParseRunCursor, type Usage } from "./parse.ts";
33
+ import { parseRun, resetParseRunCursor, tailLog, type Usage } from "./parse.ts";
34
34
  import { finalizeRun as finalizeRunCore } from "./finalization.ts";
35
35
  import { loadConfig, normalizeTools, resolveExtensionPath, SAFE_DEFAULT_TOOLS, SAFE_CLEAN_TOOLS, DEFAULT_MAX_CONCURRENT } from "./config.ts";
36
36
  import { resolveExtensions, extensionArgs } from "./extensions.ts";
@@ -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
  }
@@ -659,8 +656,7 @@ function isTerminalNavigatorStatus(status: string): boolean {
659
656
  }
660
657
 
661
658
  /** Live detail snapshot for one run (registry + log parse + health). */
662
- function navigatorDetail(id: string) {
663
- const now = Date.now();
659
+ function navigatorDetail(id: string, now: number = Date.now()) {
664
660
  return buildNavigatorDetail(id, {
665
661
  readMeta,
666
662
  effectiveStatus,
@@ -669,10 +665,7 @@ function navigatorDetail(id: string) {
669
665
  fmtElapsed,
670
666
  fmtSpend,
671
667
  now,
672
- effortFor: (m: RunMeta) => {
673
- const any = m as RunMeta & { effort?: string; modelEffort?: string };
674
- return any.effort ?? any.modelEffort;
675
- },
668
+ effortFor: (m: RunMeta) => m.effort,
676
669
  healthFor: (m: RunMeta) => observeNavigatorHealth(m, now),
677
670
  });
678
671
  }
@@ -742,9 +735,10 @@ function subagentWorkRows(now: number): BackgroundWorkRow[] {
742
735
  });
743
736
  }
744
737
 
745
- function subagentWorkDetail(id: string, now: number): BackgroundWorkDetail | null {
746
- const detail = navigatorDetail(id);
738
+ function subagentWorkDetail(id: string, now: number, options?: { logTailLines?: number }): BackgroundWorkDetail | null {
739
+ const detail = navigatorDetail(id, now);
747
740
  if (!detail) return null;
741
+ const logTailLines = options?.logTailLines ?? 10;
748
742
  const metadata = [
749
743
  { label: "provider", value: "Subagents" },
750
744
  { label: "model", value: detail.effort ? `${detail.model} · effort ${detail.effort}` : detail.model },
@@ -762,7 +756,11 @@ function subagentWorkDetail(id: string, now: number): BackgroundWorkDetail | nul
762
756
  statusTone: statusTone(detail.status),
763
757
  subtitle: detail.currentTool ? `current tool ${detail.currentTool}` : undefined,
764
758
  metadata,
765
- evidence: { label: "output", text: detail.output || "(no output yet)" },
759
+ // The shared navigator refreshes this provider once a second while the
760
+ // detail overlay is open. Read the selected logical tail rows each
761
+ // time so the evidence behaves like `tail -f`, not a single parsed
762
+ // activity/result snapshot.
763
+ evidence: { label: "log tail", text: tailLog(id, logTailLines) },
766
764
  footerActions: [detail.status === "running" || detail.status === "orphaned" ? "x stop" : "x dismiss"],
767
765
  };
768
766
  }
@@ -775,7 +773,7 @@ function ensureSubagentProvider(): void {
775
773
  priority: 10,
776
774
  visibleCount: () => navigatorRunningCount(),
777
775
  listRows: (now) => subagentWorkRows(now),
778
- detail: (id, now) => subagentWorkDetail(id, now),
776
+ detail: (id, now, options) => subagentWorkDetail(id, now, options),
779
777
  armCloseLabel: (row) => row.status === "running" || row.status === "orphaned" ? "x again to stop" : "x again to dismiss",
780
778
  close: (id) => {
781
779
  const outcome = navigatorCloseRun(id) as { action: string; id: string; status?: string };
@@ -913,7 +911,7 @@ export default function (pi: ExtensionAPI) {
913
911
  ensureSubagentProvider();
914
912
 
915
913
  type SpawnParams = {
916
- prompt: string; name?: string; model?: string; tools?: string;
914
+ prompt: string; name?: string; model?: string; thinking?: ThinkingLevel; tools?: string;
917
915
  exclude_tools?: string; clean?: boolean; sandbox?: boolean;
918
916
  sandbox_dir?: string; callback?: boolean; cwd?: string;
919
917
  git_clone_workspace?: boolean; approve?: boolean; allow_nested?: boolean;
@@ -937,7 +935,10 @@ export default function (pi: ExtensionAPI) {
937
935
  warn: string;
938
936
  sandboxDir?: string;
939
937
  }> {
938
+ assertThinkingLevel(p.thinking);
940
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);
941
942
  // Best-effort daily hygiene for durable tmp state. The marker makes
942
943
  // this effectively free after the first subagent launch each day.
943
944
  runDailyCleanupOnce({ config: cfg });
@@ -974,7 +975,6 @@ export default function (pi: ExtensionAPI) {
974
975
  });
975
976
  const cwd = workspace.cwd;
976
977
  const requestedSandboxDir = workspace.requestedSandboxDir;
977
- const model = p.model ?? cfg.defaultModel ?? (ctx.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
978
978
 
979
979
  if (requestedSandboxDir) mkdirSync(requestedSandboxDir, { recursive: true });
980
980
  writeFileSync(promptPathFor(id), p.prompt);
@@ -1013,6 +1013,7 @@ export default function (pi: ExtensionAPI) {
1013
1013
  "--session-id", id,
1014
1014
  ...extArgs,
1015
1015
  ...(model ? ["--model", model] : []),
1016
+ ...(thinking ? ["--thinking", thinking] : []),
1016
1017
  ...(allow ? ["--tools", allow] : []),
1017
1018
  ...(excludes.size ? ["--exclude-tools", [...excludes].join(",")] : []),
1018
1019
  ...(p.approve ? ["--approve"] : []),
@@ -1039,7 +1040,8 @@ export default function (pi: ExtensionAPI) {
1039
1040
 
1040
1041
  const meta: RunMeta = {
1041
1042
  id, name: p.name, status: "running",
1042
- pid: spawned.pid, spawnPid: process.pid, spawnPidStartTime: parentStartToken(), model, cwd,
1043
+ pid: spawned.pid, spawnPid: process.pid, spawnPidStartTime: parentStartToken(), model,
1044
+ effort: thinking, cwd,
1043
1045
  ...identity,
1044
1046
  promptPreview: p.prompt.slice(0, 200),
1045
1047
  startedAt: Date.now(), logPath: logPathFor(id), sessionId: id,
@@ -1084,14 +1086,15 @@ export default function (pi: ExtensionAPI) {
1084
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.",
1085
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.",
1086
1088
  "Only call subagent_result / subagent_output when the user explicitly asks how a run is going or for its result.",
1087
- "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.",
1088
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.",
1089
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.",
1090
1092
  ],
1091
1093
  parameters: Type.Object({
1092
1094
  prompt: Type.String({ description: "The task for the subagent. This is the only context it gets — be self-contained." }),
1093
1095
  name: Type.Optional(Type.String({ description: "Short label for the run (e.g. 'reviewer')." })),
1094
- 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)." })),
1095
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." })),
1096
1099
  exclude_tools: Type.Optional(Type.String({ description: "Comma-separated tool denylist, applied on top of the allowlist." })),
1097
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." })),
@@ -1156,7 +1159,8 @@ export default function (pi: ExtensionAPI) {
1156
1159
  parameters: Type.Object({
1157
1160
  batchName: Type.Optional(Type.String({ description: "Optional display label for the batch." })),
1158
1161
  shared: Type.Optional(Type.Object({
1159
- 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." })),
1160
1164
  tools: Type.Optional(Type.String({ description: "Tool allowlist applied to every job." })),
1161
1165
  exclude_tools: Type.Optional(Type.String({ description: "Comma-separated tool denylist applied to every job." })),
1162
1166
  sandbox: Type.Optional(Type.Boolean({ description: "Default TRUE: kernel-confine writes to the working dir." })),
@@ -1172,6 +1176,7 @@ export default function (pi: ExtensionAPI) {
1172
1176
  prompt: Type.String({ description: "The task for this job." }),
1173
1177
  name: Type.Optional(Type.String({ description: "Short label for this job." })),
1174
1178
  model: Type.Optional(Type.String()),
1179
+ thinking: Type.Optional(Type.String()),
1175
1180
  tools: Type.Optional(Type.String()),
1176
1181
  exclude_tools: Type.Optional(Type.String()),
1177
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.10",
3
+ "version": "0.1.13",
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/parse.ts CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  readSync,
27
27
  statSync,
28
28
  } from "node:fs";
29
- import { readBoundedTail } from "./shared-log-utils.ts";
29
+ import { readBoundedTail, tailTerminalDisplay } from "./shared-log-utils.ts";
30
30
  import { readAppendedLines, type LogCursor } from "./log-cursor.ts";
31
31
  import { logPathFor } from "./registry.ts";
32
32
 
@@ -89,13 +89,11 @@ interface TailRead {
89
89
  */
90
90
  const readTail: (path: string, maxBytes: number) => TailRead = readBoundedTail;
91
91
 
92
- /** Last `n` lines of a run's log, or a placeholder if empty/unreadable. */
92
+ /** Last `n` terminal display rows of a run's log, or a placeholder if empty/unreadable. */
93
93
  export function tailLog(id: string, n: number, maxBytes = maxRawTailBytes()): string {
94
94
  const tail = readTail(logPathFor(id), maxBytes);
95
95
  if (tail.error || tail.text.trim() === "") return "(no output yet)";
96
- const lines = tail.text.split("\n");
97
- const kept = lines.slice(Math.max(0, lines.length - n));
98
- const out = kept.join("\n").trim();
96
+ const out = tailTerminalDisplay(tail.text, n).trim();
99
97
  return out === "" ? "(no output yet)" : out;
100
98
  }
101
99
 
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
+ }