@pi-archimedes/subagent 2.0.1 → 2.1.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
@@ -2,7 +2,9 @@
2
2
 
3
3
  Subagent dispatch with live TUI streaming and cost tracking for the [Pi coding agent](https://github.com/earendil-works/pi).
4
4
 
5
- ## Features
5
+ Dispatch specialized subagents to offload complex tasks with live TUI streaming, parallel execution, cost tracking, and per-agent model overrides. By fanning out work to dedicated subagents, complex workflows can be executed concurrently while maintaining full visibility into progress and token usage.
6
+
7
+ ## What you get
6
8
 
7
9
  - **Single & parallel execution** — dispatch one task or fan out multiple tasks across different agents simultaneously
8
10
  - **Live TUI streaming** — watch subagent progress in real-time with color-coded tool calls (grey while running, green/red on completion), readable argument previews, token counts, and cost updates
@@ -38,16 +40,16 @@ Toggle which tools are available to an agent from Pi's full toolset:
38
40
 
39
41
  ![subagents tool selection](../../docs/images/subagents-tool-selection.png)
40
42
 
41
- ## Installation
43
+ ## Install
42
44
 
43
45
  ```bash
44
- pi install @pi-archimedes/subagent
46
+ pi install npm:@pi-archimedes/subagent
45
47
  ```
46
48
 
47
- Or install the full [pi-archimedes](../..) meta package for the integrated experience (cost tracking in footer, shared chrome, etc.):
49
+ Or install full meta package:
48
50
 
49
51
  ```bash
50
- pi install pi-archimedes
52
+ pi install npm:pi-archimedes
51
53
  ```
52
54
 
53
55
  ## Usage
@@ -84,14 +86,18 @@ Run `/agents` to open the interactive Agents Manager for creating, editing, and
84
86
 
85
87
  Agents are defined as `.md` files with YAML frontmatter, placed in one of:
86
88
 
87
- - **Project scope:** `<cwd>/.pi/agents/` — available only in this project
88
- - **User scope:** `~/.pi/agents/` — available across all projects
89
- - **Global scope:** installed via packages or extensions
89
+ - **Project scope:** `<repo root>/.pi/agents/` — available only in this project
90
+ - **User scope:** `~/.pi/agent/agents/` — available across all projects
91
+ - **Global scope:** `<repo root>/.agents/agents/` or `~/.agents/agents/` — shared or installed subagents
92
+
93
+ Frontmatter supports: `name`, `description`, `model`, `tools`, and `thinking`. The markdown body becomes the agent's system prompt. Unknown frontmatter fields are preserved on edit but not interpreted.
90
94
 
91
- Frontmatter supports: `name`, `model`, `tools`, `thinking`, `inheritProjectContext`, `inheritSkills`, `systemPromptMode`, and `systemPrompt`.
95
+ Per-agent `model` and `thinking` assignments made in the `/agents` TUI are stored in `~/.pi/agent/agents.local.json` (machine-local, not committed) and take precedence over frontmatter values; on save, the TUI also strips these fields from the `.md` frontmatter. Frontmatter `model:` and `thinking:` still work as a fallback for hand-written agent files.
92
96
 
93
97
  ## Integration
94
98
 
95
99
  When installed via `pi-archimedes` (the meta package), subagent cost events flow through `@pi-archimedes/core/bus` and are consumed by `@pi-archimedes/footer`'s `CostAccumulator`. This merges subagent tokens and cost into the main status bar for a unified view.
96
100
 
97
101
  The `/agents` command is also only registered by the meta package (not by standalone `@pi-archimedes/subagent`).
102
+
103
+ ← Back to [pi-archimedes](../../README.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-archimedes/subagent",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -11,7 +11,7 @@
11
11
  ],
12
12
  "main": "./src/index.ts",
13
13
  "dependencies": {
14
- "@pi-archimedes/core": "2.0.1"
14
+ "@pi-archimedes/core": "2.1.0"
15
15
  },
16
16
  "peerDependencies": {
17
17
  "@earendil-works/pi-ai": ">=0.1.0",
@@ -17,6 +17,9 @@ import { serializeAgent, validateAgentName } from "./frontmatter-io.js";
17
17
  import {
18
18
  writeLocalModel,
19
19
  deleteLocalModel,
20
+ writeLocalThinking,
21
+ deleteLocalThinking,
22
+ deleteLocalAgent,
20
23
  readLocalConfig,
21
24
  setLocalConfig,
22
25
  type LocalConfig,
@@ -1248,9 +1251,11 @@ export function saveAgent(state: ManagerState, requestRender: () => void): void
1248
1251
  const newName = agent.name.endsWith(".md") ? agent.name : `${agent.name}.md`;
1249
1252
  const newPath = path.join(dir, newName);
1250
1253
 
1251
- // Capture model before entering the try block so it is available in the
1252
- // catch block for .md rollback if a later step (JSON write, etc.) fails.
1254
+ // Capture model and thinking before entering the try block so they are
1255
+ // available in the catch block for .md rollback if a later step
1256
+ // (JSON write, etc.) fails.
1253
1257
  const model = agent.model;
1258
+ const thinking = agent.thinking;
1254
1259
 
1255
1260
  // Track whether the .md write succeeded so the catch block knows whether
1256
1261
  // to restore or clean up the on-disk file.
@@ -1277,11 +1282,13 @@ export function saveAgent(state: ManagerState, requestRender: () => void): void
1277
1282
  // Ensure directory exists
1278
1283
  fs.mkdirSync(dir, { recursive: true });
1279
1284
 
1280
- // Build a shallow copy without the model for .md serialization so the
1281
- // live edit object is NOT mutated during serialization. If the .md
1282
- // write fails below, the live object stays intact for a retry.
1285
+ // Build a shallow copy without the model/thinking for .md serialization
1286
+ // (both fields are stored in agents.local.json) so the live edit object
1287
+ // is NOT mutated during serialization. If the .md write fails below, the
1288
+ // live object stays intact for a retry.
1283
1289
  const mdAgent = { ...agent };
1284
1290
  delete mdAgent.model;
1291
+ delete mdAgent.thinking;
1285
1292
 
1286
1293
  // Serialize and write the .md file FIRST. If this fails, no JSON state
1287
1294
  // is persisted and the live edit object is untouched.
@@ -1293,21 +1300,27 @@ export function saveAgent(state: ManagerState, requestRender: () => void): void
1293
1300
  // Capture a backup of the current JSON config so we can roll it back
1294
1301
  // if a later step (re-discovery, etc.) fails after this write succeeds.
1295
1302
  jsonConfigBefore = readLocalConfig();
1296
- // Write/remove the NEW name entry first, then clean up the OLD name.
1303
+ // Write/remove the NEW name entries first, then clean up the OLD name.
1297
1304
  if (model !== undefined) {
1298
1305
  writeLocalModel(agent.name, model);
1299
1306
  } else {
1300
1307
  deleteLocalModel(agent.name);
1301
1308
  }
1309
+ if (thinking !== undefined) {
1310
+ writeLocalThinking(agent.name, thinking);
1311
+ } else {
1312
+ deleteLocalThinking(agent.name);
1313
+ }
1302
1314
  jsonWritten = true;
1303
1315
 
1304
1316
  // Handle rename: delete old JSON entry keyed by original name (after
1305
- // the new entry is safely written). Wrapped in try-catch so a failure
1306
- // here does not leave the .md written but the live object un-stripped.
1317
+ // the new entries are safely written) deleteLocalAgent covers all
1318
+ // fields (model and thinking). Wrapped in try-catch so a failure here
1319
+ // does not leave the .md written but the live object un-stripped.
1307
1320
  const originalName = state.editOriginal?.name;
1308
1321
  if (originalName && originalName !== agent.name) {
1309
1322
  try {
1310
- deleteLocalModel(originalName);
1323
+ deleteLocalAgent(originalName);
1311
1324
  } catch {
1312
1325
  // Best-effort: stale entry is harmless and will be cleaned up on
1313
1326
  // a subsequent save/rename
@@ -1348,22 +1361,24 @@ export function saveAgent(state: ManagerState, requestRender: () => void): void
1348
1361
  state.editError = null;
1349
1362
  requestRender();
1350
1363
 
1351
- // Only strip model from the live edit object AFTER all operations
1352
- // (including re-discovery) have succeeded. This ensures that if any
1353
- // step fails, the catch block can restore the model to .md and the
1354
- // live object retains it for a safe retry.
1364
+ // Only strip model/thinking from the live edit object AFTER all
1365
+ // operations (including re-discovery) have succeeded. This ensures
1366
+ // that if any step fails, the catch block can restore them to .md
1367
+ // and the live object retains them for a safe retry.
1355
1368
  delete agent.model;
1369
+ delete agent.thinking;
1356
1370
  } catch (err) {
1357
1371
  // Restore prior .md state if the write succeeded but a later step
1358
1372
  // (JSON write, re-discovery, etc.) failed:
1359
1373
  // - rename (old file not yet unlinked): check whether the old file
1360
1374
  // still exists. If so, delete newPath so only the original remains.
1361
1375
  // If the old file is gone (deleted externally or by a prior attempt),
1362
- // newPath may be the sole copy — keep it with a model fallback, or
1363
- // delete it when there is no model to fall back on.
1376
+ // newPath may be the sole copy — keep it with a model/thinking
1377
+ // fallback, or delete it when there is no model/thinking to fall
1378
+ // back on.
1364
1379
  // - existing file: write the original content back verbatim.
1365
- // - new file with a model: keep a frontmatter fallback so the model
1366
- // override survives for the next retry.
1380
+ // - new file with model/thinking: keep a frontmatter fallback so the
1381
+ // overrides survive for the next retry.
1367
1382
  // If the old .md was already unlinked during rename (oldPathDeleted),
1368
1383
  // newPath is the sole surviving copy — leave it in place.
1369
1384
  // If the .md write itself failed (mdWritten is false) there is nothing
@@ -1373,19 +1388,25 @@ export function saveAgent(state: ManagerState, requestRender: () => void): void
1373
1388
  if (oldPath && fs.existsSync(oldPath)) {
1374
1389
  // Old file still exists — safe to delete newPath and restore prior state
1375
1390
  try { fs.unlinkSync(newPath); } catch { /* best-effort */ }
1376
- } else if (model !== undefined) {
1377
- // Old file is gone — keep newPath with model as fallback
1378
- try { fs.writeFileSync(newPath, serializeAgent({ ...agent, model }), "utf-8"); } catch { /* best-effort */ }
1391
+ } else if (model !== undefined || thinking !== undefined) {
1392
+ // Old file is gone — keep newPath with model/thinking as fallback
1393
+ const fallback: AgentConfig = { ...agent };
1394
+ if (model !== undefined) fallback.model = model;
1395
+ if (thinking !== undefined) fallback.thinking = thinking;
1396
+ try { fs.writeFileSync(newPath, serializeAgent(fallback), "utf-8"); } catch { /* best-effort */ }
1379
1397
  } else {
1380
- // Old file is gone and no model — delete newPath (no prior state to restore)
1398
+ // Old file is gone and no model/thinking — delete newPath (no prior state to restore)
1381
1399
  try { fs.unlinkSync(newPath); } catch { /* best-effort */ }
1382
1400
  }
1383
1401
  } else if (originalContent !== undefined) {
1384
1402
  try { fs.writeFileSync(newPath, originalContent, "utf-8"); } catch { /* best-effort */ }
1385
- } else if (model !== undefined) {
1386
- try { fs.writeFileSync(newPath, serializeAgent({ ...agent, model }), "utf-8"); } catch { /* best-effort */ }
1403
+ } else if (model !== undefined || thinking !== undefined) {
1404
+ const fallback: AgentConfig = { ...agent };
1405
+ if (model !== undefined) fallback.model = model;
1406
+ if (thinking !== undefined) fallback.thinking = thinking;
1407
+ try { fs.writeFileSync(newPath, serializeAgent(fallback), "utf-8"); } catch { /* best-effort */ }
1387
1408
  } else {
1388
- // New file without model — delete it (no prior state to restore)
1409
+ // New file without model/thinking — delete it (no prior state to restore)
1389
1410
  try { fs.unlinkSync(newPath); } catch { /* best-effort */ }
1390
1411
  }
1391
1412
  }
@@ -14,6 +14,7 @@ process.env.PI_CODING_AGENT_DIR = testDir;
14
14
  function makeAgent(
15
15
  name: string,
16
16
  model?: string,
17
+ thinking?: string,
17
18
  ): AgentConfig {
18
19
  return {
19
20
  name,
@@ -22,6 +23,7 @@ function makeAgent(
22
23
  source: "global" as const,
23
24
  filePath: join(testDir, `${name}.md`),
24
25
  ...(model !== undefined ? { model } : {}),
26
+ ...(thinking !== undefined ? { thinking } : {}),
25
27
  };
26
28
  }
27
29
 
@@ -62,6 +64,41 @@ describe("applyLocalOverrides", () => {
62
64
  expect(() => applyLocalOverrides([])).not.toThrow();
63
65
  });
64
66
 
67
+ it("sets thinking from JSON override", () => {
68
+ const path = join(testDir, "agents.local.json");
69
+ writeFileSync(path, JSON.stringify({ codex: { thinking: "high" } }), "utf-8");
70
+
71
+ const agent = makeAgent("codex");
72
+ applyLocalOverrides([agent]);
73
+ expect(agent.thinking).toBe("high");
74
+ });
75
+
76
+ it("leaves thinking unchanged when no JSON entry exists", () => {
77
+ const agent = makeAgent("codex", undefined, "low");
78
+ applyLocalOverrides([agent]);
79
+ expect(agent.thinking).toBe("low");
80
+ });
81
+
82
+ it("leaves thinking unchanged when JSON entry has no thinking field (model still applied)", () => {
83
+ const path = join(testDir, "agents.local.json");
84
+ writeFileSync(path, JSON.stringify({ codex: { model: "o1" } }), "utf-8");
85
+
86
+ const agent = makeAgent("codex", "M1", "low");
87
+ applyLocalOverrides([agent]);
88
+ expect(agent.thinking).toBe("low");
89
+ expect(agent.model).toBe("o1");
90
+ });
91
+
92
+ it("applies model and thinking together when both are present", () => {
93
+ const path = join(testDir, "agents.local.json");
94
+ writeFileSync(path, JSON.stringify({ codex: { model: "o1", thinking: "high" } }), "utf-8");
95
+
96
+ const agent = makeAgent("codex", "M1", "low");
97
+ applyLocalOverrides([agent]);
98
+ expect(agent.model).toBe("o1");
99
+ expect(agent.thinking).toBe("high");
100
+ });
101
+
65
102
  it("works with corrupt JSON file (agent model stays unchanged)", () => {
66
103
  const path = join(testDir, "agents.local.json");
67
104
  writeFileSync(path, "{ broken json", "utf-8");
package/src/agents.ts CHANGED
@@ -138,8 +138,10 @@ export interface AgentsDiscoveryResult {
138
138
  }
139
139
 
140
140
  /**
141
- * Apply local model overrides from agents.local.json to a list of agents.
142
- * Reads the config once and mutates matching agents in place.
141
+ * Apply local overrides from agents.local.json (model and thinking) to a list of agents.
142
+ * Reads the config once and mutates matching agents in place. JSON takes
143
+ * precedence over the agent's .md values; a missing entry or field leaves the
144
+ * agent's value untouched.
143
145
  */
144
146
  export function applyLocalOverrides(agents: AgentConfig[]): void {
145
147
  const config = readLocalConfig();
@@ -148,6 +150,9 @@ export function applyLocalOverrides(agents: AgentConfig[]): void {
148
150
  if (local?.model !== undefined) {
149
151
  agent.model = local.model;
150
152
  }
153
+ if (local?.thinking !== undefined) {
154
+ agent.thinking = local.thinking;
155
+ }
151
156
  }
152
157
  }
153
158