pi-better-subagents 0.9.3 → 0.11.0

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 CHANGED
@@ -12,6 +12,7 @@ Use `pi-better-subagents` when you want Pi to launch independent agent work with
12
12
 
13
13
  ## Core Features
14
14
  - Non-blocking subagent launches, with an optional role and named-agent catalog (`docs/agent-catalog.md`, `docs/agent-catalog-lifecycle.md`).
15
+ - Typed foreground `subagent_spawn_batch` launch receipts for native Pi codemode (Pi 0.99.1+), preserving direct tool calls, per-run callbacks, and batch capacity/catalog guarantees. See [codemode batch launches](docs/usage.md#codemode-batch-launches).
15
16
  - Default OS write sandboxing on macOS and Linux.
16
17
  - Explicit tool allowlists for child sessions.
17
18
  - Durable logs, result retrieval, and [failure observations](docs/failure-observations.md) independent of lifecycle status. A live child handles its own tool errors; the parent is woken only for actionable incidents, and children can classify handled failures with `failure_disposition`.
@@ -37,21 +38,24 @@ Linux confinement requires a usable `bubblewrap` backend and Pi SDK 0.82.1 or ne
37
38
  `config.json` sets `delegationMode` to `manual`, `adaptive` (default), or
38
39
  `coordinator`. `/subagents` displays the active mode, and
39
40
  `/subagents mode manual|adaptive|coordinator` changes it for the current
40
- session without changing config. Manual delegates only on explicit user or workflow request, even with a
41
+ session without changing config. `/subagents save` writes the current mode as
42
+ the next session's default. The navigator's `main` row opens the same control:
43
+ `m` cycles the session mode, `ctrl+s` saves that default, and `x`
44
+ does not stop it. Manual delegates only on explicit user or workflow request, even with a
41
45
  plan. Adaptive delegates substantial independent work when useful. Coordinator
42
46
  uses `agents_catalog` to discover current role descriptions and delegates every
43
47
  nontrivial role-owned task, while the foreground coordinates, integrates, and
44
48
  verifies. See [usage notes](docs/usage.md#delegation-mode).
45
49
 
46
50
  `agents_catalog` shows each role's and named agent's default model and effort,
47
- such as `role developer "Developer" … default openai/gpt-6-sol@high`. Roles are
51
+ such as `role developer "Developer" … default openai/gpt-6.1-sol@high`. Roles are
48
52
  listed and passed by short name (`role: "developer"`); the stored id
49
53
  `role.developer` also works, and named agents keep their full id
50
54
  (`agent: "agent.payments"`). To launch on that default, omit
51
55
  `model` and `thinking` on a role or agent spawn; name one only for a stated
52
56
  reason. When a launch's model or effort differs from the default, its launch
53
57
  line says so, for example
54
- `model openai/gpt-6-astra@high (role developer default openai/gpt-6-sol@high)`.
58
+ `model openai/gpt-6-astra@high (role developer default openai/gpt-6.1-sol@high)`.
55
59
 
56
60
  ## When To Use
57
61
 
package/agent-commands.ts CHANGED
@@ -123,7 +123,7 @@ interface ParsedArgs {
123
123
 
124
124
  export function registerAgentCommands(pi: Pick<ExtensionAPI, "registerCommand">, deps: AgentCommandDeps = {}): void {
125
125
  pi.registerCommand("agents", {
126
- description: "List, inspect, create, reload, or import Codex agent definitions",
126
+ description: "[list|show|create|reload|import-codex] — List, inspect, create, reload, or import agent definitions",
127
127
  getArgumentCompletions(prefix: string) {
128
128
  const commands = ["list", "inspect", "show", "create", "reload", "import-codex", "help"];
129
129
  const first = prefix.trim().split(/\s+/)[0] ?? "";
package/batch.mjs CHANGED
@@ -14,6 +14,64 @@ import { assertTimingParams } from "./timing.ts";
14
14
 
15
15
  const VALID_CAPACITY_MODES = new Set(["reject", "launch-available"]);
16
16
 
17
+ /** Public codemode receipt; details alone are not returned to nested callers. */
18
+ export const batchLaunchOutputSchema = {
19
+ type: "object",
20
+ properties: {
21
+ status: { type: "string", enum: ["launched", "partial", "not-launched", "clarification-needed"] },
22
+ batchId: { type: "string" },
23
+ batchName: { type: "string" },
24
+ launched: {
25
+ type: "array",
26
+ items: {
27
+ type: "object",
28
+ properties: { job: { type: "integer", minimum: 1 }, name: { type: "string" }, id: { type: "string" }, modelNote: { type: "string" } },
29
+ required: ["job", "name", "id"],
30
+ additionalProperties: false,
31
+ },
32
+ },
33
+ failed: {
34
+ type: "array",
35
+ items: {
36
+ type: "object",
37
+ properties: { job: { type: "integer", minimum: 1 }, name: { type: "string" }, reason: { type: "string" } },
38
+ required: ["job", "name", "reason"],
39
+ additionalProperties: false,
40
+ },
41
+ },
42
+ skipped: {
43
+ type: "array",
44
+ items: {
45
+ type: "object",
46
+ properties: { job: { type: "integer", minimum: 1 }, name: { type: "string" } },
47
+ required: ["job", "name"],
48
+ additionalProperties: false,
49
+ },
50
+ },
51
+ message: { type: "string" },
52
+ choices: { type: "array", items: { type: "string" } },
53
+ },
54
+ required: ["status", "launched", "failed", "skipped"],
55
+ additionalProperties: false,
56
+ };
57
+
58
+ /** Launch success is not run completion; partial receipts retain every created run. */
59
+ export function batchLaunchResult({ batchId, batchName, launched, skipped, failed }) {
60
+ const receipt = {
61
+ status: launched.length === 0 ? "not-launched" : failed.length || skipped.length ? "partial" : "launched",
62
+ batchId,
63
+ ...(batchName !== undefined ? { batchName } : {}),
64
+ launched,
65
+ failed,
66
+ skipped,
67
+ };
68
+ return {
69
+ content: [{ type: "text", text: formatBatchLaunchResponse(receipt) }],
70
+ details: receipt,
71
+ structuredContent: receipt,
72
+ };
73
+ }
74
+
17
75
  /**
18
76
  * Overlay per-job options on top of shared options. Only defined keys are
19
77
  * inherited; booleans keep their false values.
@@ -256,9 +256,9 @@ export async function clarifyCatalogRequest(
256
256
  * Launch-line note for a catalog run whose effective model or effort differs
257
257
  * from the role or agent default. The wording names the cause, so a fallback
258
258
  * or a capped effort is not mistaken for a caller override:
259
- * - override: `model openai/gpt-6-astra@high (role developer default openai/gpt-6-sol@high)`
260
- * - fallback: `model xai/grok-4.7@high (role developer default openai/gpt-6-sol@high unavailable; foreground fallback)`
261
- * - capped effort: `model openai/gpt-6-sol@medium (role developer default openai/gpt-6-sol@high; effort capped at medium by the model)`
259
+ * - override: `model openai/gpt-6-astra@high (role developer default openai/gpt-6.1-sol@high)`
260
+ * - fallback: `model xai/grok-4.7@high (role developer default openai/gpt-6.1-sol@high unavailable; foreground fallback)`
261
+ * - capped effort: `model openai/gpt-6.1-sol@medium (role developer default openai/gpt-6.1-sol@high; effort capped at medium by the model)`
262
262
  * Undefined for a non-catalog run, a definition with no default, or a launch
263
263
  * that matches the default. Only the fields the definition sets are compared.
264
264
  */
package/catalog-schema.ts CHANGED
@@ -72,10 +72,10 @@ export const PARSER_LIMITS = {
72
72
  * runtime already uses. Tiers are catalog policy, not benchmark rankings.
73
73
  */
74
74
  export const APPROVED_ROLE_DEFAULTS = [
75
- { id: "role.researcher", name: "Researcher", model: "openai/gpt-6-sol", effort: "medium", tier: "balanced" },
75
+ { id: "role.researcher", name: "Researcher", model: "openai/gpt-6.1-sol", effort: "medium", tier: "balanced" },
76
76
  { id: "role.explorer", name: "Explorer", model: "openai/gpt-6-luna", effort: "medium", tier: "efficient" },
77
- { id: "role.product-manager", name: "Product Manager", model: "openai/gpt-6-sol", effort: "medium", tier: "balanced" },
78
- { id: "role.developer", name: "Developer", model: "openai/gpt-6-sol", effort: "high", tier: "balanced" },
77
+ { id: "role.product-manager", name: "Product Manager", model: "openai/gpt-6.1-sol", effort: "medium", tier: "balanced" },
78
+ { id: "role.developer", name: "Developer", model: "openai/gpt-6.1-sol", effort: "high", tier: "balanced" },
79
79
  { id: "role.reviewer", name: "Reviewer", model: "openai/gpt-6-astra", effort: "medium", tier: "frontier" },
80
80
  { id: "role.architect", name: "Architect", model: "openai/gpt-6-astra", effort: "high", tier: "frontier" },
81
81
  ] as const;
package/config.ts CHANGED
@@ -8,12 +8,12 @@
8
8
  * `defaultTools` absent → the built-in SAFE_DEFAULT_TOOLS.
9
9
  */
10
10
 
11
- import { existsSync, readFileSync } from "node:fs";
11
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { dirname, join } from "node:path";
14
14
  import { homedir } from "node:os";
15
15
  import { SELF_SPEC } from "./extensions.ts";
16
- import type { DelegationMode } from "./delegation.ts";
16
+ import { isDelegationMode, type DelegationMode } from "./delegation.ts";
17
17
  import type { TimingSettings } from "./timing.ts";
18
18
 
19
19
  export interface SubagentConfig extends TimingSettings {
@@ -80,24 +80,49 @@ export const SAFE_DEFAULT_TOOLS = "read, bash, edit, write, web_search, web_fetc
80
80
  export const SAFE_CLEAN_TOOLS = "read, bash";
81
81
 
82
82
  let cached: SubagentConfig | undefined;
83
+ let configPathForTests: string | undefined;
83
84
 
84
85
  /** Test seam. Pass undefined to reload config.json on the next loadConfig call. */
85
86
  export function setConfigForTests(next: SubagentConfig | undefined): void {
86
87
  cached = next;
87
88
  }
88
89
 
90
+ /** Test seam. Save and load use this file instead of the package config.json. */
91
+ export function setConfigPathForTests(path: string | undefined): void {
92
+ configPathForTests = path;
93
+ cached = undefined;
94
+ }
95
+
96
+ export function configPath(): string {
97
+ return configPathForTests ?? join(dirname(fileURLToPath(import.meta.url)), "config.json");
98
+ }
99
+
89
100
  /** Load config.json from the extension directory. Missing/invalid → {}. */
90
101
  export function loadConfig(): SubagentConfig {
91
102
  if (cached) return cached;
92
103
  try {
93
- const dir = dirname(fileURLToPath(import.meta.url));
94
- cached = JSON.parse(readFileSync(join(dir, "config.json"), "utf-8")) as SubagentConfig;
104
+ cached = JSON.parse(readFileSync(configPath(), "utf-8")) as SubagentConfig;
95
105
  } catch {
96
106
  cached = {};
97
107
  }
98
108
  return cached;
99
109
  }
100
110
 
111
+ /** Write delegationMode into config.json, preserving every other key. */
112
+ export function writeDelegationMode(mode: DelegationMode, path = configPath()): void {
113
+ if (!isDelegationMode(mode)) throw new Error(`Invalid delegation mode: ${String(mode)}`);
114
+ let current: SubagentConfig = {};
115
+ try {
116
+ const parsed = JSON.parse(readFileSync(path, "utf-8")) as unknown;
117
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) current = parsed as SubagentConfig;
118
+ } catch { /* missing file starts as an empty config */ }
119
+ current.delegationMode = mode;
120
+ const pending = `${path}.${process.pid}.tmp`;
121
+ writeFileSync(pending, `${JSON.stringify(current, null, 2)}\n`);
122
+ renameSync(pending, path);
123
+ if (path === configPath()) cached = current;
124
+ }
125
+
101
126
  /** Normalize a comma/space tool list to pi's bare comma form: "a, b" → "a,b". */
102
127
  export function normalizeTools(list: string): string {
103
128
  return list.split(",").map((t) => t.trim()).filter(Boolean).join(",");
@@ -52,7 +52,7 @@ Codex applies the agent file's model and `model_reasoning_effort` ahead of the c
52
52
 
53
53
  `agents_catalog` accepts `list` and `inspect` only. It uses the same inspection view as `/agents show`. It has no write path. Launch remains `subagent_spawn` or the batch tool, with at most one `agent` or `role` selector. Those selectors are lifecycle's wiring, not this tool.
54
54
 
55
- A list line names a role by its short name (`role developer "Developer" …`), the form the `role` field takes; the inspect header, `identity.id`, and the structured `id` keep the stored id `role.developer`. Named agents show their full `agent.<slug>` id in both. Each list line and the inspect header end with the definition's default model and effort when one is set, such as `default openai/gpt-6-sol@high`: a role's own default, or a named agent's override or inherited role default. The structured view carries the same value as `defaults: { model, effort, label }`, or `null` when neither is set. A spawn that omits `model` and `thinking` uses it. When a catalog launch's model or effort differs from it, the launch line (and each batch job line) adds a note such as `model openai/gpt-6-astra@high (role developer default openai/gpt-6-sol@high)`; a named agent's note reads `agent default …`. Only the fields the definition sets are compared. The note names the cause: a fallback from an unavailable default reads `(role developer default openai/gpt-6-sol@high unavailable; foreground fallback)` (or `same-tier` / `configured-default`), and an effort the model cannot run adds `effort capped at <level> by the model`.
55
+ A list line names a role by its short name (`role developer "Developer" …`), the form the `role` field takes; the inspect header, `identity.id`, and the structured `id` keep the stored id `role.developer`. Named agents show their full `agent.<slug>` id in both. Each list line and the inspect header end with the definition's default model and effort when one is set, such as `default openai/gpt-6.1-sol@high`: a role's own default, or a named agent's override or inherited role default. The structured view carries the same value as `defaults: { model, effort, label }`, or `null` when neither is set. A spawn that omits `model` and `thinking` uses it. When a catalog launch's model or effort differs from it, the launch line (and each batch job line) adds a note such as `model openai/gpt-6-astra@high (role developer default openai/gpt-6.1-sol@high)`; a named agent's note reads `agent default …`. Only the fields the definition sets are compared. The note names the cause: a fallback from an unavailable default reads `(role developer default openai/gpt-6.1-sol@high unavailable; foreground fallback)` (or `same-tier` / `configured-default`), and an effort the model cannot run adds `effort capped at <level> by the model`.
56
56
 
57
57
  ## Lifecycle attachment
58
58
 
@@ -16,7 +16,7 @@ id: role.developer
16
16
  name: Developer
17
17
  description: Implements a requested change in the existing system.
18
18
  defaults:
19
- model: openai/gpt-6-sol
19
+ model: openai/gpt-6.1-sol
20
20
  effort: high
21
21
  tier: balanced
22
22
  ---
@@ -100,10 +100,10 @@ Model availability, same-tier fallback, and nearest-effort adjustment are not de
100
100
 
101
101
  | Role | Stored id | Name | Model | Effort | Tier |
102
102
  | --- | --- | --- | --- | --- | --- |
103
- | `researcher` | `role.researcher` | Researcher | `openai/gpt-6-sol` | medium | balanced |
103
+ | `researcher` | `role.researcher` | Researcher | `openai/gpt-6.1-sol` | medium | balanced |
104
104
  | `explorer` | `role.explorer` | Explorer | `openai/gpt-6-luna` | medium | efficient |
105
- | `product-manager` | `role.product-manager` | Product Manager | `openai/gpt-6-sol` | medium | balanced |
106
- | `developer` | `role.developer` | Developer | `openai/gpt-6-sol` | high | balanced |
105
+ | `product-manager` | `role.product-manager` | Product Manager | `openai/gpt-6.1-sol` | medium | balanced |
106
+ | `developer` | `role.developer` | Developer | `openai/gpt-6.1-sol` | high | balanced |
107
107
  | `reviewer` | `role.reviewer` | Reviewer | `openai/gpt-6-astra` | medium | frontier |
108
108
  | `architect` | `role.architect` | Architect | `openai/gpt-6-astra` | high | frontier |
109
109
 
@@ -10,7 +10,7 @@ Normative rules are R3, R4, R8, and the resolver half of R10. Prose parsing and
10
10
  - It does not grant tools, sandbox modes, extensions, or presets. Inspection copies the catalog `capabilities` object (`grantedByCatalog: false`).
11
11
  - It does not call a provider. Availability is one `getAvailable()` snapshot from the foreground registry. `find()` only explains that a known model is outside that set.
12
12
  - It does not retry, and it does not substitute after a child has started. `automaticRetry` and `substitutionAfterStart` are false. `startupFailure.action` is `surface-error-no-retry-no-substitution`. A later provider initialization failure is a lifecycle error.
13
- - It does not invent tier candidates. Built-in tiers name the approved models and have empty candidate lists. `openai/gpt-6-sol`, `openai/gpt-6-luna`, and `openai/gpt-6-astra` are not substitutes.
13
+ - It does not invent tier candidates. Built-in tiers name the approved models and have empty candidate lists. `openai/gpt-6.1-sol`, `openai/gpt-6-luna`, and `openai/gpt-6-astra` are not substitutes.
14
14
 
15
15
  ## Precedence
16
16