@pi-archimedes/subagent 2.0.1 → 2.2.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 +54 -124
- 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/index.ts +2 -1
- 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.2.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.2.0"
|
|
15
15
|
},
|
|
16
16
|
"peerDependencies": {
|
|
17
17
|
"@earendil-works/pi-ai": ">=0.1.0",
|
package/src/agent-manager.ts
CHANGED
|
@@ -17,10 +17,22 @@ 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,
|
|
23
26
|
} from "./local-config.js";
|
|
27
|
+
import {
|
|
28
|
+
visibleWidth,
|
|
29
|
+
padEnd,
|
|
30
|
+
wrapText,
|
|
31
|
+
hardTruncate,
|
|
32
|
+
renderHeader,
|
|
33
|
+
renderFooter,
|
|
34
|
+
wrapWithBorder,
|
|
35
|
+
} from "@pi-archimedes/core/overlay";
|
|
24
36
|
|
|
25
37
|
// ── Screen constants ────────────────────────────────────────────────────────
|
|
26
38
|
|
|
@@ -148,55 +160,6 @@ function fuzzyFilter(items: AgentConfig[], query: string): AgentConfig[] {
|
|
|
148
160
|
);
|
|
149
161
|
}
|
|
150
162
|
|
|
151
|
-
function wrapText(text: string, width: number): string[] {
|
|
152
|
-
if (width <= 0) return [];
|
|
153
|
-
const lines: string[] = [];
|
|
154
|
-
const paragraphs = text.split("\n");
|
|
155
|
-
for (const para of paragraphs) {
|
|
156
|
-
if (para.length === 0) {
|
|
157
|
-
lines.push("");
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
const words = para.split(/(\s+)/).filter(Boolean);
|
|
161
|
-
let current = "";
|
|
162
|
-
for (const word of words) {
|
|
163
|
-
const test = current === "" ? word : current + word;
|
|
164
|
-
if (test.length > width && current.length > 0) {
|
|
165
|
-
lines.push(current);
|
|
166
|
-
current = word;
|
|
167
|
-
} else {
|
|
168
|
-
current = test;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
if (current) lines.push(current);
|
|
172
|
-
}
|
|
173
|
-
return lines;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function padEnd(text: string, width: number): string {
|
|
177
|
-
if (width <= 0) return "";
|
|
178
|
-
const vw = visibleWidth(text);
|
|
179
|
-
if (vw >= width) return text;
|
|
180
|
-
return text + " ".repeat(width - vw);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function visibleWidth(text: string): number {
|
|
184
|
-
// Strip ANSI escape sequences for width calculation
|
|
185
|
-
return text.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function row(text: string, width: number, theme: Theme): string {
|
|
189
|
-
return padEnd(text, width);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function renderHeader(text: string, width: number, theme: Theme): string {
|
|
193
|
-
return theme.fg("accent", padEnd(text, width));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function renderFooter(text: string, width: number, theme: Theme): string {
|
|
197
|
-
return theme.fg("dim", padEnd(text, width));
|
|
198
|
-
}
|
|
199
|
-
|
|
200
163
|
function scopeLabel(source: "global" | "user" | "project"): string {
|
|
201
164
|
if (source === "global") return "home";
|
|
202
165
|
return source === "user" ? "user" : "proj";
|
|
@@ -217,57 +180,6 @@ function filterModels(models: ModelInfo[], query: string): ModelInfo[] {
|
|
|
217
180
|
);
|
|
218
181
|
}
|
|
219
182
|
|
|
220
|
-
|
|
221
|
-
// ── Border wrapper ────────────────────────────────────────────────────────────
|
|
222
|
-
|
|
223
|
-
/** Hard-truncate by visible width — no "..." suffix. Strips ANSI, truncates, rebuilds. */
|
|
224
|
-
function hardTruncate(text: string, maxVisible: number): string {
|
|
225
|
-
if (visibleWidth(text) <= maxVisible) return text;
|
|
226
|
-
// Strip ANSI codes, truncate, then re-apply any trailing reset codes
|
|
227
|
-
const plain = text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
228
|
-
const truncated = plain.slice(0, maxVisible);
|
|
229
|
-
// Restore any ANSI codes that were in the original up to this point
|
|
230
|
-
let result = "";
|
|
231
|
-
let plainPos = 0;
|
|
232
|
-
let i = 0;
|
|
233
|
-
let copiedSgr = false;
|
|
234
|
-
while (i < text.length && plainPos < maxVisible) {
|
|
235
|
-
if (text[i] === "\x1b" && text[i + 1] === "[") {
|
|
236
|
-
// Copy the escape sequence
|
|
237
|
-
let j = i;
|
|
238
|
-
while (j < text.length && text[j] !== "m") j++;
|
|
239
|
-
result += text.slice(i, j + 1);
|
|
240
|
-
copiedSgr = true;
|
|
241
|
-
i = j + 1;
|
|
242
|
-
} else {
|
|
243
|
-
result += text[i];
|
|
244
|
-
plainPos++;
|
|
245
|
-
i++;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
// Ensure styling doesn't bleed: append reset if we copied SGR and result doesn't end with one
|
|
249
|
-
if (copiedSgr && !/\x1b\[0?m$/.test(result)) {
|
|
250
|
-
result += "\x1b[0m";
|
|
251
|
-
}
|
|
252
|
-
return result;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function wrapWithBorder(lines: string[], width: number, theme: Theme): string[] {
|
|
256
|
-
const innerWidth = Math.max(1, width - 2);
|
|
257
|
-
const contentWidth = Math.max(1, innerWidth - 2); // minus 1 space padding each side
|
|
258
|
-
const left = theme.fg("dim", "│");
|
|
259
|
-
const right = theme.fg("dim", "│");
|
|
260
|
-
const top = theme.fg("dim", `┌${"─".repeat(innerWidth)}┐`);
|
|
261
|
-
const bottom = theme.fg("dim", `└${"─".repeat(innerWidth)}┘`);
|
|
262
|
-
const result: string[] = [top];
|
|
263
|
-
for (const line of lines) {
|
|
264
|
-
const clamped = hardTruncate(line, contentWidth);
|
|
265
|
-
const padded = " " + padEnd(clamped, contentWidth) + " ";
|
|
266
|
-
result.push(left + padded + right);
|
|
267
|
-
}
|
|
268
|
-
result.push(bottom);
|
|
269
|
-
return result;
|
|
270
|
-
}
|
|
271
183
|
// ── List screen ─────────────────────────────────────────────────────────────
|
|
272
184
|
|
|
273
185
|
function renderList(state: ManagerState, width: number, theme: Theme): string[] {
|
|
@@ -1248,9 +1160,11 @@ export function saveAgent(state: ManagerState, requestRender: () => void): void
|
|
|
1248
1160
|
const newName = agent.name.endsWith(".md") ? agent.name : `${agent.name}.md`;
|
|
1249
1161
|
const newPath = path.join(dir, newName);
|
|
1250
1162
|
|
|
1251
|
-
// Capture model before entering the try block so
|
|
1252
|
-
// catch block for .md rollback if a later step
|
|
1163
|
+
// Capture model and thinking before entering the try block so they are
|
|
1164
|
+
// available in the catch block for .md rollback if a later step
|
|
1165
|
+
// (JSON write, etc.) fails.
|
|
1253
1166
|
const model = agent.model;
|
|
1167
|
+
const thinking = agent.thinking;
|
|
1254
1168
|
|
|
1255
1169
|
// Track whether the .md write succeeded so the catch block knows whether
|
|
1256
1170
|
// to restore or clean up the on-disk file.
|
|
@@ -1277,11 +1191,13 @@ export function saveAgent(state: ManagerState, requestRender: () => void): void
|
|
|
1277
1191
|
// Ensure directory exists
|
|
1278
1192
|
fs.mkdirSync(dir, { recursive: true });
|
|
1279
1193
|
|
|
1280
|
-
// Build a shallow copy without the model for .md serialization
|
|
1281
|
-
//
|
|
1282
|
-
//
|
|
1194
|
+
// Build a shallow copy without the model/thinking for .md serialization
|
|
1195
|
+
// (both fields are stored in agents.local.json) so the live edit object
|
|
1196
|
+
// is NOT mutated during serialization. If the .md write fails below, the
|
|
1197
|
+
// live object stays intact for a retry.
|
|
1283
1198
|
const mdAgent = { ...agent };
|
|
1284
1199
|
delete mdAgent.model;
|
|
1200
|
+
delete mdAgent.thinking;
|
|
1285
1201
|
|
|
1286
1202
|
// Serialize and write the .md file FIRST. If this fails, no JSON state
|
|
1287
1203
|
// is persisted and the live edit object is untouched.
|
|
@@ -1293,21 +1209,27 @@ export function saveAgent(state: ManagerState, requestRender: () => void): void
|
|
|
1293
1209
|
// Capture a backup of the current JSON config so we can roll it back
|
|
1294
1210
|
// if a later step (re-discovery, etc.) fails after this write succeeds.
|
|
1295
1211
|
jsonConfigBefore = readLocalConfig();
|
|
1296
|
-
// Write/remove the NEW name
|
|
1212
|
+
// Write/remove the NEW name entries first, then clean up the OLD name.
|
|
1297
1213
|
if (model !== undefined) {
|
|
1298
1214
|
writeLocalModel(agent.name, model);
|
|
1299
1215
|
} else {
|
|
1300
1216
|
deleteLocalModel(agent.name);
|
|
1301
1217
|
}
|
|
1218
|
+
if (thinking !== undefined) {
|
|
1219
|
+
writeLocalThinking(agent.name, thinking);
|
|
1220
|
+
} else {
|
|
1221
|
+
deleteLocalThinking(agent.name);
|
|
1222
|
+
}
|
|
1302
1223
|
jsonWritten = true;
|
|
1303
1224
|
|
|
1304
1225
|
// Handle rename: delete old JSON entry keyed by original name (after
|
|
1305
|
-
// the new
|
|
1306
|
-
//
|
|
1226
|
+
// the new entries are safely written) — deleteLocalAgent covers all
|
|
1227
|
+
// fields (model and thinking). Wrapped in try-catch so a failure here
|
|
1228
|
+
// does not leave the .md written but the live object un-stripped.
|
|
1307
1229
|
const originalName = state.editOriginal?.name;
|
|
1308
1230
|
if (originalName && originalName !== agent.name) {
|
|
1309
1231
|
try {
|
|
1310
|
-
|
|
1232
|
+
deleteLocalAgent(originalName);
|
|
1311
1233
|
} catch {
|
|
1312
1234
|
// Best-effort: stale entry is harmless and will be cleaned up on
|
|
1313
1235
|
// a subsequent save/rename
|
|
@@ -1348,22 +1270,24 @@ export function saveAgent(state: ManagerState, requestRender: () => void): void
|
|
|
1348
1270
|
state.editError = null;
|
|
1349
1271
|
requestRender();
|
|
1350
1272
|
|
|
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
|
|
1273
|
+
// Only strip model/thinking from the live edit object AFTER all
|
|
1274
|
+
// operations (including re-discovery) have succeeded. This ensures
|
|
1275
|
+
// that if any step fails, the catch block can restore them to .md
|
|
1276
|
+
// and the live object retains them for a safe retry.
|
|
1355
1277
|
delete agent.model;
|
|
1278
|
+
delete agent.thinking;
|
|
1356
1279
|
} catch (err) {
|
|
1357
1280
|
// Restore prior .md state if the write succeeded but a later step
|
|
1358
1281
|
// (JSON write, re-discovery, etc.) failed:
|
|
1359
1282
|
// - rename (old file not yet unlinked): check whether the old file
|
|
1360
1283
|
// still exists. If so, delete newPath so only the original remains.
|
|
1361
1284
|
// 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
|
|
1285
|
+
// newPath may be the sole copy — keep it with a model/thinking
|
|
1286
|
+
// fallback, or delete it when there is no model/thinking to fall
|
|
1287
|
+
// back on.
|
|
1364
1288
|
// - existing file: write the original content back verbatim.
|
|
1365
|
-
// - new file with
|
|
1366
|
-
//
|
|
1289
|
+
// - new file with model/thinking: keep a frontmatter fallback so the
|
|
1290
|
+
// overrides survive for the next retry.
|
|
1367
1291
|
// If the old .md was already unlinked during rename (oldPathDeleted),
|
|
1368
1292
|
// newPath is the sole surviving copy — leave it in place.
|
|
1369
1293
|
// If the .md write itself failed (mdWritten is false) there is nothing
|
|
@@ -1373,19 +1297,25 @@ export function saveAgent(state: ManagerState, requestRender: () => void): void
|
|
|
1373
1297
|
if (oldPath && fs.existsSync(oldPath)) {
|
|
1374
1298
|
// Old file still exists — safe to delete newPath and restore prior state
|
|
1375
1299
|
try { fs.unlinkSync(newPath); } catch { /* best-effort */ }
|
|
1376
|
-
} else if (model !== undefined) {
|
|
1377
|
-
// Old file is gone — keep newPath with model as fallback
|
|
1378
|
-
|
|
1300
|
+
} else if (model !== undefined || thinking !== undefined) {
|
|
1301
|
+
// Old file is gone — keep newPath with model/thinking as fallback
|
|
1302
|
+
const fallback: AgentConfig = { ...agent };
|
|
1303
|
+
if (model !== undefined) fallback.model = model;
|
|
1304
|
+
if (thinking !== undefined) fallback.thinking = thinking;
|
|
1305
|
+
try { fs.writeFileSync(newPath, serializeAgent(fallback), "utf-8"); } catch { /* best-effort */ }
|
|
1379
1306
|
} else {
|
|
1380
|
-
// Old file is gone and no model — delete newPath (no prior state to restore)
|
|
1307
|
+
// Old file is gone and no model/thinking — delete newPath (no prior state to restore)
|
|
1381
1308
|
try { fs.unlinkSync(newPath); } catch { /* best-effort */ }
|
|
1382
1309
|
}
|
|
1383
1310
|
} else if (originalContent !== undefined) {
|
|
1384
1311
|
try { fs.writeFileSync(newPath, originalContent, "utf-8"); } catch { /* best-effort */ }
|
|
1385
|
-
} else if (model !== undefined) {
|
|
1386
|
-
|
|
1312
|
+
} else if (model !== undefined || thinking !== undefined) {
|
|
1313
|
+
const fallback: AgentConfig = { ...agent };
|
|
1314
|
+
if (model !== undefined) fallback.model = model;
|
|
1315
|
+
if (thinking !== undefined) fallback.thinking = thinking;
|
|
1316
|
+
try { fs.writeFileSync(newPath, serializeAgent(fallback), "utf-8"); } catch { /* best-effort */ }
|
|
1387
1317
|
} else {
|
|
1388
|
-
// New file without model — delete it (no prior state to restore)
|
|
1318
|
+
// New file without model/thinking — delete it (no prior state to restore)
|
|
1389
1319
|
try { fs.unlinkSync(newPath); } catch { /* best-effort */ }
|
|
1390
1320
|
}
|
|
1391
1321
|
}
|
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
|
|