pi-squire 0.8.0 → 0.9.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/CHANGELOG.md +48 -1
- package/README.md +63 -32
- package/package.json +4 -4
- package/src/agents.ts +3 -3
- package/src/child.ts +29 -21
- package/src/index.ts +57 -9
- package/src/limits.ts +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,51 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.0] - 2026-10-21
|
|
10
|
+
|
|
11
|
+
### Breaking
|
|
12
|
+
|
|
13
|
+
- Agent definitions now live under `<agent dir>/squire/agents/<name>/AGENT.md`,
|
|
14
|
+
and only there. The old `<agent dir>/agents/<name>/AGENT.md` location is no
|
|
15
|
+
longer read, with no fallback or dual discovery, so an agent left there is
|
|
16
|
+
neither listed nor launched.
|
|
17
|
+
- Pi 1.3.0 or later is now required. The raised floor supports structured
|
|
18
|
+
per-run system-prompt sections and `before_agent_start` preparation for runs
|
|
19
|
+
started by an idle custom-message result, so parent guidance and roster
|
|
20
|
+
metadata are current before each new run.
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- Optional parent delegation guidance: Markdown in
|
|
25
|
+
`<agent dir>/squire/delegation_prompt.md` is inserted unchanged into the
|
|
26
|
+
`squire_parent_delegation` system-prompt section before each new run, only
|
|
27
|
+
while the `delegate` tool is active and not hidden. Missing, empty or deleted
|
|
28
|
+
files add no guidance; a read failure warns with the path, removes stale
|
|
29
|
+
guidance for that run, and leaves delegation working. The package ships no
|
|
30
|
+
default delegation or approval policy.
|
|
31
|
+
- The `delegate` tool description now refreshes from the current roster before
|
|
32
|
+
each new run, including a run woken by an idle delegate result. A changed
|
|
33
|
+
agent description, addition or removal shows up in the run being prepared;
|
|
34
|
+
unchanged metadata is not re-registered.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- The `delegate` tool now says to keep working or end the turn rather than poll
|
|
39
|
+
or send a blocking mail request merely to wait for a result, while a
|
|
40
|
+
substantive follow-up remains supported.
|
|
41
|
+
|
|
42
|
+
## [0.8.1] - 2026-10-20
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- A 429 whose text says the endpoint is unavailable, such as OpenCode Go's
|
|
47
|
+
`429: Endpoint is unavailable`, no longer marks the provider as
|
|
48
|
+
usage-limited. It is an outage, not a used-up quota, so `/delegate` keeps
|
|
49
|
+
launching on the provider instead of refusing it for five minutes or more.
|
|
50
|
+
Pi has already retried the request by then. A running delegate that hits
|
|
51
|
+
it moves to its next fallback model and carries on, as it does on a usage
|
|
52
|
+
limit.
|
|
53
|
+
|
|
9
54
|
## [0.8.0] - 2026-10-20
|
|
10
55
|
|
|
11
56
|
### Added
|
|
@@ -273,7 +318,9 @@ author's dotfiles; the changes below are against that copy.
|
|
|
273
318
|
|
|
274
319
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
275
320
|
|
|
276
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.
|
|
321
|
+
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.9.0...HEAD
|
|
322
|
+
[0.9.0]: https://github.com/gvanderclay/pi-squire/compare/v0.8.1...v0.9.0
|
|
323
|
+
[0.8.1]: https://github.com/gvanderclay/pi-squire/compare/v0.8.0...v0.8.1
|
|
277
324
|
[0.8.0]: https://github.com/gvanderclay/pi-squire/compare/v0.7.0...v0.8.0
|
|
278
325
|
[0.7.0]: https://github.com/gvanderclay/pi-squire/compare/v0.6.0...v0.7.0
|
|
279
326
|
[0.6.0]: https://github.com/gvanderclay/pi-squire/compare/v0.5.1...v0.6.0
|
package/README.md
CHANGED
|
@@ -10,13 +10,12 @@ answers with one message, which the parent session shows as a result.
|
|
|
10
10
|
|
|
11
11
|
You need:
|
|
12
12
|
|
|
13
|
-
- Pi
|
|
14
|
-
pi-squire is tested with.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`
|
|
19
|
-
`@earendil-works/pi-coding-agent` and `typebox`.
|
|
13
|
+
- Pi 1.3.0 or later, on macOS or Linux (not Windows). Pi 1.3.0 is the version
|
|
14
|
+
pi-squire is tested with. It provides the per-run `before_agent_start`
|
|
15
|
+
preparation for idle custom-message results, the structured system-prompt
|
|
16
|
+
options the parent guidance uses, and the extension APIs the package relies
|
|
17
|
+
on. Pi supplies the peer dependencies `@earendil-works/pi-coding-agent` and
|
|
18
|
+
`typebox`.
|
|
20
19
|
- tmux 3.0 or later, with the Pi session running in a tmux pane. That is the
|
|
21
20
|
first version with `new-window -e`. A window is opened in the pane's
|
|
22
21
|
session, so a client attached elsewhere does not matter.
|
|
@@ -45,8 +44,8 @@ The package has no runtime dependencies and no build step.
|
|
|
45
44
|
|
|
46
45
|
The package ships no agents, so create one first. `<agent dir>` is Pi's agent
|
|
47
46
|
directory: `~/.pi/agent`, or `PI_CODING_AGENT_DIR` when that is set. Save this
|
|
48
|
-
as `<agent dir>/agents/scout/AGENT.md`, replacing
|
|
49
|
-
model your Pi can use:
|
|
47
|
+
as `<agent dir>/squire/agents/scout/AGENT.md`, replacing
|
|
48
|
+
`provider/model-id` with a model your Pi can use:
|
|
50
49
|
|
|
51
50
|
```markdown
|
|
52
51
|
---
|
|
@@ -60,8 +59,10 @@ You research one question at a time and answer concisely, citing the files
|
|
|
60
59
|
you read.
|
|
61
60
|
```
|
|
62
61
|
|
|
63
|
-
The roster is read at call time, so no reload is needed.
|
|
64
|
-
|
|
62
|
+
The roster is read at call time, so no reload is needed. Delegation guidance
|
|
63
|
+
for the parent is optional: save Markdown as
|
|
64
|
+
`<agent dir>/squire/delegation_prompt.md`, and it is read before each run.
|
|
65
|
+
Then, from a Pi session running in tmux, hand it a task:
|
|
65
66
|
|
|
66
67
|
```text
|
|
67
68
|
/delegate scout --label first Which files define the command-line entry point?
|
|
@@ -75,10 +76,11 @@ closes its own window ([auto-exit](#auto-exit)).
|
|
|
75
76
|
## How a delegation works
|
|
76
77
|
|
|
77
78
|
`/delegate <agent> [--model <provider/id>] [--thinking <level>] [--label <name>] [--auto-exit | --no-auto-exit] <task…>`
|
|
78
|
-
reads the agent from `<agent dir>/agents/<name>/AGENT.md`, writes
|
|
79
|
-
the delegate's inbox through `message:send`, emits
|
|
80
|
-
extensions can shape the launch, then opens a
|
|
81
|
-
parent's own Pi in the parent's working directory
|
|
79
|
+
reads the agent from `<agent dir>/squire/agents/<name>/AGENT.md`, writes
|
|
80
|
+
the task to the delegate's inbox through `message:send`, emits
|
|
81
|
+
`session:launch` so other extensions can shape the launch, then opens a
|
|
82
|
+
detached window running the parent's own Pi in the parent's working directory
|
|
83
|
+
and agent root. The agent's
|
|
82
84
|
prompt reaches the child as a path to a private file under the system
|
|
83
85
|
temporary folder, not as text on the command line.
|
|
84
86
|
|
|
@@ -112,8 +114,9 @@ temporary folder, not as text on the command line.
|
|
|
112
114
|
|
|
113
115
|
## Agents
|
|
114
116
|
|
|
115
|
-
The roster is read at call time from
|
|
116
|
-
directory per agent, so a new
|
|
117
|
+
The roster is read at call time from
|
|
118
|
+
`<agent dir>/squire/agents/<name>/AGENT.md`, one directory per agent, so a new
|
|
119
|
+
agent is usable without a reload. Frontmatter
|
|
117
120
|
carries `description`, `model` (`provider/id`) and `thinking`, and optionally
|
|
118
121
|
`auto-exit` (`true` or `false`, default `true`; see [Auto-exit](#auto-exit)),
|
|
119
122
|
`exclude-tools`, `tools`, `extensions` and `fallback`. The body is the delegate's system prompt,
|
|
@@ -173,8 +176,23 @@ add one.
|
|
|
173
176
|
|
|
174
177
|
## Configuration
|
|
175
178
|
|
|
176
|
-
|
|
177
|
-
|
|
179
|
+
`<agent dir>/squire/` holds the roster and the optional parent guidance file.
|
|
180
|
+
`delegation_prompt.md` is Markdown, inserted unchanged as the
|
|
181
|
+
`squire_parent_delegation` system-prompt section before each new run, including
|
|
182
|
+
a run started by an idle delegate result. It is not a conversation or user
|
|
183
|
+
message, is not duplicated in the `delegate` tool description or guidelines,
|
|
184
|
+
and is not sent to the child. The guidance is added only while the `delegate`
|
|
185
|
+
tool is active and not hidden; when delegation is inactive or hidden, the
|
|
186
|
+
section is removed and tool selection is left as it was. A missing or whitespace-only
|
|
187
|
+
file adds no section. Editing the file replaces the section at the next run;
|
|
188
|
+
deleting it removes the section. A read failure other than a missing file
|
|
189
|
+
warns with the path, removes stale guidance for that run, and leaves otherwise
|
|
190
|
+
valid delegation working. The package ships no default delegation or approval
|
|
191
|
+
policy.
|
|
192
|
+
|
|
193
|
+
Refresh happens before a new run, not before every model request in a tool
|
|
194
|
+
loop, and the extension does not watch the filesystem. `pi-squire` reads no
|
|
195
|
+
settings file and no environment variable of its own. `PI_DELEGATE_PARENT` and `PI_DELEGATE_AUTO_EXIT` are set
|
|
178
196
|
for the child, never read as configuration by the parent. The window starts
|
|
179
197
|
with the tmux server's environment plus only these from the parent:
|
|
180
198
|
`PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR` (when the parent has it
|
|
@@ -213,13 +231,18 @@ and its excluded tools.
|
|
|
213
231
|
|
|
214
232
|
The model picks an agent from these lines without reading files. The
|
|
215
233
|
description also tells the model to turn `auto_exit` off when it means to keep
|
|
216
|
-
talking to the delegate by mail after its result.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
234
|
+
talking to the delegate by mail after its result. It says a task must stand
|
|
235
|
+
alone, that results arrive automatically, and that the parent should keep
|
|
236
|
+
working or end its turn rather than poll or send a blocking mail request
|
|
237
|
+
merely to wait; a substantive follow-up remains fine. The description is
|
|
238
|
+
rebuilt at session start and before each new run when the exposed roster
|
|
239
|
+
changes, so an edit, addition or removal shows up in the run being prepared,
|
|
240
|
+
including a run woken by an idle delegate result. Unchanged metadata is not
|
|
241
|
+
re-registered. Pi cannot list the session's tools while extensions load, so
|
|
242
|
+
the description built then skips the `exclude-tools` check, and the one built
|
|
243
|
+
after startup applies it. The roster itself is still read at call time, and an
|
|
244
|
+
unknown agent, model or thinking level is refused with close matches and
|
|
245
|
+
nothing starts.
|
|
223
246
|
|
|
224
247
|
`delegation_status` reports one state per delegation:
|
|
225
248
|
|
|
@@ -485,7 +508,7 @@ has been sent and just before it opens the delegate's window.
|
|
|
485
508
|
| --- | --- |
|
|
486
509
|
| `args` | the child `pi`'s complete argument list, launcher first |
|
|
487
510
|
| `env` | the child's environment, as `KEY`/`value` pairs |
|
|
488
|
-
| `agent` | the agent's name, as in `<agent dir>/agents/<name>/AGENT.md`; read it, do not change it |
|
|
511
|
+
| `agent` | the agent's name, as in `<agent dir>/squire/agents/<name>/AGENT.md`; read it, do not change it |
|
|
489
512
|
|
|
490
513
|
Listeners may only append to `args` and add keys to `env`; there is no veto,
|
|
491
514
|
and `pi-squire` passes both to tmux unchanged. Do all work synchronously: the
|
|
@@ -520,10 +543,6 @@ to the provider.
|
|
|
520
543
|
Inside a delegate, `pi-squire` also takes over the task: a `request` whose
|
|
521
544
|
`from` is the parent's address. It sets `handled` and sends the task as a user
|
|
522
545
|
prompt labelled `[delegate]`, so the run starts the way a typed prompt does.
|
|
523
|
-
Pi 1.0.0 starts a turn for an injected custom message without preparing the
|
|
524
|
-
system prompt, so a fresh delegate's first request would otherwise carry no
|
|
525
|
-
context files, skills or role paragraph
|
|
526
|
-
([earendil-works/pi#5581](https://github.com/earendil-works/pi/issues/5581)).
|
|
527
546
|
The provider still arms the automatic answer for a request a listener took
|
|
528
547
|
over. Every other message to the delegate is left to the provider. `pi-squire` reads `from` and requires
|
|
529
548
|
it to be the sender's address, which for a delegate is its session id; a reply
|
|
@@ -549,6 +568,16 @@ written just before a window closed is never mistaken for no reply. **If
|
|
|
549
568
|
because a waiting reply cannot be ruled out. The provider's README carries the
|
|
550
569
|
contract.
|
|
551
570
|
|
|
571
|
+
## Testing limits
|
|
572
|
+
|
|
573
|
+
The automated tests drive the extension through a fake Pi session. They check
|
|
574
|
+
the registered tool definitions, the structured prompt options a simulated run
|
|
575
|
+
preparation receives, file handling, visibility gating and the existing launch
|
|
576
|
+
and result paths. They do not prove actual Pi event scheduling, final provider
|
|
577
|
+
requests, compaction, cache behavior, or model obedience. The idle-result wake
|
|
578
|
+
is simulated by dispatching the next preparation; the suite does not assert
|
|
579
|
+
that the runtime emits it.
|
|
580
|
+
|
|
552
581
|
## Compatibility
|
|
553
582
|
|
|
554
583
|
While pi-squire is below 1.0, breaking any contract below bumps the minor
|
|
@@ -565,7 +594,9 @@ Other extensions may rely on:
|
|
|
565
594
|
- the `/delegate` syntax and flags;
|
|
566
595
|
- the `[delegate]` result text and the closed-without-a-result text;
|
|
567
596
|
- `AGENT.md`'s frontmatter keys and its location,
|
|
568
|
-
`<agent dir>/agents/<name>/AGENT.md`;
|
|
597
|
+
`<agent dir>/squire/agents/<name>/AGENT.md`;
|
|
598
|
+
- the optional `<agent dir>/squire/delegation_prompt.md` file and the
|
|
599
|
+
`squire_parent_delegation` system-prompt section;
|
|
569
600
|
- the child environment variables `PI_DELEGATE_PARENT`,
|
|
570
601
|
`PI_DELEGATE_AUTO_EXIT` and `PI_UNATTENDED=1`.
|
|
571
602
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-squire",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Starts a delegate Pi session in a background tmux window, sends it the task over message:*, and shows the parent its result",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@earendil-works/pi-coding-agent": "
|
|
47
|
+
"@earendil-works/pi-coding-agent": ">=1.3.0",
|
|
48
48
|
"@earendil-works/pi-tui": "*",
|
|
49
49
|
"typebox": "*"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@biomejs/biome": "2.5.14",
|
|
53
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
54
|
-
"@earendil-works/pi-tui": "1.
|
|
53
|
+
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
54
|
+
"@earendil-works/pi-tui": "1.3.0",
|
|
55
55
|
"@types/node": "26.6.3",
|
|
56
56
|
"typebox": "1.3.27",
|
|
57
57
|
"typescript": "7.0.2"
|
package/src/agents.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The parent's agent roster: `<agent dir>/agents/<name>/AGENT.md`, read at
|
|
1
|
+
// The parent's agent roster: `<agent dir>/squire/agents/<name>/AGENT.md`, read at
|
|
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
|
|
@@ -179,14 +179,14 @@ function flag(value: unknown): boolean | undefined | null {
|
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
/**
|
|
182
|
-
* Read every agent under `<agentDir>/agents/`, in name order. A directory
|
|
182
|
+
* Read every agent under `<agentDir>/squire/agents/`, in name order. A directory
|
|
183
183
|
* with no `AGENT.md` is not an agent and is passed over quietly; an
|
|
184
184
|
* `AGENT.md` that cannot be used is warned about and left out. `tools` are
|
|
185
185
|
* the session's tool names, which `exclude-tools` is checked against;
|
|
186
186
|
* undefined skips that check, as before the session's tools are known.
|
|
187
187
|
*/
|
|
188
188
|
export function readRoster(agentDir: string, tools?: readonly string[]): Roster {
|
|
189
|
-
const dir = join(agentDir, "agents");
|
|
189
|
+
const dir = join(agentDir, "squire", "agents");
|
|
190
190
|
let names: string[];
|
|
191
191
|
try {
|
|
192
192
|
names = readdirSync(dir).sort();
|
package/src/child.ts
CHANGED
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
// edxeth/pi-subagents' `auto-exit` (README "Child lifecycle", `cf6dbf4`).
|
|
14
14
|
//
|
|
15
15
|
// It also takes over the parent's task request on `message:inbound` and sends
|
|
16
|
-
// it as a user prompt
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
16
|
+
// it as a user prompt, so the task starts through the same path as a typed
|
|
17
|
+
// prompt, labelled `[delegate]`. Pi 1.2.0 already prepares the system prompt
|
|
18
|
+
// for custom-message runs (earendil-works/pi#5581); this takeover remains the
|
|
19
|
+
// package's pinned startup delivery path. The provider still arms the
|
|
20
|
+
// automatic answer for a request a listener took over.
|
|
21
21
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
22
22
|
|
|
23
23
|
import { activeMark, classify, type Response, recordLimit } from "./limits.ts";
|
|
@@ -84,27 +84,39 @@ function registerTakeover(pi: ExtensionAPI, parent: string): void {
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* A usage limit moves the run to the next fallback
|
|
88
|
-
* marked, and continues it in this session; each
|
|
89
|
-
* Returns the setter for
|
|
87
|
+
* A usage limit or an unavailable endpoint moves the run to the next fallback
|
|
88
|
+
* that is usable and not marked, and continues it in this session; each
|
|
89
|
+
* fallback is tried once. Returns the setter for what ended the last run, as
|
|
90
|
+
* `<provider/model> <what happened>`.
|
|
90
91
|
*/
|
|
91
|
-
function registerFallback(pi: ExtensionAPI): (
|
|
92
|
+
function registerFallback(pi: ExtensionAPI): (failure: string | undefined) => void {
|
|
92
93
|
/** The models still to move to on a usage limit, in order. */
|
|
93
94
|
const fallback = (process.env[FALLBACK_ENV] ?? "").split(",").filter((model) => model.includes("/"));
|
|
94
|
-
let
|
|
95
|
+
let failure: string | undefined;
|
|
95
96
|
pi.on("agent_before_settle", async (_event, ctx) => {
|
|
96
|
-
const from =
|
|
97
|
-
|
|
97
|
+
const from = failure;
|
|
98
|
+
failure = undefined;
|
|
98
99
|
const next = from === undefined ? undefined : await switchModel(pi, ctx, fallback);
|
|
99
100
|
if (next === undefined) return;
|
|
100
|
-
const content = `[delegate] ${from}
|
|
101
|
+
const content = `[delegate] ${from}, so this session switched to ${next}. Carry on with the task from where it stopped.`;
|
|
101
102
|
return { continue: true, entries: [{ type: "custom_message", customType: "delegate", content, display: true }] };
|
|
102
103
|
});
|
|
103
|
-
return (
|
|
104
|
-
|
|
104
|
+
return (value) => {
|
|
105
|
+
failure = value;
|
|
105
106
|
};
|
|
106
107
|
}
|
|
107
108
|
|
|
109
|
+
/** Why a failed run should move to a fallback, as `<provider/model> <what happened>`; undefined when it should not. */
|
|
110
|
+
function fallbackReason(last: Message | undefined): string | undefined {
|
|
111
|
+
const error = last?.errorMessage;
|
|
112
|
+
if (typeof error !== "string") return undefined;
|
|
113
|
+
const model = `${String(last?.provider)}/${String(last?.model)}`;
|
|
114
|
+
if (classify(error) !== undefined) return `${model} hit a usage limit`;
|
|
115
|
+
// Pi has already retried an unavailable endpoint; it marks nothing, but this run moves on.
|
|
116
|
+
if (/unavailable/i.test(error)) return `${model} was unavailable (${error})`;
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
108
120
|
/** This delegate's footer icon, picked once per load. */
|
|
109
121
|
const ICON = Math.random() < 0.5 ? "👦" : "👧";
|
|
110
122
|
|
|
@@ -120,7 +132,7 @@ function showStatus(ctx: ExtensionContext, armed: boolean): void {
|
|
|
120
132
|
|
|
121
133
|
export function registerChild(pi: ExtensionAPI, parent: string): void {
|
|
122
134
|
registerTakeover(pi, parent);
|
|
123
|
-
const
|
|
135
|
+
const setFailure = registerFallback(pi);
|
|
124
136
|
|
|
125
137
|
let armed = process.env[AUTO_EXIT_ENV] === "1";
|
|
126
138
|
/** Whether the run that just ended finished without the user stopping it. */
|
|
@@ -162,11 +174,7 @@ export function registerChild(pi: ExtensionAPI, parent: string): void {
|
|
|
162
174
|
const stopped = last?.stopReason === "aborted" || ctx.signal?.aborted === true;
|
|
163
175
|
if (stopped) disarm(ctx);
|
|
164
176
|
if (!stopped && last?.stopReason === "error") recordUsageLimit(last, ctx, response);
|
|
165
|
-
|
|
166
|
-
!stopped && typeof last?.errorMessage === "string" && classify(last.errorMessage) !== undefined
|
|
167
|
-
? `${String(last.provider)}/${String(last.model)}`
|
|
168
|
-
: undefined,
|
|
169
|
-
);
|
|
177
|
+
setFailure(stopped ? undefined : fallbackReason(last));
|
|
170
178
|
// A failure ends the work like a completion: the parent gets the error, the session file stays.
|
|
171
179
|
completed = !stopped;
|
|
172
180
|
});
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// `/delegate <agent> [--model <provider/id>] [--thinking <level>]
|
|
5
5
|
// [--label <name>] [--auto-exit | --no-auto-exit] <task…>`
|
|
6
|
-
// reads the agent from `<agent dir>/agents/<name>/AGENT.md`, writes the task
|
|
6
|
+
// reads the agent from `<agent dir>/squire/agents/<name>/AGENT.md`, writes the task
|
|
7
7
|
// to the delegate's inbox through `message:send`, emits `session:launch` so
|
|
8
8
|
// listeners can add arguments and environment, then opens a window in the
|
|
9
9
|
// parent's working directory running the parent's own Pi. The window and the
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
// The `session:launch` contract this package provides, and the `message:*`
|
|
35
35
|
// hooks it consumes, live in this package's README.
|
|
36
36
|
import { randomUUID } from "node:crypto";
|
|
37
|
-
import { mkdtempSync, writeFileSync } from "node:fs";
|
|
37
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
38
38
|
import { tmpdir } from "node:os";
|
|
39
39
|
import { basename, join } from "node:path";
|
|
40
40
|
|
|
@@ -54,6 +54,10 @@ import { createTmuxClient, type TmuxClient } from "./tmux.ts";
|
|
|
54
54
|
import { createTracking, toolResult } from "./tracking.ts";
|
|
55
55
|
import { createProactive, createUsageClient, type Proactive, type UsageClient } from "./usage.ts";
|
|
56
56
|
|
|
57
|
+
/** The optional parent guidance file under `<agent dir>/squire/`. */
|
|
58
|
+
const PARENT_PROMPT = "delegation_prompt.md";
|
|
59
|
+
/** The parent system-prompt section this extension owns. */
|
|
60
|
+
const PARENT_SECTION = "squire_parent_delegation";
|
|
57
61
|
/** The hook a provider of `message:*` answers with the written request. */
|
|
58
62
|
const SEND = "message:send";
|
|
59
63
|
/** The hook emitted just before a delegate's window opens. */
|
|
@@ -325,13 +329,33 @@ function thinkingProblem(value: string): string | undefined {
|
|
|
325
329
|
}`;
|
|
326
330
|
}
|
|
327
331
|
|
|
332
|
+
/** Read `<agent dir>/squire/delegation_prompt.md`; missing or whitespace-only means no guidance. */
|
|
333
|
+
function readParentPrompt(): { text?: string; error?: string } {
|
|
334
|
+
const path = join(getAgentDir(), "squire", PARENT_PROMPT);
|
|
335
|
+
try {
|
|
336
|
+
const text = readFileSync(path, "utf8");
|
|
337
|
+
return text.trim() === "" ? {} : { text };
|
|
338
|
+
} catch (err) {
|
|
339
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
340
|
+
return {
|
|
341
|
+
error: `could not read ${path}: ${(err as Error).message}; parent delegation guidance is off for this run`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/** Whether this run exposes `delegate` after tool selection and hidden declarations. */
|
|
347
|
+
function delegateVisible(options: { selectedTools: string[]; hiddenTools: string[] }): boolean {
|
|
348
|
+
return options.selectedTools.includes("delegate") && !options.hiddenTools.includes("delegate");
|
|
349
|
+
}
|
|
350
|
+
|
|
328
351
|
/** The `delegate` tool's description: what delegation does, and the roster as it stands. */
|
|
329
352
|
function toolDescription(agents: readonly Agent[]): string {
|
|
330
353
|
const intro =
|
|
331
|
-
"Hand a self-contained task to a delegate Pi session in a background tmux window, so it works while you do not. Each call starts the delegation at once; the delegate runs in this session's working directory and agent root and reports back later as one message. While it runs, keep working or end your turn: its result starts your next turn on its own
|
|
354
|
+
"Hand a self-contained task to a delegate Pi session in a background tmux window, so it works while you do not. Each call starts the delegation at once; the delegate runs in this session's working directory and agent root and reports back later as one message. While it runs, keep working or end your turn: its result starts your next turn on its own. Do not poll delegation_status or send a blocking mail request merely to wait for the result; send a substantive follow-up only when you have something to add or ask. Use it when the work is self-contained and you would rather keep your own context for it.";
|
|
332
355
|
if (agents.length === 0) {
|
|
333
356
|
return `${intro}\n\nNo agents are defined yet. Add ${join(
|
|
334
357
|
getAgentDir(),
|
|
358
|
+
"squire",
|
|
335
359
|
"agents",
|
|
336
360
|
"<name>",
|
|
337
361
|
"AGENT.md",
|
|
@@ -411,6 +435,7 @@ export default function delegate(
|
|
|
411
435
|
throw new Error(
|
|
412
436
|
`no agents are defined; add ${join(
|
|
413
437
|
getAgentDir(),
|
|
438
|
+
"squire",
|
|
414
439
|
"agents",
|
|
415
440
|
"<name>",
|
|
416
441
|
"AGENT.md",
|
|
@@ -541,11 +566,13 @@ export default function delegate(
|
|
|
541
566
|
}
|
|
542
567
|
}
|
|
543
568
|
|
|
544
|
-
/** The
|
|
545
|
-
|
|
569
|
+
/** The roster description last registered, so unchanged metadata is not registered again. */
|
|
570
|
+
let registeredDescription: string | undefined;
|
|
571
|
+
|
|
572
|
+
const delegateTool = (description: string) => ({
|
|
546
573
|
name: "delegate",
|
|
547
574
|
label: "Delegate",
|
|
548
|
-
description
|
|
575
|
+
description,
|
|
549
576
|
parameters: Type.Object({
|
|
550
577
|
agent: Type.String({ description: "Roster agent name to run." }),
|
|
551
578
|
task: Type.String({
|
|
@@ -619,11 +646,32 @@ export default function delegate(
|
|
|
619
646
|
},
|
|
620
647
|
});
|
|
621
648
|
|
|
649
|
+
const registerDelegate = (description: string): void => {
|
|
650
|
+
pi.registerTool(delegateTool(description) as never);
|
|
651
|
+
registeredDescription = description;
|
|
652
|
+
};
|
|
653
|
+
|
|
622
654
|
pi.on("session_start", (_event, ctx) => {
|
|
623
655
|
registry = ctx.modelRegistry;
|
|
624
656
|
tracking.restore(ctx);
|
|
625
|
-
// The roster is read at call time; the description
|
|
626
|
-
|
|
657
|
+
// The roster is read at call time; the description is rebuilt here and before each run.
|
|
658
|
+
registerDelegate(toolDescription(currentRoster().agents));
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
// One roster/prompt snapshot prepares this run: refresh the description only when
|
|
662
|
+
// it changed, and replace only this extension's prompt section from the same snapshot.
|
|
663
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
664
|
+
const description = toolDescription(currentRoster().agents);
|
|
665
|
+
const prompt = readParentPrompt();
|
|
666
|
+
const visible = delegateVisible(event.systemPromptOptions);
|
|
667
|
+
// Report a read failure even when guidance cannot be used, so it does not go unseen.
|
|
668
|
+
if (prompt.error !== undefined) ctx.ui.notify(`delegate: ${prompt.error}`, "warning");
|
|
669
|
+
if (visible && description !== registeredDescription) registerDelegate(description);
|
|
670
|
+
if (!visible || prompt.error !== undefined || prompt.text === undefined) {
|
|
671
|
+
delete event.systemPromptOptions.sections[PARENT_SECTION];
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
event.systemPromptOptions.sections[PARENT_SECTION] = prompt.text;
|
|
627
675
|
});
|
|
628
676
|
|
|
629
677
|
pi.on("session_shutdown", () => {
|
|
@@ -680,6 +728,6 @@ export default function delegate(
|
|
|
680
728
|
},
|
|
681
729
|
});
|
|
682
730
|
|
|
683
|
-
|
|
731
|
+
registerDelegate(toolDescription(currentRoster().agents));
|
|
684
732
|
tracking.registerTools();
|
|
685
733
|
}
|
package/src/limits.ts
CHANGED
|
@@ -145,10 +145,12 @@ export function headerReset(response: Response | undefined, now = Date.now()): n
|
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
147
|
* Whether an error message (Pi's `<status>: <body>`) is a usage limit, and
|
|
148
|
-
* the reset time the text states, if any. Overloaded and 5xx
|
|
148
|
+
* the reset time the text states, if any. Overloaded, unavailable and 5xx
|
|
149
|
+
* errors never are: OpenCode Go sends "429: Endpoint is unavailable" for an
|
|
150
|
+
* outage, with quota left.
|
|
149
151
|
*/
|
|
150
152
|
export function classify(message: string, now = Date.now()): { resetAt?: number } | undefined {
|
|
151
|
-
if (/overloaded/i.test(message) || /^\s*5\d\d\b/.test(message)) return undefined;
|
|
153
|
+
if (/overloaded|unavailable/i.test(message) || /^\s*5\d\d\b/.test(message)) return undefined;
|
|
152
154
|
if (!LIMIT_PATTERN.test(message) && !/^\s*429\b/.test(message)) return undefined;
|
|
153
155
|
const resetAt = statedReset(message, now);
|
|
154
156
|
return resetAt !== undefined && resetAt > now ? { resetAt } : {};
|