@tshu-w/pi-agents 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/pd.mjs CHANGED
@@ -7,12 +7,13 @@ import { createRequire } from 'node:module';
7
7
  import { dirname, join, resolve } from 'node:path';
8
8
  import { pathToFileURL } from 'node:url';
9
9
  import { styleText } from 'node:util';
10
- import { attach, settledHosts, startHost } from '../src/host/client.mjs';
10
+ import { attach, hostSessionFile, settledHosts, startHost } from '../src/host/client.mjs';
11
11
  import { detachKey, detachMatcher } from '../src/host/keys.mjs';
12
12
  import { discoverRoots, findRoot } from '../src/roots/discovery.mjs';
13
- import { agentDir, configuredSessionDir, defaultSessionDir, resolvePath, sessionDir, statePaths } from '../src/roots/paths.mjs';
13
+ import { defaultSessionDir, resolvePath, sessionDir, statePaths } from '../src/roots/paths.mjs';
14
14
  import { piPackage } from '../src/pi.mjs';
15
15
  import { markedFiles } from '../src/daemon/sessions.mjs';
16
+ import { argGroups } from '../src/host/rpc-client.mjs';
16
17
 
17
18
  const real = process.env.PATH.split(':').map(dir => join(dir, 'pi')).find(file => {
18
19
  try { accessSync(file, constants.X_OK); return true; } catch { return false; }
@@ -23,9 +24,13 @@ if (!real) {
23
24
  }
24
25
  const args = process.argv.slice(2);
25
26
  const paths = statePaths();
27
+ const dist = file => import(pathToFileURL(join(piPackage(real), 'dist', file)).href);
28
+ // Pi's own parsing, so that `pd` reads every argument as `pi` does.
29
+ const { parseArgs } = await dist('cli/args.js');
30
+ const { ENV_SESSION_DIR, getAgentDir } = await dist('config.js');
31
+ const parsed = parseArgs(args);
26
32
 
27
33
  const COMMANDS = new Set(['install', 'remove', 'uninstall', 'update', 'list', 'config', 'auth', 'mcp']);
28
- const DIRECT_FLAGS = new Set(['-h', '--help', '-v', '--version', '--export', '--list-models', '--no-session']);
29
34
 
30
35
  function runDirectly() {
31
36
  const child = spawn(real, args, { stdio: 'inherit' });
@@ -44,20 +49,12 @@ function error(text) {
44
49
  return styleText('red', text, { stream: process.stderr });
45
50
  }
46
51
 
47
- function option(name, alias) {
48
- for (let i = 0; i < args.length; i++) {
49
- if (args[i] === '--') return;
50
- if (args[i] === name || args[i] === alias) return args[i + 1] ?? '';
51
- if (args[i].startsWith(`${name}=`)) return args[i].slice(name.length + 1);
52
- }
53
- }
54
-
55
52
 
56
53
  /** Attaches the terminal to the host at `socket`; `send`, if given, sends messages once attached, and its error is reported once detached. */
57
54
  async function attachAndReport(socket, send) {
58
55
  const tui = await import(pathToFileURL(createRequire(join(piPackage(real), 'package.json')).resolve('@earendil-works/pi-tui')).href);
59
56
  let sent;
60
- const result = await attach(socket, { isDetach: detachMatcher(tui, detachKey(agentDir())), onAttach: () => { sent = send?.(); } });
57
+ const result = await attach(socket, { isDetach: detachMatcher(tui, detachKey(getAgentDir())), onAttach: () => { sent = send?.(); } });
61
58
  const failure = await sent;
62
59
  if (failure) process.stderr.write(`${error(failure)}\n`);
63
60
  const id = result.session?.id;
@@ -74,31 +71,30 @@ async function attachAndReport(socket, send) {
74
71
  /** The running host already holding the Session that `--session`, `--session-id`, or `-c` would open. */
75
72
  async function runningTarget() {
76
73
  let running;
77
- const session = option('--session');
78
- const sessionId = option('--session-id');
74
+ const { session, sessionId } = parsed;
79
75
  if (session) {
80
76
  const file = resolve(session);
81
- running = host => host.session && (host.session.id.startsWith(session) || host.session.file === file);
82
- } else if (sessionId && option('--fork') === undefined) {
77
+ running = host => host.session && (host.session.id.startsWith(session) || hostSessionFile(host) === file);
78
+ } else if (sessionId && parsed.fork === undefined) {
83
79
  // With `--fork`, the ID names a new Session.
84
80
  const file = await sessionById(sessionId);
85
81
  // A Session without messages has no file yet; its host reports where it will be.
86
- const directory = sessionDirOption() ?? defaultSessionDir(process.cwd());
82
+ const directory = await sessionDirOption() ?? defaultSessionDir(process.cwd(), getAgentDir());
87
83
  running = host => host.session && (file
88
- ? host.session.file === file
89
- : host.session.id === sessionId && dirname(host.session.file) === directory && host.session.cwd === process.cwd());
90
- } else if (args.includes('-c') || args.includes('--continue')) {
84
+ ? hostSessionFile(host) === file
85
+ : host.session.id === sessionId && dirname(hostSessionFile(host)) === directory && host.session.cwd === process.cwd());
86
+ } else if (parsed.continue) {
91
87
  const file = await recentSession();
92
- if (file) running = host => host.session?.file === file;
88
+ if (file) running = host => hostSessionFile(host) === file;
93
89
  }
94
90
  return running && (await settledHosts(paths, running)).find(running);
95
91
  }
96
92
 
97
93
  /** The Session `-c` continues, chosen by Pi's own `continueRecent` rules. */
98
94
  async function recentSession() {
99
- const { findMostRecentSession } = await import(pathToFileURL(join(piPackage(real), 'dist/core/session-manager.js')).href);
100
- const configured = sessionDirOption();
101
- const fallback = defaultSessionDir(process.cwd());
95
+ const { findMostRecentSession } = await dist('core/session-manager.js');
96
+ const configured = await sessionDirOption();
97
+ const fallback = defaultSessionDir(process.cwd(), getAgentDir());
102
98
  const directory = configured ?? fallback;
103
99
  // A configured directory may hold other cwds' Sessions.
104
100
  return findMostRecentSession(directory, directory === fallback ? undefined : process.cwd()) ?? undefined;
@@ -106,13 +102,14 @@ async function recentSession() {
106
102
 
107
103
  /** The Session `--session-id` would open, found by Pi's own rules, if it exists. */
108
104
  async function sessionById(id) {
109
- const { SessionManager } = await import(pathToFileURL(join(piPackage(real), 'dist/core/session-manager.js')).href);
110
- return SessionManager.findById(process.cwd(), id, sessionDirOption());
105
+ const { SessionManager } = await dist('core/session-manager.js');
106
+ return SessionManager.findById(process.cwd(), id, await sessionDirOption());
111
107
  }
112
108
 
113
- function sessionDirOption() {
114
- const flag = option('--session-dir');
115
- return flag ? resolvePath(flag) : configuredSessionDir();
109
+ /** The session directory configured for Pi in this cwd, as Pi resolves it: `--session-dir`, its environment variable, then its settings. */
110
+ async function sessionDirOption() {
111
+ const configured = parsed.sessionDir || process.env[ENV_SESSION_DIR] || (await dist('core/settings-manager.js')).SettingsManager.create(process.cwd(), getAgentDir()).getSessionDir();
112
+ return configured ? resolvePath(configured) : undefined;
116
113
  }
117
114
 
118
115
  async function attachCommand(query) {
@@ -147,16 +144,14 @@ async function attachCommand(query) {
147
144
 
148
145
  /** Warns about the options a running Session does not apply. */
149
146
  function warnIgnored(options) {
150
- const ignored = options.filter((arg, i) => !['--session', '--session-id', '--session-dir', '-c', '--continue'].includes(arg)
151
- && !arg.startsWith('--session=') && !arg.startsWith('--session-id=') && !['--session', '--session-id', '--session-dir'].includes(options[i - 1]));
147
+ const ignored = argGroups(parseArgs, options).filter(([option]) => !['--session', '--session-id', '--session-dir', '-c', '--continue'].includes(option)).flat();
152
148
  if (ignored.length) process.stderr.write(`${styleText('yellow', `The Session is already running; using it without applying: ${ignored.join(' ')}`, { stream: process.stderr })}\n`);
153
149
  }
154
150
 
155
151
  /** Pi's mode for `args` when it is not interactive: 'text', 'json', or 'rpc'. */
156
152
  function headlessMode() {
157
- const mode = option('--mode');
158
- if (mode === 'json' || mode === 'rpc') return mode;
159
- if (!interactive || args.includes('-p') || args.includes('--print')) return 'text';
153
+ if (parsed.mode === 'json' || parsed.mode === 'rpc') return parsed.mode;
154
+ if (!interactive || parsed.print) return 'text';
160
155
  }
161
156
 
162
157
  const interactive = process.stdin.isTTY && process.stdout.isTTY;
@@ -166,7 +161,9 @@ else if (args[0] === 'agents' && interactive) {
166
161
  await workbench({ pi: real, paths, sessionDir: sessionDir() });
167
162
  process.exit(0);
168
163
  }
169
- else if (COMMANDS.has(args[0]) || args.some(arg => DIRECT_FLAGS.has(arg.split('=')[0]))) runDirectly();
164
+ else if (COMMANDS.has(args[0]) || parsed.help || parsed.version || parsed.export !== undefined || parsed.listModels !== undefined || parsed.noSession) {
165
+ runDirectly();
166
+ }
170
167
  else if (headlessMode()) {
171
168
  const { runHeadless } = await import('../src/host/rpc-client.mjs');
172
169
  await runHeadless({ pi: real, paths, args, mode: headlessMode(), findRunning: runningTarget, warnIgnored });
package/docs/HOSTS.md CHANGED
@@ -2,169 +2,105 @@
2
2
 
3
3
  ## 1. Overview
4
4
 
5
- `pd` runs each Pi Session in a host of its own, so the Session keeps running
6
- after its terminal closes, and terminals, scripts, and other programs can use
7
- it at once. The workbench, opened with `pd agents`, shows the user's root
8
- Sessions in one terminal view: from it the user opens a Session, reads its
9
- latest result, or sends it a message.
5
+ `pd` runs each Pi Session in a host of its own, so the Session keeps running after its terminal closes, and terminals, scripts, and other programs can use it at once. The workbench, opened with `pd agents`, shows the user's root Sessions in one terminal view: from it the user opens a Session, reads its latest result, or sends it a message.
10
6
 
11
7
  Three kinds of process take part:
12
8
 
13
- - **Host**: one per Session. It runs Pi's full TUI in a pseudo-terminal and
14
- relays it to whichever terminal is attached.
15
- - **Daemon**: one per user. It tracks Sessions, delivers messages, and starts
16
- hosts.
9
+ - **Host**: one per Session. It runs Pi's full TUI in a pseudo-terminal and relays it to whichever terminal is attached.
10
+ - **Daemon**: one per user. It tracks Sessions, delivers messages, and starts hosts.
17
11
  - **Workbench**: the daemon's terminal view, opened with `pd agents`.
18
12
 
19
- Every running root Session reports its state to the daemon. The daemon starts
20
- when a Session or the workbench first needs it, and exits soon after its last
21
- connection closes.
13
+ Every running root Session reports its state to the daemon. The daemon starts when a Session or the workbench first needs it, and exits soon after its last connection closes.
22
14
 
23
15
  Files live in `${XDG_STATE_HOME:-~/.local/state}/pi/agents/`:
24
16
 
25
17
  - `workbench.json`: seen marks and which Sessions are listed.
26
18
  - `host.log`, `daemon.log`: logs.
27
19
 
28
- Sockets and Session locks go in `$XDG_RUNTIME_DIR/pi-agents/`. macOS has no
29
- `XDG_RUNTIME_DIR`, so there they go in `runtime/` under the state directory.
20
+ Sockets and Session locks go in `$XDG_RUNTIME_DIR/pi-agents/`. macOS has no `XDG_RUNTIME_DIR`, so there they go in `runtime/` under the state directory.
30
21
 
31
22
  ## 2. Setup
32
23
 
33
- Install with npm to get the `pd` command, also named `pi-agents`, then have Pi
34
- load the extension from the same copy, so the command and the extension are
35
- always the same version:
24
+ Install with npm to get the `pd` command, also named `pi-agents`, then have Pi load the extension from the same copy, so the command and the extension are always the same version:
36
25
 
37
- npm install -g @tshu-w/pi-agents
38
- pi install "$(npm root -g)/@tshu-w/pi-agents"
26
+ ```sh
27
+ npm install -g @tshu-w/pi-agents
28
+ pi install "$(npm root -g)/@tshu-w/pi-agents"
29
+ ```
39
30
 
40
- `pd` runs whichever `pi` is on `PATH`. Sessions started with plain `pi` also
41
- load the extension, but run outside hosts and end with their terminal.
31
+ `pd` runs whichever `pi` is on `PATH`. Sessions started with plain `pi` also load the extension, but run outside hosts and end with their terminal.
42
32
 
43
33
  ## 3. Commands
44
34
 
45
- - `pd [pi options]` starts Pi interactively in a new host and attaches the
46
- terminal. If the Session is already running in a host, `pd` attaches to it
47
- instead, sending it the messages as follow-ups and ignoring other options.
35
+ - `pd [pi options]` starts Pi interactively in a new host and attaches the terminal. If the Session is already running in a host, `pd` attaches to it instead, sending it the messages as follow-ups and ignoring other options.
48
36
  - `pd agents` opens the workbench.
49
- - `pd attach <session>` attaches to a Session, starting it first if it is not
50
- running.
51
- - Non-interactive starts (`-p`, `--mode json`, `--mode rpc`, piped stdin) work
52
- as in Pi, on the Session in a host, so several of them and an attached
53
- terminal can use one Session at once. Their commands act on that Session,
54
- as `abort` stops whichever turn is running. Unlike Pi:
55
- - Dialogs are not sent to them. A dialog waits while a terminal is attached
56
- and is cancelled otherwise.
57
- - Extensions see interactive mode, so what an extension writes to stdout
58
- does not reach them; `/program` prints as with `pi -p`.
59
- - `new_session`, `switch_session`, `fork`, and `clone` move only the caller
60
- to the other Session.
61
- - `-p` and `--mode json` wait until the Session is idle, then run their
62
- messages back to back. Their output covers their own turns, including
63
- input other clients add to them.
64
- - `SIGINT`, `SIGTERM`, or `SIGHUP` (as from Ctrl+C, `kill`, or a closed
65
- terminal) makes `-p` and `--mode json` withdraw their prompts that have
66
- not started and stop their running turn, including input other clients
67
- added to it. Killed with `SIGKILL`, they leave their turn running.
68
- - An RPC client that ends leaves its turn running. One whose Session stops
69
- opens it again; requests awaiting a response then fail.
70
- - `--no-session` starts and package commands (`pd install`, `pd update`, ...)
71
- run Pi directly.
72
-
73
- A host keeps the options and environment of the process that started it, as
74
- Pi reads them only at startup.
75
-
76
- A Session running outside a host still appears in the workbench, but can be
77
- attached only after it exits.
37
+ - `pd attach <session>` attaches to a Session, starting it first if it is not running.
38
+ - Non-interactive starts (`-p`, `--mode json`, `--mode rpc`, piped stdin) work as in Pi, on the Session in a host, so several of them and an attached terminal can use one Session at once. Their commands act on that Session, as `abort` stops whichever turn is running. Unlike Pi:
39
+ - Dialogs are not sent to them. A dialog waits while a terminal is attached and is cancelled otherwise.
40
+ - Extensions see interactive mode, so what an extension writes to stdout does not reach them; `/program` prints as with `pi -p`.
41
+ - `new_session`, `switch_session`, `fork`, and `clone` move only the caller to the other Session.
42
+ - `-p` and `--mode json` wait until the Session is idle, then run their messages back to back. Their output covers their own turns, including input other clients add to them.
43
+ - `SIGINT`, `SIGTERM`, or `SIGHUP` (as from Ctrl+C, `kill`, or a closed terminal) makes `-p` and `--mode json` withdraw their prompts that have not started and stop their running turn, including input other clients added to it. Killed with `SIGKILL`, they leave their turn running.
44
+ - An RPC client that ends leaves its turn running. One whose Session stops opens it again; requests awaiting a response then fail.
45
+ - `--no-session` starts and package commands (`pd install`, `pd update`, ...) run Pi directly.
46
+
47
+ A host keeps the options and environment of the process that started it, as Pi reads them only at startup.
48
+
49
+ A Session running outside a host still appears in the workbench, but can be attached only after it exits.
78
50
 
79
51
  ## 4. Attach and detach
80
52
 
81
- Only one terminal is attached at a time. A new terminal takes over, and the
82
- previous one is told how to reattach.
53
+ Only one terminal is attached at a time. A new terminal takes over, and the previous one is told how to reattach.
83
54
 
84
- On attach, the host restores Pi's screen and the terminal modes Pi has set.
85
- Pi detects the colors and keyboard protocol of the first terminal that
86
- attaches, as it would without a host.
55
+ On attach, the host restores Pi's screen and the terminal modes Pi has set. Pi detects the colors and keyboard protocol of the first terminal that attaches, as it would without a host.
87
56
 
88
- Closing the window, pressing `Ctrl+Z` (the `pi-agents.detach` keybinding), or
89
- running `/detach` detaches. The Session keeps running, and the terminal prints:
57
+ Closing the window, pressing `Ctrl+Z` (`pi-agents.detach`), or running `/detach` detaches. The Session keeps running, and the terminal prints:
90
58
 
91
- Pi keeps running. To reattach: pd attach <session>
59
+ ```text
60
+ Pi keeps running. To reattach: pd attach <session>
61
+ ```
92
62
 
93
63
  A host disables Pi's suspend.
94
64
 
95
- A detached Session exits after 10 minutes with no live work in its tree and no
96
- `pd --mode rpc` connected. A message to a Session that is not running starts it
97
- in a host, detached.
65
+ A detached Session exits after 10 minutes with no live work in its tree and no `pd --mode rpc` connected. A message to a Session that is not running starts it in a host, detached.
98
66
 
99
67
  ## 5. Workbench
100
68
 
101
- The list shows running Sessions, Sessions that ran without a terminal (started
102
- here, woken by a message, or detached), Sessions opened here, and Sessions
103
- with an unseen turn. Older Sessions are reached with `/resume`.
69
+ The list shows running Sessions, Sessions that ran without a terminal (started here, woken by a message, or detached), Sessions opened here, and Sessions with an unseen turn. Older Sessions are reached with `/resume`.
104
70
 
105
- Each row shows a root Session's name and what it did last. The name is the
106
- Session name, or else the first line of its first message. While the Session
107
- works, the row shows the running tool; otherwise, the start of its last reply.
108
- Running owned Agents and Programs are counted, as in `3 agents running`.
71
+ Each row shows a root Session's name and what it did last. The name is the Session name, or else the first line of its first message. While the Session works, the row shows the running tool; otherwise, the start of its last reply. Running owned Agents and Programs are counted, as in `3 agents running`.
109
72
 
110
73
  Rows are grouped by state, newest first in each group:
111
74
 
112
- - **Needs attention**: a dialog waiting for the user, or a finished turn the user
113
- has not seen. A turn that failed after Pi's retries, or a host that exited
114
- abnormally, is marked failed.
75
+ - **Needs attention**: a dialog waiting for the user, or a finished turn the user has not seen. A turn that failed after Pi's retries, or a host that exited abnormally, is marked failed.
115
76
  - **Working**: the Session or its tree has live work.
116
77
  - **Completed**: everything else.
117
78
 
118
- While a Session works, its icon spins. Otherwise the shape tells whether Pi is
119
- still running (`●`) or has exited (`○`), and the color marks a finished (green)
120
- or failed (red) turn, or a dialog waiting for the user (yellow). The header
121
- shows Pi's logo, the size of each group, and key hints.
79
+ While a Session works, its icon spins. Otherwise the shape tells whether Pi is still running (`●`) or has exited (`○`), and the color marks a finished (green) or failed (red) turn, or a dialog waiting for the user (yellow). The header shows Pi's logo, the size of each group, and key hints.
122
80
 
123
- A turn counts as seen once it has been shown in an attached terminal or a
124
- preview. Sessions
125
- from before the workbench start out seen. A Session that holds its lock but is
126
- not reporting to the daemon, as while its Pi exits, shows as unknown (`?`).
81
+ A turn counts as seen once it has been shown in an attached terminal or a preview. Sessions from before the workbench start out seen. A Session that holds its lock but is not reporting to the daemon, as while its Pi exits, shows as unknown (`?`).
127
82
 
128
83
  Keys:
129
84
 
130
- - `Enter` attaches; detaching returns here.
131
- - `Space` previews the selected Session below the list: the conversation
132
- since the last compaction, with thinking left out and each run of tool
133
- calls folded to one line, such as `... (6 tool calls: bash, read, edit)`.
134
- The preview follows the Session as it works.
135
- The input keeps `@<session>` at its start, so a message goes to the
136
- previewed Session. In the preview:
137
- - `Enter` sends a typed message; with none typed, it attaches.
138
- - `Ctrl+O` (`app.tools.expand`) attaches.
139
- - It scrolls as fullscreen mode does; with no message typed, `↑`/`↓`
140
- scroll too.
141
- - `Esc` clears a typed message; with none typed, it closes the preview.
142
- - `Ctrl+Z` closes the preview.
85
+ - `Enter` (`tui.select.confirm`) attaches; detaching returns here.
86
+ - `Space` previews the selected Session below the list: the conversation since the last compaction, with thinking left out, and each run of tool calls, branch summary, and custom message folded to one line, such as `... (6 tool calls: bash, read, edit)`. The preview follows the Session as it works. The input keeps `@<session>` at its start, so a message goes to the previewed Session. In the preview:
87
+ - With no message typed, `Enter` attaches, as `Ctrl+O` (`app.tools.expand`) always does.
88
+ - It scrolls as fullscreen mode does; with no message typed, `↑`/`↓` (`tui.select.up`/`tui.select.down`) scroll too.
89
+ - `Esc` (`tui.select.cancel`) clears a typed message; with none typed, it or `Space` closes the preview, as `Ctrl+Z` always does.
143
90
  - `Ctrl+X` stops the current turn, as `app.interrupt` does.
144
- - `Ctrl+D` removes the selected Session from the list. Its file stays, and
145
- `/resume` brings it back. An idle Session in a host that no terminal is
146
- attached to quits first; any other running Session must quit itself.
147
- - `Ctrl+Z` (`pi-agents.detach`) exits the workbench, as detaching leaves a
148
- Session; every Session keeps running.
91
+ - `Ctrl+D` (`app.session.delete`) removes the selected Session from the list. Its file stays, and `/resume` brings it back. An idle Session in a host that no terminal is attached to quits first; any other running Session must quit itself.
92
+ - `Ctrl+Z` (`pi-agents.detach`) exits the workbench, as detaching leaves a Session; every Session keeps running.
149
93
 
150
94
  The input at the bottom:
151
95
 
152
- - `@` on an empty input opens Pi's session picker. The chosen Session is
153
- selected in the list, and the input starts with `@<session>`; the text after
154
- it goes to that Session as a user message. After `/`, Tab completes a
155
- command.
156
- - `/resume` opens Pi's session picker over all directories, newest first, and
157
- attaches the chosen Session. `Ctrl+D` in the picker deletes a Session file.
158
- - `/quit` exits the workbench, as do `Esc` and `Ctrl+C` twice on an empty
159
- input.
160
- - Any other text starts a new Session in the current directory with Pi's
161
- default settings, detached.
96
+ - `@` on an empty input opens Pi's session picker. The chosen Session is selected in the list, and the input starts with `@<session>`; the text after it goes to that Session as a user message.
97
+ - `/resume` opens Pi's session picker over all directories, newest first, and attaches the chosen Session.
98
+ - `/quit` exits the workbench, as do `Esc` and `Ctrl+C` (`app.clear`) twice on an empty input.
99
+ - Any other text starts a new Session in the current directory with Pi's default settings, detached.
162
100
 
163
101
  ## 6. Upgrades
164
102
 
165
- A host keeps running the version it started with until its Session exits.
166
- When the daemon sees a newer pi-agents version, it restarts once idle, and
167
- Sessions reconnect to the new daemon.
103
+ A host keeps running the version it started with until its Session exits. When the daemon sees a newer pi-agents version, it restarts once idle, and Sessions reconnect to the new daemon.
168
104
 
169
105
  ## 7. Platform
170
106
 
@@ -172,8 +108,4 @@ macOS and Linux.
172
108
 
173
109
  ## Not in this version
174
110
 
175
- Attaching several terminals to one Session, Sessions on other machines,
176
- model-written summaries, and a confirmation before quitting while work is
177
- running. Keeping Sessions alive across host crashes and reboots needs a
178
- durable runtime, which [Pi Durable](https://earendil.com/posts/pi-durable/)
179
- explores.
111
+ Attaching several terminals to one Session, Sessions on other machines, model-written summaries, and a confirmation before quitting while work is running. Keeping Sessions alive across host crashes and reboots needs a durable runtime, which [Pi Durable](https://earendil.com/posts/pi-durable/) explores.
package/docs/PROGRAM.md CHANGED
@@ -1,34 +1,18 @@
1
1
  # Pi Agents Program Specification
2
2
 
3
- A Program is JavaScript that calls tools and Agents. Agents are defined in
4
- SPEC.md.
3
+ A Program is JavaScript that calls tools and Agents. Agents are defined in SPEC.md.
5
4
 
6
- The `program` tool replaces Pi's `codemode` tool and keeps its script API,
7
- tool list, result format, and truncation. This document describes what
8
- Programs add.
5
+ The `program` tool replaces Pi's `codemode` tool and keeps its script API, tool list, result format, and truncation. This document describes what Programs add.
9
6
 
10
7
  ## 1. Semantic model
11
8
 
12
- An Agent runs a **Program** with the `program` tool. That Agent is the
13
- Program's **caller**. The Program can call the caller's tools and create
14
- Agents.
9
+ An Agent runs a **Program** with the `program` tool. That Agent is the Program's **caller**. The Program can call the caller's tools and create Agents.
15
10
 
16
- A Program runs in the **foreground** or the **background**. In the
17
- foreground, the tool call returns the Program's result. In the background,
18
- the tool call returns the Program's ID, and the caller gets the result with
19
- `wait`.
11
+ A Program runs in the **foreground** or the **background**. In the foreground, the tool call returns the Program's result. In the background, the tool call returns the Program's ID, and the caller gets the result with `wait`.
20
12
 
21
- A Program is shown by its short ID: the shortest prefix of its ID, at least
22
- 8 characters, that none of the caller's other Programs' IDs starts with. A
23
- target is an ID or a unique ID prefix.
13
+ A Program is shown by its short ID: the shortest prefix of its ID, at least 8 characters, that none of the caller's other Programs' IDs starts with. A target is an ID or a unique ID prefix.
24
14
 
25
- Agents created with `agent()` belong to the Program: it gets their results
26
- from `send()`, without `wait` or notifications, and only it can abort
27
- them. Each of these Agents and the Agents under it see only one another,
28
- and no other Agent sees them. The user sees them in the task panel and `/tasks`,
29
- and can talk to them or interrupt them in the viewer; an interrupted input
30
- makes its `send()` throw. When the Program ends, their unfinished inputs end
31
- as `aborted`, and the Agents go offline for good, from the bottom up.
15
+ Agents created with `agent()` belong to the Program: it gets their results from `send()`, without `wait` or notifications, and only it can abort them. Each of these Agents and the Agents under it see only one another, and no other Agent sees them. The user sees them in the task panel and `/tasks`, and can talk to them or interrupt them in the viewer; an interrupted input makes its `send()` throw. When the Program ends, their unfinished inputs end as `aborted`, and the Agents go offline for good, from the bottom up.
32
16
 
33
17
  A Program is `running` until it ends in one of three ways:
34
18
 
@@ -38,60 +22,38 @@ A Program is `running` until it ends in one of three ways:
38
22
 
39
23
  ## 2. Operations
40
24
 
41
- - `run` runs a Program in the foreground or background, with an optional
42
- timeout.
25
+ - `run` runs a Program in the foreground or background, with an optional timeout.
43
26
  - `wait` waits for background Programs and returns their results.
44
27
  - `list` lists the caller's background Programs.
45
28
  - `stop` stops a running Program.
46
29
 
47
- When a background Program ends and its caller is not waiting for it, the
48
- caller gets a notification, as in SPEC §5. A steer input ends `wait` early,
49
- as in SPEC §5.
30
+ When a background Program ends and its caller is not waiting for it, the caller gets a notification, as in SPEC §5. A steer input ends `wait` early, as in SPEC §5.
50
31
 
51
32
  ## 3. Program API
52
33
 
53
- `tools` holds the caller's tools, except `program`; which tools the caller
54
- can use is checked at each call. If a Program fails or is stopped, the
55
- error or stop reason follows its output. Its `store()` writes are kept
56
- only if it completes.
34
+ `tools` holds the caller's tools, except `program`; which tools the caller can use is checked at each call. If a Program fails or is stopped, the error or stop reason follows its output. Its `store()` writes are kept only if it completes.
57
35
 
58
- A Program also creates idle Agents with `agent()`, typed in the Agent API
59
- (§6). Options work like `agent` `spawn`; an omitted name is generated,
60
- unique within the Program. `send()` sends a message and returns its result.
61
- `abort()` works like `agent` `abort`. The Agent gets its tools as if the caller had spawned it.
36
+ A Program also creates idle Agents with `agent()`, typed in the Agent API (§6). Options work like `agent` `spawn`; an omitted name is generated, unique within the Program. `send()` sends a message and returns its result. `abort()` works like `agent` `abort`. The Agent gets its tools as if the caller had spawned it.
62
37
 
63
- A result is the Agent's answer. If the input fails or is aborted, the
64
- result throws an Error. A write returns `undefined` once accepted.
38
+ A result is the Agent's answer. If the input fails or is aborted, the result throws an Error. A write returns `undefined` once accepted.
65
39
 
66
- With `schema`, the Agent must call `submit_result` with a value that
67
- matches the schema, and the result is that value. If it does not, the
68
- input fails.
40
+ With `schema`, the Agent must call `submit_result` with a value that matches the schema, and the result is that value. If it does not, the input fails.
69
41
 
70
- With `timeout`, an input that has not ended within that many seconds of
71
- `send()`, including time queued for a slot, is aborted; the Agent's queued
72
- inputs are unaffected. The result throws a timeout Error.
42
+ With `timeout`, an input that has not ended within that many seconds of `send()`, including time queued for a slot, is aborted; the Agent's queued inputs are unaffected. The result throws a timeout Error.
73
43
 
74
44
  `send()` throws if `schema` or `timeout` is used without `followUp`.
75
45
 
76
46
  ## 4. Lifecycle
77
47
 
78
- When a Program ends, it cancels its unfinished tool calls and aborts its
79
- Agents' unfinished inputs, and waits for its Agents to go offline. It
80
- returns its result after cleanup. Work not stopped within the cleanup
81
- timeout is abandoned and may keep running; the Program's result and `stop`
82
- report it as an error, and the Program's state is unchanged.
48
+ When a Program ends, it cancels its unfinished tool calls and aborts its Agents' unfinished inputs, and waits for its Agents to go offline. It returns its result after cleanup. Work not stopped within the cleanup timeout is abandoned and may keep running; the Program's result and `stop` report it as an error, and the Program's state is unchanged.
83
49
 
84
- When the caller goes offline, its running Programs stop. Their results
85
- remain available when the caller is loaded again.
50
+ When the caller goes offline, its running Programs stop. Their results remain available when the caller is loaded again.
86
51
 
87
52
  ## 5. Limits
88
53
 
89
- A Program's Agents and the Agents under them count toward the caller's
90
- tree: they share its slots and `maxOutstanding` limit, as in SPEC §4. At
91
- `maxOutstanding`, `send()` throws.
54
+ A Program's Agents and the Agents under them count toward the caller's tree: they share its slots and `maxOutstanding` limit, as in SPEC §4. At `maxOutstanding`, `send()` throws.
92
55
 
93
- An owned caller gives up its slot while a foreground Program runs or while
94
- it waits for background Programs, and takes one again before continuing.
56
+ An owned caller gives up its slot while a foreground Program runs or while it waits for background Programs, and takes one again before continuing.
95
57
 
96
58
  ## 6. Model-facing tool
97
59
 
@@ -99,21 +61,11 @@ it waits for background Programs, and takes one again before continuing.
99
61
 
100
62
  description:
101
63
 
102
- Run JavaScript that composes tool calls and Agents; only its output and
103
- return value reach the caller. `run` runs a Program in the foreground, or
104
- in the background with `background`; `wait` waits for background Programs
105
- and returns their results; `list` lists background Programs; `stop` stops
106
- a running Program. When a background Program ends while its caller is not
107
- waiting for it, the caller receives a notification; `wait` returns the
108
- result.
64
+ Run JavaScript that composes tool calls and Agents; only its output and return value reach the caller. `run` runs a Program in the foreground, or in the background with `background`; `wait` waits for background Programs and returns their results; `list` lists background Programs; `stop` stops a running Program. When a background Program ends while its caller is not waiting for it, the caller receives a notification; `wait` returns the result.
109
65
 
110
- Program code is the body of an async function. It calls the caller's tools
111
- through `tools.*`, which excludes `program`, and creates Agents through
112
- `agent()`. These Agents belong to the Program, are isolated from other
113
- Agents, and go offline when it ends.
66
+ Program code is the body of an async function. It calls the caller's tools through `tools.*`, which excludes `program`, and creates Agents through `agent()`. These Agents belong to the Program, are isolated from other Agents, and go offline when it ends.
114
67
 
115
- `program` replaces the `codemode` tool; wherever `codemode` is mentioned,
116
- use `program`.
68
+ `program` replaces the `codemode` tool; wherever `codemode` is mentioned, use `program`.
117
69
 
118
70
  [the script API of `codemode`, without its Model API]
119
71
 
@@ -163,14 +115,9 @@ Run JavaScript that composes tool calls and Agents
163
115
 
164
116
  promptGuidelines:
165
117
 
166
- - Use `program(action='run', code=...)` to call tools or Agents several
167
- times without a model turn between the calls, for example to read many
168
- files, filter large tool output, fan out Agents, or loop until a
169
- condition holds.
170
- - Use `Promise.allSettled` for independent calls. Return only what the
171
- caller needs; use `text()` for progress worth reading.
172
- - Use `background=true` for long Programs, and `program(action='wait', ...)`
173
- when the next step depends on their results.
118
+ - Use `program(action='run', code=...)` to call tools or Agents several times without a model turn between the calls, for example to read many files, filter large tool output, fan out Agents, or loop until a condition holds.
119
+ - Use `Promise.allSettled` for independent calls. Return only what the caller needs; use `text()` for progress worth reading.
120
+ - Use `background=true` for long Programs, and `program(action='wait', ...)` when the next step depends on their results.
174
121
 
175
122
  parameters:
176
123
 
@@ -186,10 +133,7 @@ results:
186
133
 
187
134
  In these formats, `<id>` is the Program's short ID.
188
135
 
189
- `run` in the foreground returns the Program result, as an error if the
190
- Program failed or was stopped.
191
- A Program result lists its `send()` calls with its tool calls, and names the
192
- calls still running when the script returned, which were cancelled.
136
+ `run` in the foreground returns the Program result, as an error if the Program failed or was stopped. A Program result lists its `send()` calls with its tool calls, and names the calls still running when the script returned, which were cancelled.
193
137
 
194
138
  `run` in the background:
195
139
 
@@ -197,8 +141,7 @@ calls still running when the script returned, which were cancelled.
197
141
  Program <id> started.
198
142
  ```
199
143
 
200
- `wait` returns one block per result, followed by running Programs on
201
- timeout:
144
+ `wait` returns one block per result, followed by running Programs on timeout:
202
145
 
203
146
  ```text
204
147
  <program-result id="<id>" status="<outcome>">
@@ -210,8 +153,7 @@ Still running: <id>, ...
210
153
 
211
154
  When a steer input ended the wait, the line of SPEC §6 follows.
212
155
 
213
- A single completed result, with nothing running, is returned alone. When
214
- there are no results or running Programs:
156
+ A single completed result, with nothing running, is returned alone. When there are no results or running Programs:
215
157
 
216
158
  ```text
217
159
  No results.
@@ -242,8 +184,7 @@ Program <id> stopped.
242
184
  Program <id> has already ended.
243
185
  ```
244
186
 
245
- When some of the Program's work does not stop within the timeout, `stop`
246
- fails with the following; only restarting Pi stops it.
187
+ When some of the Program's work does not stop within the timeout, `stop` fails with the following; only restarting Pi stops it.
247
188
 
248
189
  ```text
249
190
  Program <id> did not stop within the timeout and may still be running.
@@ -257,8 +198,7 @@ Cleanup error: some of the Program's work did not stop within the timeout and ma
257
198
 
258
199
  errors:
259
200
 
260
- Invalid calls fail with an error. Errors that need a follow-up say what to
261
- do:
201
+ Invalid calls fail with an error. Errors that need a follow-up say what to do:
262
202
 
263
203
  ```text
264
204
  No Program matches "<target>". Use list to find Programs.
@@ -270,10 +210,7 @@ No Program matches "<target>". Use list to find Programs.
270
210
 
271
211
  truncation:
272
212
 
273
- `wait` text beyond Pi's output limits is truncated as in SPEC §6. `wait`
274
- adds results in order; the result that crosses the limit is truncated but keeps
275
- its closing tag, and later results are omitted. A truncated result counts
276
- as returned. Omitted results stay unreturned, and the text ends with:
213
+ `wait` text beyond Pi's output limits is truncated as in SPEC §6. `wait` adds results in order; the result that crosses the limit is truncated but keeps its closing tag, and later results are omitted. A truncated result counts as returned. Omitted results stay unreturned, and the text ends with:
277
214
 
278
215
  ```text
279
216
  [Results omitted: <id>, ... Use wait with fewer targets.]
@@ -289,19 +226,10 @@ Program <id> <outcome>.
289
226
 
290
227
  ## 7. Human interface
291
228
 
292
- The task panel (SPEC §7) lists running background Programs.
293
- `/tasks` opens a background Program in the task viewer (SPEC §7).
229
+ The task panel (SPEC §7) lists running background Programs. `/tasks` opens a background Program in the task viewer (SPEC §7).
294
230
 
295
- `/program <path> [args]` runs the script in a file as a foreground Program of
296
- the current Agent, with the text after the path as the global `args`. Esc
297
- stops it. Like a `!` command, it and its result join the context without
298
- starting a turn. With `pi -p`, the result is printed, and Pi exits with
299
- status 1 if the run fails.
231
+ `/program <path> [args]` runs the script in a file as a foreground Program of the current Agent, with the text after the path as the global `args`. Like a `!` command, it and its result join the context without starting a turn. With `pi -p`, the result is printed, and Pi exits with status 1 if the run fails.
300
232
 
301
233
  ## 8. Configuration
302
234
 
303
- `program` is inactive by default, like `codemode`; enable it with `"defaultTools": ["+program"]` or `--tools`. `/program` does not need it.
304
- MCP treats `program` as `codemode`: it activates `program` when its servers need a script tool and neither is active, unless `autoEnableCodemode` is false, and a Program waits for the MCP servers its code names.
305
- When both `program` and `codemode` are active, Pi warns once per Session, since they do a similar job.
306
- `codemode.mode` decides how the tool list is presented, as for `codemode`.
307
- `--program-only` activates `program` and uses `only` for the process, including its owned Agents.
235
+ `program` is inactive by default, like `codemode`; enable it with `"defaultTools": ["+program"]` or `--tools`. `/program` does not need it. MCP treats `program` as `codemode`: it activates `program` when its servers need a script tool and neither is active, unless `autoEnableCodemode` is false, and a Program waits for the MCP servers its code names. When both `program` and `codemode` are active, Pi warns once per Session, since they do a similar job. `codemode.mode` decides how the tool list is presented, as for `codemode`. `--program-only` activates `program` and uses `only` for the process, including its owned Agents.