@herbertgao/pi-subagents 0.15.1 → 0.15.3
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/CHANGELOG.md +20 -0
- package/README.md +6 -2
- package/package.json +3 -3
- package/src/agent-manager.ts +1 -0
- package/src/agent-runner.ts +3 -1
- package/src/custom-agents.ts +85 -11
- package/src/index.ts +61 -39
- package/src/nested-tools.ts +26 -35
- package/src/prompts.ts +15 -1
- package/src/settings.ts +12 -0
- package/src/types.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#27](https://github.com/HerbertGao/pi-extensions/pull/27) [`32bb76c`](https://github.com/HerbertGao/pi-extensions/commit/32bb76c117971de27db9c2b521d19df3ba9ea322) Thanks [@HerbertGao](https://github.com/HerbertGao)! - Keep worktree-isolated agents inside their copy, preserve real Agent tool startup errors, and render unknown or failed Agent results without misleading completion status.
|
|
8
|
+
|
|
9
|
+
## 0.15.2
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#15](https://github.com/HerbertGao/pi-extensions/pull/15) [`bc083d2`](https://github.com/HerbertGao/pi-extensions/commit/bc083d27c974f6b5239561dbd2295b6dd53526c0) Thanks [@HerbertGao](https://github.com/HerbertGao)! - Skip unreadable or malformed custom agent files by default, warn when an earlier same-named definition remains active, and add opt-in strict startup validation.
|
|
14
|
+
|
|
3
15
|
All notable changes to this project will be documented in this file.
|
|
4
16
|
|
|
5
17
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
@@ -7,6 +19,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
19
|
|
|
8
20
|
## [Unreleased]
|
|
9
21
|
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- **`strictAgentFiles` — fail startup on a broken agent file instead of skipping it.** Off by default and applied only during initial extension activation; later per-call reloads remain tolerant.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **One malformed agent file no longer aborts extension activation** ([#212](https://github.com/tintinweb/pi-subagents/issues/212) — thanks [@daromaj](https://github.com/daromaj)). Unreadable and unparseable files are skipped with a path-specific warning, including the earlier source that remains active when a broken file was an override.
|
|
29
|
+
|
|
10
30
|
## [0.15.1] - 2026-08-10
|
|
11
31
|
|
|
12
32
|
### Fixed
|
package/README.md
CHANGED
|
@@ -180,6 +180,8 @@ Agents are discovered from three locations (higher priority wins):
|
|
|
180
180
|
|
|
181
181
|
Project-level agents override global ones with the same name, so you can customize a global agent for a specific project. If both project locations define the same name, **`.pi/agents/` wins** — `.pi` stays the project authority; `.agents/agents/` is an additional read location for projects that keep their agent assets in the `.agents` workspace. The global location follows the upstream `PI_CODING_AGENT_DIR` env var — set it to relocate all pi-coding-agent state (agents, skills, settings) to a custom directory.
|
|
182
182
|
|
|
183
|
+
An unreadable or unparseable agent file is skipped by default, with a warning that names the file and error. If the skipped file was overriding a same-named agent, another warning names the earlier file that remains active. Set `strictAgentFiles: true` in `subagents.json` (or `/agents → Settings → Strict agent files`) to fail startup on a broken file instead; mid-session reloads remain tolerant.
|
|
184
|
+
|
|
183
185
|
### Example: `.pi/agents/auditor.md`
|
|
184
186
|
|
|
185
187
|
```markdown
|
|
@@ -430,12 +432,14 @@ When on, each subagent spawn's effective model is validated against pi's own `en
|
|
|
430
432
|
|
|
431
433
|
## Persistent Settings
|
|
432
434
|
|
|
433
|
-
Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, nested depth, fallback agent, default join mode, scheduling on/off, scope models on/off, disable defaults on/off, output transcript on/off, tool description full/compact/custom, widget all/background/off) persist across pi restarts. Two files, merged on load:
|
|
435
|
+
Runtime tuning values set via `/agents` → Settings (max concurrency, default max turns, grace turns, nested depth, fallback agent, default join mode, scheduling on/off, scope models on/off, strict agent files on/off, disable defaults on/off, output transcript on/off, tool description full/compact/custom, widget all/background/off) persist across pi restarts. Two files, merged on load:
|
|
434
436
|
|
|
435
437
|
- **Global:** `~/.pi/agent/subagents.json` — your machine-wide defaults. Edit by hand; the `/agents` menu never writes here.
|
|
436
438
|
- **Project:** `<cwd>/.pi/subagents.json` — per-project overrides. Written by `/agents` → Settings.
|
|
437
439
|
|
|
438
|
-
**Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `4`, default max turns unlimited, grace turns `5`, nested depth `2`, join mode `smart`, defaults enabled).
|
|
440
|
+
**Precedence:** project overrides global on any field present in both. Missing fields fall back to the hardcoded defaults (max concurrency `4`, default max turns unlimited, grace turns `5`, nested depth `2`, join mode `smart`, strict agent files disabled, defaults enabled).
|
|
441
|
+
|
|
442
|
+
**Strict agent files** (`strictAgentFiles`, default `false`): fail extension startup when any discovered agent file is unreadable or malformed. Enable via `/agents → Settings → Strict agent files` or set `true` in `subagents.json`. Strictness applies only to startup; reloads before later Agent calls remain tolerant so a file edited incorrectly mid-session is skipped with a warning instead of aborting the call.
|
|
439
443
|
|
|
440
444
|
**Nested depth** (`maxSubagentDepth`, default `2`): the hard ceiling on [nested delegation](#nested-subagents), counted from the main session (main = 0, its subagents = 1). `0` or `1` disables nesting project-wide regardless of any agent's `allowed_subagents`. Read when a subagent session is built, so a change applies to agents started after it.
|
|
441
445
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@herbertgao/pi-subagents",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
4
4
|
"description": "Claude Code-style autonomous subagents for Pi, with HerbertGao-maintained UI extensions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
},
|
|
65
65
|
"x-upstream": {
|
|
66
66
|
"package": "@tintinweb/pi-subagents",
|
|
67
|
-
"version": "0.
|
|
67
|
+
"version": "0.15.0",
|
|
68
68
|
"repository": "https://github.com/tintinweb/pi-subagents",
|
|
69
|
-
"commit": "
|
|
69
|
+
"commit": "c83dd82"
|
|
70
70
|
}
|
|
71
71
|
}
|
package/src/agent-manager.ts
CHANGED
|
@@ -321,6 +321,7 @@ export class AgentManager {
|
|
|
321
321
|
// stay undefined otherwise so plain worktree runs keep resolving config
|
|
322
322
|
// (incl. relative extension paths and memory) inside the worktree copy.
|
|
323
323
|
cwd: worktreeCwd ?? customCwd,
|
|
324
|
+
worktreeBase: worktreeCwd ? baseCwd : undefined,
|
|
324
325
|
configCwd:
|
|
325
326
|
options.configCwd ?? (customCwd !== undefined ? ctx.cwd : undefined),
|
|
326
327
|
signal: record.abortController!.signal,
|
package/src/agent-runner.ts
CHANGED
|
@@ -414,6 +414,8 @@ export interface RunOptions {
|
|
|
414
414
|
thinkingLevel?: ThinkingLevel
|
|
415
415
|
/** Override working directory (e.g. for worktree isolation). */
|
|
416
416
|
cwd?: string
|
|
417
|
+
/** Original checkout path when cwd is an isolated worktree copy. */
|
|
418
|
+
worktreeBase?: string
|
|
417
419
|
/**
|
|
418
420
|
* Where .pi config is discovered (project extensions, skills, pi settings,
|
|
419
421
|
* agent memory). Default: same as the working directory. The manager sets
|
|
@@ -602,7 +604,7 @@ export async function runAgent(
|
|
|
602
604
|
const parentSystemPrompt = ctx.getSystemPrompt()
|
|
603
605
|
|
|
604
606
|
// Build prompt extras (memory, skill preloading)
|
|
605
|
-
const extras: PromptExtras = {}
|
|
607
|
+
const extras: PromptExtras = { worktreeBase: options.worktreeBase }
|
|
606
608
|
|
|
607
609
|
// Resolve extensions/skills: isolated overrides to false
|
|
608
610
|
const extensions = options.isolated ? false : config.extensions
|
package/src/custom-agents.ts
CHANGED
|
@@ -8,6 +8,13 @@ import { getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent"
|
|
|
8
8
|
import { BUILTIN_TOOL_NAMES } from "./agent-types.js"
|
|
9
9
|
import type { AgentConfig, MemoryScope, ThinkingLevel } from "./types.js"
|
|
10
10
|
|
|
11
|
+
interface WarningState {
|
|
12
|
+
previous: Set<string>
|
|
13
|
+
current: Set<string>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const warningHistoryByCwd = new Map<string, Set<string>>()
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* Scan for custom agent .md files from multiple locations.
|
|
13
20
|
* Discovery hierarchy (higher priority wins):
|
|
@@ -20,15 +27,36 @@ import type { AgentConfig, MemoryScope, ThinkingLevel } from "./types.js"
|
|
|
20
27
|
* authority; .agents/agents is an additional read location.
|
|
21
28
|
* Any name is allowed — names matching defaults (e.g. "Explore") override them.
|
|
22
29
|
*/
|
|
23
|
-
export function loadCustomAgents(
|
|
30
|
+
export function loadCustomAgents(
|
|
31
|
+
cwd: string,
|
|
32
|
+
strict = false,
|
|
33
|
+
): Map<string, AgentConfig> {
|
|
24
34
|
const globalDir = join(getAgentDir(), "agents")
|
|
25
35
|
const workspaceProjectDir = join(cwd, ".agents", "agents")
|
|
26
36
|
const projectDir = join(cwd, ".pi", "agents")
|
|
27
37
|
|
|
28
38
|
const agents = new Map<string, AgentConfig>()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
const skippedOverrides = new Set<string>()
|
|
40
|
+
const warnings: WarningState = {
|
|
41
|
+
previous: warningHistoryByCwd.get(cwd) ?? new Set(),
|
|
42
|
+
current: new Set(),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
loadFromDir(globalDir, agents, "global", strict, warnings, skippedOverrides) // lowest priority
|
|
46
|
+
loadFromDir(
|
|
47
|
+
workspaceProjectDir,
|
|
48
|
+
agents,
|
|
49
|
+
"project",
|
|
50
|
+
strict,
|
|
51
|
+
warnings,
|
|
52
|
+
skippedOverrides,
|
|
53
|
+
) // shared workspace
|
|
54
|
+
loadFromDir(projectDir, agents, "project", strict, warnings, skippedOverrides) // highest priority (overwrites)
|
|
55
|
+
|
|
56
|
+
for (const name of skippedOverrides) {
|
|
57
|
+
warnSkippedOverride(name, agents, warnings)
|
|
58
|
+
}
|
|
59
|
+
warningHistoryByCwd.set(cwd, warnings.current)
|
|
32
60
|
return agents
|
|
33
61
|
}
|
|
34
62
|
|
|
@@ -37,6 +65,9 @@ function loadFromDir(
|
|
|
37
65
|
dir: string,
|
|
38
66
|
agents: Map<string, AgentConfig>,
|
|
39
67
|
source: "project" | "global",
|
|
68
|
+
strict: boolean,
|
|
69
|
+
warnings: WarningState,
|
|
70
|
+
skippedOverrides: Set<string>,
|
|
40
71
|
): void {
|
|
41
72
|
if (!existsSync(dir)) return
|
|
42
73
|
|
|
@@ -49,16 +80,15 @@ function loadFromDir(
|
|
|
49
80
|
|
|
50
81
|
for (const file of files) {
|
|
51
82
|
const name = basename(file, ".md")
|
|
83
|
+
const path = join(dir, file)
|
|
52
84
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
} catch {
|
|
85
|
+
const parsed = readAgentFile(path, strict, warnings)
|
|
86
|
+
if (!parsed) {
|
|
87
|
+
skippedOverrides.add(name)
|
|
57
88
|
continue
|
|
58
89
|
}
|
|
59
|
-
|
|
60
|
-
const { frontmatter: fm, body } =
|
|
61
|
-
parseFrontmatter<Record<string, unknown>>(content)
|
|
90
|
+
skippedOverrides.delete(name)
|
|
91
|
+
const { frontmatter: fm, body } = parsed
|
|
62
92
|
|
|
63
93
|
const { builtinToolNames, extSelectors } = parseToolsField(fm.tools)
|
|
64
94
|
|
|
@@ -97,10 +127,54 @@ function loadFromDir(
|
|
|
97
127
|
isolation: fm.isolation === "worktree" ? "worktree" : undefined,
|
|
98
128
|
enabled: fm.enabled !== false, // default true; explicitly false disables
|
|
99
129
|
source,
|
|
130
|
+
sourcePath: path,
|
|
100
131
|
})
|
|
101
132
|
}
|
|
102
133
|
}
|
|
103
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Read and parse one agent file, or warn and return undefined for the caller to
|
|
137
|
+
* skip. Under strict mode the same failure aborts startup while naming the file.
|
|
138
|
+
*/
|
|
139
|
+
function readAgentFile(
|
|
140
|
+
path: string,
|
|
141
|
+
strict: boolean,
|
|
142
|
+
warnings: WarningState,
|
|
143
|
+
): { frontmatter: Record<string, unknown>; body: string } | undefined {
|
|
144
|
+
try {
|
|
145
|
+
return parseFrontmatter<Record<string, unknown>>(
|
|
146
|
+
readFileSync(path, "utf-8"),
|
|
147
|
+
)
|
|
148
|
+
} catch (err) {
|
|
149
|
+
const reason = err instanceof Error ? err.message : String(err)
|
|
150
|
+
if (strict) throw new Error(`${path}: ${reason}`)
|
|
151
|
+
warnIfNew(`Skipping agent file ${path}: ${reason}`, warnings)
|
|
152
|
+
return undefined
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Warn when a broken higher-priority file exposes an earlier definition. */
|
|
157
|
+
function warnSkippedOverride(
|
|
158
|
+
name: string,
|
|
159
|
+
agents: Map<string, AgentConfig>,
|
|
160
|
+
warnings: WarningState,
|
|
161
|
+
): void {
|
|
162
|
+
const surviving = agents.get(name)
|
|
163
|
+
if (!surviving?.sourcePath || surviving.enabled === false) return
|
|
164
|
+
warnIfNew(
|
|
165
|
+
`Agent "${name}" now loads from ${surviving.sourcePath} instead`,
|
|
166
|
+
warnings,
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Warn once while an error is unchanged, but report it again after recovery. */
|
|
171
|
+
function warnIfNew(message: string, warnings: WarningState): void {
|
|
172
|
+
if (warnings.current.has(message)) return
|
|
173
|
+
warnings.current.add(message)
|
|
174
|
+
if (warnings.previous.has(message)) return
|
|
175
|
+
console.warn(`[pi-subagents] ${message}`)
|
|
176
|
+
}
|
|
177
|
+
|
|
104
178
|
// ---- Field parsers ----
|
|
105
179
|
// All follow the same convention: omitted → default, "none"/empty → nothing, value → exact.
|
|
106
180
|
|
package/src/index.ts
CHANGED
|
@@ -83,6 +83,7 @@ import { SubagentScheduler } from "./schedule.js"
|
|
|
83
83
|
import { resolveStorePath, ScheduleStore } from "./schedule-store.js"
|
|
84
84
|
import {
|
|
85
85
|
applyAndEmitLoaded,
|
|
86
|
+
loadSettings,
|
|
86
87
|
type SubagentsSettings,
|
|
87
88
|
saveAndEmitChanged,
|
|
88
89
|
type ToolDescriptionMode,
|
|
@@ -426,14 +427,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
426
427
|
},
|
|
427
428
|
)
|
|
428
429
|
|
|
430
|
+
// This setting controls the initial load, which runs before the normal settings
|
|
431
|
+
// application below. Later per-call reloads deliberately remain tolerant.
|
|
432
|
+
let strictAgentFiles = loadSettings(process.cwd()).strictAgentFiles === true
|
|
433
|
+
|
|
429
434
|
/** Reload agents from project/global custom agent dirs and merge with defaults (called on init and each Agent invocation). */
|
|
430
|
-
const reloadCustomAgents = () => {
|
|
431
|
-
const userAgents = loadCustomAgents(process.cwd())
|
|
435
|
+
const reloadCustomAgents = (strict = false) => {
|
|
436
|
+
const userAgents = loadCustomAgents(process.cwd(), strict)
|
|
432
437
|
registerAgents(userAgents)
|
|
433
438
|
}
|
|
434
439
|
|
|
435
|
-
// Initial load
|
|
436
|
-
reloadCustomAgents()
|
|
440
|
+
// Initial load — the only strict one.
|
|
441
|
+
reloadCustomAgents(strictAgentFiles)
|
|
437
442
|
|
|
438
443
|
// ---- Agent activity tracking + widget ----
|
|
439
444
|
const agentActivity = new Map<string, AgentActivity>()
|
|
@@ -990,6 +995,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
990
995
|
setDefaultJoinMode,
|
|
991
996
|
setSchedulingEnabled,
|
|
992
997
|
setScopeModels: setScopeModelsEnabled,
|
|
998
|
+
setStrictAgentFiles: (enabled) => {
|
|
999
|
+
strictAgentFiles = enabled
|
|
1000
|
+
},
|
|
993
1001
|
setDisableDefaultAgents: setDisableDefaultAgents,
|
|
994
1002
|
setToolDescriptionMode: setToolDescriptionMode,
|
|
995
1003
|
setFleetView: setFleetViewEnabled,
|
|
@@ -1233,12 +1241,12 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1233
1241
|
)
|
|
1234
1242
|
},
|
|
1235
1243
|
|
|
1236
|
-
renderResult(result, { expanded, isPartial }, theme) {
|
|
1244
|
+
renderResult(result, { expanded, isPartial }, theme, renderContext) {
|
|
1245
|
+
const resultText =
|
|
1246
|
+
result.content[0]?.type === "text" ? result.content[0].text : ""
|
|
1237
1247
|
const details = result.details as AgentDetails | undefined
|
|
1238
|
-
if (!details) {
|
|
1239
|
-
|
|
1240
|
-
result.content[0]?.type === "text" ? result.content[0].text : ""
|
|
1241
|
-
return new Text(text, 0, 0)
|
|
1248
|
+
if (renderContext?.isError || !details?.status) {
|
|
1249
|
+
return new Text(resultText, 0, 0)
|
|
1242
1250
|
}
|
|
1243
1251
|
|
|
1244
1252
|
// Helper: build "haiku · thinking: high · ↻5≤30 · 3 tool uses · 33.8k tokens" stats string
|
|
@@ -1293,8 +1301,6 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1293
1301
|
line += " " + theme.fg("dim", "·") + " " + theme.fg("dim", duration)
|
|
1294
1302
|
|
|
1295
1303
|
if (expanded) {
|
|
1296
|
-
const resultText =
|
|
1297
|
-
result.content[0]?.type === "text" ? result.content[0].text : ""
|
|
1298
1304
|
if (resultText) {
|
|
1299
1305
|
const lines = resultText.split("\n").slice(0, 50)
|
|
1300
1306
|
for (const l of lines) {
|
|
@@ -1324,6 +1330,10 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1324
1330
|
return new Text(line, 0, 0)
|
|
1325
1331
|
}
|
|
1326
1332
|
|
|
1333
|
+
if (details.status !== "error" && details.status !== "aborted") {
|
|
1334
|
+
return new Text(resultText, 0, 0)
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1327
1337
|
// ---- Error / Aborted (hard max_turns) ----
|
|
1328
1338
|
const s = stats(details)
|
|
1329
1339
|
let line = theme.fg("error", "✗") + (s ? " " + s : "")
|
|
@@ -1590,23 +1600,22 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1590
1600
|
}
|
|
1591
1601
|
}
|
|
1592
1602
|
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
}
|
|
1603
|
+
// A throw here means the agent never started. Let it out: Pi marks a
|
|
1604
|
+
// tool call failed only when execute throws, while a returned message
|
|
1605
|
+
// reads to the model as a subagent that ran and reported this.
|
|
1606
|
+
id = manager.spawn(pi, ctx, subagentType, params.prompt, {
|
|
1607
|
+
description: params.description,
|
|
1608
|
+
model,
|
|
1609
|
+
maxTurns: effectiveMaxTurns,
|
|
1610
|
+
isolated,
|
|
1611
|
+
inheritContext,
|
|
1612
|
+
thinkingLevel: thinking,
|
|
1613
|
+
isBackground: true,
|
|
1614
|
+
isolation,
|
|
1615
|
+
invocation: agentInvocation,
|
|
1616
|
+
rootSessionId: ctx.sessionManager.getSessionId(),
|
|
1617
|
+
...bgCallbacks,
|
|
1618
|
+
})
|
|
1610
1619
|
|
|
1611
1620
|
// Set output file + join mode synchronously after spawn, before the
|
|
1612
1621
|
// event loop yields — onSessionCreated is async so this is safe.
|
|
@@ -1766,18 +1775,15 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
1766
1775
|
},
|
|
1767
1776
|
)
|
|
1768
1777
|
record = fgResult.record
|
|
1769
|
-
}
|
|
1778
|
+
} finally {
|
|
1779
|
+
// A startup throw propagates as a failed tool call without leaving
|
|
1780
|
+
// the spinner running or a finished agent in the widget.
|
|
1770
1781
|
clearInterval(spinnerInterval)
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
// Clean up foreground agent from widget
|
|
1777
|
-
if (fgId) {
|
|
1778
|
-
agentActivity.delete(fgId)
|
|
1779
|
-
widget.markFinished(fgId)
|
|
1780
|
-
fleet.onAgentFinished(fgId)
|
|
1782
|
+
if (fgId) {
|
|
1783
|
+
agentActivity.delete(fgId)
|
|
1784
|
+
widget.markFinished(fgId)
|
|
1785
|
+
fleet.onAgentFinished(fgId)
|
|
1786
|
+
}
|
|
1781
1787
|
}
|
|
1782
1788
|
|
|
1783
1789
|
// Get final token count
|
|
@@ -2673,6 +2679,7 @@ ${systemPrompt}
|
|
|
2673
2679
|
defaultJoinMode: getDefaultJoinMode(),
|
|
2674
2680
|
schedulingEnabled: isSchedulingEnabled(),
|
|
2675
2681
|
scopeModels: isScopeModelsEnabled(),
|
|
2682
|
+
strictAgentFiles,
|
|
2676
2683
|
disableDefaultAgents: isDefaultsDisabled(),
|
|
2677
2684
|
toolDescriptionMode: getToolDescriptionMode(),
|
|
2678
2685
|
fleetView: isFleetViewEnabled(),
|
|
@@ -2762,6 +2769,14 @@ ${systemPrompt}
|
|
|
2762
2769
|
currentValue: isScopeModelsEnabled() ? "on" : "off",
|
|
2763
2770
|
values: ["on", "off"],
|
|
2764
2771
|
},
|
|
2772
|
+
{
|
|
2773
|
+
id: "strictAgentFiles",
|
|
2774
|
+
label: "Strict agent files",
|
|
2775
|
+
description:
|
|
2776
|
+
"Fail startup on an unreadable or unparseable agent .md instead of skipping it with a warning",
|
|
2777
|
+
currentValue: strictAgentFiles ? "on" : "off",
|
|
2778
|
+
values: ["on", "off"],
|
|
2779
|
+
},
|
|
2765
2780
|
{
|
|
2766
2781
|
id: "disableDefaultAgents",
|
|
2767
2782
|
label: "Disable defaults",
|
|
@@ -2867,6 +2882,13 @@ ${systemPrompt}
|
|
|
2867
2882
|
const enabled = value === "on"
|
|
2868
2883
|
setScopeModelsEnabled(enabled)
|
|
2869
2884
|
notifyApplied(ctx, `Scope models ${enabled ? "enabled" : "disabled"}`)
|
|
2885
|
+
} else if (id === "strictAgentFiles") {
|
|
2886
|
+
const enabled = value === "on"
|
|
2887
|
+
strictAgentFiles = enabled
|
|
2888
|
+
notifyApplied(
|
|
2889
|
+
ctx,
|
|
2890
|
+
`Strict agent files ${enabled ? "enabled" : "disabled"}. Takes effect on next pi session.`,
|
|
2891
|
+
)
|
|
2870
2892
|
} else if (id === "disableDefaultAgents") {
|
|
2871
2893
|
const enabled = value === "on"
|
|
2872
2894
|
setDisableDefaultAgents(enabled)
|
package/src/nested-tools.ts
CHANGED
|
@@ -399,47 +399,38 @@ export function createNestedSubagentTools(
|
|
|
399
399
|
// one earlier would silently give a grandchild the wrong worktree base, the
|
|
400
400
|
// wrong conversation under inherit_context, and the wrong inherited model.
|
|
401
401
|
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const id = context.manager.spawn(
|
|
408
|
-
context.pi,
|
|
409
|
-
ctx,
|
|
410
|
-
resolvedType,
|
|
411
|
-
params.prompt,
|
|
412
|
-
{
|
|
413
|
-
...options,
|
|
414
|
-
isBackground: true,
|
|
415
|
-
},
|
|
416
|
-
)
|
|
417
|
-
// Synchronous, before the event loop yields — onSessionCreated fires
|
|
418
|
-
// asynchronously inside runAgent, so the file is attached in time.
|
|
419
|
-
attachTranscript(id)
|
|
420
|
-
return textResult(
|
|
421
|
-
`Nested agent started in background. Agent ID: ${id}`,
|
|
422
|
-
)
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const { record } = await context.manager.spawnAndWait(
|
|
402
|
+
// Startup throws mean no child ran. Let them propagate so Pi marks the
|
|
403
|
+
// nested Agent tool call failed instead of presenting the message as a
|
|
404
|
+
// successful child result.
|
|
405
|
+
if (invocation.runInBackground) {
|
|
406
|
+
const id = context.manager.spawn(
|
|
426
407
|
context.pi,
|
|
427
408
|
ctx,
|
|
428
409
|
resolvedType,
|
|
429
410
|
params.prompt,
|
|
430
|
-
{
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
formatRecord(record, "inline"),
|
|
435
|
-
record.status === "error",
|
|
436
|
-
)
|
|
437
|
-
} catch (err) {
|
|
438
|
-
return textResult(
|
|
439
|
-
err instanceof Error ? err.message : String(err),
|
|
440
|
-
true,
|
|
411
|
+
{
|
|
412
|
+
...options,
|
|
413
|
+
isBackground: true,
|
|
414
|
+
},
|
|
441
415
|
)
|
|
416
|
+
// Synchronous, before the event loop yields — onSessionCreated fires
|
|
417
|
+
// asynchronously inside runAgent, so the file is attached in time.
|
|
418
|
+
attachTranscript(id)
|
|
419
|
+
return textResult(`Nested agent started in background. Agent ID: ${id}`)
|
|
442
420
|
}
|
|
421
|
+
|
|
422
|
+
const { record } = await context.manager.spawnAndWait(
|
|
423
|
+
context.pi,
|
|
424
|
+
ctx,
|
|
425
|
+
resolvedType,
|
|
426
|
+
params.prompt,
|
|
427
|
+
{ ...options, signal },
|
|
428
|
+
attachTranscript,
|
|
429
|
+
)
|
|
430
|
+
return textResult(
|
|
431
|
+
formatRecord(record, "inline"),
|
|
432
|
+
record.status === "error",
|
|
433
|
+
)
|
|
443
434
|
},
|
|
444
435
|
})
|
|
445
436
|
|
package/src/prompts.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface PromptExtras {
|
|
|
10
10
|
memoryBlock?: string
|
|
11
11
|
/** Preloaded skill contents to inject. */
|
|
12
12
|
skillBlocks?: { name: string; content: string }[]
|
|
13
|
+
/** Original checkout path when cwd is an isolated worktree copy. */
|
|
14
|
+
worktreeBase?: string
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
/**
|
|
@@ -55,6 +57,12 @@ Platform: ${env.platform}`
|
|
|
55
57
|
}
|
|
56
58
|
const extrasSuffix =
|
|
57
59
|
extraSections.length > 0 ? "\n\n" + extraSections.join("\n") : ""
|
|
60
|
+
const worktreeSection = extras?.worktreeBase
|
|
61
|
+
? `\n\n<worktree_isolation>
|
|
62
|
+
Your working directory is an isolated git worktree copy of ${extras.worktreeBase}.
|
|
63
|
+
Work only inside it — never in ${extras.worktreeBase}, even if other instructions name that path as your working directory.
|
|
64
|
+
</worktree_isolation>`
|
|
65
|
+
: ""
|
|
58
66
|
|
|
59
67
|
if (config.promptMode === "append") {
|
|
60
68
|
const identity = parentSystemPrompt || genericBase
|
|
@@ -88,6 +96,7 @@ You are operating as a sub-agent invoked to handle a specific task.
|
|
|
88
96
|
"\n\n" +
|
|
89
97
|
activeAgentTag +
|
|
90
98
|
envBlock +
|
|
99
|
+
worktreeSection +
|
|
91
100
|
customSection +
|
|
92
101
|
extrasSuffix
|
|
93
102
|
)
|
|
@@ -100,7 +109,12 @@ You have been invoked to handle a specific task autonomously.
|
|
|
100
109
|
${envBlock}`
|
|
101
110
|
|
|
102
111
|
return (
|
|
103
|
-
activeAgentTag +
|
|
112
|
+
activeAgentTag +
|
|
113
|
+
replaceHeader +
|
|
114
|
+
worktreeSection +
|
|
115
|
+
"\n\n" +
|
|
116
|
+
config.systemPrompt +
|
|
117
|
+
extrasSuffix
|
|
104
118
|
)
|
|
105
119
|
}
|
|
106
120
|
|
package/src/settings.ts
CHANGED
|
@@ -49,6 +49,12 @@ export interface SubagentsSettings {
|
|
|
49
49
|
* against. Defaults to false: subagents may use any model.
|
|
50
50
|
*/
|
|
51
51
|
scopeModels?: boolean
|
|
52
|
+
/**
|
|
53
|
+
* When true, an unreadable or unparseable agent `.md` aborts extension load
|
|
54
|
+
* instead of being skipped with a warning. This applies only during startup;
|
|
55
|
+
* later per-call reloads remain tolerant. Defaults to false.
|
|
56
|
+
*/
|
|
57
|
+
strictAgentFiles?: boolean
|
|
52
58
|
/**
|
|
53
59
|
* When true, the three built-in default agents (general-purpose, Explore, Plan)
|
|
54
60
|
* are not registered at startup. User-defined agents from project/global custom
|
|
@@ -129,6 +135,7 @@ export interface SettingsAppliers {
|
|
|
129
135
|
setDefaultJoinMode: (mode: JoinMode) => void
|
|
130
136
|
setSchedulingEnabled: (b: boolean) => void
|
|
131
137
|
setScopeModels: (enabled: boolean) => void
|
|
138
|
+
setStrictAgentFiles: (b: boolean) => void
|
|
132
139
|
setDisableDefaultAgents: (b: boolean) => void
|
|
133
140
|
setToolDescriptionMode: (mode: ToolDescriptionMode) => void
|
|
134
141
|
setFleetView: (b: boolean) => void
|
|
@@ -207,6 +214,9 @@ function sanitize(raw: unknown): SubagentsSettings {
|
|
|
207
214
|
if (typeof r.scopeModels === "boolean") {
|
|
208
215
|
out.scopeModels = r.scopeModels
|
|
209
216
|
}
|
|
217
|
+
if (typeof r.strictAgentFiles === "boolean") {
|
|
218
|
+
out.strictAgentFiles = r.strictAgentFiles
|
|
219
|
+
}
|
|
210
220
|
if (typeof r.disableDefaultAgents === "boolean") {
|
|
211
221
|
out.disableDefaultAgents = r.disableDefaultAgents
|
|
212
222
|
}
|
|
@@ -315,6 +325,8 @@ export function applySettings(
|
|
|
315
325
|
if (typeof s.schedulingEnabled === "boolean")
|
|
316
326
|
appliers.setSchedulingEnabled(s.schedulingEnabled)
|
|
317
327
|
if (typeof s.scopeModels === "boolean") appliers.setScopeModels(s.scopeModels)
|
|
328
|
+
if (typeof s.strictAgentFiles === "boolean")
|
|
329
|
+
appliers.setStrictAgentFiles(s.strictAgentFiles)
|
|
318
330
|
if (typeof s.disableDefaultAgents === "boolean")
|
|
319
331
|
appliers.setDisableDefaultAgents(s.disableDefaultAgents)
|
|
320
332
|
if (s.toolDescriptionMode)
|
package/src/types.ts
CHANGED
|
@@ -77,6 +77,8 @@ export interface AgentConfig {
|
|
|
77
77
|
enabled?: boolean
|
|
78
78
|
/** Where this agent was loaded from */
|
|
79
79
|
source?: "default" | "project" | "global"
|
|
80
|
+
/** Path of the .md it was loaded from. Unset for embedded defaults. */
|
|
81
|
+
sourcePath?: string
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
export type JoinMode = "async" | "group" | "smart"
|