@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 +15 -9
- package/package.json +2 -2
- package/src/agent-manager.ts +45 -24
- package/src/agents.test.ts +37 -0
- package/src/agents.ts +7 -2
- package/src/compact.test.ts +437 -0
- package/src/cost.test.ts +62 -0
- package/src/frontmatter-io.test.ts +119 -0
- package/src/local-config.test.ts +96 -0
- package/src/local-config.ts +50 -6
- package/src/save-agent.test.ts +317 -1
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
|
-
|
|
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
|

|
|
40
42
|
|
|
41
|
-
##
|
|
43
|
+
## Install
|
|
42
44
|
|
|
43
45
|
```bash
|
|
44
|
-
pi install
|
|
46
|
+
pi install npm:@pi-archimedes/subagent
|
|
45
47
|
```
|
|
46
48
|
|
|
47
|
-
Or install
|
|
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:** `<
|
|
88
|
-
- **User scope:** `~/.pi/agents/` — available across all projects
|
|
89
|
-
- **Global scope:**
|
|
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
|
-
|
|
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
|
|
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
|
|
14
|
+
"@pi-archimedes/core": "2.1.0"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
17
|
"@earendil-works/pi-ai": ">=0.1.0",
|
package/src/agent-manager.ts
CHANGED
|
@@ -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
|
|
1252
|
-
// catch block for .md rollback if a later step
|
|
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
|
|
1281
|
-
//
|
|
1282
|
-
//
|
|
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
|
|
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
|
|
1306
|
-
//
|
|
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
|
-
|
|
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
|
|
1352
|
-
// (including re-discovery) have succeeded. This ensures
|
|
1353
|
-
// step fails, the catch block can restore
|
|
1354
|
-
// live object retains
|
|
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
|
|
1363
|
-
// delete it when there is no model to fall
|
|
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
|
|
1366
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/agents.test.ts
CHANGED
|
@@ -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
|
|
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
|
|