@hheei/pi-subagents 0.2.3 → 0.3.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 +32 -17
- package/dist/agent-resolver.d.ts +9 -0
- package/dist/agent-resolver.js +278 -51
- package/dist/bridge-client.d.ts +5 -0
- package/dist/bridge-client.js +6 -0
- package/dist/builtin-agents.js +50 -37
- package/dist/child-bridge.d.ts +0 -8
- package/dist/child-bridge.js +60 -43
- package/dist/child-control.d.ts +5 -3
- package/dist/child-control.js +176 -35
- package/dist/commands.d.ts +1 -4
- package/dist/commands.js +1 -52
- package/dist/domain.d.ts +12 -12
- package/dist/domain.js +2 -1
- package/dist/extension.d.ts +5 -3
- package/dist/extension.js +42 -44
- package/dist/host-adapter.js +1 -1
- package/dist/info.d.ts +9 -0
- package/dist/info.js +54 -0
- package/dist/launch-spec.d.ts +0 -1
- package/dist/launch-spec.js +38 -18
- package/dist/manager.d.ts +4 -2
- package/dist/manager.js +299 -114
- package/dist/registry.js +19 -5
- package/dist/rpc-events.d.ts +1 -1
- package/dist/rpc-events.js +0 -1
- package/dist/runtime.js +8 -0
- package/dist/session-bootstrap.d.ts +2 -1
- package/dist/session-bootstrap.js +22 -14
- package/dist/state.d.ts +13 -0
- package/dist/state.js +59 -16
- package/dist/status.d.ts +14 -0
- package/dist/status.js +36 -0
- package/dist/tool-activation.d.ts +3 -4
- package/dist/tool-activation.js +13 -215
- package/dist/tool-presentation.d.ts +17 -0
- package/dist/tool-presentation.js +160 -0
- package/dist/tools.d.ts +6 -1
- package/dist/tools.js +133 -180
- package/package.json +6 -6
- package/dist/session-fork.d.ts +0 -16
- package/dist/session-fork.js +0 -67
- package/dist/widget.d.ts +0 -12
- package/dist/widget.js +0 -149
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ file, its own runtime, and its own durable identity.
|
|
|
5
5
|
|
|
6
6
|
The package is a single concrete extension with one `pi.extensions` entry
|
|
7
7
|
(`dist/extension.js`). The same entry runs on both sides: the parent branch registers the
|
|
8
|
-
model-facing tools, commands, and
|
|
8
|
+
model-facing tools, commands, compact footer status, and info entries; the child branch registers only the reporting
|
|
9
9
|
bridge and session-leave reporting.
|
|
10
10
|
|
|
11
11
|
## Parent tools
|
|
@@ -15,16 +15,20 @@ children and `/subagents stop [id]` to stop one without conflicting with other e
|
|
|
15
15
|
|
|
16
16
|
| Tool | Purpose |
|
|
17
17
|
| --- | --- |
|
|
18
|
-
| `
|
|
19
|
-
| `
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `list_agents({})` | List available interactive agent definitions and running subagents owned by this parent session. |
|
|
23
|
-
| `stop_agent({ id })` | Persist a stopped intent, then end the runtime: a background child is killed, a panel child has its panel closed and verified gone. Requires `subagent_enable`. |
|
|
18
|
+
| `agent_spawn({ task, agent, cwd?, title? })` | Spawn an independent background subagent task. Runs in a new Herdr tab (or cmux surface) targeting the parent's workspace when a host is available, and headless in the background otherwise. **Prefer reusing existing subagents** via `agent_send` for related/follow-up work to maintain context and maximize prompt cache and memory efficiency. Tool description lists available agent names and roles, excluding disabled or hidden definitions. Deferred by default; discover via `tool_search({ query: "agent" })`. Do not poll for completion; results return automatically. |
|
|
19
|
+
| `agent_send({ id, message, mode? })` | Send `steer`, `follow_up`, or `auto` input to a specific child. **Primary tool for assigning follow-up or functionally related tasks** to existing subagents: idle, blocked, done, or errored children resume their existing session automatically with historical context intact. Deferred by default; discover via `tool_search({ query: "agent" })`. |
|
|
20
|
+
| `agent_get({ id, keys? })` | Inspect one subagent by id. `keys` is a comma-separated selection of `status` (default), `progress` (default), and `diagnostic`; unknown keys fail. The view never returns the child's task parameters or terminal result, which arrive automatically as reports. From the second successful inspection of the same child in this parent runtime, the result adds a short reminder that results return automatically and must not be polled. Deferred by default; discover via `tool_search({ query: "agent" })`. |
|
|
21
|
+
| `agent_stop({ id })` | Stop one subagent's runtime by id. The child keeps its session, so a later `agent_send` can resume it. Deferred by default; discover via `tool_search({ query: "agent" })`. |
|
|
24
22
|
|
|
25
23
|
### Unified Task Management
|
|
26
24
|
|
|
27
|
-
|
|
25
|
+
`agent_spawn` returns JSON `{ id, path }` on success. `path` is the absolute child session file path
|
|
26
|
+
assigned by Pi; its first write may still be pending. `agent_get` exposes the same `progress.path`.
|
|
27
|
+
`agent_send` and `agent_stop` return one confirmation
|
|
28
|
+
sentence. Failures return only their reason. Operation results omit session IDs, presentation, and
|
|
29
|
+
state snapshots; use `agent_get` for runtime inspection.
|
|
30
|
+
|
|
31
|
+
Subagent execution tasks register with ext-core's `TaskRegistry` when pi-ext-tools provides it, alongside background Bash jobs. Child reports arrive automatically; do not poll status to detect completion. Bash uses separate `bg_bash_*` tools. Deferred subagent tools are loaded via `tool_search`; activation supplies `subagent_guidance`. Both get tools return the same `status`, `progress`, and optional `diagnostic` envelope. Diagnostics describe blockers, failed turns, or uncertain runtime state.
|
|
28
32
|
|
|
29
33
|
## Child execution and reporting
|
|
30
34
|
|
|
@@ -35,20 +39,18 @@ Subagents communicate with the parent over a dedicated Unix domain socket bridge
|
|
|
35
39
|
- **Blockers & Decisions**: The child registers `contact_parent({ message, reason? })` strictly for reporting when it is blocked or urgently requires a parent decision midway (`reason` defaults to `'blocked'`). Blocked reports wake the parent immediately.
|
|
36
40
|
- **Automatic Retry**: If a subagent encounters a transient error, the harness allows one automatic retry before marking the task failed or delivering a blocked notification.
|
|
37
41
|
- **Panel Failure Auto-Close**: When a subagent running in a panel encounters a fatal error or reports a blocker, the manager starts a 15-second countdown after notifying the parent, automatically closing the panel tab to avoid workspace clutter unless new instructions are dispatched.
|
|
38
|
-
|
|
42
|
+
## Compact status and reports
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
Running children use the native Pi status line: `🤖 #21 #22 #23`. With pi-status installed, this appears at the bottom right before the existing TODO segment, separated by ` ‧ `. Display shorthand `#21` refers to tool ID `agent-21`. No running children means no agent segment; no multi-line parent widget is mounted.
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
Confirmed done, error, and blocked reports appear once as `subagent-info` transcript entries. Collapsed entries show the outcome, child ID, and agent name; expand to read the complete report. They survive resume and do not enter model context. The original automatic `subagent-report` still reaches the model, with its duplicate transcript display hidden. Transient errors remain subject to settlement and retry checks before reporting.
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
- **`done`** (green checkmark `✓`): Succeeded and automatically reported final assistant text back to the parent.
|
|
46
|
-
- **`blocked`** (red/dim `!`): Failed, interrupted, stopped, or explicitly blocked via `contact_parent`.
|
|
48
|
+
Use `/subagents` for complete status and management. `agent_send` can resume a done, blocked, errored, or stopped child with its session context intact; retrying does not guarantee the underlying failure is resolved.
|
|
47
49
|
|
|
48
50
|
## Host Integration and Isolation
|
|
49
51
|
|
|
50
52
|
- **Herdr Workspace Affinity**: When spawning in Herdr, tabs are explicitly created in the parent process's current workspace (`--workspace <WORKSPACE_ID>`), preventing child tabs from jumping to whichever workspace happens to have user focus.
|
|
51
|
-
- **Hindsight Memory Isolation**:
|
|
53
|
+
- **Hindsight Memory Isolation**: `PI_HINDSIGHT_DISABLE=1` is set in the child environment and memory auto-registration/recall/writeback are disabled. The child bridge blocks all `mcp__hindsight__*` calls (including nested codemode calls and future server tools), even if that server is configured in a file. Memory recall prompt blocks are stripped. This boundary applies to the reserved `hindsight` server name, not arbitrary external servers registered under other names.
|
|
52
54
|
|
|
53
55
|
## Agent definitions
|
|
54
56
|
|
|
@@ -56,11 +58,24 @@ Agent Markdown files are discovered in this order, most specific first, by `name
|
|
|
56
58
|
|
|
57
59
|
```text
|
|
58
60
|
<cwd>/.pi/agents/*.md
|
|
59
|
-
<cwd>/.
|
|
61
|
+
<cwd>/.pi/agents/*.md
|
|
60
62
|
~/.pi/agent/agents/*.md
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
Frontmatter is parsed with Pi's `parseFrontmatter` and validated against this package's schema. Supported fields: `name`, `display_name`, `description`, `hidden`, `model`, `thinking`, `tools`, `exclude_tools`, `extensions`, `skills`, `
|
|
65
|
+
Frontmatter is parsed with Pi's `parseFrontmatter` and validated against this package's schema. Supported fields: `name`, `display_name`, `description`, `hidden`, `model`, `thinking`, `tools`, `exclude_tools`, `extensions`, `skills`, `mcp`, `codemode`.
|
|
66
|
+
|
|
67
|
+
### Unified Resource Selection (Pi Semantics)
|
|
68
|
+
|
|
69
|
+
`tools`, `skills`, and `mcp` follow Pi's unified selection semantics:
|
|
70
|
+
|
|
71
|
+
- **`boolean` toggle**:
|
|
72
|
+
- `true` (or omitted): Full inheritance from host defaults.
|
|
73
|
+
- `false`: Completely disabled (passes `--no-mcp`, `--no-skills`, `--no-tools` respectively; `--no-mcp` physically shuts down the MCP subsystem so no server processes or network connections start).
|
|
74
|
+
- MCP selection (`mcp: [server1]`) acts at the tool exposure and execution boundary via `--tools` / `--exclude-tools` patterns and child-bridge tool call gating. Non-whitelisted or excluded server tools cannot be viewed or called.
|
|
75
|
+
- **Exact list (Whitelist / Replace)**:
|
|
76
|
+
- `[item1, item2]`: Activates only the named entries.
|
|
77
|
+
- **Incremental modifiers (`+` / `-`)**:
|
|
78
|
+
- `["+itemA", "-itemB"]`: Adds or excludes entries relative to base defaults. Exact names and modifiers cannot be mixed.
|
|
64
79
|
|
|
65
80
|
### Built-in Agents
|
|
66
81
|
|
package/dist/agent-resolver.d.ts
CHANGED
|
@@ -47,6 +47,15 @@ export interface DiscoveredAgent {
|
|
|
47
47
|
readonly body: string;
|
|
48
48
|
readonly enabled?: boolean;
|
|
49
49
|
}
|
|
50
|
+
export type PiSelectionType = "all" | "none" | "whitelist" | "modifier";
|
|
51
|
+
export interface ParsedPiSelection {
|
|
52
|
+
readonly type: PiSelectionType;
|
|
53
|
+
readonly names?: readonly string[];
|
|
54
|
+
readonly modifiers?: readonly string[];
|
|
55
|
+
readonly add?: readonly string[];
|
|
56
|
+
readonly remove?: readonly string[];
|
|
57
|
+
}
|
|
58
|
+
export declare function parsePiSelection(value: unknown, field: string, path: string): ParsedPiSelection;
|
|
50
59
|
export declare function discoverAgents(cwd: string, homeDirectory?: string): Promise<readonly DiscoveredAgent[]>;
|
|
51
60
|
/**
|
|
52
61
|
* Resolves one agent definition into an immutable, non-secret policy snapshot.
|
package/dist/agent-resolver.js
CHANGED
|
@@ -22,8 +22,8 @@ const SUPPORTED_FIELDS = {
|
|
|
22
22
|
exclude_tools: true,
|
|
23
23
|
extensions: true,
|
|
24
24
|
skills: true,
|
|
25
|
-
interactive: true,
|
|
26
25
|
codemode: true,
|
|
26
|
+
mcp: true,
|
|
27
27
|
};
|
|
28
28
|
/** Detects whether codemode is configured with mode: "only" in settings or environment. */
|
|
29
29
|
export function detectCodemodeOnly(cwd, homeDirectory) {
|
|
@@ -35,18 +35,27 @@ export function detectCodemodeOnly(cwd, homeDirectory) {
|
|
|
35
35
|
: homeDirectory !== undefined
|
|
36
36
|
? join(homeDirectory, ".pi", "agent")
|
|
37
37
|
: getAgentDir();
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
38
|
+
// Project settings (.pi/settings.json in cwd) take precedence over user settings (.pi/agent/settings.json)
|
|
39
|
+
if (cwd) {
|
|
40
|
+
const projectSettings = join(cwd, ".pi", "settings.json");
|
|
41
|
+
if (existsSync(projectSettings)) {
|
|
42
|
+
const content = JSON.parse(readFileSync(projectSettings, "utf8"));
|
|
43
|
+
if (isRecord(content) &&
|
|
44
|
+
isRecord(content.codemode) &&
|
|
45
|
+
typeof content.codemode.mode === "string") {
|
|
46
|
+
return content.codemode.mode === "only";
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
+
const userSettings = join(agentDir, "settings.json");
|
|
51
|
+
if (existsSync(userSettings)) {
|
|
52
|
+
const content = JSON.parse(readFileSync(userSettings, "utf8"));
|
|
53
|
+
if (isRecord(content) &&
|
|
54
|
+
isRecord(content.codemode) &&
|
|
55
|
+
typeof content.codemode.mode === "string") {
|
|
56
|
+
return content.codemode.mode === "only";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
50
59
|
}
|
|
51
60
|
catch {
|
|
52
61
|
// Ignore filesystem or JSON parse failures
|
|
@@ -104,6 +113,161 @@ function resolveResourcePaths(value, field, path, homeDirectory) {
|
|
|
104
113
|
return resolve(dirname(path), trimmed);
|
|
105
114
|
});
|
|
106
115
|
}
|
|
116
|
+
const DEFAULT_PI_TOOLS = ["read", "bash", "edit", "write"];
|
|
117
|
+
function isModifier(entry) {
|
|
118
|
+
return entry.startsWith("+") || entry.startsWith("-");
|
|
119
|
+
}
|
|
120
|
+
export function parsePiSelection(value, field, path) {
|
|
121
|
+
if (value === undefined || value === true || value === "all") {
|
|
122
|
+
return { type: "all" };
|
|
123
|
+
}
|
|
124
|
+
if (value === false || value === "none") {
|
|
125
|
+
return { type: "none" };
|
|
126
|
+
}
|
|
127
|
+
const raw = typeof value === "string" ? value.split(",") : value;
|
|
128
|
+
if (!Array.isArray(raw)) {
|
|
129
|
+
throw readableError(path, `${field} must be a boolean, string, or string array`);
|
|
130
|
+
}
|
|
131
|
+
const entries = raw.map((item) => requiredString(item, field, path));
|
|
132
|
+
if (entries.length === 0) {
|
|
133
|
+
return { type: "none" };
|
|
134
|
+
}
|
|
135
|
+
const modifiers = entries.filter(isModifier);
|
|
136
|
+
if (modifiers.length > 0) {
|
|
137
|
+
if (modifiers.length < entries.length) {
|
|
138
|
+
throw readableError(path, `cannot mix exact names with +name or -name entries in ${field}`);
|
|
139
|
+
}
|
|
140
|
+
const add = [];
|
|
141
|
+
const remove = [];
|
|
142
|
+
for (const entry of modifiers) {
|
|
143
|
+
if (entry.includes("*")) {
|
|
144
|
+
throw readableError(path, `+name and -name entries take exact names, not patterns: ${entry}`);
|
|
145
|
+
}
|
|
146
|
+
const name = entry.slice(1).trim();
|
|
147
|
+
if (name === "") {
|
|
148
|
+
throw readableError(path, `${field} entry has empty modifier name`);
|
|
149
|
+
}
|
|
150
|
+
if (entry.startsWith("+")) {
|
|
151
|
+
if (!add.includes(name))
|
|
152
|
+
add.push(name);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
if (!remove.includes(name))
|
|
156
|
+
remove.push(name);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
type: "modifier",
|
|
161
|
+
modifiers: Object.freeze(entries),
|
|
162
|
+
add: Object.freeze(add),
|
|
163
|
+
remove: Object.freeze(remove),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (new Set(entries).size !== entries.length) {
|
|
167
|
+
throw readableError(path, `${field} selects the same entry twice`);
|
|
168
|
+
}
|
|
169
|
+
return { type: "whitelist", names: Object.freeze(entries) };
|
|
170
|
+
}
|
|
171
|
+
function applyModifiers(base, modifiers) {
|
|
172
|
+
const result = [...base];
|
|
173
|
+
for (const entry of modifiers) {
|
|
174
|
+
const name = entry.slice(1).trim();
|
|
175
|
+
if (entry.startsWith("+")) {
|
|
176
|
+
if (!result.includes(name))
|
|
177
|
+
result.push(name);
|
|
178
|
+
}
|
|
179
|
+
else if (entry.startsWith("-")) {
|
|
180
|
+
const idx = result.indexOf(name);
|
|
181
|
+
if (idx !== -1)
|
|
182
|
+
result.splice(idx, 1);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
function mergeDiscoveredFrontmatter(builtinFm, userFm, path, inheritBaseDefaults) {
|
|
188
|
+
const merged = inheritBaseDefaults
|
|
189
|
+
? { ...builtinFm, ...userFm }
|
|
190
|
+
: { ...userFm };
|
|
191
|
+
if (userFm.tools !== undefined) {
|
|
192
|
+
const parsedUser = parsePiSelection(userFm.tools, "tools", path);
|
|
193
|
+
if (parsedUser.type === "modifier") {
|
|
194
|
+
const baseTools = stringList(builtinFm.tools, "tools", path);
|
|
195
|
+
const evaluatedTools = applyModifiers(baseTools, parsedUser.modifiers ?? []);
|
|
196
|
+
merged.tools = evaluatedTools;
|
|
197
|
+
const existing = Array.isArray(merged.exclude_tools)
|
|
198
|
+
? merged.exclude_tools
|
|
199
|
+
: typeof merged.exclude_tools === "string"
|
|
200
|
+
? [merged.exclude_tools]
|
|
201
|
+
: [];
|
|
202
|
+
const currentExcludes = existing.filter((e) => !evaluatedTools.includes(e));
|
|
203
|
+
const finalRemovals = (parsedUser.remove ?? []).filter((r) => !evaluatedTools.includes(r));
|
|
204
|
+
merged.exclude_tools = Array.from(new Set([...currentExcludes, ...finalRemovals]));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (userFm.skills !== undefined) {
|
|
208
|
+
const parsedUser = parsePiSelection(userFm.skills, "skills", path);
|
|
209
|
+
if (parsedUser.type === "modifier") {
|
|
210
|
+
if (Array.isArray(builtinFm.skills)) {
|
|
211
|
+
const baseSkills = stringList(builtinFm.skills, "skills", path);
|
|
212
|
+
merged.skills = applyModifiers(baseSkills, parsedUser.modifiers ?? []);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
merged.skills = userFm.skills;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (userFm.mcp !== undefined) {
|
|
220
|
+
const parsedUser = parsePiSelection(userFm.mcp, "mcp", path);
|
|
221
|
+
if (parsedUser.type === "modifier") {
|
|
222
|
+
if (Array.isArray(builtinFm.mcp)) {
|
|
223
|
+
const baseMcp = stringList(builtinFm.mcp, "mcp", path);
|
|
224
|
+
merged.mcp = applyModifiers(baseMcp, parsedUser.modifiers ?? []);
|
|
225
|
+
}
|
|
226
|
+
else if (builtinFm.mcp === false) {
|
|
227
|
+
const evaluated = applyModifiers([], parsedUser.modifiers ?? []);
|
|
228
|
+
merged.mcp = evaluated.length > 0 ? evaluated : false;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
merged.mcp = userFm.mcp;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return merged;
|
|
236
|
+
}
|
|
237
|
+
function mcpSelection(value, path) {
|
|
238
|
+
const parsed = parsePiSelection(value, "mcp", path);
|
|
239
|
+
if (parsed.type === "none") {
|
|
240
|
+
return Object.freeze({ enabled: false });
|
|
241
|
+
}
|
|
242
|
+
if (parsed.type === "all") {
|
|
243
|
+
return Object.freeze({ enabled: true });
|
|
244
|
+
}
|
|
245
|
+
if (parsed.type === "whitelist") {
|
|
246
|
+
const servers = parsed.names;
|
|
247
|
+
return Object.freeze({
|
|
248
|
+
enabled: true,
|
|
249
|
+
...(servers !== undefined ? { servers } : {}),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const excluded = [];
|
|
253
|
+
for (const entry of parsed.modifiers ?? []) {
|
|
254
|
+
const name = entry.slice(1).trim();
|
|
255
|
+
if (entry.startsWith("-")) {
|
|
256
|
+
if (!excluded.includes(name))
|
|
257
|
+
excluded.push(name);
|
|
258
|
+
}
|
|
259
|
+
else if (entry.startsWith("+")) {
|
|
260
|
+
const idx = excluded.indexOf(name);
|
|
261
|
+
if (idx !== -1)
|
|
262
|
+
excluded.splice(idx, 1);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const result = {
|
|
266
|
+
enabled: true,
|
|
267
|
+
...(excluded.length > 0 ? { excluded } : {}),
|
|
268
|
+
};
|
|
269
|
+
return Object.freeze(result);
|
|
270
|
+
}
|
|
107
271
|
async function resourceSelection(value, field, path, homeDirectory) {
|
|
108
272
|
if (value === undefined || value === true)
|
|
109
273
|
return { discovery: true, paths: [] };
|
|
@@ -119,9 +283,6 @@ async function resourceSelection(value, field, path, homeDirectory) {
|
|
|
119
283
|
}
|
|
120
284
|
return { discovery: true, paths };
|
|
121
285
|
}
|
|
122
|
-
/** `skills: all` is the default and `skills: none` the opt-out; a list is a whitelist. */
|
|
123
|
-
const SKILL_ALL = "all";
|
|
124
|
-
const SKILL_NONE = "none";
|
|
125
286
|
/**
|
|
126
287
|
* Skills are chosen as a whitelist, unlike extensions: a list means "exactly these", because
|
|
127
288
|
* inheriting every discovered skill is the thing a definition usually wants to narrow. A bare
|
|
@@ -129,32 +290,59 @@ const SKILL_NONE = "none";
|
|
|
129
290
|
* absolute path the child needs; anything path-shaped stays what it was, and a name the catalog
|
|
130
291
|
* does not know is dropped with a warning rather than silently matching nothing.
|
|
131
292
|
*/
|
|
132
|
-
async function skillSelection(value, path, homeDirectory, catalog
|
|
133
|
-
|
|
293
|
+
async function skillSelection(value, path, homeDirectory, catalog) {
|
|
294
|
+
const parsed = parsePiSelection(value, "skills", path);
|
|
295
|
+
if (parsed.type === "all") {
|
|
134
296
|
return { discovery: true, paths: [] };
|
|
135
297
|
}
|
|
136
|
-
if (
|
|
298
|
+
if (parsed.type === "none") {
|
|
137
299
|
return { discovery: false, paths: [] };
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
300
|
+
}
|
|
301
|
+
if (parsed.type === "whitelist") {
|
|
302
|
+
const paths = [];
|
|
303
|
+
for (const entry of parsed.names ?? []) {
|
|
304
|
+
if (LOCAL_PATH_PATTERN.test(entry)) {
|
|
305
|
+
const selected = resolve(dirname(path), expandHome(entry, homeDirectory));
|
|
306
|
+
const exists = await stat(selected).then(() => true, () => false);
|
|
307
|
+
if (!exists)
|
|
308
|
+
throw readableError(path, `skills entry does not exist: ${selected}`);
|
|
309
|
+
paths.push(selected);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (entry.includes(":") || entry.includes("/") || entry.includes("\\")) {
|
|
313
|
+
paths.push(entry);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
const match = catalog?.find((skill) => skill.name === entry);
|
|
317
|
+
if (match === undefined) {
|
|
318
|
+
// Pass uncatalogued skill name down to Pi CLI so Pi validates it and reports in the subagent panel
|
|
319
|
+
paths.push(entry);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
paths.push(match.path);
|
|
147
323
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
324
|
+
return { discovery: false, paths };
|
|
325
|
+
}
|
|
326
|
+
if (catalog === undefined || catalog.length === 0) {
|
|
327
|
+
const paths = [];
|
|
328
|
+
for (const entry of parsed.modifiers ?? []) {
|
|
329
|
+
if (entry.startsWith("+")) {
|
|
330
|
+
paths.push(entry.slice(1).trim());
|
|
331
|
+
}
|
|
151
332
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
333
|
+
return { discovery: false, paths };
|
|
334
|
+
}
|
|
335
|
+
const baseSkillNames = catalog.map((skill) => skill.name);
|
|
336
|
+
const activeSkillNames = applyModifiers(baseSkillNames, parsed.modifiers ?? []);
|
|
337
|
+
const paths = [];
|
|
338
|
+
for (const name of activeSkillNames) {
|
|
339
|
+
const match = catalog.find((skill) => skill.name === name);
|
|
340
|
+
if (match !== undefined) {
|
|
341
|
+
paths.push(match.path);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
paths.push(name);
|
|
156
345
|
}
|
|
157
|
-
paths.push(match.path);
|
|
158
346
|
}
|
|
159
347
|
return { discovery: false, paths };
|
|
160
348
|
}
|
|
@@ -175,9 +363,11 @@ async function markdownFiles(directory) {
|
|
|
175
363
|
}
|
|
176
364
|
/** Most specific scope first; the first definition of a name wins deterministically. */
|
|
177
365
|
function agentDirectories(cwd, homeDirectory) {
|
|
178
|
-
const userAgentDir =
|
|
179
|
-
? join(
|
|
180
|
-
:
|
|
366
|
+
const userAgentDir = homeDirectory === homedir()
|
|
367
|
+
? join(getAgentDir(), "agents")
|
|
368
|
+
: process.env.PI_CODING_AGENT_DIR
|
|
369
|
+
? join(process.env.PI_CODING_AGENT_DIR, "agents")
|
|
370
|
+
: join(homeDirectory, ".pi", "agent", "agents");
|
|
181
371
|
return [join(cwd, ".pi", "agents"), join(cwd, ".agents", "agents"), userAgentDir];
|
|
182
372
|
}
|
|
183
373
|
async function readAgentDefinition(path) {
|
|
@@ -200,13 +390,25 @@ export async function discoverAgents(cwd, homeDirectory = homedir()) {
|
|
|
200
390
|
for (const directory of agentDirectories(resolve(cwd), resolve(homeDirectory))) {
|
|
201
391
|
for (const path of await markdownFiles(directory)) {
|
|
202
392
|
const raw = await readAgentDefinition(path);
|
|
393
|
+
if (selected.has(raw.name))
|
|
394
|
+
continue;
|
|
203
395
|
const builtin = builtins.get(raw.name);
|
|
204
396
|
const isBuiltin = builtin !== undefined;
|
|
205
397
|
const isBodyEmpty = raw.body.trim() === "";
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
398
|
+
let frontmatter;
|
|
399
|
+
let body;
|
|
400
|
+
if (isBuiltin && isBodyEmpty) {
|
|
401
|
+
frontmatter = mergeDiscoveredFrontmatter(builtin.frontmatter, raw.frontmatter, path, true);
|
|
402
|
+
body = builtin.body;
|
|
403
|
+
}
|
|
404
|
+
else if (isBuiltin && !isBodyEmpty) {
|
|
405
|
+
frontmatter = mergeDiscoveredFrontmatter(builtin.frontmatter, raw.frontmatter, path, false);
|
|
406
|
+
body = raw.body;
|
|
407
|
+
}
|
|
408
|
+
else {
|
|
409
|
+
frontmatter = raw.frontmatter;
|
|
410
|
+
body = raw.body;
|
|
411
|
+
}
|
|
210
412
|
const isCompliant = typeof frontmatter.model === "string" &&
|
|
211
413
|
frontmatter.model.trim() !== "" &&
|
|
212
414
|
(frontmatter.model.trim() === "inherit" || frontmatter.model.includes("/"));
|
|
@@ -303,18 +505,47 @@ export async function resolveAgent(options) {
|
|
|
303
505
|
homeDirectory,
|
|
304
506
|
frontmatterCodemode: codemodeValue,
|
|
305
507
|
});
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
if (excludeTools.includes(CONTACT_PARENT_TOOL_NAME)) {
|
|
508
|
+
const excludeToolsRaw = stringList(discovered.frontmatter.exclude_tools, "exclude_tools", path);
|
|
509
|
+
if (excludeToolsRaw.includes(CONTACT_PARENT_TOOL_NAME)) {
|
|
309
510
|
throw readableError(path, `exclude_tools cannot disable the required ${CONTACT_PARENT_TOOL_NAME} bridge`);
|
|
310
511
|
}
|
|
512
|
+
const parsedTools = parsePiSelection(discovered.frontmatter.tools, "tools", path);
|
|
513
|
+
let rawTools;
|
|
514
|
+
const excludeTools = [...excludeToolsRaw];
|
|
515
|
+
if (parsedTools.type === "none") {
|
|
516
|
+
rawTools = [CONTACT_PARENT_TOOL_NAME];
|
|
517
|
+
}
|
|
518
|
+
else if (parsedTools.type === "whitelist") {
|
|
519
|
+
rawTools = [...(parsedTools.names ?? [])];
|
|
520
|
+
}
|
|
521
|
+
else if (parsedTools.type === "modifier") {
|
|
522
|
+
const baseTools = [...DEFAULT_PI_TOOLS, CONTACT_PARENT_TOOL_NAME];
|
|
523
|
+
if (parsedTools.remove?.includes(CONTACT_PARENT_TOOL_NAME)) {
|
|
524
|
+
throw readableError(path, `tools modifier cannot remove the required ${CONTACT_PARENT_TOOL_NAME} bridge`);
|
|
525
|
+
}
|
|
526
|
+
rawTools = applyModifiers(baseTools, parsedTools.modifiers ?? []);
|
|
527
|
+
const finalExclusions = (parsedTools.remove ?? []).filter((r) => !rawTools.includes(r));
|
|
528
|
+
for (const r of finalExclusions) {
|
|
529
|
+
if (!excludeTools.includes(r))
|
|
530
|
+
excludeTools.push(r);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
else {
|
|
534
|
+
rawTools = [];
|
|
535
|
+
}
|
|
311
536
|
const overlap = rawTools.find((tool) => excludeTools.includes(tool));
|
|
312
537
|
if (overlap !== undefined)
|
|
313
538
|
throw readableError(path, `tool ${overlap} is both allowed and excluded`);
|
|
314
539
|
if (rawTools.length > 0 && !rawTools.includes(CONTACT_PARENT_TOOL_NAME)) {
|
|
315
540
|
throw readableError(path, `tools must include the required ${CONTACT_PARENT_TOOL_NAME} bridge`);
|
|
316
541
|
}
|
|
317
|
-
|
|
542
|
+
if (excludeTools.length > 0) {
|
|
543
|
+
rawTools = rawTools.filter((t) => !excludeTools.includes(t));
|
|
544
|
+
}
|
|
545
|
+
const tools = rawTools.length > 0 &&
|
|
546
|
+
codemodeOnly &&
|
|
547
|
+
!rawTools.includes("codemode") &&
|
|
548
|
+
!excludeTools.includes("codemode")
|
|
318
549
|
? [...rawTools, "codemode"]
|
|
319
550
|
: rawTools;
|
|
320
551
|
const thinkingValue = discovered.frontmatter.thinking;
|
|
@@ -327,14 +558,10 @@ export async function resolveAgent(options) {
|
|
|
327
558
|
...extensionSelection.paths.filter((selected) => selected !== bridgeExtensionPath),
|
|
328
559
|
bridgeExtensionPath,
|
|
329
560
|
];
|
|
330
|
-
const skills = await skillSelection(discovered.frontmatter.skills, path, homeDirectory, options.skillCatalog
|
|
561
|
+
const skills = await skillSelection(discovered.frontmatter.skills, path, homeDirectory, options.skillCatalog);
|
|
562
|
+
const mcp = mcpSelection(discovered.frontmatter.mcp, path);
|
|
331
563
|
const displayName = optionalString(discovered.frontmatter.display_name, "display_name", path);
|
|
332
564
|
const description = optionalString(discovered.frontmatter.description, "description", path);
|
|
333
|
-
const interactiveValue = discovered.frontmatter.interactive;
|
|
334
|
-
if (interactiveValue !== undefined && typeof interactiveValue !== "boolean") {
|
|
335
|
-
throw readableError(path, "interactive must be boolean");
|
|
336
|
-
}
|
|
337
|
-
const interactive = interactiveValue === true;
|
|
338
565
|
return Object.freeze({
|
|
339
566
|
agent: Object.freeze({
|
|
340
567
|
name,
|
|
@@ -360,7 +587,7 @@ export async function resolveAgent(options) {
|
|
|
360
587
|
discovery: skills.discovery,
|
|
361
588
|
paths: Object.freeze([...skills.paths]),
|
|
362
589
|
}),
|
|
363
|
-
|
|
590
|
+
mcp,
|
|
364
591
|
enabled: discovered.enabled !== false,
|
|
365
592
|
});
|
|
366
593
|
}
|
package/dist/bridge-client.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export interface ChildBridgeClientOptions {
|
|
|
10
10
|
readonly identity: ChildIdentity;
|
|
11
11
|
/** Executes a parent request; a thrown error becomes a failure response. */
|
|
12
12
|
readonly handleRequest: (request: ChildBridgeRequest) => Promise<unknown>;
|
|
13
|
+
/**
|
|
14
|
+
* Called after every successful handshake, including the first. A reconnecting child uses it to
|
|
15
|
+
* resend whatever state the parent needs to stay synchronized.
|
|
16
|
+
*/
|
|
17
|
+
readonly onConnect?: () => void;
|
|
13
18
|
readonly reconnectDelayMs?: number;
|
|
14
19
|
readonly maxBufferedReports?: number;
|
|
15
20
|
readonly maxFrameBytes?: number;
|
package/dist/bridge-client.js
CHANGED
|
@@ -274,6 +274,12 @@ export class ChildBridgeClient {
|
|
|
274
274
|
this.#connected = true;
|
|
275
275
|
socket.on("close", () => this.#teardown(socket));
|
|
276
276
|
socket.on("error", () => this.#teardown(socket));
|
|
277
|
+
try {
|
|
278
|
+
this.#options.onConnect?.();
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
this.#diagnose(`onConnect handler failed: ${errorMessage(error)}`);
|
|
282
|
+
}
|
|
277
283
|
void this.#flush();
|
|
278
284
|
return true;
|
|
279
285
|
}
|