pi-repl-py 0.2.3 → 0.2.6

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 CHANGED
@@ -1,12 +1,12 @@
1
- # pi-repl
1
+ # pi-repl-py
2
2
 
3
3
  A [pi](https://pi.dev) extension that gives the agent a single `execute` tool backed by a
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 into a Python workspace that lives for the
8
- whole session, and only the printed result comes back. That is the part of a REPL an agent wants:
9
- lasting state and code-as-a-workspace, without the interactive loop in the way.
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
@@ -32,20 +32,33 @@ A plain `pi` session is untouched; the extension is dormant until `--repl` is pa
32
32
 
33
33
  ## Installing as a pi package
34
34
 
35
- `npm install` runs a `postinstall` that creates the Python venv the evaluator needs, at a stable
35
+ Install the package from npm or directly from GitHub:
36
+
37
+ ```bash
38
+ pi install npm:pi-repl-py
39
+ # or
40
+ pi install github:k3-2o/pi-repl-py
41
+ ```
42
+
43
+ The install runs a `postinstall` that creates the Python venv the evaluator needs, at a stable
36
44
  per-user path (`~/.pi/agent/pi-repl/venv`). If `python3` or the network is missing, it prints a
37
45
  clear notice. How the interpreter is resolved is in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
38
46
 
47
+ ### Termux / Android
48
+
49
+ On **Termux (Android)**, the `postinstall` venv build can fail because `ipykernel` depends on
50
+ `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.
51
+
39
52
  ## What you get
40
53
 
41
54
  - **A persistent namespace.** Variables, functions, imports, and data survive across cells and
42
55
  turns; snapshots preserve them across a best-effort restart.
43
56
  - **A real `ipython` kernel**, not a hand-rolled `exec` loop.
44
- - **Shell and file IO as plain Python.** `!cmd` and `%%bash` run shell fire-and-forget,
45
- `subprocess.run(...)` brings the result back into a variable, and `open()` / `pathlib`
46
- read and write files. No wrapper API to learn, and nothing extra to describe to the model.
57
+ - **Shell and file IO as plain Python.** Use `!cmd` or `%%bash` for shell commands. Use
58
+ `subprocess.run(...)` when you need the result in a variable, and use `open()` or `pathlib`
59
+ for files. There is no extra wrapper API to learn.
47
60
  - **Error survival.** A cell that throws reports the traceback and the kernel keeps going.
48
- - **An honest evaluator.** If it restarts, it names what state it could revive and what it lost, so you don't trust memory that's gone.
61
+ - **Explicit recovery.** If it restarts, pi-repl reports which state it restored and which state it lost.
49
62
 
50
63
  ## Helpers
51
64
 
@@ -80,8 +93,10 @@ The Python interpreter is auto-resolved (the venv, else `$PYTHON`/`python3`).
80
93
 
81
94
  ## More
82
95
 
83
- - Why this design: [docs/philosophy.md](docs/philosophy.md)
84
- - How it works, the venv, config reference: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
96
+ - Why this design: [docs/design.md](docs/design.md)
97
+ - How it works, the venv, and the kernel: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
98
+ - How to write and load helpers: [docs/helpers.md](docs/helpers.md)
99
+ - Termux / Android installation: [docs/termux.md](docs/termux.md)
85
100
 
86
101
  ## It is not
87
102
 
@@ -1,8 +1,8 @@
1
1
  # Architecture
2
2
 
3
- pi-repl runs in **two processes**: the host lives inside pi and talks the standard Jupyter
4
- protocol directly to a real `ipykernel` subprocess. There is no middleman and no invented
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; the evaluator is Python in its own process. That split is what makes
20
- a bad cell survivable: a cell can raise, leak memory, or wedge the kernel without taking pi
21
- down — and the host, being not the thing that failed, always gets to report what happened.
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 actually needs a DEALER
32
- socket for the shell and control channels, a SUB socket for iopub (`src/engine/zmtp.ts`).
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
- - **HMAC signed and verified by the host itself.** The old design carried a nonce scheme to
39
- stop a cell from forging its own completion signal; with the host signing every message
40
- with the kernel's HMAC key, that concern is moot.
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. That is a hard runtime dependency you cannot fake it with a script.
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 the output `stream`, `execute_result`, `display_data`, and `error`
77
- messages, plus private-MIME payloads that carry snapshot, restore, and namespace data.
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 wire subtleties are load-bearing, and both are pinned by the contract tests.
82
+ Two details of this protocol are important enough to have dedicated contract tests.
82
83
 
83
- **A cell is not done until two things arrive.** The shell reply and the iopub output stream
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 flagged per message a single 10 MB print trips the cap
91
- within that one message, not only once a later message exhausts the budget and the host
92
- appends an explicit truncation marker so the model knows output was cut.
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 **one** helpers directory — the fixed
102
- `~/.pi/agent/pi-repl/helpers`, created empty on install with nothing seeded into it. There is
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** execs each `*.py` into its namespace, so the file's functions become
106
- callable.
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
- Because both sides read the same directory, anything the model is told about is also
111
- callable, and a file renamed with a `_` prefix is skipped by both. The
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 and the kernel loads
114
- helpers only at boot anyway.
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 the description, the docstring, disabling — see
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. Whatever could not be pickled
135
- live handles, some objects is reported by name. If the evaluator was rebuilt mid-session,
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
- proves the guarantees the old Python contract pinned: persistence across cells,
155
- error-survival, output attribution, helpers loading, snapshot/restore round-trips, output
156
- caps, silence timeout, abort, and rebuild-from-snapshot after a dead kernel.
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` biome (format + lint) plus the host tests. `just integration`
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 and no knobs. Everything lives under one directory in the
164
- user's home, and the default is the only option:
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 every *.py loads into every kernel
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 resolved by the order above no setting
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
- - Philosophy and design rationale: [philosophy.md](philosophy.md)
182
+ - Design rationale: [design.md](design.md)
182
183
  - Adding a helper: [helpers.md](helpers.md)
@@ -1,4 +1,4 @@
1
- # Philosophy: why a persistent Python workspace
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 bet: **give the model one persistent Python workspace, and let it
11
- write the composition itself.** Reading, running, searching, and editing all happen in code,
12
- in a single living namespace. The model's interface to the world never grows the *code* it
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 is what an agent actually wants from a "REPL". Not an interactive prompt to type into,
16
- but long-lived working memory the model owns.
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 branches on with normal code no re-parsing a tool's text output.
28
+ the agent can branch on with normal code. It does not need to re-parse tool output.
30
29
 
31
- The savings compound for small models. Holding a whole file in context to avoid re-reading
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** a stuck cell can be interrupted mid-run without losing the session;
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** a cell that throws leaves the kernel, and
44
- everything defined before it, intact.
41
+ - **a namespace that survives errors:** a cell that throws leaves the kernel and everything
42
+ defined before it intact.
45
43
 
46
- It is also an honest isolation boundary. The kernel is its own process, not part of pi. A
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, at a stable per-user path
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
- At runtime the host resolves the interpreter in a short, fixed order:
66
-
67
- 1. the repo's own `.venv` (development)
68
- 2. a venv in the current directory (per-project)
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 reach for a real sandbox the way you
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
- # How to write a helper
1
+ # Helpers
2
2
 
3
- A **helper** is a `.py` file that gets exec'd into every kernel, so anything it defines —
4
- functions, classes, constants, imports, or a module that owns a fragile or opaque piece of
5
- work — becomes part of the agent's workspace; you drop the file into one folder, restart
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
- This guide shows the smallest callable helper that works, then explains the parts of a
9
- helper file and the habits that make a helper useful.
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
- ## Prerequisites
10
+ ## Where helpers live
12
11
 
13
- - A working `pi --repl` session (the extension is installed — see the README).
14
- - The helpers folder, `~/.pi/agent/pi-repl/helpers/`. It is created empty on install.
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
- ## The smallest helper
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
- Create this file:
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
- Restart the session (`/reload`, or relaunch `pi --repl`), then check it loaded:
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
- If `double` appears in the namespace and runs, it's loaded. That is the whole loop: write
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
- ## How it actually works
42
+ ## A helper can expose an object
43
43
 
44
- Two things happen with the file you wrote:
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
- 1. **The kernel execs it at boot.** The file's source runs in the kernel before the first
47
- cell, so `def double` defines a callable `double` in the workspace. Separately, the
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
- Shell and file IO are not helpers — they are already ordinary Python
56
- (`subprocess.run`, `!cmd`, `%%bash`, `open`, `pathlib`). A helper is only worth writing for
57
- the fragile or opaque part the model can't reliably reconstruct on its own: a `web_search`
58
- with provider failover, a client wrapper, a conversion it keeps getting wrong.
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
- | Part | Required? | What it does | Who sees it |
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
- **The function** is the implementation. Its public name must match the filename: `double.py`
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
- **The description** is the model's first impression. It is the only part that reaches the
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
- - Start with the call shape: `double(x) — multiply a value by two.`
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
- **The docstring** holds the depth — argument types and defaults, return value, error
81
- behaviour, environment facts ("this runs in a project-local venv, not the system Python").
82
- It never enters the prompt. The model reads it on demand with `print(name.__doc__)` or
83
- `help(name)` when the description is not enough.
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
- - **Description buried in detail.** Move anything beyond the call shape and one consequence
88
- into the docstring. A long description bills into every turn.
89
- - **The helper decides too much.** A helper should own the murky part (the call, the
90
- parsing), not the decision. If it picks the command, the routing, or the judgement, the
91
- model stops reasoning.
92
- - **No docstring.** Without one, the model sees only a signature and the gotchas vanish.
93
- - **Top-level side effects.** The file runs at boot in every kernel. Keep module-level code
94
- to definitions — no prints, no network, no slow imports at module scope.
95
- - **Filename and `def` name differ.** `helpers/foo.py` exposing `def bar` confuses callers
96
- and the prompt. Keep them identical.
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
- ## Disable a file without deleting it
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
- Rename it with a leading underscore (`_scratch.py`). The loader skips any file whose name
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
- - [ ] Filename matches the public `def` name
106
- - [ ] `helper_description` is short and starts with the call shape
107
- - [ ] Depth lives in the docstring, not the description
108
- - [ ] No top-level side effects
109
- - [ ] Session restarted or `/reload`-ed
110
- - [ ] Name appears in `globals()` and the function runs
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",
3
+ "version": "0.2.6",
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": [
@@ -9,8 +9,7 @@ export const executeToolDescription =
9
9
  "You have one tool: a persistent Python workspace backed by a real `ipython` kernel. " +
10
10
  "Variables, imports, and definitions survive across cells and turns — it is your working memory and action " +
11
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 " +
12
+ "Helpers in `~/.pi/agent/pi-repl/helpers/` load at boot. A cell returns its final expression; printed output is " +
14
13
  "captured separately.";
15
14
 
16
15
  export const executePromptSnippet =
@@ -40,6 +39,17 @@ export function buildPromptGuidelines(preloaded: string[]): string[] {
40
39
  "Inspect what is present — count, print a few lines, list what is loaded — before committing. Build one " +
41
40
  "step, run it, and use its output to choose the next.",
42
41
  "",
42
+ "## Precise file and search work",
43
+ "Search narrowly and inspect only the lines needed. Do not dump whole files or repeat unchanged context. " +
44
+ "For existing files, prefer a surgical old-text/new-text replacement over rewriting the file. Read the " +
45
+ "target region first, make the smallest unique replacement, then verify the changed region and file validity. " +
46
+ "Use complete writes only for new files or intentional full rewrites. Never leave a bare final expression: " +
47
+ "IPython displays it automatically; assign results and explicitly print only what you need.",
48
+ "",
49
+ "## Repository discipline",
50
+ "Inspect before changing. Preserve project conventions and unrelated content. Make the smallest valid change, " +
51
+ "verify it afterward, and never invent files, APIs, conventions, or test results.",
52
+ "",
43
53
  "## Batch and print sparingly",
44
54
  "Batch as much independent work as reasonably possible into one call. Keep large values in variables; " +
45
55
  "print slices, counts, and summaries.",
@@ -48,7 +58,7 @@ export function buildPromptGuidelines(preloaded: string[]): string[] {
48
58
  ? [
49
59
  "## Helpers",
50
60
  "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('_')]`.",
61
+ "appear below.",
52
62
  "",
53
63
  ...preloaded,
54
64
  "",