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 +3 -0
- package/index.ts +27 -22
- package/package.json +1 -1
- package/parse.ts +3 -5
- 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
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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: "
|
|
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: "
|
|
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
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`
|
|
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
|
|
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
|
+
}
|