pi-squire 0.3.1 → 0.3.2
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 +23 -1
- package/README.md +20 -2
- package/package.json +1 -1
- package/src/agents.ts +43 -3
- package/src/index.ts +20 -8
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.2] - 2026-10-09
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Agent frontmatter `extensions` lists extension paths loaded only by that
|
|
14
|
+
agent's delegate, each passed to its Pi as `-e` on top of its normal
|
|
15
|
+
extensions. `~` expands and relative paths resolve against the `AGENT.md`'s
|
|
16
|
+
directory; a path that does not exist leaves the agent out with a warning.
|
|
17
|
+
- Agent frontmatter `tools` lists the delegate's visible tools, passed as
|
|
18
|
+
`--tools`. Without discovery tools such as `codemode` or `tool_search`, only
|
|
19
|
+
listed tools are visible; Pi can keep unlisted MCP tools registered unless
|
|
20
|
+
the list includes an `mcp__` entry, and discovery tools can expose them.
|
|
21
|
+
Names are not checked against the parent's tools, and `exclude-tools` still
|
|
22
|
+
applies alongside.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- The `delegate` tool treats `null` or the string `"null"` in `model`,
|
|
27
|
+
`thinking` or `label` as unset, so the agent's defaults apply. Before, a
|
|
28
|
+
`null` model crashed the call and `"null"` was refused as not provider/id.
|
|
29
|
+
|
|
9
30
|
## [0.3.1] - 2026-10-03
|
|
10
31
|
|
|
11
32
|
### Changed
|
|
@@ -148,7 +169,8 @@ author's dotfiles; the changes below are against that copy.
|
|
|
148
169
|
|
|
149
170
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
150
171
|
|
|
151
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.3.
|
|
172
|
+
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.3.2...HEAD
|
|
173
|
+
[0.3.2]: https://github.com/gvanderclay/pi-squire/compare/v0.3.1...v0.3.2
|
|
152
174
|
[0.3.1]: https://github.com/gvanderclay/pi-squire/compare/v0.3.0...v0.3.1
|
|
153
175
|
[0.3.0]: https://github.com/gvanderclay/pi-squire/compare/v0.2.2...v0.3.0
|
|
154
176
|
[0.2.2]: https://github.com/gvanderclay/pi-squire/compare/v0.2.1...v0.2.2
|
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ The roster is read at call time from `<agent dir>/agents/<name>/AGENT.md`, one
|
|
|
116
116
|
directory per agent, so a new agent is usable without a reload. Frontmatter
|
|
117
117
|
carries `description`, `model` (`provider/id`) and `thinking`, and optionally
|
|
118
118
|
`auto-exit` (`true` or `false`, default `true`; see [Auto-exit](#auto-exit)),
|
|
119
|
-
`exclude-tools` and `fallback`. The body is the delegate's system prompt,
|
|
119
|
+
`exclude-tools`, `tools`, `extensions` and `fallback`. The body is the delegate's system prompt,
|
|
120
120
|
appended to Pi's own prompt; it reaches the child as a path to a private file
|
|
121
121
|
under the system temporary folder, so the text stays off the command line. The
|
|
122
122
|
[first-use example](#first-use) is a complete `AGENT.md`.
|
|
@@ -133,6 +133,23 @@ delegate's result reaches the parent through the `message:*` provider's reply
|
|
|
133
133
|
when its run settles, not through a tool, so excluding tools cannot stop it
|
|
134
134
|
answering.
|
|
135
135
|
|
|
136
|
+
`tools` lists tools for the delegate as a comma-separated or YAML list
|
|
137
|
+
(`tools: device_run`). It reaches the child as `--tools device_run`, which
|
|
138
|
+
covers extension tools too. Without discovery tools such as `codemode` or
|
|
139
|
+
`tool_search`, only the listed tools are visible. Pi can keep unlisted MCP
|
|
140
|
+
tools registered unless the list includes an `mcp__` entry; discovery tools can
|
|
141
|
+
also expose them. Names are not checked against the parent's tools, since a
|
|
142
|
+
tool from one of the agent's own `extensions` exists only in the child. Empty or
|
|
143
|
+
absent passes no flag. `exclude-tools` still applies alongside.
|
|
144
|
+
|
|
145
|
+
`extensions` lists extension paths only this agent's delegate loads, as a
|
|
146
|
+
comma-separated or YAML list. Each reaches the child as a repeated
|
|
147
|
+
`-e <absolute path>`, on top of its normal extensions, so pi-squire and the
|
|
148
|
+
`message:*` provider still load and the result still reaches the parent. `~`
|
|
149
|
+
and `~/` expand to the home directory and a relative path resolves against the
|
|
150
|
+
directory holding that `AGENT.md`. A path that does not exist when the roster
|
|
151
|
+
is read leaves the agent out, with a warning.
|
|
152
|
+
|
|
136
153
|
`fallback` lists models to try, in order, when the agent's `model` is
|
|
137
154
|
usage-limited (see [Usage limits](#usage-limits)), as a comma-separated list
|
|
138
155
|
or a YAML list of `provider/id`. An entry that is not `provider/id` is dropped
|
|
@@ -150,7 +167,8 @@ An `AGENT.md` that cannot be used is left out of the roster and named in a
|
|
|
150
167
|
warning. That covers missing fields, a model that is not `provider/id`, an
|
|
151
168
|
unknown thinking level, an `auto-exit` that is not `true` or `false`, an
|
|
152
169
|
`exclude-tools` that is not a list of names or names a tool the session does
|
|
153
|
-
not have,
|
|
170
|
+
not have, a `tools` or `extensions` that is not a list, an extension path that
|
|
171
|
+
does not exist, and an empty body. With no agents at all, `/delegate` says how to
|
|
154
172
|
add one.
|
|
155
173
|
|
|
156
174
|
## Configuration
|
package/package.json
CHANGED
package/src/agents.ts
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
// call time so a new agent is usable without a reload. Frontmatter carries
|
|
3
3
|
// `description`, `model` and `thinking`, and optionally `auto-exit` (default
|
|
4
4
|
// true) and `exclude-tools` (tools the delegate goes without, passed to its Pi
|
|
5
|
-
// as `--exclude-tools`; each must be a tool the parent session has)
|
|
5
|
+
// as `--exclude-tools`; each must be a tool the parent session has), `tools`
|
|
6
|
+
// (the delegate's complete tool allowlist, passed as `--tools`, not checked
|
|
7
|
+
// against the parent) and `extensions` (extra extension paths for the
|
|
8
|
+
// delegate only, each passed as `-e`; `~` expands, relative paths resolve
|
|
9
|
+
// against the AGENT.md's directory, and a missing path leaves the agent out); the body
|
|
6
10
|
// is the delegate's system prompt. A file that is missing or malformed is left out, with a warning.
|
|
7
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
8
|
-
import {
|
|
11
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
9
14
|
|
|
10
15
|
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
11
16
|
|
|
@@ -33,6 +38,10 @@ export type Agent = {
|
|
|
33
38
|
autoExit: boolean;
|
|
34
39
|
/** Tools the delegate goes without, from `exclude-tools`; empty means all of them. */
|
|
35
40
|
excludeTools: string[];
|
|
41
|
+
/** The delegate's complete tool allowlist, from `tools`; empty means no `--tools` flag. */
|
|
42
|
+
tools: string[];
|
|
43
|
+
/** Absolute paths of extra extensions the delegate loads, from `extensions`; empty when absent. */
|
|
44
|
+
extensions: string[];
|
|
36
45
|
/** Models to try, in order, when `model` is usage-limited; from `fallback`, empty when absent. */
|
|
37
46
|
fallback: string[];
|
|
38
47
|
/** The body of `AGENT.md`: the delegate's system prompt. */
|
|
@@ -96,6 +105,8 @@ function parseAgent(
|
|
|
96
105
|
return {
|
|
97
106
|
warning: `${path} has exclude-tools ${JSON.stringify(unknownTool)}, not a tool this session has; skipped`,
|
|
98
107
|
};
|
|
108
|
+
const launch = launchFields(path, frontmatter);
|
|
109
|
+
if ("warning" in launch) return launch;
|
|
99
110
|
// A bad fallback entry is dropped with a note; it never costs the agent.
|
|
100
111
|
const notes: string[] = [];
|
|
101
112
|
const listedFallback = toolList(frontmatter.fallback);
|
|
@@ -115,12 +126,41 @@ function parseAgent(
|
|
|
115
126
|
thinking: thinking as Thinking,
|
|
116
127
|
autoExit: autoExit ?? true,
|
|
117
128
|
excludeTools,
|
|
129
|
+
...launch,
|
|
118
130
|
fallback,
|
|
119
131
|
prompt: body,
|
|
120
132
|
},
|
|
121
133
|
};
|
|
122
134
|
}
|
|
123
135
|
|
|
136
|
+
/** The `tools` and `extensions` fields, or the warning that leaves the agent out. */
|
|
137
|
+
function launchFields(
|
|
138
|
+
path: string,
|
|
139
|
+
frontmatter: Record<string, unknown>,
|
|
140
|
+
): { tools: string[]; extensions: string[] } | { warning: string } {
|
|
141
|
+
const tools = toolList(frontmatter.tools);
|
|
142
|
+
if (tools === null)
|
|
143
|
+
return {
|
|
144
|
+
warning: `${path} has tools ${JSON.stringify(frontmatter.tools)}, not a comma-separated list of tool names; skipped`,
|
|
145
|
+
};
|
|
146
|
+
const paths = toolList(frontmatter.extensions);
|
|
147
|
+
if (paths === null)
|
|
148
|
+
return {
|
|
149
|
+
warning: `${path} has extensions ${JSON.stringify(frontmatter.extensions)}, not a comma-separated list of paths; skipped`,
|
|
150
|
+
};
|
|
151
|
+
const extensions = (paths ?? []).map((entry) => resolveExtension(entry, dirname(path)));
|
|
152
|
+
const absent = extensions.find((entry) => !existsSync(entry));
|
|
153
|
+
if (absent !== undefined) return { warning: `${path} has extension ${absent}, which does not exist; skipped` };
|
|
154
|
+
return { tools: tools ?? [], extensions };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** An extension path as an absolute one: `~` is the home directory, a relative path is against the AGENT.md's directory. */
|
|
158
|
+
function resolveExtension(entry: string, dir: string): string {
|
|
159
|
+
if (entry === "~") return homedir();
|
|
160
|
+
if (entry.startsWith("~/")) return join(homedir(), entry.slice(2));
|
|
161
|
+
return isAbsolute(entry) ? entry : resolve(dir, entry);
|
|
162
|
+
}
|
|
163
|
+
|
|
124
164
|
/** A list of names (tools or models), comma-separated or a YAML list of names; undefined when absent, null when it is neither. */
|
|
125
165
|
function toolList(value: unknown): string[] | undefined | null {
|
|
126
166
|
if (value === undefined || value === null) return undefined;
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
// call) the delegate closes its own window after a normal completion; the
|
|
18
18
|
// delegate's side lives in `child.ts`. With it off the window stays open.
|
|
19
19
|
// An agent's `exclude-tools` goes to the child as `--exclude-tools`; a name the
|
|
20
|
-
// session has no tool for leaves the agent out of the roster.
|
|
20
|
+
// session has no tool for leaves the agent out of the roster. Its `tools` goes
|
|
21
|
+
// as `--tools` and each of its `extensions` as `-e`, both for the child alone.
|
|
21
22
|
//
|
|
22
23
|
// The model gets three tools. `delegate` takes agent, task and optional model
|
|
23
24
|
// and thinking, validates them the way the command does, and starts the
|
|
@@ -308,7 +309,7 @@ function toolDescription(agents: readonly Agent[]): string {
|
|
|
308
309
|
"agents",
|
|
309
310
|
"<name>",
|
|
310
311
|
"AGENT.md",
|
|
311
|
-
)} with frontmatter description, model and thinking and the prompt as its body; /delegate explains the format too.`;
|
|
312
|
+
)} with frontmatter description, model and thinking (optionally auto-exit, exclude-tools, tools, extensions and fallback) and the prompt as its body; /delegate explains the format too.`;
|
|
312
313
|
}
|
|
313
314
|
return [
|
|
314
315
|
intro,
|
|
@@ -318,7 +319,7 @@ function toolDescription(agents: readonly Agent[]): string {
|
|
|
318
319
|
(agent) =>
|
|
319
320
|
`- ${agent.name} — ${agent.description} (default ${[agent.model, ...agent.fallback.map((model) => `fallback ${model}`)].join(", ")}, thinking ${agent.thinking}${agent.autoExit ? "" : ", auto-exit off"}${
|
|
320
321
|
agent.excludeTools.length > 0 ? `, no ${agent.excludeTools.join("/")}` : ""
|
|
321
|
-
})`,
|
|
322
|
+
}${agent.tools.length > 0 ? `, tools: ${agent.tools.join("/")}` : ""})`,
|
|
322
323
|
),
|
|
323
324
|
"",
|
|
324
325
|
"`model` and `thinking` override the agent's defaults for this call; an unknown value is refused with close matches. `label` names the delegate's window and session; give one that says what the task is. `delegation_status` reports a delegation, and `delegation_close` ends one.",
|
|
@@ -387,7 +388,7 @@ export default function delegate(
|
|
|
387
388
|
"agents",
|
|
388
389
|
"<name>",
|
|
389
390
|
"AGENT.md",
|
|
390
|
-
)} with frontmatter description, model and thinking, and the prompt as its body`,
|
|
391
|
+
)} with frontmatter description, model and thinking (optionally auto-exit, exclude-tools, tools, extensions and fallback), and the prompt as its body`,
|
|
391
392
|
);
|
|
392
393
|
}
|
|
393
394
|
const agent = roster.agents.find((candidate) => candidate.name === request.agent);
|
|
@@ -447,6 +448,8 @@ export default function delegate(
|
|
|
447
448
|
start.model,
|
|
448
449
|
"--thinking",
|
|
449
450
|
start.thinking,
|
|
451
|
+
...start.agent.extensions.flatMap((extension) => ["-e", extension]),
|
|
452
|
+
...(start.agent.tools.length > 0 ? ["--tools", start.agent.tools.join(",")] : []),
|
|
450
453
|
...(start.agent.excludeTools.length > 0 ? ["--exclude-tools", start.agent.excludeTools.join(",")] : []),
|
|
451
454
|
"--append-system-prompt",
|
|
452
455
|
prompt,
|
|
@@ -536,18 +539,27 @@ export default function delegate(
|
|
|
536
539
|
}),
|
|
537
540
|
async execute(
|
|
538
541
|
_toolCallId: string,
|
|
539
|
-
params: {
|
|
542
|
+
params: {
|
|
543
|
+
agent: string;
|
|
544
|
+
task: string;
|
|
545
|
+
model?: string | null;
|
|
546
|
+
thinking?: string | null;
|
|
547
|
+
label?: string | null;
|
|
548
|
+
auto_exit?: boolean | null;
|
|
549
|
+
},
|
|
540
550
|
_signal: unknown,
|
|
541
551
|
_onUpdate: unknown,
|
|
542
552
|
ctx: ExtensionContext,
|
|
543
553
|
) {
|
|
544
554
|
if (!tmux.insideTmux()) throw new Error(NOT_IN_TMUX);
|
|
555
|
+
// Some models fill every optional field with null or the string "null"; both mean unset.
|
|
556
|
+
const given = (value: string | null | undefined) => (value == null || value === "null" ? undefined : value);
|
|
545
557
|
const start = await validate(
|
|
546
558
|
{
|
|
547
559
|
agent: params.agent,
|
|
548
|
-
model: params.model,
|
|
549
|
-
thinking: params.thinking,
|
|
550
|
-
label: params.label,
|
|
560
|
+
model: given(params.model),
|
|
561
|
+
thinking: given(params.thinking),
|
|
562
|
+
label: given(params.label),
|
|
551
563
|
autoExit: typeof params.auto_exit === "boolean" ? params.auto_exit : undefined,
|
|
552
564
|
task: params.task ?? "",
|
|
553
565
|
},
|