pi-repl-py 0.2.2 → 0.2.5
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 +16 -9
- package/docs/ARCHITECTURE.md +42 -41
- package/docs/{philosophy.md → design.md} +18 -29
- package/docs/helpers.md +145 -70
- package/docs/termux.md +49 -0
- package/package.json +1 -1
- package/src/extension/prompt.ts +39 -45
package/README.md
CHANGED
|
@@ -4,9 +4,9 @@ A [pi](https://pi.dev) extension that gives the agent a single `execute` tool ba
|
|
|
4
4
|
**persistent Python evaluator**: a real `ipython` kernel that keeps variables, functions, imports,
|
|
5
5
|
and data alive across every call and turn.
|
|
6
6
|
|
|
7
|
-
There is no interactive shell. The agent batches code
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
There is no interactive shell. The agent sends batches of Python code to a workspace that stays
|
|
8
|
+
alive for the session. Only the output comes back to the conversation. This keeps state in Python
|
|
9
|
+
without requiring an interactive prompt.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
✓ repl · data = load_json("records.json") · done
|
|
@@ -36,16 +36,21 @@ A plain `pi` session is untouched; the extension is dormant until `--repl` is pa
|
|
|
36
36
|
per-user path (`~/.pi/agent/pi-repl/venv`). If `python3` or the network is missing, it prints a
|
|
37
37
|
clear notice. How the interpreter is resolved is in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
|
38
38
|
|
|
39
|
+
### Termux / Android
|
|
40
|
+
|
|
41
|
+
On **Termux (Android)**, the `postinstall` venv build can fail because `ipykernel` depends on
|
|
42
|
+
`psutil`, and PyPI does not provide a compatible Android wheel. The [Termux / Android setup guide](docs/termux.md) shows how to build `psutil` from source and finish the installation.
|
|
43
|
+
|
|
39
44
|
## What you get
|
|
40
45
|
|
|
41
46
|
- **A persistent namespace.** Variables, functions, imports, and data survive across cells and
|
|
42
47
|
turns; snapshots preserve them across a best-effort restart.
|
|
43
48
|
- **A real `ipython` kernel**, not a hand-rolled `exec` loop.
|
|
44
|
-
- **Shell and file IO as plain Python.** `!cmd`
|
|
45
|
-
`subprocess.run(...)`
|
|
46
|
-
|
|
49
|
+
- **Shell and file IO as plain Python.** Use `!cmd` or `%%bash` for shell commands. Use
|
|
50
|
+
`subprocess.run(...)` when you need the result in a variable, and use `open()` or `pathlib`
|
|
51
|
+
for files. There is no extra wrapper API to learn.
|
|
47
52
|
- **Error survival.** A cell that throws reports the traceback and the kernel keeps going.
|
|
48
|
-
- **
|
|
53
|
+
- **Explicit recovery.** If it restarts, pi-repl reports which state it restored and which state it lost.
|
|
49
54
|
|
|
50
55
|
## Helpers
|
|
51
56
|
|
|
@@ -80,8 +85,10 @@ The Python interpreter is auto-resolved (the venv, else `$PYTHON`/`python3`).
|
|
|
80
85
|
|
|
81
86
|
## More
|
|
82
87
|
|
|
83
|
-
- Why this design: [docs/
|
|
84
|
-
- How it works, the venv,
|
|
88
|
+
- Why this design: [docs/design.md](docs/design.md)
|
|
89
|
+
- How it works, the venv, and the kernel: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
|
|
90
|
+
- How to write and load helpers: [docs/helpers.md](docs/helpers.md)
|
|
91
|
+
- Termux / Android installation: [docs/termux.md](docs/termux.md)
|
|
85
92
|
|
|
86
93
|
## It is not
|
|
87
94
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Architecture
|
|
2
2
|
|
|
3
|
-
pi-repl runs in **two processes
|
|
4
|
-
|
|
5
|
-
framing between them.
|
|
3
|
+
pi-repl runs in **two processes**. pi hosts the TypeScript extension, and the extension manages a
|
|
4
|
+
separate Python `ipykernel` process where user code runs. The host talks to that kernel using the
|
|
5
|
+
standard Jupyter protocol. There is no Python middleman and no private framing layer between them.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
pi
|
|
@@ -16,9 +16,9 @@ pi
|
|
|
16
16
|
└─ python -m ipykernel -f <connection-file> the evaluator
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The host is TypeScript
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
The host is TypeScript, and the evaluator is Python in a separate process. This means a cell can
|
|
20
|
+
raise an exception or make the kernel unusable without taking pi down. The host can still report
|
|
21
|
+
what happened.
|
|
22
22
|
|
|
23
23
|
## Why the host speaks ZMTP itself
|
|
24
24
|
|
|
@@ -28,21 +28,22 @@ design put a Python middleman (`guest.py`) between the host and the kernel, tran
|
|
|
28
28
|
private JSON protocol over a file descriptor into the real Jupyter protocol.
|
|
29
29
|
|
|
30
30
|
The current design removes the middleman. Instead of working around the missing library, the
|
|
31
|
-
host implements the small slice of ZMTP 3.0 a Jupyter client
|
|
32
|
-
|
|
31
|
+
host implements the small slice of ZMTP 3.0 that a Jupyter client needs. ZMTP is the socket
|
|
32
|
+
protocol used by Jupyter's channels: the host uses a DEALER socket for shell and control, and a
|
|
33
|
+
SUB socket for iopub (`src/engine/zmtp.ts`).
|
|
33
34
|
The payoff:
|
|
34
35
|
|
|
35
36
|
- **one process boundary** instead of two;
|
|
36
37
|
- **one standard protocol** (Jupyter) instead of a private one on top of it;
|
|
37
38
|
- **no invented framing** to maintain;
|
|
38
|
-
- **
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
- **Messages are authenticated with HMAC.** The host signs and verifies Jupyter messages with
|
|
40
|
+
the kernel's HMAC key. The earlier design used a nonce to prevent false completion messages;
|
|
41
|
+
the standard message signature now provides that check.
|
|
41
42
|
|
|
42
43
|
## The Python environment (the venv)
|
|
43
44
|
|
|
44
45
|
The evaluator is a real `ipykernel` kernel, so it needs a Python environment with
|
|
45
|
-
`ipykernel` installed.
|
|
46
|
+
`ipykernel` installed. This is a hard runtime dependency. A script cannot replace it.
|
|
46
47
|
(`jupyter_client` is *not* needed: the host is the client.)
|
|
47
48
|
|
|
48
49
|
When installed as a pi package, `npm install` runs `postinstall`
|
|
@@ -73,23 +74,23 @@ connection file in the temp directory, connects the three channels as ZMTP socke
|
|
|
73
74
|
waits for a `kernel_info_reply` before declaring the kernel ready. Cells run as standard
|
|
74
75
|
Jupyter `execute_request`s, routed by `msg_id`:
|
|
75
76
|
|
|
76
|
-
- **iopub** carries
|
|
77
|
-
|
|
77
|
+
- **iopub** carries output messages such as `stream`, `execute_result`, `display_data`, and
|
|
78
|
+
`error`. It also carries private-MIME payloads for snapshot, restore, and namespace data.
|
|
78
79
|
- **shell** carries the authoritative `execute_reply` (status, ename, evalue).
|
|
79
80
|
- **control** carries interrupts (`interrupt_request`) and shutdown.
|
|
80
81
|
|
|
81
|
-
Two
|
|
82
|
+
Two details of this protocol are important enough to have dedicated contract tests.
|
|
82
83
|
|
|
83
|
-
**A cell is not
|
|
84
|
+
**A cell is not complete until two messages arrive.** The shell reply and the iopub output stream
|
|
84
85
|
travel on different connections, so a tiny reply can arrive before a large output has
|
|
85
86
|
finished draining on iopub. A cell settles only when **both** the `execute_reply` and the
|
|
86
87
|
matching iopub `status idle` (published after every byte of output) have arrived. Settling
|
|
87
88
|
on the reply alone would drop output that was still in flight.
|
|
88
89
|
|
|
89
90
|
**Output is capped per channel.** Each channel accumulates output against a character budget
|
|
90
|
-
(`maxOutputChars`). Overflow is
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
(`maxOutputChars`). Overflow is checked within each message. A single 10 MB print trips the
|
|
92
|
+
cap immediately instead of waiting for a later message to exhaust the budget. The host appends
|
|
93
|
+
an explicit truncation marker so the model knows output was cut.
|
|
93
94
|
|
|
94
95
|
**Cancellation is real.** An abort sends an `interrupt_request` on the control channel,
|
|
95
96
|
which raises a genuine `KeyboardInterrupt` in the running cell; the namespace survives. As a
|
|
@@ -98,20 +99,19 @@ kills the kernel after 500 ms, and the next call rebuilds it from the last snaps
|
|
|
98
99
|
|
|
99
100
|
## Helpers loading
|
|
100
101
|
|
|
101
|
-
At boot, the kernel and the host both read
|
|
102
|
-
`~/.pi/agent/pi-repl/helpers
|
|
103
|
-
no shipped toolbox that merges in.
|
|
102
|
+
At boot, the kernel and the host both read the same helpers directory:
|
|
103
|
+
`~/.pi/agent/pi-repl/helpers`. It is created empty on install. No shipped toolbox is merged in.
|
|
104
104
|
|
|
105
|
-
- **The kernel**
|
|
106
|
-
|
|
105
|
+
- **The kernel** executes each eligible `*.py` file in its namespace, so the file's definitions
|
|
106
|
+
and imports become available.
|
|
107
107
|
- **The host** reads the same files to build the helper list shown in the `execute` tool's
|
|
108
108
|
prompt, so the model sees each `helper_description` verbatim.
|
|
109
109
|
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
Both sides read the same directory, so the names described to the model come from files the
|
|
111
|
+
kernel also loads. A file renamed with a `_` prefix is skipped by both sides. The
|
|
112
112
|
`promptGuidelines` are built once, when the `execute` tool is registered, so a helpers
|
|
113
|
-
change needs a **session restart or `/reload`** to reach the prompt
|
|
114
|
-
helpers only at boot
|
|
113
|
+
change needs a **session restart or `/reload`** to reach the prompt. The kernel also loads
|
|
114
|
+
helpers only at boot.
|
|
115
115
|
|
|
116
116
|
There are no custom discovery intrinsics (`ls()` / `help()`) injected into a bare kernel.
|
|
117
117
|
The model discovers what is loaded by listing the namespace with ordinary Python:
|
|
@@ -120,7 +120,7 @@ The model discovers what is loaded by listing the namespace with ordinary Python
|
|
|
120
120
|
[k for k in globals() if not k.startswith('_')]
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
For the full helper contract
|
|
123
|
+
For the full helper contract, including descriptions, docstrings, and disabling, see
|
|
124
124
|
[helpers.md](helpers.md).
|
|
125
125
|
|
|
126
126
|
## Snapshots & honest resets
|
|
@@ -131,8 +131,8 @@ only itself) and publishes the result back over a private MIME payload. The host
|
|
|
131
131
|
`namespace.snapshot`, keyed to the session file under
|
|
132
132
|
`~/.pi/agent/pi-repl/state/<session>/`.
|
|
133
133
|
|
|
134
|
-
When a fresh engine is built, it restores that snapshot.
|
|
135
|
-
|
|
134
|
+
When a fresh engine is built, it restores that snapshot. It reports the names of values that
|
|
135
|
+
could not be pickled, such as live handles and some runtime objects. If the evaluator was rebuilt mid-session,
|
|
136
136
|
the result is prefixed with a `<repl_engine_reset>` block that names what was revived and
|
|
137
137
|
what was lost, so the model re-verifies before reusing state that may be gone.
|
|
138
138
|
|
|
@@ -151,32 +151,33 @@ what was lost, so the model re-verifies before reusing state that may be gone.
|
|
|
151
151
|
- **Host (fast):** `test/units.test.ts` covers engine orchestration, rendering, and config;
|
|
152
152
|
`test/preview-core.test.ts` covers the preview logic.
|
|
153
153
|
- **Contract (slow):** `test/engine.integration.test.ts` boots a real kernel per engine and
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
verifies persistence across cells, error survival, output attribution, helper loading,
|
|
155
|
+
snapshot/restore round-trips, output caps, silence timeout, abort, and rebuilding from a
|
|
156
|
+
snapshot after the kernel dies.
|
|
157
157
|
|
|
158
|
-
The gate is `just check
|
|
159
|
-
adds the real-kernel suite.
|
|
158
|
+
The gate is `just check`. It runs Biome formatting and linting, dead-code checks, and host tests.
|
|
159
|
+
`just integration` adds the real-kernel suite.
|
|
160
160
|
|
|
161
161
|
## The fixed layout
|
|
162
162
|
|
|
163
|
-
There is no configuration file
|
|
164
|
-
|
|
163
|
+
There is no configuration file. Most state lives under one directory in the user's home. A
|
|
164
|
+
small number of environment variables can still change runtime behavior, such as the silence
|
|
165
|
+
watchdog timeout.
|
|
165
166
|
|
|
166
167
|
```
|
|
167
168
|
~/.pi/agent/pi-repl/
|
|
168
169
|
venv/ the Python interpreter + ipykernel
|
|
169
|
-
helpers/ the helpers directory
|
|
170
|
+
helpers/ the helpers directory; every eligible *.py loads into each kernel
|
|
170
171
|
state/ per-session namespace snapshots
|
|
171
172
|
```
|
|
172
173
|
|
|
173
174
|
The helpers directory is fixed at `~/.pi/agent/pi-repl/helpers` (matching the kernel's
|
|
174
175
|
`readHelperSources` default), so both sides are guaranteed to read the same directory. The
|
|
175
|
-
venv is built automatically and the interpreter
|
|
176
|
+
venv is built automatically, and the interpreter follows the order above. No setting is
|
|
176
177
|
needed. The per-cell silence watchdog is off by default (`PI_REPL_TIMEOUT_MS=0`: a silent
|
|
177
178
|
but working cell may run on).
|
|
178
179
|
|
|
179
180
|
## Reference documentation
|
|
180
181
|
|
|
181
|
-
-
|
|
182
|
+
- Design rationale: [design.md](design.md)
|
|
182
183
|
- Adding a helper: [helpers.md](helpers.md)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Design rationale: why a persistent Python workspace
|
|
2
2
|
|
|
3
3
|
## The bet
|
|
4
4
|
|
|
@@ -7,13 +7,12 @@ a search tool, each with its own schema, its own failure modes, and its own toke
|
|
|
7
7
|
describe. The model spends context deciding *which* tool to call, then *how* to thread one
|
|
8
8
|
tool's output into the next.
|
|
9
9
|
|
|
10
|
-
pi-repl makes the opposite
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
writes adapts instead.
|
|
10
|
+
pi-repl makes the opposite choice: **give the model one persistent Python workspace and let it
|
|
11
|
+
compose the work there.** Reading, running, searching, and editing happen in code inside one
|
|
12
|
+
namespace. The interface stays small while the code changes to fit the task.
|
|
14
13
|
|
|
15
|
-
This
|
|
16
|
-
|
|
14
|
+
This project assumes an agent benefits from a REPL that keeps working state alive. It is not an
|
|
15
|
+
interactive prompt for a person to type into; it is long-lived working memory for the model.
|
|
17
16
|
|
|
18
17
|
## What persistence buys
|
|
19
18
|
|
|
@@ -26,10 +25,9 @@ In a persistent kernel that work happens once and stays put:
|
|
|
26
25
|
- a variable assigned in one cell is still there in the next cell, and the next turn;
|
|
27
26
|
- a function defined once is reusable for the whole session;
|
|
28
27
|
- `subprocess.run(...)` returns a structured result (`.returncode`, `.stdout`, `.stderr`)
|
|
29
|
-
the agent
|
|
28
|
+
the agent can branch on with normal code. It does not need to re-parse tool output.
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
it is expensive precisely when context is scarce. The kernel lets the model load, filter,
|
|
30
|
+
Holding a whole file in context just to avoid re-reading it is expensive when context is scarce. The kernel lets the model load, filter,
|
|
33
31
|
and store in code, printing only what the current step needs.
|
|
34
32
|
|
|
35
33
|
## Why a real kernel
|
|
@@ -38,12 +36,12 @@ pi-repl does not hand-roll an `exec` loop. It drives a genuine `ipython` kernel
|
|
|
38
36
|
separate process. That buys four things a script string passed to `exec` cannot give:
|
|
39
37
|
|
|
40
38
|
- **rich, real tracebacks** instead of a wrapped `except`;
|
|
41
|
-
- **real interrupts
|
|
39
|
+
- **real interrupts:** a stuck cell can be interrupted mid-run without losing the session;
|
|
42
40
|
- **last-expression capture** (a cell's final expression becomes its result);
|
|
43
|
-
- **a namespace that survives errors
|
|
44
|
-
|
|
41
|
+
- **a namespace that survives errors:** a cell that throws leaves the kernel and everything
|
|
42
|
+
defined before it intact.
|
|
45
43
|
|
|
46
|
-
|
|
44
|
+
The kernel is a separate process, not part of pi. This is a process boundary, not a security sandbox. A
|
|
47
45
|
cell that raises leaves pi answering and the namespace intact, because pi is not the process
|
|
48
46
|
that failed. A cell that wedges the *whole* kernel instead stops cells from running until
|
|
49
47
|
the next call notices the dead kernel and rebuilds it from the last completed snapshot.
|
|
@@ -56,31 +54,22 @@ revived and what it lost, so the model re-verifies before trusting state that ma
|
|
|
56
54
|
Because the evaluator is real Python, it needs a real Python environment with `ipykernel`.
|
|
57
55
|
You cannot conjure that from a script; it is a hard runtime dependency.
|
|
58
56
|
|
|
59
|
-
The package's `postinstall` creates it once
|
|
57
|
+
The package's `postinstall` creates it once at a stable per-user path
|
|
60
58
|
(`~/.pi/agent/pi-repl/venv`), so a `pi install` normally ends with a working evaluator. If
|
|
61
59
|
`python3` or the network is missing at install time, `postinstall` prints a clear notice and
|
|
62
60
|
the host falls back to `$PYTHON` or `python3` at runtime. Updates never lose it, because the
|
|
63
61
|
venv lives outside the ephemeral package directory where it would vanish on every update.
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
3. the install venv at `~/.pi/agent/pi-repl/venv`
|
|
70
|
-
4. `$PYTHON`, then `python3` (the fallback)
|
|
71
|
-
|
|
72
|
-
The first one that exists wins. The system interpreter is the fallback, never the
|
|
73
|
-
assumption, because the whole tool quietly breaks if it silently runs in the wrong
|
|
74
|
-
environment. The tool's prompt tells the model this, so it does not leak the wrong
|
|
75
|
-
assumption into commands.
|
|
63
|
+
The host has a fallback order for finding Python, with the project and installed environments
|
|
64
|
+
preferred over the system interpreter. The exact order and the reasons for it are documented in
|
|
65
|
+
[ARCHITECTURE.md](ARCHITECTURE.md). This keeps the design rationale here focused on why the
|
|
66
|
+
venv is persistent rather than on runtime lookup details.
|
|
76
67
|
|
|
77
68
|
## Trust, not a sandbox
|
|
78
69
|
|
|
79
70
|
This is deliberately **not a sandbox.** The kernel runs with your user's permissions, can
|
|
80
71
|
read and write anywhere you can, and helpers are trusted as written. If you need to guard
|
|
81
|
-
against an untrusted model, this is the wrong tool
|
|
82
|
-
would for any untrusted code. The philosophy here prefers a sharp, honest tool over a
|
|
83
|
-
pretend-safe one.
|
|
72
|
+
against an untrusted model, this is the wrong tool. Use a real sandbox for untrusted code. The design favors a clear limitation over a false promise of safety.
|
|
84
73
|
|
|
85
74
|
## What it isn't
|
|
86
75
|
|
package/docs/helpers.md
CHANGED
|
@@ -1,110 +1,185 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Helpers
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
the session, and it's available. Like a bookmark for code the agent keeps reaching for.
|
|
3
|
+
Helpers are optional Python files that pi-repl loads into the persistent workspace. Use one when
|
|
4
|
+
code is worth reusing. A helper can also give the model a reliable wrapper instead of making it
|
|
5
|
+
rebuild the same plumbing in every cell.
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
A helper can define a function, class, constant, import, or configured object. The filename labels the helper entry shown in the prompt. It does not have to match a function
|
|
8
|
+
name or any other public name in the file.
|
|
10
9
|
|
|
11
|
-
##
|
|
10
|
+
## Where helpers live
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
```text
|
|
13
|
+
~/.pi/agent/pi-repl/helpers/
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The directory is created empty when pi-repl is installed. Every `.py` file in it is loaded when the evaluator starts. Files whose names begin with `_` are ignored.
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
After adding, changing, renaming, or disabling a helper, run `/reload` or start a new `pi --repl` session. The running evaluator does not watch the directory for changes.
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
## A small function helper
|
|
21
|
+
|
|
22
|
+
Create `double.py`:
|
|
19
23
|
|
|
20
24
|
```python
|
|
21
|
-
# ~/.pi/agent/pi-repl/helpers/double.py
|
|
22
25
|
helper_description = """double(x) — multiply a value by two."""
|
|
23
26
|
|
|
27
|
+
|
|
24
28
|
def double(x):
|
|
25
29
|
"""Return x * 2. Works on ints, floats, and lists."""
|
|
26
30
|
return x * 2
|
|
27
31
|
```
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
Reload pi, then call it from `execute`:
|
|
30
34
|
|
|
31
35
|
```python
|
|
32
|
-
print([k for k in globals() if not k.startswith('_')])
|
|
33
|
-
# ['double', ...]
|
|
34
|
-
|
|
35
36
|
print(double(21))
|
|
36
37
|
# 42
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
the file, restart, use it.
|
|
40
|
+
The evaluator runs the file in its global namespace, so `double` is directly available. You do not register or separately install an individual helper.
|
|
41
41
|
|
|
42
|
-
##
|
|
42
|
+
## A helper can expose an object
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
A helper does not need to expose a function with the same name as its file. For example, `web.py` can create a configured `web` object:
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
filename sets the label the prompt uses to advertise it — `double.py` is listed as
|
|
49
|
-
`double`. Keep the two identical (see *Common mistakes*).
|
|
46
|
+
```python
|
|
47
|
+
helper_description = """web — search, read, and map websites."""
|
|
50
48
|
|
|
51
|
-
2. **The model sees the description.** The `execute` tool's prompt lists each helper's
|
|
52
|
-
`helper_description`, rendered **verbatim**. Nothing is parsed: the text you put between
|
|
53
|
-
the triple-quotes is exactly what the model reads.
|
|
54
49
|
|
|
55
|
-
|
|
56
|
-
(
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
class Web:
|
|
51
|
+
def search(self, query):
|
|
52
|
+
"""Search the web and return normalized results."""
|
|
53
|
+
raise NotImplementedError
|
|
59
54
|
|
|
60
|
-
## The three parts of a helper file
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
| `def name(...)` | yes | the implementation | runs in the kernel |
|
|
65
|
-
| `helper_description` | no | one line the model reads first | the prompt, verbatim |
|
|
66
|
-
| a docstring | no | the full detail | `print(name.__doc__)` on demand |
|
|
56
|
+
web = Web()
|
|
57
|
+
```
|
|
67
58
|
|
|
68
|
-
|
|
69
|
-
exposes `double`. Keep the `def` name identical so callers and the namespace agree.
|
|
59
|
+
The model calls `web.search(...)`, not `web(...)`. See [`example/helper/web.py`](../example/helper/web.py) for the full provider-backed example.
|
|
70
60
|
|
|
71
|
-
|
|
72
|
-
prompt, and it is billed into context every turn, so it wants to be short. Two habits help
|
|
73
|
-
(not requirements — the loader parses nothing):
|
|
61
|
+
## What the model sees
|
|
74
62
|
|
|
75
|
-
|
|
76
|
-
- Add an "Instead of:" line naming the hand-rolled code it replaces, so the model reaches
|
|
77
|
-
for the helper rather than rewriting the raw call. *Good:* `Instead of: subprocess.run
|
|
78
|
-
with a hand-rolled kill-on-timeout.` *Weak:* `Instead of: doing it manually.`
|
|
63
|
+
A helper may define `helper_description`:
|
|
79
64
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
65
|
+
```python
|
|
66
|
+
helper_description = """double(x) — multiply a value by two."""
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The host reads this value and puts it in the `execute` tool description verbatim. It is guidance for the model, not a registration mechanism or generated API. Keep it short: it
|
|
70
|
+
is included in the model's context on every turn.
|
|
71
|
+
|
|
72
|
+
A useful description answers three questions:
|
|
73
|
+
|
|
74
|
+
1. What does this helper provide?
|
|
75
|
+
2. How should the model call it?
|
|
76
|
+
3. What important behavior or limitation should it know before calling it?
|
|
77
|
+
|
|
78
|
+
For a helper that replaces hand-written plumbing, an `Instead of:` line can be useful:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
helper_description = """web — search, read, and map websites.
|
|
82
|
+
Use web.search(query), web.read(url), and web.map(url).
|
|
83
|
+
Instead of: writing provider requests and parsing each response by hand."""
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Do not put the complete API contract in the description. Long explanations consume context on every call. Put detailed behavior in docstrings instead.
|
|
87
|
+
|
|
88
|
+
## Docstrings are on-demand detail
|
|
89
|
+
|
|
90
|
+
Docstrings stay in the Python workspace and do not appear in the tool description automatically:
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
def double(x):
|
|
94
|
+
"""Return x * 2.
|
|
95
|
+
|
|
96
|
+
Accepts numbers and lists. Raises no custom exceptions.
|
|
97
|
+
"""
|
|
98
|
+
return x * 2
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
When the description is not enough, inspect the helper in the workspace:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
help(double)
|
|
105
|
+
print(double.__doc__)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Use docstrings for argument details, defaults, return values, errors, environment requirements, and side effects.
|
|
109
|
+
|
|
110
|
+
## How loading works
|
|
111
|
+
|
|
112
|
+
At startup, two parts of pi-repl read the same helper directory:
|
|
113
|
+
|
|
114
|
+
1. The kernel executes each eligible `.py` file. Its definitions become names in the Python workspace.
|
|
115
|
+
2. The host reads `helper_description` to build the helper guidance shown to the model.
|
|
116
|
+
|
|
117
|
+
The host does not inspect `def` lines or infer signatures from filenames. A helper does not need to define one particular symbol. The file is the unit of loading; its public names are the names it defines or imports for use in
|
|
118
|
+
the workspace.
|
|
119
|
+
|
|
120
|
+
Because helpers execute at kernel startup, top-level code has consequences. Definitions are fine; imports should be reasonable; network calls, prints, subprocesses, and expensive work should usually happen inside an explicit function or method call.
|
|
121
|
+
|
|
122
|
+
## Choosing what belongs in a helper
|
|
123
|
+
|
|
124
|
+
Write a helper when it owns a part of the work that is easy to get wrong or tedious to repeat:
|
|
125
|
+
|
|
126
|
+
- a web client that handles authentication, fallback, and response normalization;
|
|
127
|
+
- a conversion with awkward edge cases;
|
|
128
|
+
- a project-specific API client;
|
|
129
|
+
- a small collection of related operations with shared configuration.
|
|
130
|
+
|
|
131
|
+
Do not make a helper for ordinary Python that the model can write clearly in one cell. File access and subprocess work are already available through normal Python:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from pathlib import Path
|
|
135
|
+
import subprocess
|
|
136
|
+
|
|
137
|
+
text = Path("notes.txt").read_text()
|
|
138
|
+
result = subprocess.run(["git", "status", "--short"], capture_output=True, text=True, check=False)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The helper should handle the plumbing. The model should still decide what to inspect, which sources matter, and what the evidence supports.
|
|
84
142
|
|
|
85
143
|
## Common mistakes
|
|
86
144
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
145
|
+
### Description too long
|
|
146
|
+
|
|
147
|
+
The description is repeated in the tool prompt. Put the call shape and the few facts needed to choose the helper there; put examples and edge cases in docstrings.
|
|
148
|
+
|
|
149
|
+
### Public names are unclear
|
|
150
|
+
|
|
151
|
+
If `web.py` exposes `web`, document `web.search()` and `web.read()`. Do not describe it as `web()` unless the file actually defines a callable named `web`.
|
|
152
|
+
|
|
153
|
+
### Side effects happen during loading
|
|
154
|
+
|
|
155
|
+
The file is executed before the first cell. A top-level print pollutes every new session, and a top-level network request can make startup slow or fail before the model calls anything. Constructing a lightweight object is usually fine; defer expensive work to a method.
|
|
97
156
|
|
|
98
|
-
|
|
157
|
+
### A changed helper is not visible
|
|
158
|
+
|
|
159
|
+
The prompt guidance and kernel namespace are established during startup. Run `/reload` after editing the file.
|
|
160
|
+
|
|
161
|
+
### A helper hides the decision
|
|
162
|
+
|
|
163
|
+
A helper can normalize responses or manage retries. For example, the web helper can hide
|
|
164
|
+
provider authentication and fallback. It should not silently decide which source proves a claim
|
|
165
|
+
or which file should be edited; those decisions belong to the model.
|
|
166
|
+
|
|
167
|
+
## Disable a helper without deleting it
|
|
168
|
+
|
|
169
|
+
Rename the file with a leading underscore:
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
web.py → _web.py
|
|
173
|
+
```
|
|
99
174
|
|
|
100
|
-
|
|
101
|
-
starts with `_`, so it never reaches the kernel or the prompt. Handy for scratch work.
|
|
175
|
+
The loader skips it. Rename it back and reload when you want it again.
|
|
102
176
|
|
|
103
177
|
## Checklist
|
|
104
178
|
|
|
105
|
-
- [ ]
|
|
106
|
-
- [ ]
|
|
107
|
-
- [ ]
|
|
108
|
-
- [ ]
|
|
109
|
-
- [ ]
|
|
110
|
-
- [ ]
|
|
179
|
+
- [ ] The file is in `~/.pi/agent/pi-repl/helpers/`.
|
|
180
|
+
- [ ] Its public names and call shapes are clear.
|
|
181
|
+
- [ ] `helper_description` is short enough for every-turn context.
|
|
182
|
+
- [ ] Detailed behavior is in docstrings.
|
|
183
|
+
- [ ] Top-level code has no unnecessary side effects.
|
|
184
|
+
- [ ] The helper keeps plumbing separate from model judgment.
|
|
185
|
+
- [ ] Pi was reloaded after the file changed.
|
package/docs/termux.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Termux / Android setup
|
|
2
|
+
|
|
3
|
+
On **Termux (Android)**, the `postinstall` venv build can fail because `ipykernel` depends on
|
|
4
|
+
`psutil`, and PyPI does not provide a compatible Android wheel. Two common alternatives do not
|
|
5
|
+
solve the problem:
|
|
6
|
+
|
|
7
|
+
- `pkg install python-psutil`: Termux's `.deb` post-install runs the same failing `pip install
|
|
8
|
+
psutil`, so no usable psutil is left.
|
|
9
|
+
- `pip install psutil-android`: the prebuilt `.so` links `libpython3.14.so`; on an older
|
|
10
|
+
Termux Python it fails with `dlopen failed: library "libpython3.14.so" not found`. It works
|
|
11
|
+
only when Termux's Python matches the wheel's ABI (currently 3.14).
|
|
12
|
+
|
|
13
|
+
The reliable route is to build the documented `psutil` release from source with a small
|
|
14
|
+
Android-specific change, then install the evaluator venv. Run these commands from a writable
|
|
15
|
+
working directory. If the release changes, update the source URL, version numbers, and patch
|
|
16
|
+
before running them:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# 1. show a compiler + Python headers
|
|
20
|
+
pkg install clang python
|
|
21
|
+
|
|
22
|
+
# 2. fetch and patch the psutil source so Android counts as Linux
|
|
23
|
+
curl -sL -o psutil.tar.gz https://files.pythonhosted.org/packages/source/p/psutil/psutil-7.2.2.tar.gz && tar -xzf psutil.tar.gz
|
|
24
|
+
cd psutil-7.2.2
|
|
25
|
+
sed -i 's/LINUX = sys.platform.startswith("linux")/LINUX = sys.platform.startswith(("linux", "android"))/' psutil/_common.py
|
|
26
|
+
python3 setup.py bdist_wheel
|
|
27
|
+
# If this reports that wheel is missing:
|
|
28
|
+
python3 -m pip install wheel
|
|
29
|
+
|
|
30
|
+
# 3. (re)build the evaluator venv and install the patched wheel first
|
|
31
|
+
python3 -m venv --clear ~/.pi/agent/pi-repl/venv
|
|
32
|
+
~/.pi/agent/pi-repl/venv/bin/pip install dist/psutil-7.2.2-*.whl
|
|
33
|
+
~/.pi/agent/pi-repl/venv/bin/pip install ipykernel
|
|
34
|
+
|
|
35
|
+
# 4. finish the package install
|
|
36
|
+
pi install npm:pi-repl-py
|
|
37
|
+
|
|
38
|
+
# 5. verify
|
|
39
|
+
~/.pi/agent/pi-repl/venv/bin/python3 -c "import psutil, ipykernel; print(psutil.__version__, ipykernel.__version__)"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The final command checks that both packages import successfully. Then start pi-repl:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pi --repl
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The venv already contains the working `ipykernel` that the evaluator needs.
|
|
49
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-repl-py",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A pi extension with a single tool: execute, running a TypeScript host with a persistent Python (ipykernel) evaluator and a user-configurable toolbox of functions.",
|
|
6
6
|
"keywords": [
|
package/src/extension/prompt.ts
CHANGED
|
@@ -1,76 +1,70 @@
|
|
|
1
1
|
// --- prompt: the execute tool's model-facing contract (pure, no pi/helper dep) ---
|
|
2
|
+
//
|
|
3
|
+
// Verbatim clauses from CodeAct (arXiv 2402.01030) and RLM (arXiv 2512.24601)
|
|
4
|
+
// are trimmed to what pi-repl actually has — no sub-LLMs, no recursion, no
|
|
5
|
+
// context variable — and the rest is stripped for lean context. Less prose,
|
|
6
|
+
// more signal; the machine reads every line every turn.
|
|
2
7
|
|
|
3
8
|
export const executeToolDescription =
|
|
4
9
|
"You have one tool: a persistent Python workspace backed by a real `ipython` kernel. " +
|
|
5
|
-
"Variables, imports,
|
|
6
|
-
"
|
|
7
|
-
"Helpers in `~/.pi/agent/pi-repl/helpers/`
|
|
8
|
-
"`[k for k in globals() if not k.startswith('_')]`. A cell returns its final expression; printed output " +
|
|
9
|
-
"
|
|
10
|
+
"Variables, imports, and definitions survive across cells and turns — it is your working memory and action " +
|
|
11
|
+
"language. " +
|
|
12
|
+
"Helpers in `~/.pi/agent/pi-repl/helpers/` load at boot; list them with " +
|
|
13
|
+
"`[k for k in globals() if not k.startswith('_')]`. A cell returns its final expression; printed output is " +
|
|
14
|
+
"captured separately.";
|
|
10
15
|
|
|
11
16
|
export const executePromptSnippet =
|
|
12
|
-
"
|
|
13
|
-
"
|
|
17
|
+
"Work in the workspace: keep artifacts in variables, compose related actions in Python, print only what " +
|
|
18
|
+
"the next step needs, and revise from what you observe.";
|
|
14
19
|
|
|
15
20
|
// --- the workspace doctrine riding the execute tool ---
|
|
16
21
|
export function buildPromptGuidelines(preloaded: string[]): string[] {
|
|
17
22
|
return [
|
|
18
|
-
"##
|
|
19
|
-
"
|
|
20
|
-
"
|
|
23
|
+
"## Your only workspace",
|
|
24
|
+
"`execute` is the only callable tool. Python replaces a read, shell, search, and edit tool rack. State " +
|
|
25
|
+
"persists across cells and turns.",
|
|
21
26
|
"",
|
|
22
|
-
"##
|
|
23
|
-
"
|
|
27
|
+
"## Work in the workspace, not the transcript",
|
|
28
|
+
"Load files, command results, search hits, and computed artifacts into variables once; filter, compare, " +
|
|
29
|
+
"branch, edit, and verify them in later cells. Do not re-read or paste raw material back. Print only the " +
|
|
30
|
+
"small observation needed for the next decision; keep the full artifact in a variable.",
|
|
24
31
|
"",
|
|
25
|
-
"##
|
|
26
|
-
"
|
|
27
|
-
"
|
|
32
|
+
"## A cell is a small program",
|
|
33
|
+
"Compose filesystem access, shell commands, searches, transforms, checks, and edits in ordinary Python " +
|
|
34
|
+
"when they belong to the same step.",
|
|
28
35
|
"",
|
|
29
|
-
"##
|
|
30
|
-
"
|
|
31
|
-
"code that depends on them.",
|
|
36
|
+
"## Revise on observations",
|
|
37
|
+
"Revise prior actions or emit new actions upon new observations.", // CodeAct core
|
|
32
38
|
"",
|
|
33
|
-
"##
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"silent cell automatically). `open()` / `pathlib` read and write files. For safe edits: read the full " +
|
|
37
|
-
"file, modify in memory, write once, then re-read to verify.",
|
|
39
|
+
"## Probe, then build",
|
|
40
|
+
"Inspect what is present — count, print a few lines, list what is loaded — before committing. Build one " +
|
|
41
|
+
"step, run it, and use its output to choose the next.",
|
|
38
42
|
"",
|
|
39
|
-
"## Batch
|
|
40
|
-
"Batch independent
|
|
41
|
-
"
|
|
42
|
-
"",
|
|
43
|
-
"## Search efficiently",
|
|
44
|
-
"Use `rg`, `fd`, `grep`, `find` via `subprocess.run` for deep searches, not Python loops.",
|
|
43
|
+
"## Batch and print sparingly",
|
|
44
|
+
"Batch as much independent work as reasonably possible into one call. Keep large values in variables; " +
|
|
45
|
+
"print slices, counts, and summaries.",
|
|
45
46
|
"",
|
|
46
47
|
...(preloaded.length
|
|
47
48
|
? [
|
|
48
49
|
"## Helpers",
|
|
49
|
-
"User helpers load from `~/.pi/agent/pi-repl/helpers
|
|
50
|
-
"List what
|
|
50
|
+
"User helpers load from `~/.pi/agent/pi-repl/helpers/` as workspace definitions. Their descriptions " +
|
|
51
|
+
"appear below. List what is loaded with `[k for k in globals() if not k.startswith('_')]`.",
|
|
51
52
|
"",
|
|
52
53
|
...preloaded,
|
|
53
54
|
"",
|
|
54
55
|
]
|
|
55
56
|
: []),
|
|
56
|
-
"##
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
"## Output discipline",
|
|
60
|
-
"Printing is a context cost: everything a cell prints stays in the transcript. Print slices, " +
|
|
61
|
-
"counts, and summaries. Keep large values in variables. End a cell with `;` to suppress the " +
|
|
62
|
-
"last-expression echo.",
|
|
57
|
+
"## Shell and search",
|
|
58
|
+
"`subprocess.run(..., timeout=...)` when you need a result — always set a `timeout`, the evaluator does not " +
|
|
59
|
+
"kill a silent cell. Use `rg`/`grep`/`find` via `subprocess.run` for deep searches, not Python loops.",
|
|
63
60
|
"",
|
|
64
61
|
"## Environment boundary",
|
|
65
62
|
"The evaluator runs in a project-local venv, not the system Python. Do not install a target project's " +
|
|
66
|
-
"dependencies into the evaluator
|
|
67
|
-
"through their own interface and normal commands.",
|
|
63
|
+
"dependencies into the evaluator. Run external projects through their own interface and normal commands.",
|
|
68
64
|
"",
|
|
69
65
|
"## Engine reset guard",
|
|
70
|
-
"If
|
|
71
|
-
"
|
|
72
|
-
"
|
|
73
|
-
"command until you have confirmed it still holds what you expect. Functions, classes, and live " +
|
|
74
|
-
"handles cannot be snapshotted and must be redefined.",
|
|
66
|
+
"If output begins with `<repl_engine_reset>`, the kernel was rebuilt from the last snapshot. Re-verify a " +
|
|
67
|
+
"revived variable before reusing it — especially in a shell command. Functions, classes, and live handles " +
|
|
68
|
+
"are not snapshotted and must be redefined.",
|
|
75
69
|
];
|
|
76
70
|
}
|