pi-squire 0.1.0 → 0.2.1
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 +60 -1
- package/README.md +205 -118
- package/package.json +3 -7
- package/{child.ts → src/child.ts} +25 -1
- package/{index.ts → src/index.ts} +38 -230
- package/{results.ts → src/results.ts} +42 -10
- package/{tmux.ts → src/tmux.ts} +15 -5
- package/src/tracking.ts +310 -0
- /package/{agents.ts → src/agents.ts} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,63 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.1] - 2026-10-02
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- A delegate's task now arrives as a user prompt labelled `[delegate]` instead
|
|
14
|
+
of a `[mailbox]` message. Pi 1.0.0 started the injected message's turn
|
|
15
|
+
without the system prompt, so a fresh delegate saw no context files, skills
|
|
16
|
+
or role paragraph until after its first tool call, and could reject them as
|
|
17
|
+
an injection (earendil-works/pi#5581).
|
|
18
|
+
|
|
19
|
+
## [0.2.0] - 2026-10-02
|
|
20
|
+
|
|
21
|
+
### Breaking
|
|
22
|
+
|
|
23
|
+
- A model without configured credentials is refused, by `/delegate` and the
|
|
24
|
+
`delegate` tool alike, instead of failing in the delegate's window.
|
|
25
|
+
- A reply counts as a delegation's result only when it comes from the delegate
|
|
26
|
+
(`envelope.from` equals the delegation's id); a reply to its request from any
|
|
27
|
+
other sender is left to the provider as ordinary mail.
|
|
28
|
+
- Providers must set `envelope.from` to the sender's address.
|
|
29
|
+
- The `reply()` fixture in `test/poll.test.ts` now sends the delegation id as
|
|
30
|
+
`from` instead of `"d"`, to match.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- The sources moved to `src/`, so load the package directory (`pi -e .`), not
|
|
35
|
+
`index.ts`. Installs through npm or git are unaffected.
|
|
36
|
+
- Each check (a poll tick, a `delegation_status` call, a `delegation_close`
|
|
37
|
+
call) asks tmux once, however many delegations run, instead of once per
|
|
38
|
+
delegation.
|
|
39
|
+
- The appended system prompt reaches the delegate as a path to a private file
|
|
40
|
+
under the system temporary folder, so its text stays off the command line
|
|
41
|
+
and out of `ps`. A `session:launch` listener now sees that path where the
|
|
42
|
+
prompt text was. The argv tests in `test/delegate.test.ts` read the prompt
|
|
43
|
+
from the file instead of from argv, a deliberate behaviour change.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- `delegation_status` details name a labelled delegation's session
|
|
48
|
+
(`scout-roster`), as its text does, instead of `<agent>-<first 8 of the id>`.
|
|
49
|
+
- A recycled tmux window id is no longer mistaken for a delegate's window, nor
|
|
50
|
+
killed: a window counts as the delegate's only when its id and its name both
|
|
51
|
+
match.
|
|
52
|
+
- A delegate whose program exited in a window kept open (`remain-on-exit`)
|
|
53
|
+
reads as gone instead of running.
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- `session:launch` payloads carry the agent's name as `agent`.
|
|
58
|
+
- The README's Install section names its requirements: tmux 3.0 or later, Pi
|
|
59
|
+
0.80.5 or later, the first installable release with `agent_settled` (tested
|
|
60
|
+
with Pi 1.0.0), macOS and Linux, and
|
|
61
|
+
`pi install npm:pi-session-mail` as an example `message:*` provider.
|
|
62
|
+
- A README "Compatibility" section names the contracts other extensions rely
|
|
63
|
+
on, the below-1.0 rule that breaking one bumps the minor version, and what a
|
|
64
|
+
`message:*` provider must do.
|
|
65
|
+
|
|
9
66
|
## [0.1.0] - 2026-10-02
|
|
10
67
|
|
|
11
68
|
The first public release. Before this repository the package lived in its
|
|
@@ -22,5 +79,7 @@ author's dotfiles; the changes below are against that copy.
|
|
|
22
79
|
|
|
23
80
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
24
81
|
|
|
25
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.1
|
|
82
|
+
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.2.1...HEAD
|
|
83
|
+
[0.2.1]: https://github.com/gvanderclay/pi-squire/compare/v0.2.0...v0.2.1
|
|
84
|
+
[0.2.0]: https://github.com/gvanderclay/pi-squire/compare/v0.1.0...v0.2.0
|
|
26
85
|
[0.1.0]: https://github.com/gvanderclay/pi-squire/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,73 +1,114 @@
|
|
|
1
1
|
# pi-squire
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
`/delegate` command and the `delegate` tool.
|
|
3
|
+
Starts a delegate Pi session in a background tmux window and shows you its result.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
extensions can shape the launch, then opens a detached window running the
|
|
10
|
-
parent's own Pi in the parent's working directory and agent root. The window
|
|
11
|
-
and the child's Pi session share one name: `<agent>-<label>`, or
|
|
12
|
-
`<agent>-<first 8 characters of the id>` without a label. A label is at most
|
|
13
|
-
32 letters, digits, `_` or `-`, starting with a letter or digit; `-` rather
|
|
14
|
-
than `:` joins it, since tmux reads `:` in a target as session and window.
|
|
15
|
-
Labels need not be unique: the delegation id stays the handle for
|
|
16
|
-
`delegation_status`, `delegation_close` and mail. The delegate id is fresh, so a
|
|
17
|
-
delegate never reopens a live session, and it is also the delegate's mailbox
|
|
18
|
-
address: the task waits in its inbox and is delivered at session start, with
|
|
19
|
-
no handshake.
|
|
20
|
-
|
|
21
|
-
Starting a delegation adds nothing to the parent's context — the model sees
|
|
22
|
-
neither the command nor the task. The model can also start one itself with the
|
|
23
|
-
`delegate` tool, which starts the delegation at once without opening a
|
|
24
|
-
dialog, so several calls in one message each start their own;
|
|
25
|
-
`delegation_status` and `delegation_close` let it check on a delegation
|
|
26
|
-
and end one. When the delegate settles, its answer is taken over on
|
|
27
|
-
`message:inbound` and shown to the parent as one result message that quotes
|
|
28
|
-
the task. By default the delegate then closes its own window
|
|
29
|
-
([auto-exit](#auto-exit)); with auto-exit off the window stays open so you can
|
|
30
|
-
read it or keep working in it. Delegations are recorded in the parent session
|
|
31
|
-
without the task text.
|
|
32
|
-
|
|
33
|
-
Inside a delegate (`PI_DELEGATE_PARENT` set) the extension registers only
|
|
34
|
-
auto-exit's side, the `/auto-exit` command and its handlers, so a delegate
|
|
35
|
-
cannot start delegates of its own. Without a UI the command
|
|
36
|
-
and the `delegate` tool still work; it needs tmux and a provider of `message:*`, and refuses with a
|
|
37
|
-
message when either is missing.
|
|
5
|
+
You give a task to a named agent with the `/delegate` command, or the model does
|
|
6
|
+
it with the `delegate` tool. The delegate works in its own tmux window and
|
|
7
|
+
answers with one message, which the parent session shows as a result.
|
|
38
8
|
|
|
39
9
|
## Install
|
|
40
10
|
|
|
41
|
-
|
|
11
|
+
You need:
|
|
12
|
+
|
|
13
|
+
- Pi 0.80.5 or later, on macOS or Linux (not Windows). Pi 1.0.0 is the version
|
|
14
|
+
pi-squire is tested with. The floor is the first installable release with
|
|
15
|
+
the `agent_settled` event (added in 0.80.4, which was never published to
|
|
16
|
+
npm); `--exclude-tools` needs 0.77.0 and `--session-id`
|
|
17
|
+
0.76.0. The extension uses the `pi.events` bus, `pi.registerCommand` and
|
|
18
|
+
`pi.registerTool`; Pi supplies the peer dependencies
|
|
19
|
+
`@earendil-works/pi-coding-agent` and `typebox`.
|
|
20
|
+
- tmux 3.0 or later, with the Pi session running in a tmux pane. That is the
|
|
21
|
+
first version with `new-window -e`. A window is opened in the pane's
|
|
22
|
+
session, so a client attached elsewhere does not matter.
|
|
23
|
+
- A provider of `message:*`;
|
|
24
|
+
[pi-session-mail](https://github.com/gvanderclay/pi-session-mail) is one
|
|
25
|
+
example. Without a provider there is no way to send the task or get an
|
|
26
|
+
answer, so `/delegate` refuses rather than start a delegate that cannot
|
|
27
|
+
report back.
|
|
28
|
+
|
|
29
|
+
With pi-session-mail as the provider, for example, install both from npm:
|
|
42
30
|
|
|
43
31
|
```bash
|
|
32
|
+
pi install npm:pi-session-mail
|
|
44
33
|
pi install npm:pi-squire
|
|
45
34
|
```
|
|
46
35
|
|
|
47
|
-
To follow the latest commit instead, install it from GitHub:
|
|
36
|
+
To follow the latest commit of pi-squire instead, install it from GitHub:
|
|
48
37
|
|
|
49
38
|
```bash
|
|
50
39
|
pi install git:github.com/gvanderclay/pi-squire
|
|
51
40
|
```
|
|
52
41
|
|
|
53
|
-
The package has no runtime dependencies and no build step.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
-
|
|
66
|
-
|
|
67
|
-
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
42
|
+
The package has no runtime dependencies and no build step.
|
|
43
|
+
|
|
44
|
+
## First use
|
|
45
|
+
|
|
46
|
+
The package ships no agents, so create one first. `<agent dir>` is Pi's agent
|
|
47
|
+
directory: `~/.pi/agent`, or `PI_CODING_AGENT_DIR` when that is set. Save this
|
|
48
|
+
as `<agent dir>/agents/scout/AGENT.md`, replacing `provider/model-id` with a
|
|
49
|
+
model your Pi can use:
|
|
50
|
+
|
|
51
|
+
```markdown
|
|
52
|
+
---
|
|
53
|
+
description: Looks things up and answers in one message
|
|
54
|
+
model: provider/model-id
|
|
55
|
+
thinking: medium
|
|
56
|
+
exclude-tools: edit, write
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
You research one question at a time and answer concisely, citing the files
|
|
60
|
+
you read.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The roster is read at call time, so no reload is needed. Then, from a Pi
|
|
64
|
+
session running in tmux, hand it a task:
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
/delegate scout --label first Which files define the command-line entry point?
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A detached tmux window named `scout-first` opens in your working directory.
|
|
71
|
+
When the delegate finishes, its answer reaches the parent as one `[delegate]`
|
|
72
|
+
message that quotes the task (see [Results](#results)), and the delegate then
|
|
73
|
+
closes its own window ([auto-exit](#auto-exit)).
|
|
74
|
+
|
|
75
|
+
## How a delegation works
|
|
76
|
+
|
|
77
|
+
`/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 the task to
|
|
79
|
+
the delegate's inbox through `message:send`, emits `session:launch` so other
|
|
80
|
+
extensions can shape the launch, then opens a detached window running the
|
|
81
|
+
parent's own Pi in the parent's working directory and agent root. The agent's
|
|
82
|
+
prompt reaches the child as a path to a private file under the system
|
|
83
|
+
temporary folder, not as text on the command line.
|
|
84
|
+
|
|
85
|
+
- The window and the child's Pi session share one name: `<agent>-<label>`, or
|
|
86
|
+
`<agent>-<first 8 characters of the id>` without a label. A label is at most
|
|
87
|
+
32 letters, digits, `_` or `-`, starting with a letter or digit. A `-` joins
|
|
88
|
+
it rather than `:`, since tmux reads `:` in a target as session and window.
|
|
89
|
+
- Labels need not be unique. The delegation id stays the handle for
|
|
90
|
+
`delegation_status`, `delegation_close` and mail.
|
|
91
|
+
- The delegate id is fresh, so a delegate never reopens a live session. It is
|
|
92
|
+
also the delegate's mailbox address: the task waits in its inbox and is
|
|
93
|
+
delivered at session start, with no handshake.
|
|
94
|
+
- Starting a delegation adds nothing to the parent's context: the model sees
|
|
95
|
+
neither the command nor the task.
|
|
96
|
+
- The model can start one itself with the `delegate` tool, which starts the
|
|
97
|
+
delegation at once without opening a dialog, so several calls in one message
|
|
98
|
+
each start their own. `delegation_status` and `delegation_close` let it check
|
|
99
|
+
on a delegation and end one.
|
|
100
|
+
- When the delegate settles, its answer is taken over on `message:inbound` and
|
|
101
|
+
shown to the parent as one result message that quotes the task. By default
|
|
102
|
+
the delegate then closes its own window ([auto-exit](#auto-exit)). With
|
|
103
|
+
auto-exit off the window stays open so you can read it or keep working in it.
|
|
104
|
+
- Delegations are recorded in the parent session without the task text.
|
|
105
|
+
- Inside a delegate (`PI_DELEGATE_PARENT` set) the extension registers only
|
|
106
|
+
auto-exit's side, the `/auto-exit` command and its handlers, so a delegate
|
|
107
|
+
cannot start delegates of its own.
|
|
108
|
+
- Without a UI the command and the `delegate` tool still work. They need tmux
|
|
109
|
+
and a provider of `message:*`, and refuse with a message when either is
|
|
110
|
+
missing. In print mode (`pi -p`) Pi does not show the refusal notices, so
|
|
111
|
+
the refusal is silent there.
|
|
71
112
|
|
|
72
113
|
## Agents
|
|
73
114
|
|
|
@@ -75,8 +116,10 @@ The roster is read at call time from `<agent dir>/agents/<name>/AGENT.md`, one
|
|
|
75
116
|
directory per agent, so a new agent is usable without a reload. Frontmatter
|
|
76
117
|
carries `description`, `model` (`provider/id`) and `thinking`, and optionally
|
|
77
118
|
`auto-exit` (`true` or `false`, default `true`; see [Auto-exit](#auto-exit))
|
|
78
|
-
and `exclude-tools
|
|
79
|
-
Pi's own prompt
|
|
119
|
+
and `exclude-tools`. The body is the delegate's system prompt, appended to
|
|
120
|
+
Pi's own prompt; it reaches the child as a path to a private file under the
|
|
121
|
+
system temporary folder, so the text stays off the command line. The
|
|
122
|
+
[first-use example](#first-use) is a complete `AGENT.md`.
|
|
80
123
|
|
|
81
124
|
`exclude-tools` names the tools the delegate goes without, as a
|
|
82
125
|
comma-separated list (`exclude-tools: edit, write`) or a YAML list
|
|
@@ -85,42 +128,30 @@ comma-separated list (`exclude-tools: edit, write`) or a YAML list
|
|
|
85
128
|
otherwise turn on. It is a denylist only: without it the delegate gets every
|
|
86
129
|
tool, and no flag is passed. There is no per-call override. Each name is
|
|
87
130
|
checked against the tools the parent session has registered
|
|
88
|
-
(`pi.getAllTools()`), since Pi ignores an unknown name without a word
|
|
89
|
-
delegate's result reaches the parent through the `message:*` provider's reply
|
|
90
|
-
settles, not through a tool, so excluding tools cannot stop it
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
131
|
+
(`pi.getAllTools()`), since Pi ignores an unknown name without a word. The
|
|
132
|
+
delegate's result reaches the parent through the `message:*` provider's reply
|
|
133
|
+
when its run settles, not through a tool, so excluding tools cannot stop it
|
|
134
|
+
answering.
|
|
135
|
+
|
|
136
|
+
The body is followed by one fixed final-message line and a paragraph naming the
|
|
137
|
+
parent session's address. The delegate's task arrives as a message from that
|
|
138
|
+
session, and that message and every later one from the same address are
|
|
139
|
+
instructions to follow as given. Other sessions are colleagues: the delegate
|
|
140
|
+
may answer them, but checks with the parent before doing work the parent did
|
|
141
|
+
not ask for. The paragraph names no provider, package or tool, so it holds
|
|
99
142
|
whatever `message:*` provider delivers the task.
|
|
100
143
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
---
|
|
108
|
-
|
|
109
|
-
You research one question at a time and answer concisely, citing the files
|
|
110
|
-
you read.
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
An `AGENT.md` that cannot be used — missing fields, a model that is not
|
|
114
|
-
`provider/id`, an unknown thinking level, an `auto-exit` that is not `true` or
|
|
115
|
-
`false`, an `exclude-tools` that is not a list of names or names a tool the
|
|
116
|
-
session does not have, or an empty body — is left out of the
|
|
117
|
-
roster and named in a warning. With no agents at all, `/delegate` says how to
|
|
118
|
-
add one. The package ships no agents.
|
|
144
|
+
An `AGENT.md` that cannot be used is left out of the roster and named in a
|
|
145
|
+
warning. That covers missing fields, a model that is not `provider/id`, an
|
|
146
|
+
unknown thinking level, an `auto-exit` that is not `true` or `false`, an
|
|
147
|
+
`exclude-tools` that is not a list of names or names a tool the session does
|
|
148
|
+
not have, and an empty body. With no agents at all, `/delegate` says how to
|
|
149
|
+
add one.
|
|
119
150
|
|
|
120
151
|
## Configuration
|
|
121
152
|
|
|
122
153
|
None beyond the roster. `pi-squire` reads no settings file and no environment
|
|
123
|
-
variable of its own
|
|
154
|
+
variable of its own. `PI_DELEGATE_PARENT` and `PI_DELEGATE_AUTO_EXIT` are set
|
|
124
155
|
for the child, never read as configuration by the parent. The window starts
|
|
125
156
|
with the tmux server's environment plus only these from the parent:
|
|
126
157
|
`PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR` (when the parent has it
|
|
@@ -128,13 +159,16 @@ set), `PI_DELEGATE_PARENT` and `PI_DELEGATE_AUTO_EXIT`. Anything else the
|
|
|
128
159
|
parent process has, such as a `CLAUDE_CONFIG_DIR` set by a shell alias,
|
|
129
160
|
reaches the child only through a `session:launch` contributor. `--model` and
|
|
130
161
|
`--thinking` override the agent's defaults for one call, and an unknown value
|
|
131
|
-
is refused with close matches
|
|
162
|
+
is refused with close matches, and so is a model with no configured
|
|
163
|
+
credentials. An API key that exists only as an environment variable in the
|
|
164
|
+
parent's shell does not reach the delegate, so log in through Pi or make the
|
|
165
|
+
variable part of the tmux server's environment. `--auto-exit` and `--no-auto-exit` override the
|
|
132
166
|
agent's `auto-exit`.
|
|
133
167
|
|
|
134
168
|
## Tools
|
|
135
169
|
|
|
136
170
|
The model gets three tools. They share the command's launch path and the
|
|
137
|
-
session's delegation records
|
|
171
|
+
session's delegation records. None of them is registered inside a delegate.
|
|
138
172
|
|
|
139
173
|
| Tool | Parameters | What it does |
|
|
140
174
|
| --- | --- | --- |
|
|
@@ -142,9 +176,9 @@ session's delegation records; none of them is registered inside a delegate.
|
|
|
142
176
|
| `delegation_status` | optional `id` | Reports each delegation as running, done, closed or closed without a result, with the window, the delegate's session and the result envelope's path. |
|
|
143
177
|
| `delegation_close` | `id` | Kills the window through tmux and records the close, so `delegation_status` reports it closed. |
|
|
144
178
|
|
|
145
|
-
The `delegate` tool's description lists the current roster
|
|
146
|
-
description, default model and thinking, auto-exit when it is off,
|
|
147
|
-
excluded tools
|
|
179
|
+
The `delegate` tool's description lists the current roster, one line per agent:
|
|
180
|
+
its name, description, default model and thinking, auto-exit when it is off,
|
|
181
|
+
and its excluded tools.
|
|
148
182
|
|
|
149
183
|
```text
|
|
150
184
|
- scout — Read-only recon (default provider/model-id, thinking low, auto-exit off, no edit/write)
|
|
@@ -154,10 +188,11 @@ The model picks an agent from these lines without reading files. The
|
|
|
154
188
|
description also tells the model to turn `auto_exit` off when it means to keep
|
|
155
189
|
talking to the delegate by mail after its result. A tool's description is
|
|
156
190
|
fixed when it is registered, so the description is rebuilt at every session
|
|
157
|
-
start
|
|
158
|
-
session's tools while extensions load, so the description built then skips
|
|
159
|
-
|
|
160
|
-
|
|
191
|
+
start, and a roster change shows up in the next session. Pi cannot list the
|
|
192
|
+
session's tools while extensions load, so the description built then skips the
|
|
193
|
+
`exclude-tools` check, and the one built at session start applies it. The
|
|
194
|
+
roster itself is read at call time, and an unknown agent, model or thinking
|
|
195
|
+
level is refused with close matches and nothing starts.
|
|
161
196
|
|
|
162
197
|
`delegation_status` reports one state per delegation:
|
|
163
198
|
|
|
@@ -167,21 +202,21 @@ model or thinking level is refused with close matches and nothing starts.
|
|
|
167
202
|
<id> scout (provider/model, thinking low)
|
|
168
203
|
name: scout-<first 8 of the id>
|
|
169
204
|
state: running — the window is open and no result has arrived
|
|
170
|
-
window: scout-<id> (@1)
|
|
205
|
+
window: scout-<first 8 of the id> (@1)
|
|
171
206
|
auto-exit: on — the delegate closes its window after a normal completion unless the user took over there
|
|
172
207
|
session: <the delegate's session file, or its id when there is none yet>
|
|
173
208
|
|
|
174
209
|
<id> researcher (provider/other, thinking high)
|
|
175
210
|
name: researcher-<first 8 of the id>
|
|
176
211
|
state: done — result status: done
|
|
177
|
-
window: researcher-<id> (@2)
|
|
212
|
+
window: researcher-<first 8 of the id> (@2)
|
|
178
213
|
auto-exit: off — the window stays open after the result
|
|
179
214
|
session: <session file>
|
|
180
215
|
envelope: <the reply's path in the parent's cur/>
|
|
181
216
|
```
|
|
182
217
|
|
|
183
218
|
- **Name** is the delegate's Pi session name, its agent and the first 8
|
|
184
|
-
characters of its delegation id
|
|
219
|
+
characters of its delegation id.
|
|
185
220
|
- **Running** means the window is open and no result has arrived. An open
|
|
186
221
|
window is connection, not task state: the delegate may still be working, or
|
|
187
222
|
waiting for the user, and only a result means done.
|
|
@@ -195,13 +230,14 @@ model or thinking level is refused with close matches and nothing starts.
|
|
|
195
230
|
longer there. A recorded close wins over a result, and a result wins over a
|
|
196
231
|
window that is gone.
|
|
197
232
|
- **Closed without a result** is what the poll records when a window is gone
|
|
198
|
-
and no reply arrived (see Results). A result outranks it, so a
|
|
199
|
-
turns it into **done**; a close from `delegation_close` outranks
|
|
233
|
+
and no reply arrived (see [Results](#results)). A result outranks it, so a
|
|
234
|
+
late reply turns it into **done**; a close from `delegation_close` outranks
|
|
235
|
+
both.
|
|
200
236
|
- When tmux cannot be asked about a window, the state is `unknown`, never
|
|
201
237
|
`closed`, because a failed check is not evidence the window is gone.
|
|
202
238
|
|
|
203
239
|
`delegation_close` kills the window and records the close even when the
|
|
204
|
-
result has already arrived
|
|
240
|
+
result has already arrived. When the window is already gone it only records
|
|
205
241
|
the close. An unknown or already closed id returns a message and kills
|
|
206
242
|
nothing. The delegate's session file and result envelope stay on disk.
|
|
207
243
|
|
|
@@ -222,15 +258,13 @@ The parent resolves each delegation's auto-exit from the call
|
|
|
222
258
|
- `/auto-exit` inside the delegate turns it on again for the next normal
|
|
223
259
|
completion, whatever the launch said.
|
|
224
260
|
|
|
225
|
-
|
|
226
|
-
(README "Child lifecycle"); unlike it, the default is on, and there is no
|
|
227
|
-
`subagent_done` tool: auto-exit off only means the window stays.
|
|
261
|
+
There is no `subagent_done` tool: auto-exit off only means the window stays.
|
|
228
262
|
|
|
229
263
|
## Results
|
|
230
264
|
|
|
231
|
-
A delegate answers its task with one reply through the `message:*` provider.
|
|
232
|
-
reply over before the provider can inject it, and sends
|
|
233
|
-
message labelled delegate output:
|
|
265
|
+
A delegate answers its task with one reply through the `message:*` provider.
|
|
266
|
+
`pi-squire` takes that reply over before the provider can inject it, and sends
|
|
267
|
+
the parent one message labelled delegate output:
|
|
234
268
|
|
|
235
269
|
```
|
|
236
270
|
[delegate] Result from scout (provider/model), delegation <id>, request <request id>.
|
|
@@ -263,17 +297,19 @@ Delegate session: <session file or id>
|
|
|
263
297
|
- The footer counts the running delegations (`⇄ N running`), clears one entry
|
|
264
298
|
per result or close and hides itself at zero.
|
|
265
299
|
- Each result and each close is recorded in the parent session, and the
|
|
266
|
-
records are rebuilt synchronously at session start
|
|
267
|
-
recognises replies
|
|
268
|
-
closed
|
|
300
|
+
records are rebuilt synchronously at session start. A resume still
|
|
301
|
+
recognises replies, including one that arrived while the parent was
|
|
302
|
+
closed, and still reports a recorded close as closed. A result is shown
|
|
269
303
|
once and never again.
|
|
270
304
|
- Plain replies, and every request, are left to the provider.
|
|
271
305
|
|
|
272
306
|
While any delegation is running (no result, no close), the parent polls every
|
|
273
307
|
5 seconds; the timer does not keep the process alive and stops when nothing is
|
|
274
308
|
running and on `session_shutdown`. Each tick first emits `message:scan`, so the
|
|
275
|
-
provider claims waiting replies, and then
|
|
276
|
-
|
|
309
|
+
provider claims waiting replies, and then lists the tmux windows once and
|
|
310
|
+
matches each still-running delegation to a window by id and name (window ids
|
|
311
|
+
restart with every tmux server). A window whose program has exited counts as
|
|
312
|
+
gone. A delegation whose window is gone and still has no result after that
|
|
277
313
|
scan is recorded as closed without a result, a record separate from
|
|
278
314
|
`delegation_close`'s, and the parent gets one `[delegate]` message naming the
|
|
279
315
|
delegation, with the delegate session path when one is found, sent as a
|
|
@@ -292,13 +328,14 @@ that block.
|
|
|
292
328
|
|
|
293
329
|
### `session:launch`
|
|
294
330
|
|
|
295
|
-
`pi-squire` provides this hook. It emits `{ args, env }` after the task
|
|
296
|
-
sent and just before it opens the delegate's window.
|
|
331
|
+
`pi-squire` provides this hook. It emits `{ args, env, agent }` after the task
|
|
332
|
+
has been sent and just before it opens the delegate's window.
|
|
297
333
|
|
|
298
334
|
| Field | Meaning |
|
|
299
335
|
| --- | --- |
|
|
300
336
|
| `args` | the child `pi`'s complete argument list, launcher first |
|
|
301
337
|
| `env` | the child's environment, as `KEY`/`value` pairs |
|
|
338
|
+
| `agent` | the agent's name, as in `<agent dir>/agents/<name>/AGENT.md`; read it, do not change it |
|
|
302
339
|
|
|
303
340
|
Listeners may only append to `args` and add keys to `env`; there is no veto,
|
|
304
341
|
and `pi-squire` passes both to tmux unchanged. Do all work synchronously: the
|
|
@@ -324,12 +361,24 @@ provider's own README carries the contract.
|
|
|
324
361
|
### `message:inbound`
|
|
325
362
|
|
|
326
363
|
`pi-squire` consumes this hook for answers: a reply whose `in_reply_to` names a
|
|
327
|
-
recorded delegation's request is taken over
|
|
364
|
+
recorded delegation's request is taken over. `pi-squire` sets `handled`, so
|
|
328
365
|
the provider injects nothing, and shows the parent the result message above.
|
|
329
366
|
The reply's request copies fill the quoted task and its `cur/` path is the
|
|
330
|
-
envelope it names. A reply to a request this session did not delegate
|
|
331
|
-
|
|
332
|
-
|
|
367
|
+
envelope it names. A reply to a request this session did not delegate is left
|
|
368
|
+
to the provider.
|
|
369
|
+
|
|
370
|
+
Inside a delegate, `pi-squire` also takes over the task: a `request` whose
|
|
371
|
+
`from` is the parent's address. It sets `handled` and sends the task as a user
|
|
372
|
+
prompt labelled `[delegate]`, so the run starts the way a typed prompt does.
|
|
373
|
+
Pi 1.0.0 starts a turn for an injected custom message without preparing the
|
|
374
|
+
system prompt, so a fresh delegate's first request would otherwise carry no
|
|
375
|
+
context files, skills or role paragraph
|
|
376
|
+
([earendil-works/pi#5581](https://github.com/earendil-works/pi/issues/5581)).
|
|
377
|
+
The provider still arms the automatic answer for a request a listener took
|
|
378
|
+
over. Every other message to the delegate is left to the provider. `pi-squire` reads `from` and requires
|
|
379
|
+
it to be the sender's address, which for a delegate is its session id; a reply
|
|
380
|
+
from any other sender is left to the provider, even when it answers a recorded
|
|
381
|
+
request. The provider's README carries the contract.
|
|
333
382
|
|
|
334
383
|
`pi-squire` rebuilds its records synchronously in its own `session_start`, so it
|
|
335
384
|
relies on the provider not emitting `message:inbound` from inside a
|
|
@@ -349,3 +398,41 @@ written just before a window closed is never mistaken for no reply. **If
|
|
|
349
398
|
`scanned` is not set, nobody answered**, and the tick declares nothing closed,
|
|
350
399
|
because a waiting reply cannot be ruled out. The provider's README carries the
|
|
351
400
|
contract.
|
|
401
|
+
|
|
402
|
+
## Compatibility
|
|
403
|
+
|
|
404
|
+
While pi-squire is below 1.0, breaking any contract below bumps the minor
|
|
405
|
+
version and is listed under "Breaking" in [CHANGELOG.md](CHANGELOG.md).
|
|
406
|
+
|
|
407
|
+
Other extensions may rely on:
|
|
408
|
+
|
|
409
|
+
- the `session:launch` payload `{ args, env, agent }`, emitted synchronously
|
|
410
|
+
after the task is sent and before the window opens (see [Hooks](#hooks));
|
|
411
|
+
- the `message:send`, `message:inbound` and `message:scan` hooks, consumed as
|
|
412
|
+
their sections document;
|
|
413
|
+
- the `delegate`, `delegation_status` and `delegation_close` tool names and
|
|
414
|
+
parameters;
|
|
415
|
+
- the `/delegate` syntax and flags;
|
|
416
|
+
- the `[delegate]` result text and the closed-without-a-result text;
|
|
417
|
+
- `AGENT.md`'s frontmatter keys and its location,
|
|
418
|
+
`<agent dir>/agents/<name>/AGENT.md`;
|
|
419
|
+
- the child environment variables `PI_DELEGATE_PARENT` and
|
|
420
|
+
`PI_DELEGATE_AUTO_EXIT`.
|
|
421
|
+
|
|
422
|
+
A `message:*` provider must:
|
|
423
|
+
|
|
424
|
+
- implement `message:send`, `message:inbound` and `message:scan` as documented
|
|
425
|
+
under [Hooks](#hooks);
|
|
426
|
+
- set a reply's `from` to the sender's address, so the delegate's reply can be
|
|
427
|
+
matched to its task;
|
|
428
|
+
- reply on its own when the delegate's run settles, since that reply is what
|
|
429
|
+
becomes the result.
|
|
430
|
+
|
|
431
|
+
## Contributing
|
|
432
|
+
|
|
433
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) to set up, run the tests and open a pull
|
|
434
|
+
request. Changes by release are in [CHANGELOG.md](CHANGELOG.md).
|
|
435
|
+
|
|
436
|
+
## License
|
|
437
|
+
|
|
438
|
+
MIT, as stated in [LICENSE](LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-squire",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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",
|
|
@@ -24,15 +24,11 @@
|
|
|
24
24
|
"packageManager": "pnpm@10.33.0",
|
|
25
25
|
"pi": {
|
|
26
26
|
"extensions": [
|
|
27
|
-
"./index.ts"
|
|
27
|
+
"./src/index.ts"
|
|
28
28
|
]
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
|
-
"
|
|
32
|
-
"child.ts",
|
|
33
|
-
"agents.ts",
|
|
34
|
-
"tmux.ts",
|
|
35
|
-
"results.ts",
|
|
31
|
+
"src",
|
|
36
32
|
"README.md",
|
|
37
33
|
"LICENSE",
|
|
38
34
|
"CHANGELOG.md"
|
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
// taking over — typing here, or stopping a run — turns it off for the rest of
|
|
9
9
|
// the session, with a notice; `/auto-exit` turns it back on. Modelled on
|
|
10
10
|
// edxeth/pi-subagents' `auto-exit` (README "Child lifecycle", `cf6dbf4`).
|
|
11
|
+
//
|
|
12
|
+
// It also takes over the parent's task request on `message:inbound` and sends
|
|
13
|
+
// it as a user prompt. Pi 1.0.0 starts a custom-message turn without preparing
|
|
14
|
+
// the system prompt, so a fresh delegate's first request would carry no
|
|
15
|
+
// AGENTS.md, skills or role addendum (earendil-works/pi#5581). The provider
|
|
16
|
+
// still arms the automatic answer for a request a listener took over.
|
|
17
|
+
// ponytail: drop the takeover once #5581 lands and the minimum Pi has it.
|
|
11
18
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
19
|
|
|
13
20
|
/** The launch variable that carries the parent's resolved setting. */
|
|
@@ -19,7 +26,24 @@ const ON_NOTICE = "delegate: auto-exit is on; this delegate closes after its nex
|
|
|
19
26
|
|
|
20
27
|
type Message = { role?: string; stopReason?: unknown };
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
type Inbound = { envelope?: { from?: unknown; kind?: unknown; body?: unknown }; handled?: boolean };
|
|
30
|
+
|
|
31
|
+
export function registerChild(pi: ExtensionAPI, parent: string): void {
|
|
32
|
+
let current: ExtensionContext | undefined;
|
|
33
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
34
|
+
current = ctx;
|
|
35
|
+
});
|
|
36
|
+
pi.events.on("message:inbound", (data) => {
|
|
37
|
+
const payload = data as Inbound;
|
|
38
|
+
const envelope = payload?.envelope;
|
|
39
|
+
if (payload.handled === true || envelope?.kind !== "request" || envelope.from !== parent) return;
|
|
40
|
+
payload.handled = true;
|
|
41
|
+
const text = `[delegate] Your task, from the session that started you (${parent}). Your final message this turn goes back to it automatically.\n\n${String(envelope.body ?? "")}`;
|
|
42
|
+
Promise.resolve(pi.sendUserMessage(text, { deliverAs: "steer" })).catch((err) =>
|
|
43
|
+
current?.ui.notify(`delegate: could not start the task: ${(err as Error).message}`, "error"),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
23
47
|
let armed = process.env[AUTO_EXIT_ENV] === "1";
|
|
24
48
|
/** Whether the run that just ended finished normally: not stopped, not failed. */
|
|
25
49
|
let completed = false;
|