@wassname2/pi-supervise 0.0.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/README.md +264 -0
- package/package.json +56 -0
- package/src/index.ts +982 -0
- package/src/prompts.ts +282 -0
- package/src/protocol.ts +110 -0
- package/src/subagents.ts +38 -0
- package/src/view.ts +302 -0
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# pi-intercom-supervisor
|
|
2
|
+
|
|
3
|
+
Original ask:
|
|
4
|
+
> https://github.com/monotykamary/pi-supervisor but with pi-intercom and linked to matrix or telegram
|
|
5
|
+
> - wassname
|
|
6
|
+
|
|
7
|
+
[monotykamary/pi-supervisor](https://github.com/monotykamary/pi-supervisor) runs the supervisor as a
|
|
8
|
+
hidden in-memory session. This runs it as a second real pi session and uses
|
|
9
|
+
[pi-intercom](https://github.com/tintinweb/pi-intercom) as the wire between the two. You can see the
|
|
10
|
+
supervisor, talk to it, and bridge it to your phone with any chat extension, because it is just a pi
|
|
11
|
+
session. That swap deleted the context pipeline, the four reframe tiers, the JSON verdict parser,
|
|
12
|
+
the widget and the plugin API: 5201 lines of `src` down to 1712, tests excluded on both sides.
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
pi install https://github.com/wassname/pi-intercom-supervisor # needs npm:pi-intercom too
|
|
16
|
+
|
|
17
|
+
# terminal 1, the worker # terminal 2, the supervisor
|
|
18
|
+
pi pi
|
|
19
|
+
/supervise make the results table
|
|
20
|
+
> do the work
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
You type one command, in one session. That session becomes the supervisor and its target becomes
|
|
24
|
+
the worker, so the role is decided at pairing time and neither session is one until then. The
|
|
25
|
+
worker needs no command, only the extension loaded, and it prints `supervised by <id>: <goal>` so
|
|
26
|
+
you can see which one was picked.
|
|
27
|
+
|
|
28
|
+
`/supervise` takes the only free session in this directory, so the whole line is the goal. With
|
|
29
|
+
more than one it shows a picker, since guessing is worse than asking. Run `/name worker`
|
|
30
|
+
in the one you want and then `/supervise @worker make the table`, or use `@` and the id it printed,
|
|
31
|
+
which is the start of the `pi --session <id>` line in that terminal. The `@` is what keeps a goal
|
|
32
|
+
with spaces in it out of the target: a first word that names no session is refused, where it used
|
|
33
|
+
to be folded into the goal without a word. `/supervise stop` ends it.
|
|
34
|
+
|
|
35
|
+
A goal that is one word with a slash or a dot in it is a path, and the file is the goal:
|
|
36
|
+
`/supervise docs/GOAL.md`, or `/supervise goal docs/GOAL.md` to change it later. A goal worth
|
|
37
|
+
grading against runs to paragraphs of acceptance evidence, and retyping it each run is how the copy
|
|
38
|
+
you steer by drifts from the copy you grade by. A missing file stops the pairing and names the path.
|
|
39
|
+
|
|
40
|
+
`/supervise goal <new goal>` changes the goal in place. The worker hears it, a fresh view follows,
|
|
41
|
+
and the supervisor keeps its memory of the steers it has already sent, which stopping and pairing
|
|
42
|
+
again would throw away.
|
|
43
|
+
|
|
44
|
+
A supervisor that comes back from a crash, a credit failure or a `/reload` says the goal and the
|
|
45
|
+
answer shape again, then asks the worker for a view, so restarting the session is the whole
|
|
46
|
+
recovery. Only the worker makes views, and a supervisor holding a stale one answers from the stale
|
|
47
|
+
one, which is how it invents a fact. `/supervise look` does the same by hand if you want it now.
|
|
48
|
+
The repeat matters because `/reload` is how a changed prompt reaches a running session, and the
|
|
49
|
+
full brief goes out only at pairing.
|
|
50
|
+
|
|
51
|
+
A live pairing puts two words in the footer, `👁 watching 3` in the supervisor and `👁 watched` in
|
|
52
|
+
the worker, where 3 is the instruction count. The notice printed at pairing scrolls away, and after
|
|
53
|
+
that a paired session looks like any other prompt, which is how you end up staring at a supervisor
|
|
54
|
+
that stopped supervising an hour ago. It stays that short because `pi-powerline-footer` appends it
|
|
55
|
+
to its own line for the whole session, unlike a status that shows only during a run. Same mechanism as
|
|
56
|
+
[@diegopetrucci/pi-oracle](https://www.npmjs.com/package/@diegopetrucci/pi-oracle) uses for its
|
|
57
|
+
runs.
|
|
58
|
+
|
|
59
|
+
With no target named, `/supervise` asks rather than guesses: it broadcasts a roll call and waits
|
|
60
|
+
half a second. A session knows things about itself that nothing outside it can see, so each one
|
|
61
|
+
answers for itself. A `pi-subagents` child run stays quiet because it reads `PI_SUBAGENT_CHILD` in
|
|
62
|
+
its own environment, a session already paired stays quiet because it is taken, and a registration
|
|
63
|
+
whose process has gone cannot answer at all.
|
|
64
|
+
|
|
65
|
+
One session answering is the ordinary case and it pairs with no question asked. Otherwise you get a
|
|
66
|
+
picker, and every session in the directory is on it, including the quiet ones, marked. Excluding
|
|
67
|
+
them outright reads as `0 free sessions` and leaves you nowhere, which is what happened the first
|
|
68
|
+
time this shipped: the worker was running an older copy of this extension and could not answer a
|
|
69
|
+
roll call it had never heard of.
|
|
70
|
+
|
|
71
|
+
Guessing from the outside is what this replaces, and every version of it was wrong. The name is no
|
|
72
|
+
test: `pi-subagents` sets an id starting `subagent` only when it passes an intercom target, and
|
|
73
|
+
`pi-intercom` calls any unnamed session `subagent-chat-<id>`. The process tree is no test either: a
|
|
74
|
+
child pi is often started through an intermediate `node` process (`pi-subagents`
|
|
75
|
+
`async-execution.ts:459`), which breaks the pi-to-pi parent chain the check looked for. That left
|
|
76
|
+
five child runs offered as workers in one directory.
|
|
77
|
+
|
|
78
|
+
Naming a target with `@` skips the roll call, because you named it. The pair acknowledgement is
|
|
79
|
+
then the test of whether it can take the job.
|
|
80
|
+
|
|
81
|
+
Working on this extension: `pi install /path/to/pi-supervise` points pi at the working tree, so
|
|
82
|
+
`/reload` picks up an edit with no commit and no push.
|
|
83
|
+
|
|
84
|
+

|
|
85
|
+
|
|
86
|
+
The supervisor tools are hidden in a session that is not supervising. A worker that can see
|
|
87
|
+
`steer` and `worker_view` starts guessing: one here, given an ordinary coding task, spent twelve
|
|
88
|
+
turns reasoning "these are supervisor tools ... so I might be the supervisor", called `worker_view`,
|
|
89
|
+
and took "the worker has not stopped since pairing" as proof of a pairing it never had.
|
|
90
|
+
|
|
91
|
+
Both sessions load this extension and pick their role at runtime. The worker runs as ordinary pi.
|
|
92
|
+
When it stops, the supervisor gets a view of it and calls `steer` with one concrete next action, or
|
|
93
|
+
`done`. If it needs a human it replies in plain text instead of calling a tool, and that is what
|
|
94
|
+
reaches your phone. Supervising takes the writing tools off that session, `bash` and the edit tools,
|
|
95
|
+
and gives them back when supervision ends: it shares a working directory with the worker, and two
|
|
96
|
+
agents writing the same files is not supervision. It keeps `read` and `grep`, because checking a
|
|
97
|
+
claim against a file is the job. Policy comes from `<cwd>/.pi/SUPERVISOR.md`, then `<agent dir>/SUPERVISOR.md`,
|
|
98
|
+
then a built-in default, same precedence as the original, so an existing file keeps working. A
|
|
99
|
+
verdict here is a tool call, so nothing can fail to parse.
|
|
100
|
+
|
|
101
|
+
## How it works
|
|
102
|
+
|
|
103
|
+
The pi-intercom extension channel carries the data. It never enters a transcript and never starts a
|
|
104
|
+
turn, so each side triggers its own turn locally with `pi.sendUserMessage`, and the steer lands in
|
|
105
|
+
the worker's user and assistant trajectory where it belongs. Pairing is on `fromSessionId`, which
|
|
106
|
+
the broker stamps from its own registry, so a payload cannot forge it. The broker has no socket
|
|
107
|
+
authentication, so the trust level is any process running as you, and every session that loads this
|
|
108
|
+
extension sees every message. Do not load it in a session you would not trust with the transcript.
|
|
109
|
+
|
|
110
|
+
Each view carries only the turns since the last one, the way you read the new lines on a screen
|
|
111
|
+
rather than the scrollback. The supervisor is a real session that keeps every view it has read, so
|
|
112
|
+
re-sending the whole transcript would put a second copy of its own context in front of it, and the
|
|
113
|
+
cost would grow with every review. A one-line goal repeats in full. A multi-line goal shows its
|
|
114
|
+
first line with `[...]`; its complete rubric enters the supervisor context at pairing, goal change,
|
|
115
|
+
reload, supervisor compaction, and before every fifth review. A worker compaction restarts the
|
|
116
|
+
view's turn count, and the view says so and carries the summary.
|
|
117
|
+
|
|
118
|
+
The view body is [pi-vcc](https://github.com/sting8k/pi-vcc)'s `compile()`, the same algorithmic
|
|
119
|
+
compactor (no LLM calls) you can run as your own. On top of it the view carries what a compactor
|
|
120
|
+
has no reason to track: tool calls with no result, child pi processes, and whether anything
|
|
121
|
+
changed since the last review.
|
|
122
|
+
|
|
123
|
+
pi-vcc drops the worker's reasoning, which you do see on screen, so this keeps the last two blocks
|
|
124
|
+
by rewriting them as text before compiling. They stay where they happened, next to the tool call
|
|
125
|
+
each one produced, because that is the order you read a session in. A worker going in circles says
|
|
126
|
+
so there first, while it names the approach it is about to retry, before any file or commit
|
|
127
|
+
changes. Two, cut to the last 400 characters each: one worker session here held 161 reasoning
|
|
128
|
+
blocks, and all of them together would be a second transcript.
|
|
129
|
+
|
|
130
|
+
The complete goal is never cut when it enters the supervisor context. A multi-line goal uses only
|
|
131
|
+
its first line plus `[...]` in ordinary views; that is a locator, while the complete rubric arrives
|
|
132
|
+
at the fixed review cadence. The byte limit trims the transcript instead.
|
|
133
|
+
|
|
134
|
+
Every view names the worker's model and how full its context is, read off the worker's own intercom
|
|
135
|
+
presence record. Steering a small fast model wants smaller steps than steering a frontier one, and
|
|
136
|
+
a worker near the top of its context is about to compact and lose detail.
|
|
137
|
+
|
|
138
|
+
Resume and `/reload` both restore the goal and the round count out of the transcript, which is
|
|
139
|
+
where they belong. The paired session ID is not restored that way, because it addresses a live
|
|
140
|
+
process that may have exited while you were away. Each side asks the broker who is actually
|
|
141
|
+
connected and then says which it is: still supervising, or dropped because the other session is
|
|
142
|
+
gone. A resumed supervisor also has its writing tools taken off again, which the `/supervise`
|
|
143
|
+
handler alone would not do.
|
|
144
|
+
|
|
145
|
+
The worker sends a view when it is paired, again whenever it stops, and every half hour in between.
|
|
146
|
+
The pairing one exists because a worker paired while it sits at the prompt never settles, so waiting
|
|
147
|
+
for its first stop can mean waiting for ever.
|
|
148
|
+
|
|
149
|
+
Only the view starts a supervisor turn. The brief, the reanchor after a reload and a goal change
|
|
150
|
+
all say "a view follows", so none of them is a thing to judge, and each goes in as a custom message
|
|
151
|
+
with `triggerTurn: false` (`pi-coding-agent/core/agent-session.d.ts:398`). They are in the context
|
|
152
|
+
and they start nothing. As a user message the brief was a turn, and a supervisor holding three
|
|
153
|
+
verdict tools and no view still answers it: 98 `let_it_run` calls in one turn, once every two
|
|
154
|
+
seconds, each over "waiting for the first view". No wording fixed that, because the turn should not
|
|
155
|
+
have existed.
|
|
156
|
+
|
|
157
|
+
Every verdict result has to name a way to end the turn. A turn ends when the assistant writes text
|
|
158
|
+
and calls no tool, so a result that does not say so leaves another tool call as the only move, and
|
|
159
|
+
one that forbids text ("Say nothing more until the next view arrives") forbids the exit outright.
|
|
160
|
+
That cost a spare verdict on every look of an overnight run: a true reason ("job 23 progressing
|
|
161
|
+
normally, 438/600 steps"), then a second true reason two seconds later. A second `let_it_run` in one
|
|
162
|
+
look is answered rather than cut, saying the verdict is already recorded and naming the way out. It
|
|
163
|
+
is not an error, because nothing happened: `let_it_run` reaches nobody.
|
|
164
|
+
|
|
165
|
+
A runaway is still cut with pi's own `ctx.abort()`, past five verdicts in one look, which no
|
|
166
|
+
sign-off explains and one run reached 645 of. Words in a tool result cannot stop a loop,
|
|
167
|
+
because the loop is what reads them. The result is still recorded, since the agent loop pushes tool
|
|
168
|
+
results before it streams the next assistant message (`pi-agent-core/agent-loop.js:124-129`). "One
|
|
169
|
+
look" means one view: the count resets when a view arrives as well as on `agent_start`, because a
|
|
170
|
+
view that lands while the supervisor is busy is queued as a follow up, and a follow up runs inside
|
|
171
|
+
the agent loop already going, so `agent_start` does not fire again.
|
|
172
|
+
|
|
173
|
+
A stop and a check in ask for different things. A stop is a decision point. A check in leans on `let_it_run`,
|
|
174
|
+
because interrupting a working agent costs it its train of thought. The original also ran two
|
|
175
|
+
mechanical checks mid-turn, five tool errors in a row and five reads of one file with no edit.
|
|
176
|
+
Those are deleted. A failed tool result appears once in the fresh VCC turns, rather than becoming
|
|
177
|
+
persistent state. Ported and kept: the process tree check (`src/subagents.ts`, `ps` for child pi
|
|
178
|
+
processes, so a settled worker with a subagent still running is not called finished) and the
|
|
179
|
+
SUPERVISOR.md precedence.
|
|
180
|
+
|
|
181
|
+
`src/prompts.ts` holds every word the supervisor reads, in the order it reads them. Only the nudge
|
|
182
|
+
and the view repeat per look; the policy and verdict rules are sent once. A multi-line goal returns
|
|
183
|
+
at pairing, goal change, reload, compaction, and before every fifth review. The verdict rules live
|
|
184
|
+
in the tool descriptions, which the API sends at every model call, so a supervisor compaction
|
|
185
|
+
cannot lose them.
|
|
186
|
+
|
|
187
|
+
The process check is a snapshot where the original polls for two minutes. pi awaits the settle
|
|
188
|
+
handler, so polling there holds the worker's own settle for the whole poll. The loop already does
|
|
189
|
+
the waiting: the supervisor sees the process listed, `done` is refused, and it steers the worker to
|
|
190
|
+
wait instead. That puts the waiting in the transcript where you can read it.
|
|
191
|
+
|
|
192
|
+
## Decisions
|
|
193
|
+
|
|
194
|
+
No round cap, no budget, no automatic stop. Supervision runs until you type `/supervise stop` or
|
|
195
|
+
the supervisor calls `done` on evidence. Ending early is the failure this exists to prevent: at a
|
|
196
|
+
fixed model, scaffolds that keep re-prompting scored 8.7% on MLE-bench against 0.8% for scaffolds
|
|
197
|
+
that let the model stop ([arXiv:2410.07095](https://arxiv.org/abs/2410.07095)). A cap would be a
|
|
198
|
+
competing stopping objective.
|
|
199
|
+
|
|
200
|
+
There are three verdicts, not two. `let_it_run` was added after watching a real run: with only
|
|
201
|
+
`steer` and `done` on offer, a supervisor with nothing to say wrote "the harness demands a tool
|
|
202
|
+
call ... the least-bad option is a steer that adds something new", ran a command that printed
|
|
203
|
+
nothing, and reported the job was at "turn 47 of 80". The log said 75 of 80. Forcing a verdict at
|
|
204
|
+
every look is what bought that number, so now the usual answer at a check in is to say nothing.
|
|
205
|
+
It was called `wait` until wassname read one and could not tell whether the supervisor had chosen
|
|
206
|
+
to wait or the harness was waiting on something.
|
|
207
|
+
|
|
208
|
+
Two guards remain and both prevent a false ending. `steer` is refused while no goal is set, so the
|
|
209
|
+
supervisor asks you or calls `set_goal`, which sends the goal to the worker and heads every later
|
|
210
|
+
view. `done` is refused while a tool call has no result or a child pi process is running. Neither is
|
|
211
|
+
as strong as it sounds. The first stops an ungrounded instruction, not an ungrounded goal, since the
|
|
212
|
+
supervisor can call `set_goal` with something vague; quoting it to you is the only real check. The
|
|
213
|
+
second proves no tracked work is missing, not that nothing is running.
|
|
214
|
+
|
|
215
|
+
Against a supervisor that circles, the view reports how many reviews in a row produced no new file
|
|
216
|
+
or commit, and an instruction that reuses a recent one's vocabulary comes back named. Neither
|
|
217
|
+
stops anything. Word overlap runs about 0.44 on a rewording against under 0.2 on two different
|
|
218
|
+
instructions, and 0 on a paraphrase sharing no words, so it is a floor on repetition, not a bound.
|
|
219
|
+
|
|
220
|
+
Watching is cheap enough to put a good model on it. One 16 hour pairing on the same model both
|
|
221
|
+
sides, deepseek-v4-pro, 75 looks and 23 steers: the worker spent $16.11 over 789 model calls and
|
|
222
|
+
the supervisor $0.38 over 182, about 40x less. The supervisor reads a lot and writes almost
|
|
223
|
+
nothing, and most of what it reads is a cache hit at $0.004/Mtok against $0.44 for fresh input.
|
|
224
|
+
|
|
225
|
+
So the reason to keep its context short is not the bill, it is that a model judging one view should
|
|
226
|
+
not read 234k tokens to do it. Before each call, a look older than the last three loses its view
|
|
227
|
+
body and its thinking, and keeps its verdict. The verdicts are the supervisor's running notes on the
|
|
228
|
+
worker and they are 7% of the characters; the stale views and thinking are 88%. On that session the
|
|
229
|
+
pruner holds the context near 25k instead of climbing, 82% off at the last look.
|
|
230
|
+
`npx tsx scripts/measure-prune.ts <session.jsonl>` replays a real session and prints the table.
|
|
231
|
+
|
|
232
|
+
The other saving is not asking at all. A timer look at a worker that has done nothing since the last
|
|
233
|
+
look shows the supervisor a view it has already read, which was 13 of that session's 92 verdicts.
|
|
234
|
+
Those looks are skipped, comparing the view without its status line, which carries a clock. After
|
|
235
|
+
three skips one goes out anyway, because a worker that has not moved in two hours is worth seeing.
|
|
236
|
+
|
|
237
|
+
A stopped worker is the exception: it is always reported, and the timer keeps running for it. The
|
|
238
|
+
timer used to stop when the worker did, on the reasoning that a stopped worker cannot change. That
|
|
239
|
+
is true and beside the point, since a stopped worker is the state an instruction is for. It cost one
|
|
240
|
+
run two and a half hours of silence, `let_it_run` on a stopped worker with nothing left to raise it
|
|
241
|
+
again, so the result of `let_it_run` now says in words that a stopped worker stays stopped.
|
|
242
|
+
|
|
243
|
+
Nor is a human message in the worker a handover. They say a word and go to bed, so the brief, the
|
|
244
|
+
stopped nudge and the `let_it_run` description all say that the human being present is never a
|
|
245
|
+
reason to stand back. Every status line carries `no new turn for 2h27m`, measured from the worker's
|
|
246
|
+
last session entry, which covers both sitting at the prompt and hanging inside a command that never
|
|
247
|
+
returns. `RESEARCH_JOURNAL.md` has the sessions these came out of.
|
|
248
|
+
|
|
249
|
+
## Limits
|
|
250
|
+
|
|
251
|
+
The `done` guard sees a child process named `pi` directly under the worker, so it misses a detached
|
|
252
|
+
job, a queue, a training run, and a subagent started through an intermediate `node` process. Between
|
|
253
|
+
looks the supervisor is blind, so half an hour is the worst case for spotting a wrong path. Shorten
|
|
254
|
+
`WATCH_INTERVAL_MS` to pay more for a closer eye. Views are cut to 15 KB, oldest turns first,
|
|
255
|
+
because the broker drops anything over 16 KiB and never tells the extension. The last pair wins and
|
|
256
|
+
nothing authenticates it. The stagnation count lives in memory and restarts with the worker.
|
|
257
|
+
|
|
258
|
+
## Testing
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
npm test # 93 tests, free apart from a ps call
|
|
262
|
+
npx tsx scripts/e2e.ts # two real pi processes and a real model, costs cents
|
|
263
|
+
PI_SUPERVISOR_DEBUG=1 pi ... # trace the wire to stderr, since the channel is invisible
|
|
264
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wassname2/pi-supervise",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "pi extension: supervised pi sessions (renamed to pi-intercom-supervisor, kept for git: install)",
|
|
5
|
+
"author": "wassname",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/wassname/pi-supervise.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/wassname/pi-supervise#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/wassname/pi-supervise/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"pi-package",
|
|
17
|
+
"pi",
|
|
18
|
+
"pi-extension",
|
|
19
|
+
"supervisor",
|
|
20
|
+
"pi-supervise",
|
|
21
|
+
"multi-agent"
|
|
22
|
+
],
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./src/index.ts"
|
|
26
|
+
],
|
|
27
|
+
"image": "https://cdn.jsdelivr.net/gh/wassname/pi-intercom-supervisor@main/media/screenshot.png"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"src",
|
|
31
|
+
"!src/*.test.ts",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"pretest": "tsx scripts/make-fixture.ts",
|
|
39
|
+
"test": "tsx --test src/*.test.ts"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@sting8k/pi-vcc": ">=0.5.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
46
|
+
"pi-intercom": "*",
|
|
47
|
+
"typebox": "*"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@earendil-works/pi-coding-agent": "^0.84.1",
|
|
51
|
+
"pi-intercom": "^0.10.0",
|
|
52
|
+
"tsx": "^4.20.0",
|
|
53
|
+
"typebox": "^1.3.12"
|
|
54
|
+
},
|
|
55
|
+
"license": "MIT"
|
|
56
|
+
}
|