@tshu-w/pi-agents 0.1.0 → 0.1.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/bin/daemon.mjs +1 -1
- package/docs/WORKBENCH.md +4 -2
- package/package.json +7 -5
- package/src/daemon/sessions.mjs +7 -0
- package/src/host/client.mjs +1 -1
- package/src/workbench.mjs +31 -12
package/bin/daemon.mjs
CHANGED
|
@@ -152,7 +152,7 @@ function openWorkbench(channel) {
|
|
|
152
152
|
channel.onMessage(message => {
|
|
153
153
|
if (typeof message?.id !== 'string') return;
|
|
154
154
|
if (message.type === 'interrupt') channels.get(message.id)?.send({ type: 'interrupt', id: message.id });
|
|
155
|
-
else if (message.type === 'show' || message.type === 'hide') {
|
|
155
|
+
else if (message.type === 'show' || message.type === 'hide' || message.type === 'see') {
|
|
156
156
|
sessions[message.type](message.id);
|
|
157
157
|
changed();
|
|
158
158
|
}
|
package/docs/WORKBENCH.md
CHANGED
|
@@ -106,7 +106,8 @@ still running (`●`) or has exited (`○`), and the color marks a finished (gre
|
|
|
106
106
|
or failed (red) turn, or a dialog waiting for the user (yellow). The header
|
|
107
107
|
shows Pi's logo, the size of each group, and key hints.
|
|
108
108
|
|
|
109
|
-
A turn counts as seen once it has been shown in an attached terminal
|
|
109
|
+
A turn counts as seen once it has been shown in an attached terminal or a
|
|
110
|
+
preview. Sessions
|
|
110
111
|
from before the workbench start out seen. A Session that holds its lock but is
|
|
111
112
|
not reporting to the daemon, as while its Pi exits, shows as unknown (`?`); the
|
|
112
113
|
daemon checks it again every second until that changes.
|
|
@@ -127,7 +128,8 @@ Keys:
|
|
|
127
128
|
- `Esc` clears a typed message; with none typed, it closes the preview.
|
|
128
129
|
- `Ctrl+X` stops the current turn, as `app.interrupt` does.
|
|
129
130
|
- `Ctrl+D` removes the selected Session from the list. Its file stays, and
|
|
130
|
-
`/resume` brings it back.
|
|
131
|
+
`/resume` brings it back. An idle Session in a host that no terminal is
|
|
132
|
+
attached to quits first; any other running Session must quit itself.
|
|
131
133
|
- `Ctrl+Z` (`pi-agents.detach`) exits the workbench, as detaching leaves a
|
|
132
134
|
Session; every Session keeps running.
|
|
133
135
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tshu-w/pi-agents",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Multi-agent runtime for Pi: async agents, programmatic delegation,
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Multi-agent runtime for Pi: async agents, programmatic delegation, inter-agent messaging, and a workbench for your Sessions",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
"keywords": [
|
|
22
22
|
"pi-package",
|
|
23
23
|
"pi-extension",
|
|
24
|
-
"
|
|
25
|
-
"
|
|
24
|
+
"pi-coding-agent",
|
|
25
|
+
"pi",
|
|
26
|
+
"multi-agent",
|
|
27
|
+
"rlm"
|
|
26
28
|
],
|
|
27
29
|
"pi": {
|
|
28
30
|
"extensions": [
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
]
|
|
31
33
|
},
|
|
32
34
|
"scripts": {
|
|
33
|
-
"test": "node --test --test-timeout=30000 tests
|
|
35
|
+
"test": "node --test --test-timeout=30000 \"tests/*.test.mjs\"",
|
|
34
36
|
"typecheck": "tsc -p tsconfig.json"
|
|
35
37
|
},
|
|
36
38
|
"dependencies": {
|
package/src/daemon/sessions.mjs
CHANGED
|
@@ -87,6 +87,13 @@ export async function createSessions(paths, sessionDir) {
|
|
|
87
87
|
(marks[id] ??= {}).listed = true;
|
|
88
88
|
save();
|
|
89
89
|
},
|
|
90
|
+
/** Marks a Session's last turn seen, as when the user previews it. */
|
|
91
|
+
see(id) {
|
|
92
|
+
const mark = marks[id];
|
|
93
|
+
if (!mark?.last || (mark.seen ?? 0) >= mark.last.at) return;
|
|
94
|
+
mark.seen = mark.last.at;
|
|
95
|
+
save();
|
|
96
|
+
},
|
|
90
97
|
/** Removes a Session from the workbench list and clears its unseen turn; its file stays. */
|
|
91
98
|
hide(id) {
|
|
92
99
|
const mark = marks[id];
|
package/src/host/client.mjs
CHANGED
package/src/workbench.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { createRequire } from 'node:module';
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { pathToFileURL } from 'node:url';
|
|
9
9
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
10
|
-
import { attach, listHosts, startHost } from './host/client.mjs';
|
|
10
|
+
import { alive, attach, listHosts, startHost } from './host/client.mjs';
|
|
11
11
|
import { detachKey, detachMatcher } from './host/keys.mjs';
|
|
12
12
|
import { piPackage } from './pi.mjs';
|
|
13
13
|
import { ensureDaemon, sendViaDaemon } from './daemon/client.mjs';
|
|
@@ -66,6 +66,8 @@ function age(ms) {
|
|
|
66
66
|
|
|
67
67
|
const time = session => Math.max(session.finishedAt ?? 0, Date.parse(session.updatedAt ?? '') || 0);
|
|
68
68
|
const label = session => session.name || session.title || session.id.slice(0, 8);
|
|
69
|
+
// Notices quote the label, since a title can read as part of the notice.
|
|
70
|
+
const named = session => `"${label(session)}"`;
|
|
69
71
|
const COMMANDS = [
|
|
70
72
|
{ value: '/resume', label: 'resume', description: 'Open a past Session' },
|
|
71
73
|
{ value: '/quit', label: 'quit', description: 'Exit the workbench' },
|
|
@@ -283,7 +285,7 @@ export async function workbench({ pi: executable, paths, sessionDir }) {
|
|
|
283
285
|
return say('Press Ctrl+C again to quit');
|
|
284
286
|
}
|
|
285
287
|
if (T.matchesKey(data, 'ctrl+x')) return interrupt();
|
|
286
|
-
if (empty && keybindings.matches(data, 'app.session.delete')) return remove(selected()?.session);
|
|
288
|
+
if (empty && keybindings.matches(data, 'app.session.delete')) return void remove(selected()?.session);
|
|
287
289
|
// The preview's mention stays: an edit that would remove or change it is undone.
|
|
288
290
|
if (pane) {
|
|
289
291
|
const before = editor.getText();
|
|
@@ -366,9 +368,9 @@ export async function workbench({ pi: executable, paths, sessionDir }) {
|
|
|
366
368
|
const hosted = (await listHosts(paths)).find(host => host.session?.id === session.id);
|
|
367
369
|
if (hosted) return await attachTo(hosted.socket);
|
|
368
370
|
if (session.running || session.status === 'unknown') {
|
|
369
|
-
return say(`${
|
|
371
|
+
return say(`${named(session)} runs outside a host; attach it after it exits.`, 'warning');
|
|
370
372
|
}
|
|
371
|
-
say(`Starting ${
|
|
373
|
+
say(`Starting ${named(session)}…`);
|
|
372
374
|
const socket = await startHost(paths, {
|
|
373
375
|
pi: executable, args: ['--session', session.sessionFile], cwd: session.cwd,
|
|
374
376
|
cols: process.stdout.columns, rows: process.stdout.rows, waitForTerminal: true,
|
|
@@ -385,20 +387,32 @@ export async function workbench({ pi: executable, paths, sessionDir }) {
|
|
|
385
387
|
const session = selected()?.session;
|
|
386
388
|
if (!['working', 'blocked'].includes(session?.status) || !stream) return say('The selected Session is not working.', 'warning');
|
|
387
389
|
stream.send({ type: 'interrupt', id: session.id });
|
|
388
|
-
say(`Stopping ${
|
|
390
|
+
say(`Stopping ${named(session)}…`);
|
|
389
391
|
}
|
|
390
392
|
|
|
391
|
-
/**
|
|
392
|
-
|
|
393
|
+
/**
|
|
394
|
+
* Removes a Session from the list; `/resume` brings it back. An idle Session in a host no terminal
|
|
395
|
+
* is attached to quits first, as Pi does on SIGTERM; any other running Session always stays listed.
|
|
396
|
+
*/
|
|
397
|
+
async function remove(session) {
|
|
393
398
|
if (!session || !stream) return;
|
|
394
|
-
if (session.running
|
|
399
|
+
if (session.running) {
|
|
400
|
+
const host = session.status === 'idle' && (await listHosts(paths)).find(host => host.session?.id === session.id);
|
|
401
|
+
if (!host || host.attached) return say(`Quit ${named(session)} before removing it.`, 'warning');
|
|
402
|
+
say(`Quitting ${named(session)}…`);
|
|
403
|
+
process.kill(host.pid, 'SIGTERM');
|
|
404
|
+
for (let wait = 0; alive(host.pid); wait += 100) {
|
|
405
|
+
if (wait >= 5000) return say(`${named(session)} did not quit; attach it to see why.`, 'warning');
|
|
406
|
+
await delay(100);
|
|
407
|
+
}
|
|
408
|
+
} else if (session.status !== 'idle') return say(`Quit ${named(session)} before removing it.`, 'warning');
|
|
395
409
|
stream.send({ type: 'hide', id: session.id });
|
|
396
410
|
if (pane?.session.id === session.id) pane.close();
|
|
397
411
|
const choices = selectable();
|
|
398
412
|
const index = choices.findIndex(row => row.key === session.id);
|
|
399
413
|
selectedKey = (choices[index + 1] ?? choices[index - 1])?.key;
|
|
400
414
|
sessions = sessions.filter(other => other.id !== session.id);
|
|
401
|
-
say(`Removed ${
|
|
415
|
+
say(`Removed ${named(session)}; /resume brings it back.`);
|
|
402
416
|
}
|
|
403
417
|
|
|
404
418
|
async function preview(session) {
|
|
@@ -409,17 +423,22 @@ export async function workbench({ pi: executable, paths, sessionDir }) {
|
|
|
409
423
|
let body = conversation(messages);
|
|
410
424
|
const border = themes.getEditorTheme().borderColor;
|
|
411
425
|
let scroll = 0, page = 1, timer;
|
|
426
|
+
// A turn the preview shows counts as seen.
|
|
427
|
+
const see = () => stream?.send({ type: 'see', id: session.id });
|
|
428
|
+
see();
|
|
412
429
|
// Pi appends each finished message to the file, so the preview follows the Session as it works.
|
|
413
430
|
const watcher = watch(session.sessionFile, () => {
|
|
414
431
|
clearTimeout(timer);
|
|
415
432
|
timer = setTimeout(async () => {
|
|
416
433
|
try { body = conversation(await branchMessages(session.sessionFile)); }
|
|
417
434
|
catch (error) { return say(error.message, 'error'); }
|
|
435
|
+
see();
|
|
418
436
|
ui.requestRender();
|
|
419
437
|
}, 100);
|
|
420
438
|
});
|
|
421
439
|
const close = () => {
|
|
422
440
|
watcher.close();
|
|
441
|
+
see();
|
|
423
442
|
clearTimeout(timer);
|
|
424
443
|
// A draft stays, still addressed to the Session.
|
|
425
444
|
if (editor.getText() === pane?.mention) editor.setText('');
|
|
@@ -507,12 +526,12 @@ export async function workbench({ pi: executable, paths, sessionDir }) {
|
|
|
507
526
|
}
|
|
508
527
|
|
|
509
528
|
async function send(session, body) {
|
|
510
|
-
say(`Sending to ${
|
|
529
|
+
say(`Sending to ${named(session)}…`);
|
|
511
530
|
const message = { id: randomUUID(), user: true, recipient: session.id, body };
|
|
512
531
|
try {
|
|
513
532
|
const receipt = await sendViaDaemon(paths, message, { sessionDir, launch: { command: executable, args: [], env } });
|
|
514
|
-
if (receipt?.accepted !== true || receipt.messageId !== message.id) throw new Error(`Invalid acknowledgement from ${
|
|
515
|
-
say(`Sent to ${
|
|
533
|
+
if (receipt?.accepted !== true || receipt.messageId !== message.id) throw new Error(`Invalid acknowledgement from ${named(session)}; it may have received the message.`);
|
|
534
|
+
say(`Sent to ${named(session)}.`);
|
|
516
535
|
} catch (error) { say(error.message, 'error'); }
|
|
517
536
|
}
|
|
518
537
|
|